faststep 0.1.0

UIKit-inspired embedded UI framework built on embedded-graphics
Documentation
mod indicator;

use embedded_graphics::{draw_target::DrawTarget, pixelcolor::Rgb565, primitives::Rectangle};

use crate::{FsTheme, ScrollViewState, TouchEvent};

pub use indicator::ScrollBar;
use indicator::{draw_scrollbar, motion_content_rect, scroll_bar_dirty_rect, scroll_bar_thumb};

const SCROLLBAR_REVEAL_ALPHA: u8 = 210;
const SCROLLBAR_FADE_HOLD_MS: u32 = 420;
const SCROLLBAR_FADE_PER_MS: u16 = 2;

/// Scroll container state with UIKit-style indicator behavior.
#[derive(Clone, Copy, Debug, Default)]
pub struct ScrollView {
    state: ScrollViewState,
    shows_vertical_scroll_indicator: bool,
    indicator_alpha: u8,
    indicator_hold_ms: u32,
}

impl ScrollView {
    /// Creates a scroll view with an enabled vertical indicator.
    pub const fn new() -> Self {
        Self {
            state: ScrollViewState::new(),
            shows_vertical_scroll_indicator: true,
            indicator_alpha: 0,
            indicator_hold_ms: 0,
        }
    }

    /// Returns the underlying scroll state.
    pub const fn state(&self) -> &ScrollViewState {
        &self.state
    }

    /// Returns the underlying scroll state mutably.
    pub fn state_mut(&mut self) -> &mut ScrollViewState {
        &mut self.state
    }

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

    /// Enables or disables the vertical indicator.
    pub fn set_shows_vertical_scroll_indicator(&mut self, shows: bool) {
        self.shows_vertical_scroll_indicator = shows;
        if !shows {
            self.indicator_alpha = 0;
            self.indicator_hold_ms = 0;
        }
    }

    /// Returns whether a touch drag is active.
    pub const fn is_dragging(&self) -> bool {
        self.state.is_dragging()
    }

    /// Returns whether the view is actively scrolling.
    pub const fn is_scrolling(&self) -> bool {
        self.state.is_scrolling()
    }

    /// Returns the integer content offset.
    pub fn content_offset_y(&self) -> i32 {
        self.state.content_offset_y()
    }

    /// Starts a drag sequence.
    pub fn begin_drag(&mut self, touch: TouchEvent) {
        self.state.begin_drag(touch);
    }

    /// Updates dragging with a new touch sample.
    pub fn drag(&mut self, touch: TouchEvent, content_height: u32, viewport_height: u32) -> bool {
        let changed = self.state.drag(touch, content_height, viewport_height);
        if changed || self.state.is_scrolling() {
            self.reveal_indicator();
        }
        changed
    }

    /// Ends dragging and starts any fling animation.
    pub fn end_drag(
        &mut self,
        touch: TouchEvent,
        content_height: u32,
        viewport_height: u32,
    ) -> bool {
        let changed = self.state.end_drag(touch, content_height, viewport_height);
        if changed || self.state.is_animating(content_height, viewport_height) {
            self.reveal_indicator();
        }
        changed
    }

    /// Advances animation and indicator fade state.
    pub fn tick(&mut self, dt_ms: u32, content_height: u32, viewport_height: u32) -> bool {
        let scrolled = self.state.tick(dt_ms, content_height, viewport_height);
        if !self.shows_vertical_scroll_indicator || content_height <= viewport_height {
            let indicator_changed = self.indicator_alpha != 0;
            self.indicator_alpha = 0;
            self.indicator_hold_ms = 0;
            return scrolled || indicator_changed;
        }

        let was_alpha = self.indicator_alpha;
        if self.state.is_scrolling() || self.state.is_animating(content_height, viewport_height) {
            self.reveal_indicator();
        } else if self.indicator_hold_ms > 0 {
            self.indicator_hold_ms = self.indicator_hold_ms.saturating_sub(dt_ms);
        } else if self.indicator_alpha > 0 {
            self.indicator_alpha = self.indicator_alpha.saturating_sub(
                (dt_ms.saturating_mul(u32::from(SCROLLBAR_FADE_PER_MS))).min(255) as u8,
            );
        }
        scrolled || was_alpha != self.indicator_alpha
    }

    /// Returns the current scrollbar thumb geometry, if visible.
    pub fn scroll_bar(&self, viewport: Rectangle, content_height: u32) -> Option<ScrollBar> {
        if !self.shows_vertical_scroll_indicator
            || self.indicator_alpha == 0
            || content_height <= viewport.size.height
        {
            return None;
        }

        scroll_bar_thumb(
            viewport,
            content_height,
            self.state.content_offset(),
            self.indicator_alpha,
        )
    }

    /// Returns the viewport area that may be affected by scrollbar updates.
    pub fn scroll_bar_dirty(&self, viewport: Rectangle, content_height: u32) -> Option<Rectangle> {
        if !self.shows_vertical_scroll_indicator || content_height <= viewport.size.height {
            return None;
        }
        Some(scroll_bar_dirty_rect(viewport))
    }

    /// Returns the motion content rect, excluding reserved indicator gutter.
    pub fn motion_content_rect(&self, viewport: Rectangle, content_height: u32) -> Rectangle {
        if !self.shows_vertical_scroll_indicator || content_height <= viewport.size.height {
            return viewport;
        }
        motion_content_rect(viewport)
    }

    /// Draws the vertical scrollbar thumb.
    pub fn draw_scroll_bar<D>(
        &self,
        display: &mut D,
        viewport: Rectangle,
        content_height: u32,
        theme: &FsTheme,
    ) where
        D: DrawTarget<Color = Rgb565>,
    {
        if let Some(indicator) = self.scroll_bar(viewport, content_height) {
            draw_scrollbar(display, indicator, theme);
        }
    }

    fn reveal_indicator(&mut self) {
        self.indicator_alpha = SCROLLBAR_REVEAL_ALPHA;
        self.indicator_hold_ms = SCROLLBAR_FADE_HOLD_MS;
    }
}