mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 17:40:14 -06:00
Merge pull request #5038 from dotCooCoo/feat/new-toolbar-r
Toolbars: New Toolbar makes a named toolbar filled from the All Tools drawer
This commit is contained in:
@@ -501,6 +501,10 @@ impl VectorcraftApp {
|
||||
}
|
||||
if self.ui.toolbar && self.ui.screen_mode < 3 {
|
||||
toolbar::show(self, ui);
|
||||
} else if self.ui.screen_mode < 3 && self.ui.tab_hid.is_none() {
|
||||
// Window › Tools hides the Tools panel alone; Tab hides the toolbars made with New
|
||||
// Toolbar too.
|
||||
toolbar::show_custom(self, ui.ctx());
|
||||
}
|
||||
panels::library_panel::tidy(self);
|
||||
// The canvas and the panels: one docking layout. Tab hides the panels but not the canvas.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -295,6 +295,7 @@ registry! {
|
||||
Preferences: ["preferences"] => DialogSpec::window(crate::prefs_dialog::show, |app, _| crate::prefs_dialog::confirm(app)),
|
||||
Shortcuts: ["shortcuts"] => DialogSpec::window(crate::shortcut_editor::show, |app, _| crate::shortcut_editor::confirm(app)),
|
||||
Workspaces: ["newWorkspace", "manageWorkspaces"] => DialogSpec::window(crate::workspaces::show, |app, _| crate::workspaces::confirm(app)),
|
||||
NewToolbar: [crate::custom_toolbars::KIND] => DialogSpec::window(crate::custom_toolbars::show_dialog, |app, _| crate::custom_toolbars::confirm(app)),
|
||||
FindFont: ["findFont"] => DialogSpec::window(crate::find_font::show, |app, _| crate::find_font::confirm(app)),
|
||||
SwatchOptions: [swatch_options::KIND] => swatch_options::SPEC,
|
||||
Confirm: [confirm::KIND] => confirm::SPEC,
|
||||
|
||||
@@ -11,7 +11,9 @@
|
||||
//! is open, and the ones already in the toolbar show dimmed in it).
|
||||
//!
|
||||
//! Each toolbar (Basic, Advanced) keeps its own edits in `UiState::toolbar_edits`, saved with the
|
||||
//! preferences; `toolbar.addTool`, `toolbar.removeTool` and `toolbar.reset` make them.
|
||||
//! preferences; `toolbar.addTool`, `toolbar.removeTool` and `toolbar.reset` make them. With their
|
||||
//! `toolbar` param they edit a toolbar made with New Toolbar instead (`custom_toolbars`), whose •••
|
||||
//! button opens the drawer beside it.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::sync::OnceLock;
|
||||
@@ -154,11 +156,42 @@ fn edit(app: &mut VectorcraftApp, f: impl FnOnce(&mut Vec<ToolbarCategory>) -> R
|
||||
Ok(slots_json(app))
|
||||
}
|
||||
|
||||
/// Change the toolbar made with New Toolbar called `name` with `f`, and the groups torn off it with
|
||||
/// it ([`crate::custom_toolbars::keep_strips`]). Returns its slots.
|
||||
fn edit_custom(app: &mut VectorcraftApp, name: &str, f: impl FnOnce(&mut Vec<ToolbarCategory>) -> Result<(), String>) -> Result<Value, String> {
|
||||
let bar = crate::custom_toolbars::find_mut(&mut app.ui, name).ok_or_else(|| format!("no toolbar named `{name}`"))?;
|
||||
let mut cats = vec![ToolbarCategory { name: None, slots: bar.slots.clone() }];
|
||||
f(&mut cats)?;
|
||||
prune(&mut cats);
|
||||
bar.slots = cats.into_iter().flat_map(|c| c.slots).collect();
|
||||
crate::custom_toolbars::keep_strips(bar);
|
||||
Ok(json!(bar.slots))
|
||||
}
|
||||
|
||||
/// Change the toolbar the `toolbar` param names (none: the current Basic or Advanced toolbar).
|
||||
fn edit_target(app: &mut VectorcraftApp, p: &Value, f: impl FnOnce(&mut Vec<ToolbarCategory>, bool) -> Result<(), String>) -> Result<Value, String> {
|
||||
match crate::custom_toolbars::target(&app.ui, p)? {
|
||||
Some(name) => edit_custom(app, &name, |cats| f(cats, false)),
|
||||
None => {
|
||||
let named = !app.ui.toolbar_advanced;
|
||||
edit(app, |cats| f(cats, named))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The current toolbar's slots, each a list of tool ids (`toolbar.*` commands return it).
|
||||
fn slots_json(app: &VectorcraftApp) -> Value {
|
||||
json!(crate::toolbar::slots(app).into_iter().map(|(_, s)| s).collect::<Vec<_>>())
|
||||
}
|
||||
|
||||
/// The tools of the toolbar the drawer edits: one made with New Toolbar, or the Tools panel's.
|
||||
fn target_tools(app: &VectorcraftApp) -> BTreeSet<&'static str> {
|
||||
match crate::custom_toolbars::drawer_target(&app.ui).and_then(|name| crate::custom_toolbars::find(&app.ui, name)) {
|
||||
Some(bar) => bar.slots.iter().flatten().filter_map(|id| tool_info(id).map(|t| t.id)).collect(),
|
||||
None => crate::toolbar::slots(app).into_iter().flat_map(|(_, s)| s).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Put `tool` by the slot numbered `slot` (over the toolbar's slots, 0 = the first; none or null:
|
||||
/// at the end), moving it if it is in the toolbar already. A tool appended to a toolbar left without
|
||||
/// categories starts one (`named`: under its own heading).
|
||||
@@ -222,24 +255,36 @@ fn tool_param(p: &Value) -> Result<&'static str, String> {
|
||||
/// The Edit Toolbar commands; `None` for other ids.
|
||||
pub(crate) fn command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<Result<Value, String>> {
|
||||
Some(match id {
|
||||
"window.editToolbar" => crate::menus::opt_bool(p, "open").map(|open| {
|
||||
let open = open.unwrap_or(!app.ui.tools_drawer);
|
||||
"window.editToolbar" => (|| {
|
||||
let open = crate::menus::opt_bool(p, "open")?;
|
||||
let target = crate::custom_toolbars::target(&app.ui, p)?;
|
||||
// Toggled: open, or open beside another toolbar, closes or moves it.
|
||||
let open = open.unwrap_or(!(app.ui.tools_drawer && app.ui.drawer_toolbar == target));
|
||||
app.ui.tools_drawer = open;
|
||||
app.ui.drawer_toolbar = target.filter(|_| open);
|
||||
if open {
|
||||
app.ui.flyout = None;
|
||||
// The toolbar the drawer opens beside shows.
|
||||
match app.ui.drawer_toolbar.clone() {
|
||||
Some(name) => {
|
||||
if let Some(bar) = crate::custom_toolbars::find_mut(&mut app.ui, &name) {
|
||||
bar.shown = true;
|
||||
}
|
||||
}
|
||||
None => app.ui.toolbar = true,
|
||||
}
|
||||
}
|
||||
json!(open)
|
||||
}),
|
||||
Ok(json!(open))
|
||||
})(),
|
||||
"toolbar.addTool" => (|| {
|
||||
let tool = tool_param(p)?;
|
||||
let place = Place::parse(p)?;
|
||||
let named = !app.ui.toolbar_advanced;
|
||||
edit(app, |cats| add(cats, tool, p.get("slot"), place, named))
|
||||
edit_target(app, p, |cats, named| add(cats, tool, p.get("slot"), place, named))
|
||||
})(),
|
||||
"toolbar.removeTool" => (|| {
|
||||
let tool = tool_param(p)?;
|
||||
let group = crate::menus::opt_bool(p, "group")?.unwrap_or(false);
|
||||
edit(app, |cats| {
|
||||
edit_target(app, p, |cats, _| {
|
||||
remove(cats, tool, group);
|
||||
Ok(())
|
||||
})
|
||||
@@ -257,10 +302,17 @@ pub(crate) fn command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<R
|
||||
}
|
||||
Ok(app.ui.toolbar_show.json())
|
||||
})(),
|
||||
"toolbar.reset" => {
|
||||
"toolbar.reset" => (|| {
|
||||
if let Some(name) = crate::custom_toolbars::target(&app.ui, p)? {
|
||||
// A toolbar made with New Toolbar goes back to blank, as it was made.
|
||||
return edit_custom(app, &name, |cats| {
|
||||
cats.clear();
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
*current(&mut app.ui.toolbar_edits, app.ui.toolbar_advanced) = None;
|
||||
Ok(slots_json(app))
|
||||
}
|
||||
})(),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
@@ -290,9 +342,9 @@ pub(crate) fn drop_target(slots: &[(usize, Rect)], at: Pos2, cols: usize) -> (Op
|
||||
(Some(k), if before { Place::Before } else { Place::After })
|
||||
}
|
||||
|
||||
/// While a tool is dragged over the toolbar (`bounds`): mark where it would go, and on the release
|
||||
/// put it there (`toolbar.addTool`).
|
||||
pub(crate) fn toolbar_drop(app: &mut VectorcraftApp, ui: &Ui, slots: &[(usize, Rect)], cols: usize, bounds: Rect) {
|
||||
/// While a tool is dragged over the toolbar (`bounds`; `toolbar`: one made with New Toolbar): mark
|
||||
/// where it would go, and on the release put it there (`toolbar.addTool`).
|
||||
pub(crate) fn toolbar_drop(app: &mut VectorcraftApp, ui: &Ui, slots: &[(usize, Rect)], cols: usize, bounds: Rect, toolbar: Option<&str>) {
|
||||
let Some(drag) = DragAndDrop::payload::<ToolDrag>(ui.ctx()) else { return };
|
||||
let Some(at) = ui.ctx().pointer_interact_pos().filter(|p| bounds.contains(*p)) else { return };
|
||||
let t = Tokens::get(ui.ctx());
|
||||
@@ -319,7 +371,7 @@ pub(crate) fn toolbar_drop(app: &mut VectorcraftApp, ui: &Ui, slots: &[(usize, R
|
||||
}
|
||||
if ui.input(|i| i.pointer.any_released()) {
|
||||
DragAndDrop::clear_payload(ui.ctx());
|
||||
let p = json!({ "tool": drag.tool, "slot": slot, "place": place.name() });
|
||||
let p = json!({ "tool": drag.tool, "slot": slot, "place": place.name(), "toolbar": toolbar });
|
||||
if let Err(e) = app.run("toolbar.addTool", p) {
|
||||
app.ui.status = e;
|
||||
}
|
||||
@@ -420,7 +472,8 @@ pub(crate) fn drawer(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
// buttons) and the bars under it.
|
||||
let top = bar.top().max(screen.top() + GAP);
|
||||
let y = button.top().min(screen.bottom() - size.y - GAP).max(top);
|
||||
let in_toolbar: BTreeSet<&str> = crate::toolbar::slots(app).into_iter().flat_map(|(_, s)| s).collect();
|
||||
let in_toolbar = target_tools(app);
|
||||
let target = crate::custom_toolbars::drawer_target(&app.ui).map(str::to_string);
|
||||
let active = app.session.tool_id();
|
||||
let dragged = DragAndDrop::payload::<ToolDrag>(ctx).map(|d| *d);
|
||||
let pointer = ctx.pointer_interact_pos();
|
||||
@@ -428,13 +481,15 @@ pub(crate) fn drawer(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
let removing = dragged.is_some_and(|d| d.from_toolbar) && ctx.memory(|m| m.area_rect(id)).is_some_and(over);
|
||||
let mut picked = None;
|
||||
let mut reset = false;
|
||||
let mut new_toolbar = false;
|
||||
let mut advanced = None;
|
||||
let mut show_toggle = None;
|
||||
let resp = egui::Area::new(id).order(egui::Order::Foreground).fixed_pos(pos2(x, y)).show(ctx, |ui| {
|
||||
crate::toolbar::flyout_frame(&t, removing).inner_margin(egui::Margin::same(8)).show(ui, |ui| {
|
||||
let width = WIDTH.min(screen.width() - 2.0 * GAP - 16.0).max(160.0);
|
||||
ui.set_width(width);
|
||||
// All Tools, and the drawer's flyout menu: Reset, and the Basic or Advanced toolbar.
|
||||
// All Tools, and the drawer's flyout menu: Reset, the Basic or Advanced toolbar, and New
|
||||
// Toolbar.
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(egui::RichText::new(tl!("All Tools")).size(13.0).color(t.text_strong));
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
@@ -453,6 +508,10 @@ pub(crate) fn drawer(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
advanced = Some(adv);
|
||||
}
|
||||
}
|
||||
ui.separator();
|
||||
if widgets::menu_item(ui, "New Toolbar…", true, false) {
|
||||
new_toolbar = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -481,7 +540,11 @@ pub(crate) fn drawer(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
}
|
||||
}
|
||||
});
|
||||
// Show: the controls under the toolbar's tools.
|
||||
// Show: the controls under the Tools panel's tools (a toolbar made with New Toolbar has
|
||||
// none).
|
||||
if target.is_some() {
|
||||
return;
|
||||
}
|
||||
ui.add_space(4.0);
|
||||
ui.separator();
|
||||
ui.horizontal(|ui| {
|
||||
@@ -501,16 +564,18 @@ pub(crate) fn drawer(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
&& ctx.input(|i| i.pointer.any_released())
|
||||
{
|
||||
DragAndDrop::clear_payload(ctx);
|
||||
if let Err(e) = app.run("toolbar.removeTool", json!({ "tool": d.tool })) {
|
||||
if let Err(e) = app.run("toolbar.removeTool", json!({ "tool": d.tool, "toolbar": target })) {
|
||||
app.ui.status = e;
|
||||
}
|
||||
}
|
||||
if reset && let Err(e) = app.run("toolbar.reset", json!({})) {
|
||||
if reset && let Err(e) = app.run("toolbar.reset", json!({ "toolbar": target })) {
|
||||
app.ui.status = e;
|
||||
}
|
||||
// Basic or Advanced: the menu item of the other toolbar switches to it.
|
||||
if advanced.is_some_and(|a| a != app.ui.toolbar_advanced)
|
||||
&& let Err(e) = app.run("window.toolbarAdvanced", json!({}))
|
||||
if new_toolbar && let Err(e) = app.run("window.toolbar.new", json!({})) {
|
||||
app.ui.status = e;
|
||||
}
|
||||
if let Some(advanced) = advanced
|
||||
&& let Err(e) = pick_layout(app, advanced)
|
||||
{
|
||||
app.ui.status = e;
|
||||
}
|
||||
@@ -530,6 +595,19 @@ pub(crate) fn drawer(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
ghost(ctx, &t);
|
||||
}
|
||||
|
||||
/// The drawer menu's Basic or Advanced: the menu item of the other toolbar switches the Tools panel
|
||||
/// to it, and the drawer goes on to edit the Tools panel's toolbar, also from a toolbar made with
|
||||
/// New Toolbar.
|
||||
fn pick_layout(app: &mut VectorcraftApp, advanced: bool) -> Result<(), String> {
|
||||
if crate::custom_toolbars::drawer_target(&app.ui).is_some() {
|
||||
app.run("window.editToolbar", json!({ "open": true }))?;
|
||||
}
|
||||
if advanced != app.ui.toolbar_advanced {
|
||||
app.run("window.toolbarAdvanced", json!({}))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A Show section toggle: lit while its control shows under the toolbar's tools. Fill and Stroke
|
||||
/// and Coloring are drawn as small copies of those controls, the modes with their buttons' icons.
|
||||
fn show_button(ui: &mut Ui, t: &Tokens, name: &str, label: &str, icon: &str, on: bool) -> bool {
|
||||
|
||||
@@ -356,6 +356,143 @@ fn tools_drag_between_the_drawer_and_the_toolbar() {
|
||||
assert_eq!(slots(&app), before);
|
||||
}
|
||||
|
||||
/// A toolbar made with New Toolbar floats blank; its ••• button opens the drawer beside it, and
|
||||
/// tools dragged from the drawer fill it, leaving the Tools panel's toolbar as it was.
|
||||
#[test]
|
||||
fn a_new_toolbar_fills_from_the_drawer_beside_it() {
|
||||
let mut app = app();
|
||||
let ctx = ctx();
|
||||
let shipped = slots(&app);
|
||||
app.run("window.toolbar.new", json!({"name": "Mine"})).unwrap();
|
||||
frame(&mut app, &ctx, 0.0, vec![]);
|
||||
frame(&mut app, &ctx, 0.1, vec![]);
|
||||
let area = || ctx.memory(|m| m.area_rect(crate::custom_toolbars::area_id("Mine"))).expect("the toolbar is shown");
|
||||
let bar = area();
|
||||
let edit = crate::toolbar::tests::widget_rects(&ctx, vec2(26.0, 26.0)).into_iter().find(|r| bar.contains(r.center())).expect("its ••• button");
|
||||
// Open beside the Tools panel's toolbar, the drawer moves to this one on its ••• click.
|
||||
app.run("window.editToolbar", json!({})).unwrap();
|
||||
frame(&mut app, &ctx, 0.5, vec![]);
|
||||
click(&mut app, &ctx, 1.0, edit.center());
|
||||
assert!(app.ui.tools_drawer && app.ui.drawer_toolbar.as_deref() == Some("Mine"));
|
||||
assert_eq!(crate::menus::checked(&app, "window.editToolbar", &json!({"toolbar": "Mine"})), Some(true));
|
||||
assert_eq!(crate::menus::checked(&app, "window.editToolbar", &json!({})), Some(false));
|
||||
let shown = frame(&mut app, &ctx, 1.2, vec![]);
|
||||
let drawer = drawer_rect(&ctx).unwrap();
|
||||
assert!((drawer.left() - bar.right() - GAP).abs() < 2.0, "beside the new toolbar: {drawer:?} {bar:?}");
|
||||
// The Tools panel's tools aren't dimmed: they aren't in this toolbar. The Show section, for
|
||||
// the controls under the Tools panel's tools, is left out.
|
||||
assert!(!shown.iter().any(|(s, p, _)| s == "Show:" && drawer.contains(*p)));
|
||||
let dim = Tokens::get(&ctx).text_disabled;
|
||||
assert_ne!(cell_at(&shown, drawer, "Ellipse Tool").1, dim);
|
||||
// Dragged onto the blank toolbar, a tool is added to it.
|
||||
let (ellipse, _) = cell_at(&shown, drawer, "Ellipse Tool");
|
||||
drag(&mut app, &ctx, 2.0, ellipse, bar.center());
|
||||
assert_eq!(crate::custom_toolbars::find(&app.ui, "Mine").unwrap().slots, [["ellipse"]]);
|
||||
assert_eq!(slots(&app), shipped, "the Tools panel's toolbar is untouched");
|
||||
let shown = frame(&mut app, &ctx, 2.9, vec![]);
|
||||
let drawer = drawer_rect(&ctx).unwrap();
|
||||
assert_eq!(cell_at(&shown, drawer, "Ellipse Tool").1, dim, "now in the toolbar the drawer edits");
|
||||
// A second one, below the first.
|
||||
let tools: Vec<Rect> = crate::toolbar::tests::widget_rects(&ctx, BUTTON).into_iter().filter(|r| area().contains(r.center())).collect();
|
||||
let (artboard, _) = cell_at(&shown, drawer, "Artboard Tool");
|
||||
drag(&mut app, &ctx, 3.0, artboard, tools[0].center_bottom() - vec2(0.0, 3.0));
|
||||
assert_eq!(crate::custom_toolbars::find(&app.ui, "Mine").unwrap().slots, [["ellipse"], ["artboard"]]);
|
||||
// A click on its tool picks it; the drawer stays open.
|
||||
let tools: Vec<Rect> = crate::toolbar::tests::widget_rects(&ctx, BUTTON).into_iter().filter(|r| area().contains(r.center())).collect();
|
||||
click(&mut app, &ctx, 4.0, tools[1].center());
|
||||
assert_eq!(app.session.tool_id(), "artboard");
|
||||
assert!(app.ui.tools_drawer);
|
||||
// Dragged from it into the drawer, a tool leaves it.
|
||||
drag(&mut app, &ctx, 5.0, tools[0].center(), drawer.center());
|
||||
assert_eq!(crate::custom_toolbars::find(&app.ui, "Mine").unwrap().slots, [["artboard"]]);
|
||||
// The Tools panel's ••• button takes the drawer back beside its toolbar.
|
||||
let main_edit = crate::toolbar::tests::widget_rects(&ctx, vec2(26.0, 26.0)).into_iter().rfind(|r| r.right() < 60.0).unwrap();
|
||||
click(&mut app, &ctx, 5.5, main_edit.center());
|
||||
assert!(app.ui.tools_drawer && app.ui.drawer_toolbar.is_none());
|
||||
// The drawer closed, a drag on the grip bar moves the toolbar.
|
||||
app.run("window.editToolbar", json!({"open": false})).unwrap();
|
||||
frame(&mut app, &ctx, 6.0, vec![]);
|
||||
let grip = ctx.viewport(|vp| vp.prev_pass.widgets.get(crate::custom_toolbars::area_id("Mine").with("grip")).map(|w| w.sense));
|
||||
assert!(grip.is_some_and(|s| s.senses_drag() && !s.is_focusable()), "only the pointer drags the grip, so Tab does not stop on it: {grip:?}");
|
||||
let before = area();
|
||||
drag(&mut app, &ctx, 7.0, before.center_top() + vec2(0.0, 5.0), before.center_top() + vec2(120.0, 205.0));
|
||||
frame(&mut app, &ctx, 7.5, vec![]);
|
||||
let after = area();
|
||||
assert!((after.min - before.min - vec2(120.0, 200.0)).length() < 2.0, "{before:?} {after:?}");
|
||||
// Hidden, it isn't drawn.
|
||||
app.run("window.toolbar.custom", json!({"name": "Mine", "show": false})).unwrap();
|
||||
frame(&mut app, &ctx, 8.0, vec![]);
|
||||
frame(&mut app, &ctx, 8.1, vec![]);
|
||||
assert!(!ctx.memory(|m| m.areas().visible_last_frame(&egui::LayerId::new(egui::Order::Middle, crate::custom_toolbars::area_id("Mine")))));
|
||||
}
|
||||
|
||||
/// New Toolbar… in the drawer's menu opens the New Toolbar dialog, which asks for a name.
|
||||
#[test]
|
||||
fn new_toolbar_in_the_drawer_menu_asks_for_a_name() {
|
||||
let mut app = app();
|
||||
let ctx = ctx();
|
||||
app.run("window.editToolbar", json!({"open": true})).unwrap();
|
||||
frame(&mut app, &ctx, 0.0, vec![]);
|
||||
frame(&mut app, &ctx, 0.1, vec![]);
|
||||
let drawer = drawer_rect(&ctx).expect("the drawer is open");
|
||||
let menu = crate::toolbar::tests::widget_rects(&ctx, vec2(18.0, 18.0))
|
||||
.into_iter()
|
||||
.find(|r| drawer.contains(r.center()))
|
||||
.expect("the drawer menu button");
|
||||
click(&mut app, &ctx, 1.0, menu.center());
|
||||
// The menu is sized in its first frame and drawn from the second.
|
||||
frame(&mut app, &ctx, 1.2, vec![]);
|
||||
let shown = frame(&mut app, &ctx, 1.3, vec![]);
|
||||
// A menu item's label is indented where a checked item has its ✓.
|
||||
let item = shown.iter().find(|(s, ..)| s.trim() == "New Toolbar…").map(|(_, p, _)| *p).expect("New Toolbar… in the drawer menu");
|
||||
assert!(app.ui.dialog.is_none());
|
||||
click(&mut app, &ctx, 2.0, item);
|
||||
let d = app.ui.dialog.clone().expect("the dialog asks for a name");
|
||||
assert_eq!((d.kind.as_str(), d.str("name").as_str()), (crate::custom_toolbars::KIND, "Toolbar 1"));
|
||||
}
|
||||
|
||||
/// A click on another toolbar's ••• button takes the drawer there, whichever was made first.
|
||||
#[test]
|
||||
fn the_drawer_moves_between_new_toolbars() {
|
||||
let mut app = app();
|
||||
let ctx = ctx();
|
||||
for name in ["A", "B"] {
|
||||
app.run("window.toolbar.new", json!({"name": name})).unwrap();
|
||||
}
|
||||
app.ui.custom_toolbars[1].pos = [600.0, 900.0];
|
||||
frame(&mut app, &ctx, 0.0, vec![]);
|
||||
frame(&mut app, &ctx, 0.1, vec![]);
|
||||
let edit_of = |name: &str| {
|
||||
let bar = ctx.memory(|m| m.area_rect(crate::custom_toolbars::area_id(name))).unwrap();
|
||||
crate::toolbar::tests::widget_rects(&ctx, vec2(26.0, 26.0)).into_iter().find(|r| bar.contains(r.center())).unwrap()
|
||||
};
|
||||
let (a, b) = (edit_of("A"), edit_of("B"));
|
||||
for (k, (button, name)) in [(b, "B"), (a, "A"), (b, "B")].into_iter().enumerate() {
|
||||
click(&mut app, &ctx, 1.0 + k as f64, button.center());
|
||||
frame(&mut app, &ctx, 1.5 + k as f64, vec![]);
|
||||
assert_eq!(crate::custom_toolbars::drawer_target(&app.ui), Some(name), "after the click on {name}'s ••• button");
|
||||
}
|
||||
}
|
||||
|
||||
/// Basic or Advanced in the drawer's menu, picked while the drawer edits a toolbar made with New
|
||||
/// Toolbar, switches the Tools panel and takes the drawer to it, so Reset and drops edit it.
|
||||
#[test]
|
||||
fn basic_or_advanced_takes_the_drawer_to_the_tools_panel() {
|
||||
let mut app = app();
|
||||
app.run("window.toolbar.new", json!({"name": "Mine"})).unwrap();
|
||||
app.run("toolbar.addTool", json!({"tool": "pen", "toolbar": "Mine"})).unwrap();
|
||||
// With the Tools panel hidden too: it shows.
|
||||
app.run("window.toolbar", json!({})).unwrap();
|
||||
for advanced in [false, true] {
|
||||
app.run("window.editToolbar", json!({"toolbar": "Mine", "open": true})).unwrap();
|
||||
pick_layout(&mut app, advanced).unwrap();
|
||||
assert!(app.ui.tools_drawer && crate::custom_toolbars::drawer_target(&app.ui).is_none(), "{advanced}");
|
||||
assert_eq!(app.ui.toolbar_advanced, advanced);
|
||||
assert!(app.ui.toolbar, "the Tools panel shows beside the drawer");
|
||||
}
|
||||
assert_eq!(crate::custom_toolbars::find(&app.ui, "Mine").unwrap().slots, [["pen"]], "Mine as it was");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drop_targets_follow_the_buttons_edges() {
|
||||
let r = |y: f32| Rect::from_min_size(pos2(6.0, y), BUTTON);
|
||||
|
||||
@@ -2019,6 +2019,10 @@ font-technology Variable Променливи
|
||||
New Without Dialog Нов без диалогов прозорец
|
||||
New Workspace Ново работно пространство
|
||||
New Workspace… Ново работно пространство…
|
||||
New Toolbar… Нова лента с инструменти…
|
||||
New Toolbar Нова лента с инструменти
|
||||
Show Toolbar Показване на лента с инструменти
|
||||
List Toolbars Списък на лентите с инструменти
|
||||
New {kind} Style… Нов стил ({kind})…
|
||||
New… Нов…
|
||||
Next Следващ
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -636,6 +636,10 @@
|
||||
Reset Typography Vrati Tipografiju na početno
|
||||
Reset Web Vrati Web na početno
|
||||
New Workspace… Novi radni prostor…
|
||||
New Toolbar… Nova traka s alatima…
|
||||
New Toolbar Nova traka s alatima
|
||||
Show Toolbar Prikaži traku s alatima
|
||||
List Toolbars Izlistaj trake s alatima
|
||||
Manage Workspaces… Upravljaj radnim prostorima…
|
||||
Control Kontrola
|
||||
Contextual Task Bar Kontekstna traka zadataka
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2550,6 +2550,10 @@ digits Malayalam Malaiàlam
|
||||
New Without Dialog Nou sense diàleg
|
||||
New Workspace Espai de treball nou
|
||||
New Workspace… Espai de treball nou…
|
||||
New Toolbar… Barra d’eines nova…
|
||||
New Toolbar Barra d’eines nova
|
||||
Show Toolbar Mostra la barra d’eines
|
||||
List Toolbars Llista les barres d’eines
|
||||
New {kind} Style… Nou estil de {kind}…
|
||||
New… Nou…
|
||||
Next Següent
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -646,6 +646,10 @@
|
||||
Reset Typography Obnovit Typography
|
||||
Reset Web Obnovit Web
|
||||
New Workspace… Nový pracovní prostor…
|
||||
New Toolbar… Nový panel nástrojů…
|
||||
New Toolbar Nový panel nástrojů
|
||||
Show Toolbar Zobrazit panel nástrojů
|
||||
List Toolbars Seznam panelů nástrojů
|
||||
Manage Workspaces… Spravovat pracovní prostory…
|
||||
Control Ovládací panel
|
||||
Contextual Task Bar Kontextový panel úloh
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -596,6 +596,10 @@
|
||||
Reset Typography Nulstil Typography
|
||||
Reset Web Nulstil Web
|
||||
New Workspace… Nyt arbejdsområde…
|
||||
New Toolbar… Ny værktøjslinje…
|
||||
New Toolbar Ny værktøjslinje
|
||||
Show Toolbar Vis værktøjslinje
|
||||
List Toolbars Liste over værktøjslinjer
|
||||
Manage Workspaces… Administrér arbejdsområder…
|
||||
Control Kontrol
|
||||
Contextual Task Bar Kontekstuel opgavelinje
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2032,6 +2032,10 @@ file-filter Templates Vorlagen
|
||||
New Without Dialog Neu ohne Dialogfeld
|
||||
New Workspace Neuer Arbeitsbereich
|
||||
New Workspace… Neuer Arbeitsbereich…
|
||||
New Toolbar… Neue Werkzeugleiste…
|
||||
New Toolbar Neue Werkzeugleiste
|
||||
Show Toolbar Werkzeugleiste einblenden
|
||||
List Toolbars Werkzeugleisten auflisten
|
||||
New {kind} Style… Neues {kind}format…
|
||||
New… Neu…
|
||||
Next Weiter
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1700,6 +1700,10 @@ font-technology Variable Μεταβλητές
|
||||
New Without Dialog Νέο χωρίς παράθυρο διαλόγου
|
||||
New Workspace Νέος χώρος εργασίας
|
||||
New Workspace… Νέος χώρος εργασίας…
|
||||
New Toolbar… Νέα γραμμή εργαλείων…
|
||||
New Toolbar Νέα γραμμή εργαλείων
|
||||
Show Toolbar Εμφάνιση γραμμής εργαλείων
|
||||
List Toolbars Λίστα γραμμών εργαλείων
|
||||
New… Νέο…
|
||||
Next Επόμενο
|
||||
No document Δεν υπάρχει έγγραφο
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 163 and column 5.
|
@@ -2080,6 +2080,10 @@ font-technology Variable Variables
|
||||
New Without Dialog Nuevo sin cuadro de diálogo
|
||||
New Workspace Nuevo espacio de trabajo
|
||||
New Workspace… Nuevo espacio de trabajo…
|
||||
New Toolbar… Nueva barra de herramientas…
|
||||
New Toolbar Nueva barra de herramientas
|
||||
Show Toolbar Mostrar barra de herramientas
|
||||
List Toolbars Lista de barras de herramientas
|
||||
New {kind} Style… Nuevo estilo de {kind}…
|
||||
New… Nuevo…
|
||||
Next Siguiente
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -637,6 +637,10 @@
|
||||
Reset Typography Palauta Typography
|
||||
Reset Web Palauta Web
|
||||
New Workspace… Uusi työtila…
|
||||
New Toolbar… Uusi työkalurivi…
|
||||
New Toolbar Uusi työkalurivi
|
||||
Show Toolbar Näytä työkalurivi
|
||||
List Toolbars Luettele työkalurivit
|
||||
Manage Workspaces… Hallitse työtiloja…
|
||||
Control Ohjaus
|
||||
Contextual Task Bar Kontekstuaalinen tehtäväpalkki
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2072,6 +2072,10 @@ font-technology Variable Variables
|
||||
New Without Dialog Nouveau sans boîte de dialogue
|
||||
New Workspace Nouvel espace de travail
|
||||
New Workspace… Nouvel espace de travail…
|
||||
New Toolbar… Nouvelle barre d’outils…
|
||||
New Toolbar Nouvelle barre d’outils
|
||||
Show Toolbar Afficher la barre d’outils
|
||||
List Toolbars Liste des barres d’outils
|
||||
New {kind} Style… Nouveau style de {kind}…
|
||||
New… Nouveau…
|
||||
Next Suivant
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2535,6 +2535,10 @@ file-filter Templates Sablonok
|
||||
New Without Dialog Új párbeszédablak nélkül
|
||||
New Workspace Új munkaterület
|
||||
New Workspace… Új munkaterület…
|
||||
New Toolbar… Új eszköztár…
|
||||
New Toolbar Új eszköztár
|
||||
Show Toolbar Eszköztár megjelenítése
|
||||
List Toolbars Eszköztárak listája
|
||||
New {kind} Style… Új stílus: {kind}…
|
||||
New… Új…
|
||||
Next Következő
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -596,6 +596,10 @@
|
||||
Reset Typography Atur ulang Typography
|
||||
Reset Web Atur ulang Web
|
||||
New Workspace… Ruang kerja baru…
|
||||
New Toolbar… Bilah alat baru…
|
||||
New Toolbar Bilah alat baru
|
||||
Show Toolbar Tampilkan bilah alat
|
||||
List Toolbars Daftar bilah alat
|
||||
Manage Workspaces… Kelola ruang kerja…
|
||||
Control Kontrol
|
||||
Contextual Task Bar Bilah tugas kontekstual
|
||||
|
||||
|
@@ -2108,6 +2108,10 @@ file-filter Templates Modelli
|
||||
New Without Dialog Nuovo senza finestra di dialogo
|
||||
New Workspace Nuova area di lavoro
|
||||
New Workspace… Nuova area di lavoro…
|
||||
New Toolbar… Nuova barra degli strumenti…
|
||||
New Toolbar Nuova barra degli strumenti
|
||||
Show Toolbar Mostra barra degli strumenti
|
||||
List Toolbars Elenco barre degli strumenti
|
||||
New {kind} Style… Nuovo stile {kind}…
|
||||
New… Nuovo…
|
||||
Next Successivo
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -650,6 +650,10 @@ digits Malayalam マラヤーラム数字
|
||||
Workspace ワークスペース
|
||||
Reset Essentials 初期設定をリセット
|
||||
New Workspace… 新規ワークスペース…
|
||||
New Toolbar… 新規ツールバー…
|
||||
New Toolbar 新規ツールバー
|
||||
Show Toolbar ツールバーを表示
|
||||
List Toolbars ツールバーの一覧
|
||||
Manage Workspaces… ワークスペースを管理…
|
||||
Control コントロール
|
||||
Contextual Task Bar コンテキストタスクバー
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -540,6 +540,10 @@
|
||||
Workspace სამუშაო სივრცე
|
||||
Reset Essentials Essentials-ის ჩამოყრა
|
||||
New Workspace… ახალი სამუშაო სივრცე…
|
||||
New Toolbar… ახალი ხელსაწყოთა ზოლი…
|
||||
New Toolbar ახალი ხელსაწყოთა ზოლი
|
||||
Show Toolbar ხელსაწყოთა ზოლის ჩვენება
|
||||
List Toolbars ხელსაწყოთა ზოლების სია
|
||||
Manage Workspaces… სამუშაო სივრცეების მართვა…
|
||||
Control მართვა
|
||||
Contextual Task Bar კონტექსტური ამოცანების ზოლი
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -643,6 +643,10 @@ digits Malayalam 말라얄람어
|
||||
Workspace 작업 영역
|
||||
Reset Essentials Essentials 재설정
|
||||
New Workspace… 새 작업 영역…
|
||||
New Toolbar… 새 도구 막대…
|
||||
New Toolbar 새 도구 막대
|
||||
Show Toolbar 도구 막대 표시
|
||||
List Toolbars 도구 막대 목록
|
||||
Manage Workspaces… 작업 영역 관리…
|
||||
Control 컨트롤
|
||||
Contextual Task Bar 상황별 작업 표시줄
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -610,6 +610,10 @@
|
||||
Reset Typography Typography herstellen
|
||||
Reset Web Web herstellen
|
||||
New Workspace… Nieuwe werkruimte…
|
||||
New Toolbar… Nieuwe werkbalk…
|
||||
New Toolbar Nieuwe werkbalk
|
||||
Show Toolbar Werkbalk tonen
|
||||
List Toolbars Lijst met werkbalken
|
||||
Manage Workspaces… Werkruimten beheren…
|
||||
Control Bediening
|
||||
Contextual Task Bar Contextuele taakbalk
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -596,6 +596,10 @@
|
||||
Workspace Obszar roboczy
|
||||
Reset Essentials Resetuj Essentials
|
||||
New Workspace… Nowy obszar roboczy…
|
||||
New Toolbar… Nowy pasek narzędzi…
|
||||
New Toolbar Nowy pasek narzędzi
|
||||
Show Toolbar Pokaż pasek narzędzi
|
||||
List Toolbars Lista pasków narzędzi
|
||||
Manage Workspaces… Zarządzaj obszarami roboczymi…
|
||||
Control Sterowanie
|
||||
Contextual Task Bar Kontekstowy pasek zadań
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1700,6 +1700,10 @@
|
||||
New Without Dialog Novo sem Diálogo
|
||||
New Workspace Novo Espaço de Trabalho
|
||||
New Workspace… Novo Espaço de Trabalho…
|
||||
New Toolbar… Nova Barra de Ferramentas…
|
||||
New Toolbar Nova Barra de Ferramentas
|
||||
Show Toolbar Mostrar Barra de Ferramentas
|
||||
List Toolbars Listar Barras de Ferramentas
|
||||
New file Novo arquivo
|
||||
New from Template Novo a partir de Modelo
|
||||
New from Template… Novo a partir de Modelo…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -2103,6 +2103,10 @@ file-filter Templates Шаблоны
|
||||
New Without Dialog Создать без диалогового окна
|
||||
New Workspace Новая рабочая среда
|
||||
New Workspace… Новая рабочая среда…
|
||||
New Toolbar… Новая панель инструментов…
|
||||
New Toolbar Новая панель инструментов
|
||||
Show Toolbar Показать панель инструментов
|
||||
List Toolbars Список панелей инструментов
|
||||
New {kind} Style… Новый стиль {kind}…
|
||||
New… Создать…
|
||||
Next Далее
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2019,6 +2019,10 @@ font-technology Variable Variabla
|
||||
New Without Dialog Ny utan dialogruta
|
||||
New Workspace Ny arbetslayout
|
||||
New Workspace… Ny arbetslayout…
|
||||
New Toolbar… Nytt verktygsfält…
|
||||
New Toolbar Nytt verktygsfält
|
||||
Show Toolbar Visa verktygsfält
|
||||
List Toolbars Lista verktygsfält
|
||||
New {kind} Style… Ny {kind}stil…
|
||||
New… Ny…
|
||||
Next Nästa
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -1892,6 +1892,10 @@ font-technology Variable Değişken
|
||||
Previous Document Önceki Belge
|
||||
New Workspace Yeni Çalışma Alanı
|
||||
New Workspace… Yeni Çalışma Alanı…
|
||||
New Toolbar… Yeni Araç Çubuğu…
|
||||
New Toolbar Yeni Araç Çubuğu
|
||||
Show Toolbar Araç Çubuğunu Göster
|
||||
List Toolbars Araç Çubuklarını Listele
|
||||
New {kind} Style… Yeni {kind} Stili…
|
||||
New… Yeni…
|
||||
Next Sonraki
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -2100,6 +2100,10 @@ file-filter Templates Шаблони
|
||||
New Without Dialog Створити без діалогового вікна
|
||||
New Workspace Нове робоче середовище
|
||||
New Workspace… Нове робоче середовище…
|
||||
New Toolbar… Нова панель інструментів…
|
||||
New Toolbar Нова панель інструментів
|
||||
Show Toolbar Показати панель інструментів
|
||||
List Toolbars Список панелей інструментів
|
||||
New {kind} Style… Новий стиль {kind}…
|
||||
New… Створити…
|
||||
Next Далі
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 177 and column 5.
|
@@ -2345,6 +2345,10 @@ digits Hindi Hindi
|
||||
New Without Dialog Mới không qua hộp thoại
|
||||
New Workspace Không gian làm việc mới
|
||||
New Workspace… Không gian làm việc mới…
|
||||
New Toolbar… Thanh công cụ mới…
|
||||
New Toolbar Thanh công cụ mới
|
||||
Show Toolbar Hiện thanh công cụ
|
||||
List Toolbars Danh sách thanh công cụ
|
||||
New {kind} Style… Kiểu {kind} mới…
|
||||
New… Mới…
|
||||
Next Tiếp theo
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 199 and column 5.
|
@@ -1979,6 +1979,10 @@ file-filter Templates 模板
|
||||
New Without Dialog 无对话框新建
|
||||
New Workspace 新建工作区
|
||||
New Workspace… 新建工作区…
|
||||
New Toolbar… 新建工具栏…
|
||||
New Toolbar 新建工具栏
|
||||
Show Toolbar 显示工具栏
|
||||
List Toolbars 列出工具栏
|
||||
New {kind} Style… 新建{kind}样式…
|
||||
New… 新建…
|
||||
Next 下一个
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2947,6 +2947,10 @@ file-filter Templates 範本
|
||||
New Without Dialog 新增但不開啟對話方塊
|
||||
New Workspace 新增工作區
|
||||
New Workspace… 新增工作區…
|
||||
New Toolbar… 新增工具列…
|
||||
New Toolbar 新增工具列
|
||||
Show Toolbar 顯示工具列
|
||||
List Toolbars 列出工具列
|
||||
New {kind} Style… 新增{kind}樣式…
|
||||
New… 新增…
|
||||
Next 下一個
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -32,6 +32,7 @@ pub mod community;
|
||||
pub mod control;
|
||||
pub mod credits;
|
||||
pub mod cursors;
|
||||
mod custom_toolbars;
|
||||
pub mod dialogs;
|
||||
pub mod dock;
|
||||
mod edit_toolbar;
|
||||
|
||||
@@ -138,7 +138,7 @@ pub(super) fn shows_a_name(id: &str, label: &str) -> bool {
|
||||
];
|
||||
SLOTS.iter().any(|s| id.starts_with(s))
|
||||
|| saved_selection_slot(id).is_some()
|
||||
|| matches!(id, "text.setStyle" | "plugin.dialog")
|
||||
|| matches!(id, "text.setStyle" | "plugin.dialog" | "window.toolbar.custom")
|
||||
|| (id == "window.workspace" && !crate::workspaces::is_builtin(label))
|
||||
|| (matches!(id, "effect.apply" | "effect.dialog") && vectorcraft_effects::plugin_effects().iter().any(|e| e.label == label))
|
||||
}
|
||||
|
||||
@@ -74,6 +74,7 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<R
|
||||
if let Some(r) = crate::shortcut_editor::run_command(app, id, p)
|
||||
.or_else(|| crate::workspaces::run_command(app, id, p))
|
||||
.or_else(|| crate::edit_toolbar::command(app, id, p))
|
||||
.or_else(|| crate::custom_toolbars::command(app, id, p))
|
||||
{
|
||||
return Some(r);
|
||||
}
|
||||
@@ -427,11 +428,21 @@ fn run_ui_part5(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<Result<
|
||||
}
|
||||
"window.control" => flag(&mut app.ui.control_bar),
|
||||
"window.toolbar" => flag(&mut app.ui.toolbar),
|
||||
"window.toolbarColumns" => opt_bool(p, "double").map(|on| {
|
||||
let on = on.unwrap_or(!app.ui.toolbar_double);
|
||||
app.ui.toolbar_double = on;
|
||||
json!(on)
|
||||
}),
|
||||
"window.toolbarColumns" => {
|
||||
let toolbar = match crate::custom_toolbars::target(&app.ui, p) {
|
||||
Ok(t) => t,
|
||||
Err(e) => return Some(Err(e)),
|
||||
};
|
||||
let double = match toolbar.as_deref().and_then(|name| crate::custom_toolbars::find_mut(&mut app.ui, name)) {
|
||||
Some(b) => &mut b.double,
|
||||
None => &mut app.ui.toolbar_double,
|
||||
};
|
||||
opt_bool(p, "double").map(|on| {
|
||||
let on = on.unwrap_or(!*double);
|
||||
*double = on;
|
||||
json!(on)
|
||||
})
|
||||
}
|
||||
"window.toolbarAdvanced" => flag(&mut app.ui.toolbar_advanced),
|
||||
"imageTrace.preview" => opt_bool(p, "on").map(|on| {
|
||||
let on = on.unwrap_or(!app.ui.image_trace_preview);
|
||||
@@ -444,7 +455,11 @@ fn run_ui_part5(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<Result<
|
||||
Err(e) => return Some(Err(e)),
|
||||
};
|
||||
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)
|
||||
let toolbar = match crate::custom_toolbars::target(&app.ui, p) {
|
||||
Ok(t) => t,
|
||||
Err(e) => return Some(Err(e)),
|
||||
};
|
||||
crate::toolbar::float_group(app, &tool, floating, toolbar.as_deref()).map(Value::Bool)
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
@@ -482,13 +497,16 @@ fn run_ui_part6(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<Result<
|
||||
// The bars the last Tab hid show again, unless the toolbar or the Control bar was shown
|
||||
// another way since then (Window › Tools, Window › Control). Panels shown with Shift+Tab
|
||||
// in between stay shown. Switching workspaces clears the record (`workspaces::apply`).
|
||||
let hid = ui.tab_hid.take().filter(|h| (h.dock || h.toolbar || h.control_bar) && !ui.toolbar && !ui.control_bar);
|
||||
// A shown toolbar made with New Toolbar counts as a bar Tab hides, so a record with none
|
||||
// of the three bars is of the toolbars made with New Toolbar alone.
|
||||
let custom = ui.custom_toolbars.iter().any(|b| b.shown);
|
||||
let hid = ui.tab_hid.take().filter(|_| !ui.toolbar && !ui.control_bar);
|
||||
let on = if let Some(h) = hid {
|
||||
ui.dock |= h.dock;
|
||||
ui.toolbar = h.toolbar;
|
||||
ui.control_bar = h.control_bar;
|
||||
true
|
||||
} else if ui.dock || ui.toolbar || ui.control_bar {
|
||||
} else if ui.dock || ui.toolbar || ui.control_bar || custom {
|
||||
ui.tab_hid = Some(crate::state::TabHid { dock: ui.dock, toolbar: ui.toolbar, control_bar: ui.control_bar });
|
||||
ui.dock = false;
|
||||
ui.toolbar = false;
|
||||
|
||||
@@ -61,11 +61,22 @@ 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.toolbarColumns" => app.ui.toolbar_double,
|
||||
"window.editToolbar" => app.ui.tools_drawer,
|
||||
"window.toolbarColumns" => match p.get("toolbar").and_then(Value::as_str) {
|
||||
None => app.ui.toolbar_double,
|
||||
Some(name) => crate::custom_toolbars::find(&app.ui, name).is_some_and(|b| b.double),
|
||||
},
|
||||
"window.editToolbar" => {
|
||||
app.ui.tools_drawer
|
||||
&& crate::custom_toolbars::drawer_target(&app.ui).map(crate::custom_toolbars::key)
|
||||
== p.get("toolbar").and_then(Value::as_str).map(crate::custom_toolbars::key)
|
||||
}
|
||||
"window.toolbar.custom" => {
|
||||
p.get("name").and_then(Value::as_str).and_then(|name| crate::custom_toolbars::find(&app.ui, name)).is_some_and(|b| b.shown)
|
||||
}
|
||||
"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))
|
||||
let toolbar = p.get("toolbar").and_then(Value::as_str);
|
||||
crate::toolbar::strips(&app.ui, toolbar).iter().any(|f| f.tools.iter().any(|id| id == tool))
|
||||
}
|
||||
"window.taskBar" => app.ui.task_bar,
|
||||
"window.taskBar.pin" => app.ui.task_bar_place.pinned,
|
||||
|
||||
@@ -621,7 +621,7 @@ pub fn menu_tree_named(english_names: bool) -> Vec<(&'static str, Vec<Item>)> {
|
||||
c("Control", "window.control"),
|
||||
c("Contextual Task Bar", "window.taskBar"),
|
||||
c("Tools", "window.toolbar"),
|
||||
sub("Toolbars", vec![c("Advanced", "window.toolbarAdvanced"), c("Double Column", "window.toolbarColumns")]),
|
||||
sub("Toolbars", crate::custom_toolbars::menu_items()),
|
||||
Sep,
|
||||
];
|
||||
let lang = crate::i18n::current();
|
||||
|
||||
@@ -432,14 +432,14 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
||||
"window.toolbarColumns",
|
||||
"Toolbar: Single/Double Column",
|
||||
"",
|
||||
"{double?: bool} show the toolbar's tools in two columns (true), one (false) or toggle (omitted), as the double arrow at the top of the toolbar does; returns the new state",
|
||||
"{double?: bool, toolbar?: name} show the toolbar's tools (with `toolbar`, that toolbar made with New Toolbar's) in two columns (true), one (false) or toggle (omitted), as the double arrow at the top of the toolbar does; returns the new state",
|
||||
),
|
||||
("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",
|
||||
"{tool: id, floating?: bool, toolbar?: name} float the toolbar group holding `tool` (in the current Basic or Advanced layout; with `toolbar`, in that toolbar made with New Toolbar) 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",
|
||||
),
|
||||
(
|
||||
"toolbar.show",
|
||||
@@ -451,26 +451,39 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
||||
"window.editToolbar",
|
||||
"Edit Toolbar",
|
||||
"",
|
||||
"{open?: bool} open (true), close (false) or toggle (omitted) the All Tools drawer beside the toolbar, as the toolbar's ••• button does; while it is open, tools drag from it onto the toolbar and from the toolbar into it (Escape or a click outside closes it); returns the new state",
|
||||
"{open?: bool, toolbar?: name} open (true), close (false) or toggle (omitted) the All Tools drawer beside the toolbar (with `toolbar`, beside that toolbar made with New Toolbar, which shows), as the toolbar's ••• button does; while it is open, tools drag from it onto the toolbar and from the toolbar into it (Escape or a click outside closes it); returns the new state",
|
||||
),
|
||||
(
|
||||
"toolbar.addTool",
|
||||
"Toolbar: Add Tool",
|
||||
"",
|
||||
"{tool: id, slot?: n, place?: \"before\"|\"after\"|\"group\"} put `tool` in the current (Basic or Advanced) toolbar, moving it if it is there already: before (default) or after slot `slot` (0 = the first; omitted: at the end) in its own slot, or into that slot's group (its flyout), as dropping a tool from the Edit Toolbar drawer does; saved with the preferences; returns the toolbar's slots (lists of tool ids)",
|
||||
"{tool: id, slot?: n, place?: \"before\"|\"after\"|\"group\", toolbar?: name} put `tool` in the current (Basic or Advanced) toolbar (with `toolbar`, in that toolbar made with New Toolbar), moving it if it is there already: before (default) or after slot `slot` (0 = the first; omitted: at the end) in its own slot, or into that slot's group (its flyout), as dropping a tool from the Edit Toolbar drawer does; saved with the preferences; returns the toolbar's slots (lists of tool ids)",
|
||||
),
|
||||
(
|
||||
"toolbar.removeTool",
|
||||
"Toolbar: Remove Tool",
|
||||
"",
|
||||
"{tool: id, group?: bool} take `tool` (with `group`, its whole group) out of the current toolbar (one not in it stays out), as dragging it into the Edit Toolbar drawer does; returns the toolbar's slots",
|
||||
"{tool: id, group?: bool, toolbar?: name} take `tool` (with `group`, its whole group) out of the current toolbar (with `toolbar`, out of that toolbar made with New Toolbar; one not in it stays out), as dragging it into the Edit Toolbar drawer does; returns the toolbar's slots",
|
||||
),
|
||||
(
|
||||
"toolbar.reset",
|
||||
"Toolbar: Reset",
|
||||
"",
|
||||
"{} put the current (Basic or Advanced) toolbar back as it ships, undoing Edit Toolbar's changes; returns its slots",
|
||||
"{toolbar?: name} put the current (Basic or Advanced) toolbar back as it ships, undoing Edit Toolbar's changes (with `toolbar`, make that toolbar made with New Toolbar blank again); returns its slots",
|
||||
),
|
||||
(
|
||||
"window.toolbar.new",
|
||||
"New Toolbar…",
|
||||
"",
|
||||
"{name?: string} make a blank toolbar called `name`, of at most 200 characters (omitted: a dialog asks for the name), floating beside the Tools panel and listed under Window › Toolbars; its ••• button opens the All Tools drawer to fill it (window.editToolbar with `toolbar`); it is kept with the current workspace; returns {toolbar: name}",
|
||||
),
|
||||
(
|
||||
"window.toolbar.custom",
|
||||
"Show Toolbar",
|
||||
"",
|
||||
"{name: string, show?: bool} show (true), hide (false) or toggle (omitted) the toolbar made with New Toolbar called `name`, as its item under Window › Toolbars does (its title bar's × hides it); shown after Tab hid the toolbars, it shows alone and the others are turned off; returns the new state",
|
||||
),
|
||||
("window.toolbar.list", "List Toolbars", "", "{} the toolbars made with New Toolbar in the current workspace: [{name, shown, double, slots}]"),
|
||||
("window.taskBar", "Contextual Task Bar", "", "{}"),
|
||||
(
|
||||
"window.taskBar.pin",
|
||||
|
||||
@@ -124,6 +124,7 @@ pub fn sync(ui: &UiState) {
|
||||
GENERATION.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
crate::workspaces::sync(ui);
|
||||
crate::custom_toolbars::sync(ui);
|
||||
}
|
||||
|
||||
/// Registry default shortcut of a command (engine or UI command).
|
||||
|
||||
@@ -383,6 +383,13 @@ pub struct UiState {
|
||||
/// Edit Toolbar's All Tools drawer is open beside the toolbar (`window.editToolbar`).
|
||||
#[serde(skip)]
|
||||
pub tools_drawer: bool,
|
||||
/// The toolbars made with New Toolbar, in the order they were made.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub custom_toolbars: Vec<CustomToolbar>,
|
||||
/// The toolbar made with New Toolbar that the open All Tools drawer edits (none: the Tools
|
||||
/// panel's toolbar).
|
||||
#[serde(skip)]
|
||||
pub drawer_toolbar: Option<String>,
|
||||
/// Floating panel groups as files from before the canvas joined [`Self::docking`] saved
|
||||
/// them. Read only to convert those files ([`crate::layout_migration`]), never written.
|
||||
#[serde(default, skip_serializing)]
|
||||
@@ -657,6 +664,44 @@ pub struct FloatingFlyout {
|
||||
pub pos: [f32; 2],
|
||||
}
|
||||
|
||||
impl FloatingFlyout {
|
||||
/// Keep strips of known tools, one per group (a hand-edited preferences file).
|
||||
pub fn sanitize(strips: &mut Vec<FloatingFlyout>) {
|
||||
let mut seen = std::collections::BTreeSet::new();
|
||||
strips.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()))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// A toolbar made with Window › Toolbars › New Toolbar (`window.toolbar.new`): it floats on its
|
||||
/// own, filled with tools from the All Tools drawer, and is kept with the workspace it was made in.
|
||||
/// The groups torn off it are kept with it (`floating`), so the Tools panel's strips
|
||||
/// (`UiState::floating_flyouts`) hold none of them.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct CustomToolbar {
|
||||
/// The name it is listed under in Window › Toolbars.
|
||||
pub name: String,
|
||||
/// Its slots, each a tool group (the first tool is the slot's default).
|
||||
#[serde(default)]
|
||||
pub slots: Vec<Vec<String>>,
|
||||
/// It shows (Window › Toolbars › its name toggles it).
|
||||
#[serde(default = "yes")]
|
||||
pub shown: bool,
|
||||
/// Top-left corner in screen points.
|
||||
pub pos: [f32; 2],
|
||||
/// The tool each slot of several tools shows, chosen last (keyed by the slot's first tool).
|
||||
#[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
|
||||
pub slot_tool: std::collections::BTreeMap<String, String>,
|
||||
/// Its tools show in two columns (the double arrow on its title bar toggles it).
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
pub double: bool,
|
||||
/// The groups torn off it, each floating as its own strip; they show while it does.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub floating: Vec<FloatingFlyout>,
|
||||
}
|
||||
|
||||
/// A category of an edited toolbar: its name (one of the Basic toolbar's; none in the Advanced
|
||||
/// toolbar) and its slots, each a tool group (the first tool is the slot's default).
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
@@ -796,6 +841,8 @@ impl UiState {
|
||||
self.dialog = None;
|
||||
self.flyout = None;
|
||||
self.tools_drawer = false;
|
||||
self.drawer_toolbar = None;
|
||||
crate::custom_toolbars::sanitize(&mut self.custom_toolbars);
|
||||
self.palette_open = false;
|
||||
self.status.clear();
|
||||
self.about = false;
|
||||
@@ -803,12 +850,7 @@ 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()))
|
||||
});
|
||||
FloatingFlyout::sanitize(&mut self.floating_flyouts);
|
||||
sanitize_toolbar_pos(&mut self.toolbar_pos);
|
||||
crate::edit_toolbar::sanitize(&mut self.toolbar_edits);
|
||||
// `read_preferences` converted the file's older layouts already; this covers a state read
|
||||
@@ -863,6 +905,8 @@ impl Default for UiState {
|
||||
toolbar_edits: ToolbarEdits::default(),
|
||||
toolbar_show: ToolbarShow::default(),
|
||||
tools_drawer: false,
|
||||
custom_toolbars: vec![],
|
||||
drawer_toolbar: None,
|
||||
floating_panels: vec![],
|
||||
docking: None,
|
||||
docking_hidden: Default::default(),
|
||||
|
||||
+137
-49
@@ -15,7 +15,7 @@ use vectorcraft_tools::{Mods, TOOL_GROUPS, ToolInfo, ToolKey, tool_info};
|
||||
use crate::theme::{self, Tokens};
|
||||
use crate::{VectorcraftApp, icons, widgets};
|
||||
|
||||
const PITCH: f32 = 30.0;
|
||||
pub(crate) const PITCH: f32 = 30.0;
|
||||
const WIDTH: f32 = 48.0;
|
||||
/// Seconds a press on a tool group's button is held before its flyout opens.
|
||||
const LONG_PRESS: f64 = 0.35;
|
||||
@@ -92,6 +92,7 @@ pub fn remember(app: &mut VectorcraftApp, id: &str) {
|
||||
app.ui.slot_tool.insert(s[0].to_string(), id.to_string());
|
||||
}
|
||||
}
|
||||
crate::custom_toolbars::remember(&mut app.ui, id);
|
||||
}
|
||||
|
||||
pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
@@ -130,19 +131,34 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
}
|
||||
// Before the flyout: one torn off this frame floats from the next (its bar mustn't be in two
|
||||
// layers in one frame).
|
||||
floating(app, ui.ctx());
|
||||
floating(app, ui.ctx(), false);
|
||||
crate::dialogs::dimension::floating(app, ui.ctx());
|
||||
crate::custom_toolbars::show(app, ui.ctx());
|
||||
flyout(app, ui.ctx());
|
||||
crate::edit_toolbar::drawer(app, ui.ctx());
|
||||
}
|
||||
|
||||
/// The toolbars made with New Toolbar while the Tools panel is hidden (Window › Tools off): their
|
||||
/// flyouts and the groups torn off them, and the All Tools drawer beside the one it edits.
|
||||
pub fn show_custom(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
floating(app, ctx, true);
|
||||
crate::custom_toolbars::show(app, ctx);
|
||||
// A flyout of the hidden Tools panel closes with it.
|
||||
if ctx.data(|d| d.get_temp::<Option<String>>(flyout_toolbar_id())).flatten().is_none() {
|
||||
app.ui.flyout = None;
|
||||
}
|
||||
flyout(app, ctx);
|
||||
if crate::custom_toolbars::drawer_target(&app.ui).is_some() {
|
||||
crate::edit_toolbar::drawer(app, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
/// The Tools panel's contents, docked or floating at `floating`.
|
||||
fn body(app: &mut VectorcraftApp, ui: &mut Ui, cols: usize, floating: Option<egui::Pos2>) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let all = slots(app);
|
||||
let bounds = ui.max_rect();
|
||||
// The drawer opens beside it; a click in it leaves the drawer open.
|
||||
ui.data_mut(|d| d.insert_temp(crate::edit_toolbar::toolbar_id(), bounds));
|
||||
let editing = app.ui.tools_drawer && app.ui.drawer_toolbar.is_none();
|
||||
ui.spacing_mut().item_spacing = vec2(0.0, 0.0);
|
||||
// Title bar with » and a grip strip: a drag on either floats the panel or moves it.
|
||||
let (hdr, hresp) = ui.allocate_exact_size(vec2(ui.available_width(), 14.0), Sense::click_and_drag());
|
||||
@@ -181,9 +197,8 @@ fn body(app: &mut VectorcraftApp, ui: &mut Ui, cols: usize, floating: Option<egu
|
||||
ui.data_mut(|d| d.remove::<egui::Id>(held_id));
|
||||
}
|
||||
let held: Option<egui::Id> = ui.data(|d| d.get_temp(held_id));
|
||||
// While Edit Toolbar's drawer is open, the tools drag (to new places or into the drawer)
|
||||
// instead of opening their flyouts.
|
||||
let editing = app.ui.tools_drawer;
|
||||
// While Edit Toolbar's drawer edits this toolbar, the tools drag (to new places or into the
|
||||
// drawer) instead of opening their flyouts.
|
||||
let mut buttons: Vec<(usize, egui::Rect)> = vec![];
|
||||
let mut open_flyout: Option<(Vec<&'static str>, egui::Rect)> = None;
|
||||
// A group torn off into a floating flyout is raised instead of opening its flyout.
|
||||
@@ -244,7 +259,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut Ui, cols: usize, floating: Option<egu
|
||||
let press = if slot.len() > 1 && !editing { flyout_press(ui, &resp, rect) } else { None };
|
||||
let alt = ui.input(|inp| inp.modifiers.alt);
|
||||
if press.is_some() {
|
||||
if is_floating(app, slot[0]) {
|
||||
if is_floating(app, None, slot[0]) {
|
||||
raise = Some(slot[0]);
|
||||
} else {
|
||||
open_flyout = Some((slot.clone(), rect));
|
||||
@@ -270,26 +285,32 @@ fn body(app: &mut VectorcraftApp, ui: &mut Ui, cols: usize, floating: Option<egu
|
||||
i += row.len();
|
||||
}
|
||||
if editing {
|
||||
crate::edit_toolbar::toolbar_drop(app, ui, &buttons, cols, bounds);
|
||||
crate::edit_toolbar::toolbar_drop(app, ui, &buttons, cols, bounds, None);
|
||||
}
|
||||
if let Some(key) = raise {
|
||||
raise_floating(ui.ctx(), key);
|
||||
raise_floating(ui.ctx(), None, key);
|
||||
}
|
||||
if let Some((slot, rect)) = open_flyout {
|
||||
app.ui.flyout = Some(0);
|
||||
ui.data_mut(|d| {
|
||||
d.insert_temp(egui::Id::unique("flyout-anchor"), rect);
|
||||
d.insert_temp(egui::Id::unique("flyout-tools"), slot.iter().map(|s| s.to_string()).collect::<Vec<String>>());
|
||||
d.insert_temp(flyout_toolbar_id(), None::<String>);
|
||||
});
|
||||
}
|
||||
ui.add_space(8.0);
|
||||
bottom_controls(app, ui, &t, cols);
|
||||
});
|
||||
// While the drawer edits this toolbar (also from the ••• click just made), it opens beside it; a
|
||||
// click in it leaves the drawer open.
|
||||
if app.ui.tools_drawer && app.ui.drawer_toolbar.is_none() {
|
||||
ui.data_mut(|d| d.insert_temp(crate::edit_toolbar::toolbar_id(), bounds));
|
||||
}
|
||||
}
|
||||
|
||||
/// How a press on a tool group's button opens its flyout.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
enum FlyoutPress {
|
||||
pub(crate) enum FlyoutPress {
|
||||
/// A right press (it opens at once, before the release).
|
||||
Secondary,
|
||||
/// A primary press on the corner triangle, or one held [`LONG_PRESS`] seconds: its release
|
||||
@@ -300,7 +321,7 @@ enum FlyoutPress {
|
||||
/// Whether this frame's pointer state on a tool button opens its group's flyout. A primary press
|
||||
/// still short of a long one asks for the frame where it becomes long: egui draws only on input,
|
||||
/// so a still mouse would otherwise never get there.
|
||||
fn flyout_press(ui: &Ui, resp: &egui::Response, rect: egui::Rect) -> Option<FlyoutPress> {
|
||||
pub(crate) fn flyout_press(ui: &Ui, resp: &egui::Response, rect: egui::Rect) -> Option<FlyoutPress> {
|
||||
if resp.hovered() && ui.input(|inp| inp.pointer.button_pressed(egui::PointerButton::Secondary)) {
|
||||
return Some(FlyoutPress::Secondary);
|
||||
}
|
||||
@@ -543,11 +564,15 @@ fn bottom_controls(app: &mut VectorcraftApp, ui: &mut Ui, t: &Tokens, cols: usiz
|
||||
app.run("view.screenMode", json!({})).ok();
|
||||
}
|
||||
// Edit Toolbar: the All Tools drawer opens beside the toolbar, by this button.
|
||||
let edit = widgets::icon_button(ui, "ellipsis", "Edit Toolbar", app.ui.tools_drawer, 26.0);
|
||||
ui.data_mut(|d| d.insert_temp(crate::edit_toolbar::button_id(), edit.rect));
|
||||
let editing = |app: &VectorcraftApp| app.ui.tools_drawer && app.ui.drawer_toolbar.is_none();
|
||||
let edit = widgets::icon_button(ui, "ellipsis", "Edit Toolbar", editing(app), 26.0);
|
||||
if edit.clicked() {
|
||||
app.run("window.editToolbar", json!({})).ok();
|
||||
}
|
||||
// After the click, which may have opened the drawer here.
|
||||
if editing(app) {
|
||||
ui.data_mut(|d| d.insert_temp(crate::edit_toolbar::button_id(), edit.rect));
|
||||
}
|
||||
});
|
||||
let _ = t;
|
||||
}
|
||||
@@ -558,38 +583,72 @@ 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 {
|
||||
/// strip's × do (`window.floatTools`; `toolbar`: the group of the toolbar made with New Toolbar
|
||||
/// called that). Returns the new state.
|
||||
pub fn float_group(app: &mut VectorcraftApp, tool: &str, floating: Option<bool>, toolbar: Option<&str>) -> Result<bool, String> {
|
||||
let groups: Vec<Vec<&'static str>> = match toolbar {
|
||||
None => slots(app).into_iter().map(|(_, s)| s).collect(),
|
||||
Some(name) => crate::custom_toolbars::find(&app.ui, name)
|
||||
.map(|b| b.slots.iter().map(|s| s.iter().filter_map(|id| tool_info(id).map(|t| t.id)).collect()).collect())
|
||||
.unwrap_or_default(),
|
||||
};
|
||||
let Some(group) = groups.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 was = is_floating(app, toolbar, key);
|
||||
let on = floating.unwrap_or(!was);
|
||||
let Some(strips) = strips_mut(&mut app.ui, toolbar) else { return Err(format!("no toolbar named `{}`", toolbar.unwrap_or_default())) };
|
||||
if on && !was {
|
||||
let n = app.ui.floating_flyouts.len() as f32;
|
||||
let n = strips.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)] });
|
||||
strips.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));
|
||||
strips.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))
|
||||
/// The groups torn off the Tools panel's toolbar (`toolbar` none) or off the toolbar made with New
|
||||
/// Toolbar called `toolbar` (none when there is no such toolbar).
|
||||
pub(crate) fn strips<'a>(ui: &'a crate::state::UiState, toolbar: Option<&str>) -> &'a [crate::state::FloatingFlyout] {
|
||||
match toolbar {
|
||||
None => &ui.floating_flyouts,
|
||||
Some(name) => crate::custom_toolbars::find(ui, name).map_or(&[], |b| b.floating.as_slice()),
|
||||
}
|
||||
}
|
||||
|
||||
/// The area of the floating flyout of the group whose first tool is `key`.
|
||||
fn strips_mut<'a>(ui: &'a mut crate::state::UiState, toolbar: Option<&str>) -> Option<&'a mut Vec<crate::state::FloatingFlyout>> {
|
||||
match toolbar {
|
||||
None => Some(&mut ui.floating_flyouts),
|
||||
Some(name) => crate::custom_toolbars::find_mut(ui, name).map(|b| &mut b.floating),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the group whose first tool is `key`, of the Tools panel's toolbar (`toolbar` none) or of
|
||||
/// the toolbar made with New Toolbar called `toolbar`, floats as its own panel.
|
||||
pub(crate) fn is_floating(app: &VectorcraftApp, toolbar: Option<&str>, key: &str) -> bool {
|
||||
strips(&app.ui, toolbar).iter().any(|f| f.tools.first().is_some_and(|k| k == key))
|
||||
}
|
||||
|
||||
/// The area of the floating flyout of the Tools panel's group whose first tool is `key`.
|
||||
fn floating_area(key: &str) -> egui::Id {
|
||||
egui::Id::unique(("tool-floating", key))
|
||||
}
|
||||
|
||||
/// The area of the floating flyout of the group whose first tool is `key`, torn off the toolbar
|
||||
/// made with New Toolbar called `toolbar` (none: the Tools panel's).
|
||||
fn strip_area(toolbar: Option<&str>, key: &str) -> egui::Id {
|
||||
match toolbar {
|
||||
None => floating_area(key),
|
||||
Some(name) => floating_area(key).with(crate::custom_toolbars::key(name)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Bring a floating flyout to the front and flash its border.
|
||||
fn raise_floating(ctx: &egui::Context, key: &str) {
|
||||
let id = floating_area(key);
|
||||
pub(crate) fn raise_floating(ctx: &egui::Context, toolbar: Option<&str>, key: &str) {
|
||||
let id = strip_area(toolbar, key);
|
||||
ctx.move_to_top(egui::LayerId::new(egui::Order::Middle, id));
|
||||
let now = ctx.input(|i| i.time);
|
||||
ctx.data_mut(|d| d.insert_temp(id, now));
|
||||
@@ -616,10 +675,12 @@ 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.
|
||||
fn flyout_body(ui: &mut Ui, t: &Tokens, active: &str, tools: &[String], floating: bool) -> FlyoutInput {
|
||||
/// that tears a flyout off carries on moving the floating flyout it becomes (`toolbar`: the toolbar
|
||||
/// made with New Toolbar the group is in, none for the Tools panel's).
|
||||
fn flyout_body(ui: &mut Ui, t: &Tokens, active: &str, tools: &[String], floating: bool, toolbar: Option<&str>) -> FlyoutInput {
|
||||
let mut out = FlyoutInput::default();
|
||||
let key = tools.first().map_or("", String::as_str);
|
||||
let bar_id = bar_id(toolbar, key);
|
||||
ui.spacing_mut().item_spacing = vec2(0.0, 0.0);
|
||||
ui.horizontal(|ui| {
|
||||
let rows = if floating { ui.horizontal(|ui| tool_strip(ui, t, active, tools)) } else { ui.vertical(|ui| tool_rows(ui, t, active, tools)) };
|
||||
@@ -628,7 +689,7 @@ fn flyout_body(ui: &mut Ui, t: &Tokens, active: &str, tools: &[String], floating
|
||||
ui.painter().rect_filled(bar, 0.0, t.tab_strip);
|
||||
let grip = if floating {
|
||||
let x = egui::Rect::from_min_size(bar.min, vec2(BAR, CLOSE));
|
||||
let xr = ui.interact(x, egui::Id::unique(("tool-flyout-close", key)), Sense::click());
|
||||
let xr = ui.interact(x, bar_id.with("close"), Sense::click());
|
||||
icons::paint(ui, "x", x.shrink2(vec2(1.0, 3.0)), if xr.hovered() { t.text_strong } else { t.text });
|
||||
xr.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, ui.is_enabled(), tl!("Put back in the toolbar")));
|
||||
out.close = xr.on_hover_text(tl!("Put back in the toolbar")).clicked();
|
||||
@@ -637,7 +698,7 @@ fn flyout_body(ui: &mut Ui, t: &Tokens, active: &str, tools: &[String], floating
|
||||
bar
|
||||
};
|
||||
// Not focusable (`Sense::drag()` is): only the pointer drags the bar, so Tab does not stop on it.
|
||||
let resp = ui.interact(grip, bar_id(key), Sense::DRAG);
|
||||
let resp = ui.interact(grip, bar_id, Sense::DRAG);
|
||||
let half = (grip.height() / 2.0 - 3.0).clamp(0.0, 9.0);
|
||||
for k in 0..3 {
|
||||
let x = grip.center().x - 2.0 + k as f32 * 2.0;
|
||||
@@ -713,14 +774,19 @@ fn rows_width<'a>(rows: impl Iterator<Item = &'a RowText>) -> f32 {
|
||||
}
|
||||
|
||||
/// The grab bar of the group whose first tool is `key` (one id, flyout or floating, so a drag
|
||||
/// that tears the flyout off carries on moving it).
|
||||
fn bar_id(key: &str) -> egui::Id {
|
||||
egui::Id::unique(("tool-flyout-bar", key))
|
||||
/// that tears the flyout off carries on moving it), in the toolbar made with New Toolbar called
|
||||
/// `toolbar` (none: the Tools panel's).
|
||||
fn bar_id(toolbar: Option<&str>, key: &str) -> egui::Id {
|
||||
let id = egui::Id::unique(("tool-flyout-bar", key));
|
||||
match toolbar {
|
||||
None => id,
|
||||
Some(name) => id.with(crate::custom_toolbars::key(name)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Where the pointer holds a floating flyout's bar, from the flyout's corner, while it's dragged.
|
||||
fn grab_id(key: &str) -> egui::Id {
|
||||
bar_id(key).with("grab")
|
||||
fn grab_id(toolbar: Option<&str>, key: &str) -> egui::Id {
|
||||
bar_id(toolbar, key).with("grab")
|
||||
}
|
||||
|
||||
/// One icon button per tool, the active one lit, with its name and shortcut on hover. Returns the
|
||||
@@ -768,11 +834,12 @@ fn flyout(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
ctx.data(|d| d.get_temp(egui::Id::unique("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();
|
||||
let toolbar = ctx.data(|d| d.get_temp::<Option<String>>(flyout_toolbar_id())).flatten();
|
||||
let mut input = FlyoutInput::default();
|
||||
let resp = egui::Area::new(egui::Id::unique("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, toolbar.as_deref()));
|
||||
},
|
||||
);
|
||||
// Dragging the bar tears the flyout off, and so does releasing a press over it (a click, or the
|
||||
@@ -787,9 +854,12 @@ fn flyout(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
let grab = vec2(1.0 + strip_width(&tools) - BAR / 2.0, 1.0 + CLOSE + 4.0);
|
||||
let p = at - grab;
|
||||
if let Some(key) = tools.first() {
|
||||
ctx.data_mut(|m| m.insert_temp(grab_id(key), grab));
|
||||
ctx.data_mut(|m| m.insert_temp(grab_id(toolbar.as_deref(), key), grab));
|
||||
}
|
||||
// A toolbar made with New Toolbar that has gone since its flyout opened keeps no strip.
|
||||
if let Some(strips) = strips_mut(&mut app.ui, toolbar.as_deref()) {
|
||||
strips.push(crate::state::FloatingFlyout { tools, pos: [p.x, p.y] });
|
||||
}
|
||||
app.ui.floating_flyouts.push(crate::state::FloatingFlyout { tools, pos: [p.x, p.y] });
|
||||
app.ui.flyout = None;
|
||||
return;
|
||||
}
|
||||
@@ -803,20 +873,38 @@ fn flyout(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
let _ = theme::semibold;
|
||||
}
|
||||
|
||||
/// The toolbar made with New Toolbar whose group the open flyout shows (none: the Tools panel's).
|
||||
pub(crate) fn flyout_toolbar_id() -> egui::Id {
|
||||
egui::Id::unique("flyout-toolbar")
|
||||
}
|
||||
|
||||
/// 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;
|
||||
/// `custom_only`: the Tools panel is hidden, and only those torn off toolbars made with New Toolbar
|
||||
/// show.
|
||||
fn floating(app: &mut VectorcraftApp, ctx: &egui::Context, custom_only: bool) {
|
||||
// A group torn off a toolbar made with New Toolbar shows while that toolbar does.
|
||||
let owners: Vec<Option<String>> = (!custom_only)
|
||||
.then_some(None)
|
||||
.into_iter()
|
||||
.chain(app.ui.custom_toolbars.iter().filter(|b| b.shown && app.ui.tab_hid.is_none() && !b.floating.is_empty()).map(|b| Some(b.name.clone())))
|
||||
.collect();
|
||||
for owner in owners {
|
||||
floating_strips(app, ctx, owner.as_deref());
|
||||
}
|
||||
}
|
||||
|
||||
/// The strips torn off one toolbar (`toolbar` none: the Tools panel's).
|
||||
fn floating_strips(app: &mut VectorcraftApp, ctx: &egui::Context, toolbar: Option<&str>) {
|
||||
let t = Tokens::get(ctx);
|
||||
let screen = ctx.content_rect();
|
||||
let now = ctx.input(|i| i.time);
|
||||
let active = app.session.tool_id();
|
||||
let mut chosen = None;
|
||||
let mut closed = None;
|
||||
for (k, f) in app.ui.floating_flyouts.iter_mut().enumerate() {
|
||||
let Some(strips) = strips_mut(&mut app.ui, toolbar) else { return };
|
||||
for (k, f) in strips.iter_mut().enumerate() {
|
||||
let Some(key) = f.tools.first() else { continue };
|
||||
let id = floating_area(key);
|
||||
let id = strip_area(toolbar, key);
|
||||
let since = ctx.data(|d| d.get_temp::<f64>(id)).map(|t0| now - t0);
|
||||
let flash = since.is_some_and(|s| (0.0..FLASH).contains(&s));
|
||||
if let Some(s) = since.filter(|_| flash) {
|
||||
@@ -827,12 +915,12 @@ fn floating(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
let pos = crate::tools_float::clamp(f.pos, size, screen);
|
||||
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, toolbar));
|
||||
});
|
||||
// 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 grab = grab_id(toolbar, key);
|
||||
if ctx.is_being_dragged(bar_id(toolbar, key))
|
||||
&& let Some(at) = ctx.input(|i| i.pointer.interact_pos())
|
||||
{
|
||||
let off = ctx.data_mut(|m| *m.get_temp_mut_or_insert_with(grab, || at - pos));
|
||||
@@ -848,8 +936,8 @@ fn floating(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
}
|
||||
chosen = chosen.or(input.chosen);
|
||||
}
|
||||
if let Some(k) = closed.filter(|k| *k < app.ui.floating_flyouts.len()) {
|
||||
app.ui.floating_flyouts.remove(k);
|
||||
if let Some(k) = closed.filter(|k| *k < strips.len()) {
|
||||
strips.remove(k);
|
||||
}
|
||||
if let Some(id) = chosen {
|
||||
app.select_tool(id);
|
||||
|
||||
@@ -1037,9 +1037,9 @@ fn tab_skips_the_grip_and_the_tear_off_bars() {
|
||||
assert!(!buttons.is_empty(), "the Tools panel's buttons are drawn");
|
||||
let stops = crate::widgets::accessibility_tests::tab_stops(&ctx, 120, frame);
|
||||
assert!(stops.iter().any(|id| buttons.contains(id)), "Tab reaches the Tools panel's buttons: {stops:?}");
|
||||
let close = egui::Id::unique(("tool-flyout-close", "polygon"));
|
||||
let close = bar_id(None, "polygon").with("close");
|
||||
assert!(stops.contains(&close), "Tab reaches the strip's Put back in the toolbar button: {stops:?}");
|
||||
for (name, id) in [("the grip", grip), ("the flyout's bar", bar_id("rectangle")), ("the strip's bar", bar_id("polygon"))] {
|
||||
for (name, id) in [("the grip", grip), ("the flyout's bar", bar_id(None, "rectangle")), ("the strip's bar", bar_id(None, "polygon"))] {
|
||||
assert!(!stops.contains(&id), "Tab stops on {name}: {stops:?}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ use std::sync::atomic::Ordering;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::state::{Dialog, DockTab, FloatingFlyout, FloatingPanels, UiState};
|
||||
use crate::state::{CustomToolbar, Dialog, DockTab, FloatingFlyout, FloatingPanels, UiState};
|
||||
use crate::theme::Tokens;
|
||||
use crate::{VectorcraftApp, widgets};
|
||||
|
||||
@@ -57,6 +57,9 @@ pub struct Workspace {
|
||||
/// Main dock column width in screen points while `docking` is none (#1753).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub dock_width: Option<f32>,
|
||||
/// The toolbars made with New Toolbar in this workspace (built-in workspaces: none).
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub toolbars: Vec<CustomToolbar>,
|
||||
}
|
||||
|
||||
impl Default for Workspace {
|
||||
@@ -142,7 +145,8 @@ pub fn builtins() -> Vec<Workspace> {
|
||||
|
||||
/// Preferences › Devices › Enable Touch Workspace (#394): on, a touch screen switches to the Touch
|
||||
/// workspace; off, or when no touch screen is reported, the workspace before that switch is
|
||||
/// restored if Touch was entered automatically.
|
||||
/// restored if Touch was entered automatically. Each switch keeps the layout it leaves and brings
|
||||
/// back the one it returns to as it was left, as Window › Workspace does.
|
||||
pub fn sync_touch_workspace(app: &mut VectorcraftApp, touch_screen: bool) {
|
||||
let auto = app.session.prefs.touch_workspace && touch_screen;
|
||||
if auto {
|
||||
@@ -150,15 +154,13 @@ pub fn sync_touch_workspace(app: &mut VectorcraftApp, touch_screen: bool) {
|
||||
if app.ui.touch_workspace_restore.is_none() {
|
||||
app.ui.touch_workspace_restore = Some(app.ui.workspace.clone());
|
||||
}
|
||||
if let Some(w) = find(&app.ui, TOUCH) {
|
||||
apply(&mut app.ui, &w);
|
||||
}
|
||||
// Touch is built in, so the switch finds it.
|
||||
let _ = switch(&mut app.ui, TOUCH);
|
||||
}
|
||||
} else if app.ui.touch_workspace_restore.is_some() && app.ui.workspace == TOUCH {
|
||||
let back = app.ui.touch_workspace_restore.take().filter(|n| find(&app.ui, n).is_some()).unwrap_or_else(|| ESSENTIALS.to_string());
|
||||
if let Some(w) = find(&app.ui, &back) {
|
||||
apply(&mut app.ui, &w);
|
||||
}
|
||||
// `back` names a workspace that exists (or Essentials), so the switch finds it.
|
||||
let _ = switch(&mut app.ui, &back);
|
||||
} else if !auto {
|
||||
app.ui.touch_workspace_restore = None;
|
||||
}
|
||||
@@ -192,6 +194,7 @@ pub fn capture(ui: &UiState, name: &str) -> Workspace {
|
||||
docking_collapsed: Default::default(),
|
||||
toolbar_pos: ui.toolbar_pos,
|
||||
dock_width: ui.dock_width,
|
||||
toolbars: ui.custom_toolbars.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,6 +220,12 @@ pub fn apply(ui: &mut UiState, w: &Workspace) {
|
||||
ui.toolbar_pos = w.toolbar_pos;
|
||||
ui.dock_width = w.dock_width;
|
||||
crate::state::sanitize_toolbar_pos(&mut ui.toolbar_pos);
|
||||
ui.custom_toolbars = w.toolbars.clone();
|
||||
crate::custom_toolbars::sanitize(&mut ui.custom_toolbars);
|
||||
// The drawer edits a toolbar of the workspace left: it closes.
|
||||
if ui.drawer_toolbar.take().is_some() {
|
||||
ui.tools_drawer = false;
|
||||
}
|
||||
ui.flyout = None;
|
||||
ui.workspace = w.name.clone();
|
||||
}
|
||||
@@ -246,10 +255,26 @@ pub fn switch(ui: &mut UiState, name: &str) -> Result<String, String> {
|
||||
pub fn reset(ui: &mut UiState) -> String {
|
||||
let w = find_saved(ui, &ui.workspace.clone()).unwrap_or_default();
|
||||
ui.workspace_changes.retain(|x| !x.name.eq_ignore_ascii_case(&w.name));
|
||||
// The toolbars made with New Toolbar stay in the workspace they were made in.
|
||||
let toolbars = std::mem::take(&mut ui.custom_toolbars);
|
||||
apply(ui, &w);
|
||||
ui.custom_toolbars = toolbars;
|
||||
w.name
|
||||
}
|
||||
|
||||
/// Keep the toolbars made with New Toolbar in the current workspace's changes, and nothing else of
|
||||
/// its layout that its changes do not already hold.
|
||||
fn keep_toolbars(ui: &mut UiState) {
|
||||
let Some(saved) = find_saved(ui, &ui.workspace.clone()) else { return };
|
||||
if let Some(w) = ui.workspace_changes.iter_mut().find(|x| x.name.eq_ignore_ascii_case(&saved.name)) {
|
||||
w.toolbars = ui.custom_toolbars.clone();
|
||||
} else if saved.toolbars != ui.custom_toolbars {
|
||||
ui.workspace_changes.push(Workspace { toolbars: ui.custom_toolbars.clone(), ..saved });
|
||||
let over = ui.workspace_changes.len().saturating_sub(64);
|
||||
ui.workspace_changes.drain(..over);
|
||||
}
|
||||
}
|
||||
|
||||
/// A workspace by name as saved (user workspaces shadow nothing: names are unique across both lists).
|
||||
pub fn find_saved(ui: &UiState, name: &str) -> Option<Workspace> {
|
||||
ui.custom_workspaces
|
||||
@@ -279,7 +304,11 @@ pub fn save_as(ui: &mut UiState, name: &str) -> Result<(), String> {
|
||||
return Err(format!("`{name}` is a built-in workspace; choose another name"));
|
||||
}
|
||||
// The changes go into the workspace saved, which starts with none; the one left keeps the
|
||||
// changes it had (none of these).
|
||||
// changes it had (none of these), and the toolbars made with New Toolbar, which the workspace
|
||||
// saved starts with a copy of.
|
||||
if !ui.workspace.eq_ignore_ascii_case(name) {
|
||||
keep_toolbars(ui);
|
||||
}
|
||||
// A user workspace of that name in another case is replaced, as `find_saved` would find it.
|
||||
let w = capture(ui, name);
|
||||
match ui.custom_workspaces.iter_mut().find(|x| x.name.eq_ignore_ascii_case(name)) {
|
||||
|
||||
@@ -363,6 +363,22 @@ of `fillStroke`, `coloring`, `drawMode`, `screenMode`) shows or hides the contro
|
||||
saved with the preferences (`ui.toolbar_show`). The toolbar names its groups only with Preferences › User
|
||||
Interface › Show Tool Group Labels, off as it ships (#1736).
|
||||
|
||||
New Toolbar: Window › Toolbars › New Toolbar… (or New Toolbar… in the drawer's ☰ menu) asks for a name, and
|
||||
`window.toolbar.new {name}` makes the toolbar at once: a blank toolbar that floats on its own, moved by its title
|
||||
bar; its tools scroll when the window is too short for them. The title bar's × hides it, and its double arrow
|
||||
(`window.toolbarColumns {toolbar, double?}`) shows its tools in one column or two. Window › Toolbars lists each
|
||||
toolbar by its name, and its item (`window.toolbar.custom {name, show?}`) shows or hides it. Window › Tools hides
|
||||
the Tools panel alone, and Tab hides these toolbars with the other panels. Window › Tools after Tab shows the Tools
|
||||
panel without them. A toolbar shown with its item or made with New Toolbar after Tab shows alone, and the other
|
||||
toolbars Tab hid are turned off. A toolbar's ••• button
|
||||
(`window.editToolbar {toolbar}`) opens the All Tools drawer beside it, and the drawer's drags and
|
||||
`toolbar.addTool`, `toolbar.removeTool` and `toolbar.reset` with `toolbar` edit it; the drawer has no Show section
|
||||
there. `window.floatTools {tool, toolbar}` floats one of its groups, and a group torn off it shows and hides with
|
||||
it. `window.toolbar.list` returns the toolbars (`ui.custom_toolbars` in `ui.inspect`: name, slots, shown flag,
|
||||
two-column flag, top-left corner, the groups torn off it). They are kept with the workspace they were made in:
|
||||
switching workspaces shows that workspace's toolbars, Reset Workspace keeps them, and New Workspace starts the new
|
||||
workspace with a copy of them.
|
||||
|
||||
The Contextual Task Bar: its handle (the grip at its left end) drags it anywhere on the canvas, which always holds
|
||||
it whole, also when the window shrinks. Unpinned, the bar keeps that offset as it follows the selection, until
|
||||
another document becomes active. Its More Options (…) menu has Hide Bar (`window.taskBar`); Pin Bar Position
|
||||
|
||||
+3
-2
@@ -570,8 +570,9 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall
|
||||
- **Done:** custom document ruler origin (#1212): corner drag with preview, double-click reset to the active artboard, relative X/Y inputs and native save/reopen.
|
||||
- **Done:** View › Rulers › Change to Global / Artboard Rulers (`view.globalRulers`, Cmd+Alt+R): artboard rulers, a new document's, count from the active artboard, each artboard with its own zero point; rulers, X/Y fields and guide readouts follow the mode, which is saved with the document. Estimate unchanged.
|
||||
- **Done (#2849):** View › Rulers › Show Video Rulers (`view.videoRulers`): the rulers show and count the active artboard in video pixels from its top-left corner, a pixel as many points wide as the artboard's Video Ruler Pixel Aspect Ratio and one point tall ([public source](https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/organize-manage-artboards/set-video-display-options-for-artboards.html)); Hide Video Rulers returns them to the General unit and the ruler origin. Shift-snapping a new guide to ruler ticks still uses the General unit's ticks. Estimate unchanged.
|
||||
- **Missing (noted for reporters, 2026-10-14):** custom toolbars (Window › Toolbars › New Toolbar, #1759);
|
||||
panel widths kept with a workspace (#1753). Inside the estimate below.
|
||||
- **Partial:** custom toolbars (#1759): Window › Toolbars › New Toolbar makes them, kept with the workspace they
|
||||
were made in; Manage Toolbars (rename, copy, delete) is missing. Inside the estimate below.
|
||||
- **Done (#1753):** a saved workspace keeps the width of the dock column.
|
||||
- **Done (#4135):** floating panels open in OS windows of their own on the desktop, so they can sit on another monitor; on the web and on Wayland they float inside the window.
|
||||
- **Done:** View › New View… and Edit Views… ([public source](https://helpx.adobe.com/illustrator/using/viewing-artwork.html)): a document keeps up to 25 named views (zoom, centre and rotation), saved with the file; every one is listed by name at the bottom of the View menu (`view.goto1`–`view.goto25`), and choosing one changes the current window to it. Edit Views… lists them to rename or delete, applied together on OK as one undo step (`view.saved.edit {edits}`); a file's views are checked when it opens. Estimate unchanged.
|
||||
- **Done:** Window › Arrange › Tile (`window.arrange.tile`) shows every open document in its own pane, edge to edge in the document area, and Consolidate All Windows (`window.arrange.consolidateAllWindows`) goes back to one document under its tab, on the desktop and the web. Estimate unchanged.
|
||||
|
||||
@@ -596,6 +596,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- Screen readers get the value of the color and channel sliders that panels and dialogs draw (the Color panel, the Color Picker, Swatch Options, the Navigator's Zoom, the Character panel's variable font axes, the Perspective Grid and flattener options, and the sliders of the effect, tool option and transform dialogs): the number and text of the field beside each one, such as 128 for G or 50% for an opacity, with the slider's range, which extends to include a number past the slider's end. The harmony wheel's Brightness slider, which has no field, reports its brightness in percent.
|
||||
- Panels undock (#495): a dock tab, a panel icon or a popped-out panel's title dragged out of the dock floats that panel (in an OS window of its own on the desktop, #4135), and the strip right of the dock's tabs floats the whole tabbed group. A floating group moves by its title bar, a tab dragged out of it floats on its own, and dropped on another group's title bar or tabs it stacks with that group; dropped on a dock group's tabs, which light up under the pointer, its panels join that group, and on the group's upper or lower half they make a new group above or below it (`window.panel.dock {anchor, zone}`); dropped on the icon column they become icons there, the tabbed group's panels too (`window.panel.dock {icon: true}`); dropped anywhere else on the dock, which lights up, they go back where they live (the tabbed group or the icon column), as its × and the panel menus' Float Panel / Dock Panel do (`window.panel.float`, `window.panel.dock`). The Tools panel floats by its title bar and docks again on the window's left edge. Floating groups stack into sets: dropped on the upper or lower half of another group (a line shows where), a group joins it right above or below it, and the set's groups stack top to bottom, each showing one panel, moving together by the top group's title bar, whose × docks them all; a group dragged by its tab strip leaves the set (`window.panel.float {below, above}`). A double-click on a tab collapses a floating group to its tabs (`collapsed`), and a dock group too, a group below it moving up into the room; a second double-click, or a click on another of its tabs, expands it (`window.panel.collapse`, kept with the preferences and in saved workspaces). Sets dropped on the left edge of the icon column or of another dock column dock as dock columns (a multi-column dock, `window.panel.dock {column}`), whose groups stack, tab and tear out as a floating set's do, and whose top strip floats the column again. Floating panels, their sets and the Tools panel's position are kept with the preferences and in user workspaces, and clamped into a smaller window. Not done: panels in separate OS windows, reordering tabs by dragging, collapsing a set or dock column to icons, dock columns right of the default dock, resizing floating panels and the groups of a set.
|
||||
- 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.
|
||||
- Window › Toolbars › New Toolbar… and New Toolbar… in the All Tools drawer's menu ask for a name and make a blank toolbar that floats on its own, moved by its title bar and listed under Window › Toolbars, whose item shows or hides it. The title bar's × hides it, and its double arrow shows its tools in one column or two. Its ••• button opens the All Tools drawer beside it, and tools dragged from the drawer fill it. The toolbars are kept with the workspace they were made in: switching workspaces shows that workspace's toolbars, Reset Workspace keeps them, and New Workspace starts the new workspace with a copy of them (<https://helpx.adobe.com/illustrator/using/tools.html>; `window.toolbar.new`, `window.toolbar.custom`, the `toolbar` param of `window.editToolbar` and `toolbar.addTool`/`removeTool`/`reset`; `tools_added_to_a_new_toolbar_survive_a_workspace_reload`). Manage Toolbars is not done.
|
||||
- Czech render-site labels: desktop/web file filters, diagnostics, suggested preset names and contributor tooltips/plurals now use catalog lookups; gradients have Czech adjective agreement and printer marks have a separate context (PDF/Print and Summary). Search, Home, toolbar/panel tooltips and Preferences already look up their labels. Every complete catalog has rows for these labels and for the Roman and Japanese printer mark styles. Evidence: `i18n::tests::czech_render_site_follow_up`, `i18n::tests::complete_catalogs_translate_render_site_labels`, `dialogs::tests_modal::printer_mark_labels_are_distinct_from_language_names`; see [localization-parity.md](localization-parity.md).
|
||||
- The History panel, Edit › Undo and Edit › Redo translate the names of the steps that apply or redefine a character or paragraph style and of the step that redefines a graphic style: each such step keeps its template (`Apply {name}`, `Redefine {name}`, `Redefine Graphic Style “{name}”`) and the style's name, every complete catalog has a row for each template, and the panel and the menu show the style's name as the document holds it, except the Normal styles' names, which they show in the interface language (`steps_named_after_a_style_list_the_template_around_the_name`, `undo_and_redo_name_a_style_step_around_the_style_name`).
|
||||
- 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, Korean, Vietnamese, German, Spanish, French, Italian, Polish, Brazilian Portuguese, Russian, Ukrainian, Georgian and Catalan ship complete, and Czech covers every menu label (and more), with system-locale detection on macOS, Windows, Linux and the web (the browser's languages, or `?lang=`), a Language preference and, on macOS, a VectorCraft ▸ Language menu. Names are never translated (fonts and their styles, layers, artboards, saved presets, user libraries, custom workspaces, recent files, plug-ins).
|
||||
|
||||
+2
-1
@@ -290,7 +290,8 @@ next unit in the order a right-click on a ruler lists them, from Feet back to Po
|
||||
| Panels dragged onto a dock group's tabs join it; onto its upper or lower half they make a new group above or below it (a top/bottom split, resizable); onto the icon column they become icons, the tabbed group's panels too; a dock tab's context menu closes it (Window menu reopens it) | done | `window.panel.dock {anchor, zone}`, `window.panel.dock {icon: true}`, `window.panel.close` |
|
||||
| A double-click on a dock group's tab collapses the group to its tabs (a group below it moves up into the room) and a second one expands it; a click on another of its tabs expands it showing that panel ([helpx: Workspace basics](https://helpx.adobe.com/illustrator/using/workspace-basics.html), panel groups) | done | `window.panel.collapse {panel, collapsed?}`; in dock groups and floating windows; a group beside another (a left/right split) doesn't collapse |
|
||||
| Panels outside the app window (another monitor) | done | floating groups in OS windows of their own on the desktop (#4135) |
|
||||
| Edit Toolbar (•••): the All Tools drawer opens beside the toolbar by its button (Escape or a click outside closes it), dims the tools already in the toolbar; while it is open, tools drag from it onto the toolbar (between two tools, or onto one to join its group), from the toolbar into it, and within the toolbar; each toolbar keeps its edits | done | #1018; `window.editToolbar`, `toolbar.addTool`/`removeTool`/`reset`; the drawer's Reset (Illustrator has it in a drawer menu); dragging a toolbar button moves the tool it shows, not its whole group; New/Manage Toolbars missing; tool flyouts are as wide as their longest translated name and shortcut need (#1616) |
|
||||
| Edit Toolbar (•••): the All Tools drawer opens beside the toolbar by its button (Escape or a click outside closes it), dims the tools already in the toolbar; while it is open, tools drag from it onto the toolbar (between two tools, or onto one to join its group), from the toolbar into it, and within the toolbar; each toolbar keeps its edits | done | #1018; `window.editToolbar`, `toolbar.addTool`/`removeTool`/`reset`; the drawer's Reset (Illustrator has it in a drawer menu); dragging a toolbar button moves the tool it shows, not its whole group; Manage Toolbars missing; tool flyouts are as wide as their longest translated name and shortcut need (#1616) |
|
||||
| Window › Toolbars › New Toolbar… (also in the All Tools drawer's menu): a name and OK make a blank floating toolbar, listed under Window › Toolbars (its item shows or hides it); its title bar moves it, its × hides it and its double arrow shows its tools in one column or two; its ••• button opens the All Tools drawer beside it to fill it; kept with the workspace it was made in | done | #1759 in part; `window.toolbar.new`, `window.toolbar.custom`, `window.toolbar.list`, the `toolbar` param of `window.toolbarColumns`, `window.editToolbar` and `toolbar.addTool`/`removeTool`/`reset` |
|
||||
| Quick Image Trace presets trace at once and leave the panel as it was; the Image Trace button (Control bar, Properties) opens the editable controls | done | Per the public documentation, choosing a preset traces the image with it straight away, and the panel is opened with the Image Trace button in the Control panel or in Properties' Image Trace section (<https://helpx.adobe.com/illustrator/using/image-trace-results-optimization.html>). The controls show Threshold, Paths, Corners and Noise with Advanced open; `panels/image_trace.rs` headless tests (`a_quick_trace_leaves_the_panel_as_it_was`, `sketch_quick_trace_reveals_all_adjustments_and_keeps_the_source` for the button's route). No algorithm parity claim. |
|
||||
| Image Trace panel: holding the eye button next to View down shows the selected Image Trace objects' source image on the canvas | done | Per the public documentation, the control next to View is pressed and held to compare the trace result with the source image (<https://web.archive.org/web/20250831090623/https://helpx.adobe.com/illustrator/using/image-trace.html>). On release the objects show their View again; the document, its View and the undo history are unchanged. `RenderOptions::trace_source`; `panels/image_trace.rs` headless test `holding_the_eye_button_shows_the_source_image_until_released`, render test `trace_source_draws_the_source_image_of_the_listed_objects`. The Control bar and Properties have no such button |
|
||||
| Live trace result bounds and selection highlights exclude the retained bitmap; source-image views retain it | done | All TraceView modes and rotated result bounds regression-covered; ordinary hidden group behavior retained |
|
||||
|
||||
Reference in New Issue
Block a user