mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
Artboards: one active artboard for the panel, the navigator and the Artboard tool (review of #351)
As in Illustrator, double-clicking an artboard's number goes to it (fits it in the window) and double-clicking the name renames it. A row's Options button chooses the Artboard tool on that row's artboard, clicking a row moves the Artboard tool to it while it is chosen, and an artboard clicked with the Artboard tool becomes the active one the panel highlights. Note it in the ROADMAP. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
a54be8298f
commit
3b2376af9c
3 files changed
+85
-7
No files matched your search
+1
-1
@@ -118,7 +118,7 @@ grade by behaviour against `plan/illustrator/`, not by whether a menu item exist
|
||||
- **Tool double-clicks:** double-clicking a tool's button opens its options: Hand fits the artboard in the window, Zoom shows 100 %, Rotate, Scale, Reflect and Shear open their Object › Transform dialogs, Gradient, Eyedropper, Blend and the Liquify tools their panels or options.
|
||||
- **Layers panel:** every layer, sublayer, group and object is a clickable row at any depth: a click highlights it and makes its layer current (new art goes there), Shift-click a range, Ctrl/Cmd-click toggles. Sublayers are no longer treated as objects: clicks, marquees, Select All and the Select menu take the art inside them, and each sublayer selects in its own colour. Selection squares on rows with selected art (click selects a row's art, Shift adds or removes; dragging the square moves the selection to another row, Alt copies), the eye and lock on every row with drag-down-the-column (one undo step), Ctrl/Cmd-click an eye for per-layer Outline, Alt-click an eye or lock to hide or lock the other layers (again: show or unlock them all), triangles with Alt to open or close everything inside, drag and drop rows above, below or into layers and groups with a drop line (Alt copies, the trash deletes), inline rename, Delete key on highlighted rows. Buttons and the full panel menu: New Layer/Sublayer (Alt: options, Ctrl: on top), Duplicate, Delete, Options for Selection, clipping mask, isolation, Locate Object, Merge Selected, Flatten Artwork, Collect in New Layer, Release to Layers (Sequence and Build), Reverse Order, Template, Hide/Outline/Lock Others and Show/Preview/Unlock All, Paste Remembers Layers, Panel Options (row size, thumbnails per kind, Show Layers Only). Layer Options: name, colour (presets or custom), Template, Lock, Show, Print, Preview (outline drawing and clicking) and Dim Images to %. Every operation is a command (`layer.move`, `layer.highlight`, `layer.merge`…) with validation against cycles and invalid nesting.
|
||||
- **Drawing aids:** Smart Guides and snapping, guides dragged out of the rulers, and Draw Normal / Behind / Inside modes.
|
||||
- **Views:** the status bar's artboard navigator (first, previous, next, last; `view.goToArtboard`) goes to an artboard and fits it, and Fit Artboard in Window and Actual Size show the navigator's artboard.
|
||||
- **Views:** the status bar's artboard navigator (first, previous, next, last; `view.goToArtboard`) goes to an artboard and fits it, and Fit Artboard in Window and Actual Size show the navigator's artboard. The Artboards panel highlights that active artboard: clicking a row makes it active (the Artboard tool's too, while it is chosen) as does clicking an artboard with the Artboard tool, double-clicking its number also fits it in the window and double-clicking its name renames it.
|
||||
- **Geometry and effects:**
|
||||
- Pathfinder (10 exact curve booleans), Offset, Outline Stroke, Simplify, Clean Up, Split Into Grid, Divide Objects Below.
|
||||
- Live effects with previewing dialogs: Distort & Transform, Path, Convert to Shape, 15 Warp styles, Round Corners, Scribble, Effect → Pathfinder (all 10 operations, live on groups; several loose objects are grouped first, in one undo step), Color Adjustments (Brightness/Contrast, Curves, Levels, Hue/Saturation, Shift to Color, Temperature/Tint, on vectors, live type and embedded images), and raster drop shadow, glows and feather. SVG and PDF export keep live effects (geometry baked; SVG raster effects as filters).
|
||||
|
||||
@@ -614,6 +614,14 @@ fn drag_art_out(app: &mut VectorcraftApp, ui: &Ui, resp: &egui::Response, p: Pos
|
||||
/// Send a pointer event to the active tool and act on UI requests (dialogs, tool switches).
|
||||
pub fn dispatch(app: &mut VectorcraftApp, ev: &PointerEvent, view: vectorcraft_engine::ViewInfo) {
|
||||
let r = app.session.pointer(ev, view);
|
||||
// The artboard the Artboard tool makes active is the active one (the navigator's, the
|
||||
// Artboards panel's).
|
||||
if app.session.tool_id() == "artboard"
|
||||
&& let Some(i) = app.session.tool_options()["active"].as_u64().and_then(|i| usize::try_from(i).ok())
|
||||
&& let Some(v) = app.view_mut()
|
||||
{
|
||||
v.artboard = i;
|
||||
}
|
||||
apply_requests(app, r);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Artboards panel: numbered list with inline rename (double-click), move up / down, new, delete.
|
||||
//! The highlighted row is the active artboard, the one the status bar's navigator shows and Fit
|
||||
//! Artboard in Window fits: clicking a row makes it active.
|
||||
//! Artboards panel: numbered list with inline rename (double-click the name), move up / down, new,
|
||||
//! delete. The highlighted row is the active artboard, the one the status bar's navigator shows and
|
||||
//! Fit Artboard in Window fits: clicking a row makes it active, double-clicking its number also
|
||||
//! fits it in the window.
|
||||
|
||||
use egui::{Sense, Ui, pos2, vec2};
|
||||
use serde_json::json;
|
||||
@@ -15,11 +16,15 @@ fn selected(app: &VectorcraftApp, n: usize) -> usize {
|
||||
app.view().map_or(0, |v| v.artboard).min(n.saturating_sub(1))
|
||||
}
|
||||
|
||||
/// Make artboard `i` the active one, leaving the view where it is.
|
||||
/// Make artboard `i` the active one, leaving the view where it is: the navigator's and, while it
|
||||
/// is the tool, the Artboard tool's.
|
||||
fn select(app: &mut VectorcraftApp, i: usize) {
|
||||
if let Some(v) = app.view_mut() {
|
||||
v.artboard = i;
|
||||
}
|
||||
if app.session.tool_id() == "artboard" {
|
||||
app.session.set_tool_option("active", &json!(i));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
@@ -72,13 +77,18 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let oresp = ui.interact(opt, ui.id().with(("ab-opt", i)), Sense::click());
|
||||
icons::paint(ui, "dc-artboard-options", opt, if oresp.hovered() { t.text_strong } else { t.icon });
|
||||
if oresp.on_hover_text(tl!("Artboard Options: edit with the Artboard tool")).clicked() {
|
||||
select(app, i);
|
||||
app.select_tool("artboard");
|
||||
select(app, i);
|
||||
}
|
||||
if resp.clicked() {
|
||||
select(app, i);
|
||||
}
|
||||
if resp.double_clicked() {
|
||||
// Double-clicking the number goes to the artboard (fits it in the window); the name
|
||||
// renames it.
|
||||
let on_number = resp.interact_pointer_pos().is_some_and(|p| p.x < name_rect.left());
|
||||
if resp.double_clicked() && on_number {
|
||||
app.run("view.goToArtboard", json!({ "index": i })).ok();
|
||||
} else if resp.double_clicked() {
|
||||
set_pstate(ui.ctx(), "ab-edit", Some(i));
|
||||
}
|
||||
}
|
||||
@@ -170,4 +180,64 @@ mod tests {
|
||||
crate::menus::invoke(&mut app, "view.goToArtboard", json!({"index": "previous"}));
|
||||
assert_eq!(selected(&app, 2), 0, "and the panel follows the navigator");
|
||||
}
|
||||
/// Double-clicking a row's number goes to its artboard and its name renames it; a row's
|
||||
/// Options button edits that artboard with the Artboard tool, and the tool's artboard is the
|
||||
/// active one.
|
||||
#[test]
|
||||
fn double_clicks_go_to_or_rename_and_options_edits_the_rows_artboard() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.session.execute("file.new", &json!({"width": 200, "height": 150})).unwrap();
|
||||
app.session.execute("artboard.new", &json!({"x": 400, "y": 300, "width": 200, "height": 150})).unwrap();
|
||||
app.view_mut().unwrap().artboard = 0;
|
||||
let ctx = egui::Context::default();
|
||||
let screen = Some(egui::Rect::from_min_size(egui::Pos2::ZERO, vec2(240.0, 400.0)));
|
||||
let frame = |app: &mut VectorcraftApp, time: f64, events: Vec<egui::Event>| {
|
||||
let mut out = ctx.run_ui(egui::RawInput { events, time: Some(time), screen_rect: screen, ..Default::default() }, |ui| show(app, ui));
|
||||
out.textures_delta.clear();
|
||||
out.shapes
|
||||
};
|
||||
let shapes = frame(&mut app, 0.0, vec![]);
|
||||
let name = shapes
|
||||
.iter()
|
||||
.find_map(|c| match &c.shape {
|
||||
egui::Shape::Text(t) if t.galley.text() == "Artboard 2" => Some(t.pos + vec2(4.0, 4.0)),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap();
|
||||
// The highlighted row (Artboard 1's), for its Options button at the right end.
|
||||
let row = shapes
|
||||
.iter()
|
||||
.find_map(|c| match &c.shape {
|
||||
egui::Shape::Rect(r) if r.fill == Tokens::get(&ctx).row_selected => Some(r.rect),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap();
|
||||
let events = |at: egui::Pos2, clicks: usize| {
|
||||
let button =
|
||||
|pressed| egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
|
||||
std::iter::once(egui::Event::PointerMoved(at)).chain((0..clicks).flat_map(|_| [button(true), button(false)])).collect::<Vec<_>>()
|
||||
};
|
||||
// Artboard 2's number: it is active, no rename starts.
|
||||
frame(&mut app, 1.0, events(name - vec2(16.0, 0.0), 2));
|
||||
assert_eq!(app.view().unwrap().artboard, 1);
|
||||
assert_eq!(pstate::<Option<usize>>(&ctx, "ab-edit"), None);
|
||||
// Its name: a rename.
|
||||
frame(&mut app, 2.0, events(name, 2));
|
||||
assert_eq!(pstate::<Option<usize>>(&ctx, "ab-edit"), Some(1));
|
||||
set_pstate::<Option<usize>>(&ctx, "ab-edit", None);
|
||||
frame(&mut app, 3.0, vec![]);
|
||||
// Artboard 1's Options button: the Artboard tool, editing Artboard 1.
|
||||
frame(&mut app, 4.0, events(egui::pos2(row.right() - 14.0, row.center().y), 1));
|
||||
assert_eq!(app.session.tool_id(), "artboard");
|
||||
assert_eq!((app.view().unwrap().artboard, app.session.tool_options()["active"].clone()), (0, json!(0)));
|
||||
// While the Artboard tool is chosen, clicking a row moves the tool to that artboard too.
|
||||
frame(&mut app, 5.0, events(name, 1));
|
||||
assert_eq!(app.session.tool_options()["active"], json!(1));
|
||||
// And clicking an artboard with the tool makes it the active one, highlighted in the panel.
|
||||
let view = app.view_info();
|
||||
for kind in [vectorcraft_tools::PointerKind::Down, vectorcraft_tools::PointerKind::Up] {
|
||||
crate::canvas::dispatch(&mut app, &vectorcraft_tools::PointerEvent::new(kind, 100.0, 100.0), view);
|
||||
}
|
||||
assert_eq!((app.session.tool_options()["active"].clone(), selected(&app, 2)), (json!(0), 0));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user