tawasuyu

tawasuyu/llimphi

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

Rust 238TOML 102Markdown 83Shell 24178 Definiciones

⎇ c5a232cf24788ab75a850ffe81eca5a7af50e2f8 widgets/nodegraph/examples/nodegraph_demo.rs Rust · 174 líneas · 6.3 KB Historia Quién tocó qué crudo

chore: refresco desde el monorepo — 27 crates nuevos (anim, lottie, svg, mesh, image, layer, test, wire-view, widgets rag-sidebar/router/lazy-list/rive-button) y sync de los 95 existentes 93253cc22 · 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::{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}