Add the canvas engine and a canvas playground example
The canvas draws a frame in three steps. Scene::build works out what's shown and where: folding decides which nodes show, boxes are sized from their wrapped titles (with tags and a +N badge in the bottom border), the camera turns canvas positions into screen cells, and every branch and cross-link gets a right-angle route. The line layer records which arms meet in each cell, so borders and lines join with the right box-drawing character (├ ┼ ┨ ╭), in any mix of light, thick, double, dashed and dotted. Rendering then writes lines, arrowheads and labels, and the nodes on top. Branches leave sideways and bend near their parent, so siblings share one bus; cross-links take the wider gap and carry their label on the longest level run. Far zoom keeps the full width and halves the height (one line per node), Overview shows dots. hjkl picks the nearest node within 45° of the direction, and new nodes find a free spot next to their parent or sibling. The theme works out looks in layers (default, tag style, named style, the node's own), with a mid-tone palette for dark and light terminals, and an ASCII fallback for fonts without box drawing. `cargo run --example canvas` opens a sample document to move around, zoom, fold, edit and drag with the mouse. The tests compare rendered text against the DESIGN.md mockup. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
69aa7eee58
commit
3b58d6494d
23 files changed
+3641
-7
No files matched your search
@@ -0,0 +1,80 @@
|
||||
//! Where new nodes go on the canvas: next to their parent or sibling, in a
|
||||
//! free spot. Nothing else moves to make room.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::model::{CanvasPos, CanvasRect, CanvasSize, Document, NodeId};
|
||||
|
||||
/// Columns between a parent and its children: room for the branch lines.
|
||||
const BRANCH_GAP: i32 = 6;
|
||||
/// Rows kept free between boxes stacked one above another.
|
||||
const ROW_GAP: i32 = 1;
|
||||
|
||||
/// Where a new last child of `parent` goes: to the right of the parent, below
|
||||
/// its last child (level with the parent for a first child), then further
|
||||
/// down until it's clear of every other box.
|
||||
pub fn child_spot(
|
||||
doc: &Document,
|
||||
parent: NodeId,
|
||||
size: CanvasSize,
|
||||
rects: &BTreeMap<NodeId, CanvasRect>,
|
||||
) -> CanvasPos {
|
||||
let Some(parent_rect) = rects.get(&parent) else { return CanvasPos::default() };
|
||||
let start = match doc.children(parent).last().and_then(|child| rects.get(child)) {
|
||||
Some(last) => CanvasPos::new(last.left(), last.bottom() + ROW_GAP),
|
||||
None => CanvasPos::new(parent_rect.right() + BRANCH_GAP, parent_rect.top()),
|
||||
};
|
||||
free_spot(start, size, rects)
|
||||
}
|
||||
|
||||
/// Where a new node just after `sibling` goes: right below it, then further
|
||||
/// down until it's clear.
|
||||
pub fn below_spot(sibling: NodeId, size: CanvasSize, rects: &BTreeMap<NodeId, CanvasRect>) -> CanvasPos {
|
||||
let Some(rect) = rects.get(&sibling) else { return CanvasPos::default() };
|
||||
free_spot(CanvasPos::new(rect.left(), rect.bottom() + ROW_GAP), size, rects)
|
||||
}
|
||||
|
||||
/// The first spot at `start` or below it where a box of `size` keeps clear of
|
||||
/// every box in `rects` (by a row above and below).
|
||||
pub fn free_spot(start: CanvasPos, size: CanvasSize, rects: &BTreeMap<NodeId, CanvasRect>) -> CanvasPos {
|
||||
let mut spot = start;
|
||||
// Each step jumps below the box in the way, and no box is in the way
|
||||
// twice, so there are at most as many steps as boxes.
|
||||
for _ in 0..=rects.len() {
|
||||
let wanted = CanvasRect::new(
|
||||
CanvasPos::new(spot.x, spot.y - ROW_GAP),
|
||||
CanvasSize::new(size.width, size.height + 2 * ROW_GAP as u16),
|
||||
);
|
||||
match rects.values().filter(|rect| rect.overlaps(wanted)).map(|rect| rect.bottom()).max() {
|
||||
Some(bottom) => spot.y = bottom + ROW_GAP,
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
spot
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::model::test_support::garden;
|
||||
use crate::ui::glyphs::Glyphs;
|
||||
use crate::view::canvas::boxes::box_rects;
|
||||
|
||||
#[test]
|
||||
fn new_nodes_find_a_free_spot_nearby() {
|
||||
let (doc, g) = garden();
|
||||
let rects = box_rects(&doc, 30, Glyphs::Unicode);
|
||||
let size = CanvasSize::new(10, 3);
|
||||
assert_eq!(child_spot(&doc, g.root, size, &rects), CanvasPos::new(22, 12), "below the last child");
|
||||
assert_eq!(
|
||||
child_spot(&doc, g.cedar, size, &rects),
|
||||
CanvasPos::new(55, 4),
|
||||
"first child: level with it"
|
||||
);
|
||||
assert_eq!(
|
||||
below_spot(g.soil, size, &rects),
|
||||
CanvasPos::new(22, 12),
|
||||
"Raised beds and Watering are in the way"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user