mirror of
https://github.com/trezor/trezor-firmware.git
synced 2024-11-20 14:39:22 +00:00
chore(core/rust): Add dynamic place system
This commit is contained in:
parent
7b41946789
commit
801679bccf
@ -15,9 +15,10 @@ mod trezorhal;
|
||||
|
||||
#[cfg(feature = "ui")]
|
||||
#[macro_use]
|
||||
mod ui;
|
||||
pub mod ui;
|
||||
mod util;
|
||||
|
||||
#[cfg(not(test))]
|
||||
#[cfg(not(feature = "test"))]
|
||||
#[panic_handler]
|
||||
fn panic(_info: &core::panic::PanicInfo) -> ! {
|
||||
|
@ -22,6 +22,12 @@ pub struct Buffer {
|
||||
len: usize,
|
||||
}
|
||||
|
||||
impl Buffer {
|
||||
pub fn empty() -> Self {
|
||||
Self::from("")
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Obj> for Buffer {
|
||||
type Error = Error;
|
||||
|
||||
@ -35,6 +41,12 @@ impl TryFrom<Obj> for Buffer {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Buffer {
|
||||
fn default() -> Self {
|
||||
Self::empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Buffer {
|
||||
type Target = [u8];
|
||||
|
||||
|
@ -6,7 +6,10 @@ use heapless::Vec;
|
||||
use crate::ui::model_t1::event::ButtonEvent;
|
||||
#[cfg(feature = "model_tt")]
|
||||
use crate::ui::model_tt::event::TouchEvent;
|
||||
use crate::{time::Duration, ui::geometry::Rect};
|
||||
use crate::{
|
||||
time::Duration,
|
||||
ui::{component::Map, geometry::Rect},
|
||||
};
|
||||
|
||||
/// Type used by components that do not return any messages.
|
||||
///
|
||||
@ -14,12 +17,38 @@ use crate::{time::Duration, ui::geometry::Rect};
|
||||
pub enum Never {}
|
||||
|
||||
/// User interface is composed of components that can react to `Event`s through
|
||||
/// the `event` method and know how to paint themselves to screen through the
|
||||
/// the `event` method, and know how to paint themselves to screen through the
|
||||
/// `paint` method. Components can emit messages as a reaction to events.
|
||||
pub trait Component {
|
||||
type Msg;
|
||||
|
||||
/// Position the component into some available space, specified by `bounds`.
|
||||
///
|
||||
/// Component should lay itself out, together with all children, and return
|
||||
/// the total bounding box. This area can, occasionally, be larger than
|
||||
/// `bounds` (it is a soft-limit), but the component **should never** paint
|
||||
/// outside of it.
|
||||
///
|
||||
/// No painting should be done in this phase.
|
||||
fn place(&mut self, bounds: Rect) -> 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,
|
||||
/// and usually call `EventCtx::request_paint` to mark itself for painting.
|
||||
/// Component can also optionally return a message as a result of the
|
||||
/// interaction.
|
||||
///
|
||||
/// No painting should be done in this phase.
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg>;
|
||||
|
||||
/// Render to screen, based on current internal state.
|
||||
///
|
||||
/// To prevent unnecessary over-draw, dirty state checking is performed in
|
||||
/// the `Child` wrapper.
|
||||
fn paint(&mut self);
|
||||
|
||||
/// Report current paint bounds of this component. Used for debugging.
|
||||
fn bounds(&self, _sink: &mut dyn FnMut(Rect)) {}
|
||||
}
|
||||
|
||||
@ -78,6 +107,10 @@ where
|
||||
{
|
||||
type Msg = T::Msg;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
self.component.place(bounds)
|
||||
}
|
||||
|
||||
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
|
||||
@ -112,7 +145,47 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<M, T, U> Component for (T, U)
|
||||
where
|
||||
T: Component<Msg = M>,
|
||||
U: Component<Msg = M>,
|
||||
{
|
||||
type Msg = M;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
self.0.place(bounds).union(self.1.place(bounds))
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
self.0
|
||||
.event(ctx, event)
|
||||
.or_else(|| self.1.event(ctx, event))
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
self.0.paint();
|
||||
self.1.paint();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<T, U> crate::trace::Trace for (T, U)
|
||||
where
|
||||
T: Component,
|
||||
T: crate::trace::Trace,
|
||||
U: Component,
|
||||
U: crate::trace::Trace,
|
||||
{
|
||||
fn trace(&self, d: &mut dyn crate::trace::Tracer) {
|
||||
d.open("Tuple");
|
||||
d.field("0", &self.0);
|
||||
d.field("1", &self.1);
|
||||
d.close();
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ComponentExt: Sized {
|
||||
fn map<F>(self, func: F) -> Map<Self, F>;
|
||||
fn into_child(self) -> Child<Self>;
|
||||
fn request_complete_repaint(&mut self, ctx: &mut EventCtx);
|
||||
}
|
||||
@ -121,6 +194,10 @@ impl<T> ComponentExt for T
|
||||
where
|
||||
T: Component,
|
||||
{
|
||||
fn map<F>(self, func: F) -> Map<Self, F> {
|
||||
Map::new(self, func)
|
||||
}
|
||||
|
||||
fn into_child(self) -> Child<Self> {
|
||||
Child::new(self)
|
||||
}
|
||||
@ -172,6 +249,7 @@ impl TimerToken {
|
||||
pub struct EventCtx {
|
||||
timers: Vec<(TimerToken, Duration), { Self::MAX_TIMERS }>,
|
||||
next_token: u32,
|
||||
place_requested: bool,
|
||||
paint_requested: bool,
|
||||
anim_frame_scheduled: bool,
|
||||
}
|
||||
@ -194,11 +272,26 @@ impl EventCtx {
|
||||
Self {
|
||||
timers: Vec::new(),
|
||||
next_token: Self::STARTING_TIMER_TOKEN,
|
||||
paint_requested: false,
|
||||
place_requested: true, // We need to perform a place pass in the beginning.
|
||||
paint_requested: false, /* We also need to paint, but this is supplemented by
|
||||
* `Child::marked_for_paint` being true. */
|
||||
anim_frame_scheduled: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Indicate that position or sizes of components inside the component tree
|
||||
/// have changed, and we should perform a place pass before next event or
|
||||
/// paint traversals.
|
||||
pub fn request_place(&mut self) {
|
||||
self.place_requested = true;
|
||||
}
|
||||
|
||||
/// Returns `true` if we should first perform a place traversal before
|
||||
/// processing events or painting.
|
||||
pub fn needs_place_before_next_event_or_paint(&self) -> bool {
|
||||
self.place_requested
|
||||
}
|
||||
|
||||
/// Indicate that the inner state of the component has changed, any screen
|
||||
/// content it has painted before is now invalid, and it should be painted
|
||||
/// again by the nearest `Child` wrapper.
|
||||
@ -226,6 +319,7 @@ impl EventCtx {
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.place_requested = false;
|
||||
self.paint_requested = false;
|
||||
self.anim_frame_scheduled = false;
|
||||
}
|
||||
|
@ -1,10 +1,15 @@
|
||||
use super::{Component, Event, EventCtx, Never};
|
||||
use crate::ui::geometry::Rect;
|
||||
|
||||
pub struct Empty;
|
||||
|
||||
impl Component for Empty {
|
||||
type Msg = Never;
|
||||
|
||||
fn place(&mut self, _bounds: Rect) -> Rect {
|
||||
Rect::zero()
|
||||
}
|
||||
|
||||
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
|
||||
None
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ use core::ops::Deref;
|
||||
use crate::ui::{
|
||||
component::{Component, Event, EventCtx, Never},
|
||||
display::{self, Color, Font},
|
||||
geometry::{Alignment, Point, Rect},
|
||||
geometry::{Alignment, Offset, Rect},
|
||||
};
|
||||
|
||||
pub struct LabelStyle {
|
||||
@ -14,6 +14,7 @@ pub struct LabelStyle {
|
||||
|
||||
pub struct Label<T> {
|
||||
area: Rect,
|
||||
align: Alignment,
|
||||
style: LabelStyle,
|
||||
text: T,
|
||||
}
|
||||
@ -22,45 +23,25 @@ impl<T> Label<T>
|
||||
where
|
||||
T: Deref<Target = [u8]>,
|
||||
{
|
||||
pub fn new(origin: Point, align: Alignment, text: T, style: LabelStyle) -> Self {
|
||||
let width = style.font.text_width(&text);
|
||||
let height = style.font.line_height();
|
||||
let area = match align {
|
||||
// `origin` is the top-left point.
|
||||
Alignment::Start => Rect {
|
||||
x0: origin.x,
|
||||
y0: origin.y,
|
||||
x1: origin.x + width,
|
||||
y1: origin.y + height,
|
||||
},
|
||||
// `origin` is the top-centered point.
|
||||
Alignment::Center => Rect {
|
||||
x0: origin.x - width / 2,
|
||||
y0: origin.y,
|
||||
x1: origin.x + width / 2,
|
||||
y1: origin.y + height,
|
||||
},
|
||||
// `origin` is the top-right point.
|
||||
Alignment::End => Rect {
|
||||
x0: origin.x - width,
|
||||
y0: origin.y,
|
||||
x1: origin.x,
|
||||
y1: origin.y + height,
|
||||
},
|
||||
};
|
||||
Self { area, style, text }
|
||||
pub fn new(text: T, align: Alignment, style: LabelStyle) -> Self {
|
||||
Self {
|
||||
area: Rect::zero(),
|
||||
align,
|
||||
style,
|
||||
text,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn left_aligned(origin: Point, text: T, style: LabelStyle) -> Self {
|
||||
Self::new(origin, Alignment::Start, text, style)
|
||||
pub fn left_aligned(text: T, style: LabelStyle) -> Self {
|
||||
Self::new(text, Alignment::Start, style)
|
||||
}
|
||||
|
||||
pub fn right_aligned(origin: Point, text: T, style: LabelStyle) -> Self {
|
||||
Self::new(origin, Alignment::End, text, style)
|
||||
pub fn right_aligned(text: T, style: LabelStyle) -> Self {
|
||||
Self::new(text, Alignment::End, style)
|
||||
}
|
||||
|
||||
pub fn centered(origin: Point, text: T, style: LabelStyle) -> Self {
|
||||
Self::new(origin, Alignment::Center, text, style)
|
||||
pub fn centered(text: T, style: LabelStyle) -> Self {
|
||||
Self::new(text, Alignment::Center, style)
|
||||
}
|
||||
|
||||
pub fn text(&self) -> &T {
|
||||
@ -74,6 +55,35 @@ where
|
||||
{
|
||||
type Msg = Never;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
let size = Offset::new(
|
||||
self.style.font.text_width(&self.text),
|
||||
self.style.font.line_height(),
|
||||
);
|
||||
self.area = match self.align {
|
||||
Alignment::Start => Rect::from_top_left_and_size(bounds.top_left(), size),
|
||||
Alignment::Center => {
|
||||
let origin = bounds.top_left().center(bounds.top_right());
|
||||
Rect {
|
||||
x0: origin.x - size.x / 2,
|
||||
y0: origin.y,
|
||||
x1: origin.x + size.x / 2,
|
||||
y1: origin.y + size.y,
|
||||
}
|
||||
}
|
||||
Alignment::End => {
|
||||
let origin = bounds.top_right();
|
||||
Rect {
|
||||
x0: origin.x - size.x,
|
||||
y0: origin.y,
|
||||
x1: origin.x,
|
||||
y1: origin.y + size.y,
|
||||
}
|
||||
}
|
||||
};
|
||||
self.area
|
||||
}
|
||||
|
||||
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
|
||||
None
|
||||
}
|
||||
|
@ -19,6 +19,10 @@ where
|
||||
{
|
||||
type Msg = U;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
self.inner.place(bounds)
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
self.inner.event(ctx, event).and_then(&self.func)
|
||||
}
|
||||
|
@ -19,12 +19,12 @@ impl<T> Maybe<T> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn visible(area: Rect, clear: Color, inner: T) -> Self {
|
||||
Self::new(Pad::with_background(area, clear), inner, true)
|
||||
pub fn visible(clear: Color, inner: T) -> Self {
|
||||
Self::new(Pad::with_background(clear), inner, true)
|
||||
}
|
||||
|
||||
pub fn hidden(area: Rect, clear: Color, inner: T) -> Self {
|
||||
Self::new(Pad::with_background(area, clear), inner, false)
|
||||
pub fn hidden(clear: Color, inner: T) -> Self {
|
||||
Self::new(Pad::with_background(clear), inner, false)
|
||||
}
|
||||
}
|
||||
|
||||
@ -72,6 +72,12 @@ where
|
||||
{
|
||||
type Msg = T::Msg;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
let area = self.inner.place(bounds);
|
||||
self.pad.place(area);
|
||||
area
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
if self.visible {
|
||||
self.inner.event(ctx, event)
|
||||
|
@ -7,8 +7,9 @@ pub mod map;
|
||||
pub mod maybe;
|
||||
pub mod pad;
|
||||
pub mod paginated;
|
||||
pub mod painter;
|
||||
pub mod placed;
|
||||
pub mod text;
|
||||
pub mod tuple;
|
||||
|
||||
pub use base::{Child, Component, ComponentExt, Event, EventCtx, Never, TimerToken};
|
||||
pub use empty::Empty;
|
||||
@ -17,6 +18,8 @@ pub use map::Map;
|
||||
pub use maybe::Maybe;
|
||||
pub use pad::Pad;
|
||||
pub use paginated::{PageMsg, Paginate};
|
||||
pub use painter::Painter;
|
||||
pub use placed::GridPlaced;
|
||||
pub use text::{
|
||||
formatted::FormattedText,
|
||||
layout::{LineBreaking, PageBreaking, TextLayout},
|
||||
|
@ -10,14 +10,18 @@ pub struct Pad {
|
||||
}
|
||||
|
||||
impl Pad {
|
||||
pub fn with_background(area: Rect, color: Color) -> Self {
|
||||
pub fn with_background(color: Color) -> Self {
|
||||
Self {
|
||||
area,
|
||||
color,
|
||||
area: Rect::zero(),
|
||||
clear: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn place(&mut self, area: Rect) {
|
||||
self.area = area;
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.clear = true;
|
||||
}
|
||||
|
38
core/embed/rust/src/ui/component/painter.rs
Normal file
38
core/embed/rust/src/ui/component/painter.rs
Normal file
@ -0,0 +1,38 @@
|
||||
use crate::ui::{
|
||||
component::{Component, Event, EventCtx, Never},
|
||||
geometry::Rect,
|
||||
};
|
||||
|
||||
pub struct Painter<F> {
|
||||
area: Rect,
|
||||
func: F,
|
||||
}
|
||||
|
||||
impl<F> Painter<F> {
|
||||
pub fn new(func: F) -> Self {
|
||||
Self {
|
||||
func,
|
||||
area: Rect::zero(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<F> Component for Painter<F>
|
||||
where
|
||||
F: FnMut(Rect),
|
||||
{
|
||||
type Msg = Never;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
self.area = bounds;
|
||||
self.area
|
||||
}
|
||||
|
||||
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
|
||||
None
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
(self.func)(self.area);
|
||||
}
|
||||
}
|
79
core/embed/rust/src/ui/component/placed.rs
Normal file
79
core/embed/rust/src/ui/component/placed.rs
Normal file
@ -0,0 +1,79 @@
|
||||
use crate::ui::{
|
||||
component::{Component, Event, EventCtx},
|
||||
geometry::{Grid, GridCellSpan, Rect},
|
||||
};
|
||||
|
||||
pub struct GridPlaced<T> {
|
||||
inner: T,
|
||||
grid: Grid,
|
||||
cells: GridCellSpan,
|
||||
}
|
||||
|
||||
impl<T> GridPlaced<T> {
|
||||
pub fn new(inner: T) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
grid: Grid::new(Rect::zero(), 0, 0),
|
||||
cells: GridCellSpan {
|
||||
from: (0, 0),
|
||||
to: (0, 0),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_grid(mut self, rows: usize, cols: usize) -> Self {
|
||||
self.grid.rows = rows;
|
||||
self.grid.cols = cols;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_spacing(mut self, spacing: i32) -> Self {
|
||||
self.grid.spacing = spacing;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_row_col(mut self, row: usize, col: usize) -> Self {
|
||||
self.cells.from = (row, col);
|
||||
self.cells.to = (row, col);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_from_to(mut self, from: (usize, usize), to: (usize, usize)) -> Self {
|
||||
self.cells.from = from;
|
||||
self.cells.to = to;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Component for GridPlaced<T>
|
||||
where
|
||||
T: Component,
|
||||
{
|
||||
type Msg = T::Msg;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
self.grid.area = bounds;
|
||||
self.inner.place(self.grid.cells(self.cells))
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
self.inner.event(ctx, event)
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
self.inner.paint()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<T> crate::trace::Trace for GridPlaced<T>
|
||||
where
|
||||
T: Component,
|
||||
T: crate::trace::Trace,
|
||||
{
|
||||
fn trace(&self, d: &mut dyn crate::trace::Tracer) {
|
||||
d.open("GridPlaced");
|
||||
d.field("inner", &self.inner);
|
||||
d.close();
|
||||
}
|
||||
}
|
@ -26,10 +26,10 @@ pub struct FormattedText<F, T> {
|
||||
}
|
||||
|
||||
impl<F, T> FormattedText<F, T> {
|
||||
pub fn new<D: DefaultTextTheme>(area: Rect, format: F) -> Self {
|
||||
pub fn new<D: DefaultTextTheme>(format: F) -> Self {
|
||||
Self {
|
||||
layout: TextLayout::new::<D>(area),
|
||||
format,
|
||||
layout: TextLayout::new::<D>(),
|
||||
args: LinearMap::new(),
|
||||
char_offset: 0,
|
||||
}
|
||||
@ -113,6 +113,11 @@ where
|
||||
{
|
||||
type Msg = Never;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
self.layout.bounds = bounds;
|
||||
self.layout.bounds
|
||||
}
|
||||
|
||||
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
|
||||
None
|
||||
}
|
||||
@ -237,32 +242,30 @@ impl<'a> Iterator for Tokenizer<'a> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::array::IntoIter;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn tokenizer_yields_expected_tokens() {
|
||||
assert!(Tokenizer::new(b"").eq(IntoIter::new([])));
|
||||
assert!(Tokenizer::new(b"x").eq(IntoIter::new([Token::Literal(b"x")])));
|
||||
assert!(Tokenizer::new(b"x\0y").eq(IntoIter::new([Token::Literal("x\0y".as_bytes())])));
|
||||
assert!(Tokenizer::new(b"{").eq(IntoIter::new([])));
|
||||
assert!(Tokenizer::new(b"x{").eq(IntoIter::new([Token::Literal(b"x")])));
|
||||
assert!(Tokenizer::new(b"x{y").eq(IntoIter::new([Token::Literal(b"x")])));
|
||||
assert!(Tokenizer::new(b"{}").eq(IntoIter::new([Token::Argument(b"")])));
|
||||
assert!(Tokenizer::new(b"x{}y{").eq(IntoIter::new([
|
||||
assert!(Tokenizer::new(b"").eq([]));
|
||||
assert!(Tokenizer::new(b"x").eq([Token::Literal(b"x")]));
|
||||
assert!(Tokenizer::new(b"x\0y").eq([Token::Literal("x\0y".as_bytes())]));
|
||||
assert!(Tokenizer::new(b"{").eq([]));
|
||||
assert!(Tokenizer::new(b"x{").eq([Token::Literal(b"x")]));
|
||||
assert!(Tokenizer::new(b"x{y").eq([Token::Literal(b"x")]));
|
||||
assert!(Tokenizer::new(b"{}").eq([Token::Argument(b"")]));
|
||||
assert!(Tokenizer::new(b"x{}y{").eq([
|
||||
Token::Literal(b"x"),
|
||||
Token::Argument(b""),
|
||||
Token::Literal(b"y"),
|
||||
])));
|
||||
assert!(Tokenizer::new(b"{\0}").eq(IntoIter::new([Token::Argument("\0".as_bytes()),])));
|
||||
assert!(Tokenizer::new(b"{{y}").eq(IntoIter::new([Token::Argument(b"{y"),])));
|
||||
assert!(Tokenizer::new(b"{{{{xyz").eq(IntoIter::new([])));
|
||||
assert!(Tokenizer::new(b"x{}{{}}}}").eq(IntoIter::new([
|
||||
]));
|
||||
assert!(Tokenizer::new(b"{\0}").eq([Token::Argument("\0".as_bytes()),]));
|
||||
assert!(Tokenizer::new(b"{{y}").eq([Token::Argument(b"{y"),]));
|
||||
assert!(Tokenizer::new(b"{{{{xyz").eq([]));
|
||||
assert!(Tokenizer::new(b"x{}{{}}}}").eq([
|
||||
Token::Literal(b"x"),
|
||||
Token::Argument(b""),
|
||||
Token::Argument(b"{"),
|
||||
Token::Literal(b"}}}"),
|
||||
])));
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
283
core/embed/rust/src/ui/component/text/iter.rs
Normal file
283
core/embed/rust/src/ui/component/text/iter.rs
Normal file
@ -0,0 +1,283 @@
|
||||
use crate::ui::{
|
||||
component::{text::layout::Op, LineBreaking},
|
||||
display::Font,
|
||||
geometry::Offset,
|
||||
};
|
||||
use core::iter;
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
|
||||
struct LineBreak {
|
||||
/// Index of character **after** the line-break.
|
||||
offset: usize,
|
||||
/// Distance from the last line-break of the sequence, in pixels.
|
||||
width: i32,
|
||||
style: BreakStyle,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
|
||||
enum BreakStyle {
|
||||
Hard,
|
||||
AtWhitespaceOrWordBoundary,
|
||||
InsideWord,
|
||||
}
|
||||
|
||||
fn limit_line_breaks(
|
||||
breaks: impl Iterator<Item = LineBreak>,
|
||||
line_height: i32,
|
||||
available_height: i32,
|
||||
) -> impl Iterator<Item = LineBreak> {
|
||||
breaks.take(available_height as usize / line_height as usize)
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
|
||||
enum Appendix {
|
||||
None,
|
||||
Hyphen,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
|
||||
struct Span<'a> {
|
||||
text: &'a str,
|
||||
append: Appendix,
|
||||
}
|
||||
|
||||
fn break_text_to_spans(
|
||||
text: &str,
|
||||
text_font: impl GlyphMetrics,
|
||||
hyphen_font: impl GlyphMetrics,
|
||||
breaking: LineBreaking,
|
||||
available_width: i32,
|
||||
) -> impl Iterator<Item = Span> {
|
||||
let mut finished = false;
|
||||
let mut last_break = LineBreak {
|
||||
offset: 0,
|
||||
width: 0,
|
||||
style: BreakStyle::AtWhitespaceOrWordBoundary,
|
||||
};
|
||||
let mut breaks = select_line_breaks(
|
||||
text.char_indices(),
|
||||
text_font,
|
||||
hyphen_font,
|
||||
breaking,
|
||||
available_width,
|
||||
);
|
||||
iter::from_fn(move || {
|
||||
if finished {
|
||||
None
|
||||
} else if let Some(lb) = breaks.next() {
|
||||
let start_of_line = last_break.offset;
|
||||
let end_of_line = lb.offset; // Not inclusive.
|
||||
last_break = lb;
|
||||
if let BreakStyle::AtWhitespaceOrWordBoundary = lb.style {
|
||||
last_break.offset += 1;
|
||||
}
|
||||
Some(Span {
|
||||
text: &text[start_of_line..end_of_line],
|
||||
append: match lb.style {
|
||||
BreakStyle::Hard | BreakStyle::AtWhitespaceOrWordBoundary => Appendix::None,
|
||||
BreakStyle::InsideWord => Appendix::Hyphen,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
finished = true;
|
||||
Some(Span {
|
||||
text: &text[last_break.offset..],
|
||||
append: Appendix::None,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn select_line_breaks(
|
||||
chars: impl Iterator<Item = (usize, char)>,
|
||||
text_font: impl GlyphMetrics,
|
||||
hyphen_font: impl GlyphMetrics,
|
||||
breaking: LineBreaking,
|
||||
available_width: i32,
|
||||
) -> impl Iterator<Item = LineBreak> {
|
||||
let hyphen_width = hyphen_font.char_width('-');
|
||||
|
||||
let mut proposed = None;
|
||||
let mut line_width = 0;
|
||||
let mut found_any_whitespace = false;
|
||||
|
||||
chars.filter_map(move |(offset, ch)| {
|
||||
let char_width = text_font.char_width(ch);
|
||||
let exceeds_available_width = line_width + char_width > available_width;
|
||||
let have_space_for_break = line_width + char_width + hyphen_width <= available_width;
|
||||
let can_break_word =
|
||||
matches!(breaking, LineBreaking::BreakWordsAndInsertHyphen) || !found_any_whitespace;
|
||||
|
||||
let break_line = match ch {
|
||||
'\n' | '\r' => {
|
||||
// Immediate hard break.
|
||||
Some(LineBreak {
|
||||
offset,
|
||||
width: line_width,
|
||||
style: BreakStyle::Hard,
|
||||
})
|
||||
}
|
||||
' ' | '\t' => {
|
||||
// Whitespace, propose a line-break before this character.
|
||||
proposed = Some(LineBreak {
|
||||
offset,
|
||||
width: line_width,
|
||||
style: BreakStyle::AtWhitespaceOrWordBoundary,
|
||||
});
|
||||
found_any_whitespace = true;
|
||||
None
|
||||
}
|
||||
_ if have_space_for_break && can_break_word => {
|
||||
// Propose a word-break after this character. In case the next character is
|
||||
// whitespace, the proposed word break is replaced by a whitespace break.
|
||||
proposed = Some(LineBreak {
|
||||
offset: offset + 1,
|
||||
width: line_width + char_width + hyphen_width,
|
||||
style: BreakStyle::InsideWord,
|
||||
});
|
||||
None
|
||||
}
|
||||
_ if exceeds_available_width => {
|
||||
// Consume the last proposed line-break. In case we don't have anything
|
||||
// proposed, we hard-break immediately before this character. This only happens
|
||||
// if the first character of the line doesn't fit.
|
||||
Some(proposed.unwrap_or(LineBreak {
|
||||
offset,
|
||||
width: line_width,
|
||||
style: BreakStyle::Hard,
|
||||
}))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
if break_line.is_some() {
|
||||
// Reset the state.
|
||||
proposed = None;
|
||||
line_width = 0;
|
||||
found_any_whitespace = false;
|
||||
} else {
|
||||
// Shift cursor.
|
||||
line_width += char_width;
|
||||
}
|
||||
break_line
|
||||
})
|
||||
}
|
||||
|
||||
trait GlyphMetrics {
|
||||
fn char_width(&self, ch: char) -> i32;
|
||||
fn line_height(&self) -> i32;
|
||||
}
|
||||
|
||||
impl GlyphMetrics for Font {
|
||||
fn char_width(&self, ch: char) -> i32 {
|
||||
self.text_width(&[ch as u8])
|
||||
}
|
||||
|
||||
fn line_height(&self) -> i32 {
|
||||
Font::line_height(*self)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_selected_line_breaks() {
|
||||
assert_eq!(line_breaks("abcd ef", 34), vec![inside_word(2, 25)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_break_text() {
|
||||
assert_eq!(
|
||||
break_text("abcd ef", 24),
|
||||
vec![
|
||||
Span {
|
||||
text: "a",
|
||||
append: Appendix::Hyphen
|
||||
},
|
||||
Span {
|
||||
text: "bcd",
|
||||
append: Appendix::None
|
||||
},
|
||||
Span {
|
||||
text: "ef",
|
||||
append: Appendix::None
|
||||
}
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
struct Fixed {
|
||||
width: i32,
|
||||
height: i32,
|
||||
}
|
||||
|
||||
impl GlyphMetrics for Fixed {
|
||||
fn char_width(&self, _ch: char) -> i32 {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn line_height(&self) -> i32 {
|
||||
self.height
|
||||
}
|
||||
}
|
||||
|
||||
fn break_text(s: &str, w: i32) -> Vec<Span> {
|
||||
break_text_to_spans(
|
||||
s,
|
||||
Fixed {
|
||||
width: 10,
|
||||
height: 10,
|
||||
},
|
||||
Fixed {
|
||||
width: 5,
|
||||
height: 10,
|
||||
},
|
||||
LineBreaking::BreakWordsAndInsertHyphen,
|
||||
w,
|
||||
)
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
fn line_breaks(s: &str, w: i32) -> Vec<LineBreak> {
|
||||
select_line_breaks(
|
||||
s.char_indices(),
|
||||
Fixed {
|
||||
width: 10,
|
||||
height: 10,
|
||||
},
|
||||
Fixed {
|
||||
width: 5,
|
||||
height: 10,
|
||||
},
|
||||
LineBreaking::BreakWordsAndInsertHyphen,
|
||||
w,
|
||||
)
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
fn hard(offset: usize, width: i32) -> LineBreak {
|
||||
LineBreak {
|
||||
offset,
|
||||
width,
|
||||
style: BreakStyle::Hard,
|
||||
}
|
||||
}
|
||||
|
||||
fn whitespace(offset: usize, width: i32) -> LineBreak {
|
||||
LineBreak {
|
||||
offset,
|
||||
width,
|
||||
style: BreakStyle::AtWhitespaceOrWordBoundary,
|
||||
}
|
||||
}
|
||||
|
||||
fn inside_word(offset: usize, width: i32) -> LineBreak {
|
||||
LineBreak {
|
||||
offset,
|
||||
width,
|
||||
style: BreakStyle::InsideWord,
|
||||
}
|
||||
}
|
||||
}
|
@ -82,9 +82,11 @@ pub trait DefaultTextTheme {
|
||||
}
|
||||
|
||||
impl TextLayout {
|
||||
pub fn new<T: DefaultTextTheme>(bounds: Rect) -> Self {
|
||||
/// Create a new text layout, with empty size and default text parameters
|
||||
/// filled from `T`.
|
||||
pub fn new<T: DefaultTextTheme>() -> Self {
|
||||
Self {
|
||||
bounds,
|
||||
bounds: Rect::zero(),
|
||||
padding_top: 0,
|
||||
padding_bottom: 0,
|
||||
background_color: T::BACKGROUND_COLOR,
|
||||
@ -103,10 +105,23 @@ impl TextLayout {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_bounds(mut self, bounds: Rect) -> Self {
|
||||
self.bounds = bounds;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn initial_cursor(&self) -> Point {
|
||||
self.bounds.top_left() + Offset::y(self.text_font.text_height() + self.padding_top)
|
||||
}
|
||||
|
||||
pub fn fit_text(&self, text: &[u8]) -> LayoutFit {
|
||||
self.layout_text(text, &mut self.initial_cursor(), &mut TextNoOp)
|
||||
}
|
||||
|
||||
pub fn render_text(&self, text: &[u8]) {
|
||||
self.layout_text(text, &mut self.initial_cursor(), &mut TextRenderer);
|
||||
}
|
||||
|
||||
pub fn layout_ops<'o>(
|
||||
mut self,
|
||||
ops: &mut dyn Iterator<Item = Op<'o>>,
|
||||
@ -235,16 +250,6 @@ impl TextLayout {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn measure_ops_height(self, ops: &mut dyn Iterator<Item = Op>) -> i32 {
|
||||
self.layout_ops(ops, &mut self.initial_cursor(), &mut TextNoOp)
|
||||
.height()
|
||||
}
|
||||
|
||||
pub fn measure_text_height(self, text: &[u8]) -> i32 {
|
||||
self.layout_text(text, &mut self.initial_cursor(), &mut TextNoOp)
|
||||
.height()
|
||||
}
|
||||
|
||||
fn layout_height(&self, init_cursor: Point, end_cursor: Point) -> i32 {
|
||||
self.padding_top
|
||||
+ self.text_font.text_height()
|
||||
|
@ -1,3 +1,4 @@
|
||||
pub mod formatted;
|
||||
mod iter;
|
||||
pub mod layout;
|
||||
pub mod paragraphs;
|
||||
|
@ -3,10 +3,10 @@ use heapless::Vec;
|
||||
use crate::ui::{
|
||||
component::{Component, Event, EventCtx, Never, Paginate},
|
||||
display::Font,
|
||||
geometry::{Dimensions, Insets, LinearLayout, Offset, Rect},
|
||||
geometry::{Dimensions, Insets, LinearPlacement, Rect},
|
||||
};
|
||||
|
||||
use super::layout::{DefaultTextTheme, LayoutFit, TextLayout, TextNoOp, TextRenderer};
|
||||
use super::layout::{DefaultTextTheme, LayoutFit, TextLayout};
|
||||
|
||||
pub const MAX_PARAGRAPHS: usize = 6;
|
||||
/// Maximum space between paragraphs. Actual result may be smaller (even 0) if
|
||||
@ -22,7 +22,7 @@ pub const PARAGRAPH_BOTTOM_SPACE: i32 = 5;
|
||||
pub struct Paragraphs<T> {
|
||||
area: Rect,
|
||||
list: Vec<Paragraph<T>, MAX_PARAGRAPHS>,
|
||||
layout: LinearLayout,
|
||||
placement: LinearPlacement,
|
||||
offset: PageOffset,
|
||||
visible: usize,
|
||||
}
|
||||
@ -31,11 +31,11 @@ impl<T> Paragraphs<T>
|
||||
where
|
||||
T: AsRef<[u8]>,
|
||||
{
|
||||
pub fn new(area: Rect) -> Self {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
area,
|
||||
area: Rect::zero(),
|
||||
list: Vec::new(),
|
||||
layout: LinearLayout::vertical()
|
||||
placement: LinearPlacement::vertical()
|
||||
.align_at_center()
|
||||
.with_spacing(DEFAULT_SPACING),
|
||||
offset: PageOffset::default(),
|
||||
@ -43,13 +43,13 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_layout(mut self, layout: LinearLayout) -> Self {
|
||||
self.layout = layout;
|
||||
pub fn with_placement(mut self, placement: LinearPlacement) -> Self {
|
||||
self.placement = placement;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_spacing(mut self, spacing: i32) -> Self {
|
||||
self.layout = self.layout.with_spacing(spacing);
|
||||
self.placement = self.placement.with_spacing(spacing);
|
||||
self
|
||||
}
|
||||
|
||||
@ -63,7 +63,7 @@ where
|
||||
text_font,
|
||||
padding_top: PARAGRAPH_TOP_SPACE,
|
||||
padding_bottom: PARAGRAPH_BOTTOM_SPACE,
|
||||
..TextLayout::new::<D>(self.area)
|
||||
..TextLayout::new::<D>()
|
||||
},
|
||||
);
|
||||
if self.list.push(paragraph).is_err() {
|
||||
@ -82,26 +82,29 @@ where
|
||||
let mut char_offset = offset.chr;
|
||||
let mut remaining_area = self.area;
|
||||
|
||||
for paragraph in self.list.iter_mut().skip(offset.par) {
|
||||
paragraph.set_area(remaining_area);
|
||||
for paragraph in &mut self.list[self.offset.par..] {
|
||||
paragraph.fit(remaining_area);
|
||||
let height = paragraph
|
||||
.layout
|
||||
.measure_text_height(¶graph.content.as_ref()[char_offset..]);
|
||||
.fit_text(paragraph.content(char_offset))
|
||||
.height();
|
||||
if height == 0 {
|
||||
break;
|
||||
}
|
||||
let (used, free) = remaining_area.split_top(height);
|
||||
paragraph.set_area(used);
|
||||
paragraph.fit(used);
|
||||
remaining_area = free;
|
||||
self.visible += 1;
|
||||
char_offset = 0;
|
||||
}
|
||||
|
||||
let visible_paragraphs = &mut self.list[offset.par..offset.par + self.visible];
|
||||
self.layout.arrange(self.area, visible_paragraphs);
|
||||
self.placement.arrange(
|
||||
self.area,
|
||||
&mut self.list[offset.par..offset.par + self.visible],
|
||||
);
|
||||
}
|
||||
|
||||
fn break_pages<'a>(&'a self) -> PageBreakIterator<'a, T> {
|
||||
fn break_pages(&self) -> PageBreakIterator<T> {
|
||||
PageBreakIterator {
|
||||
paragraphs: self,
|
||||
current: None,
|
||||
@ -115,45 +118,61 @@ where
|
||||
{
|
||||
type Msg = Never;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
self.area = bounds;
|
||||
self.change_offset(self.offset);
|
||||
self.area
|
||||
}
|
||||
|
||||
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
|
||||
None
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
let mut char_offset = self.offset.chr;
|
||||
for paragraph in self.list.iter().skip(self.offset.par).take(self.visible) {
|
||||
paragraph.layout.layout_text(
|
||||
¶graph.content.as_ref()[char_offset..],
|
||||
&mut paragraph.layout.initial_cursor(),
|
||||
&mut TextRenderer,
|
||||
);
|
||||
for paragraph in &self.list[self.offset.par..self.offset.par + self.visible] {
|
||||
paragraph.layout.render_text(paragraph.content(char_offset));
|
||||
char_offset = 0;
|
||||
}
|
||||
}
|
||||
|
||||
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
||||
sink(self.area);
|
||||
for paragraph in self.list.iter().skip(self.offset.par).take(self.visible) {
|
||||
for paragraph in &self.list[self.offset.par..self.offset.par + self.visible] {
|
||||
sink(paragraph.layout.bounds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Dimensions for Paragraphs<T> {
|
||||
fn get_size(&mut self) -> Offset {
|
||||
self.area.size()
|
||||
impl<T> Paginate for Paragraphs<T>
|
||||
where
|
||||
T: AsRef<[u8]>,
|
||||
{
|
||||
fn page_count(&mut self) -> usize {
|
||||
// There's always at least one page.
|
||||
self.break_pages().count().max(1)
|
||||
}
|
||||
|
||||
fn set_area(&mut self, area: Rect) {
|
||||
self.area = area
|
||||
fn change_page(&mut self, to_page: usize) {
|
||||
if let Some(offset) = self.break_pages().nth(to_page) {
|
||||
self.change_offset(offset)
|
||||
} else {
|
||||
// Should not happen, set index past last paragraph to render empty page.
|
||||
self.offset = PageOffset {
|
||||
par: self.list.len(),
|
||||
chr: 0,
|
||||
};
|
||||
self.visible = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
pub mod trace {
|
||||
use super::*;
|
||||
use crate::ui::component::text::layout::trace::TraceSink;
|
||||
|
||||
use super::*;
|
||||
|
||||
impl<T> crate::trace::Trace for Paragraphs<T>
|
||||
where
|
||||
T: AsRef<[u8]>,
|
||||
@ -163,7 +182,7 @@ pub mod trace {
|
||||
let mut char_offset = self.offset.chr;
|
||||
for paragraph in self.list.iter().skip(self.offset.par).take(self.visible) {
|
||||
paragraph.layout.layout_text(
|
||||
¶graph.content.as_ref()[char_offset..],
|
||||
paragraph.content(char_offset),
|
||||
&mut paragraph.layout.initial_cursor(),
|
||||
&mut TraceSink(t),
|
||||
);
|
||||
@ -187,18 +206,22 @@ where
|
||||
pub fn new(content: T, layout: TextLayout) -> Self {
|
||||
Self { content, layout }
|
||||
}
|
||||
|
||||
pub fn content(&self, char_offset: usize) -> &[u8] {
|
||||
&self.content.as_ref()[char_offset..]
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Dimensions for Paragraph<T>
|
||||
where
|
||||
T: AsRef<[u8]>,
|
||||
{
|
||||
fn get_size(&mut self) -> Offset {
|
||||
self.layout.bounds.size()
|
||||
fn fit(&mut self, area: Rect) {
|
||||
self.layout.bounds = area;
|
||||
}
|
||||
|
||||
fn set_area(&mut self, area: Rect) {
|
||||
self.layout.bounds = area;
|
||||
fn area(&self) -> Rect {
|
||||
self.layout.bounds
|
||||
}
|
||||
}
|
||||
|
||||
@ -240,39 +263,32 @@ where
|
||||
let mut progress = false;
|
||||
|
||||
for paragraph in self.paragraphs.list.iter().skip(current.par) {
|
||||
loop {
|
||||
let mut temp_layout = paragraph.layout;
|
||||
temp_layout.bounds = remaining_area;
|
||||
let fit = paragraph
|
||||
.layout
|
||||
.with_bounds(remaining_area)
|
||||
.fit_text(paragraph.content(current.chr));
|
||||
match fit {
|
||||
LayoutFit::Fitting { height, .. } => {
|
||||
// Text fits, update remaining area.
|
||||
remaining_area = remaining_area.inset(Insets::top(height));
|
||||
|
||||
let fit = temp_layout.layout_text(
|
||||
¶graph.content.as_ref()[current.chr..],
|
||||
&mut temp_layout.initial_cursor(),
|
||||
&mut TextNoOp,
|
||||
);
|
||||
match fit {
|
||||
LayoutFit::Fitting { height, .. } => {
|
||||
// Text fits, update remaining area.
|
||||
remaining_area = remaining_area.inset(Insets::top(height));
|
||||
|
||||
// Continue with start of next paragraph.
|
||||
current.par += 1;
|
||||
current.chr = 0;
|
||||
progress = true;
|
||||
break;
|
||||
}
|
||||
LayoutFit::OutOfBounds {
|
||||
processed_chars, ..
|
||||
} => {
|
||||
// Text does not fit, assume whatever fits takes the entire remaining area.
|
||||
current.chr += processed_chars;
|
||||
if processed_chars == 0 && !progress {
|
||||
// Nothing fits yet page is empty: terminate iterator to avoid looping
|
||||
// forever.
|
||||
return None;
|
||||
}
|
||||
// Return current offset.
|
||||
return self.current;
|
||||
// Continue with start of next paragraph.
|
||||
current.par += 1;
|
||||
current.chr = 0;
|
||||
progress = true;
|
||||
}
|
||||
LayoutFit::OutOfBounds {
|
||||
processed_chars, ..
|
||||
} => {
|
||||
// Text does not fit, assume whatever fits takes the entire remaining area.
|
||||
current.chr += processed_chars;
|
||||
if processed_chars == 0 && !progress {
|
||||
// Nothing fits yet page is empty: terminate iterator to avoid looping
|
||||
// forever.
|
||||
return None;
|
||||
}
|
||||
// Return current offset.
|
||||
return self.current;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -281,26 +297,3 @@ where
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Paginate for Paragraphs<T>
|
||||
where
|
||||
T: AsRef<[u8]>,
|
||||
{
|
||||
fn page_count(&mut self) -> usize {
|
||||
// There's always at least one page.
|
||||
self.break_pages().count().max(1)
|
||||
}
|
||||
|
||||
fn change_page(&mut self, to_page: usize) {
|
||||
if let Some(offset) = self.break_pages().skip(to_page).next() {
|
||||
self.change_offset(offset)
|
||||
} else {
|
||||
// Should not happen, set index past last paragraph to render empty page.
|
||||
self.offset = PageOffset {
|
||||
par: self.list.len(),
|
||||
chr: 0,
|
||||
};
|
||||
self.visible = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,84 +0,0 @@
|
||||
use super::{Component, Event, EventCtx};
|
||||
use crate::ui::geometry::Rect;
|
||||
|
||||
impl<T, A, B> Component for (A, B)
|
||||
where
|
||||
A: Component<Msg = T>,
|
||||
B: Component<Msg = T>,
|
||||
{
|
||||
type Msg = T;
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
self.0
|
||||
.event(ctx, event)
|
||||
.or_else(|| self.1.event(ctx, event))
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
self.0.paint();
|
||||
self.1.paint();
|
||||
}
|
||||
|
||||
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
||||
self.0.bounds(sink);
|
||||
self.1.bounds(sink);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, A, B, C> Component for (A, B, C)
|
||||
where
|
||||
A: Component<Msg = T>,
|
||||
B: Component<Msg = T>,
|
||||
C: Component<Msg = T>,
|
||||
{
|
||||
type Msg = T;
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
self.0
|
||||
.event(ctx, event)
|
||||
.or_else(|| self.1.event(ctx, event))
|
||||
.or_else(|| self.2.event(ctx, event))
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
self.0.paint();
|
||||
self.1.paint();
|
||||
self.2.paint();
|
||||
}
|
||||
|
||||
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
||||
self.0.bounds(sink);
|
||||
self.1.bounds(sink);
|
||||
self.2.bounds(sink);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<T, A, B> crate::trace::Trace for (A, B)
|
||||
where
|
||||
A: Component<Msg = T> + crate::trace::Trace,
|
||||
B: Component<Msg = T> + crate::trace::Trace,
|
||||
{
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("Tuple");
|
||||
t.field("0", &self.0);
|
||||
t.field("1", &self.1);
|
||||
t.close();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<T, A, B, C> crate::trace::Trace for (A, B, C)
|
||||
where
|
||||
A: Component<Msg = T> + crate::trace::Trace,
|
||||
B: Component<Msg = T> + crate::trace::Trace,
|
||||
C: Component<Msg = T> + crate::trace::Trace,
|
||||
{
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("Tuple");
|
||||
t.field("0", &self.0);
|
||||
t.field("1", &self.1);
|
||||
t.field("2", &self.2);
|
||||
t.close();
|
||||
}
|
||||
}
|
@ -148,6 +148,22 @@ impl Rect {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_top_left(self, p0: Point) -> Self {
|
||||
Self::from_top_left_and_size(p0, self.size())
|
||||
}
|
||||
|
||||
pub fn with_size(self, size: Offset) -> Self {
|
||||
Self::from_top_left_and_size(self.top_left(), size)
|
||||
}
|
||||
|
||||
pub fn with_width(self, width: i32) -> Self {
|
||||
self.with_size(Offset::new(width, self.height()))
|
||||
}
|
||||
|
||||
pub fn with_height(self, height: i32) -> Self {
|
||||
self.with_size(Offset::new(self.width(), height))
|
||||
}
|
||||
|
||||
pub fn width(&self) -> i32 {
|
||||
self.x1 - self.x0
|
||||
}
|
||||
@ -399,16 +415,28 @@ impl Grid {
|
||||
pub fn cell(&self, index: usize) -> Rect {
|
||||
self.row_col(index / self.cols, index % self.cols)
|
||||
}
|
||||
|
||||
pub fn cells(&self, cells: GridCellSpan) -> Rect {
|
||||
let from = self.row_col(cells.from.0, cells.to.1);
|
||||
let to = self.row_col(cells.to.0, cells.to.1);
|
||||
from.union(to)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct LinearLayout {
|
||||
pub struct GridCellSpan {
|
||||
pub from: (usize, usize),
|
||||
pub to: (usize, usize),
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct LinearPlacement {
|
||||
axis: Axis,
|
||||
align: Alignment,
|
||||
spacing: i32,
|
||||
}
|
||||
|
||||
impl LinearLayout {
|
||||
impl LinearPlacement {
|
||||
pub fn horizontal() -> Self {
|
||||
Self {
|
||||
axis: Axis::Horizontal,
|
||||
@ -445,42 +473,20 @@ impl LinearLayout {
|
||||
self
|
||||
}
|
||||
|
||||
fn compute_spacing(&self, area: Rect, count: usize, size_sum: i32) -> (i32, i32) {
|
||||
let spacing_count = count.saturating_sub(1);
|
||||
let spacing_sum = spacing_count as i32 * self.spacing;
|
||||
let naive_size = size_sum + spacing_sum;
|
||||
let available_space = area.size().axis(self.axis);
|
||||
|
||||
// scale down spacing to fit everything into area
|
||||
let (total_size, spacing) = if naive_size > available_space {
|
||||
let scaled_space = (available_space - size_sum) / spacing_count as i32;
|
||||
// forbid negative spacing
|
||||
(available_space, scaled_space.max(0))
|
||||
} else {
|
||||
(naive_size, self.spacing)
|
||||
};
|
||||
|
||||
let init_cursor = match self.align {
|
||||
Alignment::Start => 0,
|
||||
Alignment::Center => available_space / 2 - total_size / 2,
|
||||
Alignment::End => available_space - total_size,
|
||||
};
|
||||
|
||||
(init_cursor, spacing)
|
||||
}
|
||||
|
||||
/// Arranges all `items` by parameters configured in `self` into `area`.
|
||||
/// Does not change the size of the items (only the position), but it needs
|
||||
/// to iterate (and ask for the size) twice.
|
||||
/// Does not change the size of the items (only the position).
|
||||
pub fn arrange(&self, area: Rect, items: &mut [impl Dimensions]) {
|
||||
let item_sum: i32 = items.iter_mut().map(|i| i.get_size().axis(self.axis)).sum();
|
||||
let (mut cursor, spacing) = self.compute_spacing(area, items.len(), item_sum);
|
||||
let size_sum: i32 = items
|
||||
.iter_mut()
|
||||
.map(|i| i.area().size().axis(self.axis))
|
||||
.sum();
|
||||
let (mut cursor, spacing) = self.compute_spacing(area, items.len(), size_sum);
|
||||
|
||||
for item in items {
|
||||
let top_left = area.top_left() + Offset::on_axis(self.axis, cursor);
|
||||
let size = item.get_size();
|
||||
item.set_area(Rect::from_top_left_and_size(top_left, size));
|
||||
cursor += size.axis(self.axis);
|
||||
let item_origin = area.top_left() + Offset::on_axis(self.axis, cursor);
|
||||
let item_area = item.area().with_top_left(item_origin);
|
||||
item.fit(item_area);
|
||||
cursor += item_area.size().axis(self.axis);
|
||||
cursor += spacing;
|
||||
}
|
||||
}
|
||||
@ -509,10 +515,34 @@ impl LinearLayout {
|
||||
cursor += spacing;
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_spacing(&self, area: Rect, count: usize, size_sum: i32) -> (i32, i32) {
|
||||
let spacing_count = count.saturating_sub(1);
|
||||
let spacing_sum = spacing_count as i32 * self.spacing;
|
||||
let naive_size = size_sum + spacing_sum;
|
||||
let available_space = area.size().axis(self.axis);
|
||||
|
||||
// scale down spacing to fit everything into area
|
||||
let (total_size, spacing) = if naive_size > available_space {
|
||||
let scaled_space = (available_space - size_sum) / spacing_count.max(1) as i32;
|
||||
// forbid negative spacing
|
||||
(available_space, scaled_space.max(0))
|
||||
} else {
|
||||
(naive_size, self.spacing)
|
||||
};
|
||||
|
||||
let initial_cursor = match self.align {
|
||||
Alignment::Start => 0,
|
||||
Alignment::Center => available_space / 2 - total_size / 2,
|
||||
Alignment::End => available_space - total_size,
|
||||
};
|
||||
|
||||
(initial_cursor, spacing)
|
||||
}
|
||||
}
|
||||
|
||||
/// Types that have a size and a position.
|
||||
/// Types that can place themselves within area specified by `bounds`.
|
||||
pub trait Dimensions {
|
||||
fn get_size(&mut self) -> Offset;
|
||||
fn set_area(&mut self, area: Rect);
|
||||
fn fit(&mut self, bounds: Rect);
|
||||
fn area(&self) -> Rect;
|
||||
}
|
||||
|
@ -55,6 +55,7 @@ mod maybe_trace {
|
||||
impl<T> MaybeTrace for T {}
|
||||
}
|
||||
|
||||
use crate::ui::display;
|
||||
/// Stand-in for the optionally-compiled trait `Trace`.
|
||||
/// If UI debugging is enabled, `MaybeTrace` implies `Trace` and is implemented
|
||||
/// for everything that implements Trace. If disabled, `MaybeTrace` is
|
||||
@ -69,6 +70,7 @@ use maybe_trace::MaybeTrace;
|
||||
/// T>` field. `Component` itself is not object-safe because of `Component::Msg`
|
||||
/// associated type.
|
||||
pub trait ObjComponent: MaybeTrace {
|
||||
fn obj_place(&mut self, bounds: Rect) -> Rect;
|
||||
fn obj_event(&mut self, ctx: &mut EventCtx, event: Event) -> Result<Obj, Error>;
|
||||
fn obj_paint(&mut self);
|
||||
fn obj_bounds(&self, sink: &mut dyn FnMut(Rect));
|
||||
@ -78,6 +80,10 @@ impl<T> ObjComponent for Child<T>
|
||||
where
|
||||
T: ComponentMsgObj + MaybeTrace,
|
||||
{
|
||||
fn obj_place(&mut self, bounds: Rect) -> Rect {
|
||||
self.place(bounds)
|
||||
}
|
||||
|
||||
fn obj_event(&mut self, ctx: &mut EventCtx, event: Event) -> Result<Obj, Error> {
|
||||
if let Some(msg) = self.event(ctx, event) {
|
||||
self.inner().msg_try_into_obj(msg)
|
||||
@ -143,6 +149,12 @@ impl LayoutObj {
|
||||
fn obj_event(&self, event: Event) -> Result<Obj, Error> {
|
||||
let inner = &mut *self.inner.borrow_mut();
|
||||
|
||||
// Place the root component on the screen in case it was previously requested.
|
||||
if inner.event_ctx.needs_place_before_next_event_or_paint() {
|
||||
// SAFETY: `inner.root` is unique because of the `inner.borrow_mut()`.
|
||||
unsafe { Gc::as_mut(&mut inner.root) }.obj_place(display::screen());
|
||||
}
|
||||
|
||||
// Clear the leftover flags from the previous event pass.
|
||||
inner.event_ctx.clear();
|
||||
|
||||
@ -170,6 +182,13 @@ impl LayoutObj {
|
||||
/// Run a paint pass over the component tree.
|
||||
fn obj_paint_if_requested(&self) {
|
||||
let mut inner = self.inner.borrow_mut();
|
||||
|
||||
// Place the root component on the screen in case it was previously requested.
|
||||
if inner.event_ctx.needs_place_before_next_event_or_paint() {
|
||||
// SAFETY: `inner.root` is unique because of the `inner.borrow_mut()`.
|
||||
unsafe { Gc::as_mut(&mut inner.root) }.obj_place(display::screen());
|
||||
}
|
||||
|
||||
// SAFETY: `inner.root` is unique because of the `inner.borrow_mut()`.
|
||||
unsafe { Gc::as_mut(&mut inner.root) }.obj_paint();
|
||||
}
|
||||
|
@ -38,34 +38,23 @@ pub struct Button<T> {
|
||||
}
|
||||
|
||||
impl<T: AsRef<[u8]>> Button<T> {
|
||||
pub fn new(
|
||||
area: Rect,
|
||||
pos: ButtonPos,
|
||||
content: ButtonContent<T>,
|
||||
styles: ButtonStyleSheet,
|
||||
) -> Self {
|
||||
let (area, baseline) = Self::placement(area, pos, &content, &styles);
|
||||
pub fn new(pos: ButtonPos, content: ButtonContent<T>, styles: ButtonStyleSheet) -> Self {
|
||||
Self {
|
||||
area,
|
||||
pos,
|
||||
baseline,
|
||||
content,
|
||||
styles,
|
||||
baseline: Point::zero(),
|
||||
area: Rect::zero(),
|
||||
state: State::Released,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_text(area: Rect, pos: ButtonPos, text: T, styles: ButtonStyleSheet) -> Self {
|
||||
Self::new(area, pos, ButtonContent::Text(text), styles)
|
||||
pub fn with_text(pos: ButtonPos, text: T, styles: ButtonStyleSheet) -> Self {
|
||||
Self::new(pos, ButtonContent::Text(text), styles)
|
||||
}
|
||||
|
||||
pub fn with_icon(
|
||||
area: Rect,
|
||||
pos: ButtonPos,
|
||||
image: &'static [u8],
|
||||
styles: ButtonStyleSheet,
|
||||
) -> Self {
|
||||
Self::new(area, pos, ButtonContent::Icon(image), styles)
|
||||
pub fn with_icon(pos: ButtonPos, image: &'static [u8], styles: ButtonStyleSheet) -> Self {
|
||||
Self::new(pos, ButtonContent::Icon(image), styles)
|
||||
}
|
||||
|
||||
pub fn content(&self) -> &ButtonContent<T> {
|
||||
@ -109,9 +98,19 @@ impl<T: AsRef<[u8]>> Button<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AsRef<[u8]>> Component for Button<T> {
|
||||
impl<T> Component for Button<T>
|
||||
where
|
||||
T: AsRef<[u8]>,
|
||||
{
|
||||
type Msg = ButtonMsg;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
let (area, baseline) = Self::placement(bounds, self.pos, &self.content, &self.styles);
|
||||
self.area = area;
|
||||
self.baseline = baseline;
|
||||
self.area
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
match event {
|
||||
Event::Button(ButtonEvent::ButtonPressed(which)) if self.pos.hit(&which) => {
|
||||
|
@ -1,5 +1,5 @@
|
||||
use super::{
|
||||
button::{Button, ButtonMsg::Clicked, ButtonPos},
|
||||
button::{Button, ButtonMsg::Clicked},
|
||||
theme,
|
||||
};
|
||||
use crate::ui::{
|
||||
@ -19,34 +19,36 @@ pub struct Dialog<T, U> {
|
||||
right_btn: Option<Child<Button<U>>>,
|
||||
}
|
||||
|
||||
impl<T: Component, U: AsRef<[u8]>> Dialog<T, U> {
|
||||
pub fn new(
|
||||
area: Rect,
|
||||
content: impl FnOnce(Rect) -> T,
|
||||
left: Option<impl FnOnce(Rect, ButtonPos) -> Button<U>>,
|
||||
right: Option<impl FnOnce(Rect, ButtonPos) -> Button<U>>,
|
||||
) -> Self {
|
||||
let (content_area, button_area) = Self::areas(area);
|
||||
let content = Child::new(content(content_area));
|
||||
let left_btn = left.map(|f| Child::new(f(button_area, ButtonPos::Left)));
|
||||
let right_btn = right.map(|f| Child::new(f(button_area, ButtonPos::Right)));
|
||||
impl<T, U> Dialog<T, U>
|
||||
where
|
||||
T: Component,
|
||||
U: AsRef<[u8]>,
|
||||
{
|
||||
pub fn new(content: T, left: Option<Button<U>>, right: Option<Button<U>>) -> Self {
|
||||
Self {
|
||||
content,
|
||||
left_btn,
|
||||
right_btn,
|
||||
content: Child::new(content),
|
||||
left_btn: left.map(Child::new),
|
||||
right_btn: right.map(Child::new),
|
||||
}
|
||||
}
|
||||
|
||||
fn areas(area: Rect) -> (Rect, Rect) {
|
||||
let button_height = theme::FONT_BOLD.line_height() + 2;
|
||||
let (content_area, button_area) = area.split_bottom(button_height);
|
||||
(content_area, button_area)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Component, U: AsRef<[u8]>> Component for Dialog<T, U> {
|
||||
impl<T, U> Component for Dialog<T, U>
|
||||
where
|
||||
T: Component,
|
||||
U: AsRef<[u8]>,
|
||||
{
|
||||
type Msg = DialogMsg<T::Msg>;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
let button_height = theme::FONT_BOLD.line_height() + 2;
|
||||
let (content_area, button_area) = bounds.split_bottom(button_height);
|
||||
self.content.place(content_area);
|
||||
self.left_btn.as_mut().map(|b| b.place(button_area));
|
||||
self.right_btn.as_mut().map(|b| b.place(button_area));
|
||||
bounds
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
if let Some(msg) = self.content.event(ctx, event) {
|
||||
Some(DialogMsg::Content(msg))
|
||||
|
@ -1,8 +1,8 @@
|
||||
use super::theme;
|
||||
use crate::ui::{
|
||||
component::{Child, Component, ComponentExt, Event, EventCtx},
|
||||
component::{Child, Component, Event, EventCtx},
|
||||
display,
|
||||
geometry::{Offset, Rect},
|
||||
geometry::{Insets, Offset, Rect},
|
||||
};
|
||||
|
||||
pub struct Frame<T, U> {
|
||||
@ -11,30 +11,38 @@ pub struct Frame<T, U> {
|
||||
content: Child<T>,
|
||||
}
|
||||
|
||||
impl<T: Component, U: AsRef<[u8]>> Frame<T, U> {
|
||||
pub fn new(area: Rect, title: U, content: impl FnOnce(Rect) -> T) -> Self {
|
||||
let (title_area, content_area) = Self::areas(area);
|
||||
impl<T, U> Frame<T, U>
|
||||
where
|
||||
T: Component,
|
||||
U: AsRef<[u8]>,
|
||||
{
|
||||
pub fn new(title: U, content: T) -> Self {
|
||||
Self {
|
||||
area: title_area,
|
||||
title,
|
||||
content: content(content_area).into_child(),
|
||||
area: Rect::zero(),
|
||||
content: Child::new(content),
|
||||
}
|
||||
}
|
||||
|
||||
fn areas(area: Rect) -> (Rect, Rect) {
|
||||
const HEADER_SPACE: i32 = 4;
|
||||
let header_height = theme::FONT_BOLD.line_height();
|
||||
|
||||
let (header_area, content_area) = area.split_top(header_height);
|
||||
let (_space, content_area) = content_area.split_top(HEADER_SPACE);
|
||||
|
||||
(header_area, content_area)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Component, U: AsRef<[u8]>> Component for Frame<T, U> {
|
||||
impl<T, U> Component for Frame<T, U>
|
||||
where
|
||||
T: Component,
|
||||
U: AsRef<[u8]>,
|
||||
{
|
||||
type Msg = T::Msg;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
const TITLE_SPACE: i32 = 4;
|
||||
|
||||
let (title_area, content_area) = bounds.split_top(theme::FONT_BOLD.line_height());
|
||||
let content_area = content_area.inset(Insets::top(TITLE_SPACE));
|
||||
|
||||
self.area = title_area;
|
||||
self.content.place(content_area);
|
||||
bounds
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
self.content.event(ctx, event)
|
||||
}
|
||||
|
@ -21,54 +21,18 @@ where
|
||||
T: Paginate,
|
||||
T: Component,
|
||||
{
|
||||
pub fn new(area: Rect, content: impl FnOnce(Rect) -> T, background: Color) -> Self {
|
||||
let (content_area, scrollbar_area, button_area) = Self::areas(area);
|
||||
let mut content = content(content_area);
|
||||
let pad = Pad::with_background(area, background);
|
||||
|
||||
// Always start at the first page.
|
||||
let scrollbar = ScrollBar::vertical_right(scrollbar_area, content.page_count(), 0);
|
||||
|
||||
// Create the button controls.
|
||||
let prev = Button::with_text(button_area, ButtonPos::Left, "BACK", theme::button_cancel());
|
||||
let next = Button::with_text(
|
||||
button_area,
|
||||
ButtonPos::Right,
|
||||
"NEXT",
|
||||
theme::button_default(),
|
||||
);
|
||||
let cancel = Button::with_text(
|
||||
button_area,
|
||||
ButtonPos::Left,
|
||||
"CANCEL",
|
||||
theme::button_cancel(),
|
||||
);
|
||||
let confirm = Button::with_text(
|
||||
button_area,
|
||||
ButtonPos::Right,
|
||||
"CONFIRM",
|
||||
theme::button_default(),
|
||||
);
|
||||
|
||||
pub fn new(content: T, background: Color) -> Self {
|
||||
Self {
|
||||
content,
|
||||
scrollbar,
|
||||
pad,
|
||||
prev,
|
||||
next,
|
||||
cancel,
|
||||
confirm,
|
||||
scrollbar: ScrollBar::vertical(),
|
||||
pad: Pad::with_background(background),
|
||||
prev: Button::with_text(ButtonPos::Left, "BACK", theme::button_cancel()),
|
||||
next: Button::with_text(ButtonPos::Right, "NEXT", theme::button_default()),
|
||||
cancel: Button::with_text(ButtonPos::Left, "CANCEL", theme::button_cancel()),
|
||||
confirm: Button::with_text(ButtonPos::Right, "CONFIRM", theme::button_default()),
|
||||
}
|
||||
}
|
||||
|
||||
fn areas(area: Rect) -> (Rect, Rect, Rect) {
|
||||
let button_height = theme::FONT_BOLD.line_height() + 2;
|
||||
let (content_area, button_area) = area.split_bottom(button_height);
|
||||
let (content_area, scrollbar_area) = content_area.split_right(ScrollBar::WIDTH);
|
||||
let (content_area, _) = content_area.split_bottom(1);
|
||||
(content_area, scrollbar_area, button_area)
|
||||
}
|
||||
|
||||
fn change_page(&mut self, ctx: &mut EventCtx, page: usize) {
|
||||
// Change the page in the content, clear the background under it and make sure
|
||||
// it gets completely repainted.
|
||||
@ -85,6 +49,23 @@ where
|
||||
{
|
||||
type Msg = PageMsg<T::Msg, bool>;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
let button_height = theme::FONT_BOLD.line_height() + 2;
|
||||
let (content_area, button_area) = bounds.split_bottom(button_height);
|
||||
let (content_area, scrollbar_area) = content_area.split_right(ScrollBar::WIDTH);
|
||||
let content_area = content_area.inset(Insets::top(1));
|
||||
self.pad.place(bounds);
|
||||
self.content.place(content_area);
|
||||
let page_count = self.content.page_count();
|
||||
self.scrollbar.set_count_and_active_page(page_count, 0);
|
||||
self.scrollbar.place(scrollbar_area);
|
||||
self.prev.place(button_area);
|
||||
self.next.place(button_area);
|
||||
self.cancel.place(button_area);
|
||||
self.confirm.place(button_area);
|
||||
bounds
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
if self.scrollbar.has_previous_page() {
|
||||
if let Some(ButtonMsg::Clicked) = self.prev.event(ctx, event) {
|
||||
@ -156,14 +137,19 @@ impl ScrollBar {
|
||||
pub const DOT_SIZE: Offset = Offset::new(4, 4);
|
||||
pub const DOT_INTERVAL: i32 = 6;
|
||||
|
||||
pub fn vertical_right(area: Rect, page_count: usize, active_page: usize) -> Self {
|
||||
pub fn vertical() -> Self {
|
||||
Self {
|
||||
area,
|
||||
page_count,
|
||||
active_page,
|
||||
area: Rect::zero(),
|
||||
page_count: 0,
|
||||
active_page: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_count_and_active_page(&mut self, page_count: usize, active_page: usize) {
|
||||
self.page_count = page_count;
|
||||
self.active_page = active_page;
|
||||
}
|
||||
|
||||
pub fn has_next_page(&self) -> bool {
|
||||
self.active_page < self.page_count - 1
|
||||
}
|
||||
@ -208,6 +194,11 @@ impl ScrollBar {
|
||||
impl Component for ScrollBar {
|
||||
type Msg = Never;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
self.area = bounds;
|
||||
self.area
|
||||
}
|
||||
|
||||
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
|
||||
None
|
||||
}
|
||||
|
@ -4,8 +4,8 @@ use crate::{
|
||||
micropython::{buffer::Buffer, map::Map, obj::Obj, qstr::Qstr},
|
||||
ui::{
|
||||
component::{text::paragraphs::Paragraphs, FormattedText},
|
||||
display,
|
||||
layout::obj::LayoutObj,
|
||||
model_t1::component::ButtonPos,
|
||||
},
|
||||
util,
|
||||
};
|
||||
@ -40,21 +40,19 @@ extern "C" fn ui_layout_new_confirm_action(
|
||||
};
|
||||
|
||||
let left = verb_cancel
|
||||
.map(|label| |area, pos| Button::with_text(area, pos, label, theme::button_cancel()));
|
||||
let right = verb
|
||||
.map(|label| |area, pos| Button::with_text(area, pos, label, theme::button_default()));
|
||||
.map(|label| Button::with_text(ButtonPos::Left, label, theme::button_cancel()));
|
||||
let right =
|
||||
verb.map(|label| Button::with_text(ButtonPos::Right, label, theme::button_default()));
|
||||
|
||||
let obj = LayoutObj::new(Frame::new(display::screen(), title, |area| {
|
||||
let obj = LayoutObj::new(Frame::new(
|
||||
title,
|
||||
ButtonPage::new(
|
||||
area,
|
||||
|area| {
|
||||
FormattedText::new::<theme::T1DefaultText>(area, format)
|
||||
.with(b"action", action.unwrap_or("".into()))
|
||||
.with(b"description", description.unwrap_or("".into()))
|
||||
},
|
||||
FormattedText::new::<theme::T1DefaultText>(format)
|
||||
.with(b"action", action.unwrap_or_default())
|
||||
.with(b"description", description.unwrap_or_default()),
|
||||
theme::BG,
|
||||
)
|
||||
}))?;
|
||||
),
|
||||
))?;
|
||||
Ok(obj.into())
|
||||
};
|
||||
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
||||
@ -72,20 +70,18 @@ extern "C" fn ui_layout_new_confirm_text(
|
||||
let description: Option<Buffer> =
|
||||
kwargs.get(Qstr::MP_QSTR_description)?.try_into_option()?;
|
||||
|
||||
let obj = LayoutObj::new(Frame::new(display::screen(), title, |area| {
|
||||
let obj = LayoutObj::new(Frame::new(
|
||||
title,
|
||||
ButtonPage::new(
|
||||
area,
|
||||
|area| {
|
||||
Paragraphs::new(area)
|
||||
.add::<theme::T1DefaultText>(
|
||||
theme::FONT_NORMAL,
|
||||
description.unwrap_or("".into()),
|
||||
)
|
||||
.add::<theme::T1DefaultText>(theme::FONT_BOLD, data)
|
||||
},
|
||||
Paragraphs::new()
|
||||
.add::<theme::T1DefaultText>(
|
||||
theme::FONT_NORMAL,
|
||||
description.unwrap_or_default(),
|
||||
)
|
||||
.add::<theme::T1DefaultText>(theme::FONT_BOLD, data),
|
||||
theme::BG,
|
||||
)
|
||||
}))?;
|
||||
),
|
||||
))?;
|
||||
Ok(obj.into())
|
||||
};
|
||||
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
||||
@ -96,7 +92,11 @@ mod tests {
|
||||
use crate::{
|
||||
error::Error,
|
||||
trace::Trace,
|
||||
ui::model_t1::component::{Dialog, DialogMsg},
|
||||
ui::{
|
||||
component::Component,
|
||||
display,
|
||||
model_t1::component::{Dialog, DialogMsg},
|
||||
},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
@ -125,18 +125,23 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn trace_example_layout() {
|
||||
let layout = Dialog::new(
|
||||
display::screen(),
|
||||
|area| {
|
||||
FormattedText::new::<theme::T1DefaultText>(
|
||||
area,
|
||||
"Testing text layout, with some text, and some more text. And {param}",
|
||||
)
|
||||
.with(b"param", b"parameters!")
|
||||
},
|
||||
Some(|area, pos| Button::with_text(area, pos, "Left", theme::button_cancel())),
|
||||
Some(|area, pos| Button::with_text(area, pos, "Right", theme::button_default())),
|
||||
let mut layout = Dialog::new(
|
||||
FormattedText::new::<theme::T1DefaultText>(
|
||||
"Testing text layout, with some text, and some more text. And {param}",
|
||||
)
|
||||
.with(b"param", b"parameters!"),
|
||||
Some(Button::with_text(
|
||||
ButtonPos::Left,
|
||||
"Left",
|
||||
theme::button_cancel(),
|
||||
)),
|
||||
Some(Button::with_text(
|
||||
ButtonPos::Right,
|
||||
"Right",
|
||||
theme::button_default(),
|
||||
)),
|
||||
);
|
||||
layout.place(display::screen());
|
||||
assert_eq!(
|
||||
trace(&layout),
|
||||
r#"<Dialog content:<Text content:Testing text layout,
|
||||
@ -148,20 +153,26 @@ arameters! > left:<Button text:Left > right:<Button text:Right > >"#
|
||||
|
||||
#[test]
|
||||
fn trace_layout_title() {
|
||||
let layout = Frame::new(display::screen(), "Please confirm", |area| {
|
||||
let mut layout = Frame::new(
|
||||
"Please confirm",
|
||||
Dialog::new(
|
||||
area,
|
||||
|area| {
|
||||
FormattedText::new::<theme::T1DefaultText>(
|
||||
area,
|
||||
"Testing text layout, with some text, and some more text. And {param}",
|
||||
)
|
||||
.with(b"param", b"parameters!")
|
||||
},
|
||||
Some(|area, pos| Button::with_text(area, pos, "Left", theme::button_cancel())),
|
||||
Some(|area, pos| Button::with_text(area, pos, "Right", theme::button_default())),
|
||||
)
|
||||
});
|
||||
FormattedText::new::<theme::T1DefaultText>(
|
||||
"Testing text layout, with some text, and some more text. And {param}",
|
||||
)
|
||||
.with(b"param", b"parameters!"),
|
||||
Some(Button::with_text(
|
||||
ButtonPos::Left,
|
||||
"Left",
|
||||
theme::button_cancel(),
|
||||
)),
|
||||
Some(Button::with_text(
|
||||
ButtonPos::Right,
|
||||
"Right",
|
||||
theme::button_default(),
|
||||
)),
|
||||
),
|
||||
);
|
||||
layout.place(display::screen());
|
||||
assert_eq!(
|
||||
trace(&layout),
|
||||
r#"<Frame title:Please confirm content:<Dialog content:<Text content:Testing text layout,
|
||||
|
@ -1,7 +1,7 @@
|
||||
use crate::ui::{
|
||||
component::{Component, Event, EventCtx, Map},
|
||||
component::{Component, ComponentExt, Event, EventCtx, GridPlaced, Map},
|
||||
display::{self, Color, Font},
|
||||
geometry::{Grid, Insets, Offset, Rect},
|
||||
geometry::{Insets, Offset, Rect},
|
||||
};
|
||||
|
||||
use super::{event::TouchEvent, theme};
|
||||
@ -27,25 +27,25 @@ impl<T> Button<T> {
|
||||
/// (positive).
|
||||
pub const BASELINE_OFFSET: i32 = -3;
|
||||
|
||||
pub fn new(area: Rect, content: ButtonContent<T>) -> Self {
|
||||
pub fn new(content: ButtonContent<T>) -> Self {
|
||||
Self {
|
||||
area,
|
||||
content,
|
||||
area: Rect::zero(),
|
||||
styles: theme::button_default(),
|
||||
state: State::Initial,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_text(area: Rect, text: T) -> Self {
|
||||
Self::new(area, ButtonContent::Text(text))
|
||||
pub fn with_text(text: T) -> Self {
|
||||
Self::new(ButtonContent::Text(text))
|
||||
}
|
||||
|
||||
pub fn with_icon(area: Rect, image: &'static [u8]) -> Self {
|
||||
Self::new(area, ButtonContent::Icon(image))
|
||||
pub fn with_icon(image: &'static [u8]) -> Self {
|
||||
Self::new(ButtonContent::Icon(image))
|
||||
}
|
||||
|
||||
pub fn empty(area: Rect) -> Self {
|
||||
Self::new(area, ButtonContent::Empty)
|
||||
pub fn empty() -> Self {
|
||||
Self::new(ButtonContent::Empty)
|
||||
}
|
||||
|
||||
pub fn styled(mut self, styles: ButtonStyleSheet) -> Self {
|
||||
@ -53,6 +53,14 @@ impl<T> Button<T> {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn enable_if(&mut self, ctx: &mut EventCtx, enabled: bool) {
|
||||
if enabled {
|
||||
self.enable(ctx);
|
||||
} else {
|
||||
self.disable(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn enable(&mut self, ctx: &mut EventCtx) {
|
||||
self.set(ctx, State::Initial)
|
||||
}
|
||||
@ -61,14 +69,6 @@ impl<T> Button<T> {
|
||||
self.set(ctx, State::Disabled)
|
||||
}
|
||||
|
||||
pub fn enabled(&mut self, ctx: &mut EventCtx, enabled: bool) {
|
||||
if enabled {
|
||||
self.enable(ctx);
|
||||
} else {
|
||||
self.disable(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_enabled(&self) -> bool {
|
||||
matches!(
|
||||
self.state,
|
||||
@ -186,6 +186,11 @@ where
|
||||
{
|
||||
type Msg = ButtonMsg;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
self.area = bounds;
|
||||
self.area
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
match event {
|
||||
Event::Touch(TouchEvent::TouchStart(pos)) => {
|
||||
@ -297,26 +302,29 @@ pub struct ButtonStyle {
|
||||
}
|
||||
|
||||
impl<T> Button<T> {
|
||||
pub fn array2<F0, F1, R>(
|
||||
area: Rect,
|
||||
left: impl FnOnce(Rect) -> Button<T>,
|
||||
pub fn left_right<F0, F1, R>(
|
||||
left: Button<T>,
|
||||
left_map: F0,
|
||||
right: impl FnOnce(Rect) -> Button<T>,
|
||||
right: Button<T>,
|
||||
right_map: F1,
|
||||
) -> (Map<Self, F0>, Map<Self, F1>)
|
||||
) -> (Map<GridPlaced<Self>, F0>, Map<GridPlaced<Self>, F1>)
|
||||
where
|
||||
F0: Fn(ButtonMsg) -> Option<R>,
|
||||
F1: Fn(ButtonMsg) -> Option<R>,
|
||||
T: AsRef<[u8]>,
|
||||
{
|
||||
const BUTTON_SPACING: i32 = 6;
|
||||
let grid = Grid::new(area, 1, 3).with_spacing(BUTTON_SPACING);
|
||||
let left = left(grid.row_col(0, 0));
|
||||
let right = right(Rect::new(
|
||||
grid.row_col(0, 1).top_left(),
|
||||
grid.row_col(0, 2).bottom_right(),
|
||||
));
|
||||
|
||||
(Map::new(left, left_map), Map::new(right, right_map))
|
||||
(
|
||||
GridPlaced::new(left)
|
||||
.with_grid(1, 3)
|
||||
.with_spacing(BUTTON_SPACING)
|
||||
.with_row_col(0, 0)
|
||||
.map(left_map),
|
||||
GridPlaced::new(right)
|
||||
.with_grid(1, 3)
|
||||
.with_spacing(BUTTON_SPACING)
|
||||
.with_from_to((0, 1), (0, 2))
|
||||
.map(right_map),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -3,10 +3,11 @@ use crate::{
|
||||
ui::{
|
||||
component::{Child, Component, ComponentExt, Event, EventCtx, Pad},
|
||||
geometry::Rect,
|
||||
model_tt::component::DialogLayout,
|
||||
},
|
||||
};
|
||||
|
||||
use super::{theme, Button, ButtonMsg, DialogLayout, Loader, LoaderMsg};
|
||||
use super::{theme, Button, ButtonMsg, Loader, LoaderMsg};
|
||||
|
||||
pub enum HoldToConfirmMsg<T> {
|
||||
Content(T),
|
||||
@ -26,14 +27,13 @@ impl<T> HoldToConfirm<T>
|
||||
where
|
||||
T: Component,
|
||||
{
|
||||
pub fn new(area: Rect, content: impl FnOnce(Rect) -> T) -> Self {
|
||||
let layout = DialogLayout::middle(area);
|
||||
pub fn new(content: T) -> Self {
|
||||
Self {
|
||||
loader: Loader::new(0),
|
||||
content: content(layout.content).into_child(),
|
||||
cancel: Button::with_text(layout.left, "Cancel").into_child(),
|
||||
confirm: Button::with_text(layout.right, "Hold").into_child(),
|
||||
pad: Pad::with_background(layout.content, theme::BG),
|
||||
content: Child::new(content),
|
||||
cancel: Child::new(Button::with_text("Cancel")),
|
||||
confirm: Child::new(Button::with_text("Hold")),
|
||||
pad: Pad::with_background(theme::BG),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -44,6 +44,15 @@ where
|
||||
{
|
||||
type Msg = HoldToConfirmMsg<T::Msg>;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
let layout = DialogLayout::middle(bounds);
|
||||
self.loader.place(layout.content);
|
||||
self.content.place(layout.content);
|
||||
self.cancel.place(layout.left);
|
||||
self.confirm.place(layout.right);
|
||||
bounds
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
let now = Instant::now();
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
use crate::ui::{
|
||||
component::{base::ComponentExt, Child, Component, Event, EventCtx},
|
||||
component::{Child, Component, Event, EventCtx},
|
||||
geometry::{Grid, Rect},
|
||||
};
|
||||
|
||||
@ -21,17 +21,11 @@ where
|
||||
L: Component,
|
||||
R: Component,
|
||||
{
|
||||
pub fn new(
|
||||
area: Rect,
|
||||
content: impl FnOnce(Rect) -> T,
|
||||
left: impl FnOnce(Rect) -> L,
|
||||
right: impl FnOnce(Rect) -> R,
|
||||
) -> Self {
|
||||
let layout = DialogLayout::middle(area);
|
||||
pub fn new(content: T, left: L, right: R) -> Self {
|
||||
Self {
|
||||
content: content(layout.content).into_child(),
|
||||
left: left(layout.left).into_child(),
|
||||
right: right(layout.right).into_child(),
|
||||
content: Child::new(content),
|
||||
left: Child::new(left),
|
||||
right: Child::new(right),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -44,6 +38,14 @@ where
|
||||
{
|
||||
type Msg = DialogMsg<T::Msg, L::Msg, R::Msg>;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
let layout = DialogLayout::middle(bounds);
|
||||
self.content.place(layout.content);
|
||||
self.left.place(layout.left);
|
||||
self.right.place(layout.right);
|
||||
bounds
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
self.content
|
||||
.event(ctx, event)
|
||||
|
@ -1,6 +1,6 @@
|
||||
use super::theme;
|
||||
use crate::ui::{
|
||||
component::{Child, Component, ComponentExt, Event, EventCtx},
|
||||
component::{Child, Component, Event, EventCtx},
|
||||
display,
|
||||
geometry::{Insets, Rect},
|
||||
};
|
||||
@ -16,25 +16,13 @@ where
|
||||
T: Component,
|
||||
U: AsRef<[u8]>,
|
||||
{
|
||||
pub fn new(area: Rect, title: U, content: impl FnOnce(Rect) -> T) -> Self {
|
||||
let (title_area, content_area) = Self::areas(area);
|
||||
pub fn new(title: U, content: T) -> Self {
|
||||
Self {
|
||||
area: title_area,
|
||||
title,
|
||||
content: content(content_area).into_child(),
|
||||
area: Rect::zero(),
|
||||
content: Child::new(content),
|
||||
}
|
||||
}
|
||||
|
||||
fn areas(area: Rect) -> (Rect, Rect) {
|
||||
// Same as PageLayout::BUTTON_SPACE.
|
||||
const TITLE_SPACE: i32 = 6;
|
||||
|
||||
let (title_area, content_area) = area.split_top(theme::FONT_BOLD.text_height());
|
||||
let title_area = title_area.inset(Insets::left(theme::CONTENT_BORDER));
|
||||
let content_area = content_area.inset(Insets::top(TITLE_SPACE));
|
||||
|
||||
(title_area, content_area)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, U> Component for Frame<T, U>
|
||||
@ -44,6 +32,21 @@ where
|
||||
{
|
||||
type Msg = T::Msg;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
// Same as PageLayout::BUTTON_SPACE.
|
||||
const TITLE_SPACE: i32 = 6;
|
||||
|
||||
let (title_area, content_area) = bounds
|
||||
.inset(theme::borders_scroll())
|
||||
.split_top(theme::FONT_BOLD.text_height());
|
||||
let title_area = title_area.inset(Insets::left(theme::CONTENT_BORDER));
|
||||
let content_area = content_area.inset(Insets::top(TITLE_SPACE));
|
||||
|
||||
self.area = title_area;
|
||||
self.content.place(content_area);
|
||||
bounds
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
self.content.event(ctx, event)
|
||||
}
|
||||
|
@ -27,15 +27,6 @@ pub struct Bip39Input {
|
||||
}
|
||||
|
||||
impl MnemonicInput for Bip39Input {
|
||||
fn new(area: Rect) -> Self {
|
||||
Self {
|
||||
button: Button::empty(area),
|
||||
textbox: TextBox::empty(),
|
||||
multi_tap: MultiTapKeyboard::new(),
|
||||
suggested_word: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the key set. Keys are further specified as indices into this
|
||||
/// array.
|
||||
fn keys() -> [&'static str; MNEMONIC_KEY_COUNT] {
|
||||
@ -78,6 +69,10 @@ impl MnemonicInput for Bip39Input {
|
||||
impl Component for Bip39Input {
|
||||
type Msg = MnemonicInputMsg;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
self.button.place(bounds)
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
if self.multi_tap.is_timeout_event(event) {
|
||||
self.on_timeout(ctx)
|
||||
@ -146,6 +141,15 @@ impl Component for Bip39Input {
|
||||
}
|
||||
|
||||
impl Bip39Input {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
button: Button::empty(),
|
||||
textbox: TextBox::empty(),
|
||||
multi_tap: MultiTapKeyboard::new(),
|
||||
suggested_word: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute a bitmask of all letters contained in given key text. Lowest bit
|
||||
/// is 'a', second lowest 'b', etc.
|
||||
fn key_mask(key: usize) -> u32 {
|
||||
|
@ -199,16 +199,3 @@ impl<const L: usize> TextBox<L> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Analogue to `[T]::enumerate().map(...)`.
|
||||
pub fn array_map_enumerate<T, U, const L: usize>(
|
||||
array: [T; L],
|
||||
mut func: impl FnMut(usize, T) -> U,
|
||||
) -> [U; L] {
|
||||
let mut i = 0;
|
||||
array.map(|t| {
|
||||
let u = func(i, t);
|
||||
i += 1;
|
||||
u
|
||||
})
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ use crate::ui::{
|
||||
component::{Child, Component, Event, EventCtx, Label, Maybe},
|
||||
geometry::{Grid, Rect},
|
||||
model_tt::{
|
||||
component::{keyboard::common::array_map_enumerate, Button, ButtonMsg},
|
||||
component::{Button, ButtonMsg},
|
||||
theme,
|
||||
},
|
||||
};
|
||||
@ -28,45 +28,24 @@ impl<T> MnemonicKeyboard<T>
|
||||
where
|
||||
T: MnemonicInput,
|
||||
{
|
||||
pub fn new(area: Rect, prompt: &'static [u8]) -> Self {
|
||||
let grid = Grid::new(area, 3, 4);
|
||||
let back_area = grid.row_col(0, 0);
|
||||
let input_area = grid.row_col(0, 1).union(grid.row_col(0, 3));
|
||||
let prompt_area = grid.row_col(0, 0).union(grid.row_col(0, 3));
|
||||
let prompt_origin = prompt_area.top_left();
|
||||
|
||||
let input = T::new(input_area);
|
||||
let keys = T::keys();
|
||||
|
||||
pub fn new(input: T, prompt: &'static [u8]) -> Self {
|
||||
Self {
|
||||
prompt: Child::new(Maybe::visible(
|
||||
prompt_area,
|
||||
theme::BG,
|
||||
Label::left_aligned(prompt_origin, prompt, theme::label_default()),
|
||||
Label::left_aligned(prompt, theme::label_default()),
|
||||
)),
|
||||
back: Child::new(Maybe::hidden(
|
||||
back_area,
|
||||
theme::BG,
|
||||
Button::with_icon(back_area, theme::ICON_BACK).styled(theme::button_clear()),
|
||||
Button::with_icon(theme::ICON_BACK).styled(theme::button_clear()),
|
||||
)),
|
||||
input: Child::new(Maybe::hidden(input_area, theme::BG, input)),
|
||||
keys: Self::key_buttons(keys, &grid, grid.cols), // Start in the second row.
|
||||
input: Child::new(Maybe::hidden(theme::BG, input)),
|
||||
keys: T::keys()
|
||||
.map(str::as_bytes)
|
||||
.map(Button::with_text)
|
||||
.map(Child::new),
|
||||
}
|
||||
}
|
||||
|
||||
fn key_buttons(
|
||||
keys: [&'static str; MNEMONIC_KEY_COUNT],
|
||||
grid: &Grid,
|
||||
offset: usize,
|
||||
) -> [Child<Button<&'static [u8]>>; MNEMONIC_KEY_COUNT] {
|
||||
array_map_enumerate(keys, |index, text| {
|
||||
Child::new(Button::with_text(
|
||||
grid.cell(offset + index),
|
||||
text.as_bytes(),
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn on_input_change(&mut self, ctx: &mut EventCtx) {
|
||||
self.toggle_key_buttons(ctx);
|
||||
self.toggle_prompt_or_input(ctx);
|
||||
@ -81,7 +60,7 @@ where
|
||||
.inner()
|
||||
.inner()
|
||||
.can_key_press_lead_to_a_valid_word(key);
|
||||
btn.mutate(ctx, |ctx, b| b.enabled(ctx, enabled));
|
||||
btn.mutate(ctx, |ctx, b| b.enable_if(ctx, enabled));
|
||||
}
|
||||
}
|
||||
|
||||
@ -104,6 +83,21 @@ where
|
||||
{
|
||||
type Msg = MnemonicKeyboardMsg;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
let grid = Grid::new(bounds, 3, 4);
|
||||
let back_area = grid.row_col(0, 0);
|
||||
let input_area = grid.row_col(0, 1).union(grid.row_col(0, 3));
|
||||
let prompt_area = grid.row_col(0, 0).union(grid.row_col(0, 3));
|
||||
|
||||
self.prompt.place(prompt_area);
|
||||
self.back.place(back_area);
|
||||
self.input.place(input_area);
|
||||
for (key, btn) in self.keys.iter_mut().enumerate() {
|
||||
btn.place(grid.cell(key + grid.cols)); // Start in the second row.
|
||||
}
|
||||
bounds
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
match self.input.event(ctx, event) {
|
||||
Some(MnemonicInputMsg::Confirmed) => {
|
||||
@ -145,7 +139,6 @@ where
|
||||
}
|
||||
|
||||
pub trait MnemonicInput: Component<Msg = MnemonicInputMsg> {
|
||||
fn new(area: Rect) -> Self;
|
||||
fn keys() -> [&'static str; MNEMONIC_KEY_COUNT];
|
||||
fn can_key_press_lead_to_a_valid_word(&self, key: usize) -> bool;
|
||||
fn on_key_click(&mut self, ctx: &mut EventCtx, key: usize);
|
||||
|
@ -4,7 +4,7 @@ use crate::ui::{
|
||||
geometry::{Grid, Rect},
|
||||
model_tt::component::{
|
||||
button::{Button, ButtonContent, ButtonMsg::Clicked},
|
||||
keyboard::common::{array_map_enumerate, MultiTapKeyboard, TextBox},
|
||||
keyboard::common::{MultiTapKeyboard, TextBox},
|
||||
swipe::{Swipe, SwipeDirection},
|
||||
theme,
|
||||
},
|
||||
@ -38,50 +38,30 @@ const KEYBOARD: [[&str; KEY_COUNT]; PAGE_COUNT] = [
|
||||
const MAX_LENGTH: usize = 50;
|
||||
|
||||
impl PassphraseKeyboard {
|
||||
pub fn new(area: Rect) -> Self {
|
||||
let input_area = Grid::new(area, 5, 1).row_col(0, 0);
|
||||
let confirm_btn_area = Grid::new(area, 5, 3).cell(14);
|
||||
let back_btn_area = Grid::new(area, 5, 3).cell(12);
|
||||
let key_grid = Grid::new(area, 5, 3);
|
||||
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
page_swipe: Swipe::horizontal(area),
|
||||
input: Input::new(input_area).into_child(),
|
||||
confirm: Button::with_text(confirm_btn_area, "Confirm")
|
||||
page_swipe: Swipe::horizontal(),
|
||||
input: Input::new().into_child(),
|
||||
confirm: Button::with_text("Confirm")
|
||||
.styled(theme::button_confirm())
|
||||
.into_child(),
|
||||
back: Button::with_text(back_btn_area, "Back")
|
||||
back: Button::with_text("Back")
|
||||
.styled(theme::button_clear())
|
||||
.into_child(),
|
||||
keys: Self::generate_keyboard(&key_grid),
|
||||
keys: KEYBOARD.map(|page| {
|
||||
page.map(|text| {
|
||||
if text == " " {
|
||||
let icon = theme::ICON_SPACE;
|
||||
Child::new(Button::with_icon(icon))
|
||||
} else {
|
||||
Child::new(Button::with_text(text))
|
||||
}
|
||||
})
|
||||
}),
|
||||
key_page: STARTING_PAGE,
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_keyboard(grid: &Grid) -> [[Child<Button<&'static str>>; KEY_COUNT]; PAGE_COUNT] {
|
||||
array_map_enumerate(KEYBOARD, |_, page| {
|
||||
array_map_enumerate(page, |key, text| Self::generate_key(grid, key, text))
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_key(grid: &Grid, key: usize, text: &'static str) -> Child<Button<&'static str>> {
|
||||
// Assign the keys in each page to buttons on a 5x3 grid, starting from the
|
||||
// second row.
|
||||
let area = grid.cell(if key < 9 {
|
||||
// The grid has 3 columns, and we skip the first row.
|
||||
key + 3
|
||||
} else {
|
||||
// For the last key (the "0" position) we skip one cell.
|
||||
key + 1 + 3
|
||||
});
|
||||
if text == " " {
|
||||
let icon = theme::ICON_SPACE;
|
||||
Child::new(Button::with_icon(area, icon))
|
||||
} else {
|
||||
Child::new(Button::with_text(area, text))
|
||||
}
|
||||
}
|
||||
|
||||
fn key_text(content: &ButtonContent<&'static str>) -> &'static str {
|
||||
match content {
|
||||
ButtonContent::Text(text) => text,
|
||||
@ -118,6 +98,31 @@ impl PassphraseKeyboard {
|
||||
impl Component for PassphraseKeyboard {
|
||||
type Msg = PassphraseKeyboardMsg;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
let input_area = Grid::new(bounds, 5, 1).row_col(0, 0);
|
||||
let confirm_btn_area = Grid::new(bounds, 5, 3).cell(14);
|
||||
let back_btn_area = Grid::new(bounds, 5, 3).cell(12);
|
||||
let key_grid = Grid::new(bounds, 5, 3);
|
||||
|
||||
self.page_swipe.place(bounds);
|
||||
self.input.place(input_area);
|
||||
self.confirm.place(confirm_btn_area);
|
||||
self.back.place(back_btn_area);
|
||||
for (key, btn) in self.keys[self.key_page].iter_mut().enumerate() {
|
||||
// Assign the keys in each page to buttons on a 5x3 grid, starting from the
|
||||
// second row.
|
||||
let area = key_grid.cell(if key < 9 {
|
||||
// The grid has 3 columns, and we skip the first row.
|
||||
key + 3
|
||||
} else {
|
||||
// For the last key (the "0" position) we skip one cell.
|
||||
key + 1 + 3
|
||||
});
|
||||
btn.place(area);
|
||||
}
|
||||
bounds
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
if self.input.inner().multi_tap.is_timeout_event(event) {
|
||||
self.input
|
||||
@ -180,9 +185,9 @@ struct Input {
|
||||
}
|
||||
|
||||
impl Input {
|
||||
fn new(area: Rect) -> Self {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
area,
|
||||
area: Rect::zero(),
|
||||
textbox: TextBox::empty(),
|
||||
multi_tap: MultiTapKeyboard::new(),
|
||||
}
|
||||
@ -192,6 +197,11 @@ impl Input {
|
||||
impl Component for Input {
|
||||
type Msg = Never;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
self.area = bounds;
|
||||
self.area
|
||||
}
|
||||
|
||||
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
|
||||
None
|
||||
}
|
||||
|
@ -37,99 +37,45 @@ pub struct PinKeyboard {
|
||||
}
|
||||
|
||||
impl PinKeyboard {
|
||||
pub fn new(area: Rect, major_prompt: &'static [u8], minor_prompt: &'static [u8]) -> Self {
|
||||
pub fn new(major_prompt: &'static [u8], minor_prompt: &'static [u8]) -> Self {
|
||||
let digits = Vec::new();
|
||||
|
||||
// Prompts and PIN dots display.
|
||||
let grid = if minor_prompt.is_empty() {
|
||||
// Make the major prompt bigger if the minor one is empty.
|
||||
Grid::new(area, 5, 1)
|
||||
} else {
|
||||
Grid::new(area, 6, 1)
|
||||
};
|
||||
let major_prompt = Label::centered(
|
||||
grid.row_col(0, 0).center(),
|
||||
major_prompt,
|
||||
theme::label_default(),
|
||||
);
|
||||
let minor_prompt = Label::centered(
|
||||
grid.row_col(0, 1).center(),
|
||||
minor_prompt,
|
||||
theme::label_default(),
|
||||
);
|
||||
let dots =
|
||||
PinDots::new(grid.row_col(0, 0), digits.len(), theme::label_default()).into_child();
|
||||
|
||||
// Control buttons.
|
||||
let grid = Grid::new(area, 5, 3);
|
||||
let reset_btn = Button::with_text(grid.row_col(4, 0), "Reset")
|
||||
.styled(theme::button_clear())
|
||||
.into_child();
|
||||
let cancel_btn = Button::with_icon(grid.row_col(4, 0), theme::ICON_CANCEL)
|
||||
.styled(theme::button_cancel())
|
||||
.into_child();
|
||||
let confirm_btn = Button::with_icon(grid.row_col(4, 2), theme::ICON_CONFIRM)
|
||||
.styled(theme::button_clear())
|
||||
.into_child();
|
||||
|
||||
// PIN digit buttons.
|
||||
let digit_btns = Self::generate_digit_buttons(&grid);
|
||||
|
||||
Self {
|
||||
major_prompt: Label::centered(major_prompt, theme::label_default()),
|
||||
minor_prompt: Label::centered(minor_prompt, theme::label_default()),
|
||||
dots: PinDots::new(digits.len(), theme::label_default()).into_child(),
|
||||
reset_btn: Button::with_text("Reset")
|
||||
.styled(theme::button_clear())
|
||||
.into_child(),
|
||||
cancel_btn: Button::with_icon(theme::ICON_CANCEL)
|
||||
.styled(theme::button_cancel())
|
||||
.into_child(),
|
||||
confirm_btn: Button::with_icon(theme::ICON_CONFIRM)
|
||||
.styled(theme::button_clear())
|
||||
.into_child(),
|
||||
digit_btns: Self::generate_digit_buttons(),
|
||||
digits,
|
||||
major_prompt,
|
||||
minor_prompt,
|
||||
dots,
|
||||
reset_btn,
|
||||
cancel_btn,
|
||||
confirm_btn,
|
||||
digit_btns,
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_digit_buttons(grid: &Grid) -> [Child<Button<&'static str>>; DIGIT_COUNT] {
|
||||
fn generate_digit_buttons() -> [Child<Button<&'static str>>; DIGIT_COUNT] {
|
||||
// Generate a random sequence of digits from 0 to 9.
|
||||
let mut digits = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"];
|
||||
random::shuffle(&mut digits);
|
||||
|
||||
// Assign the digits to buttons on a 5x3 grid, starting from the second row.
|
||||
let btn = |i| {
|
||||
let area = grid.cell(if i < 9 {
|
||||
// The grid has 3 columns, and we skip the first row.
|
||||
i + 3
|
||||
} else {
|
||||
// For the last key (the "0" position) we skip one cell.
|
||||
i + 1 + 3
|
||||
});
|
||||
let text = digits[i];
|
||||
Child::new(Button::with_text(area, text))
|
||||
};
|
||||
[
|
||||
btn(0),
|
||||
btn(1),
|
||||
btn(2),
|
||||
btn(3),
|
||||
btn(4),
|
||||
btn(5),
|
||||
btn(6),
|
||||
btn(7),
|
||||
btn(8),
|
||||
btn(9),
|
||||
]
|
||||
digits.map(Button::with_text).map(Child::new)
|
||||
}
|
||||
|
||||
fn pin_modified(&mut self, ctx: &mut EventCtx) {
|
||||
let is_full = self.digits.is_full();
|
||||
for btn in &mut self.digit_btns {
|
||||
btn.mutate(ctx, |ctx, btn| btn.enabled(ctx, !is_full));
|
||||
btn.mutate(ctx, |ctx, btn| btn.enable_if(ctx, !is_full));
|
||||
}
|
||||
let is_empty = self.digits.is_empty();
|
||||
self.reset_btn
|
||||
.mutate(ctx, |ctx, btn| btn.enabled(ctx, !is_empty));
|
||||
.mutate(ctx, |ctx, btn| btn.enable_if(ctx, !is_empty));
|
||||
self.cancel_btn
|
||||
.mutate(ctx, |ctx, btn| btn.enabled(ctx, is_empty));
|
||||
.mutate(ctx, |ctx, btn| btn.enable_if(ctx, is_empty));
|
||||
self.confirm_btn
|
||||
.mutate(ctx, |ctx, btn| btn.enabled(ctx, !is_empty));
|
||||
.mutate(ctx, |ctx, btn| btn.enable_if(ctx, !is_empty));
|
||||
let digit_count = self.digits.len();
|
||||
self.dots
|
||||
.mutate(ctx, |ctx, dots| dots.update(ctx, digit_count));
|
||||
@ -143,6 +89,40 @@ impl PinKeyboard {
|
||||
impl Component for PinKeyboard {
|
||||
type Msg = PinKeyboardMsg;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
// Prompts and PIN dots display.
|
||||
let grid = if self.minor_prompt.text().is_empty() {
|
||||
// Make the major prompt bigger if the minor one is empty.
|
||||
Grid::new(bounds, 5, 1)
|
||||
} else {
|
||||
Grid::new(bounds, 6, 1)
|
||||
};
|
||||
self.major_prompt.place(grid.row_col(0, 0));
|
||||
self.minor_prompt.place(grid.row_col(0, 1));
|
||||
self.dots.place(grid.row_col(0, 0));
|
||||
|
||||
// Control buttons.
|
||||
let grid = Grid::new(bounds, 5, 3);
|
||||
self.reset_btn.place(grid.row_col(4, 0));
|
||||
self.cancel_btn.place(grid.row_col(4, 0));
|
||||
self.confirm_btn.place(grid.row_col(4, 2));
|
||||
|
||||
// Digit buttons.
|
||||
for (i, btn) in self.digit_btns.iter_mut().enumerate() {
|
||||
// Assign the digits to buttons on a 5x3 grid, starting from the second row.
|
||||
let area = grid.cell(if i < 9 {
|
||||
// The grid has 3 columns, and we skip the first row.
|
||||
i + 3
|
||||
} else {
|
||||
// For the last key (the "0" position) we skip one cell.
|
||||
i + 1 + 3
|
||||
});
|
||||
btn.place(area);
|
||||
}
|
||||
|
||||
bounds
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
if let Some(Clicked) = self.confirm_btn.event(ctx, event) {
|
||||
return Some(PinKeyboardMsg::Confirmed);
|
||||
@ -196,16 +176,16 @@ impl PinDots {
|
||||
const DOT: i32 = 10;
|
||||
const PADDING: i32 = 4;
|
||||
|
||||
fn new(area: Rect, digit_count: usize, style: LabelStyle) -> Self {
|
||||
fn new(digit_count: usize, style: LabelStyle) -> Self {
|
||||
Self {
|
||||
area,
|
||||
style,
|
||||
digit_count,
|
||||
area: Rect::zero(),
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, ctx: &mut EventCtx, digit_count: usize) {
|
||||
if digit_count != self.digit_count {
|
||||
if self.digit_count != digit_count {
|
||||
self.digit_count = digit_count;
|
||||
ctx.request_paint();
|
||||
}
|
||||
@ -215,6 +195,11 @@ impl PinDots {
|
||||
impl Component for PinDots {
|
||||
type Msg = Never;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
self.area = bounds;
|
||||
self.area
|
||||
}
|
||||
|
||||
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
|
||||
None
|
||||
}
|
||||
|
@ -33,16 +33,6 @@ pub struct Slip39Input {
|
||||
}
|
||||
|
||||
impl MnemonicInput for Slip39Input {
|
||||
fn new(area: Rect) -> Self {
|
||||
Self {
|
||||
button: Button::empty(area),
|
||||
textbox: TextBox::empty(),
|
||||
multi_tap: MultiTapKeyboard::new(),
|
||||
final_word: None,
|
||||
input_mask: Slip39Mask::full(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the key set. Keys are further specified as indices into this
|
||||
/// array.
|
||||
fn keys() -> [&'static str; MNEMONIC_KEY_COUNT] {
|
||||
@ -94,6 +84,10 @@ impl MnemonicInput for Slip39Input {
|
||||
impl Component for Slip39Input {
|
||||
type Msg = MnemonicInputMsg;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
self.button.place(bounds)
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
if self.multi_tap.is_timeout_event(event) {
|
||||
// Timeout occurred. Reset the pending key.
|
||||
@ -180,6 +174,16 @@ impl Component for Slip39Input {
|
||||
}
|
||||
|
||||
impl Slip39Input {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
button: Button::empty(),
|
||||
textbox: TextBox::empty(),
|
||||
multi_tap: MultiTapKeyboard::new(),
|
||||
final_word: None,
|
||||
input_mask: Slip39Mask::full(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a key index into the key digit. This is what we push into the
|
||||
/// input buffer.
|
||||
///
|
||||
|
@ -4,7 +4,7 @@ use crate::{
|
||||
animation::Animation,
|
||||
component::{Component, Event, EventCtx},
|
||||
display::{self, Color},
|
||||
geometry::Offset,
|
||||
geometry::{Offset, Rect},
|
||||
},
|
||||
};
|
||||
|
||||
@ -111,6 +111,11 @@ impl Loader {
|
||||
impl Component for Loader {
|
||||
type Msg = LoaderMsg;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
// TODO: Return the correct size.
|
||||
bounds
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
let now = Instant::now();
|
||||
|
||||
|
@ -3,7 +3,7 @@ use crate::ui::{
|
||||
base::ComponentExt, paginated::PageMsg, Component, Event, EventCtx, Never, Pad, Paginate,
|
||||
},
|
||||
display::{self, Color},
|
||||
geometry::{Dimensions, LinearLayout, Offset, Rect},
|
||||
geometry::{LinearPlacement, Offset, Rect},
|
||||
};
|
||||
|
||||
use super::{theme, Button, Swipe, SwipeDirection};
|
||||
@ -21,58 +21,31 @@ impl<T, U> SwipePage<T, U>
|
||||
where
|
||||
T: Paginate,
|
||||
T: Component,
|
||||
T: Dimensions,
|
||||
U: Component,
|
||||
{
|
||||
pub fn new(
|
||||
area: Rect,
|
||||
background: Color,
|
||||
content: impl FnOnce(Rect) -> T,
|
||||
controls: impl FnOnce(Rect) -> U,
|
||||
) -> Self {
|
||||
let layout = PageLayout::new(area);
|
||||
let mut content = Self::make_content(&layout, content);
|
||||
|
||||
// Always start at the first page.
|
||||
let scrollbar = ScrollBar::vertical_right(layout.scrollbar, content.page_count(), 0);
|
||||
|
||||
let swipe = Self::make_swipe(area, &scrollbar);
|
||||
let pad = Pad::with_background(area, background);
|
||||
pub fn new(content: T, buttons: U, background: Color) -> Self {
|
||||
Self {
|
||||
content,
|
||||
buttons: controls(layout.buttons),
|
||||
scrollbar,
|
||||
swipe,
|
||||
pad,
|
||||
buttons,
|
||||
scrollbar: ScrollBar::vertical(),
|
||||
swipe: Swipe::new(),
|
||||
pad: Pad::with_background(background),
|
||||
fade: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn make_swipe(area: Rect, scrollbar: &ScrollBar) -> Swipe {
|
||||
let mut swipe = Swipe::new(area);
|
||||
swipe.allow_up = scrollbar.has_next_page();
|
||||
swipe.allow_down = scrollbar.has_previous_page();
|
||||
swipe
|
||||
fn setup_swipe(&mut self) {
|
||||
self.swipe.allow_up = self.scrollbar.has_next_page();
|
||||
self.swipe.allow_down = self.scrollbar.has_previous_page();
|
||||
}
|
||||
|
||||
fn make_content(layout: &PageLayout, content: impl FnOnce(Rect) -> T) -> T {
|
||||
// Check if content fits on single page.
|
||||
let mut content = content(layout.content_single_page);
|
||||
if content.page_count() > 1 {
|
||||
// Reduce area to make space for scrollbar if it doesn't fit.
|
||||
content.set_area(layout.content);
|
||||
}
|
||||
content.change_page(0);
|
||||
content
|
||||
}
|
||||
|
||||
fn change_page(&mut self, ctx: &mut EventCtx, page: usize) {
|
||||
// Adjust the swipe parameters.
|
||||
self.swipe = Self::make_swipe(self.swipe.area, &self.scrollbar);
|
||||
fn on_page_change(&mut self, ctx: &mut EventCtx) {
|
||||
// Adjust the swipe parameters according to the scrollbar.
|
||||
self.setup_swipe();
|
||||
|
||||
// Change the page in the content, make sure it gets completely repainted and
|
||||
// clear the background under it.
|
||||
self.content.change_page(page);
|
||||
self.content.change_page(self.scrollbar.active_page);
|
||||
self.content.request_complete_repaint(ctx);
|
||||
self.pad.clear();
|
||||
|
||||
@ -96,24 +69,52 @@ impl<T, U> Component for SwipePage<T, U>
|
||||
where
|
||||
T: Paginate,
|
||||
T: Component,
|
||||
T: Dimensions,
|
||||
U: Component,
|
||||
{
|
||||
type Msg = PageMsg<T::Msg, U::Msg>;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
let layout = PageLayout::new(bounds);
|
||||
self.pad.place(bounds);
|
||||
self.swipe.place(bounds);
|
||||
self.buttons.place(layout.buttons);
|
||||
self.scrollbar.place(layout.scrollbar);
|
||||
|
||||
// Layout the content. Try to fit it on a single page first, and reduce the area
|
||||
// to make space for a scrollbar if it doesn't fit.
|
||||
self.content.place(layout.content_single_page);
|
||||
let page_count = {
|
||||
let count = self.content.page_count();
|
||||
if count > 1 {
|
||||
self.content.place(layout.content);
|
||||
self.content.page_count() // Make sure to re-count it with the
|
||||
// new size.
|
||||
} else {
|
||||
count // Content fits on a single page.
|
||||
}
|
||||
};
|
||||
|
||||
// Now that we finally have the page count, we can setup the scrollbar and the
|
||||
// swiper.
|
||||
self.scrollbar.set_count_and_active_page(page_count, 0);
|
||||
self.setup_swipe();
|
||||
|
||||
bounds
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
if let Some(swipe) = self.swipe.event(ctx, event) {
|
||||
match swipe {
|
||||
SwipeDirection::Up => {
|
||||
// Scroll down, if possible.
|
||||
self.scrollbar.go_to_next_page();
|
||||
self.change_page(ctx, self.scrollbar.active_page);
|
||||
self.on_page_change(ctx);
|
||||
return None;
|
||||
}
|
||||
SwipeDirection::Down => {
|
||||
// Scroll up, if possible.
|
||||
self.scrollbar.go_to_previous_page();
|
||||
self.change_page(ctx, self.scrollbar.active_page);
|
||||
self.on_page_change(ctx);
|
||||
return None;
|
||||
}
|
||||
_ => {
|
||||
@ -193,14 +194,19 @@ impl ScrollBar {
|
||||
const ICON_UP: &'static [u8] = include_res!("model_tt/res/scroll-up.toif");
|
||||
const ICON_DOWN: &'static [u8] = include_res!("model_tt/res/scroll-down.toif");
|
||||
|
||||
pub fn vertical_right(area: Rect, page_count: usize, active_page: usize) -> Self {
|
||||
pub fn vertical() -> Self {
|
||||
Self {
|
||||
area,
|
||||
page_count,
|
||||
active_page,
|
||||
area: Rect::zero(),
|
||||
page_count: 0,
|
||||
active_page: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_count_and_active_page(&mut self, page_count: usize, active_page: usize) {
|
||||
self.page_count = page_count;
|
||||
self.active_page = active_page;
|
||||
}
|
||||
|
||||
pub fn has_pages(&self) -> bool {
|
||||
self.page_count > 1
|
||||
}
|
||||
@ -229,12 +235,17 @@ impl ScrollBar {
|
||||
impl Component for ScrollBar {
|
||||
type Msg = Never;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
self.area = bounds;
|
||||
self.area
|
||||
}
|
||||
|
||||
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
|
||||
None
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
let layout = LinearLayout::vertical()
|
||||
let layout = LinearPlacement::vertical()
|
||||
.align_at_center()
|
||||
.with_spacing(Self::DOT_INTERVAL);
|
||||
|
||||
@ -315,7 +326,6 @@ mod tests {
|
||||
trace::Trace,
|
||||
ui::{
|
||||
component::{text::paragraphs::Paragraphs, Empty},
|
||||
display,
|
||||
geometry::Point,
|
||||
model_tt::{event::TouchEvent, theme},
|
||||
},
|
||||
@ -357,12 +367,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn paragraphs_empty() {
|
||||
let mut page = SwipePage::new(
|
||||
display::screen(),
|
||||
theme::BG,
|
||||
|area| Paragraphs::<&[u8]>::new(area),
|
||||
|_| Empty,
|
||||
);
|
||||
let mut page = SwipePage::new(Paragraphs::<&str>::new(), Empty, theme::BG);
|
||||
page.place(display::screen());
|
||||
|
||||
let expected =
|
||||
"<SwipePage active_page:0 page_count:1 content:<Paragraphs > buttons:<Empty > >";
|
||||
@ -377,21 +383,19 @@ mod tests {
|
||||
#[test]
|
||||
fn paragraphs_single() {
|
||||
let mut page = SwipePage::new(
|
||||
display::screen(),
|
||||
Paragraphs::new()
|
||||
.add::<theme::TTDefaultText>(
|
||||
theme::FONT_NORMAL,
|
||||
"This is the first paragraph and it should fit on the screen entirely.",
|
||||
)
|
||||
.add::<theme::TTDefaultText>(
|
||||
theme::FONT_BOLD,
|
||||
"Second, bold, paragraph should also fit on the screen whole I think.",
|
||||
),
|
||||
Empty,
|
||||
theme::BG,
|
||||
|area| {
|
||||
Paragraphs::new(area)
|
||||
.add::<theme::TTDefaultText>(
|
||||
theme::FONT_NORMAL,
|
||||
"This is the first paragraph and it should fit on the screen entirely.",
|
||||
)
|
||||
.add::<theme::TTDefaultText>(
|
||||
theme::FONT_BOLD,
|
||||
"Second, bold, paragraph should also fit on the screen whole I think.",
|
||||
)
|
||||
},
|
||||
|_| Empty,
|
||||
);
|
||||
page.place(display::screen());
|
||||
|
||||
let expected = "<SwipePage active_page:0 page_count:1 content:<Paragraphs This is the first paragraph\nand it should fit on the\nscreen entirely.\nSecond, bold, paragraph\nshould also fit on the\nscreen whole I think.\n> buttons:<Empty > >";
|
||||
|
||||
@ -405,17 +409,15 @@ mod tests {
|
||||
#[test]
|
||||
fn paragraphs_one_long() {
|
||||
let mut page = SwipePage::new(
|
||||
display::screen(),
|
||||
Paragraphs::new()
|
||||
.add::<theme::TTDefaultText>(
|
||||
theme::FONT_BOLD,
|
||||
"This is somewhat long paragraph that goes on and on and on and on and on and will definitely not fit on just a single screen. You have to swipe a bit to see all the text it contains I guess. There's just so much letters in it.",
|
||||
),
|
||||
Empty,
|
||||
theme::BG,
|
||||
|area| {
|
||||
Paragraphs::new(area)
|
||||
.add::<theme::TTDefaultText>(
|
||||
theme::FONT_BOLD,
|
||||
"This is somewhat long paragraph that goes on and on and on and on and on and will definitely not fit on just a single screen. You have to swipe a bit to see all the text it contains I guess. There's just so much letters in it.",
|
||||
)
|
||||
},
|
||||
|_| Empty,
|
||||
);
|
||||
page.place(display::screen());
|
||||
|
||||
let expected1 = "<SwipePage active_page:0 page_count:2 content:<Paragraphs This is somewhat long\nparagraph that goes\non and on and on and\non and on and will\ndefinitely not fit on\njust a single screen.\nYou have to swipe a bit\nto see all the text it...\n> buttons:<Empty > >";
|
||||
let expected2 = "<SwipePage active_page:1 page_count:2 content:<Paragraphs contains I guess.\nThere's just so much\nletters in it.\n> buttons:<Empty > >";
|
||||
@ -434,25 +436,23 @@ mod tests {
|
||||
#[test]
|
||||
fn paragraphs_three_long() {
|
||||
let mut page = SwipePage::new(
|
||||
display::screen(),
|
||||
Paragraphs::new()
|
||||
.add::<theme::TTDefaultText>(
|
||||
theme::FONT_BOLD,
|
||||
"This paragraph is using a bold font. It doesn't need to be all that long.",
|
||||
)
|
||||
.add::<theme::TTDefaultText>(
|
||||
theme::FONT_MONO,
|
||||
"And this one is using MONO. Monospace is nice for numbers, they have the same width and can be scanned quickly. Even if they span several pages or something.",
|
||||
)
|
||||
.add::<theme::TTDefaultText>(
|
||||
theme::FONT_BOLD,
|
||||
"Let's add another one for a good measure. This one should overflow all the way to the third page with a bit of luck.",
|
||||
),
|
||||
Empty,
|
||||
theme::BG,
|
||||
|area| {
|
||||
Paragraphs::new(area)
|
||||
.add::<theme::TTDefaultText>(
|
||||
theme::FONT_BOLD,
|
||||
"This paragraph is using a bold font. It doesn't need to be all that long.",
|
||||
)
|
||||
.add::<theme::TTDefaultText>(
|
||||
theme::FONT_MONO,
|
||||
"And this one is using MONO. Monospace is nice for numbers, they have the same width and can be scanned quickly. Even if they span several pages or something.",
|
||||
)
|
||||
.add::<theme::TTDefaultText>(
|
||||
theme::FONT_BOLD,
|
||||
"Let's add another one for a good measure. This one should overflow all the way to the third page with a bit of luck.",
|
||||
)
|
||||
},
|
||||
|_| Empty,
|
||||
);
|
||||
page.place(display::screen());
|
||||
|
||||
let expected1 = "<SwipePage active_page:0 page_count:3 content:<Paragraphs This paragraph is\nusing a bold font. It\ndoesn't need to be all\nthat long.\nAnd this one is\nusing MONO.\nMonospace is nice\nfor numbers, they...\n> buttons:<Empty > >";
|
||||
let expected2 = "<SwipePage active_page:1 page_count:3 content:<Paragraphs have the same\nwidth and can be\nscanned quickly.\nEven if they span\nseveral pages or\nsomething.\nLet's add another one\nfor a good measure....\n> buttons:<Empty > >";
|
||||
|
@ -28,9 +28,9 @@ impl Swipe {
|
||||
const DISTANCE: i32 = 120;
|
||||
const THRESHOLD: f32 = 0.3;
|
||||
|
||||
pub fn new(area: Rect) -> Self {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
area,
|
||||
area: Rect::zero(),
|
||||
allow_up: false,
|
||||
allow_down: false,
|
||||
allow_left: false,
|
||||
@ -41,12 +41,12 @@ impl Swipe {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn vertical(area: Rect) -> Self {
|
||||
Self::new(area).up().down()
|
||||
pub fn vertical() -> Self {
|
||||
Self::new().up().down()
|
||||
}
|
||||
|
||||
pub fn horizontal(area: Rect) -> Self {
|
||||
Self::new(area).left().right()
|
||||
pub fn horizontal() -> Self {
|
||||
Self::new().left().right()
|
||||
}
|
||||
|
||||
pub fn up(mut self) -> Self {
|
||||
@ -88,6 +88,11 @@ impl Swipe {
|
||||
impl Component for Swipe {
|
||||
type Msg = SwipeDirection;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
self.area = bounds;
|
||||
self.area
|
||||
}
|
||||
|
||||
fn event(&mut self, _ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
if !self.is_active() {
|
||||
return None;
|
||||
|
@ -4,8 +4,7 @@ use crate::{
|
||||
error::Error,
|
||||
micropython::{buffer::Buffer, map::Map, obj::Obj, qstr::Qstr},
|
||||
ui::{
|
||||
component::{base::ComponentExt, text::paragraphs::Paragraphs, Child, FormattedText},
|
||||
display,
|
||||
component::{base::ComponentExt, text::paragraphs::Paragraphs, FormattedText},
|
||||
layout::obj::LayoutObj,
|
||||
},
|
||||
util,
|
||||
@ -52,13 +51,12 @@ where
|
||||
#[no_mangle]
|
||||
extern "C" fn ui_layout_new_example(_param: Obj) -> Obj {
|
||||
let block = move || {
|
||||
let layout = LayoutObj::new(HoldToConfirm::new(display::screen(), |area| {
|
||||
let layout = LayoutObj::new(HoldToConfirm::new(
|
||||
FormattedText::new::<theme::TTDefaultText>(
|
||||
area,
|
||||
"Testing text layout, with some text, and some more text. And {param}",
|
||||
)
|
||||
.with(b"param", b"parameters!")
|
||||
}))?;
|
||||
.with(b"param", b"parameters!"),
|
||||
))?;
|
||||
Ok(layout.into())
|
||||
};
|
||||
unsafe { util::try_or_raise(block) }
|
||||
@ -78,41 +76,32 @@ extern "C" fn ui_layout_new_confirm_action(
|
||||
let verb: Option<Buffer> = kwargs.get(Qstr::MP_QSTR_verb)?.try_into_option()?;
|
||||
let reverse: bool = kwargs.get(Qstr::MP_QSTR_reverse)?.try_into()?;
|
||||
|
||||
let paragraphs = {
|
||||
let action = action.unwrap_or_default();
|
||||
let description = description.unwrap_or_default();
|
||||
let mut paragraphs = Paragraphs::new();
|
||||
if !reverse {
|
||||
paragraphs = paragraphs
|
||||
.add::<theme::TTDefaultText>(theme::FONT_BOLD, action)
|
||||
.add::<theme::TTDefaultText>(theme::FONT_NORMAL, description);
|
||||
} else {
|
||||
paragraphs = paragraphs
|
||||
.add::<theme::TTDefaultText>(theme::FONT_NORMAL, description)
|
||||
.add::<theme::TTDefaultText>(theme::FONT_BOLD, action);
|
||||
}
|
||||
paragraphs
|
||||
};
|
||||
|
||||
let buttons = Button::left_right(
|
||||
Button::with_icon(theme::ICON_CANCEL),
|
||||
|msg| (matches!(msg, ButtonMsg::Clicked)).then(|| false),
|
||||
Button::with_text(verb.unwrap_or_else(|| "CONFIRM".into()))
|
||||
.styled(theme::button_confirm()),
|
||||
|msg| (matches!(msg, ButtonMsg::Clicked)).then(|| true),
|
||||
);
|
||||
|
||||
let obj = LayoutObj::new(
|
||||
Frame::new(theme::borders(), title, |area| {
|
||||
SwipePage::new(
|
||||
area,
|
||||
theme::BG,
|
||||
|area| {
|
||||
let action = action.unwrap_or("".into());
|
||||
let description = description.unwrap_or("".into());
|
||||
let mut para = Paragraphs::new(area);
|
||||
if !reverse {
|
||||
para = para
|
||||
.add::<theme::TTDefaultText>(theme::FONT_BOLD, action)
|
||||
.add::<theme::TTDefaultText>(theme::FONT_NORMAL, description);
|
||||
} else {
|
||||
para = para
|
||||
.add::<theme::TTDefaultText>(theme::FONT_NORMAL, description)
|
||||
.add::<theme::TTDefaultText>(theme::FONT_BOLD, action);
|
||||
}
|
||||
para
|
||||
},
|
||||
|area| {
|
||||
Button::array2(
|
||||
area,
|
||||
|area| Button::with_icon(area, theme::ICON_CANCEL),
|
||||
|msg| (matches!(msg, ButtonMsg::Clicked)).then(|| false),
|
||||
|area| {
|
||||
Button::with_text(area, verb.unwrap_or("CONFIRM".into()))
|
||||
.styled(theme::button_confirm())
|
||||
},
|
||||
|msg| (matches!(msg, ButtonMsg::Clicked)).then(|| true),
|
||||
)
|
||||
},
|
||||
)
|
||||
})
|
||||
.into_child(),
|
||||
Frame::new(title, SwipePage::new(paragraphs, buttons, theme::BG)).into_child(),
|
||||
)?;
|
||||
Ok(obj.into())
|
||||
};
|
||||
@ -123,7 +112,11 @@ extern "C" fn ui_layout_new_confirm_action(
|
||||
mod tests {
|
||||
use crate::{
|
||||
trace::Trace,
|
||||
ui::model_tt::component::{Button, Dialog},
|
||||
ui::{
|
||||
component::Component,
|
||||
display,
|
||||
model_tt::component::{Button, Dialog},
|
||||
},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
@ -136,18 +129,15 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn trace_example_layout() {
|
||||
let layout = Child::new(Dialog::new(
|
||||
display::screen(),
|
||||
|area| {
|
||||
FormattedText::new::<theme::TTDefaultText>(
|
||||
area,
|
||||
"Testing text layout, with some text, and some more text. And {param}",
|
||||
)
|
||||
.with(b"param", b"parameters!")
|
||||
},
|
||||
|area| Button::with_text(area, b"Left"),
|
||||
|area| Button::with_text(area, b"Right"),
|
||||
));
|
||||
let mut layout = Dialog::new(
|
||||
FormattedText::new::<theme::TTDefaultText>(
|
||||
"Testing text layout, with some text, and some more text. And {param}",
|
||||
)
|
||||
.with(b"param", b"parameters!"),
|
||||
Button::with_text(b"Left"),
|
||||
Button::with_text(b"Right"),
|
||||
);
|
||||
layout.place(display::screen());
|
||||
assert_eq!(
|
||||
trace(&layout),
|
||||
"<Dialog content:<Text content:Testing text layout, with\nsome text, and some more\ntext. And parameters! > left:<Button text:Left > right:<Button text:Right > >",
|
||||
|
@ -1,7 +1,7 @@
|
||||
use crate::ui::{
|
||||
component::{label::LabelStyle, text::layout::DefaultTextTheme},
|
||||
display::{self, Color, Font},
|
||||
geometry::{Insets, Rect},
|
||||
display::{Color, Font},
|
||||
geometry::Insets,
|
||||
};
|
||||
|
||||
use super::component::{ButtonStyle, ButtonStyleSheet, LoaderStyle, LoaderStyleSheet};
|
||||
@ -165,6 +165,6 @@ pub const CONTENT_BORDER: i32 = 5;
|
||||
/// | +----+ |
|
||||
/// | 14 |
|
||||
/// +----------+
|
||||
pub fn borders() -> Rect {
|
||||
display::screen().inset(Insets::new(13, 5, 14, 10))
|
||||
pub fn borders_scroll() -> Insets {
|
||||
Insets::new(13, 5, 14, 10)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user