fission-core 0.14.1

Core runtime, state, actions, effects, resources, input, and UI model for Fission
Documentation
use fission_core::event::{InputEvent, PointerButton, PointerEvent};
use fission_core::{ActionEnvelope, ActionId, GlobalState, Runtime};
use fission_ir::semantics::ActionTrigger;
use fission_ir::{
    ActionEntry, ActionSet, CompositeStyle, CoreIR, CoreNode, Op, Role, Semantics, WidgetId,
};
use fission_layout::{LayoutNodeGeometry, LayoutPoint, LayoutRect, LayoutSize, LayoutSnapshot};

#[derive(Debug, Default)]
struct ClickState {
    primary: usize,
    secondary: usize,
}

impl GlobalState for ClickState {}

#[test]
fn primary_click_dispatches_default_action_only() -> anyhow::Result<()> {
    let (mut runtime, ir, layout, _) = click_runtime(true)?;

    click(
        &mut runtime,
        &ir,
        &layout,
        PointerButton::Primary,
        PointerButton::Primary,
    )?;

    let state = runtime.get_app_state::<ClickState>().expect("click state");
    assert_eq!(state.primary, 1);
    assert_eq!(state.secondary, 0);
    Ok(())
}

#[test]
fn secondary_click_never_falls_through_to_default_action() -> anyhow::Result<()> {
    let (mut runtime, ir, layout, node_id) = click_runtime(true)?;

    click(
        &mut runtime,
        &ir,
        &layout,
        PointerButton::Secondary,
        PointerButton::Secondary,
    )?;

    let state = runtime.get_app_state::<ClickState>().expect("click state");
    assert_eq!(state.primary, 0);
    assert_eq!(state.secondary, 1);
    assert_eq!(runtime.runtime_state.interaction.focused, None);
    assert!(!runtime.runtime_state.interaction.is_pressed(node_id));
    Ok(())
}

#[test]
fn secondary_click_without_secondary_action_does_not_activate_default() -> anyhow::Result<()> {
    let (mut runtime, ir, layout, _) = click_runtime(false)?;

    click(
        &mut runtime,
        &ir,
        &layout,
        PointerButton::Secondary,
        PointerButton::Secondary,
    )?;

    let state = runtime.get_app_state::<ClickState>().expect("click state");
    assert_eq!(state.primary, 0);
    assert_eq!(state.secondary, 0);
    Ok(())
}

#[test]
fn mismatched_pointer_buttons_cancel_activation() -> anyhow::Result<()> {
    let (mut runtime, ir, layout, _) = click_runtime(true)?;

    click(
        &mut runtime,
        &ir,
        &layout,
        PointerButton::Secondary,
        PointerButton::Primary,
    )?;

    let state = runtime.get_app_state::<ClickState>().expect("click state");
    assert_eq!(state.primary, 0);
    assert_eq!(state.secondary, 0);
    Ok(())
}

#[test]
fn primary_release_without_primary_press_does_not_activate_default() -> anyhow::Result<()> {
    let (mut runtime, ir, layout, _) = click_runtime(true)?;
    let point = LayoutPoint::new(20.0, 20.0);

    runtime.handle_input(
        InputEvent::Pointer(PointerEvent::Up {
            pointer_id: Default::default(),
            kind: Default::default(),
            point,
            button: PointerButton::Primary,
            modifiers: 0,
        }),
        &ir,
        &layout,
    )?;

    let state = runtime.get_app_state::<ClickState>().expect("click state");
    assert_eq!(state.primary, 0);
    assert_eq!(state.secondary, 0);
    Ok(())
}

#[test]
fn semantic_context_menu_request_dispatches_secondary_only() -> anyhow::Result<()> {
    let (mut runtime, ir, layout, _) = click_runtime(true)?;

    runtime.handle_input(
        InputEvent::ContextMenuRequested {
            point: LayoutPoint::new(20.0, 20.0),
            modifiers: 0,
        },
        &ir,
        &layout,
    )?;

    let state = runtime.get_app_state::<ClickState>().expect("click state");
    assert_eq!(state.primary, 0);
    assert_eq!(state.secondary, 1);
    Ok(())
}

fn click_runtime(
    with_secondary_action: bool,
) -> anyhow::Result<(Runtime, CoreIR, LayoutSnapshot, WidgetId)> {
    let node_id = WidgetId::explicit("button");
    let primary_id = ActionId::from_name("pointer_button_test::Primary");
    let secondary_id = ActionId::from_name("pointer_button_test::Secondary");
    let mut entries = vec![action_entry(ActionTrigger::Default, primary_id)];
    if with_secondary_action {
        entries.push(action_entry(ActionTrigger::SecondaryClick, secondary_id));
    }

    let mut ir = CoreIR::default();
    ir.root = Some(node_id);
    ir.nodes.insert(
        node_id,
        CoreNode {
            id: node_id,
            op: Op::Semantics(Semantics {
                role: Role::Button,
                focusable: true,
                actions: ActionSet { entries },
                ..Default::default()
            }),
            composite: CompositeStyle::default(),
            children: Vec::new(),
            parent: None,
            hash: 0,
        },
    );

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

    let mut runtime = Runtime::default();
    runtime.add_app_state(Box::new(ClickState::default()))?;
    runtime.register_reducer::<ClickState>(primary_id, record_primary)?;
    runtime.register_reducer::<ClickState>(secondary_id, record_secondary)?;
    Ok((runtime, ir, layout, node_id))
}

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

fn click(
    runtime: &mut Runtime,
    ir: &CoreIR,
    layout: &LayoutSnapshot,
    down_button: PointerButton,
    up_button: PointerButton,
) -> anyhow::Result<()> {
    let point = LayoutPoint::new(20.0, 20.0);
    runtime.handle_input(
        InputEvent::Pointer(PointerEvent::Down {
            pointer_id: Default::default(),
            kind: Default::default(),
            point,
            button: down_button,
            modifiers: 0,
        }),
        ir,
        layout,
    )?;
    runtime.handle_input(
        InputEvent::Pointer(PointerEvent::Up {
            pointer_id: Default::default(),
            kind: Default::default(),
            point,
            button: up_button,
            modifiers: 0,
        }),
        ir,
        layout,
    )
}

fn record_primary(
    state: &mut ClickState,
    _action: &ActionEnvelope,
    _target: WidgetId,
) -> anyhow::Result<()> {
    state.primary += 1;
    Ok(())
}

fn record_secondary(
    state: &mut ClickState,
    _action: &ActionEnvelope,
    _target: WidgetId,
) -> anyhow::Result<()> {
    state.secondary += 1;
    Ok(())
}