1mod 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
48fn 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 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 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 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)), (PinRef::new(1, 1), PinRef::new(2, 0)), ],
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 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 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 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 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 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 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)), 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 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}