Merge pull request #1751 from AyberkA0/feat/toolbar-drawer-groups

Fix #1736: toolbar without group labels, and an All Tools drawer with flyout groups, shortcuts, menu and Show section
This commit is contained in:
BFlat 2026-10-14 07:58:09 +07:00 committed by GitHub
commit 225f3d7b0c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 513 additions and 82 deletions

View File

@ -716,7 +716,7 @@ pub struct Prefs {
pub canvas_color: String,
pub auto_collapse_icon_panels: bool,
/// User Interface › Show Tool Group Labels: the toolbar's group names (Select, Shapes, Draw…);
/// off, a faint dash separates the groups instead (#663).
/// off (as it ships, #1736), the tools run on from group to group with nothing between them.
pub tool_group_labels: bool,
pub open_documents_as_tabs: bool,
pub large_tabs: bool,
@ -942,7 +942,7 @@ impl Default for Prefs {
ui_brightness: s("mediumDark"),
canvas_color: s("matchUi"),
auto_collapse_icon_panels: false,
tool_group_labels: true,
tool_group_labels: false,
open_documents_as_tabs: true,
large_tabs: false,
ui_scaling: 1.0,

View File

@ -24,11 +24,11 @@ use crate::state::{ToolbarCategory, ToolbarEdits};
use crate::theme::Tokens;
use crate::{VectorcraftApp, icons, widgets};
/// A tool's cell in the drawer: its icon and name.
const CELL: egui::Vec2 = vec2(176.0, 28.0);
const ICON: f32 = 20.0;
/// The drawer's width: two cells, the gap between them and room for the scroll bar.
const WIDTH: f32 = 2.0 * CELL.x + 24.0;
/// A tool's row in the drawer: its icon, name and shortcut.
const ROW: f32 = 26.0;
const ICON: f32 = 18.0;
/// The drawer's width: one column of rows, framed in groups, and room for the scroll bar.
const WIDTH: f32 = 268.0;
/// Gap between the toolbar and the drawer, and the drawer's least distance from the window's edges.
const GAP: f32 = 4.0;
/// Size of the tool following the pointer while it is dragged.
@ -244,6 +244,19 @@ pub(crate) fn command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<R
Ok(())
})
})(),
"toolbar.show" => (|| {
let on = crate::menus::opt_bool(p, "on")?;
if let Some(name) = p.get("control").filter(|v| !v.is_null()) {
let name = name.as_str().ok_or("control must be fillStroke, coloring, drawMode or screenMode")?;
let flag = app
.ui
.toolbar_show
.get_mut(name)
.ok_or_else(|| format!("unknown control `{name}`: fillStroke, coloring, drawMode or screenMode"))?;
*flag = on.unwrap_or(!*flag);
}
Ok(app.ui.toolbar_show.json())
})(),
"toolbar.reset" => {
*current(&mut app.ui.toolbar_edits, app.ui.toolbar_advanced) = None;
Ok(slots_json(app))
@ -362,6 +375,30 @@ fn sections() -> &'static [(&'static str, Vec<&'static ToolInfo>)] {
})
}
/// A heading of the drawer and the flyout groups listed under it.
type Heading = (&'static str, Vec<&'static [ToolInfo]>);
/// The headings in order, each with the flyout groups listed under it (a group under the heading
/// most of its tools have, the first tool's on a tie), each in its tools' order: what the drawer
/// frames together.
fn groups() -> &'static [Heading] {
static GROUPS: OnceLock<Vec<Heading>> = OnceLock::new();
GROUPS.get_or_init(|| {
let mut out: Vec<Heading> = sections().iter().map(|(cat, _)| (*cat, vec![])).collect();
for g in TOOL_GROUPS {
let Some(first) = g.first() else { continue };
let count = |h: &str| g.iter().filter(|t| heading(t.id) == h).count();
let h = g.iter().map(|t| heading(t.id)).fold(heading(first.id), |best, h| if count(h) > count(best) { h } else { best });
match out.iter_mut().find(|(c, _)| *c == h) {
Some((_, gs)) => gs.push(g),
None => out.push((h, vec![g])),
}
}
out.retain(|(_, gs)| !gs.is_empty());
out
})
}
/// The All Tools drawer beside the toolbar, its top by the ••• button (higher when that would put
/// it past the window's bottom), while `tools_drawer` is on.
pub(crate) fn drawer(app: &mut VectorcraftApp, ctx: &egui::Context) {
@ -374,7 +411,10 @@ pub(crate) fn drawer(app: &mut VectorcraftApp, ctx: &egui::Context) {
let id = drawer_id();
let size = ctx.memory(|m| m.area_rect(id)).map_or(vec2(WIDTH, 200.0), |r| r.size());
let x = (bar.right() + GAP).min(screen.right() - size.x - GAP).max(screen.left());
let y = button.top().min(screen.bottom() - size.y - GAP).max(screen.top() + GAP);
// Never above the toolbar's top: higher up are the window's title bar (and on macOS its
// 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 active = app.session.tool_id();
let dragged = DragAndDrop::payload::<ToolDrag>(ctx).map(|d| *d);
@ -383,29 +423,69 @@ 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 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| {
ui.set_width(WIDTH.min(screen.width() - 2.0 * GAP - 16.0).max(CELL.x));
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.
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| reset = widgets::flat_button(ui, "Reset", 64.0).clicked());
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let (r, menu) = ui.allocate_exact_size(vec2(18.0, 18.0), Sense::click());
icons::paint(ui, "menu", r.shrink(1.0), if menu.hovered() { t.text_strong } else { t.text_dim });
menu.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, tl!("Drawer menu")));
let menu = menu.on_hover_text(tl!("Drawer menu"));
egui::Popup::menu(&menu).id(menu_id()).show(|ui| {
ui.set_min_width(160.0);
if widgets::menu_item(ui, "Reset", true, false) {
reset = true;
}
ui.separator();
for (label, adv) in [("Basic", false), ("Advanced", true)] {
if widgets::menu_item(ui, label, true, app.ui.toolbar_advanced == adv) {
advanced = Some(adv);
}
}
});
});
});
ui.add_space(4.0);
// As tall as its tools, up to the window's height (it scrolls then); the Area's size
// from the last frame mustn't cap it.
let room = (screen.height() - 2.0 * GAP - 68.0).max(120.0);
let room = (screen.bottom() - top - GAP - 104.0).max(120.0);
egui::ScrollArea::vertical().max_height(room).min_scrolled_height(room).auto_shrink([false, true]).show(ui, |ui| {
for (cat, tools) in sections() {
widgets::subheader(ui, tl!(cat));
ui.horizontal_wrapped(|ui| {
ui.spacing_mut().item_spacing = vec2(4.0, 2.0);
for tool in tools {
if cell(ui, &t, tool, tool.id == active, in_toolbar.contains(tool.id)) {
picked = Some(tool.id);
}
}
});
ui.spacing_mut().item_spacing = vec2(0.0, 0.0);
let inner = ui.available_width() - 10.0;
for (cat, gs) in groups() {
ui.add_space(6.0);
ui.label(egui::RichText::new(format!("{}:", tl!(cat))).size(12.0).color(t.text_strong));
ui.add_space(4.0);
for g in gs {
// A flyout group: its tools framed together, as one toolbar button holds them.
egui::Frame::NONE.stroke(Stroke::new(1.0, t.divider)).corner_radius(2.0).show(ui, |ui| {
ui.set_width(inner);
for tool in g.iter() {
if row(ui, &t, tool, tool.id == active, in_toolbar.contains(tool.id)) {
picked = Some(tool.id);
}
}
});
ui.add_space(5.0);
}
}
});
// Show: the controls under the toolbar's tools.
ui.add_space(4.0);
ui.separator();
ui.horizontal(|ui| {
ui.label(egui::RichText::new(tl!("Show:")).size(12.0).color(t.text));
ui.add_space(4.0);
for (name, label, icon) in crate::state::ToolbarShow::CONTROLS {
if show_button(ui, &t, name, label, icon, app.ui.toolbar_show.get(name)) {
show_toggle = Some(name);
}
}
});
});
@ -423,10 +503,21 @@ pub(crate) fn drawer(app: &mut VectorcraftApp, ctx: &egui::Context) {
if reset && let Err(e) = app.run("toolbar.reset", json!({})) {
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!({}))
{
app.ui.status = e;
}
if let Some(name) = show_toggle
&& let Err(e) = app.run("toolbar.show", json!({ "control": name }))
{
app.ui.status = e;
}
if let Some(id) = picked {
app.select_tool(id);
app.ui.tools_drawer = false;
} else if resp.response.clicked_elsewhere() && !pointer.is_some_and(|p| bar.contains(p)) {
} else if resp.response.clicked_elsewhere() && !pointer.is_some_and(|p| bar.contains(p)) && !egui::Popup::is_id_open(ctx, menu_id()) {
// A click in the toolbar (picking a tool, or the ••• button that toggles the drawer) leaves
// it open.
app.ui.tools_drawer = false;
@ -434,19 +525,68 @@ pub(crate) fn drawer(app: &mut VectorcraftApp, ctx: &egui::Context) {
ghost(ctx, &t);
}
/// A tool's cell in the drawer: icon and name, dimmed when the tool is in the toolbar already (it
/// can't be dragged in again). Returns whether it was clicked.
fn cell(ui: &mut Ui, t: &Tokens, tool: &'static ToolInfo, on: bool, dimmed: bool) -> bool {
let (rect, resp) = ui.allocate_exact_size(CELL, if dimmed { Sense::click() } else { Sense::click_and_drag() });
/// 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 {
let (r, resp) = ui.allocate_exact_size(vec2(26.0, 26.0), Sense::click());
resp.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::Checkbox, true, on, tl!(label)));
let p = ui.painter();
if on {
p.rect_filled(r, 3.0, t.tool_active);
} else if resp.hovered() {
p.rect_filled(r, 3.0, t.hover);
}
let ink = if on { t.text_strong } else { t.icon };
let c = r.center();
match name {
"fillStroke" => {
// The proxy: a filled square in front of a stroked one.
let back = Rect::from_center_size(c + vec2(3.0, 3.0), vec2(10.0, 10.0));
p.rect_stroke(back, 0.0, Stroke::new(2.0, ink), StrokeKind::Inside);
p.rect_filled(Rect::from_center_size(c - vec2(3.0, 3.0), vec2(10.0, 10.0)), 0.0, ink);
}
"coloring" => {
// Color, Gradient and None.
for (k, x) in [-6.5f32, 0.0, 6.5].into_iter().enumerate() {
let chip = Rect::from_center_size(c + vec2(x, 0.0), vec2(5.5, 9.0));
match k {
0 => {
p.rect_filled(chip, 0.0, ink);
}
1 => {
p.rect_filled(chip, 0.0, ink.gamma_multiply(0.5));
}
_ => {
p.rect_stroke(chip, 0.0, Stroke::new(1.0, ink), StrokeKind::Inside);
p.line_segment([chip.left_bottom(), chip.right_top()], Stroke::new(1.0, ink));
}
}
}
}
_ => icons::paint(ui, icon, r.shrink(5.0), ink),
}
resp.on_hover_text(tl!(label)).clicked()
}
/// The drawer's flyout menu.
fn menu_id() -> egui::Id {
egui::Id::new("tools-drawer-menu")
}
/// A tool's row in the drawer: icon, name and shortcut, dimmed when the tool is in the toolbar
/// already (it can't be dragged in again). Returns whether it was clicked.
fn row(ui: &mut Ui, t: &Tokens, tool: &'static ToolInfo, on: bool, dimmed: bool) -> bool {
let (rect, resp) = ui.allocate_exact_size(vec2(ui.available_width(), ROW), if dimmed { Sense::click() } else { Sense::click_and_drag() });
if !dimmed {
resp.dnd_set_drag_payload(ToolDrag { tool: tool.id, from_toolbar: false });
}
let fill = rect.shrink(1.0);
if on {
ui.painter().rect_filled(rect, 3.0, t.selection);
ui.painter().rect_filled(fill, 2.0, t.selection);
} else if resp.hovered() {
ui.painter().rect_filled(rect, 3.0, t.hover);
ui.painter().rect_filled(fill, 2.0, t.hover);
}
// The active tool's cell is lit, dimmed or not.
// The active tool's row is lit, dimmed or not.
let color = if on {
t.text_strong
} else if dimmed {
@ -454,12 +594,31 @@ fn cell(ui: &mut Ui, t: &Tokens, tool: &'static ToolInfo, on: bool, dimmed: bool
} else {
t.text
};
let icon = Rect::from_center_size(rect.left_center() + vec2(6.0 + ICON / 2.0, 0.0), vec2(ICON, ICON));
let icon = Rect::from_center_size(rect.left_center() + vec2(8.0 + ICON / 2.0, 0.0), vec2(ICON, ICON));
icons::paint(ui, icons::tool_icon(tool.icon), icon, if dimmed || on { color } else { t.icon });
let name = tl!(tool.label);
let name = if crate::i18n::current() == crate::i18n::Lang::EN { name.trim_end_matches(" Tool") } else { name };
let text_at = pos2(icon.right() + 8.0, rect.center().y);
ui.painter().with_clip_rect(rect).text(text_at, egui::Align2::LEFT_CENTER, name, egui::FontId::proportional(12.0), color);
let painter = ui.painter().with_clip_rect(rect);
let shortcut = crate::shortcut_editor::tool_shortcut(tool.id);
let keys_w = shortcut.as_ref().map_or(0.0, |k| {
let keys = if on {
t.text_strong
} else if dimmed {
t.text_disabled
} else {
t.text_dim
};
let g = painter.layout_no_wrap(k.to_string(), egui::FontId::proportional(11.5), keys);
let w = g.size().x;
painter.galley(pos2(rect.right() - 10.0 - w, rect.center().y - g.size().y / 2.0), g, keys);
w + 12.0
});
let text_at = pos2(icon.right() + 10.0, rect.center().y);
painter.with_clip_rect(Rect::from_min_max(rect.min, pos2(rect.right() - 10.0 - keys_w, rect.bottom()))).text(
text_at,
egui::Align2::LEFT_CENTER,
tl!(tool.label),
egui::FontId::proportional(12.5),
color,
);
resp.on_hover_text(crate::toolbar::tip(tool)).clicked()
}
@ -483,7 +642,7 @@ mod tests {
use super::*;
/// Tall enough for the whole Basic toolbar and the whole drawer.
const SCREEN: egui::Vec2 = vec2(1000.0, 2400.0);
const SCREEN: egui::Vec2 = vec2(1000.0, 4000.0);
/// A toolbar tool button.
const BUTTON: egui::Vec2 = vec2(36.0, 29.0);
@ -561,7 +720,7 @@ mod tests {
ctx.memory(|m| m.areas().visible_last_frame(&egui::LayerId::new(egui::Order::Foreground, drawer_id())))
}
/// A point inside the drawer's cell of the tool named `name` (as the drawer shows it), and the
/// A point inside the drawer's row of the tool named `name` (as the drawer shows it), and the
/// colour of its name.
fn cell_at(shown: &[(String, Pos2, Color32)], drawer: Rect, name: &str) -> (Pos2, Color32) {
shown
@ -571,6 +730,40 @@ mod tests {
.unwrap_or_else(|| panic!("no {name} in the drawer {drawer:?}"))
}
/// The drawer frames each flyout group whole, under the heading most of its tools have: every
/// tool once, the Pencil's group (led by the Shaper) under Draw.
#[test]
fn the_drawer_frames_every_flyout_group_once() {
let listed: Vec<&str> = groups().iter().flat_map(|(_, gs)| gs.iter().flat_map(|g| g.iter().map(|t| t.id))).collect();
let all: Vec<&str> = TOOL_GROUPS.iter().flat_map(|g| g.iter().map(|t| t.id)).collect();
assert_eq!(listed.len(), all.len());
assert!(all.iter().all(|id| listed.contains(id)));
let under = |id: &str| groups().iter().find(|(_, gs)| gs.iter().any(|g| g.iter().any(|t| t.id == id))).map(|(c, _)| *c);
assert_eq!(under("pencil"), Some("Draw"));
assert_eq!(under("shaper"), Some("Draw"), "framed with the Pencil");
assert_eq!(under("rectangle"), Some("Shapes"));
}
/// The Show section's controls (#1736): each shows (true), hides (false) or toggles, saved with
/// the preferences; an unknown control is an error naming them.
#[test]
fn toolbar_show_turns_the_controls_under_the_tools_on_and_off() {
let mut app = app();
let all_on = json!({"fillStroke": true, "coloring": true, "drawMode": true, "screenMode": true});
assert_eq!(app.run("toolbar.show", json!({})).unwrap(), all_on);
assert_eq!(app.run("toolbar.show", json!({"control": "coloring"})).unwrap()["coloring"], json!(false));
assert_eq!(app.run("toolbar.show", json!({"control": "screenMode", "on": false})).unwrap()["screenMode"], json!(false));
assert!(!app.ui.toolbar_show.coloring && !app.ui.toolbar_show.screen_mode && app.ui.toolbar_show.fill_stroke);
let err = app.run("toolbar.show", json!({"control": "swatches"})).unwrap_err();
assert!(err.contains("swatches") && err.contains("fillStroke"), "{err}");
// Saved with the preferences; older preferences show them all.
let saved = serde_json::to_value(&app.ui).unwrap();
assert_eq!(serde_json::from_value::<crate::state::UiState>(saved).unwrap().toolbar_show, app.ui.toolbar_show);
let mut old = serde_json::to_value(&app.ui).unwrap();
old.as_object_mut().unwrap().remove("toolbar_show");
assert_eq!(serde_json::from_value::<crate::state::UiState>(old).unwrap().toolbar_show, crate::state::ToolbarShow::default());
}
/// Every tool is listed once, under a heading; the Basic toolbar's groups come first, in its order.
#[test]
fn every_tool_is_listed_once_under_a_heading() {
@ -698,8 +891,9 @@ mod tests {
let bar = ctx.data(|d| d.get_temp::<Rect>(toolbar_id())).unwrap();
assert!((drawer.left() - bar.right() - GAP).abs() < 1.0, "beside the toolbar: {drawer:?} {bar:?}");
assert!((drawer.top() - edit.top()).abs() < 1.0 || drawer.bottom() >= SCREEN.y - GAP - 1.0, "by the button: {drawer:?} {edit:?}");
assert!(drawer.top() >= bar.top() - 0.5, "never above the toolbar (the title bar's there): {drawer:?} {bar:?}");
assert!(drawer.y_range().contains(edit.center().y), "by the button: {drawer:?} {edit:?}");
assert!(drawer.width() < 3.0 * CELL.x, "two tools wide: {drawer:?}");
assert!(drawer.width() < WIDTH + 24.0, "one row of tools wide: {drawer:?}");
// A click on a toolbar tool picks it and leaves the drawer open.
let tools = crate::toolbar::tests::widget_rects(&ctx, BUTTON);
let ellipse = tools[slot_of(&app, "ellipse").unwrap()].center();
@ -724,7 +918,7 @@ mod tests {
app.run("window.editToolbar", json!({})).unwrap();
frame(&mut app, &ctx, 7.0, vec![]);
let shown = frame(&mut app, &ctx, 7.1, vec![]);
let (at, _) = cell_at(&shown, drawer_rect(&ctx).unwrap(), "Artboard");
let (at, _) = cell_at(&shown, drawer_rect(&ctx).unwrap(), "Artboard Tool");
click(&mut app, &ctx, 8.0, at);
assert!(!app.ui.tools_drawer && app.session.tool_id() == "artboard");
}
@ -739,13 +933,13 @@ mod tests {
let shown = frame(&mut app, &ctx, 0.1, vec![]);
let drawer = drawer_rect(&ctx).unwrap();
let dim = Tokens::get(&ctx).text_disabled;
assert_eq!(cell_at(&shown, drawer, "Ellipse").1, dim);
assert_ne!(cell_at(&shown, drawer, "Artboard").1, dim);
assert_eq!(cell_at(&shown, drawer, "Ellipse Tool").1, dim);
assert_ne!(cell_at(&shown, drawer, "Artboard Tool").1, dim);
app.run("toolbar.addTool", json!({"tool": "artboard"})).unwrap();
app.run("toolbar.removeTool", json!({"tool": "ellipse"})).unwrap();
let shown = frame(&mut app, &ctx, 0.2, vec![]);
assert_ne!(cell_at(&shown, drawer, "Ellipse").1, dim);
assert_eq!(cell_at(&shown, drawer, "Artboard").1, dim);
assert_ne!(cell_at(&shown, drawer, "Ellipse Tool").1, dim);
assert_eq!(cell_at(&shown, drawer, "Artboard Tool").1, dim);
}
/// #1018: tools drag from the drawer onto the toolbar (between two tools, or onto one to join
@ -761,19 +955,24 @@ mod tests {
let drawer = drawer_rect(&ctx).unwrap();
let tools = crate::toolbar::tests::widget_rects(&ctx, BUTTON);
// Onto the divider under the Selection tool: its own slot, second.
let (artboard, _) = cell_at(&shown, drawer, "Artboard");
let (artboard, _) = cell_at(&shown, drawer, "Artboard Tool");
drag(&mut app, &ctx, 1.0, artboard, tools[0].center_bottom() - vec2(0.0, 3.0));
assert_eq!(&slots(&app)[..2], [&["selection"][..], &["artboard"]]);
assert!(app.ui.tools_drawer, "the drawer stays open");
// Onto the middle of the Ellipse tool: into its group.
let tools = crate::toolbar::tests::widget_rects(&ctx, BUTTON);
let ellipse = slot_of(&app, "ellipse").unwrap();
let (slice, _) = cell_at(&shown, drawer, "Slice");
// The drawer moves with the ••• button the new slot pushed down: read it again.
let shown = frame(&mut app, &ctx, 1.9, vec![]);
let drawer = drawer_rect(&ctx).unwrap();
let (slice, _) = cell_at(&shown, drawer, "Slice Tool");
drag(&mut app, &ctx, 2.0, slice, tools[ellipse].center());
assert_eq!(slots(&app)[ellipse], ["ellipse", "slice"]);
// A dimmed tool (in the toolbar already) doesn't drag.
let before = slots(&app);
let (rectangle, _) = cell_at(&shown, drawer, "Rectangle");
let shown = frame(&mut app, &ctx, 2.9, vec![]);
let drawer = drawer_rect(&ctx).unwrap();
let (rectangle, _) = cell_at(&shown, drawer, "Rectangle Tool");
drag(&mut app, &ctx, 3.0, rectangle, tools[0].center_bottom() - vec2(0.0, 3.0));
assert_eq!(slots(&app), before);
// From the toolbar into the drawer: out of the toolbar.

View File

@ -4747,6 +4747,14 @@ mark-style Japanese Japonské
@msg unknown parameter `{key}`; the command takes: {keys} neznámý parametr `{key}`; příkaz přijímá: {keys}
Load EPS into Clipboard Načíst EPS do schránky
@msg an image of {w} × {h} pixels is too large to draw obraz o velikosti {w} × {h} obrazových bodů je příliš velký na vykreslení
Fill and Stroke Controls Ovládací prvky výplně a tahu
Coloring Controls Ovládací prvky barev
Draw Mode Controls Ovládací prvky režimu kreslení
Screen Mode Controls Ovládací prvky režimu zobrazení
Drawer menu Nabídka zásuvky
Toolbar: Show Controls Panel nástrojů: Zobrazit ovládací prvky
@msg control must be fillStroke, coloring, drawMode or screenMode control musí být fillStroke, coloring, drawMode nebo screenMode
@msg unknown control `{name}`: fillStroke, coloring, drawMode or screenMode neznámý ovládací prvek `{name}`: fillStroke, coloring, drawMode nebo screenMode
@msg give a fraction `t` from 0 to 1 zadejte hodnotu `t` od 0 do 1
@msg give the path `id` zadejte `id` cesty
@msg invalid stroke widths neplatné šířky tahu

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -4271,6 +4271,15 @@ theme Appearance Erscheinungsbild
@msg unknown parameter `{key}`; the command takes: {keys} unbekannter Parameter `{key}`; der Befehl akzeptiert: {keys}
Load EPS into Clipboard EPS in die Zwischenablage laden
@msg an image of {w} × {h} pixels is too large to draw ein Bild mit {w} × {h} Pixeln ist zu groß, um gezeichnet zu werden
Basic Basis
Fill and Stroke Controls Steuerelemente für Fläche und Kontur
Coloring Controls Steuerelemente für Farbe
Draw Mode Controls Steuerelemente für Zeichenmodus
Screen Mode Controls Steuerelemente für Bildschirmmodus
Drawer menu Menü der Werkzeugschublade
Toolbar: Show Controls Werkzeugleiste: Steuerelemente einblenden
@msg control must be fillStroke, coloring, drawMode or screenMode control muss fillStroke, coloring, drawMode oder screenMode sein
@msg unknown control `{name}`: fillStroke, coloring, drawMode or screenMode unbekanntes Steuerelement `{name}`: fillStroke, coloring, drawMode oder screenMode
@msg give a fraction `t` from 0 to 1 geben Sie einen Wert `t` von 0 bis 1 an
@msg give the path `id` geben Sie die `id` des Pfads an
@msg invalid stroke widths ungültige Konturbreiten

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -4688,6 +4688,14 @@ mark-style Japanese Japonesas
@msg unknown parameter `{key}`; the command takes: {keys} parámetro desconocido `{key}`; el comando acepta: {keys}
Load EPS into Clipboard Cargar EPS en el portapapeles
@msg an image of {w} × {h} pixels is too large to draw una imagen de {w} × {h} píxeles es demasiado grande para dibujarse
Fill and Stroke Controls Controles de relleno y trazo
Coloring Controls Controles de color
Draw Mode Controls Controles de modo de dibujo
Screen Mode Controls Controles de modo de pantalla
Drawer menu Menú del cajón
Toolbar: Show Controls Barra de herramientas: Mostrar controles
@msg control must be fillStroke, coloring, drawMode or screenMode control debe ser fillStroke, coloring, drawMode o screenMode
@msg unknown control `{name}`: fillStroke, coloring, drawMode or screenMode control desconocido `{name}`: fillStroke, coloring, drawMode o screenMode
@msg give a fraction `t` from 0 to 1 indica un valor `t` de 0 a 1
@msg give the path `id` indica el `id` del trazado
@msg invalid stroke widths anchuras de trazo no válidas

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -4747,6 +4747,14 @@ mark-style Japanese Japonais
theme Appearance Apparence
Load EPS into Clipboard Charger un EPS dans le presse-papiers
@msg an image of {w} × {h} pixels is too large to draw une image de {w} × {h} pixels est trop grande pour être dessinée
Fill and Stroke Controls Commandes de fond et de contour
Coloring Controls Commandes de couleur
Draw Mode Controls Commandes de mode de dessin
Screen Mode Controls Commandes de mode d'affichage
Drawer menu Menu du tiroir
Toolbar: Show Controls Barre d’outils : Afficher les commandes
@msg control must be fillStroke, coloring, drawMode or screenMode control doit être fillStroke, coloring, drawMode ou screenMode
@msg unknown control `{name}`: fillStroke, coloring, drawMode or screenMode commande inconnue `{name}` : fillStroke, coloring, drawMode ou screenMode
@msg give a fraction `t` from 0 to 1 indiquez une valeur `t` comprise entre 0 et 1
@msg give the path `id` indiquez l’`id` du tracé
@msg invalid stroke widths largeurs de contour non valides

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -4714,6 +4714,14 @@ imageTrace Outline Körvonal
@msg unknown parameter `{key}`; the command takes: {keys} ismeretlen paraméter: `{key}`; a parancs ezeket fogadja el: {keys}
Load EPS into Clipboard EPS betöltése a vágólapra
@msg an image of {w} × {h} pixels is too large to draw egy {w} × {h} pixeles kép túl nagy a kirajzoláshoz
Fill and Stroke Controls Kitöltés és körvonal vezérlői
Coloring Controls Színezés vezérlői
Draw Mode Controls Rajzolási mód vezérlői
Screen Mode Controls Képernyőmód vezérlői
Drawer menu Fiók menüje
Toolbar: Show Controls Eszköztár: Vezérlők megjelenítése
@msg control must be fillStroke, coloring, drawMode or screenMode a control értéke fillStroke, coloring, drawMode vagy screenMode lehet
@msg unknown control `{name}`: fillStroke, coloring, drawMode or screenMode ismeretlen vezérlő: `{name}`; fillStroke, coloring, drawMode vagy screenMode
@msg give a fraction `t` from 0 to 1 add meg a `t` értékét 0 és 1 között
@msg give the path `id` add meg az útvonal `id` értékét
@msg invalid stroke widths érvénytelen körvonalszélességek

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -4316,6 +4316,15 @@ lighting Light Luce
@msg unknown parameter `{key}`; the command takes: {keys} parametro sconosciuto `{key}`; il comando accetta: {keys}
Load EPS into Clipboard Carica EPS negli appunti
@msg an image of {w} × {h} pixels is too large to draw un'immagine di {w} × {h} pixel è troppo grande per essere disegnata
Basic Base
Fill and Stroke Controls Controlli di riempimento e traccia
Coloring Controls Controlli colore
Draw Mode Controls Controlli modalità di disegno
Screen Mode Controls Controlli modalità schermo
Drawer menu Menu del cassetto
Toolbar: Show Controls Barra degli strumenti: Mostra controlli
@msg control must be fillStroke, coloring, drawMode or screenMode control deve essere fillStroke, coloring, drawMode o screenMode
@msg unknown control `{name}`: fillStroke, coloring, drawMode or screenMode controllo sconosciuto `{name}`: fillStroke, coloring, drawMode o screenMode
@msg give a fraction `t` from 0 to 1 specifica un valore `t` da 0 a 1
@msg give the path `id` specifica l'`id` del tracciato
@msg invalid stroke widths larghezze della traccia non valide

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -4310,6 +4310,15 @@ lighting Light ライト
@msg unknown parameter `{key}`; the command takes: {keys} `{key}` は不明なパラメーターです。このコマンドで使えるパラメーター: {keys}
Load EPS into Clipboard EPS をクリップボードに読み込む
@msg an image of {w} × {h} pixels is too large to draw {w} × {h} ピクセルの画像は大きすぎて描画できません
Basic 基本
Fill and Stroke Controls 塗りと線のコントロール
Coloring Controls カラーのコントロール
Draw Mode Controls 描画モードのコントロール
Screen Mode Controls スクリーンモードのコントロール
Drawer menu ドロワーメニュー
Toolbar: Show Controls ツールバー: コントロールを表示
@msg control must be fillStroke, coloring, drawMode or screenMode control は fillStroke、coloring、drawMode、screenMode のいずれかです
@msg unknown control `{name}`: fillStroke, coloring, drawMode or screenMode 不明なコントロール `{name}`:fillStroke、coloring、drawMode、screenMode のいずれかです
@msg give a fraction `t` from 0 to 1 0 から 1 までの値 `t` を指定してください
@msg give the path `id` パスの `id` を指定してください
@msg invalid stroke widths 線幅が無効です

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -4280,6 +4280,15 @@ lighting Light 광원
@msg unknown parameter `{key}`; the command takes: {keys} 알 수 없는 매개변수 `{key}`: 이 명령이 받는 매개변수: {keys}
Load EPS into Clipboard EPS를 클립보드로 불러오기
@msg an image of {w} × {h} pixels is too large to draw {w} × {h}픽셀 이미지는 너무 커서 그릴 수 없습니다
Basic 기본
Fill and Stroke Controls 칠 및 획 컨트롤
Coloring Controls 색상 컨트롤
Draw Mode Controls 그리기 모드 컨트롤
Screen Mode Controls 화면 모드 컨트롤
Drawer menu 서랍 메뉴
Toolbar: Show Controls 도구 막대: 컨트롤 표시
@msg control must be fillStroke, coloring, drawMode or screenMode control은 fillStroke, coloring, drawMode, screenMode 중 하나여야 합니다
@msg unknown control `{name}`: fillStroke, coloring, drawMode or screenMode 알 수 없는 컨트롤 `{name}`: fillStroke, coloring, drawMode, screenMode 중 하나
@msg give a fraction `t` from 0 to 1 0에서 1 사이의 값 `t`를 지정하세요
@msg give the path `id` 패스 `id`를 지정하세요
@msg invalid stroke widths 선 너비가 유효하지 않습니다

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -4589,6 +4589,15 @@ mark-style Japanese Japońskie
@msg unknown parameter `{key}`; the command takes: {keys} nieznany parametr `{key}`; polecenie przyjmuje: {keys}
Load EPS into Clipboard Wczytaj EPS do schowka
@msg an image of {w} × {h} pixels is too large to draw obraz o wymiarach {w} × {h} pikseli jest zbyt duży, aby go narysować
Basic Podstawowy
Fill and Stroke Controls Elementy sterujące wypełnieniem i obrysem
Coloring Controls Elementy sterujące kolorem
Draw Mode Controls Elementy sterujące trybem rysowania
Screen Mode Controls Elementy sterujące trybem ekranu
Drawer menu Menu szuflady
Toolbar: Show Controls Pasek narzędzi: Pokaż elementy sterujące
@msg control must be fillStroke, coloring, drawMode or screenMode control musi mieć wartość fillStroke, coloring, drawMode lub screenMode
@msg unknown control `{name}`: fillStroke, coloring, drawMode or screenMode nieznany element sterujący `{name}`: fillStroke, coloring, drawMode lub screenMode
@msg give a fraction `t` from 0 to 1 podaj wartość `t` od 0 do 1
@msg give the path `id` podaj `id` ścieżki
@msg invalid stroke widths nieprawidłowe szerokości obrysu

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -3750,7 +3750,8 @@ axis Both Ambos
@msg unknown parameter `{key}`; the command takes no parameters parâmetro desconhecido `{key}`; o comando não tem parâmetros
@msg unknown parameter `{key}`; the command takes: {keys} parâmetro desconhecido `{key}`; o comando aceita: {keys}
Load EPS into Clipboard Carregar EPS na Área de Transferência
Drawer menu Menu da gaveta
Toolbar: Show Controls Barra de ferramentas: Mostrar controles
# Corner Radius: a field per corner of a live rectangle.
Single Corner Radius Raio de Canto Único
Top Left: Superior Esquerdo:

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -4312,6 +4312,15 @@ lighting Light Свет
@msg unknown parameter `{key}`; the command takes: {keys} неизвестный параметр `{key}`; команда принимает: {keys}
Load EPS into Clipboard Загрузить EPS в буфер обмена
@msg an image of {w} × {h} pixels is too large to draw изображение размером {w} × {h} пикселей слишком велико для отрисовки
Basic Базовый
Fill and Stroke Controls Элементы управления заливкой и обводкой
Coloring Controls Элементы управления цветом
Draw Mode Controls Элементы управления режимом рисования
Screen Mode Controls Элементы управления режимом экрана
Drawer menu Меню ящика
Toolbar: Show Controls Панель инструментов: Показать элементы управления
@msg control must be fillStroke, coloring, drawMode or screenMode control должен быть fillStroke, coloring, drawMode или screenMode
@msg unknown control `{name}`: fillStroke, coloring, drawMode or screenMode неизвестный элемент управления `{name}`: fillStroke, coloring, drawMode или screenMode
@msg give a fraction `t` from 0 to 1 укажите значение `t` от 0 до 1
@msg give the path `id` укажите `id` контура
@msg invalid stroke widths недопустимые значения ширины обводки

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -4296,6 +4296,15 @@ lighting Light Світло
@msg unknown parameter `{key}`; the command takes: {keys} невідомий параметр `{key}`; команда приймає: {keys}
Load EPS into Clipboard Завантажити EPS у буфер обміну
@msg an image of {w} × {h} pixels is too large to draw зображення розміром {w} × {h} пікселів завелике для відображення
Basic Базовий
Fill and Stroke Controls Елементи керування заливкою та обведенням
Coloring Controls Елементи керування кольором
Draw Mode Controls Елементи керування режимом малювання
Screen Mode Controls Елементи керування режимом екрана
Drawer menu Меню шухляди
Toolbar: Show Controls Панель інструментів: Показати елементи керування
@msg control must be fillStroke, coloring, drawMode or screenMode control має бути fillStroke, coloring, drawMode або screenMode
@msg unknown control `{name}`: fillStroke, coloring, drawMode or screenMode невідомий елемент керування `{name}`: fillStroke, coloring, drawMode або screenMode
@msg give a fraction `t` from 0 to 1 вкажіть значення `t` від 0 до 1
@msg give the path `id` вкажіть `id` контуру
@msg invalid stroke widths неприпустимі значення ширини обведення

Can't render this file because it has a wrong number of fields in line 13.

View File

@ -4298,6 +4298,15 @@ theme Appearance Giao diện
@msg {tool} isn't in a toolbar group of several tools {tool} không nằm trong nhóm thanh công cụ có nhiều công cụ
Load EPS into Clipboard Tải EPS vào bảng tạm
@msg an image of {w} × {h} pixels is too large to draw ảnh {w} × {h} điểm ảnh quá lớn nên không thể vẽ
Basic Cơ bản
Fill and Stroke Controls Điều khiển tô và nét
Coloring Controls Điều khiển màu
Draw Mode Controls Điều khiển chế độ vẽ
Screen Mode Controls Điều khiển chế độ màn hình
Drawer menu Menu ngăn kéo
Toolbar: Show Controls Thanh công cụ: Hiện điều khiển
@msg control must be fillStroke, coloring, drawMode or screenMode control phải là fillStroke, coloring, drawMode hoặc screenMode
@msg unknown control `{name}`: fillStroke, coloring, drawMode or screenMode điều khiển không xác định `{name}`: fillStroke, coloring, drawMode hoặc screenMode
@msg give a fraction `t` from 0 to 1 hãy cung cấp giá trị `t` từ 0 đến 1
@msg give the path `id` hãy cung cấp `id` đường dẫn
@msg invalid stroke widths chiều rộng nét không hợp lệ

Can't render this file because it has a wrong number of fields in line 11.

View File

@ -3902,7 +3902,14 @@ lighting Light 光源
@msg unknown parameter `{key}`; the command takes: {keys} 未知参数 `{key}`;该命令接受:{keys}
Load EPS into Clipboard 将 EPS 载入剪贴板
@msg an image of {w} × {h} pixels is too large to draw {w} × {h} 像素的图像过大,无法绘制
Fill and Stroke Controls 填色和描边控件
Coloring Controls 颜色控件
Draw Mode Controls 绘图模式控件
Screen Mode Controls 屏幕模式控件
Drawer menu 抽屉菜单
Toolbar: Show Controls 工具栏:显示控件
@msg control must be fillStroke, coloring, drawMode or screenMode control 必须是 fillStroke、coloring、drawMode 或 screenMode
@msg unknown control `{name}`: fillStroke, coloring, drawMode or screenMode 未知控件 `{name}`:fillStroke、coloring、drawMode 或 screenMode
# Corner Radius: a field per corner of a live rectangle.
Single Corner Radius 统一圆角半径
Top Left: 左上:

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -3683,7 +3683,15 @@ lighting Light 光源
@msg unknown parameter `{key}`; the command takes: {keys} 未知的參數 `{key}`;此指令接受:{keys}
Load EPS into Clipboard 將 EPS 載入剪貼簿
@msg an image of {w} × {h} pixels is too large to draw {w} × {h} 像素的影像過大,無法繪製
Basic 基本
Fill and Stroke Controls 填色和筆畫控制項
Coloring Controls 顏色控制項
Draw Mode Controls 繪圖模式控制項
Screen Mode Controls 螢幕模式控制項
Drawer menu 抽屜選單
Toolbar: Show Controls 工具列:顯示控制項
@msg control must be fillStroke, coloring, drawMode or screenMode control 必須是 fillStroke、coloring、drawMode 或 screenMode
@msg unknown control `{name}`: fillStroke, coloring, drawMode or screenMode 未知控制項 `{name}`:fillStroke、coloring、drawMode 或 screenMode
# Corner Radius: a field per corner of a live rectangle.
Single Corner Radius 統一圓角半徑
Top Left: 左上:

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -262,6 +262,12 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
"",
"{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",
),
(
"toolbar.show",
"Toolbar: Show Controls",
"",
"{control?: \"fillStroke\"|\"coloring\"|\"drawMode\"|\"screenMode\", on?: bool} show (true), hide (false) or toggle (omitted) one of the controls under the toolbar's tools (Fill and Stroke, Coloring, Draw Mode, Screen Mode), as the All Tools drawer's Show section does; saved with the preferences; returns every control's state",
),
(
"window.editToolbar",
"Edit Toolbar",

View File

@ -277,6 +277,10 @@ pub struct UiState {
/// The Basic and Advanced toolbars as Edit Toolbar left them (none: as they ship).
#[serde(default)]
pub toolbar_edits: ToolbarEdits,
/// The controls under the toolbar's tools that the All Tools drawer's Show section shows
/// (`toolbar.show`).
#[serde(default)]
pub toolbar_show: ToolbarShow,
/// Edit Toolbar's All Tools drawer is open beside the toolbar (`window.editToolbar`).
#[serde(skip)]
pub tools_drawer: bool,
@ -458,6 +462,57 @@ pub struct ToolbarCategory {
pub slots: Vec<Vec<String>>,
}
/// The controls under the toolbar's tools, each shown or hidden from the All Tools drawer's Show
/// section: Fill and Stroke (the proxy), Coloring (Color, Gradient, None), Draw Mode and Screen
/// Mode. All shown as it ships.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ToolbarShow {
#[serde(default = "yes")]
pub fill_stroke: bool,
#[serde(default = "yes")]
pub coloring: bool,
#[serde(default = "yes")]
pub draw_mode: bool,
#[serde(default = "yes")]
pub screen_mode: bool,
}
impl Default for ToolbarShow {
fn default() -> Self {
Self { fill_stroke: true, coloring: true, draw_mode: true, screen_mode: true }
}
}
impl ToolbarShow {
/// The controls as (`toolbar.show` name, label, icon), in the Show section's order.
pub const CONTROLS: [(&'static str, &'static str, &'static str); 4] = [
("fillStroke", "Fill and Stroke Controls", "dc-stroke"),
("coloring", "Coloring Controls", "dc-gradient"),
("drawMode", "Draw Mode Controls", "dc-draw-normal"),
("screenMode", "Screen Mode Controls", "dc-screen-mode"),
];
/// The flag of the control named `name`.
pub fn get_mut(&mut self, name: &str) -> Option<&mut bool> {
match name {
"fillStroke" => Some(&mut self.fill_stroke),
"coloring" => Some(&mut self.coloring),
"drawMode" => Some(&mut self.draw_mode),
"screenMode" => Some(&mut self.screen_mode),
_ => None,
}
}
pub fn get(self, name: &str) -> bool {
let mut s = self;
s.get_mut(name).is_some_and(|on| *on)
}
pub fn json(self) -> serde_json::Value {
serde_json::json!({"fillStroke": self.fill_stroke, "coloring": self.coloring, "drawMode": self.draw_mode, "screenMode": self.screen_mode})
}
}
/// The toolbars Edit Toolbar changed: each one is `None` while it is as it ships.
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct ToolbarEdits {
@ -614,6 +669,7 @@ impl Default for UiState {
slot_tool: Default::default(),
floating_flyouts: vec![],
toolbar_edits: ToolbarEdits::default(),
toolbar_show: ToolbarShow::default(),
tools_drawer: false,
floating_panels: vec![],
docking: None,

View File

@ -182,12 +182,13 @@ fn body(app: &mut VectorcraftApp, ui: &mut Ui, cols: usize, floating: Option<egu
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.
let mut raise: Option<&'static str> = None;
let labels = app.session.prefs.tool_group_labels;
let mut i = 0;
while i < all.len() {
if all[i].0.is_some() && !app.session.prefs.tool_group_labels {
// User Interface › Show Tool Group Labels off: a faint dash between the groups.
let (r, _) = ui.allocate_exact_size(vec2(ui.available_width(), 9.0), Sense::hover());
ui.painter().line_segment([r.center() - vec2(6.0, 0.0), r.center() + vec2(6.0, 0.0)], egui::Stroke::new(1.0, t.divider));
if all[i].0.is_some() && !labels {
// User Interface › Show Tool Group Labels off: the tools run on from group to group,
// in one column or two (as the reference's Basic toolbar in two), with nothing
// between the groups.
} else if let Some(cat) = all[i].0 {
let (r, _) = ui.allocate_exact_size(vec2(ui.available_width(), 18.0), Sense::hover());
// A long name is cut to its first four characters in the single column.
@ -196,9 +197,10 @@ fn body(app: &mut VectorcraftApp, ui: &mut Ui, cols: usize, floating: Option<egu
if cols == 1 && cat.chars().count() > 6 { format!("{}...", cat.chars().take(4).collect::<String>()) } else { cat.to_string() };
ui.painter().text(r.center() + vec2(0.0, 2.0), egui::Align2::CENTER_CENTER, label, egui::FontId::proportional(11.0), t.text);
}
// One row = `cols` slots (a category label always starts a new row).
// One row = `cols` slots (a category label always starts a new row; without labels,
// nothing does).
let mut row = vec![i];
while row.len() < cols && i + row.len() < all.len() && all[i + row.len()].0.is_none() {
while row.len() < cols && i + row.len() < all.len() && (all[i + row.len()].0.is_none() || !labels) {
row.push(i + row.len());
}
ui.horizontal(|ui| {
@ -443,38 +445,44 @@ fn crop_options(app: &mut VectorcraftApp, ui: &mut Ui) {
/// The Fill/Stroke proxy, Color/Gradient/None, the drawing modes, screen mode and Edit Toolbar, as
/// large as the toolbar's `cols` (one or two) leave room for (#1129): in two columns the proxy and
/// buttons grow and the three drawing modes show side by side.
/// The All Tools drawer's Show section hides any of them but Edit Toolbar.
fn bottom_controls(app: &mut VectorcraftApp, ui: &mut Ui, t: &Tokens, cols: usize) {
let two = cols == 2;
ui.vertical_centered(|ui| crate::panels::proxy(app, ui, if two { 54.0 } else { 36.0 }));
ui.add_space(5.0);
let show = app.ui.toolbar_show;
if show.fill_stroke {
ui.vertical_centered(|ui| crate::panels::proxy(app, ui, if two { 54.0 } else { 36.0 }));
ui.add_space(5.0);
}
// Color (the last solid colour), Gradient (the last gradient) and None, as commands.
let (chip, gap) = if two { (17.0, 4.0) } else { (11.0, 2.0) };
ui.horizontal(|ui| {
ui.add_space((ui.available_width() - 3.0 * chip - 2.0 * gap) / 2.0);
ui.spacing_mut().item_spacing.x = gap;
let mut clicked = None;
for (tip, cmd) in [("Color (,)", "paint.lastColor"), ("Gradient (.)", "paint.lastGradient"), ("None (/)", "paint.none")] {
let (r, resp) = ui.allocate_exact_size(vec2(chip, chip), Sense::click());
match cmd {
"paint.lastColor" => widgets::paint_chip(ui, r, &Paint::solid(app.session.last_solid)),
"paint.lastGradient" => widgets::gradient_chip(ui, r, &app.session.last_gradient.gradient),
_ => widgets::paint_chip(ui, r, &Paint::None),
if show.coloring {
ui.horizontal(|ui| {
ui.add_space((ui.available_width() - 3.0 * chip - 2.0 * gap) / 2.0);
ui.spacing_mut().item_spacing.x = gap;
let mut clicked = None;
for (tip, cmd) in [("Color (,)", "paint.lastColor"), ("Gradient (.)", "paint.lastGradient"), ("None (/)", "paint.none")] {
let (r, resp) = ui.allocate_exact_size(vec2(chip, chip), Sense::click());
match cmd {
"paint.lastColor" => widgets::paint_chip(ui, r, &Paint::solid(app.session.last_solid)),
"paint.lastGradient" => widgets::gradient_chip(ui, r, &app.session.last_gradient.gradient),
_ => widgets::paint_chip(ui, r, &Paint::None),
}
if resp.on_hover_text(tl!(tip)).clicked() {
clicked = Some(cmd);
}
}
if resp.on_hover_text(tl!(tip)).clicked() {
clicked = Some(cmd);
if let Some(cmd) = clicked {
app.run(cmd, json!({})).ok();
}
}
if let Some(cmd) = clicked {
app.run(cmd, json!({})).ok();
}
});
ui.add_space(6.0);
});
ui.add_space(6.0);
}
let modes = [
(vectorcraft_engine::DrawMode::Normal, "dc-draw-normal", "Draw Normal", "normal"),
(vectorcraft_engine::DrawMode::Behind, "dc-draw-behind", "Draw Behind", "behind"),
(vectorcraft_engine::DrawMode::Inside, "dc-draw-inside", "Draw Inside", "inside"),
];
if two {
if two && show.draw_mode {
// Side by side, each one a click away.
ui.horizontal(|ui| {
ui.add_space((ui.available_width() - 3.0 * 22.0) / 2.0);
@ -488,12 +496,13 @@ fn bottom_controls(app: &mut VectorcraftApp, ui: &mut Ui, t: &Tokens, cols: usiz
ui.vertical_centered(|ui| {
// One column: the current mode's button, which cycles them.
if !two
&& show.draw_mode
&& let Some((mode, icon, name, _)) = modes.into_iter().find(|m| m.0 == app.session.draw_mode)
&& widgets::icon_button(ui, icon, &format!("{} (Shift+D)", tl!(name)), mode != vectorcraft_engine::DrawMode::Normal, 26.0).clicked()
{
app.run("view.drawMode", json!({})).ok();
}
if widgets::icon_button(ui, "dc-screen-mode", "Change Screen Mode (F)", false, 26.0).clicked() {
if show.screen_mode && widgets::icon_button(ui, "dc-screen-mode", "Change Screen Mode (F)", false, 26.0).clicked() {
app.run("view.screenMode", json!({})).ok();
}
// Edit Toolbar: the All Tools drawer opens beside the toolbar, by this button.
@ -1252,6 +1261,42 @@ pub(crate) mod tests {
lefts.len()
}
/// Rows of tool buttons by their tops: how many buttons each row holds, top to bottom.
fn row_sizes(rects: &[egui::Rect]) -> Vec<usize> {
let mut rows: Vec<(i32, usize)> = vec![];
for r in rects {
let top = r.top().round() as i32;
match rows.last_mut() {
Some((t, n)) if *t == top => *n += 1,
_ => rows.push((top, 1)),
}
}
rows.into_iter().map(|(_, n)| n).collect()
}
/// #1736: without group labels (as it ships), the tools run on from group to group: one column
/// evenly spaced, with nothing between the groups; two columns with every row full but maybe
/// the last. With the labels, each group starts its own row under its name.
#[test]
fn the_tools_run_on_without_group_labels() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 200, "height": 200})).unwrap();
let ctx = egui::Context::default();
let one = frame_in(&mut app, &ctx, 0.0, vec![], 1600.0);
assert_eq!(one.len(), slots(&app).len());
let steps: Vec<f32> = one.windows(2).map(|w| w[1].top() - w[0].top()).collect();
assert!(steps.iter().all(|d| (d - steps[0]).abs() < 0.5), "evenly spaced: {steps:?}");
app.ui.toolbar_double = true;
let rows = row_sizes(&frame_in(&mut app, &ctx, 0.5, vec![], 1600.0));
let slots = slots(&app).len();
assert_eq!(rows.iter().sum::<usize>(), slots);
assert!(rows[..rows.len() - 1].iter().all(|n| *n == 2), "{rows:?}");
assert_eq!(rows.len(), slots.div_ceil(2));
app.run("prefs.set", json!({"key": "toolGroupLabels", "value": true})).unwrap();
let labelled = row_sizes(&frame_in(&mut app, &ctx, 1.0, vec![], 1600.0));
assert!(labelled.len() > rows.len() && labelled.contains(&1), "{labelled:?}");
}
#[test]
fn the_double_arrow_toggles_one_and_two_columns_in_a_short_window() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
@ -1433,8 +1478,8 @@ pub(crate) mod tests {
assert!(r.min.x.is_finite() && r.min.y.is_finite() && r.left() >= 0.0, "on screen: {r:?}");
}
/// User Interface › Show Tool Group Labels (#663): on, the toolbar names its groups; off, it
/// shows none of the names, a faint dash between the groups instead.
/// User Interface › Show Tool Group Labels (#663): off as it ships (#1736), the toolbar shows
/// none of its groups' names and nothing between the groups; on, it names them.
#[test]
fn tool_group_labels_can_be_hidden() {
fn texts(s: &egui::Shape, out: &mut Vec<String>) {
@ -1455,11 +1500,11 @@ pub(crate) mod tests {
out.shapes.iter().for_each(|c| texts(&c.shape, &mut shown));
shown
};
let with = draw(&mut app);
assert!(with.iter().any(|t| t == "Shapes") && with.iter().any(|t| t == "Draw"), "{with:?}");
app.run("prefs.set", json!({"key": "toolGroupLabels", "value": false})).unwrap();
let without = draw(&mut app);
assert!(!without.iter().any(|t| ["Select", "Shapes", "Draw", "Modify", "Type"].contains(&t.as_str())), "{without:?}");
app.run("prefs.set", json!({"key": "toolGroupLabels", "value": true})).unwrap();
let with = draw(&mut app);
assert!(with.iter().any(|t| t == "Shapes") && with.iter().any(|t| t == "Draw"), "{with:?}");
}
/// Hover `at` for two seconds, coming from elsewhere: the texts painted meanwhile.

View File

@ -286,7 +286,12 @@ drawer beside the toolbar; Escape or a click outside closes it. While it is open
toolbar to add it (onto a button's top or bottom edge: its own slot before or after; onto its middle: into its
group), from the toolbar into it to remove it, or within the toolbar to move it. `toolbar.addTool {tool, slot?,
place?}`, `toolbar.removeTool {tool, group?}` and `toolbar.reset` do the same for the current Basic or Advanced
toolbar and return its slots; the edits (`ui.toolbar_edits`) are saved with the preferences.
toolbar and return its slots; the edits (`ui.toolbar_edits`) are saved with the preferences. The drawer lists
the tools one per row (icon, name, shortcut), each flyout group framed together under its heading; its ☰ menu
has Reset and the Basic and Advanced toolbars. Its Show section (`toolbar.show {control?, on?}`, `control` one
of `fillStroke`, `coloring`, `drawMode`, `screenMode`) shows or hides the controls under the toolbar's tools,
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).
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