darktimer/fw/dtlogic/src/ui.rs

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);
}
}