From 86e085f73d34590a9753e92ecf0a935ae0698060 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 19:04:05 -0800 Subject: [PATCH] Control bar: a narrow bar keeps the Transform link With something selected, the Fill and Stroke, Opacity, Graphic Style and Character controls now show only while the Transform link and the separator ahead of it still have room after them, the way the Paragraph, vertical alignment and Align groups keep the link's room. A narrow bar leaves those controls out first and keeps the link, instead of leaving out the link. - The Character controls show whole or not at all. While they are left out, the Paragraph, vertical alignment and Align controls after them are left out too, so a wider bar never drops a group that a narrower one showed. - The room checks for the width profile, Recolor Artwork and Global Edit Options count the spacing after each control. Tests: the_control_bar_s_controls_stay_inside_the_bar_in_a_narrow_window now also checks selected type. --- crates/ui-egui/src/chrome/control_bar.rs | 69 +++++++++++++++--------- crates/ui-egui/src/chrome/tests.rs | 9 +++- crates/ui-egui/src/panels/character.rs | 36 ++++++++++--- crates/ui-egui/src/tests/font_menu.rs | 4 +- docs/target-app-parity.md | 2 +- 5 files changed, 82 insertions(+), 38 deletions(-) diff --git a/crates/ui-egui/src/chrome/control_bar.rs b/crates/ui-egui/src/chrome/control_bar.rs index 8c37343667..fa4f2f8401 100644 --- a/crates/ui-egui/src/chrome/control_bar.rs +++ b/crates/ui-egui/src/chrome/control_bar.rs @@ -164,9 +164,9 @@ const GLOBAL_EDIT_OPTIONS_WIDTH: f32 = 24.0; /// It opens the Properties panel's Global Edit Options, which set the same session options, and is /// off and closed while Global Edit is on, as that panel's dropdown is. It sits after Recolor /// Artwork, ahead of the X/Y/W/H fields that the bar leaves out when it runs short of room, and is -/// left out only when no room is left for it. -fn global_edit_options(app: &mut VectorcraftApp, ui: &mut Ui) { - if !crate::menus::enabled(app, "select.globalEdit") || ui.available_width() < GLOBAL_EDIT_OPTIONS_WIDTH { +/// left out when the room left, less `reserve` ([`transform_reserve`]), cannot hold it. +fn global_edit_options(app: &mut VectorcraftApp, ui: &mut Ui, reserve: f32) { + if !crate::menus::enabled(app, "select.globalEdit") || ui.available_width() < row_width(ui, &[GLOBAL_EDIT_OPTIONS_WIDTH]) + reserve { return; } let on = app.session.active().is_some_and(|d| d.global_edit.is_some()); @@ -333,20 +333,30 @@ fn end_width(app: &VectorcraftApp, ui: &Ui, nothing_selected: bool) -> f32 { } } +/// The room the Control bar's Fill and Stroke, Opacity, Graphic Style and Character groups leave +/// for the Transform link: the link and the gap and separator ahead of it, while something is +/// `selected` and the panel menu has the Transform group on, or none. Those groups show each control +/// only while this room is left after it, so the bar leaves them out before the link. +fn transform_reserve(app: &VectorcraftApp, ui: &Ui, selected: bool) -> f32 { + if selected && group_on(app, "transform") { group_end_width(ui) + crate::panels::transform::link_width(ui) } else { 0.0 } +} + /// Draws the Control bar's groups between its Character controls and the Transform link, and /// returns whether the Transform link and X/Y/W/H follow (false: nothing is selected, and Document /// Setup and Preferences, drawn here, end the bar, or the Transform group is off). The groups are /// the Paragraph section and area type's vertical alignment (#3759), then the Align buttons, the /// Align To menu and, for two or more objects, the Distribute buttons (a single object keeps the bar /// as it was); each shows only while the end of the bar keeps its room. Groups the panel menu has -/// off are left out, and take no room. -fn groups_before_transform(app: &mut VectorcraftApp, ui: &mut Ui, objects: usize) -> bool { +/// off are left out, and take no room. Without `room` (the Character group had none), all of these +/// groups are left out, so the bar trims them from its right end before the Character group. +fn groups_before_transform(app: &mut VectorcraftApp, ui: &mut Ui, objects: usize, room: bool) -> bool { let nothing = objects == 0; let end = end_width(app, ui, nothing); - if group_on(app, "paragraph") && crate::panels::paragraph::control_bar(app, ui, BUTTON, |ui| group_end_width(ui) + end) { + if room && group_on(app, "paragraph") && crate::panels::paragraph::control_bar(app, ui, BUTTON, |ui| group_end_width(ui) + end) { group_end(ui); } - if group_on(app, "verticalAlignment") + if room + && group_on(app, "verticalAlignment") && crate::panels::properties::vertical_align::shown(app).is_some() && ui.available_width() >= buttons_width(ui, 4) + group_end_width(ui) + end && crate::panels::properties::vertical_align::buttons(app, ui, BUTTON) @@ -367,7 +377,7 @@ fn groups_before_transform(app: &mut VectorcraftApp, ui: &mut Ui, objects: usize } let transform = group_on(app, "transform"); let end = if transform { separator_width(ui) + end } else { 0.0 }; - if group_on(app, "alignDistribute") && ui.available_width() >= buttons_width(ui, 7) + end { + if room && group_on(app, "alignDistribute") && ui.available_width() >= buttons_width(ui, 7) + end { crate::panels::align::align_buttons(app, ui, BUTTON); if ui.available_width() >= crate::panels::align::align_to_menu_width(BUTTON) + ui.spacing().item_spacing.x + end { crate::panels::align::align_to_menu(app, ui, BUTTON); @@ -582,9 +592,10 @@ fn panel_menu(app: &mut VectorcraftApp, ui: &mut Ui, rect: egui::Rect) { /// The Control bar's Fill and Stroke group: the fill and stroke chips, the Stroke link, the weight /// and the width profile, and the separator after them while a control follows ([`group_separator`]). /// Short of room, the bar leaves out the width profile first, then the Stroke link with the weight, -/// then the two chips; each shows only while the bar has room for all of it. -fn fill_and_stroke(app: &mut VectorcraftApp, ui: &mut Ui) { - if ui.available_width() < row_width(ui, &[CHIP_WIDTH, CHIP_WIDTH]) { +/// then the two chips; each shows only while the bar has room for all of it and `reserve` after it +/// ([`transform_reserve`]). +fn fill_and_stroke(app: &mut VectorcraftApp, ui: &mut Ui, reserve: f32) { + if ui.available_width() < row_width(ui, &[CHIP_WIDTH, CHIP_WIDTH]) + reserve { return; } let shown_stroke = crate::panels::current_stroke(app); @@ -595,10 +606,10 @@ fn fill_and_stroke(app: &mut VectorcraftApp, ui: &mut Ui) { // The link opens the Stroke panel as a popover under it; then the weight spinner // (with presets) and the width profile. let link = widgets::text_width(ui, stroke_panel::link_text(ui, tl!("Stroke:"))); - if ui.available_width() >= row_width(ui, &[link, WEIGHT_FIELD_WIDTH]) { + if ui.available_width() >= row_width(ui, &[link, WEIGHT_FIELD_WIDTH]) + reserve { stroke_panel::link(app, ui, tl!("Stroke:")); stroke_panel::weight_field(app, ui, "cb-stroke", weight, WEIGHT_FIELD_WIDTH); - if ui.available_width() >= stroke_panel::PROFILE_DROPDOWN_WIDTH + if ui.available_width() >= row_width(ui, &[stroke_panel::PROFILE_DROPDOWN_WIDTH]) + reserve && let Some(id) = stroke_panel::profile_dropdown(app, ui, shown_stroke.as_ref().and_then(|s| s.profile.as_ref())) { app.run("stroke.set", json!({"profile": id})).ok(); @@ -611,13 +622,14 @@ fn fill_and_stroke(app: &mut VectorcraftApp, ui: &mut Ui) { /// `selected`, its Graphic Style group (Style, Recolor Artwork and Global Edit Options), each while /// the panel menu has it on, and the separator after them while a control follows. Short of room, /// the bar leaves out Global Edit Options first, then Recolor Artwork, then the Style link with its -/// chip, then the Opacity link with its field; each shows only while the bar has room for all of it. -fn opacity_and_style(app: &mut VectorcraftApp, ui: &mut Ui, t: &Tokens, opacity: f32, selected: bool) { +/// chip, then the Opacity link with its field; each shows only while the bar has room for all of it +/// and `reserve` after it ([`transform_reserve`]). +fn opacity_and_style(app: &mut VectorcraftApp, ui: &mut Ui, t: &Tokens, opacity: f32, selected: bool, reserve: f32) { let (opacity_on, style_on) = (group_on(app, "opacity"), group_on(app, "graphicStyle") && selected); let mut drawn = false; if opacity_on { let text = egui::RichText::new(tl!("Opacity:")).size(12.0).color(t.text).underline(); - if ui.available_width() < row_width(ui, &[widgets::text_width(ui, text.clone()), OPACITY_FIELD_WIDTH]) { + if ui.available_width() < row_width(ui, &[widgets::text_width(ui, text.clone()), OPACITY_FIELD_WIDTH]) + reserve { return; } // A click opens the Transparency panel; a drag scrubs the field (#4088). @@ -632,7 +644,7 @@ fn opacity_and_style(app: &mut VectorcraftApp, ui: &mut Ui, t: &Tokens, opacity: drawn = true; } let text = egui::RichText::new(tl!("Style:")).size(12.0).color(t.text).underline(); - if style_on && ui.available_width() >= GROUP_GAP + row_width(ui, &[widgets::text_width(ui, text.clone()), CHIP_WIDTH]) { + if style_on && ui.available_width() >= GROUP_GAP + row_width(ui, &[widgets::text_width(ui, text.clone()), CHIP_WIDTH]) + reserve { // Style picker: the selection's graphic style; its menu applies another. ui.add_space(GROUP_GAP); if ui.link(text).clicked() { @@ -643,10 +655,13 @@ fn opacity_and_style(app: &mut VectorcraftApp, ui: &mut Ui, t: &Tokens, opacity: egui::Popup::menu(&resp).show(|ui| crate::panels::graphic_styles::picker(app, ui)); // Recolor Artwork, when the selection has colours to recolour. let colored = app.session.active().is_some_and(|st| crate::canvas::has_colors(&st.doc, &st.selection.objects)); - if colored && ui.available_width() >= BUTTON && widgets::icon_button(ui, "palette", tl!("Recolor Artwork"), false, BUTTON).clicked() { + if colored + && ui.available_width() >= buttons_width(ui, 1) + reserve + && widgets::icon_button(ui, "palette", tl!("Recolor Artwork"), false, BUTTON).clicked() + { crate::menus::invoke(app, "ui.recolorDialog", json!({})); } - global_edit_options(app, ui); + global_edit_options(app, ui, reserve); drawn = true; } if drawn { @@ -837,20 +852,22 @@ fn contents(app: &mut VectorcraftApp, ui: &mut Ui, t: &Tokens, bar: egui::Rect, crate::panels::symbols::control_bar(app, ui); } let opacity = if has_first { crate::panels::current_transparency(app).map_or(1.0, |t| t.0) } else { 1.0 }; + let reserve = transform_reserve(app, ui, !sel.is_empty()); if !image && group_on(app, "fillStroke") { - fill_and_stroke(app, ui); + fill_and_stroke(app, ui, reserve); } - opacity_and_style(app, ui, t, opacity, !sel.is_empty()); - if group_on(app, "character") { - crate::panels::character::control_bar(app, ui); - } - if !groups_before_transform(app, ui, sel.len()) { + opacity_and_style(app, ui, t, opacity, !sel.is_empty(), reserve); + // The Character group shows whole, with the Transform link's room left after it, or not at + // all; left out, it leaves out the groups between it and the link too. + let room = !group_on(app, "character") || crate::panels::character::control_bar(app, ui, reserve); + if !groups_before_transform(app, ui, sel.len(), room) { erase_trailing(ui); return; } // The Transform link opens the whole Transform panel (reference point, rotate, // shear, options) in a popover; X/Y/W/H follow inline while the bar has room. The - // groups ahead of the link keep its room, but those ahead of them can still take it. + // groups from Fill and Stroke on keep its room, but the controls ahead of them can + // still take it. if ui.available_width() < crate::panels::transform::link_width(ui) { erase_trailing(ui); return; diff --git a/crates/ui-egui/src/chrome/tests.rs b/crates/ui-egui/src/chrome/tests.rs index 801b2e5bd2..30cbdc8270 100644 --- a/crates/ui-egui/src/chrome/tests.rs +++ b/crates/ui-egui/src/chrome/tests.rs @@ -3056,8 +3056,8 @@ fn control_bar_overflow(app: &mut crate::VectorcraftApp, ctx: &egui::Context, sc Some(shapes.collect()) } -/// In a narrow window, the Control bar leaves out a control it has no room for, Fill and Stroke and -/// Opacity included: every control it shows is drawn whole, left of the panel menu button, which +/// In a narrow window, the Control bar leaves out a control it has no room for, Fill and Stroke, +/// Opacity and Character included: every control it shows is drawn whole, left of the panel menu button, which /// shows in every width, with the bar docked to the top or the bottom or floating. #[test] fn the_control_bar_s_controls_stay_inside_the_bar_in_a_narrow_window() { @@ -3093,6 +3093,11 @@ fn the_control_bar_s_controls_stay_inside_the_bar_in_a_narrow_window() { app.run("select.anchors", json!({"id": id, "anchors": [[0, 0]]})).unwrap(); check(&mut app, "an anchor selected"); app.select_tool("selection"); + let text = app.run("text.create", json!({"x": 20, "y": 120, "text": "Point"})).unwrap()["id"].clone(); + app.run("select.set", json!({ "ids": [text] })).unwrap(); + let bar = control_frame(&mut app, &ctx, vec![]); + assert!(has(&bar, "Character:"), "the Character group shows in a wide window: {bar:?}"); + check(&mut app, "type selected"); app.run("select.set", json!({ "ids": [] })).unwrap(); check(&mut app, "nothing selected"); assert!(faults.is_empty(), "a control drawn past the bar's room: {faults:#?}"); diff --git a/crates/ui-egui/src/panels/character.rs b/crates/ui-egui/src/panels/character.rs index 44ae643e86..3a5e13a353 100644 --- a/crates/ui-egui/src/panels/character.rs +++ b/crates/ui-egui/src/panels/character.rs @@ -1165,20 +1165,40 @@ fn axes_cmd(app: &mut VectorcraftApp, axes: Value, phase: widgets::Live) { } /// The Control bar's type controls while type is selected or edited: the Character link (the -/// Character panel in a popover), the font family and style menus and the Font Size combo. -pub(crate) fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) { - let Some((s, _)) = text_style(app) else { return }; - let link = ui - .link(egui::RichText::new(tl!("Character:")).size(12.0).color(Tokens::get(ui.ctx()).text).underline()) - .on_hover_text(tl!("Character options")); +/// Character panel in a popover), the font family and style menus and the Font Size combo. They +/// show whole, with `reserve` left after them, or not at all: it returns false when type is selected +/// or edited and the row has no room for them. +pub(crate) fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui, reserve: f32) -> bool { + let Some((s, _)) = text_style(app) else { return true }; + if ui.available_width() < control_bar_width(ui) + reserve { + return false; + } + let link = ui.link(control_bar_link_text(ui)).on_hover_text(tl!("Character options")); widgets::popover(&link, link.clicked(), |ui| { ui.set_width(260.0); show_owned(app, ui, crate::font_menu::WheelOwner::ControlPopover(link.id)); }); - font_pickers(app, ui, &s, ("cb-font", "cb-font-style"), (150.0, 96.0)); - size_field(app, ui, "cb-font-size", 96.0); + let [family, style, size] = CONTROL_BAR_WIDTHS; + font_pickers(app, ui, &s, ("cb-font", "cb-font-style"), (family, style)); + size_field(app, ui, "cb-font-size", size); ui.add_space(4.0); crate::chrome::group_separator(ui); + true +} + +/// The Control bar's widths of the font family menu, the font style dropdown and the Font Size combo. +const CONTROL_BAR_WIDTHS: [f32; 3] = [150.0, 96.0, 96.0]; + +/// The text of the Control bar's Character link, as [`control_bar`] draws it. +fn control_bar_link_text(ui: &Ui) -> egui::RichText { + egui::RichText::new(tl!("Character:")).size(12.0).color(Tokens::get(ui.ctx()).text).underline() +} + +/// The room [`control_bar`]'s link, menus and Font Size combo take in the Control bar's row, with +/// the spacing after each. +fn control_bar_width(ui: &Ui) -> f32 { + let spacing = ui.spacing().item_spacing.x; + widgets::text_width(ui, control_bar_link_text(ui)) + CONTROL_BAR_WIDTHS.iter().sum::() + 4.0 * spacing } /// The Contextual Task Bar's widths of the font family menu, the font style dropdown and the diff --git a/crates/ui-egui/src/tests/font_menu.rs b/crates/ui-egui/src/tests/font_menu.rs index 2c600d1aa3..eded3a20c8 100644 --- a/crates/ui-egui/src/tests/font_menu.rs +++ b/crates/ui-egui/src/tests/font_menu.rs @@ -522,7 +522,9 @@ fn the_wheel_steps_the_families_in_properties_and_the_control_bar() { type Draw = fn(&mut VectorcraftApp, &mut egui::Ui); let places: [(&str, Draw); 2] = [ ("Properties", |app, ui| crate::panels::properties::type_sections(app, ui)), - ("Control bar", |app, ui| crate::panels::character::control_bar(app, ui)), + ("Control bar", |app, ui| { + crate::panels::character::control_bar(app, ui, 0.0); + }), ]; let at = |f: &str| FontDb::global().menu_family_list().iter().position(|x| x == f); for (place, draw) in places { diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index cc347751ac..fb3ef74183 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -642,7 +642,7 @@ since 2026-10-01. - **Control bar docks to the top or the bottom, or floats:** 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 neither while the bar floats. Dragging the bar's gripper and releasing it within 24 points of the top (under the menu bar) or the bottom of the window docks it at that edge, with a band along that edge while it moves; releasing it farther from both edges floats the bar there, with its outline under the pointer while it moves, and a floating bar's gripper moves it or docks it again (`window.control.dock {edge: "top"|"bottom"|"float"}`), as the help page describes ([Control panel overview](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/control-panel-overview.html)). Escape cancels a gripper drag. The 24-point zones, the floating bar's 800-point width and first position, a release outside the window leaving the bar where it was, a window too short for a floating bar docking it at the nearer edge, the floating bar staying inside a window that shrinks, and keeping the edge and the floating position with the workspace, with older workspaces docking the bar at their edge, are VectorCraft's own rules. 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`, `dragging_the_control_bar_gripper_away_from_both_edges_floats_it`, `floating_control_bar_moves_and_redocks_from_each_edge`, `floating_control_bar_menu_docks_to_either_edge`, `control_bar_drag_escape_restores_placement`, `control_bar_drag_release_outside_window_keeps_previous_placement`, `floating_control_bar_stays_reachable_in_tiny_and_resized_windows`, `the_control_bar_dock_command_takes_top_bottom_or_float` 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` and `saved_workspaces_restore_floating_control_bar_position` in `crates/ui-egui/src/workspaces.rs`. - **Control bar panel menu turns groups of options on or off:** the Control bar's panel menu lists the bar's groups of options (Snapping, Anchor Points, Tool Options, Object Options, Image Options, Fill and Stroke, Opacity, Graphic Style, Character, Paragraph, Vertical Alignment, Align and Distribute, Transform, Artboard Options and Document Options) above Dock to Top and Dock to Bottom, each checked while it is on; a click turns that group off or on (`window.control.options {group, visible?}`), and a group that is off stays out of the bar even while its tool or object is selected, as the help page describes ([Control panel overview](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/control-panel-overview.html)). The list of groups and what each holds, every group on to start, a group that is off leaving its room to the groups after it, and keeping the groups with the preferences and the workspace are VectorCraft's own rules. Regression tests: `the_control_bar_panel_menu_turns_each_group_on_or_off`, `the_control_bar_keeps_its_label_and_menu_with_every_group_off`, `the_control_bar_panel_menu_scrolls_in_a_short_window`, `the_control_bar_options_command_turns_a_group_on_or_off`, `groups_that_are_off_leave_their_room_to_the_later_groups`, `the_artboard_tool_s_control_bar_follows_its_two_groups` and `the_anchor_points_group_holds_convert_and_anchors` in `crates/ui-egui/src/chrome/tests.rs`, `the_image_options_group_holds_the_image_and_trace_controls` in `crates/ui-egui/src/tests/place.rs`, `the_character_and_paragraph_groups_hold_the_type_controls` in `crates/ui-egui/src/panels/paragraph/tests.rs`, `saved_workspaces_restore_the_control_bar_groups` in `crates/ui-egui/src/workspaces.rs`. - **Control bar separators stand only between groups:** the Control bar draws a separator between two groups of options only when a control follows it in the bar. When the groups after a separator are off in the panel menu, do not apply to the selection or the tool, or have no room in the window, the bar ends with the last control and no separator; two separators with nothing between them show as one. Each separator keeps its room, so the groups keep their places. This is VectorCraft's own rule. Regression test: `no_separator_is_left_at_the_end_of_the_control_bar` in `crates/ui-egui/src/chrome/tests.rs`. -- **Fill and Stroke and Opacity stay inside a narrow Control bar:** the Control bar leaves out a Fill and Stroke, Opacity or Graphic Style control it has no room for, whole, instead of drawing it past the panel menu button or under it. The bar draws its controls from left to right, each only when the room left still holds it, so a narrower control can still show after a wider one that was left out: the Opacity link with its field can show when the Stroke link with its weight field did not fit. The Snapping menu, the selection's label, the Convert and Anchors groups, the isolation button, Document Setup and Preferences also show only while they fit, and a group separator only while it has room. This is VectorCraft's own rule. Regression test: `the_control_bar_s_controls_stay_inside_the_bar_in_a_narrow_window` in `crates/ui-egui/src/chrome/tests.rs`. +- **Fill and Stroke and Opacity stay inside a narrow Control bar:** the Control bar leaves out a Fill and Stroke, Opacity or Graphic Style control it has no room for, whole, instead of drawing it past the panel menu button or under it. The bar draws its controls from left to right, each only when the room left still holds it, so a narrower control can still show after a wider one that was left out: the Opacity link with its field can show when the Stroke link with its weight field did not fit. With something selected, the Fill and Stroke, Opacity, Graphic Style and Character controls show only while the Transform link still has room after them, so the bar keeps the link and leaves those controls out first. The Character controls show whole or not at all, and while they are left out the Paragraph, vertical alignment and Align controls after them are left out too. The Snapping menu, the selection's label, the Convert and Anchors groups, the isolation button, Document Setup and Preferences also show only while they fit, and a group separator only while it has room. This is VectorCraft's own rule. Regression tests: `the_control_bar_s_controls_stay_inside_the_bar_in_a_narrow_window` and `the_control_bar_shows_the_inline_transform_fields_only_when_they_fit` in `crates/ui-egui/src/chrome/tests.rs`, `the_control_bar_trims_its_paragraph_section_after_the_controls_to_its_right` in `crates/ui-egui/src/panels/paragraph/tests.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`.