//! Showcase de `llimphi-widget-nodegraph` como **editor navegable**: //! cadena de audio (`Source → Filter → Mixer → Output`) + un `LFO` suelto. //! //! - Arrastra la title bar de un nodo → lo mueve. //! - Arrastra desde un pin de salida (derecha) a uno de entrada (izquierda) → //! conecta. //! - Arrastra el **fondo vacío** → paneo del lienzo. //! - **Rueda** → zoom (centrado en el viewport). //! - Click en el cuerpo de un nodo → lo selecciona (borde de acento). Click en //! el fondo → deselecciona. //! - **Supr/Backspace** → borra el nodo seleccionado (y sus cables). **Esc** → //! deselecciona. //! //! Corre con: `cargo run -p llimphi-widget-nodegraph --example nodegraph_demo --release`. use std::sync::Arc; use llimphi_theme::Theme; use llimphi_ui::llimphi_layout::taffy::prelude::{percent, Size, Style}; use llimphi_ui::llimphi_text::Alignment; use llimphi_ui::{App, DragPhase, Handle, Key, KeyEvent, KeyState, NamedKey, View}; use llimphi_widget_nodegraph::{ nodegraph_view_spec, zoom_at_center, zoom_step, Camera, CuerpoNodo, NodeId, NodeSpec, NodegraphMetrics, NodegraphPalette, NodegraphSpec, PinIdx, Wire, }; const VIEWPORT_W: f32 = 1100.0; const VIEWPORT_H: f32 = 720.0; #[derive(Clone)] enum Msg { DragNode { id: NodeId, dx: f32, dy: f32 }, Connect { from_node: NodeId, from_pin: PinIdx, to_node: NodeId, to_pin: PinIdx }, Pan { dx: f32, dy: f32 }, Zoom(f32), Select(NodeId), Deselect, DeleteSelected, } struct Model { nodes: Vec, wires: Vec, camera: Camera, selected: Option, } const ID_SOURCE: NodeId = 1; const ID_FILTER: NodeId = 2; const ID_MIXER: NodeId = 3; const ID_OUTPUT: NodeId = 4; const ID_LFO: NodeId = 5; struct Showcase; impl App for Showcase { type Model = Model; type Msg = Msg; fn title() -> &'static str { "llimphi · nodegraph editor (pan fondo · rueda zoom · click selecciona · Supr borra)" } fn initial_size() -> (u32, u32) { (VIEWPORT_W as u32, VIEWPORT_H as u32) } fn init(_: &Handle) -> Model { Model { nodes: vec![ NodeSpec { id: ID_SOURCE, label: "Source".into(), x: 60.0, y: 80.0, inputs: vec![], outputs: vec!["out".into()] }, NodeSpec { id: ID_FILTER, label: "Filter".into(), x: 290.0, y: 80.0, inputs: vec!["in".into(), "mod".into()], outputs: vec!["out".into()] }, NodeSpec { id: ID_MIXER, label: "Mixer".into(), x: 520.0, y: 80.0, inputs: vec!["a".into(), "b".into()], outputs: vec!["out".into()] }, NodeSpec { id: ID_OUTPUT, label: "Output".into(), x: 750.0, y: 80.0, inputs: vec!["in".into()], outputs: vec![] }, NodeSpec { id: ID_LFO, label: "LFO".into(), x: 290.0, y: 260.0, inputs: vec![], outputs: vec!["out".into()] }, ], wires: vec![ Wire { from_node: ID_SOURCE, from_output: 0, to_node: ID_FILTER, to_input: 0 }, Wire { from_node: ID_FILTER, from_output: 0, to_node: ID_MIXER, to_input: 0 }, Wire { from_node: ID_MIXER, from_output: 0, to_node: ID_OUTPUT, to_input: 0 }, ], camera: Camera::default(), selected: None, } } fn on_key(_model: &Model, event: &KeyEvent) -> Option { if event.state != KeyState::Pressed { return None; } match &event.key { Key::Named(NamedKey::Delete) | Key::Named(NamedKey::Backspace) => { Some(Msg::DeleteSelected) } Key::Named(NamedKey::Escape) => Some(Msg::Deselect), _ => None, } } fn update(model: Model, msg: Msg, _: &Handle) -> Model { let mut m = model; match msg { Msg::DragNode { id, dx, dy } => { if let Some(n) = m.nodes.iter_mut().find(|n| n.id == id) { n.x += dx; n.y += dy; } } Msg::Connect { from_node, from_pin, to_node, to_pin } => { if from_node != to_node && !m.wires.iter().any(|w| { w.from_node == from_node && w.from_output == from_pin && w.to_node == to_node && w.to_input == to_pin }) { m.wires.push(Wire { from_node, from_output: from_pin, to_node, to_input: to_pin }); } } Msg::Pan { dx, dy } => { m.camera.pan_x += dx; m.camera.pan_y += dy; } Msg::Zoom(dy) => { let nz = zoom_step(m.camera.zoom, dy); m.camera = zoom_at_center(m.camera, nz, VIEWPORT_W, VIEWPORT_H); } Msg::Select(id) => m.selected = Some(id), Msg::Deselect => m.selected = None, Msg::DeleteSelected => { if let Some(id) = m.selected.take() { m.nodes.retain(|n| n.id != id); m.wires.retain(|w| w.from_node != id && w.to_node != id); } } } m } fn view(model: &Model) -> View { let theme = Theme::dark(); let palette = NodegraphPalette::from_theme(&theme); let metrics = NodegraphMetrics::default(); let selected = model.selected; nodegraph_view_spec(NodegraphSpec { nodes: &model.nodes, wires: &model.wires, edges: &[], palette: &palette, metrics: &metrics, camera: model.camera, on_drag_node: Arc::new(|id, _phase, dx, dy| Some(Msg::DragNode { id, dx, dy })), on_connect: Arc::new(|from_node, from_pin, to_node, to_pin| { Some(Msg::Connect { from_node, from_pin, to_node, to_pin }) }), on_right_click_node: None, node_tint: None, wire_tint: None, wire_dash: None, edge_tint: None, edge_dash: None, flujo: 0.0, selected: Some(&move |id| Some(id) == selected), on_select_node: Some(Arc::new(|id| Some(Msg::Select(id)))), on_canvas_click: Some(Msg::Deselect), on_pan: Some(Arc::new(|phase, dx, dy| match phase { DragPhase::Move => Some(Msg::Pan { dx, dy }), DragPhase::End => None, })), on_zoom: Some(Arc::new(|dy| Some(Msg::Zoom(dy)))), // El nodo seleccionado muestra un cuerpo: es la forma más corta de // ver que el cuerpo crece el nodo sin correr los pins ni los cables. cuerpo: Some(&move |id, zoom| { (Some(id) == selected).then(|| CuerpoNodo { alto: 34.0, vista: View::new(Style { size: Size { width: percent(1.0_f32), height: percent(1.0_f32) }, ..Default::default() }) .text_aligned( "cuerpo embebido".to_string(), 10.0 * zoom, theme.fg_muted, Alignment::Center, ), }) }), }) } } fn main() { llimphi_ui::run::(); }