Apply cargo fmt

This commit is contained in:
2026-09-10 11:49:20 +02:00
parent ada2c3d89d
commit 4884aca3dd
8 changed files with 302 additions and 122 deletions
+27 -7
View File
@@ -236,9 +236,13 @@ impl App {
} }
fn insert_text_node(&mut self, at: Pos2, ctx: &Context) { fn insert_text_node(&mut self, at: Pos2, ctx: &Context) {
let id = self let id = self.view.insert_node(
.view &mut self.doc,
.insert_node(&mut self.doc, NodeKind::Text { text: String::new() }, at); NodeKind::Text {
text: String::new(),
},
at,
);
self.view.edit_node(&id); self.view.edit_node(&id);
self.set_status("Added a text node", ctx); self.set_status("Added a text node", ctx);
} }
@@ -513,7 +517,11 @@ impl App {
} }
// Arrow keys nudge the selection. // Arrow keys nudge the selection.
let step = if ctx.input(|i| i.modifiers.shift) { 10.0 } else { 1.0 }; let step = if ctx.input(|i| i.modifiers.shift) {
10.0
} else {
1.0
};
let mut delta = egui::Vec2::ZERO; let mut delta = egui::Vec2::ZERO;
for (key, direction) in [ for (key, direction) in [
(Key::ArrowLeft, egui::vec2(-1.0, 0.0)), (Key::ArrowLeft, egui::vec2(-1.0, 0.0)),
@@ -742,10 +750,18 @@ impl App {
fn toolbar(&mut self, ui: &mut Ui, ctx: &Context) { fn toolbar(&mut self, ui: &mut Ui, ctx: &Context) {
ui.horizontal(|ui| { ui.horizontal(|ui| {
if ui.button("New").on_hover_text("New canvas (Ctrl+N)").clicked() { if ui
.button("New")
.on_hover_text("New canvas (Ctrl+N)")
.clicked()
{
self.new_document(ctx); self.new_document(ctx);
} }
if ui.button("Open").on_hover_text("Open a canvas (Ctrl+O)").clicked() { if ui
.button("Open")
.on_hover_text("Open a canvas (Ctrl+O)")
.clicked()
{
self.open_dialog(ctx); self.open_dialog(ctx);
} }
if ui.button("Save").on_hover_text("Save (Ctrl+S)").clicked() { if ui.button("Save").on_hover_text("Save (Ctrl+S)").clicked() {
@@ -798,7 +814,11 @@ impl App {
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
ui.toggle_value(&mut self.show_inspector, "Inspector"); ui.toggle_value(&mut self.show_inspector, "Inspector");
ui.toggle_value(&mut self.view.snap_to_grid, "Snap"); ui.toggle_value(&mut self.view.snap_to_grid, "Snap");
let icon = if self.dark_mode { "Light theme" } else { "Dark theme" }; let icon = if self.dark_mode {
"Light theme"
} else {
"Dark theme"
};
if ui.button(icon).on_hover_text("Switch theme").clicked() { if ui.button(icon).on_hover_text("Switch theme").clicked() {
self.dark_mode = !self.dark_mode; self.dark_mode = !self.dark_mode;
} }
+5 -4
View File
@@ -231,9 +231,9 @@ mod tests {
} }
#[test] #[test]
fn saving_and_loading_round_trips( fn saving_and_loading_round_trips() -> Result<(), Box<dyn std::error::Error>> {
) -> Result<(), Box<dyn std::error::Error>> { let dir =
let dir = std::env::temp_dir().join(format!("jsoncanvas-test-{}", crate::model::random_id())); std::env::temp_dir().join(format!("jsoncanvas-test-{}", crate::model::random_id()));
fs::create_dir_all(&dir)?; fs::create_dir_all(&dir)?;
let path = dir.join("test.canvas"); let path = dir.join("test.canvas");
@@ -252,7 +252,8 @@ mod tests {
#[test] #[test]
fn opening_a_broken_file_reports_an_error() { fn opening_a_broken_file_reports_an_error() {
let dir = std::env::temp_dir().join(format!("jsoncanvas-test-{}", crate::model::random_id())); let dir =
std::env::temp_dir().join(format!("jsoncanvas-test-{}", crate::model::random_id()));
fs::create_dir_all(&dir).unwrap(); fs::create_dir_all(&dir).unwrap();
let path = dir.join("bad.canvas"); let path = dir.join("bad.canvas");
fs::write(&path, "{ not json").unwrap(); fs::write(&path, "{ not json").unwrap();
+7 -2
View File
@@ -99,7 +99,9 @@ pub fn bezier_point(p: [Pos2; 4], t: f32) -> Pos2 {
/// Derivative of a cubic Bézier at `t`; the direction the curve travels in. /// Derivative of a cubic Bézier at `t`; the direction the curve travels in.
pub fn bezier_tangent(p: [Pos2; 4], t: f32) -> Vec2 { pub fn bezier_tangent(p: [Pos2; 4], t: f32) -> Vec2 {
let u = 1.0 - t; let u = 1.0 - t;
let d = (p[1] - p[0]) * (3.0 * u * u) + (p[2] - p[1]) * (6.0 * u * t) + (p[3] - p[2]) * (3.0 * t * t); let d = (p[1] - p[0]) * (3.0 * u * u)
+ (p[2] - p[1]) * (6.0 * u * t)
+ (p[3] - p[2]) * (3.0 * t * t);
if d.length() > f32::EPSILON { if d.length() > f32::EPSILON {
d.normalized() d.normalized()
} else { } else {
@@ -126,7 +128,10 @@ pub fn distance_to_curve(p: [Pos2; 4], point: Pos2) -> f32 {
pub fn curve_bounds(p: [Pos2; 4]) -> Rect { pub fn curve_bounds(p: [Pos2; 4]) -> Rect {
let mut rect = Rect::from_points(&[p[0], p[3]]); let mut rect = Rect::from_points(&[p[0], p[3]]);
for i in 1..SAMPLES { for i in 1..SAMPLES {
rect = rect.union(Rect::from_points(&[bezier_point(p, i as f32 / SAMPLES as f32)])); rect = rect.union(Rect::from_points(&[bezier_point(
p,
i as f32 / SAMPLES as f32,
)]));
} }
rect rect
} }
+57 -19
View File
@@ -53,7 +53,11 @@ fn node_section(
let Some(node) = doc.canvas.node(id).cloned() else { let Some(node) = doc.canvas.node(id).cloned() else {
return; return;
}; };
heading(ui, palette, &format!("{} node", capitalise(node.kind.type_name()))); heading(
ui,
palette,
&format!("{} node", capitalise(node.kind.type_name())),
);
match &node.kind { match &node.kind {
NodeKind::Text { text } => { NodeKind::Text { text } => {
@@ -66,7 +70,8 @@ fn node_section(
); );
if response.changed() { if response.changed() {
doc.begin_change("Edit text"); doc.begin_change("Edit text");
if let Some(NodeKind::Text { text }) = doc.canvas.node_mut(id).map(|n| &mut n.kind) { if let Some(NodeKind::Text { text }) = doc.canvas.node_mut(id).map(|n| &mut n.kind)
{
*text = buffer; *text = buffer;
} }
} }
@@ -89,7 +94,11 @@ fn node_section(
doc.change("Set file"); doc.change("Set file");
set_file(doc, id, Some(path.clone()), None); set_file(doc, id, Some(path.clone()), None);
} }
if ui.button("Open").on_hover_text("Open with the system default application").clicked() { if ui
.button("Open")
.on_hover_text("Open with the system default application")
.clicked()
{
out.open_target = Some(file.clone()); out.open_target = Some(file.clone());
} }
}); });
@@ -270,15 +279,29 @@ fn geometry_section(ui: &mut Ui, doc: &mut Document, palette: &Palette, id: &str
ui.label(field_label(palette, "Position and size")); ui.label(field_label(palette, "Position and size"));
let mut changed = false; let mut changed = false;
ui.horizontal(|ui| { ui.horizontal(|ui| {
changed |= ui.add(DragValue::new(&mut x).speed(1).prefix("x ")).changed(); changed |= ui
changed |= ui.add(DragValue::new(&mut y).speed(1).prefix("y ")).changed(); .add(DragValue::new(&mut x).speed(1).prefix("x "))
.changed();
changed |= ui
.add(DragValue::new(&mut y).speed(1).prefix("y "))
.changed();
}); });
ui.horizontal(|ui| { ui.horizontal(|ui| {
changed |= ui changed |= ui
.add(DragValue::new(&mut w).speed(1).range(1..=100_000).prefix("w ")) .add(
DragValue::new(&mut w)
.speed(1)
.range(1..=100_000)
.prefix("w "),
)
.changed(); .changed();
changed |= ui changed |= ui
.add(DragValue::new(&mut h).speed(1).range(1..=100_000).prefix("h ")) .add(
DragValue::new(&mut h)
.speed(1)
.range(1..=100_000)
.prefix("h "),
)
.changed(); .changed();
}); });
if changed { if changed {
@@ -316,7 +339,11 @@ fn edge_section(
.unwrap_or_else(|| format!("missing node {node_id}")) .unwrap_or_else(|| format!("missing node {node_id}"))
}; };
ui.label( ui.label(
RichText::new(format!("{} → {}", name_of(&edge.from_node), name_of(&edge.to_node))) RichText::new(format!(
"{} → {}",
name_of(&edge.from_node),
name_of(&edge.to_node)
))
.size(12.0) .size(12.0)
.color(palette.text_muted), .color(palette.text_muted),
); );
@@ -393,10 +420,7 @@ fn edge_section(
fn side_combo(ui: &mut Ui, label: &str, side: &mut Option<Side>) { fn side_combo(ui: &mut Ui, label: &str, side: &mut Option<Side>) {
ComboBox::from_id_salt(label) ComboBox::from_id_salt(label)
.selected_text(format!( .selected_text(format!("{label}: {}", side.map_or("auto", Side::label)))
"{label}: {}",
side.map_or("auto", Side::label)
))
.width(110.0) .width(110.0)
.show_ui(ui, |ui| { .show_ui(ui, |ui| {
ui.selectable_value(side, None, "auto"); ui.selectable_value(side, None, "auto");
@@ -592,7 +616,10 @@ fn canvas_section(ui: &mut Ui, doc: &mut Document, palette: &Palette, out: &mut
if ui.button("Remove them").clicked() { if ui.button("Remove them").clicked() {
doc.change("Remove dangling edges"); doc.change("Remove dangling edges");
let removed = doc.canvas.drop_dangling_edges(); let removed = doc.canvas.drop_dangling_edges();
out.status = Some(format!("Removed {removed} dangling edge{}", plural(removed))); out.status = Some(format!(
"Removed {removed} dangling edge{}",
plural(removed)
));
} }
} }
@@ -604,7 +631,11 @@ fn canvas_section(ui: &mut Ui, doc: &mut Document, palette: &Palette, out: &mut
"Middle- or right-drag to pan, Ctrl+wheel to zoom", "Middle- or right-drag to pan, Ctrl+wheel to zoom",
"Hold Shift to add to the selection", "Hold Shift to add to the selection",
] { ] {
ui.label(RichText::new(format!("• {tip}")).size(12.0).color(palette.text_muted)); ui.label(
RichText::new(format!("• {tip}"))
.size(12.0)
.color(palette.text_muted),
);
} }
} }
@@ -758,10 +789,8 @@ fn pick_any_file(doc: &Document) -> Option<String> {
} }
fn pick_image_file(doc: &Document) -> Option<String> { fn pick_image_file(doc: &Document) -> Option<String> {
let mut dialog = rfd::FileDialog::new().add_filter( let mut dialog =
"Images", rfd::FileDialog::new().add_filter("Images", &["png", "jpg", "jpeg", "gif", "bmp", "webp"]);
&["png", "jpg", "jpeg", "gif", "bmp", "webp"],
);
if let Some(dir) = doc.base_dir() { if let Some(dir) = doc.base_dir() {
dialog = dialog.set_directory(dir); dialog = dialog.set_directory(dir);
} }
@@ -787,7 +816,16 @@ mod tests {
use crate::view::CanvasView; use crate::view::CanvasView;
fn node(id: &str, x: i32, y: i32, w: i32, h: i32) -> Node { fn node(id: &str, x: i32, y: i32, w: i32, h: i32) -> Node {
Node::new(id, NodeKind::Text { text: String::new() }, x, y, w, h) Node::new(
id,
NodeKind::Text {
text: String::new(),
},
x,
y,
w,
h,
)
} }
#[test] #[test]
+64 -20
View File
@@ -44,12 +44,25 @@ pub type Cell = Vec<Span>;
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub enum Block { pub enum Block {
Heading { level: u8, spans: Vec<Span> }, Heading {
level: u8,
spans: Vec<Span>,
},
Paragraph(Vec<Span>), Paragraph(Vec<Span>),
List { indent: usize, marker: Marker, spans: Vec<Span> }, List {
indent: usize,
marker: Marker,
spans: Vec<Span>,
},
Quote(Vec<Span>), Quote(Vec<Span>),
Code { language: String, text: String }, Code {
Table { header: Vec<Cell>, rows: Vec<Vec<Cell>> }, language: String,
text: String,
},
Table {
header: Vec<Cell>,
rows: Vec<Vec<Cell>>,
},
Rule, Rule,
} }
@@ -129,9 +142,7 @@ pub fn parse(source: &str) -> Vec<Block> {
} }
// A pipe table: a header row, a `| --- |` separator, then the body. // A pipe table: a header row, a `| --- |` separator, then the body.
if trimmed.contains('|') if trimmed.contains('|') && lines.peek().is_some_and(|next| is_separator_row(next)) {
&& lines.peek().is_some_and(|next| is_separator_row(next))
{
flush(&mut paragraph, &mut blocks, false); flush(&mut paragraph, &mut blocks, false);
flush(&mut quote, &mut blocks, true); flush(&mut quote, &mut blocks, true);
let header = split_row(trimmed); let header = split_row(trimmed);
@@ -218,7 +229,8 @@ fn heading_of(line: &str) -> Option<(u8, &str)> {
if (1..=6).contains(&hashes) { if (1..=6).contains(&hashes) {
let rest = &line[hashes..]; let rest = &line[hashes..];
// ATX headings need a space after the hashes. // ATX headings need a space after the hashes.
rest.strip_prefix(' ').map(|rest| (hashes as u8, rest.trim())) rest.strip_prefix(' ')
.map(|rest| (hashes as u8, rest.trim()))
} else { } else {
None None
} }
@@ -237,12 +249,18 @@ fn list_item_of(line: &str) -> Option<(usize, Marker, &str)> {
let rest_trimmed = rest.trim_start(); let rest_trimmed = rest.trim_start();
let lowered = rest_trimmed.to_ascii_lowercase(); let lowered = rest_trimmed.to_ascii_lowercase();
if let Some(task) = lowered.strip_prefix("[ ] ") { if let Some(task) = lowered.strip_prefix("[ ] ") {
(Marker::Task(false), &rest_trimmed[rest_trimmed.len() - task.len()..]) (
Marker::Task(false),
&rest_trimmed[rest_trimmed.len() - task.len()..],
)
} else if let Some(task) = lowered } else if let Some(task) = lowered
.strip_prefix("[x] ") .strip_prefix("[x] ")
.or_else(|| lowered.strip_prefix("[/] ")) .or_else(|| lowered.strip_prefix("[/] "))
{ {
(Marker::Task(true), &rest_trimmed[rest_trimmed.len() - task.len()..]) (
Marker::Task(true),
&rest_trimmed[rest_trimmed.len() - task.len()..],
)
} else { } else {
(Marker::Bullet, rest) (Marker::Bullet, rest)
} }
@@ -252,7 +270,9 @@ fn list_item_of(line: &str) -> Option<(usize, Marker, &str)> {
return None; return None;
} }
let rest = &trimmed[digits.len()..]; let rest = &trimmed[digits.len()..];
let rest = rest.strip_prefix(". ").or_else(|| rest.strip_prefix(") "))?; let rest = rest
.strip_prefix(". ")
.or_else(|| rest.strip_prefix(") "))?;
(Marker::Number(digits.parse().ok()?), rest) (Marker::Number(digits.parse().ok()?), rest)
}; };
@@ -304,7 +324,10 @@ fn parse_inline_into(text: &str, style: Style, out: &mut Vec<Span>) {
flush!(); flush!();
out.push(Span { out.push(Span {
text: inner.to_owned(), text: inner.to_owned(),
style: Style { code: true, ..style }, style: Style {
code: true,
..style
},
link: None, link: None,
image: false, image: false,
}); });
@@ -314,21 +337,42 @@ fn parse_inline_into(text: &str, style: Style, out: &mut Vec<Span>) {
if let Some((inner, tail)) = delimited(rest, "**").or_else(|| delimited(rest, "__")) { if let Some((inner, tail)) = delimited(rest, "**").or_else(|| delimited(rest, "__")) {
flush!(); flush!();
parse_inline_into(inner, Style { bold: true, ..style }, out); parse_inline_into(
inner,
Style {
bold: true,
..style
},
out,
);
rest = tail; rest = tail;
continue; continue;
} }
if let Some((inner, tail)) = delimited(rest, "~~") { if let Some((inner, tail)) = delimited(rest, "~~") {
flush!(); flush!();
parse_inline_into(inner, Style { strike: true, ..style }, out); parse_inline_into(
inner,
Style {
strike: true,
..style
},
out,
);
rest = tail; rest = tail;
continue; continue;
} }
if let Some((inner, tail)) = delimited(rest, "*").or_else(|| delimited(rest, "_")) { if let Some((inner, tail)) = delimited(rest, "*").or_else(|| delimited(rest, "_")) {
flush!(); flush!();
parse_inline_into(inner, Style { italic: true, ..style }, out); parse_inline_into(
inner,
Style {
italic: true,
..style
},
out,
);
rest = tail; rest = tail;
continue; continue;
} }
@@ -540,10 +584,8 @@ pub fn render(ui: &mut Ui, blocks: &[Block], palette: &Palette, base_size: f32)
Block::Rule => { Block::Rule => {
let stroke = egui::Stroke::new(1.0, palette.border); let stroke = egui::Stroke::new(1.0, palette.border);
let width = ui.available_width(); let width = ui.available_width();
let (rect, _) = ui.allocate_exact_size( let (rect, _) = ui
egui::vec2(width, base_size * 0.6), .allocate_exact_size(egui::vec2(width, base_size * 0.6), egui::Sense::hover());
egui::Sense::hover(),
);
ui.painter().line_segment( ui.painter().line_segment(
[ [
egui::pos2(rect.left(), rect.center().y), egui::pos2(rect.left(), rect.center().y),
@@ -712,7 +754,9 @@ mod tests {
#[test] #[test]
fn parses_pipe_tables() { fn parses_pipe_tables() {
let blocks = parse("| Key | What it does |\n| --- | :---: |\n| `a` | first |\n| b | second |\n\nafter"); let blocks = parse(
"| Key | What it does |\n| --- | :---: |\n| `a` | first |\n| b | second |\n\nafter",
);
let Block::Table { header, rows } = &blocks[0] else { let Block::Table { header, rows } = &blocks[0] else {
panic!("expected a table, got {blocks:?}"); panic!("expected a table, got {blocks:?}");
}; };
+15 -18
View File
@@ -46,8 +46,9 @@ impl PreviewCache {
} }
fn text_preview(&mut self, path: &Path) -> &Result<String, String> { fn text_preview(&mut self, path: &Path) -> &Result<String, String> {
self.text.entry(path.to_path_buf()).or_insert_with(|| { self.text
match std::fs::read(path) { .entry(path.to_path_buf())
.or_insert_with(|| match std::fs::read(path) {
Ok(bytes) => { Ok(bytes) => {
let truncated = bytes.len() > MAX_PREVIEW_BYTES; let truncated = bytes.len() > MAX_PREVIEW_BYTES;
let head = &bytes[..bytes.len().min(MAX_PREVIEW_BYTES)]; let head = &bytes[..bytes.len().min(MAX_PREVIEW_BYTES)];
@@ -58,7 +59,6 @@ impl PreviewCache {
Ok(text) Ok(text)
} }
Err(err) => Err(format!("{err}")), Err(err) => Err(format!("{err}")),
}
}) })
} }
} }
@@ -167,11 +167,7 @@ fn render_file(
activated = Some(file.to_owned()); activated = Some(file.to_owned());
} }
if let Some(subpath) = subpath { if let Some(subpath) = subpath {
ui.label( ui.label(RichText::new(subpath).size(12.0).color(palette.text_muted));
RichText::new(subpath)
.size(12.0)
.color(palette.text_muted),
);
} }
}); });
@@ -299,10 +295,7 @@ fn render_link(
/// The host part of a URL, for the link node header. /// The host part of a URL, for the link node header.
fn host_of(url: &str) -> &str { fn host_of(url: &str) -> &str {
let rest = url let rest = url.split_once("://").map(|(_, rest)| rest).unwrap_or(url);
.split_once("://")
.map(|(_, rest)| rest)
.unwrap_or(url);
let host = rest.split(['/', '?', '#']).next().unwrap_or(rest); let host = rest.split(['/', '?', '#']).next().unwrap_or(rest);
if host.is_empty() { url } else { host } if host.is_empty() { url } else { host }
} }
@@ -327,11 +320,12 @@ mod tests {
PathBuf::from("/vault/boards/notes/a.md") PathBuf::from("/vault/boards/notes/a.md")
); );
// Absolute paths are left alone. // Absolute paths are left alone.
let absolute = if cfg!(windows) { "C:\\tmp\\a.md" } else { "/tmp/a.md" }; let absolute = if cfg!(windows) {
assert_eq!( "C:\\tmp\\a.md"
resolve_path(Some(base), absolute), } else {
PathBuf::from(absolute) "/tmp/a.md"
); };
assert_eq!(resolve_path(Some(base), absolute), PathBuf::from(absolute));
} }
#[test] #[test]
@@ -358,7 +352,10 @@ mod tests {
#[test] #[test]
fn shows_the_host_of_a_link() { fn shows_the_host_of_a_link() {
assert_eq!(host_of("https://jsoncanvas.org/spec/1.0/"), "jsoncanvas.org"); assert_eq!(
host_of("https://jsoncanvas.org/spec/1.0/"),
"jsoncanvas.org"
);
assert_eq!(host_of("example.com"), "example.com"); assert_eq!(host_of("example.com"), "example.com");
} }
} }
+2 -1
View File
@@ -129,7 +129,8 @@ pub fn blend(fg: Color32, bg: Color32, strength: f32) -> Color32 {
/// Black or white, whichever stays readable on `background`. /// Black or white, whichever stays readable on `background`.
pub fn contrasting_text(background: Color32) -> Color32 { pub fn contrasting_text(background: Color32) -> Color32 {
let luma = 0.299 * background.r() as f32 + 0.587 * background.g() as f32 let luma = 0.299 * background.r() as f32
+ 0.587 * background.g() as f32
+ 0.114 * background.b() as f32; + 0.114 * background.b() as f32;
if luma > 150.0 { if luma > 150.0 {
Color32::from_rgb(0x1e, 0x1e, 0x24) Color32::from_rgb(0x1e, 0x1e, 0x24)
+114 -40
View File
@@ -6,9 +6,7 @@ use std::collections::HashMap;
use eframe::egui::{ use eframe::egui::{
self, Align, Atom, Button, Color32, CornerRadius, CursorIcon, Id, LayerId, Layout, self, Align, Atom, Button, Color32, CornerRadius, CursorIcon, Id, LayerId, Layout,
PointerButton, Pos2, Rect, RichText, Sense, Shape, Stroke, StrokeKind, TextEdit, UiBuilder, PointerButton, Pos2, Rect, RichText, Sense, Shape, Stroke, StrokeKind, TextEdit, UiBuilder,
Vec2, Vec2, containers::scroll_area::ScrollSource, epaint, pos2, vec2,
containers::scroll_area::ScrollSource,
epaint, pos2, vec2,
}; };
use eframe::emath::TSTransform; use eframe::emath::TSTransform;
@@ -17,7 +15,7 @@ use crate::geometry::{
anchor, auto_sides, bezier_point, bezier_tangent, bounds_of, curve_between, distance_to_curve, anchor, auto_sides, bezier_point, bezier_tangent, bounds_of, curve_between, distance_to_curve,
edge_curve, node_rect, edge_curve, node_rect,
}; };
use crate::model::{Canvas, Color, Edge, EndShape, Extra, Node, NodeKind, Side, MIN_NODE_SIZE}; use crate::model::{Canvas, Color, Edge, EndShape, Extra, MIN_NODE_SIZE, Node, NodeKind, Side};
use crate::nodes::{self, BodyContext, PreviewCache}; use crate::nodes::{self, BodyContext, PreviewCache};
use crate::theme::{self, Palette}; use crate::theme::{self, Palette};
@@ -215,9 +213,15 @@ enum Gesture {
accumulated: Vec2, accumulated: Vec2,
}, },
/// Rubber band selection from `start` (canvas coordinates). /// Rubber band selection from `start` (canvas coordinates).
Band { start: Pos2, add: bool }, Band {
start: Pos2,
add: bool,
},
/// Dragging a new edge out of `from`/`side`. /// Dragging a new edge out of `from`/`side`.
Connect { from: String, side: Side }, Connect {
from: String,
side: Side,
},
} }
/// A node the user asked for from the canvas context menu. Insertion goes /// A node the user asked for from the canvas context menu. Insertion goes
@@ -480,7 +484,9 @@ impl CanvasView {
let edges: Vec<Edge> = canvas let edges: Vec<Edge> = canvas
.edges .edges
.iter() .iter()
.filter(|e| self.selection.has_node(&e.from_node) && self.selection.has_node(&e.to_node)) .filter(|e| {
self.selection.has_node(&e.from_node) && self.selection.has_node(&e.to_node)
})
.cloned() .cloned()
.collect(); .collect();
Some(Clipboard { nodes, edges }) Some(Clipboard { nodes, edges })
@@ -552,7 +558,11 @@ impl CanvasView {
if self.selection.nodes.is_empty() { if self.selection.nodes.is_empty() {
return; return;
} }
doc.change(if to_front { "Bring to front" } else { "Send to back" }); doc.change(if to_front {
"Bring to front"
} else {
"Send to back"
});
let mut moved: Vec<Node> = Vec::new(); let mut moved: Vec<Node> = Vec::new();
let mut rest: Vec<Node> = Vec::new(); let mut rest: Vec<Node> = Vec::new();
for node in doc.canvas.nodes.drain(..) { for node in doc.canvas.nodes.drain(..) {
@@ -588,19 +598,15 @@ impl CanvasView {
// -- the frame -------------------------------------------------------- // -- the frame --------------------------------------------------------
pub fn show( pub fn show(&mut self, ui: &mut egui::Ui, doc: &mut Document, palette: &Palette) -> ViewOutput {
&mut self,
ui: &mut egui::Ui,
doc: &mut Document,
palette: &Palette,
) -> ViewOutput {
let mut output = ViewOutput::default(); let mut output = ViewOutput::default();
self.selection.prune(&doc.canvas); self.selection.prune(&doc.canvas);
// Deleted nodes leave a scroll position behind. The map only ever holds // Deleted nodes leave a scroll position behind. The map only ever holds
// nodes that have been on screen, so it outgrows the canvas only once // nodes that have been on screen, so it outgrows the canvas only once
// one of them is gone. // one of them is gone.
if self.body_scroll.len() > doc.canvas.nodes.len() { if self.body_scroll.len() > doc.canvas.nodes.len() {
self.body_scroll.retain(|id, _| doc.canvas.node(id).is_some()); self.body_scroll
.retain(|id, _| doc.canvas.node(id).is_some());
} }
if self if self
.editing .editing
@@ -753,7 +759,11 @@ impl CanvasView {
// Zoomed out far enough the dots crowd into noise, so only every fifth // Zoomed out far enough the dots crowd into noise, so only every fifth
// one is kept; below that they are dropped altogether. // one is kept; below that they are dropped altogether.
let scale = self.to_global.scaling; let scale = self.to_global.scaling;
let step = if GRID * scale < 10.0 { GRID * MAJOR } else { GRID }; let step = if GRID * scale < 10.0 {
GRID * MAJOR
} else {
GRID
};
if step * scale < 6.0 { if step * scale < 6.0 {
return; return;
} }
@@ -803,12 +813,14 @@ impl CanvasView {
continue; continue;
} }
painter.add(Shape::CubicBezier(epaint::CubicBezierShape::from_points_stroke( painter.add(Shape::CubicBezier(
epaint::CubicBezierShape::from_points_stroke(
curve, curve,
false, false,
Color32::TRANSPARENT, Color32::TRANSPARENT,
Stroke::new(width, color), Stroke::new(width, color),
))); ),
));
if edge.end_shape() == EndShape::Arrow { if edge.end_shape() == EndShape::Arrow {
paint_arrow(painter, curve[3], bezier_tangent(curve, 1.0), width, color); paint_arrow(painter, curve[3], bezier_tangent(curve, 1.0), width, color);
@@ -937,7 +949,13 @@ impl CanvasView {
// nodes here would pull the canvas out from under this very loop, // nodes here would pull the canvas out from under this very loop,
// so `handle_interactions` carries the action out afterwards. // so `handle_interactions` carries the action out afterwards.
response.context_menu(|ui| { response.context_menu(|ui| {
node_menu(ui, palette, color.as_ref(), selected, &mut interactions.menu); node_menu(
ui,
palette,
color.as_ref(),
selected,
&mut interactions.menu,
);
}); });
} }
interactions interactions
@@ -985,7 +1003,12 @@ impl CanvasView {
1.5 1.5
}; };
let stroke_color = if selected { palette.accent } else { border }; let stroke_color = if selected { palette.accent } else { border };
painter.rect_stroke(rect, corner, Stroke::new(width, stroke_color), StrokeKind::Inside); painter.rect_stroke(
rect,
corner,
Stroke::new(width, stroke_color),
StrokeKind::Inside,
);
} }
} }
@@ -1039,8 +1062,7 @@ impl CanvasView {
let (fill, _) = palette.group_colors(doc.canvas.nodes[index].color.as_ref()); let (fill, _) = palette.group_colors(doc.canvas.nodes[index].color.as_ref());
let chip_fill = theme::blend(fill, palette.surface, 0.5); let chip_fill = theme::blend(fill, palette.surface, 0.5);
let text_color = theme::contrasting_text(chip_fill); let text_color = theme::contrasting_text(chip_fill);
let galley = let galley = painter.layout_no_wrap(text, egui::FontId::proportional(14.0), text_color);
painter.layout_no_wrap(text, egui::FontId::proportional(14.0), text_color);
let chip = Rect::from_min_size(label_rect.min, galley.size() + vec2(12.0, 6.0)); let chip = Rect::from_min_size(label_rect.min, galley.size() + vec2(12.0, 6.0));
painter.rect_filled(chip, CornerRadius::same(4), chip_fill); painter.rect_filled(chip, CornerRadius::same(4), chip_fill);
painter.galley(chip.min + vec2(6.0, 3.0), galley, text_color); painter.galley(chip.min + vec2(6.0, 3.0), galley, text_color);
@@ -1098,7 +1120,9 @@ impl CanvasView {
fn paint_selection(&self, ui: &egui::Ui, canvas: &Canvas, palette: &Palette) { fn paint_selection(&self, ui: &egui::Ui, canvas: &Canvas, palette: &Palette) {
let painter = ui.painter(); let painter = ui.painter();
for id in &self.selection.nodes { for id in &self.selection.nodes {
let Some(node) = canvas.node(id) else { continue }; let Some(node) = canvas.node(id) else {
continue;
};
painter.rect_stroke( painter.rect_stroke(
node_rect(node).expand(2.5), node_rect(node).expand(2.5),
CornerRadius::same((NODE_CORNER + 2.0) as u8), CornerRadius::same((NODE_CORNER + 2.0) as u8),
@@ -1214,7 +1238,10 @@ impl CanvasView {
if response.dragged() { if response.dragged() {
used = true; used = true;
if let Gesture::Resize { if let Gesture::Resize {
accumulated, origin, handle, .. accumulated,
origin,
handle,
..
} = &mut self.gesture } = &mut self.gesture
{ {
*accumulated += response.drag_delta(); *accumulated += response.drag_delta();
@@ -1264,7 +1291,9 @@ impl CanvasView {
); );
} }
Gesture::Connect { from, side } => { Gesture::Connect { from, side } => {
let Some(node) = canvas.node(from) else { return }; let Some(node) = canvas.node(from) else {
return;
};
let Some(pointer) = ui let Some(pointer) = ui
.ctx() .ctx()
.pointer_latest_pos() .pointer_latest_pos()
@@ -1281,13 +1310,21 @@ impl CanvasView {
_ => (pointer, side.opposite()), _ => (pointer, side.opposite()),
}; };
let curve = curve_between(anchor(node_rect(node), *side), *side, end, to_side); let curve = curve_between(anchor(node_rect(node), *side), *side, end, to_side);
painter.add(Shape::CubicBezier(epaint::CubicBezierShape::from_points_stroke( painter.add(Shape::CubicBezier(
epaint::CubicBezierShape::from_points_stroke(
curve, curve,
false, false,
Color32::TRANSPARENT, Color32::TRANSPARENT,
Stroke::new(2.0, palette.accent), Stroke::new(2.0, palette.accent),
))); ),
paint_arrow(painter, curve[3], bezier_tangent(curve, 1.0), 2.0, palette.accent); ));
paint_arrow(
painter,
curve[3],
bezier_tangent(curve, 1.0),
2.0,
palette.accent,
);
} }
_ => {} _ => {}
} }
@@ -1451,7 +1488,13 @@ impl CanvasView {
&& self.hovered_edge.is_none() && self.hovered_edge.is_none()
&& let Some(at) = pointer && let Some(at) = pointer
{ {
let id = self.insert_node(doc, NodeKind::Text { text: String::new() }, at); let id = self.insert_node(
doc,
NodeKind::Text {
text: String::new(),
},
at,
);
self.edit_node(&id); self.edit_node(&id);
output.status = Some("Added a text node".to_owned()); output.status = Some("Added a text node".to_owned());
} }
@@ -1472,7 +1515,9 @@ impl CanvasView {
/// Opens a file/link node, or starts editing a text node. /// Opens a file/link node, or starts editing a text node.
fn activate_node(&mut self, doc: &mut Document, id: &str, output: &mut ViewOutput) { fn activate_node(&mut self, doc: &mut Document, id: &str, output: &mut ViewOutput) {
let Some(node) = doc.canvas.node(id) else { return }; let Some(node) = doc.canvas.node(id) else {
return;
};
match &node.kind { match &node.kind {
NodeKind::Text { .. } | NodeKind::Group { .. } => self.edit_node(id), NodeKind::Text { .. } | NodeKind::Group { .. } => self.edit_node(id),
NodeKind::File { file, .. } => output.open_target = Some(file.clone()), NodeKind::File { file, .. } => output.open_target = Some(file.clone()),
@@ -1507,7 +1552,13 @@ impl CanvasView {
} }
None => { None => {
// Dropping on empty canvas creates the node the edge points to. // Dropping on empty canvas creates the node the edge points to.
let new_id = self.insert_node(doc, NodeKind::Text { text: String::new() }, pointer); let new_id = self.insert_node(
doc,
NodeKind::Text {
text: String::new(),
},
pointer,
);
let edge_id = doc.canvas.fresh_id(); let edge_id = doc.canvas.fresh_id();
doc.canvas doc.canvas
.edges .edges
@@ -1885,8 +1936,7 @@ fn paint_group_background(
) )
} }
}; };
ui.painter() ui.painter().image(texture.id, target, uv, Color32::WHITE);
.image(texture.id, target, uv, Color32::WHITE);
} }
fn paint_arrow(painter: &egui::Painter, tip: Pos2, direction: Vec2, width: f32, color: Color32) { fn paint_arrow(painter: &egui::Painter, tip: Pos2, direction: Vec2, width: f32, color: Color32) {
@@ -2147,7 +2197,13 @@ mod tests {
let at = pos2(600.0, 450.0); let at = pos2(600.0, 450.0);
// Three passes: egui hit-tests against the rectangles of the previous // Three passes: egui hit-tests against the rectangles of the previous
// one, and a click is a press followed by a release. // one, and a click is a press followed by a release.
pass(&ctx, &mut view, &mut doc, screen, vec![egui::Event::PointerMoved(at)]); pass(
&ctx,
&mut view,
&mut doc,
screen,
vec![egui::Event::PointerMoved(at)],
);
pass(&ctx, &mut view, &mut doc, screen, vec![click(at)]); pass(&ctx, &mut view, &mut doc, screen, vec![click(at)]);
pass(&ctx, &mut view, &mut doc, screen, vec![release(at)]); pass(&ctx, &mut view, &mut doc, screen, vec![release(at)]);
@@ -2272,10 +2328,16 @@ mod tests {
canvas canvas
.edges .edges
.push(Edge::new("e", "a", Side::Right, "b", Side::Left)); .push(Edge::new("e", "a", Side::Right, "b", Side::Left));
assert_eq!(view.node_at(&canvas, pos2(50.0, 50.0)).as_deref(), Some("a")); assert_eq!(
view.node_at(&canvas, pos2(50.0, 50.0)).as_deref(),
Some("a")
);
assert_eq!(view.node_at(&canvas, pos2(-5.0, 50.0)), None); assert_eq!(view.node_at(&canvas, pos2(-5.0, 50.0)), None);
// The edge runs from (100,50) to (300,50). // The edge runs from (100,50) to (300,50).
assert_eq!(view.edge_at(&canvas, pos2(200.0, 50.0)).as_deref(), Some("e")); assert_eq!(
view.edge_at(&canvas, pos2(200.0, 50.0)).as_deref(),
Some("e")
);
assert_eq!(view.edge_at(&canvas, pos2(200.0, 200.0)), None); assert_eq!(view.edge_at(&canvas, pos2(200.0, 200.0)), None);
} }
@@ -2298,8 +2360,14 @@ mod tests {
), ),
); );
let view = CanvasView::new(); let view = CanvasView::new();
assert_eq!(view.node_at(&canvas, pos2(50.0, 50.0)).as_deref(), Some("a")); assert_eq!(
assert_eq!(view.node_at(&canvas, pos2(200.0, 200.0)).as_deref(), Some("g")); view.node_at(&canvas, pos2(50.0, 50.0)).as_deref(),
Some("a")
);
assert_eq!(
view.node_at(&canvas, pos2(200.0, 200.0)).as_deref(),
Some("g")
);
} }
#[test] #[test]
@@ -2491,7 +2559,10 @@ mod tests {
view.set_selection_color(&mut doc, Some(Color::Preset(4))); view.set_selection_color(&mut doc, Some(Color::Preset(4)));
assert!( assert!(
doc.canvas.nodes.iter().all(|n| n.color == Some(Color::Preset(4))), doc.canvas
.nodes
.iter()
.all(|n| n.color == Some(Color::Preset(4))),
"some nodes kept their old colour" "some nodes kept their old colour"
); );
@@ -2505,7 +2576,10 @@ mod tests {
#[test] #[test]
fn counts_read_naturally() { fn counts_read_naturally() {
assert_eq!(count_phrase(&[(1, "node"), (0, "edge")]), "1 node"); assert_eq!(count_phrase(&[(1, "node"), (0, "edge")]), "1 node");
assert_eq!(count_phrase(&[(2, "node"), (3, "edge")]), "2 nodes and 3 edges"); assert_eq!(
count_phrase(&[(2, "node"), (3, "edge")]),
"2 nodes and 3 edges"
);
assert_eq!(count_phrase(&[(0, "node")]), "nothing"); assert_eq!(count_phrase(&[(0, "node")]), "nothing");
} }
} }