notebook: draw edges and arrows

Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
William Casarin
2025-07-19 10:30:12 -07:00
parent f592015c0c
commit 17f72f6127
2 changed files with 167 additions and 12 deletions

View File

@@ -0,0 +1,26 @@
/*
fn debug_slider(
ui: &mut egui::Ui,
id: egui::Id,
point: Pos2,
initial: f32,
range: std::ops::RangeInclusive<f32>,
) -> f32 {
let mut val = ui.data_mut(|d| *d.get_temp_mut_or::<f32>(id, initial));
let nudge = vec2(10.0, 10.0);
let slider = Rect::from_min_max(point - nudge, point + nudge);
let label = Rect::from_min_max(point + nudge * 2.0, point - nudge * 2.0);
let old_val = val;
ui.put(slider, egui::Slider::new(&mut val, range));
ui.put(label, egui::Label::new(format!("{val}")));
if val != old_val {
ui.data_mut(|d| d.insert_temp(id, val))
}
val
}
*/

View File

@@ -1,6 +1,12 @@
use egui::{Align, Label, Pos2, Rect, TextWrapMode};
use jsoncanvas::{FileNode, GroupNode, JsonCanvas, LinkNode, Node, TextNode, node::*};
use egui::{Align, Label, Pos2, Rect, Shape, Stroke, TextWrapMode, epaint::CubicBezierShape, vec2};
use jsoncanvas::{
FileNode, GroupNode, JsonCanvas, LinkNode, Node, NodeId, TextNode,
edge::{Edge, Side},
node::GenericNode,
};
use notedeck::{AppAction, AppContext};
use std::collections::HashMap;
use std::ops::Neg;
pub struct Notebook {
canvas: JsonCanvas,
@@ -8,6 +14,12 @@ pub struct Notebook {
loaded: bool,
}
impl Notebook {
pub fn new() -> Self {
Notebook::default()
}
}
impl Default for Notebook {
fn default() -> Self {
Notebook {
@@ -28,15 +40,138 @@ impl notedeck::App for Notebook {
}
egui::Scene::new().show(ui, &mut self.scene_rect, |ui| {
// render nodes
for (_node_id, node) in self.canvas.get_nodes().iter() {
let _resp = node_ui(ui, node);
}
// render edges
for (_edge_id, edge) in self.canvas.get_edges().iter() {
let _resp = edge_ui(ui, self.canvas.get_nodes(), edge);
}
});
None
}
}
fn node_rect(node: &GenericNode) -> Rect {
let x = node.x as f32;
let y = node.y as f32;
let width = node.width as f32;
let height = node.height as f32;
let min = Pos2::new(x, y);
let max = Pos2::new(x + width, y + height);
Rect::from_min_max(min, max)
}
fn side_point(side: &Side, node: &GenericNode) -> Pos2 {
let rect = node_rect(node);
match side {
Side::Top => rect.center_top(),
Side::Left => rect.left_center(),
Side::Right => rect.right_center(),
Side::Bottom => rect.center_bottom(),
}
}
/// a unit vector pointing outward from the given side
fn side_tangent(side: &Side) -> egui::Vec2 {
match side {
Side::Top => vec2(0.0, -1.0),
Side::Bottom => vec2(0.0, 1.0),
Side::Left => vec2(-1.0, 0.0),
Side::Right => vec2(1.0, 0.0),
}
}
fn edge_ui(
ui: &mut egui::Ui,
nodes: &HashMap<NodeId, Node>,
edge: &Edge,
) -> Option<egui::Response> {
let from_node = nodes.get(edge.from_node())?;
let to_node = nodes.get(edge.to_node())?;
let to_side = edge.to_side()?;
let from_side = edge.from_side()?;
// anchor from-side
let p0 = side_point(from_side, from_node.node());
// anchor b
let to_anchor = side_point(to_side, to_node.node());
// to-point is slightly offset to accomidate arrow
let p3 = to_anchor + side_tangent(to_side) * 2.0;
// bend debug
//let bend = debug_slider(ui, ui.id().with("bend"), p3, 0.25, 0.0..=1.0);
let bend = 0.28;
// How far to pull the tangents.
// ¼ of the distance between anchors feels very “Obsidian”.
let d = (p3 - p0).length() * bend;
// c1 = anchor A + (outward tangent) * d
let c1 = p0 + side_tangent(from_side) * d;
// c2 = anchor B + (inward tangent) * d
let c2 = p3 - side_tangent(to_side).neg() * d;
let color = ui.visuals().noninteractive().bg_stroke.color;
let stroke = egui::Stroke::new(4.0, color);
let bezier = CubicBezierShape::from_points_stroke([p0, c1, c2, p3], false, color, stroke);
ui.painter().add(Shape::CubicBezier(bezier));
arrow_ui(ui, to_side, to_anchor, color);
None
}
/// Paint a tiny triangular “arrow”.
///
/// * `ui` the egui `Ui` youre painting in
/// * `side` which edge of the box were attaching to
/// * `point` the exact spot on that edge the arrows tip should touch
/// * `fill` colour to fill the arrow with (usually your popups background)
pub fn arrow_ui(ui: &mut egui::Ui, side: &Side, point: Pos2, fill: egui::Color32) {
let len: f32 = 12.0; // distance from tip to base
let width: f32 = 16.0; // length of the base
let stroke: f32 = 1.0; // length of the base
let verts = match side {
Side::Top => [
point, // tip
Pos2::new(point.x - width * 0.5, point.y - len), // baseleft (above)
Pos2::new(point.x + width * 0.5, point.y - len), // baseright (above)
],
Side::Bottom => [
point,
Pos2::new(point.x + width * 0.5, point.y + len), // below
Pos2::new(point.x - width * 0.5, point.y + len),
],
Side::Left => [
point,
Pos2::new(point.x - len, point.y + width * 0.5), // left
Pos2::new(point.x - len, point.y - width * 0.5),
],
Side::Right => [
point,
Pos2::new(point.x + len, point.y - width * 0.5), // right
Pos2::new(point.x + len, point.y + width * 0.5),
],
};
ui.painter().add(egui::Shape::convex_polygon(
verts.to_vec(),
fill,
Stroke::new(stroke, fill), // add a stroke here if you want an outline
));
}
fn node_ui(ui: &mut egui::Ui, node: &Node) -> egui::Response {
match node {
Node::Text(text_node) => text_node_ui(ui, text_node),
@@ -79,21 +214,15 @@ fn node_box_ui(
node: &GenericNode,
contents: impl FnOnce(&mut egui::Ui),
) -> egui::Response {
let x = node.x as f32;
let y = node.y as f32;
let width = node.width as f32;
let height = node.height as f32;
let pos = node_rect(node);
let min = Pos2::new(x, y);
let max = Pos2::new(x + width, y + height);
ui.put(Rect::from_min_max(min, max), |ui: &mut egui::Ui| {
ui.put(pos, |ui: &mut egui::Ui| {
egui::Frame::default()
.fill(ui.visuals().noninteractive().weak_bg_fill)
.inner_margin(egui::Margin::same(4))
.inner_margin(egui::Margin::same(16))
.corner_radius(egui::CornerRadius::same(10))
.stroke(egui::Stroke::new(
1.0,
2.0,
ui.visuals().noninteractive().bg_stroke.color,
))
.show(ui, contents)