1mod graph;
36mod nodes;
37
38#[cfg(target_arch = "wasm32")]
39use wasm_bindgen::prelude::*;
40
41#[cfg(target_arch = "wasm32")]
42#[wasm_bindgen(start)]
43pub fn wasm_init() {
44 console_error_panic_hook::set_once();
45}
46
47use graph::generate_procedural_graph;
48use iced::{
49 Color, Element, Length, Point, Rectangle, Subscription, Theme, Vector, mouse,
50 widget::{canvas, column, container, opaque, row, stack, text, toggler},
51};
52use iced_nodegraph::{
53 Counts, GraphInfo, PinInfo, PinRef, PinStatus, PinStyle, default_pin_style, edge, node,
54};
55use nodes::NodeType;
56
57fn pin_style(
59 theme: &iced::Theme,
60 pin: &PinInfo<'_, usize, ::std::any::TypeId>,
61 _other: Option<&PinInfo<'_, usize, ::std::any::TypeId>>,
62 status: PinStatus,
63) -> PinStyle {
64 use nodes::colors;
65 use std::any::TypeId;
66 let ty = *pin.info();
67 let color = if ty == TypeId::of::<colors::Float>() {
68 colors::PIN_FLOAT
69 } else if ty == TypeId::of::<colors::Vec2>() {
70 colors::PIN_VEC2
71 } else if ty == TypeId::of::<colors::Vec3>() {
72 colors::PIN_VEC3
73 } else if ty == TypeId::of::<colors::Vec4>() {
74 colors::PIN_VEC4
75 } else {
76 colors::PIN_GENERIC_IN
77 };
78 PinStyle {
79 color: color.into(),
80 ..default_pin_style(theme, status)
81 }
82}
83use std::collections::{HashSet, VecDeque};
84
85const HIST_CAP: usize = 160;
87
88pub fn main() -> iced::Result {
89 #[cfg(target_arch = "wasm32")]
90 let window_settings = iced::window::Settings {
91 platform_specific: iced::window::settings::PlatformSpecific {
92 target: Some(String::from("demo-canvas-container")),
93 },
94 ..Default::default()
95 };
96
97 #[cfg(not(target_arch = "wasm32"))]
98 let window_settings = iced::window::Settings::default();
99
100 iced::application(Application::new, Application::update, Application::view)
101 .subscription(Application::subscription)
102 .title("500 Node Benchmark - iced_nodegraph")
103 .theme(Application::theme)
104 .window(window_settings)
105 .run()
106}
107
108#[cfg(target_arch = "wasm32")]
109#[wasm_bindgen]
110pub fn run_demo() {
111 let _ = main();
112}
113
114#[derive(Debug, Clone)]
115#[allow(dead_code)]
116enum ApplicationMessage {
117 Noop,
118 EdgeConnected {
119 from: PinRef<usize, usize>,
120 to: PinRef<usize, usize>,
121 },
122 EdgeDisconnected {
123 from: PinRef<usize, usize>,
124 to: PinRef<usize, usize>,
125 },
126 SelectionChanged(Vec<usize>),
127 NodesMoved {
128 delta: Vector,
129 indices: Vec<usize>,
130 },
131 Info(GraphInfo),
132 StatsToggled(bool),
135 CameraReport {
139 pos: Point,
140 zoom: f32,
141 },
142}
143
144struct Application {
145 edges: Vec<(PinRef<usize, usize>, PinRef<usize, usize>)>,
146 nodes: Vec<(Point, NodeType)>,
147 current_theme: Theme,
148 selected_nodes: HashSet<usize>,
149 camera: (Point, f32),
152 latest_info: Option<GraphInfo>,
154 history: VecDeque<Vec<f32>>,
157 stats_visible: bool,
160}
161
162impl Default for Application {
163 fn default() -> Self {
164 let (nodes, edges) = generate_procedural_graph();
165 Self {
166 edges,
167 nodes,
168 current_theme: Theme::CatppuccinMocha,
169 selected_nodes: HashSet::new(),
170 camera: (Point::ORIGIN, 1.0),
171 latest_info: None,
172 history: VecDeque::with_capacity(HIST_CAP),
173 stats_visible: true,
174 }
175 }
176}
177
178impl Application {
179 fn new() -> Self {
180 Self::default()
181 }
182
183 fn update(&mut self, message: ApplicationMessage) {
184 match message {
185 ApplicationMessage::Noop => (),
186 ApplicationMessage::EdgeConnected { from, to } => {
187 self.edges.push((from, to));
188 }
189 ApplicationMessage::EdgeDisconnected { from, to } => {
190 self.edges.retain(|(f, t)| !(f == &from && t == &to));
191 }
192 ApplicationMessage::SelectionChanged(indices) => {
193 self.selected_nodes = indices.into_iter().collect();
194 }
195 ApplicationMessage::NodesMoved { delta, indices } => {
196 for idx in indices {
197 if let Some((pos, _)) = self.nodes.get_mut(idx) {
198 pos.x += delta.x;
199 pos.y += delta.y;
200 }
201 }
202 }
203 ApplicationMessage::CameraReport { pos, zoom } => {
204 self.camera = (pos, zoom);
205 }
206 ApplicationMessage::StatsToggled(on) => {
207 self.stats_visible = on;
208 if !on {
209 self.latest_info = None;
212 self.history.clear();
213 }
214 }
215 ApplicationMessage::Info(info) => {
216 let frame: Vec<f32> = info
217 .timings
218 .iter()
219 .map(|t| t.duration.as_secs_f32() * 1_000_000.0)
220 .collect();
221 if self.history.len() == HIST_CAP {
222 self.history.pop_front();
223 }
224 self.history.push_back(frame);
225 self.latest_info = Some(info);
226 }
227 }
228 }
229
230 fn theme(&self) -> Theme {
231 self.current_theme.clone()
232 }
233
234 fn view(&self) -> iced::Element<'_, ApplicationMessage> {
235 let mut ng: ::iced_nodegraph::NodeGraph<usize, usize, ::std::any::TypeId, _, _, _> =
236 ::iced_nodegraph::NodeGraph::default()
237 .on_connect(|from, to| ApplicationMessage::EdgeConnected { from, to })
238 .on_disconnect(|from, to| ApplicationMessage::EdgeDisconnected { from, to })
239 .on_move(|delta, indices| ApplicationMessage::NodesMoved { delta, indices })
240 .on_select(ApplicationMessage::SelectionChanged)
241 .selection(&self.selected_nodes)
242 .on_pan(|pos, zoom| ApplicationMessage::CameraReport { pos, zoom });
243 if self.stats_visible {
247 ng = ng.on_info(ApplicationMessage::Info);
248 }
249
250 for (index, (position, node_type)) in self.nodes.iter().enumerate() {
252 ng.push_node(
253 node(index, *position, node_type.create_node(&self.current_theme))
254 .pin_style(pin_style),
255 );
256 }
257
258 for (from, to) in &self.edges {
260 ng.push_edge(edge!(*from, *to));
261 }
262
263 let toggle = container(
267 toggler(self.stats_visible)
268 .label("stats")
269 .size(16.0)
270 .text_size(11)
271 .on_toggle(ApplicationMessage::StatsToggled),
272 )
273 .style(panel_style)
274 .padding([4, 10]);
275
276 let mut overlay = column![toggle].spacing(8).align_x(iced::Alignment::End);
277 if self.stats_visible {
278 overlay = overlay.push(self.stats_panel());
279 }
280
281 let graph_view: iced::Element<'_, ApplicationMessage> = ng.into();
282
283 stack![
284 graph_view,
285 container(opaque(overlay))
286 .width(Length::Fill)
287 .height(Length::Fill)
288 .padding(10)
289 .align_x(iced::alignment::Horizontal::Right)
290 .align_y(iced::alignment::Vertical::Top)
291 ]
292 .width(Length::Fill)
293 .height(Length::Fill)
294 .into()
295 }
296
297 fn stats_panel(&self) -> Element<'_, ApplicationMessage> {
298 let palette = self.current_theme.extended_palette();
299
300 let counts_line = |label: &str, c: Counts| {
301 text(format!(
302 "{label}: {} ({} in view, {} culled)",
303 c.total, c.in_view, c.culled
304 ))
305 .size(12)
306 };
307
308 let (nodes_c, pins_c, edges_c, entries, tiles) = match &self.latest_info {
309 Some(i) => (i.nodes, i.pins, i.edges, i.sdf_entries, i.sdf_tiles),
310 None => (
311 Counts::default(),
312 Counts::default(),
313 Counts::default(),
314 0,
315 0,
316 ),
317 };
318
319 let ops = self.latest_info.as_ref().map_or(0, |i| i.timings.len());
322 let order: Vec<usize> = (0..ops).collect();
323
324 let legend: Element<'_, ApplicationMessage> = match &self.latest_info {
325 Some(info) if !order.is_empty() => column(order.iter().map(|&k| {
326 let t = &info.timings[k];
327 let us = t.duration.as_secs_f32() * 1_000_000.0;
328 row![
329 swatch(op_color(palette, k)),
330 text(format!("{us:>5.0} µs {}", t.label)).size(11),
331 ]
332 .spacing(8)
333 .align_y(iced::Alignment::Center)
334 .into()
335 }))
336 .spacing(3)
337 .into(),
338 _ => text("collecting…").size(11).into(),
339 };
340
341 let chart = canvas(TimingChart {
342 history: &self.history,
343 order,
344 })
345 .width(Length::Fill)
346 .height(Length::Fixed(110.0));
347
348 let body = column![
349 text("Frame CPU — stacked by operation").size(13),
350 chart,
351 legend,
352 counts_line("Nodes", nodes_c),
353 counts_line("Pins", pins_c),
354 counts_line("Edges", edges_c),
355 text(format!("SDF: {entries} entries · {tiles} tiles")).size(12),
356 text(format!(
357 "cam: ({:.1}, {:.1}) zoom: {:.5}",
358 self.camera.0.x, self.camera.0.y, self.camera.1
359 ))
360 .size(12),
361 text("Scroll: Zoom · Right-drag: Pan").size(11),
362 ]
363 .spacing(8)
364 .padding(12)
365 .width(Length::Fixed(248.0));
366
367 container(body).style(panel_style).into()
368 }
369
370 fn subscription(&self) -> Subscription<ApplicationMessage> {
371 Subscription::none()
373 }
374}
375
376fn panel_style(theme: &Theme) -> container::Style {
378 let palette = theme.extended_palette();
379 let bg = palette.background.base.color;
380 container::Style {
381 background: Some(iced::Background::Color(Color { a: 0.92, ..bg })),
382 border: iced::Border {
383 color: palette.background.strong.color,
384 width: 1.0,
385 radius: 10.0.into(),
386 },
387 ..container::Style::default()
388 }
389}
390
391fn op_color(palette: &iced::theme::palette::Extended, i: usize) -> Color {
393 match i {
394 0 => palette.primary.base.color,
395 1 => palette.secondary.base.color,
396 2 => palette.success.base.color,
397 3 => palette.danger.base.color,
398 _ => palette.background.strong.color,
399 }
400}
401
402fn swatch(color: Color) -> Element<'static, ApplicationMessage> {
404 container(text(""))
405 .width(Length::Fixed(12.0))
406 .height(Length::Fixed(12.0))
407 .style(move |_theme: &Theme| container::Style {
408 background: Some(iced::Background::Color(color)),
409 border: iced::Border {
410 radius: 3.0.into(),
411 ..Default::default()
412 },
413 ..container::Style::default()
414 })
415 .into()
416}
417
418struct TimingChart<'a> {
420 history: &'a VecDeque<Vec<f32>>,
421 order: Vec<usize>,
423}
424
425impl canvas::Program<ApplicationMessage, Theme> for TimingChart<'_> {
426 type State = ();
427
428 fn draw(
429 &self,
430 _state: &(),
431 renderer: &iced::Renderer,
432 theme: &Theme,
433 bounds: Rectangle,
434 _cursor: mouse::Cursor,
435 ) -> Vec<canvas::Geometry> {
436 let mut frame = canvas::Frame::new(renderer, bounds.size());
437 let n = self.history.len();
438 if n == 0 {
439 return vec![frame.into_geometry()];
440 }
441 let palette = theme.extended_palette();
442 let max_total = self
443 .history
444 .iter()
445 .map(|f| f.iter().sum::<f32>())
446 .fold(1.0_f32, f32::max);
447 let w = bounds.width;
448 let h = bounds.height;
449 let dx = w / HIST_CAP as f32;
450 let x_of = |i: usize| w - (n - 1 - i) as f32 * dx;
451 let y_of = |v: f32| h - (v / max_total) * h;
452
453 let cum = |vals: &[f32], upto: usize| -> f32 {
455 self.order[..upto.min(self.order.len())]
456 .iter()
457 .map(|&j| vals.get(j).copied().unwrap_or(0.0))
458 .sum()
459 };
460 for (p, &k) in self.order.iter().enumerate() {
461 let path = canvas::Path::new(|b| {
462 let mut started = false;
463 for i in 0..n {
464 let pt = iced::Point::new(x_of(i), y_of(cum(&self.history[i], p + 1)));
465 if started {
466 b.line_to(pt);
467 } else {
468 b.move_to(pt);
469 started = true;
470 }
471 }
472 for i in (0..n).rev() {
473 b.line_to(iced::Point::new(x_of(i), y_of(cum(&self.history[i], p))));
474 }
475 b.close();
476 });
477 frame.fill(&path, op_color(palette, k));
478 }
479 vec![frame.into_geometry()]
480 }
481}