From 9bd011c098f16109416a2e34931b76dca2e9baaf Mon Sep 17 00:00:00 2001 From: cepetr Date: Fri, 26 Apr 2024 08:28:51 +0200 Subject: [PATCH] WIP - drawlib animation demo --- core/embed/firmware/main.c | 4 + core/embed/rust/rust_ui.h | 2 + core/embed/rust/src/ui/demo/demo_core.rs | 125 ++++++++++++++++++ core/embed/rust/src/ui/demo/mod.rs | 3 + core/embed/rust/src/ui/demo/screen1.rs | 158 +++++++++++++++++++++++ core/embed/rust/src/ui/demo/screen2.rs | 139 ++++++++++++++++++++ core/embed/rust/src/ui/layout/obj.rs | 40 +++++- core/embed/rust/src/ui/mod.rs | 4 +- core/embed/unix/main.c | 6 + 9 files changed, 473 insertions(+), 8 deletions(-) create mode 100644 core/embed/rust/src/ui/demo/demo_core.rs create mode 100644 core/embed/rust/src/ui/demo/mod.rs create mode 100644 core/embed/rust/src/ui/demo/screen1.rs create mode 100644 core/embed/rust/src/ui/demo/screen2.rs diff --git a/core/embed/firmware/main.c b/core/embed/firmware/main.c index 64e42da49..11bdc1a28 100644 --- a/core/embed/firmware/main.c +++ b/core/embed/firmware/main.c @@ -237,6 +237,10 @@ int main(void) { mp_obj_list_init(mp_sys_path, 0); mp_obj_list_append(mp_sys_path, MP_OBJ_NEW_QSTR(MP_QSTR__dot_frozen)); + drawlib_demo(); // TODO: !!! remove + while (1) + ; + // Execute the main script printf("CORE: Executing main script\n"); pyexec_frozen_module("main.py"); diff --git a/core/embed/rust/rust_ui.h b/core/embed/rust/rust_ui.h index 897c6a331..68fa1f1c1 100644 --- a/core/embed/rust/rust_ui.h +++ b/core/embed/rust/rust_ui.h @@ -4,3 +4,5 @@ #include "rust_ui_bootloader.h" #include "rust_ui_common.h" + +void drawlib_demo(); diff --git a/core/embed/rust/src/ui/demo/demo_core.rs b/core/embed/rust/src/ui/demo/demo_core.rs new file mode 100644 index 000000000..cc4edf1bb --- /dev/null +++ b/core/embed/rust/src/ui/demo/demo_core.rs @@ -0,0 +1,125 @@ +use crate::ui::{ + component::{Event, TimerToken}, + display::{self, Color, Font}, + event::TouchEvent, + geometry::{Offset, Point, Rect}, + shape::{self, Viewport}, +}; + +use crate::{ + time::{Duration, Stopwatch}, + trezorhal::io::io_touch_read, +}; +use core::fmt::Write; +use heapless::String; + +use super::{screen1::build_screen1, screen2::build_screen2}; + +fn render_time_overlay(duration: Duration) { + shape::render_on_display( + Some(Viewport::new(Rect::new( + Point::new(200, 0), + Point::new(240, 20), + ))), + Some(Color::rgb(0, 0, 255)), + |target| { + let text_color = Color::rgb(255, 255, 0); + + let mut info = String::<128>::new(); + write!(info, "{}", duration.to_millis()).unwrap(); + + let font = Font::NORMAL; + let pt = Point::new(200, font.vert_center(0, 20, "A")); + shape::Text::new(pt, info.as_str()) + .with_fg(text_color) + .with_font(font) + .render(target); + }, + ); +} + +fn touch_event() -> Option { + let event = io_touch_read(); + if event == 0 { + return None; + } + let event_type = event >> 24; + let ex = ((event >> 12) & 0xFFF) as i16; + let ey = (event & 0xFFF) as i16; + + TouchEvent::new(event_type, ex as _, ey as _).ok() +} + +// Animates the split point between two screens +// (ranging from -240 to 0) + +#[derive(Default)] +struct ScreenTransitionAnim { + timer: Stopwatch, +} + +impl ScreenTransitionAnim { + pub fn is_active(&self) -> bool { + self.timer.is_running() + } + + pub fn eval(&self) -> i16 { + let anim = pareen::constant(0.0) + .seq_ease_in_out( + 0.0, + pareen::easer::functions::Back, + 1.5, + pareen::constant(-240.0), + ) + .seq_ease_in_out( + 5.0, + pareen::easer::functions::Back, + 0.75, + pareen::constant(0.0), + ) + .dur(10.0) + .repeat(); + + anim.eval(self.timer.elapsed().into()) as i16 + } +} + +extern "C" fn void() { +} + +#[no_mangle] +extern "C" fn drawlib_demo() { + let screen1 = build_screen1().unwrap(); + let screen2 = build_screen2().unwrap(); + + screen1.obj_event(Event::Attach).unwrap(); + screen2.obj_event(Event::Attach).unwrap(); + + let mut anim = ScreenTransitionAnim::default(); + anim.timer.start(); + + loop { + //screen1.obj_event(Event::Timer(TimerToken::INVALID)).unwrap(); + //screen2.obj_event(Event::Timer(TimerToken::INVALID)).unwrap(); + + if let Some(e) = touch_event() { + screen1.obj_event(Event::Touch(e)).unwrap(); + screen2.obj_event(Event::Touch(e)).unwrap(); + } + + let split = anim.eval(); + + display::sync(); + + let stopwatch = Stopwatch::new_started(); + + //screen1.obj_paint_if_requested(); + + screen1.obj_render(Offset::x(split)); + screen2.obj_render(Offset::x(240 + split)); + + render_time_overlay(stopwatch.elapsed()); + + display::refresh(); + } +} diff --git a/core/embed/rust/src/ui/demo/mod.rs b/core/embed/rust/src/ui/demo/mod.rs new file mode 100644 index 000000000..d9e61c97d --- /dev/null +++ b/core/embed/rust/src/ui/demo/mod.rs @@ -0,0 +1,3 @@ +pub mod demo_core; +pub mod screen1; +pub mod screen2; diff --git a/core/embed/rust/src/ui/demo/screen1.rs b/core/embed/rust/src/ui/demo/screen1.rs new file mode 100644 index 000000000..10a48ecaa --- /dev/null +++ b/core/embed/rust/src/ui/demo/screen1.rs @@ -0,0 +1,158 @@ +use crate::ui::{ + component::{Component, Event, EventCtx, Never}, + display::{font::Font, Color}, + event::TouchEvent, + geometry::{Point, Rect}, + layout::obj::{ComponentMsgObj, LayoutObj}, + model_tt::{component::Frame, theme}, + shape, + shape::Renderer, +}; + +use crate::time::{Duration, Stopwatch}; + +use crate::{ + error::Error, + micropython::{gc::Gc, obj::Obj}, +}; +use pareen; + +#[derive(Default)] +struct BallAnim { + pub timer: Stopwatch, +} + +impl BallAnim { + const DURATION: f32 = 1.5; + + pub fn is_active(&self) -> bool { + self.timer.is_running_within(Duration::from(Self::DURATION)) + } + + pub fn eval(&self) -> Point { + let x_anim = pareen::constant(30.0).seq_ease_out( + 0.0, + pareen::easer::functions::Cubic, + Self::DURATION, + pareen::constant(210.0), + ); + + let y_anim = pareen::constant(30.0).seq_ease_out( + 0.0, + pareen::easer::functions::Bounce, + Self::DURATION, + pareen::constant(210.0), + ); + + let t = self.timer.elapsed().into(); + + Point::new(x_anim.eval(t) as i16, y_anim.eval(t) as i16) + } +} + +#[derive(Default)] +struct TextAnim { + pub timer: Stopwatch, +} + +impl TextAnim { + pub fn is_active(&self) -> bool { + self.timer.is_running_within(Duration::from(3.4)) + } + + pub fn eval(&self) -> Point { + let x_anim = pareen::constant(30.0) + .seq_ease_in( + 0.0, + pareen::easer::functions::Cubic, + 0.3, + pareen::constant(240.0), + ) + .seq_ease_out( + 2.5, + pareen::easer::functions::Elastic, + 0.9, + pareen::constant(30.0), + ); + + let t = self.timer.elapsed().into(); + + Point::new(x_anim.eval(t) as i16, 120) + } +} + +pub struct Screen1 { + ball_anim: BallAnim, + text_anim: TextAnim, +} + +impl Screen1 { + pub fn new() -> Self { + Self { + ball_anim: Default::default(), + text_anim: Default::default(), + } + } +} + +impl Component for Screen1 { + type Msg = Never; + + fn place(&mut self, bounds: Rect) -> Rect { + bounds + } + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + match event { + Event::Touch(TouchEvent::TouchStart(_)) => { + self.ball_anim.timer.start(); + self.text_anim.timer.start(); + } + _ => {} + } + + if self.ball_anim.is_active() && self.text_anim.is_active() { + ctx.request_anim_frame(); + } + + None + } + + fn paint(&mut self) {} + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + let r = Rect::new(Point::new(10, 30), Point::new(230, 230)); + shape::Bar::new(r) + .with_bg(Color::rgb(0, 20, 40)) + .render(target); + + shape::Text::new(self.text_anim.eval(), "Touch to start") + .with_font(Font::BOLD) + .render(target); + + shape::Circle::new(self.ball_anim.eval(), 16) + .with_bg(Color::rgb(96, 128, 128)) + .render(target); + } +} + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for Screen1 { + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.component("Demo screen"); + } +} + +impl ComponentMsgObj for Screen1 { + fn msg_try_into_obj(&self, _msg: Self::Msg) -> Result { + unreachable!(); + } +} + +pub fn build_screen1() -> Result, Error> { + LayoutObj::new(Frame::left_aligned( + theme::label_title(), + "Animation Demo".into(), + Screen1::new(), + )) +} diff --git a/core/embed/rust/src/ui/demo/screen2.rs b/core/embed/rust/src/ui/demo/screen2.rs new file mode 100644 index 000000000..849ac384d --- /dev/null +++ b/core/embed/rust/src/ui/demo/screen2.rs @@ -0,0 +1,139 @@ +use crate::ui::{ + component::{Component, Event, EventCtx, Never}, + display::Color, + geometry::{Point, Rect}, + layout::obj::{ComponentMsgObj, LayoutObj}, + model_tt::{component::Frame, theme}, + shape, + shape::Renderer, +}; + +use crate::{ + error::Error, + micropython::{gc::Gc, obj::Obj}, +}; +use pareen; + +use crate::time::Stopwatch; + +#[derive(Default)] +struct LoaderAnim { + pub timer: Stopwatch, +} + +impl LoaderAnim { + pub fn is_active(&self) -> bool { + self.timer.is_running() + } + + pub fn eval(&self) -> (i16, i16, i16) { + let r1_anim = pareen::circle().cos().scale_min_max(60.0, 64.0).repeat(1.0); + + let r2_anim = pareen::circle() + .cos() + .scale_min_max(52.0, 58.0) + .squeeze(0.0..=0.5) + .repeat(0.5); + + let r3_anim = pareen::circle() + .cos() + .scale_min_max(40.0, 50.0) + .squeeze(0.0..=2.0) + .repeat(2.0); + + let t = self.timer.elapsed().into(); + + ( + r1_anim.eval(t) as i16, + r2_anim.eval(t) as i16, + r3_anim.eval(t) as i16, + ) + } +} + +pub struct Screen1 { + loader_anim: LoaderAnim, +} + +impl Screen1 { + pub fn new() -> Self { + Self { + loader_anim: Default::default(), + } + } +} + +impl Component for Screen1 { + type Msg = Never; + + fn place(&mut self, bounds: Rect) -> Rect { + bounds + } + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + match event { + Event::Attach => { + self.loader_anim.timer.start(); + } + _ => {} + } + + if self.loader_anim.is_active() { + ctx.request_anim_frame(); + } + + None + } + + fn paint(&mut self) {} + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + let bg_color = Color::rgb(40, 20, 0); + + let r = Rect::new(Point::new(10, 30), Point::new(230, 230)); + shape::Bar::new(r).with_bg(bg_color).render(target); + + let center = Point::new(120, 120); + + let (r1, r2, r3) = self.loader_anim.eval(); + + shape::Circle::new(center, r1) + .with_thickness(2) + .with_bg(bg_color) + .with_fg(Color::rgb(0, 120, 0)) + .render(target); + + shape::Circle::new(center, r2) + .with_thickness(3) + .with_bg(bg_color) + .with_fg(Color::rgb(0, 120, 30)) + .render(target); + + shape::Circle::new(center, r3) + .with_thickness(4) + .with_bg(bg_color) + .with_fg(Color::rgb(0, 120, 30)) + .render(target); + } +} + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for Screen1 { + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.component("Demo screen"); + } +} + +impl ComponentMsgObj for Screen1 { + fn msg_try_into_obj(&self, _msg: Self::Msg) -> Result { + unreachable!(); + } +} + +pub fn build_screen2() -> Result, Error> { + LayoutObj::new(Frame::left_aligned( + theme::label_title(), + "Animation Demo 2".into(), + Screen1::new(), + )) +} diff --git a/core/embed/rust/src/ui/layout/obj.rs b/core/embed/rust/src/ui/layout/obj.rs index 6c00316ad..786fa5b16 100644 --- a/core/embed/rust/src/ui/layout/obj.rs +++ b/core/embed/rust/src/ui/layout/obj.rs @@ -20,8 +20,8 @@ use crate::{ component::{Component, Event, EventCtx, Never, Root, TimerToken}, constant, display::{sync, Color}, - geometry::Rect, - shape::render_on_display, + geometry::{Offset, Rect}, + shape::{render_on_display, Viewport}, }, }; @@ -48,6 +48,8 @@ pub trait ObjComponent: MaybeTrace { fn obj_place(&mut self, bounds: Rect) -> Rect; fn obj_event(&mut self, ctx: &mut EventCtx, event: Event) -> Result; fn obj_paint(&mut self) -> bool; + #[cfg(feature = "new_rendering")] + fn obj_render(&mut self, offset: Offset); fn obj_bounds(&self, _sink: &mut dyn FnMut(Rect)) {} fn obj_skip_paint(&mut self) {} fn obj_request_clear(&mut self) {} @@ -92,6 +94,15 @@ where } } + #[cfg(feature = "new_rendering")] + fn obj_render(&mut self, offset: Offset) { + let screen = crate::ui::constant::screen(); + let vp = Viewport::new(screen).translate(offset); + render_on_display(Some(vp), Some(Color::black()), |target| { + self.render(target); + }); + } + #[cfg(feature = "ui_bounds")] fn obj_bounds(&self, sink: &mut dyn FnMut(Rect)) { self.bounds(sink) @@ -160,7 +171,7 @@ impl LayoutObj { /// pending timers are drained into `self.timer_callback`. Returns `Err` /// in case the timer callback raises or one of the components returns /// an error, `Ok` with the message otherwise. - fn obj_event(&self, event: Event) -> Result { + pub fn obj_event(&self, event: Event) -> Result { let inner = &mut *self.inner.borrow_mut(); // Place the root component on the screen in case it was previously requested. @@ -180,14 +191,14 @@ impl LayoutObj { // painting by now, and we're prepared for a paint pass. // Drain any pending timers into the callback. - while let Some((token, deadline)) = inner.event_ctx.pop_timer() { - let token = token.try_into(); + while let Some((_token, _deadline)) = inner.event_ctx.pop_timer() { + /*let token = token.try_into(); let deadline = deadline.try_into(); if let (Ok(token), Ok(deadline)) = (token, deadline) { inner.timer_fn.call_with_n_args(&[token, deadline])?; } else { // Failed to convert token or deadline into `Obj`, skip. - } + }*/ } if let Some(count) = inner.event_ctx.page_count() { @@ -205,7 +216,7 @@ impl LayoutObj { /// Run a paint pass over the component tree. Returns true if any component /// actually requested painting since last invocation of the function. - fn obj_paint_if_requested(&self) -> bool { + pub fn obj_paint_if_requested(&self) -> bool { let mut inner = self.inner.borrow_mut(); // Place the root component on the screen in case it was previously requested. @@ -220,6 +231,21 @@ impl LayoutObj { unsafe { Gc::as_mut(&mut inner.root) }.obj_paint() } + /// Run a paint pass over the component tree. Returns true if any component + /// actually requested painting since last invocation of the function. + pub fn obj_render(&self, offset: Offset) { + let mut inner = self.inner.borrow_mut(); + + // Place the root component on the screen in case it was previously requested. + if inner.event_ctx.needs_place_before_next_event_or_paint() { + // SAFETY: `inner.root` is unique because of the `inner.borrow_mut()`. + unsafe { Gc::as_mut(&mut inner.root) }.obj_place(constant::screen()); + } + + // SAFETY: `inner.root` is unique because of the `inner.borrow_mut()`. + unsafe { Gc::as_mut(&mut inner.root) }.obj_render(offset) + } + /// Run a tracing pass over the component tree. Passed `callback` is called /// with each piece of tracing information. Panics in case the callback /// raises an exception. diff --git a/core/embed/rust/src/ui/mod.rs b/core/embed/rust/src/ui/mod.rs index b08f77789..ab29227b7 100644 --- a/core/embed/rust/src/ui/mod.rs +++ b/core/embed/rust/src/ui/mod.rs @@ -23,5 +23,7 @@ pub mod model_tr; #[cfg(feature = "model_tt")] pub mod model_tt; pub mod ui_features; - pub use ui_features::UIFeaturesCommon; + +#[cfg(any(feature = "model_tt", feature = "model_mercury"))] +pub mod demo; // !!! REMOVE diff --git a/core/embed/unix/main.c b/core/embed/unix/main.c index 2acd5b528..a14acef66 100644 --- a/core/embed/unix/main.c +++ b/core/embed/unix/main.c @@ -56,6 +56,8 @@ #include "common.h" +#include "rust_ui.h" + // Command line options, with their defaults STATIC bool compile_only = false; STATIC uint emit_opt = MP_EMIT_OPT_NONE; @@ -547,6 +549,10 @@ MP_NOINLINE int main_(int argc, char **argv) { mp_obj_list_init(MP_OBJ_TO_PTR(mp_sys_argv), 0); + display_backlight(255); + drawlib_demo(); // TODO: !!! remove + while (1) ; + // Here is some example code to create a class and instance of that class. // First is the Python, then the C code. //