windjammer_ui/components/generated/
node_graph.rs

1#![allow(clippy::all)]
2#![allow(noop_method_call)]
3use super::traits::Renderable;
4
5#[derive(Clone, Debug, PartialEq, Copy)]
6pub enum PinType {
7    Flow,
8    Bool,
9    Int,
10    Float,
11    Vec2,
12    Vec3,
13    Vec4,
14    Color,
15    Texture,
16    Object,
17    Any,
18}
19
20#[derive(Debug, Clone, PartialEq)]
21pub struct NodePin {
22    pub id: String,
23    pub name: String,
24    pub pin_type: PinType,
25    pub is_input: bool,
26    pub connected_to: Option<String>,
27    pub default_value: String,
28}
29
30impl NodePin {
31    #[inline]
32    pub fn input(id: String, name: String, pin_type: PinType) -> NodePin {
33        NodePin {
34            id,
35            name,
36            pin_type,
37            is_input: true,
38            connected_to: None,
39            default_value: "".to_string(),
40        }
41    }
42    #[inline]
43    pub fn output(id: String, name: String, pin_type: PinType) -> NodePin {
44        NodePin {
45            id,
46            name,
47            pin_type,
48            is_input: false,
49            connected_to: None,
50            default_value: "".to_string(),
51        }
52    }
53    #[inline]
54    pub fn default_value(mut self, value: String) -> NodePin {
55        self.default_value = value;
56        self
57    }
58    #[inline]
59    pub fn connect(mut self, target: String) -> NodePin {
60        self.connected_to = Some(target.to_string());
61        self
62    }
63    #[inline]
64    pub fn get_color(&self) -> String {
65        match self.pin_type {
66            PinType::Flow => "#ffffff".to_string(),
67            PinType::Bool => "#e94560".to_string(),
68            PinType::Int => "#00d9ff".to_string(),
69            PinType::Float => "#4ade80".to_string(),
70            PinType::Vec2 => "#facc15".to_string(),
71            PinType::Vec3 => "#f59e0b".to_string(),
72            PinType::Vec4 => "#a855f7".to_string(),
73            PinType::Color => "#ec4899".to_string(),
74            PinType::Texture => "#fb923c".to_string(),
75            PinType::Object => "#3b82f6".to_string(),
76            PinType::Any => "#888888".to_string(),
77        }
78    }
79}
80
81#[derive(Clone, Debug, PartialEq, Copy)]
82pub enum NodeCategory {
83    Math,
84    Logic,
85    Texture,
86    Color,
87    Vector,
88    Flow,
89    Event,
90    Variable,
91    Custom,
92}
93
94#[derive(Debug, Clone)]
95pub struct GraphNode {
96    pub id: String,
97    pub title: String,
98    pub category: NodeCategory,
99    pub x: f32,
100    pub y: f32,
101    pub inputs: Vec<NodePin>,
102    pub outputs: Vec<NodePin>,
103    pub collapsed: bool,
104    pub preview_enabled: bool,
105}
106
107impl GraphNode {
108    #[inline]
109    pub fn new(id: String, title: String, category: NodeCategory) -> GraphNode {
110        GraphNode {
111            id,
112            title,
113            category,
114            x: 0.0,
115            y: 0.0,
116            inputs: Vec::new(),
117            outputs: Vec::new(),
118            collapsed: false,
119            preview_enabled: false,
120        }
121    }
122    #[inline]
123    pub fn position(mut self, x: f32, y: f32) -> GraphNode {
124        self.x = x;
125        self.y = y;
126        self
127    }
128    #[inline]
129    pub fn input(mut self, pin: NodePin) -> GraphNode {
130        self.inputs.push(pin);
131        self
132    }
133    #[inline]
134    pub fn output(mut self, pin: NodePin) -> GraphNode {
135        self.outputs.push(pin);
136        self
137    }
138    #[inline]
139    pub fn collapsed(mut self, collapsed: bool) -> GraphNode {
140        self.collapsed = collapsed;
141        self
142    }
143    #[inline]
144    pub fn preview(mut self, enabled: bool) -> GraphNode {
145        self.preview_enabled = enabled;
146        self
147    }
148    #[inline]
149    pub fn get_category_color(&self) -> String {
150        match self.category {
151            NodeCategory::Math => "#4ade80".to_string(),
152            NodeCategory::Logic => "#e94560".to_string(),
153            NodeCategory::Texture => "#fb923c".to_string(),
154            NodeCategory::Color => "#ec4899".to_string(),
155            NodeCategory::Vector => "#facc15".to_string(),
156            NodeCategory::Flow => "#ffffff".to_string(),
157            NodeCategory::Event => "#3b82f6".to_string(),
158            NodeCategory::Variable => "#a855f7".to_string(),
159            NodeCategory::Custom => "#888888".to_string(),
160        }
161    }
162}
163
164impl Renderable for GraphNode {
165    #[inline]
166    fn render(self) -> String {
167        let header_color = self.get_category_color();
168        let mut inputs_html = "".to_string();
169        for pin in &self.inputs {
170            let color = pin.get_color();
171            let connected_class = match pin.connected_to.clone() {
172                Some(_) => "connected".to_string(),
173                None => "".to_string(),
174            };
175            inputs_html += format!(
176                "
177                <div class='node-pin input {}'>
178                    <div class='pin-socket' style='background: {};' data-pin='{}'></div>
179                    <span class='pin-name'>{}</span>
180                </div>
181            ",
182                connected_class,
183                color,
184                pin.id,
185                pin.name.clone()
186            )
187            .as_str();
188        }
189        let mut outputs_html = "".to_string();
190        for pin in &self.outputs {
191            let color = pin.get_color();
192            let connected_class = match pin.connected_to.clone() {
193                Some(_) => "connected".to_string(),
194                None => "".to_string(),
195            };
196            outputs_html += format!(
197                "
198                <div class='node-pin output {}'>
199                    <span class='pin-name'>{}</span>
200                    <div class='pin-socket' style='background: {};' data-pin='{}'></div>
201                </div>
202            ",
203                connected_class,
204                pin.name.clone(),
205                color,
206                pin.id
207            )
208            .as_str();
209        }
210        let preview_html = {
211            if self.preview_enabled {
212                "<div class='node-preview'><canvas class='preview-canvas'></canvas></div>"
213                    .to_string()
214            } else {
215                "".to_string()
216            }
217        };
218        format!(
219            "
220            <div class='graph-node' id='{}' style='left: {}px; top: {}px;'>
221                <div class='node-header' style='background: {};'>
222                    <span class='node-title'>{}</span>
223                    <div class='node-actions'>
224                        <button class='node-btn preview' title='Preview'>👁</button>
225                        <button class='node-btn collapse' title='Collapse'>−</button>
226                    </div>
227                </div>
228                <div class='node-body'>
229                    <div class='node-inputs'>
230                        {}
231                    </div>
232                    <div class='node-outputs'>
233                        {}
234                    </div>
235                </div>
236                {}
237            </div>
238        ",
239            self.id,
240            self.x,
241            self.y,
242            header_color,
243            self.title,
244            inputs_html,
245            outputs_html,
246            preview_html
247        )
248    }
249}
250
251#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
252pub struct NodeConnection {
253    pub from_node: String,
254    pub from_pin: String,
255    pub to_node: String,
256    pub to_pin: String,
257}
258
259#[derive(Debug, Clone)]
260pub struct NodeGraph {
261    pub width: i32,
262    pub height: i32,
263    pub nodes: Vec<GraphNode>,
264    pub connections: Vec<NodeConnection>,
265    pub zoom: f32,
266    pub pan_x: f32,
267    pub pan_y: f32,
268    pub show_grid: bool,
269    pub on_change: String,
270}
271
272impl NodeGraph {
273    #[inline]
274    pub fn new() -> NodeGraph {
275        NodeGraph {
276            width: 800,
277            height: 600,
278            nodes: Vec::new(),
279            connections: Vec::new(),
280            zoom: 1.0,
281            pan_x: 0.0,
282            pan_y: 0.0,
283            show_grid: true,
284            on_change: "".to_string(),
285        }
286    }
287    #[inline]
288    pub fn size(mut self, width: i32, height: i32) -> NodeGraph {
289        self.width = width;
290        self.height = height;
291        self
292    }
293    #[inline]
294    pub fn node(mut self, node: GraphNode) -> NodeGraph {
295        self.nodes.push(node);
296        self
297    }
298    #[inline]
299    pub fn connect(
300        mut self,
301        from_node: String,
302        from_pin: String,
303        to_node: String,
304        to_pin: String,
305    ) -> NodeGraph {
306        self.connections.push(NodeConnection {
307            from_node,
308            from_pin,
309            to_node,
310            to_pin,
311        });
312        self
313    }
314    #[inline]
315    pub fn zoom(mut self, zoom: f32) -> NodeGraph {
316        self.zoom = zoom;
317        self
318    }
319    #[inline]
320    pub fn pan(mut self, x: f32, y: f32) -> NodeGraph {
321        self.pan_x = x;
322        self.pan_y = y;
323        self
324    }
325}
326
327impl Renderable for NodeGraph {
328    #[inline]
329    fn render(self) -> String {
330        let mut nodes_html = "".to_string();
331        for n in &self.nodes {
332            nodes_html = format!(
333                "{}{}{}",
334                nodes_html,
335                n.clone().render().as_str(),
336                "
337"
338            );
339        }
340        let mut connections_html = "".to_string();
341        for c in &self.connections {
342            connections_html += format!(
343                "
344                <path class='node-connection' 
345                      data-from='{}:{}' 
346                      data-to='{}:{}'/>
347            ",
348                c.from_node.clone(),
349                c.from_pin.clone(),
350                c.to_node.clone(),
351                c.to_pin.clone()
352            )
353            .as_str();
354        }
355        let grid_class = {
356            if self.show_grid {
357                "show-grid".to_string()
358            } else {
359                "".to_string()
360            }
361        };
362        format!(
363            "
364            <div class='node-graph {}' style='width: {}px; height: {}px;'>
365                <div class='graph-toolbar'>
366                    <button onclick='addNode()'>+ Add Node</button>
367                    <span class='toolbar-sep'></span>
368                    <button onclick='zoomIn()'>🔍+</button>
369                    <button onclick='zoomOut()'>🔍−</button>
370                    <button onclick='fitAll()'>⊞</button>
371                    <span class='zoom-level'>{:.0}%</span>
372                </div>
373                <div class='graph-canvas' 
374                     style='transform: scale({}) translate({}px, {}px);'>
375                    <svg class='connections-layer'>
376                        {}
377                    </svg>
378                    <div class='nodes-layer'>
379                        {}
380                    </div>
381                </div>
382                <div class='graph-minimap'>
383                    <div class='minimap-viewport'></div>
384                </div>
385            </div>
386        ",
387            grid_class,
388            self.width,
389            self.height,
390            self.zoom * 100.0,
391            self.zoom,
392            self.pan_x,
393            self.pan_y,
394            connections_html,
395            nodes_html
396        )
397    }
398}
399
400#[inline]
401pub fn node_graph_styles() -> String {
402    "
403    .node-graph {
404        position: relative;
405        background: #0a0a1a;
406        border-radius: 8px;
407        overflow: hidden;
408    }
409    
410    .node-graph.show-grid {
411        background-image: 
412            linear-gradient(rgba(255,255,255,0.03) 1px, transparent 1px),
413            linear-gradient(90deg, rgba(255,255,255,0.03) 1px, transparent 1px);
414        background-size: 20px 20px;
415    }
416    
417    .graph-toolbar {
418        position: absolute;
419        top: 8px;
420        left: 8px;
421        display: flex;
422        gap: 4px;
423        padding: 4px;
424        background: rgba(22, 33, 62, 0.9);
425        border-radius: 4px;
426        z-index: 100;
427    }
428    
429    .graph-toolbar button {
430        padding: 4px 8px;
431        border: none;
432        border-radius: 4px;
433        background: #0f3460;
434        color: #888;
435        cursor: pointer;
436    }
437    
438    .graph-toolbar button:hover {
439        background: #1a4a8a;
440        color: #e0e0e0;
441    }
442    
443    .toolbar-sep {
444        width: 1px;
445        background: #333;
446    }
447    
448    .zoom-level {
449        padding: 0 8px;
450        font-size: 12px;
451        color: #666;
452    }
453    
454    .graph-canvas {
455        position: absolute;
456        top: 0;
457        left: 0;
458        width: 100%;
459        height: 100%;
460        transform-origin: center center;
461    }
462    
463    .connections-layer {
464        position: absolute;
465        top: 0;
466        left: 0;
467        width: 100%;
468        height: 100%;
469        pointer-events: none;
470    }
471    
472    .node-connection {
473        fill: none;
474        stroke: #666;
475        stroke-width: 2;
476    }
477    
478    .nodes-layer {
479        position: absolute;
480        top: 0;
481        left: 0;
482    }
483    
484    /* Graph Node */
485    .graph-node {
486        position: absolute;
487        min-width: 180px;
488        background: #16213e;
489        border-radius: 8px;
490        box-shadow: 0 4px 12px rgba(0,0,0,0.3);
491        user-select: none;
492    }
493    
494    .node-header {
495        display: flex;
496        align-items: center;
497        justify-content: space-between;
498        padding: 8px 12px;
499        border-radius: 8px 8px 0 0;
500        cursor: move;
501    }
502    
503    .node-title {
504        font-size: 12px;
505        font-weight: 600;
506        color: #1a1a2e;
507    }
508    
509    .node-actions {
510        display: flex;
511        gap: 4px;
512    }
513    
514    .node-btn {
515        width: 20px;
516        height: 20px;
517        border: none;
518        background: rgba(0,0,0,0.2);
519        border-radius: 4px;
520        font-size: 10px;
521        cursor: pointer;
522        color: rgba(0,0,0,0.6);
523    }
524    
525    .node-btn:hover {
526        background: rgba(0,0,0,0.4);
527        color: rgba(0,0,0,0.8);
528    }
529    
530    .node-body {
531        display: flex;
532        justify-content: space-between;
533        padding: 8px 0;
534    }
535    
536    .node-inputs, .node-outputs {
537        display: flex;
538        flex-direction: column;
539        gap: 4px;
540    }
541    
542    .node-pin {
543        display: flex;
544        align-items: center;
545        gap: 8px;
546        padding: 4px 12px;
547        cursor: pointer;
548    }
549    
550    .node-pin.input {
551        flex-direction: row;
552    }
553    
554    .node-pin.output {
555        flex-direction: row-reverse;
556    }
557    
558    .pin-socket {
559        width: 12px;
560        height: 12px;
561        border-radius: 50%;
562        border: 2px solid rgba(255,255,255,0.3);
563        transition: transform 0.15s;
564    }
565    
566    .node-pin:hover .pin-socket {
567        transform: scale(1.3);
568        border-color: white;
569    }
570    
571    .node-pin.connected .pin-socket {
572        border-color: white;
573    }
574    
575    .pin-name {
576        font-size: 11px;
577        color: #888;
578    }
579    
580    .node-preview {
581        padding: 8px;
582        border-top: 1px solid rgba(255,255,255,0.1);
583    }
584    
585    .preview-canvas {
586        width: 100%;
587        height: 60px;
588        background: #0a0a1a;
589        border-radius: 4px;
590    }
591    
592    /* Minimap */
593    .graph-minimap {
594        position: absolute;
595        bottom: 8px;
596        right: 8px;
597        width: 150px;
598        height: 100px;
599        background: rgba(22, 33, 62, 0.9);
600        border-radius: 4px;
601        border: 1px solid #333;
602    }
603    
604    .minimap-viewport {
605        position: absolute;
606        border: 2px solid #e94560;
607        background: rgba(233, 69, 96, 0.1);
608    }
609    "
610    .to_string()
611}