1use std::sync::Arc;
4
5use blinc_app::prelude::*;
6use blinc_app::windowed::WindowedContext;
7use blinc_core::State;
8use kyu_api::Database;
9
10use crate::canvas::{interaction, renderer};
11use crate::graph::{layout, loader};
12use crate::state::{CameraState, GraphData, SchemaData, Selection, Vec2};
13use crate::theme;
14use crate::transitions::CanvasMode;
15use crate::ui::{inspector, query_bar, sidebar};
16
17pub struct InitialData {
19 pub schema: SchemaData,
20 pub graph: GraphData,
21 pub layout_temp: f32,
22}
23
24pub fn load_initial_data(db: &Database) -> InitialData {
26 let schema = loader::load_schema(db);
27
28 let conn = db.connect();
29 let mut graph = match loader::load_full_graph(&conn, db) {
30 Ok(g) => g,
31 Err(_) => GraphData::new(),
32 };
33
34 if !graph.nodes.is_empty() {
36 let mut temp = layout::reset_temperature(graph.nodes.len());
37 while layout::layout_batch(&mut graph, &mut temp, 10) {}
38 layout::center_graph(&mut graph);
39 }
40
41 InitialData {
42 schema,
43 graph,
44 layout_temp: 0.0,
45 }
46}
47
48pub fn build_ui(
53 ctx: &WindowedContext,
54 db: Arc<Database>,
55 initial: &InitialData,
56) -> impl ElementBuilder + use<> {
57 let graph_data = ctx.use_state_keyed("graph_data", || initial.graph.clone());
59 let camera = ctx.use_state_keyed("camera", CameraState::default);
60 let selection = ctx.use_state_keyed("selection", || Selection::None);
61 let query_input = text_input_state_with_placeholder("MATCH (n) RETURN n LIMIT 100");
62 let query_error = ctx.use_state_keyed("query_error", || Option::<String>::None);
63 let schema_data = ctx.use_state_keyed("schema_data", || initial.schema.clone());
64 let layout_temp = ctx.use_state_keyed("layout_temp", || initial.layout_temp);
65 let dragged_node = ctx.use_state_keyed("dragged_node", || Option::<usize>::None);
66 let prev_drag = ctx.use_state_keyed("prev_drag", || (0.0_f32, 0.0_f32));
67
68 let sidebar_on_click = {
70 let gd = graph_data.clone();
71 let cam = camera.clone();
72 let sel = selection.clone();
73 move |table_name: &str| {
74 let graph = gd.get();
75 let positions: Vec<Vec2> = {
78 let node_matches: Vec<Vec2> = graph
79 .nodes
80 .iter()
81 .filter(|n| n.label == table_name)
82 .map(|n| n.pos)
83 .collect();
84 if !node_matches.is_empty() {
85 node_matches
86 } else {
87 graph
89 .edges
90 .iter()
91 .filter(|e| e.rel_type == table_name)
92 .flat_map(|e| [graph.nodes[e.src].pos, graph.nodes[e.dst].pos])
93 .collect()
94 }
95 };
96 if positions.is_empty() {
97 return;
98 }
99 let n = positions.len() as f32;
100 let cx = positions.iter().map(|p| p.x).sum::<f32>() / n;
101 let cy = positions.iter().map(|p| p.y).sum::<f32>() / n;
102 let mut camera = cam.get();
105 camera.offset_x = -cx * camera.zoom;
106 camera.offset_y = -cy * camera.zoom;
107 cam.set(camera);
108 sel.set(Selection::None);
109 }
110 };
111
112 let conn_query = Arc::new(db.connect());
114 let on_execute = {
115 let conn = conn_query.clone();
116 let qe = query_error.clone();
117 let qi = query_input.clone();
118 move |_: ()| {
119 let cypher = qi.lock().unwrap().value.clone();
120 if cypher.trim().is_empty() {
121 return;
122 }
123 match conn.query(&cypher) {
124 Ok(_result) => {
125 qe.set(None);
126 }
128 Err(e) => {
129 qe.set(Some(format!("{e}")));
130 }
131 }
132 }
133 };
134
135 let canvas_view = build_canvas_view(
137 graph_data.clone(),
138 camera.clone(),
139 selection.clone(),
140 layout_temp.clone(),
141 dragged_node.clone(),
142 prev_drag.clone(),
143 ctx.width,
144 ctx.height,
145 );
146
147 div()
159 .id("app")
160 .flex_col()
161 .w(ctx.width)
162 .h(ctx.height)
163 .child(
164 div()
165 .id("header")
166 .h(40.0)
167 .flex_row()
168 .items_center()
169 .p(4.0)
170 .bg(theme::BG)
171 .border_bottom(1.0, theme::BORDER)
172 .child(h2("KyuGraph Visualizer").color(theme::ACCENT)),
173 )
174 .child(
175 div()
176 .id("main")
177 .flex_grow()
178 .child(
179 div()
180 .id("sidebar")
181 .flex_col()
182 .w(280.0)
183 .flex_shrink()
184 .border_right(1.0, theme::BORDER)
185 .child(sidebar::sidebar_view(schema_data.clone(), sidebar_on_click))
186 .child(inspector::inspector_view(
187 selection.clone(),
188 graph_data.clone(),
189 )),
190 )
191 .child(div().id("canvas-wrap").flex_grow().child(canvas_view)),
192 )
193 .child(query_bar::query_bar_view(
194 query_error,
195 query_input.clone(),
196 on_execute,
197 ))
198}
199
200#[allow(clippy::too_many_arguments)]
202fn build_canvas_view(
203 graph_data: State<GraphData>,
204 camera: State<CameraState>,
205 selection: State<Selection>,
206 _layout_temp: State<f32>,
207 dragged_node: State<Option<usize>>,
208 prev_drag: State<(f32, f32)>,
209 window_w: f32,
210 window_h: f32,
211) -> impl ElementBuilder {
212 let gd = graph_data.clone();
213 let cam = camera.clone();
214 let sel = selection.clone();
215
216 stateful::<CanvasMode>()
217 .deps([
218 graph_data.signal_id(),
219 camera.signal_id(),
220 selection.signal_id(),
221 ])
222 .on_state(move |_ctx| {
223 let gd = gd.clone();
224 let cam = cam.clone();
225 let sel = sel.clone();
226
227 div().id("canvas-area").w_full().h_full().child(
228 canvas(move |draw_ctx, bounds| {
229 let graph = gd.get();
230 let camera = cam.get();
231 let sel = sel.get();
232 renderer::render_graph(
233 draw_ctx,
234 bounds.width,
235 bounds.height,
236 &graph,
237 &camera,
238 &sel,
239 );
240 })
241 .w_full()
242 .h_full(),
243 )
244 })
245 .on_click({
246 let gd = graph_data.clone();
247 let cam = camera.clone();
248 let sel = selection.clone();
249 move |evt| {
250 let graph = gd.get();
251 let camera = cam.get();
252 let world = interaction::screen_to_world(
253 Vec2::new(evt.local_x, evt.local_y),
254 &camera,
255 window_w - 280.0,
256 window_h - 50.0,
257 );
258 let hit = interaction::hit_test(world, &graph);
259 sel.set(hit);
260 }
261 })
262 .on_mouse_down({
263 let gd = graph_data.clone();
264 let cam = camera.clone();
265 let dn = dragged_node.clone();
266 let pd = prev_drag.clone();
267 move |evt| {
268 pd.set((0.0, 0.0));
269 let graph = gd.get();
270 let camera = cam.get();
271 let world = interaction::screen_to_world(
272 Vec2::new(evt.local_x, evt.local_y),
273 &camera,
274 window_w - 280.0,
275 window_h - 50.0,
276 );
277 if let Some(idx) = interaction::hit_test_node(world, &graph) {
278 dn.set(Some(idx));
279 } else {
280 dn.set(None);
281 }
282 }
283 })
284 .on_drag({
285 let gd = graph_data.clone();
286 let cam = camera.clone();
287 let dn = dragged_node.clone();
288 let pd = prev_drag.clone();
289 move |evt| {
290 let (prev_x, prev_y) = pd.get();
292 let dx = evt.drag_delta_x - prev_x;
293 let dy = evt.drag_delta_y - prev_y;
294 pd.set((evt.drag_delta_x, evt.drag_delta_y));
295
296 if let Some(idx) = dn.get() {
297 let mut graph = gd.get();
298 let camera = cam.get();
299 if let Some(node) = graph.nodes.get_mut(idx) {
300 node.pos.x += dx / camera.zoom;
301 node.pos.y += dy / camera.zoom;
302 node.pinned = true;
303 }
304 gd.set(graph);
305 } else {
306 let mut camera = cam.get();
307 camera.offset_x += dx;
308 camera.offset_y += dy;
309 cam.set(camera);
310 }
311 }
312 })
313 .on_drag_end({
314 let dn = dragged_node.clone();
315 move |_| {
316 dn.set(None);
317 }
318 })
319 .on_scroll({
320 let cam = camera.clone();
321 move |evt| {
322 let mut camera = cam.get();
323 let zoom_delta = evt.scroll_delta_y * 0.001;
324 camera.zoom *= 1.0 + zoom_delta;
325 camera.clamp_zoom();
326 cam.set(camera);
327 }
328 })
329 .w_full()
330 .h_full()
331}