diff --git a/core/embed/extmod/modtrezorui/buffers.c b/core/embed/extmod/modtrezorui/buffers.c index df3194f61..0d987a6e7 100644 --- a/core/embed/extmod/modtrezorui/buffers.c +++ b/core/embed/extmod/modtrezorui/buffers.c @@ -22,8 +22,6 @@ #include "fonts/fonts.h" #include "memzero.h" -#if USE_DMA2D - #define BUFFERS_16BPP 3 #define BUFFERS_4BPP 3 #define BUFFERS_TEXT 1 @@ -103,5 +101,3 @@ buffer_blurring_t* buffers_get_blurring_buffer(uint16_t idx, bool clear) { } return &blurring_buffers[idx]; } - -#endif diff --git a/core/embed/extmod/modtrezorui/buffers.h b/core/embed/extmod/modtrezorui/buffers.h index 217561c70..339876fe7 100644 --- a/core/embed/extmod/modtrezorui/buffers.h +++ b/core/embed/extmod/modtrezorui/buffers.h @@ -33,9 +33,9 @@ #error Text buffer height is too small, please adjust to match used fonts #endif -#define LINE_BUFFER_16BPP_SIZE BUFFER_PIXELS * 2 -#define LINE_BUFFER_4BPP_SIZE BUFFER_PIXELS / 2 -#define TEXT_BUFFER_SIZE (BUFFER_PIXELS * TEXT_BUFFER_HEIGHT) / 2 +#define LINE_BUFFER_16BPP_SIZE (BUFFER_PIXELS * 2) +#define LINE_BUFFER_4BPP_SIZE (BUFFER_PIXELS / 2) +#define TEXT_BUFFER_SIZE ((BUFFER_PIXELS * TEXT_BUFFER_HEIGHT) / 2) #define JPEG_BUFFER_SIZE (BUFFER_PIXELS * 16) // 3100 is needed according to tjpgd docs, diff --git a/core/embed/rust/src/ui/component/marquee.rs b/core/embed/rust/src/ui/component/marquee.rs new file mode 100644 index 000000000..d3559521d --- /dev/null +++ b/core/embed/rust/src/ui/component/marquee.rs @@ -0,0 +1,240 @@ +use crate::{ + time::{Duration, Instant}, + ui::{ + animation::Animation, + component::{Component, Event, EventCtx, Never, TimerToken}, + display, + display::{Color, Font}, + geometry::Rect, + }, +}; + +const MILLIS_PER_LETTER_M: u32 = 300; + +enum State { + Initial, + Left(Animation), + PauseLeft, + Right(Animation), + PauseRight, +} + +pub struct Marquee { + area: Rect, + pause_token: Option, + min_offset: i16, + max_offset: i16, + state: State, + text: T, + font: Font, + fg: Color, + bg: Color, + duration: Duration, + pause: Duration, +} + +impl Marquee +where + T: AsRef, +{ + pub fn new(text: T, font: Font, fg: Color, bg: Color) -> Self { + Self { + area: Rect::zero(), + pause_token: None, + min_offset: 0, + max_offset: 0, + state: State::Initial, + text, + font, + fg, + bg, + duration: Duration::from_millis(2000), + pause: Duration::from_millis(1000), + } + } + + pub fn start(&mut self, ctx: &mut EventCtx, now: Instant) { + if let State::Initial = self.state { + let text_width = self.font.text_width(self.text.as_ref()); + let max_offset = self.area.width() - text_width; + + self.min_offset = 0; + self.max_offset = max_offset; + + let anim = Animation::new(self.min_offset, max_offset, self.duration, now); + + self.state = State::Left(anim); + + // The animation is starting, request an animation frame event. + ctx.request_anim_frame(); + + // We don't have to wait for the animation frame event with the first paint, + // let's do that now. + ctx.request_paint(); + } + } + + pub fn reset(&mut self) { + self.state = State::Initial; + } + + pub fn animation(&self) -> Option<&Animation> { + match &self.state { + State::Initial => None, + State::Left(a) => Some(a), + State::PauseLeft => None, + State::Right(a) => Some(a), + State::PauseRight => None, + } + } + + pub fn is_at_right(&self, now: Instant) -> bool { + if let Some(p) = self.progress(now) { + return p == self.min_offset; + } + false + } + + pub fn is_at_left(&self, now: Instant) -> bool { + if let Some(p) = self.progress(now) { + return p == self.max_offset; + } + false + } + + pub fn progress(&self, now: Instant) -> Option { + self.animation().map(|a| a.value(now)) + } + + pub fn is_animating(&self) -> bool { + self.animation().is_some() + } + + pub fn paint_anim(&mut self, offset: i16) { + display::marquee( + self.area, + self.text.as_ref(), + offset, + self.font, + self.fg, + self.bg, + ); + } +} + +impl Component for Marquee +where + T: AsRef, +{ + type Msg = Never; + + fn place(&mut self, bounds: Rect) -> Rect { + let base_width = self.font.text_width("M"); + let text_width = self.font.text_width(self.text.as_ref()); + let area_width = bounds.width(); + + let shift_width = if area_width > text_width { + area_width - text_width + } else { + text_width - area_width + }; + + let mut duration = (MILLIS_PER_LETTER_M * shift_width as u32) / base_width as u32; + if duration < MILLIS_PER_LETTER_M { + duration = MILLIS_PER_LETTER_M; + } + + self.duration = Duration::from_millis(duration); + self.area = bounds; + self.area + } + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + let now = Instant::now(); + + if let Event::Timer(token) = event { + if self.pause_token == Some(token) { + match self.state { + State::PauseLeft => { + let anim = + Animation::new(self.max_offset, self.min_offset, self.duration, now); + self.state = State::Right(anim); + } + State::PauseRight => { + let anim = + Animation::new(self.min_offset, self.max_offset, self.duration, now); + self.state = State::Left(anim); + } + _ => {} + } + // We have something to paint, so request to be painted in the next pass. + ctx.request_paint(); + // There is further progress in the animation, request an animation frame event. + ctx.request_anim_frame(); + } + + if token == EventCtx::ANIM_FRAME_TIMER { + if self.is_animating() { + // We have something to paint, so request to be painted in the next pass. + ctx.request_paint(); + // There is further progress in the animation, request an animation frame + // event. + ctx.request_anim_frame(); + } + + match self.state { + State::Right(_) => { + if self.is_at_right(now) { + self.pause_token = Some(ctx.request_timer(self.pause)); + self.state = State::PauseRight; + } + } + State::Left(_) => { + if self.is_at_left(now) { + self.pause_token = Some(ctx.request_timer(self.pause)); + self.state = State::PauseLeft; + } + } + _ => {} + } + } + } + None + } + + fn paint(&mut self) { + let now = Instant::now(); + + match self.state { + State::Initial => { + self.paint_anim(0); + } + State::PauseRight => { + self.paint_anim(self.min_offset); + } + State::PauseLeft => { + self.paint_anim(self.max_offset); + } + _ => { + let progress = self.progress(now); + if let Some(done) = progress { + self.paint_anim(done as i16); + } else { + self.paint_anim(0); + } + } + } + } +} + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for Marquee +where + T: AsRef, +{ + fn trace(&self, d: &mut dyn crate::trace::Tracer) { + d.open("Marquee"); + d.field("text", &self.text.as_ref()); + d.close(); + } +} diff --git a/core/embed/rust/src/ui/component/mod.rs b/core/embed/rust/src/ui/component/mod.rs index 210e0e2aa..96aa50b37 100644 --- a/core/embed/rust/src/ui/component/mod.rs +++ b/core/embed/rust/src/ui/component/mod.rs @@ -6,6 +6,7 @@ pub mod empty; pub mod image; pub mod label; pub mod map; +pub mod marquee; pub mod maybe; pub mod pad; pub mod paginated; @@ -19,6 +20,7 @@ pub use border::Border; pub use empty::Empty; pub use label::Label; pub use map::Map; +pub use marquee::Marquee; pub use maybe::Maybe; pub use pad::Pad; pub use paginated::{PageMsg, Paginate}; diff --git a/core/embed/rust/src/ui/display/mod.rs b/core/embed/rust/src/ui/display/mod.rs index 6e225ed19..c146d3544 100644 --- a/core/embed/rust/src/ui/display/mod.rs +++ b/core/embed/rust/src/ui/display/mod.rs @@ -9,7 +9,7 @@ use super::{ }; #[cfg(feature = "dma2d")] use crate::trezorhal::{ - buffers::{get_buffer_16bpp, get_buffer_4bpp, get_text_buffer}, + buffers::{get_buffer_16bpp, get_buffer_4bpp}, dma2d::{ dma2d_setup_4bpp_over_16bpp, dma2d_setup_4bpp_over_4bpp, dma2d_start_blend, dma2d_wait_for_transfer, @@ -22,7 +22,7 @@ use crate::ui::geometry::TOP_LEFT; use crate::{ error::Error, time::Duration, - trezorhal::{display, qr, time, uzlib::UzlibContext}, + trezorhal::{buffers::get_text_buffer, display, qr, time, uzlib::UzlibContext}, ui::{component::image::Image, lerp::Lerp}, }; use core::slice; @@ -756,6 +756,36 @@ pub fn bar_with_text_and_fill( pixeldata_dirty(); } +pub fn marquee(area: Rect, text: &str, offset: i16, font: Font, fg: Color, bg: Color) { + let buffer = unsafe { get_text_buffer(0, true) }; + + let area = area.translate(get_offset()); + let clamped = area.clamp(constant::screen()); + set_window(clamped); + + display::text_into_buffer(text, font.into(), buffer, offset); + let tbl = get_color_table(fg, bg); + + for y in 0..clamped.height() { + for x in 0..clamped.width() { + let pixel = y * constant::WIDTH + x; + let byte_idx = pixel / 2; + if byte_idx < buffer.buffer.len() as _ { + let data = if pixel % 2 != 0 { + buffer.buffer[byte_idx as usize] >> 4 + } else { + buffer.buffer[byte_idx as usize] & 0xF + }; + pixeldata(tbl[data as usize]); + } else { + pixeldata(bg); + } + } + } + + pixeldata_dirty(); +} + // Used on T1 only. pub fn dotted_line(start: Point, width: i16, color: Color) { for x in (start.x..width).step_by(2) {