notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

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 }