Apply cargo fmt
This commit is contained in:
+27
-7
@@ -236,9 +236,13 @@ impl App {
|
||||
}
|
||||
|
||||
fn insert_text_node(&mut self, at: Pos2, ctx: &Context) {
|
||||
let id = self
|
||||
.view
|
||||
.insert_node(&mut self.doc, NodeKind::Text { text: String::new() }, at);
|
||||
let id = self.view.insert_node(
|
||||
&mut self.doc,
|
||||
NodeKind::Text {
|
||||
text: String::new(),
|
||||
},
|
||||
at,
|
||||
);
|
||||
self.view.edit_node(&id);
|
||||
self.set_status("Added a text node", ctx);
|
||||
}
|
||||
@@ -513,7 +517,11 @@ impl App {
|
||||
}
|
||||
|
||||
// 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;
|
||||
for (key, direction) in [
|
||||
(Key::ArrowLeft, egui::vec2(-1.0, 0.0)),
|
||||
@@ -742,10 +750,18 @@ impl App {
|
||||
|
||||
fn toolbar(&mut self, ui: &mut Ui, ctx: &Context) {
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
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.toggle_value(&mut self.show_inspector, "Inspector");
|
||||
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() {
|
||||
self.dark_mode = !self.dark_mode;
|
||||
}
|
||||
|
||||
+5
-4
@@ -231,9 +231,9 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn saving_and_loading_round_trips(
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let dir = std::env::temp_dir().join(format!("jsoncanvas-test-{}", crate::model::random_id()));
|
||||
fn saving_and_loading_round_trips() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let dir =
|
||||
std::env::temp_dir().join(format!("jsoncanvas-test-{}", crate::model::random_id()));
|
||||
fs::create_dir_all(&dir)?;
|
||||
let path = dir.join("test.canvas");
|
||||
|
||||
@@ -252,7 +252,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
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();
|
||||
let path = dir.join("bad.canvas");
|
||||
fs::write(&path, "{ not json").unwrap();
|
||||
|
||||
+7
-2
@@ -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.
|
||||
pub fn bezier_tangent(p: [Pos2; 4], t: f32) -> Vec2 {
|
||||
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 {
|
||||
d.normalized()
|
||||
} else {
|
||||
@@ -126,7 +128,10 @@ pub fn distance_to_curve(p: [Pos2; 4], point: Pos2) -> f32 {
|
||||
pub fn curve_bounds(p: [Pos2; 4]) -> Rect {
|
||||
let mut rect = Rect::from_points(&[p[0], p[3]]);
|
||||
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
|
||||
}
|
||||
|
||||
+57
-19
@@ -53,7 +53,11 @@ fn node_section(
|
||||
let Some(node) = doc.canvas.node(id).cloned() else {
|
||||
return;
|
||||
};
|
||||
heading(ui, palette, &format!("{} node", capitalise(node.kind.type_name())));
|
||||
heading(
|
||||
ui,
|
||||
palette,
|
||||
&format!("{} node", capitalise(node.kind.type_name())),
|
||||
);
|
||||
|
||||
match &node.kind {
|
||||
NodeKind::Text { text } => {
|
||||
@@ -66,7 +70,8 @@ fn node_section(
|
||||
);
|
||||
if response.changed() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -89,7 +94,11 @@ fn node_section(
|
||||
doc.change("Set file");
|
||||
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());
|
||||
}
|
||||
});
|
||||
@@ -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"));
|
||||
let mut changed = false;
|
||||
ui.horizontal(|ui| {
|
||||
changed |= ui.add(DragValue::new(&mut x).speed(1).prefix("x ")).changed();
|
||||
changed |= ui.add(DragValue::new(&mut y).speed(1).prefix("y ")).changed();
|
||||
changed |= ui
|
||||
.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| {
|
||||
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 |= 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();
|
||||
});
|
||||
if changed {
|
||||
@@ -316,7 +339,11 @@ fn edge_section(
|
||||
.unwrap_or_else(|| format!("missing node {node_id}"))
|
||||
};
|
||||
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)
|
||||
.color(palette.text_muted),
|
||||
);
|
||||
@@ -393,10 +420,7 @@ fn edge_section(
|
||||
|
||||
fn side_combo(ui: &mut Ui, label: &str, side: &mut Option<Side>) {
|
||||
ComboBox::from_id_salt(label)
|
||||
.selected_text(format!(
|
||||
"{label}: {}",
|
||||
side.map_or("auto", Side::label)
|
||||
))
|
||||
.selected_text(format!("{label}: {}", side.map_or("auto", Side::label)))
|
||||
.width(110.0)
|
||||
.show_ui(ui, |ui| {
|
||||
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() {
|
||||
doc.change("Remove 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",
|
||||
"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> {
|
||||
let mut dialog = rfd::FileDialog::new().add_filter(
|
||||
"Images",
|
||||
&["png", "jpg", "jpeg", "gif", "bmp", "webp"],
|
||||
);
|
||||
let mut dialog =
|
||||
rfd::FileDialog::new().add_filter("Images", &["png", "jpg", "jpeg", "gif", "bmp", "webp"]);
|
||||
if let Some(dir) = doc.base_dir() {
|
||||
dialog = dialog.set_directory(dir);
|
||||
}
|
||||
@@ -787,7 +816,16 @@ mod tests {
|
||||
use crate::view::CanvasView;
|
||||
|
||||
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]
|
||||
|
||||
+64
-20
@@ -44,12 +44,25 @@ pub type Cell = Vec<Span>;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Block {
|
||||
Heading { level: u8, spans: Vec<Span> },
|
||||
Heading {
|
||||
level: u8,
|
||||
spans: Vec<Span>,
|
||||
},
|
||||
Paragraph(Vec<Span>),
|
||||
List { indent: usize, marker: Marker, spans: Vec<Span> },
|
||||
List {
|
||||
indent: usize,
|
||||
marker: Marker,
|
||||
spans: Vec<Span>,
|
||||
},
|
||||
Quote(Vec<Span>),
|
||||
Code { language: String, text: String },
|
||||
Table { header: Vec<Cell>, rows: Vec<Vec<Cell>> },
|
||||
Code {
|
||||
language: String,
|
||||
text: String,
|
||||
},
|
||||
Table {
|
||||
header: Vec<Cell>,
|
||||
rows: Vec<Vec<Cell>>,
|
||||
},
|
||||
Rule,
|
||||
}
|
||||
|
||||
@@ -129,9 +142,7 @@ pub fn parse(source: &str) -> Vec<Block> {
|
||||
}
|
||||
|
||||
// A pipe table: a header row, a `| --- |` separator, then the body.
|
||||
if trimmed.contains('|')
|
||||
&& lines.peek().is_some_and(|next| is_separator_row(next))
|
||||
{
|
||||
if trimmed.contains('|') && lines.peek().is_some_and(|next| is_separator_row(next)) {
|
||||
flush(&mut paragraph, &mut blocks, false);
|
||||
flush(&mut quote, &mut blocks, true);
|
||||
let header = split_row(trimmed);
|
||||
@@ -218,7 +229,8 @@ fn heading_of(line: &str) -> Option<(u8, &str)> {
|
||||
if (1..=6).contains(&hashes) {
|
||||
let rest = &line[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 {
|
||||
None
|
||||
}
|
||||
@@ -237,12 +249,18 @@ fn list_item_of(line: &str) -> Option<(usize, Marker, &str)> {
|
||||
let rest_trimmed = rest.trim_start();
|
||||
let lowered = rest_trimmed.to_ascii_lowercase();
|
||||
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
|
||||
.strip_prefix("[x] ")
|
||||
.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 {
|
||||
(Marker::Bullet, rest)
|
||||
}
|
||||
@@ -252,7 +270,9 @@ fn list_item_of(line: &str) -> Option<(usize, Marker, &str)> {
|
||||
return None;
|
||||
}
|
||||
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)
|
||||
};
|
||||
|
||||
@@ -304,7 +324,10 @@ fn parse_inline_into(text: &str, style: Style, out: &mut Vec<Span>) {
|
||||
flush!();
|
||||
out.push(Span {
|
||||
text: inner.to_owned(),
|
||||
style: Style { code: true, ..style },
|
||||
style: Style {
|
||||
code: true,
|
||||
..style
|
||||
},
|
||||
link: None,
|
||||
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, "__")) {
|
||||
flush!();
|
||||
parse_inline_into(inner, Style { bold: true, ..style }, out);
|
||||
parse_inline_into(
|
||||
inner,
|
||||
Style {
|
||||
bold: true,
|
||||
..style
|
||||
},
|
||||
out,
|
||||
);
|
||||
rest = tail;
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some((inner, tail)) = delimited(rest, "~~") {
|
||||
flush!();
|
||||
parse_inline_into(inner, Style { strike: true, ..style }, out);
|
||||
parse_inline_into(
|
||||
inner,
|
||||
Style {
|
||||
strike: true,
|
||||
..style
|
||||
},
|
||||
out,
|
||||
);
|
||||
rest = tail;
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some((inner, tail)) = delimited(rest, "*").or_else(|| delimited(rest, "_")) {
|
||||
flush!();
|
||||
parse_inline_into(inner, Style { italic: true, ..style }, out);
|
||||
parse_inline_into(
|
||||
inner,
|
||||
Style {
|
||||
italic: true,
|
||||
..style
|
||||
},
|
||||
out,
|
||||
);
|
||||
rest = tail;
|
||||
continue;
|
||||
}
|
||||
@@ -540,10 +584,8 @@ pub fn render(ui: &mut Ui, blocks: &[Block], palette: &Palette, base_size: f32)
|
||||
Block::Rule => {
|
||||
let stroke = egui::Stroke::new(1.0, palette.border);
|
||||
let width = ui.available_width();
|
||||
let (rect, _) = ui.allocate_exact_size(
|
||||
egui::vec2(width, base_size * 0.6),
|
||||
egui::Sense::hover(),
|
||||
);
|
||||
let (rect, _) = ui
|
||||
.allocate_exact_size(egui::vec2(width, base_size * 0.6), egui::Sense::hover());
|
||||
ui.painter().line_segment(
|
||||
[
|
||||
egui::pos2(rect.left(), rect.center().y),
|
||||
@@ -712,7 +754,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
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 {
|
||||
panic!("expected a table, got {blocks:?}");
|
||||
};
|
||||
|
||||
+15
-18
@@ -46,8 +46,9 @@ impl PreviewCache {
|
||||
}
|
||||
|
||||
fn text_preview(&mut self, path: &Path) -> &Result<String, String> {
|
||||
self.text.entry(path.to_path_buf()).or_insert_with(|| {
|
||||
match std::fs::read(path) {
|
||||
self.text
|
||||
.entry(path.to_path_buf())
|
||||
.or_insert_with(|| match std::fs::read(path) {
|
||||
Ok(bytes) => {
|
||||
let truncated = bytes.len() > MAX_PREVIEW_BYTES;
|
||||
let head = &bytes[..bytes.len().min(MAX_PREVIEW_BYTES)];
|
||||
@@ -58,7 +59,6 @@ impl PreviewCache {
|
||||
Ok(text)
|
||||
}
|
||||
Err(err) => Err(format!("{err}")),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -167,11 +167,7 @@ fn render_file(
|
||||
activated = Some(file.to_owned());
|
||||
}
|
||||
if let Some(subpath) = subpath {
|
||||
ui.label(
|
||||
RichText::new(subpath)
|
||||
.size(12.0)
|
||||
.color(palette.text_muted),
|
||||
);
|
||||
ui.label(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.
|
||||
fn host_of(url: &str) -> &str {
|
||||
let rest = url
|
||||
.split_once("://")
|
||||
.map(|(_, rest)| rest)
|
||||
.unwrap_or(url);
|
||||
let rest = url.split_once("://").map(|(_, rest)| rest).unwrap_or(url);
|
||||
let host = rest.split(['/', '?', '#']).next().unwrap_or(rest);
|
||||
if host.is_empty() { url } else { host }
|
||||
}
|
||||
@@ -327,11 +320,12 @@ mod tests {
|
||||
PathBuf::from("/vault/boards/notes/a.md")
|
||||
);
|
||||
// Absolute paths are left alone.
|
||||
let absolute = if cfg!(windows) { "C:\\tmp\\a.md" } else { "/tmp/a.md" };
|
||||
assert_eq!(
|
||||
resolve_path(Some(base), absolute),
|
||||
PathBuf::from(absolute)
|
||||
);
|
||||
let absolute = if cfg!(windows) {
|
||||
"C:\\tmp\\a.md"
|
||||
} else {
|
||||
"/tmp/a.md"
|
||||
};
|
||||
assert_eq!(resolve_path(Some(base), absolute), PathBuf::from(absolute));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -358,7 +352,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -129,7 +129,8 @@ pub fn blend(fg: Color32, bg: Color32, strength: f32) -> Color32 {
|
||||
|
||||
/// Black or white, whichever stays readable on `background`.
|
||||
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;
|
||||
if luma > 150.0 {
|
||||
Color32::from_rgb(0x1e, 0x1e, 0x24)
|
||||
|
||||
+114
-40
@@ -6,9 +6,7 @@ use std::collections::HashMap;
|
||||
use eframe::egui::{
|
||||
self, Align, Atom, Button, Color32, CornerRadius, CursorIcon, Id, LayerId, Layout,
|
||||
PointerButton, Pos2, Rect, RichText, Sense, Shape, Stroke, StrokeKind, TextEdit, UiBuilder,
|
||||
Vec2,
|
||||
containers::scroll_area::ScrollSource,
|
||||
epaint, pos2, vec2,
|
||||
Vec2, containers::scroll_area::ScrollSource, epaint, pos2, vec2,
|
||||
};
|
||||
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,
|
||||
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::theme::{self, Palette};
|
||||
|
||||
@@ -215,9 +213,15 @@ enum Gesture {
|
||||
accumulated: Vec2,
|
||||
},
|
||||
/// 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`.
|
||||
Connect { from: String, side: Side },
|
||||
Connect {
|
||||
from: String,
|
||||
side: Side,
|
||||
},
|
||||
}
|
||||
|
||||
/// A node the user asked for from the canvas context menu. Insertion goes
|
||||
@@ -480,7 +484,9 @@ impl CanvasView {
|
||||
let edges: Vec<Edge> = canvas
|
||||
.edges
|
||||
.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()
|
||||
.collect();
|
||||
Some(Clipboard { nodes, edges })
|
||||
@@ -552,7 +558,11 @@ impl CanvasView {
|
||||
if self.selection.nodes.is_empty() {
|
||||
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 rest: Vec<Node> = Vec::new();
|
||||
for node in doc.canvas.nodes.drain(..) {
|
||||
@@ -588,19 +598,15 @@ impl CanvasView {
|
||||
|
||||
// -- the frame --------------------------------------------------------
|
||||
|
||||
pub fn show(
|
||||
&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
doc: &mut Document,
|
||||
palette: &Palette,
|
||||
) -> ViewOutput {
|
||||
pub fn show(&mut self, ui: &mut egui::Ui, doc: &mut Document, palette: &Palette) -> ViewOutput {
|
||||
let mut output = ViewOutput::default();
|
||||
self.selection.prune(&doc.canvas);
|
||||
// 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
|
||||
// one of them is gone.
|
||||
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
|
||||
.editing
|
||||
@@ -753,7 +759,11 @@ impl CanvasView {
|
||||
// Zoomed out far enough the dots crowd into noise, so only every fifth
|
||||
// one is kept; below that they are dropped altogether.
|
||||
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 {
|
||||
return;
|
||||
}
|
||||
@@ -803,12 +813,14 @@ impl CanvasView {
|
||||
continue;
|
||||
}
|
||||
|
||||
painter.add(Shape::CubicBezier(epaint::CubicBezierShape::from_points_stroke(
|
||||
painter.add(Shape::CubicBezier(
|
||||
epaint::CubicBezierShape::from_points_stroke(
|
||||
curve,
|
||||
false,
|
||||
Color32::TRANSPARENT,
|
||||
Stroke::new(width, color),
|
||||
)));
|
||||
),
|
||||
));
|
||||
|
||||
if edge.end_shape() == EndShape::Arrow {
|
||||
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,
|
||||
// so `handle_interactions` carries the action out afterwards.
|
||||
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
|
||||
@@ -985,7 +1003,12 @@ impl CanvasView {
|
||||
1.5
|
||||
};
|
||||
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 chip_fill = theme::blend(fill, palette.surface, 0.5);
|
||||
let text_color = theme::contrasting_text(chip_fill);
|
||||
let galley =
|
||||
painter.layout_no_wrap(text, egui::FontId::proportional(14.0), text_color);
|
||||
let galley = 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));
|
||||
painter.rect_filled(chip, CornerRadius::same(4), chip_fill);
|
||||
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) {
|
||||
let painter = ui.painter();
|
||||
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(
|
||||
node_rect(node).expand(2.5),
|
||||
CornerRadius::same((NODE_CORNER + 2.0) as u8),
|
||||
@@ -1214,7 +1238,10 @@ impl CanvasView {
|
||||
if response.dragged() {
|
||||
used = true;
|
||||
if let Gesture::Resize {
|
||||
accumulated, origin, handle, ..
|
||||
accumulated,
|
||||
origin,
|
||||
handle,
|
||||
..
|
||||
} = &mut self.gesture
|
||||
{
|
||||
*accumulated += response.drag_delta();
|
||||
@@ -1264,7 +1291,9 @@ impl CanvasView {
|
||||
);
|
||||
}
|
||||
Gesture::Connect { from, side } => {
|
||||
let Some(node) = canvas.node(from) else { return };
|
||||
let Some(node) = canvas.node(from) else {
|
||||
return;
|
||||
};
|
||||
let Some(pointer) = ui
|
||||
.ctx()
|
||||
.pointer_latest_pos()
|
||||
@@ -1281,13 +1310,21 @@ impl CanvasView {
|
||||
_ => (pointer, side.opposite()),
|
||||
};
|
||||
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,
|
||||
false,
|
||||
Color32::TRANSPARENT,
|
||||
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()
|
||||
&& 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);
|
||||
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.
|
||||
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 {
|
||||
NodeKind::Text { .. } | NodeKind::Group { .. } => self.edit_node(id),
|
||||
NodeKind::File { file, .. } => output.open_target = Some(file.clone()),
|
||||
@@ -1507,7 +1552,13 @@ impl CanvasView {
|
||||
}
|
||||
None => {
|
||||
// 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();
|
||||
doc.canvas
|
||||
.edges
|
||||
@@ -1885,8 +1936,7 @@ fn paint_group_background(
|
||||
)
|
||||
}
|
||||
};
|
||||
ui.painter()
|
||||
.image(texture.id, target, uv, Color32::WHITE);
|
||||
ui.painter().image(texture.id, target, uv, Color32::WHITE);
|
||||
}
|
||||
|
||||
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);
|
||||
// Three passes: egui hit-tests against the rectangles of the previous
|
||||
// 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![release(at)]);
|
||||
|
||||
@@ -2272,10 +2328,16 @@ mod tests {
|
||||
canvas
|
||||
.edges
|
||||
.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);
|
||||
// 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);
|
||||
}
|
||||
|
||||
@@ -2298,8 +2360,14 @@ mod tests {
|
||||
),
|
||||
);
|
||||
let view = CanvasView::new();
|
||||
assert_eq!(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"));
|
||||
assert_eq!(
|
||||
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]
|
||||
@@ -2491,7 +2559,10 @@ mod tests {
|
||||
|
||||
view.set_selection_color(&mut doc, Some(Color::Preset(4)));
|
||||
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"
|
||||
);
|
||||
|
||||
@@ -2505,7 +2576,10 @@ mod tests {
|
||||
#[test]
|
||||
fn counts_read_naturally() {
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user