You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
306 lines
10 KiB
306 lines
10 KiB
use crate::ui::{
|
|
component::{Component, Event, EventCtx, Never, Pad, Paginate},
|
|
display,
|
|
geometry::{Offset, Point, Rect},
|
|
shape,
|
|
shape::Renderer,
|
|
};
|
|
|
|
use super::super::theme;
|
|
|
|
use heapless::Vec;
|
|
|
|
/// Scrollbar to be painted horizontally at the top right of the screen.
|
|
pub struct ScrollBar {
|
|
pad: Pad,
|
|
page_count: usize,
|
|
pub active_page: usize,
|
|
}
|
|
|
|
/// Carrying the appearance of the scrollbar dot.
|
|
#[derive(Debug)]
|
|
enum DotType {
|
|
BigFull, // *
|
|
Big, // O
|
|
Middle, // o
|
|
Small, // .
|
|
}
|
|
|
|
pub const SCROLLBAR_SPACE: i16 = 5;
|
|
|
|
/// How many dots at most will there be
|
|
const MAX_DOTS: usize = 5;
|
|
|
|
impl ScrollBar {
|
|
/// Maximum size (width/height) of a dot
|
|
pub const MAX_DOT_SIZE: i16 = 5;
|
|
/// Distance between two dots
|
|
pub const DOTS_DISTANCE: i16 = 2;
|
|
pub const DOTS_INTERVAL: i16 = Self::MAX_DOT_SIZE + Self::DOTS_DISTANCE;
|
|
pub const MAX_WIDTH: i16 = Self::dots_width(MAX_DOTS);
|
|
|
|
pub fn new(page_count: usize) -> Self {
|
|
Self {
|
|
pad: Pad::with_background(theme::BG),
|
|
page_count,
|
|
active_page: 0,
|
|
}
|
|
}
|
|
|
|
/// Page count will be given later as it is not available yet.
|
|
pub fn to_be_filled_later() -> Self {
|
|
Self::new(0)
|
|
}
|
|
|
|
pub const fn dots_width(dots_shown: usize) -> i16 {
|
|
Self::DOTS_INTERVAL * dots_shown as i16 - Self::DOTS_DISTANCE
|
|
}
|
|
|
|
/// The width the scrollbar will really occupy.
|
|
pub fn overall_width(&self) -> i16 {
|
|
let dots_shown = self.page_count.min(MAX_DOTS);
|
|
Self::dots_width(dots_shown)
|
|
}
|
|
|
|
pub fn set_page_count(&mut self, page_count: usize) {
|
|
self.page_count = page_count;
|
|
}
|
|
|
|
/// Create a (seemingly circular) dot given its top left point.
|
|
/// Make it full when it is active, otherwise paint just the perimeter and
|
|
/// leave center empty.
|
|
fn paint_dot(&self, dot_type: &DotType, top_right: Point) {
|
|
let full_square =
|
|
Rect::from_top_right_and_size(top_right, Offset::uniform(Self::MAX_DOT_SIZE));
|
|
|
|
match dot_type {
|
|
DotType::BigFull | DotType::Big => {
|
|
// FG - painting the full square
|
|
display::rect_fill(full_square, theme::FG);
|
|
|
|
// BG - erase four corners
|
|
display::rect_fill_corners(full_square, theme::BG);
|
|
|
|
// BG - erasing the middle when not full
|
|
if matches!(dot_type, DotType::Big) {
|
|
display::rect_fill(full_square.shrink(1), theme::BG)
|
|
}
|
|
}
|
|
DotType::Middle => {
|
|
let middle_square = full_square.shrink(1);
|
|
|
|
// FG - painting the middle square
|
|
display::rect_fill(middle_square, theme::FG);
|
|
|
|
// BG - erase four corners
|
|
display::rect_fill_corners(middle_square, theme::BG);
|
|
|
|
// BG - erasing the middle
|
|
display::rect_fill(middle_square.shrink(1), theme::BG)
|
|
}
|
|
DotType::Small => {
|
|
// FG - painting the small square
|
|
display::rect_fill(full_square.shrink(2), theme::FG)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Create a (seemingly circular) dot given its top left point.
|
|
/// Make it full when it is active, otherwise paint just the perimeter and
|
|
/// leave center empty.
|
|
fn render_dot<'s>(&self, target: &mut impl Renderer<'s>, dot_type: &DotType, top_right: Point) {
|
|
let full_square =
|
|
Rect::from_top_right_and_size(top_right, Offset::uniform(Self::MAX_DOT_SIZE));
|
|
|
|
match dot_type {
|
|
DotType::BigFull => shape::Bar::new(full_square)
|
|
.with_radius(2)
|
|
.with_bg(theme::FG)
|
|
.render(target),
|
|
|
|
DotType::Big => shape::Bar::new(full_square)
|
|
.with_radius(2)
|
|
.with_fg(theme::FG)
|
|
.render(target),
|
|
|
|
DotType::Middle => shape::Bar::new(full_square.shrink(1))
|
|
.with_radius(1)
|
|
.with_fg(theme::FG)
|
|
.render(target),
|
|
|
|
DotType::Small => shape::Bar::new(full_square.shrink(2))
|
|
.with_bg(theme::FG)
|
|
.render(target),
|
|
}
|
|
}
|
|
|
|
/// Get a sequence of dots to be drawn, with specifying their appearance.
|
|
/// Painting only big dots in case of 2 and 3 pages,
|
|
/// three big and 1 middle in case of 4 pages,
|
|
/// and three big, one middle and one small in case of 5 and more pages.
|
|
fn get_drawable_dots(&self) -> Vec<DotType, MAX_DOTS> {
|
|
let mut dots = Vec::new();
|
|
|
|
match self.page_count {
|
|
0..=3 => {
|
|
// *OO
|
|
// O*O
|
|
// OO*
|
|
for i in 0..self.page_count {
|
|
if i == self.active_page {
|
|
unwrap!(dots.push(DotType::BigFull));
|
|
} else {
|
|
unwrap!(dots.push(DotType::Big));
|
|
}
|
|
}
|
|
}
|
|
4 => {
|
|
// *OOo
|
|
// O*Oo
|
|
// oO*O
|
|
// oOO*
|
|
match self.active_page {
|
|
0 => unwrap!(dots.push(DotType::BigFull)),
|
|
1 => unwrap!(dots.push(DotType::Big)),
|
|
_ => unwrap!(dots.push(DotType::Middle)),
|
|
};
|
|
match self.active_page {
|
|
1 => unwrap!(dots.push(DotType::BigFull)),
|
|
_ => unwrap!(dots.push(DotType::Big)),
|
|
};
|
|
match self.active_page {
|
|
2 => unwrap!(dots.push(DotType::BigFull)),
|
|
_ => unwrap!(dots.push(DotType::Big)),
|
|
};
|
|
match self.active_page {
|
|
3 => unwrap!(dots.push(DotType::BigFull)),
|
|
2 => unwrap!(dots.push(DotType::Big)),
|
|
_ => unwrap!(dots.push(DotType::Middle)),
|
|
};
|
|
}
|
|
_ => {
|
|
// *OOo.
|
|
// O*Oo.
|
|
// oO*Oo
|
|
// ...
|
|
// oO*Oo
|
|
// .oO*O
|
|
// .oOO*
|
|
let full_dot_index = match self.active_page {
|
|
0 => 0,
|
|
1 => 1,
|
|
last_but_one if last_but_one == self.page_count - 2 => 3,
|
|
last if last == self.page_count - 1 => 4,
|
|
_ => 2,
|
|
};
|
|
match full_dot_index {
|
|
0 => unwrap!(dots.push(DotType::BigFull)),
|
|
1 => unwrap!(dots.push(DotType::Big)),
|
|
2 => unwrap!(dots.push(DotType::Middle)),
|
|
_ => unwrap!(dots.push(DotType::Small)),
|
|
};
|
|
match full_dot_index {
|
|
0 => unwrap!(dots.push(DotType::Big)),
|
|
1 => unwrap!(dots.push(DotType::BigFull)),
|
|
2 => unwrap!(dots.push(DotType::Big)),
|
|
_ => unwrap!(dots.push(DotType::Middle)),
|
|
};
|
|
match full_dot_index {
|
|
2 => unwrap!(dots.push(DotType::BigFull)),
|
|
_ => unwrap!(dots.push(DotType::Big)),
|
|
};
|
|
match full_dot_index {
|
|
0 | 1 => unwrap!(dots.push(DotType::Middle)),
|
|
3 => unwrap!(dots.push(DotType::BigFull)),
|
|
_ => unwrap!(dots.push(DotType::Big)),
|
|
};
|
|
match full_dot_index {
|
|
0 | 1 => unwrap!(dots.push(DotType::Small)),
|
|
2 => unwrap!(dots.push(DotType::Middle)),
|
|
3 => unwrap!(dots.push(DotType::Big)),
|
|
_ => unwrap!(dots.push(DotType::BigFull)),
|
|
};
|
|
}
|
|
}
|
|
dots
|
|
}
|
|
|
|
/// Drawing the dots horizontally and aligning to the right.
|
|
fn paint_horizontal(&mut self) {
|
|
let mut top_right = self.pad.area.top_right();
|
|
for dot in self.get_drawable_dots().iter().rev() {
|
|
self.paint_dot(dot, top_right);
|
|
top_right.x -= Self::DOTS_INTERVAL;
|
|
}
|
|
}
|
|
|
|
fn render_horizontal<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
|
let mut top_right = self.pad.area.top_right();
|
|
for dot in self.get_drawable_dots().iter().rev() {
|
|
self.render_dot(target, dot, top_right);
|
|
top_right.x -= Self::DOTS_INTERVAL;
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Component for ScrollBar {
|
|
type Msg = Never;
|
|
|
|
fn place(&mut self, bounds: Rect) -> Rect {
|
|
// Occupying as little space as possible (according to the number of pages),
|
|
// aligning to the right.
|
|
let scrollbar_area = Rect::from_top_right_and_size(
|
|
bounds.top_right() + Offset::y(1), // offset for centering vertically
|
|
Offset::new(self.overall_width(), Self::MAX_DOT_SIZE),
|
|
);
|
|
self.pad.place(scrollbar_area);
|
|
scrollbar_area
|
|
}
|
|
|
|
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
|
|
None
|
|
}
|
|
|
|
/// Displaying one dot for each page.
|
|
fn paint(&mut self) {
|
|
// Not showing the scrollbar dot when there is only one page
|
|
if self.page_count <= 1 {
|
|
return;
|
|
}
|
|
|
|
self.pad.clear();
|
|
self.pad.paint();
|
|
self.paint_horizontal();
|
|
}
|
|
|
|
/// Displaying one dot for each page.
|
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
|
// Not showing the scrollbar dot when there is only one page
|
|
if self.page_count <= 1 {
|
|
return;
|
|
}
|
|
|
|
self.pad.render(target);
|
|
self.render_horizontal(target);
|
|
}
|
|
}
|
|
|
|
impl Paginate for ScrollBar {
|
|
fn page_count(&mut self) -> usize {
|
|
self.page_count
|
|
}
|
|
|
|
fn change_page(&mut self, active_page: usize) {
|
|
self.active_page = active_page;
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "ui_debug")]
|
|
impl crate::trace::Trace for ScrollBar {
|
|
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
|
t.component("ScrollBar");
|
|
t.int("scrollbar_page_count", self.page_count as i64);
|
|
t.int("scrollbar_active_page", self.active_page as i64);
|
|
}
|
|
}
|