fission-shell-winit 0.4.0

Shared winit shell runtime for desktop and mobile Fission hosts
Documentation
use anyhow::Result;
use fission_core::action::GlobalState as CoreGlobalState;
use fission_core::env::Env;
use fission_core::internal::BuildCtx;
use fission_core::internal::InternalLoweringCx;
use fission_core::ui::{TextInput, Widget};
use fission_core::{reduce_with, ReducerContext, Runtime, View, WidgetIdExt};
use fission_layout::{LayoutEngine, LayoutSize, LineMetric, TextMeasurer};
use fission_render::{DisplayList, RenderScene, Renderer};
use fission_widgets::{Checkbox, Portal};
use std::sync::{Arc, Mutex};

use fission_shell_winit::Pipeline;

#[derive(Debug, Default, Clone)]
struct GlobalState {
    text: String,
    checked: bool,
    show_portal: bool,
}
impl CoreGlobalState for GlobalState {}

#[fission_macros::fission_action]
struct Toggle;
fn on_toggle(state: &mut GlobalState, _a: Toggle, _ctx: &mut ReducerContext<GlobalState>) {
    state.checked = !state.checked;
}

#[fission_macros::fission_action]
struct UpdateText(String);
fn on_update(state: &mut GlobalState, a: UpdateText, _ctx: &mut ReducerContext<GlobalState>) {
    state.text = a.0;
}

#[derive(Clone, Default)]
struct MockRenderer(pub Arc<Mutex<Option<DisplayList>>>);
impl Renderer for MockRenderer {
    fn render_scene(&mut self, scene: &RenderScene) -> Result<()> {
        *self.0.lock().unwrap() = Some(scene.flatten());
        Ok(())
    }
}

struct MockMeasurer;
impl TextMeasurer for MockMeasurer {
    fn measure(&self, _text: &str, _font_size: f32, _available_width: Option<f32>) -> (f32, f32) {
        (0.0, 0.0)
    }
    fn hit_test(
        &self,
        _text: &str,
        _font_size: f32,
        _available_width: Option<f32>,
        _x: f32,
        _y: f32,
    ) -> usize {
        0
    }
    fn get_line_metrics(
        &self,
        _text: &str,
        _font_size: f32,
        _available_width: Option<f32>,
    ) -> Vec<LineMetric> {
        vec![]
    }
}

#[derive(Clone)]
struct Root;
impl From<Root> for Widget {
    fn from(_component: Root) -> Self {
        let (ctx, view) = fission_core::build::current::<GlobalState>();
        use fission_core::ui::{Column, Row};
        let mut children: Vec<Widget> = vec![
            Checkbox {
                checked: view.state().checked,
                on_toggle: Some(ctx.bind(Toggle, reduce_with!(on_toggle))),
                label: Some("check".into()),
                ..Default::default()
            }
            .into(),
            TextInput {
                value: view.state().text.clone(),
                placeholder: Some("type".into()),
                on_change: Some(ctx.bind(UpdateText("".into()), reduce_with!(on_update))),
                width: Some(200.0),
                height: Some(40.0),
                ..Default::default()
            }
            .into(),
        ];
        if view.state().show_portal {
            use fission_core::ui::{Overlay, ZStack};
            let overlay = Overlay {
                id: None,
                content: Row::default().into(),
                overlay: ZStack {
                    id: None,
                    children: vec![
                        // Nested portal to test recursion
                        Portal {
                            child: Row::default().into(),
                        }
                        .into(),
                    ],
                    ..Default::default()
                }
                .into(),
            };
            children.push(
                Portal {
                    child: overlay.into(),
                }
                .into(),
            );
        }
        // Add more nodes to ensure structure
        children.push(Row::default().into());

        Column {
            children,
            ..Default::default()
        }
        .into()
    }
}
fn pump<W>(
    runtime: &mut Runtime,
    layout: &mut LayoutEngine,
    pipe: &mut Pipeline,
    env: &Env,
    root: &W,
) -> (fission_ir::CoreIR, fission_layout::LayoutSnapshot)
where
    W: Clone + Into<Widget>,
{
    // Build + wrap portals like desktop
    let node_tree = {
        let state = runtime.get_global_state::<GlobalState>().unwrap();
        let view = View::new(
            state,
            &runtime.runtime_state,
            env,
            pipe.last_snapshot.as_ref(),
        );
        let mut ctx = BuildCtx::new();
        let mut tree = fission_core::build::enter(&mut ctx, &view, || (*root).clone().into());
        runtime.clear_reducers();
        let anim = ctx.take_animation_requests();
        for (t, r) in anim {
            runtime.enqueue_animation(t, r);
        }
        let vids = ctx.take_video_registrations();
        runtime.sync_video_nodes(&vids);
        let portals_with_ids = ctx.take_portals();
        let portals = portals_with_ids
            .into_iter()
            .map(|(id, node)| {
                if let Some(id) = id {
                    fission_core::ui::Container::new(node).id(id).into()
                } else {
                    node
                }
            })
            .collect::<Vec<_>>();
        if !portals.is_empty() {
            use fission_core::ui::{Overlay, Row, ZStack};
            let mut children = Vec::with_capacity(1 + portals.len());
            children.push(tree);
            for p in portals {
                children.push(
                    Overlay {
                        id: None,
                        content: Row::default().into(),
                        overlay: p,
                    }
                    .into(),
                );
            }
            tree = ZStack {
                id: None,
                children,
                ..Default::default()
            }
            .into();
        }
        tree
    };
    // InternalLower
    let mut cx = InternalLoweringCx::new(
        env,
        &runtime.runtime_state,
        None,
        pipe.last_snapshot.as_ref(),
    );
    let root_id = fission_core::internal::lower_widget(&node_tree, &mut cx);
    cx.ir.root = Some(root_id);
    let ir = cx.ir;
    // Render via pipeline (performing layout inside)
    let viewport = LayoutSize {
        width: 800.0,
        height: 600.0,
    };
    let mut renderer = MockRenderer::default();
    let env = Env::default();
    let _ = pipe
        .render(
            ir.clone(),
            viewport,
            layout,
            &runtime.runtime_state.scroll,
            &mut renderer,
            &runtime.runtime_state.video,
            &runtime.runtime_state.web,
            &env,
        )
        .expect("render ok");
    let snap = pipe.last_snapshot.clone().expect("snapshot");
    (ir, snap)
}

#[test]
fn test_composition_cycles_and_portals() -> Result<()> {
    // This test ensures that portal collection and wrapping doesn't cause infinite rebuild cycles
    // or ID collisions if done repeatedly.
    let env = Env::default();
    let mut runtime = Runtime::default();
    runtime.add_global_state(Box::new(GlobalState::default()))?;
    let mut layout = LayoutEngine::new().with_measurer(Arc::new(MockMeasurer));
    let mut pipe = Pipeline::new();
    let root = Root;

    // Pump frame 1
    let (ir1, _snap1) = pump(&mut runtime, &mut layout, &mut pipe, &env, &root);

    // Toggle portal ON
    {
        let state = runtime.get_global_state_mut::<GlobalState>().unwrap();
        state.show_portal = true;
    }

    let (ir2, _snap2) = pump(&mut runtime, &mut layout, &mut pipe, &env, &root);

    // Toggle portal OFF
    {
        let state = runtime.get_global_state_mut::<GlobalState>().unwrap();
        state.show_portal = false;
    }

    let (ir3, _snap3) = pump(&mut runtime, &mut layout, &mut pipe, &env, &root);

    // Verify structural changes
    assert_ne!(ir1.nodes.len(), ir2.nodes.len());
    assert_eq!(ir1.nodes.len(), ir3.nodes.len()); // Should return to base count (roughly)

    Ok(())
}