mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-07-20 21:48: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.
|
/// No painting should be done in this phase.
|
||||||
fn place(&mut self, bounds: Rect) -> Rect;
|
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.
|
/// React to an outside event. See the `Event` type for possible cases.
|
||||||
///
|
///
|
||||||
/// Component should modify its internal state as a response to the event,
|
/// Component should modify its internal state as a response to the event,
|
||||||
@ -145,10 +131,6 @@ where
|
|||||||
self.component.place(bounds)
|
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> {
|
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||||
self.mutate(ctx, |ctx, c| {
|
self.mutate(ctx, |ctx, c| {
|
||||||
// Handle the internal invalidation event here, so components don't have to. We
|
// Handle the internal invalidation event here, so components don't have to. We
|
||||||
|
@ -7,6 +7,7 @@ use crate::{
|
|||||||
display,
|
display,
|
||||||
display::{Color, Font},
|
display::{Color, Font},
|
||||||
geometry::Rect,
|
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.
|
// We have something to paint, so request to be painted in the next pass.
|
||||||
|
if !animation_disabled() {
|
||||||
ctx.request_paint();
|
ctx.request_paint();
|
||||||
|
}
|
||||||
// There is further progress in the animation, request an animation frame event.
|
// There is further progress in the animation, request an animation frame event.
|
||||||
ctx.request_anim_frame();
|
ctx.request_anim_frame();
|
||||||
}
|
}
|
||||||
@ -169,7 +172,9 @@ impl Component for Marquee {
|
|||||||
if token == EventCtx::ANIM_FRAME_TIMER {
|
if token == EventCtx::ANIM_FRAME_TIMER {
|
||||||
if self.is_animating() {
|
if self.is_animating() {
|
||||||
// We have something to paint, so request to be painted in the next pass.
|
// We have something to paint, so request to be painted in the next pass.
|
||||||
|
if !animation_disabled() {
|
||||||
ctx.request_paint();
|
ctx.request_paint();
|
||||||
|
}
|
||||||
// There is further progress in the animation, request an animation frame
|
// There is further progress in the animation, request an animation frame
|
||||||
// event.
|
// event.
|
||||||
ctx.request_anim_frame();
|
ctx.request_anim_frame();
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use crate::ui::{
|
use crate::ui::{
|
||||||
display::{self, Font},
|
display::{self, Font},
|
||||||
geometry::{Offset, Point, Rect},
|
geometry::Point,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::theme;
|
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) {
|
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::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::{
|
ui::{
|
||||||
component::{Child, Component, ComponentExt, Event, EventCtx, Pad},
|
component::{Child, Component, ComponentExt, Event, EventCtx, Pad},
|
||||||
geometry::Rect,
|
geometry::Rect,
|
||||||
|
model_tr::component::{scrollbar::SCROLLBAR_SPACE, title::Title},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
common, theme, ButtonAction, ButtonController, ButtonControllerMsg, ButtonLayout, ButtonPos,
|
theme, ButtonAction, ButtonController, ButtonControllerMsg, ButtonLayout, ButtonPos, FlowPages,
|
||||||
FlowPages, Page, ScrollBar,
|
Page, ScrollBar,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// To be returned directly from Flow.
|
/// To be returned directly from Flow.
|
||||||
@ -24,7 +25,7 @@ pub struct Flow<F, const M: usize> {
|
|||||||
/// Instance of the current Page
|
/// Instance of the current Page
|
||||||
current_page: Page<M>,
|
current_page: Page<M>,
|
||||||
/// Title being shown at the top in bold
|
/// Title being shown at the top in bold
|
||||||
common_title: Option<StrBuffer>,
|
title: Option<Title>,
|
||||||
scrollbar: Child<ScrollBar>,
|
scrollbar: Child<ScrollBar>,
|
||||||
content_area: Rect,
|
content_area: Rect,
|
||||||
title_area: Rect,
|
title_area: Rect,
|
||||||
@ -43,7 +44,7 @@ where
|
|||||||
Self {
|
Self {
|
||||||
pages,
|
pages,
|
||||||
current_page,
|
current_page,
|
||||||
common_title: None,
|
title: None,
|
||||||
content_area: Rect::zero(),
|
content_area: Rect::zero(),
|
||||||
title_area: Rect::zero(),
|
title_area: Rect::zero(),
|
||||||
scrollbar: Child::new(ScrollBar::to_be_filled_later()),
|
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
|
/// Adding a common title to all pages. The title will not be colliding
|
||||||
/// with the page content, as the content will be offset.
|
/// with the page content, as the content will be offset.
|
||||||
pub fn with_common_title(mut self, title: StrBuffer) -> Self {
|
pub fn with_common_title(mut self, title: StrBuffer) -> Self {
|
||||||
self.common_title = Some(title);
|
self.title = Some(Title::new(title));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,8 +76,11 @@ where
|
|||||||
/// position.
|
/// position.
|
||||||
fn change_current_page(&mut self) {
|
fn change_current_page(&mut self) {
|
||||||
self.current_page = self.pages.get(self.page_counter);
|
self.current_page = self.pages.get(self.page_counter);
|
||||||
if self.common_title.is_some() && let Some(title) = self.current_page.title() {
|
if self.title.is_some() {
|
||||||
self.common_title = Some(title);
|
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
|
let scrollbar_active_index = self
|
||||||
.pages
|
.pages
|
||||||
@ -148,12 +152,20 @@ where
|
|||||||
fn event_consumed_by_current_choice(&mut self, ctx: &mut EventCtx, pos: ButtonPos) -> bool {
|
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() {
|
if matches!(pos, ButtonPos::Left) && self.current_page.has_prev_page() {
|
||||||
self.current_page.go_to_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);
|
self.update(ctx, false);
|
||||||
true
|
true
|
||||||
} else if matches!(pos, ButtonPos::Right) && self.current_page.has_next_page() {
|
} else if matches!(pos, ButtonPos::Right) && self.current_page.has_next_page() {
|
||||||
self.current_page.go_to_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);
|
self.update(ctx, false);
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
@ -171,22 +183,27 @@ where
|
|||||||
fn place(&mut self, bounds: Rect) -> Rect {
|
fn place(&mut self, bounds: Rect) -> Rect {
|
||||||
let (title_content_area, button_area) = bounds.split_bottom(theme::BUTTON_HEIGHT);
|
let (title_content_area, button_area) = bounds.split_bottom(theme::BUTTON_HEIGHT);
|
||||||
// Accounting for possible title
|
// 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())
|
title_content_area.split_top(theme::FONT_HEADER.line_height())
|
||||||
} else {
|
} else {
|
||||||
(Rect::zero(), title_content_area)
|
(Rect::zero(), title_content_area)
|
||||||
};
|
};
|
||||||
self.title_area = title_area;
|
|
||||||
self.content_area = content_area;
|
self.content_area = content_area;
|
||||||
|
|
||||||
// Placing a scrollbar in case the title is there
|
// 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
|
// Finding out the total amount of pages in this flow
|
||||||
let complete_page_count = self.pages.scrollbar_page_count(content_area);
|
let complete_page_count = self.pages.scrollbar_page_count(content_area);
|
||||||
self.scrollbar
|
self.scrollbar
|
||||||
.inner_mut()
|
.inner_mut()
|
||||||
.set_page_count(complete_page_count);
|
.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.
|
// 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> {
|
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);
|
let button_event = self.buttons.event(ctx, event);
|
||||||
|
|
||||||
// Do something when a button was triggered
|
// Do something when a button was triggered
|
||||||
@ -249,9 +267,9 @@ where
|
|||||||
fn paint(&mut self) {
|
fn paint(&mut self) {
|
||||||
self.pad.paint();
|
self.pad.paint();
|
||||||
// Scrollbars are painted only with a title
|
// Scrollbars are painted only with a title
|
||||||
if let Some(title) = self.common_title {
|
if self.title.is_some() {
|
||||||
self.scrollbar.paint();
|
self.scrollbar.paint();
|
||||||
common::paint_header_left(title, self.title_area);
|
self.title.paint();
|
||||||
}
|
}
|
||||||
self.buttons.paint();
|
self.buttons.paint();
|
||||||
// On purpose painting current page at the end, after buttons,
|
// On purpose painting current page at the end, after buttons,
|
||||||
@ -296,8 +314,8 @@ where
|
|||||||
|
|
||||||
self.report_btn_actions(t);
|
self.report_btn_actions(t);
|
||||||
|
|
||||||
if let Some(title) = &self.common_title {
|
if self.title.is_some() {
|
||||||
t.title(title.as_ref());
|
t.field("title", &self.title);
|
||||||
}
|
}
|
||||||
t.field("content_area", &self.content_area);
|
t.field("content_area", &self.content_area);
|
||||||
t.field("buttons", &self.buttons);
|
t.field("buttons", &self.buttons);
|
||||||
|
@ -183,6 +183,10 @@ impl<const M: usize> Page<M> {
|
|||||||
pub fn go_to_next_page(&mut self) {
|
pub fn go_to_next_page(&mut self) {
|
||||||
self.current_page += 1;
|
self.current_page += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_current_page(&self) -> usize {
|
||||||
|
self.current_page
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// For `layout.rs` - single operations
|
// For `layout.rs` - single operations
|
||||||
|
@ -1,19 +1,16 @@
|
|||||||
use super::{common, theme, ScrollBar};
|
use super::{theme, ScrollBar};
|
||||||
use crate::{
|
use crate::{
|
||||||
micropython::buffer::StrBuffer,
|
micropython::buffer::StrBuffer,
|
||||||
ui::{
|
ui::{
|
||||||
component::{Child, Component, Event, EventCtx},
|
component::{Child, Component, ComponentExt, Event, EventCtx},
|
||||||
geometry::{Insets, Rect},
|
geometry::{Insets, Rect},
|
||||||
|
model_tr::component::{scrollbar::SCROLLBAR_SPACE, title::Title},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Component for holding another component and displaying a title.
|
/// Component for holding another component and displaying a title.
|
||||||
/// Also is allocating space for a scrollbar.
|
|
||||||
pub struct Frame<T> {
|
pub struct Frame<T> {
|
||||||
area: Rect,
|
title: Title,
|
||||||
title: StrBuffer,
|
|
||||||
title_centered: bool,
|
|
||||||
account_for_scrollbar: bool,
|
|
||||||
content: Child<T>,
|
content: Child<T>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -23,10 +20,7 @@ where
|
|||||||
{
|
{
|
||||||
pub fn new(title: StrBuffer, content: T) -> Self {
|
pub fn new(title: StrBuffer, content: T) -> Self {
|
||||||
Self {
|
Self {
|
||||||
title,
|
title: Title::new(title),
|
||||||
area: Rect::zero(),
|
|
||||||
title_centered: false,
|
|
||||||
account_for_scrollbar: true,
|
|
||||||
content: Child::new(content),
|
content: Child::new(content),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -36,16 +30,8 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Aligning the title to the center, instead of the left.
|
/// 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 {
|
pub fn with_title_centered(mut self) -> Self {
|
||||||
self.title_centered = true;
|
self.title = self.title.with_centered();
|
||||||
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
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -59,36 +45,105 @@ where
|
|||||||
fn place(&mut self, bounds: Rect) -> Rect {
|
fn place(&mut self, bounds: Rect) -> Rect {
|
||||||
const TITLE_SPACE: i16 = 2;
|
const TITLE_SPACE: i16 = 2;
|
||||||
|
|
||||||
let (title_and_scrollbar_area, content_area) =
|
let (title_area, content_area) = bounds.split_top(theme::FONT_HEADER.line_height());
|
||||||
bounds.split_top(theme::FONT_HEADER.line_height());
|
|
||||||
let content_area = content_area.inset(Insets::top(TITLE_SPACE));
|
let content_area = content_area.inset(Insets::top(TITLE_SPACE));
|
||||||
|
|
||||||
// Title area is different based on scrollbar.
|
self.title.place(title_area);
|
||||||
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.content.place(content_area);
|
self.content.place(content_area);
|
||||||
bounds
|
bounds
|
||||||
}
|
}
|
||||||
|
|
||||||
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.content.event(ctx, event)
|
self.content.event(ctx, event)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn paint(&mut self) {
|
fn paint(&mut self) {
|
||||||
if self.title_centered {
|
self.title.paint();
|
||||||
common::paint_header_centered(&self.title, self.area);
|
self.content.paint();
|
||||||
} else {
|
|
||||||
common::paint_header_left(&self.title, self.area);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
self.content.paint();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -102,7 +157,23 @@ where
|
|||||||
{
|
{
|
||||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||||
t.open("Frame");
|
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.field("content", &self.content);
|
||||||
t.close();
|
t.close();
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ use crate::{
|
|||||||
display::{self, Color, Font},
|
display::{self, Color, Font},
|
||||||
geometry::{Offset, Rect},
|
geometry::{Offset, Rect},
|
||||||
model_tr::theme,
|
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.
|
// There is further progress in the animation, request an animation frame event.
|
||||||
ctx.request_anim_frame();
|
ctx.request_anim_frame();
|
||||||
// We have something to paint, so request to be painted in the next pass.
|
// We have something to paint, so request to be painted in the next pass.
|
||||||
|
if !animation_disabled() {
|
||||||
ctx.request_paint();
|
ctx.request_paint();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ mod result_anim;
|
|||||||
mod result_popup;
|
mod result_popup;
|
||||||
mod scrollbar;
|
mod scrollbar;
|
||||||
mod share_words;
|
mod share_words;
|
||||||
|
mod title;
|
||||||
|
|
||||||
use super::theme;
|
use super::theme;
|
||||||
|
|
||||||
@ -30,7 +31,7 @@ pub use button_controller::{ButtonController, ButtonControllerMsg};
|
|||||||
pub use changing_text::ChangingTextLine;
|
pub use changing_text::ChangingTextLine;
|
||||||
pub use flow::{Flow, FlowMsg};
|
pub use flow::{Flow, FlowMsg};
|
||||||
pub use flow_pages::{FlowPages, Page};
|
pub use flow_pages::{FlowPages, Page};
|
||||||
pub use frame::Frame;
|
pub use frame::{Frame, ScrollableContent, ScrollableFrame};
|
||||||
pub use homescreen::{Homescreen, HomescreenMsg, Lockscreen};
|
pub use homescreen::{Homescreen, HomescreenMsg, Lockscreen};
|
||||||
pub use input_methods::{
|
pub use input_methods::{
|
||||||
choice::{Choice, ChoiceFactory, ChoicePage, ChoicePageMsg},
|
choice::{Choice, ChoiceFactory, ChoicePage, ChoicePageMsg},
|
||||||
|
@ -4,15 +4,12 @@ use crate::ui::{
|
|||||||
geometry::{Insets, Rect},
|
geometry::{Insets, Rect},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{theme, ButtonController, ButtonControllerMsg, ButtonDetails, ButtonLayout, ButtonPos};
|
||||||
theme, ButtonController, ButtonControllerMsg, ButtonDetails, ButtonLayout, ButtonPos, ScrollBar,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct ButtonPage<T> {
|
pub struct ButtonPage<T> {
|
||||||
|
page_count: usize,
|
||||||
|
active_page: usize,
|
||||||
content: Child<T>,
|
content: Child<T>,
|
||||||
scrollbar: Child<ScrollBar>,
|
|
||||||
/// Optional available area for scrollbar defined by parent component.
|
|
||||||
parent_scrollbar_area: Option<Rect>,
|
|
||||||
pad: Pad,
|
pad: Pad,
|
||||||
/// Left button of the first screen
|
/// Left button of the first screen
|
||||||
cancel_btn_details: Option<ButtonDetails>,
|
cancel_btn_details: Option<ButtonDetails>,
|
||||||
@ -25,8 +22,6 @@ pub struct ButtonPage<T> {
|
|||||||
/// Right button of every screen apart the last one
|
/// Right button of every screen apart the last one
|
||||||
next_btn_details: Option<ButtonDetails>,
|
next_btn_details: Option<ButtonDetails>,
|
||||||
buttons: Child<ButtonController>,
|
buttons: Child<ButtonController>,
|
||||||
/// Scrollbar may or may not be shown (but will be counting pages anyway).
|
|
||||||
show_scrollbar: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> ButtonPage<T>
|
impl<T> ButtonPage<T>
|
||||||
@ -35,9 +30,9 @@ where
|
|||||||
{
|
{
|
||||||
pub fn new(content: T, background: Color) -> Self {
|
pub fn new(content: T, background: Color) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
page_count: 0, // will be set in place()
|
||||||
|
active_page: 0,
|
||||||
content: Child::new(content),
|
content: Child::new(content),
|
||||||
scrollbar: Child::new(ScrollBar::to_be_filled_later()),
|
|
||||||
parent_scrollbar_area: None,
|
|
||||||
pad: Pad::with_background(background).with_clear(),
|
pad: Pad::with_background(background).with_clear(),
|
||||||
cancel_btn_details: Some(ButtonDetails::cancel_icon()),
|
cancel_btn_details: Some(ButtonDetails::cancel_icon()),
|
||||||
confirm_btn_details: Some(ButtonDetails::text("CONFIRM".into())),
|
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
|
// Initial button layout will be set in `place()` after we can call
|
||||||
// `content.page_count()`.
|
// `content.page_count()`.
|
||||||
buttons: Child::new(ButtonController::new(ButtonLayout::empty())),
|
buttons: Child::new(ButtonController::new(ButtonLayout::empty())),
|
||||||
show_scrollbar: true,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,9 +66,20 @@ where
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_scrollbar(mut self, show: bool) -> Self {
|
pub fn has_next_page(&self) -> bool {
|
||||||
self.show_scrollbar = show;
|
self.active_page < self.page_count - 1
|
||||||
self
|
}
|
||||||
|
|
||||||
|
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
|
/// 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
|
/// Change the page in the content, clear the background under it and make
|
||||||
/// sure it gets completely repainted. Also updating the buttons.
|
/// sure it gets completely repainted. Also updating the buttons.
|
||||||
fn change_page(&mut self, ctx: &mut EventCtx) {
|
fn change_page(&mut self, ctx: &mut EventCtx) {
|
||||||
let active_page = self.scrollbar.inner().active_page;
|
self.content.inner_mut().change_page(self.active_page);
|
||||||
self.content.inner_mut().change_page(active_page);
|
|
||||||
self.content.request_complete_repaint(ctx);
|
self.content.request_complete_repaint(ctx);
|
||||||
self.scrollbar.request_complete_repaint(ctx);
|
|
||||||
self.update_buttons(ctx);
|
self.update_buttons(ctx);
|
||||||
self.pad.clear();
|
self.pad.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reflecting the current page in the buttons.
|
/// Reflecting the current page in the buttons.
|
||||||
fn update_buttons(&mut self, ctx: &mut EventCtx) {
|
fn update_buttons(&mut self, ctx: &mut EventCtx) {
|
||||||
let btn_layout = self.get_button_layout(
|
let btn_layout = self.get_button_layout(self.has_previous_page(), self.has_next_page());
|
||||||
self.scrollbar.inner().has_previous_page(),
|
|
||||||
self.scrollbar.inner().has_next_page(),
|
|
||||||
);
|
|
||||||
self.buttons.mutate(ctx, |_ctx, buttons| {
|
self.buttons.mutate(ctx, |_ctx, buttons| {
|
||||||
buttons.set(btn_layout);
|
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>
|
impl<T> Component for ButtonPage<T>
|
||||||
where
|
where
|
||||||
T: Component + Paginate,
|
T: Component + Paginate,
|
||||||
@ -151,36 +160,23 @@ where
|
|||||||
self.content.place(content_area);
|
self.content.place(content_area);
|
||||||
// Need to be called here, only after content is placed
|
// Need to be called here, only after content is placed
|
||||||
// and we can calculate the page count.
|
// and we can calculate the page count.
|
||||||
let page_count = self.content.inner_mut().page_count();
|
self.page_count = self.content.inner_mut().page_count();
|
||||||
self.scrollbar.inner_mut().set_page_count(page_count);
|
self.set_buttons_for_initial_page(self.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.buttons.place(button_area);
|
self.buttons.place(button_area);
|
||||||
bounds
|
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> {
|
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);
|
let button_event = self.buttons.event(ctx, event);
|
||||||
|
|
||||||
if let Some(ButtonControllerMsg::Triggered(pos)) = button_event {
|
if let Some(ButtonControllerMsg::Triggered(pos)) = button_event {
|
||||||
match pos {
|
match pos {
|
||||||
ButtonPos::Left => {
|
ButtonPos::Left => {
|
||||||
if self.scrollbar.inner().has_previous_page() {
|
if self.has_previous_page() {
|
||||||
// Clicked BACK. Scroll up.
|
// Clicked BACK. Scroll up.
|
||||||
self.scrollbar.inner_mut().go_to_previous_page();
|
self.go_to_previous_page();
|
||||||
self.change_page(ctx);
|
self.change_page(ctx);
|
||||||
} else {
|
} else {
|
||||||
// Clicked CANCEL. Send result.
|
// Clicked CANCEL. Send result.
|
||||||
@ -188,9 +184,9 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ButtonPos::Right => {
|
ButtonPos::Right => {
|
||||||
if self.scrollbar.inner().has_next_page() {
|
if self.has_next_page() {
|
||||||
// Clicked NEXT. Scroll down.
|
// Clicked NEXT. Scroll down.
|
||||||
self.scrollbar.inner_mut().go_to_next_page();
|
self.go_to_next_page();
|
||||||
self.change_page(ctx);
|
self.change_page(ctx);
|
||||||
} else {
|
} else {
|
||||||
// Clicked CONFIRM. Send result.
|
// Clicked CONFIRM. Send result.
|
||||||
@ -210,9 +206,6 @@ where
|
|||||||
fn paint(&mut self) {
|
fn paint(&mut self) {
|
||||||
self.pad.paint();
|
self.pad.paint();
|
||||||
self.content.paint();
|
self.content.paint();
|
||||||
if self.show_scrollbar {
|
|
||||||
self.scrollbar.paint();
|
|
||||||
}
|
|
||||||
self.buttons.paint();
|
self.buttons.paint();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -221,18 +214,19 @@ where
|
|||||||
|
|
||||||
#[cfg(feature = "ui_debug")]
|
#[cfg(feature = "ui_debug")]
|
||||||
use super::ButtonAction;
|
use super::ButtonAction;
|
||||||
|
use crate::ui::model_tr::component::frame::ScrollableContent;
|
||||||
#[cfg(feature = "ui_debug")]
|
#[cfg(feature = "ui_debug")]
|
||||||
use heapless::String;
|
use heapless::String;
|
||||||
|
|
||||||
#[cfg(feature = "ui_debug")]
|
#[cfg(feature = "ui_debug")]
|
||||||
impl<T> crate::trace::Trace for ButtonPage<T>
|
impl<T> crate::trace::Trace for ButtonPage<T>
|
||||||
where
|
where
|
||||||
T: crate::trace::Trace,
|
T: crate::trace::Trace + Paginate + Component,
|
||||||
{
|
{
|
||||||
fn get_btn_action(&self, pos: ButtonPos) -> String<25> {
|
fn get_btn_action(&self, pos: ButtonPos) -> String<25> {
|
||||||
match pos {
|
match pos {
|
||||||
ButtonPos::Left => {
|
ButtonPos::Left => {
|
||||||
if self.scrollbar.inner().has_previous_page() {
|
if self.has_previous_page() {
|
||||||
ButtonAction::PrevPage.string()
|
ButtonAction::PrevPage.string()
|
||||||
} else if self.cancel_btn_details.is_some() {
|
} else if self.cancel_btn_details.is_some() {
|
||||||
ButtonAction::Cancel.string()
|
ButtonAction::Cancel.string()
|
||||||
@ -241,7 +235,7 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ButtonPos::Right => {
|
ButtonPos::Right => {
|
||||||
if self.scrollbar.inner().has_next_page() {
|
if self.has_next_page() {
|
||||||
ButtonAction::NextPage.string()
|
ButtonAction::NextPage.string()
|
||||||
} else if self.confirm_btn_details.is_some() {
|
} else if self.confirm_btn_details.is_some() {
|
||||||
ButtonAction::Confirm.string()
|
ButtonAction::Confirm.string()
|
||||||
@ -255,14 +249,8 @@ where
|
|||||||
|
|
||||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||||
t.open("ButtonPage");
|
t.open("ButtonPage");
|
||||||
t.kw_pair(
|
t.kw_pair("active_page", inttostr!(self.active_page as u8));
|
||||||
"active_page",
|
t.kw_pair("page_count", inttostr!(self.page_count as u8));
|
||||||
inttostr!(self.scrollbar.inner().active_page as u8),
|
|
||||||
);
|
|
||||||
t.kw_pair(
|
|
||||||
"page_count",
|
|
||||||
inttostr!(self.scrollbar.inner().page_count as u8),
|
|
||||||
);
|
|
||||||
self.report_btn_actions(t);
|
self.report_btn_actions(t);
|
||||||
// TODO: it seems the button text is not updated when paginating (but actions
|
// TODO: it seems the button text is not updated when paginating (but actions
|
||||||
// above are)
|
// above are)
|
||||||
|
@ -23,6 +23,8 @@ enum DotType {
|
|||||||
Small, // .
|
Small, // .
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const SCROLLBAR_SPACE: i16 = 5;
|
||||||
|
|
||||||
/// How many dots at most will there be
|
/// How many dots at most will there be
|
||||||
const MAX_DOTS: usize = 5;
|
const MAX_DOTS: usize = 5;
|
||||||
|
|
||||||
@ -61,23 +63,9 @@ impl ScrollBar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_active_page(&mut self, active_page: usize) {
|
pub fn set_active_page(&mut self, active_page: usize) {
|
||||||
|
if active_page != self.active_page {
|
||||||
self.active_page = 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.
|
/// 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),
|
// Occupying as little space as possible (according to the number of pages),
|
||||||
// aligning to the right.
|
// aligning to the right.
|
||||||
let scrollbar_area = Rect::from_top_right_and_size(
|
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),
|
Offset::new(self.overall_width(), Self::MAX_DOT_SIZE),
|
||||||
);
|
);
|
||||||
self.pad.place(scrollbar_area);
|
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 heapless::{String, Vec};
|
||||||
|
|
||||||
use super::common::display;
|
use super::common::display;
|
||||||
@ -22,19 +26,24 @@ const WORD_FONT: Font = Font::NORMAL;
|
|||||||
/// Showing the given share words.
|
/// Showing the given share words.
|
||||||
pub struct ShareWords<const N: usize> {
|
pub struct ShareWords<const N: usize> {
|
||||||
area: Rect,
|
area: Rect,
|
||||||
title: StrBuffer,
|
title: Child<Title>,
|
||||||
|
scrollbar: Child<ScrollBar>,
|
||||||
share_words: Vec<StrBuffer, N>,
|
share_words: Vec<StrBuffer, N>,
|
||||||
page_index: usize,
|
page_index: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<const N: usize> ShareWords<N> {
|
impl<const N: usize> ShareWords<N> {
|
||||||
pub fn new(title: StrBuffer, share_words: Vec<StrBuffer, N>) -> Self {
|
pub fn new(title: StrBuffer, share_words: Vec<StrBuffer, N>) -> Self {
|
||||||
Self {
|
let mut instance = Self {
|
||||||
area: Rect::zero(),
|
area: Rect::zero(),
|
||||||
title,
|
title: Child::new(Title::new(title)),
|
||||||
|
scrollbar: Child::new(ScrollBar::to_be_filled_later()),
|
||||||
share_words,
|
share_words,
|
||||||
page_index: 0,
|
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 {
|
fn word_index(&self) -> usize {
|
||||||
@ -64,15 +73,7 @@ impl<const N: usize> ShareWords<N> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Display the first page with user information.
|
/// Display the first page with user information.
|
||||||
fn render_entry_page(&self) {
|
fn paint_entry_page(&mut self) {
|
||||||
display(
|
|
||||||
self.area
|
|
||||||
.top_left()
|
|
||||||
.ofs(Offset::y(Font::BOLD.line_height())),
|
|
||||||
&self.title,
|
|
||||||
Font::BOLD,
|
|
||||||
);
|
|
||||||
|
|
||||||
text_multiline(
|
text_multiline(
|
||||||
self.area.split_top(15).1,
|
self.area.split_top(15).1,
|
||||||
&build_string!(
|
&build_string!(
|
||||||
@ -88,8 +89,7 @@ impl<const N: usize> ShareWords<N> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Display the second page with user information.
|
/// Display the second page with user information.
|
||||||
fn render_second_page(&self) {
|
fn paint_second_page(&mut self) {
|
||||||
// Creating a small vertical distance to make it centered
|
|
||||||
text_multiline(
|
text_multiline(
|
||||||
self.area.split_top(15).1,
|
self.area.split_top(15).1,
|
||||||
"Do NOT make\ndigital copies!",
|
"Do NOT make\ndigital copies!",
|
||||||
@ -100,9 +100,7 @@ impl<const N: usize> ShareWords<N> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Display the final page with user confirmation.
|
/// Display the final page with user confirmation.
|
||||||
fn render_final_page(&self) {
|
fn paint_final_page(&mut self) {
|
||||||
// Moving vertically down to avoid collision with the scrollbar
|
|
||||||
// and to look better.
|
|
||||||
text_multiline(
|
text_multiline(
|
||||||
self.area.split_top(12).1,
|
self.area.split_top(12).1,
|
||||||
&build_string!(
|
&build_string!(
|
||||||
@ -118,7 +116,7 @@ impl<const N: usize> ShareWords<N> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Display current set of recovery words.
|
/// Display current set of recovery words.
|
||||||
fn render_words(&self) {
|
fn paint_words(&mut self) {
|
||||||
let mut y_offset = 0;
|
let mut y_offset = 0;
|
||||||
// Showing the word index and the words itself
|
// Showing the word index and the words itself
|
||||||
for i in 0..WORDS_PER_PAGE {
|
for i in 0..WORDS_PER_PAGE {
|
||||||
@ -139,23 +137,37 @@ impl<const N: usize> Component for ShareWords<N> {
|
|||||||
type Msg = Never;
|
type Msg = Never;
|
||||||
|
|
||||||
fn place(&mut self, bounds: Rect) -> Rect {
|
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 = bounds;
|
||||||
self.area
|
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
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn paint(&mut self) {
|
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() {
|
if self.is_entry_page() {
|
||||||
self.render_entry_page();
|
self.title.paint();
|
||||||
|
self.paint_entry_page();
|
||||||
} else if self.is_second_page() {
|
} else if self.is_second_page() {
|
||||||
self.render_second_page();
|
self.paint_second_page();
|
||||||
} else if self.is_final_page() {
|
} else if self.is_final_page() {
|
||||||
self.render_final_page();
|
self.paint_final_page();
|
||||||
} else {
|
} 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) {
|
fn change_page(&mut self, active_page: usize) {
|
||||||
self.page_index = active_page;
|
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},
|
result::{CANCELLED, CONFIRMED, INFO},
|
||||||
util::{iter_into_objs, iter_into_vec, upy_disable_animation},
|
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 {
|
impl ComponentMsgObj for Progress {
|
||||||
fn msg_try_into_obj(&self, _msg: Self::Msg) -> Result<Obj, Error> {
|
fn msg_try_into_obj(&self, _msg: Self::Msg) -> Result<Obj, Error> {
|
||||||
unreachable!()
|
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);
|
.with_confirm_btn(confirm_btn);
|
||||||
|
|
||||||
let obj = if title.as_ref().is_empty() {
|
let obj = if title.as_ref().is_empty() {
|
||||||
LayoutObj::new(content)?
|
LayoutObj::new(ScrollableFrame::new(content))?
|
||||||
} else {
|
} else {
|
||||||
LayoutObj::new(Frame::new(title, content))?
|
LayoutObj::new(ScrollableFrame::new(content).with_title(title))?
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(obj.into())
|
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());
|
let confirm_btn = Some(ButtonDetails::text("CONFIRM".into()).with_default_duration());
|
||||||
content = content.with_confirm_btn(confirm_btn);
|
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())
|
Ok(obj.into())
|
||||||
};
|
};
|
||||||
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
||||||
|
Loading…
Reference in New Issue
Block a user