mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-07-19 21:18:14 +00:00
feat(core/rust): use marquee in TR titles
[no changelog]
This commit is contained in:
parent
d768d56e07
commit
989f2cc872
@ -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::Msg> {
|
||||
self.mutate(ctx, |ctx, c| {
|
||||
// Handle the internal invalidation event here, so components don't have to. We
|
||||
|
@ -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.
|
||||
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.
|
||||
if !animation_disabled() {
|
||||
ctx.request_paint();
|
||||
}
|
||||
// There is further progress in the animation, request an animation frame
|
||||
// event.
|
||||
ctx.request_anim_frame();
|
||||
|
@ -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<T: AsRef<str>>(baseline: Point, text: &T, font: Font) {
|
||||
pub fn display_right<T: AsRef<str>>(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<T: AsRef<str>>(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<T: AsRef<str>>(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
|
||||
}
|
||||
|
@ -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<F, const M: usize> {
|
||||
/// Instance of the current Page
|
||||
current_page: Page<M>,
|
||||
/// Title being shown at the top in bold
|
||||
common_title: Option<StrBuffer>,
|
||||
title: Option<Title>,
|
||||
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);
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ use crate::{
|
||||
display::{self, Color, Font},
|
||||
geometry::{Offset, Rect},
|
||||
model_tr::theme,
|
||||
util::animation_disabled,
|
||||
},
|
||||
};
|
||||
|
||||
@ -188,10 +189,12 @@ 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.
|
||||
if !animation_disabled() {
|
||||
ctx.request_paint();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
|
@ -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},
|
||||
|
@ -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)
|
||||
|
@ -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) {
|
||||
if active_page != self.active_page {
|
||||
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);
|
||||
}
|
||||
|
||||
/// 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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
108
core/embed/rust/src/ui/model_tr/component/title.rs
Normal file
108
core/embed/rust/src/ui/model_tr/component/title.rs
Normal file
@ -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();
|
||||
}
|
||||
}
|
@ -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) }
|
||||
|
Loading…
Reference in New Issue
Block a user