1
0
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:
tychovrahe 2023-01-09 17:49:26 +01:00 committed by grdddj
parent d768d56e07
commit 989f2cc872
13 changed files with 370 additions and 209 deletions

View File

@ -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

View File

@ -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();

View File

@ -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
}

View File

@ -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);

View File

@ -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

View File

@ -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();
}

View File

@ -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
}

View File

@ -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},

View File

@ -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)

View File

@ -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);

View File

@ -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);
}
}

View 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();
}
}

View File

@ -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) }