ui.rs (6046B)
1 use egui::{Align, Label, Pos2, Rect, Shape, Stroke, TextWrapMode, epaint::CubicBezierShape, vec2}; 2 use jsoncanvas::{ 3 FileNode, GroupNode, LinkNode, Node, NodeId, TextNode, 4 edge::{Edge, Side}, 5 node::GenericNode, 6 }; 7 use std::collections::HashMap; 8 use std::ops::Neg; 9 10 fn node_rect(node: &GenericNode) -> Rect { 11 let x = node.x as f32; 12 let y = node.y as f32; 13 let width = node.width as f32; 14 let height = node.height as f32; 15 16 let min = Pos2::new(x, y); 17 let max = Pos2::new(x + width, y + height); 18 19 Rect::from_min_max(min, max) 20 } 21 22 fn side_point(side: &Side, node: &GenericNode) -> Pos2 { 23 let rect = node_rect(node); 24 25 match side { 26 Side::Top => rect.center_top(), 27 Side::Left => rect.left_center(), 28 Side::Right => rect.right_center(), 29 Side::Bottom => rect.center_bottom(), 30 } 31 } 32 33 /// a unit vector pointing outward from the given side 34 fn side_tangent(side: &Side) -> egui::Vec2 { 35 match side { 36 Side::Top => vec2(0.0, -1.0), 37 Side::Bottom => vec2(0.0, 1.0), 38 Side::Left => vec2(-1.0, 0.0), 39 Side::Right => vec2(1.0, 0.0), 40 } 41 } 42 43 pub fn edge_ui( 44 ui: &mut egui::Ui, 45 nodes: &HashMap<NodeId, Node>, 46 edge: &Edge, 47 ) -> Option<egui::Response> { 48 let from_node = nodes.get(edge.from_node())?; 49 let to_node = nodes.get(edge.to_node())?; 50 let to_side = edge.to_side()?; 51 let from_side = edge.from_side()?; 52 53 // anchor from-side 54 let p0 = side_point(from_side, from_node.node()); 55 56 // anchor b 57 let to_anchor = side_point(to_side, to_node.node()); 58 59 // to-point is slightly offset to accomidate arrow 60 let p3 = to_anchor + side_tangent(to_side) * 2.0; 61 62 // bend debug 63 //let bend = debug_slider(ui, ui.id().with("bend"), p3, 0.25, 0.0..=1.0); 64 let bend = 0.28; 65 66 // How far to pull the tangents. 67 // ¼ of the distance between anchors feels very “Obsidian”. 68 let d = (p3 - p0).length() * bend; 69 70 // c1 = anchor A + (outward tangent) * d 71 let c1 = p0 + side_tangent(from_side) * d; 72 73 // c2 = anchor B + (inward tangent) * d 74 let c2 = p3 - side_tangent(to_side).neg() * d; 75 76 let color = ui.visuals().noninteractive().bg_stroke.color; 77 let stroke = egui::Stroke::new(4.0, color); 78 let bezier = CubicBezierShape::from_points_stroke([p0, c1, c2, p3], false, color, stroke); 79 80 ui.painter().add(Shape::CubicBezier(bezier)); 81 arrow_ui(ui, to_side, to_anchor, color); 82 83 None 84 } 85 86 /// Paint a tiny triangular “arrow”. 87 /// 88 /// * `ui` – the egui `Ui` you’re painting in 89 /// * `side` – which edge of the box we’re attaching to 90 /// * `point` – the exact spot on that edge the arrow’s tip should touch 91 /// * `fill` – colour to fill the arrow with (usually your popup’s background) 92 pub fn arrow_ui(ui: &mut egui::Ui, side: &Side, point: Pos2, fill: egui::Color32) { 93 let len: f32 = 12.0; // distance from tip to base 94 let width: f32 = 16.0; // length of the base 95 let stroke: f32 = 1.0; // length of the base 96 97 let verts = match side { 98 Side::Top => [ 99 point, // tip 100 Pos2::new(point.x - width * 0.5, point.y - len), // base‑left (above) 101 Pos2::new(point.x + width * 0.5, point.y - len), // base‑right (above) 102 ], 103 Side::Bottom => [ 104 point, 105 Pos2::new(point.x + width * 0.5, point.y + len), // below 106 Pos2::new(point.x - width * 0.5, point.y + len), 107 ], 108 Side::Left => [ 109 point, 110 Pos2::new(point.x - len, point.y + width * 0.5), // left 111 Pos2::new(point.x - len, point.y - width * 0.5), 112 ], 113 Side::Right => [ 114 point, 115 Pos2::new(point.x + len, point.y - width * 0.5), // right 116 Pos2::new(point.x + len, point.y + width * 0.5), 117 ], 118 }; 119 120 ui.painter().add(egui::Shape::convex_polygon( 121 verts.to_vec(), 122 fill, 123 Stroke::new(stroke, fill), // add a stroke here if you want an outline 124 )); 125 } 126 127 pub fn node_ui(ui: &mut egui::Ui, node: &Node) -> egui::Response { 128 match node { 129 Node::Text(text_node) => text_node_ui(ui, text_node), 130 Node::File(file_node) => file_node_ui(ui, file_node), 131 Node::Link(link_node) => link_node_ui(ui, link_node), 132 Node::Group(group_node) => group_node_ui(ui, group_node), 133 } 134 } 135 136 fn text_node_ui(ui: &mut egui::Ui, node: &TextNode) -> egui::Response { 137 node_box_ui(ui, node.node(), |ui| { 138 egui::ScrollArea::vertical() 139 .show(ui, |ui| { 140 ui.with_layout(egui::Layout::left_to_right(Align::Min), |ui| { 141 ui.add(Label::new(node.text()).wrap_mode(TextWrapMode::Wrap)) 142 }) 143 }) 144 .inner 145 .response 146 }) 147 } 148 149 fn file_node_ui(ui: &mut egui::Ui, node: &FileNode) -> egui::Response { 150 node_box_ui(ui, node.node(), |ui| ui.label("file node")) 151 } 152 153 fn link_node_ui(ui: &mut egui::Ui, node: &LinkNode) -> egui::Response { 154 node_box_ui(ui, node.node(), |ui| ui.label("link node")) 155 } 156 157 fn group_node_ui(ui: &mut egui::Ui, node: &GroupNode) -> egui::Response { 158 node_box_ui(ui, node.node(), |ui| ui.label("group node")) 159 } 160 161 fn node_box_ui( 162 ui: &mut egui::Ui, 163 node: &GenericNode, 164 contents: impl FnOnce(&mut egui::Ui) -> egui::Response, 165 ) -> egui::Response { 166 let pos = node_rect(node); 167 168 ui.put(pos, |ui: &mut egui::Ui| { 169 egui::Frame::default() 170 .fill(ui.visuals().noninteractive().weak_bg_fill) 171 .inner_margin(egui::Margin::same(16)) 172 .corner_radius(egui::CornerRadius::same(10)) 173 .stroke(egui::Stroke::new( 174 2.0, 175 ui.visuals().noninteractive().bg_stroke.color, 176 )) 177 .show(ui, |ui| { 178 let rect = ui.available_rect_before_wrap(); 179 ui.allocate_at_least(ui.available_size(), egui::Sense::click()); 180 ui.put(rect, contents); 181 }) 182 .response 183 }) 184 }