From 936ed18e48d9f46d263a979621c26cc6679d8e6c Mon Sep 17 00:00:00 2001 From: cepetr Date: Thu, 22 Feb 2024 16:25:05 +0100 Subject: [PATCH] refactor(core): integrate new drawing library --- core/SConscript.firmware | 1 + core/SConscript.unix | 1 + core/embed/rust/Cargo.toml | 1 + core/embed/rust/src/ui/component/base.rs | 28 +++ core/embed/rust/src/ui/component/border.rs | 9 +- core/embed/rust/src/ui/component/connect.rs | 17 +- core/embed/rust/src/ui/component/empty.rs | 4 +- core/embed/rust/src/ui/component/image.rs | 17 ++ core/embed/rust/src/ui/component/label.rs | 5 + core/embed/rust/src/ui/component/map.rs | 6 +- core/embed/rust/src/ui/component/marquee.rs | 41 +++- core/embed/rust/src/ui/component/maybe.rs | 8 + core/embed/rust/src/ui/component/pad.rs | 8 + core/embed/rust/src/ui/component/painter.rs | 14 ++ core/embed/rust/src/ui/component/placed.rs | 18 ++ core/embed/rust/src/ui/component/qr_code.rs | 35 +++ .../rust/src/ui/component/text/formatted.rs | 7 +- .../rust/src/ui/component/text/layout.rs | 72 ++++++ .../rust/src/ui/component/text/paragraphs.rs | 48 ++++ core/embed/rust/src/ui/component/text/util.rs | 59 +++++ core/embed/rust/src/ui/component/timeout.rs | 3 + core/embed/rust/src/ui/display/tjpgd.rs | 79 +++++++ core/embed/rust/src/ui/layout/obj.rs | 26 ++- .../rust/src/ui/model_tr/bootloader/intro.rs | 23 ++ .../rust/src/ui/model_tr/bootloader/menu.rs | 29 ++- .../src/ui/model_tr/bootloader/welcome.rs | 28 ++- .../ui/model_tr/component/address_details.rs | 11 + .../src/ui/model_tr/component/bl_confirm.rs | 28 +++ .../rust/src/ui/model_tr/component/button.rs | 84 +++++++ .../model_tr/component/button_controller.rs | 26 +++ .../ui/model_tr/component/changing_text.rs | 41 ++++ .../model_tr/component/coinjoin_progress.rs | 58 ++++- .../rust/src/ui/model_tr/component/error.rs | 28 +++ .../rust/src/ui/model_tr/component/flow.rs | 18 ++ .../src/ui/model_tr/component/flow_pages.rs | 5 + .../rust/src/ui/model_tr/component/frame.rs | 12 + .../ui/model_tr/component/hold_to_confirm.rs | 5 + .../src/ui/model_tr/component/homescreen.rs | 184 +++++++++++++-- .../component/input_methods/choice.rs | 139 ++++++++++++ .../component/input_methods/choice_item.rs | 102 +++++++++ .../component/input_methods/number_input.rs | 5 + .../component/input_methods/passphrase.rs | 6 + .../model_tr/component/input_methods/pin.rs | 7 + .../component/input_methods/simple_choice.rs | 5 + .../component/input_methods/wordlist.rs | 6 + .../rust/src/ui/model_tr/component/loader.rs | 75 ++++++- .../rust/src/ui/model_tr/component/page.rs | 7 + .../src/ui/model_tr/component/progress.rs | 28 ++- .../rust/src/ui/model_tr/component/result.rs | 15 ++ .../src/ui/model_tr/component/scrollbar.rs | 50 +++++ .../src/ui/model_tr/component/share_words.rs | 56 ++++- .../src/ui/model_tr/component/show_more.rs | 6 + .../rust/src/ui/model_tr/component/title.rs | 35 ++- .../ui/model_tr/component/welcome_screen.rs | 26 +++ .../src/ui/model_tr/cshape/dotted_line.rs | 88 ++++++++ .../src/ui/model_tr/cshape/loader_circular.rs | 117 ++++++++++ .../src/ui/model_tr/cshape/loader_small.rs | 89 ++++++++ .../src/ui/model_tr/cshape/loader_starry.rs | 106 +++++++++ core/embed/rust/src/ui/model_tr/cshape/mod.rs | 9 + core/embed/rust/src/ui/model_tr/layout.rs | 14 +- core/embed/rust/src/ui/model_tr/mod.rs | 1 + core/embed/rust/src/ui/model_tr/screens.rs | 29 ++- .../rust/src/ui/model_tt/bootloader/intro.rs | 10 + .../rust/src/ui/model_tt/bootloader/menu.rs | 9 + .../src/ui/model_tt/bootloader/welcome.rs | 28 ++- .../ui/model_tt/component/address_details.rs | 9 + .../src/ui/model_tt/component/bl_confirm.rs | 36 +++ .../rust/src/ui/model_tt/component/button.rs | 61 +++++ .../model_tt/component/coinjoin_progress.rs | 40 +++- .../rust/src/ui/model_tt/component/dialog.rs | 12 + .../rust/src/ui/model_tt/component/error.rs | 17 ++ .../rust/src/ui/model_tt/component/fido.rs | 32 +++ .../rust/src/ui/model_tt/component/frame.rs | 8 + .../ui/model_tt/component/homescreen/mod.rs | 211 +++++++++++++++++- .../ui/model_tt/component/keyboard/bip39.rs | 49 +++- .../ui/model_tt/component/keyboard/common.rs | 23 ++ .../model_tt/component/keyboard/mnemonic.rs | 14 ++ .../model_tt/component/keyboard/passphrase.rs | 52 ++++- .../src/ui/model_tt/component/keyboard/pin.rs | 93 +++++++- .../ui/model_tt/component/keyboard/slip39.rs | 69 +++++- .../model_tt/component/keyboard/word_count.rs | 7 + .../rust/src/ui/model_tt/component/loader.rs | 51 ++++- .../src/ui/model_tt/component/number_input.rs | 30 ++- .../rust/src/ui/model_tt/component/page.rs | 36 ++- .../src/ui/model_tt/component/progress.rs | 46 +++- .../rust/src/ui/model_tt/component/result.rs | 33 +++ .../rust/src/ui/model_tt/component/scroll.rs | 45 ++++ .../src/ui/model_tt/component/simple_page.rs | 13 ++ .../rust/src/ui/model_tt/component/swipe.rs | 3 + .../ui/model_tt/component/welcome_screen.rs | 38 +++- core/embed/rust/src/ui/model_tt/constant.rs | 5 + core/embed/rust/src/ui/model_tt/screens.rs | 29 ++- 92 files changed, 3142 insertions(+), 75 deletions(-) create mode 100644 core/embed/rust/src/ui/model_tr/cshape/dotted_line.rs create mode 100644 core/embed/rust/src/ui/model_tr/cshape/loader_circular.rs create mode 100644 core/embed/rust/src/ui/model_tr/cshape/loader_small.rs create mode 100644 core/embed/rust/src/ui/model_tr/cshape/loader_starry.rs create mode 100644 core/embed/rust/src/ui/model_tr/cshape/mod.rs diff --git a/core/SConscript.firmware b/core/SConscript.firmware index 13fa447e7..08545e565 100644 --- a/core/SConscript.firmware +++ b/core/SConscript.firmware @@ -741,6 +741,7 @@ def cargo_build(): features.append('universal_fw') features.append('ui') features.append('translations') + features.append('new_rendering') if PYOPT == '0': features.append('debug') features.append('ui_debug') diff --git a/core/SConscript.unix b/core/SConscript.unix index 619a161be..448f3a78d 100644 --- a/core/SConscript.unix +++ b/core/SConscript.unix @@ -830,6 +830,7 @@ def cargo_build(): features.append('universal_fw') features.append('ui') features.append('translations') + features.append('new_rendering') if PYOPT == '0': features.append('debug') if DMA2D: diff --git a/core/embed/rust/Cargo.toml b/core/embed/rust/Cargo.toml index 5de592a8f..2381cdfa7 100644 --- a/core/embed/rust/Cargo.toml +++ b/core/embed/rust/Cargo.toml @@ -21,6 +21,7 @@ ui_bounds = [] ui_antialiasing = [] ui_blurring = [] ui_jpeg_decoder = [] +new_rendering = [] bootloader = [] button = [] touch = [] diff --git a/core/embed/rust/src/ui/component/base.rs b/core/embed/rust/src/ui/component/base.rs index 29e88cc9c..b82f12076 100644 --- a/core/embed/rust/src/ui/component/base.rs +++ b/core/embed/rust/src/ui/component/base.rs @@ -8,6 +8,7 @@ use crate::{ component::{maybe::PaintOverlapping, MsgMap}, display::{self, Color}, geometry::{Offset, Rect}, + shape::Renderer, }, }; @@ -60,6 +61,8 @@ pub trait Component { /// the `Child` wrapper. fn paint(&mut self); + fn render<'s>(&'s self, _target: &mut impl Renderer<'s>); + #[cfg(feature = "ui_bounds")] /// Report current paint bounds of this component. Used for debugging. fn bounds(&self, _sink: &mut dyn FnMut(Rect)) {} @@ -153,6 +156,10 @@ where } } + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.component.render(target); + } + #[cfg(feature = "ui_bounds")] fn bounds(&self, sink: &mut dyn FnMut(Rect)) { self.component.bounds(sink) @@ -253,6 +260,10 @@ where self.inner.paint(); } + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.inner.render(target); + } + #[cfg(feature = "ui_bounds")] fn bounds(&self, sink: &mut dyn FnMut(Rect)) { self.inner.bounds(sink) @@ -291,6 +302,11 @@ where self.1.paint(); } + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.0.render(target); + self.1.render(target); + } + #[cfg(feature = "ui_bounds")] fn bounds(&self, sink: &mut dyn FnMut(Rect)) { self.0.bounds(sink); @@ -340,6 +356,12 @@ where self.2.paint(); } + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.0.render(target); + self.1.render(target); + self.2.render(target); + } + #[cfg(feature = "ui_bounds")] fn bounds(&self, sink: &mut dyn FnMut(Rect)) { self.0.bounds(sink); @@ -367,6 +389,12 @@ where } } + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + if let Some(ref c) = self { + c.render(target) + } + } + fn place(&mut self, bounds: Rect) -> Rect { match self { Some(ref mut c) => c.place(bounds), diff --git a/core/embed/rust/src/ui/component/border.rs b/core/embed/rust/src/ui/component/border.rs index e61cf381a..47941ae62 100644 --- a/core/embed/rust/src/ui/component/border.rs +++ b/core/embed/rust/src/ui/component/border.rs @@ -1,5 +1,8 @@ use super::{Component, Event, EventCtx}; -use crate::ui::geometry::{Insets, Rect}; +use crate::ui::{ + geometry::{Insets, Rect}, + shape::Renderer, +}; pub struct Border { border: Insets, @@ -39,6 +42,10 @@ where self.inner.paint() } + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.inner.render(target); + } + #[cfg(feature = "ui_bounds")] fn bounds(&self, sink: &mut dyn FnMut(Rect)) { self.inner.bounds(sink); diff --git a/core/embed/rust/src/ui/component/connect.rs b/core/embed/rust/src/ui/component/connect.rs index 5faa38924..3d7897bdf 100644 --- a/core/embed/rust/src/ui/component/connect.rs +++ b/core/embed/rust/src/ui/component/connect.rs @@ -3,7 +3,8 @@ use crate::{ ui::{ component::{Component, Event, EventCtx, Never, Pad}, display::{self, Color, Font}, - geometry::{Offset, Rect}, + geometry::{Alignment, Offset, Rect}, + shape::{self, Renderer}, }, }; @@ -55,6 +56,20 @@ impl Component for Connect { ) }); } + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + let font = Font::NORMAL; + + self.bg.render(target); + + self.message.map(|t| { + shape::Text::new(self.bg.area.center() + Offset::y(font.text_height() / 2), t) + .with_fg(self.fg) + .with_font(font) + .with_align(Alignment::Center) + .render(target); + }); + } } #[cfg(feature = "micropython")] diff --git a/core/embed/rust/src/ui/component/empty.rs b/core/embed/rust/src/ui/component/empty.rs index ce2a3b883..15fb017dc 100644 --- a/core/embed/rust/src/ui/component/empty.rs +++ b/core/embed/rust/src/ui/component/empty.rs @@ -1,5 +1,5 @@ use super::{Component, Event, EventCtx, Never}; -use crate::ui::geometry::Rect; +use crate::ui::{geometry::Rect, shape::Renderer}; pub struct Empty; @@ -15,6 +15,8 @@ impl Component for Empty { } fn paint(&mut self) {} + + fn render<'s>(&'s self, _target: &mut impl Renderer<'s>) {} } #[cfg(feature = "ui_debug")] diff --git a/core/embed/rust/src/ui/component/image.rs b/core/embed/rust/src/ui/component/image.rs index 41c1a6883..959ccf2c4 100644 --- a/core/embed/rust/src/ui/component/image.rs +++ b/core/embed/rust/src/ui/component/image.rs @@ -6,6 +6,8 @@ use crate::ui::{ Color, Icon, }, geometry::{Alignment2D, Offset, Point, Rect}, + shape, + shape::Renderer, }; #[derive(PartialEq, Eq, Clone, Copy)] @@ -48,6 +50,12 @@ impl Component for Image { self.draw(self.area.center(), Alignment2D::CENTER); } + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + shape::ToifImage::new(self.area.center(), self.toif) + .with_align(Alignment2D::CENTER) + .render(target); + } + #[cfg(feature = "ui_bounds")] fn bounds(&self, sink: &mut dyn FnMut(Rect)) { sink(Rect::from_center_and_size( @@ -130,6 +138,15 @@ impl Component for BlendedImage { self.paint_image(); } + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + shape::ToifImage::new(self.bg_top_left, self.bg.toif) + .with_fg(self.bg_color) + .render(target); + shape::ToifImage::new(self.bg_top_left + self.fg_offset, self.fg.toif) + .with_fg(self.fg_color) + .render(target); + } + #[cfg(feature = "ui_bounds")] fn bounds(&self, sink: &mut dyn FnMut(Rect)) { sink(Rect::from_top_left_and_size( diff --git a/core/embed/rust/src/ui/component/label.rs b/core/embed/rust/src/ui/component/label.rs index 0bbc21a21..ae05b3eaf 100644 --- a/core/embed/rust/src/ui/component/label.rs +++ b/core/embed/rust/src/ui/component/label.rs @@ -2,6 +2,7 @@ use crate::ui::{ component::{Component, Event, EventCtx, Never}, display::Font, geometry::{Alignment, Insets, Offset, Point, Rect}, + shape::Renderer, }; use super::{text::TextStyle, TextLayout}; @@ -119,6 +120,10 @@ where self.layout.render_text(self.text.as_ref()); } + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.layout.render_text2(self.text.as_ref(), target); + } + #[cfg(feature = "ui_bounds")] fn bounds(&self, sink: &mut dyn FnMut(Rect)) { sink(self.layout.bounds) diff --git a/core/embed/rust/src/ui/component/map.rs b/core/embed/rust/src/ui/component/map.rs index 867d51f59..76100cbf5 100644 --- a/core/embed/rust/src/ui/component/map.rs +++ b/core/embed/rust/src/ui/component/map.rs @@ -1,5 +1,5 @@ use super::{Component, Event, EventCtx}; -use crate::ui::geometry::Rect; +use crate::ui::{geometry::Rect, shape::Renderer}; pub struct MsgMap { inner: T, @@ -31,6 +31,10 @@ where self.inner.paint() } + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.inner.render(target); + } + #[cfg(feature = "ui_bounds")] fn bounds(&self, sink: &mut dyn FnMut(Rect)) { self.inner.bounds(sink); diff --git a/core/embed/rust/src/ui/component/marquee.rs b/core/embed/rust/src/ui/component/marquee.rs index fe38a8f22..1a1f533d8 100644 --- a/core/embed/rust/src/ui/component/marquee.rs +++ b/core/embed/rust/src/ui/component/marquee.rs @@ -3,9 +3,9 @@ use crate::{ ui::{ animation::Animation, component::{Component, Event, EventCtx, Never, TimerToken}, - display, - display::{Color, Font}, - geometry::Rect, + display::{self, Color, Font}, + geometry::{Offset, Rect}, + shape::{self, Renderer}, util::animation_disabled, }, }; @@ -131,6 +131,17 @@ where self.bg, ); } + + pub fn render_anim<'s>(&'s self, target: &mut impl Renderer<'s>, offset: i16) { + target.in_window(self.area, &mut |target| { + let text_height = self.font.text_height(); + let pos = self.area.top_left() + Offset::new(offset, text_height - 1); + shape::Text::new(pos, self.text.as_ref()) + .with_font(self.font) + .with_fg(self.fg) + .render(target); + }); + } } impl Component for Marquee @@ -225,6 +236,30 @@ where } } } + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + let now = Instant::now(); + + match self.state { + State::Initial => { + self.render_anim(target, 0); + } + State::PauseRight => { + self.render_anim(target, self.min_offset); + } + State::PauseLeft => { + self.render_anim(target, self.max_offset); + } + _ => { + let progress = self.progress(now); + if let Some(done) = progress { + self.render_anim(target, done); + } else { + self.render_anim(target, 0); + } + } + } + } } #[cfg(feature = "ui_debug")] diff --git a/core/embed/rust/src/ui/component/maybe.rs b/core/embed/rust/src/ui/component/maybe.rs index 739f1ab82..bbc843bca 100644 --- a/core/embed/rust/src/ui/component/maybe.rs +++ b/core/embed/rust/src/ui/component/maybe.rs @@ -2,6 +2,7 @@ use crate::ui::{ component::{Component, ComponentExt, Event, EventCtx, Pad}, display::{self, Color}, geometry::Rect, + shape::Renderer, }; pub struct Maybe { @@ -94,6 +95,13 @@ where } } + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.pad.render(target); + if self.visible { + self.inner.render(target); + } + } + #[cfg(feature = "ui_bounds")] fn bounds(&self, sink: &mut dyn FnMut(Rect)) { sink(self.pad.area); diff --git a/core/embed/rust/src/ui/component/pad.rs b/core/embed/rust/src/ui/component/pad.rs index b26d90d77..fa6b5b065 100644 --- a/core/embed/rust/src/ui/component/pad.rs +++ b/core/embed/rust/src/ui/component/pad.rs @@ -1,6 +1,8 @@ use crate::ui::{ display::{self, Color}, geometry::Rect, + shape, + shape::Renderer, }; pub struct Pad { @@ -52,4 +54,10 @@ impl Pad { display::rect_fill(self.area, self.color); } } + + pub fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + shape::Bar::new(self.area) + .with_bg(self.color) + .render(target); + } } diff --git a/core/embed/rust/src/ui/component/painter.rs b/core/embed/rust/src/ui/component/painter.rs index 9d6b18e37..d361ef177 100644 --- a/core/embed/rust/src/ui/component/painter.rs +++ b/core/embed/rust/src/ui/component/painter.rs @@ -4,6 +4,8 @@ use crate::ui::{ component::{image::Image, Component, Event, EventCtx, Never}, display, geometry::{Alignment2D, Rect}, + shape, + shape::Renderer, }; pub struct Painter { @@ -39,6 +41,15 @@ where (self.func)(self.area); } + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + let area = self.area; + shape::Bar::new(area) + .with_thickness(1) + .with_fg(display::Color::white()) + .render(target); + shape::Text::new(area.top_left(), "Paint").render(target); // !@# replace + } + #[cfg(feature = "ui_bounds")] fn bounds(&self, sink: &mut dyn FnMut(Rect)) { sink(self.area) @@ -64,7 +75,10 @@ pub fn jpeg_painter<'a>( scale: u8, ) -> Painter { let off = Offset::new(size.x / (2 << scale), size.y / (2 << scale)); + #[cfg(not(feature = "new_rendering"))] let f = move |area: Rect| display::tjpgd::jpeg(image(), area.center() - off, scale); + #[cfg(feature = "new_rendering")] + let f = move |area: Rect| {}; Painter::new(f) } diff --git a/core/embed/rust/src/ui/component/placed.rs b/core/embed/rust/src/ui/component/placed.rs index c4a758045..18fcc1e18 100644 --- a/core/embed/rust/src/ui/component/placed.rs +++ b/core/embed/rust/src/ui/component/placed.rs @@ -1,6 +1,7 @@ use crate::ui::{ component::{Component, Event, EventCtx}, geometry::{Alignment, Alignment2D, Axis, Grid, GridCellSpan, Insets, Offset, Rect}, + shape::Renderer, }; pub struct GridPlaced { @@ -63,6 +64,10 @@ where fn paint(&mut self) { self.inner.paint() } + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.inner.render(target); + } } #[cfg(feature = "ui_debug")] @@ -106,6 +111,10 @@ where fn paint(&mut self) { self.inner.paint() } + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.inner.render(target); + } } #[cfg(feature = "ui_debug")] @@ -178,6 +187,10 @@ where fn paint(&mut self) { self.inner.paint() } + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.inner.render(target); + } } #[cfg(feature = "ui_debug")] @@ -269,6 +282,11 @@ where self.first.paint(); self.second.paint(); } + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.first.render(target); + self.second.render(target); + } } #[cfg(feature = "ui_debug")] diff --git a/core/embed/rust/src/ui/component/qr_code.rs b/core/embed/rust/src/ui/component/qr_code.rs index e99236e3a..6b13e116d 100644 --- a/core/embed/rust/src/ui/component/qr_code.rs +++ b/core/embed/rust/src/ui/component/qr_code.rs @@ -8,6 +8,8 @@ use crate::{ constant, display::{pixeldata, pixeldata_dirty, rect_fill_rounded, set_window, Color}, geometry::{Insets, Offset, Rect}, + shape, + shape::Renderer, }, }; @@ -141,6 +143,39 @@ impl Component for Qr { Self::draw(&qr, area, self.border, scale); } + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + let mut outbuffer = [0u8; QR_MAX_VERSION.buffer_len()]; + let mut tempbuffer = [0u8; QR_MAX_VERSION.buffer_len()]; + + let qr = QrCode::encode_text( + self.text.as_ref(), + &mut tempbuffer, + &mut outbuffer, + QrCodeEcc::Medium, + Version::MIN, + QR_MAX_VERSION, + None, + true, + ); + let qr = unwrap!(qr); + + let scale = (self.area.width().min(self.area.height()) - self.border) / (qr.size() as i16); + let side = scale * qr.size() as i16; + let qr_area = Rect::from_center_and_size(self.area.center(), Offset::uniform(side)); + + if self.border > 0 { + shape::Bar::new(qr_area.expand(self.border)) + .with_bg(LIGHT) + .with_radius(CORNER_RADIUS as i16 + 1) // !@# + 1 to fix difference on TR + .render(target); + } + + shape::QrImage::new(qr_area, &qr) + .with_fg(LIGHT) + .with_bg(DARK) + .render(target); + } + #[cfg(feature = "ui_bounds")] fn bounds(&self, sink: &mut dyn FnMut(Rect)) { sink(self.area) diff --git a/core/embed/rust/src/ui/component/text/formatted.rs b/core/embed/rust/src/ui/component/text/formatted.rs index 58613f3ae..8ebe7527a 100644 --- a/core/embed/rust/src/ui/component/text/formatted.rs +++ b/core/embed/rust/src/ui/component/text/formatted.rs @@ -3,11 +3,12 @@ use crate::{ ui::{ component::{Component, Event, EventCtx, Never, Paginate}, geometry::{Alignment, Offset, Rect}, + shape::Renderer, }, }; use super::{ - layout::{LayoutFit, LayoutSink, TextNoOp, TextRenderer}, + layout::{LayoutFit, LayoutSink, TextNoOp, TextRenderer, TextRenderer2}, op::OpTextLayout, }; @@ -136,6 +137,10 @@ impl Component for FormattedText { self.layout_content(&mut TextRenderer); } + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.layout_content(&mut TextRenderer2::new(target)); + } + #[cfg(feature = "ui_bounds")] fn bounds(&self, sink: &mut dyn FnMut(Rect)) { sink(self.op_layout.layout.bounds) diff --git a/core/embed/rust/src/ui/component/text/layout.rs b/core/embed/rust/src/ui/component/text/layout.rs index 5c86089df..682d41425 100644 --- a/core/embed/rust/src/ui/component/text/layout.rs +++ b/core/embed/rust/src/ui/component/text/layout.rs @@ -2,6 +2,8 @@ use crate::ui::{ display, display::{toif::Icon, Color, Font, GlyphMetrics}, geometry::{Alignment, Alignment2D, Dimensions, Offset, Point, Rect}, + shape, + shape::Renderer, }; const ELLIPSIS: &str = "..."; @@ -235,6 +237,15 @@ impl TextLayout { self.layout_text(text, &mut self.initial_cursor(), &mut TextRenderer) } + /// Draw as much text as possible on the current screen. + pub fn render_text2<'s>(&self, text: &str, target: &mut impl Renderer<'s>) -> LayoutFit { + self.layout_text( + text, + &mut self.initial_cursor(), + &mut TextRenderer2::new(target), + ) + } + /// Loop through the `text` and try to fit it on the current screen, /// reporting events to `sink`, which may do something with them (e.g. draw /// on screen). @@ -530,6 +541,67 @@ impl LayoutSink for TextRenderer { } } +pub struct TextRenderer2<'a, 's, R>(pub &'a mut R, core::marker::PhantomData<&'s ()>) +where + R: Renderer<'s>; + +impl<'a, 's, R> TextRenderer2<'a, 's, R> +where + R: Renderer<'s>, +{ + pub fn new(target: &'a mut R) -> Self { + Self(target, core::marker::PhantomData) + } +} + +impl<'a, 's, R> LayoutSink for TextRenderer2<'a, 's, R> +where + R: Renderer<'s>, +{ + fn text(&mut self, cursor: Point, layout: &TextLayout, text: &str) { + shape::Text::new(cursor, text) + .with_font(layout.style.text_font) + .with_fg(layout.style.text_color) + .render(self.0); + } + + fn hyphen(&mut self, cursor: Point, layout: &TextLayout) { + shape::Text::new(cursor, "-") + .with_font(layout.style.text_font) + .with_fg(layout.style.hyphen_color) + .render(self.0); + } + + fn ellipsis(&mut self, cursor: Point, layout: &TextLayout) { + if let Some((icon, margin)) = layout.style.ellipsis_icon { + let bottom_left = cursor + Offset::x(margin); + shape::ToifImage::new(bottom_left, icon.toif) + .with_align(Alignment2D::BOTTOM_LEFT) + .with_fg(layout.style.ellipsis_color) + .render(self.0); + } else { + shape::Text::new(cursor, ELLIPSIS) + .with_font(layout.style.text_font) + .with_fg(layout.style.ellipsis_color) + .render(self.0); + } + } + + fn prev_page_ellipsis(&mut self, cursor: Point, layout: &TextLayout) { + if let Some((icon, _margin)) = layout.style.prev_page_ellipsis_icon { + shape::ToifImage::new(cursor, icon.toif) + .with_align(Alignment2D::BOTTOM_LEFT) + .with_fg(layout.style.ellipsis_color) + .render(self.0); + } else { + shape::Text::new(cursor, ELLIPSIS) + .with_font(layout.style.text_font) + .with_fg(layout.style.ellipsis_color) + .render(self.0); + } + } +} + #[cfg(feature = "ui_debug")] pub mod trace { use crate::{trace::ListTracer, ui::geometry::Point}; diff --git a/core/embed/rust/src/ui/component/text/paragraphs.rs b/core/embed/rust/src/ui/component/text/paragraphs.rs index 919341e0a..3a1fc0884 100644 --- a/core/embed/rust/src/ui/component/text/paragraphs.rs +++ b/core/embed/rust/src/ui/component/text/paragraphs.rs @@ -8,6 +8,8 @@ use crate::{ geometry::{ Alignment, Alignment2D, Dimensions, Insets, LinearPlacement, Offset, Point, Rect, }, + shape, + shape::Renderer, }, }; @@ -188,6 +190,17 @@ where ) } + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + Self::foreach_visible( + &self.source, + &self.visible, + self.offset, + &mut |layout, content| { + layout.render_text2(content, target); + }, + ) + } + #[cfg(feature = "ui_bounds")] fn bounds(&self, sink: &mut dyn FnMut(Rect)) { sink(self.area); @@ -606,6 +619,19 @@ impl Checklist { layout.style.background_color, ); } + + fn render_icon<'s>( + &self, + layout: &TextLayout, + icon: Icon, + offset: Offset, + target: &mut impl Renderer<'s>, + ) { + let top_left = Point::new(self.area.x0, layout.bounds.y0); + shape::ToifImage::new(top_left + offset, icon.toif) + .with_fg(layout.style.text_color) + .render(target); + } } impl Component for Checklist @@ -645,6 +671,28 @@ where } } + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.paragraphs.render(target); + + let current_visible = self.current.saturating_sub(self.paragraphs.offset.par); + for layout in self.paragraphs.visible.iter().take(current_visible) { + self.render_icon( + &layout.layout(&self.paragraphs.source), + self.icon_done, + self.done_offset, + target, + ); + } + if let Some(layout) = self.paragraphs.visible.iter().nth(current_visible) { + self.render_icon( + &layout.layout(&self.paragraphs.source), + self.icon_current, + self.current_offset, + target, + ); + } + } + #[cfg(feature = "ui_bounds")] fn bounds(&self, sink: &mut dyn FnMut(Rect)) { sink(self.area); diff --git a/core/embed/rust/src/ui/component/text/util.rs b/core/embed/rust/src/ui/component/text/util.rs index b417539ca..69ddd4de4 100644 --- a/core/embed/rust/src/ui/component/text/util.rs +++ b/core/embed/rust/src/ui/component/text/util.rs @@ -3,6 +3,7 @@ use crate::{ ui::{ display::{Color, Font}, geometry::{Alignment, Rect}, + shape::Renderer, }, }; @@ -37,6 +38,33 @@ pub fn text_multiline( } } +/// Draws longer multiline texts inside an area. +/// Splits lines on word boundaries/whitespace. +/// When a word is too long to fit one line, splitting +/// it on multiple lines with "-" at the line-ends. +/// +/// If it fits, returns the rest of the area. +/// If it does not fit, returns `None`. +pub fn text_multiline2<'s>( + target: &mut impl Renderer<'s>, + area: Rect, + text: TString<'_>, + font: Font, + fg_color: Color, + bg_color: Color, + alignment: Alignment, +) -> Option { + let text_style = TextStyle::new(font, fg_color, bg_color, fg_color, fg_color); + let text_layout = TextLayout::new(text_style) + .with_bounds(area) + .with_align(alignment); + let layout_fit = text.map(|t| text_layout.render_text2(t, target)); + match layout_fit { + LayoutFit::Fitting { height, .. } => Some(area.split_top(height).1), + LayoutFit::OutOfBounds { .. } => None, + } +} + /// Same as `text_multiline` above, but aligns the text to the bottom of the /// area. pub fn text_multiline_bottom( @@ -66,3 +94,34 @@ pub fn text_multiline_bottom( } }) } + +/// Same as `text_multiline` above, but aligns the text to the bottom of the +/// area. +pub fn text_multiline_bottom2<'s>( + target: &mut impl Renderer<'s>, + area: Rect, + text: TString<'_>, + font: Font, + fg_color: Color, + bg_color: Color, + alignment: Alignment, +) -> Option { + let text_style = TextStyle::new(font, fg_color, bg_color, fg_color, fg_color); + let mut text_layout = TextLayout::new(text_style) + .with_bounds(area) + .with_align(alignment); + // When text fits the area, displaying it in the bottom part. + // When not, render it "normally". + text.map(|t| match text_layout.fit_text(t) { + LayoutFit::Fitting { height, .. } => { + let (top, bottom) = area.split_bottom(height); + text_layout = text_layout.with_bounds(bottom); + text_layout.render_text2(t, target); + Some(top) + } + LayoutFit::OutOfBounds { .. } => { + text_layout.render_text2(t, target); + None + } + }) +} diff --git a/core/embed/rust/src/ui/component/timeout.rs b/core/embed/rust/src/ui/component/timeout.rs index 67d29c750..6da62e725 100644 --- a/core/embed/rust/src/ui/component/timeout.rs +++ b/core/embed/rust/src/ui/component/timeout.rs @@ -3,6 +3,7 @@ use crate::{ ui::{ component::{Component, Event, EventCtx, TimerToken}, geometry::Rect, + shape::Renderer, }, }; @@ -44,6 +45,8 @@ impl Component for Timeout { } fn paint(&mut self) {} + + fn render<'s>(&'s self, _target: &mut impl Renderer<'s>) {} } #[cfg(feature = "ui_debug")] diff --git a/core/embed/rust/src/ui/display/tjpgd.rs b/core/embed/rust/src/ui/display/tjpgd.rs index 734cad50c..f9803b441 100644 --- a/core/embed/rust/src/ui/display/tjpgd.rs +++ b/core/embed/rust/src/ui/display/tjpgd.rs @@ -24,6 +24,7 @@ pub fn jpeg(data: &[u8], pos: Point, scale: u8) { } } +#[cfg(not(feature = "new_rendering"))] pub fn jpeg_info(data: &[u8]) -> Option<(Offset, i16)> { let mut buffer = BufferJpegWork::get_cleared(); let pool = buffer.buffer.as_mut_slice(); @@ -40,6 +41,84 @@ pub fn jpeg_info(data: &[u8]) -> Option<(Offset, i16)> { result } +#[cfg(feature = "new_rendering")] +pub fn jpeg_info(data: &[u8]) -> Option<(Offset, i16)> { + const M_SOI: u16 = 0xFFD8; + const M_SOF0: u16 = 0xFFC0; + const M_DRI: u16 = 0xFFDD; + const M_RST0: u16 = 0xFFD0; + const M_RST7: u16 = 0xFFD7; + const M_SOS: u16 = 0xFFDA; + const M_EOI: u16 = 0xFFD9; + + let mut result = None; + let mut ofs = 0; + + let read_u16 = |ofs| -> Option { + if ofs + 1 < data.len() { + let result = Some(((data[ofs] as u16) << 8) + data[ofs + 1] as u16); + result + } else { + None + } + }; + + let read_u8 = |ofs| -> Option { + if ofs < data.len() { + let result = Some(data[ofs]); + result + } else { + None + } + }; + + while ofs < data.len() { + if read_u16(ofs)? == M_SOI { + break; + } + ofs += 1; + } + + loop { + let marker = read_u16(ofs)?; + + if (marker & 0xFF00) != 0xFF00 { + return None; + } + + ofs += 2; + + ofs += match marker { + M_SOI => 0, + M_SOF0 => { + let w = read_u16(ofs + 3)? as i16; + let h = read_u16(ofs + 5)? as i16; + // Number of components + let nc = read_u8(ofs + 7)?; + if (nc != 1) && (nc != 3) { + return None; + } + // Sampling factor of the first component + let c1 = read_u8(ofs + 9)?; + if (c1 != 0x11) && (c1 != 0x21) & (c1 != 0x22) { + return None; + }; + let mcu_height = (8 * (c1 & 15)) as i16; + result = Some((Offset::new(w, h), mcu_height)); + + read_u16(ofs)? + } + M_DRI => 4, + M_EOI => return None, + M_RST0..=M_RST7 => 0, + M_SOS => break, + _ => read_u16(ofs)?, + } as usize; + } + + result +} + pub fn jpeg_test(data: &[u8]) -> bool { let mut buffer = BufferJpegWork::get_cleared(); let pool = buffer.buffer.as_mut_slice(); diff --git a/core/embed/rust/src/ui/layout/obj.rs b/core/embed/rust/src/ui/layout/obj.rs index 612faeb32..62dcb48ae 100644 --- a/core/embed/rust/src/ui/layout/obj.rs +++ b/core/embed/rust/src/ui/layout/obj.rs @@ -19,8 +19,9 @@ use crate::{ ui::{ component::{Component, Event, EventCtx, Never, Root, TimerToken}, constant, - display::sync, + display::{sync, Color}, geometry::Rect, + shape::render_on_display, }, }; @@ -69,9 +70,26 @@ where } fn obj_paint(&mut self) -> bool { - let will_paint = self.inner().will_paint(); - self.paint(); - will_paint + #[cfg(not(feature = "new_rendering"))] + let legacy_mode = true; + + #[cfg(feature = "new_rendering")] + let legacy_mode = false; + + if legacy_mode { + let will_paint = self.inner().will_paint(); + self.paint(); + will_paint + } else { + let will_paint = self.inner().will_paint(); + if will_paint { + render_on_display(None, Some(Color::black()), |target| { + self.render(target); + }); + self.skip_paint(); + } + will_paint + } } #[cfg(feature = "ui_bounds")] diff --git a/core/embed/rust/src/ui/model_tr/bootloader/intro.rs b/core/embed/rust/src/ui/model_tr/bootloader/intro.rs index df114fc4c..969815f15 100644 --- a/core/embed/rust/src/ui/model_tr/bootloader/intro.rs +++ b/core/embed/rust/src/ui/model_tr/bootloader/intro.rs @@ -2,6 +2,8 @@ use crate::ui::{ component::{Child, Component, Event, EventCtx, Label, Pad}, geometry::{Alignment, Alignment2D, Rect}, layout::simplified::ReturnToC, + shape, + shape::Renderer, }; use super::super::{ @@ -101,6 +103,27 @@ impl<'a> Component for Intro<'a> { self.buttons.paint(); } + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.bg.render(target); + self.title.render(target); + + let area = self.bg.area; + + shape::ToifImage::new(area.top_left(), ICON_WARN_TITLE.toif) + .with_align(Alignment2D::TOP_LEFT) + .with_fg(BLD_FG) + .render(target); + + shape::ToifImage::new(area.top_left(), ICON_WARN_TITLE.toif) + .with_align(Alignment2D::TOP_RIGHT) + .with_fg(BLD_FG) + .render(target); + + self.warn.render(target); + self.text.render(target); + self.buttons.render(target); + } + #[cfg(feature = "ui_bounds")] fn bounds(&self, sink: &mut dyn FnMut(Rect)) { self.title.bounds(sink); diff --git a/core/embed/rust/src/ui/model_tr/bootloader/menu.rs b/core/embed/rust/src/ui/model_tr/bootloader/menu.rs index d0a5fa0e1..6ba41fb6e 100644 --- a/core/embed/rust/src/ui/model_tr/bootloader/menu.rs +++ b/core/embed/rust/src/ui/model_tr/bootloader/menu.rs @@ -7,8 +7,10 @@ use crate::{ constant::screen, display, display::{Font, Icon}, - geometry::{Alignment2D, Offset, Point, Rect}, + geometry::{Alignment, Alignment2D, Offset, Point, Rect}, layout::simplified::ReturnToC, + shape, + shape::Renderer, }, }; @@ -69,6 +71,26 @@ impl Choice for MenuChoice { ); } + fn render_center<'s>(&self, target: &mut impl Renderer<'s>, _area: Rect, _inverse: bool) { + // Icon on top and two lines of text below + shape::ToifImage::new(SCREEN_CENTER + Offset::y(-20), self.icon.toif) + .with_align(Alignment2D::CENTER) + .with_fg(BLD_FG) + .render(target); + + shape::Text::new(SCREEN_CENTER, self.first_line) + .with_align(Alignment::Center) + .with_font(Font::NORMAL) + .with_fg(BLD_FG) + .render(target); + + shape::Text::new(SCREEN_CENTER + Offset::y(10), self.second_line) + .with_align(Alignment::Center) + .with_font(Font::NORMAL) + .with_fg(BLD_FG) + .render(target); + } + fn btn_layout(&self) -> ButtonLayout { ButtonLayout::arrow_armed_arrow("SELECT".into()) } @@ -162,6 +184,11 @@ impl Component for Menu { self.choice_page.paint(); } + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.pad.render(target); + self.choice_page.render(target); + } + #[cfg(feature = "ui_bounds")] fn bounds(&self, sink: &mut dyn FnMut(Rect)) { self.choice_page.bounds(sink) diff --git a/core/embed/rust/src/ui/model_tr/bootloader/welcome.rs b/core/embed/rust/src/ui/model_tr/bootloader/welcome.rs index 0a5e03a29..ad261e3b1 100644 --- a/core/embed/rust/src/ui/model_tr/bootloader/welcome.rs +++ b/core/embed/rust/src/ui/model_tr/bootloader/welcome.rs @@ -1,7 +1,9 @@ use crate::ui::{ component::{Component, Event, EventCtx, Never, Pad}, display::{self, Font}, - geometry::{Offset, Rect}, + geometry::{Alignment, Offset, Rect}, + shape, + shape::Renderer, }; use super::super::theme::bootloader::{BLD_BG, BLD_FG}; @@ -57,4 +59,28 @@ impl Component for Welcome { BLD_BG, ); } + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.bg.render(target); + + let top_center = self.bg.area.top_center(); + + shape::Text::new(top_center + Offset::y(24), "Get started with") + .with_align(Alignment::Center) + .with_font(Font::NORMAL) + .with_fg(BLD_FG) + .render(target); + + shape::Text::new(top_center + Offset::y(32), "your Trezor at") + .with_align(Alignment::Center) + .with_font(Font::NORMAL) + .with_fg(BLD_FG) + .render(target); + + shape::Text::new(top_center + Offset::y(48), "trezor.io/start") + .with_align(Alignment::Center) + .with_font(Font::BOLD) + .with_fg(BLD_FG) + .render(target); + } } diff --git a/core/embed/rust/src/ui/model_tr/component/address_details.rs b/core/embed/rust/src/ui/model_tr/component/address_details.rs index 436c1e7fd..44b407c4a 100644 --- a/core/embed/rust/src/ui/model_tr/component/address_details.rs +++ b/core/embed/rust/src/ui/model_tr/component/address_details.rs @@ -10,6 +10,7 @@ use crate::{ Child, Component, Event, EventCtx, Pad, Paginate, Qr, }, geometry::Rect, + shape::Renderer, }, }; @@ -256,6 +257,16 @@ impl Component for AddressDetails { } } + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.pad.render(target); + self.buttons.render(target); + match self.current_page { + 0 => self.qr_code.render(target), + 1 => self.details_view.render(target), + _ => self.xpub_view.render(target), + } + } + #[cfg(feature = "ui_bounds")] fn bounds(&self, sink: &mut dyn FnMut(Rect)) { sink(self.area) diff --git a/core/embed/rust/src/ui/model_tr/component/bl_confirm.rs b/core/embed/rust/src/ui/model_tr/component/bl_confirm.rs index 34a1d5271..6eb18ba85 100644 --- a/core/embed/rust/src/ui/model_tr/component/bl_confirm.rs +++ b/core/embed/rust/src/ui/model_tr/component/bl_confirm.rs @@ -4,6 +4,8 @@ use crate::{ component::{Child, Component, ComponentExt, Event, EventCtx, Label, Pad}, display::{self, Color, Font}, geometry::{Point, Rect}, + shape, + shape::Renderer, }, }; @@ -227,6 +229,32 @@ where self.buttons.paint(); } + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.bg.render(target); + + let mut display_top_left = |text: TString<'static>| { + text.map(|t| { + shape::Text::new(Point::zero(), t) + .with_font(Font::BOLD) + .with_fg(WHITE) + .render(target); + }); + }; + + // We are either on the info screen or on the "main" screen + if self.showing_info_screen { + if let Some(title) = self.info_title { + display_top_left(title); + } + self.info_text.render(target); + } else { + display_top_left(self.title); + self.message.render(target); + self.alert.render(target); + } + self.buttons.render(target); + } + #[cfg(feature = "ui_bounds")] fn bounds(&self, sink: &mut dyn FnMut(Rect)) { self.buttons.bounds(sink); diff --git a/core/embed/rust/src/ui/model_tr/component/button.rs b/core/embed/rust/src/ui/model_tr/component/button.rs index 04d7762db..329d202e8 100644 --- a/core/embed/rust/src/ui/model_tr/component/button.rs +++ b/core/embed/rust/src/ui/model_tr/component/button.rs @@ -7,6 +7,8 @@ use crate::{ display::{self, Color, Font, Icon}, event::PhysicalButton, geometry::{Alignment2D, Offset, Point, Rect}, + shape, + shape::Renderer, }, }; @@ -265,6 +267,88 @@ impl Component for Button { } } } + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + let style = self.style(); + let fg_color = style.text_color; + let bg_color = fg_color.negate(); + let area = self.get_current_area(); + let inversed_colors = bg_color != theme::BG; + + // Filling the background (with 2-pixel rounding when applicable) + if inversed_colors { + shape::Bar::new(area) + .with_radius(3) + .with_bg(bg_color) + .render(target); + } else if style.with_outline { + shape::Bar::new(area) + .with_radius(3) + .with_fg(fg_color) + .render(target); + } else { + shape::Bar::new(area).with_bg(bg_color).render(target); + } + + // Optionally display "arms" at both sides of content - always in FG and BG + // colors (they are not inverted). + if style.with_arms { + shape::ToifImage::new(area.left_center(), theme::ICON_ARM_LEFT.toif) + .with_align(Alignment2D::TOP_RIGHT) + .with_fg(theme::FG) + .render(target); + + shape::ToifImage::new(area.right_center(), theme::ICON_ARM_RIGHT.toif) + .with_align(Alignment2D::TOP_LEFT) + .with_fg(theme::FG) + .render(target); + } + + // Painting the content + match &self.content { + ButtonContent::Text(text) => text.map(|t| { + shape::Text::new( + self.get_text_baseline(style) - Offset::x(style.font.start_x_bearing(t)), + t, + ) + .with_font(style.font) + .with_fg(fg_color) + .render(target); + }), + ButtonContent::Icon(icon) => { + // Allowing for possible offset of the area from current style + let icon_area = area.translate(style.offset); + if style.with_outline { + shape::ToifImage::new(icon_area.center(), icon.toif) + .with_align(Alignment2D::CENTER) + .with_fg(fg_color) + .render(target); + } else { + // Positioning the icon in the corresponding corner/center + match self.pos { + ButtonPos::Left => { + shape::ToifImage::new(icon_area.bottom_left(), icon.toif) + .with_align(Alignment2D::BOTTOM_LEFT) + .with_fg(fg_color) + .render(target) + } + + ButtonPos::Right => { + shape::ToifImage::new(icon_area.bottom_right(), icon.toif) + .with_align(Alignment2D::BOTTOM_RIGHT) + .with_fg(fg_color) + .render(target) + } + + ButtonPos::Middle => shape::ToifImage::new(icon_area.center(), icon.toif) + .with_align(Alignment2D::CENTER) + .with_fg(fg_color) + .render(target), + } + } + } + } + } } #[derive(PartialEq, Eq)] diff --git a/core/embed/rust/src/ui/model_tr/component/button_controller.rs b/core/embed/rust/src/ui/model_tr/component/button_controller.rs index 76d053faa..280938879 100644 --- a/core/embed/rust/src/ui/model_tr/component/button_controller.rs +++ b/core/embed/rust/src/ui/model_tr/component/button_controller.rs @@ -7,6 +7,7 @@ use crate::{ component::{base::Event, Component, EventCtx, Pad, TimerToken}, event::{ButtonEvent, PhysicalButton}, geometry::Rect, + shape::Renderer, }, }; @@ -93,6 +94,18 @@ impl ButtonType { Self::Nothing => {} } } + + pub fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + match self { + Self::Button(button) => { + button.render(target); + } + Self::HoldToConfirm(htc) => { + htc.render(target); + } + Self::Nothing => {} + } + } } /// Wrapping a button and its state, so that it can be easily @@ -154,6 +167,10 @@ impl ButtonContainer { self.button_type.paint(); } + pub fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.button_type.render(target); + } + /// Setting the visual state of the button - released/pressed. pub fn set_pressed(&mut self, ctx: &mut EventCtx, is_pressed: bool) { if let ButtonType::Button(btn) = &mut self.button_type { @@ -575,6 +592,13 @@ impl Component for ButtonController { self.right_btn.paint(); } + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.pad.render(target); + self.left_btn.render(target); + self.middle_btn.render(target); + self.right_btn.render(target); + } + fn place(&mut self, bounds: Rect) -> Rect { // Saving button area so that we can re-place the buttons // when they get updated @@ -754,6 +778,8 @@ impl Component for AutomaticMover { fn paint(&mut self) {} + fn render<'s>(&'s self, _target: &mut impl Renderer<'s>) {} + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { // Moving automatically only when we receive a TimerToken that we have // requested before diff --git a/core/embed/rust/src/ui/model_tr/component/changing_text.rs b/core/embed/rust/src/ui/model_tr/component/changing_text.rs index 35ec84457..de5cbe6d7 100644 --- a/core/embed/rust/src/ui/model_tr/component/changing_text.rs +++ b/core/embed/rust/src/ui/model_tr/component/changing_text.rs @@ -2,6 +2,8 @@ use crate::ui::{ component::{Component, Event, EventCtx, Never, Pad}, display::Font, geometry::{Alignment, Point, Rect}, + shape, + shape::Renderer, util::long_line_content_with_ellipsis, }; @@ -109,16 +111,39 @@ where common::display_left(baseline, &self.text, self.font); } + fn render_left<'s>(&'s self, target: &mut impl Renderer<'s>) { + let baseline = Point::new(self.pad.area.x0, self.y_baseline()); + shape::Text::new(baseline, &self.text.as_ref()) + .with_font(self.font) + .render(target); + } + fn paint_center(&self) { let baseline = Point::new(self.pad.area.bottom_center().x, self.y_baseline()); common::display_center(baseline, &self.text, self.font); } + fn render_center<'s>(&'s self, target: &mut impl Renderer<'s>) { + let baseline = Point::new(self.pad.area.bottom_center().x, self.y_baseline()); + shape::Text::new(baseline, &self.text.as_ref()) + .with_align(Alignment::Center) + .with_font(self.font) + .render(target); + } + fn paint_right(&self) { let baseline = Point::new(self.pad.area.x1, self.y_baseline()); common::display_right(baseline, &self.text, self.font); } + fn render_right<'s>(&'s self, target: &mut impl Renderer<'s>) { + let baseline = Point::new(self.pad.area.x1, self.y_baseline()); + shape::Text::new(baseline, &self.text.as_ref()) + .with_align(Alignment::End) + .with_font(self.font) + .render(target); + } + fn paint_long_content_with_ellipsis(&self) { let text_to_display = long_line_content_with_ellipsis( self.text.as_ref(), @@ -175,4 +200,20 @@ where } } } + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.pad.render(target); + if self.show_content { + // In the case text cannot fit, show ellipsis and its right part + if !self.text_fits_completely() { + self.paint_long_content_with_ellipsis(); + } else { + match self.alignment { + Alignment::Start => self.render_left(target), + Alignment::Center => self.render_center(target), + Alignment::End => self.render_right(target), + } + } + } + } } diff --git a/core/embed/rust/src/ui/model_tr/component/coinjoin_progress.rs b/core/embed/rust/src/ui/model_tr/component/coinjoin_progress.rs index 6afda9991..6d570518f 100644 --- a/core/embed/rust/src/ui/model_tr/component/coinjoin_progress.rs +++ b/core/embed/rust/src/ui/model_tr/component/coinjoin_progress.rs @@ -6,11 +6,16 @@ use crate::{ ui::{ component::{ base::Never, - text::util::{text_multiline, text_multiline_bottom}, + text::util::{ + text_multiline, text_multiline2, text_multiline_bottom, text_multiline_bottom2, + }, Component, Event, EventCtx, }, display::{self, Font}, - geometry::{Alignment, Insets, Rect}, + geometry::{Alignment, Insets, Offset, Rect}, + model_tr::cshape, + shape, + shape::Renderer, util::animation_disabled, }, }; @@ -130,6 +135,55 @@ where ); } } + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + // TOP + let center = self.area.center() + Offset::y(self.loader_y_offset); + + if self.indeterminate { + text_multiline2( + target, + self.area, + TR::coinjoin__title_progress.into(), + Font::BOLD, + theme::FG, + theme::BG, + Alignment::Center, + ); + cshape::LoaderSmall::new(center, self.value) + .with_color(theme::FG) + .render(target); + } else { + cshape::LoaderCircular::new(center, self.value) + .with_color(theme::FG) + .render(target); + shape::ToifImage::new(center, theme::ICON_TICK_FAT.toif) + .with_fg(theme::FG) + .render(target); + } + + // BOTTOM + let top_rest = text_multiline_bottom2( + target, + self.area, + TR::coinjoin__do_not_disconnect.into(), + Font::BOLD, + theme::FG, + theme::BG, + Alignment::Center, + ); + if let Some(rest) = top_rest { + text_multiline_bottom2( + target, + rest.inset(Insets::bottom(FOOTER_TEXT_MARGIN)), + self.text.as_ref().into(), + Font::NORMAL, + theme::FG, + theme::BG, + Alignment::Center, + ); + } + } } #[cfg(feature = "ui_debug")] diff --git a/core/embed/rust/src/ui/model_tr/component/error.rs b/core/embed/rust/src/ui/model_tr/component/error.rs index 29624f40b..f05f6ea7f 100644 --- a/core/embed/rust/src/ui/model_tr/component/error.rs +++ b/core/embed/rust/src/ui/model_tr/component/error.rs @@ -3,6 +3,9 @@ use crate::ui::{ constant::{screen, WIDTH}, display, geometry::{Alignment2D, Offset, Point, Rect}, + model_tr::cshape, + shape, + shape::Renderer, }; use super::super::{ @@ -95,4 +98,29 @@ impl> Component for ErrorScreen { self.footer.paint(); } + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.bg.render(target); + + if self.show_icons { + shape::ToifImage::new(screen().top_left(), theme::ICON_WARN_TITLE.toif) + .with_align(Alignment2D::TOP_LEFT) + .with_fg(FG) + .render(target); + + shape::ToifImage::new(screen().top_right(), theme::ICON_WARN_TITLE.toif) + .with_align(Alignment2D::TOP_RIGHT) + .with_fg(FG) + .render(target); + } + self.title.render(target); + self.message.render(target); + + cshape::HorizontalLine::new(Point::new(0, DIVIDER_POSITION), WIDTH) + .with_step(3) + .with_color(FG) + .render(target); + + self.footer.render(target); + } } diff --git a/core/embed/rust/src/ui/model_tr/component/flow.rs b/core/embed/rust/src/ui/model_tr/component/flow.rs index b161566d7..6db519a53 100644 --- a/core/embed/rust/src/ui/model_tr/component/flow.rs +++ b/core/embed/rust/src/ui/model_tr/component/flow.rs @@ -3,6 +3,7 @@ use crate::{ ui::{ component::{Child, Component, ComponentExt, Event, EventCtx, Pad, Paginate}, geometry::Rect, + shape::Renderer, }, }; @@ -308,6 +309,23 @@ where // (and painting buttons last would cover the lower part). self.current_page.paint(); } + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.pad.render(target); + // Scrollbars are painted only with a title and when requested + if self.title.is_some() { + if self.show_scrollbar { + self.scrollbar.render(target); + } + self.title.render(target); + } + self.buttons.render(target); + // On purpose painting current page at the end, after buttons, + // because we sometimes (in the case of QR code) need to use the + // whole height of the display for showing the content + // (and painting buttons last would cover the lower part). + self.current_page.render(target); + } } // DEBUG-ONLY SECTION BELOW diff --git a/core/embed/rust/src/ui/model_tr/component/flow_pages.rs b/core/embed/rust/src/ui/model_tr/component/flow_pages.rs index 25eeb91e8..ae712a9d0 100644 --- a/core/embed/rust/src/ui/model_tr/component/flow_pages.rs +++ b/core/embed/rust/src/ui/model_tr/component/flow_pages.rs @@ -3,6 +3,7 @@ use crate::{ ui::{ component::{base::Component, FormattedText, Paginate}, geometry::Rect, + shape::Renderer, }, }; @@ -132,6 +133,10 @@ where self.formatted.paint(); } + pub fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.formatted.render(target); + } + pub fn place(&mut self, bounds: Rect) -> Rect { self.formatted.place(bounds); self.page_count = self.page_count(); diff --git a/core/embed/rust/src/ui/model_tr/component/frame.rs b/core/embed/rust/src/ui/model_tr/component/frame.rs index 0cdce56ec..25b735c36 100644 --- a/core/embed/rust/src/ui/model_tr/component/frame.rs +++ b/core/embed/rust/src/ui/model_tr/component/frame.rs @@ -3,6 +3,7 @@ use crate::{ ui::{ component::{Child, Component, ComponentExt, Event, EventCtx, Paginate}, geometry::{Insets, Rect}, + shape::Renderer, }, }; @@ -83,6 +84,11 @@ where self.title.paint(); self.content.paint(); } + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.title.render(target); + self.content.render(target); + } } impl Paginate for Frame @@ -204,6 +210,12 @@ where self.scrollbar.paint(); self.content.paint(); } + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.title.render(target); + self.scrollbar.render(target); + self.content.render(target); + } } // DEBUG-ONLY SECTION BELOW diff --git a/core/embed/rust/src/ui/model_tr/component/hold_to_confirm.rs b/core/embed/rust/src/ui/model_tr/component/hold_to_confirm.rs index 06f652b76..d0afb1e2c 100644 --- a/core/embed/rust/src/ui/model_tr/component/hold_to_confirm.rs +++ b/core/embed/rust/src/ui/model_tr/component/hold_to_confirm.rs @@ -5,6 +5,7 @@ use crate::{ component::{Component, Event, EventCtx}, event::ButtonEvent, geometry::Rect, + shape::Renderer, }, }; @@ -122,6 +123,10 @@ impl Component for HoldToConfirm { fn paint(&mut self) { self.loader.paint(); } + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.loader.render(target); + } } // DEBUG-ONLY SECTION BELOW diff --git a/core/embed/rust/src/ui/model_tr/component/homescreen.rs b/core/embed/rust/src/ui/model_tr/component/homescreen.rs index 657cf5bc4..16eefc4aa 100644 --- a/core/embed/rust/src/ui/model_tr/component/homescreen.rs +++ b/core/embed/rust/src/ui/model_tr/component/homescreen.rs @@ -1,6 +1,10 @@ use crate::{ error::Error, - micropython::buffer::StrBuffer, + micropython::{ + buffer::{get_buffer, StrBuffer}, + gc::Gc, + obj::Obj, + }, strutil::StringType, translations::TR, trezorhal::usb::usb_configured, @@ -13,8 +17,10 @@ use crate::{ Font, Icon, }, event::USBEvent, - geometry::{Alignment2D, Insets, Offset, Point, Rect}, + geometry::{Alignment, Alignment2D, Insets, Offset, Point, Rect}, layout::util::get_user_custom_image, + shape, + shape::Renderer, }, }; @@ -48,6 +54,16 @@ fn paint_default_image() { ); } +fn render_default_image<'s>(target: &mut impl Renderer<'s>) { + shape::ToifImage::new( + TOP_CENTER + Offset::y(LOGO_ICON_TOP_MARGIN), + theme::ICON_LOGO.toif, + ) + .with_align(Alignment2D::TOP_CENTER) + .with_fg(theme::FG) + .render(target); +} + enum CurrentScreen { EmptyAtStart, Homescreen, @@ -62,6 +78,7 @@ where // always painted, so we need to always paint the label too label: Label, notification: Option<(T, u8)>, + custom_image: Option>, /// Used for HTC functionality to lock device from homescreen invisible_buttons: Child, /// Holds the loader component @@ -85,6 +102,7 @@ where Self { label: Label::centered(label, theme::TEXT_BIG), notification, + custom_image: get_user_custom_image().ok(), invisible_buttons: Child::new(ButtonController::new(invisible_btn_layout)), loader, show_loader: false, @@ -93,8 +111,8 @@ where } fn paint_homescreen_image(&self) { - let homescreen_bytes = get_user_custom_image().ok(); - let homescreen = homescreen_bytes + let homescreen = self + .custom_image .as_ref() .and_then(|data| Toif::new(data.as_ref()).ok()) .filter(check_homescreen_format); @@ -105,6 +123,22 @@ where } } + fn render_homescreen_image<'s>(&'s self, target: &mut impl Renderer<'s>) { + let homescreen = self + .custom_image + .as_ref() + .and_then(|data| Toif::new(data.as_ref()).ok()) + .filter(check_homescreen_format); + if let Some(toif) = homescreen { + shape::ToifImage::new(TOP_CENTER, toif) + .with_align(Alignment2D::TOP_CENTER) + .with_fg(theme::FG) + .render(target); + } else { + render_default_image(target); + } + } + fn paint_notification(&self) { let baseline = TOP_CENTER + Offset::y(NOTIFICATION_FONT.line_height()); if !usb_configured() { @@ -136,6 +170,49 @@ where } } + fn render_notification<'s>(&'s self, target: &mut impl Renderer<'s>) { + let baseline = TOP_CENTER + Offset::y(NOTIFICATION_FONT.line_height()); + if !usb_configured() { + shape::Bar::new(AREA.split_top(NOTIFICATION_HEIGHT).0) + .with_bg(theme::BG) + .render(target); + + // TODO: fill warning icons here as well? + TR::homescreen__title_no_usb_connection.map_translated(|t| { + shape::Text::new(baseline, t) + .with_align(Alignment::Center) + .with_font(NOTIFICATION_FONT) + .render(target) + }); + } else if let Some((notification, _level)) = &self.notification { + shape::Bar::new(AREA.split_top(NOTIFICATION_HEIGHT).0) + .with_bg(theme::BG) + .render(target); + + shape::Text::new(baseline, notification.as_ref()) + .with_align(Alignment::Center) + .with_font(NOTIFICATION_FONT) + .render(target); + + // Painting warning icons in top corners when the text is short enough not to + // collide with them + let icon_width = NOTIFICATION_ICON.toif.width(); + let text_width = NOTIFICATION_FONT.text_width(notification.as_ref()); + if AREA.width() >= text_width + (icon_width + 1) * 2 { + shape::ToifImage::new(AREA.top_left(), NOTIFICATION_ICON.toif) + .with_align(Alignment2D::TOP_LEFT) + .with_fg(theme::FG) + .with_bg(theme::BG) + .render(target); + shape::ToifImage::new(AREA.top_right(), NOTIFICATION_ICON.toif) + .with_align(Alignment2D::TOP_RIGHT) + .with_fg(theme::FG) + .with_bg(theme::BG) + .render(target); + } + } + } + fn paint_label(&mut self) { // paint black background to place the label let mut outset = Insets::uniform(LABEL_OUTSET); @@ -146,6 +223,19 @@ where self.label.paint(); } + fn render_label<'s>(&'s self, target: &mut impl Renderer<'s>) { + // paint black background to place the label + let mut outset = Insets::uniform(LABEL_OUTSET); + // the margin at top is bigger (caused by text-height vs line-height?) + // compensate by shrinking the outset + outset.top -= 5; + shape::Bar::new(self.label.text_area().outset(outset)) + .with_bg(theme::BG) + .render(target); + + self.label.render(target); + } + /// So that notification is well visible even on homescreen image fn fill_notification_background(&self) { rect_fill(AREA.split_top(NOTIFICATION_HEIGHT).0, theme::BG); @@ -236,6 +326,19 @@ where self.paint_label(); } } + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + // Redraw the whole screen when the screen changes (loader vs homescreen) + if self.show_loader { + self.loader.render(target); + } else { + // Painting the homescreen image first, as the notification and label + // should be "on top of it" + self.render_homescreen_image(target); + self.render_notification(target); + self.render_label(target); + } + } } pub struct Lockscreen @@ -320,35 +423,58 @@ where ) } } + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + if self.screensaver { + // keep screen blank + return; + } + shape::ToifImage::new( + TOP_CENTER + Offset::y(LOCK_ICON_TOP_MARGIN), + theme::ICON_LOCK.toif, + ) + .with_align(Alignment2D::TOP_CENTER) + .with_fg(theme::FG) + .render(target); + + self.instruction.render(target); + self.label.render(target); + + if let Some(icon) = &self.coinjoin_icon { + shape::ToifImage::new(COINJOIN_CORNER, icon.toif) + .with_align(Alignment2D::TOP_RIGHT) + .with_fg(theme::FG) + .render(target); + } + } } -pub struct ConfirmHomescreen +pub struct ConfirmHomescreen where T: StringType, { title: Child>, - buffer_func: F, + image: Obj, buttons: Child, } -impl ConfirmHomescreen +impl ConfirmHomescreen where T: StringType + Clone, { - pub fn new(title: T, buffer_func: F) -> Self { + pub fn new(title: T, image: Obj) -> Self { let btn_layout = ButtonLayout::cancel_none_text(TR::buttons__change.into()); ConfirmHomescreen { title: Child::new(Label::centered(title, theme::TEXT_BOLD)), - buffer_func, + image, buttons: Child::new(ButtonController::new(btn_layout)), } } } -impl<'a, T, F> Component for ConfirmHomescreen +impl<'a, T> Component for ConfirmHomescreen where T: StringType + Clone, - F: Fn() -> &'a [u8], { type Msg = CancelConfirmMsg; @@ -375,11 +501,13 @@ where fn paint(&mut self) { // Drawing the image full-screen first and then other things on top - let buffer = (self.buffer_func)(); - if buffer.is_empty() { + // SAFETY: We expect no existing mutable reference. Resulting reference is + // discarded before returning to micropython. + let image_data = unwrap!(unsafe { get_buffer(self.image) }); + if image_data.is_empty() { paint_default_image(); } else { - let toif_data = unwrap!(Toif::new(buffer)); + let toif_data = unwrap!(Toif::new(image_data)); toif_data.draw(TOP_CENTER, Alignment2D::TOP_CENTER, theme::FG, theme::BG); }; // Need to make all the title background black, so the title text is well @@ -389,6 +517,32 @@ where self.title.paint(); self.buttons.paint(); } + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + // Drawing the image full-screen first and then other things on top + // SAFETY: We expect no existing mutable reference. Resulting reference is + // discarded before returning to micropython. + let image_data = unwrap!(unsafe { get_buffer(self.image) }); + if image_data.is_empty() { + render_default_image(target); + } else { + let toif_data = unwrap!(Toif::new(image_data)); + shape::ToifImage::new(TOP_CENTER, toif_data) + .with_align(Alignment2D::TOP_CENTER) + .with_fg(theme::FG) + .render(target); + }; + // Need to make all the title background black, so the title text is well + // visible + let title_area = self.title.inner().area(); + + shape::Bar::new(title_area) + .with_bg(theme::BG) + .render(target); + + self.title.render(target); + self.buttons.render(target); + } } pub fn check_homescreen_format(toif: &Toif) -> bool { @@ -420,7 +574,7 @@ where } #[cfg(feature = "ui_debug")] -impl crate::trace::Trace for ConfirmHomescreen +impl crate::trace::Trace for ConfirmHomescreen where T: StringType, { diff --git a/core/embed/rust/src/ui/model_tr/component/input_methods/choice.rs b/core/embed/rust/src/ui/model_tr/component/input_methods/choice.rs index 5ddf7fabc..dd33f378b 100644 --- a/core/embed/rust/src/ui/model_tr/component/input_methods/choice.rs +++ b/core/embed/rust/src/ui/model_tr/component/input_methods/choice.rs @@ -1,6 +1,7 @@ use crate::ui::{ component::{Child, Component, Event, EventCtx, Pad}, geometry::{Insets, Offset, Rect}, + shape::Renderer, util::animation_disabled, }; @@ -14,11 +15,17 @@ pub trait Choice { // Only `paint_center` is required, the rest is optional // and therefore has a default implementation. fn paint_center(&self, area: Rect, inverse: bool); + + fn render_center<'s>(&self, target: &mut impl Renderer<'s>, _area: Rect, _inverse: bool); + fn width_center(&self) -> i16 { 0 } fn paint_side(&self, _area: Rect) {} + + fn render_side<'s>(&self, _target: &mut impl Renderer<'s>, _area: Rect) {} + fn width_side(&self) -> i16 { 0 } @@ -248,6 +255,43 @@ where } } + /// Display current, previous and next choices according to + /// the current ChoiceItem. + fn render_choices<'s>(&'s self, target: &mut impl Renderer<'s>) { + // Getting the row area for the choices - so that displaying + // items in the used font will show them in the middle vertically. + let area_height_half = self.pad.area.height() / 2; + let font_size_half = theme::FONT_CHOICE_ITEMS.visible_text_height("Ay") / 2; + let center_row_area = self + .pad + .area + .split_top(area_height_half) + .0 + .outset(Insets::bottom(font_size_half)); + + // Drawing the current item in the middle. + self.show_current_choice2(target, center_row_area); + + // Not drawing the rest when not wanted + if self.show_only_one_item { + return; + } + + // Getting the remaining left and right areas. + let center_width = self.get_current_item().width_center(); + let (left_area, _center_area, right_area) = center_row_area.split_center(center_width); + + // Possibly drawing on the left side. + if self.has_previous_choice() || self.is_carousel { + self.show_left_choices2(target, left_area); + } + + // Possibly drawing on the right side. + if self.has_next_choice() || self.is_carousel { + self.show_right_choices2(target, right_area); + } + } + /// Setting current buttons, and clearing. fn update(&mut self, ctx: &mut EventCtx) { self.set_buttons(ctx); @@ -296,6 +340,12 @@ where .paint_center(area, self.inverse_selected_item); } + /// Display the current choice in the middle. + fn show_current_choice2<'s>(&'s self, target: &mut impl Renderer<'s>, area: Rect) { + self.get_current_item() + .render_center(target, area, self.inverse_selected_item); + } + /// Display all the choices fitting on the left side. /// Going as far as possible. fn show_left_choices(&self, area: Rect) { @@ -338,6 +388,48 @@ where } } + /// Display all the choices fitting on the left side. + /// Going as far as possible. + fn show_left_choices2<'s>(&'s self, target: &mut impl Renderer<'s>, area: Rect) { + // NOTE: page index can get negative here, so having it as i16 instead of usize + let mut page_index = self.page_counter as i16 - 1; + let mut current_area = area.split_right(self.items_distance).0; + while current_area.width() > 0 { + // Breaking out of the loop if we exhausted left items + // and the carousel mode is not enabled. + if page_index < 0 { + if self.is_carousel { + // Moving to the last page. + page_index = self.last_page_index() as i16; + } else { + break; + } + } + + let (choice, _) = self.choices.get(page_index as usize); + let choice_width = choice.width_side(); + + if current_area.width() <= choice_width && !self.show_incomplete { + // early break for an item that will not fit the remaining space + break; + } + + // We need to calculate the area explicitly because we want to allow it + // to exceed the bounds of the original area. + let choice_area = Rect::from_top_right_and_size( + current_area.top_right(), + Offset::new(choice_width, current_area.height()), + ); + choice.render_side(target, choice_area); + + // Updating loop variables. + current_area = current_area + .split_right(choice_width + self.items_distance) + .0; + page_index -= 1; + } + } + /// Display all the choices fitting on the right side. /// Going as far as possible. fn show_right_choices(&self, area: Rect) { @@ -379,6 +471,47 @@ where } } + /// Display all the choices fitting on the right side. + /// Going as far as possible. + fn show_right_choices2<'s>(&'s self, target: &mut impl Renderer<'s>, area: Rect) { + let mut page_index = self.page_counter + 1; + let mut current_area = area.split_left(self.items_distance).1; + while current_area.width() > 0 { + // Breaking out of the loop if we exhausted right items + // and the carousel mode is not enabled. + if page_index > self.last_page_index() { + if self.is_carousel { + // Moving to the first page. + page_index = 0; + } else { + break; + } + } + + let (choice, _) = self.choices.get(page_index); + let choice_width = choice.width_side(); + + if current_area.width() <= choice_width && !self.show_incomplete { + // early break for an item that will not fit the remaining space + break; + } + + // We need to calculate the area explicitly because we want to allow it + // to exceed the bounds of the original area. + let choice_area = Rect::from_top_left_and_size( + current_area.top_left(), + Offset::new(choice_width, current_area.height()), + ); + choice.render_side(target, choice_area); + + // Updating loop variables. + current_area = current_area + .split_left(choice_width + self.items_distance) + .1; + page_index += 1; + } + } + /// Decrease the page counter to the previous page. fn decrease_page_counter(&mut self) { self.page_counter -= 1; @@ -586,6 +719,12 @@ where self.buttons.paint(); self.paint_choices(); } + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.pad.render(target); + self.buttons.render(target); + self.render_choices(target); + } } // DEBUG-ONLY SECTION BELOW diff --git a/core/embed/rust/src/ui/model_tr/component/input_methods/choice_item.rs b/core/embed/rust/src/ui/model_tr/component/input_methods/choice_item.rs index 0661e3be3..ed948d699 100644 --- a/core/embed/rust/src/ui/model_tr/component/input_methods/choice_item.rs +++ b/core/embed/rust/src/ui/model_tr/component/input_methods/choice_item.rs @@ -3,6 +3,8 @@ use crate::{ ui::{ display::{self, rect_fill, rect_fill_corners, rect_outline_rounded, Font, Icon}, geometry::{Alignment2D, Offset, Rect}, + shape, + shape::Renderer, }, }; @@ -107,6 +109,27 @@ impl Choice for ChoiceItem { ); } + /// Painting the item as the main choice in the middle. + /// Showing both the icon and text, if the icon is available. + fn render_center<'s>(&self, target: &mut impl Renderer<'s>, area: Rect, inverse: bool) { + let width = text_icon_width(Some(self.text.as_ref()), self.icon, self.font); + render_rounded_highlight( + target, + area, + Offset::new(width, self.font.visible_text_height("Ay")), + inverse, + ); + render_text_icon( + target, + area, + width, + Some(self.text.as_ref()), + self.icon, + self.font, + inverse, + ); + } + /// Getting the overall width in pixels when displayed in center. /// That means both the icon and text will be shown. fn width_center(&self) -> i16 { @@ -125,6 +148,20 @@ impl Choice for ChoiceItem { paint_text_icon(area, width, self.side_text(), self.icon, self.font, false); } + /// Painting smaller version of the item on the side. + fn render_side<'s>(&self, target: &mut impl Renderer<'s>, area: Rect) { + let width = text_icon_width(self.side_text(), self.icon, self.font); + render_text_icon( + target, + area, + width, + self.side_text(), + self.icon, + self.font, + false, + ); + } + /// Getting current button layout. fn btn_layout(&self) -> ButtonLayout { self.btn_layout.clone() @@ -151,6 +188,37 @@ fn paint_rounded_highlight(area: Rect, size: Offset, inverse: bool) { } } +fn render_rounded_highlight<'s>( + target: &mut impl Renderer<'s>, + area: Rect, + size: Offset, + inverse: bool, +) { + let bound = theme::BUTTON_OUTLINE; + let left_bottom = area.bottom_center() + Offset::new(-size.x / 2 - bound, bound + 1); + let x_size = size.x + 2 * bound; + let y_size = size.y + 2 * bound; + let outline_size = Offset::new(x_size, y_size); + let outline = Rect::from_bottom_left_and_size(left_bottom, outline_size); + if inverse { + shape::Bar::new(outline) + .with_radius(1) + .with_bg(theme::FG) + .render(target); + } else { + // Draw outline by drawing two rounded rectangles + shape::Bar::new(outline) + .with_radius(1) + .with_bg(theme::FG) + .render(target); + + shape::Bar::new(outline.shrink(1)) + .with_radius(1) + .with_bg(theme::BG) + .render(target); + } +} + fn text_icon_width(text: Option<&str>, icon: Option, font: Font) -> i16 { match (text, icon) { (Some(text), Some(icon)) => { @@ -194,6 +262,40 @@ fn paint_text_icon( } } +fn render_text_icon<'s>( + target: &mut impl Renderer<'s>, + area: Rect, + width: i16, + text: Option<&str>, + icon: Option, + font: Font, + inverse: bool, +) { + let fg_color = if inverse { theme::BG } else { theme::FG }; + + let mut baseline = area.bottom_center() - Offset::x(width / 2); + if let Some(icon) = icon { + let height_diff = font.visible_text_height("Ay") - icon.toif.height(); + let vertical_offset = Offset::y(-height_diff / 2); + shape::ToifImage::new(baseline + vertical_offset, icon.toif) + .with_align(Alignment2D::BOTTOM_LEFT) + .with_fg(fg_color) + .render(target); + + baseline = baseline + Offset::x(icon.toif.width() + ICON_RIGHT_PADDING); + } + + if let Some(text) = text { + // Possibly shifting the baseline left, when there is a text bearing. + // This is to center the text properly. + baseline = baseline - Offset::x(font.start_x_bearing(text)); + shape::Text::new(baseline, text) + .with_font(font) + .with_fg(fg_color) + .render(target); + } +} + // DEBUG-ONLY SECTION BELOW #[cfg(feature = "ui_debug")] diff --git a/core/embed/rust/src/ui/model_tr/component/input_methods/number_input.rs b/core/embed/rust/src/ui/model_tr/component/input_methods/number_input.rs index 151011f5b..d67124917 100644 --- a/core/embed/rust/src/ui/model_tr/component/input_methods/number_input.rs +++ b/core/embed/rust/src/ui/model_tr/component/input_methods/number_input.rs @@ -3,6 +3,7 @@ use crate::{ ui::{ component::{Component, Event, EventCtx}, geometry::Rect, + shape::Renderer, }, }; @@ -81,6 +82,10 @@ impl Component for NumberInput { fn paint(&mut self) { self.choice_page.paint(); } + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.choice_page.render(target); + } } // DEBUG-ONLY SECTION BELOW diff --git a/core/embed/rust/src/ui/model_tr/component/input_methods/passphrase.rs b/core/embed/rust/src/ui/model_tr/component/input_methods/passphrase.rs index 085ed8dfb..3bb471463 100644 --- a/core/embed/rust/src/ui/model_tr/component/input_methods/passphrase.rs +++ b/core/embed/rust/src/ui/model_tr/component/input_methods/passphrase.rs @@ -6,6 +6,7 @@ use crate::{ component::{text::common::TextBox, Child, Component, ComponentExt, Event, EventCtx}, display::Icon, geometry::Rect, + shape::Renderer, util::char_to_string, }, }; @@ -448,6 +449,11 @@ impl Component for PassphraseEntry { self.passphrase_dots.paint(); self.choice_page.paint(); } + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.passphrase_dots.render(target); + self.choice_page.render(target); + } } // DEBUG-ONLY SECTION BELOW diff --git a/core/embed/rust/src/ui/model_tr/component/input_methods/pin.rs b/core/embed/rust/src/ui/model_tr/component/input_methods/pin.rs index 7d434e7bc..764e02b05 100644 --- a/core/embed/rust/src/ui/model_tr/component/input_methods/pin.rs +++ b/core/embed/rust/src/ui/model_tr/component/input_methods/pin.rs @@ -6,6 +6,7 @@ use crate::{ component::{text::common::TextBox, Child, Component, ComponentExt, Event, EventCtx}, display::{Font, Icon}, geometry::Rect, + shape::Renderer, }, }; @@ -329,6 +330,12 @@ where self.pin_line.paint(); self.choice_page.paint(); } + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.header_line.render(target); + self.pin_line.render(target); + self.choice_page.render(target); + } } // DEBUG-ONLY SECTION BELOW diff --git a/core/embed/rust/src/ui/model_tr/component/input_methods/simple_choice.rs b/core/embed/rust/src/ui/model_tr/component/input_methods/simple_choice.rs index 23bc9bd1c..f322c8cf1 100644 --- a/core/embed/rust/src/ui/model_tr/component/input_methods/simple_choice.rs +++ b/core/embed/rust/src/ui/model_tr/component/input_methods/simple_choice.rs @@ -4,6 +4,7 @@ use crate::{ ui::{ component::{Component, Event, EventCtx}, geometry::Rect, + shape::Renderer, }, }; @@ -115,6 +116,10 @@ impl Component for SimpleChoice { fn paint(&mut self) { self.choice_page.paint(); } + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.choice_page.render(target); + } } // DEBUG-ONLY SECTION BELOW diff --git a/core/embed/rust/src/ui/model_tr/component/input_methods/wordlist.rs b/core/embed/rust/src/ui/model_tr/component/input_methods/wordlist.rs index f04f88893..d29f757b8 100644 --- a/core/embed/rust/src/ui/model_tr/component/input_methods/wordlist.rs +++ b/core/embed/rust/src/ui/model_tr/component/input_methods/wordlist.rs @@ -4,6 +4,7 @@ use crate::{ ui::{ component::{text::common::TextBox, Child, Component, ComponentExt, Event, EventCtx}, geometry::Rect, + shape::Renderer, util::char_to_string, }, }; @@ -310,6 +311,11 @@ impl Component for WordlistEntry { self.chosen_letters.paint(); self.choice_page.paint(); } + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.chosen_letters.render(target); + self.choice_page.render(target); + } } // DEBUG-ONLY SECTION BELOW diff --git a/core/embed/rust/src/ui/model_tr/component/loader.rs b/core/embed/rust/src/ui/model_tr/component/loader.rs index 5ccb06595..f0c3a28e4 100644 --- a/core/embed/rust/src/ui/model_tr/component/loader.rs +++ b/core/embed/rust/src/ui/model_tr/component/loader.rs @@ -6,7 +6,9 @@ use crate::{ component::{Child, Component, Event, EventCtx}, constant, display::{self, Color, Font, LOADER_MAX}, - geometry::{Offset, Rect}, + geometry::{Offset, Point, Rect}, + shape, + shape::Renderer, util::animation_disabled, }, }; @@ -164,6 +166,51 @@ impl Loader { invert_from as i16, ); } + + pub fn render_loader<'s>( + &'s self, + target: &mut impl Renderer<'s>, + style: &LoaderStyle, + done: i32, + ) { + let width = self.area.width(); + // NOTE: need to calculate this in `i32`, it would overflow using `i16` + let split_point = (((width as i32 + 1) * done) / (display::LOADER_MAX as i32)) as i16; + let (r_left, r_right) = self.area.split_left(split_point); + let parts = [(r_left, true), (r_right, false)]; + parts.map(|(r, invert)| { + target.in_clip(r, &|target| { + if invert { + shape::Bar::new(self.area) + .with_radius(3) + .with_bg(style.fg_color) + .render(target); + } else { + shape::Bar::new(self.area) + .with_radius(3) + .with_fg(style.fg_color) + .render(target); + } + + let text_color = if invert { + style.bg_color + } else { + style.fg_color + }; + + self.get_text().map(|t| { + let pt = Point::new( + style.font.horz_center(self.area.x0, self.area.x1, t), + style.font.vert_center(self.area.y0, self.area.y1, "A"), + ); + shape::Text::new(pt, t) + .with_font(style.font) + .with_fg(text_color) + .render(target); + }); + }); + }); + } } impl Component for Loader { @@ -223,6 +270,28 @@ impl Component for Loader { } } } + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + // TODO: Consider passing the current instant along with the event -- that way, + // we could synchronize painting across the component tree. Also could be useful + // in automated tests. + // In practice, taking the current instant here is more precise in case some + // other component in the tree takes a long time to draw. + let now = Instant::now(); + + if let State::Initial = self.state { + self.render_loader(target, self.styles.normal, 0); + } else if let State::Grown = self.state { + self.render_loader(target, self.styles.normal, display::LOADER_MAX as i32); + } else { + let progress = self.progress(now); + if let Some(done) = progress { + self.render_loader(target, self.styles.normal, done as i32); + } else { + self.render_loader(target, self.styles.normal, 0); + } + } + } } pub struct LoaderStyleSheet { @@ -332,6 +401,10 @@ where fn paint(&mut self) { self.loader.paint(); } + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.loader.render(target); + } } // DEBUG-ONLY SECTION BELOW diff --git a/core/embed/rust/src/ui/model_tr/component/page.rs b/core/embed/rust/src/ui/model_tr/component/page.rs index 31e6505e6..935f184c8 100644 --- a/core/embed/rust/src/ui/model_tr/component/page.rs +++ b/core/embed/rust/src/ui/model_tr/component/page.rs @@ -4,6 +4,7 @@ use crate::{ component::{Child, Component, ComponentExt, Event, EventCtx, Pad, PageMsg, Paginate}, display::Color, geometry::{Insets, Rect}, + shape::Renderer, }, }; @@ -215,6 +216,12 @@ where self.content.paint(); self.buttons.paint(); } + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.pad.render(target); + self.content.render(target); + self.buttons.render(target); + } } // DEBUG-ONLY SECTION BELOW diff --git a/core/embed/rust/src/ui/model_tr/component/progress.rs b/core/embed/rust/src/ui/model_tr/component/progress.rs index 2346eb7e2..e41356e73 100644 --- a/core/embed/rust/src/ui/model_tr/component/progress.rs +++ b/core/embed/rust/src/ui/model_tr/component/progress.rs @@ -11,7 +11,10 @@ use crate::{ }, constant, display::{self, Font, Icon, LOADER_MAX}, - geometry::Rect, + geometry::{Alignment2D, Offset, Rect}, + model_tr::cshape, + shape, + shape::Renderer, util::animation_disabled, }, }; @@ -182,6 +185,29 @@ where self.description.paint(); } + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.title.render(target); + + let area = constant::screen(); + let center = area.center() + Offset::y(self.loader_y_offset); + + if self.indeterminate { + cshape::LoaderStarry::new(center, self.value) + .with_color(theme::FG) + .render(target); + } else { + cshape::LoaderCircular::new(center, self.value) + .with_color(theme::FG) + .render(target); + shape::ToifImage::new(center, self.icon.toif) + .with_align(Alignment2D::CENTER) + .with_fg(theme::FG) + .render(target); + } + self.description_pad.render(target); + self.description.render(target); + } + #[cfg(feature = "ui_bounds")] fn bounds(&self, sink: &mut dyn FnMut(Rect)) { sink(Self::AREA); diff --git a/core/embed/rust/src/ui/model_tr/component/result.rs b/core/embed/rust/src/ui/model_tr/component/result.rs index 0fea93cf4..cc1bf8f03 100644 --- a/core/embed/rust/src/ui/model_tr/component/result.rs +++ b/core/embed/rust/src/ui/model_tr/component/result.rs @@ -3,6 +3,8 @@ use crate::ui::{ constant::{screen, HEIGHT, WIDTH}, display::{Color, Icon}, geometry::{Alignment2D, Offset, Point, Rect}, + shape, + shape::Renderer, }; const MESSAGE_AREA_START: i16 = 24 + 11; @@ -107,4 +109,17 @@ impl<'a> Component for ResultScreen<'a> { self.message_top.paint(); self.message_bottom.paint(); } + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.bg.render(target); + self.small_pad.render(target); + + shape::ToifImage::new(screen().top_center() + Offset::y(ICON_TOP), self.icon.toif) + .with_align(Alignment2D::CENTER) + .with_fg(self.fg_color) + .render(target); + + self.message_top.render(target); + self.message_bottom.render(target); + } } diff --git a/core/embed/rust/src/ui/model_tr/component/scrollbar.rs b/core/embed/rust/src/ui/model_tr/component/scrollbar.rs index d394da7be..3e812ee91 100644 --- a/core/embed/rust/src/ui/model_tr/component/scrollbar.rs +++ b/core/embed/rust/src/ui/model_tr/component/scrollbar.rs @@ -2,6 +2,8 @@ use crate::ui::{ component::{Component, Event, EventCtx, Never, Pad, Paginate}, display, geometry::{Offset, Point, Rect}, + shape, + shape::Renderer, }; use super::super::theme; @@ -103,6 +105,35 @@ impl ScrollBar { } } + /// Create a (seemingly circular) dot given its top left point. + /// Make it full when it is active, otherwise paint just the perimeter and + /// leave center empty. + fn render_dot<'s>(&self, target: &mut impl Renderer<'s>, dot_type: &DotType, top_right: Point) { + let full_square = + Rect::from_top_right_and_size(top_right, Offset::uniform(Self::MAX_DOT_SIZE)); + + match dot_type { + DotType::BigFull => shape::Bar::new(full_square) + .with_radius(2) + .with_bg(theme::FG) + .render(target), + + DotType::Big => shape::Bar::new(full_square) + .with_radius(2) + .with_fg(theme::FG) + .render(target), + + DotType::Middle => shape::Bar::new(full_square.shrink(1)) + .with_radius(1) + .with_fg(theme::FG) + .render(target), + + DotType::Small => shape::Bar::new(full_square.shrink(2)) + .with_bg(theme::FG) + .render(target), + } + } + /// Get a sequence of dots to be drawn, with specifying their appearance. /// Painting only big dots in case of 2 and 3 pages, /// three big and 1 middle in case of 4 pages, @@ -202,6 +233,14 @@ impl ScrollBar { top_right.x -= Self::DOTS_INTERVAL; } } + + fn render_horizontal<'s>(&'s self, target: &mut impl Renderer<'s>) { + let mut top_right = self.pad.area.top_right(); + for dot in self.get_drawable_dots().iter().rev() { + self.render_dot(target, dot, top_right); + top_right.x -= Self::DOTS_INTERVAL; + } + } } impl Component for ScrollBar { @@ -233,6 +272,17 @@ impl Component for ScrollBar { self.pad.paint(); self.paint_horizontal(); } + + /// Displaying one dot for each page. + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + // Not showing the scrollbar dot when there is only one page + if self.page_count <= 1 { + return; + } + + self.pad.render(target); + self.render_horizontal(target); + } } impl Paginate for ScrollBar { diff --git a/core/embed/rust/src/ui/model_tr/component/share_words.rs b/core/embed/rust/src/ui/model_tr/component/share_words.rs index 24283950a..c9ca06667 100644 --- a/core/embed/rust/src/ui/model_tr/component/share_words.rs +++ b/core/embed/rust/src/ui/model_tr/component/share_words.rs @@ -3,10 +3,13 @@ use crate::{ translations::TR, ui::{ component::{ - text::util::text_multiline, Child, Component, Event, EventCtx, Never, Paginate, + text::util::{text_multiline, text_multiline2}, + Child, Component, Event, EventCtx, Never, Paginate, }, display::Font, geometry::{Alignment, Offset, Rect}, + shape, + shape::Renderer, }, }; @@ -95,6 +98,20 @@ where ); } + /// Display the final page with user confirmation. + fn render_final_page<'s>(&'s self, target: &mut impl Renderer<'s>) { + let final_text = self.get_final_text(); + text_multiline2( + target, + self.area.split_top(INFO_TOP_OFFSET).1, + final_text.as_str().into(), + Font::NORMAL, + theme::FG, + theme::BG, + Alignment::Start, + ); + } + /// Display current set of recovery words. fn paint_words(&mut self) { let mut y_offset = 0; @@ -112,6 +129,32 @@ where display_left(baseline + Offset::x(WORD_X_OFFSET), word, WORD_FONT); } } + + /// Display current set of recovery words. + fn render_words<'s>(&'s self, target: &mut impl Renderer<'s>) { + let mut y_offset = 0; + // Showing the word index and the words itself + for i in 0..WORDS_PER_PAGE { + y_offset += NUMBER_FONT.line_height() + EXTRA_LINE_HEIGHT; + let index = self.word_index() + i; + if index >= self.share_words.len() { + break; + } + let word = &self.share_words[index]; + let baseline = self.area.top_left() + Offset::y(y_offset); + let ordinal = build_string!(5, inttostr!(index as u8 + 1), "."); + + shape::Text::new(baseline + Offset::x(NUMBER_X_OFFSET), &ordinal) + .with_font(NUMBER_FONT) + .with_fg(theme::FG) + .render(target); + + shape::Text::new(baseline + Offset::x(WORD_X_OFFSET), word.as_ref()) + .with_font(WORD_FONT) + .with_fg(theme::FG) + .render(target); + } + } } impl Component for ShareWords @@ -147,6 +190,17 @@ where self.paint_words(); } } + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + // Showing scrollbar in all cases + // Individual pages are responsible for not colliding with it + self.scrollbar.render(target); + if self.is_final_page() { + self.render_final_page(target); + } else { + self.render_words(target); + } + } } impl Paginate for ShareWords diff --git a/core/embed/rust/src/ui/model_tr/component/show_more.rs b/core/embed/rust/src/ui/model_tr/component/show_more.rs index 717481d90..c2a10e1b5 100644 --- a/core/embed/rust/src/ui/model_tr/component/show_more.rs +++ b/core/embed/rust/src/ui/model_tr/component/show_more.rs @@ -3,6 +3,7 @@ use crate::{ ui::{ component::{Child, Component, Event, EventCtx}, geometry::{Insets, Rect}, + shape::Renderer, }, }; @@ -77,6 +78,11 @@ where self.content.paint(); self.buttons.paint(); } + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.content.render(target); + self.buttons.render(target); + } } // DEBUG-ONLY SECTION BELOW diff --git a/core/embed/rust/src/ui/model_tr/component/title.rs b/core/embed/rust/src/ui/model_tr/component/title.rs index 1f1c116c5..07d99d6ab 100644 --- a/core/embed/rust/src/ui/model_tr/component/title.rs +++ b/core/embed/rust/src/ui/model_tr/component/title.rs @@ -4,7 +4,9 @@ use crate::{ ui::{ component::{Component, Event, EventCtx, Marquee, Never}, display, - geometry::{Offset, Rect}, + geometry::{Alignment, Offset, Rect}, + shape, + shape::Renderer, }, }; @@ -69,6 +71,16 @@ where ); } + /// Display title/header at the top left of the given area. + pub fn render_header_left<'s>(target: &mut impl Renderer<'s>, title: &T, area: Rect) { + let text_height = theme::FONT_HEADER.text_height(); + let title_baseline = area.top_left() + Offset::y(text_height - 1); + shape::Text::new(title_baseline, title.as_ref()) + .with_font(theme::FONT_HEADER) + .with_fg(theme::FG) + .render(target); + } + /// Display title/header centered at the top of the given area. pub fn paint_header_centered(title: &T, area: Rect) { let text_height = theme::FONT_HEADER.text_height(); @@ -81,6 +93,17 @@ where theme::BG, ); } + + /// Display title/header centered at the top of the given area. + pub fn render_header_centered<'s>(target: &mut impl Renderer<'s>, title: &T, area: Rect) { + let text_height = theme::FONT_HEADER.text_height(); + let title_baseline = area.top_center() + Offset::y(text_height - 1); + shape::Text::new(title_baseline, title.as_ref()) + .with_align(Alignment::Center) + .with_font(theme::FONT_HEADER) + .with_fg(theme::FG) + .render(target); + } } impl Component for Title @@ -116,6 +139,16 @@ where Self::paint_header_left(&self.title, self.area); } } + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + if self.needs_marquee { + self.marquee.render(target); + } else if self.centered { + Self::render_header_centered(target, &self.title, self.area); + } else { + Self::render_header_left(target, &self.title, self.area); + } + } } // DEBUG-ONLY SECTION BELOW diff --git a/core/embed/rust/src/ui/model_tr/component/welcome_screen.rs b/core/embed/rust/src/ui/model_tr/component/welcome_screen.rs index e9ae5985b..a824d85e7 100644 --- a/core/embed/rust/src/ui/model_tr/component/welcome_screen.rs +++ b/core/embed/rust/src/ui/model_tr/component/welcome_screen.rs @@ -1,6 +1,8 @@ use crate::ui::{ component::{Component, Event, EventCtx, Never}, geometry::{Alignment2D, Offset, Rect}, + shape, + shape::Renderer, }; use super::super::theme; @@ -52,6 +54,30 @@ impl Component for WelcomeScreen { theme::BG, ); } + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + shape::ToifImage::new( + self.area.bottom_center() - Offset::y(5), + theme::ICON_DEVICE_NAME.toif, + ) + .with_align(Alignment2D::BOTTOM_CENTER) + .with_fg(theme::FG) + .render(target); + + let icon = if self.empty_lock { + theme::ICON_LOGO_EMPTY + } else { + theme::ICON_LOGO + }; + + shape::ToifImage::new( + self.area.top_center() + Offset::y(ICON_TOP_MARGIN), + icon.toif, + ) + .with_align(Alignment2D::TOP_CENTER) + .with_fg(theme::FG) + .render(target); + } } #[cfg(feature = "ui_debug")] diff --git a/core/embed/rust/src/ui/model_tr/cshape/dotted_line.rs b/core/embed/rust/src/ui/model_tr/cshape/dotted_line.rs new file mode 100644 index 000000000..30e576879 --- /dev/null +++ b/core/embed/rust/src/ui/model_tr/cshape/dotted_line.rs @@ -0,0 +1,88 @@ +use crate::ui::{ + canvas::Canvas, + display::Color, + geometry::{Offset, Point, Rect}, + shape::{DrawingCache, Renderer, Shape, ShapeClone}, +}; + +use without_alloc::alloc::LocalAllocLeakExt; + +// Shape of horizontal solid/dotted line +pub struct HorizontalLine { + /// Position of the left-top point + pos: Point, + // Length of the line + length: i16, + /// Line thickness (default 1) + thickness: u8, + /// Steps of dots (default 0 - full line) + step: u8, + /// Color + color: Color, +} + +impl HorizontalLine { + pub fn new(pos: Point, length: i16) -> Self { + Self { + pos, + length, + thickness: 1, + step: 0, + color: Color::white(), + } + } + + pub fn with_color(self, color: Color) -> Self { + Self { color, ..self } + } + + pub fn with_thickness(self, thickness: u8) -> Self { + Self { thickness, ..self } + } + + pub fn with_step(self, step: u8) -> Self { + Self { step, ..self } + } + + pub fn render<'s>(self, renderer: &mut impl Renderer<'s>) { + renderer.render_shape(self); + } +} + +impl<'s> Shape<'s> for HorizontalLine { + fn bounds(&self, _cache: &DrawingCache<'s>) -> Rect { + let size = Offset::new(self.length, self.thickness as i16); + Rect::from_top_left_and_size(self.pos, size) + } + + fn cleanup(&mut self, _cache: &DrawingCache) {} + + fn draw(&mut self, canvas: &mut dyn Canvas, _cache: &DrawingCache) { + if self.step <= self.thickness { + // Solid line + let size = Offset::new(self.length, self.thickness as i16); + let r = Rect::from_top_left_and_size(self.pos, size); + canvas.fill_rect(r, self.color, 255); + } else { + // Dotted line + let thickness = self.thickness as i16; + for x in (0..self.length - thickness).step_by(self.step as usize) { + let r = Rect::from_top_left_and_size( + self.pos + Offset::x(x), + Offset::uniform(thickness), + ); + canvas.fill_rect(r, self.color, 255); + } + } + } +} + +impl<'s> ShapeClone<'s> for HorizontalLine { + fn clone_at_bump<'alloc, T>(self, bump: &'alloc T) -> Option<&'alloc mut dyn Shape<'s>> + where + T: LocalAllocLeakExt<'alloc>, + { + let clone = bump.alloc_t::()?; + Some(clone.uninit.init(HorizontalLine { ..self })) + } +} diff --git a/core/embed/rust/src/ui/model_tr/cshape/loader_circular.rs b/core/embed/rust/src/ui/model_tr/cshape/loader_circular.rs new file mode 100644 index 000000000..3e2c9cbf0 --- /dev/null +++ b/core/embed/rust/src/ui/model_tr/cshape/loader_circular.rs @@ -0,0 +1,117 @@ +use crate::ui::{ + canvas::Canvas, + display::Color, + geometry::{Offset, Point, Rect}, + shape::{DrawingCache, Renderer, Shape, ShapeClone}, +}; + +use without_alloc::alloc::LocalAllocLeakExt; + +static CELLS: [Offset; 24] = [ + Offset::new(1, -4), + Offset::new(2, -4), + Offset::new(3, -3), + Offset::new(4, -2), + Offset::new(4, -1), + Offset::new(4, 0), + Offset::new(4, 1), + Offset::new(4, 2), + Offset::new(3, 3), + Offset::new(2, 4), + Offset::new(1, 4), + Offset::new(0, 4), + Offset::new(-1, 4), + Offset::new(-2, 4), + Offset::new(-3, 3), + Offset::new(-4, 2), + Offset::new(-4, 1), + Offset::new(-4, 0), + Offset::new(-4, -1), + Offset::new(-4, -2), + Offset::new(-3, -3), + Offset::new(-2, -4), + Offset::new(-1, -4), + Offset::new(0, -4), +]; + +pub struct LoaderCircular { + /// Position of point (0,0) + pos: Point, + /// Value 0..1000 + value: u16, + /// Color + color: Color, + /// Scale (length of square size) + scale: i16, +} + +impl LoaderCircular { + pub fn new(pos: Point, value: u16) -> Self { + Self { + pos, + value, + color: Color::white(), + scale: 2, + } + } + + pub fn with_color(self, color: Color) -> Self { + Self { color, ..self } + } + + pub fn with_scale(self, scale: i16) -> Self { + Self { scale, ..self } + } + + fn cells(&self) -> &[Offset] { + let value = self.value.clamp(0, 1000); + let last = (CELLS.len() * value as usize) / 1000; + &CELLS[..last] + } + + fn cell_rect(&self, offset: Offset) -> Rect { + let pt = Point::new( + self.pos.x + (offset.x * self.scale) - self.scale / 2, + self.pos.y + (offset.y * self.scale) - self.scale / 2, + ); + Rect::from_top_left_and_size(pt, Offset::uniform(self.scale)) + } + + pub fn render<'s>(self, renderer: &mut impl Renderer<'s>) { + renderer.render_shape(self); + } +} + +impl<'s> Shape<'s> for LoaderCircular { + fn bounds(&self, _cache: &DrawingCache<'s>) -> Rect { + let cells = self.cells(); + + if cells.is_empty() { + Rect::zero() + } else { + let mut b = self.cell_rect(cells[0]); + cells[1..] + .iter() + .for_each(|c| b = b.union(self.cell_rect(*c))); + b + } + } + + fn cleanup(&mut self, _cache: &DrawingCache) {} + + fn draw(&mut self, canvas: &mut dyn Canvas, _cache: &DrawingCache) { + for c in self.cells().iter() { + canvas.fill_rect(self.cell_rect(*c), self.color, 255); + } + } +} + +impl ShapeClone<'_> for LoaderCircular { + fn clone_at_bump<'alloc, T>(self, bump: &'alloc T) -> Option<&'alloc mut dyn Shape> + where + T: LocalAllocLeakExt<'alloc>, + { + let clone = bump.alloc_t::()?; + Some(clone.uninit.init(LoaderCircular { ..self })) + } +} diff --git a/core/embed/rust/src/ui/model_tr/cshape/loader_small.rs b/core/embed/rust/src/ui/model_tr/cshape/loader_small.rs new file mode 100644 index 000000000..6797eb567 --- /dev/null +++ b/core/embed/rust/src/ui/model_tr/cshape/loader_small.rs @@ -0,0 +1,89 @@ +use crate::ui::{ + canvas::Canvas, + display::Color, + geometry::{Offset, Point, Rect}, + shape::{DrawingCache, Renderer, Shape, ShapeClone}, +}; + +use without_alloc::alloc::LocalAllocLeakExt; + +use core::f32::consts::SQRT_2; + +const STAR_COUNT: usize = 8; +const RADIUS: i16 = 3; +const DIAGONAL: i16 = ((RADIUS as f32 * SQRT_2) / 2_f32) as i16; + +// Offset of the normal point and then the extra offset for the main point +static STARS: [(Offset, Offset); STAR_COUNT] = [ + (Offset::y(-RADIUS), Offset::y(-1)), + (Offset::new(DIAGONAL, -DIAGONAL), Offset::new(1, -1)), + (Offset::x(RADIUS), Offset::x(1)), + (Offset::new(DIAGONAL, DIAGONAL), Offset::new(1, 1)), + (Offset::y(RADIUS), Offset::y(1)), + (Offset::new(-DIAGONAL, DIAGONAL), Offset::new(-1, 1)), + (Offset::x(-RADIUS), Offset::x(-1)), + (Offset::new(-DIAGONAL, -DIAGONAL), Offset::new(-1, -1)), +]; + +/// A shape of a TS3 small loader +pub struct LoaderSmall { + /// Position of point (0,0) + pos: Point, + /// Value 0..1000 + value: u16, + /// Color + color: Color, +} + +impl LoaderSmall { + pub fn new(pos: Point, value: u16) -> Self { + Self { + pos, + value, + color: Color::white(), + } + } + + pub fn with_color(self, color: Color) -> Self { + Self { color, ..self } + } + + pub fn render<'s>(self, renderer: &mut impl Renderer<'s>) { + renderer.render_shape(self); + } +} + +impl Shape<'_> for LoaderSmall { + fn bounds(&self, _cache: &DrawingCache) -> Rect { + Rect::from_top_left_and_size(self.pos, Offset::uniform(1)).expand(RADIUS + 1) + } + + fn cleanup(&mut self, _cache: &DrawingCache) {} + + fn draw(&mut self, canvas: &mut dyn Canvas, _cache: &DrawingCache) { + // Calculate index of the highlighted star + let sel_idx = (STAR_COUNT * self.value as usize / 1000) % STAR_COUNT; + + for (i, (star_offset, hili_offset)) in STARS.iter().enumerate() { + if (sel_idx + 1) % STAR_COUNT != i { + // Draw a star if it's not behind the highlighted one (clockwise) + let star_pos = self.pos + *star_offset; + canvas.draw_pixel(star_pos, self.color); + if sel_idx == i { + // Higlight the main star + canvas.draw_pixel(star_pos + *hili_offset, self.color); + } + } + } + } +} + +impl ShapeClone<'_> for LoaderSmall { + fn clone_at_bump<'alloc, T>(self, bump: &'alloc T) -> Option<&'alloc mut dyn Shape> + where + T: LocalAllocLeakExt<'alloc>, + { + let clone = bump.alloc_t::()?; + Some(clone.uninit.init(LoaderSmall { ..self })) + } +} diff --git a/core/embed/rust/src/ui/model_tr/cshape/loader_starry.rs b/core/embed/rust/src/ui/model_tr/cshape/loader_starry.rs new file mode 100644 index 000000000..ebd09e412 --- /dev/null +++ b/core/embed/rust/src/ui/model_tr/cshape/loader_starry.rs @@ -0,0 +1,106 @@ +use crate::ui::{ + canvas::Canvas, + display::Color, + geometry::{Offset, Point, Rect}, + shape::{DrawingCache, Renderer, Shape, ShapeClone}, +}; + +use without_alloc::alloc::LocalAllocLeakExt; + +use core::f32::consts::SQRT_2; + +const STAR_COUNT: usize = 8; +const STAR_SMALL: i16 = 2; +const STAR_MEDIUM: i16 = 4; +const STAR_LARGE: i16 = 6; + +const RADIUS: i16 = 13; +const DIAGONAL: i16 = ((RADIUS as f32 * SQRT_2) / 2_f32) as i16; + +// Offset of the normal point and then the extra offset for the main point +static STARS: [Offset; STAR_COUNT] = [ + Offset::y(-RADIUS), + Offset::new(DIAGONAL, -DIAGONAL), + Offset::x(RADIUS), + Offset::new(DIAGONAL, DIAGONAL), + Offset::y(RADIUS), + Offset::new(-DIAGONAL, DIAGONAL), + Offset::x(-RADIUS), + Offset::new(-DIAGONAL, -DIAGONAL), +]; + +/// A shape of a TS3 starry loader +pub struct LoaderStarry { + /// Position of point (0,0) + pos: Point, + /// Value 0..1000 + value: u16, + /// Color + color: Color, +} + +impl LoaderStarry { + pub fn new(pos: Point, value: u16) -> Self { + Self { + pos, + value, + color: Color::white(), + } + } + + pub fn with_color(self, color: Color) -> Self { + Self { color, ..self } + } + + pub fn render<'s>(self, renderer: &mut impl Renderer<'s>) { + renderer.render_shape(self); + } + + fn draw_large_star(&self, canvas: &mut dyn Canvas, offset: Offset) { + let r = Rect::from_center_and_size(self.pos + offset, Offset::uniform(STAR_LARGE)); + canvas.fill_round_rect(r, 2, self.color, 255); + } + + fn draw_medium_star(&self, canvas: &mut dyn Canvas, offset: Offset) { + let r = Rect::from_center_and_size(self.pos + offset, Offset::uniform(STAR_MEDIUM)); + canvas.fill_round_rect(r, 1, self.color, 255); + } + + fn draw_small_star(&self, canvas: &mut dyn Canvas, offset: Offset) { + let r = Rect::from_center_and_size(self.pos + offset, Offset::uniform(STAR_SMALL)); + canvas.fill_rect(r, self.color, 255); + } +} + +impl Shape<'_> for LoaderStarry { + fn bounds(&self, _cache: &DrawingCache) -> Rect { + Rect::from_top_left_and_size(self.pos, Offset::uniform(1)).expand(RADIUS + STAR_LARGE) + } + + fn cleanup(&mut self, _cache: &DrawingCache) {} + + fn draw(&mut self, canvas: &mut dyn Canvas, _cache: &DrawingCache) { + // Calculate the index of the big star + let sel_idx = (STAR_COUNT * self.value as usize / 1000) % STAR_COUNT; + + for (i, c) in STARS.iter().enumerate() { + if i == sel_idx { + self.draw_large_star(canvas, *c); + } else if (sel_idx + 1) % 8 == i || (sel_idx - 1) % 8 == i { + self.draw_medium_star(canvas, *c); + } else { + self.draw_small_star(canvas, *c); + } + } + } +} + +impl ShapeClone<'_> for LoaderStarry { + fn clone_at_bump<'alloc, T>(self, bump: &'alloc T) -> Option<&'alloc mut dyn Shape> + where + T: LocalAllocLeakExt<'alloc>, + { + let clone = bump.alloc_t::()?; + Some(clone.uninit.init(LoaderStarry { ..self })) + } +} diff --git a/core/embed/rust/src/ui/model_tr/cshape/mod.rs b/core/embed/rust/src/ui/model_tr/cshape/mod.rs new file mode 100644 index 000000000..99d5db684 --- /dev/null +++ b/core/embed/rust/src/ui/model_tr/cshape/mod.rs @@ -0,0 +1,9 @@ +pub mod dotted_line; +pub mod loader_circular; +pub mod loader_small; +pub mod loader_starry; + +pub use dotted_line::HorizontalLine; +pub use loader_circular::LoaderCircular; +pub use loader_small::LoaderSmall; +pub use loader_starry::LoaderStarry; diff --git a/core/embed/rust/src/ui/model_tr/layout.rs b/core/embed/rust/src/ui/model_tr/layout.rs index 84f99be6f..731d58271 100644 --- a/core/embed/rust/src/ui/model_tr/layout.rs +++ b/core/embed/rust/src/ui/model_tr/layout.rs @@ -234,10 +234,9 @@ where } } -impl<'a, T, F> ComponentMsgObj for ConfirmHomescreen +impl<'a, T> ComponentMsgObj for ConfirmHomescreen where T: StringType + Clone, - F: Fn() -> &'a [u8], { fn msg_try_into_obj(&self, msg: Self::Msg) -> Result { match msg { @@ -422,15 +421,8 @@ extern "C" fn new_confirm_properties(n_args: usize, args: *const Obj, kwargs: *m extern "C" fn new_confirm_homescreen(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { let block = move |_args: &[Obj], kwargs: &Map| { let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?; - let data: Obj = kwargs.get(Qstr::MP_QSTR_image)?; - - // Layout needs to hold the Obj to play nice with GC. Obj is resolved to &[u8] - // in every paint pass. - // SAFETY: We expect no existing mutable reference. Resulting reference is - // discarded before returning to micropython. - let buffer_func = move || unsafe { unwrap!(get_buffer(data)) }; - - let obj = LayoutObj::new(ConfirmHomescreen::new(title, buffer_func))?; + let image: Obj = kwargs.get(Qstr::MP_QSTR_image)?; + let obj = LayoutObj::new(ConfirmHomescreen::new(title, image))?; Ok(obj.into()) }; diff --git a/core/embed/rust/src/ui/model_tr/mod.rs b/core/embed/rust/src/ui/model_tr/mod.rs index 0825f52bc..57e02ed19 100644 --- a/core/embed/rust/src/ui/model_tr/mod.rs +++ b/core/embed/rust/src/ui/model_tr/mod.rs @@ -3,6 +3,7 @@ pub mod bootloader; pub mod common_messages; pub mod component; pub mod constant; +pub mod cshape; #[cfg(feature = "micropython")] pub mod layout; pub mod screens; diff --git a/core/embed/rust/src/ui/model_tr/screens.rs b/core/embed/rust/src/ui/model_tr/screens.rs index efce22354..4bce551c0 100644 --- a/core/embed/rust/src/ui/model_tr/screens.rs +++ b/core/embed/rust/src/ui/model_tr/screens.rs @@ -1,9 +1,14 @@ #[cfg(feature = "micropython")] use crate::micropython::buffer::StrBuffer; use crate::ui::{ - component::base::Component, constant::screen, display, model_tr::component::WelcomeScreen, + component::base::Component, constant::screen, model_tr::component::WelcomeScreen, }; +#[cfg(not(feature = "new_rendering"))] +use crate::ui::display; +#[cfg(feature = "new_rendering")] +use crate::ui::{display::Color, shape::render_on_display}; + use super::{component::ErrorScreen, constant}; #[cfg(not(feature = "micropython"))] @@ -27,6 +32,13 @@ pub fn screen_fatal_error(title: &str, msg: &str, footer: &str) { let mut frame = ErrorScreen::new(title, msg, footer); frame.place(constant::screen()); + + #[cfg(feature = "new_rendering")] + render_on_display(None, Some(Color::black()), |target| { + frame.render(target); + }); + + #[cfg(not(feature = "new_rendering"))] frame.paint(); } @@ -34,7 +46,16 @@ pub fn screen_fatal_error(title: &str, msg: &str, footer: &str) { extern "C" fn screen_boot_full() { let mut frame = WelcomeScreen::new(false); frame.place(screen()); - display::sync(); - frame.paint(); - display::refresh(); + + #[cfg(feature = "new_rendering")] + render_on_display(None, Some(Color::black()), |target| { + frame.render(target); + }); + + #[cfg(not(feature = "new_rendering"))] + { + display::sync(); + frame.paint(); + display::refresh(); + } } diff --git a/core/embed/rust/src/ui/model_tt/bootloader/intro.rs b/core/embed/rust/src/ui/model_tt/bootloader/intro.rs index b113c9c6a..033d86c04 100644 --- a/core/embed/rust/src/ui/model_tt/bootloader/intro.rs +++ b/core/embed/rust/src/ui/model_tt/bootloader/intro.rs @@ -11,6 +11,7 @@ use crate::ui::{ CONTENT_PADDING, CORNER_BUTTON_AREA, MENU32, TEXT_NORMAL, TEXT_WARNING, TITLE_AREA, }, }, + shape::Renderer, }; #[repr(u32)] @@ -102,6 +103,15 @@ impl<'a> Component for Intro<'a> { self.menu.paint(); } + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.bg.render(target); + self.title.render(target); + self.text.render(target); + self.warn.render(target); + self.host.render(target); + self.menu.render(target); + } + #[cfg(feature = "ui_bounds")] fn bounds(&self, sink: &mut dyn FnMut(Rect)) { self.menu.bounds(sink); diff --git a/core/embed/rust/src/ui/model_tt/bootloader/menu.rs b/core/embed/rust/src/ui/model_tt/bootloader/menu.rs index 9cbf43df1..234f70146 100644 --- a/core/embed/rust/src/ui/model_tt/bootloader/menu.rs +++ b/core/embed/rust/src/ui/model_tt/bootloader/menu.rs @@ -13,6 +13,7 @@ use crate::{ X32, }, }, + shape::Renderer, }, }; @@ -108,6 +109,14 @@ impl Component for Menu { self.reset.paint(); } + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.bg.render(target); + self.title.render(target); + self.close.render(target); + self.reboot.render(target); + self.reset.render(target); + } + #[cfg(feature = "ui_bounds")] fn bounds(&self, sink: &mut dyn FnMut(Rect)) { self.close.bounds(sink); diff --git a/core/embed/rust/src/ui/model_tt/bootloader/welcome.rs b/core/embed/rust/src/ui/model_tt/bootloader/welcome.rs index 5a84bccfe..0e2e3302f 100644 --- a/core/embed/rust/src/ui/model_tt/bootloader/welcome.rs +++ b/core/embed/rust/src/ui/model_tt/bootloader/welcome.rs @@ -1,12 +1,14 @@ use crate::ui::{ component::{Component, Event, EventCtx, Never, Pad}, constant::screen, - display::{self, Font, Icon}, - geometry::{Alignment2D, Offset, Rect}, + display::{self, toif::Toif, Font, Icon}, + geometry::{Alignment, Alignment2D, Offset, Rect}, model_tt::theme::{ bootloader::{START_URL, WELCOME_COLOR}, BLACK, GREY_MEDIUM, WHITE, }, + shape, + shape::Renderer, }; pub struct Welcome { @@ -56,4 +58,26 @@ impl Component for Welcome { BLACK, ); } + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.bg.render(target); + + shape::Text::new(screen().top_center() + Offset::y(102), "Get started with") + .with_align(Alignment::Center) + .with_font(Font::NORMAL) + .with_fg(GREY_MEDIUM) + .render(target); + + shape::Text::new(screen().top_center() + Offset::y(126), "your Trezor at") + .with_align(Alignment::Center) + .with_font(Font::NORMAL) + .with_fg(GREY_MEDIUM) + .render(target); + + let icon = unwrap!(Toif::new(START_URL)); + shape::ToifImage::new(screen().top_center() + Offset::y(135), icon) + .with_align(Alignment2D::TOP_CENTER) + .with_fg(WHITE) + .render(target); + } } diff --git a/core/embed/rust/src/ui/model_tt/component/address_details.rs b/core/embed/rust/src/ui/model_tt/component/address_details.rs index e9ec605d8..3308198f6 100644 --- a/core/embed/rust/src/ui/model_tt/component/address_details.rs +++ b/core/embed/rust/src/ui/model_tt/component/address_details.rs @@ -11,6 +11,7 @@ use crate::{ Component, Event, EventCtx, Paginate, Qr, }, geometry::Rect, + shape::Renderer, }, }; @@ -187,6 +188,14 @@ where } } + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + match self.current_page { + 0 => self.qr_code.render(target), + 1 => self.details.render(target), + _ => self.xpub_view.render(target), + } + } + #[cfg(feature = "ui_bounds")] fn bounds(&self, sink: &mut dyn FnMut(Rect)) { match self.current_page { diff --git a/core/embed/rust/src/ui/model_tt/component/bl_confirm.rs b/core/embed/rust/src/ui/model_tt/component/bl_confirm.rs index 0f79bb790..daa002b1a 100644 --- a/core/embed/rust/src/ui/model_tt/component/bl_confirm.rs +++ b/core/embed/rust/src/ui/model_tt/component/bl_confirm.rs @@ -15,6 +15,8 @@ use crate::ui::{ WHITE, }, }, + shape, + shape::Renderer, }; const ICON_TOP: i16 = 17; @@ -234,6 +236,40 @@ where } } + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.bg.render(target); + self.content_pad.render(target); + + if let Some(info) = self.info.as_ref() { + if self.show_info { + info.close_button.render(target); + info.title.render(target); + info.text.render(target); + self.left_button.render(target); + self.right_button.render(target); + // short-circuit before painting the main components + return; + } else { + info.info_button.render(target); + // pass through to the rest of the paint + } + } + + self.message.render(target); + self.alert.render(target); + self.left_button.render(target); + self.right_button.render(target); + match &self.title { + ConfirmTitle::Text(label) => label.render(target), + ConfirmTitle::Icon(icon) => { + shape::ToifImage::new(Point::new(screen().center().x, ICON_TOP), icon.toif) + .with_align(Alignment2D::TOP_CENTER) + .with_fg(WHITE) + .render(target); + } + } + } + #[cfg(feature = "ui_bounds")] fn bounds(&self, sink: &mut dyn FnMut(Rect)) { self.left_button.bounds(sink); diff --git a/core/embed/rust/src/ui/model_tt/component/button.rs b/core/embed/rust/src/ui/model_tt/component/button.rs index d9e2a558c..a97f74651 100644 --- a/core/embed/rust/src/ui/model_tt/component/button.rs +++ b/core/embed/rust/src/ui/model_tt/component/button.rs @@ -9,6 +9,8 @@ use crate::{ display::{self, toif::Icon, Color, Font}, event::TouchEvent, geometry::{Alignment2D, Insets, Offset, Point, Rect}, + shape, + shape::Renderer, }, }; @@ -189,6 +191,18 @@ impl Button { } } + pub fn render_background<'s>(&self, target: &mut impl Renderer<'s>, style: &ButtonStyle) { + match &self.content { + ButtonContent::IconBlend(_, _, _) => {} + _ => shape::Bar::new(self.area) + .with_bg(style.button_color) + .with_fg(style.border_color) + .with_thickness(style.border_width) + .with_radius(style.border_radius as i16) + .render(target), + } + } + pub fn paint_content(&self, style: &ButtonStyle) where T: AsRef, @@ -229,6 +243,47 @@ impl Button { ), } } + + pub fn render_content<'s>(&self, target: &mut impl Renderer<'s>, style: &ButtonStyle) + where + T: AsRef, + { + match &self.content { + ButtonContent::Empty => {} + ButtonContent::Text(text) => { + let text = text.as_ref(); + let width = style.font.text_width(text); + let height = style.font.text_height(); + let start_of_baseline = self.area.center() + + Offset::new(-width / 2, height / 2) + + Offset::y(Self::BASELINE_OFFSET); + shape::Text::new(start_of_baseline, text) + .with_font(style.font) + .with_fg(style.text_color) + .render(target); + } + ButtonContent::Icon(icon) => { + shape::ToifImage::new(self.area.center(), icon.toif) + .with_align(Alignment2D::CENTER) + .with_fg(style.text_color) + .render(target); + } + ButtonContent::IconAndText(child) => { + child.paint(self.area, self.style(), Self::BASELINE_OFFSET); + } + ButtonContent::IconBlend(bg, fg, offset) => { + shape::Bar::new(self.area) + .with_bg(style.background_color) + .render(target); + shape::ToifImage::new(self.area.top_left(), bg.toif) + .with_fg(style.button_color) + .render(target); + shape::ToifImage::new(self.area.top_left() + *offset, fg.toif) + .with_fg(style.text_color) + .render(target); + } + } + } } impl Component for Button @@ -320,6 +375,12 @@ where self.paint_content(style); } + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + let style = self.style(); + self.render_background(target, style); + self.render_content(target, style); + } + #[cfg(feature = "ui_bounds")] fn bounds(&self, sink: &mut dyn FnMut(Rect)) { sink(self.area); diff --git a/core/embed/rust/src/ui/model_tt/component/coinjoin_progress.rs b/core/embed/rust/src/ui/model_tt/component/coinjoin_progress.rs index 5a615328f..1e451d42c 100644 --- a/core/embed/rust/src/ui/model_tt/component/coinjoin_progress.rs +++ b/core/embed/rust/src/ui/model_tt/component/coinjoin_progress.rs @@ -6,12 +6,16 @@ use crate::{ micropython::buffer::StrBuffer, translations::TR, ui::{ + canvas::algo::PI4, component::{ base::Never, painter, Child, Component, ComponentExt, Empty, Event, EventCtx, Label, Split, }, + constant, display::loader::{loader_circular_uncompress, LoaderDimensions}, - geometry::{Insets, Rect}, + geometry::{Insets, Offset, Rect}, + shape, + shape::Renderer, util::animation_disabled, }, }; @@ -129,6 +133,40 @@ where ); self.label.paint(); } + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.content.render(target); + + let center = constant::screen().center() + Offset::y(LOADER_OFFSET); + let active_color = theme::FG; + let background_color = theme::BG; + let inactive_color = background_color.blend(active_color, 85); + + let start = (self.value as i16 - 100) % 1000; + let end = (self.value as i16 + 100) % 1000; + let start = ((start as i32 * 8 * PI4 as i32) / 1000) as i16; + let end = ((end as i32 * 8 * PI4 as i32) / 1000) as i16; + + shape::Circle::new(center, LOADER_OUTER) + .with_bg(inactive_color) + .render(target); + + shape::Circle::new(center, LOADER_OUTER) + .with_bg(active_color) + .with_start_angle(start) + .with_end_angle(end) + .render(target); + + shape::Circle::new(center, LOADER_INNER + 2) + .with_bg(active_color) + .render(target); + + shape::Circle::new(center, LOADER_INNER) + .with_bg(background_color) + .render(target); + + self.label.render(target); + } } #[cfg(feature = "ui_debug")] diff --git a/core/embed/rust/src/ui/model_tt/component/dialog.rs b/core/embed/rust/src/ui/model_tt/component/dialog.rs index 2e7f409b6..49c344ca8 100644 --- a/core/embed/rust/src/ui/model_tt/component/dialog.rs +++ b/core/embed/rust/src/ui/model_tt/component/dialog.rs @@ -10,6 +10,7 @@ use crate::{ Child, Component, Event, EventCtx, Never, }, geometry::{Insets, LinearPlacement, Rect}, + shape::Renderer, }, }; @@ -71,6 +72,11 @@ where self.controls.paint(); } + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.content.render(target); + self.controls.render(target); + } + #[cfg(feature = "ui_bounds")] fn bounds(&self, sink: &mut dyn FnMut(Rect)) { self.content.bounds(sink); @@ -198,6 +204,12 @@ where self.controls.paint(); } + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.image.render(target); + self.paragraphs.render(target); + self.controls.render(target); + } + #[cfg(feature = "ui_bounds")] fn bounds(&self, sink: &mut dyn FnMut(Rect)) { self.image.bounds(sink); diff --git a/core/embed/rust/src/ui/model_tt/component/error.rs b/core/embed/rust/src/ui/model_tt/component/error.rs index 739770f4f..597959a13 100644 --- a/core/embed/rust/src/ui/model_tt/component/error.rs +++ b/core/embed/rust/src/ui/model_tt/component/error.rs @@ -2,6 +2,8 @@ use crate::ui::{ component::{Child, Component, Event, EventCtx, Label, Never, Pad}, constant::screen, geometry::{Alignment2D, Point, Rect}, + shape, + shape::Renderer, }; use crate::ui::model_tt::{ @@ -86,4 +88,19 @@ impl> Component for ErrorScreen<'_, T> { self.message.paint(); self.footer.paint(); } + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.bg.render(target); + + let icon = ICON_WARNING40; + shape::ToifImage::new(Point::new(screen().center().x, ICON_TOP), icon.toif) + .with_fg(WHITE) + .with_bg(FATAL_ERROR_COLOR) + .with_align(Alignment2D::TOP_CENTER) + .render(target); + + self.title.render(target); + self.message.render(target); + self.footer.render(target); + } } diff --git a/core/embed/rust/src/ui/model_tt/component/fido.rs b/core/embed/rust/src/ui/model_tt/component/fido.rs index 3b7f64caa..02c44a628 100644 --- a/core/embed/rust/src/ui/model_tt/component/fido.rs +++ b/core/embed/rust/src/ui/model_tt/component/fido.rs @@ -7,6 +7,8 @@ use crate::ui::{ swipe::{Swipe, SwipeDirection}, theme, ScrollBar, }, + shape, + shape::Renderer, }; use super::CancelConfirmMsg; @@ -210,6 +212,36 @@ where } } + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.icon.render(target); + self.controls.render(target); + self.app_name.render(target); + + if self.scrollbar.page_count > 1 { + self.scrollbar.render(target); + } + + // Erasing the old text content before writing the new one. + let account_name_area = self.account_name.area(); + let real_area = account_name_area + .with_height(account_name_area.height() + self.account_name.font().text_baseline() + 1); + shape::Bar::new(real_area).with_bg(theme::BG).render(target); + + // Account name is optional. + // Showing it only if it differs from app name. + // (Dummy requests usually have some text as both app_name and account_name.) + let account_name = self.account_name.text().as_ref(); + let app_name = self.app_name.text().as_ref(); + if !account_name.is_empty() && account_name != app_name { + self.account_name.render(target); + } + + if self.fade.take() { + // Note that this is blocking and takes some time. + display::fade_backlight(theme::BACKLIGHT_NORMAL); + } + } + #[cfg(feature = "ui_bounds")] fn bounds(&self, sink: &mut dyn FnMut(Rect)) { self.icon.bounds(sink); diff --git a/core/embed/rust/src/ui/model_tt/component/frame.rs b/core/embed/rust/src/ui/model_tt/component/frame.rs index ebd5aae12..de2705aab 100644 --- a/core/embed/rust/src/ui/model_tt/component/frame.rs +++ b/core/embed/rust/src/ui/model_tt/component/frame.rs @@ -6,6 +6,7 @@ use crate::ui::{ display::Icon, geometry::{Alignment, Insets, Offset, Rect}, model_tt::component::{Button, ButtonMsg, CancelInfoConfirmMsg}, + shape::Renderer, }; pub struct Frame { @@ -169,6 +170,13 @@ where self.content.paint(); } + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.title.render(target); + self.subtitle.render(target); + self.button.render(target); + self.content.render(target); + } + #[cfg(feature = "ui_bounds")] fn bounds(&self, sink: &mut dyn FnMut(Rect)) { self.title.bounds(sink); diff --git a/core/embed/rust/src/ui/model_tt/component/homescreen/mod.rs b/core/embed/rust/src/ui/model_tt/component/homescreen/mod.rs index 7937472eb..890cd52d9 100644 --- a/core/embed/rust/src/ui/model_tt/component/homescreen/mod.rs +++ b/core/embed/rust/src/ui/model_tt/component/homescreen/mod.rs @@ -1,6 +1,7 @@ mod render; use crate::{ + micropython::gc::Gc, strutil::TString, time::{Duration, Instant}, translations::TR, @@ -9,9 +10,10 @@ use crate::{ component::{Component, Event, EventCtx, Pad, TimerToken}, display::{self, tjpgd::jpeg_info, toif::Icon, Color, Font}, event::{TouchEvent, USBEvent}, - geometry::{Offset, Point, Rect}, + geometry::{Alignment, Alignment2D, Insets, Offset, Point, Rect}, layout::util::get_user_custom_image, model_tt::{constant, theme::IMAGE_HOMESCREEN}, + shape::{self, Renderer}, }, }; @@ -20,7 +22,7 @@ use crate::{ ui::{ constant::HEIGHT, display::{ - tjpgd::{jpeg_test, BufferInput}, + tjpgd::BufferInput, toif::{Toif, ToifFormat}, }, model_tt::component::homescreen::render::{ @@ -49,6 +51,7 @@ const LOADER_DURATION: Duration = Duration::from_millis(2000); pub struct Homescreen { label: TString<'static>, notification: Option<(TString<'static>, u8)>, + custom_image: Option>, hold_to_lock: bool, loader: Loader, pad: Pad, @@ -69,6 +72,7 @@ impl Homescreen { Self { label, notification, + custom_image: get_user_custom_image().ok(), hold_to_lock, loader: Loader::with_lock_icon().with_durations(LOADER_DURATION, LOADER_DURATION / 3), pad: Pad::with_background(theme::BG), @@ -119,6 +123,16 @@ impl Homescreen { self.loader.paint() } + fn render_loader<'s>(&'s self, target: &mut impl Renderer<'s>) { + TR::progress__locking_device.map_translated(|t| { + shape::Text::new(TOP_CENTER + Offset::y(HOLD_Y), t) + .with_align(Alignment::Center) + .with_font(Font::NORMAL) + .with_fg(theme::FG); + }); + self.loader.render(target) + } + pub fn set_paint_notification(&mut self) { self.paint_notification_only = true; } @@ -210,10 +224,9 @@ impl Component for Homescreen { let notification = self.get_notification(); - let res = get_user_custom_image(); let mut show_default = true; - if let Ok(data) = res { + if let Some(ref data) = self.custom_image { if is_image_jpeg(data.as_ref()) { let input = BufferInput(data.as_ref()); let mut pool = BufferJpegWork::get_cleared(); @@ -254,6 +267,86 @@ impl Component for Homescreen { } } + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.pad.render(target); + if self.loader.is_animating() || self.loader.is_completely_grown(Instant::now()) { + self.render_loader(target); + } else { + let img_data = match self.custom_image { + Some(ref img) => img.as_ref(), + None => IMAGE_HOMESCREEN, + }; + + if is_image_jpeg(img_data) { + shape::JpegImage::new(self.pad.area.center(), img_data) + .with_align(Alignment2D::CENTER) + .render(target); + } else if is_image_toif(img_data) { + shape::ToifImage::new(self.pad.area.center(), unwrap!(Toif::new(img_data))) + .with_align(Alignment2D::CENTER) + .render(target); + } + + self.label.map(|t| { + let r = Rect::new(Point::new(6, 198), Point::new(234, 233)); + shape::Bar::new(r) + .with_bg(Color::black()) + .with_alpha(89) + .with_radius(3) + .render(target); + + let style = theme::TEXT_DEMIBOLD; + let pos = Point::new(self.pad.area.center().x, LABEL_Y); + shape::Text::new(pos, t) + .with_align(Alignment::Center) + .with_font(style.text_font) + .with_fg(theme::FG) + .render(target); + }); + + if let Some(notif) = self.get_notification() { + const NOTIFICATION_HEIGHT: i16 = 36; + const NOTIFICATION_BORDER: i16 = 6; + const TEXT_ICON_SPACE: i16 = 8; + + let banner = self + .pad + .area + .inset(Insets::sides(NOTIFICATION_BORDER)) + .with_height(NOTIFICATION_HEIGHT) + .translate(Offset::y(NOTIFICATION_BORDER)); + + shape::Bar::new(banner) + .with_radius(2) + .with_bg(notif.color) + .render(target); + + notif.text.map(|t| { + let style = theme::TEXT_BOLD; + let icon_width = notif.icon.toif.width() + TEXT_ICON_SPACE; + let text_pos = Point::new( + style + .text_font + .horz_center(banner.x0 + icon_width, banner.x1, t), + style.text_font.vert_center(banner.y0, banner.y1, "A"), + ); + + shape::Text::new(text_pos, t) + .with_font(style.text_font) + .with_fg(style.text_color) + .render(target); + + let icon_pos = Point::new(text_pos.x - icon_width, banner.center().y); + + shape::ToifImage::new(icon_pos, notif.icon.toif) + .with_fg(style.text_color) + .with_align(Alignment2D::CENTER_LEFT) + .render(target); + }); + } + } + } + #[cfg(feature = "ui_bounds")] fn bounds(&self, sink: &mut dyn FnMut(Rect)) { self.loader.bounds(sink); @@ -271,6 +364,7 @@ impl crate::trace::Trace for Homescreen { pub struct Lockscreen { label: TString<'static>, + custom_image: Option>, bootscreen: bool, coinjoin_authorized: bool, } @@ -279,6 +373,7 @@ impl Lockscreen { pub fn new(label: TString<'static>, bootscreen: bool, coinjoin_authorized: bool) -> Self { Lockscreen { label, + custom_image: get_user_custom_image().ok(), bootscreen, coinjoin_authorized, } @@ -343,10 +438,9 @@ impl Component for Lockscreen { texts = &texts[1..]; } - let res = get_user_custom_image(); let mut show_default = true; - if let Ok(data) = res { + if let Some(ref data) = self.custom_image { if is_image_jpeg(data.as_ref()) { let input = BufferInput(data.as_ref()); let mut pool = BufferJpegWork::get_cleared(); @@ -370,10 +464,113 @@ impl Component for Lockscreen { homescreen_blurred(&mut hs_img, texts); } } + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + let img_data = match self.custom_image { + Some(ref img) => img.as_ref(), + None => IMAGE_HOMESCREEN, + }; + + let center = constant::screen().center(); + + if is_image_jpeg(img_data) { + shape::JpegImage::new(center, img_data) + .with_align(Alignment2D::CENTER) + .with_blur(4) + .with_dim(140) + .render(target); + } else if is_image_toif(img_data) { + shape::ToifImage::new(center, unwrap!(Toif::new(img_data))) + .with_align(Alignment2D::CENTER) + //.with_blur(5) + .render(target); + } + + let (locked, tap) = if self.bootscreen { + ( + TR::lockscreen__title_not_connected, + TR::lockscreen__tap_to_connect, + ) + } else { + (TR::lockscreen__title_locked, TR::lockscreen__tap_to_unlock) + }; + + let mut label_style = theme::TEXT_DEMIBOLD; + label_style.text_color = theme::GREY_LIGHT; + + let mut texts: &[HomescreenText] = &[ + HomescreenText { + text: "".into(), + style: theme::TEXT_NORMAL, + offset: Offset::new(2, COINJOIN_Y), + icon: Some(theme::ICON_COINJOIN), + }, + HomescreenText { + text: locked.into(), + style: theme::TEXT_BOLD, + offset: Offset::y(LOCKED_Y), + icon: Some(theme::ICON_LOCK), + }, + HomescreenText { + text: tap.into(), + style: theme::TEXT_NORMAL, + offset: Offset::y(TAP_Y), + icon: None, + }, + HomescreenText { + text: self.label, + style: label_style, + offset: Offset::y(LABEL_Y), + icon: None, + }, + ]; + + if !self.coinjoin_authorized { + texts = &texts[1..]; + } + + for item in texts.iter() { + item.text.map(|t| { + const TEXT_ICON_SPACE: i16 = 2; + + let icon_width = match item.icon { + Some(icon) => icon.toif.width() + TEXT_ICON_SPACE, + None => 0, + }; + + let area = constant::screen(); + + let text_pos = Point::new( + item.style + .text_font + .horz_center(area.x0 + icon_width, area.x1, t), + 0, + ) + item.offset; + + shape::Text::new(text_pos, t) + .with_font(item.style.text_font) + .with_fg(item.style.text_color) + .render(target); + + if let Some(icon) = item.icon { + let icon_pos = Point::new(text_pos.x - icon_width, text_pos.y); + shape::ToifImage::new(icon_pos, icon.toif) + .with_align(Alignment2D::BOTTOM_LEFT) + .with_fg(item.style.text_color) + .render(target); + } + }); + } + } } pub fn check_homescreen_format(buffer: &[u8]) -> bool { - is_image_jpeg(buffer) && jpeg_test(buffer) + #[cfg(not(feature = "new_rendering"))] + let result = is_image_jpeg(buffer) && crate::ui::display::tjpgd::jpeg_test(buffer); + #[cfg(feature = "new_rendering")] + let result = is_image_jpeg(buffer); // !@# TODO: test like if `new_rendering` is off + + result } fn is_image_jpeg(buffer: &[u8]) -> bool { diff --git a/core/embed/rust/src/ui/model_tt/component/keyboard/bip39.rs b/core/embed/rust/src/ui/model_tt/component/keyboard/bip39.rs index 5aa9cb16b..3badda21c 100644 --- a/core/embed/rust/src/ui/model_tt/component/keyboard/bip39.rs +++ b/core/embed/rust/src/ui/model_tt/component/keyboard/bip39.rs @@ -7,13 +7,15 @@ use crate::{ model_tt::{ component::{ keyboard::{ - common::{paint_pending_marker, MultiTapKeyboard}, + common::{paint_pending_marker, render_pending_marker, MultiTapKeyboard}, mnemonic::{MnemonicInput, MnemonicInputMsg, MNEMONIC_KEY_COUNT}, }, Button, ButtonContent, ButtonMsg, }, theme, }, + shape, + shape::Renderer, }, }; use heapless::String; @@ -154,6 +156,51 @@ impl Component for Bip39Input { } } + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + let area = self.button.area(); + let style = self.button.style(); + + // First, paint the button background. + self.button.render_background(target, style); + + // Paint the entered content (the prefix of the suggested word). + let text = self.textbox.content(); + let width = style.font.text_width(text); + // Content starts in the left-center point, offset by 16px to the right and 8px + // to the bottom. + let text_baseline = area.top_left().center(area.bottom_left()) + Offset::new(16, 8); + shape::Text::new(text_baseline, text) + .with_font(style.font) + .with_fg(style.text_color) + .render(target); + + // Paint the rest of the suggested dictionary word. + if let Some(word) = self.suggested_word.and_then(|w| w.get(text.len()..)) { + let word_baseline = text_baseline + Offset::new(width, 0); + let style = self.button_suggestion.style(); + shape::Text::new(word_baseline, word) + .with_font(style.font) + .with_fg(style.text_color) + .render(target); + } + + // Paint the pending marker. + if self.multi_tap.pending_key().is_some() { + render_pending_marker(target, text_baseline, text, style.font, style.text_color); + } + + // Paint the icon. + if let ButtonContent::Icon(icon) = self.button.content() { + // Icon is painted in the right-center point, of expected size 16x16 pixels, and + // 16px from the right edge. + let icon_center = area.top_right().center(area.bottom_right()) - Offset::new(16 + 8, 0); + shape::ToifImage::new(icon_center, icon.toif) + .with_align(Alignment2D::CENTER) + .with_fg(style.text_color) + .render(target); + } + } + #[cfg(feature = "ui_bounds")] fn bounds(&self, sink: &mut dyn FnMut(Rect)) { self.button.bounds(sink); diff --git a/core/embed/rust/src/ui/model_tt/component/keyboard/common.rs b/core/embed/rust/src/ui/model_tt/component/keyboard/common.rs index 0220ad4f7..9868770bc 100644 --- a/core/embed/rust/src/ui/model_tt/component/keyboard/common.rs +++ b/core/embed/rust/src/ui/model_tt/component/keyboard/common.rs @@ -4,6 +4,8 @@ use crate::{ component::{text::common::TextEdit, Event, EventCtx, TimerToken}, display::{self, Color, Font}, geometry::{Offset, Point, Rect}, + shape, + shape::Renderer, }, }; @@ -127,3 +129,24 @@ pub fn paint_pending_marker(text_baseline: Point, text: &str, font: Font, color: display::rect_fill(marker_rect, color); } } + +/// Create a visible "underscoring" of the last letter of a text. +pub fn render_pending_marker<'s>( + target: &mut impl Renderer<'s>, + text_baseline: Point, + text: &str, + font: Font, + color: Color, +) { + // Measure the width of the last character of input. + if let Some(last) = text.chars().last() { + let width = font.text_width(text); + let last_width = font.char_width(last); + // Draw the marker 2px under the start of the baseline of the last character. + let marker_origin = text_baseline + Offset::new(width - last_width, 2); + // Draw the marker 1px longer than the last character, and 3px thick. + let marker_rect = + Rect::from_top_left_and_size(marker_origin, Offset::new(last_width + 1, 3)); + shape::Bar::new(marker_rect).with_bg(color).render(target); + } +} diff --git a/core/embed/rust/src/ui/model_tt/component/keyboard/mnemonic.rs b/core/embed/rust/src/ui/model_tt/component/keyboard/mnemonic.rs index 0ca86598d..807a5e2ec 100644 --- a/core/embed/rust/src/ui/model_tt/component/keyboard/mnemonic.rs +++ b/core/embed/rust/src/ui/model_tt/component/keyboard/mnemonic.rs @@ -5,6 +5,7 @@ use crate::ui::{ component::{Button, ButtonMsg, Swipe, SwipeDirection}, theme, }, + shape::Renderer, }; pub const MNEMONIC_KEY_COUNT: usize = 9; @@ -182,6 +183,19 @@ where } } + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + if self.input.inner().inner().is_empty() { + self.prompt.render(target); + } else { + self.input.render(target); + self.back.render(target); + } + + for btn in &self.keys { + btn.render(target); + } + } + #[cfg(feature = "ui_bounds")] fn bounds(&self, sink: &mut dyn FnMut(Rect)) { self.prompt.bounds(sink); diff --git a/core/embed/rust/src/ui/model_tt/component/keyboard/passphrase.rs b/core/embed/rust/src/ui/model_tt/component/keyboard/passphrase.rs index ab74d7a69..4140e3f50 100644 --- a/core/embed/rust/src/ui/model_tt/component/keyboard/passphrase.rs +++ b/core/embed/rust/src/ui/model_tt/component/keyboard/passphrase.rs @@ -6,10 +6,12 @@ use crate::ui::{ geometry::{Grid, Offset, Rect}, model_tt::component::{ button::{Button, ButtonContent, ButtonMsg}, - keyboard::common::{paint_pending_marker, MultiTapKeyboard}, + keyboard::common::{paint_pending_marker, render_pending_marker, MultiTapKeyboard}, swipe::{Swipe, SwipeDirection}, theme, ScrollBar, }, + shape, + shape::Renderer, util::long_line_content_with_ellipsis, }; @@ -296,6 +298,20 @@ impl Component for PassphraseKeyboard { } } + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.input.render(target); + self.scrollbar.render(target); + self.confirm.render(target); + self.back.render(target); + for btn in &self.keys { + btn.render(target); + } + if self.fade.take() { + // Note that this is blocking and takes some time. + display::fade_backlight(theme::BACKLIGHT_NORMAL); + } + } + #[cfg(feature = "ui_bounds")] fn bounds(&self, sink: &mut dyn FnMut(Rect)) { self.input.bounds(sink); @@ -376,6 +392,40 @@ impl Component for Input { } } + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + let style = theme::label_keyboard(); + + let text_baseline = self.area.top_left() + Offset::y(style.text_font.text_height()) + - Offset::y(style.text_font.text_baseline()); + + let text = self.textbox.content(); + + shape::Bar::new(self.area).with_bg(theme::BG).render(target); + + // Find out how much text can fit into the textbox. + // Accounting for the pending marker, which draws itself one pixel longer than + // the last character + let available_area_width = self.area.width() - 1; + let text_to_display = + long_line_content_with_ellipsis(text, "...", style.text_font, available_area_width); + + shape::Text::new(text_baseline, &text_to_display) + .with_font(style.text_font) + .with_fg(style.text_color) + .render(target); + + // Paint the pending marker. + if self.multi_tap.pending_key().is_some() { + render_pending_marker( + target, + text_baseline, + &text_to_display, + style.text_font, + style.text_color, + ); + } + } + #[cfg(feature = "ui_bounds")] fn bounds(&self, sink: &mut dyn FnMut(Rect)) { sink(self.area) diff --git a/core/embed/rust/src/ui/model_tt/component/keyboard/pin.rs b/core/embed/rust/src/ui/model_tt/component/keyboard/pin.rs index d45e4c473..386ff4c98 100644 --- a/core/embed/rust/src/ui/model_tt/component/keyboard/pin.rs +++ b/core/embed/rust/src/ui/model_tt/component/keyboard/pin.rs @@ -11,11 +11,13 @@ use crate::{ }, display::{self, Font}, event::TouchEvent, - geometry::{Alignment2D, Grid, Insets, Offset, Rect}, + geometry::{Alignment, Alignment2D, Grid, Insets, Offset, Rect}, model_tt::component::{ button::{Button, ButtonContent, ButtonMsg, ButtonMsg::Clicked}, theme, }, + shape, + shape::Renderer, }, }; @@ -267,6 +269,26 @@ where } } + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.erase_btn.render(target); + self.textbox_pad.render(target); + if self.textbox.inner().is_empty() { + if let Some(ref w) = self.major_warning { + w.render(target); + } else { + self.major_prompt.render(target); + } + self.minor_prompt.render(target); + self.cancel_btn.render(target); + } else { + self.textbox.render(target); + } + self.confirm_btn.render(target); + for btn in &self.digit_btns { + btn.render(target); + } + } + #[cfg(feature = "ui_bounds")] fn bounds(&self, sink: &mut dyn FnMut(Rect)) { self.major_prompt.bounds(sink); @@ -367,6 +389,27 @@ impl PinDots { } } + fn render_digits<'s>(&self, area: Rect, target: &mut impl Renderer<'s>) { + let center = area.center() + Offset::y(Font::MONO.text_height() / 2); + let right = center + Offset::x(Font::MONO.text_width("0") * (MAX_VISIBLE_DOTS as i16) / 2); + let digits = self.digits.len(); + + if digits <= MAX_VISIBLE_DOTS { + shape::Text::new(center, &self.digits) + .with_align(Alignment::Center) + .with_font(Font::MONO) + .with_fg(self.style.text_color) + .render(target); + } else { + let offset: usize = digits.saturating_sub(MAX_VISIBLE_DIGITS); + shape::Text::new(right, &self.digits[offset..]) + .with_align(Alignment::End) + .with_font(Font::MONO) + .with_fg(self.style.text_color) + .render(target); + } + } + fn paint_dots(&self, area: Rect) { let mut cursor = self.size().snap(area.center(), Alignment2D::CENTER); @@ -410,6 +453,44 @@ impl PinDots { cursor.x += step; } } + + fn render_dots<'s>(&self, area: Rect, target: &mut impl Renderer<'s>) { + let mut cursor = self.size().snap(area.center(), Alignment2D::CENTER); + + let digits = self.digits.len(); + let dots_visible = digits.min(MAX_VISIBLE_DOTS); + let step = Self::DOT + Self::PADDING; + + // Jiggle when overflowed. + if digits > dots_visible && digits % 2 == 0 { + cursor.x += Self::TWITCH + } + + // Small leftmost dot. + if digits > dots_visible + 1 { + shape::ToifImage::new(cursor - Offset::x(2 * step), theme::DOT_SMALL.toif) + .with_align(Alignment2D::TOP_LEFT) + .with_fg(self.style.text_color) + .render(target); + } + + // Greyed out dot. + if digits > dots_visible { + shape::ToifImage::new(cursor - Offset::x(step), theme::DOT_ACTIVE.toif) + .with_align(Alignment2D::TOP_LEFT) + .with_fg(theme::GREY_LIGHT) + .render(target); + } + + // Draw a dot for each PIN digit. + for _ in 0..dots_visible { + shape::ToifImage::new(cursor, theme::DOT_ACTIVE.toif) + .with_align(Alignment2D::TOP_LEFT) + .with_fg(self.style.text_color) + .render(target); + cursor.x += step; + } + } } impl Component for PinDots { @@ -452,6 +533,16 @@ impl Component for PinDots { } } + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + let dot_area = self.area.inset(HEADER_PADDING); + self.pad.render(target); + if self.display_digits { + self.render_digits(dot_area, target) + } else { + self.render_dots(dot_area, target) + } + } + #[cfg(feature = "ui_bounds")] fn bounds(&self, sink: &mut dyn FnMut(Rect)) { sink(self.area); diff --git a/core/embed/rust/src/ui/model_tt/component/keyboard/slip39.rs b/core/embed/rust/src/ui/model_tt/component/keyboard/slip39.rs index 3f927392b..b5be2da33 100644 --- a/core/embed/rust/src/ui/model_tt/component/keyboard/slip39.rs +++ b/core/embed/rust/src/ui/model_tt/component/keyboard/slip39.rs @@ -14,13 +14,15 @@ use crate::{ model_tt::{ component::{ keyboard::{ - common::{paint_pending_marker, MultiTapKeyboard}, + common::{paint_pending_marker, render_pending_marker, MultiTapKeyboard}, mnemonic::{MnemonicInput, MnemonicInputMsg, MNEMONIC_KEY_COUNT}, }, Button, ButtonContent, ButtonMsg, }, theme, }, + shape, + shape::Renderer, util::ResultExt, }, }; @@ -185,6 +187,71 @@ impl Component for Slip39Input { } } + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + let area = self.button.area(); + let style = self.button.style(); + + // First, paint the button background. + self.button.render_background(target, style); + + // Content starts in the left-center point, offset by 16px to the right and 8px + // to the bottom. + let text_baseline = area.top_left().center(area.bottom_left()) + Offset::new(16, 8); + + // To simplify things, we always copy the printed string here, even if it + // wouldn't be strictly necessary. + let mut text: String = String::new(); + + if let Some(word) = self.final_word { + // We're done with input, paint the full word. + text.push_str(word) + .assert_if_debugging_ui("Text buffer is too small"); + } else { + // Paint an asterisk for each letter of input. + for ch in iter::repeat('*').take(self.textbox.content().len()) { + text.push(ch) + .assert_if_debugging_ui("Text buffer is too small"); + } + // If we're in the pending state, paint the pending character at the end. + if let (Some(key), Some(press)) = + (self.multi_tap.pending_key(), self.multi_tap.pending_press()) + { + assert!(!Self::keys()[key].is_empty()); + // Now we can be sure that the looped iterator will return a value. + let ch = unwrap!(Self::keys()[key].chars().cycle().nth(press)); + text.pop(); + text.push(ch) + .assert_if_debugging_ui("Text buffer is too small"); + } + } + shape::Text::new(text_baseline, text.as_str()) + .with_font(style.font) + .with_fg(style.text_color) + .render(target); + + // Paint the pending marker. + if self.multi_tap.pending_key().is_some() && self.final_word.is_none() { + render_pending_marker( + target, + text_baseline, + text.as_str(), + style.font, + style.text_color, + ); + } + + // Paint the icon. + if let ButtonContent::Icon(icon) = self.button.content() { + // Icon is painted in the right-center point, of expected size 16x16 pixels, and + // 16px from the right edge. + let icon_center = area.top_right().center(area.bottom_right()) - Offset::new(16 + 8, 0); + shape::ToifImage::new(icon_center, icon.toif) + .with_align(Alignment2D::CENTER) + .with_fg(style.text_color) + .render(target); + } + } + #[cfg(feature = "ui_bounds")] fn bounds(&self, sink: &mut dyn FnMut(Rect)) { self.button.bounds(sink); diff --git a/core/embed/rust/src/ui/model_tt/component/keyboard/word_count.rs b/core/embed/rust/src/ui/model_tt/component/keyboard/word_count.rs index 8202a03fa..98ec6d8e4 100644 --- a/core/embed/rust/src/ui/model_tt/component/keyboard/word_count.rs +++ b/core/embed/rust/src/ui/model_tt/component/keyboard/word_count.rs @@ -5,6 +5,7 @@ use crate::ui::{ component::button::{Button, ButtonMsg}, theme, }, + shape::Renderer, }; const NUMBERS: [u32; 5] = [12, 18, 20, 24, 33]; @@ -57,6 +58,12 @@ impl Component for SelectWordCount { } } + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + for btn in self.button.iter() { + btn.render(target) + } + } + #[cfg(feature = "ui_bounds")] fn bounds(&self, sink: &mut dyn FnMut(Rect)) { for btn in self.button.iter() { diff --git a/core/embed/rust/src/ui/model_tt/component/loader.rs b/core/embed/rust/src/ui/model_tt/component/loader.rs index ebac3b3aa..175a77d6b 100644 --- a/core/embed/rust/src/ui/model_tt/component/loader.rs +++ b/core/embed/rust/src/ui/model_tt/component/loader.rs @@ -4,10 +4,12 @@ use crate::{ time::{Duration, Instant}, ui::{ animation::Animation, + canvas::algo::PI4, component::{Component, Event, EventCtx, Pad}, display::{self, toif::Icon, Color}, - geometry::{Offset, Rect}, + geometry::{Alignment2D, Offset, Rect}, model_tt::constant, + shape::{self, Renderer}, util::animation_disabled, }, }; @@ -206,6 +208,53 @@ impl Component for Loader { ); } } + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + // TODO: Consider passing the current instant along with the event -- that way, + // we could synchronize painting across the component tree. Also could be useful + // in automated tests. + // In practice, taking the current instant here is more precise in case some + // other component in the tree takes a long time to draw. + let now = Instant::now(); + + if let Some(progress) = self.progress(now) { + let style = if progress < display::LOADER_MAX { + self.styles.normal + } else { + self.styles.active + }; + + self.pad.render(target); + + let center = self.pad.area.center(); + + let inactive_color = Color::black().blend(style.loader_color, 85); + + shape::Circle::new(center, constant::LOADER_OUTER) + .with_bg(inactive_color) + .render(target); + + shape::Circle::new(center, constant::LOADER_OUTER) + .with_bg(style.loader_color) + .with_end_angle(((progress as i32 * PI4 as i32 * 8) / 1000) as i16) + .render(target); + + shape::Circle::new(center, constant::LOADER_INNER + 2) + .with_bg(style.loader_color) + .render(target); + + shape::Circle::new(center, constant::LOADER_INNER) + .with_bg(style.background_color) + .render(target); + + if let Some((icon, color)) = style.icon { + shape::ToifImage::new(center, icon.toif) + .with_align(Alignment2D::CENTER) + .with_fg(color) + .render(target); + } + } + } } pub struct LoaderStyleSheet { diff --git a/core/embed/rust/src/ui/model_tt/component/number_input.rs b/core/embed/rust/src/ui/model_tt/component/number_input.rs index e5745870a..43aab67cb 100644 --- a/core/embed/rust/src/ui/model_tt/component/number_input.rs +++ b/core/embed/rust/src/ui/model_tt/component/number_input.rs @@ -11,7 +11,8 @@ use crate::{ Child, Component, Event, EventCtx, Pad, }, display::{self, Font}, - geometry::{Grid, Insets, Offset, Rect}, + geometry::{Alignment, Grid, Insets, Offset, Rect}, + shape::{self, Renderer}, }, }; @@ -123,6 +124,14 @@ where self.confirm_button.paint(); } + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.input.render(target); + self.paragraphs_pad.render(target); + self.paragraphs.render(target); + self.info_button.render(target); + self.confirm_button.render(target); + } + #[cfg(feature = "ui_bounds")] fn bounds(&self, sink: &mut dyn FnMut(Rect)) { sink(self.area); @@ -231,6 +240,25 @@ impl Component for NumberInput { self.inc.paint(); } + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + let mut buf = [0u8; 10]; + + if let Some(text) = strutil::format_i64(self.value as i64, &mut buf) { + let digit_font = Font::DEMIBOLD; + let y_offset = digit_font.text_height() / 2 + Button::<&str>::BASELINE_OFFSET; + + shape::Bar::new(self.area).with_bg(theme::BG).render(target); + shape::Text::new(self.area.center() + Offset::y(y_offset), text) + .with_align(Alignment::Center) + .with_fg(theme::FG) + .with_font(digit_font) + .render(target); + } + + self.dec.render(target); + self.inc.render(target); + } + #[cfg(feature = "ui_bounds")] fn bounds(&self, sink: &mut dyn FnMut(Rect)) { self.dec.bounds(sink); diff --git a/core/embed/rust/src/ui/model_tt/component/page.rs b/core/embed/rust/src/ui/model_tt/component/page.rs index 5b874d6c9..371ca11e6 100644 --- a/core/embed/rust/src/ui/model_tt/component/page.rs +++ b/core/embed/rust/src/ui/model_tt/component/page.rs @@ -8,6 +8,7 @@ use crate::{ constant, display::{self, Color}, geometry::{Insets, Rect}, + shape::Renderer, util::animation_disabled, }, }; @@ -17,6 +18,8 @@ use super::{ SwipeDirection, }; +use core::cell::Cell; + /// Allows pagination of inner component. Shows scroll bar, confirm & cancel /// buttons. Optionally handles hold-to-confirm with loader. pub struct ButtonPage { @@ -40,7 +43,7 @@ pub struct ButtonPage { /// Whether to pass-through right swipe to parent component. swipe_right: bool, /// Fade to given backlight level on next paint(). - fade: Option, + fade: Cell>, } impl ButtonPage @@ -76,7 +79,7 @@ where cancel_from_any_page: false, swipe_left: false, swipe_right: false, - fade: None, + fade: Cell::new(None), } } @@ -156,7 +159,7 @@ where // Swipe has dimmed the screen, so fade back to normal backlight after the next // paint. - self.fade = Some(theme::BACKLIGHT_NORMAL); + self.fade.set(Some(theme::BACKLIGHT_NORMAL)); } fn is_cancel_visible(&self) -> bool { @@ -412,6 +415,33 @@ where } } + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.pad.render(target); + match &self.loader { + Some(l) if l.is_animating() => self.loader.render(target), + _ => { + self.content.render(target); + if self.scrollbar.has_pages() { + self.scrollbar.render(target); + } + } + } + if self.button_cancel.is_some() && self.is_cancel_visible() { + self.button_cancel.render(target); + } else { + self.button_prev.render(target); + } + if self.scrollbar.has_next_page() { + self.button_next.render(target); + } else { + self.button_confirm.render(target); + } + if let Some(val) = self.fade.take() { + // Note that this is blocking and takes some time. + display::fade_backlight(val); + } + } + #[cfg(feature = "ui_bounds")] fn bounds(&self, sink: &mut dyn FnMut(Rect)) { sink(self.pad.area); diff --git a/core/embed/rust/src/ui/model_tt/component/progress.rs b/core/embed/rust/src/ui/model_tt/component/progress.rs index c6083ff75..13da2894e 100644 --- a/core/embed/rust/src/ui/model_tt/component/progress.rs +++ b/core/embed/rust/src/ui/model_tt/component/progress.rs @@ -4,6 +4,7 @@ use crate::{ error::Error, strutil::StringType, ui::{ + canvas::algo::PI4, component::{ base::ComponentExt, paginated::Paginate, @@ -11,8 +12,10 @@ use crate::{ Child, Component, Event, EventCtx, Label, Never, Pad, }, display::{self, Font}, - geometry::{Insets, Rect}, + geometry::{Insets, Offset, Rect}, model_tt::constant, + shape, + shape::Renderer, util::animation_disabled, }, }; @@ -120,6 +123,47 @@ where self.description.paint(); } + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.title.render(target); + + let center = constant::screen().center() + Offset::y(self.loader_y_offset); + let active_color = theme::FG; + let background_color = theme::BG; + let inactive_color = background_color.blend(active_color, 85); + + let (start, end) = if self.indeterminate { + let start = (self.value as i16 - 100) % 1000; + let end = (self.value as i16 + 100) % 1000; + let start = ((start as i32 * 8 * PI4 as i32) / 1000) as i16; + let end = ((end as i32 * 8 * PI4 as i32) / 1000) as i16; + (start, end) + } else { + let end = ((self.value as i32 * 8 * PI4 as i32) / 1000) as i16; + (0, end) + }; + + shape::Circle::new(center, constant::LOADER_OUTER) + .with_bg(inactive_color) + .render(target); + + shape::Circle::new(center, constant::LOADER_OUTER) + .with_bg(active_color) + .with_start_angle(start) + .with_end_angle(end) + .render(target); + + shape::Circle::new(center, constant::LOADER_INNER + 2) + .with_bg(active_color) + .render(target); + + shape::Circle::new(center, constant::LOADER_INNER) + .with_bg(background_color) + .render(target); + + self.description_pad.render(target); + self.description.render(target); + } + #[cfg(feature = "ui_bounds")] fn bounds(&self, sink: &mut dyn FnMut(Rect)) { sink(Self::AREA); diff --git a/core/embed/rust/src/ui/model_tt/component/result.rs b/core/embed/rust/src/ui/model_tt/component/result.rs index 26bffb84f..58d3b70af 100644 --- a/core/embed/rust/src/ui/model_tt/component/result.rs +++ b/core/embed/rust/src/ui/model_tt/component/result.rs @@ -6,6 +6,8 @@ use crate::{ display::{self, Color, Font, Icon}, geometry::{Alignment2D, Insets, Offset, Point, Rect}, model_tt::theme::FG, + shape, + shape::Renderer, }, }; @@ -90,6 +92,20 @@ impl> Component for ResultFooter<'_, T> { self.text.paint(); } + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + // divider line + let bar = Rect::from_center_and_size( + Point::new(self.area.center().x, self.area.y0), + Offset::new(self.area.width(), 1), + ); + shape::Bar::new(bar) + .with_fg(self.style.divider_color) + .render(target); + + // footer text + self.text.render(target); + } + fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option { None } @@ -164,4 +180,21 @@ impl<'a, T: StringType> Component for ResultScreen<'a, T> { self.message.paint(); self.footer.paint(); } + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.bg.render(target); + self.footer_pad.render(target); + + shape::ToifImage::new( + Point::new(screen().center().x, ICON_CENTER_Y), + self.icon.toif, + ) + .with_align(Alignment2D::CENTER) + .with_fg(self.style.fg_color) + .with_bg(self.style.bg_color) + .render(target); + + self.message.render(target); + self.footer.render(target); + } } diff --git a/core/embed/rust/src/ui/model_tt/component/scroll.rs b/core/embed/rust/src/ui/model_tt/component/scroll.rs index b6267fcbf..1ea96fd96 100644 --- a/core/embed/rust/src/ui/model_tt/component/scroll.rs +++ b/core/embed/rust/src/ui/model_tt/component/scroll.rs @@ -2,6 +2,8 @@ use crate::ui::{ component::{Component, Event, EventCtx, Never}, display::toif::Icon, geometry::{Alignment2D, Axis, LinearPlacement, Offset, Rect}, + shape, + shape::Renderer, }; use super::theme; @@ -122,6 +124,49 @@ impl Component for ScrollBar { } } + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + fn dotsize(distance: usize, nhidden: usize) -> Icon { + match (nhidden.saturating_sub(distance)).min(2 - distance) { + 0 => theme::DOT_INACTIVE, + 1 => theme::DOT_INACTIVE_HALF, + _ => theme::DOT_INACTIVE_QUARTER, + } + } + + // Number of visible dots. + let num_shown = self.page_count.min(Self::MAX_DOTS); + // Page indices corresponding to the first (and last) dot. + let first_shown = self + .active_page + .saturating_sub(Self::MAX_DOTS / 2) + .min(self.page_count.saturating_sub(Self::MAX_DOTS)); + let last_shown = first_shown + num_shown - 1; + + let mut cursor = self.area.center() + - Offset::on_axis( + self.layout.axis, + Self::DOT_INTERVAL * (num_shown.saturating_sub(1) as i16) / 2, + ); + for i in first_shown..(last_shown + 1) { + let icon = if i == self.active_page { + theme::DOT_ACTIVE + } else if i <= first_shown + 1 { + let before_first_shown = first_shown; + dotsize(i - first_shown, before_first_shown) + } else if i >= last_shown - 1 { + let after_last_shown = self.page_count - 1 - last_shown; + dotsize(last_shown - i, after_last_shown) + } else { + theme::DOT_INACTIVE + }; + shape::ToifImage::new(cursor, icon.toif) + .with_align(Alignment2D::CENTER) + .with_fg(theme::FG) + .render(target); + cursor = cursor + Offset::on_axis(self.layout.axis, Self::DOT_INTERVAL); + } + } + fn place(&mut self, bounds: Rect) -> Rect { self.area = bounds; bounds diff --git a/core/embed/rust/src/ui/model_tt/component/simple_page.rs b/core/embed/rust/src/ui/model_tt/component/simple_page.rs index 92bb4c53a..160c049c5 100644 --- a/core/embed/rust/src/ui/model_tt/component/simple_page.rs +++ b/core/embed/rust/src/ui/model_tt/component/simple_page.rs @@ -2,6 +2,7 @@ use crate::ui::{ component::{base::ComponentExt, Component, Event, EventCtx, Pad, PageMsg, Paginate}, display::{self, Color}, geometry::{Axis, Insets, Rect}, + shape::Renderer, }; use super::{theme, ScrollBar, Swipe, SwipeDirection}; @@ -165,6 +166,18 @@ where } } + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.pad.render(target); + self.content.render(target); + if self.scrollbar.has_pages() { + self.scrollbar.render(target); + } + if let Some(val) = self.fade.take() { + // Note that this is blocking and takes some time. + display::fade_backlight(val); + } + } + #[cfg(feature = "ui_bounds")] fn bounds(&self, sink: &mut dyn FnMut(Rect)) { sink(self.pad.area); diff --git a/core/embed/rust/src/ui/model_tt/component/swipe.rs b/core/embed/rust/src/ui/model_tt/component/swipe.rs index b43a2fd22..b4cab1d2e 100644 --- a/core/embed/rust/src/ui/model_tt/component/swipe.rs +++ b/core/embed/rust/src/ui/model_tt/component/swipe.rs @@ -3,6 +3,7 @@ use crate::ui::{ display, event::TouchEvent, geometry::{Point, Rect}, + shape::Renderer, }; use super::theme; @@ -159,4 +160,6 @@ impl Component for Swipe { } fn paint(&mut self) {} + + fn render<'s>(&'s self, _target: &mut impl Renderer<'s>) {} } diff --git a/core/embed/rust/src/ui/model_tt/component/welcome_screen.rs b/core/embed/rust/src/ui/model_tt/component/welcome_screen.rs index b0526f8aa..a3978b2e5 100644 --- a/core/embed/rust/src/ui/model_tt/component/welcome_screen.rs +++ b/core/embed/rust/src/ui/model_tt/component/welcome_screen.rs @@ -2,9 +2,11 @@ use crate::ui::{ component::{Component, Event, EventCtx, Never}, geometry::{Alignment2D, Offset, Rect}, model_tt::theme, + shape, + shape::Renderer, }; #[cfg(feature = "bootloader")] -use crate::ui::{display::Icon, model_tt::theme::bootloader::DEVICE_NAME}; +use crate::ui::{display::{Icon, toif::Toif}, model_tt::theme::bootloader::DEVICE_NAME}; const TEXT_BOTTOM_MARGIN: i16 = 24; // matching the homescreen label margin const ICON_TOP_MARGIN: i16 = 48; @@ -67,6 +69,40 @@ impl Component for WelcomeScreen { theme::BG, ); } + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + let logo = if self.empty_lock { + theme::ICON_LOGO_EMPTY + } else { + theme::ICON_LOGO + }; + shape::ToifImage::new( + self.area.top_center() + Offset::y(ICON_TOP_MARGIN), + logo.toif, + ) + .with_align(Alignment2D::TOP_CENTER) + .with_fg(theme::FG) + .with_bg(theme::BG) + .render(target); + + #[cfg(not(feature = "bootloader"))] + shape::Text::new( + self.area.bottom_center() - Offset::y(TEXT_BOTTOM_MARGIN), + model::FULL_NAME, + ) + .with_font(MODEL_NAME_FONT) + .with_fg(theme::FG) + .render(target); + + #[cfg(feature = "bootloader")] + shape::ToifImage::new( + self.area.bottom_center() - Offset::y(TEXT_BOTTOM_MARGIN), + unwrap!(Toif::new(DEVICE_NAME)), + ) + .with_align(Alignment2D::BOTTOM_CENTER) + .with_fg(theme::FG) + .render(target); + } } #[cfg(feature = "ui_debug")] diff --git a/core/embed/rust/src/ui/model_tt/constant.rs b/core/embed/rust/src/ui/model_tt/constant.rs index 3d4f7005b..914a6ac42 100644 --- a/core/embed/rust/src/ui/model_tt/constant.rs +++ b/core/embed/rust/src/ui/model_tt/constant.rs @@ -7,8 +7,13 @@ pub const HEIGHT: i16 = DISPLAY_RESY as _; pub const LINE_SPACE: i16 = 4; pub const FONT_BPP: i16 = 4; +#[cfg(not(feature = "new_rendering"))] pub const LOADER_OUTER: i16 = 60; +#[cfg(feature = "new_rendering")] +pub const LOADER_OUTER: i16 = 59; + pub const LOADER_INNER: i16 = 42; + pub const LOADER_ICON_MAX_SIZE: i16 = 64; pub const fn size() -> Offset { diff --git a/core/embed/rust/src/ui/model_tt/screens.rs b/core/embed/rust/src/ui/model_tt/screens.rs index 5f25e00c1..880e45812 100644 --- a/core/embed/rust/src/ui/model_tt/screens.rs +++ b/core/embed/rust/src/ui/model_tt/screens.rs @@ -3,13 +3,18 @@ use crate::micropython::buffer::StrBuffer; use crate::ui::{ component::Component, constant::screen, - display, model_tt::{ component::{ErrorScreen, WelcomeScreen}, constant, }, }; +#[cfg(not(feature = "new_rendering"))] +use crate::ui::display; +#[cfg(feature = "new_rendering")] +use crate::ui::{display::Color, shape::render_on_display}; + + #[cfg(not(feature = "micropython"))] // SAFETY: Actually safe but see below unsafe fn get_str(text: &str) -> &str { @@ -31,6 +36,13 @@ pub fn screen_fatal_error(title: &str, msg: &str, footer: &str) { let mut frame = ErrorScreen::new(title, msg, footer); frame.place(constant::screen()); + + #[cfg(feature = "new_rendering")] + render_on_display(None, Some(Color::black()), |target| { + frame.render(target); + }); + + #[cfg(not(feature = "new_rendering"))] frame.paint(); } @@ -38,7 +50,16 @@ pub fn screen_fatal_error(title: &str, msg: &str, footer: &str) { extern "C" fn screen_boot_full() { let mut frame = WelcomeScreen::new(false); frame.place(screen()); - display::sync(); - frame.paint(); - display::refresh(); + + #[cfg(feature = "new_rendering")] + render_on_display(None, Some(Color::black()), |target| { + frame.render(target); + }); + + #[cfg(not(feature = "new_rendering"))] + { + display::sync(); + frame.paint(); + display::refresh(); + } }