380 lines
10 KiB
Rust
380 lines
10 KiB
Rust
use core::fmt::Write;
|
|
|
|
use embedded_graphics::{
|
|
pixelcolor::{RgbColor, PixelColor},
|
|
draw_target::DrawTarget,
|
|
prelude::{Point, Size, Primitive, Drawable},
|
|
primitives::{Rectangle, PrimitiveStyle},
|
|
mono_font::MonoTextStyleBuilder,
|
|
text::{Text, Baseline},
|
|
};
|
|
|
|
use profont::{
|
|
PROFONT_7_POINT,
|
|
PROFONT_18_POINT,
|
|
PROFONT_24_POINT,
|
|
};
|
|
|
|
use crate::input;
|
|
|
|
/// Color space that we expect to support: some form of RGB.
|
|
pub trait ColorSpace = RgbColor + PixelColor;
|
|
|
|
/// Context passed to state tick functions.
|
|
pub struct Context {
|
|
pub buttons: input::Buttons,
|
|
pub ticks: u32,
|
|
}
|
|
|
|
/// Transitions (ie. FSM edges) returned by state tick functions.
|
|
pub enum Transition {
|
|
/// Do nothing (keep current state).
|
|
Keep,
|
|
/// Start the 'Running' state.
|
|
Running {
|
|
/// Tickcount at which the timer started running.
|
|
start: u32,
|
|
/// Number of milliseconds that the timer should run.
|
|
msec: u32,
|
|
},
|
|
/// Start the 'Time Select' state.
|
|
TimeSelect,
|
|
}
|
|
|
|
/// State combining state logic, transition logic and drawing logic.
|
|
pub trait State<D: DrawTarget<Color: ColorSpace>> {
|
|
/// Called when the state becomes active, before tick is called.
|
|
fn enter(&mut self, tr: Transition);
|
|
/// Called on each tick of the application when the state is active.
|
|
fn tick(&mut self, ctx: &Context) -> Transition;
|
|
/// Called whenever the application can/should draw. Called after tick.
|
|
fn draw(&mut self, target: &mut D) -> Result<(), <D as DrawTarget>::Error>;
|
|
|
|
/// Return the size of the target display. Helper function for draw().
|
|
fn size(&mut self, target: &mut D) -> (u32, u32)
|
|
{
|
|
let bb = target.bounding_box();
|
|
(bb.size.width, bb.size.height)
|
|
}
|
|
|
|
/// Fully clear the target display with a given color. Helper function for draw().
|
|
fn clear(&mut self, target: &mut D, color: D::Color) -> Result<(), <D as DrawTarget>::Error>
|
|
{
|
|
let (width, height) = self.size(target);
|
|
Rectangle::new(Point::new(0, 0), Size::new(width, height))
|
|
.into_styled(PrimitiveStyle::with_fill(color))
|
|
.draw(target)
|
|
}
|
|
|
|
/// Draw bottom soft buttons. Each button should be one-character string. Helper for draw().
|
|
fn draw_softbuttons(&mut self, target: &mut D, labels: [&str; 4]) ->
|
|
Result<(), <D as DrawTarget>::Error>
|
|
{
|
|
let (width, height) = self.size(target);
|
|
let font = &PROFONT_18_POINT;
|
|
let style = MonoTextStyleBuilder::new()
|
|
.font(font)
|
|
.text_color(D::Color::RED)
|
|
.background_color(D::Color::BLACK)
|
|
.build();
|
|
|
|
let chsize = font.character_size;
|
|
let y = height - chsize.height;
|
|
let xs = [
|
|
0,
|
|
width / 3 - chsize.width,
|
|
2 * (width / 3) - chsize.width,
|
|
width - chsize.width,
|
|
];
|
|
for (&x, v) in xs.iter().zip(labels.iter()) {
|
|
Text::with_baseline(
|
|
v,
|
|
Point::new(x as i32, y as i32),
|
|
style,
|
|
Baseline::Top
|
|
).draw(target)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// State used to select the time for the timer. Has +/- buttons and a start button (which
|
|
/// transitions to Running).
|
|
pub struct TimeSelect {
|
|
cleared: bool,
|
|
bw: Option<input::ButtonWatcher>,
|
|
msec: u32,
|
|
ticks: u32,
|
|
}
|
|
|
|
impl TimeSelect {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
cleared: false,
|
|
bw: None,
|
|
msec: 1000,
|
|
ticks: 0,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<D: DrawTarget<Color: ColorSpace>> State<D> for TimeSelect {
|
|
fn enter(&mut self, _tr: Transition) {
|
|
self.cleared = false;
|
|
}
|
|
|
|
fn tick(&mut self, ctx: &Context) -> Transition {
|
|
if let Some(bw) = &mut self.bw {
|
|
bw.update(&ctx.buttons);
|
|
} else {
|
|
self.bw = Some(ctx.buttons.watch());
|
|
}
|
|
|
|
for btn in self.bw.as_mut().unwrap().events() {
|
|
match btn {
|
|
0 => self.msec += 500,
|
|
1 => {
|
|
if self.msec > 0 {
|
|
self.msec -= 500;
|
|
}
|
|
},
|
|
3 => {
|
|
return Transition::Running {
|
|
start: ctx.ticks,
|
|
msec: self.msec,
|
|
};
|
|
},
|
|
_ => (),
|
|
}
|
|
}
|
|
|
|
if self.msec >= 3600000 {
|
|
self.msec = 3600000;
|
|
}
|
|
self.ticks = ctx.ticks;
|
|
|
|
Transition::Keep
|
|
}
|
|
|
|
fn draw(&mut self, target: &mut D) -> Result<(), <D as DrawTarget>::Error>
|
|
{
|
|
if !self.cleared {
|
|
self.clear(target, D::Color::BLACK)?;
|
|
self.cleared = true;
|
|
}
|
|
|
|
let (width, height) = self.size(target);
|
|
|
|
self.draw_softbuttons(target, ["+", "-", " ", "S"])?;
|
|
|
|
{
|
|
let style = MonoTextStyleBuilder::new()
|
|
.font(&PROFONT_24_POINT)
|
|
.text_color(D::Color::RED)
|
|
.background_color(D::Color::BLACK)
|
|
.build();
|
|
let chsize = PROFONT_24_POINT.character_size;
|
|
let chars = 8;
|
|
|
|
let twidth = chars * chsize.width;
|
|
let theight = chsize.height;
|
|
let x = (width - twidth) / 2;
|
|
let y = (height - theight)/ 2;
|
|
|
|
let min = (self.msec / 1000) / 60;
|
|
let sec = (self.msec / 1000) % 60;
|
|
let csec = (self.msec % 1000) / 10;
|
|
|
|
let mut buf = arrayvec::ArrayString::<8>::new();
|
|
write!(&mut buf, "{:02}:{:02}.{:02}", min, sec, csec).unwrap();
|
|
Text::with_baseline(
|
|
&buf,
|
|
Point::new(x as i32, y as i32),
|
|
style,
|
|
Baseline::Top
|
|
).draw(target)?;
|
|
}
|
|
|
|
{
|
|
let style = MonoTextStyleBuilder::new()
|
|
.font(&PROFONT_7_POINT)
|
|
.text_color(D::Color::RED)
|
|
.background_color(D::Color::BLACK)
|
|
.build();
|
|
let mut buf = arrayvec::ArrayString::<16>::new();
|
|
write!(&mut buf, "ticks: {:08}", self.ticks).unwrap();
|
|
Text::with_baseline(
|
|
&buf,
|
|
Point::new(0, 0),
|
|
style,
|
|
Baseline::Top
|
|
).draw(target)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// State when the timer is counting down.
|
|
pub struct Running {
|
|
cleared: bool,
|
|
msec: u32,
|
|
msec_display: u32,
|
|
start: u32,
|
|
}
|
|
|
|
impl Running {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
cleared: false,
|
|
msec_display: 0,
|
|
msec: 0,
|
|
start: 0,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<D: DrawTarget<Color: ColorSpace>> State<D> for Running {
|
|
fn enter(&mut self, tr: Transition) {
|
|
self.cleared = false;
|
|
if let Transition::Running { msec, start } = tr {
|
|
self.msec = msec;
|
|
self.start = start;
|
|
}
|
|
}
|
|
|
|
fn tick(&mut self, ctx: &Context) -> Transition {
|
|
let elapsed = ctx.ticks - self.start;
|
|
if elapsed > self.msec {
|
|
return Transition::TimeSelect;
|
|
}
|
|
|
|
self.msec_display = self.msec - elapsed;
|
|
|
|
Transition::Keep
|
|
}
|
|
|
|
fn draw(&mut self, target: &mut D) -> Result<(), <D as DrawTarget>::Error>
|
|
{
|
|
if !self.cleared {
|
|
self.clear(target, D::Color::RED)?;
|
|
self.cleared = true;
|
|
}
|
|
|
|
let (width, height) = self.size(target);
|
|
|
|
{
|
|
let style = MonoTextStyleBuilder::new()
|
|
.font(&PROFONT_24_POINT)
|
|
.text_color(D::Color::BLACK)
|
|
.background_color(D::Color::RED)
|
|
.build();
|
|
let chsize = PROFONT_24_POINT.character_size;
|
|
let chars = 8;
|
|
|
|
let twidth = chars * chsize.width;
|
|
let theight = chsize.height;
|
|
let x = (width - twidth) / 2;
|
|
let y = (height - theight)/ 2;
|
|
|
|
let min = (self.msec_display / 1000) / 60;
|
|
let sec = (self.msec_display / 1000) % 60;
|
|
let csec = (self.msec_display % 1000) / 10;
|
|
|
|
let mut buf = arrayvec::ArrayString::<8>::new();
|
|
write!(&mut buf, "{:02}:{:02}.{:02}", min, sec, csec).unwrap();
|
|
Text::with_baseline(
|
|
&buf,
|
|
Point::new(x as i32, y as i32),
|
|
style,
|
|
Baseline::Top
|
|
).draw(target)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::ui::{
|
|
Context,
|
|
TimeSelect,
|
|
State,
|
|
Transition,
|
|
};
|
|
use crate::input::Buttons;
|
|
use embedded_graphics::{
|
|
pixelcolor::Rgb565,
|
|
prelude::*,
|
|
};
|
|
use embedded_graphics_simulator::{
|
|
SimulatorDisplay,
|
|
OutputSettingsBuilder,
|
|
Window,
|
|
};
|
|
|
|
#[test]
|
|
fn time_select() {
|
|
let mut display : SimulatorDisplay<Rgb565> = SimulatorDisplay::new(Size::new(160, 80));
|
|
let output_settings = OutputSettingsBuilder::new()
|
|
.build();
|
|
let mut buttons = Buttons::new();
|
|
let mut ts = TimeSelect::new();
|
|
|
|
{
|
|
let s: &mut dyn State<SimulatorDisplay<Rgb565>> = &mut ts;
|
|
s.enter(Transition::TimeSelect);
|
|
s.tick(&Context{
|
|
buttons: buttons.clone(),
|
|
ticks: 0,
|
|
});
|
|
s.draw(&mut display);
|
|
}
|
|
|
|
|
|
buttons.update(100, [true, false, false, false]);
|
|
{
|
|
let s: &mut dyn State<SimulatorDisplay<Rgb565>> = &mut ts;
|
|
s.tick(&Context{
|
|
buttons: buttons.clone(),
|
|
ticks: 100,
|
|
});
|
|
}
|
|
assert_eq!(ts.msec, 1500);
|
|
|
|
buttons.update(200, [false, false, false, false]);
|
|
{
|
|
let s: &mut dyn State<SimulatorDisplay<Rgb565>> = &mut ts;
|
|
s.tick(&Context{
|
|
buttons: buttons.clone(),
|
|
ticks: 200,
|
|
});
|
|
}
|
|
assert_eq!(ts.msec, 1500);
|
|
|
|
buttons.update(300, [true, false, false, false]);
|
|
{
|
|
let s: &mut dyn State<SimulatorDisplay<Rgb565>> = &mut ts;
|
|
s.tick(&Context{
|
|
buttons: buttons.clone(),
|
|
ticks: 300,
|
|
});
|
|
}
|
|
assert_eq!(ts.msec, 2000);
|
|
|
|
buttons.update(400, [false, true, false, false]);
|
|
{
|
|
let s: &mut dyn State<SimulatorDisplay<Rgb565>> = &mut ts;
|
|
s.tick(&Context{
|
|
buttons: buttons.clone(),
|
|
ticks: 400,
|
|
});
|
|
}
|
|
assert_eq!(ts.msec, 1500);
|
|
|
|
Window::new("TimeSelect", &output_settings).show_static(&display);
|
|
}
|
|
}
|