faststep 0.1.0

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

use faststep::{
    ChildView, FsTheme, I18n, ListActivity, ListDataSource, ListDelegate, ListRow, ListSelection,
    ListView, Localized, SplitAxis, SplitView, TouchEvent, TouchPhase, ViewEnvironment, ViewKind,
    ViewRedraw, ViewRegistration,
};

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum DemoViewId {
    List,
}

#[test]
fn registration_tracks_view_properties_and_children() {
    let mut registration =
        ViewRegistration::<DemoViewId, 2>::new(Rectangle::new(Point::zero(), Size::new(320, 240)));
    registration.set_title(Localized::new("root.title", "Root"));
    registration.set_alpha(220);
    registration.set_hidden(false);
    registration.set_clips_to_bounds(true);
    let _ = registration.add_child(
        ChildView::new(
            DemoViewId::List,
            Rectangle::new(Point::new(0, 0), Size::new(320, 240)),
        )
        .with_kind(ViewKind::List)
        .with_title(Localized::new("devices.title", "Devices")),
    );

    assert_eq!(
        registration.properties().title.map(|title| title.fallback),
        Some("Root")
    );
    assert_eq!(registration.properties().alpha, 220);
    assert!(registration.properties().clips_to_bounds);
    assert_eq!(registration.children().len(), 1);
    assert_eq!(registration.children()[0].kind, ViewKind::List);
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum DemoMessage {
    Selected(u8),
}

struct DemoDataSource;

impl ListDataSource for DemoDataSource {
    type ItemId = u8;

    fn item_count(&self) -> usize {
        3
    }

    fn item_id(&self, index: usize) -> Self::ItemId {
        index as u8
    }

    fn item_height(&self, index: usize) -> u32 {
        match index {
            0 => 40,
            1 => 56,
            _ => 48,
        }
    }
}

struct DemoDelegate;

impl ListDelegate<'static, u8> for DemoDelegate {
    type Message = DemoMessage;

    fn draw_row<D>(&self, _display: &mut D, _row: ListRow<u8>, _env: &ViewEnvironment<'_, 'static>)
    where
        D: embedded_graphics::draw_target::DrawTarget<
                Color = embedded_graphics::pixelcolor::Rgb565,
            >,
    {
    }

    fn did_select_item(&mut self, selection: ListSelection<u8>) -> Option<Self::Message> {
        Some(DemoMessage::Selected(selection.id))
    }
}

#[test]
fn list_view_uses_datasource_and_delegate_for_selection() {
    let viewport = Rectangle::new(Point::zero(), Size::new(320, 120));
    let mut list = ListView::new(DemoDataSource, DemoDelegate);

    let start = list.handle_touch(
        TouchEvent::new(Point::new(12, 12), TouchPhase::Start, 1),
        viewport,
    );
    assert!(start.captured);
    assert_eq!(start.activity, ListActivity::Interactive);
    assert_eq!(start.redraw, ViewRedraw::Dirty(viewport));

    let end = list.handle_touch(
        TouchEvent::new(Point::new(12, 12), TouchPhase::End, 2),
        viewport,
    );
    assert_eq!(end.message, Some(DemoMessage::Selected(0)));
    assert_eq!(
        list.selected_item(),
        Some(ListSelection { id: 0, index: 0 })
    );
}

#[test]
fn list_view_tick_marks_viewport_dirty_when_animating() {
    let viewport = Rectangle::new(Point::zero(), Size::new(320, 120));
    let mut list = ListView::new(DemoDataSource, DemoDelegate);

    let _ = list.handle_touch(
        TouchEvent::new(Point::new(16, 80), TouchPhase::Start, 1),
        viewport,
    );
    let _ = list.handle_touch(
        TouchEvent::new(Point::new(16, 20), TouchPhase::Move, 2),
        viewport,
    );
    let _ = list.handle_touch(
        TouchEvent::new(Point::new(16, 20), TouchPhase::End, 3),
        viewport,
    );

    assert_eq!(list.tick(16, viewport), ViewRedraw::Dirty(viewport));
    assert!(list.scroll_bar(viewport).is_some());
    let _ = FsTheme::default();
    let _ = I18n::new("en", "en", &[]);
}

#[test]
fn list_view_enters_motion_mode_as_soon_as_drag_slop_is_crossed() {
    let viewport = Rectangle::new(Point::zero(), Size::new(320, 120));
    let mut list = ListView::new(DemoDataSource, DemoDelegate);

    let start = list.handle_touch(
        TouchEvent::new(Point::new(16, 16), TouchPhase::Start, 1),
        viewport,
    );
    assert_eq!(start.activity, ListActivity::Interactive);

    let move_event = list.handle_touch(
        TouchEvent::new(Point::new(16, 24), TouchPhase::Move, 2),
        viewport,
    );
    assert_eq!(move_event.activity, ListActivity::Motion);
    assert_eq!(move_event.redraw, ViewRedraw::Dirty(viewport));
}

#[test]
fn split_view_partitions_frame() {
    let split = SplitView::new(SplitAxis::Horizontal, 625, 12);
    let layout = split.layout(Rectangle::new(Point::zero(), Size::new(320, 120)));

    assert_eq!(layout.primary.size.height, 120);
    assert_eq!(layout.secondary.size.height, 120);
    assert_eq!(
        layout.primary.size.width + layout.secondary.size.width + 12,
        320
    );
    assert_eq!(
        layout.secondary.top_left.x,
        layout.primary.top_left.x + layout.primary.size.width as i32 + 12
    );
}

#[test]
fn theme_can_be_configured_at_startup() {
    let theme = FsTheme::default()
        .with_accent(
            embedded_graphics::pixelcolor::Rgb565::new(3, 18, 28),
            embedded_graphics::pixelcolor::Rgb565::new(31, 63, 31),
        )
        .with_status_colors(
            embedded_graphics::pixelcolor::Rgb565::new(6, 38, 12),
            embedded_graphics::pixelcolor::Rgb565::new(31, 45, 4),
            embedded_graphics::pixelcolor::Rgb565::new(27, 12, 10),
            embedded_graphics::pixelcolor::Rgb565::new(31, 63, 31),
        );

    assert_eq!(
        theme.accent,
        embedded_graphics::pixelcolor::Rgb565::new(3, 18, 28)
    );
    assert_eq!(
        theme.text_on_accent,
        embedded_graphics::pixelcolor::Rgb565::new(31, 63, 31)
    );
    assert_eq!(
        theme.danger,
        embedded_graphics::pixelcolor::Rgb565::new(27, 12, 10)
    );
    assert_eq!(
        theme.text_on_danger,
        embedded_graphics::pixelcolor::Rgb565::new(31, 63, 31)
    );
}