diff --git a/backend-embedded-graphics/Cargo.toml b/backend-embedded-graphics/Cargo.toml index 46d61da..f13da61 100644 --- a/backend-embedded-graphics/Cargo.toml +++ b/backend-embedded-graphics/Cargo.toml @@ -5,6 +5,7 @@ authors = ["Dániel Buga "] edition = "2018" [dependencies] +embedded-canvas = "0.2.0" embedded-graphics = "0.7.0" embedded-text = { version = "0.5.0", features = ["plugin"] } embedded-gui = { path = ".." } @@ -14,4 +15,5 @@ object-chain = "0.1" [features] ansi = ["embedded-text/ansi"] +std = ["embedded-canvas/alloc"] default = ["ansi"] diff --git a/backend-embedded-graphics/src/lib.rs b/backend-embedded-graphics/src/lib.rs index 1ed0633..6a01f0b 100644 --- a/backend-embedded-graphics/src/lib.rs +++ b/backend-embedded-graphics/src/lib.rs @@ -49,6 +49,8 @@ use embedded_gui::{ Canvas, }; +pub use embedded_canvas; + trait ToPoint { fn to_point(self) -> Point; } diff --git a/backend-embedded-graphics/src/widgets/canvas.rs b/backend-embedded-graphics/src/widgets/canvas.rs new file mode 100644 index 0000000..9828d24 --- /dev/null +++ b/backend-embedded-graphics/src/widgets/canvas.rs @@ -0,0 +1,368 @@ +use embedded_canvas::CCanvasAt; +use embedded_graphics::{ + draw_target::Cropped, + prelude::{Dimensions, DrawTarget, DrawTargetExt, PixelColor, Point, Size}, + Drawable, +}; +use embedded_gui::{ + geometry::{measurement::MeasureSpec, BoundingBox, MeasuredSize}, + input::{ + controller::InputContext, + event::{InputEvent, PointerEvent}, + }, + prelude::WrapperBindable, + state::{ + selection::{Selected, Unselected}, + WidgetState, + }, + widgets::Widget, + WidgetRenderer, +}; + +use crate::{themes::Theme, EgCanvas, ToRectangle}; + +pub trait CanvasProperties { + type Color; + type Canvas: DrawTarget + DrawTargetExt + Drawable; + + fn canvas(&mut self) -> &mut Self::Canvas; + fn clear_color(&mut self) -> Self::Color; + fn measure(&self) -> MeasuredSize; +} + +pub struct CCanvasStyle +where + C: PixelColor, +{ + pub clear_color: C, + pub canvas: CCanvasAt, +} + +impl CanvasProperties for CCanvasStyle +where + C: PixelColor, +{ + type Color = C; + type Canvas = CCanvasAt; + + fn canvas(&mut self) -> &mut Self::Canvas { + &mut self.canvas + } + + fn clear_color(&mut self) -> Self::Color { + self.clear_color + } + + fn measure(&self) -> MeasuredSize { + MeasuredSize { + width: W as u32, + height: H as u32, + } + } +} + +impl Default for CCanvasStyle +where + C: Theme, +{ + fn default() -> Self { + Self { + clear_color: C::BACKGROUND_COLOR, + canvas: CCanvasAt::new(Point::zero()), + } + } +} + +//#[cfg(feature = "std")] +use embedded_canvas::CanvasAt; + +//#[cfg(feature = "std")] +pub struct CanvasStyle +where + C: PixelColor, +{ + pub clear_color: C, + pub canvas: CanvasAt, +} + +//#[cfg(feature = "std")] +impl CanvasStyle +where + C: Theme, +{ + pub fn new(size: Size) -> Self { + Self { + clear_color: C::BACKGROUND_COLOR, + canvas: CanvasAt::new(Point::zero(), size), + } + } +} + +//#[cfg(feature = "std")] +impl CanvasProperties for CanvasStyle +where + C: PixelColor, +{ + type Color = C; + type Canvas = CanvasAt; + + fn canvas(&mut self) -> &mut Self::Canvas { + &mut self.canvas + } + + fn clear_color(&mut self) -> Self::Color { + self.clear_color + } + + fn measure(&self) -> MeasuredSize { + MeasuredSize { + width: self.canvas.bounding_box().size.width, + height: self.canvas.bounding_box().size.height, + } + } +} + +//#[cfg(feature = "std")] +impl Default for CanvasStyle +where + C: Theme, +{ + fn default() -> Self { + Self { + clear_color: C::BACKGROUND_COLOR, + canvas: CanvasAt::new(Point::zero(), Size::zero()), + } + } +} + +pub struct Canvas { + pub bounds: BoundingBox, + pub parent_index: usize, + pub canvas_properties: P, + pub state: WidgetState, + handler: Option, + on_draw: D, + draw: bool, +} + +impl Canvas<(), (), ()> { + pub const STATE_SELECTED: Selected = Selected; + pub const STATE_UNSELECTED: Unselected = Unselected; +} + +impl

Canvas +where + P: CanvasProperties, +{ + pub fn new() -> Canvas bool, fn(&mut Cropped<'_, P::Canvas>)> + where + P: Default, + { + Canvas { + parent_index: 0, + bounds: BoundingBox::default(), + canvas_properties: P::default(), + state: WidgetState::default(), + handler: None, + on_draw: |_| {}, + draw: true, + } + } + + pub fn with_properties( + properties: P, + ) -> Canvas bool, fn(&mut Cropped<'_, P::Canvas>)> + where + P: Default, + { + Canvas { + parent_index: 0, + bounds: BoundingBox::default(), + canvas_properties: properties, + state: WidgetState::default(), + handler: None, + on_draw: |_| {}, + draw: true, + } + } +} + +impl Canvas +where + P: CanvasProperties, + H: FnMut(InputContext, InputEvent) -> bool, + D: FnMut(&mut Cropped<'_, P::Canvas>), +{ + pub fn invalidate(&mut self) { + self.draw = true; + } + + pub fn with_input_handler

(self, handler: H2) -> Canvas + where + H2: FnMut(InputContext, InputEvent) -> bool, + { + Canvas { + parent_index: self.parent_index, + bounds: self.bounds, + canvas_properties: self.canvas_properties, + state: self.state, + handler: Some(handler), + on_draw: self.on_draw, + draw: self.draw, + } + } + + pub fn with_on_draw(self, on_draw: D2) -> Canvas + where + P: CanvasProperties, + D2: FnMut(&mut Cropped<'_, P::Canvas>), + { + Canvas { + parent_index: self.parent_index, + bounds: self.bounds, + canvas_properties: self.canvas_properties, + state: self.state, + handler: self.handler, + on_draw, + draw: self.draw, + } + } +} + +impl WrapperBindable for Canvas +where + P: CanvasProperties, + H: FnMut(InputContext, InputEvent) -> bool, + D: FnMut(&mut Cropped<'_, P::Canvas>), +{ +} + +impl Widget for Canvas +where + P: CanvasProperties, + H: FnMut(InputContext, InputEvent) -> bool, + D: FnMut(&mut Cropped<'_, P::Canvas>), +{ + fn bounding_box(&self) -> BoundingBox { + self.bounds + } + + fn bounding_box_mut(&mut self) -> &mut BoundingBox { + &mut self.bounds + } + + fn measure(&mut self, measure_spec: MeasureSpec) { + let canvas_size = self.canvas_properties.measure(); + + let width = measure_spec.width.apply_to_measured(canvas_size.width); + let height = measure_spec.height.apply_to_measured(canvas_size.height); + + self.bounds.size = MeasuredSize { width, height }; + } + + fn parent_index(&self) -> usize { + self.parent_index + } + + fn set_parent(&mut self, index: usize) { + self.parent_index = index; + } + + fn on_state_changed(&mut self, _: WidgetState) {} + + fn test_input(&mut self, event: InputEvent) -> Option { + let bounds = self.bounding_box(); + + let state = &mut self.state; + self.handler.as_mut().and_then(|_| match event { + InputEvent::Cancel => { + state.set_state(Canvas::STATE_UNSELECTED); + None + } + + InputEvent::PointerEvent(position, PointerEvent::Down) => { + if bounds.contains(position) { + Some(0) + } else { + // Allow a potentially clicked widget to handle the event. + state.set_state(Canvas::STATE_UNSELECTED); + None + } + } + + // We want controls drawn above the Canvas to get input events. + InputEvent::PointerEvent(_, PointerEvent::Hover) => None, + + InputEvent::PointerEvent(position, PointerEvent::Drag) => { + if bounds.contains(position) { + Some(0) + } else { + None + } + } + + InputEvent::KeyEvent(_) => { + if state.has_state(Canvas::STATE_SELECTED) { + Some(0) + } else { + None + } + } + + _ => Some(0), + }) + } + + fn handle_input(&mut self, ctxt: InputContext, event: InputEvent) -> bool { + let state = &mut self.state; + self.handler + .as_mut() + .map(|handler| { + match event { + InputEvent::Cancel => { + state.set_state(Canvas::STATE_UNSELECTED); + } + InputEvent::PointerEvent(_, PointerEvent::Down) => { + state.set_state(Canvas::STATE_SELECTED); + } + _ => {} + } + + handler(ctxt, event) + }) + .unwrap_or(false) + } + + fn is_selectable(&self) -> bool { + self.handler.is_some() + } +} + +impl WidgetRenderer> for Canvas +where + C: PixelColor, + DT: DrawTarget, + P: CanvasProperties, + H: FnMut(InputContext, InputEvent) -> bool, + D: FnMut(&mut Cropped<'_, P::Canvas>), +{ + fn draw(&mut self, canvas: &mut EgCanvas
) -> Result<(), DT::Error> { + if self.draw { + self.draw = false; + let bounds = self.bounding_box().to_rectangle(); + let clear_color = self.canvas_properties.clear_color(); + let mut canvas = self.canvas_properties.canvas().cropped(&bounds); + + _ = canvas.clear(clear_color); + + (self.on_draw)(&mut canvas); + } + + let bounds = self.bounding_box().to_rectangle(); + self.canvas_properties + .canvas() + .draw(&mut canvas.target.clipped(&bounds))?; + + Ok(()) + } +} diff --git a/backend-embedded-graphics/src/widgets/mod.rs b/backend-embedded-graphics/src/widgets/mod.rs index b964d23..d0d7a34 100644 --- a/backend-embedded-graphics/src/widgets/mod.rs +++ b/backend-embedded-graphics/src/widgets/mod.rs @@ -1,5 +1,6 @@ pub mod background; pub mod border; +pub mod canvas; pub mod graphical; pub mod label; pub mod scroll; diff --git a/examples/canvas.rs b/examples/canvas.rs new file mode 100644 index 0000000..d691575 --- /dev/null +++ b/examples/canvas.rs @@ -0,0 +1,268 @@ +use std::{thread, time::Duration}; + +use backend_embedded_graphics::{ + themes::{default::DefaultTheme, Theme}, + widgets::canvas::{Canvas, CanvasStyle}, + EgCanvas, +}; +use embedded_graphics::{ + draw_target::DrawTarget, + pixelcolor::Rgb888, + prelude::{Dimensions, RgbColor, Size as EgSize}, + primitives::{Circle, Primitive, PrimitiveStyle, Rectangle}, + Drawable, +}; +use embedded_graphics_simulator::{ + sdl2::{Keycode, Mod, MouseButton}, + OutputSettingsBuilder, SimulatorDisplay, SimulatorEvent, Window as SimWindow, +}; +use embedded_gui::{ + data::BoundData, + geometry::Position, + input::event::{InputEvent, Key, KeyEvent, Modifier, PointerEvent}, + prelude::*, + widgets::{border::Border, fill::FillParent, layouts::frame::Frame}, +}; + +trait Convert { + type Output; + + fn convert(self) -> Self::Output; +} + +impl Convert for Keycode { + type Output = Option; + + fn convert(self) -> Self::Output { + match self { + Keycode::Backspace => Some(Key::Backspace), + Keycode::Tab => Some(Key::Tab), + Keycode::Return => Some(Key::Enter), + Keycode::Space => Some(Key::Space), + Keycode::KpComma | Keycode::Comma => Some(Key::Comma), + Keycode::KpMinus | Keycode::Minus => Some(Key::Minus), + Keycode::KpPeriod | Keycode::Period => Some(Key::Period), + Keycode::Kp1 | Keycode::Num0 => Some(Key::N0), + Keycode::Kp2 | Keycode::Num1 => Some(Key::N1), + Keycode::Kp3 | Keycode::Num2 => Some(Key::N2), + Keycode::Kp4 | Keycode::Num3 => Some(Key::N3), + Keycode::Kp5 | Keycode::Num4 => Some(Key::N4), + Keycode::Kp6 | Keycode::Num5 => Some(Key::N5), + Keycode::Kp7 | Keycode::Num6 => Some(Key::N6), + Keycode::Kp8 | Keycode::Num7 => Some(Key::N7), + Keycode::Kp9 | Keycode::Num8 => Some(Key::N8), + Keycode::Kp0 | Keycode::Num9 => Some(Key::N9), + Keycode::A => Some(Key::A), + Keycode::B => Some(Key::B), + Keycode::C => Some(Key::C), + Keycode::D => Some(Key::D), + Keycode::E => Some(Key::E), + Keycode::F => Some(Key::F), + Keycode::G => Some(Key::G), + Keycode::H => Some(Key::H), + Keycode::I => Some(Key::I), + Keycode::J => Some(Key::J), + Keycode::K => Some(Key::K), + Keycode::L => Some(Key::L), + Keycode::M => Some(Key::M), + Keycode::N => Some(Key::N), + Keycode::O => Some(Key::O), + Keycode::P => Some(Key::P), + Keycode::Q => Some(Key::Q), + Keycode::R => Some(Key::R), + Keycode::S => Some(Key::S), + Keycode::T => Some(Key::T), + Keycode::U => Some(Key::U), + Keycode::V => Some(Key::V), + Keycode::W => Some(Key::W), + Keycode::X => Some(Key::X), + Keycode::Y => Some(Key::Y), + Keycode::Z => Some(Key::Z), + Keycode::Delete => Some(Key::Del), + Keycode::Right => Some(Key::ArrowRight), + Keycode::Left => Some(Key::ArrowLeft), + Keycode::Down => Some(Key::ArrowDown), + Keycode::Up => Some(Key::ArrowUp), + _ => None, + } + } +} + +impl Convert for Mod { + type Output = Modifier; + + fn convert(self) -> Self::Output { + if self.contains(Mod::RALTMOD) { + Modifier::Alt + } else if self.intersects(Mod::LSHIFTMOD | Mod::RSHIFTMOD) { + Modifier::Shift + } else if self.contains(Mod::CAPSMOD) { + Modifier::Shift + } else { + Modifier::None + } + } +} + +fn convert_input(event: SimulatorEvent) -> Result { + unsafe { + // This is fine for a demo + static mut MOUSE_DOWN: bool = false; + match event { + SimulatorEvent::MouseButtonUp { + mouse_btn: MouseButton::Left, + point, + } => { + MOUSE_DOWN = false; + Ok(InputEvent::PointerEvent( + Position { + x: point.x, + y: point.y, + }, + PointerEvent::Up, + )) + } + SimulatorEvent::MouseButtonDown { + mouse_btn: MouseButton::Left, + point, + } => { + MOUSE_DOWN = true; + Ok(InputEvent::PointerEvent( + Position { + x: point.x, + y: point.y, + }, + PointerEvent::Down, + )) + } + SimulatorEvent::MouseMove { point } => Ok(InputEvent::PointerEvent( + Position { + x: point.x, + y: point.y, + }, + if MOUSE_DOWN { + PointerEvent::Drag + } else { + PointerEvent::Hover + }, + )), + SimulatorEvent::KeyDown { + keycode, keymod, .. + } => Ok(InputEvent::KeyEvent(KeyEvent::KeyDown( + keycode.convert().ok_or(false)?, + keymod.convert(), + 0, + ))), + + SimulatorEvent::KeyUp { + keycode, keymod, .. + } => Ok(InputEvent::KeyEvent(KeyEvent::KeyUp( + keycode.convert().ok_or(false)?, + keymod.convert(), + ))), + SimulatorEvent::Quit => Err(true), + _ => Err(false), + } + } +} + +struct AnimationState { + enabled: bool, + time: u32, +} + +fn main() { + let display = SimulatorDisplay::new(EgSize::new(256, 128)); + + let state = BoundData::new( + AnimationState { + enabled: false, + time: 0, + }, + |_| {}, + ); + + let mut gui = Window::new( + EgCanvas::new(display), + Frame::new() + .add_layer(FillParent::both(Border::new( + Canvas::with_properties(CanvasStyle::::new(EgSize::new(256, 128))) + .with_input_handler(|_ctxt, input| { + state.update(|data| match input { + InputEvent::PointerEvent(_, event) => match event { + PointerEvent::Up => data.enabled = true, + PointerEvent::Down => data.enabled = false, + PointerEvent::Hover | PointerEvent::Drag => {} + }, + InputEvent::KeyEvent(KeyEvent::KeyDown(Key::Space, _, _)) => { + data.enabled = !data.enabled; + } + _ => (), + }); + true + }) + .with_on_draw(|canvas| { + let t = state.with_data(|state| state.time % 100); + + let increment = if t < 50 { t } else { 50 - (t - 50) }; + + let rectangle = Rectangle::with_center( + canvas.bounding_box().center(), + EgSize { + width: 50 + increment, + height: 50 + increment, + }, + ) + .into_styled(PrimitiveStyle::with_stroke(Rgb888::BLUE, 1)); + rectangle.draw(canvas).unwrap(); + + let circle = + Circle::with_center(canvas.bounding_box().center(), 40 + increment) + .into_styled(PrimitiveStyle::with_stroke(Rgb888::RED, 1)); + circle.draw(canvas).unwrap(); + }) + .bind(&state) + .on_data_changed(|widget, _| widget.invalidate()), + ))) + .add_layer( + DefaultTheme::primary_button("Animate") + .bind(&state) + .on_clicked(|state| state.enabled = !state.enabled), + ), + ); + + let output_settings = OutputSettingsBuilder::new().scale(2).build(); + let mut window = SimWindow::new("GUI demonstration", &output_settings); + + loop { + gui.canvas.target.clear(Rgb888::BACKGROUND_COLOR).unwrap(); + + gui.update(); + gui.measure(); + gui.arrange(); + gui.draw().unwrap(); + + state.update(|state| { + if state.enabled { + state.time = state.time.wrapping_add(1); + } + }); + + // Update the window. + window.update(&gui.canvas.target); + + // Handle key and mouse events. + for event in window.events() { + match convert_input(event) { + Ok(input) => { + gui.input_event(input); + } + Err(true) => return, + _ => {} + } + } + + // Wait for a little while. + thread::sleep(Duration::from_millis(10)); + } +}