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:
bflatasticandClaude Opus 5.5 committed 2026-10-08 13:01:40 +07:00
1 parent dcc8a24f66
commit 97906f2104
12 files changed
+165 -20

No files matched your search

+2 -2
View File
@@ -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.
+1
View File
@@ -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.
+1
View File
@@ -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.
+1
View File
@@ -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.
+1
View File
@@ -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.
+1
View File
@@ -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.
+1
View File
@@ -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.
+19
View File
@@ -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("");
+6
View File
@@ -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
View File
@@ -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]
+5 -1
View File
@@ -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();
}
+8
View File
@@ -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`,