mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
Toolbar: a click on the tear-off bar floats the group, window.floatTools, strips in workspaces (review of #437)
- A click on a flyout's tear-off bar, or the release of the long press
that opened it over the bar, tears the flyout off too, as in
Illustrator; dragging it still does.
- window.floatTools {tool, floating?} floats or puts back the group
holding a tool, for the control channel and MCP's invoke_menu.
- Floating strips are part of user workspaces; the built-in ones float
none, so Reset puts them back.
- Loaded preferences keep one strip per group, of known tools.
- No per-frame clone of the strips or copy of the active tool's id.
- docs/control-protocol.md, ROADMAP, Float Tool Group translations.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
dcc8a24f66
commit
97906f2104
12 files changed
+165
-20
No files matched your search
+2
-2
@@ -4,7 +4,7 @@ VectorCraft is a clean-room, open-source, pure-Rust reimplementation of the Adob
|
||||
|
||||
This file tracks **how far we are and what's left**. Time estimates are wall-clock hours of continuous Claude Opus 5.5 agent work (including builds and the CI gate), given both for **one agent** and for **4–6 parallel agents** on disjoint crates. They are counted from the remaining work (see [Parity estimate](#parity-estimate)), calibrated against measured throughput, and updated as work lands.
|
||||
|
||||
_Last updated: 2026-10-08 (after M4.14–M4.98, M8.1–M8.20, M14.4–M14.7, the MCP prompts, resource templates, completions and logging, the 2026-10-07 issue fixes, the Layers panel rework and the 2026-10-08 community contributions: Pen and shape-tool modifiers, path editing, Layers, Artboards, Swatches/Brushes and dialogs, Japanese composition and PDF text, Hebrew/Arabic type, interface languages (Spanish among them), the font menu, saved selections, MCP and EPS, the missing-font export warning (#319), open paths in the Shape Builder (#374), PDF/`.ai` import keeping strokes live and hidden and nested layers (#372), per-corner Live Corners (#381), SVG type opening at the size it draws at (#396), the Wayland clipboard with copied-file paste (#398), ruler guides that can be selected, moved and deleted (#414), two batches of Preferences options that were stored but unused (#394), skeleton reads with `document.find` for large documents over MCP (#416) and anchors dragged with Direct Selection snapping to points, segments and smart guides, clipped SVG layers opening as clipping layers (#420), PDF/`.ai` layers read on pages of tens of millions of operators (#419), Document Color Mode putting RGB greys on the black plate on request and converting pattern tiles (#421) and movable dialogs with Offset Path's Joins dropdown (#423) and numeric fields scrubbed by dragging their label (#400), stroke weight presets in the stroke unit (#426), the Appearance panel's Opacity popup (#430), type on a path brackets, flip and Type on a Path Options that keep the type in place (#429), Layers rows dragged with a ghost and dropped on the trash or New Layer (#435), tool flyouts opening on a right or corner press (#436); see [Honest assessment](#honest-assessment-2026-10-05))._
|
||||
_Last updated: 2026-10-08 (after M4.14–M4.98, M8.1–M8.20, M14.4–M14.7, the MCP prompts, resource templates, completions and logging, the 2026-10-07 issue fixes, the Layers panel rework and the 2026-10-08 community contributions: Pen and shape-tool modifiers, path editing, Layers, Artboards, Swatches/Brushes and dialogs, Japanese composition and PDF text, Hebrew/Arabic type, interface languages (Spanish among them), the font menu, saved selections, MCP and EPS, the missing-font export warning (#319), open paths in the Shape Builder (#374), PDF/`.ai` import keeping strokes live and hidden and nested layers (#372), per-corner Live Corners (#381), SVG type opening at the size it draws at (#396), the Wayland clipboard with copied-file paste (#398), ruler guides that can be selected, moved and deleted (#414), two batches of Preferences options that were stored but unused (#394), skeleton reads with `document.find` for large documents over MCP (#416) and anchors dragged with Direct Selection snapping to points, segments and smart guides, clipped SVG layers opening as clipping layers (#420), PDF/`.ai` layers read on pages of tens of millions of operators (#419), Document Color Mode putting RGB greys on the black plate on request and converting pattern tiles (#421) and movable dialogs with Offset Path's Joins dropdown (#423) and numeric fields scrubbed by dragging their label (#400), stroke weight presets in the stroke unit (#426), the Appearance panel's Opacity popup (#430), type on a path brackets, flip and Type on a Path Options that keep the type in place (#429), Layers rows dragged with a ghost and dropped on the trash or New Layer (#435), tool flyouts opening on a right or corner press (#436), tool flyouts torn off into floating toolbars (#437); see [Honest assessment](#honest-assessment-2026-10-05))._
|
||||
|
||||
## Where we are
|
||||
|
||||
@@ -97,7 +97,7 @@ grade by behaviour against `plan/illustrator/`, not by whether a menu item exist
|
||||
- **UI:** Illustrator 2026 layout restyled to measured values:
|
||||
- Medium Dark theme, categorized and Advanced toolbars (a tool group's flyout opens on a right press, a long press or a press on its corner triangle), 35 pt document tabs, 33 pt panel tabs.
|
||||
- Hint bar, contextual task bar, 19 dock panels with ≡ menus.
|
||||
- Tool flyouts tear off: dragging the grab bar on a flyout's right side floats its group as a strip of tool buttons, moved by the same bar and put back in the toolbar by the × at its top. While a group floats, the presses that would open its flyout raise the strip instead. Floating strips are kept with the preferences.
|
||||
- Tool flyouts tear off: dragging or clicking the tear-off bar on a flyout's right side (or releasing the long press over it) floats its group as a strip of tool buttons, moved by the same bar and put back in the toolbar by the × at its top (`window.floatTools`). While a group floats, the presses that would open its flyout raise the strip instead. Floating strips are kept with the preferences and in user workspaces. Not done: turning a strip vertical, docking it.
|
||||
- The dock's double arrow collapses Properties, Layers and Libraries to icons at the top of the icon column, popping out like the other icon panels, and expands them again (`window.collapseDock`, kept with the preferences and in saved workspaces). The icon column's own arrow (expanding the icon panels into a full column) is not done.
|
||||
- Localised UI: menus (in-window and native), panels, dialogs, toolbar and Preferences read from per-language catalogs (`crates/ui-egui/src/i18n`, see `docs/development.md`); Traditional Chinese (Taiwan), Simplified Chinese (mainland), Japanese and Spanish ship complete, and Czech and Brazilian Portuguese cover every menu label (and more), with system-locale detection on macOS, Windows and Linux, a VectorCraft ▸ Language menu and a Language preference. Names are never translated (fonts and their styles, layers, artboards, saved presets, user libraries, custom workspaces, recent files, plug-ins).
|
||||
- Native macOS menu bar, vector tool cursors, a ⌘K command palette, middle-button panning with any tool.
|
||||
|
||||
@@ -607,3 +607,4 @@
|
||||
Drag to move Přetažením přesunete
|
||||
Drag to float as a toolbar Přetažením vytvoříte plovoucí panel nástrojů
|
||||
Put back in the toolbar Vrátit do panelu nástrojů
|
||||
Float Tool Group Plovoucí skupina nástrojů
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3032,3 +3032,4 @@ axis Both Ambos
|
||||
Drag to move Arrastrar para mover
|
||||
Drag to float as a toolbar Arrastrar para flotar como barra de herramientas
|
||||
Put back in the toolbar Devolver a la barra de herramientas
|
||||
Float Tool Group Hacer flotar el grupo de herramientas
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3021,3 +3021,4 @@ axis Both 両方
|
||||
Drag to move 移動するにはドラッグ
|
||||
Drag to float as a toolbar ドラッグしてツールバーとして浮かせる
|
||||
Put back in the toolbar ツールバーに戻す
|
||||
Float Tool Group ツールグループをフローティング
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2624,3 +2624,4 @@
|
||||
Drag to move Arraste para mover
|
||||
Drag to float as a toolbar Arraste para flutuar como barra de ferramentas
|
||||
Put back in the toolbar Devolver à barra de ferramentas
|
||||
Float Tool Group Flutuar Grupo de Ferramentas
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -3278,3 +3278,4 @@ axis Both 两者
|
||||
Drag to move 拖动以移动
|
||||
Drag to float as a toolbar 拖动使其浮动为工具栏
|
||||
Put back in the toolbar 放回工具栏
|
||||
Float Tool Group 浮动工具组
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3052,3 +3052,4 @@ axis Both 兩者
|
||||
Drag to move 拖曳以移動
|
||||
Drag to float as a toolbar 拖曳使其浮動為工具列
|
||||
Put back in the toolbar 放回工具列
|
||||
Float Tool Group 浮動工具組
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -199,6 +199,12 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
||||
("window.toolbar", "Tools", "", "{}"),
|
||||
("window.toolbarColumns", "Toolbar: Single/Double Column", "", "{}"),
|
||||
("window.toolbarAdvanced", "Toolbar: Advanced / Basic", "", "{}"),
|
||||
(
|
||||
"window.floatTools",
|
||||
"Float Tool Group",
|
||||
"",
|
||||
"{tool: id, floating?: bool} float the toolbar group holding `tool` (in the current Basic or Advanced layout) as its own strip of tool buttons (true), put it back in the toolbar (false) or toggle (omitted), as dragging or clicking a flyout's tear-off bar and the strip's × do; returns the new state",
|
||||
),
|
||||
("window.taskBar", "Contextual Task Bar", "", "{}"),
|
||||
("window.dock", "Panels", "Tab", "{} show/hide all panels"),
|
||||
("window.panel", "Show Panel", "", "{panel: id} e.g. layers, swatches, stroke (case-insensitive; display labels like \"Layers\" work too)"),
|
||||
@@ -980,6 +986,15 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<R
|
||||
"window.toolbar" => flag(&mut app.ui.toolbar),
|
||||
"window.toolbarColumns" => flag(&mut app.ui.toolbar_double),
|
||||
"window.toolbarAdvanced" => flag(&mut app.ui.toolbar_advanced),
|
||||
"window.floatTools" => {
|
||||
let floating = match p.get("floating") {
|
||||
None | Some(Value::Null) => None,
|
||||
Some(Value::Bool(b)) => Some(*b),
|
||||
Some(_) => return Some(Err("floating must be true or false".into())),
|
||||
};
|
||||
let Some(tool) = s("tool") else { return Some(Err("tool (a tool id) is required".into())) };
|
||||
crate::toolbar::float_group(app, &tool, floating).map(Value::Bool)
|
||||
}
|
||||
"window.taskBar" => flag(&mut app.ui.task_bar),
|
||||
"window.dock" => {
|
||||
let on = !(app.ui.dock && app.ui.toolbar);
|
||||
@@ -1302,6 +1317,10 @@ pub fn checked(app: &VectorcraftApp, id: &str, p: &Value) -> Option<bool> {
|
||||
"window.control" => app.ui.control_bar,
|
||||
"window.toolbar" => app.ui.toolbar,
|
||||
"window.toolbarAdvanced" => app.ui.toolbar_advanced,
|
||||
"window.floatTools" => {
|
||||
let tool = p.get("tool").and_then(Value::as_str).unwrap_or("");
|
||||
app.ui.floating_flyouts.iter().any(|f| f.tools.iter().any(|id| id == tool))
|
||||
}
|
||||
"window.taskBar" => app.ui.task_bar,
|
||||
"window.panel" => {
|
||||
let panel = p.get("panel").and_then(Value::as_str).unwrap_or("");
|
||||
|
||||
@@ -392,6 +392,12 @@ impl UiState {
|
||||
if self.group_tool.len() != vectorcraft_tools::TOOL_GROUPS.len() {
|
||||
self.group_tool = UiState::default().group_tool;
|
||||
}
|
||||
// One strip per group, of known tools (a hand-edited preferences file).
|
||||
let mut seen = std::collections::BTreeSet::new();
|
||||
self.floating_flyouts.retain_mut(|f| {
|
||||
f.tools.retain(|id| vectorcraft_tools::tool_info(id).is_some());
|
||||
f.tools.first().is_some_and(|k| seen.insert(k.clone()))
|
||||
});
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
+119
-17
@@ -382,6 +382,31 @@ fn bottom_controls(app: &mut VectorcraftApp, ui: &mut Ui, t: &Tokens) {
|
||||
let _ = t;
|
||||
}
|
||||
|
||||
/// Where `window.floatTools` puts a strip (the next one a row lower), right of the toolbar; it's
|
||||
/// kept on screen and moved by its bar.
|
||||
const FLOAT_AT: [f32; 2] = [90.0, 100.0];
|
||||
|
||||
/// Float the toolbar group (of the current layout) holding `tool` as a strip of tool buttons, put
|
||||
/// it back in the toolbar, or toggle (`floating` omitted), as a flyout's tear-off bar and the
|
||||
/// strip's × do (`window.floatTools`). Returns the new state.
|
||||
pub fn float_group(app: &mut VectorcraftApp, tool: &str, floating: Option<bool>) -> Result<bool, String> {
|
||||
let Some((_, group)) = slots(app).into_iter().find(|(_, s)| s.len() > 1 && s.contains(&tool)) else {
|
||||
return Err(format!("{tool} isn't in a toolbar group of several tools"));
|
||||
};
|
||||
let key = group.first().copied().unwrap_or_default();
|
||||
let was = is_floating(app, key);
|
||||
let on = floating.unwrap_or(!was);
|
||||
if on && !was {
|
||||
let n = app.ui.floating_flyouts.len() as f32;
|
||||
let tools = group.iter().map(|id| id.to_string()).collect();
|
||||
app.ui.floating_flyouts.push(crate::state::FloatingFlyout { tools, pos: [FLOAT_AT[0], FLOAT_AT[1] + n * (PITCH + 12.0)] });
|
||||
app.ui.flyout = None;
|
||||
} else if !on {
|
||||
app.ui.floating_flyouts.retain(|f| f.tools.first().is_none_or(|k| k != key));
|
||||
}
|
||||
Ok(on)
|
||||
}
|
||||
|
||||
/// Whether the group whose first tool is `key` floats as its own panel.
|
||||
fn is_floating(app: &VectorcraftApp, key: &str) -> bool {
|
||||
app.ui.floating_flyouts.iter().any(|f| f.tools.first().is_some_and(|k| k == key))
|
||||
@@ -420,8 +445,8 @@ struct FlyoutInput {
|
||||
}
|
||||
|
||||
/// A flyout's tool rows (a floating flyout's strip of tool buttons) with the grab bar down their
|
||||
/// right side; a floating flyout's bar has a × at its top. The bar's id is the group's, so a drag that tears a flyout off carries on moving
|
||||
/// the floating flyout it becomes.
|
||||
/// right side; a floating flyout's bar has a × at its top. The bar's id is the group's, so a drag
|
||||
/// that tears a flyout off carries on moving the floating flyout it becomes.
|
||||
fn flyout_body(ui: &mut Ui, t: &Tokens, active: &str, tools: &[String], floating: bool) -> FlyoutInput {
|
||||
let mut out = FlyoutInput::default();
|
||||
let key = tools.first().map_or("", String::as_str);
|
||||
@@ -546,20 +571,22 @@ fn flyout(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
let anchor: egui::Rect =
|
||||
ctx.data(|d| d.get_temp(egui::Id::new("flyout-anchor"))).unwrap_or(egui::Rect::from_min_size(pos2(40.0, 100.0), vec2(32.0, 32.0)));
|
||||
let t = Tokens::get(ctx);
|
||||
let active = app.session.tool_id().to_string();
|
||||
let active = app.session.tool_id();
|
||||
let mut input = FlyoutInput::default();
|
||||
let resp = egui::Area::new(egui::Id::new("tool-flyout")).order(egui::Order::Foreground).fixed_pos(anchor.right_top() + vec2(8.0, -1.0)).show(
|
||||
ctx,
|
||||
|ui| {
|
||||
flyout_frame(&t, false).show(ui, |ui| input = flyout_body(ui, &t, &active, &tools, false));
|
||||
flyout_frame(&t, false).show(ui, |ui| input = flyout_body(ui, &t, active, &tools, false));
|
||||
},
|
||||
);
|
||||
// Dragging the bar tears the flyout off: it floats where the drag puts it.
|
||||
if input.bar.is_some_and(|b| b.dragged())
|
||||
&& let Some(d) = press_travel(ctx).filter(|d| d.length() > TEAR)
|
||||
{
|
||||
// Dragging the bar tears the flyout off, and so does releasing a press over it (a click, or the
|
||||
// end of the long press that opened the flyout): it floats where the pointer is.
|
||||
let at = ctx.input(|i| i.pointer.interact_pos());
|
||||
let released_on_bar = input.bar.as_ref().is_some_and(|b| ctx.input(|i| i.pointer.primary_released()) && at.is_some_and(|p| b.rect.contains(p)));
|
||||
let dragged_off = input.bar.as_ref().is_some_and(egui::Response::dragged) && press_travel(ctx).is_some_and(|d| d.length() > TEAR);
|
||||
if released_on_bar || dragged_off {
|
||||
// The strip is far narrower than the flyout: it floats with its bar under the pointer.
|
||||
let at = ctx.input(|i| i.pointer.interact_pos()).unwrap_or(resp.response.rect.min + d);
|
||||
let at = at.unwrap_or(resp.response.rect.right_center());
|
||||
// (Past the frame's 1 pt border.)
|
||||
let grab = vec2(1.0 + strip_width(&tools) - BAR / 2.0, 1.0 + CLOSE + 4.0);
|
||||
let p = at - grab;
|
||||
@@ -582,13 +609,16 @@ fn flyout(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
|
||||
/// The flyouts torn off the toolbar, each floating where its bar was dragged; its × puts it back.
|
||||
fn floating(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
if app.ui.floating_flyouts.is_empty() {
|
||||
return;
|
||||
}
|
||||
let t = Tokens::get(ctx);
|
||||
let screen = ctx.content_rect();
|
||||
let now = ctx.input(|i| i.time);
|
||||
let active = app.session.tool_id().to_string();
|
||||
let active = app.session.tool_id();
|
||||
let mut chosen = None;
|
||||
let mut closed = None;
|
||||
for (k, f) in app.ui.floating_flyouts.clone().into_iter().enumerate() {
|
||||
for (k, f) in app.ui.floating_flyouts.iter_mut().enumerate() {
|
||||
let Some(key) = f.tools.first() else { continue };
|
||||
let id = floating_area(key);
|
||||
let since = ctx.data(|d| d.get_temp::<f64>(id)).map(|t0| now - t0);
|
||||
@@ -603,19 +633,18 @@ fn floating(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
let pos = pos2(x.min(screen.right() - size.x).max(screen.left()), y.min(screen.bottom() - size.y).max(screen.top()));
|
||||
let mut input = FlyoutInput::default();
|
||||
egui::Area::new(id).order(egui::Order::Middle).fixed_pos(pos).show(ctx, |ui| {
|
||||
flyout_frame(&t, flash).show(ui, |ui| input = flyout_body(ui, &t, &active, &f.tools, true));
|
||||
flyout_frame(&t, flash).show(ui, |ui| input = flyout_body(ui, &t, active, &f.tools, true));
|
||||
});
|
||||
// Dragging the bar keeps the flyout where the pointer holds it (by an offset rather than
|
||||
// each frame's motion, which a just torn-off flyout's first, unseen frame would drop).
|
||||
let grab = grab_id(key);
|
||||
if ctx.is_being_dragged(bar_id(key))
|
||||
&& let Some(at) = ctx.input(|i| i.pointer.interact_pos())
|
||||
&& let Some(slot) = app.ui.floating_flyouts.get_mut(k)
|
||||
{
|
||||
let off = ctx.data_mut(|m| *m.get_temp_mut_or_insert_with(grab, || at - pos));
|
||||
let p = at - off;
|
||||
if p.x.is_finite() && p.y.is_finite() {
|
||||
slot.pos = [p.x, p.y];
|
||||
f.pos = [p.x, p.y];
|
||||
}
|
||||
} else if !ctx.input(|i| i.pointer.any_down()) {
|
||||
ctx.data_mut(|m| m.remove::<egui::Vec2>(grab));
|
||||
@@ -1005,14 +1034,87 @@ pub(crate) mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_click_on_a_flyouts_bar_doesnt_tear_it_off() {
|
||||
fn a_click_on_a_flyouts_bar_tears_it_off() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
let ctx = egui::Context::default();
|
||||
let at = shapes_button(&mut app, &ctx).center();
|
||||
click(&mut app, &ctx, 1.0, at, PointerButton::Secondary);
|
||||
let menu = ctx.memory(|m| m.area_rect(egui::Id::new("tool-flyout"))).unwrap();
|
||||
click(&mut app, &ctx, 2.0, pos2(menu.right() - BAR / 2.0, menu.center().y), PointerButton::Primary);
|
||||
assert!(app.ui.flyout.is_some() && app.ui.floating_flyouts.is_empty());
|
||||
let bar = pos2(menu.right() - BAR / 2.0, menu.center().y);
|
||||
click(&mut app, &ctx, 2.0, bar, PointerButton::Primary);
|
||||
frame(&mut app, &ctx, 2.2, vec![]);
|
||||
assert_eq!(app.ui.flyout, None);
|
||||
assert_eq!(app.ui.floating_flyouts.iter().map(|f| f.tools[0].as_str()).collect::<Vec<_>>(), ["rectangle"]);
|
||||
let float = ctx.memory(|m| m.area_rect(floating_area("rectangle"))).expect("the floating flyout is shown");
|
||||
assert!(float.contains(bar), "its bar is under the pointer: {float:?}");
|
||||
assert_eq!(app.session.tool_id(), "selection", "no tool picked");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn releasing_a_long_press_over_the_flyouts_bar_tears_it_off() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
let ctx = egui::Context::default();
|
||||
let at = shapes_button(&mut app, &ctx).center();
|
||||
let b = |pos, pressed| Event::PointerButton { pos, button: PointerButton::Primary, pressed, modifiers: Default::default() };
|
||||
frame(&mut app, &ctx, 1.0, vec![Event::PointerMoved(at), b(at, true)]);
|
||||
frame(&mut app, &ctx, 1.0 + LONG_PRESS + 0.01, vec![]);
|
||||
frame(&mut app, &ctx, 1.45, vec![]);
|
||||
let menu = ctx.memory(|m| m.area_rect(egui::Id::new("tool-flyout"))).expect("the long press opens the flyout");
|
||||
let bar = pos2(menu.right() - BAR / 2.0, menu.center().y);
|
||||
frame(&mut app, &ctx, 1.5, vec![Event::PointerMoved(bar)]);
|
||||
frame(&mut app, &ctx, 1.55, vec![b(bar, false)]);
|
||||
frame(&mut app, &ctx, 1.6, vec![]);
|
||||
assert_eq!((app.ui.flyout, app.ui.floating_flyouts.len(), app.session.tool_id()), (None, 1, "selection"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn window_float_tools_floats_and_puts_back_a_group() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
let check = |app: &VectorcraftApp| crate::menus::checked(app, "window.floatTools", &json!({"tool": "star"}));
|
||||
assert_eq!(app.run("window.floatTools", json!({"tool": "star"})).unwrap(), json!(true));
|
||||
let [f] = app.ui.floating_flyouts.as_slice() else { panic!("one strip: {:?}", app.ui.floating_flyouts) };
|
||||
assert_eq!(f.tools.first().map(String::as_str), Some("rectangle"), "the Basic toolbar's Shapes group");
|
||||
assert_eq!(check(&app), Some(true));
|
||||
// Already floating: stays where it is.
|
||||
app.ui.floating_flyouts[0].pos = [300.0, 400.0];
|
||||
assert_eq!(app.run("window.floatTools", json!({"tool": "rectangle", "floating": true})).unwrap(), json!(true));
|
||||
assert_eq!(app.ui.floating_flyouts.iter().map(|f| f.pos).collect::<Vec<_>>(), [[300.0, 400.0]]);
|
||||
// Omitted toggles it back.
|
||||
assert_eq!(app.run("window.floatTools", json!({"tool": "star"})).unwrap(), json!(false));
|
||||
assert!(app.ui.floating_flyouts.is_empty() && check(&app) == Some(false));
|
||||
assert_eq!(app.run("window.floatTools", json!({"tool": "pen", "floating": false})).unwrap(), json!(false));
|
||||
// A tool alone in its slot, an unknown tool, a bad flag.
|
||||
assert!(app.run("window.floatTools", json!({"tool": "ellipse"})).is_err());
|
||||
assert!(app.run("window.floatTools", json!({"tool": "nope"})).is_err());
|
||||
assert!(app.run("window.floatTools", json!({})).is_err());
|
||||
assert!(app.run("window.floatTools", json!({"tool": "pen", "floating": "yes"})).is_err());
|
||||
assert!(app.ui.floating_flyouts.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspaces_keep_floating_flyouts_and_reset_puts_them_back() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("window.floatTools", json!({"tool": "pen"})).unwrap();
|
||||
app.run("window.workspace.new", json!({"name": "Strips"})).unwrap();
|
||||
app.run("window.workspace", json!({"name": "Essentials"})).unwrap();
|
||||
assert!(app.ui.floating_flyouts.is_empty(), "the built-in workspaces float none");
|
||||
app.run("window.workspace", json!({"name": "Strips"})).unwrap();
|
||||
assert_eq!(app.ui.floating_flyouts.iter().map(|f| f.tools[0].as_str()).collect::<Vec<_>>(), ["pen"]);
|
||||
// Workspaces saved before floating strips load without any.
|
||||
let mut old = serde_json::to_value(crate::workspaces::Workspace::default()).unwrap();
|
||||
old.as_object_mut().unwrap().remove("floatingFlyouts");
|
||||
assert!(serde_json::from_value::<crate::workspaces::Workspace>(old).unwrap().floating_flyouts.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn loaded_floating_flyouts_keep_known_tools_one_strip_per_group() {
|
||||
let f = |tools: &[&str]| crate::state::FloatingFlyout { tools: tools.iter().map(|s| s.to_string()).collect(), pos: [0.0, 0.0] };
|
||||
let ui = crate::state::UiState {
|
||||
floating_flyouts: vec![f(&["pen", "nope", "addAnchor"]), f(&["pen", "addAnchor"]), f(&["nope"]), f(&[])],
|
||||
..Default::default()
|
||||
};
|
||||
let ui = ui.sanitized();
|
||||
assert_eq!(ui.floating_flyouts.iter().map(|f| f.tools.join(",")).collect::<Vec<_>>(), ["pen,addAnchor"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -7,7 +7,7 @@ use std::sync::atomic::Ordering;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::state::{Dialog, DockTab, UiState};
|
||||
use crate::state::{Dialog, DockTab, FloatingFlyout, UiState};
|
||||
use crate::theme::Tokens;
|
||||
use crate::{VectorcraftApp, widgets};
|
||||
|
||||
@@ -27,6 +27,8 @@ pub struct Workspace {
|
||||
pub dock_collapsed: bool,
|
||||
pub open_panel: Option<String>,
|
||||
pub status_bar: bool,
|
||||
/// Tool groups torn off the toolbar into floating strips (built-in workspaces: none).
|
||||
pub floating_flyouts: Vec<FloatingFlyout>,
|
||||
}
|
||||
|
||||
impl Default for Workspace {
|
||||
@@ -120,6 +122,7 @@ pub fn capture(ui: &UiState, name: &str) -> Workspace {
|
||||
dock_collapsed: ui.dock_collapsed,
|
||||
open_panel: ui.open_panel.clone(),
|
||||
status_bar: ui.status_bar,
|
||||
floating_flyouts: ui.floating_flyouts.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,6 +139,7 @@ pub fn apply(ui: &mut UiState, w: &Workspace) {
|
||||
ui.dock_collapsed = w.dock_collapsed;
|
||||
ui.open_panel = w.open_panel.clone();
|
||||
ui.status_bar = w.status_bar;
|
||||
ui.floating_flyouts = w.floating_flyouts.clone();
|
||||
ui.flyout = None;
|
||||
ui.workspace = w.name.clone();
|
||||
}
|
||||
|
||||
@@ -192,6 +192,14 @@ pop the panel out next to the column like the other icon panels (`ui.dock_collap
|
||||
`ui.inspect`); expanding with one popped out shows its tab. The state is saved with the preferences and in user
|
||||
workspaces; the built-in workspaces expand the dock.
|
||||
|
||||
Floating tool groups: dragging or clicking the tear-off bar down a tool group's flyout (or releasing the long press
|
||||
that opened it over the bar) floats the group as a strip of tool buttons, moved by the same bar and put back in the
|
||||
toolbar by the × at its top; `window.floatTools {tool, floating?}` does the same for the group of the current
|
||||
layout holding `tool` (omitted toggles; a tool alone in its slot is an error). While a group floats, the presses
|
||||
that open its flyout raise the strip instead. The strips (`ui.floating_flyouts` in `ui.inspect`: the group's tools
|
||||
and the strip's top-left corner) are saved with the preferences and in user workspaces; the built-in workspaces
|
||||
float none.
|
||||
|
||||
Flatten Transparency: `ui.flattenTransparencyDialog` opens the `flattenTransparency` dialog for the selection
|
||||
(fields `preset`: a preset name, setting it loads that preset's options; the option keys of
|
||||
`object.flattenTransparency`: `balance` 0–100, `lineArtPpi` and `gradientPpi` 1–2400, `textToOutlines`,
|
||||
|
||||
Reference in new issue
Block a user