Merge pull request #6090 from dotCooCoo/feat/control-bar-dock

Control bar: the panel menu and the gripper dock it to the top or the bottom of the window
This commit is contained in:
BFlat
2026-11-08 14:11:27 +08:00
committed by GitHub
44 changed files with 393 additions and 27 deletions
+3 -2
View File
@@ -534,8 +534,9 @@ impl VectorcraftApp {
if self.ui.status_bar && self.ui.screen_mode < 3 {
chrome::status_bar(self, ui);
chrome::hint_bar(self, ui);
} else {
// Keeps the previous frame's value valid for menus drawn above, before the bars.
} else if !(self.ui.control_bar && self.ui.control_bar_bottom && self.ui.screen_mode < 2) {
// Keeps the previous frame's value valid for menus drawn above, before the bars. A
// Control bar docked at the bottom has set it to the bar's top.
widgets::set_menu_floor(&ctx, ctx.content_rect().bottom());
}
if self.ui.toolbar && self.ui.screen_mode < 3 {
+1 -1
View File
@@ -20,7 +20,7 @@ mod tabs;
#[cfg(test)]
use control_bar::ISOLATION_BUTTON;
pub use control_bar::{AnchorControls, anchor_buttons, anchor_controls, control_bar};
pub use control_bar::{AnchorControls, CONTROL_BAR_EDGES, anchor_buttons, anchor_controls, control_bar};
#[cfg(test)]
use hints::hint_segments;
pub use hints::{hint_bar, hint_strings};
+81 -8
View File
@@ -261,19 +261,83 @@ fn groups_before_transform(app: &mut VectorcraftApp, ui: &mut Ui, objects: usize
true
}
/// The Control bar (Window → Control), context-sensitive like Illustrator's.
/// The edges of the window the Control bar docks to (`window.control.dock {edge}`): the top, then
/// the bottom.
pub const CONTROL_BAR_EDGES: [&str; 2] = ["top", "bottom"];
/// The side of the Control bar's panel menu button, at the bar's right end.
const MENU_BUTTON: f32 = 16.0;
/// The height of the band drawn along the edge of the window that a gripper drag docks the bar to.
const DOCK_BAND: f32 = 4.0;
/// Docks the Control bar to `edge` of [`CONTROL_BAR_EDGES`], through its command.
fn dock(app: &mut VectorcraftApp, edge: &str) {
crate::menus::invoke(app, "window.control.dock", json!({ "edge": edge }));
}
/// The Control bar's panel menu: Dock to Top and Dock to Bottom, the edge the bar is docked to
/// checked.
fn panel_menu(app: &mut VectorcraftApp, ui: &mut Ui, rect: egui::Rect) {
let resp = crate::panels::menu_button(ui, "control", rect);
egui::Popup::menu(&resp).show(|ui| {
ui.set_min_width(160.0);
for (i, label) in [tl!("Dock to Top"), tl!("Dock to Bottom")].into_iter().enumerate() {
if widgets::menu_item(ui, label, true, usize::from(app.ui.control_bar_bottom) == i) {
dock(app, CONTROL_BAR_EDGES[i]);
}
}
});
}
/// The Control bar's gripper. Dragging it shows a band along the top or the bottom edge of the
/// window, whichever the pointer is nearer, and docks the bar there on release.
fn gripper(app: &mut VectorcraftApp, ui: &mut Ui, t: &Tokens) {
let (g, resp) = ui.allocate_exact_size(vec2(6.0, 20.0), Sense::drag());
for i in 0..5 {
ui.painter().circle_filled(g.center_top() + vec2(0.0, 2.0 + i as f32 * 4.0), 0.9, t.text_disabled);
}
let resp = resp.on_hover_cursor(egui::CursorIcon::Grab);
if !resp.dragged() && !resp.drag_stopped() {
return;
}
let Some(pos) = resp.interact_pointer_pos().or_else(|| ui.ctx().pointer_latest_pos()) else { return };
let window = ui.ctx().content_rect();
let bottom = pos.y > window.center().y;
if resp.drag_stopped() {
if bottom != app.ui.control_bar_bottom {
dock(app, CONTROL_BAR_EDGES[usize::from(bottom)]);
}
return;
}
ui.ctx().set_cursor_icon(egui::CursorIcon::Grabbing);
let band = if bottom {
egui::Rect::from_min_max(egui::pos2(window.left(), window.bottom() - DOCK_BAND), window.max)
} else {
egui::Rect::from_min_max(window.min, egui::pos2(window.right(), window.top() + DOCK_BAND))
};
let layer = egui::LayerId::new(egui::Order::Foreground, egui::Id::unique("control_bar_dock_band"));
ui.ctx().layer_painter(layer).rect_filled(band, 0.0, t.accent);
}
/// The Control bar (Window → Control), context-sensitive like Illustrator's, docked to the top or
/// the bottom of the window.
pub fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) {
let t = Tokens::get(ui.ctx());
egui::Panel::top("control_bar")
let bottom = app.ui.control_bar_bottom;
let panel = if bottom { egui::Panel::bottom("control_bar") } else { egui::Panel::top("control_bar") };
let bar = panel
.exact_size(34.0)
.frame(egui::Frame::NONE.fill(t.panel).inner_margin(egui::Margin::symmetric(10, 0)).stroke(Stroke::new(1.0, t.border)))
.show(ui, |ui| {
ui.horizontal_centered(|ui| {
// Gripper
let (g, _) = ui.allocate_exact_size(vec2(6.0, 20.0), Sense::hover());
for i in 0..5 {
ui.painter().circle_filled(g.center_top() + vec2(0.0, 2.0 + i as f32 * 4.0), 0.9, t.text_disabled);
}
// The panel menu button takes the right end, and the controls trim to the room left of it:
// they are laid out in that room, so every room check below leaves the button's width.
let full = ui.max_rect();
let menu = egui::Rect::from_center_size(egui::pos2(full.right() - MENU_BUTTON / 2.0, full.center().y), vec2(MENU_BUTTON, MENU_BUTTON));
let room = egui::Rect::from_min_max(full.min, egui::pos2(menu.left() - ui.spacing().item_spacing.x, full.bottom()));
let layout = egui::Layout::left_to_right(egui::Align::Center);
let controls = ui.scope_builder(egui::UiBuilder::new().max_rect(room).layout(layout), |ui| {
gripper(app, ui, &t);
if app.session.active().is_none() {
ui.label(egui::RichText::new(tl!("No Document")).color(t.text_dim));
return;
@@ -405,5 +469,14 @@ pub fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) {
}
}
});
// Controls that keep their size can still run into the button's room in a narrow window;
// the button is left out then.
if controls.response.rect.right() <= menu.left() {
panel_menu(app, ui, menu);
}
});
// Docked at the bottom, the bar is a bottom bar that menus stop above.
if bottom {
widgets::set_menu_floor(ui.ctx(), bar.response.rect.top());
}
}
+144 -3
View File
@@ -1801,6 +1801,139 @@ fn full_screen_modes_fill_the_screen_and_normal_mode_restores_the_window() {
assert!(sent(&mut app, true).is_empty(), "still full screen, as the user had it");
}
/// The window the Control bar docking tests draw the bar in.
const DOCK_SCREEN: egui::Rect = egui::Rect { min: egui::Pos2::ZERO, max: egui::pos2(1200.0, 300.0) };
/// One headless frame of the Control bar alone; returns its accessibility nodes.
fn control_bar_frame(app: &mut crate::VectorcraftApp, ctx: &egui::Context, events: Vec<egui::Event>) -> Vec<egui::accesskit::Node> {
let mut out = ctx.run_ui(egui::RawInput { events, screen_rect: Some(DOCK_SCREEN), ..Default::default() }, |ui| super::control_bar(app, ui));
out.textures_delta.clear();
out.platform_output.accesskit_update.map(|u| u.nodes.into_iter().map(|(_, n)| n).collect()).unwrap_or_default()
}
/// The center of the bounds of the node whose label, without a check mark, is `name`.
fn node_center(nodes: &[egui::accesskit::Node], name: &str) -> Option<egui::Pos2> {
let n = nodes.iter().find(|n| n.label().is_some_and(|l| l.trim_start_matches('✓').trim() == name))?;
let b = n.bounds()?;
Some(egui::pos2(((b.x0 + b.x1) / 2.0) as f32, ((b.y0 + b.y1) / 2.0) as f32))
}
/// A primary-button press or release at `pos`.
fn press_at(pos: egui::Pos2, pressed: bool) -> egui::Event {
egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() }
}
/// The Control bar's panel menu has Dock to Top and Dock to Bottom, the edge in use checked, and
/// each docks the bar to that edge of the window.
#[test]
fn the_control_bar_panel_menu_docks_it_to_the_top_or_the_bottom() {
crate::i18n::set_current(crate::i18n::Lang::EN);
let mut app = crate::VectorcraftApp::new(Session::new(), Default::default());
let ctx = egui::Context::default();
ctx.enable_accesskit();
crate::theme::install_fonts(&ctx);
let pick = |app: &mut crate::VectorcraftApp, item: &str| {
control_bar_frame(app, &ctx, vec![]);
let menu = node_center(&control_bar_frame(app, &ctx, vec![]), "Panel menu").unwrap_or_else(|| panic!("the bar has a panel menu"));
control_bar_frame(app, &ctx, vec![egui::Event::PointerMoved(menu), press_at(menu, true)]);
control_bar_frame(app, &ctx, vec![press_at(menu, false)]);
let nodes = control_bar_frame(app, &ctx, vec![]);
let checked: Vec<String> = nodes.iter().filter_map(|n| n.label()).filter(|l| l.starts_with('✓')).map(str::to_string).collect();
let at = node_center(&nodes, item).unwrap_or_else(|| panic!("the menu lists {item}"));
// Below the button at the top, and above it at the bottom: the menu opens inside the window.
assert!(DOCK_SCREEN.contains(at) && (at.y < menu.y) == app.ui.control_bar_bottom, "{item} at {at:?}, the button at {menu:?}");
control_bar_frame(app, &ctx, vec![egui::Event::PointerMoved(at), press_at(at, true)]);
control_bar_frame(app, &ctx, vec![press_at(at, false)]);
control_bar_frame(app, &ctx, vec![]);
let menu = node_center(&control_bar_frame(app, &ctx, vec![]), "Panel menu").unwrap_or_else(|| panic!("the bar still shows"));
(checked, menu.y)
};
let (checked, y) = pick(&mut app, "Dock to Bottom");
assert_eq!(checked, ["✓ Dock to Top"], "the bar starts docked at the top");
assert!(app.ui.control_bar_bottom, "Dock to Bottom docks it at the bottom");
assert!(y > DOCK_SCREEN.bottom() - 34.0, "the bar is drawn along the bottom of the window: {y}");
let (checked, y) = pick(&mut app, "Dock to Top");
assert_eq!(checked, ["✓ Dock to Bottom"]);
assert!(!app.ui.control_bar_bottom && y < 34.0, "Dock to Top docks it at the top again: {y}");
}
/// Dragging the Control bar's gripper docks the bar at the edge of the window the pointer is
/// released nearer; a drag released nearer the edge it is at leaves it there.
#[test]
fn dragging_the_control_bar_gripper_docks_it_at_the_nearer_edge() {
let mut app = crate::VectorcraftApp::new(Session::new(), Default::default());
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
// The gripper is the bar's first item, inside its 10-point margin, centered in the 34-point bar.
let grip = |bottom: bool| egui::pos2(13.0, if bottom { DOCK_SCREEN.bottom() - 17.0 } else { 17.0 });
let drag = |app: &mut crate::VectorcraftApp, from: egui::Pos2, to: egui::Pos2| {
control_bar_frame(app, &ctx, vec![]);
control_bar_frame(app, &ctx, vec![egui::Event::PointerMoved(from), press_at(from, true)]);
for k in 1..=4 {
control_bar_frame(app, &ctx, vec![egui::Event::PointerMoved(from.lerp(to, k as f32 / 4.0))]);
}
control_bar_frame(app, &ctx, vec![press_at(to, false)]);
};
drag(&mut app, grip(false), egui::pos2(400.0, 200.0));
assert!(app.ui.control_bar_bottom, "released nearer the bottom: docked at the bottom");
drag(&mut app, grip(true), egui::pos2(400.0, 160.0));
assert!(app.ui.control_bar_bottom, "released nearer the bottom again: still at the bottom");
drag(&mut app, grip(true), egui::pos2(400.0, 120.0));
assert!(!app.ui.control_bar_bottom, "released nearer the top: docked at the top");
drag(&mut app, grip(false), egui::pos2(400.0, 140.0));
assert!(!app.ui.control_bar_bottom, "released nearer the top again: still at the top");
}
/// `window.control.dock {edge}` docks the Control bar to the top or the bottom of the window, and
/// the panel menu checks the edge in use; another edge, or none, is an error and changes nothing.
#[test]
fn the_control_bar_dock_command_takes_top_or_bottom() {
let mut app = crate::VectorcraftApp::new(Session::new(), Default::default());
assert_eq!(app.run("window.control.dock", json!({"edge": "bottom"})).unwrap(), json!({"edge": "bottom"}));
assert!(app.ui.control_bar_bottom);
assert_eq!(crate::menus::checked(&app, "window.control.dock", &json!({"edge": "bottom"})), Some(true));
assert_eq!(crate::menus::checked(&app, "window.control.dock", &json!({"edge": "top"})), Some(false));
for p in [json!({"edge": "left"}), json!({"edge": "Bottom"}), json!({"edge": 1}), json!({})] {
let err = app.run("window.control.dock", p.clone()).unwrap_err();
assert_eq!(err, "`edge` must be one of top, bottom", "{p}");
assert!(app.ui.control_bar_bottom, "{p} leaves the bar at the bottom");
}
assert_eq!(app.run("window.control.dock", json!({"edge": "top"})).unwrap(), json!({"edge": "top"}));
assert!(!app.ui.control_bar_bottom);
}
/// Menus stop above a Control bar docked at the bottom when the status bar is hidden, above the
/// status and hint bars when they show, and at the bottom of the window when the bar is docked at
/// the top or a screen mode hides it.
#[test]
fn menus_stop_above_a_control_bar_docked_at_the_bottom() {
let mut app = crate::VectorcraftApp::new(Session::new(), Default::default());
let ctx = egui::Context::default();
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(800.0, 600.0));
let floor = |app: &mut crate::VectorcraftApp, status: bool, bottom: bool, screen_mode: u8| {
(app.ui.status_bar, app.ui.control_bar, app.ui.control_bar_bottom, app.ui.screen_mode) = (status, true, bottom, screen_mode);
for _ in 0..3 {
let mut out = ctx.run_ui(egui::RawInput { screen_rect: Some(screen), ..Default::default() }, |ui| {
app.logic(ui.ctx());
app.ui(ui);
});
out.textures_delta.clear();
}
crate::widgets::menu_floor(&ctx)
};
let bar_top = screen.bottom() - 34.0;
let f = floor(&mut app, false, true, 0);
assert!((f - bar_top).abs() < 1.0, "status bar hidden: menus stop at the Control bar's top, {f}");
let f = floor(&mut app, false, true, 2);
assert_eq!(f, screen.bottom(), "screen mode 2 hides the Control bar: menus stop at the window's bottom");
let f = floor(&mut app, false, true, 0);
assert!((f - bar_top).abs() < 1.0, "shown again: {f}");
let f = floor(&mut app, false, false, 0);
assert_eq!(f, screen.bottom(), "docked at the top: menus stop at the window's bottom");
let f = floor(&mut app, true, true, 0);
assert!(f < bar_top - 1.0, "the status and hint bars show above the Control bar: menus stop above them, {f}");
}
/// Two or more selected objects show all six Distribute Objects buttons in the Control bar;
/// a single object keeps the bar as it was. A click distributes as the Align panel's button does.
#[test]
@@ -1955,6 +2088,7 @@ fn the_control_bar_shows_the_inline_transform_fields_only_when_they_fit() {
ctx.enable_accesskit();
crate::theme::install_fonts(&ctx);
let mut seen = false;
let mut menu_seen = false;
// The Align group, then for two objects the Distribute group (#5372): each by its controls.
let mut groups: Vec<(&str, Vec<&str>)> = vec![(
"Align",
@@ -2002,16 +2136,22 @@ fn the_control_bar_shows_the_inline_transform_fields_only_when_they_fit() {
let fields = ["X:", "Y:", "W:", "H:"].map(right);
assert!(fields.iter().all(Option::is_some) || fields.iter().all(Option::is_none), "{count} objects, {width}: {fields:?}");
assert!(nodes.iter().any(|(_, n)| n.label() == Some("Transform")), "{count} objects, {width}: the Transform link shows");
// The bar's content ends at its 10-point inner margin.
// The panel menu button takes the bar's right end, and the bar's content ends before it
// (without the button, at the bar's 10-point inner margin). Once a bar shows the button,
// a wider one keeps it: the fields and groups trim to the room left of it.
let menu = nodes.iter().find(|(_, n)| n.label() == Some("Panel menu")).and_then(|(_, n)| n.bounds());
assert!(!menu_seen || menu.is_some(), "{count} objects, {width}: the panel menu button left out after a narrower bar showed it");
menu_seen |= menu.is_some();
let edge = menu.map_or(f64::from(width) - 10.0, |m| m.x0);
if let Some(h) = fields[3] {
assert!(h <= f64::from(width) - 10.0, "{count} objects, {width}: H ends at {h}");
assert!(h <= edge, "{count} objects, {width}: H ends at {h}, the content's room at {edge}");
}
assert!(!seen || fields[3].is_some(), "{count} objects, {width}: left out after a narrower bar showed them");
seen |= fields[3].is_some();
// The Align and Distribute buttons show whole inside the margin, with the Transform
// link after them, or are left out; once shown, a wider bar keeps them.
let end = |name: &str| nodes.iter().find(|(_, n)| n.label() == Some(name)).and_then(|(_, n)| n.bounds()).map(|b| b.x1);
let inside = |x: f64| x <= f64::from(width) - 10.0;
let inside = |x: f64| x <= edge;
for ((group, names), was) in groups.iter().zip(groups_seen.iter_mut()) {
let ends: Vec<Option<f64>> = names.iter().map(|n| end(n)).collect();
let shown = ends.iter().all(Option::is_some);
@@ -2026,6 +2166,7 @@ fn the_control_bar_shows_the_inline_transform_fields_only_when_they_fit() {
}
}
assert!(seen, "{count} objects: a wide bar has room for the fields");
assert!(menu_seen, "{count} objects: a wide bar has room for the panel menu button");
assert!(groups_seen.iter().all(|s| *s), "{count} objects: a wide bar shows every group: {groups_seen:?}");
}
}
+3
View File
@@ -3848,6 +3848,9 @@ axis Both И двете
Put back in the toolbar Връщане в лентата с инструменти
Float Panel Плаващ панел
Dock Panel Закотвен панел
Dock to Top Закотвяне горе
Dock to Bottom Закотвяне долу
Dock Control Bar Закотвяне на контролната лента
Drag to float the toolbar Плъзнете, за да направите лентата с инструменти плаваща
Drag to the window's left edge to dock Плъзнете до левия ръб на прозореца за закотвяне
Float Tool Group Плаваща група инструменти
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -809,6 +809,9 @@
Put back in the toolbar Vrati u traku s alatima
Float Panel Odvoji panel
Dock Panel Usidri panel
Dock to Top Usidri na vrh
Dock to Bottom Usidri na dno
Dock Control Bar Usidri kontrolnu traku
Float Tool Group Odvoji grupu alata
Services Usluge
Hide VectorCraft Sakrij VectorCraft
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -1166,6 +1166,9 @@
Do Not Scale No escalis
Do you want to save the changes you made to “{name}”? Voleu desar els canvis que heu fet a “{name}”?
Dock Panel Acobla el panell
Dock to Top Acobla a dalt
Dock to Bottom Acobla a baix
Dock Control Bar Acobla la barra de control
Document Document
Document Color Mode Mode de color del document
Document Info Informació del document
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -819,6 +819,9 @@
Put back in the toolbar Vrátit do panelu nástrojů
Float Panel Plovoucí panel
Dock Panel Ukotvit panel
Dock to Top Ukotvit nahoru
Dock to Bottom Ukotvit dolů
Dock Control Bar Ukotvit ovládací panel
Float Tool Group Plovoucí skupina nástrojů
# The macOS menu bar (native_menu): the app and Window menus' system items, Settings pages.
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -753,6 +753,9 @@
Put back in the toolbar Sæt tilbage på værktøjslinjen
Float Panel Flydende panel
Dock Panel Dok panel
Dock to Top Dok øverst
Dock to Bottom Dok nederst
Dock Control Bar Dok kontrollinjen
Float Tool Group Flydende værktøjsgruppe
Services Tjenester
Hide VectorCraft Skjul VectorCraft
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -3851,6 +3851,9 @@ theme Appearance Erscheinungsbild
Put back in the toolbar Zurück in die Werkzeugleiste setzen
Float Panel Bedienfeld ablösen
Dock Panel Bedienfeld andocken
Dock to Top Oben andocken
Dock to Bottom Unten andocken
Dock Control Bar Steuerungsleiste andocken
Drag to float the toolbar Ziehen, um die Werkzeugleiste abzulösen
Drag to the window's left edge to dock Zum Andocken an den linken Fensterrand ziehen
Float Tool Group Werkzeuggruppe ablösen
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -2139,6 +2139,9 @@ axis Vertical Κατακόρυφα
axis Both Και τα δύο
Float Panel Αιωρούμενο πάνελ
Dock Panel Αγκύρωση πάνελ
Dock to Top Αγκύρωση επάνω
Dock to Bottom Αγκύρωση κάτω
Dock Control Bar Αγκύρωση γραμμής ελέγχου
@msg no library `{id}` Δεν υπάρχει βιβλιοθήκη με αναγνωριστικό `{id}`
@msg select some type first Επιλέξτε πρώτα κάποιο κείμενο
@msg kerning must be a number, "auto", or "optical" Η διαγραμμάτωση πρέπει να είναι αριθμός, «auto» ή «optical»
Can't render this file because it contains an unexpected character in line 163 and column 5.
+3
View File
@@ -3936,6 +3936,9 @@ axis Both Ambos
Put back in the toolbar Devolver a la barra de herramientas
Float Panel Hacer flotar el panel
Dock Panel Anclar panel
Dock to Top Anclar arriba
Dock to Bottom Anclar abajo
Dock Control Bar Anclar barra de control
Drag to float the toolbar Arrastrar para hacer flotar la barra de herramientas
Drag to the window's left edge to dock Arrastrar al borde izquierdo de la ventana para anclar
Float Tool Group Hacer flotar el grupo de herramientas
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -807,6 +807,9 @@
Put back in the toolbar Palauta työkaluriviin
Float Panel Kelluta paneeli
Dock Panel Telakoi paneeli
Dock to Top Telakoi ylös
Dock to Bottom Telakoi alas
Dock Control Bar Telakoi ohjauspalkki
Float Tool Group Kelluta työkaluryhmä
Services Palvelut
Hide VectorCraft Piilota VectorCraft
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -3926,6 +3926,9 @@ axis Both Les deux
Put back in the toolbar Remettre dans la barre d’outils
Float Panel Rendre le panneau flottant
Dock Panel Ancrer le panneau
Dock to Top Ancrer en haut
Dock to Bottom Ancrer en bas
Dock Control Bar Ancrer la barre de contrôle
Drag to float the toolbar Faire glisser pour rendre la barre d’outils flottante
Drag to the window's left edge to dock Faire glisser vers le bord gauche de la fenêtre pour ancrer
Float Tool Group Rendre le groupe d’outils flottant
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -1300,6 +1300,9 @@
Do Not Scale स्केल न करें
Do you want to save the changes you made to “{name}”? क्या आप "{name}" में किए गए परिवर्तनों को सेव करना चाहते हैं?
Dock Panel पैनल डॉक करें
Dock to Top ऊपर डॉक करें
Dock to Bottom नीचे डॉक करें
Dock Control Bar नियंत्रण बार डॉक करें
Document डॉक्यूमेंट
Document Color Mode डॉक्यूमेंट रंग मोड
Document Health Check डॉक्यूमेंट स्वास्थ्य जांच
Can't render this file because it is too large.
+3
View File
@@ -1073,6 +1073,9 @@ font-technology Variable Változó
Do Not Scale Nincs méretezés
Do you want to save the changes you made to “{name}”? Mented a módosításokat ebben: „{name}”?
Dock Panel Panel dokkolása
Dock to Top Dokkolás felülre
Dock to Bottom Dokkolás alulra
Dock Control Bar Vezérlősáv dokkolása
Document Dokumentum
Document Color Mode Dokumentum színmódja
Document Info Dokumentuminformáció
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -753,6 +753,9 @@
Put back in the toolbar Kembalikan ke bilah alat
Float Panel Mengambangkan panel
Dock Panel Tambatkan panel
Dock to Top Tambatkan ke atas
Dock to Bottom Tambatkan ke bawah
Dock Control Bar Tambatkan bilah kontrol
Float Tool Group Mengambangkan grup alat
Services Layanan
Hide VectorCraft Sembunyikan VectorCraft
1 # Indonesian (id) catalog. Same format as zh-hant.tsv: context <TAB> source <TAB> translation
753
754
755
756
757
758
759
760
761
+3
View File
@@ -3978,6 +3978,9 @@ axis Both Entrambi
Put back in the toolbar Riporta nella barra degli strumenti
Float Panel Rendi mobile pannello
Dock Panel Ancora pannello
Dock to Top Ancora in alto
Dock to Bottom Ancora in basso
Dock Control Bar Ancora barra di controllo
Drag to float the toolbar Trascina per rendere mobile la barra degli strumenti
Drag to the window's left edge to dock Trascina sul bordo sinistro della finestra per ancorare
Float Tool Group Rendi mobile gruppo strumenti
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -3920,6 +3920,9 @@ axis Both 両方
Put back in the toolbar ツールバーに戻す
Float Panel パネルをフローティング
Dock Panel パネルをドッキング
Dock to Top 上部にドッキング
Dock to Bottom 下部にドッキング
Dock Control Bar コントロールバーをドッキング
Drag to float the toolbar ドラッグしてツールバーをフローティング
Drag to the window's left edge to dock ウィンドウの左端までドラッグしてドッキング
Float Tool Group ツールグループをフローティング
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -703,6 +703,9 @@
Put back in the toolbar ხელსაწყოთა ზოლში დაბრუნება
Float Panel მცურავი პანელი
Dock Panel პანელის მიმაგრება
Dock to Top ზემოთ მიმაგრება
Dock to Bottom ქვემოთ მიმაგრება
Dock Control Bar მართვის ზოლის მიმაგრება
Float Tool Group მცურავი ხელსაწყოთა ჯგუფი
Services სერვისები
Hide VectorCraft VectorCraft-ის დამალვა
Can't render this file because it is too large.
+3
View File
@@ -3904,6 +3904,9 @@ axis Both 양쪽
Put back in the toolbar 도구 모음으로 되돌리기
Float Panel 패널 분리
Dock Panel 패널 고정
Dock to Top 위쪽에 고정
Dock to Bottom 아래쪽에 고정
Dock Control Bar 컨트롤 막대 고정
Drag to float the toolbar 드래그하여 도구 모음 분리
Drag to the window's left edge to dock 창 왼쪽 가장자리로 드래그하여 고정
Float Tool Group 도구 그룹 분리
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -767,6 +767,9 @@
Put back in the toolbar Terug in de werkbalk zetten
Float Panel Paneel laten zweven
Dock Panel Paneel dokken
Dock to Top Bovenaan dokken
Dock to Bottom Onderaan dokken
Dock Control Bar Bedieningsbalk dokken
Float Tool Group Gereedschapsgroep laten zweven
Services Diensten
Hide VectorCraft VectorCraft verbergen
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -765,6 +765,9 @@
Put back in the toolbar Odłóż z powrotem na pasek narzędzi
Float Panel Odepnij panel
Dock Panel Zadokuj panel
Dock to Top Zadokuj u góry
Dock to Bottom Zadokuj u dołu
Dock Control Bar Zadokuj pasek sterowania
Float Tool Group Odepnij grupę narzędzi
Services Usługi
Hide VectorCraft Ukryj VectorCraft
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -3192,6 +3192,9 @@ imageTrace Outline Contorno
Put back in the toolbar Devolver à barra de ferramentas
Float Panel Flutuar Painel
Dock Panel Encaixar Painel
Dock to Top Encaixar na Parte Superior
Dock to Bottom Encaixar na Parte Inferior
Dock Control Bar Encaixar Barra de Controle
Float Tool Group Flutuar Grupo de Ferramentas
Services Serviços
Hide VectorCraft Ocultar o VectorCraft
Can't render this file because it contains an unexpected character in line 6 and column 47.
+3
View File
@@ -3973,6 +3973,9 @@ axis Both Оба
Put back in the toolbar Вернуть на панель инструментов
Float Panel Сделать панель плавающей
Dock Panel Прикрепить панель
Dock to Top Прикрепить сверху
Dock to Bottom Прикрепить снизу
Dock Control Bar Прикрепить панель управления
Drag to float the toolbar Перетащите, чтобы сделать панель инструментов плавающей
Drag to the window's left edge to dock Перетащите к левому краю окна для прикрепления
Float Tool Group Сделать группу инструментов плавающей
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -3853,6 +3853,9 @@ axis Both Båda
Put back in the toolbar Lägg tillbaka i verktygsfältet
Float Panel Gör panelen flytande
Dock Panel Docka panel
Dock to Top Docka överst
Dock to Bottom Docka nederst
Dock Control Bar Docka kontrollfältet
Drag to float the toolbar Dra för att göra verktygsfältet flytande
Drag to the window's left edge to dock Dra till fönstrets vänsterkant för att docka
Float Tool Group Gör verktygsgruppen flytande
Can't render this file because it contains an unexpected character in line 5 and column 25.
+3
View File
@@ -3741,6 +3741,9 @@ axis Both Her İkisi
Put back in the toolbar Araç çubuğuna geri yerleştir
Float Panel Paneli Yüzer Yap
Dock Panel Bağlantı İstasyonu Paneli
Dock to Top Üste yerleştir
Dock to Bottom Alta yerleştir
Dock Control Bar Kontrol çubuğunu yerleştir
Drag to float the toolbar Araç çubuğunu ayırmak için sürükleyin
Drag to the window's left edge to dock Sabitlemek için pencerenin sol kenarına sürükleyin
Float Tool Group Araç Grubunu Yüzer Yap
Can't render this file because it contains an unexpected character in line 5 and column 25.
+3
View File
@@ -3967,6 +3967,9 @@ axis Both Обидві
Put back in the toolbar Повернути на панель інструментів
Float Panel Від’єднати панель
Dock Panel Прикріпити панель
Dock to Top Прикріпити вгорі
Dock to Bottom Прикріпити внизу
Dock Control Bar Прикріпити панель керування
Drag to float the toolbar Перетягніть, щоб від’єднати панель інструментів
Drag to the window's left edge to dock Перетягніть до лівого краю вікна для прикріплення
Float Tool Group Від’єднати групу інструментів
Can't render this file because it contains an unexpected character in line 177 and column 5.
+3
View File
@@ -963,6 +963,9 @@ font-technology Variable Biến thiên
Do Not Move Không di chuyển
Do you want to save the changes you made to “{name}”? Bạn có muốn lưu các thay đổi đã thực hiện với “{name}”?
Dock Panel Neo bảng
Dock to Top Neo lên trên
Dock to Bottom Neo xuống dưới
Dock Control Bar Neo thanh điều khiển
Document Tài liệu
Document Color Mode Chế độ màu tài liệu
Document Info Thông tin tài liệu
Can't render this file because it contains an unexpected character in line 199 and column 5.
+3
View File
@@ -5042,6 +5042,9 @@ axis Both 两者
Put back in the toolbar 放回工具栏
Float Panel 浮动面板
Dock Panel 停放面板
Dock to Top 停放到顶部
Dock to Bottom 停放到底部
Dock Control Bar 停放控制栏
Drag to float the toolbar 拖动以浮动工具栏
Drag to the window's left edge to dock 拖到窗口左边缘以停放
Float Tool Group 浮动工具组
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -4853,6 +4853,9 @@ axis Both 兩者
Put back in the toolbar 放回工具列
Float Panel 浮動面板
Dock Panel 停駐面板
Dock to Top 停駐到頂部
Dock to Bottom 停駐到底部
Dock Control Bar 停駐控制列
Drag to float the toolbar 拖曳以浮動工具列
Drag to the window's left edge to dock 拖曳到視窗左側邊緣以停駐
Float Tool Group 浮動工具組
Can't render this file because it contains an unexpected character in line 6 and column 47.
+12
View File
@@ -448,6 +448,18 @@ fn run_ui_part5(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<Result<
Ok(Value::Null)
}
"window.control" => flag(&mut app.ui.control_bar),
// The Control bar's panel menu and gripper: the edge of the window the bar docks to.
"window.control.dock" => {
let key = "edge";
let edges = crate::chrome::CONTROL_BAR_EDGES;
match edges.iter().position(|e| s(key).as_deref() == Some(*e)) {
Some(i) => {
app.ui.control_bar_bottom = i == 1;
Ok(json!({ key: edges[i] }))
}
None => Err(format!("`{key}` must be one of {}", edges.join(", "))),
}
}
"window.toolbar" => flag(&mut app.ui.toolbar),
"window.toolbarColumns" => {
let toolbar = match crate::custom_toolbars::target(&app.ui, p) {
+3
View File
@@ -59,6 +59,9 @@ pub fn checked(app: &VectorcraftApp, id: &str, p: &Value) -> Option<bool> {
// a document no item is checked.
"view.rotate" => app.view().zip(p.get("angle").and_then(Value::as_f64)).is_some_and(|(view, a)| (view.rotation - a).abs() < 1e-9),
"window.control" => app.ui.control_bar,
"window.control.dock" => {
p.get("edge").and_then(Value::as_str) == Some(crate::chrome::CONTROL_BAR_EDGES[usize::from(app.ui.control_bar_bottom)])
}
"window.toolbar" => app.ui.toolbar,
"window.toolbarAdvanced" => app.ui.toolbar_advanced,
"window.toolbarColumns" => match p.get("toolbar").and_then(Value::as_str) {
+6
View File
@@ -447,6 +447,12 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
),
("view.rotateReset", "Reset Rotate View", "Cmd+Shift+1", "{}"),
("window.control", "Control", "", "{}"),
(
"window.control.dock",
"Dock Control Bar",
"",
"{edge: \"top\"|\"bottom\"} dock the Control bar to the top of the window, under the menu bar, or to its bottom, as the bar's panel menu (Dock to Top, Dock to Bottom) and a drag of its gripper do; kept with the workspace; another edge is an error and changes nothing; returns {edge}",
),
("window.toolbar", "Tools", "", "{}"),
(
"window.toolbarColumns",
+1 -1
View File
@@ -242,7 +242,7 @@ pub fn panel_menu_items(app: &mut VectorcraftApp, ui: &mut Ui, id: &str) -> bool
}
/// The ≡ button drawn into `rect` that opens a panel menu.
fn menu_button(ui: &mut Ui, id: &str, rect: Rect) -> egui::Response {
pub(crate) fn menu_button(ui: &mut Ui, id: &str, rect: Rect) -> egui::Response {
let t = Tokens::get(ui.ctx());
let resp = ui.interact(rect, ui.scope_id().with(("panel-menu", id)), Sense::click());
icons::paint(ui, "menu", rect.shrink(1.0), if resp.hovered() { t.text_strong } else { t.text_dim });
+10 -4
View File
@@ -48,10 +48,15 @@ const ART_OPTIONS_WIDTH: f32 = 370.0;
/// less, they show only in the Transform link's popover.
const INLINE_FIELDS_WIDTH: f32 = 480.0;
/// The room the Control bar needs from the artboard's name field to Move and Scale Artwork with
/// Artboard shown as their boxes; with less, the name field narrows from 120 to 100 points.
const NAME_FIELD_ROOM: f32 = 330.0;
/// The Control bar while the Artboard tool is in use, in place of the object controls: the active
/// artboard's preset and orientation, New Artboard and Delete Artboard, its name, Artboard Options,
/// Rearrange All Artboards and the Transform link (the Transform panel in a popover), which all fit
/// in the narrowest window; then Move and Scale Artwork with Artboard (the boxes alone, with their
/// artboard's preset and orientation, New Artboard and Delete Artboard, its name (in a narrower
/// field when the bar is short of room), Artboard Options, Rearrange All Artboards and the Transform
/// link (the Transform panel in a popover), which all fit in the narrowest window, left of the bar's
/// panel menu button; then Move and Scale Artwork with Artboard (the boxes alone, with their
/// names in tooltips, when the bar is short of room) and, while the bar has room, the reference
/// point the Transform panel also uses with the artboard's X and Y, and its W and H with the
/// Constrain Width and Height Proportions link. Each control runs the command its Properties panel
@@ -77,7 +82,8 @@ pub(crate) fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) -> bool {
// The field shows the name as it is (names are never translated). A new name is applied on Enter
// or a click elsewhere, and an empty field or Escape keeps the old one, as in the Properties panel.
ui.label(egui::RichText::new(tl!("Name:")).size(12.0).color(t.text));
if let Some(name) = widgets::text_field_named(ui, ("cb-ab", "name"), Some(tl!("Name:")), Some(&ab.name), 120.0, 1).filter(|n| !n.is_empty()) {
let width = if ui.available_width() < NAME_FIELD_ROOM { 100.0 } else { 120.0 };
if let Some(name) = widgets::text_field_named(ui, ("cb-ab", "name"), Some(tl!("Name:")), Some(&ab.name), width, 1).filter(|n| !n.is_empty()) {
app.run("artboard.setProps", json!({"index": i, "name": name})).ok();
}
if widgets::icon_button(ui, "dc-artboard-options", tl!("Artboard Options"), false, 24.0).clicked() {
+7 -3
View File
@@ -849,11 +849,15 @@ fn the_control_bar_shows_the_artboard_tools_controls() {
assert!(!has(&texts, "Snapping"), "{texts:?}");
let link = text_rect(&texts, "Transform");
assert!(link.right() < 790.0 && text_rect(&texts, "Artboard 1").right() < link.left(), "{texts:?}");
// Move and Scale Artwork with Artboard show as their boxes alone, inside the window, and the
// second one turns Scale Artwork with Artboard on.
// Move and Scale Artwork with Artboard show as their boxes alone, inside the window and left of
// the panel menu button at the bar's right end, and the second one turns Scale Artwork with
// Artboard on.
assert!(!has(&texts, "Move Artwork with Artboard"), "{texts:?}");
let boxes = bar_buttons(&ctx, link.right(), 790.0);
let mut boxes = bar_buttons(&ctx, link.right(), 790.0);
let menu = boxes.pop().filter(|r| (r.width() - 16.0).abs() < 0.5 && r.right() > 785.0);
let menu = menu.unwrap_or_else(|| panic!("the panel menu button ends the bar: {boxes:?}"));
assert_eq!(boxes.len(), 2, "{boxes:?}");
assert!(boxes[1].right() <= menu.left(), "{boxes:?} and the panel menu button at {menu:?}");
let scale = |app: &VectorcraftApp| app.session.tool_options()["scaleArt"].as_bool();
assert_eq!(scale(&app), Some(false));
let press =
+3 -2
View File
@@ -599,8 +599,9 @@ const BAR_ALIGNMENTS: [&str; 3] = ["Align left", "Align center", "Align right"];
fn the_control_bar_shows_the_paragraph_alignment_of_the_selected_type() {
let ctx = control_bar_ctx();
let mut app = control_bar_app(&[json!({"x": 20, "y": 50, "text": "Point"})]);
// Pinned, so that the room the buttons need can't creep up past common window widths.
for width in [1440.0, 1920.0] {
// Pinned, so that the room the buttons need can't creep up past common window widths. The
// bar's right end keeps the panel menu button's room.
for width in [1536.0, 1920.0] {
let nodes = control_bar_nodes(&ctx, &mut app, width, vec![]);
let ends = BAR_ALIGNMENTS.map(|n| control_bar_end(&nodes, n));
assert!(ends.iter().all(|e| e.is_some_and(|x| x <= f64::from(width) - 10.0)), "pinned: the buttons show on a {width} pt window: {ends:?}");
+4
View File
@@ -370,6 +370,9 @@ pub struct UiState {
#[serde(skip)]
pub transient_focus: bool,
pub control_bar: bool,
/// The Control bar is docked to the bottom of the window instead of the top
/// (`window.control.dock`).
pub control_bar_bottom: bool,
pub toolbar: bool,
pub toolbar_double: bool,
/// Advanced toolbar (every tool group) instead of the categorized Basic toolbar.
@@ -961,6 +964,7 @@ impl Default for UiState {
transient_panel: None,
transient_focus: false,
control_bar: false,
control_bar_bottom: false,
toolbar: true,
toolbar_double: false,
toolbar_advanced: false,
+2
View File
@@ -43,6 +43,8 @@ pub use drag::{
};
#[cfg(test)]
use dropdowns::combo_button_id;
#[cfg(test)]
pub(crate) use dropdowns::menu_floor;
pub(crate) use dropdowns::{combo, combo_menu, combo_menu_field, combo_named, popover_is_open};
pub use dropdowns::{
dropdown, dropdown_named, dropdown_names, dropdown_names_named, dropdown_with, dropdown_with_named, list_scroll_style, menu_room, menu_scroll,
+4 -3
View File
@@ -252,8 +252,9 @@ pub(crate) fn popover_is_open(ctx: &egui::Context, anchor: egui::Id) -> bool {
ctx.data(|d| d.get_temp::<u64>(anchor.with("popover"))).is_some_and(|f| f + 1 >= frame)
}
/// Records where the bottom bars (status and hint bars) start this frame in the window being drawn,
/// so its menus can stop above them (#2086). Without bars it is the bottom of the window.
/// Records where the bottom bars (the status and hint bars, and the Control bar docked at the
/// bottom) start this frame in the window being drawn, so its menus can stop above them (#2086).
/// Without bars it is the bottom of the window.
pub fn set_menu_floor(ctx: &egui::Context, y: f32) {
let id = menu_floor_id(ctx);
ctx.data_mut(|d| d.insert_temp(id, y));
@@ -266,7 +267,7 @@ fn menu_floor_id(ctx: &egui::Context) -> egui::Id {
/// Where menus of the window being drawn stop: its recorded floor, or its bottom edge when nothing
/// recorded one, as in a floating panel's OS window.
pub(super) fn menu_floor(ctx: &egui::Context) -> f32 {
pub(crate) fn menu_floor(ctx: &egui::Context) -> f32 {
let id = menu_floor_id(ctx);
ctx.data(|d| d.get_temp::<f32>(id)).unwrap_or_else(|| ctx.content_rect().bottom())
}
+24
View File
@@ -21,6 +21,9 @@ use crate::{VectorcraftApp, widgets};
pub struct Workspace {
pub name: String,
pub control_bar: bool,
/// The Control bar is docked to the bottom of the window (built-in workspaces, and workspaces
/// saved without this field: the top).
pub control_bar_bottom: bool,
pub rulers: bool,
pub toolbar: bool,
pub toolbar_double: bool,
@@ -175,6 +178,7 @@ pub fn capture(ui: &UiState, name: &str) -> Workspace {
Workspace {
name: name.into(),
control_bar: ui.control_bar,
control_bar_bottom: ui.control_bar_bottom,
rulers: ui.view.rulers,
toolbar: ui.toolbar,
toolbar_double: ui.toolbar_double,
@@ -201,6 +205,7 @@ pub fn capture(ui: &UiState, name: &str) -> Workspace {
/// Apply a workspace's layout flags (brightness, prefs, shortcuts etc. untouched).
pub fn apply(ui: &mut UiState, w: &Workspace) {
ui.control_bar = w.control_bar;
ui.control_bar_bottom = w.control_bar_bottom;
ui.view.rulers = w.rulers;
ui.toolbar = w.toolbar;
ui.toolbar_double = w.toolbar_double;
@@ -766,4 +771,23 @@ mod tests {
let back: UiState = serde_json::from_value(serde_json::to_value(&ui).unwrap()).unwrap();
assert_eq!(back.custom_workspaces[1].dock_width, Some(260.0));
}
/// A workspace keeps the edge the Control bar is docked to, and one saved without it docks the
/// bar at the top.
#[test]
fn saved_workspaces_restore_the_control_bar_edge() {
let mut ui = UiState { control_bar: true, control_bar_bottom: true, ..Default::default() };
save_as(&mut ui, "Bottom").unwrap();
switch(&mut ui, ESSENTIALS).unwrap();
assert!(!ui.control_bar_bottom, "Essentials docks the bar at the top");
switch(&mut ui, "Bottom").unwrap();
assert!(ui.control_bar_bottom, "the saved workspace docks it at the bottom");
let back: UiState = serde_json::from_value(serde_json::to_value(&ui).unwrap()).unwrap();
assert!(back.control_bar_bottom && back.custom_workspaces[0].control_bar_bottom, "both survive the preferences' JSON");
let mut old = serde_json::to_value(&back.custom_workspaces[0]).unwrap();
assert!(old.as_object_mut().unwrap().remove("controlBarBottom").is_some());
let old: Workspace = serde_json::from_value(old).unwrap();
apply(&mut ui, &old);
assert!(ui.control_bar && !ui.control_bar_bottom, "an older workspace docks the bar at the top");
}
}
+6
View File
@@ -3955,6 +3955,12 @@ with an `index` it closes the document and all its views. `window.nextDocument`
through the views in tab order and add `view` to their reply for a document with more than one view. `ui.inspect`
reports `windows` (every window as `[document index, view number]`, in tab order), `currentWindow` and each pane's
`view`. Views are not saved.
`window.control.dock {edge}` (desktop app) docks the Control bar to the top of the window, under the menu bar
(`edge: "top"`, the default), or to its bottom (`"bottom"`), and returns `{edge}`. The bar's panel menu runs it with
Dock to Top and Dock to Bottom, the edge in use checked, and so does a drag of the bar's gripper, which docks the bar at
the edge the pointer is released nearer and shows a band along that edge while it moves. The edge is kept with the
workspace, and a workspace saved without it docks the bar at the top. Another `edge`, or none, fails with "`edge` must
be one of top, bottom" and leaves the bar where it is.
```json
{"name":"run_command","arguments":{"command":"file.new","params":{"preset":"A4","orientation":"landscape","artboards":4,"artboardLayout":{"columns":2},"bleed":9}}}
+1
View File
@@ -546,6 +546,7 @@ since 2026-10-01.
- **Global Edit Options in the Control bar:** while Select › Start Global Edit can run, the Control bar shows a Global Edit Options dropdown that opens the same options as the Properties panel's (Match: Appearance, Size; On Artboards; Include Objects On Canvas; Result(s)); both set the same options. The dropdown is off while Global Edit is on; it sits after Recolor Artwork, ahead of the X/Y/W/H fields ([Edit similar objects globally](https://helpx.adobe.com/illustrator/desktop/manage-objects/edit-objects/edit-similar-objects.html)). Regression test: `the_control_bar_shows_the_global_edit_options` in `crates/ui-egui/src/chrome/tests.rs`.
- **Global Edit's enabled check on large paths:** the check that decides whether Select › Start Global Edit can run on the selected path, compound path or symbol instance (which the Control bar makes every frame) measures the box of its anchors and handles instead of building and normalizing a copy of every anchor, with the same answer. On a 200,000-anchor path the check takes 0.7 ms instead of 2.2 to 3.0 ms. Regression tests: `support_*` in `crates/engine/src/cmd/globaledit.rs`.
- **Control bar's inline X, Y, W and H fields fit the bar:** the Control bar shows the inline X, Y, W and H fields only when all four, their labels as drawn in the current language and the W/H link fit inside the bar's margin; otherwise it shows the Transform link alone. This is VectorCraft's own layout rule. Regression test: `the_control_bar_shows_the_inline_transform_fields_only_when_they_fit` in `crates/ui-egui/src/chrome/tests.rs`.
- **Control bar docks to the top or the bottom:** the Control bar's panel menu, at the bar's right end, has Dock to Top and Dock to Bottom, the edge in use checked, and dragging the bar's gripper docks it at the top or the bottom edge of the window, whichever the pointer is released nearer, with a band along that edge while it moves (`window.control.dock {edge}`), as the help page describes ([Control panel overview](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/control-panel-overview.html)). Keeping the edge with the workspace, with older workspaces docking the bar at the top, is VectorCraft's own rule. Not done: dragging the gripper away from both edges to float the bar. Regression tests: `the_control_bar_panel_menu_docks_it_to_the_top_or_the_bottom`, `dragging_the_control_bar_gripper_docks_it_at_the_nearer_edge`, `the_control_bar_dock_command_takes_top_or_bottom` and `menus_stop_above_a_control_bar_docked_at_the_bottom` in `crates/ui-egui/src/chrome/tests.rs`, `saved_workspaces_restore_the_control_bar_edge` in `crates/ui-egui/src/workspaces.rs`.
- **Pattern-only backquote drags:** With Selection move, bounding-box scale or rotate, or the Rotate, Reflect, Scale or Shear tool, backquote held at mouse release applies the drag matrix to selected pattern paint while object geometry stays in place. The key may be pressed after mouse press; if released before the mouse button, the drag commits as an ordinary transform. Objects without pattern paint stay still; stroke-only patterns and patterned objects in mixed selections take the same matrix. Regression tests: `backquote_move_uses_release_state_and_one_commit` and `backquote_box_scale_and_rotate_use_pattern_matrix_at_release` in `select/tests.rs`, `backquote_drag_previews_and_commits_pattern_only_for_each_transform_tool` in `xform/transform.rs`, `selection_backquote_drag_changes_pattern_fill_and_stroke_without_moving_mixed_art` and `selection_backquote_drag_changes_stroke_only_pattern` in `tests/pattern.rs`, `egui_backtick_drag_uses_key_state_at_release` in `canvas/tests.rs`, and `synthetic_pointer_backquote_keeps_solid_object_in_place` in `control/tests.rs`. Scores and hours unchanged.
- **Existing-guide Escape cancellation (G1):** Escape clears a pending guide press or drag through Selection and Direct/Group Selection, cancelling a begun preview. Eight app-input regressions cover both axes, move/copy, temporary selection-tool borrowing from idle Pen, preselected-guide document/interaction-start selection and Undo/Redo/saved-state preservation, later on-canvas motion/release, Shift-press cancellation, and ordinary-release/Enter/idle controls. Historical focused checks on `8ab574b7` recorded five expected RED failures with three passing controls, then all eight GREEN tests and the Direct Selection Escape control passed. All nine also passed in current full CI. A full CI retry on the five-unit combined source at `a0671777` passed all seven stages with 6,613 test outcomes on 2026-10-14. An earlier full-CI run on the identical source panicked in the randomized `ai_mutated_save_never_panics` test; the cause remains unresolved. This five-fix batch does not claim to repair that failure. The original 1280×960 headless app capture from `b076025a` was reviewed for one vertical Selection guide after Escape; source review found its scene unchanged on `a0671777`. Scores and hours unchanged.
- **Escape in more tool drags:** Escape during an Anchor Point drag, a Mesh tool drag, a Direct Selection drag of a mesh point, a corner widget or the brackets of type on a path, or a Shape Builder drag cancels it: the art goes back to how the press found it, no undo step is added, the release does nothing, and the tool stays chosen. Escape during a Measure tool drag brings back the measurement before it. Regression tests: `escape_cancels_*` in `draw2/anchor/tests.rs`, `meshedit.rs`, `direct/tests.rs`, `builder/tests.rs` and `xform/measure.rs`, and `escape_cancels_an_anchor_point_mesh_shape_builder_or_measure_drag` in `tests/toolescape.rs`.