commit d12e5b363cb2328650ef1c1a0bd4f513ff57a0e0
parent cc8bafddffe6b670e4252d37904870e79523bdbe
Author: William Casarin <jb55@jb55.com>
Date: Sat, 19 Jul 2025 11:47:10 -0700
notebook: move ui code into its own file
Signed-off-by: William Casarin <jb55@jb55.com>
Diffstat:
2 files changed, 189 insertions(+), 184 deletions(-)
diff --git a/crates/notedeck_notebook/src/lib.rs b/crates/notedeck_notebook/src/lib.rs
@@ -1,12 +1,9 @@
-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 crate::ui::{edge_ui, node_ui};
+use egui::{Pos2, Rect};
+use jsoncanvas::JsonCanvas;
use notedeck::{AppAction, AppContext};
-use std::collections::HashMap;
-use std::ops::Neg;
+
+mod ui;
pub struct Notebook {
canvas: JsonCanvas,
@@ -55,182 +52,6 @@ impl notedeck::App for Notebook {
}
}
-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` you’re painting in
-/// * `side` – which edge of the box we’re attaching to
-/// * `point` – the exact spot on that edge the arrow’s tip should touch
-/// * `fill` – colour to fill the arrow with (usually your popup’s 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), // base‑left (above)
- Pos2::new(point.x + width * 0.5, point.y - len), // base‑right (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),
- Node::File(file_node) => file_node_ui(ui, file_node),
- Node::Link(link_node) => link_node_ui(ui, link_node),
- Node::Group(group_node) => group_node_ui(ui, group_node),
- }
-}
-
-fn text_node_ui(ui: &mut egui::Ui, node: &TextNode) -> egui::Response {
- node_box_ui(ui, node.node(), |ui| {
- egui::ScrollArea::vertical()
- .show(ui, |ui| {
- ui.with_layout(egui::Layout::left_to_right(Align::Min), |ui| {
- ui.add(Label::new(node.text()).wrap_mode(TextWrapMode::Wrap))
- })
- })
- .inner
- .response
- })
-}
-
-fn file_node_ui(ui: &mut egui::Ui, node: &FileNode) -> egui::Response {
- node_box_ui(ui, node.node(), |ui| ui.label("file node"))
-}
-
-fn link_node_ui(ui: &mut egui::Ui, node: &LinkNode) -> egui::Response {
- node_box_ui(ui, node.node(), |ui| ui.label("link node"))
-}
-
-fn group_node_ui(ui: &mut egui::Ui, node: &GroupNode) -> egui::Response {
- node_box_ui(ui, node.node(), |ui| ui.label("group node"))
-}
-
-fn node_box_ui(
- ui: &mut egui::Ui,
- node: &GenericNode,
- contents: impl FnOnce(&mut egui::Ui) -> egui::Response,
-) -> egui::Response {
- let pos = node_rect(node);
-
- ui.put(pos, |ui: &mut egui::Ui| {
- egui::Frame::default()
- .fill(ui.visuals().noninteractive().weak_bg_fill)
- .inner_margin(egui::Margin::same(16))
- .corner_radius(egui::CornerRadius::same(10))
- .stroke(egui::Stroke::new(
- 2.0,
- ui.visuals().noninteractive().bg_stroke.color,
- ))
- .show(ui, |ui| {
- let rect = ui.available_rect_before_wrap();
- ui.allocate_at_least(ui.available_size(), egui::Sense::click());
- ui.put(rect, contents);
- })
- .response
- })
-}
-
fn demo_canvas() -> JsonCanvas {
let demo_json: String = include_str!("../demo.canvas").to_string();
diff --git a/crates/notedeck_notebook/src/ui.rs b/crates/notedeck_notebook/src/ui.rs
@@ -0,0 +1,184 @@
+use egui::{Align, Label, Pos2, Rect, Shape, Stroke, TextWrapMode, epaint::CubicBezierShape, vec2};
+use jsoncanvas::{
+ FileNode, GroupNode, LinkNode, Node, NodeId, TextNode,
+ edge::{Edge, Side},
+ node::GenericNode,
+};
+use std::collections::HashMap;
+use std::ops::Neg;
+
+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),
+ }
+}
+
+pub 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` you’re painting in
+/// * `side` – which edge of the box we’re attaching to
+/// * `point` – the exact spot on that edge the arrow’s tip should touch
+/// * `fill` – colour to fill the arrow with (usually your popup’s 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), // base‑left (above)
+ Pos2::new(point.x + width * 0.5, point.y - len), // base‑right (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
+ ));
+}
+
+pub fn node_ui(ui: &mut egui::Ui, node: &Node) -> egui::Response {
+ match node {
+ Node::Text(text_node) => text_node_ui(ui, text_node),
+ Node::File(file_node) => file_node_ui(ui, file_node),
+ Node::Link(link_node) => link_node_ui(ui, link_node),
+ Node::Group(group_node) => group_node_ui(ui, group_node),
+ }
+}
+
+fn text_node_ui(ui: &mut egui::Ui, node: &TextNode) -> egui::Response {
+ node_box_ui(ui, node.node(), |ui| {
+ egui::ScrollArea::vertical()
+ .show(ui, |ui| {
+ ui.with_layout(egui::Layout::left_to_right(Align::Min), |ui| {
+ ui.add(Label::new(node.text()).wrap_mode(TextWrapMode::Wrap))
+ })
+ })
+ .inner
+ .response
+ })
+}
+
+fn file_node_ui(ui: &mut egui::Ui, node: &FileNode) -> egui::Response {
+ node_box_ui(ui, node.node(), |ui| ui.label("file node"))
+}
+
+fn link_node_ui(ui: &mut egui::Ui, node: &LinkNode) -> egui::Response {
+ node_box_ui(ui, node.node(), |ui| ui.label("link node"))
+}
+
+fn group_node_ui(ui: &mut egui::Ui, node: &GroupNode) -> egui::Response {
+ node_box_ui(ui, node.node(), |ui| ui.label("group node"))
+}
+
+fn node_box_ui(
+ ui: &mut egui::Ui,
+ node: &GenericNode,
+ contents: impl FnOnce(&mut egui::Ui) -> egui::Response,
+) -> egui::Response {
+ let pos = node_rect(node);
+
+ ui.put(pos, |ui: &mut egui::Ui| {
+ egui::Frame::default()
+ .fill(ui.visuals().noninteractive().weak_bg_fill)
+ .inner_margin(egui::Margin::same(16))
+ .corner_radius(egui::CornerRadius::same(10))
+ .stroke(egui::Stroke::new(
+ 2.0,
+ ui.visuals().noninteractive().bg_stroke.color,
+ ))
+ .show(ui, |ui| {
+ let rect = ui.available_rect_before_wrap();
+ ui.allocate_at_least(ui.available_size(), egui::Sense::click());
+ ui.put(rect, contents);
+ })
+ .response
+ })
+}