tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
chore: refresco desde el monorepo — 128 miembros, los 76 widgets y los 11 módulos
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}