Browser: Canvases folder (show/hide, Edit Canvas, Calibrate, Rename, Delete; a picked canvas goes with the Delete key)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
f6b42f8750
commit
0f4a0e7053
4 files changed
+232
-1
No files matched your search
@@ -34,6 +34,8 @@ pub struct TreeState {
|
|||||||
pub picked_components: Vec<u64>,
|
pub picked_components: Vec<u64>,
|
||||||
/// Canvases picked in the tree.
|
/// Canvases picked in the tree.
|
||||||
pub picked_canvases: Vec<u64>,
|
pub picked_canvases: Vec<u64>,
|
||||||
|
/// The Edit Canvas / Calibrate panel.
|
||||||
|
pub canvas_panel: Option<CanvasPanel>,
|
||||||
/// The last row clicked (for Shift ranges).
|
/// The last row clicked (for Shift ranges).
|
||||||
anchor: Option<String>,
|
anchor: Option<String>,
|
||||||
/// Row keys in drawing order this frame (for Shift ranges).
|
/// Row keys in drawing order this frame (for Shift ranges).
|
||||||
@@ -403,6 +405,7 @@ pub fn browser(app: &mut SolveApp, ui: &mut egui::Ui) {
|
|||||||
crate::context_menu::open_for(app, at, target);
|
crate::context_menu::open_for(app, at, target);
|
||||||
}
|
}
|
||||||
occurrence_panel(app, ui.ctx());
|
occurrence_panel(app, ui.ctx());
|
||||||
|
canvas_panel(app, ui.ctx());
|
||||||
redefine_panel(app, ui.ctx());
|
redefine_panel(app, ui.ctx());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -487,6 +490,7 @@ fn component_rows(app: &mut SolveApp, ui: &mut egui::Ui, id: u64, depth: usize,
|
|||||||
if id == 0 {
|
if id == 0 {
|
||||||
doc_settings(app, ui, d);
|
doc_settings(app, ui, d);
|
||||||
origin_rows(app, ui, d);
|
origin_rows(app, ui, d);
|
||||||
|
canvas_rows(app, ui, d, acts);
|
||||||
}
|
}
|
||||||
let _ = t;
|
let _ = t;
|
||||||
for folder in ["bodies", "sketches", "construction"] {
|
for folder in ["bodies", "sketches", "construction"] {
|
||||||
@@ -570,6 +574,183 @@ fn origin_rows(app: &mut SolveApp, ui: &mut egui::Ui, depth: usize) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The Canvases folder (reference images) with an eye per canvas.
|
||||||
|
fn canvas_rows(app: &mut SolveApp, ui: &mut egui::Ui, depth: usize, acts: &mut Actions) {
|
||||||
|
let canvases: Vec<(u64, String, bool)> = app.session.doc.canvases.iter().map(|c| (c.id, c.name.clone(), c.visible)).collect();
|
||||||
|
if canvases.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let key = "canvases";
|
||||||
|
let open = is_open(app, key, true);
|
||||||
|
let any = canvases.iter().any(|c| c.2);
|
||||||
|
let r =
|
||||||
|
draw_row(ui, ui.id().with(key), &Row { depth, fold: Some(open), eye: Some(any), icon: "folder", label: "Canvases", ..Default::default() });
|
||||||
|
if r.fold || r.clicked {
|
||||||
|
toggle(app, key, true);
|
||||||
|
}
|
||||||
|
if r.eye {
|
||||||
|
for (id, _, _) in &canvases {
|
||||||
|
let _ = app.run("canvas.edit", json!({ "canvas": id, "visible": !any }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !open {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (id, name, visible) in canvases {
|
||||||
|
let selected = app.tree.picked_canvases.contains(&id);
|
||||||
|
let r = draw_row(
|
||||||
|
ui,
|
||||||
|
ui.id().with(("canvas", id)),
|
||||||
|
&Row { depth: depth + 1, eye: Some(visible), icon: "canvas", label: &name, selected, dim: !visible, ..Default::default() },
|
||||||
|
);
|
||||||
|
if r.eye {
|
||||||
|
let _ = app.run("canvas.edit", json!({ "canvas": id, "visible": !visible }));
|
||||||
|
}
|
||||||
|
if r.clicked {
|
||||||
|
if !ui.input(|i| i.modifiers.command || i.modifiers.ctrl) {
|
||||||
|
app.tree.picked_canvases.clear();
|
||||||
|
app.tree.picked_components.clear();
|
||||||
|
let _ = app.run("select.clear", json!({}));
|
||||||
|
}
|
||||||
|
if selected {
|
||||||
|
app.tree.picked_canvases.retain(|c| *c != id);
|
||||||
|
} else {
|
||||||
|
app.tree.picked_canvases.push(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if r.double {
|
||||||
|
app.tree.canvas_panel = Some(CanvasPanel::new(app, id, false));
|
||||||
|
}
|
||||||
|
if let Some(p) = r.secondary {
|
||||||
|
if !selected {
|
||||||
|
app.tree.picked_canvases = vec![id];
|
||||||
|
}
|
||||||
|
acts.menu = Some((p, Target::Canvas { id }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The menu of a canvas.
|
||||||
|
pub fn canvas_items(app: &SolveApp, id: u64) -> Vec<Item> {
|
||||||
|
let visible = app.session.doc.canvases.iter().find(|c| c.id == id).is_some_and(|c| c.visible);
|
||||||
|
vec![
|
||||||
|
Item::action("ui.editCanvas", "Edit Canvas", "canvas").with(json!({ "canvas": id })),
|
||||||
|
Item::action("ui.calibrateCanvas", "Calibrate", "measure").with(json!({ "canvas": id })),
|
||||||
|
Item::sep(),
|
||||||
|
Item::action("ui.delete", "Delete", "delete").with(json!({ "canvases": [id] })),
|
||||||
|
Item::action("ui.rename", "Rename", "").with(json!({ "canvas": id })),
|
||||||
|
Item::sep(),
|
||||||
|
Item::action("ui.canvasVisible", if visible { "Hide" } else { "Show" }, "eye").with(json!({ "canvas": id, "visible": !visible })),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Editing a canvas (position, size, angle, opacity, flip) or calibrating it (two points on
|
||||||
|
/// the image and their true distance). Live edits of one opening are one undo step.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct CanvasPanel {
|
||||||
|
pub canvas: u64,
|
||||||
|
pub calibrate: bool,
|
||||||
|
/// Undo depth when the panel opened.
|
||||||
|
depth: usize,
|
||||||
|
pub a: [f64; 2],
|
||||||
|
pub b: [f64; 2],
|
||||||
|
pub distance: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CanvasPanel {
|
||||||
|
pub fn new(app: &SolveApp, canvas: u64, calibrate: bool) -> Self {
|
||||||
|
// Calibration starts from the image's left and right edge midpoints.
|
||||||
|
let (a, b, d) = app
|
||||||
|
.session
|
||||||
|
.doc
|
||||||
|
.canvases
|
||||||
|
.iter()
|
||||||
|
.find(|c| c.id == canvas)
|
||||||
|
.map(|c| ([c.center.x - c.width / 2.0, c.center.y], [c.center.x + c.width / 2.0, c.center.y], c.width))
|
||||||
|
.unwrap_or(([0.0, 0.0], [100.0, 0.0], 100.0));
|
||||||
|
CanvasPanel { canvas, calibrate, depth: app.session.undo.len(), a, b, distance: d }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn canvas_panel(app: &mut SolveApp, ctx: &egui::Context) {
|
||||||
|
let Some(mut m) = app.tree.canvas_panel.clone() else { return };
|
||||||
|
let Some(c) = app.session.doc.canvases.iter().find(|c| c.id == m.canvas).cloned() else {
|
||||||
|
app.tree.canvas_panel = None;
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let mut open = true;
|
||||||
|
let mut edit: Option<Value> = None;
|
||||||
|
let mut done = false;
|
||||||
|
let title = if m.calibrate { format!("Calibrate: {}", c.name) } else { format!("Edit Canvas: {}", c.name) };
|
||||||
|
egui::Window::new(title)
|
||||||
|
.id(egui::Id::new("sc_canvas_panel"))
|
||||||
|
.default_pos(panel_pos(app))
|
||||||
|
.open(&mut open)
|
||||||
|
.resizable(false)
|
||||||
|
.collapsible(false)
|
||||||
|
.show(ctx, |ui| {
|
||||||
|
egui::Grid::new("sc_canvas_grid").num_columns(2).spacing([12.0, 6.0]).show(ui, |ui| {
|
||||||
|
if m.calibrate {
|
||||||
|
for (label, p) in [("Point A", &mut m.a), ("Point B", &mut m.b)] {
|
||||||
|
ui.label(label);
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.add(egui::DragValue::new(&mut p[0]).speed(0.5).suffix(" mm"));
|
||||||
|
ui.add(egui::DragValue::new(&mut p[1]).speed(0.5).suffix(" mm"));
|
||||||
|
});
|
||||||
|
ui.end_row();
|
||||||
|
}
|
||||||
|
ui.label("Distance");
|
||||||
|
ui.add(egui::DragValue::new(&mut m.distance).speed(0.5).range(0.001..=1e6).suffix(" mm"));
|
||||||
|
ui.end_row();
|
||||||
|
} else {
|
||||||
|
let (mut x, mut y, mut w, mut ang, mut op, mut flip) = (c.center.x, c.center.y, c.width, c.angle.to_degrees(), c.opacity, c.flip);
|
||||||
|
ui.label("Center X");
|
||||||
|
let r1 = ui.add(egui::DragValue::new(&mut x).speed(0.5).suffix(" mm"));
|
||||||
|
ui.end_row();
|
||||||
|
ui.label("Center Y");
|
||||||
|
let r2 = ui.add(egui::DragValue::new(&mut y).speed(0.5).suffix(" mm"));
|
||||||
|
ui.end_row();
|
||||||
|
ui.label("Width");
|
||||||
|
let r3 = ui.add(egui::DragValue::new(&mut w).speed(0.5).range(0.01..=1e6).suffix(" mm"));
|
||||||
|
ui.end_row();
|
||||||
|
ui.label("Angle");
|
||||||
|
let r4 = ui.add(egui::DragValue::new(&mut ang).speed(1.0).suffix(" deg"));
|
||||||
|
ui.end_row();
|
||||||
|
ui.label("Opacity");
|
||||||
|
let r5 = ui.add(egui::Slider::new(&mut op, 0.0..=1.0));
|
||||||
|
ui.end_row();
|
||||||
|
ui.label("Flip");
|
||||||
|
let r6 = ui.checkbox(&mut flip, "");
|
||||||
|
ui.end_row();
|
||||||
|
if [r1, r2, r3, r4, r5, r6].iter().any(|r| r.changed()) {
|
||||||
|
edit = Some(json!({ "canvas": c.id, "center": [x, y], "width": w, "angle": ang, "opacity": op, "flip": flip }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ui.add_space(6.0);
|
||||||
|
if ui.button(if m.calibrate { "Calibrate" } else { "OK" }).clicked() {
|
||||||
|
done = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if let Some(p) = edit
|
||||||
|
&& app.run("canvas.edit", p).is_ok()
|
||||||
|
{
|
||||||
|
// One undo step for the whole edit.
|
||||||
|
if app.session.undo.len() > m.depth + 1 {
|
||||||
|
let keep = app.session.undo.get(m.depth).cloned();
|
||||||
|
app.session.undo.truncate(m.depth);
|
||||||
|
app.session.undo.extend(keep);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if done && m.calibrate {
|
||||||
|
if app.run("canvas.calibrate", json!({ "canvas": c.id, "a": m.a, "b": m.b, "distance": m.distance })).is_ok() {
|
||||||
|
app.tree.canvas_panel = None;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
app.tree.canvas_panel = if done || !open { None } else { Some(m) };
|
||||||
|
}
|
||||||
|
|
||||||
/// A browser click on a plane or axis: it goes to the open dialog's input if that takes it,
|
/// A browser click on a plane or axis: it goes to the open dialog's input if that takes it,
|
||||||
/// otherwise it becomes the selection.
|
/// otherwise it becomes the selection.
|
||||||
fn pick_from_browser(app: &mut SolveApp, x: Sel) {
|
fn pick_from_browser(app: &mut SolveApp, x: Sel) {
|
||||||
|
|||||||
@@ -38,6 +38,10 @@ pub enum Target {
|
|||||||
Group {
|
Group {
|
||||||
id: u64,
|
id: u64,
|
||||||
},
|
},
|
||||||
|
/// A canvas (reference image).
|
||||||
|
Canvas {
|
||||||
|
id: u64,
|
||||||
|
},
|
||||||
/// A component's Bodies, Sketches or Construction folder.
|
/// A component's Bodies, Sketches or Construction folder.
|
||||||
Folder {
|
Folder {
|
||||||
component: u64,
|
component: u64,
|
||||||
@@ -138,6 +142,7 @@ pub enum RenameWhat {
|
|||||||
Feature(u64),
|
Feature(u64),
|
||||||
Component(u64),
|
Component(u64),
|
||||||
Group(u64),
|
Group(u64),
|
||||||
|
Canvas(u64),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@@ -197,7 +202,7 @@ pub fn target_bodies(app: &SolveApp, target: &Target) -> Vec<String> {
|
|||||||
Target::Body { name } => vec![name.clone()],
|
Target::Body { name } => vec![name.clone()],
|
||||||
Target::Component { id } => component_bodies(app, *id),
|
Target::Component { id } => component_bodies(app, *id),
|
||||||
Target::Group { id } => crate::browser::group_bodies(app, *id),
|
Target::Group { id } => crate::browser::group_bodies(app, *id),
|
||||||
Target::Sketch { .. } | Target::Folder { .. } => Vec::new(),
|
Target::Sketch { .. } | Target::Folder { .. } | Target::Canvas { .. } => Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,6 +259,7 @@ pub fn items(app: &SolveApp, target: &Target) -> Vec<Item> {
|
|||||||
Target::Component { id } => component_items(app, *id),
|
Target::Component { id } => component_items(app, *id),
|
||||||
Target::Group { id } => crate::browser::group_items(app, *id),
|
Target::Group { id } => crate::browser::group_items(app, *id),
|
||||||
Target::Folder { component, folder } => crate::browser::folder_items(app, *component, folder),
|
Target::Folder { component, folder } => crate::browser::folder_items(app, *component, folder),
|
||||||
|
Target::Canvas { id } => crate::browser::canvas_items(app, *id),
|
||||||
};
|
};
|
||||||
// Several items of the folder selected: they can be grouped.
|
// Several items of the folder selected: they can be grouped.
|
||||||
if let Some(p) = crate::browser::selection_group(app, target) {
|
if let Some(p) = crate::browser::selection_group(app, target) {
|
||||||
@@ -566,6 +572,8 @@ pub fn run_item(app: &mut SolveApp, item: &Item, at: Pos2) {
|
|||||||
RenameWhat::Component(c)
|
RenameWhat::Component(c)
|
||||||
} else if let Some(g) = id_of(&p, "group") {
|
} else if let Some(g) = id_of(&p, "group") {
|
||||||
RenameWhat::Group(g)
|
RenameWhat::Group(g)
|
||||||
|
} else if let Some(c) = id_of(&p, "canvas") {
|
||||||
|
RenameWhat::Canvas(c)
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -646,6 +654,12 @@ pub fn run_item(app: &mut SolveApp, item: &Item, at: Pos2) {
|
|||||||
app.tree.occurrence_move = Some(crate::browser::OccMove::new(o));
|
app.tree.occurrence_move = Some(crate::browser::OccMove::new(o));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"ui.canvasVisible" => drop(app.run("canvas.edit", p)),
|
||||||
|
"ui.editCanvas" | "ui.calibrateCanvas" => {
|
||||||
|
if let Some(c) = id_of(&p, "canvas") {
|
||||||
|
app.tree.canvas_panel = Some(crate::browser::CanvasPanel::new(app, c, item.id == "ui.calibrateCanvas"));
|
||||||
|
}
|
||||||
|
}
|
||||||
"ui.newGroup" | "ui.ungroup" | "ui.groupSelected" => crate::browser::group_action(app, &item.id, &p),
|
"ui.newGroup" | "ui.ungroup" | "ui.groupSelected" => crate::browser::group_action(app, &item.id, &p),
|
||||||
id if item.params.is_some() => {
|
id if item.params.is_some() => {
|
||||||
let _ = app.run(id, p);
|
let _ = app.run(id, p);
|
||||||
@@ -689,6 +703,7 @@ pub fn start_rename(app: &mut SolveApp, what: RenameWhat, at: Pos2) {
|
|||||||
RenameWhat::Component(0) => doc.name.clone(),
|
RenameWhat::Component(0) => doc.name.clone(),
|
||||||
RenameWhat::Component(c) => doc.components.iter().find(|x| x.id == *c).map(|x| x.name.clone()).unwrap_or_default(),
|
RenameWhat::Component(c) => doc.components.iter().find(|x| x.id == *c).map(|x| x.name.clone()).unwrap_or_default(),
|
||||||
RenameWhat::Group(g) => crate::browser::group_name(app, *g).unwrap_or_default(),
|
RenameWhat::Group(g) => crate::browser::group_name(app, *g).unwrap_or_default(),
|
||||||
|
RenameWhat::Canvas(c) => doc.canvases.iter().find(|x| x.id == *c).map(|x| x.name.clone()).unwrap_or_default(),
|
||||||
};
|
};
|
||||||
app.menu.rename = Some(Rename { what, text, at, focused: false });
|
app.menu.rename = Some(Rename { what, text, at, focused: false });
|
||||||
}
|
}
|
||||||
@@ -711,6 +726,7 @@ fn commit_rename(app: &mut SolveApp, r: &Rename) {
|
|||||||
RenameWhat::Feature(f) => drop(app.run("FusionRenameTimelineEntryCommand", json!({ "feature": f, "name": name }))),
|
RenameWhat::Feature(f) => drop(app.run("FusionRenameTimelineEntryCommand", json!({ "feature": f, "name": name }))),
|
||||||
RenameWhat::Component(c) => drop(app.run("component.rename", json!({ "component": c, "name": name }))),
|
RenameWhat::Component(c) => drop(app.run("component.rename", json!({ "component": c, "name": name }))),
|
||||||
RenameWhat::Group(g) => crate::browser::group_action(app, "ui.renameGroup", &json!({ "group": g, "name": name })),
|
RenameWhat::Group(g) => crate::browser::group_action(app, "ui.renameGroup", &json!({ "group": g, "name": name })),
|
||||||
|
RenameWhat::Canvas(c) => drop(app.run("canvas.edit", json!({ "canvas": c, "name": name }))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -438,6 +438,12 @@ pub fn paint(p: &Painter, r: Rect, name: &str, ink: Color32, fill: Color32, acce
|
|||||||
pen.iso_box(2.0, 13.0, 9.0, 6.0, 6.0);
|
pen.iso_box(2.0, 13.0, 9.0, 6.0, 6.0);
|
||||||
pen.iso_box(9.0, 6.0, 5.0, 4.0, 4.0);
|
pen.iso_box(9.0, 6.0, 5.0, 4.0, 4.0);
|
||||||
}
|
}
|
||||||
|
// Canvas: a framed picture (sky and a hill).
|
||||||
|
"canvas" => {
|
||||||
|
pen.poly(&[(3.0, 5.0), (21.0, 5.0), (21.0, 19.0), (3.0, 19.0)], Color32::from_rgb(170, 205, 240), ink);
|
||||||
|
pen.poly(&[(4.0, 18.0), (10.0, 11.0), (14.0, 15.0), (16.0, 13.0), (20.0, 18.0)], Color32::from_rgb(110, 170, 100), ink);
|
||||||
|
pen.circle(16.5, 9.0, 1.6, Color32::from_rgb(250, 210, 80), Color32::from_rgb(250, 210, 80));
|
||||||
|
}
|
||||||
"folder" => pen.poly(&[(3.0, 7.0), (9.0, 7.0), (11.0, 9.0), (21.0, 9.0), (21.0, 19.0), (3.0, 19.0)], Color32::from_rgb(236, 200, 110), ink),
|
"folder" => pen.poly(&[(3.0, 7.0), (9.0, 7.0), (11.0, 9.0), (21.0, 9.0), (21.0, 19.0), (3.0, 19.0)], Color32::from_rgb(236, 200, 110), ink),
|
||||||
"origin" => {
|
"origin" => {
|
||||||
pen.arrow((6.0, 18.0), (20.0, 18.0), Color32::from_rgb(210, 60, 60));
|
pen.arrow((6.0, 18.0), (20.0, 18.0), Color32::from_rgb(210, 60, 60));
|
||||||
|
|||||||
@@ -252,3 +252,31 @@ fn menu_delete_items_use_the_same_path() {
|
|||||||
act(&mut app, &Target::Sketch { id }, "Delete");
|
act(&mut app, &Target::Sketch { id }, "Delete");
|
||||||
assert!(app.menu.confirm.is_some());
|
assert!(app.menu.confirm.is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn canvas_menu_hides_renames_calibrates_and_deletes() {
|
||||||
|
let mut app = sample_app();
|
||||||
|
let mut png = b"\x89PNG\r\n\x1a\n\0\0\0\rIHDR".to_vec();
|
||||||
|
png.extend(400u32.to_be_bytes());
|
||||||
|
png.extend(200u32.to_be_bytes());
|
||||||
|
png.extend([8, 6, 0, 0, 0, 0, 0, 0, 0]);
|
||||||
|
let data = solvecraft_engine::doc::canvas::base64_encode(&png);
|
||||||
|
let id = app.run("FusionAddCanvasCommand", json!({"data": data, "plane": "XY", "width": 80})).unwrap()["canvas"].as_u64().unwrap();
|
||||||
|
let t = Target::Canvas { id };
|
||||||
|
act(&mut app, &t, "Hide");
|
||||||
|
assert!(!app.session.doc.canvases[0].visible);
|
||||||
|
act(&mut app, &t, "Rename");
|
||||||
|
assert!(context_menu::finish_rename(&mut app, Some("Photo"), true));
|
||||||
|
assert_eq!(app.session.doc.canvases[0].name, "Photo");
|
||||||
|
act(&mut app, &t, "Calibrate");
|
||||||
|
let p = app.tree.canvas_panel.clone().unwrap();
|
||||||
|
assert!(p.calibrate && (p.distance - 80.0).abs() < 1e-9, "starts from the image's width");
|
||||||
|
act(&mut app, &t, "Delete");
|
||||||
|
assert!(app.session.doc.canvases.is_empty());
|
||||||
|
app.run("UndoCommand", json!({})).unwrap();
|
||||||
|
assert_eq!(app.session.doc.canvases.len(), 1);
|
||||||
|
// A canvas picked in the browser goes with the Delete key.
|
||||||
|
app.tree.picked_canvases = vec![id];
|
||||||
|
crate::delete::delete_selection(&mut app);
|
||||||
|
assert!(app.session.doc.canvases.is_empty());
|
||||||
|
}
|
||||||
Reference in new issue
Block a user