faststep 0.1.0

UIKit-inspired embedded UI framework built on embedded-graphics
Documentation
use embedded_graphics::{prelude::Point, primitives::Rectangle};

use crate::{TouchEvent, TouchPhase, ViewRedraw};

use super::{ListActivity, ListDataSource, ListDelegate, ListEvent, ListSelection, ListView};

impl<DataSource, Delegate> ListView<DataSource, Delegate>
where
    DataSource: ListDataSource,
{
    /// Routes one touch event to the list.
    pub fn handle_touch<'text>(
        &mut self,
        touch: TouchEvent,
        viewport: Rectangle,
    ) -> ListEvent<Delegate::Message>
    where
        Delegate: ListDelegate<'text, DataSource::ItemId>,
    {
        if !touch.within(viewport) && !self.scroll_view.is_dragging() {
            return ListEvent::none();
        }

        let local_touch = offset_touch(touch, viewport.top_left);
        let touched_index = self.item_index_at_point(touch.point, viewport);
        let was_dragging = self.scroll_view.is_dragging();
        let previous_highlight = self.highlighted_index;
        let previous_selection = self.selected_index;
        let mut activity = ListActivity::None;
        let mut message = None;

        let scrolled = match touch.phase {
            TouchPhase::Start => {
                self.touch_started_inside = touch.within(viewport);
                self.tap_candidate = self.touch_started_inside;
                self.scroll_view.begin_drag(local_touch);
                message = message.or(
                    self.set_highlighted_index(touched_index.filter(|_| self.touch_started_inside))
                );
                false
            }
            TouchPhase::Move => {
                let changed =
                    self.scroll_view
                        .drag(local_touch, self.content_height(), viewport.size.height);
                if changed || self.scroll_view.is_scrolling() {
                    self.tap_candidate = false;
                    message = message.or(self.set_highlighted_index(None));
                } else {
                    let next_highlight = if self.touch_started_inside && touch.within(viewport) {
                        touched_index
                    } else {
                        None
                    };
                    message = message.or(self.set_highlighted_index(next_highlight));
                }
                changed
            }
            TouchPhase::End | TouchPhase::Cancel => {
                let changed = self.scroll_view.end_drag(
                    local_touch,
                    self.content_height(),
                    viewport.size.height,
                );

                if matches!(touch.phase, TouchPhase::End)
                    && self.touch_started_inside
                    && self.tap_candidate
                    && touch.within(viewport)
                {
                    if let Some(index) = touched_index {
                        if self.allows_selection {
                            self.selected_index = Some(index);
                        }
                        if let Some(selection) = self.selection_for_index(index) {
                            message = message.or(self.delegate.did_select_item(selection));
                        }
                    }
                }

                message = message.or(self.set_highlighted_index(None));
                self.touch_started_inside = false;
                self.tap_candidate = false;
                changed
            }
        };

        let visual_state_changed = previous_highlight != self.highlighted_index
            || previous_selection != self.selected_index;
        let motion_active = scrolled || self.scroll_view.is_scrolling();

        if motion_active {
            activity = ListActivity::Motion;
        } else if visual_state_changed {
            activity = ListActivity::Interactive;
        }

        ListEvent {
            redraw: match activity {
                ListActivity::None => ViewRedraw::None,
                ListActivity::Interactive | ListActivity::Motion => ViewRedraw::Dirty(viewport),
            },
            captured: was_dragging || touch.within(viewport) || self.touch_started_inside,
            message,
            activity,
        }
    }

    /// Returns the selected item at a given point, if any.
    pub fn item_at_point(
        &self,
        point: Point,
        viewport: Rectangle,
    ) -> Option<ListSelection<DataSource::ItemId>> {
        self.selection_for_index(self.item_index_at_point(point, viewport)?)
    }

    fn item_index_at_point(&self, point: Point, viewport: Rectangle) -> Option<usize> {
        if !point_in_rect(point, viewport) {
            return None;
        }

        let mut cursor = self.scroll_view.content_offset_y();
        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),
                embedded_graphics::prelude::Size::new(viewport.size.width, height),
            );
            if point_in_rect(point, frame) {
                return Some(index);
            }
            cursor += height as i32;
        }
        None
    }

    pub(super) fn selection_for_index(
        &self,
        index: usize,
    ) -> Option<ListSelection<DataSource::ItemId>> {
        (index < self.data_source.item_count()).then_some(ListSelection {
            id: self.data_source.item_id(index),
            index,
        })
    }

    fn set_highlighted_index<'text>(&mut self, next: Option<usize>) -> Option<Delegate::Message>
    where
        Delegate: ListDelegate<'text, DataSource::ItemId>,
    {
        if self.highlighted_index == next {
            return None;
        }

        let previous = self.highlighted_index.take();
        self.highlighted_index = next.filter(|index| *index < self.data_source.item_count());

        let mut message = None;
        if let Some(index) = previous
            && let Some(selection) = self.selection_for_index(index)
        {
            message = message.or(self.delegate.did_highlight_item(selection, false));
        }
        if let Some(index) = self.highlighted_index
            && let Some(selection) = self.selection_for_index(index)
        {
            message = message.or(self.delegate.did_highlight_item(selection, true));
        }
        message
    }
}

fn offset_touch(touch: TouchEvent, top_left: Point) -> TouchEvent {
    TouchEvent::new(
        Point::new(touch.point.x - top_left.x, touch.point.y - top_left.y),
        touch.phase,
        touch.timestamp_ms,
    )
}

fn point_in_rect(point: Point, rect: Rectangle) -> bool {
    let right = rect.top_left.x + rect.size.width as i32;
    let bottom = rect.top_left.y + rect.size.height as i32;
    point.x >= rect.top_left.x && point.x < right && point.y >= rect.top_left.y && point.y < bottom
}