1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-11-17 21:22:10 +00:00

feat(core/rust): basic marquee implementation

[no changelog]
This commit is contained in:
tychovrahe 2022-11-22 16:42:54 +01:00 committed by TychoVrahe
parent bfda33dddc
commit 9f7edf210e
5 changed files with 277 additions and 9 deletions

View File

@ -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

View File

@ -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,

View File

@ -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<i16>),
PauseLeft,
Right(Animation<i16>),
PauseRight,
}
pub struct Marquee<T> {
area: Rect,
pause_token: Option<TimerToken>,
min_offset: i16,
max_offset: i16,
state: State,
text: T,
font: Font,
fg: Color,
bg: Color,
duration: Duration,
pause: Duration,
}
impl<T> Marquee<T>
where
T: AsRef<str>,
{
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<i16>> {
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<i16> {
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<T> Component for Marquee<T>
where
T: AsRef<str>,
{
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<Self::Msg> {
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<T> crate::trace::Trace for Marquee<T>
where
T: AsRef<str>,
{
fn trace(&self, d: &mut dyn crate::trace::Tracer) {
d.open("Marquee");
d.field("text", &self.text.as_ref());
d.close();
}
}

View File

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

View File

@ -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) {