1
0
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:
Jan Pochyla 2022-02-11 11:01:29 -03:00 committed by matejcik
parent 7b41946789
commit 801679bccf
39 changed files with 1365 additions and 843 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

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

View File

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

View File

@ -1,3 +1,4 @@
pub mod formatted;
mod iter;
pub mod layout;
pub mod paragraphs;

View File

@ -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(&paragraph.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(
&paragraph.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(
&paragraph.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(
&paragraph.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;
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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