Skip to main content

demo_styling/
lib.rs

1//! # Styling Demo
2//!
3//! Interactive demonstration of node graph styling capabilities.
4//!
5//! This demo showcases:
6//! - Per-node styling with NodeStyle
7//! - Live style controls (corner radius, opacity, border width)
8//! - Theme switching
9//! - Different node type presets (Input, Process, Output, Comment)
10//!
11//! ## Interactive Demo
12//!
13//! <link rel="stylesheet" href="pkg/demo.css">
14//! <div id="demo-container">
15//!   <div id="demo-loading">
16//!     <div class="demo-spinner"></div>
17//!     <p>Loading demo...</p>
18//!   </div>
19//!   <div id="demo-canvas-container"></div>
20//!   <div id="demo-error">
21//!     <strong>Failed to load demo.</strong> WebGPU required.
22//!   </div>
23//! </div>
24//! <script type="module" src="pkg/demo-loader.js"></script>
25//!
26//! ## Controls
27//!
28//! - **Select nodes** - Click nodes in the right panel to select
29//! - **Style sliders** - Adjust corner radius, opacity, border width
30//! - **Presets** - Apply Input/Process/Output/Comment presets
31//! - **Theme picker** - Switch between color themes
32//! - **Scroll** - Zoom in/out
33//! - **Middle-drag** - Pan the canvas
34
35mod nodes;
36
37use iced::{
38    Element, Length, Point, Subscription, Task, Theme, Vector,
39    widget::{button, column, container, opaque, pick_list, row, slider, stack, text},
40};
41use iced_nodegraph::{
42    GraphStyle, NodeStatus, NodeStyle, Pattern, PinDirection, PinInfo, PinRef, PinStatus, PinStyle,
43    SelectionStyle, TilingBackground, default_pin_style, edge, node,
44};
45use nodes::styled_node;
46use std::collections::HashSet;
47
48/// Pin style for the styling demo: blue inputs, orange outputs.
49fn styling_pin_style(
50    theme: &Theme,
51    pin: &PinInfo<'_, usize, ::std::any::TypeId>,
52    _other: Option<&PinInfo<'_, usize, ::std::any::TypeId>>,
53    status: PinStatus,
54) -> PinStyle {
55    let color = match pin.direction() {
56        PinDirection::Output => iced::Color::from_rgb(0.9, 0.7, 0.5),
57        _ => iced::Color::from_rgb(0.5, 0.7, 0.9),
58    };
59    PinStyle {
60        color: color.into(),
61        ..default_pin_style(theme, status)
62    }
63}
64
65#[cfg(target_arch = "wasm32")]
66use wasm_bindgen::prelude::*;
67
68#[cfg(target_arch = "wasm32")]
69#[wasm_bindgen(start)]
70pub fn wasm_init() {
71    console_error_panic_hook::set_once();
72}
73
74pub fn main() -> iced::Result {
75    #[cfg(target_arch = "wasm32")]
76    let window_settings = iced::window::Settings {
77        platform_specific: iced::window::settings::PlatformSpecific {
78            target: Some(String::from("demo-canvas-container")),
79        },
80        ..Default::default()
81    };
82
83    #[cfg(not(target_arch = "wasm32"))]
84    let window_settings = iced::window::Settings::default();
85
86    iced::application(Application::new, Application::update, Application::view)
87        .subscription(Application::subscription)
88        .title("Styling Demo - iced_nodegraph")
89        .theme(Application::theme)
90        .window(window_settings)
91        .run()
92}
93
94#[cfg(target_arch = "wasm32")]
95#[wasm_bindgen]
96pub fn run_demo() {
97    let _ = main();
98}
99
100#[derive(Debug, Clone)]
101enum Message {
102    // Graph events
103    EdgeConnected {
104        from: PinRef<usize, usize>,
105        to: PinRef<usize, usize>,
106    },
107    EdgeDisconnected {
108        from: PinRef<usize, usize>,
109        to: PinRef<usize, usize>,
110    },
111    SelectionChanged(Vec<usize>),
112    NodesMoved {
113        delta: Vector,
114        indices: Vec<usize>,
115    },
116
117    // Style controls
118    CornerRadiusChanged(f32),
119    OpacityChanged(f32),
120    BorderWidthChanged(f32),
121    SelectNode(usize),
122    ApplyPreset(NodePreset),
123    ChangeTheme(Theme),
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
127enum NodePreset {
128    Input,
129    Process,
130    Output,
131    Comment,
132}
133
134impl std::fmt::Display for NodePreset {
135    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136        match self {
137            NodePreset::Input => write!(f, "Input"),
138            NodePreset::Process => write!(f, "Process"),
139            NodePreset::Output => write!(f, "Output"),
140            NodePreset::Comment => write!(f, "Comment"),
141        }
142    }
143}
144
145impl NodePreset {
146    const ALL: [NodePreset; 4] = [
147        NodePreset::Input,
148        NodePreset::Process,
149        NodePreset::Output,
150        NodePreset::Comment,
151    ];
152}
153
154struct Application {
155    edges: Vec<(PinRef<usize, usize>, PinRef<usize, usize>)>,
156    nodes: Vec<(Point, String, NodeStyle)>,
157    current_theme: Theme,
158    selected_node: Option<usize>,
159    graph_selection: HashSet<usize>,
160
161    // Control panel state
162    corner_radius: f32,
163    opacity: f32,
164    border_width: f32,
165}
166
167impl Default for Application {
168    fn default() -> Self {
169        Self {
170            edges: vec![
171                (PinRef::new(0, 1), PinRef::new(1, 0)), // Input node output -> Process node input
172                (PinRef::new(1, 1), PinRef::new(2, 0)), // Process node output -> Output node input
173            ],
174            nodes: vec![
175                (
176                    Point::new(100.0, 150.0),
177                    "Input Data".to_string(),
178                    NodeStyle::input(),
179                ),
180                (
181                    Point::new(350.0, 200.0),
182                    "Transform".to_string(),
183                    NodeStyle::process(),
184                ),
185                (
186                    Point::new(600.0, 150.0),
187                    "Output Result".to_string(),
188                    NodeStyle::output(),
189                ),
190                (
191                    Point::new(350.0, 400.0),
192                    "Note: This is a comment".to_string(),
193                    NodeStyle::comment(),
194                ),
195            ],
196            current_theme: Theme::CatppuccinFrappe,
197            selected_node: Some(0),
198            graph_selection: HashSet::new(),
199            corner_radius: 5.0,
200            opacity: 0.75,
201            border_width: 1.5,
202        }
203    }
204}
205
206impl Application {
207    fn new() -> (Self, Task<Message>) {
208        (Self::default(), Task::none())
209    }
210
211    fn subscription(&self) -> Subscription<Message> {
212        Subscription::none()
213    }
214
215    fn theme(&self) -> Theme {
216        self.current_theme.clone()
217    }
218
219    fn update(&mut self, message: Message) -> Task<Message> {
220        match message {
221            Message::EdgeConnected { from, to } => {
222                self.edges.push((from, to));
223            }
224            Message::EdgeDisconnected { from, to } => {
225                self.edges.retain(|(f, t)| !(f == &from && t == &to));
226            }
227            Message::SelectionChanged(indices) => {
228                self.graph_selection = indices.into_iter().collect();
229            }
230            Message::NodesMoved { delta, indices } => {
231                for idx in indices {
232                    if let Some((pos, _, _)) = self.nodes.get_mut(idx) {
233                        pos.x += delta.x;
234                        pos.y += delta.y;
235                    }
236                }
237            }
238            Message::CornerRadiusChanged(value) => {
239                self.corner_radius = value;
240                self.apply_style_to_selected();
241            }
242            Message::OpacityChanged(value) => {
243                self.opacity = value;
244                self.apply_style_to_selected();
245            }
246            Message::BorderWidthChanged(value) => {
247                self.border_width = value;
248                self.apply_style_to_selected();
249            }
250            Message::SelectNode(index) => {
251                self.selected_node = Some(index);
252                // Load the selected node's style into controls
253                if let Some((_, _, style)) = self.nodes.get(index) {
254                    self.corner_radius = style.corner_radius;
255                    self.opacity = style.opacity;
256                    self.border_width = style.border_pattern.thickness;
257                }
258            }
259            Message::ApplyPreset(preset) => {
260                if let Some(index) = self.selected_node {
261                    let new_style = match preset {
262                        NodePreset::Input => NodeStyle::input(),
263                        NodePreset::Process => NodeStyle::process(),
264                        NodePreset::Output => NodeStyle::output(),
265                        NodePreset::Comment => NodeStyle::comment(),
266                    };
267                    if let Some((_, _, style)) = self.nodes.get_mut(index) {
268                        *style = new_style.clone();
269                        self.corner_radius = new_style.corner_radius;
270                        self.opacity = new_style.opacity;
271                        self.border_width = new_style.border_pattern.thickness;
272                    }
273                }
274            }
275            Message::ChangeTheme(theme) => {
276                self.current_theme = theme;
277            }
278        }
279        Task::none()
280    }
281
282    fn apply_style_to_selected(&mut self) {
283        if let Some(index) = self.selected_node
284            && let Some((_, _, style)) = self.nodes.get_mut(index)
285        {
286            style.corner_radius = self.corner_radius;
287            style.opacity = self.opacity;
288            style.border_pattern = Pattern::solid(self.border_width);
289        }
290    }
291
292    fn view(&self) -> Element<'_, Message> {
293        let control_panel = self.build_control_panel();
294        let graph = self.build_graph();
295
296        // Use stack to overlay control panel on right side of graph.
297        // `opaque` claims mouse interaction for the menu's 280px strip so the
298        // NodeGraph below doesn't receive wheel/click events that fall on it.
299        // The outer Fill x Fill container is just for alignment and must stay
300        // transparent.
301        stack![
302            graph,
303            container(opaque(
304                container(control_panel)
305                    .width(Length::Fixed(280.0))
306                    .height(Length::Fill)
307                    .padding(15)
308                    .style(|theme: &Theme| {
309                        let palette = theme.extended_palette();
310                        container::Style {
311                            background: Some(palette.background.weak.color.into()),
312                            ..Default::default()
313                        }
314                    })
315            ))
316            .width(Length::Fill)
317            .height(Length::Fill)
318            .align_x(iced::alignment::Horizontal::Right),
319        ]
320        .into()
321    }
322
323    fn build_control_panel(&self) -> Element<'_, Message> {
324        let theme = &self.current_theme;
325        let palette = theme.extended_palette();
326        let text_color = palette.background.base.text;
327
328        let title = text("Style Controls").size(18).color(text_color);
329
330        let selected_label = if let Some(index) = self.selected_node {
331            let name = &self.nodes[index].1;
332            text(format!("Selected: {}", name))
333                .size(14)
334                .color(text_color)
335        } else {
336            text("No node selected").size(14).color(text_color)
337        };
338
339        // Node selection buttons
340        let node_buttons: Element<'_, Message> = column(
341            self.nodes
342                .iter()
343                .enumerate()
344                .map(|(i, (_, name, _))| {
345                    let is_selected = self.selected_node == Some(i);
346                    button(text(name.clone()).size(12))
347                        .on_press(Message::SelectNode(i))
348                        .style(move |theme: &Theme, status| {
349                            if is_selected {
350                                button::primary(theme, status)
351                            } else {
352                                button::secondary(theme, status)
353                            }
354                        })
355                        .width(Length::Fill)
356                        .into()
357                })
358                .collect::<Vec<_>>(),
359        )
360        .spacing(5)
361        .into();
362
363        // Style sliders
364        let corner_slider = column![
365            text("Corner Radius").size(12).color(text_color),
366            row![
367                slider(0.0..=20.0, self.corner_radius, Message::CornerRadiusChanged),
368                text(format!("{:.1}", self.corner_radius))
369                    .size(12)
370                    .color(text_color),
371            ]
372            .spacing(10),
373        ]
374        .spacing(4);
375
376        let opacity_slider = column![
377            text("Opacity").size(12).color(text_color),
378            row![
379                slider(0.1..=1.0, self.opacity, Message::OpacityChanged).step(0.05_f32),
380                text(format!("{:.2}", self.opacity))
381                    .size(12)
382                    .color(text_color),
383            ]
384            .spacing(10),
385        ]
386        .spacing(4);
387
388        let border_slider = column![
389            text("Border Width").size(12).color(text_color),
390            row![
391                slider(0.5..=5.0, self.border_width, Message::BorderWidthChanged).step(0.5_f32),
392                text(format!("{:.1}", self.border_width))
393                    .size(12)
394                    .color(text_color),
395            ]
396            .spacing(10),
397        ]
398        .spacing(4);
399
400        // Preset buttons
401        let preset_label = text("Apply Preset").size(12).color(text_color);
402        let preset_buttons: Element<'_, Message> = row(NodePreset::ALL
403            .iter()
404            .map(|preset| {
405                button(text(preset.to_string()).size(11))
406                    .on_press(Message::ApplyPreset(*preset))
407                    .padding([4, 8])
408                    .into()
409            })
410            .collect::<Vec<_>>())
411        .spacing(5)
412        .wrap()
413        .into();
414
415        // Theme selector
416        let theme_label = text("Theme").size(12).color(text_color);
417        let themes = vec![
418            Theme::Dark,
419            Theme::Light,
420            Theme::CatppuccinFrappe,
421            Theme::CatppuccinMocha,
422            Theme::Dracula,
423            Theme::Nord,
424            Theme::SolarizedDark,
425            Theme::SolarizedLight,
426            Theme::GruvboxDark,
427            Theme::GruvboxLight,
428        ];
429        let theme_picker = pick_list(
430            themes,
431            Some(self.current_theme.clone()),
432            Message::ChangeTheme,
433        )
434        .width(Length::Fill);
435
436        column![
437            title,
438            text("").height(Length::Fixed(10.0)), // Spacer
439            selected_label,
440            text("").height(Length::Fixed(10.0)),
441            text("Select Node").size(12).color(text_color),
442            node_buttons,
443            text("").height(Length::Fixed(20.0)),
444            corner_slider,
445            text("").height(Length::Fixed(10.0)),
446            opacity_slider,
447            text("").height(Length::Fixed(10.0)),
448            border_slider,
449            text("").height(Length::Fixed(20.0)),
450            preset_label,
451            preset_buttons,
452            text("").height(Length::Fixed(20.0)),
453            theme_label,
454            theme_picker,
455        ]
456        .spacing(5)
457        .into()
458    }
459
460    fn build_graph(&self) -> Element<'_, Message> {
461        let theme = &self.current_theme;
462
463        let mut ng: ::iced_nodegraph::NodeGraph<usize, usize, ::std::any::TypeId, _, _, _> =
464            ::iced_nodegraph::NodeGraph::default()
465                .on_connect(|from, to| Message::EdgeConnected { from, to })
466                .on_disconnect(|from, to| Message::EdgeDisconnected { from, to })
467                .on_move(|delta, indices| Message::NodesMoved { delta, indices })
468                .on_select(Message::SelectionChanged)
469                .selection(&self.graph_selection)
470                .graph_style(|theme: &Theme| {
471                    let line = theme.extended_palette().background.strong.color;
472                    GraphStyle::from_theme(theme).tiling(TilingBackground::grid(
473                        40.0,
474                        1.0,
475                        iced::Color { a: 0.5, ..line },
476                    ))
477                });
478
479        for (index, (position, name, style)) in self.nodes.iter().enumerate() {
480            // The demo stores a fully resolved style per node; the callback
481            // returns it and layers the selection feedback on top when selected.
482            let node_style = style.clone();
483            ng.push_node(
484                node(index, *position, styled_node(name, style, theme))
485                    .style(move |theme, status| {
486                        let mut resolved = node_style.clone();
487                        if status == NodeStatus::Selected {
488                            let sel = SelectionStyle::from_theme(theme);
489                            resolved.border_color = sel.selected_border_color.into();
490                            resolved.border_pattern = Pattern::solid(sel.selected_border_width);
491                        }
492                        resolved
493                    })
494                    .pin_style(styling_pin_style),
495            );
496        }
497
498        for (from, to) in &self.edges {
499            ng.push_edge(edge!(*from, *to));
500        }
501
502        ng.into()
503    }
504}