tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
⎇ c5a232cf24788ab75a850ffe81eca5a7af50e2f8
widgets/nodegraph/examples/nodegraph_demo.rs
Historia
Quién tocó qué
crudo
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::{App, DragPhase, Handle, Key, KeyEvent, KeyState, NamedKey, View};
20use llimphi_widget_nodegraph::{
21 nodegraph_view_spec, zoom_at_center, zoom_step, Camera, NodeId, NodeSpec, NodegraphMetrics,
22 NodegraphPalette, NodegraphSpec, PinIdx, Wire,
23};
24
25const VIEWPORT_W: f32 = 1100.0;
26const VIEWPORT_H: f32 = 720.0;
27
28#[derive(Clone)]
29enum Msg {
30 DragNode { id: NodeId, dx: f32, dy: f32 },
31 Connect { from_node: NodeId, from_pin: PinIdx, to_node: NodeId, to_pin: PinIdx },
32 Pan { dx: f32, dy: f32 },
33 Zoom(f32),
34 Select(NodeId),
35 Deselect,
36 DeleteSelected,
37}
38
39struct Model {
40 nodes: Vec<NodeSpec>,
41 wires: Vec<Wire>,
42 camera: Camera,
43 selected: Option<NodeId>,
44}
45
46const ID_SOURCE: NodeId = 1;
47const ID_FILTER: NodeId = 2;
48const ID_MIXER: NodeId = 3;
49const ID_OUTPUT: NodeId = 4;
50const ID_LFO: NodeId = 5;
51
52struct Showcase;
53
54impl App for Showcase {
55 type Model = Model;
56 type Msg = Msg;
57
58 fn title() -> &'static str {
59 "llimphi · nodegraph editor (pan fondo · rueda zoom · click selecciona · Supr borra)"
60 }
61
62 fn initial_size() -> (u32, u32) {
63 (VIEWPORT_W as u32, VIEWPORT_H as u32)
64 }
65
66 fn init(_: &Handle<Msg>) -> Model {
67 Model {
68 nodes: vec![
69 NodeSpec { id: ID_SOURCE, label: "Source".into(), x: 60.0, y: 80.0, inputs: vec![], outputs: vec!["out".into()] },
70 NodeSpec { id: ID_FILTER, label: "Filter".into(), x: 290.0, y: 80.0, inputs: vec!["in".into(), "mod".into()], outputs: vec!["out".into()] },
71 NodeSpec { id: ID_MIXER, label: "Mixer".into(), x: 520.0, y: 80.0, inputs: vec!["a".into(), "b".into()], outputs: vec!["out".into()] },
72 NodeSpec { id: ID_OUTPUT, label: "Output".into(), x: 750.0, y: 80.0, inputs: vec!["in".into()], outputs: vec![] },
73 NodeSpec { id: ID_LFO, label: "LFO".into(), x: 290.0, y: 260.0, inputs: vec![], outputs: vec!["out".into()] },
74 ],
75 wires: vec![
76 Wire { from_node: ID_SOURCE, from_output: 0, to_node: ID_FILTER, to_input: 0 },
77 Wire { from_node: ID_FILTER, from_output: 0, to_node: ID_MIXER, to_input: 0 },
78 Wire { from_node: ID_MIXER, from_output: 0, to_node: ID_OUTPUT, to_input: 0 },
79 ],
80 camera: Camera::default(),
81 selected: None,
82 }
83 }
84
85 fn on_key(_model: &Model, event: &KeyEvent) -> Option<Msg> {
86 if event.state != KeyState::Pressed {
87 return None;
88 }
89 match &event.key {
90 Key::Named(NamedKey::Delete) | Key::Named(NamedKey::Backspace) => {
91 Some(Msg::DeleteSelected)
92 }
93 Key::Named(NamedKey::Escape) => Some(Msg::Deselect),
94 _ => None,
95 }
96 }
97
98 fn update(model: Model, msg: Msg, _: &Handle<Msg>) -> Model {
99 let mut m = model;
100 match msg {
101 Msg::DragNode { id, dx, dy } => {
102 if let Some(n) = m.nodes.iter_mut().find(|n| n.id == id) {
103 n.x += dx;
104 n.y += dy;
105 }
106 }
107 Msg::Connect { from_node, from_pin, to_node, to_pin } => {
108 if from_node != to_node
109 && !m.wires.iter().any(|w| {
110 w.from_node == from_node
111 && w.from_output == from_pin
112 && w.to_node == to_node
113 && w.to_input == to_pin
114 })
115 {
116 m.wires.push(Wire { from_node, from_output: from_pin, to_node, to_input: to_pin });
117 }
118 }
119 Msg::Pan { dx, dy } => {
120 m.camera.pan_x += dx;
121 m.camera.pan_y += dy;
122 }
123 Msg::Zoom(dy) => {
124 let nz = zoom_step(m.camera.zoom, dy);
125 m.camera = zoom_at_center(m.camera, nz, VIEWPORT_W, VIEWPORT_H);
126 }
127 Msg::Select(id) => m.selected = Some(id),
128 Msg::Deselect => m.selected = None,
129 Msg::DeleteSelected => {
130 if let Some(id) = m.selected.take() {
131 m.nodes.retain(|n| n.id != id);
132 m.wires.retain(|w| w.from_node != id && w.to_node != id);
133 }
134 }
135 }
136 m
137 }
138
139 fn view(model: &Model) -> View<Msg> {
140 let theme = Theme::dark();
141 let palette = NodegraphPalette::from_theme(&theme);
142 let metrics = NodegraphMetrics::default();
143 let selected = model.selected;
144 nodegraph_view_spec(NodegraphSpec {
145 nodes: &model.nodes,
146 wires: &model.wires,
147 edges: &[],
148 palette: &palette,
149 metrics: &metrics,
150 camera: model.camera,
151 on_drag_node: Arc::new(|id, _phase, dx, dy| Some(Msg::DragNode { id, dx, dy })),
152 on_connect: Arc::new(|from_node, from_pin, to_node, to_pin| {
153 Some(Msg::Connect { from_node, from_pin, to_node, to_pin })
154 }),
155 on_right_click_node: None,
156 node_tint: None,
157 wire_tint: None,
158 edge_tint: None,
159 flujo: 0.0,
160 selected: Some(&move |id| Some(id) == selected),
161 on_select_node: Some(Arc::new(|id| Some(Msg::Select(id)))),
162 on_canvas_click: Some(Msg::Deselect),
163 on_pan: Some(Arc::new(|phase, dx, dy| match phase {
164 DragPhase::Move => Some(Msg::Pan { dx, dy }),
165 DragPhase::End => None,
166 })),
167 on_zoom: Some(Arc::new(|dy| Some(Msg::Zoom(dy)))),
168 })
169 }
170}
171
172fn main() {
173 llimphi_ui::run::<Showcase>();
174}