feat(core/rust): Add Paginated component

matejcik/one-of
Jan Pochyla 3 years ago committed by matejcik
parent a7a305d34d
commit a3c79bf4f7

@ -4,12 +4,14 @@ pub mod base;
pub mod empty; pub mod empty;
pub mod label; pub mod label;
pub mod map; pub mod map;
pub mod pad;
pub mod text; pub mod text;
pub mod tuple; pub mod tuple;
pub use base::{Child, Component, ComponentExt, Event, EventCtx, Never, TimerToken}; pub use base::{Child, Component, ComponentExt, Event, EventCtx, Never, TimerToken};
pub use empty::Empty; pub use empty::Empty;
pub use label::{Label, LabelStyle}; pub use label::{Label, LabelStyle};
pub use pad::Pad;
pub use text::{ pub use text::{
formatted::FormattedText, formatted::FormattedText,
layout::{LineBreaking, PageBreaking, TextLayout}, layout::{LineBreaking, PageBreaking, TextLayout},

@ -0,0 +1,32 @@
use crate::ui::{
display::{self, Color},
geometry::Rect,
};
pub struct Pad {
area: Rect,
color: Color,
clear: bool,
}
impl Pad {
pub fn with_background(area: Rect, color: Color) -> Self {
Self {
area,
color,
clear: false,
}
}
pub fn clear(&mut self) {
self.clear = true;
}
pub fn paint(&mut self) {
if self.clear {
self.clear = false;
display::rect(self.area, self.color);
}
}
}

@ -12,7 +12,8 @@ use crate::ui::{
}; };
use super::layout::{ use super::layout::{
DefaultTextTheme, LayoutSink, LineBreaking, Op, PageBreaking, TextLayout, TextRenderer, DefaultTextTheme, LayoutFit, LayoutSink, LineBreaking, Op, PageBreaking, TextLayout,
TextRenderer,
}; };
pub const MAX_ARGUMENTS: usize = 6; pub const MAX_ARGUMENTS: usize = 6;
@ -21,6 +22,7 @@ pub struct FormattedText<F, T> {
layout: TextLayout, layout: TextLayout,
format: F, format: F,
args: LinearMap<&'static [u8], T, MAX_ARGUMENTS>, args: LinearMap<&'static [u8], T, MAX_ARGUMENTS>,
char_offset: usize,
} }
impl<F, T> FormattedText<F, T> { impl<F, T> FormattedText<F, T> {
@ -29,6 +31,7 @@ impl<F, T> FormattedText<F, T> {
layout: TextLayout::new::<D>(area), layout: TextLayout::new::<D>(area),
format, format,
args: LinearMap::new(), args: LinearMap::new(),
char_offset: 0,
} }
} }
@ -65,6 +68,14 @@ impl<F, T> FormattedText<F, T> {
self self
} }
pub fn set_char_offset(&mut self, char_offset: usize) {
self.char_offset = char_offset;
}
pub fn char_offset(&mut self) -> usize {
self.char_offset
}
pub fn layout_mut(&mut self) -> &mut TextLayout { pub fn layout_mut(&mut self) -> &mut TextLayout {
&mut self.layout &mut self.layout
} }
@ -75,9 +86,10 @@ where
F: AsRef<[u8]>, F: AsRef<[u8]>,
T: AsRef<[u8]>, T: AsRef<[u8]>,
{ {
fn layout_content(&self, sink: &mut dyn LayoutSink) { pub fn layout_content(&self, sink: &mut dyn LayoutSink) -> LayoutFit {
let mut cursor = self.layout.initial_cursor(); let mut cursor = self.layout.initial_cursor();
let mut ops = Tokenizer::new(self.format.as_ref()).flat_map(|arg| match arg { let mut ops = Op::skip_n_text_bytes(
Tokenizer::new(self.format.as_ref()).flat_map(|arg| match arg {
Token::Literal(literal) => Some(Op::Text(literal)), Token::Literal(literal) => Some(Op::Text(literal)),
Token::Argument(b"mono") => Some(Op::Font(self.layout.mono_font)), Token::Argument(b"mono") => Some(Op::Font(self.layout.mono_font)),
Token::Argument(b"bold") => Some(Op::Font(self.layout.bold_font)), Token::Argument(b"bold") => Some(Op::Font(self.layout.bold_font)),
@ -86,8 +98,10 @@ where
.args .args
.get(argument) .get(argument)
.map(|value| Op::Text(value.as_ref())), .map(|value| Op::Text(value.as_ref())),
}); }),
self.layout.layout_ops(&mut ops, &mut cursor, sink); self.char_offset,
);
self.layout.layout_ops(&mut ops, &mut cursor, sink)
} }
} }

@ -210,7 +210,7 @@ impl TextLayout {
// cursor. // cursor.
let init_cursor = self.initial_cursor(); let init_cursor = self.initial_cursor();
let mut cursor = init_cursor; let mut cursor = init_cursor;
self.layout_ops(ops, &mut cursor, &mut TextNoop); self.layout_ops(ops, &mut cursor, &mut TextNoOp);
cursor.y - init_cursor.y + self.text_font.line_height() cursor.y - init_cursor.y + self.text_font.line_height()
} }
@ -219,7 +219,7 @@ impl TextLayout {
// cursor. // cursor.
let init_cursor = self.initial_cursor(); let init_cursor = self.initial_cursor();
let mut cursor = init_cursor; let mut cursor = init_cursor;
self.layout_text(text, &mut cursor, &mut TextNoop); self.layout_text(text, &mut cursor, &mut TextNoOp);
cursor.y - init_cursor.y + self.text_font.line_height() cursor.y - init_cursor.y + self.text_font.line_height()
} }
} }
@ -238,9 +238,9 @@ pub trait LayoutSink {
fn out_of_bounds(&mut self) {} fn out_of_bounds(&mut self) {}
} }
pub struct TextNoop; pub struct TextNoOp;
impl LayoutSink for TextNoop {} impl LayoutSink for TextNoOp {}
pub struct TextRenderer; pub struct TextRenderer;
@ -325,7 +325,7 @@ impl<'a> Op<'a> {
skipped = skipped.saturating_add(text.len()); skipped = skipped.saturating_add(text.len());
if skipped > skip_bytes { if skipped > skip_bytes {
let leave_bytes = skipped - skip_bytes; let leave_bytes = skipped - skip_bytes;
Some(Op::Text(&text[..text.len() - leave_bytes])) Some(Op::Text(&text[text.len() - leave_bytes..]))
} else { } else {
None None
} }

@ -1,8 +1,7 @@
use crate::{ use crate::{
time::Instant, time::Instant,
ui::{ ui::{
component::{Child, Component, ComponentExt, Event, EventCtx}, component::{Child, Component, ComponentExt, Event, EventCtx, Pad},
display::{self, Color},
geometry::Rect, geometry::Rect,
}, },
}; };
@ -110,31 +109,3 @@ where
d.close(); d.close();
} }
} }
struct Pad {
area: Rect,
color: Color,
clear: bool,
}
impl Pad {
fn with_background(area: Rect, color: Color) -> Self {
Self {
area,
color,
clear: false,
}
}
fn clear(&mut self) {
self.clear = true;
}
fn paint(&mut self) {
if self.clear {
self.clear = false;
display::rect(self.area, self.color);
}
}
}

@ -3,6 +3,7 @@ mod confirm;
mod dialog; mod dialog;
mod loader; mod loader;
mod page; mod page;
mod paginated;
mod passphrase; mod passphrase;
mod pin; mod pin;
mod swipe; mod swipe;
@ -11,6 +12,7 @@ pub use button::{Button, ButtonContent, ButtonMsg, ButtonStyle, ButtonStyleSheet
pub use confirm::{HoldToConfirm, HoldToConfirmMsg}; pub use confirm::{HoldToConfirm, HoldToConfirmMsg};
pub use dialog::{Dialog, DialogLayout, DialogMsg}; pub use dialog::{Dialog, DialogLayout, DialogMsg};
pub use loader::{Loader, LoaderMsg, LoaderStyle, LoaderStyleSheet}; pub use loader::{Loader, LoaderMsg, LoaderStyle, LoaderStyleSheet};
pub use paginated::{Paginate, Paginated};
pub use swipe::{Swipe, SwipeDirection}; pub use swipe::{Swipe, SwipeDirection};
use super::{event, theme}; use super::{event, theme};

@ -21,14 +21,30 @@ impl<T> Page<T> {
pub fn new(area: Rect, page: T, page_count: usize, active_page: usize) -> Self { pub fn new(area: Rect, page: T, page_count: usize, active_page: usize) -> Self {
let scrollbar = ScrollBar::vertical_right(area, page_count, active_page); let scrollbar = ScrollBar::vertical_right(area, page_count, active_page);
let mut swipe = Swipe::new(area); let mut swipe = Swipe::new(area);
swipe.allow_up = scrollbar.has_next_page(); Self::setup_swipe(&scrollbar, &mut swipe);
swipe.allow_down = scrollbar.has_previous_page();
Self { Self {
swipe, swipe,
scrollbar, scrollbar,
page, page,
} }
} }
fn setup_swipe(scrollbar: &ScrollBar, swipe: &mut Swipe) {
swipe.allow_up = scrollbar.has_next_page();
swipe.allow_down = scrollbar.has_previous_page();
}
pub fn inner_mut(&mut self) -> &mut T {
&mut self.page
}
pub fn page_count(&self) -> usize {
self.scrollbar.page_count
}
pub fn active_page(&self) -> usize {
self.scrollbar.active_page
}
} }
impl<T: Component> Component for Page<T> { impl<T: Component> Component for Page<T> {
@ -39,11 +55,15 @@ impl<T: Component> Component for Page<T> {
match swipe { match swipe {
SwipeDirection::Up => { SwipeDirection::Up => {
// Scroll down, if possible. // Scroll down, if possible.
return Some(PageMsg::ChangePage(self.scrollbar.next_page())); self.scrollbar.go_to_next_page();
Self::setup_swipe(&self.scrollbar, &mut self.swipe);
return Some(PageMsg::ChangePage(self.active_page()));
} }
SwipeDirection::Down => { SwipeDirection::Down => {
// Scroll up, if possible. // Scroll up, if possible.
return Some(PageMsg::ChangePage(self.scrollbar.previous_page())); self.scrollbar.go_to_previous_page();
Self::setup_swipe(&self.scrollbar, &mut self.swipe);
return Some(PageMsg::ChangePage(self.active_page()));
} }
_ => { _ => {
// Ignore other directions. // Ignore other directions.
@ -62,6 +82,20 @@ impl<T: Component> Component for Page<T> {
} }
} }
#[cfg(feature = "ui_debug")]
impl<T> crate::trace::Trace for Page<T>
where
T: crate::trace::Trace,
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.open("Page");
t.field("active_page", &self.active_page());
t.field("page_count", &self.page_count());
t.field("content", &self.page);
t.close();
}
}
pub struct ScrollBar { pub struct ScrollBar {
area: Rect, area: Rect,
page_count: usize, page_count: usize,
@ -88,12 +122,12 @@ impl ScrollBar {
self.active_page > 0 self.active_page > 0
} }
pub fn next_page(&self) -> usize { pub fn go_to_next_page(&mut self) {
self.active_page.saturating_add(1).min(self.page_count - 1) self.active_page = self.active_page.saturating_add(1).min(self.page_count - 1);
} }
pub fn previous_page(&self) -> usize { pub fn go_to_previous_page(&mut self) {
self.active_page.saturating_sub(1) self.active_page = self.active_page.saturating_sub(1);
} }
} }

@ -0,0 +1,132 @@
use crate::ui::{
component::{
text::layout::{LayoutFit, TextNoOp},
Component, ComponentExt, Event, EventCtx, FormattedText, Pad,
},
display,
geometry::Rect,
};
use super::{
page::{Page, PageMsg},
theme,
};
pub struct Paginated<T> {
page: Page<T>,
pad: Pad,
fade_after_next_paint: Option<i32>,
}
impl<T> Paginated<T>
where
T: Paginate,
{
pub fn new(area: Rect, mut content: T) -> Self {
let active_page = 0;
let page_count = content.page_count();
Self {
page: Page::new(area, content, page_count, active_page),
pad: Pad::with_background(area, theme::BG),
fade_after_next_paint: None,
}
}
}
impl<T> Component for Paginated<T>
where
T: Paginate,
T: Component,
{
type Msg = T::Msg;
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
self.page.event(ctx, event).and_then(|msg| match msg {
PageMsg::Content(c) => Some(c),
PageMsg::ChangePage(page) => {
self.fade_after_next_paint = Some(theme::BACKLIGHT_NORMAL);
self.page.inner_mut().change_page(page);
self.page.inner_mut().request_complete_repaint(ctx);
self.pad.clear();
None
}
})
}
fn paint(&mut self) {
self.pad.paint();
self.page.paint();
if let Some(val) = self.fade_after_next_paint.take() {
display::fade_backlight(val);
}
}
}
#[cfg(feature = "ui_debug")]
impl<T> crate::trace::Trace for Paginated<T>
where
T: crate::trace::Trace,
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
self.page.trace(t);
}
}
pub trait Paginate {
fn page_count(&mut self) -> usize;
fn change_page(&mut self, active_page: usize);
}
impl<F, T> Paginate for FormattedText<F, T>
where
F: AsRef<[u8]>,
T: AsRef<[u8]>,
{
fn page_count(&mut self) -> usize {
let mut page_count = 1; // There's always at least one page.
let mut char_offset = 0;
loop {
let fit = self.layout_content(&mut TextNoOp);
match fit {
LayoutFit::Fitting { .. } => {
break; // TODO: We should consider if there's more content
// to render.
}
LayoutFit::OutOfBounds { processed_chars } => {
page_count += 1;
char_offset += processed_chars;
self.set_char_offset(char_offset);
}
}
}
// Reset the char offset back to the beginning.
self.set_char_offset(0);
page_count
}
fn change_page(&mut self, to_page: usize) {
let mut active_page = 0;
let mut char_offset = 0;
// Make sure we're starting from the beginning.
self.set_char_offset(char_offset);
while active_page < to_page {
let fit = self.layout_content(&mut TextNoOp);
match fit {
LayoutFit::Fitting { .. } => {
break; // TODO: We should consider if there's more content
// to render.
}
LayoutFit::OutOfBounds { processed_chars } => {
active_page += 1;
char_offset += processed_chars;
self.set_char_offset(char_offset);
}
}
}
}
}

@ -264,7 +264,6 @@ impl TextBox {
fn replace_last(&mut self, ctx: &mut EventCtx, char: u8) { fn replace_last(&mut self, ctx: &mut EventCtx, char: u8) {
self.text.pop(); self.text.pop();
if self.text.push(char).is_err() { if self.text.push(char).is_err() {
// Should not happen unless `self.text` has zero capacity.
#[cfg(feature = "ui_debug")] #[cfg(feature = "ui_debug")]
panic!("textbox has zero capacity"); panic!("textbox has zero capacity");
} }
@ -273,7 +272,6 @@ impl TextBox {
fn append(&mut self, ctx: &mut EventCtx, char: u8) { fn append(&mut self, ctx: &mut EventCtx, char: u8) {
if self.text.push(char).is_err() { if self.text.push(char).is_err() {
// `self.text` is full, ignore this change.
#[cfg(feature = "ui_debug")] #[cfg(feature = "ui_debug")]
panic!("textbox is full"); panic!("textbox is full");
} }

Loading…
Cancel
Save