dioxus-flow 0.1.3

A react-flow-like node graph component library for Dioxus
Documentation
use super::*;
use crate::{use_flow_handle, Canvas, Flow, FlowHandle, Rect, Viewport};
use std::cell::Cell;
use std::future::Future;
use std::io::Write;
use std::rc::Rc;
use std::sync::{Arc, Mutex};
use std::task::{Context, Waker};
use std::time::{Duration, Instant};

#[derive(Clone)]
struct FixtureState {
    visible: Signal<bool>,
    measure: Signal<bool>,
    nodes: Signal<Vec<Node>>,
    handle: FlowHandle,
    core: Rc<Cell<Option<FlowCore>>>,
}

#[component]
fn Fixture(standalone: bool) -> Element {
    let visible = use_signal(|| true);
    let measure = use_signal(|| true);
    let nodes = use_signal(|| vec![Node::new("one", "One", (0.0, 0.0))]);
    let edges = use_signal(Vec::new);
    let handle = use_flow_handle();
    use_context_provider(|| FixtureState {
        visible,
        measure,
        nodes,
        handle,
        core: Rc::new(Cell::new(None)),
    });
    rsx! {
        if visible() {
            if standalone {
                Canvas {
                    world: rsx! {
                        for node in nodes() {
                            NodeItem { key: "{node.id}", nodes, node }
                        }
                    },
                    CaptureCore {}
                }
            } else {
                Flow { nodes, edges, handle, fit_view: true,
                    CaptureCore {}
                }
            }
        }
    }
}

#[component]
fn CaptureCore() -> Element {
    let state = use_context::<FixtureState>();
    let core = use_context::<FlowCore>();
    use_hook(|| state.core.set(Some(core)));
    use_effect(move || {
        core.container
            .clone()
            .set(Rect::new(0.0, 0.0, 800.0, 600.0))
    });
    rsx! {
        if (state.measure)() {
            MeasureThenUnmount {}
        }
    }
}

#[component]
fn MeasureThenUnmount() -> Element {
    let state = use_context::<FixtureState>();
    let core = use_context::<FlowCore>();
    use_effect(move || {
        store_measured(core, state.nodes, &"one".into(), Size::new(240.0, 120.0));
        // The measurement flush must survive the component that queued it.
        state.measure.clone().set(false);
    });
    rsx! {}
}

struct Harness {
    dom: VirtualDom,
    state: FixtureState,
}

impl Harness {
    fn new(standalone: bool) -> Self {
        let mut dom = VirtualDom::new_with_props(Fixture, FixtureProps { standalone });
        dom.rebuild_in_place();
        let state = dom.in_scope(ScopeId::APP, consume_context::<FixtureState>);
        Self { dom, state }
    }

    fn core(&self) -> FlowCore {
        self.state.core.get().unwrap()
    }

    fn in_canvas<R>(&self, action: impl FnOnce(FlowCore) -> R) -> R {
        let core = self.core();
        self.dom
            .in_scope(core.viewport.origin_scope(), || action(core))
    }

    fn settle(&mut self) {
        let deadline = Instant::now() + Duration::from_secs(2);
        loop {
            let idle = std::pin::pin!(self.dom.wait_for_work())
                .as_mut()
                .poll(&mut Context::from_waker(Waker::noop()))
                .is_pending();
            self.dom.render_immediate(&mut dioxus::core::NoOpMutations);
            let ready = self.in_canvas(|core| {
                !*core.size_flush_queued.peek() && !*core.handle_flush_queued.peek()
            });
            if idle && ready {
                break;
            }
            assert!(Instant::now() < deadline, "canvas flush did not complete");
            std::thread::sleep(Duration::from_millis(1));
        }
        self.dom.render_immediate(&mut dioxus::core::NoOpMutations);
    }

    fn set_visible(&mut self, visible: bool) {
        self.dom
            .in_scope(ScopeId::APP, || self.state.visible.set(visible));
        self.dom.render_immediate(&mut dioxus::core::NoOpMutations);
    }
}

#[derive(Clone, Default)]
struct WarningLog(Arc<Mutex<Vec<u8>>>);

impl Write for WarningLog {
    fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
        self.0.lock().unwrap().extend_from_slice(bytes);
        Ok(bytes.len())
    }

    fn flush(&mut self) -> std::io::Result<()> {
        Ok(())
    }
}

impl WarningLog {
    fn capture(&self) -> tracing::subscriber::DefaultGuard {
        let writer = self.clone();
        tracing::subscriber::set_default(
            tracing_subscriber::fmt()
                .without_time()
                .with_ansi(false)
                .with_max_level(tracing::Level::WARN)
                .with_writer(move || writer.clone())
                .finish(),
        )
    }

    fn assert_clean(&self) {
        let output = String::from_utf8(self.0.lock().unwrap().clone()).unwrap();
        assert!(!output.contains("A Copy Value created"), "{output}");
        assert!(!output.contains("Changing the props of `Style"), "{output}");
    }
}

#[test]
fn canvas_and_flow_flush_after_children_unmount_without_scope_warnings() {
    let log = WarningLog::default();
    let _guard = log.capture();
    for standalone in [true, false] {
        let mut harness = Harness::new(standalone);
        harness.settle();
        harness.dom.in_scope(ScopeId::APP, || {
            assert!(!*harness.state.measure.peek());
            assert_eq!(
                harness.state.nodes.peek()[0].measured,
                Some(Size::new(240.0, 120.0))
            );
        });
        harness.in_canvas(|core| {
            assert_eq!(core.handles.peek().len(), 2);
            // This rerenders the canvas and used to update document::Style's props.
            core.interaction.clone().set(Interaction::Pan);
        });
        harness.settle();
        harness
            .dom
            .in_scope(ScopeId::APP, || harness.state.nodes.clear());
        harness.settle();
        harness.in_canvas(|core| assert!(core.handles.peek().is_empty()));
    }
    log.assert_clean();
}

#[test]
fn flow_handle_detaches_and_can_be_reused_after_unmount() {
    let log = WarningLog::default();
    let _guard = log.capture();
    let mut harness = Harness::new(false);
    harness.settle();
    let first = harness.core().iid;
    harness.dom.in_scope(ScopeId::APP, || {
        let handle = harness.state.handle;
        assert_ne!(handle.viewport(), Some(Viewport::default()));
        handle.set_viewport(Viewport::new(15.0, 25.0, 1.5), 0);
        assert_eq!(handle.viewport(), Some(Viewport::new(15.0, 25.0, 1.5)));
        // This task must be cancelled with Flow, even though the caller survives.
        handle.zoom_in(200);
    });
    harness.set_visible(false);
    harness.dom.in_scope(ScopeId::APP, || {
        assert!(harness.state.handle.core().is_none());
        assert!(harness.state.handle.viewport().is_none());
        harness.state.handle.zoom_out(0);
    });
    harness.set_visible(true);
    harness.settle();
    assert_ne!(harness.core().iid, first);
    harness.dom.in_scope(ScopeId::APP, || {
        assert_eq!(harness.state.handle.core().unwrap().iid, harness.core().iid);
    });
    log.assert_clean();
}

#[test]
fn pending_flushes_are_cancelled_when_the_canvas_unmounts() {
    let log = WarningLog::default();
    let _guard = log.capture();
    for standalone in [true, false] {
        let mut harness = Harness::new(standalone);
        harness.settle();
        harness.in_canvas(|core| {
            store_measured(
                core,
                harness.state.nodes,
                &"one".into(),
                Size::new(300.0, 150.0),
            );
            let key = core.handles.peek().keys().next().unwrap().clone();
            core.queue_handle_write(key, None);
            assert!(*core.size_flush_queued.peek());
            assert!(*core.handle_flush_queued.peek());
        });
        harness.set_visible(false);
        std::thread::sleep(Duration::from_millis(5));
        harness
            .dom
            .render_immediate(&mut dioxus::core::NoOpMutations);
        harness.dom.in_scope(ScopeId::APP, || {
            assert_eq!(
                harness.state.nodes.peek()[0].measured,
                Some(Size::new(240.0, 120.0))
            );
        });
    }
    log.assert_clean();
}