From e81c97bdda6bf5313dc5ccfba631931c13195d2c Mon Sep 17 00:00:00 2001 From: obrusvit Date: Wed, 16 Apr 2025 17:43:16 +0200 Subject: [PATCH] feat(eckhart): progress screen - implement `show_progress` and `show_progress_coinjoin` FirmwareUI functions --- .../ui/layout_eckhart/component_msg_obj.rs | 12 +- .../src/ui/layout_eckhart/firmware/mod.rs | 2 + .../firmware/progress_screen.rs | 176 ++++++++++++++++++ .../rust/src/ui/layout_eckhart/ui_firmware.rs | 44 +++-- 4 files changed, 215 insertions(+), 19 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/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 2d3a4b8501..3acc096c45 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, }; @@ -919,7 +919,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 { @@ -928,23 +928,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(