Skip to main content

demo_shader_editor/
lib.rs

1//! # Visual Shader Editor Demo
2//!
3//! A visual shader editor demonstrating complex node graph functionality.
4//! Create WGSL shaders by connecting nodes visually.
5//!
6//! ## Interactive Demo
7//!
8//! <link rel="stylesheet" href="pkg/demo.css">
9//! <div id="demo-container">
10//!   <div id="demo-loading">
11//!     <div class="demo-spinner"></div>
12//!     <p>Loading demo...</p>
13//!   </div>
14//!   <div id="demo-canvas-container"></div>
15//!   <div id="demo-error">
16//!     <strong>Failed to load demo.</strong> WebGPU required.
17//!   </div>
18//! </div>
19//! <script type="module" src="pkg/demo-loader.js"></script>
20//!
21//! ## Controls
22//!
23//! - **Cmd/Ctrl+Space** - Open command palette to add shader nodes
24//! - **Drag nodes** - Move nodes around the canvas
25//! - **Drag from pins** - Create connections between compatible sockets
26//! - **Scroll** - Zoom in/out
27//! - **Middle-drag** - Pan the canvas
28//!
29//! ## Available Nodes
30//!
31//! Use the command palette to add nodes from categories: Math, Vector, Color,
32//! Texture, Input, and Output. Connect them to build WGSL fragment shaders.
33
34pub mod colors;
35mod compiler;
36mod default_shader;
37mod shader_graph;
38
39#[cfg(target_arch = "wasm32")]
40use wasm_bindgen::prelude::*;
41
42#[cfg(target_arch = "wasm32")]
43#[wasm_bindgen(start)]
44pub fn wasm_init() {
45    console_error_panic_hook::set_once();
46}
47
48use compiler::ShaderCompiler;
49use iced::{
50    Element, Event, Length, Point, Subscription, Task, Theme, Vector, event, keyboard,
51    widget::{column, container, opaque, stack, text},
52    window,
53};
54use iced_nodegraph::{
55    PinDirection, PinInfo, PinRef, PinSide, PinStatus, PinStyle, default_pin_style,
56    edge as ng_edge, node as ng_node, node_pin,
57};
58use iced_palette::{
59    Command, command, command_palette, focus_input, get_filtered_command_index, get_filtered_count,
60    navigate_down, navigate_up,
61};
62use shader_graph::ShaderGraph;
63use shader_graph::nodes::ShaderNodeType;
64use std::collections::HashSet;
65
66pub fn main() -> iced::Result {
67    #[cfg(target_arch = "wasm32")]
68    let window_settings = iced::window::Settings {
69        platform_specific: iced::window::settings::PlatformSpecific {
70            target: Some(String::from("demo-canvas-container")),
71        },
72        ..Default::default()
73    };
74
75    #[cfg(not(target_arch = "wasm32"))]
76    let window_settings = iced::window::Settings::default();
77
78    iced::application(Application::new, Application::update, Application::view)
79        .subscription(Application::subscription)
80        .title("Visual Shader Editor - iced_nodegraph")
81        .theme(Application::theme)
82        .window(window_settings)
83        .run()
84}
85
86#[cfg(target_arch = "wasm32")]
87#[wasm_bindgen]
88pub fn run_demo() {
89    let _ = main();
90}
91
92#[derive(Debug, Clone)]
93enum Message {
94    EdgeConnected {
95        from: PinRef<usize, usize>,
96        to: PinRef<usize, usize>,
97    },
98    EdgeDisconnected {
99        from: PinRef<usize, usize>,
100        to: PinRef<usize, usize>,
101    },
102    SelectionChanged(Vec<usize>),
103    NodesMoved {
104        delta: Vector,
105        indices: Vec<usize>,
106    },
107    // Command palette messages
108    ToggleCommandPalette,
109    CommandPaletteInput(String),
110    CommandPaletteNavigateUp,
111    CommandPaletteNavigateDown,
112    CommandPaletteNavigate(usize),
113    CommandPaletteSelect(usize),
114    CommandPaletteConfirm,
115    CommandPaletteCancel,
116    // Node spawning
117    SpawnNode(ShaderNodeType),
118    // Theme
119    ChangeTheme(Theme),
120    // Camera/viewport tracking
121    CameraChanged {
122        position: Point,
123        zoom: f32,
124    },
125    WindowResized(iced::Size),
126}
127
128struct Application {
129    shader_graph: ShaderGraph,
130    compiled_shader: Option<String>,
131    compilation_error: Option<String>,
132    visual_edges: Vec<(PinRef<usize, usize>, PinRef<usize, usize>)>,
133    current_theme: Theme,
134    graph_selection: HashSet<usize>,
135    // Command palette state
136    command_palette_open: bool,
137    command_input: String,
138    palette_selected_index: usize,
139    // Camera/viewport tracking for spawn-at-center
140    viewport_size: iced::Size,
141    camera_position: Point,
142    camera_zoom: f32,
143}
144
145impl Application {
146    fn new() -> (Self, iced::Task<Message>) {
147        let shader_graph = default_shader::create_default_graph();
148
149        // Convert shader graph connections to visual edges
150        // NodeGraph widget uses flat pin indices: [input0, input1, ..., output0, output1, ...]
151        // ShaderGraph uses separate indices: from_socket = output index, to_socket = input index
152        let visual_edges: Vec<(PinRef<usize, usize>, PinRef<usize, usize>)> = shader_graph
153            .connections
154            .iter()
155            .filter_map(|conn| {
156                // Get the nodes to find their input/output counts
157                let from_node = shader_graph.nodes.iter().find(|n| n.id == conn.from_node)?;
158                // Validate target node exists
159                shader_graph.nodes.iter().find(|n| n.id == conn.to_node)?;
160
161                // Find node indices (position in nodes vec)
162                let from_node_idx = shader_graph
163                    .nodes
164                    .iter()
165                    .position(|n| n.id == conn.from_node)?;
166                let to_node_idx = shader_graph
167                    .nodes
168                    .iter()
169                    .position(|n| n.id == conn.to_node)?;
170
171                // from_socket is an output index -> visual pin = num_inputs + output_index
172                let from_visual_pin = from_node.inputs.len() + conn.from_socket;
173
174                // to_socket is an input index -> visual pin = input_index (inputs come first)
175                let to_visual_pin = conn.to_socket;
176
177                Some((
178                    PinRef::new(from_node_idx, from_visual_pin),
179                    PinRef::new(to_node_idx, to_visual_pin),
180                ))
181            })
182            .collect();
183
184        let mut app = Self {
185            shader_graph,
186            compiled_shader: None,
187            compilation_error: None,
188            visual_edges,
189            current_theme: Theme::CatppuccinMocha,
190            graph_selection: HashSet::new(),
191            command_palette_open: false,
192            command_input: String::new(),
193            palette_selected_index: 0,
194            viewport_size: iced::Size::new(800.0, 600.0),
195            camera_position: Point::ORIGIN,
196            camera_zoom: 1.0,
197        };
198
199        app.recompile();
200
201        (app, iced::Task::none())
202    }
203
204    /// Calculate spawn position at screen center, converted to world coordinates.
205    fn spawn_position(&self) -> Point {
206        // Screen center
207        let screen_center_x = self.viewport_size.width / 2.0;
208        let screen_center_y = self.viewport_size.height / 2.0;
209
210        // Convert to world coordinates: world = screen / zoom - camera_position
211        let world_x = screen_center_x / self.camera_zoom - self.camera_position.x;
212        let world_y = screen_center_y / self.camera_zoom - self.camera_position.y;
213
214        // Offset for node size (approximate center)
215        Point::new(world_x - 60.0, world_y - 40.0)
216    }
217
218    fn update(&mut self, message: Message) -> iced::Task<Message> {
219        match message {
220            Message::EdgeConnected { from, to } => {
221                // Store visual edge as-is
222                self.visual_edges.push((from, to));
223
224                // Convert visual pin indices to shader socket indices
225                // First, gather the info we need
226                let connection_info = {
227                    let from_node_data = self.shader_graph.nodes.get(from.node_id);
228                    let to_node_data = self.shader_graph.nodes.get(to.node_id);
229
230                    if let (Some(from_node_data), Some(to_node_data)) =
231                        (from_node_data, to_node_data)
232                    {
233                        // from.pin_id is visual index, output starts after inputs
234                        let from_socket = from.pin_id.saturating_sub(from_node_data.inputs.len());
235                        // to.pin_id is visual index, inputs come first so it's direct
236                        let to_socket = to.pin_id;
237
238                        if from_socket < from_node_data.outputs.len()
239                            && to_socket < to_node_data.inputs.len()
240                        {
241                            Some((from_node_data.id, from_socket, to_node_data.id, to_socket))
242                        } else {
243                            None
244                        }
245                    } else {
246                        None
247                    }
248                };
249
250                // Now apply the connection
251                if let Some((from_id, from_socket, to_id, to_socket)) = connection_info {
252                    self.shader_graph.add_connection(shader_graph::Connection {
253                        from_node: from_id,
254                        from_socket,
255                        to_node: to_id,
256                        to_socket,
257                    });
258                }
259                self.recompile();
260            }
261            Message::EdgeDisconnected { from, to } => {
262                self.visual_edges.retain(|(f, t)| !(f == &from && t == &to));
263
264                // Convert visual pin indices to shader socket indices and
265                // resolve graph indices to node ids, mirroring EdgeConnected.
266                let from_node = self.shader_graph.nodes.get(from.node_id);
267                let to_node = self.shader_graph.nodes.get(to.node_id);
268                if let (Some(from_node), Some(to_node)) = (from_node, to_node) {
269                    let from_id = from_node.id;
270                    let to_id = to_node.id;
271                    let from_socket = from.pin_id.saturating_sub(from_node.inputs.len());
272                    let to_socket = to.pin_id;
273
274                    self.shader_graph.connections.retain(|c| {
275                        !(c.from_node == from_id
276                            && c.from_socket == from_socket
277                            && c.to_node == to_id
278                            && c.to_socket == to_socket)
279                    });
280                }
281                self.recompile();
282                return Task::none();
283            }
284            Message::SelectionChanged(indices) => {
285                self.graph_selection = indices.into_iter().collect();
286            }
287            Message::NodesMoved { delta, indices } => {
288                for idx in indices {
289                    if let Some(node) = self.shader_graph.get_node_by_index_mut(idx) {
290                        node.position.x += delta.x;
291                        node.position.y += delta.y;
292                    }
293                }
294            }
295            // Command palette
296            Message::ToggleCommandPalette => {
297                self.command_palette_open = !self.command_palette_open;
298                if self.command_palette_open {
299                    self.command_input.clear();
300                    self.palette_selected_index = 0;
301                    return focus_input();
302                }
303            }
304            Message::CommandPaletteInput(input) => {
305                self.command_input = input;
306                self.palette_selected_index = 0;
307            }
308            Message::CommandPaletteNavigateUp => {
309                if !self.command_palette_open {
310                    return Task::none();
311                }
312                let commands = self.build_palette_commands();
313                let count = get_filtered_count(&self.command_input, &commands);
314                self.palette_selected_index = navigate_up(self.palette_selected_index, count);
315            }
316            Message::CommandPaletteNavigateDown => {
317                if !self.command_palette_open {
318                    return Task::none();
319                }
320                let commands = self.build_palette_commands();
321                let count = get_filtered_count(&self.command_input, &commands);
322                self.palette_selected_index = navigate_down(self.palette_selected_index, count);
323            }
324            Message::CommandPaletteNavigate(index) => {
325                if !self.command_palette_open {
326                    return Task::none();
327                }
328                self.palette_selected_index = index;
329            }
330            Message::CommandPaletteSelect(index) => {
331                self.palette_selected_index = index;
332                return self.update(Message::CommandPaletteConfirm);
333            }
334            Message::CommandPaletteConfirm => {
335                if !self.command_palette_open {
336                    return Task::none();
337                }
338                let commands = self.build_palette_commands();
339                if let Some(original_idx) = get_filtered_command_index(
340                    &self.command_input,
341                    &commands,
342                    self.palette_selected_index,
343                ) {
344                    use iced_palette::CommandAction;
345                    if let CommandAction::Message(msg) = &commands[original_idx].action {
346                        let msg = msg.clone();
347                        self.command_palette_open = false;
348                        self.command_input.clear();
349                        self.palette_selected_index = 0;
350                        return self.update(msg);
351                    }
352                }
353            }
354            Message::CommandPaletteCancel => {
355                self.command_palette_open = false;
356                self.command_input.clear();
357                self.palette_selected_index = 0;
358            }
359            Message::SpawnNode(node_type) => {
360                // Spawn node at screen center (converted to world coordinates)
361                let position = self.spawn_position();
362                self.shader_graph.add_node(node_type, position);
363            }
364            Message::ChangeTheme(theme) => {
365                self.current_theme = theme;
366            }
367            Message::CameraChanged { position, zoom } => {
368                self.camera_position = position;
369                self.camera_zoom = zoom;
370            }
371            Message::WindowResized(size) => {
372                self.viewport_size = size;
373            }
374        }
375
376        Task::none()
377    }
378
379    fn view(&self) -> Element<'_, Message> {
380        // Build node graph
381        let mut graph: ::iced_nodegraph::NodeGraph<usize, usize, ::std::any::TypeId, _, _, _> =
382            ::iced_nodegraph::NodeGraph::default()
383                .on_connect(|from, to| Message::EdgeConnected { from, to })
384                .on_move(|delta, indices| Message::NodesMoved { delta, indices })
385                .on_disconnect(|from, to| Message::EdgeDisconnected { from, to })
386                .on_select(Message::SelectionChanged)
387                .on_pan(|position, zoom| Message::CameraChanged { position, zoom })
388                .selection(&self.graph_selection);
389
390        // Add all shader graph nodes
391        for (node_idx, node) in self.shader_graph.nodes.iter().enumerate() {
392            let node_content = create_node_widget(&node.node_type, &self.current_theme);
393            graph.push_node(ng_node(node_idx, node.position, node_content).pin_style(pin_style));
394        }
395
396        // Add all edges
397        for (from, to) in &self.visual_edges {
398            graph.push_edge(ng_edge(*from, *to, ()));
399        }
400
401        let graph_element: Element<Message> = graph.into();
402
403        // Show command palette overlay if open. `opaque` blocks wheel events
404        // from reaching the NodeGraph behind it; the palette's internal
405        // `mouse_area` only captures `on_press`, not scroll.
406        if self.command_palette_open {
407            let commands = self.build_palette_commands();
408            stack![
409                graph_element,
410                opaque(command_palette(
411                    &self.command_input,
412                    &commands,
413                    self.palette_selected_index,
414                    Message::CommandPaletteInput,
415                    Message::CommandPaletteSelect,
416                    Message::CommandPaletteNavigate,
417                    || Message::CommandPaletteCancel,
418                ))
419            ]
420            .into()
421        } else {
422            graph_element
423        }
424    }
425
426    fn theme(&self) -> Theme {
427        self.current_theme.clone()
428    }
429
430    fn subscription(&self) -> Subscription<Message> {
431        use iced::keyboard::key::Named;
432
433        Subscription::batch(vec![
434            // Keyboard events for command palette
435            event::listen_with(|event, _status, _id| {
436                if let Event::Keyboard(keyboard::Event::KeyPressed { key, modifiers, .. }) = event {
437                    // Ctrl+Space or Ctrl+Space to toggle palette
438                    if modifiers.command() && key == keyboard::Key::Named(Named::Space) {
439                        return Some(Message::ToggleCommandPalette);
440                    }
441
442                    // When palette is open, handle navigation
443                    match key {
444                        keyboard::Key::Named(Named::ArrowUp) => {
445                            return Some(Message::CommandPaletteNavigateUp);
446                        }
447                        keyboard::Key::Named(Named::ArrowDown) => {
448                            return Some(Message::CommandPaletteNavigateDown);
449                        }
450                        keyboard::Key::Named(Named::Enter) => {
451                            return Some(Message::CommandPaletteConfirm);
452                        }
453                        keyboard::Key::Named(Named::Escape) => {
454                            return Some(Message::CommandPaletteCancel);
455                        }
456                        _ => {}
457                    }
458                }
459                None
460            }),
461            // Window resize events
462            event::listen_with(|event, _, _| match event {
463                Event::Window(window::Event::Resized(size)) => Some(Message::WindowResized(size)),
464                _ => None,
465            }),
466        ])
467    }
468
469    fn build_palette_commands(&self) -> Vec<Command<Message>> {
470        let mut commands = Vec::new();
471
472        // Add node spawning commands for all shader node types
473        for node_type in ShaderNodeType::all() {
474            let category = node_type.category();
475            commands.push(
476                command(node_type.name(), node_type.name())
477                    .description(format!("{} node", category))
478                    .action(Message::SpawnNode(*node_type)),
479            );
480        }
481
482        // Add theme switching commands
483        commands.push(
484            command("theme-dark", "Dark Theme")
485                .description("Switch to dark theme")
486                .action(Message::ChangeTheme(Theme::Dark)),
487        );
488        commands.push(
489            command("theme-light", "Light Theme")
490                .description("Switch to light theme")
491                .action(Message::ChangeTheme(Theme::Light)),
492        );
493        commands.push(
494            command("theme-catppuccin", "Catppuccin Mocha")
495                .description("Switch to Catppuccin Mocha theme")
496                .action(Message::ChangeTheme(Theme::CatppuccinMocha)),
497        );
498        commands.push(
499            command("theme-dracula", "Dracula")
500                .description("Switch to Dracula theme")
501                .action(Message::ChangeTheme(Theme::Dracula)),
502        );
503        commands.push(
504            command("theme-nord", "Nord")
505                .description("Switch to Nord theme")
506                .action(Message::ChangeTheme(Theme::Nord)),
507        );
508
509        commands
510    }
511
512    fn recompile(&mut self) {
513        match ShaderCompiler::compile(&self.shader_graph) {
514            Ok(shader) => {
515                self.compiled_shader = Some(shader);
516                self.compilation_error = None;
517            }
518            Err(err) => {
519                self.compiled_shader = None;
520                self.compilation_error = Some(format!("{:?}", err));
521            }
522        }
523    }
524}
525
526fn create_node_widget<'a>(
527    node_type: &shader_graph::nodes::ShaderNodeType,
528    theme: &'a Theme,
529) -> iced::Element<'a, Message> {
530    use iced_nodegraph::node_pin;
531
532    let name = node_type.name();
533    let inputs = node_type.inputs();
534    let outputs = node_type.outputs();
535
536    let palette = theme.extended_palette();
537
538    // Title bar - matching hello_world pattern exactly
539    let title_bar = container(text(name).size(13).width(Length::Fill))
540        .width(Length::Fill)
541        .padding([2, 8])
542        .style(move |_theme: &iced::Theme| container::Style {
543            background: None,
544            text_color: Some(palette.background.base.text),
545            ..container::Style::default()
546        });
547
548    // Build pin list - must match hello_world's column![] macro structure
549    // Pin IDs use sequential indices: inputs first (0..n), then outputs (n..n+m)
550    let pin_section = if inputs.is_empty() && outputs.is_empty() {
551        // No pins - minimal output indicator
552        container(
553            column![
554                node_pin(
555                    PinSide::Right,
556                    0usize,
557                    container(text("out").size(11)).padding([0, 8])
558                )
559                .direction(PinDirection::Output)
560            ]
561            .spacing(2),
562        )
563        .padding([6, 0])
564    } else {
565        // Build pins dynamically but wrap in container same way
566        let mut pin_elements: Vec<iced::Element<'a, Message>> = Vec::new();
567        let num_inputs = inputs.len();
568
569        for (i, input) in inputs.into_iter().enumerate() {
570            let label = input.name.clone();
571            pin_elements.push(create_typed_pin(
572                PinSide::Left,
573                i, // Pin ID = input index
574                label,
575                PinDirection::Input,
576                &input.socket_type,
577            ));
578        }
579
580        for (i, output) in outputs.into_iter().enumerate() {
581            let label = output.name.clone();
582            pin_elements.push(create_typed_pin(
583                PinSide::Right,
584                num_inputs + i, // Pin ID = num_inputs + output index
585                label,
586                PinDirection::Output,
587                &output.socket_type,
588            ));
589        }
590
591        container(iced::widget::Column::with_children(pin_elements).spacing(2)).padding([6, 0])
592    };
593
594    column![title_bar, pin_section].width(160.0).into()
595}
596
597/// Colors a node's pins by their socket data-type marker.
598fn pin_style(
599    theme: &Theme,
600    pin: &PinInfo<'_, usize, ::std::any::TypeId>,
601    _other: Option<&PinInfo<'_, usize, ::std::any::TypeId>>,
602    status: PinStatus,
603) -> PinStyle {
604    use std::any::TypeId;
605    let ty = *pin.info();
606    let color = if ty == TypeId::of::<colors::Float>() {
607        colors::SOCKET_FLOAT
608    } else if ty == TypeId::of::<colors::Vec2>() {
609        colors::SOCKET_VEC2
610    } else if ty == TypeId::of::<colors::Vec3>() {
611        colors::SOCKET_VEC3
612    } else if ty == TypeId::of::<colors::Vec4>() {
613        colors::SOCKET_VEC4
614    } else if ty == TypeId::of::<colors::Bool>() {
615        colors::SOCKET_BOOL
616    } else {
617        colors::SOCKET_INT
618    };
619    PinStyle {
620        color: color.into(),
621        ..default_pin_style(theme, status)
622    }
623}
624
625/// Creates a typed pin element based on the socket type.
626/// Uses marker types for TypeId-based connection matching.
627fn create_typed_pin<'a, Message: Clone + 'a>(
628    side: PinSide,
629    pin_id: usize,
630    label: String,
631    direction: PinDirection,
632    socket_type: &shader_graph::sockets::SocketType,
633) -> Element<'a, Message> {
634    use shader_graph::sockets::SocketType;
635
636    let content = container(text(label).size(11)).padding([0, 8]);
637
638    match socket_type {
639        SocketType::Float => node_pin(side, pin_id, content)
640            .direction(direction)
641            .info(::std::any::TypeId::of::<colors::Float>())
642            .into(),
643        SocketType::Vec2 => node_pin(side, pin_id, content)
644            .direction(direction)
645            .info(::std::any::TypeId::of::<colors::Vec2>())
646            .into(),
647        SocketType::Vec3 => node_pin(side, pin_id, content)
648            .direction(direction)
649            .info(::std::any::TypeId::of::<colors::Vec3>())
650            .into(),
651        SocketType::Vec4 => node_pin(side, pin_id, content)
652            .direction(direction)
653            .info(::std::any::TypeId::of::<colors::Vec4>())
654            .into(),
655        SocketType::Bool => node_pin(side, pin_id, content)
656            .direction(direction)
657            .info(::std::any::TypeId::of::<colors::Bool>())
658            .into(),
659        SocketType::Int => node_pin(side, pin_id, content)
660            .direction(direction)
661            .info(::std::any::TypeId::of::<colors::Int>())
662            .into(),
663    }
664}