tawasuyu

tawasuyu/llimphi

Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.

Rust 614Markdown 274TOML 149RON 310606 Definitions

⎇ main widgets/nodegraph/examples/nodegraph_demo.rs Rust · 195 lines · 7.3 KB History Who touched what raw

chore: refresco desde el monorepo — 128 miembros, los 76 widgets y los 11 módulos b3f12e136 · Sergio ·

1//! Showcase de `llimphi-widget-nodegraph` como **editor navegable**:
2//! cadena de audio (`Source → Filter → Mixer → Output`) + un `LFO` suelto.
3//!
4//! - Arrastra la title bar de un nodo → lo mueve.
5//! - Arrastra desde un pin de salida (derecha) a uno de entrada (izquierda) →
6//!   conecta.
7//! - Arrastra el **fondo vacío** → paneo del lienzo.
8//! - **Rueda** → zoom (centrado en el viewport).
9//! - Click en el cuerpo de un nodo → lo selecciona (borde de acento). Click en
10//!   el fondo → deselecciona.
11//! - **Supr/Backspace** → borra el nodo seleccionado (y sus cables). **Esc** →
12//!   deselecciona.
13//!
14//! Corre con: `cargo run -p llimphi-widget-nodegraph --example nodegraph_demo --release`.
15
16use std::sync::Arc;
17
18use llimphi_theme::Theme;
19use llimphi_ui::llimphi_layout::taffy::prelude::{percent, Size, Style};
20use llimphi_ui::llimphi_text::Alignment;
21use llimphi_ui::{App, DragPhase, Handle, Key, KeyEvent, KeyState, NamedKey, View};
22use llimphi_widget_nodegraph::{
23    nodegraph_view_spec, zoom_at_center, zoom_step, Camera, CuerpoNodo, NodeId, NodeSpec,
24    NodegraphMetrics, NodegraphPalette, NodegraphSpec, PinIdx, Wire,
25};
26
27const VIEWPORT_W: f32 = 1100.0;
28const VIEWPORT_H: f32 = 720.0;
29
30#[derive(Clone)]
31enum Msg {
32    DragNode { id: NodeId, dx: f32, dy: f32 },
33    Connect { from_node: NodeId, from_pin: PinIdx, to_node: NodeId, to_pin: PinIdx },
34    Pan { dx: f32, dy: f32 },
35    Zoom(f32),
36    Select(NodeId),
37    Deselect,
38    DeleteSelected,
39}
40
41struct Model {
42    nodes: Vec<NodeSpec>,
43    wires: Vec<Wire>,
44    camera: Camera,
45    selected: Option<NodeId>,
46}
47
48const ID_SOURCE: NodeId = 1;
49const ID_FILTER: NodeId = 2;
50const ID_MIXER: NodeId = 3;
51const ID_OUTPUT: NodeId = 4;
52const ID_LFO: NodeId = 5;
53
54struct Showcase;
55
56impl App for Showcase {
57    type Model = Model;
58    type Msg = Msg;
59
60    fn title() -> &'static str {
61        "llimphi · nodegraph editor (pan fondo · rueda zoom · click selecciona · Supr borra)"
62    }
63
64    fn initial_size() -> (u32, u32) {
65        (VIEWPORT_W as u32, VIEWPORT_H as u32)
66    }
67
68    fn init(_: &Handle<Msg>) -> Model {
69        Model {
70            nodes: vec![
71                NodeSpec { id: ID_SOURCE, label: "Source".into(), x: 60.0, y: 80.0, inputs: vec![], outputs: vec!["out".into()] },
72                NodeSpec { id: ID_FILTER, label: "Filter".into(), x: 290.0, y: 80.0, inputs: vec!["in".into(), "mod".into()], outputs: vec!["out".into()] },
73                NodeSpec { id: ID_MIXER, label: "Mixer".into(), x: 520.0, y: 80.0, inputs: vec!["a".into(), "b".into()], outputs: vec!["out".into()] },
74                NodeSpec { id: ID_OUTPUT, label: "Output".into(), x: 750.0, y: 80.0, inputs: vec!["in".into()], outputs: vec![] },
75                NodeSpec { id: ID_LFO, label: "LFO".into(), x: 290.0, y: 260.0, inputs: vec![], outputs: vec!["out".into()] },
76            ],
77            wires: vec![
78                Wire { from_node: ID_SOURCE, from_output: 0, to_node: ID_FILTER, to_input: 0 },
79                Wire { from_node: ID_FILTER, from_output: 0, to_node: ID_MIXER, to_input: 0 },
80                Wire { from_node: ID_MIXER, from_output: 0, to_node: ID_OUTPUT, to_input: 0 },
81            ],
82            camera: Camera::default(),
83            selected: None,
84        }
85    }
86
87    fn on_key(_model: &Model, event: &KeyEvent) -> Option<Msg> {
88        if event.state != KeyState::Pressed {
89            return None;
90        }
91        match &event.key {
92            Key::Named(NamedKey::Delete) | Key::Named(NamedKey::Backspace) => {
93                Some(Msg::DeleteSelected)
94            }
95            Key::Named(NamedKey::Escape) => Some(Msg::Deselect),
96            _ => None,
97        }
98    }
99
100    fn update(model: Model, msg: Msg, _: &Handle<Msg>) -> Model {
101        let mut m = model;
102        match msg {
103            Msg::DragNode { id, dx, dy } => {
104                if let Some(n) = m.nodes.iter_mut().find(|n| n.id == id) {
105                    n.x += dx;
106                    n.y += dy;
107                }
108            }
109            Msg::Connect { from_node, from_pin, to_node, to_pin } => {
110                if from_node != to_node
111                    && !m.wires.iter().any(|w| {
112                        w.from_node == from_node
113                            && w.from_output == from_pin
114                            && w.to_node == to_node
115                            && w.to_input == to_pin
116                    })
117                {
118                    m.wires.push(Wire { from_node, from_output: from_pin, to_node, to_input: to_pin });
119                }
120            }
121            Msg::Pan { dx, dy } => {
122                m.camera.pan_x += dx;
123                m.camera.pan_y += dy;
124            }
125            Msg::Zoom(dy) => {
126                let nz = zoom_step(m.camera.zoom, dy);
127                m.camera = zoom_at_center(m.camera, nz, VIEWPORT_W, VIEWPORT_H);
128            }
129            Msg::Select(id) => m.selected = Some(id),
130            Msg::Deselect => m.selected = None,
131            Msg::DeleteSelected => {
132                if let Some(id) = m.selected.take() {
133                    m.nodes.retain(|n| n.id != id);
134                    m.wires.retain(|w| w.from_node != id && w.to_node != id);
135                }
136            }
137        }
138        m
139    }
140
141    fn view(model: &Model) -> View<Msg> {
142        let theme = Theme::dark();
143        let palette = NodegraphPalette::from_theme(&theme);
144        let metrics = NodegraphMetrics::default();
145        let selected = model.selected;
146        nodegraph_view_spec(NodegraphSpec {
147            nodes: &model.nodes,
148            wires: &model.wires,
149            edges: &[],
150            palette: &palette,
151            metrics: &metrics,
152            camera: model.camera,
153            on_drag_node: Arc::new(|id, _phase, dx, dy| Some(Msg::DragNode { id, dx, dy })),
154            on_connect: Arc::new(|from_node, from_pin, to_node, to_pin| {
155                Some(Msg::Connect { from_node, from_pin, to_node, to_pin })
156            }),
157            on_right_click_node: None,
158            node_tint: None,
159            wire_tint: None,
160            wire_dash: None,
161            edge_tint: None,
162            edge_dash: None,
163            flujo: 0.0,
164            selected: Some(&move |id| Some(id) == selected),
165            on_select_node: Some(Arc::new(|id| Some(Msg::Select(id)))),
166            on_canvas_click: Some(Msg::Deselect),
167            on_pan: Some(Arc::new(|phase, dx, dy| match phase {
168                DragPhase::Move => Some(Msg::Pan { dx, dy }),
169                DragPhase::End => None,
170            })),
171            on_zoom: Some(Arc::new(|dy| Some(Msg::Zoom(dy)))),
172            // El nodo seleccionado muestra un cuerpo: es la forma más corta de
173            // ver que el cuerpo crece el nodo sin correr los pins ni los cables.
174            cuerpo: Some(&move |id, zoom| {
175                (Some(id) == selected).then(|| CuerpoNodo {
176                    alto: 34.0,
177                    vista: View::new(Style {
178                        size: Size { width: percent(1.0_f32), height: percent(1.0_f32) },
179                        ..Default::default()
180                    })
181                    .text_aligned(
182                        "cuerpo embebido".to_string(),
183                        10.0 * zoom,
184                        theme.fg_muted,
185                        Alignment::Center,
186                    ),
187                })
188            }),
189        })
190    }
191}
192
193fn main() {
194    llimphi_ui::run::<Showcase>();
195}