Files
cumulus/src/model/link.rs
T
skkeyeandClaude Opus 5.5 69aa7eee58 Add the data model and the document file format
The model holds the rules: a document is a tree under one root, with
loose nodes beside it and cross-links between any two nodes. Every change
runs as a transaction that rolls back on error and returns an undo step
(the old and new values of whatever it touched), so undo and redo need no
code of their own per command. Text from outside (titles, tags, names) is
checked once, when it becomes a typed value. Folding (Open, Auto, Closed,
plus focus) is one function that every view will share.

storage::document_file reads and writes `.cum` files in RON, with
multi-line bodies kept readable, `\n` line endings on every OS, and errors
that name the node they're about. The tests cover the tree rules, 500
random edits undone and redone step by step, folding, and the file format
(the example from DESIGN.md is written back character for character).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-01 00:54:49 -04:00

95 lines
2.7 KiB
Rust

//! Cross-links: lines between any two nodes, besides the branches of the
//! tree.
use super::ids::NodeId;
use super::look::LineLook;
use super::text::Label;
/// Identifies the link between two nodes. Two nodes have at most one link
/// between them, whichever way it points, so the key is just the pair (the
/// smaller id first).
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct LinkKey {
low: NodeId,
high: NodeId,
}
impl LinkKey {
/// The key for the link between `a` and `b`, or `None` when they're the
/// same node (a node can't link to itself).
pub fn new(a: NodeId, b: NodeId) -> Option<LinkKey> {
if a == b {
return None;
}
Some(LinkKey { low: a.min(b), high: a.max(b) })
}
/// Both nodes, the smaller id first.
pub fn nodes(self) -> (NodeId, NodeId) {
(self.low, self.high)
}
/// Whether `node` is one of the two ends.
pub fn touches(self, node: NodeId) -> bool {
self.low == node || self.high == node
}
}
/// A cross-link between two nodes. It points from its start to its end, which
/// only matters for where the arrowheads go.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Link {
key: LinkKey,
/// True when the link points from the higher id to the lower one.
downward: bool,
/// Text shown in the middle of the line.
pub label: Option<Label>,
/// How the line is drawn.
pub look: LineLook,
}
impl Link {
/// A link from `start` to `end` with the theme's look, or `None` when
/// they're the same node.
pub fn new(start: NodeId, end: NodeId) -> Option<Link> {
let key = LinkKey::new(start, end)?;
Some(Link { key, downward: start > end, label: None, look: LineLook::default() })
}
/// The node the link starts from.
pub fn start(&self) -> NodeId {
if self.downward { self.key.high } else { self.key.low }
}
/// The node the link points to.
pub fn end(&self) -> NodeId {
if self.downward { self.key.low } else { self.key.high }
}
/// The pair of nodes it joins.
pub fn key(&self) -> LinkKey {
self.key
}
/// The same link, pointing the other way.
pub fn reversed(self) -> Link {
Link { downward: !self.downward, ..self }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_link_keeps_its_direction_and_its_key_ignores_it() {
let (a, b) = (NodeId::new(7), NodeId::new(3));
let link = Link::new(a, b).unwrap();
assert_eq!((link.start(), link.end()), (a, b));
assert_eq!(link.key(), LinkKey::new(b, a).unwrap());
let back = link.reversed();
assert_eq!((back.start(), back.end()), (b, a));
assert!(Link::new(a, a).is_none());
}
}