From 989f2cc872cb9f5d8f89a32efec62e75d64e16a7 Mon Sep 17 00:00:00 2001 From: tychovrahe Date: Mon, 9 Jan 2023 17:49:26 +0100 Subject: [PATCH] feat(core/rust): use marquee in TR titles [no changelog] --- core/embed/rust/src/ui/component/base.rs | 18 --- core/embed/rust/src/ui/component/marquee.rs | 9 +- .../rust/src/ui/model_tr/component/common.rs | 32 +--- .../rust/src/ui/model_tr/component/flow.rs | 52 ++++-- .../src/ui/model_tr/component/flow_pages.rs | 4 + .../rust/src/ui/model_tr/component/frame.rs | 149 +++++++++++++----- .../rust/src/ui/model_tr/component/loader.rs | 5 +- .../rust/src/ui/model_tr/component/mod.rs | 3 +- .../rust/src/ui/model_tr/component/page.rs | 98 +++++------- .../src/ui/model_tr/component/scrollbar.rs | 24 +-- .../src/ui/model_tr/component/share_words.rs | 61 ++++--- .../rust/src/ui/model_tr/component/title.rs | 108 +++++++++++++ core/embed/rust/src/ui/model_tr/layout.rs | 16 +- 13 files changed, 370 insertions(+), 209 deletions(-) create mode 100644 core/embed/rust/src/ui/model_tr/component/title.rs diff --git a/core/embed/rust/src/ui/component/base.rs b/core/embed/rust/src/ui/component/base.rs index c54e79f5f7..05a659eb22 100644 --- a/core/embed/rust/src/ui/component/base.rs +++ b/core/embed/rust/src/ui/component/base.rs @@ -38,20 +38,6 @@ pub trait Component { /// No painting should be done in this phase. fn place(&mut self, bounds: Rect) -> Rect; - /// Define the available area for scrollbar, when it is outside of the - /// component itself (in the area of parent component). - /// - /// This area is the *MAXIMUM* area that scrollbar can occupy. - /// However, the scrollbar itself may be found smaller than this - /// (after the content is placed and we know the page count). - /// In this case, its area will/should be decreased and taken from the right - /// (to minimize the area that is affected - as scrollbar has a `Pad` - /// that is cleared periodically). - /// - /// Use-case is putting the scrollbar on the same line as the title in - /// `Frame` and operating this scrollbar from the Child component. - fn set_scrollbar_area(&mut self, _area: Rect) {} - /// React to an outside event. See the `Event` type for possible cases. /// /// Component should modify its internal state as a response to the event, @@ -145,10 +131,6 @@ where self.component.place(bounds) } - fn set_scrollbar_area(&mut self, area: Rect) { - self.component.set_scrollbar_area(area); - } - fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { self.mutate(ctx, |ctx, c| { // Handle the internal invalidation event here, so components don't have to. We diff --git a/core/embed/rust/src/ui/component/marquee.rs b/core/embed/rust/src/ui/component/marquee.rs index 38cf1fe143..f8a80712ea 100644 --- a/core/embed/rust/src/ui/component/marquee.rs +++ b/core/embed/rust/src/ui/component/marquee.rs @@ -7,6 +7,7 @@ use crate::{ display, display::{Color, Font}, geometry::Rect, + util::animation_disabled, }, }; @@ -161,7 +162,9 @@ impl Component for Marquee { _ => {} } // We have something to paint, so request to be painted in the next pass. - ctx.request_paint(); + if !animation_disabled() { + ctx.request_paint(); + } // There is further progress in the animation, request an animation frame event. ctx.request_anim_frame(); } @@ -169,7 +172,9 @@ impl Component for Marquee { if token == EventCtx::ANIM_FRAME_TIMER { if self.is_animating() { // We have something to paint, so request to be painted in the next pass. - ctx.request_paint(); + if !animation_disabled() { + ctx.request_paint(); + } // There is further progress in the animation, request an animation frame // event. ctx.request_anim_frame(); diff --git a/core/embed/rust/src/ui/model_tr/component/common.rs b/core/embed/rust/src/ui/model_tr/component/common.rs index 2e1a44c3c5..9f628b1d53 100644 --- a/core/embed/rust/src/ui/model_tr/component/common.rs +++ b/core/embed/rust/src/ui/model_tr/component/common.rs @@ -1,6 +1,6 @@ use crate::ui::{ display::{self, Font}, - geometry::{Offset, Point, Rect}, + geometry::Point, }; use super::theme; @@ -26,33 +26,3 @@ pub fn display_center>(baseline: Point, text: &T, font: Font) { pub fn display_right>(baseline: Point, text: &T, font: Font) { display::text_right(baseline, text.as_ref(), font, theme::FG, theme::BG); } - -/// Display title/header at the top left of the given area. -/// Returning the painted height of the whole header. -pub fn paint_header_left>(title: T, area: Rect) -> i16 { - let text_heigth = theme::FONT_HEADER.text_height(); - let title_baseline = area.top_left() + Offset::y(text_heigth); - display::text_left( - title_baseline, - title.as_ref(), - theme::FONT_HEADER, - theme::FG, - theme::BG, - ); - text_heigth -} - -/// Display title/header centered at the top of the given area. -/// Returning the painted height of the whole header. -pub fn paint_header_centered>(title: T, area: Rect) -> i16 { - let text_heigth = theme::FONT_HEADER.text_height(); - let title_baseline = area.top_center() + Offset::y(text_heigth); - display::text_center( - title_baseline, - title.as_ref(), - theme::FONT_HEADER, - theme::FG, - theme::BG, - ); - text_heigth -} 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 38387008c6..0c9e4a38bf 100644 --- a/core/embed/rust/src/ui/model_tr/component/flow.rs +++ b/core/embed/rust/src/ui/model_tr/component/flow.rs @@ -3,12 +3,13 @@ use crate::{ ui::{ component::{Child, Component, ComponentExt, Event, EventCtx, Pad}, geometry::Rect, + model_tr::component::{scrollbar::SCROLLBAR_SPACE, title::Title}, }, }; use super::{ - common, theme, ButtonAction, ButtonController, ButtonControllerMsg, ButtonLayout, ButtonPos, - FlowPages, Page, ScrollBar, + theme, ButtonAction, ButtonController, ButtonControllerMsg, ButtonLayout, ButtonPos, FlowPages, + Page, ScrollBar, }; /// To be returned directly from Flow. @@ -24,7 +25,7 @@ pub struct Flow { /// Instance of the current Page current_page: Page, /// Title being shown at the top in bold - common_title: Option, + title: Option, scrollbar: Child<ScrollBar>, content_area: Rect, title_area: Rect, @@ -43,7 +44,7 @@ where Self { pages, current_page, - common_title: None, + title: None, content_area: Rect::zero(), title_area: Rect::zero(), scrollbar: Child::new(ScrollBar::to_be_filled_later()), @@ -60,7 +61,7 @@ where /// Adding a common title to all pages. The title will not be colliding /// with the page content, as the content will be offset. pub fn with_common_title(mut self, title: StrBuffer) -> Self { - self.common_title = Some(title); + self.title = Some(Title::new(title)); self } @@ -75,8 +76,11 @@ where /// position. fn change_current_page(&mut self) { self.current_page = self.pages.get(self.page_counter); - if self.common_title.is_some() && let Some(title) = self.current_page.title() { - self.common_title = Some(title); + if self.title.is_some() { + if let Some(title) = self.current_page.title() { + self.title = Some(Title::new(title)); + self.title.place(self.title_area); + } } let scrollbar_active_index = self .pages @@ -148,12 +152,20 @@ where fn event_consumed_by_current_choice(&mut self, ctx: &mut EventCtx, pos: ButtonPos) -> bool { if matches!(pos, ButtonPos::Left) && self.current_page.has_prev_page() { self.current_page.go_to_prev_page(); - self.scrollbar.inner_mut().go_to_previous_page(); + let inner_page = self.current_page.get_current_page(); + self.scrollbar + .inner_mut() + .set_active_page(self.page_counter as usize + inner_page); + self.scrollbar.request_complete_repaint(ctx); self.update(ctx, false); true } else if matches!(pos, ButtonPos::Right) && self.current_page.has_next_page() { self.current_page.go_to_next_page(); - self.scrollbar.inner_mut().go_to_next_page(); + let inner_page = self.current_page.get_current_page(); + self.scrollbar + .inner_mut() + .set_active_page(self.page_counter as usize + inner_page); + self.scrollbar.request_complete_repaint(ctx); self.update(ctx, false); true } else { @@ -171,22 +183,27 @@ where fn place(&mut self, bounds: Rect) -> Rect { let (title_content_area, button_area) = bounds.split_bottom(theme::BUTTON_HEIGHT); // Accounting for possible title - let (title_area, content_area) = if self.common_title.is_some() { + let (title_area, content_area) = if self.title.is_some() { title_content_area.split_top(theme::FONT_HEADER.line_height()) } else { (Rect::zero(), title_content_area) }; - self.title_area = title_area; self.content_area = content_area; // Placing a scrollbar in case the title is there - if self.common_title.is_some() { + if self.title.is_some() { // Finding out the total amount of pages in this flow let complete_page_count = self.pages.scrollbar_page_count(content_area); self.scrollbar .inner_mut() .set_page_count(complete_page_count); - self.scrollbar.place(title_area); + + let (title_area, scrollbar_area) = + title_area.split_right(self.scrollbar.inner().overall_width() + SCROLLBAR_SPACE); + + self.title.place(title_area); + self.title_area = title_area; + self.scrollbar.place(scrollbar_area); } // We finally found how long is the first page, and can set its button layout. @@ -199,6 +216,7 @@ where } fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> { + self.title.event(ctx, event); let button_event = self.buttons.event(ctx, event); // Do something when a button was triggered @@ -249,9 +267,9 @@ where fn paint(&mut self) { self.pad.paint(); // Scrollbars are painted only with a title - if let Some(title) = self.common_title { + if self.title.is_some() { self.scrollbar.paint(); - common::paint_header_left(title, self.title_area); + self.title.paint(); } self.buttons.paint(); // On purpose painting current page at the end, after buttons, @@ -296,8 +314,8 @@ where self.report_btn_actions(t); - if let Some(title) = &self.common_title { - t.title(title.as_ref()); + if self.title.is_some() { + t.field("title", &self.title); } t.field("content_area", &self.content_area); t.field("buttons", &self.buttons); 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 329b28249e..acd22a13f9 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 @@ -183,6 +183,10 @@ impl<const M: usize> Page<M> { pub fn go_to_next_page(&mut self) { self.current_page += 1; } + + pub fn get_current_page(&self) -> usize { + self.current_page + } } // For `layout.rs` - single operations 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 ed68f9c4cb..45d0ac083d 100644 --- a/core/embed/rust/src/ui/model_tr/component/frame.rs +++ b/core/embed/rust/src/ui/model_tr/component/frame.rs @@ -1,19 +1,16 @@ -use super::{common, theme, ScrollBar}; +use super::{theme, ScrollBar}; use crate::{ micropython::buffer::StrBuffer, ui::{ - component::{Child, Component, Event, EventCtx}, + component::{Child, Component, ComponentExt, Event, EventCtx}, geometry::{Insets, Rect}, + model_tr::component::{scrollbar::SCROLLBAR_SPACE, title::Title}, }, }; /// Component for holding another component and displaying a title. -/// Also is allocating space for a scrollbar. pub struct Frame<T> { - area: Rect, - title: StrBuffer, - title_centered: bool, - account_for_scrollbar: bool, + title: Title, content: Child<T>, } @@ -23,10 +20,7 @@ where { pub fn new(title: StrBuffer, content: T) -> Self { Self { - title, - area: Rect::zero(), - title_centered: false, - account_for_scrollbar: true, + title: Title::new(title), content: Child::new(content), } } @@ -36,16 +30,8 @@ where } /// Aligning the title to the center, instead of the left. - /// Also disabling scrollbar, as they are not compatible. pub fn with_title_centered(mut self) -> Self { - self.title_centered = true; - self.account_for_scrollbar = false; - self - } - - /// Allocating space for scrollbar in the top right. True by default. - pub fn with_scrollbar(mut self, account_for_scrollbar: bool) -> Self { - self.account_for_scrollbar = account_for_scrollbar; + self.title = self.title.with_centered(); self } } @@ -59,36 +45,105 @@ where fn place(&mut self, bounds: Rect) -> Rect { const TITLE_SPACE: i16 = 2; - let (title_and_scrollbar_area, content_area) = - bounds.split_top(theme::FONT_HEADER.line_height()); + let (title_area, content_area) = bounds.split_top(theme::FONT_HEADER.line_height()); let content_area = content_area.inset(Insets::top(TITLE_SPACE)); - // Title area is different based on scrollbar. - let title_area = if self.account_for_scrollbar { - let (title_area, scrollbar_area) = - title_and_scrollbar_area.split_right(ScrollBar::MAX_WIDTH); - // Sending the scrollbar area to the child component. - self.content.set_scrollbar_area(scrollbar_area); - title_area - } else { - title_and_scrollbar_area - }; - - self.area = title_area; + self.title.place(title_area); self.content.place(content_area); bounds } fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> { + self.title.event(ctx, event); self.content.event(ctx, event) } fn paint(&mut self) { - if self.title_centered { - common::paint_header_centered(&self.title, self.area); - } else { - common::paint_header_left(&self.title, self.area); + self.title.paint(); + self.content.paint(); + } +} + +pub trait ScrollableContent { + fn page_count(&self) -> usize; + fn active_page(&self) -> usize; +} + +/// Component for holding another component and displaying a title. +/// Also is allocating space for a scrollbar. +pub struct ScrollableFrame<T> { + title: Option<Child<Title>>, + scrollbar: ScrollBar, + content: Child<T>, +} + +impl<T> ScrollableFrame<T> +where + T: Component + ScrollableContent, +{ + pub fn new(content: T) -> Self { + Self { + title: None, + scrollbar: ScrollBar::to_be_filled_later(), + content: Child::new(content), } + } + + pub fn inner(&self) -> &T { + self.content.inner() + } + + pub fn with_title(mut self, title: StrBuffer) -> Self { + self.title = Some(Child::new(Title::new(title))); + self + } +} + +impl<T> Component for ScrollableFrame<T> +where + T: Component + ScrollableContent, +{ + type Msg = T::Msg; + + fn place(&mut self, bounds: Rect) -> Rect { + // Depending whether there is a title or not + let (content_area, scrollbar_area, title_area) = if self.title.is_none() { + let (scrollbar_area, content_area) = bounds.split_top(ScrollBar::MAX_DOT_SIZE); + (content_area, scrollbar_area, Rect::zero()) + } else { + const TITLE_SPACE: i16 = 2; + + let (title_and_scrollbar_area, content_area) = + bounds.split_top(theme::FONT_HEADER.line_height()); + let content_area = content_area.inset(Insets::top(TITLE_SPACE)); + + let (title_area, scrollbar_area) = title_and_scrollbar_area + .split_right(self.scrollbar.overall_width() + SCROLLBAR_SPACE); + + (content_area, scrollbar_area, title_area) + }; + + self.content.place(content_area); + self.scrollbar + .set_page_count(self.content.inner().page_count()); + self.scrollbar.place(scrollbar_area); + self.title.place(title_area); + bounds + } + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> { + let msg = self.content.event(ctx, event); + self.scrollbar + .set_active_page(self.content.inner().active_page()); + self.scrollbar.request_complete_repaint(ctx); + self.title.event(ctx, event); + self.scrollbar.event(ctx, event); + msg + } + + fn paint(&mut self) { + self.title.paint(); + self.scrollbar.paint(); self.content.paint(); } } @@ -102,7 +157,23 @@ where { fn trace(&self, t: &mut dyn crate::trace::Tracer) { t.open("Frame"); - t.title(self.title.as_ref()); + t.field("title", &self.title); + t.field("content", &self.content); + t.close(); + } +} + +// DEBUG-ONLY SECTION BELOW + +#[cfg(feature = "ui_debug")] +impl<T> crate::trace::Trace for ScrollableFrame<T> +where + T: crate::trace::Trace, +{ + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.open("ScrollableFrame"); + t.field("title", &self.title); + t.field("scrollbar", &self.title); t.field("content", &self.content); t.close(); } 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 1f5e0900a1..48c71c8d4f 100644 --- a/core/embed/rust/src/ui/model_tr/component/loader.rs +++ b/core/embed/rust/src/ui/model_tr/component/loader.rs @@ -7,6 +7,7 @@ use crate::{ display::{self, Color, Font}, geometry::{Offset, Rect}, model_tr::theme, + util::animation_disabled, }, }; @@ -188,7 +189,9 @@ impl Component for Loader { // There is further progress in the animation, request an animation frame event. ctx.request_anim_frame(); // We have something to paint, so request to be painted in the next pass. - ctx.request_paint(); + if !animation_disabled() { + ctx.request_paint(); + } } } } diff --git a/core/embed/rust/src/ui/model_tr/component/mod.rs b/core/embed/rust/src/ui/model_tr/component/mod.rs index 819f755259..e8258666c2 100644 --- a/core/embed/rust/src/ui/model_tr/component/mod.rs +++ b/core/embed/rust/src/ui/model_tr/component/mod.rs @@ -17,6 +17,7 @@ mod result_anim; mod result_popup; mod scrollbar; mod share_words; +mod title; use super::theme; @@ -30,7 +31,7 @@ pub use button_controller::{ButtonController, ButtonControllerMsg}; pub use changing_text::ChangingTextLine; pub use flow::{Flow, FlowMsg}; pub use flow_pages::{FlowPages, Page}; -pub use frame::Frame; +pub use frame::{Frame, ScrollableContent, ScrollableFrame}; pub use homescreen::{Homescreen, HomescreenMsg, Lockscreen}; pub use input_methods::{ choice::{Choice, ChoiceFactory, ChoicePage, ChoicePageMsg}, 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 4e94973740..8ffe3f41df 100644 --- a/core/embed/rust/src/ui/model_tr/component/page.rs +++ b/core/embed/rust/src/ui/model_tr/component/page.rs @@ -4,15 +4,12 @@ use crate::ui::{ geometry::{Insets, Rect}, }; -use super::{ - theme, ButtonController, ButtonControllerMsg, ButtonDetails, ButtonLayout, ButtonPos, ScrollBar, -}; +use super::{theme, ButtonController, ButtonControllerMsg, ButtonDetails, ButtonLayout, ButtonPos}; pub struct ButtonPage<T> { + page_count: usize, + active_page: usize, content: Child<T>, - scrollbar: Child<ScrollBar>, - /// Optional available area for scrollbar defined by parent component. - parent_scrollbar_area: Option<Rect>, pad: Pad, /// Left button of the first screen cancel_btn_details: Option<ButtonDetails>, @@ -25,8 +22,6 @@ pub struct ButtonPage<T> { /// Right button of every screen apart the last one next_btn_details: Option<ButtonDetails>, buttons: Child<ButtonController>, - /// Scrollbar may or may not be shown (but will be counting pages anyway). - show_scrollbar: bool, } impl<T> ButtonPage<T> @@ -35,9 +30,9 @@ where { pub fn new(content: T, background: Color) -> Self { Self { + page_count: 0, // will be set in place() + active_page: 0, content: Child::new(content), - scrollbar: Child::new(ScrollBar::to_be_filled_later()), - parent_scrollbar_area: None, pad: Pad::with_background(background).with_clear(), cancel_btn_details: Some(ButtonDetails::cancel_icon()), confirm_btn_details: Some(ButtonDetails::text("CONFIRM".into())), @@ -48,7 +43,6 @@ where // Initial button layout will be set in `place()` after we can call // `content.page_count()`. buttons: Child::new(ButtonController::new(ButtonLayout::empty())), - show_scrollbar: true, } } @@ -72,9 +66,20 @@ where self } - pub fn with_scrollbar(mut self, show: bool) -> Self { - self.show_scrollbar = show; - self + pub fn has_next_page(&self) -> bool { + self.active_page < self.page_count - 1 + } + + pub fn has_previous_page(&self) -> bool { + self.active_page > 0 + } + + pub fn go_to_next_page(&mut self) { + self.active_page = self.active_page.saturating_add(1).min(self.page_count - 1); + } + + pub fn go_to_previous_page(&mut self) { + self.active_page = self.active_page.saturating_sub(1); } /// Basically just determining whether the right button for @@ -89,20 +94,15 @@ where /// Change the page in the content, clear the background under it and make /// sure it gets completely repainted. Also updating the buttons. fn change_page(&mut self, ctx: &mut EventCtx) { - let active_page = self.scrollbar.inner().active_page; - self.content.inner_mut().change_page(active_page); + self.content.inner_mut().change_page(self.active_page); self.content.request_complete_repaint(ctx); - self.scrollbar.request_complete_repaint(ctx); self.update_buttons(ctx); self.pad.clear(); } /// Reflecting the current page in the buttons. fn update_buttons(&mut self, ctx: &mut EventCtx) { - let btn_layout = self.get_button_layout( - self.scrollbar.inner().has_previous_page(), - self.scrollbar.inner().has_next_page(), - ); + let btn_layout = self.get_button_layout(self.has_previous_page(), self.has_next_page()); self.buttons.mutate(ctx, |_ctx, buttons| { buttons.set(btn_layout); }); @@ -137,6 +137,15 @@ where } } +impl<T> ScrollableContent for ButtonPage<T> { + fn page_count(&self) -> usize { + self.page_count + } + fn active_page(&self) -> usize { + self.active_page + } +} + impl<T> Component for ButtonPage<T> where T: Component + Paginate, @@ -151,36 +160,23 @@ where self.content.place(content_area); // Need to be called here, only after content is placed // and we can calculate the page count. - let page_count = self.content.inner_mut().page_count(); - self.scrollbar.inner_mut().set_page_count(page_count); - self.set_buttons_for_initial_page(page_count); - - // Placing the scrollbar when requested. - // Put it into its dedicated area when parent component already chose it, - // otherwise place it into the right top of the content. - if self.show_scrollbar { - let scrollbar_area = self.parent_scrollbar_area.unwrap_or(content_area); - self.scrollbar.place(scrollbar_area); - } + self.page_count = self.content.inner_mut().page_count(); + self.set_buttons_for_initial_page(self.page_count); self.buttons.place(button_area); bounds } - fn set_scrollbar_area(&mut self, area: Rect) { - self.parent_scrollbar_area = Some(area); - } - fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> { - ctx.set_page_count(self.scrollbar.inner().page_count); + ctx.set_page_count(self.page_count); let button_event = self.buttons.event(ctx, event); if let Some(ButtonControllerMsg::Triggered(pos)) = button_event { match pos { ButtonPos::Left => { - if self.scrollbar.inner().has_previous_page() { + if self.has_previous_page() { // Clicked BACK. Scroll up. - self.scrollbar.inner_mut().go_to_previous_page(); + self.go_to_previous_page(); self.change_page(ctx); } else { // Clicked CANCEL. Send result. @@ -188,9 +184,9 @@ where } } ButtonPos::Right => { - if self.scrollbar.inner().has_next_page() { + if self.has_next_page() { // Clicked NEXT. Scroll down. - self.scrollbar.inner_mut().go_to_next_page(); + self.go_to_next_page(); self.change_page(ctx); } else { // Clicked CONFIRM. Send result. @@ -210,9 +206,6 @@ where fn paint(&mut self) { self.pad.paint(); self.content.paint(); - if self.show_scrollbar { - self.scrollbar.paint(); - } self.buttons.paint(); } } @@ -221,18 +214,19 @@ where #[cfg(feature = "ui_debug")] use super::ButtonAction; +use crate::ui::model_tr::component::frame::ScrollableContent; #[cfg(feature = "ui_debug")] use heapless::String; #[cfg(feature = "ui_debug")] impl<T> crate::trace::Trace for ButtonPage<T> where - T: crate::trace::Trace, + T: crate::trace::Trace + Paginate + Component, { fn get_btn_action(&self, pos: ButtonPos) -> String<25> { match pos { ButtonPos::Left => { - if self.scrollbar.inner().has_previous_page() { + if self.has_previous_page() { ButtonAction::PrevPage.string() } else if self.cancel_btn_details.is_some() { ButtonAction::Cancel.string() @@ -241,7 +235,7 @@ where } } ButtonPos::Right => { - if self.scrollbar.inner().has_next_page() { + if self.has_next_page() { ButtonAction::NextPage.string() } else if self.confirm_btn_details.is_some() { ButtonAction::Confirm.string() @@ -255,14 +249,8 @@ where fn trace(&self, t: &mut dyn crate::trace::Tracer) { t.open("ButtonPage"); - t.kw_pair( - "active_page", - inttostr!(self.scrollbar.inner().active_page as u8), - ); - t.kw_pair( - "page_count", - inttostr!(self.scrollbar.inner().page_count as u8), - ); + t.kw_pair("active_page", inttostr!(self.active_page as u8)); + t.kw_pair("page_count", inttostr!(self.page_count as u8)); self.report_btn_actions(t); // TODO: it seems the button text is not updated when paginating (but actions // above are) 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 6c744b3cad..b8d3c385ff 100644 --- a/core/embed/rust/src/ui/model_tr/component/scrollbar.rs +++ b/core/embed/rust/src/ui/model_tr/component/scrollbar.rs @@ -23,6 +23,8 @@ enum DotType { Small, // . } +pub const SCROLLBAR_SPACE: i16 = 5; + /// How many dots at most will there be const MAX_DOTS: usize = 5; @@ -61,23 +63,9 @@ impl ScrollBar { } pub fn set_active_page(&mut self, active_page: usize) { - self.active_page = active_page; - } - - pub fn has_next_page(&self) -> bool { - self.active_page < self.page_count - 1 - } - - pub fn has_previous_page(&self) -> bool { - self.active_page > 0 - } - - pub fn go_to_next_page(&mut self) { - self.active_page = self.active_page.saturating_add(1).min(self.page_count - 1); - } - - pub fn go_to_previous_page(&mut self) { - self.active_page = self.active_page.saturating_sub(1); + if active_page != self.active_page { + self.active_page = active_page; + } } /// Create a (seemingly circular) dot given its top left point. @@ -231,7 +219,7 @@ impl Component for ScrollBar { // Occupying as little space as possible (according to the number of pages), // aligning to the right. let scrollbar_area = Rect::from_top_right_and_size( - bounds.top_right(), + bounds.top_right() + Offset::y(1), // offset for centering vertically Offset::new(self.overall_width(), Self::MAX_DOT_SIZE), ); self.pad.place(scrollbar_area); 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 63f2b4200e..0322e524a7 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 @@ -8,6 +8,10 @@ use crate::{ }, }; +use crate::ui::{ + component::Child, + model_tr::component::{scrollbar::SCROLLBAR_SPACE, title::Title, ScrollBar}, +}; use heapless::{String, Vec}; use super::common::display; @@ -22,19 +26,24 @@ const WORD_FONT: Font = Font::NORMAL; /// Showing the given share words. pub struct ShareWords<const N: usize> { area: Rect, - title: StrBuffer, + title: Child<Title>, + scrollbar: Child<ScrollBar>, share_words: Vec<StrBuffer, N>, page_index: usize, } impl<const N: usize> ShareWords<N> { pub fn new(title: StrBuffer, share_words: Vec<StrBuffer, N>) -> Self { - Self { + let mut instance = Self { area: Rect::zero(), - title, + title: Child::new(Title::new(title)), + scrollbar: Child::new(ScrollBar::to_be_filled_later()), share_words, page_index: 0, - } + }; + let page_count = instance.total_page_count(); + instance.scrollbar.inner_mut().set_page_count(page_count); + instance } fn word_index(&self) -> usize { @@ -64,15 +73,7 @@ impl<const N: usize> ShareWords<N> { } /// Display the first page with user information. - fn render_entry_page(&self) { - display( - self.area - .top_left() - .ofs(Offset::y(Font::BOLD.line_height())), - &self.title, - Font::BOLD, - ); - + fn paint_entry_page(&mut self) { text_multiline( self.area.split_top(15).1, &build_string!( @@ -88,8 +89,7 @@ impl<const N: usize> ShareWords<N> { } /// Display the second page with user information. - fn render_second_page(&self) { - // Creating a small vertical distance to make it centered + fn paint_second_page(&mut self) { text_multiline( self.area.split_top(15).1, "Do NOT make\ndigital copies!", @@ -100,9 +100,7 @@ impl<const N: usize> ShareWords<N> { } /// Display the final page with user confirmation. - fn render_final_page(&self) { - // Moving vertically down to avoid collision with the scrollbar - // and to look better. + fn paint_final_page(&mut self) { text_multiline( self.area.split_top(12).1, &build_string!( @@ -118,7 +116,7 @@ impl<const N: usize> ShareWords<N> { } /// Display current set of recovery words. - fn render_words(&self) { + fn paint_words(&mut self) { let mut y_offset = 0; // Showing the word index and the words itself for i in 0..WORDS_PER_PAGE { @@ -139,23 +137,37 @@ impl<const N: usize> Component for ShareWords<N> { type Msg = Never; fn place(&mut self, bounds: Rect) -> Rect { + let (title_area, _) = bounds.split_top(theme::FONT_HEADER.line_height()); + + let (title_area, scrollbar_area) = + title_area.split_right(self.scrollbar.inner().overall_width() + SCROLLBAR_SPACE); + + self.title.place(title_area); + self.scrollbar.place(scrollbar_area); + self.area = bounds; self.area } - fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> { + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> { + self.title.event(ctx, event); + self.scrollbar.event(ctx, event); None } fn paint(&mut self) { + // Showing scrollbar in all cases + // Individual pages are responsible for not colliding with it + self.scrollbar.paint(); if self.is_entry_page() { - self.render_entry_page(); + self.title.paint(); + self.paint_entry_page(); } else if self.is_second_page() { - self.render_second_page(); + self.paint_second_page(); } else if self.is_final_page() { - self.render_final_page(); + self.paint_final_page(); } else { - self.render_words(); + self.paint_words(); } } } @@ -168,6 +180,7 @@ impl<const N: usize> Paginate for ShareWords<N> { fn change_page(&mut self, active_page: usize) { self.page_index = active_page; + self.scrollbar.inner_mut().set_active_page(active_page); } } diff --git a/core/embed/rust/src/ui/model_tr/component/title.rs b/core/embed/rust/src/ui/model_tr/component/title.rs new file mode 100644 index 0000000000..6543acd4f3 --- /dev/null +++ b/core/embed/rust/src/ui/model_tr/component/title.rs @@ -0,0 +1,108 @@ +use crate::{ + micropython::buffer::StrBuffer, + time::Instant, + ui::{ + component::{Component, Event, EventCtx, Marquee, Never}, + display, + geometry::{Offset, Rect}, + model_tr::theme, + }, +}; + +pub struct Title { + area: Rect, + title: StrBuffer, + marquee: Marquee, + needs_marquee: bool, + centered: bool, +} + +impl Title { + pub fn new(title: StrBuffer) -> Self { + Self { + title, + marquee: Marquee::new(title, theme::FONT_HEADER, theme::FG, theme::BG), + needs_marquee: false, + area: Rect::zero(), + centered: false, + } + } + + pub fn with_centered(mut self) -> Self { + self.centered = true; + self + } + + /// Display title/header at the top left of the given area. + /// Returning the painted height of the whole header. + pub fn paint_header_left(title: StrBuffer, area: Rect) -> i16 { + let text_height = theme::FONT_HEADER.text_height(); + let title_baseline = area.top_left() + Offset::y(text_height - 1); + display::text_left( + title_baseline, + title.as_ref(), + theme::FONT_HEADER, + theme::FG, + theme::BG, + ); + text_height + } + + /// Display title/header centered at the top of the given area. + /// Returning the painted height of the whole header. + pub fn paint_header_centered(title: StrBuffer, area: Rect) -> i16 { + let text_height = theme::FONT_HEADER.text_height(); + let title_baseline = area.top_center() + Offset::y(text_height - 1); + display::text_center( + title_baseline, + title.as_ref(), + theme::FONT_HEADER, + theme::FG, + theme::BG, + ); + text_height + } +} + +impl Component for Title { + type Msg = Never; + + fn place(&mut self, bounds: Rect) -> Rect { + self.area = bounds; + self.marquee.place(bounds); + let width = theme::FONT_HEADER.text_width(self.title.as_ref()); + self.needs_marquee = width > self.area.width(); + bounds + } + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> { + if self.needs_marquee { + if !self.marquee.is_animating() { + self.marquee.start(ctx, Instant::now()); + } + return self.marquee.event(ctx, event); + } + None + } + + fn paint(&mut self) { + if self.needs_marquee { + self.marquee.paint(); + } else if self.centered { + Self::paint_header_centered(self.title, self.area); + } else { + Self::paint_header_left(self.title, self.area); + } + } +} + +// DEBUG-ONLY SECTION BELOW + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for Title { + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.open("Title"); + t.title(self.title.as_ref()); + t.close(); + } +} diff --git a/core/embed/rust/src/ui/model_tr/layout.rs b/core/embed/rust/src/ui/model_tr/layout.rs index 29ed6280f0..e227d78497 100644 --- a/core/embed/rust/src/ui/model_tr/layout.rs +++ b/core/embed/rust/src/ui/model_tr/layout.rs @@ -32,6 +32,7 @@ use crate::{ result::{CANCELLED, CONFIRMED, INFO}, util::{iter_into_objs, iter_into_vec, upy_disable_animation}, }, + model_tr::component::{ScrollableContent, ScrollableFrame}, }, }; @@ -153,6 +154,15 @@ where } } +impl<T> ComponentMsgObj for ScrollableFrame<T> +where + T: ComponentMsgObj + ScrollableContent, +{ + fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> { + self.inner().msg_try_into_obj(msg) + } +} + impl ComponentMsgObj for Progress { fn msg_try_into_obj(&self, _msg: Self::Msg) -> Result<Obj, Error> { unreachable!() @@ -233,9 +243,9 @@ extern "C" fn new_confirm_action(n_args: usize, args: *const Obj, kwargs: *mut M .with_confirm_btn(confirm_btn); let obj = if title.as_ref().is_empty() { - LayoutObj::new(content)? + LayoutObj::new(ScrollableFrame::new(content))? } else { - LayoutObj::new(Frame::new(title, content))? + LayoutObj::new(ScrollableFrame::new(content).with_title(title))? }; Ok(obj.into()) @@ -275,7 +285,7 @@ extern "C" fn new_confirm_properties(n_args: usize, args: *const Obj, kwargs: *m let confirm_btn = Some(ButtonDetails::text("CONFIRM".into()).with_default_duration()); content = content.with_confirm_btn(confirm_btn); } - let obj = LayoutObj::new(Frame::new(title, content))?; + let obj = LayoutObj::new(ScrollableFrame::new(content).with_title(title))?; Ok(obj.into()) }; unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }