Popout editor for all cards
This commit is contained in:
@@ -32,7 +32,7 @@ reshuffle.
|
||||
| Double-click a note | Edit its Markdown in an editor window |
|
||||
| Double-click a file/link node | Edit a plain-text file, or open anything else with the system default application |
|
||||
| Right-click empty canvas | Menu of every node type, added where you clicked |
|
||||
| Right-click a node | Colour, bring to front / send to back, delete |
|
||||
| Right-click a node | Edit, fit the card to its text, colour, bring to front / send to back, delete |
|
||||
| Drag a dot on a node's edge | Draw a connection; drop it on empty canvas to create the node too |
|
||||
| Drag a corner or side handle | Resize |
|
||||
| Drag a group | Moves the group and everything inside it |
|
||||
|
||||
+9
-2
@@ -12,7 +12,7 @@ use crate::geometry::{bounds_of, node_rect};
|
||||
use crate::inspector;
|
||||
use crate::model::{Canvas, Node, NodeKind};
|
||||
use crate::theme::Palette;
|
||||
use crate::view::{CanvasView, Clipboard, InsertRequest};
|
||||
use crate::view::{CanvasView, Clipboard, EditTarget, InsertRequest};
|
||||
|
||||
/// How long a status message stays on screen.
|
||||
const STATUS_SECONDS: f64 = 5.0;
|
||||
@@ -546,8 +546,12 @@ impl App {
|
||||
}
|
||||
if ctx.input_mut(|i| i.key_pressed(Key::F2))
|
||||
&& let Some(id) = self.view.selection.only_node().map(str::to_owned)
|
||||
&& let EditTarget::Inspector(reason) = self.view.begin_edit(&self.doc, &id)
|
||||
{
|
||||
self.view.edit_node(&id);
|
||||
// Nothing to type into, so the fields that can be changed are put
|
||||
// where they are instead.
|
||||
self.show_inspector = true;
|
||||
self.set_status(reason, ctx);
|
||||
}
|
||||
if ctx.input_mut(|i| i.key_pressed(Key::F)) && !self.view.selection.is_empty() {
|
||||
self.view.zoom_to_selection(&self.doc.canvas);
|
||||
@@ -1112,6 +1116,9 @@ impl eframe::App for App {
|
||||
if let Some((kind, at)) = out.insert {
|
||||
self.insert_requested(kind, at, &ctx);
|
||||
}
|
||||
if out.reveal_inspector {
|
||||
self.show_inspector = true;
|
||||
}
|
||||
});
|
||||
|
||||
self.dialogs(&ctx);
|
||||
|
||||
+3
-3
@@ -101,11 +101,11 @@ fn node_section(
|
||||
{
|
||||
out.open_target = Some(file.clone());
|
||||
}
|
||||
// Editing happens on the canvas, in the node itself; this is
|
||||
// the way in for anyone who has not found the double-click.
|
||||
// Editing happens in a window of its own; this is the way in
|
||||
// for anyone who has not found the double-click.
|
||||
if ui
|
||||
.button("Edit")
|
||||
.on_hover_text("Change the file itself, in the node on the canvas")
|
||||
.on_hover_text("Change the file itself, in an editor window")
|
||||
.clicked()
|
||||
{
|
||||
out.status = Some(if view.edit_file_contents(doc, id) {
|
||||
|
||||
+252
-50
@@ -250,6 +250,18 @@ pub struct ViewOutput {
|
||||
pub status: Option<String>,
|
||||
/// A node to add, and where in the canvas it goes.
|
||||
pub insert: Option<(InsertRequest, Pos2)>,
|
||||
/// Whether to show the inspector, for a node whose fields live there.
|
||||
pub reveal_inspector: bool,
|
||||
}
|
||||
|
||||
/// Where an edit request landed.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum EditTarget {
|
||||
/// The editor took it: a window, or a group's label on the canvas.
|
||||
Editor,
|
||||
/// The node has nothing to type into. The message says where its fields
|
||||
/// are instead.
|
||||
Inspector(&'static str),
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -274,11 +286,10 @@ pub struct CanvasView {
|
||||
pub snap_to_grid: bool,
|
||||
/// The text of every file the canvas shows, edits included.
|
||||
pub files: FileStore,
|
||||
/// How tall the contents of the node being edited came out this frame, so
|
||||
/// that the editor can offer to grow the card to fit them. `None` while
|
||||
/// nothing is being edited, or while the card is drawn too small to
|
||||
/// measure.
|
||||
body_height: Option<f32>,
|
||||
/// How tall each card's contents came out the last time it was drawn, so
|
||||
/// that a card can be sized to fit them. Cards drawn too small to measure
|
||||
/// are left out; the rest are one frame behind at most.
|
||||
body_heights: HashMap<String, f32>,
|
||||
/// Where the open context menu was summoned, in canvas coordinates. The
|
||||
/// pointer wanders off while the menu is up, so a node added from it has to
|
||||
/// land where the menu was opened and not where the click on the entry was.
|
||||
@@ -337,7 +348,7 @@ impl CanvasView {
|
||||
connect_target: None,
|
||||
snap_to_grid: false,
|
||||
files: FileStore::default(),
|
||||
body_height: None,
|
||||
body_heights: HashMap::new(),
|
||||
menu_at: None,
|
||||
viewport: None,
|
||||
pending_fit: None,
|
||||
@@ -361,6 +372,32 @@ impl CanvasView {
|
||||
self.focus_editor = true;
|
||||
}
|
||||
|
||||
/// Takes an edit request on any node, reporting where the editing has to
|
||||
/// happen: here, or in the inspector for a node with no text in it.
|
||||
pub fn begin_edit(&mut self, doc: &Document, id: &str) -> EditTarget {
|
||||
let Some(node) = doc.canvas.node(id) else {
|
||||
return EditTarget::Editor;
|
||||
};
|
||||
let reason = match &node.kind {
|
||||
NodeKind::Text { .. } | NodeKind::Group { .. } => None,
|
||||
NodeKind::File { .. } if self.can_edit_file(doc, id) => None,
|
||||
NodeKind::File { .. } => Some(
|
||||
"Only plain-text files are edited here \u{2014} the file and subpath are in the inspector",
|
||||
),
|
||||
NodeKind::Link { .. } => Some("A link node's address and label are in the inspector"),
|
||||
NodeKind::Unknown { .. } => {
|
||||
Some("This node type is not one this editor knows \u{2014} it is kept as it stands")
|
||||
}
|
||||
};
|
||||
match reason {
|
||||
Some(reason) => EditTarget::Inspector(reason),
|
||||
None => {
|
||||
self.edit_node(id);
|
||||
EditTarget::Editor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens the editor on the file behind a file node, reporting `false`
|
||||
/// for a file this editor will not change.
|
||||
pub fn edit_file_contents(&mut self, doc: &Document, id: &str) -> bool {
|
||||
@@ -635,8 +672,13 @@ impl CanvasView {
|
||||
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);
|
||||
// Measured again below, by drawing the card the editor is open on.
|
||||
self.body_height = None;
|
||||
// Cards that are gone leave a measurement behind. The map only ever
|
||||
// holds cards that have been drawn, so it outgrows the canvas only
|
||||
// once one of them is deleted.
|
||||
if self.body_heights.len() > doc.canvas.nodes.len() {
|
||||
self.body_heights
|
||||
.retain(|id, _| doc.canvas.node(id).is_some());
|
||||
}
|
||||
if self
|
||||
.editing
|
||||
.as_ref()
|
||||
@@ -896,6 +938,10 @@ impl CanvasView {
|
||||
.layout(Layout::top_down(Align::Min)),
|
||||
);
|
||||
body.set_clip_rect(inner.intersect(scene.clip_rect()));
|
||||
// A card is dragged and double-clicked, so its text is not
|
||||
// there to be selected: egui's selectable labels would eat
|
||||
// both gestures wherever a line of text lies under them.
|
||||
body.style_mut().interaction.selectable_labels = false;
|
||||
let mut ctx = BodyContext {
|
||||
palette,
|
||||
base_dir: base_dir.as_deref(),
|
||||
@@ -910,12 +956,13 @@ impl CanvasView {
|
||||
{
|
||||
output.open_target = Some(target);
|
||||
}
|
||||
// What the editor would have to grow the card to. Detail
|
||||
// is dropped from a card drawn small (see
|
||||
// What fitting the card would size it to. Detail is
|
||||
// dropped from a card drawn small (see
|
||||
// `nodes::render_body`), and with it any hope of measuring
|
||||
// one.
|
||||
if self.editing.as_deref() == Some(id.as_str()) && scale >= 0.4 {
|
||||
self.body_height = Some(body.min_rect().height());
|
||||
if scale >= 0.4 {
|
||||
self.body_heights
|
||||
.insert(id.clone(), body.min_rect().height());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -953,17 +1000,14 @@ impl CanvasView {
|
||||
// The menu only records what was asked for. Deleting or reordering
|
||||
// nodes here would pull the canvas out from under this very loop,
|
||||
// so `handle_interactions` carries the action out afterwards.
|
||||
let can_fit = self.can_fit_selection();
|
||||
response.context_menu(|ui| {
|
||||
// Whether the file behind the node can be edited is only worth
|
||||
// working out once the menu is open, because finding out reads
|
||||
// the file.
|
||||
let edit = selected <= 1 && self.can_edit_file(doc, &id);
|
||||
node_menu(
|
||||
ui,
|
||||
palette,
|
||||
color.as_ref(),
|
||||
selected,
|
||||
edit,
|
||||
can_fit,
|
||||
&mut interactions.menu,
|
||||
);
|
||||
});
|
||||
@@ -1120,7 +1164,7 @@ impl CanvasView {
|
||||
return;
|
||||
};
|
||||
let mut buffer = text.clone();
|
||||
let fits = self.body_height;
|
||||
let fits = self.body_heights.get(&doc.canvas.nodes[index].id).copied();
|
||||
let focus = std::mem::take(&mut self.focus_editor);
|
||||
let at = focus.then(|| self.editor_pos(&doc.canvas.nodes[index]));
|
||||
let editor = Editor {
|
||||
@@ -1184,7 +1228,7 @@ impl CanvasView {
|
||||
return;
|
||||
};
|
||||
self.file_edit = Some(FileEdit {
|
||||
id,
|
||||
id: id.clone(),
|
||||
path: path.clone(),
|
||||
section,
|
||||
buffer,
|
||||
@@ -1198,7 +1242,7 @@ impl CanvasView {
|
||||
// Said plainly, because this is the one editor whose typing lands
|
||||
// somewhere other than the canvas.
|
||||
let note = format!("Writes to {file}, and is written out when the canvas is saved");
|
||||
let fits = self.body_height;
|
||||
let fits = self.body_heights.get(&id).copied();
|
||||
let focus = std::mem::take(&mut self.focus_editor);
|
||||
let at = focus.then(|| self.editor_pos(&doc.canvas.nodes[index]));
|
||||
let editor = Editor {
|
||||
@@ -1257,14 +1301,12 @@ impl CanvasView {
|
||||
const GAP: f32 = 16.0;
|
||||
let card = self.to_global * node_rect(node);
|
||||
let view = self.viewport();
|
||||
let right = card.right() + GAP;
|
||||
let left = card.left() - GAP - EDITOR_SIZE.x;
|
||||
// Beside the card on whichever side has the room, and failing both,
|
||||
// wherever it fits.
|
||||
let x = if right + EDITOR_SIZE.x <= view.right() {
|
||||
right
|
||||
// Beside the card on whichever side has more room for it; when
|
||||
// neither side has enough, that is the side it covers least of.
|
||||
let x = if view.right() - card.right() >= card.left() - view.left() {
|
||||
card.right() + GAP
|
||||
} else {
|
||||
left
|
||||
card.left() - GAP - EDITOR_SIZE.x
|
||||
};
|
||||
let fit = |v: f32, min: f32, max: f32| v.clamp(min, max.max(min));
|
||||
pos2(
|
||||
@@ -1288,11 +1330,41 @@ impl CanvasView {
|
||||
);
|
||||
let height = (self.snap_rect(rect).height().round() as i32).max(MIN_NODE_SIZE);
|
||||
if height != node.height {
|
||||
doc.change("Fit card to text");
|
||||
// Left open, so that fitting several cards at once is one step to
|
||||
// undo; every caller closes it.
|
||||
doc.begin_change("Fit to text");
|
||||
doc.canvas.nodes[index].height = height;
|
||||
}
|
||||
}
|
||||
|
||||
/// Sizes every measured card in the selection to its contents, reporting
|
||||
/// what stood in the way when none of them could be.
|
||||
fn fit_selection(&self, doc: &mut Document) -> Option<String> {
|
||||
let mut fitted = 0;
|
||||
for id in self.selection.nodes.clone() {
|
||||
let Some(content) = self.body_heights.get(&id).copied() else {
|
||||
continue;
|
||||
};
|
||||
let Some(index) = doc.canvas.nodes.iter().position(|node| node.id == id) else {
|
||||
continue;
|
||||
};
|
||||
self.fit_card(doc, index, content);
|
||||
fitted += 1;
|
||||
}
|
||||
doc.end_change();
|
||||
(fitted == 0)
|
||||
.then(|| "Zoom in far enough for the cards to be drawn, then fit them".to_owned())
|
||||
}
|
||||
|
||||
/// Whether anything in the selection is a card that could be fitted: a
|
||||
/// group has no contents to fit, and nor has a card too small to measure.
|
||||
fn can_fit_selection(&self) -> bool {
|
||||
self.selection
|
||||
.nodes
|
||||
.iter()
|
||||
.any(|id| self.body_heights.contains_key(id))
|
||||
}
|
||||
|
||||
/// The text an editor on `path` starts from: the whole file, or the lines
|
||||
/// the section `subpath` names covers.
|
||||
///
|
||||
@@ -1733,10 +1805,14 @@ impl CanvasView {
|
||||
}
|
||||
match interactions.menu {
|
||||
Some(NodeAction::Edit) => {
|
||||
if let Some(id) = self.selection.only_node().map(str::to_owned) {
|
||||
self.edit_node(&id);
|
||||
if let Some(id) = self.selection.only_node().map(str::to_owned)
|
||||
&& let EditTarget::Inspector(reason) = self.begin_edit(doc, &id)
|
||||
{
|
||||
output.reveal_inspector = true;
|
||||
output.status = Some(reason.to_owned());
|
||||
}
|
||||
}
|
||||
Some(NodeAction::Fit) => output.status = self.fit_selection(doc),
|
||||
Some(NodeAction::SetColor(color)) => self.set_selection_color(doc, color),
|
||||
Some(NodeAction::Raise(to_front)) => self.raise_selection(doc, to_front),
|
||||
Some(NodeAction::Delete) => output.status = self.delete_selection(doc),
|
||||
@@ -1928,6 +2004,8 @@ enum Pass {
|
||||
enum NodeAction {
|
||||
/// Open the editor on the one node the menu was summoned from.
|
||||
Edit,
|
||||
/// Size every card in the selection to its contents.
|
||||
Fit,
|
||||
SetColor(Option<Color>),
|
||||
/// `true` brings the selection to the front, `false` sends it to the back.
|
||||
Raise(bool),
|
||||
@@ -1986,22 +2064,29 @@ fn node_menu(
|
||||
palette: &Palette,
|
||||
current: Option<&Color>,
|
||||
selected: usize,
|
||||
edit: bool,
|
||||
can_fit: bool,
|
||||
action: &mut Option<NodeAction>,
|
||||
) {
|
||||
// Double-clicking a file node does this too, which is not something the
|
||||
// node itself gives away.
|
||||
if edit {
|
||||
if ui
|
||||
.button("Edit contents")
|
||||
.on_hover_text("Change the file itself, here on the canvas")
|
||||
// Double-clicking does this too, which is not something a node gives away.
|
||||
if selected <= 1
|
||||
&& ui
|
||||
.button("Edit")
|
||||
.on_hover_text("Open the editor on this node")
|
||||
.clicked()
|
||||
{
|
||||
*action = Some(NodeAction::Edit);
|
||||
ui.close();
|
||||
}
|
||||
ui.separator();
|
||||
if ui
|
||||
.add_enabled(can_fit, Button::new("Fit to text"))
|
||||
.on_hover_text("Size the card to show all of the text, and no more")
|
||||
.on_disabled_hover_text("Zoom in far enough for the cards to be drawn")
|
||||
.clicked()
|
||||
{
|
||||
*action = Some(NodeAction::Fit);
|
||||
ui.close();
|
||||
}
|
||||
ui.separator();
|
||||
ui.menu_button("Colour", |ui| {
|
||||
for (index, name) in Color::PRESET_NAMES.iter().enumerate() {
|
||||
let preset = Color::Preset(index as u8 + 1);
|
||||
@@ -2150,17 +2235,19 @@ fn editor_contents(ui: &mut egui::Ui, buffer: &mut String, editor: Editor<'_>) -
|
||||
// everything that is left of it.
|
||||
egui::Panel::bottom("editor-actions").show(ui, |ui| {
|
||||
ui.add_space(6.0);
|
||||
// The row of a horizontal layout, so that the right-to-left one inside
|
||||
// it is measured against a line of buttons and not the whole window.
|
||||
ui.horizontal(|ui| {
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
pass.close = ui.button("Close").clicked();
|
||||
// The card crops what it cannot show, so the way out that
|
||||
// leaves nothing hidden sits right beside the plain one.
|
||||
pass.fit = ui
|
||||
.add_enabled(
|
||||
editor.content_height.is_some(),
|
||||
Button::new("Fit card to text"),
|
||||
)
|
||||
.add_enabled(editor.content_height.is_some(), Button::new("Close & Fit"))
|
||||
.on_hover_text("Size the card to show all of the text, and no more")
|
||||
.on_disabled_hover_text("Zoom in far enough to see the card to measure it")
|
||||
.clicked();
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
pass.close = ui.button("Done").clicked();
|
||||
pass.close |= pass.fit;
|
||||
});
|
||||
});
|
||||
ui.add_space(2.0);
|
||||
@@ -2527,10 +2614,10 @@ mod tests {
|
||||
(dir, note, doc)
|
||||
}
|
||||
|
||||
/// The editor offers to grow the card to what was written in it, which
|
||||
/// means measuring the contents of a card that crops them.
|
||||
/// Fitting a card means measuring contents the card itself crops, and
|
||||
/// fitting several of them is one thing done, not one per card.
|
||||
#[test]
|
||||
fn the_editor_measures_the_card_and_grows_it_to_fit() {
|
||||
fn cards_are_measured_and_fitted_to_their_text_in_one_step() {
|
||||
let ctx = egui::Context::default();
|
||||
let screen = Rect::from_min_size(Pos2::ZERO, vec2(1200.0, 900.0));
|
||||
let mut doc = Document::new();
|
||||
@@ -2543,19 +2630,134 @@ mod tests {
|
||||
view.edit_node("a");
|
||||
pass(&ctx, &mut view, &mut doc, screen, vec![]);
|
||||
|
||||
let height = view.body_height.expect("the card was never measured");
|
||||
let height = view
|
||||
.body_heights
|
||||
.get("a")
|
||||
.copied()
|
||||
.expect("the card was never measured");
|
||||
assert!(
|
||||
height > 100.0,
|
||||
"the text measured {height}, which the card already fits"
|
||||
);
|
||||
|
||||
view.fit_card(&mut doc, 0, height);
|
||||
// Both cards at once, the way the menu fits a whole selection.
|
||||
view.selection.set_node("a");
|
||||
view.selection.nodes.push("b".to_owned());
|
||||
assert_eq!(view.fit_selection(&mut doc), None);
|
||||
let grown = doc.canvas.node("a").unwrap().height;
|
||||
assert!(
|
||||
grown as f32 >= height,
|
||||
"the card came out {grown} tall for {height} of text"
|
||||
);
|
||||
assert_eq!(doc.undo_label(), Some("Fit card to text"));
|
||||
|
||||
// And the two of them are one step to undo, not two.
|
||||
assert_eq!(doc.undo(), Some("Fit to text".to_owned()));
|
||||
assert_eq!(doc.canvas.node("a").unwrap().height, 100);
|
||||
assert_eq!(doc.canvas.node("b").unwrap().height, 100);
|
||||
assert!(!doc.can_undo(), "fitting left more than one step behind");
|
||||
}
|
||||
|
||||
/// A double click on a card opens the editor wherever it lands — the
|
||||
/// text of a card is not there to be selected, and egui's selectable
|
||||
/// labels would take the gesture for a word.
|
||||
#[test]
|
||||
fn a_double_click_on_a_card_s_text_opens_the_editor() {
|
||||
let ctx = egui::Context::default();
|
||||
let screen = Rect::from_min_size(Pos2::ZERO, vec2(1200.0, 900.0));
|
||||
let mut doc = Document::new();
|
||||
doc.canvas = canvas_with_two_nodes();
|
||||
doc.canvas.node_mut("a").unwrap().kind = NodeKind::Text {
|
||||
text: "some words to land on".to_owned(),
|
||||
};
|
||||
let mut view = CanvasView::new();
|
||||
|
||||
// The first line of the card's text, just inside the padding.
|
||||
let at = pos2(30.0, 20.0);
|
||||
pass(
|
||||
&ctx,
|
||||
&mut view,
|
||||
&mut doc,
|
||||
screen,
|
||||
vec![egui::Event::PointerMoved(at)],
|
||||
);
|
||||
for _ in 0..2 {
|
||||
pass(&ctx, &mut view, &mut doc, screen, vec![click(at)]);
|
||||
pass(&ctx, &mut view, &mut doc, screen, vec![release(at)]);
|
||||
}
|
||||
|
||||
assert_eq!(view.editing.as_deref(), Some("a"));
|
||||
}
|
||||
|
||||
/// The editor's ways out sit in a strip along the bottom of the window,
|
||||
/// with the text taking everything above them.
|
||||
#[test]
|
||||
fn the_editor_keeps_its_actions_in_a_strip_at_the_bottom() {
|
||||
let ctx = egui::Context::default();
|
||||
let screen = Rect::from_min_size(Pos2::ZERO, vec2(1200.0, 900.0));
|
||||
let mut doc = Document::new();
|
||||
doc.canvas = canvas_with_two_nodes();
|
||||
let mut view = CanvasView::new();
|
||||
view.edit_node("a");
|
||||
// Twice: the window is laid out against the rectangles of the pass
|
||||
// before it.
|
||||
pass(&ctx, &mut view, &mut doc, screen, vec![]);
|
||||
pass(&ctx, &mut view, &mut doc, screen, vec![]);
|
||||
|
||||
let window = ctx
|
||||
.memory(|m| m.area_rect(Id::new("node-editor")))
|
||||
.expect("the editor window was never laid out");
|
||||
let actions = egui::containers::panel::PanelState::load(&ctx, Id::new("editor-actions"))
|
||||
.expect("the editor's actions were never laid out")
|
||||
.outer_rect;
|
||||
assert!(
|
||||
actions.height() < window.height() / 3.0,
|
||||
"the actions took {} of a {} window",
|
||||
actions.height(),
|
||||
window.height()
|
||||
);
|
||||
assert!(
|
||||
actions.bottom() >= window.bottom() - 8.0,
|
||||
"the actions sit at {}, not at the bottom of the window ({})",
|
||||
actions.bottom(),
|
||||
window.bottom()
|
||||
);
|
||||
|
||||
// And the plain way out is where a user would aim for it: the far end
|
||||
// of that strip.
|
||||
let at = pos2(actions.right() - 30.0, actions.center().y);
|
||||
pass(&ctx, &mut view, &mut doc, screen, vec![click(at)]);
|
||||
pass(&ctx, &mut view, &mut doc, screen, vec![release(at)]);
|
||||
assert_eq!(view.editing, None, "the button at {at:?} did not close it");
|
||||
}
|
||||
|
||||
/// Every node takes an edit request; the ones with no text in them say
|
||||
/// where their fields are instead of doing nothing.
|
||||
#[test]
|
||||
fn an_edit_request_on_a_node_with_no_text_points_at_the_inspector() {
|
||||
let mut doc = Document::new();
|
||||
doc.canvas = canvas_with_two_nodes();
|
||||
doc.canvas.nodes.push(Node::new(
|
||||
"l",
|
||||
NodeKind::Link {
|
||||
url: "https://example.com".to_owned(),
|
||||
label: None,
|
||||
},
|
||||
0,
|
||||
300,
|
||||
200,
|
||||
60,
|
||||
));
|
||||
let mut view = CanvasView::new();
|
||||
|
||||
assert_eq!(view.begin_edit(&doc, "a"), EditTarget::Editor);
|
||||
assert_eq!(view.editing.as_deref(), Some("a"));
|
||||
|
||||
let EditTarget::Inspector(reason) = view.begin_edit(&doc, "l") else {
|
||||
panic!("the editor took a link node, which has nothing to type into");
|
||||
};
|
||||
assert!(reason.contains("inspector"), "unhelpful reason: {reason:?}");
|
||||
// And the editor it could not take the node into is left as it was.
|
||||
assert_eq!(view.editing.as_deref(), Some("a"));
|
||||
}
|
||||
|
||||
/// The editor is a window with its own ways out, so the canvas no longer
|
||||
|
||||
Reference in New Issue
Block a user