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..6865e6c466 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 @@ -15,10 +15,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, + Homescreen, HomescreenMsg, MnemonicInput, MnemonicKeyboard, MnemonicKeyboardMsg, PinKeyboard, + PinKeyboardMsg, SelectWordCountMsg, SelectWordCountScreen, SelectWordMsg, SelectWordScreen, + SetBrightnessScreen, TextScreen, TextScreenMsg, }; impl ComponentMsgObj for PinKeyboard<'_> { @@ -111,16 +110,6 @@ impl ComponentMsgObj for SelectWordCountScreen { } } -impl ComponentMsgObj for NumberInputScreen { - fn msg_try_into_obj(&self, msg: Self::Msg) -> Result { - match msg { - NumberInputScreenMsg::Confirmed(i) => i.try_into(), - NumberInputScreenMsg::Cancelled => Ok(CANCELLED.as_obj()), - NumberInputScreenMsg::Menu => Ok(INFO.as_obj()), - } - } -} - impl ComponentMsgObj for ConfirmHomescreen { fn msg_try_into_obj(&self, msg: Self::Msg) -> Result { match msg { 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 9c575f8341..e2e135e0ae 100644 --- a/core/embed/rust/src/ui/layout_eckhart/firmware/mod.rs +++ b/core/embed/rust/src/ui/layout_eckhart/firmware/mod.rs @@ -12,6 +12,7 @@ mod qr_screen; mod select_word_screen; mod share_words; mod text_screen; +mod updatable_info_screen; mod vertical_menu; mod vertical_menu_screen; @@ -36,6 +37,7 @@ pub use qr_screen::{QrMsg, QrScreen}; pub use select_word_screen::{SelectWordMsg, SelectWordScreen}; pub use share_words::{ShareWordsScreen, ShareWordsScreenMsg}; pub use text_screen::{AllowedTextContent, TextScreen, TextScreenMsg}; +pub use updatable_info_screen::UpdatableInfoScreen; pub use vertical_menu::{VerticalMenu, VerticalMenuMsg, MENU_MAX_ITEMS}; pub use vertical_menu_screen::{VerticalMenuScreen, VerticalMenuScreenMsg}; diff --git a/core/embed/rust/src/ui/layout_eckhart/firmware/number_input_screen.rs b/core/embed/rust/src/ui/layout_eckhart/firmware/number_input_screen.rs index c2828e879a..9d20d49a9d 100644 --- a/core/embed/rust/src/ui/layout_eckhart/firmware/number_input_screen.rs +++ b/core/embed/rust/src/ui/layout_eckhart/firmware/number_input_screen.rs @@ -2,9 +2,11 @@ use crate::{ strutil::{self, TString}, translations::TR, ui::{ - component::{Component, Event, EventCtx, Label, Maybe, Never}, + component::{swipe_detect::SwipeConfig, Component, Event, EventCtx, Label, Maybe}, + flow::Swipable, geometry::{Alignment, Insets, Offset, Rect}, shape::{self, Renderer}, + util::Pager, }, }; @@ -20,6 +22,7 @@ use super::{ pub enum NumberInputScreenMsg { Cancelled, Confirmed(u32), + Changed(u32), Menu, } @@ -87,7 +90,7 @@ impl Component for NumberInputScreen { } fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { - self.number_input.event(ctx, event); + let changed = self.number_input.event(ctx, event); if let Some(HeaderMsg::Menu) = self.header.event(ctx, event) { return Some(NumberInputScreenMsg::Menu); @@ -103,6 +106,10 @@ impl Component for NumberInputScreen { } } + if let Some(NumberInputMsg::Changed(value)) = changed { + return Some(NumberInputScreenMsg::Changed(value)); + } + None } @@ -114,6 +121,15 @@ impl Component for NumberInputScreen { } } +impl Swipable for NumberInputScreen { + fn get_pager(&self) -> Pager { + Pager::single_page() + } + fn get_swipe_config(&self) -> SwipeConfig { + SwipeConfig::default() + } +} + #[cfg(feature = "ui_debug")] impl crate::trace::Trace for NumberInputScreen { fn trace(&self, t: &mut dyn crate::trace::Tracer) { @@ -132,6 +148,10 @@ struct NumberInput { value: u32, } +pub enum NumberInputMsg { + Changed(u32), +} + impl NumberInput { const BUTTON_PADDING: i16 = 10; const BUTTON_SIZE: Offset = Offset::new(138, 130); @@ -204,7 +224,7 @@ impl NumberInput { } impl Component for NumberInput { - type Msg = Never; + type Msg = NumberInputMsg; fn place(&mut self, bounds: Rect) -> Rect { self.area = bounds; @@ -231,12 +251,19 @@ impl Component for NumberInput { } fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + let mut changed = false; if let Some(ButtonMsg::Clicked) = self.dec.event(ctx, event) { self.decrease(ctx); + changed = true; }; if let Some(ButtonMsg::Clicked) = self.inc.event(ctx, event) { self.increase(ctx); + changed = true; }; + if changed { + ctx.request_paint(); + return Some(NumberInputMsg::Changed(self.value)); + } None } diff --git a/core/embed/rust/src/ui/layout_eckhart/firmware/updatable_info_screen.rs b/core/embed/rust/src/ui/layout_eckhart/firmware/updatable_info_screen.rs new file mode 100644 index 0000000000..2ed59527ec --- /dev/null +++ b/core/embed/rust/src/ui/layout_eckhart/firmware/updatable_info_screen.rs @@ -0,0 +1,105 @@ +use crate::{ + strutil::TString, + translations::TR, + ui::{ + component::{ + swipe_detect::SwipeConfig, + text::paragraphs::{Paragraph, ParagraphSource, Paragraphs}, + Component, Event, EventCtx, + }, + flow::Swipable, + geometry::{LinearPlacement, Rect}, + shape::Renderer, + util::Pager, + }, +}; + +use super::{theme, Header, TextScreen, TextScreenMsg}; + +pub struct UpdatableInfoScreen +where + F: Fn() -> TString<'static>, +{ + info_func: F, + paragraphs: Paragraphs>, + area: Rect, + text_screen: TextScreen>>, +} + +impl UpdatableInfoScreen +where + F: Fn() -> TString<'static>, +{ + pub fn new(info_func: F) -> Self { + let paragraphs = Paragraph::new(&theme::TEXT_REGULAR, TString::empty()) + .into_paragraphs() + .with_placement(LinearPlacement::vertical()); + let text_screen = create_text_screen(paragraphs.clone()); + Self { + info_func, + paragraphs, + area: Rect::zero(), + text_screen, + } + } + + fn update_text(&mut self, ctx: &mut EventCtx) { + let text = (self.info_func)(); + self.paragraphs.update(text); + self.text_screen = create_text_screen(self.paragraphs.clone()); + self.text_screen.place(self.area); + ctx.request_paint(); + } +} + +impl Component for UpdatableInfoScreen +where + F: Fn() -> TString<'static>, +{ + type Msg = TextScreenMsg; + + fn place(&mut self, bounds: Rect) -> Rect { + self.text_screen.place(bounds); + self.area = bounds; + bounds + } + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + if let Event::Attach(_) = event { + self.update_text(ctx); + } + + self.text_screen.event(ctx, event) + } + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.text_screen.render(target); + } +} + +fn create_text_screen( + paragraphs: Paragraphs>, +) -> TextScreen>> { + TextScreen::new(paragraphs) + .with_header(Header::new(TR::buttons__more_info.into()).with_close_button()) +} + +impl TString<'static>> Swipable for UpdatableInfoScreen { + fn get_pager(&self) -> Pager { + Pager::single_page() + } + fn get_swipe_config(&self) -> SwipeConfig { + SwipeConfig::default() + } +} + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for UpdatableInfoScreen +where + F: Fn() -> TString<'static>, +{ + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.component("UpdatableInfoScreen"); + t.child("screen", &self.text_screen); + } +} diff --git a/core/embed/rust/src/ui/layout_eckhart/flow/mod.rs b/core/embed/rust/src/ui/layout_eckhart/flow/mod.rs index aea7c2b349..58679faaaf 100644 --- a/core/embed/rust/src/ui/layout_eckhart/flow/mod.rs +++ b/core/embed/rust/src/ui/layout_eckhart/flow/mod.rs @@ -5,6 +5,7 @@ pub mod confirm_summary; pub mod continue_recovery_homepage; pub mod get_address; pub mod prompt_backup; +pub mod request_number; pub mod request_passphrase; pub mod show_danger; pub mod show_share_words; @@ -16,6 +17,7 @@ pub use confirm_summary::new_confirm_summary; pub use continue_recovery_homepage::new_continue_recovery_homepage; pub use get_address::GetAddress; pub use prompt_backup::PromptBackup; +pub use request_number::new_request_number; pub use request_passphrase::RequestPassphrase; pub use show_danger::ShowDanger; pub use show_share_words::new_show_share_words_flow; diff --git a/core/embed/rust/src/ui/layout_eckhart/flow/request_number.rs b/core/embed/rust/src/ui/layout_eckhart/flow/request_number.rs new file mode 100644 index 0000000000..c4bd625eea --- /dev/null +++ b/core/embed/rust/src/ui/layout_eckhart/flow/request_number.rs @@ -0,0 +1,122 @@ +use crate::{ + error, + strutil::TString, + translations::TR, + ui::{ + component::ComponentExt, + flow::{ + base::{Decision, DecisionBuilder as _}, + FlowController, FlowMsg, SwipeFlow, + }, + geometry::{Alignment, Direction, Offset}, + }, +}; + +use core::sync::atomic::{AtomicU16, Ordering}; + +use super::super::{ + component::Button, + firmware::{ + Header, NumberInputScreen, NumberInputScreenMsg, TextScreenMsg, UpdatableInfoScreen, + VerticalMenu, VerticalMenuScreen, VerticalMenuScreenMsg, + }, + theme, +}; + +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum RequestNumber { + Number, + Menu, + Info, +} + +impl FlowController for RequestNumber { + #[inline] + fn index(&'static self) -> usize { + *self as usize + } + + fn handle_swipe(&'static self, _direction: Direction) -> Decision { + self.do_nothing() + } + + fn handle_event(&'static self, msg: FlowMsg) -> Decision { + match (self, msg) { + (Self::Number, FlowMsg::Info) => Self::Menu.goto(), + (Self::Menu, FlowMsg::Choice(0)) => Self::Info.goto(), + (Self::Menu, FlowMsg::Choice(1)) => self.return_msg(FlowMsg::Cancelled), + (Self::Menu, FlowMsg::Cancelled) => Self::Number.goto(), + (Self::Info, FlowMsg::Cancelled) => Self::Menu.goto(), + (Self::Number, FlowMsg::Choice(n)) => self.return_msg(FlowMsg::Choice(n)), + _ => self.do_nothing(), + } + } +} + +static NUM_DISPLAYED: AtomicU16 = AtomicU16::new(0); + +#[allow(clippy::too_many_arguments)] +pub fn new_request_number( + title: TString<'static>, + count: u32, + min_count: u32, + max_count: u32, + description: TString<'static>, + info_closure: impl Fn(u32) -> TString<'static> + 'static, +) -> Result { + NUM_DISPLAYED.store(count as u16, Ordering::Relaxed); + + // wrap the closure for obtaining MoreInfo text and call it with NUM_DISPLAYED + let info_closure = move || { + let curr_number = NUM_DISPLAYED.load(Ordering::Relaxed); + info_closure(curr_number as u32) + }; + + let content_input = NumberInputScreen::new(min_count, max_count, count, description) + .with_header(Header::new(title).with_menu_button()) + .map(|msg| match msg { + NumberInputScreenMsg::Cancelled => Some(FlowMsg::Cancelled), + NumberInputScreenMsg::Confirmed(n) => { + NUM_DISPLAYED.store(n as u16, Ordering::Relaxed); + Some(FlowMsg::Choice(n as usize)) + } + NumberInputScreenMsg::Changed(n) => { + NUM_DISPLAYED.store(n as u16, Ordering::Relaxed); + None + } + NumberInputScreenMsg::Menu => Some(FlowMsg::Info), + }); + + let menu_items = VerticalMenu::empty() + .item( + Button::with_text(TR::buttons__more_info.into()) + .styled(theme::menu_item_title()) + .with_text_align(Alignment::Start) + .with_content_offset(Offset::x(12)), + ) + .item( + Button::with_text(TR::buttons__cancel.into()) + .styled(theme::menu_item_title_orange()) + .with_text_align(Alignment::Start) + .with_content_offset(Offset::x(12)), + ); + + let content_menu = VerticalMenuScreen::new(menu_items) + .with_header(Header::new(TString::empty()).with_close_button()) + .map(move |msg| match msg { + VerticalMenuScreenMsg::Selected(i) => Some(FlowMsg::Choice(i)), + VerticalMenuScreenMsg::Close => Some(FlowMsg::Cancelled), + _ => None, + }); + + let content_info = UpdatableInfoScreen::new(info_closure).map(|msg| match msg { + TextScreenMsg::Cancelled => Some(FlowMsg::Cancelled), + _ => None, + }); + + let mut res = SwipeFlow::new(&RequestNumber::Number)?; + res.add_page(&RequestNumber::Number, content_input)? + .add_page(&RequestNumber::Menu, content_menu)? + .add_page(&RequestNumber::Info, content_info)?; + Ok(res) +} 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 8fcf73f36d..5e196b87ee 100644 --- a/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs +++ b/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs @@ -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, PinKeyboard, SelectWordCountScreen, SelectWordScreen, + SetBrightnessScreen, Slip39Input, TextScreen, }, flow, fonts, theme, UIEckhart, }; @@ -619,15 +619,19 @@ impl FirmwareUI for UIEckhart { min_count: u32, max_count: u32, description: Option>, - _more_info_callback: Option TString<'static> + 'static>, + more_info_callback: Option TString<'static> + 'static>, ) -> Result { let description = description.unwrap_or(TString::empty()); - let component = NumberInputScreen::new(min_count, max_count, count, description) - .with_header(Header::new(title)); - let layout = RootComponent::new(component); - - Ok(layout) + let flow = flow::request_number::new_request_number( + title, + count, + min_count, + max_count, + description, + unwrap!(more_info_callback), + )?; + Ok(flow) } fn request_pin(