use crate::ui::{ component::{ base::ComponentExt, paginated::PageMsg, Component, Event, EventCtx, Never, Pad, Paginate, }, display::{self, Color}, geometry::{Dimensions, LinearLayout, Offset, Rect}, }; use super::{theme, Button, Swipe, SwipeDirection}; pub struct SwipePage { content: T, buttons: U, pad: Pad, swipe: Swipe, scrollbar: ScrollBar, fade: Option, } impl SwipePage 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); Self { content, buttons: controls(layout.buttons), scrollbar, swipe, pad, 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 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); // 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.request_complete_repaint(ctx); self.pad.clear(); // Swipe has dimmed the screen, so fade back to normal backlight after the next // paint. self.fade = Some(theme::BACKLIGHT_NORMAL); } fn paint_hint(&mut self) { display::text_center( self.pad.area.bottom_center() - Offset::y(3), b"SWIPE TO CONTINUE", theme::FONT_BOLD, // FIXME: Figma has this as 14px but bold is 16px theme::GREY_LIGHT, theme::BG, ); } } impl Component for SwipePage where T: Paginate, T: Component, T: Dimensions, U: Component, { type Msg = PageMsg; fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { 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); return None; } SwipeDirection::Down => { // Scroll up, if possible. self.scrollbar.go_to_previous_page(); self.change_page(ctx, self.scrollbar.active_page); return None; } _ => { // Ignore other directions. } } } if let Some(msg) = self.content.event(ctx, event) { return Some(PageMsg::Content(msg)); } if !self.scrollbar.has_next_page() { if let Some(msg) = self.buttons.event(ctx, event) { return Some(PageMsg::Controls(msg)); } } None } fn paint(&mut self) { self.pad.paint(); self.content.paint(); if self.scrollbar.has_pages() { self.scrollbar.paint(); } if self.scrollbar.has_next_page() { self.paint_hint(); } else { self.buttons.paint(); } if let Some(val) = self.fade.take() { // Note that this is blocking and takes some time. display::fade_backlight(val); } } fn bounds(&self, sink: &mut dyn FnMut(Rect)) { sink(self.scrollbar.area); sink(self.pad.area); self.content.bounds(sink); if !self.scrollbar.has_next_page() { self.buttons.bounds(sink); } } } #[cfg(feature = "ui_debug")] impl crate::trace::Trace for SwipePage where T: crate::trace::Trace, U: crate::trace::Trace, { fn trace(&self, t: &mut dyn crate::trace::Tracer) { t.open("SwipePage"); t.field("active_page", &self.scrollbar.active_page); t.field("page_count", &self.scrollbar.page_count); t.field("content", &self.content); t.field("buttons", &self.buttons); t.close(); } } pub struct ScrollBar { area: Rect, page_count: usize, active_page: usize, } impl ScrollBar { const DOT_SIZE: i32 = 6; /// Edge to edge. const DOT_INTERVAL: i32 = 6; /// Edge of last dot to center of arrow icon. const ARROW_SPACE: i32 = 26; const ICON_ACTIVE: &'static [u8] = include_res!("model_tt/res/scroll-active.toif"); const ICON_INACTIVE: &'static [u8] = include_res!("model_tt/res/scroll-inactive.toif"); 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 { Self { area, page_count, active_page, } } pub fn has_pages(&self) -> bool { self.page_count > 1 } pub fn has_next_page(&self) -> bool { self.active_page < self.page_count - 1 } pub fn has_previous_page(&self) -> bool { self.active_page > 0 } pub fn go_to_next_page(&mut self) { self.go_to(self.active_page.saturating_add(1).min(self.page_count - 1)); } pub fn go_to_previous_page(&mut self) { self.go_to(self.active_page.saturating_sub(1)); } pub fn go_to(&mut self, active_page: usize) { self.active_page = active_page; } } impl Component for ScrollBar { type Msg = Never; fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option { None } fn paint(&mut self) { let layout = LinearLayout::vertical() .align_at_center() .with_spacing(Self::DOT_INTERVAL); let mut i = 0; let mut top = None; let mut display_icon = |top_left| { let icon = if i == self.active_page { Self::ICON_ACTIVE } else { Self::ICON_INACTIVE }; display::icon_top_left(top_left, icon, theme::FG, theme::BG); i += 1; top.get_or_insert(top_left.x); }; layout.arrange_uniform( self.area, self.page_count, Offset::new(Self::DOT_SIZE, Self::DOT_SIZE), &mut display_icon, ); let arrow_distance = self.area.center().x - top.unwrap_or(0) + Self::ARROW_SPACE; if self.has_previous_page() { display::icon( self.area.center() - Offset::y(arrow_distance), Self::ICON_UP, theme::FG, theme::BG, ); } if self.has_next_page() { display::icon( self.area.center() + Offset::y(arrow_distance), Self::ICON_DOWN, theme::FG, theme::BG, ); } } } pub struct PageLayout { pub content_single_page: Rect, pub content: Rect, pub scrollbar: Rect, pub buttons: Rect, } impl PageLayout { const BUTTON_SPACE: i32 = 6; const SCROLLBAR_WIDTH: i32 = 10; const SCROLLBAR_SPACE: i32 = 10; pub fn new(area: Rect) -> Self { let (content, buttons) = area.split_bottom(Button::<&str>::HEIGHT); let (content, _space) = content.split_bottom(Self::BUTTON_SPACE); let (buttons, _space) = buttons.split_right(theme::CONTENT_BORDER); let (_space, content) = content.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); let (_space, scrollbar) = scrollbar.split_left(Self::SCROLLBAR_SPACE); Self { content_single_page, content, scrollbar, buttons, } } } #[cfg(test)] mod tests { use crate::{ trace::Trace, ui::{ component::{text::paragraphs::Paragraphs, Empty}, display, geometry::Point, model_tt::{event::TouchEvent, theme}, }, }; use super::*; fn trace(val: &impl Trace) -> String { let mut t = Vec::new(); val.trace(&mut t); String::from_utf8(t).unwrap() } fn swipe(component: &mut impl Component, points: &[(i32, i32)]) { let last = points.len().saturating_sub(1); let mut first = true; let mut ctx = EventCtx::new(); for (i, &(x, y)) in points.iter().enumerate() { let p = Point::new(x, y); let ev = if first { TouchEvent::TouchStart(p) } else if i == last { TouchEvent::TouchEnd(p) } else { TouchEvent::TouchMove(p) }; component.event(&mut ctx, Event::Touch(ev)); first = false; } } fn swipe_up(component: &mut impl Component) { swipe(component, &[(20, 100), (20, 60), (20, 20)]) } fn swipe_down(component: &mut impl Component) { swipe(component, &[(20, 20), (20, 60), (20, 100)]) } #[test] fn paragraphs_empty() { let mut page = SwipePage::new( display::screen(), theme::BG, |area| Paragraphs::<&[u8]>::new(area), |_| Empty, ); let expected = " buttons: >"; assert_eq!(trace(&page), expected); swipe_up(&mut page); assert_eq!(trace(&page), expected); swipe_down(&mut page); assert_eq!(trace(&page), expected); } #[test] fn paragraphs_single() { let mut page = SwipePage::new( display::screen(), theme::BG, |area| { Paragraphs::new(area) .add::( theme::FONT_NORMAL, "This is the first paragraph and it should fit on the screen entirely.", ) .add::( theme::FONT_BOLD, "Second, bold, paragraph should also fit on the screen whole I think.", ) }, |_| Empty, ); let expected = " buttons: >"; assert_eq!(trace(&page), expected); swipe_up(&mut page); assert_eq!(trace(&page), expected); swipe_down(&mut page); assert_eq!(trace(&page), expected); } #[test] fn paragraphs_one_long() { let mut page = SwipePage::new( display::screen(), theme::BG, |area| { Paragraphs::new(area) .add::( 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, ); let expected1 = " buttons: >"; let expected2 = " buttons: >"; assert_eq!(trace(&page), expected1); swipe_down(&mut page); assert_eq!(trace(&page), expected1); swipe_up(&mut page); assert_eq!(trace(&page), expected2); swipe_up(&mut page); assert_eq!(trace(&page), expected2); swipe_down(&mut page); assert_eq!(trace(&page), expected1); } #[test] fn paragraphs_three_long() { let mut page = SwipePage::new( display::screen(), theme::BG, |area| { Paragraphs::new(area) .add::( theme::FONT_BOLD, "This paragraph is using a bold font. It doesn't need to be all that long.", ) .add::( 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::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, ); let expected1 = " buttons: >"; let expected2 = " buttons: >"; let expected3 = " buttons: >"; assert_eq!(trace(&page), expected1); swipe_down(&mut page); assert_eq!(trace(&page), expected1); swipe_up(&mut page); assert_eq!(trace(&page), expected2); swipe_up(&mut page); assert_eq!(trace(&page), expected3); swipe_up(&mut page); assert_eq!(trace(&page), expected3); swipe_down(&mut page); assert_eq!(trace(&page), expected2); swipe_down(&mut page); assert_eq!(trace(&page), expected1); swipe_down(&mut page); assert_eq!(trace(&page), expected1); } }