1pub 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 ToggleCommandPalette,
109 CommandPaletteInput(String),
110 CommandPaletteNavigateUp,
111 CommandPaletteNavigateDown,
112 CommandPaletteNavigate(usize),
113 CommandPaletteSelect(usize),
114 CommandPaletteConfirm,
115 CommandPaletteCancel,
116 SpawnNode(ShaderNodeType),
118 ChangeTheme(Theme),
120 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_open: bool,
137 command_input: String,
138 palette_selected_index: usize,
139 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 let visual_edges: Vec<(PinRef<usize, usize>, PinRef<usize, usize>)> = shader_graph
153 .connections
154 .iter()
155 .filter_map(|conn| {
156 let from_node = shader_graph.nodes.iter().find(|n| n.id == conn.from_node)?;
158 shader_graph.nodes.iter().find(|n| n.id == conn.to_node)?;
160
161 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 let from_visual_pin = from_node.inputs.len() + conn.from_socket;
173
174 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 fn spawn_position(&self) -> Point {
206 let screen_center_x = self.viewport_size.width / 2.0;
208 let screen_center_y = self.viewport_size.height / 2.0;
209
210 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 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 self.visual_edges.push((from, to));
223
224 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 let from_socket = from.pin_id.saturating_sub(from_node_data.inputs.len());
235 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 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 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 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 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 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 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 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 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 event::listen_with(|event, _status, _id| {
436 if let Event::Keyboard(keyboard::Event::KeyPressed { key, modifiers, .. }) = event {
437 if modifiers.command() && key == keyboard::Key::Named(Named::Space) {
439 return Some(Message::ToggleCommandPalette);
440 }
441
442 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 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 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 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 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 let pin_section = if inputs.is_empty() && outputs.is_empty() {
551 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 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, 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, 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
597fn 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
625fn 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}