From 04b61ea6afca0ed3787c4678a0181c28702c45ac Mon Sep 17 00:00:00 2001 From: obrusvit Date: Sun, 4 May 2025 18:43:57 +0200 Subject: [PATCH] feat(eckhart): render_loader and ProgressScreen - `render_loader` shows classic circular loader going around the display, implemented by rendering `ScreenBorder` and hiding it by 5 black rectangles which progressively diminish - `render_loader_indeterminate` shows progress loader without known duration, implemented by rendering `ScreenBorder` in clip which travels around the SCREEN in an octagonal shape. This achieves an effect of a constant length line going around the screen shape - implement `show_progress` and `show_progress_coinjoin` FirmwareUI functions chore(eckhart): remove animation code from Header --- .../ui/layout_eckhart/component_msg_obj.rs | 12 +- .../src/ui/layout_eckhart/cshape/loader.rs | 245 ++++++++++++------ .../rust/src/ui/layout_eckhart/cshape/mod.rs | 2 +- .../src/ui/layout_eckhart/firmware/header.rs | 92 +------ .../src/ui/layout_eckhart/firmware/mod.rs | 2 + .../firmware/progress_screen.rs | 176 +++++++++++++ .../rust/src/ui/layout_eckhart/ui_firmware.rs | 44 ++-- 7 files changed, 390 insertions(+), 183 deletions(-) create mode 100644 core/embed/rust/src/ui/layout_eckhart/firmware/progress_screen.rs diff --git a/core/embed/rust/src/ui/layout_eckhart/component_msg_obj.rs b/core/embed/rust/src/ui/layout_eckhart/component_msg_obj.rs index 5c0f44cbd1..bf6bd8e98d 100644 --- a/core/embed/rust/src/ui/layout_eckhart/component_msg_obj.rs +++ b/core/embed/rust/src/ui/layout_eckhart/component_msg_obj.rs @@ -16,9 +16,9 @@ use crate::{ use super::firmware::{ AllowedTextContent, ConfirmHomescreen, ConfirmHomescreenMsg, DeviceMenuMsg, DeviceMenuScreen, Homescreen, HomescreenMsg, MnemonicInput, MnemonicKeyboard, MnemonicKeyboardMsg, - NumberInputScreen, NumberInputScreenMsg, PinKeyboard, PinKeyboardMsg, SelectWordCountMsg, - SelectWordCountScreen, SelectWordMsg, SelectWordScreen, SetBrightnessScreen, TextScreen, - TextScreenMsg, + NumberInputScreen, NumberInputScreenMsg, PinKeyboard, PinKeyboardMsg, ProgressScreen, + SelectWordCountMsg, SelectWordCountScreen, SelectWordMsg, SelectWordScreen, + SetBrightnessScreen, TextScreen, TextScreenMsg, }; impl ComponentMsgObj for PinKeyboard<'_> { @@ -80,6 +80,12 @@ impl ComponentMsgObj for Homescreen { } } +impl ComponentMsgObj for ProgressScreen { + fn msg_try_into_obj(&self, _msg: Self::Msg) -> Result { + unreachable!() + } +} + impl ComponentMsgObj for TextScreen where T: AllowedTextContent, diff --git a/core/embed/rust/src/ui/layout_eckhart/cshape/loader.rs b/core/embed/rust/src/ui/layout_eckhart/cshape/loader.rs index 37dffd024d..87c86775fe 100644 --- a/core/embed/rust/src/ui/layout_eckhart/cshape/loader.rs +++ b/core/embed/rust/src/ui/layout_eckhart/cshape/loader.rs @@ -1,96 +1,179 @@ use crate::ui::{ constant::SCREEN, - geometry::{Offset, Rect}, + geometry::{Alignment2D, Offset, Point, Rect}, lerp::Lerp, shape::{self, Renderer}, }; -use super::{super::theme::BG, ScreenBorder}; +use super::{super::theme, ScreenBorder}; -pub fn render_loader<'s>( - progress: u16, - border: &'static ScreenBorder, - target: &mut impl Renderer<'s>, -) { - // convert to ration from 0.0 to 1.0 - let progress_ratio = (progress as f32 / 1000.0).clamp(0.0, 1.0); - let (clip, top_gap) = get_clips(progress_ratio); - render_clipped_border(border, clip, top_gap, u8::MAX, target); -} - -fn get_clips(progress_ratio: f32) -> (Rect, Rect) { - /// Ratio of total_duration for the bottom part of the border - const BOTTOM_DURATION_RATIO: f32 = 0.125; - /// Ratio of total_duration for the side parts of the border - const SIDES_DURATION_RATIO: f32 = 0.5; - /// Ratio of total_duration for the top part of the border - const TOP_DURATION_RATIO: f32 = 0.375; - - const TOP_GAP_ZERO: Rect = Rect::from_center_and_size( - SCREEN.top_center().ofs(Offset::y(ScreenBorder::WIDTH / 2)), - Offset::zero(), - ); - const TOP_GAP_FULL: Rect = Rect::from_center_and_size( - SCREEN.top_center().ofs(Offset::y(ScreenBorder::WIDTH / 2)), - Offset::new(SCREEN.width(), ScreenBorder::WIDTH), - ); - - match progress_ratio { - // Bottom phase growing linearly - p if p < BOTTOM_DURATION_RATIO => { - let bottom_progress = (p / BOTTOM_DURATION_RATIO).clamp(0.0, 1.0); - let width = i16::lerp(0, SCREEN.width(), bottom_progress); - let clip = Rect::from_center_and_size( - SCREEN - .bottom_center() - .ofs(Offset::y(-ScreenBorder::WIDTH / 2)), - Offset::new(width, ScreenBorder::WIDTH), - ); - (clip, TOP_GAP_FULL) - } - - // Sides phase growing up linearly - p if p < (BOTTOM_DURATION_RATIO + SIDES_DURATION_RATIO) => { - let sides_progress = - ((p - BOTTOM_DURATION_RATIO) / SIDES_DURATION_RATIO).clamp(0.0, 1.0); - let height = i16::lerp(ScreenBorder::WIDTH, SCREEN.height(), sides_progress); - let clip = Rect::from_bottom_left_and_size( - SCREEN.bottom_left(), - Offset::new(SCREEN.width(), height), - ); - (clip, TOP_GAP_FULL) - } - - // Top gap shrinking linearly - p if p < 1.0 => { - let top_progress = ((p - BOTTOM_DURATION_RATIO - SIDES_DURATION_RATIO) - / TOP_DURATION_RATIO) - .clamp(0.0, 1.0); - let width = i16::lerp(SCREEN.width(), 0, top_progress); - let top_gap = Rect::from_center_and_size( - SCREEN.top_center().ofs(Offset::y(ScreenBorder::WIDTH / 2)), - Offset::new(width, ScreenBorder::WIDTH), - ); - (SCREEN, top_gap) - } - - // Animation complete - _ => (SCREEN, TOP_GAP_ZERO), +/// Renders the loader. Higher `progress` reveals the `border` from the top in +/// clock-wise direction. Used in ProgressScreen and Bootloader. `progress` goes +/// from 0 to 1000. +pub fn render_loader<'s>(progress: u16, border: &'s ScreenBorder, target: &mut impl Renderer<'s>) { + let progress_ratio = progress_to_ratio(progress); + // Draw the border first + border.render(u8::MAX, target); + // Draw the progressively shrinking covers + for cover in get_progress_covers(progress_ratio) { + shape::Bar::new(cover) + .with_bg(theme::BLACK) + .with_alpha(u8::MAX) + .render(target); } } -fn render_clipped_border<'s>( - border: &'static ScreenBorder, - clip: Rect, - top_gap: Rect, - alpha: u8, +/// Render the loader in indeterminate mode. A constant size portion of the +/// border is rendered at each time moving around in clock-wise direction. +pub fn render_loader_indeterminate<'s>( + progress: u16, + border: &'s ScreenBorder, target: &mut impl Renderer<'s>, ) { + let progress_ratio = progress_to_ratio(progress); + let clip = get_clip_indeterminate(progress_ratio); + // Draw the border in clip target.in_clip(clip, &|target| { - border.render(alpha, target); + border.render(u8::MAX, target); }); - shape::Bar::new(top_gap) - .with_bg(BG) - .with_fg(BG) - .render(target); +} + +fn progress_to_ratio(progress: u16) -> f32 { + // convert to ratio from 0.0 to 1.0 + (progress as f32 / 1000.0).clamp(0.0, 1.0) +} + +fn get_clip_indeterminate(progress_ratio: f32) -> Rect { + const CLIP_SIZE: i16 = 190; + + // Define 8 points (+1 duplicate) for an octagonal path around the display + // Position them to ensure the clip always shows `CLIP_SIZE`-wide part of the + // border "Right end" and "Left start" points are shifted in y-axis a little + // bit upwards to account for the irregular corner shape + const PATH_POINTS: [Point; 9] = [ + // Top start + Point::new(CLIP_SIZE / 2, -CLIP_SIZE / 2 + ScreenBorder::WIDTH), + // Top end + Point::new( + SCREEN.width() - CLIP_SIZE / 2, + -CLIP_SIZE / 2 + ScreenBorder::WIDTH, + ), + // Right start + Point::new( + SCREEN.width() + CLIP_SIZE / 2 - ScreenBorder::WIDTH, + CLIP_SIZE / 2, + ), + // Right end + Point::new( + SCREEN.width() + CLIP_SIZE / 2 - ScreenBorder::WIDTH, + SCREEN.height() - CLIP_SIZE / 2 - 60, + ), + // Bottom start + Point::new( + SCREEN.width() - CLIP_SIZE / 2 - ScreenBorder::WIDTH, + SCREEN.height() + CLIP_SIZE / 2 - ScreenBorder::WIDTH, + ), + // Bottom end + Point::new( + CLIP_SIZE / 2, + SCREEN.height() + CLIP_SIZE / 2 - ScreenBorder::WIDTH, + ), + // Left start + Point::new( + -CLIP_SIZE / 2 + ScreenBorder::WIDTH, + SCREEN.height() - CLIP_SIZE / 2 - 60, + ), + // Left end + Point::new(-CLIP_SIZE / 2 + ScreenBorder::WIDTH, CLIP_SIZE / 2), + // Top start - duplicate to close the loop + Point::new(CLIP_SIZE / 2, -CLIP_SIZE / 2 + ScreenBorder::WIDTH), + ]; + + // Calculate which segment we're in and how far along that segment + let path_length = PATH_POINTS.len() - 1; // -1 because the last point is a duplicate + let segment_position = progress_ratio * path_length as f32; + + // Integer part gives us the segment + let segment = segment_position as usize % path_length; + + // Fractional part gives us the position within the segment + let segment_ratio = segment_position - segment as f32; + + // Get the current point and the next point + let current = PATH_POINTS[segment]; + let next = PATH_POINTS[segment + 1]; + + // Linearly interpolate between the current and next points + let center = Point::lerp(current, next, segment_ratio); + + Rect::snap(center, Offset::uniform(CLIP_SIZE), Alignment2D::CENTER) +} + +fn get_progress_covers(progress_ratio: f32) -> impl Iterator { + let cover_1 = { + // Top-center to top-right + const PROGRESS_PORTION: f32 = 0.11; + const PROGRESS_START: f32 = 0.0; + const FULL_WIDTH: i16 = 190; + let progress = ((progress_ratio - PROGRESS_START) / PROGRESS_PORTION).clamp(0.0, 1.0); + let width = ((1.0 - progress) * FULL_WIDTH as f32) as i16; + Rect::snap( + SCREEN.top_right(), + Offset::new(width, theme::ICON_BORDER_TR.toif.height()), + Alignment2D::TOP_RIGHT, + ) + }; + let cover_2 = { + // Top-right to bottom-right + const PROGRESS_PORTION: f32 = 0.3; + const PROGRESS_START: f32 = 0.11; + const FULL_HEIGHT: i16 = 502; + let progress = ((progress_ratio - PROGRESS_START) / PROGRESS_PORTION).clamp(0.0, 1.0); + let height = ((1.0 - progress) * FULL_HEIGHT as f32) as i16; + Rect::snap( + SCREEN.bottom_right(), + Offset::new(theme::ICON_BORDER_BR.toif.width(), height), + Alignment2D::BOTTOM_RIGHT, + ) + }; + let cover_3 = { + // Bottom-right to bottom-left + const PROGRESS_PORTION: f32 = 0.18; + const PROGRESS_START: f32 = 0.41; + const FULL_WIDTH: i16 = 298; + let progress = ((progress_ratio - PROGRESS_START) / PROGRESS_PORTION).clamp(0.0, 1.0); + let width = ((1.0 - progress) * FULL_WIDTH as f32) as i16; + Rect::snap( + SCREEN.bottom_left() + Offset::x(theme::ICON_BORDER_BL.toif.width()), + Offset::new(width, ScreenBorder::WIDTH), + Alignment2D::BOTTOM_LEFT, + ) + }; + let cover_4 = { + // Bottom-left to top-left + const PROGRESS_PORTION: f32 = 0.3; + const PROGRESS_START: f32 = 0.59; + const FULL_HEIGHT: i16 = 502; + let progress = ((progress_ratio - PROGRESS_START) / PROGRESS_PORTION).clamp(0.0, 1.0); + let height = ((1.0 - progress) * FULL_HEIGHT as f32) as i16; + Rect::snap( + SCREEN.top_left() + Offset::y(theme::ICON_BORDER_TL.toif.height()), + Offset::new(theme::ICON_BORDER_BL.toif.width(), height), + Alignment2D::TOP_LEFT, + ) + }; + let cover_5 = { + // Top-left to top-center + const PROGRESS_PORTION: f32 = 0.11; + const PROGRESS_START: f32 = 0.89; + const FULL_WIDTH: i16 = 190; + let progress = ((progress_ratio - PROGRESS_START) / PROGRESS_PORTION).clamp(0.0, 1.0); + let width = ((1.0 - progress) * FULL_WIDTH as f32) as i16; + Rect::snap( + SCREEN.top_center(), + Offset::new(width, theme::ICON_BORDER_TL.toif.height()), + Alignment2D::TOP_RIGHT, + ) + }; + [cover_1, cover_2, cover_3, cover_4, cover_5].into_iter() } diff --git a/core/embed/rust/src/ui/layout_eckhart/cshape/mod.rs b/core/embed/rust/src/ui/layout_eckhart/cshape/mod.rs index b07738658d..e9a2772794 100644 --- a/core/embed/rust/src/ui/layout_eckhart/cshape/mod.rs +++ b/core/embed/rust/src/ui/layout_eckhart/cshape/mod.rs @@ -1,5 +1,5 @@ mod loader; mod screen_border; -pub use loader::render_loader; +pub use loader::{render_loader, render_loader_indeterminate}; pub use screen_border::ScreenBorder; diff --git a/core/embed/rust/src/ui/layout_eckhart/firmware/header.rs b/core/embed/rust/src/ui/layout_eckhart/firmware/header.rs index 4b15006341..fecbefe7f3 100644 --- a/core/embed/rust/src/ui/layout_eckhart/firmware/header.rs +++ b/core/embed/rust/src/ui/layout_eckhart/firmware/header.rs @@ -1,13 +1,10 @@ use crate::{ strutil::TString, - time::{Duration, Stopwatch}, ui::{ component::{text::TextStyle, Component, Event, EventCtx, Label}, display::{Color, Icon}, - geometry::{Alignment2D, Insets, Offset, Rect}, - lerp::Lerp, + geometry::{Alignment2D, Insets, Rect}, shape::{self, Renderer}, - util::animation_disabled, }, }; @@ -16,49 +13,6 @@ use super::{ constant, theme, }; -const ANIMATION_TIME_MS: u32 = 1000; - -#[derive(Default, Clone)] -struct AttachAnimation { - pub timer: Stopwatch, -} - -impl AttachAnimation { - pub fn is_active(&self) -> bool { - if animation_disabled() { - return false; - } - - self.timer - .is_running_within(Duration::from_millis(ANIMATION_TIME_MS)) - } - - pub fn eval(&self) -> f32 { - if animation_disabled() { - return ANIMATION_TIME_MS as f32 / 1000.0; - } - self.timer.elapsed().to_millis() as f32 / 1000.0 - } - - pub fn get_title_offset(&self, t: f32) -> i16 { - let fnc = pareen::constant(0.0).seq_ease_in_out( - 0.8, - easer::functions::Cubic, - 0.2, - pareen::constant(1.0), - ); - i16::lerp(0, 25, fnc.eval(t)) - } - - pub fn start(&mut self) { - self.timer.start(); - } - - pub fn reset(&mut self) { - self.timer = Stopwatch::new_stopped(); - } -} - const BUTTON_EXPAND_BORDER: i16 = 32; /// Component for the header of a screen. Eckhart UI shows the title (can be two @@ -77,8 +31,6 @@ pub struct Header { /// icon in the top-left corner (used instead of left button) icon: Option, icon_color: Option, - /// animation - anim: Option, } #[derive(Copy, Clone)] @@ -104,7 +56,6 @@ impl Header { left_button_msg: HeaderMsg::Cancelled, icon: None, icon_color: None, - anim: None, } } @@ -216,20 +167,6 @@ impl Component for Header { fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { self.title.event(ctx, event); - if let Some(anim) = &mut self.anim { - if let Event::Attach(_) = event { - anim.start(); - ctx.request_paint(); - ctx.request_anim_frame(); - } - if let Event::Timer(EventCtx::ANIM_FRAME_TIMER) = event { - if anim.is_active() { - ctx.request_anim_frame(); - ctx.request_paint(); - } - } - } - if let Some(ButtonMsg::Clicked) = self.left_button.event(ctx, event) { return Some(self.left_button_msg); }; @@ -241,24 +178,15 @@ impl Component for Header { } fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { - let offset = if let Some(anim) = &self.anim { - Offset::x(anim.get_title_offset(anim.eval())) - } else { - Offset::zero() - }; - - // TODO: correct animation - target.with_origin(offset, &|target| { - self.right_button.render(target); - self.left_button.render(target); - if let Some(icon) = self.icon { - shape::ToifImage::new(self.area.left_center(), icon.toif) - .with_fg(self.icon_color.unwrap_or(theme::GREY_LIGHT)) - .with_align(Alignment2D::CENTER_LEFT) - .render(target); - } - self.title.render(target); - }); + self.right_button.render(target); + self.left_button.render(target); + if let Some(icon) = self.icon { + shape::ToifImage::new(self.area.left_center(), icon.toif) + .with_fg(self.icon_color.unwrap_or(theme::GREY_LIGHT)) + .with_align(Alignment2D::CENTER_LEFT) + .render(target); + } + self.title.render(target); } } diff --git a/core/embed/rust/src/ui/layout_eckhart/firmware/mod.rs b/core/embed/rust/src/ui/layout_eckhart/firmware/mod.rs index 2ce6893b4d..b745243b8c 100644 --- a/core/embed/rust/src/ui/layout_eckhart/firmware/mod.rs +++ b/core/embed/rust/src/ui/layout_eckhart/firmware/mod.rs @@ -11,6 +11,7 @@ mod hold_to_confirm; mod homescreen; mod keyboard; mod number_input_screen; +mod progress_screen; mod qr_screen; mod select_word_screen; mod share_words; @@ -36,6 +37,7 @@ pub use keyboard::{ word_count_screen::{SelectWordCountMsg, SelectWordCountScreen}, }; pub use number_input_screen::{NumberInputScreen, NumberInputScreenMsg}; +pub use progress_screen::ProgressScreen; pub use qr_screen::{QrMsg, QrScreen}; pub use select_word_screen::{SelectWordMsg, SelectWordScreen}; pub use share_words::{ShareWordsScreen, ShareWordsScreenMsg}; diff --git a/core/embed/rust/src/ui/layout_eckhart/firmware/progress_screen.rs b/core/embed/rust/src/ui/layout_eckhart/firmware/progress_screen.rs new file mode 100644 index 0000000000..08a3618bd9 --- /dev/null +++ b/core/embed/rust/src/ui/layout_eckhart/firmware/progress_screen.rs @@ -0,0 +1,176 @@ +use core::mem; + +use crate::{ + strutil::TString, + translations::TR, + ui::{ + component::{ + text::paragraphs::{Paragraph, ParagraphSource as _, ParagraphVecShort, Paragraphs}, + Component, Event, EventCtx, Label, Never, + }, + geometry::{Alignment, Alignment2D, LinearPlacement, Offset, Rect}, + shape::{self, Renderer}, + util::animation_disabled, + }, +}; + +use super::super::{ + constant::SCREEN, + cshape::{render_loader, render_loader_indeterminate, ScreenBorder}, + fonts, theme, +}; + +const LOADER_SPEED: u16 = 5; + +pub struct ProgressScreen { + indeterminate: bool, + text: Paragraphs>, + /// Current value of the progress bar. + value: u16, + border: ScreenBorder, + /// Whether the progress is for Coinjoin BusyScreen + coinjoin_progress: bool, + coinjoin_do_not_disconnect: Option>, +} + +impl ProgressScreen { + pub fn new_progress( + title: TString<'static>, + indeterminate: bool, + description: TString<'static>, + ) -> Self { + Self { + indeterminate, + text: Self::create_paragraphs(title, description), + value: 0, + border: ScreenBorder::new(theme::GREEN_LIME), + coinjoin_progress: false, + coinjoin_do_not_disconnect: None, + } + } + + pub fn new_coinjoin_progress( + title: TString<'static>, + indeterminate: bool, + description: TString<'static>, + ) -> Self { + Self { + indeterminate, + text: Self::create_paragraphs(title, description), + value: 0, + border: ScreenBorder::new(theme::GREEN_LIME), + coinjoin_progress: true, + coinjoin_do_not_disconnect: Some( + Label::centered( + TR::coinjoin__title_do_not_disconnect.into(), + theme::TEXT_REGULAR, + ) + .vertically_centered(), + ), + } + } + + fn create_paragraphs( + title: TString<'static>, + description: TString<'static>, + ) -> Paragraphs> { + ParagraphVecShort::from_iter([ + Paragraph::new(&theme::firmware::TEXT_MEDIUM_GREY, title).centered(), + Paragraph::new(&theme::firmware::TEXT_MEDIUM_GREY, description).centered(), + ]) + .into_paragraphs() + .with_placement(LinearPlacement::vertical().align_at_center()) + } +} + +impl Component for ProgressScreen { + type Msg = Never; + + fn place(&mut self, bounds: Rect) -> Rect { + debug_assert_eq!(bounds.height(), SCREEN.height()); + debug_assert_eq!(bounds.width(), SCREEN.width()); + let bounds = bounds.inset(theme::SIDE_INSETS); + + let max_text_area = 3 * theme::TEXT_REGULAR.text_font.text_max_height(); + let middle_text_area = Rect::snap( + SCREEN.center(), + Offset::new(bounds.width(), max_text_area), + Alignment2D::CENTER, + ); + let action_bar_area = bounds.split_bottom(theme::ACTION_BAR_HEIGHT).1; + + self.coinjoin_do_not_disconnect.place(middle_text_area); + self.text.place(action_bar_area); + bounds + } + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + // ProgressScreen only reacts to Progress events + // CoinjoinProgressScreen with indeterminate reacts to ANIM_FRAME_TIMER + match event { + _ if animation_disabled() => { + return None; + } + Event::Attach(_) if self.coinjoin_progress && self.indeterminate => { + ctx.request_anim_frame(); + } + Event::Timer(EventCtx::ANIM_FRAME_TIMER) => { + self.value = (self.value + LOADER_SPEED) % 1000; + ctx.request_anim_frame(); + ctx.request_paint(); + } + Event::Progress(new_value, new_description) => { + if mem::replace(&mut self.value, new_value) != new_value { + if !animation_disabled() { + ctx.request_paint(); + } + if self.text.inner()[1].content() != &new_description { + self.text.mutate(|p| p[1].update(new_description)); + ctx.request_paint(); + } + } + } + _ => {} + } + None + } + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + let progress_val = self.value.min(1000); + if self.indeterminate { + render_loader_indeterminate(progress_val, &self.border, target); + } else { + render_loader(progress_val, &self.border, target); + if !self.coinjoin_progress { + render_percentage(progress_val, target); + } + } + if self.coinjoin_progress { + self.coinjoin_do_not_disconnect.render(target); + } + self.text.render(target); + } +} + +fn render_percentage<'s>(progress: u16, target: &mut impl Renderer<'s>) { + let progress_percent = uformat!("{}%", (progress as f32 / 10.0) as i16); + shape::Text::new( + SCREEN.center(), + &progress_percent, + fonts::FONT_SATOSHI_EXTRALIGHT_72, + ) + .with_align(Alignment::Center) + .with_fg(theme::GREY_LIGHT) + .render(target); +} + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for ProgressScreen { + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + if self.coinjoin_progress { + t.component("CoinjoinProgress"); + } else { + t.component("Progress"); + } + } +} diff --git a/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs b/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs index 5e2043585b..3f8d60b91c 100644 --- a/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs +++ b/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs @@ -15,7 +15,7 @@ use crate::{ Paragraphs, VecExt, }, }, - Empty, FormattedText, + ComponentExt as _, Empty, FormattedText, Timeout, }, geometry::{Alignment, LinearPlacement, Offset}, layout::{ @@ -33,8 +33,8 @@ use super::{ component::Button, firmware::{ ActionBar, Bip39Input, ConfirmHomescreen, DeviceMenuScreen, Header, HeaderMsg, Hint, - Homescreen, MnemonicKeyboard, NumberInputScreen, PinKeyboard, SelectWordCountScreen, - SelectWordScreen, SetBrightnessScreen, Slip39Input, TextScreen, + Homescreen, MnemonicKeyboard, NumberInputScreen, PinKeyboard, ProgressScreen, + SelectWordCountScreen, SelectWordScreen, SetBrightnessScreen, Slip39Input, TextScreen, }, flow, fonts, theme, UIEckhart, }; @@ -921,7 +921,7 @@ impl FirmwareUI for UIEckhart { fn show_progress( description: TString<'static>, - _indeterminate: bool, + indeterminate: bool, title: Option>, ) -> Result { let (title, description) = if let Some(title) = title { @@ -930,23 +930,35 @@ impl FirmwareUI for UIEckhart { (description, "".into()) }; - let paragraphs = Paragraph::new(&theme::TEXT_REGULAR, description) - .into_paragraphs() - .with_placement(LinearPlacement::vertical()); - let header = Header::new(title); - let screen = TextScreen::new(paragraphs).with_header(header); - - let layout = RootComponent::new(screen); + let layout = RootComponent::new(ProgressScreen::new_progress( + title, + indeterminate, + description, + )); Ok(layout) } fn show_progress_coinjoin( - _title: TString<'static>, - _indeterminate: bool, - _time_ms: u32, - _skip_first_paint: bool, + description: TString<'static>, + indeterminate: bool, + time_ms: u32, + skip_first_paint: bool, ) -> Result, Error> { - Err::, Error>(Error::ValueError(c"not implemented")) + let progress = ProgressScreen::new_coinjoin_progress( + TR::coinjoin__title_progress.into(), + indeterminate, + description, + ); + let obj = if time_ms > 0 && indeterminate { + let timeout = Timeout::new(time_ms); + LayoutObj::new((timeout, progress.map(|_msg| None)))? + } else { + LayoutObj::new(progress)? + }; + if skip_first_paint { + obj.skip_first_paint(); + } + Ok(obj) } fn show_share_words(