refactor(core/rust/ui): simplify button height computation

[no changelog]
pull/2496/head
Martin Milata 2 years ago
parent 885ae2a943
commit 5052594789

@ -23,7 +23,7 @@ pub use maybe::Maybe;
pub use pad::Pad;
pub use paginated::{PageMsg, Paginate};
pub use painter::{qrcode_painter, Painter};
pub use placed::GridPlaced;
pub use placed::{FixedHeightBar, GridPlaced};
pub use text::{
formatted::FormattedText,
layout::{LineBreaking, PageBreaking, TextLayout},

@ -77,3 +77,47 @@ where
d.close();
}
}
pub struct FixedHeightBar<T> {
inner: T,
height: i32,
}
impl<T> FixedHeightBar<T> {
pub const fn bottom(inner: T, height: i32) -> Self {
Self { inner, height }
}
}
impl<T> Component for FixedHeightBar<T>
where
T: Component,
{
type Msg = T::Msg;
fn place(&mut self, bounds: Rect) -> Rect {
let (_, bar) = bounds.split_bottom(self.height);
self.inner.place(bar)
}
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 FixedHeightBar<T>
where
T: Component,
T: crate::trace::Trace,
{
fn trace(&self, d: &mut dyn crate::trace::Tracer) {
d.open("FixedHeightBar");
d.field("inner", &self.inner);
d.close();
}
}

@ -1,7 +1,9 @@
use crate::{
time::Duration,
ui::{
component::{Component, ComponentExt, Event, EventCtx, GridPlaced, Map, TimerToken},
component::{
Component, ComponentExt, Event, EventCtx, FixedHeightBar, GridPlaced, Map, TimerToken,
},
display::{self, Color, Font},
event::TouchEvent,
geometry::{Insets, Offset, Rect},
@ -27,9 +29,6 @@ pub struct Button<T> {
}
impl<T> Button<T> {
/// Standard height in pixels.
pub const HEIGHT: i32 = 38;
/// Offsets the baseline of the button text either up (negative) or down
/// (positive).
pub const BASELINE_OFFSET: i32 = -3;
@ -353,7 +352,7 @@ impl<T> Button<T> {
T: AsRef<str>,
{
let columns = 1 + right_size_factor;
(
theme::button_bar((
GridPlaced::new(left)
.with_grid(1, columns)
.with_spacing(theme::BUTTON_SPACING)
@ -368,7 +367,7 @@ impl<T> Button<T> {
.map(|msg| {
(matches!(msg, ButtonMsg::Clicked)).then(|| CancelConfirmMsg::Confirmed)
}),
)
))
}
pub fn cancel_confirm_text(
@ -407,26 +406,31 @@ impl<T> Button<T> {
let right = Button::with_text(confirm).styled(theme::button_confirm());
let top = Button::with_text(info);
let left = Button::with_icon(theme::ICON_CANCEL);
(
GridPlaced::new(left)
.with_grid(2, 3)
.with_spacing(theme::BUTTON_SPACING)
.with_row_col(1, 0)
.map(|msg| {
(matches!(msg, ButtonMsg::Clicked)).then(|| CancelInfoConfirmMsg::Cancelled)
}),
GridPlaced::new(top)
.with_grid(2, 3)
.with_spacing(theme::BUTTON_SPACING)
.with_from_to((0, 0), (0, 2))
.map(|msg| (matches!(msg, ButtonMsg::Clicked)).then(|| CancelInfoConfirmMsg::Info)),
GridPlaced::new(right)
.with_grid(2, 3)
.with_spacing(theme::BUTTON_SPACING)
.with_from_to((1, 1), (1, 2))
.map(|msg| {
(matches!(msg, ButtonMsg::Clicked)).then(|| CancelInfoConfirmMsg::Confirmed)
}),
theme::button_bar_rows(
2,
(
GridPlaced::new(left)
.with_grid(2, 3)
.with_spacing(theme::BUTTON_SPACING)
.with_row_col(1, 0)
.map(|msg| {
(matches!(msg, ButtonMsg::Clicked)).then(|| CancelInfoConfirmMsg::Cancelled)
}),
GridPlaced::new(top)
.with_grid(2, 3)
.with_spacing(theme::BUTTON_SPACING)
.with_from_to((0, 0), (0, 2))
.map(|msg| {
(matches!(msg, ButtonMsg::Clicked)).then(|| CancelInfoConfirmMsg::Info)
}),
GridPlaced::new(right)
.with_grid(2, 3)
.with_spacing(theme::BUTTON_SPACING)
.with_from_to((1, 1), (1, 2))
.map(|msg| {
(matches!(msg, ButtonMsg::Clicked)).then(|| CancelInfoConfirmMsg::Confirmed)
}),
),
)
}
@ -452,25 +456,25 @@ impl<T> Button<T> {
};
let [top, middle, bottom] = words;
(btn(0, top), btn(1, middle), btn(2, bottom))
theme::button_bar_rows(3, (btn(0, top), btn(1, middle), btn(2, bottom)))
}
}
type CancelConfirm<T, F0, F1> = (
type CancelConfirm<T, F0, F1> = FixedHeightBar<(
Map<GridPlaced<Button<T>>, F0>,
Map<GridPlaced<Button<T>>, F1>,
);
)>;
pub enum CancelConfirmMsg {
Cancelled,
Confirmed,
}
type CancelInfoConfirm<T, F0, F1, F2> = (
type CancelInfoConfirm<T, F0, F1, F2> = FixedHeightBar<(
Map<GridPlaced<Button<T>>, F0>,
Map<GridPlaced<Button<T>>, F1>,
Map<GridPlaced<Button<T>>, F2>,
);
)>;
pub enum CancelInfoConfirmMsg {
Cancelled,

@ -3,7 +3,7 @@ use crate::ui::{
geometry::{Insets, LinearPlacement, Rect},
};
use super::{theme, Button};
use super::theme;
pub enum DialogMsg<T, U> {
Content(T),
@ -40,9 +40,12 @@ where
type Msg = DialogMsg<T::Msg, U::Msg>;
fn place(&mut self, bounds: Rect) -> Rect {
let layout = DialogLayout::middle(bounds);
self.content.place(layout.content);
self.controls.place(layout.controls);
let controls_area = self.controls.place(bounds);
let content_area = bounds
.inset(Insets::bottom(controls_area.height()))
.inset(Insets::bottom(theme::BUTTON_SPACING))
.inset(Insets::left(theme::CONTENT_BORDER));
self.content.place(content_area);
bounds
}
@ -64,21 +67,6 @@ where
}
}
pub struct DialogLayout {
pub content: Rect,
pub controls: Rect,
}
impl DialogLayout {
pub fn middle(area: Rect) -> Self {
let (content, controls) = area.split_bottom(Button::<&str>::HEIGHT);
let content = content
.inset(Insets::bottom(theme::BUTTON_SPACING))
.inset(Insets::left(theme::CONTENT_BORDER));
Self { content, controls }
}
}
#[cfg(feature = "ui_debug")]
impl<T, U> crate::trace::Trace for Dialog<T, U>
where
@ -145,12 +133,14 @@ where
let bounds = bounds
.inset(theme::borders())
.inset(Insets::top(Self::ICON_AREA_PADDING));
let (content, buttons) = bounds.split_bottom(Button::<&str>::HEIGHT);
let (image, content) = content.split_top(Self::ICON_AREA_HEIGHT);
self.image.place(image);
self.paragraphs.place(content);
self.controls.place(buttons);
let controls_area = self.controls.place(bounds);
let content_area = bounds.inset(Insets::bottom(controls_area.height()));
let (image_area, content_area) = content_area.split_top(Self::ICON_AREA_HEIGHT);
self.image.place(image_area);
self.paragraphs.place(content_area);
bounds
}

@ -1,9 +1,8 @@
use crate::{
time::Instant,
ui::{
component::{Child, Component, ComponentExt, Event, EventCtx, Pad},
geometry::{Grid, Rect},
model_tt::component::DialogLayout,
component::{Child, Component, ComponentExt, Event, EventCtx, FixedHeightBar, Pad},
geometry::{Grid, Insets, Rect},
},
};
@ -18,7 +17,7 @@ pub enum HoldToConfirmMsg<T> {
pub struct HoldToConfirm<T> {
loader: Loader,
content: Child<T>,
buttons: CancelHold,
buttons: FixedHeightBar<CancelHold>,
pad: Pad,
}
@ -47,11 +46,14 @@ where
type Msg = HoldToConfirmMsg<T::Msg>;
fn place(&mut self, bounds: Rect) -> Rect {
let layout = DialogLayout::middle(bounds);
self.pad.place(layout.content);
self.loader.place(layout.content);
self.content.place(layout.content);
self.buttons.place(layout.controls);
let controls_area = self.buttons.place(bounds);
let content_area = bounds
.inset(Insets::bottom(controls_area.height()))
.inset(Insets::bottom(theme::BUTTON_SPACING))
.inset(Insets::left(theme::CONTENT_BORDER));
self.pad.place(content_area);
self.loader.place(content_area);
self.content.place(content_area);
bounds
}
@ -121,18 +123,18 @@ pub enum CancelHoldMsg {
}
impl CancelHold {
pub fn new() -> Self {
Self {
pub fn new() -> FixedHeightBar<Self> {
theme::button_bar(Self {
cancel: Some(Button::with_icon(theme::ICON_CANCEL)),
hold: Button::with_text("HOLD TO CONFIRM").styled(theme::button_confirm()),
}
})
}
pub fn without_cancel() -> Self {
Self {
pub fn without_cancel() -> FixedHeightBar<Self> {
theme::button_bar(Self {
cancel: None,
hold: Button::with_text("HOLD TO CONFIRM").styled(theme::button_confirm()),
}
})
}
}

@ -71,7 +71,7 @@ where
fn place(&mut self, bounds: Rect) -> Rect {
self.area = bounds;
let button_height = Button::<&str>::HEIGHT;
let button_height = theme::BUTTON_HEIGHT;
let content_area = self.area.inset(Insets::top(2 * theme::BUTTON_SPACING));
let (input_area, content_area) = content_area.split_top(button_height);
let (content_area, button_area) = content_area.split_bottom(button_height);

@ -1,14 +1,15 @@
use crate::ui::{
component::{
base::ComponentExt, paginated::PageMsg, Component, Event, EventCtx, Label, Pad, Paginate,
base::ComponentExt, paginated::PageMsg, Component, Event, EventCtx, FixedHeightBar, Label,
Pad, Paginate,
},
display::{self, Color},
geometry::Rect,
geometry::{Insets, Rect},
};
use super::{
hold_to_confirm::{handle_hold_event, CancelHold, CancelHoldMsg},
theme, Button, CancelConfirmMsg, Loader, ScrollBar, Swipe, SwipeDirection,
theme, CancelConfirmMsg, Loader, ScrollBar, Swipe, SwipeDirection,
};
pub struct SwipePage<T, U> {
@ -19,7 +20,6 @@ pub struct SwipePage<T, U> {
scrollbar: ScrollBar,
hint: Label<&'static str>,
fade: Option<i32>,
button_rows: i32,
}
impl<T, U> SwipePage<T, U>
@ -37,15 +37,9 @@ where
pad: Pad::with_background(background),
hint: Label::centered("SWIPE TO CONTINUE", theme::label_page_hint()),
fade: None,
button_rows: 1,
}
}
pub fn with_button_rows(mut self, rows: usize) -> Self {
self.button_rows = rows as i32;
self
}
fn setup_swipe(&mut self) {
self.swipe.allow_up = self.scrollbar.has_next_page();
self.swipe.allow_down = self.scrollbar.has_previous_page();
@ -76,20 +70,27 @@ where
type Msg = PageMsg<T::Msg, U::Msg>;
fn place(&mut self, bounds: Rect) -> Rect {
let layout = PageLayout::new(bounds, self.button_rows);
let layout = PageLayout::new(bounds);
self.pad.place(bounds);
self.swipe.place(bounds);
self.hint.place(layout.hint);
self.buttons.place(layout.buttons);
self.scrollbar.place(layout.scrollbar);
let buttons_area = self.buttons.place(layout.buttons);
let buttons_height = buttons_area.height() + theme::BUTTON_SPACING;
self.scrollbar
.place(layout.scrollbar.inset(Insets::bottom(buttons_height)));
// 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);
self.content.place(
layout
.content_single_page
.inset(Insets::bottom(buttons_height)),
);
let page_count = {
let count = self.content.page_count();
if count > 1 {
self.content.place(layout.content);
self.content
.place(layout.content.inset(Insets::bottom(buttons_height)));
self.content.page_count() // Make sure to re-count it with the
// new size.
} else {
@ -197,14 +198,10 @@ impl PageLayout {
const SCROLLBAR_SPACE: i32 = 10;
const HINT_OFF: i32 = 19;
pub fn new(area: Rect, button_rows: i32) -> Self {
let buttons_height = button_rows * Button::<&str>::HEIGHT
+ button_rows.saturating_sub(1) * theme::BUTTON_SPACING;
let (content, buttons) = area.split_bottom(buttons_height);
pub fn new(area: Rect) -> Self {
let (_, hint) = area.split_bottom(Self::HINT_OFF);
let (content, _space) = content.split_bottom(theme::BUTTON_SPACING);
let (buttons, _space) = buttons.split_right(theme::CONTENT_BORDER);
let (_space, content) = content.split_left(theme::CONTENT_BORDER);
let (buttons, _space) = area.split_right(theme::CONTENT_BORDER);
let (_space, content) = area.split_left(theme::CONTENT_BORDER);
let (content_single_page, _space) = content.split_right(theme::CONTENT_BORDER);
let (content, scrollbar) =
content.split_right(Self::SCROLLBAR_SPACE + Self::SCROLLBAR_WIDTH);
@ -221,7 +218,7 @@ impl PageLayout {
}
pub struct SwipeHoldPage<T> {
inner: SwipePage<T, CancelHold>,
inner: SwipePage<T, FixedHeightBar<CancelHold>>,
loader: Loader,
}
@ -328,7 +325,7 @@ mod tests {
component::{text::paragraphs::Paragraphs, Empty},
event::TouchEvent,
geometry::Point,
model_tt::{constant, theme},
model_tt::{component::Button, constant, theme},
},
};
@ -418,13 +415,13 @@ mod tests {
theme::TEXT_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::button_bar(Button::with_text("NO")),
theme::BG,
);
page.place(SCREEN);
let expected1 = "<SwipePage active_page:0 page_count:2 content:<Paragraphs This is somewhat long\nparagraph that goes on\nand on and on and on\nand on and will definitely\nnot fit on just a single\nscreen. You have to\nswipe a bit to see all the\ntext it contains I guess....\n> buttons:<Empty > >";
let expected2 = "<SwipePage active_page:1 page_count:2 content:<Paragraphs There's just so much\nletters in it.\n> buttons:<Empty > >";
let expected1 = "<SwipePage active_page:0 page_count:2 content:<Paragraphs This is somewhat long\nparagraph that goes on\nand on and on and on\nand on and will definitely\nnot fit on just a single\nscreen. You have to\nswipe a bit to see all the\ntext it contains I guess....\n> buttons:<FixedHeightBar inner:<Button text:NO > > >";
let expected2 = "<SwipePage active_page:1 page_count:2 content:<Paragraphs There's just so much\nletters in it.\n> buttons:<FixedHeightBar inner:<Button text:NO > > >";
assert_eq!(trace(&page), expected1);
swipe_down(&mut page);
@ -453,14 +450,14 @@ mod tests {
theme::TEXT_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::button_bar(Button::with_text("IDK")),
theme::BG,
);
page.place(SCREEN);
let expected1 = "<SwipePage active_page:0 page_count:3 content:<Paragraphs This paragraph is using a\nbold font. It doesn't\nneed to be all that long.\nAnd this one is\nusing MONO.\nMonospace is\nnice for...\n> buttons:<Empty > >";
let expected2 = "<SwipePage active_page:1 page_count:3 content:<Paragraphs numbers, they\nhave the same\nwidth and can be\nscanned quickly.\nEven if they\nspan several\npages or...\n> buttons:<Empty > >";
let expected3 = "<SwipePage active_page:2 page_count:3 content:<Paragraphs something.\nLet's add another one\nfor a good measure. This\none should overflow all\nthe way to the third\npage with a bit of luck.\n> buttons:<Empty > >";
let expected1 = "<SwipePage active_page:0 page_count:3 content:<Paragraphs This paragraph is using a\nbold font. It doesn't\nneed to be all that long.\nAnd this one is\nusing MONO.\nMonospace is\nnice for...\n> buttons:<FixedHeightBar inner:<Button text:IDK > > >";
let expected2 = "<SwipePage active_page:1 page_count:3 content:<Paragraphs numbers, they\nhave the same\nwidth and can be\nscanned quickly.\nEven if they\nspan several\npages or...\n> buttons:<FixedHeightBar inner:<Button text:IDK > > >";
let expected3 = "<SwipePage active_page:2 page_count:3 content:<Paragraphs something.\nLet's add another one\nfor a good measure. This\none should overflow all\nthe way to the third\npage with a bit of luck.\n> buttons:<FixedHeightBar inner:<Button text:IDK > > >";
assert_eq!(trace(&page), expected1);
swipe_down(&mut page);
@ -489,14 +486,14 @@ mod tests {
.add_break()
.add(theme::TEXT_NORMAL, "Short three.")
.add_break(),
Empty,
theme::button_bar(Empty),
theme::BG,
);
page.place(SCREEN);
let expected1 = "<SwipePage active_page:0 page_count:3 content:<Paragraphs Short one.\n> buttons:<Empty > >";
let expected2 = "<SwipePage active_page:1 page_count:3 content:<Paragraphs Short two.\n> buttons:<Empty > >";
let expected3 = "<SwipePage active_page:2 page_count:3 content:<Paragraphs Short three.\n> buttons:<Empty > >";
let expected1 = "<SwipePage active_page:0 page_count:3 content:<Paragraphs Short one.\n> buttons:<FixedHeightBar inner:<Empty > > >";
let expected2 = "<SwipePage active_page:1 page_count:3 content:<Paragraphs Short two.\n> buttons:<FixedHeightBar inner:<Empty > > >";
let expected3 = "<SwipePage active_page:2 page_count:3 content:<Paragraphs Short three.\n> buttons:<FixedHeightBar inner:<Empty > > >";
assert_eq!(trace(&page), expected1);
swipe_up(&mut page);

@ -586,9 +586,9 @@ extern "C" fn new_show_simple(n_args: usize, args: *const Obj, kwargs: *mut Map)
t,
Dialog::new(
Paragraphs::new().add(theme::TEXT_NORMAL, description),
Button::with_text(button).map(|msg| {
theme::button_bar(Button::with_text(button).map(|msg| {
(matches!(msg, ButtonMsg::Clicked)).then(|| CancelConfirmMsg::Confirmed)
}),
})),
),
))?
.into()
@ -597,9 +597,9 @@ extern "C" fn new_show_simple(n_args: usize, args: *const Obj, kwargs: *mut Map)
theme::borders(),
Dialog::new(
Paragraphs::new().add(theme::TEXT_NORMAL, description),
Button::with_text(button).map(|msg| {
theme::button_bar(Button::with_text(button).map(|msg| {
(matches!(msg, ButtonMsg::Clicked)).then(|| CancelConfirmMsg::Confirmed)
}),
})),
),
))?
.into()
@ -629,11 +629,7 @@ extern "C" fn new_confirm_with_info(n_args: usize, args: *const Obj, kwargs: *mu
let buttons = Button::cancel_info_confirm(button, info_button);
let obj = LayoutObj::new(
Frame::new(
title,
SwipePage::new(paragraphs, buttons, theme::BG).with_button_rows(2),
)
.into_child(),
Frame::new(title, SwipePage::new(paragraphs, buttons, theme::BG)).into_child(),
)?;
Ok(obj.into())
};
@ -729,11 +725,7 @@ extern "C" fn new_select_word(n_args: usize, args: *const Obj, kwargs: *mut Map)
let buttons = Button::select_word(words);
let obj = LayoutObj::new(
Frame::new(
title,
SwipePage::new(paragraphs, buttons, theme::BG).with_button_rows(3),
)
.into_child(),
Frame::new(title, SwipePage::new(paragraphs, buttons, theme::BG)).into_child(),
)?;
Ok(obj.into())
};
@ -821,9 +813,9 @@ extern "C" fn new_show_checklist(n_args: usize, args: *const Obj, kwargs: *mut M
active,
paragraphs,
),
Button::with_text(button).map(|msg| {
theme::button_bar(Button::with_text(button).map(|msg| {
(matches!(msg, ButtonMsg::Clicked)).then(|| CancelConfirmMsg::Confirmed)
}),
})),
),
)
.with_border(theme::borders())
@ -1115,7 +1107,7 @@ mod tests {
layout.place(SCREEN);
assert_eq!(
trace(&layout),
"<Dialog content:<Text content:Testing text layout, with\nsome text, and some more\ntext. And parameters! > controls:<Tuple 0:<GridPlaced inner:<Button text:Left > > 1:<GridPlaced inner:<Button text:Right > > > >",
"<Dialog content:<Text content:Testing text layout, with\nsome text, and some more\ntext. And parameters! > controls:<FixedHeightBar inner:<Tuple 0:<GridPlaced inner:<Button text:Left > > 1:<GridPlaced inner:<Button text:Right > > > > >",
)
}
}

@ -2,6 +2,7 @@ use crate::ui::{
component::{
label::LabelStyle,
text::{formatted::FormattedFonts, TextStyle},
FixedHeightBar,
},
display::{Color, Font},
geometry::Insets,
@ -407,8 +408,22 @@ pub const FORMATTED: FormattedFonts = FormattedFonts {
pub const CONTENT_BORDER: i32 = 5;
pub const KEYBOARD_SPACING: i32 = 8;
pub const BUTTON_HEIGHT: i32 = 38;
pub const BUTTON_SPACING: i32 = 6;
pub const CHECKLIST_SPACING: i32 = 10;
/// Standard button height in pixels.
pub const fn button_rows(count: usize) -> i32 {
let count = count as i32;
BUTTON_HEIGHT * count + BUTTON_SPACING * count.saturating_sub(1)
}
pub const fn button_bar_rows<T>(rows: usize, inner: T) -> FixedHeightBar<T> {
FixedHeightBar::bottom(inner, button_rows(rows))
}
pub const fn button_bar<T>(inner: T) -> FixedHeightBar<T> {
button_bar_rows(1, inner)
}
/// +----------+
/// | 13 |

Loading…
Cancel
Save