faststep 0.1.0

UIKit-inspired embedded UI framework built on embedded-graphics
Documentation
use embedded_graphics::{
    draw_target::{DrawTarget, DrawTargetExt},
    pixelcolor::Rgb565,
    prelude::{Point, Size},
    primitives::Rectangle,
};

use crate::{ScrollBar, ViewEnvironment, ViewRedraw};

use super::{
    ListDataSource, ListDelegate, ListItem, ListRow, ListRowState, ListSelection, ListView,
};

impl<DataSource, Delegate> ListView<DataSource, Delegate>
where
    DataSource: ListDataSource,
{
    /// Returns the rounded vertical content offset.
    pub fn content_offset_y(&self) -> i32 {
        self.scroll_view.content_offset_y()
    }

    /// Returns the total content height.
    pub fn content_height(&self) -> u32 {
        (0..self.data_source.item_count())
            .map(|index| self.data_source.item_height(index))
            .sum()
    }

    /// Returns the selected row index, if any.
    pub fn selected_index(&self) -> Option<usize> {
        self.selected_index
            .filter(|index| *index < self.data_source.item_count())
    }

    /// Returns the selected row payload, if any.
    pub fn selected_item(&self) -> Option<ListSelection<DataSource::ItemId>> {
        self.selection_for_index(self.selected_index()?)
    }

    /// Returns the highlighted row payload, if any.
    pub fn highlighted_item(&self) -> Option<ListSelection<DataSource::ItemId>> {
        self.selection_for_index(self.highlighted_index?)
    }

    /// Sets the selected row index.
    pub fn set_selected_index(&mut self, index: Option<usize>) -> bool {
        let normalized = index.filter(|index| *index < self.data_source.item_count());
        if self.selected_index == normalized {
            return false;
        }
        self.selected_index = normalized;
        true
    }

    /// Clears row selection.
    pub fn clear_selection(&mut self) -> bool {
        self.set_selected_index(None)
    }

    /// Returns whether the vertical indicator is enabled.
    pub fn shows_vertical_scroll_indicator(&self) -> bool {
        self.scroll_view.shows_vertical_scroll_indicator()
    }

    /// Enables or disables the vertical indicator.
    pub fn set_shows_vertical_scroll_indicator(&mut self, shows: bool) {
        self.scroll_view.set_shows_vertical_scroll_indicator(shows);
    }

    /// Returns the current scrollbar thumb, if visible.
    pub fn scroll_bar(&self, viewport: Rectangle) -> Option<ScrollBar> {
        self.scroll_view.scroll_bar(viewport, self.content_height())
    }

    /// Returns the dirty rect for indicator updates, if any.
    pub fn scroll_bar_dirty(&self, viewport: Rectangle) -> Option<Rectangle> {
        self.scroll_view
            .scroll_bar_dirty(viewport, self.content_height())
    }

    /// Returns the content rect used for scroll blitting.
    pub fn motion_content_rect(&self, viewport: Rectangle) -> Rectangle {
        self.scroll_view
            .motion_content_rect(viewport, self.content_height())
    }

    /// Advances list animation state.
    pub fn tick(&mut self, dt_ms: u32, viewport: Rectangle) -> ViewRedraw {
        if self
            .scroll_view
            .tick(dt_ms, self.content_height(), viewport.size.height)
        {
            ViewRedraw::Dirty(viewport)
        } else {
            ViewRedraw::None
        }
    }

    /// Returns the frame for one item index, even if only partially visible.
    pub fn item_frame(&self, index: usize, viewport: Rectangle) -> Option<Rectangle> {
        if index >= self.data_source.item_count() {
            return None;
        }

        let mut cursor = self.scroll_view.content_offset_y();
        for current in 0..self.data_source.item_count() {
            let height = self.data_source.item_height(current);
            let frame = Rectangle::new(
                Point::new(viewport.top_left.x, viewport.top_left.y + cursor),
                Size::new(viewport.size.width, height),
            );
            if current == index {
                return Some(frame);
            }
            cursor += height as i32;
        }
        None
    }

    /// Draws the list and its scroll indicator.
    pub fn draw<'text, D>(
        &self,
        display: &mut D,
        viewport: Rectangle,
        env: &ViewEnvironment<'_, 'text>,
    ) where
        D: DrawTarget<Color = Rgb565>,
        Delegate: ListDelegate<'text, DataSource::ItemId>,
    {
        let mut cursor = self.scroll_view.content_offset_y();
        let mut clipped = display.clipped(&viewport);
        for index in 0..self.data_source.item_count() {
            let height = self.data_source.item_height(index);
            let frame = Rectangle::new(
                Point::new(viewport.top_left.x, viewport.top_left.y + cursor),
                Size::new(viewport.size.width, height),
            );
            if rects_intersect(frame, viewport) {
                self.delegate.draw_row(
                    &mut clipped,
                    ListRow {
                        item: ListItem {
                            id: self.data_source.item_id(index),
                            index,
                            frame,
                        },
                        state: ListRowState {
                            selected: self.selected_index() == Some(index),
                            highlighted: self.highlighted_index == Some(index),
                        },
                    },
                    env,
                );
            }
            cursor += height as i32;
        }
        self.scroll_view
            .draw_scroll_bar(display, viewport, self.content_height(), env.theme);
    }
}

fn rects_intersect(left: Rectangle, right: Rectangle) -> bool {
    let left_right = left.top_left.x + left.size.width as i32;
    let left_bottom = left.top_left.y + left.size.height as i32;
    let right_right = right.top_left.x + right.size.width as i32;
    let right_bottom = right.top_left.y + right.size.height as i32;
    left.top_left.x < right_right
        && left_right > right.top_left.x
        && left.top_left.y < right_bottom
        && left_bottom > right.top_left.y
}