fission-core 0.3.0

Core runtime, state, actions, effects, resources, input, and UI model for Fission
Documentation
use anyhow::Result;
use fission_core::action::AppState;
use fission_core::event::{InputEvent, PointerEvent};
use fission_core::{ActionId, NodeId, Runtime};
use fission_ir::semantics::{ActionTrigger, MouseCursor, Role};
use fission_ir::{ActionEntry, ActionSet, Op, Semantics};
use fission_layout::{LayoutNodeGeometry, LayoutPoint, LayoutRect, LayoutSize, LayoutSnapshot};

#[derive(Debug, Default)]
struct HoverState {
    outer_enter: usize,
    outer_exit: usize,
    inner_enter: usize,
    inner_exit: usize,
}

impl AppState for HoverState {}

fn inc_outer_enter(
    state: &mut HoverState,
    _action: &fission_core::ActionEnvelope,
    _target: NodeId,
) -> Result<()> {
    state.outer_enter += 1;
    Ok(())
}

fn inc_outer_exit(
    state: &mut HoverState,
    _action: &fission_core::ActionEnvelope,
    _target: NodeId,
) -> Result<()> {
    state.outer_exit += 1;
    Ok(())
}

fn inc_inner_enter(
    state: &mut HoverState,
    _action: &fission_core::ActionEnvelope,
    _target: NodeId,
) -> Result<()> {
    state.inner_enter += 1;
    Ok(())
}

fn inc_inner_exit(
    state: &mut HoverState,
    _action: &fission_core::ActionEnvelope,
    _target: NodeId,
) -> Result<()> {
    state.inner_exit += 1;
    Ok(())
}

fn hover_action(trigger: ActionTrigger, action_id: ActionId) -> ActionEntry {
    ActionEntry {
        trigger,
        action_id: action_id.as_u128(),
        payload_data: Some(Vec::new()),
    }
}

fn build_hover_ir() -> (fission_ir::CoreIR, LayoutSnapshot) {
    let outer_id = NodeId::explicit("outer");
    let inner_id = NodeId::explicit("inner");

    let outer_enter = ActionId::from_name("tests::hover_outer_enter");
    let outer_exit = ActionId::from_name("tests::hover_outer_exit");
    let inner_enter = ActionId::from_name("tests::hover_inner_enter");
    let inner_exit = ActionId::from_name("tests::hover_inner_exit");

    let inner_semantics = Semantics {
        role: Role::Generic,
        actions: ActionSet {
            entries: vec![
                hover_action(ActionTrigger::HoverEnter, inner_enter),
                hover_action(ActionTrigger::HoverExit, inner_exit),
                ActionEntry::hover_cursor(MouseCursor::Text),
            ],
        },
        ..Semantics::default()
    };
    let outer_semantics = Semantics {
        role: Role::Generic,
        actions: ActionSet {
            entries: vec![
                hover_action(ActionTrigger::HoverEnter, outer_enter),
                hover_action(ActionTrigger::HoverExit, outer_exit),
                ActionEntry::hover_cursor(MouseCursor::Pointer),
            ],
        },
        ..Semantics::default()
    };

    let mut ir = fission_ir::CoreIR::default();
    ir.add_node(inner_id, Op::Semantics(inner_semantics), Vec::new());
    ir.add_node(outer_id, Op::Semantics(outer_semantics), vec![inner_id]);
    ir.set_root(outer_id);

    let mut layout = LayoutSnapshot::new(LayoutSize::new(200.0, 200.0));
    layout.nodes.insert(
        outer_id,
        LayoutNodeGeometry {
            rect: LayoutRect::new(0.0, 0.0, 100.0, 100.0),
            content_size: LayoutSize::new(100.0, 100.0),
        },
    );
    layout.nodes.insert(
        inner_id,
        LayoutNodeGeometry {
            rect: LayoutRect::new(10.0, 10.0, 30.0, 30.0),
            content_size: LayoutSize::new(30.0, 30.0),
        },
    );

    (ir, layout)
}

fn register_hover_reducers(runtime: &mut Runtime) -> Result<()> {
    runtime.add_app_state(Box::new(HoverState::default()))?;
    runtime.register_reducer::<HoverState>(
        ActionId::from_name("tests::hover_outer_enter"),
        inc_outer_enter,
    )?;
    runtime.register_reducer::<HoverState>(
        ActionId::from_name("tests::hover_outer_exit"),
        inc_outer_exit,
    )?;
    runtime.register_reducer::<HoverState>(
        ActionId::from_name("tests::hover_inner_enter"),
        inc_inner_enter,
    )?;
    runtime.register_reducer::<HoverState>(
        ActionId::from_name("tests::hover_inner_exit"),
        inc_inner_exit,
    )?;
    Ok(())
}

fn move_pointer(
    runtime: &mut Runtime,
    ir: &fission_ir::CoreIR,
    layout: &LayoutSnapshot,
    x: f32,
    y: f32,
) {
    runtime
        .handle_input(
            InputEvent::Pointer(PointerEvent::Move {
                point: LayoutPoint::new(x, y),
                modifiers: 0,
            }),
            ir,
            layout,
        )
        .expect("pointer move should succeed");
}

#[test]
fn hover_dispatches_once_per_transition_and_tracks_cursor_target() -> Result<()> {
    let mut runtime = Runtime::default();
    register_hover_reducers(&mut runtime)?;
    let (ir, layout) = build_hover_ir();

    move_pointer(&mut runtime, &ir, &layout, 20.0, 20.0);
    {
        let hover = runtime.get_app_state::<HoverState>().expect("hover state");
        assert_eq!(hover.outer_enter, 1);
        assert_eq!(hover.outer_exit, 0);
        assert_eq!(hover.inner_enter, 1);
        assert_eq!(hover.inner_exit, 0);
    }
    assert_eq!(
        runtime.runtime_state.interaction.cursor(),
        MouseCursor::Text
    );

    move_pointer(&mut runtime, &ir, &layout, 25.0, 25.0);
    {
        let hover = runtime.get_app_state::<HoverState>().expect("hover state");
        assert_eq!(hover.outer_enter, 1);
        assert_eq!(hover.outer_exit, 0);
        assert_eq!(hover.inner_enter, 1);
        assert_eq!(hover.inner_exit, 0);
    }
    assert_eq!(
        runtime.runtime_state.interaction.cursor(),
        MouseCursor::Text
    );

    move_pointer(&mut runtime, &ir, &layout, 80.0, 80.0);
    {
        let hover = runtime.get_app_state::<HoverState>().expect("hover state");
        assert_eq!(hover.outer_enter, 1);
        assert_eq!(hover.outer_exit, 0);
        assert_eq!(hover.inner_enter, 1);
        assert_eq!(hover.inner_exit, 1);
    }
    assert_eq!(
        runtime.runtime_state.interaction.cursor(),
        MouseCursor::Pointer
    );

    move_pointer(&mut runtime, &ir, &layout, 150.0, 150.0);
    {
        let hover = runtime.get_app_state::<HoverState>().expect("hover state");
        assert_eq!(hover.outer_enter, 1);
        assert_eq!(hover.outer_exit, 1);
        assert_eq!(hover.inner_enter, 1);
        assert_eq!(hover.inner_exit, 1);
    }
    assert_eq!(
        runtime.runtime_state.interaction.cursor(),
        MouseCursor::Default
    );

    Ok(())
}

#[test]
fn clear_hover_state_dispatches_exit_once() -> Result<()> {
    let mut runtime = Runtime::default();
    register_hover_reducers(&mut runtime)?;
    let (ir, layout) = build_hover_ir();

    move_pointer(&mut runtime, &ir, &layout, 20.0, 20.0);
    assert!(runtime.clear_hover_state(&ir, Some(LayoutPoint::new(20.0, 20.0)))?);
    assert!(!runtime.clear_hover_state(&ir, Some(LayoutPoint::new(20.0, 20.0)))?);

    let hover = runtime.get_app_state::<HoverState>().expect("hover state");
    assert_eq!(hover.outer_enter, 1);
    assert_eq!(hover.outer_exit, 1);
    assert_eq!(hover.inner_enter, 1);
    assert_eq!(hover.inner_exit, 1);
    assert_eq!(
        runtime.runtime_state.interaction.cursor(),
        MouseCursor::Default
    );

    Ok(())
}