mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-07-23 15:08:19 +00:00
WIP - use horizontal scrollbar at the top right
This commit is contained in:
parent
1f35a64c3b
commit
ab8de63c5b
@ -38,6 +38,20 @@ 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,
|
||||
@ -131,6 +145,10 @@ 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
|
||||
|
@ -454,16 +454,24 @@ impl<T: Clone + AsRef<str>> ButtonDetails<T> {
|
||||
.with_offset(Offset::new(2, -2))
|
||||
}
|
||||
|
||||
/// Left arrow to signal going back.
|
||||
/// Left arrow to signal going back. No outline.
|
||||
pub fn left_arrow_icon() -> Self {
|
||||
Self::icon(Icon::new(theme::ICON_ARROW_LEFT)).with_no_outline()
|
||||
}
|
||||
|
||||
/// Right arrow to signal going forward.
|
||||
/// Right arrow to signal going forward. No outline.
|
||||
pub fn right_arrow_icon() -> Self {
|
||||
Self::icon(Icon::new(theme::ICON_ARROW_RIGHT)).with_no_outline()
|
||||
}
|
||||
|
||||
/// Up arrow to signal paginating back. No outline. Offsetted little right
|
||||
/// to not be on the boundary.
|
||||
pub fn up_arrow_icon() -> Self {
|
||||
Self::icon(Icon::new(theme::ICON_ARROW_UP))
|
||||
.with_no_outline()
|
||||
.with_offset(Offset::new(2, -3))
|
||||
}
|
||||
|
||||
/// Down arrow to signal paginating forward. Takes half the screen's width
|
||||
pub fn down_arrow_icon_wide() -> Self {
|
||||
Self::icon(Icon::new(theme::ICON_ARROW_DOWN)).force_width(HALF_SCREEN_BUTTON_WIDTH)
|
||||
|
@ -68,12 +68,12 @@ pub fn display_secret_center_top<T: AsRef<str>>(secret: T, offset_from_top: i16)
|
||||
}
|
||||
}
|
||||
|
||||
/// Display title/header centered at the top of the given area.
|
||||
/// Display title/header at the top left 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 {
|
||||
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_center() + Offset::y(text_heigth);
|
||||
display::text_center(
|
||||
let title_baseline = area.top_left() + Offset::y(text_heigth);
|
||||
display::text_left(
|
||||
title_baseline,
|
||||
title.as_ref(),
|
||||
theme::FONT_HEADER,
|
||||
|
@ -208,7 +208,7 @@ where
|
||||
// (not compatible with longer/centered titles)
|
||||
self.pad.paint();
|
||||
if let Some(title) = &self.common_title {
|
||||
common::paint_header_centered(title, self.title_area);
|
||||
common::paint_header_left(title, self.title_area);
|
||||
}
|
||||
self.current_page.paint();
|
||||
self.buttons.paint();
|
||||
|
@ -1,4 +1,4 @@
|
||||
use super::{common, theme};
|
||||
use super::{common, theme, ScrollBar};
|
||||
use crate::ui::{
|
||||
component::{Child, Component, Event, EventCtx},
|
||||
geometry::{Insets, Rect},
|
||||
@ -39,9 +39,15 @@ where
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
const TITLE_SPACE: i16 = 4;
|
||||
|
||||
let (title_area, content_area) = bounds.split_top(theme::FONT_HEADER.line_height());
|
||||
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(ScrollBar::MAX_WIDTH);
|
||||
|
||||
self.content.set_scrollbar_area(scrollbar_area);
|
||||
|
||||
self.area = title_area;
|
||||
self.content.place(content_area);
|
||||
bounds
|
||||
@ -52,7 +58,7 @@ where
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
common::paint_header_centered(&self.title, self.area);
|
||||
common::paint_header_left(&self.title, self.area);
|
||||
self.content.paint();
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ use crate::{
|
||||
ui::{
|
||||
component::{Child, Component, ComponentExt, Event, EventCtx, Pad, PageMsg, Paginate},
|
||||
display::Color,
|
||||
geometry::{Insets, Rect},
|
||||
geometry::{Insets, Offset, Rect},
|
||||
},
|
||||
};
|
||||
|
||||
@ -14,10 +14,18 @@ use super::{
|
||||
pub struct ButtonPage<S, T> {
|
||||
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<S>>,
|
||||
/// Right button of the last screen
|
||||
confirm_btn_details: Option<ButtonDetails<S>>,
|
||||
/// Left button of the last page
|
||||
last_back_btn_details: Option<ButtonDetails<S>>,
|
||||
/// Left button of every screen in the middle
|
||||
back_btn_details: Option<ButtonDetails<S>>,
|
||||
/// Right button of every screen apart the last one
|
||||
next_btn_details: Option<ButtonDetails<S>>,
|
||||
buttons: Child<ButtonController<S>>,
|
||||
/// Scrollbar may or may not be shown (but will be counting pages anyway).
|
||||
@ -33,11 +41,13 @@ where
|
||||
pub fn new_str(content: T, background: Color) -> Self {
|
||||
Self {
|
||||
content: Child::new(content),
|
||||
scrollbar: Child::new(ScrollBar::vertical_to_be_filled_later()),
|
||||
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")),
|
||||
back_btn_details: Some(ButtonDetails::up_arrow_icon_wide()),
|
||||
last_back_btn_details: Some(ButtonDetails::up_arrow_icon()),
|
||||
next_btn_details: Some(ButtonDetails::down_arrow_icon_wide()),
|
||||
// Setting empty layout for now, we do not yet know the page count.
|
||||
// Initial button layout will be set in `place()` after we can call
|
||||
@ -57,11 +67,13 @@ where
|
||||
pub fn new_str_buf(content: T, background: Color) -> Self {
|
||||
Self {
|
||||
content: Child::new(content),
|
||||
scrollbar: Child::new(ScrollBar::vertical_to_be_filled_later()),
|
||||
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())),
|
||||
back_btn_details: Some(ButtonDetails::up_arrow_icon_wide()),
|
||||
last_back_btn_details: Some(ButtonDetails::up_arrow_icon()),
|
||||
next_btn_details: Some(ButtonDetails::down_arrow_icon_wide()),
|
||||
// Setting empty layout for now, we do not yet know the page count.
|
||||
// Initial button layout will be set in `place()` after we can call
|
||||
@ -136,19 +148,25 @@ where
|
||||
}
|
||||
|
||||
fn get_button_layout(&self, has_prev: bool, has_next: bool) -> ButtonLayout<S> {
|
||||
let btn_left = self.get_left_button_details(has_prev);
|
||||
let btn_left = self.get_left_button_details(!has_prev, !has_next);
|
||||
let btn_right = self.get_right_button_details(has_next);
|
||||
ButtonLayout::new(btn_left, None, btn_right)
|
||||
}
|
||||
|
||||
fn get_left_button_details(&self, has_prev_page: bool) -> Option<ButtonDetails<S>> {
|
||||
if has_prev_page {
|
||||
self.back_btn_details.clone()
|
||||
} else {
|
||||
/// Get the let button details, depending whether the page is first, last,
|
||||
/// or in the middle.
|
||||
fn get_left_button_details(&self, is_first: bool, is_last: bool) -> Option<ButtonDetails<S>> {
|
||||
if is_first {
|
||||
self.cancel_btn_details.clone()
|
||||
} else if is_last {
|
||||
self.last_back_btn_details.clone()
|
||||
} else {
|
||||
self.back_btn_details.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the right button details, depending on whether there is a next
|
||||
/// page.
|
||||
fn get_right_button_details(&self, has_next_page: bool) -> Option<ButtonDetails<S>> {
|
||||
if has_next_page {
|
||||
self.next_btn_details.clone()
|
||||
@ -168,14 +186,7 @@ where
|
||||
type Msg = PageMsg<T::Msg, bool>;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
let (content_and_scrollbar_area, button_area) = bounds.split_bottom(theme::BUTTON_HEIGHT);
|
||||
let (content_area, scrollbar_area) = {
|
||||
if self.show_scrollbar {
|
||||
content_and_scrollbar_area.split_right(ScrollBar::WIDTH)
|
||||
} else {
|
||||
(content_and_scrollbar_area, Rect::zero())
|
||||
}
|
||||
};
|
||||
let (content_area, button_area) = bounds.split_bottom(theme::BUTTON_HEIGHT);
|
||||
let content_area = content_area.inset(Insets::top(1));
|
||||
// Do not pad the button area nor the scrollbar, leave it to them
|
||||
self.pad.place(content_area);
|
||||
@ -184,14 +195,34 @@ where
|
||||
// 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 = if let Some(scrollbar_area) = self.parent_scrollbar_area {
|
||||
scrollbar_area
|
||||
} else {
|
||||
Rect::from_top_right_and_size(
|
||||
content_area.top_right(),
|
||||
Offset::new(
|
||||
-self.scrollbar.inner().overall_width(),
|
||||
ScrollBar::MAX_DOT_SIZE,
|
||||
),
|
||||
)
|
||||
};
|
||||
self.scrollbar.place(scrollbar_area);
|
||||
}
|
||||
self.set_buttons_for_initial_page(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);
|
||||
let button_event = self.buttons.event(ctx, event);
|
||||
|
@ -5,46 +5,40 @@ use crate::ui::{
|
||||
model_tr::theme,
|
||||
};
|
||||
|
||||
/// In which direction should the scrollbar be positioned
|
||||
pub enum ScrollbarOrientation {
|
||||
Vertical,
|
||||
Horizontal,
|
||||
}
|
||||
|
||||
/// Scrollbar to be painted horizontally at the top right of the screen.
|
||||
pub struct ScrollBar {
|
||||
area: Rect,
|
||||
pad: Pad,
|
||||
pub page_count: usize,
|
||||
pub active_page: usize,
|
||||
pub orientation: ScrollbarOrientation,
|
||||
}
|
||||
|
||||
impl ScrollBar {
|
||||
pub const WIDTH: i16 = 8;
|
||||
pub const DOT_SIZE: Offset = Offset::new(4, 4);
|
||||
pub const DOT_INTERVAL: i16 = 6;
|
||||
/// How many dots at most will there be
|
||||
pub const MAX_DOTS: i16 = 5;
|
||||
/// Maximum size (width/height) of a dot
|
||||
pub const MAX_DOT_SIZE: i16 = 5;
|
||||
/// Distance between two dots
|
||||
pub const DOTS_DISTANCE: i16 = 2;
|
||||
pub const DOTS_INTERVAL: i16 = Self::MAX_DOT_SIZE + Self::DOTS_DISTANCE;
|
||||
pub const MAX_WIDTH: i16 = Self::DOTS_INTERVAL * Self::MAX_DOTS - Self::DOTS_DISTANCE;
|
||||
|
||||
pub fn new(page_count: usize, orientation: ScrollbarOrientation) -> Self {
|
||||
pub fn new(page_count: usize) -> Self {
|
||||
Self {
|
||||
area: Rect::zero(),
|
||||
pad: Pad::with_background(theme::BG),
|
||||
page_count,
|
||||
active_page: 0,
|
||||
orientation,
|
||||
}
|
||||
}
|
||||
|
||||
/// Page count will be given later as it is not available yet.
|
||||
pub fn vertical_to_be_filled_later() -> Self {
|
||||
Self::vertical(0)
|
||||
pub fn to_be_filled_later() -> Self {
|
||||
Self::new(0)
|
||||
}
|
||||
|
||||
pub fn vertical(page_count: usize) -> Self {
|
||||
Self::new(page_count, ScrollbarOrientation::Vertical)
|
||||
}
|
||||
|
||||
pub fn horizontal(page_count: usize) -> Self {
|
||||
Self::new(page_count, ScrollbarOrientation::Horizontal)
|
||||
pub fn overall_width(&self) -> i16 {
|
||||
Self::DOTS_INTERVAL * self.page_count as i16 - Self::DOTS_DISTANCE
|
||||
}
|
||||
|
||||
pub fn set_page_count(&mut self, page_count: usize) {
|
||||
@ -74,8 +68,9 @@ impl ScrollBar {
|
||||
/// Create a (seemingly circular) dot given its top left point.
|
||||
/// Make it full when it is active, otherwise paint just the perimeter and
|
||||
/// leave center empty.
|
||||
fn paint_dot(&self, active: bool, top_left: Point) {
|
||||
let full_square = Rect::from_top_left_and_size(top_left, ScrollBar::DOT_SIZE);
|
||||
fn paint_dot(&self, active: bool, top_right: Point) {
|
||||
let full_square =
|
||||
Rect::from_top_right_and_size(top_right, Offset::uniform(Self::MAX_DOT_SIZE));
|
||||
|
||||
// FG - painting the full square
|
||||
display::rect_fill(full_square, theme::FG);
|
||||
@ -91,45 +86,15 @@ impl ScrollBar {
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_vertical(&mut self) {
|
||||
let count = self.page_count as i16;
|
||||
let interval = {
|
||||
let available_space = self.area.height();
|
||||
let naive_space = count * Self::DOT_INTERVAL;
|
||||
if naive_space > available_space {
|
||||
available_space / count
|
||||
} else {
|
||||
Self::DOT_INTERVAL
|
||||
}
|
||||
};
|
||||
let mut top_left = Point::new(
|
||||
self.area.center().x - Self::DOT_SIZE.x / 2,
|
||||
self.area.center().y - (count / 2) * interval,
|
||||
);
|
||||
for i in 0..self.page_count {
|
||||
self.paint_dot(i == self.active_page, top_left);
|
||||
top_left.y += interval;
|
||||
}
|
||||
}
|
||||
|
||||
/// Drawing the dots horizontally and aligning to the right
|
||||
fn paint_horizontal(&mut self) {
|
||||
let count = self.page_count as i16;
|
||||
let interval = {
|
||||
let available_space = self.area.width();
|
||||
let naive_space = count * Self::DOT_INTERVAL;
|
||||
if naive_space > available_space {
|
||||
available_space / count
|
||||
} else {
|
||||
Self::DOT_INTERVAL
|
||||
}
|
||||
};
|
||||
let mut top_left = Point::new(
|
||||
self.area.center().x - (count / 2) * interval,
|
||||
self.area.center().y - Self::DOT_SIZE.y / 2,
|
||||
);
|
||||
for i in 0..self.page_count {
|
||||
self.paint_dot(i == self.active_page, top_left);
|
||||
top_left.x += interval;
|
||||
let mut top_right = self.area.top_right();
|
||||
// TODO: implement smaller dots - two more sizes
|
||||
// TODO: implement showing at most MAX_DIGITS
|
||||
for i in (0..self.page_count).rev() {
|
||||
self.paint_dot(i == self.active_page, top_right);
|
||||
top_right.x -= Self::DOTS_INTERVAL;
|
||||
top_right.print();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -156,10 +121,6 @@ impl Component for ScrollBar {
|
||||
|
||||
self.pad.clear();
|
||||
self.pad.paint();
|
||||
if matches!(self.orientation, ScrollbarOrientation::Vertical) {
|
||||
self.paint_vertical()
|
||||
} else {
|
||||
self.paint_horizontal()
|
||||
}
|
||||
self.paint_horizontal();
|
||||
}
|
||||
}
|
||||
|
@ -95,7 +95,7 @@ def _require_confirm_change_pin(ctx: Context, msg: ChangePin) -> Awaitable[None]
|
||||
"enable PIN protection?",
|
||||
[
|
||||
"PIN will be used to access this device.",
|
||||
"It should contain at least 4 digits.",
|
||||
"It must contain at least 4 digits.",
|
||||
],
|
||||
)
|
||||
|
||||
|
@ -1359,12 +1359,14 @@ async def confirm_set_new_pin(
|
||||
)
|
||||
|
||||
information.append(
|
||||
"Position of individual numbers will change between entries for enhanced security."
|
||||
"Position of individual numbers will change between entries for more security."
|
||||
)
|
||||
return await confirm_action(
|
||||
ctx,
|
||||
br_type,
|
||||
title="",
|
||||
description="\n\n".join(information),
|
||||
description="\n".join(information),
|
||||
verb="HOLD TO BEGIN",
|
||||
hold=True,
|
||||
br_code=br_code,
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user