|
| 1 | +use std::cell::Cell; |
| 2 | +use std::collections::HashMap; |
| 3 | +use std::rc::Rc; |
| 4 | + |
| 5 | +use slint::{Color, Model, ModelRc, SharedString, VecModel}; |
| 6 | +use slint_node_editor::{sugiyama_layout, Direction, NodeEditorController, SugiyamaConfig}; |
| 7 | + |
| 8 | +slint::include_modules!(); |
| 9 | + |
| 10 | +/// Deterministic LCG — produces the same sequence on first click after each |
| 11 | +/// restart, but varies across subsequent clicks within a session. |
| 12 | +fn random_f32() -> f32 { |
| 13 | + thread_local! { static SEED: Cell<u64> = Cell::new(12345); } |
| 14 | + SEED.with(|s| { |
| 15 | + let v = s.get().wrapping_mul(6364136223846793005).wrapping_add(1); |
| 16 | + s.set(v); |
| 17 | + ((v >> 33) as f32) / (u32::MAX as f32) |
| 18 | + }) |
| 19 | +} |
| 20 | + |
| 21 | +/// Build an index from node_id → model row for O(1) lookups. |
| 22 | +fn build_node_index(nodes: &VecModel<NodeData>) -> HashMap<i32, usize> { |
| 23 | + (0..nodes.row_count()) |
| 24 | + .filter_map(|i| nodes.row_data(i).map(|n| (n.id, i))) |
| 25 | + .collect() |
| 26 | +} |
| 27 | + |
| 28 | +fn main() { |
| 29 | + let window = MainWindow::new().unwrap(); |
| 30 | + let ctrl = NodeEditorController::new(); |
| 31 | + let w = window.as_weak(); |
| 32 | + |
| 33 | + // Create a DAG with 8 nodes: |
| 34 | + // |
| 35 | + // 1 ──► 2 ──► 4 ──► 7 |
| 36 | + // │ │ │ |
| 37 | + // ▼ ▼ ▼ |
| 38 | + // 3 ──► 5 ──► 6 ──► 8 |
| 39 | + // |
| 40 | + let nodes = Rc::new(VecModel::from(vec![ |
| 41 | + NodeData { id: 1, title: SharedString::from("Input"), x: 50.0, y: 50.0 }, |
| 42 | + NodeData { id: 2, title: SharedString::from("Parse"), x: 50.0, y: 120.0 }, |
| 43 | + NodeData { id: 3, title: SharedString::from("Validate"), x: 50.0, y: 190.0 }, |
| 44 | + NodeData { id: 4, title: SharedString::from("Transform"), x: 50.0, y: 260.0 }, |
| 45 | + NodeData { id: 5, title: SharedString::from("Filter"), x: 50.0, y: 330.0 }, |
| 46 | + NodeData { id: 6, title: SharedString::from("Merge"), x: 50.0, y: 400.0 }, |
| 47 | + NodeData { id: 7, title: SharedString::from("Format"), x: 50.0, y: 470.0 }, |
| 48 | + NodeData { id: 8, title: SharedString::from("Output"), x: 50.0, y: 540.0 }, |
| 49 | + ])); |
| 50 | + window.set_nodes(ModelRc::from(nodes.clone())); |
| 51 | + |
| 52 | + // DAG edges as (source_node_id, target_node_id) — single source of truth |
| 53 | + // Pin encoding: input = id*2, output = id*2+1 |
| 54 | + let dag_edges: Vec<(i32, i32)> = vec![ |
| 55 | + (1, 2), (1, 3), |
| 56 | + (2, 4), (2, 5), |
| 57 | + (3, 5), |
| 58 | + (4, 6), (4, 7), |
| 59 | + (5, 6), |
| 60 | + (6, 8), (7, 8), |
| 61 | + ]; |
| 62 | + |
| 63 | + // Derive LinkData from dag_edges so they can't drift out of sync |
| 64 | + let link_color = Color::from_argb_u8(255, 100, 180, 255); |
| 65 | + let link_data: Vec<LinkData> = dag_edges |
| 66 | + .iter() |
| 67 | + .enumerate() |
| 68 | + .map(|(i, &(src, dst))| LinkData { |
| 69 | + id: (i + 1) as i32, |
| 70 | + start_pin_id: src * 2 + 1, // output pin of source |
| 71 | + end_pin_id: dst * 2, // input pin of target |
| 72 | + color: link_color, |
| 73 | + line_width: 2.0, |
| 74 | + }) |
| 75 | + .collect(); |
| 76 | + window.set_links(ModelRc::from(Rc::new(VecModel::from(link_data)))); |
| 77 | + |
| 78 | + // Layout button callback |
| 79 | + window.on_layout_requested({ |
| 80 | + let nodes = nodes.clone(); |
| 81 | + let dag_edges = dag_edges.clone(); |
| 82 | + move || { |
| 83 | + let node_sizes: Vec<(i32, (f64, f64))> = (0..nodes.row_count()) |
| 84 | + .filter_map(|i| nodes.row_data(i)) |
| 85 | + .map(|n| (n.id, (120.0, 60.0))) |
| 86 | + .collect(); |
| 87 | + |
| 88 | + let mut config = SugiyamaConfig::default(); |
| 89 | + config.vertex_spacing = 60.0; |
| 90 | + config.direction = Direction::LeftToRight; |
| 91 | + |
| 92 | + let positions = sugiyama_layout(&dag_edges, &node_sizes, &config); |
| 93 | + |
| 94 | + let index = build_node_index(&nodes); |
| 95 | + let offset_x = 80.0_f32; |
| 96 | + let offset_y = 100.0_f32; |
| 97 | + for pos in &positions { |
| 98 | + if let Some(&row) = index.get(&pos.id) { |
| 99 | + if let Some(mut node) = nodes.row_data(row) { |
| 100 | + node.x = pos.x as f32 + offset_x; |
| 101 | + node.y = pos.y as f32 + offset_y; |
| 102 | + nodes.set_row_data(row, node); |
| 103 | + } |
| 104 | + } |
| 105 | + } |
| 106 | + } |
| 107 | + }); |
| 108 | + |
| 109 | + // Scramble button callback |
| 110 | + window.on_scramble_requested({ |
| 111 | + let nodes = nodes.clone(); |
| 112 | + move || { |
| 113 | + for i in 0..nodes.row_count() { |
| 114 | + if let Some(mut node) = nodes.row_data(i) { |
| 115 | + node.x = 50.0 + random_f32() * 800.0; |
| 116 | + node.y = 50.0 + random_f32() * 500.0; |
| 117 | + nodes.set_row_data(i, node); |
| 118 | + } |
| 119 | + } |
| 120 | + } |
| 121 | + }); |
| 122 | + |
| 123 | + // Core callbacks |
| 124 | + window.on_compute_link_path(ctrl.compute_link_path_callback()); |
| 125 | + window.on_node_drag_started(ctrl.node_drag_started_callback()); |
| 126 | + |
| 127 | + window.on_node_rect_changed({ |
| 128 | + let ctrl = ctrl.clone(); |
| 129 | + move |id, x, y, width, h| { |
| 130 | + ctrl.handle_node_rect(id, x, y, width, h); |
| 131 | + } |
| 132 | + }); |
| 133 | + |
| 134 | + window.on_pin_position_changed({ |
| 135 | + let ctrl = ctrl.clone(); |
| 136 | + move |pid, nid, ptype, x, y| { |
| 137 | + ctrl.handle_pin_position(pid, nid, ptype, x, y); |
| 138 | + } |
| 139 | + }); |
| 140 | + |
| 141 | + window.on_request_grid_update({ |
| 142 | + let ctrl = ctrl.clone(); |
| 143 | + let w = w.clone(); |
| 144 | + move || { |
| 145 | + if let Some(w) = w.upgrade() { |
| 146 | + w.set_grid_commands(ctrl.generate_initial_grid(w.get_width_(), w.get_height_())); |
| 147 | + } |
| 148 | + } |
| 149 | + }); |
| 150 | + |
| 151 | + window.on_update_viewport({ |
| 152 | + let ctrl = ctrl.clone(); |
| 153 | + let w = w.clone(); |
| 154 | + move |z, pan_x, pan_y| { |
| 155 | + if let Some(w) = w.upgrade() { |
| 156 | + ctrl.set_viewport(z, pan_x, pan_y); |
| 157 | + w.set_grid_commands(ctrl.generate_grid(w.get_width_(), w.get_height_(), pan_x, pan_y)); |
| 158 | + } |
| 159 | + } |
| 160 | + }); |
| 161 | + |
| 162 | + window.on_node_drag_ended({ |
| 163 | + let ctrl = ctrl.clone(); |
| 164 | + let nodes = nodes.clone(); |
| 165 | + move |delta_x, delta_y| { |
| 166 | + let node_id = ctrl.dragged_node_id(); |
| 167 | + for i in 0..nodes.row_count() { |
| 168 | + if let Some(mut node) = nodes.row_data(i) { |
| 169 | + if node.id == node_id { |
| 170 | + node.x += delta_x; |
| 171 | + node.y += delta_y; |
| 172 | + nodes.set_row_data(i, node); |
| 173 | + break; |
| 174 | + } |
| 175 | + } |
| 176 | + } |
| 177 | + } |
| 178 | + }); |
| 179 | + |
| 180 | + window.invoke_request_grid_update(); |
| 181 | + window.run().unwrap(); |
| 182 | +} |
0 commit comments