From 243bdb3f636a4f2cbf9b449a5fb1022b2252b275 Mon Sep 17 00:00:00 2001 From: ruchirguitar <113586074+ruchirguitar@users.noreply.github.com> Date: Tue, 10 Nov 2026 18:10:55 +0530 Subject: [PATCH] Fix #5713: resizable icon flyouts and bottom-aligned panel footers (#5973) * Fix #5713: resize icon flyouts and anchor panel footers * Fix #5713: reconcile parity evidence with upstream docs * Fix #5713: keep Actions rows above the pinned footer * Fix #5713: align documentation dates and regression claims * Fix #5713: keep short Glyphs grids above the footer * Fix #5713: pin craft-ui host minimum-size correction --------- Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> --- Cargo.lock | 6 +- Cargo.toml | 2 +- crates/ui-egui/src/panel_docking.rs | 32 ++++-- crates/ui-egui/src/panel_docking/tests.rs | 122 ++++++++++++++++++++- crates/ui-egui/src/panels/actions.rs | 64 ++++++++++- crates/ui-egui/src/panels/artboards.rs | 12 +- crates/ui-egui/src/panels/glyphs.rs | 14 ++- crates/ui-egui/src/panels/variables.rs | 84 +++++++------- crates/ui-egui/src/widgets.rs | 1 + crates/ui-egui/src/widgets/panel_chrome.rs | 92 ++++++++++++++++ docs/gaps.md | 5 + docs/roadmap.md | 9 +- docs/target-app-parity.md | 7 +- docs/ui-parity.md | 6 + 14 files changed, 386 insertions(+), 70 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 88a9abca0..c66578303 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -975,12 +975,12 @@ source = "git+https://github.com/storytold/craft-libs?rev=9be9a63782298ab59cd9b7 [[package]] name = "craft-fonts-build" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=9be9a63782298ab59cd9b7a42ee9cba4f42a5ccb#9be9a63782298ab59cd9b7a42ee9cba4f42a5ccb" +source = "git+https://github.com/storytold/craft-libs?rev=bb8320f5fa5d20fa78a026d0de06fe4e2f08a099#bb8320f5fa5d20fa78a026d0de06fe4e2f08a099" [[package]] name = "craft-i18n" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=9be9a63782298ab59cd9b7a42ee9cba4f42a5ccb#9be9a63782298ab59cd9b7a42ee9cba4f42a5ccb" +source = "git+https://github.com/storytold/craft-libs?rev=bb8320f5fa5d20fa78a026d0de06fe4e2f08a099#bb8320f5fa5d20fa78a026d0de06fe4e2f08a099" dependencies = [ "fixed_decimal", "icu_decimal", @@ -1049,7 +1049,7 @@ dependencies = [ [[package]] name = "craft-ui" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=9be9a63782298ab59cd9b7a42ee9cba4f42a5ccb#9be9a63782298ab59cd9b7a42ee9cba4f42a5ccb" +source = "git+https://github.com/storytold/craft-libs?rev=bb8320f5fa5d20fa78a026d0de06fe4e2f08a099#bb8320f5fa5d20fa78a026d0de06fe4e2f08a099" dependencies = [ "craft-fonts-build", "craft-i18n", diff --git a/Cargo.toml b/Cargo.toml index 2c0bf38ec..9b4b99c92 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ repository = "https://github.com/storytold/vectorcraft" craft-pdf = { git = "https://github.com/storytold/craft-libs", rev = "9be9a63782298ab59cd9b7a42ee9cba4f42a5ccb", default-features = false, features = ["interpret", "write"] } craft-print = { git = "https://github.com/storytold/craft-libs", rev = "9be9a63782298ab59cd9b7a42ee9cba4f42a5ccb" } craft-docpath = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "9be9a63782298ab59cd9b7a42ee9cba4f42a5ccb" } -craft-ui = { git = "https://github.com/storytold/craft-libs", rev = "9be9a63782298ab59cd9b7a42ee9cba4f42a5ccb", features = ["serde"] } +craft-ui = { git = "https://github.com/storytold/craft-libs", rev = "bb8320f5fa5d20fa78a026d0de06fe4e2f08a099", features = ["serde"] } craft-keymap = { git = "https://github.com/storytold/craft-libs", rev = "9be9a63782298ab59cd9b7a42ee9cba4f42a5ccb" } craft-proof = { git = "https://github.com/storytold/craft-libs", rev = "9be9a63782298ab59cd9b7a42ee9cba4f42a5ccb" } # Decides who draws the window frame (desktop vs. app) so the suite shares one native titlebar per diff --git a/crates/ui-egui/src/panel_docking.rs b/crates/ui-egui/src/panel_docking.rs index a74f47b6a..3447cd1e1 100644 --- a/crates/ui-egui/src/panel_docking.rs +++ b/crates/ui-egui/src/panel_docking.rs @@ -949,6 +949,7 @@ impl Transaction { } if let Some(saved) = &location.group { apply(&mut candidate, Action::ResizeGroup { panel: panel.clone(), height: saved.height })?; + apply(&mut candidate, Action::ResizeFlyoutWidth { panel: panel.clone(), width: saved.flyout_width })?; apply(&mut candidate, Action::SetMinimized { panel: panel.clone(), minimized: saved.minimized })?; } self.workspace.layout = candidate; @@ -1087,6 +1088,7 @@ fn known(action: &Action) -> bool { | Action::SetIconic { panel, .. } | Action::ResizeColumn { panel, .. } | Action::ResizeGroup { panel, .. } + | Action::ResizeFlyoutWidth { panel, .. } | Action::ResizeIconic { panel, .. } | Action::SetNative { panel, .. } | Action::SetMonitor { panel, .. } => registered(panel), @@ -1169,7 +1171,7 @@ pub(crate) fn show(app: &mut VectorcraftApp, ui: &mut egui::Ui) { if let Some(panel) = app.dock_rt.raise.take() { dock = dock.raise_panel(panel); } - let mut output = dock.show_content(ui, &layout, &style, &mut PanelContent { app: &mut *app }); + let mut output = dock.show_content(ui, &layout, &style, &mut PanelContent { app: &mut *app, layout: &layout }); app.dock_rt.space = craft_ui::docking::float_space(&ctx, area); if let Some(panel) = output.focused.take() { app.dock_rt.focused = Some(panel); @@ -1343,9 +1345,11 @@ pub(crate) fn floating_size(panel: &str) -> egui::Vec2 { /// The panels' content, labels and icons. struct PanelContent<'a> { app: &'a mut VectorcraftApp, + layout: &'a Layout, } impl craft_ui::docking::DockContent for PanelContent<'_> { + /// Draw a registered panel body, fitting untouched flyouts and pinning sized panels' footers. fn body(&mut self, ui: &mut egui::Ui, id: &String) { if id == CANVAS { crate::toolbar::show_beside_canvas(self.app, ui); @@ -1364,15 +1368,23 @@ impl craft_ui::docking::DockContent for PanelContent<'_> { } let width = ui.available_width(); let height = (ui.available_height() - 20.0).max(0.0); - if DockTab::from_id(id).is_some() { - crate::dock::panel_body(self.app, ui, id, width, height); - } else { - // Shrinking to the content's height lets craft-ui fit a float or flyout to it. - crate::widgets::panel_scroll(ui, egui::ScrollArea::vertical().id_salt(("shared-panel-body", id)).auto_shrink([false, true]), |ui| { - let width = ui.available_width(); - crate::dock::panel_body(self.app, ui, id, width, height) - }); - } + let fitted = in_iconic_column(self.layout, id) + && self.layout.location(id).ok().and_then(|location| location.group).is_none_or(|group| group.height.is_none()); + // Properties embeds other panels as sections, so their local button rows + // must remain beside those sections rather than pinning to the outer panel. + let bottom = (!fitted && id != "properties").then_some(back.bottom() - 10.0); + let info = egui::UiStackInfo::default().with_tag_value(crate::widgets::PANEL_FOOTER, bottom); + ui.scope_builder(egui::UiBuilder::new().ui_stack_info(info), |ui| { + if DockTab::from_id(id).is_some() { + crate::dock::panel_body(self.app, ui, id, width, height); + } else { + // Shrinking to the content's height lets craft-ui fit a float or flyout to it. + crate::widgets::panel_scroll(ui, egui::ScrollArea::vertical().id_salt(("shared-panel-body", id)).auto_shrink([false, true]), |ui| { + let width = ui.available_width(); + crate::dock::panel_body(self.app, ui, id, width, height) + }); + } + }); if field_had_keyboard { keep_field_ending_keys(ui); } diff --git a/crates/ui-egui/src/panel_docking/tests.rs b/crates/ui-egui/src/panel_docking/tests.rs index 1b8b1a168..2d33420b9 100644 --- a/crates/ui-egui/src/panel_docking/tests.rs +++ b/crates/ui-egui/src/panel_docking/tests.rs @@ -12,13 +12,16 @@ struct Harness { app: VectorcraftApp, ctx: egui::Context, time: f64, + cursor: egui::CursorIcon, + nodes: Vec<(egui::accesskit::NodeId, egui::accesskit::Node)>, } impl Harness { + /// Create a blank document and settle the initial docking layout over three frames. fn new() -> Self { let mut app = VectorcraftApp::new(Session::new(), Default::default()); app.run("file.new", json!({"width": 400, "height": 300})).unwrap(); - let mut h = Self { app, ctx: egui::Context::default(), time: 0.0 }; + let mut h = Self { app, ctx: egui::Context::default(), time: 0.0, cursor: egui::CursorIcon::Default, nodes: Vec::new() }; for _ in 0..3 { h.frame(vec![]); } @@ -40,6 +43,7 @@ impl Harness { self.run(input); } + /// Draw one application frame and retain its cursor and accessibility output for assertions. fn run(&mut self, input: egui::RawInput) { let app = &mut self.app; let mut out = self.ctx.run_ui(input, |ui| { @@ -47,6 +51,8 @@ impl Harness { app.ui(ui); }); out.textures_delta.clear(); + self.cursor = out.platform_output.cursor_icon; + self.nodes = out.platform_output.accesskit_update.map_or_else(Vec::new, |update| update.nodes); } fn hit(&self, hit: &Hit) -> Option { @@ -138,6 +144,120 @@ fn an_icon_opens_its_flyout_and_open_panel_follows() { assert_eq!(craft_ui::docking::flyout::(&h.ctx, area_id()), None); } +/// #5713: real Glyphs and Artboards bodies must retain a flyout resize through +/// content fitting, closing/reopening and serialization of the workspace layout. +#[test] +fn glyphs_and_artboards_flyouts_keep_user_sizes() { + for panel in ["glyphs", "artboards"] { + let mut h = Harness::new(); + let icon = h.hit(&Hit::Icon(panel.into())).unwrap().center(); + h.click(icon); + // Glyphs reserves space from the available height, so its untouched auto-fit + // converges over several frames before the first user resize. + for _ in 0..30 { + h.frame(vec![]); + } + // Area memory can include a panel body's minimum allocation beyond its clip. + // The frame's actual edge widgets describe the visible, resizable rectangle. + let rect = |h: &Harness| { + let id = area_id().with("flyout"); + let left = h.ctx.read_response(id.with("resize-left")).unwrap().rect; + let bottom = h.ctx.read_response(id.with("resize-bottom")).unwrap().rect; + Rect::from_min_max(pos2(left.left(), left.top() - 12.0), pos2(bottom.right() + 12.0, bottom.bottom())) + }; + let before = rect(&h); + for (at, cursor) in [ + (before.left_center() + vec2(2.0, 0.0), egui::CursorIcon::ResizeHorizontal), + (before.center_bottom() - vec2(0.0, 2.0), egui::CursorIcon::ResizeVertical), + ] { + h.frame(vec![Event::PointerMoved(at)]); + h.frame(vec![]); + assert_eq!(h.cursor, cursor, "{panel} at {at:?}, frame {before:?}"); + } + let from = before.left_bottom() + vec2(2.0, -2.0); + h.drag(from, from + vec2(-100.0, 150.0)); + for _ in 0..3 { + h.frame(vec![]); + } + let grown = rect(&h); + assert!((grown.width() - before.width() - 100.0).abs() < 1.0, "{panel}: {before:?} -> {grown:?}"); + assert!((grown.height() - before.height() - 150.0).abs() < 1.0, "{panel}: {before:?} -> {grown:?}"); + assert_eq!(grown.right(), before.right(), "the icon-column edge stays anchored"); + h.click(icon); + assert_eq!(craft_ui::docking::flyout::(&h.ctx, area_id()), None); + h.click(icon); + for _ in 0..3 { + h.frame(vec![]); + } + assert_eq!(rect(&h).size(), grown.size()); + let saved = serde_json::to_string(&h.layout()).unwrap(); + let mut restored = Harness::new(); + restored.app.ui.docking = Some(serde_json::from_str(&saved).unwrap()); + for _ in 0..3 { + restored.frame(vec![]); + } + restored.click(restored.hit(&Hit::Icon(panel.into())).unwrap().center()); + for _ in 0..3 { + restored.frame(vec![]); + } + assert_eq!(rect(&restored).size(), grown.size(), "{panel} saved layout"); + } +} + +/// Footer controls stay at the resized panel's bottom, including empty or short lists. +/// The same shared footer is used by the other icon panels and library windows. +#[test] +fn expanded_panel_footers_stay_at_the_bottom_with_short_and_long_lists() { + for (panel, label, artboards) in [ + ("artboards", "New Artboard", 1), + ("artboards", "New Artboard", 30), + ("brushes", "New Brush", 1), + ("swatches", "New Swatch", 1), + ("symbols", "New Symbol", 1), + ("graphicStyles", "New Graphic Style (Alt-click to name it)", 1), + ("appearance", "Add New Stroke", 1), + ("charStyles", "Create New Character Style", 1), + ("paraStyles", "Create New Paragraph Style", 1), + ("colorGuide", "Save color group to Swatch panel", 1), + ("links", "Show Link Info", 1), + ("actions", "Create New Action", 1), + ("variables", "Capture Data Set", 1), + ("assetExport", "Export for Screens…", 1), + ("cssProperties", "Generate CSS: the whole document's CSS while nothing is selected", 1), + ("svgInteractivity", "Link JavaScript Files…", 1), + ("colorThemes", "Add to Swatches", 1), + ("glyphs", "Zoom In", 1), + ] { + let mut h = Harness::new(); + h.ctx.enable_accesskit(); + for _ in 1..artboards { + h.app.run("artboard.new", json!({})).unwrap(); + } + h.app.run("window.panel.dock", json!({"panel":panel,"icon":true})).unwrap(); + let mut layout = h.layout(); + layout.apply(Action::ResizeGroup { panel: panel.into(), height: Some(540.0) }).unwrap(); + layout.apply(Action::ResizeFlyoutWidth { panel: panel.into(), width: Some(420.0) }).unwrap(); + h.app.ui.docking = Some(layout); + h.app.ui.open_panel = Some(panel.into()); + for _ in 0..5 { + h.frame(vec![]); + } + let bottom = h.ctx.read_response(area_id().with("flyout").with("resize-bottom")).unwrap().rect.bottom(); + let control = h + .nodes + .iter() + .find(|(_, node)| node.label() == Some(label)) + .and_then(|(_, node)| node.bounds()) + .unwrap_or_else(|| panic!("missing {label} in {panel}")); + assert!((control.y1 as f32 - (bottom - 10.0)).abs() < 2.0, "{panel}/{artboards}: footer {control:?}, bottom {bottom}"); + if panel == "artboards" && artboards == 1 { + let at = pos2(((control.x0 + control.x1) * 0.5) as f32, ((control.y0 + control.y1) * 0.5) as f32); + h.click(at); + assert_eq!(h.app.session.active().unwrap().doc.artboards.len(), 2, "the pinned New button works"); + } + } +} + #[test] fn a_tab_dragged_out_floats_and_comes_back_where_it_was_after_its_window_closes() { let mut h = Harness::new(); diff --git a/crates/ui-egui/src/panels/actions.rs b/crates/ui-egui/src/panels/actions.rs index 93d781e5a..2533ef029 100644 --- a/crates/ui-egui/src/panels/actions.rs +++ b/crates/ui-egui/src/panels/actions.rs @@ -1377,6 +1377,7 @@ enum InclusionChange { Step(usize, usize, usize, bool), } +/// Draw action sets, steps and playback controls, keeping the list above a pinned footer. pub fn show(app: &mut VectorcraftApp, ui: &mut egui::Ui) { let t = Tokens::get(ui.ctx()); // The selected action as (set, action). Read as the type it is stored as: egui keeps a @@ -1398,7 +1399,8 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut egui::Ui) { let mut expanded: BTreeSet = pstate(ui.ctx(), "actions-expanded"); let mut inclusion_changes = Vec::new(); let mut play_req = None; - egui::ScrollArea::vertical().max_height(260.0).show(ui, |ui| { + let list_height = widgets::panel_list_height(ui, 260.0, 0.0).min(260.0); + egui::ScrollArea::vertical().max_height(list_height).min_scrolled_height(list_height.min(64.0)).show(ui, |ui| { for (si, set) in app.ui.action_sets.iter().enumerate() { let (r, resp) = ui.allocate_exact_size(egui::vec2(ui.available_width(), 20.0), egui::Sense::click()); if sets_selected.contains(&si) && selected.is_none() { @@ -1551,9 +1553,11 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut egui::Ui) { set_pstate(ui.ctx(), SELECTED_STEP, step_selected); app.ui.action_set = current; app.ui.action_sets_selected = sets_selected; - widgets::divider(ui); + if app.ui.action_sets.is_empty() { + dim_label(ui, tl!("No actions. Press ● to record.")); + } // Bottom bar: stop, record, play, new set, new action, delete. - ui.horizontal(|ui| { + widgets::bottom_bar(ui, |ui| { let recording = app.ui.recording.is_some(); if widgets::icon_button(ui, "square", tl!("Stop Playing/Recording"), false, 24.0).clicked() && let Err(e) = app.run(STOP_RECORDING, json!({})) @@ -1598,9 +1602,6 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut egui::Ui) { app.status(e); } let _ = icons::exists; - if app.ui.action_sets.is_empty() { - dim_label(ui, tl!("No actions. Press ● to record.")); - } } #[cfg(test)] @@ -1608,6 +1609,57 @@ mod tests { use super::*; use vectorcraft_engine::Session; + /// A short sized panel clips the list above its footer and can scroll to select its final row. + #[test] + fn a_short_actions_panel_keeps_its_last_row_reachable_above_the_footer() { + use egui::{Event, PointerButton, Rect, UiBuilder, UiStackInfo, pos2, vec2}; + for height in [60.0, 120.0] { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + let ctx = egui::Context::default(); + let mut time = 0.0; + let mut frame = |app: &mut VectorcraftApp, events| { + time += 0.1; + let input = egui::RawInput { + time: Some(time), + screen_rect: Some(Rect::from_min_size(pos2(0.0, 0.0), vec2(300.0, height))), + events, + ..Default::default() + }; + let mut output = ctx.run_ui(input, |ui| { + let info = UiStackInfo::default().with_tag_value(widgets::PANEL_FOOTER, Some(height)); + ui.scope_builder(UiBuilder::new().ui_stack_info(info), |ui| show(app, ui)); + }); + output.textures_delta.clear(); + output.shapes + }; + let last_row = |shapes: &[egui::epaint::ClippedShape]| { + shapes + .iter() + .find_map(|shape| match &shape.shape { + egui::Shape::Text(text) if text.galley.text() == "Reflect Horizontal" => Some((text.visual_bounding_rect(), shape.clip_rect)), + _ => None, + }) + .expect("the final action is drawn") + }; + for _ in 0..3 { + let shapes = frame(&mut app, vec![]); + assert!(last_row(&shapes).1.bottom() <= height - 31.0, "height={height}: the list overlaps its footer"); + } + for _ in 0..12 { + frame(&mut app, crate::toolbar::tests::wheel(pos2(100.0, 12.0), 1000.0)); + } + let shapes = frame(&mut app, vec![]); + let (row, clip) = last_row(&shapes); + assert!(clip.contains_rect(row), "height={height}: the final row {row:?} is outside {clip:?}"); + let at = row.center(); + let button = |pressed| Event::PointerButton { pos: at, button: PointerButton::Primary, pressed, modifiers: Default::default() }; + for events in [vec![Event::PointerMoved(at)], vec![button(true)], vec![button(false)]] { + frame(&mut app, events); + } + assert_eq!(pstate::>(&ctx, "actions-selected"), Some((0, 5))); + } + } + #[test] fn defaults_are_valid_commands() { for set in default_sets() { diff --git a/crates/ui-egui/src/panels/artboards.rs b/crates/ui-egui/src/panels/artboards.rs index d46e5c4ac..04f2719e7 100644 --- a/crates/ui-egui/src/panels/artboards.rs +++ b/crates/ui-egui/src/panels/artboards.rs @@ -503,6 +503,7 @@ fn select_copies(app: &mut VectorcraftApp, all: &[usize], r: &Value) { } } +/// Draw the artboard list and its commands, filling the space above a sized panel's footer. pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); let abs: Vec = app.session.active().map(|d| d.doc.artboards.iter().map(|a| a.name.clone()).collect()).unwrap_or_default(); @@ -517,10 +518,17 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let mut row_clicked = false; // A row dropped on another row: (from, to), the index it lands at. let mut dropped = None; + let fixed = widgets::panel_footer_bottom(ui).is_some(); + let list_height = widgets::panel_list_height(ui, 220.0, 2.0 + ui.spacing().item_spacing.y); let list_rect = widgets::list_box(ui, |ui| { - ui.set_min_height(110.0); + ui.set_min_height(if fixed { list_height.min(110.0) } else { 110.0 }); ui.spacing_mut().item_spacing.y = 0.0; - let scrolled = egui::ScrollArea::vertical().id_salt("ab-scroll").max_height(220.0).show(ui, |ui| { + let scroll = egui::ScrollArea::vertical() + .id_salt("ab-scroll") + .max_height(list_height) + .min_scrolled_height(list_height.min(64.0)) + .auto_shrink([false, !fixed]); + let scrolled = scroll.show(ui, |ui| { for (i, name) in abs.iter().enumerate() { let (r, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 24.0), Sense::click_and_drag()); if resp.drag_started() { diff --git a/crates/ui-egui/src/panels/glyphs.rs b/crates/ui-egui/src/panels/glyphs.rs index 5c07441be..9192aa5f0 100644 --- a/crates/ui-egui/src/panels/glyphs.rs +++ b/crates/ui-egui/src/panels/glyphs.rs @@ -212,6 +212,7 @@ fn insert(app: &mut VectorcraftApp, ctx: &egui::Context, ch: char) { } } +/// Browse and insert glyphs from the chosen font, with font controls below the glyph grid. pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); let db = FontDb::global(); @@ -244,14 +245,14 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let cols = ((w / cell).floor() as usize).max(1); let rows = chars.len().div_ceil(cols); let px = (cell * ui.ctx().pixels_per_point()).round().clamp(8.0, 256.0) as u32; - let list_h = (ui.available_height() - 64.0).max(cell * 3.0); + let list_h = widgets::panel_list_height(ui, (ui.available_height() - 64.0).max(cell * 3.0), 33.0); let hover_id = egui::Id::unique("gl-hover"); let selected = selected_char(app); // The selected character and the rows shown last frame: the grid scrolls to the selected glyph // only when the selection moves to another character, so it still scrolls freely meanwhile. let follow_id = egui::Id::unique("gl-follow"); let (followed, shown): (Option, std::ops::Range) = ui.ctx().data(|d| d.get_temp(follow_id)).unwrap_or_default(); - let mut area = egui::ScrollArea::vertical().max_height(list_h).auto_shrink([false, false]); + let mut area = egui::ScrollArea::vertical().max_height(list_h).min_scrolled_height(list_h.min(64.0)).auto_shrink([false, false]); if selected != followed && let Some(c) = selected && let Ok(i) = chars.binary_search_by_key(&c, |x| x.0) @@ -308,6 +309,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { /// The family and style menus of the font the panel browses, and Zoom Out and Zoom In. fn font_row(app: &mut VectorcraftApp, ui: &mut Ui, family: &str, style: &str, w: f32) { let db = FontDb::global(); + widgets::panel_footer_space(ui, 24.0); ui.horizontal(|ui| { ui.spacing_mut().item_spacing.x = 4.0; // Picks the font the panel browses only: nothing is previewed on the document. @@ -371,10 +373,14 @@ fn alternates(app: &mut VectorcraftApp, ui: &mut Ui, w: f32) { let cols = ((w / cell).floor() as usize).max(1); let rows = list.len().div_ceil(cols); let px = (cell * ui.ctx().pixels_per_point()).round().clamp(8.0, 256.0) as u32; - let list_h = (ui.available_height() - 64.0).max(cell * 3.0); + let list_h = widgets::panel_list_height(ui, (ui.available_height() - 64.0).max(cell * 3.0), 33.0); let mut pick = None; // A scroll position of its own for each character, so the grid starts at the top for another. - let area = egui::ScrollArea::vertical().id_salt(("gl-alternates", id.0, byte)).max_height(list_h).auto_shrink([false, false]); + let area = egui::ScrollArea::vertical() + .id_salt(("gl-alternates", id.0, byte)) + .max_height(list_h) + .min_scrolled_height(list_h.min(64.0)) + .auto_shrink([false, false]); area.show_rows(ui, cell, rows, |ui, range| { for r in range { ui.horizontal(|ui| { diff --git a/crates/ui-egui/src/panels/variables.rs b/crates/ui-egui/src/panels/variables.rs index 87aa5cd13..a8987855e 100644 --- a/crates/ui-egui/src/panels/variables.rs +++ b/crates/ui-egui/src/panels/variables.rs @@ -97,6 +97,7 @@ pub(crate) fn delete_active_dataset(app: &mut VectorcraftApp) { app.run("dataset.delete", json!({})).ok(); } +/// Draw document variables, data sets and their two-row footer of capture and binding controls. pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let Some(st) = app.session.active() else { widgets::dim_label(ui, tl!("No document")); @@ -209,46 +210,49 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { // The bottom bar: make the selection dynamic, define and delete what the lists show, and // lock the variables. - ui.separator(); - ui.horizontal(|ui| { - if widgets::icon_button_enabled(ui, "type", tl!("Make Text Dynamic"), false, single_type && !locked, 24.0).clicked() { - app.run("variable.makeTextDynamic", json!({})).ok(); - } - if widgets::icon_button_enabled(ui, "eye", tl!("Make Visibility Dynamic"), false, has_selection && !locked, 24.0).clicked() { - app.run("variable.makeVisibilityDynamic", json!({})).ok(); - } - if widgets::icon_button_enabled(ui, "image", tl!("Make Image Dynamic"), false, single_image && !locked, 24.0).clicked() { - app.run("variable.makeImageDynamic", json!({})).ok(); - } - if widgets::icon_button_enabled(ui, "chart-column", tl!("Make Graph Dynamic"), false, selection_graph && !locked, 24.0).clicked() { - app.run("variable.makeGraphDynamic", json!({})).ok(); - } - if widgets::icon_button_enabled(ui, "plus", tl!("New Variable…"), false, !locked, 24.0).clicked() { - crate::menus::invoke(app, "variable.define", json!({})); - } - if widgets::icon_button_enabled(ui, "link-2-off", tl!("Unbind Variable"), false, selection_bound, 24.0).clicked() { - app.run("variable.unbind", json!({"ids": selected})).ok(); - } - // The bin deletes the highlighted variable only, as the reference panel's does. - if widgets::icon_button_enabled(ui, "trash-2", tl!("Delete Variable"), false, highlighted.is_some() && !locked, 24.0).clicked() { - delete_highlighted(app); - } - // The lock button is drawn pressed while the variables are locked. - let tip = if locked { tl!("Unlock Variables") } else { tl!("Lock Variables") }; - if widgets::icon_button(ui, "lock", tip, locked, 24.0).clicked() { - app.run("variable.lock", json!({})).ok(); - } - }); - ui.horizontal(|ui| { - // Capture records what the art holds now as a new row (paste: take it in); Update - // writes the art back into the row that is active (save: write it out). - if widgets::icon_button(ui, "clipboard-paste", tl!("Capture Data Set"), false, 24.0).clicked() { - app.run("dataset.capture", json!({})).ok(); - } - let can_update = active.is_some(); - if widgets::icon_button_enabled(ui, "save", tl!("Update Data Set"), false, can_update, 24.0).clicked() { - app.run("dataset.update", json!({})).ok(); - } + let footer_height = 48.0 + 2.0 * ui.spacing().item_spacing.y + 1.0; + widgets::panel_footer(ui, footer_height, |ui| { + ui.add(egui::Separator::default().spacing(1.0)); + ui.horizontal(|ui| { + if widgets::icon_button_enabled(ui, "type", tl!("Make Text Dynamic"), false, single_type && !locked, 24.0).clicked() { + app.run("variable.makeTextDynamic", json!({})).ok(); + } + if widgets::icon_button_enabled(ui, "eye", tl!("Make Visibility Dynamic"), false, has_selection && !locked, 24.0).clicked() { + app.run("variable.makeVisibilityDynamic", json!({})).ok(); + } + if widgets::icon_button_enabled(ui, "image", tl!("Make Image Dynamic"), false, single_image && !locked, 24.0).clicked() { + app.run("variable.makeImageDynamic", json!({})).ok(); + } + if widgets::icon_button_enabled(ui, "chart-column", tl!("Make Graph Dynamic"), false, selection_graph && !locked, 24.0).clicked() { + app.run("variable.makeGraphDynamic", json!({})).ok(); + } + if widgets::icon_button_enabled(ui, "plus", tl!("New Variable…"), false, !locked, 24.0).clicked() { + crate::menus::invoke(app, "variable.define", json!({})); + } + if widgets::icon_button_enabled(ui, "link-2-off", tl!("Unbind Variable"), false, selection_bound, 24.0).clicked() { + app.run("variable.unbind", json!({"ids": selected})).ok(); + } + // The bin deletes the highlighted variable only, as the reference panel's does. + if widgets::icon_button_enabled(ui, "trash-2", tl!("Delete Variable"), false, highlighted.is_some() && !locked, 24.0).clicked() { + delete_highlighted(app); + } + // The lock button is drawn pressed while the variables are locked. + let tip = if locked { tl!("Unlock Variables") } else { tl!("Lock Variables") }; + if widgets::icon_button(ui, "lock", tip, locked, 24.0).clicked() { + app.run("variable.lock", json!({})).ok(); + } + }); + ui.horizontal(|ui| { + // Capture records what the art holds now as a new row (paste: take it in); Update + // writes the art back into the row that is active (save: write it out). + if widgets::icon_button(ui, "clipboard-paste", tl!("Capture Data Set"), false, 24.0).clicked() { + app.run("dataset.capture", json!({})).ok(); + } + let can_update = active.is_some(); + if widgets::icon_button_enabled(ui, "save", tl!("Update Data Set"), false, can_update, 24.0).clicked() { + app.run("dataset.update", json!({})).ok(); + } + }); }); } diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index c70cca173..56bac9808 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -63,6 +63,7 @@ pub use numeric::{ }; #[cfg(test)] use numeric::{decimal_point, field_number, share_of, spinner_step, step_through, step_to_multiple, within_a_millionth}; +pub(crate) use panel_chrome::{PANEL_FOOTER, panel_footer, panel_footer_bottom, panel_footer_space, panel_list_height}; pub use panel_chrome::{bottom_bar, layout_button, list_box, orientation_button, tab_bar}; pub(crate) use text::{ arm_field_focus, dialog_focus_flag, dialog_reject_commit_flag, disarm_field_focus, enter_modifiers, focus_field, own_field_shift_enter, diff --git a/crates/ui-egui/src/widgets/panel_chrome.rs b/crates/ui-egui/src/widgets/panel_chrome.rs index e07367dab..2c8471628 100644 --- a/crates/ui-egui/src/widgets/panel_chrome.rs +++ b/crates/ui-egui/src/widgets/panel_chrome.rs @@ -1,5 +1,41 @@ use super::{Color32, CornerRadius, Pos2, Rect, Response, Sense, Stroke, StrokeKind, Tokens, Ui, Vec2, icons, pos2, theme, vec2}; +pub(crate) const PANEL_FOOTER: &str = "vectorcraft-panel-footer"; +const FOOTER_HEIGHT: f32 = 31.0; + +/// The finite body edge supplied by a sized dock, float or user-resized flyout. +/// Untouched flyouts omit it so their content can still determine their height. +pub(crate) fn panel_footer_bottom(ui: &Ui) -> Option { + ui.stack().iter().find_map(|stack| stack.tags().get_downcast::>(PANEL_FOOTER)).copied().flatten() +} + +/// Space left for a list above the footer and any intervening controls, or its unfitted fallback. +pub(crate) fn panel_list_height(ui: &Ui, fallback: f32, after: f32) -> f32 { + panel_footer_bottom(ui).map_or(fallback, |bottom| (bottom - ui.next_widget_position().y - FOOTER_HEIGHT - after).max(0.0)) +} + +/// Pad short content so a footer of `height` ends at the panel's saved bottom edge. +pub(crate) fn panel_footer_space(ui: &mut Ui, height: f32) { + if let Some(bottom) = panel_footer_bottom(ui) { + ui.add_space((bottom - ui.next_widget_position().y - height).max(0.0)); + } +} + +/// Pin a custom footer (for example the Variables panel's two rows). +pub(crate) fn panel_footer(ui: &mut Ui, height: f32, add: impl FnOnce(&mut Ui)) { + let Some(bottom) = panel_footer_bottom(ui) else { return add(ui) }; + let top = bottom - height; + if ui.next_widget_position().y > top { + ui.add_space(height); + } + let rect = Rect::from_min_size(pos2(ui.max_rect().left(), top), vec2(ui.available_width(), height)); + ui.painter().rect_filled(rect, 0.0, Tokens::get(ui.ctx()).panel); + // Own the blank parts too, so a click cannot select a scrolled row behind the footer. + let _background = ui.interact(rect, ui.scope_id().with("panel-footer-background"), Sense::CLICK); + ui.scope_builder(egui::UiBuilder::new().max_rect(rect), add); + ui.advance_cursor_after_rect(rect); +} + /// A bordered list box (Swatches tiles, Appearance rows, Artboards list). pub fn list_box(ui: &mut Ui, add: impl FnOnce(&mut Ui) -> R) -> R { let t = Tokens::get(ui.ctx()); @@ -9,6 +45,25 @@ pub fn list_box(ui: &mut Ui, add: impl FnOnce(&mut Ui) -> R) -> R { /// A bottom button bar separated from the content by a divider (panel footers). pub fn bottom_bar(ui: &mut Ui, add: impl FnOnce(&mut Ui)) { let t = Tokens::get(ui.ctx()); + if let Some(bottom) = panel_footer_bottom(ui) { + // Reserve scrollable room after long content so its final row can move above + // the pinned footer instead of remaining hidden underneath its buttons. + let top = bottom - FOOTER_HEIGHT; + if ui.next_widget_position().y > top { + ui.add_space(FOOTER_HEIGHT); + } + let footer = Rect::from_min_size(pos2(ui.max_rect().left(), top), vec2(ui.available_width(), FOOTER_HEIGHT)); + ui.painter().rect_filled(footer, 0.0, t.panel); + let _background = ui.interact(footer, ui.scope_id().with("panel-footer-background"), Sense::CLICK); + ui.painter().hline(footer.x_range(), top + 4.0, Stroke::new(1.0, t.divider)); + let row = Rect::from_min_max(pos2(footer.left(), top + 7.0), footer.max); + ui.scope_builder(egui::UiBuilder::new().max_rect(row).layout(egui::Layout::left_to_right(egui::Align::Center)), |ui| { + ui.spacing_mut().item_spacing.x = 4.0; + add(ui); + }); + ui.advance_cursor_after_rect(footer); + return; + } ui.add_space(4.0); let (r, _) = ui.allocate_exact_size(vec2(ui.available_width(), 1.0), Sense::hover()); ui.painter().rect_filled(r, 0.0, t.divider); @@ -202,3 +257,40 @@ pub(super) fn chevron_cell(ui: &mut Ui, h: f32) -> Response { dresp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, ui.is_enabled(), tl!("Presets"))); dresp } + +#[cfg(test)] +mod footer_tests { + use super::{PANEL_FOOTER, bottom_bar, panel_footer}; + + /// Blank footer space intercepts clicks instead of selecting list rows underneath it. + #[test] + fn blank_footer_space_does_not_click_a_row_scrolled_behind_it() { + use egui::{Event, PointerButton, Rect, Sense, UiBuilder, UiStackInfo, pos2}; + for custom in [false, true] { + let ctx = egui::Context::default(); + let mut clicked = false; + let at = pos2(150.0, 190.0); + let button = |pressed| Event::PointerButton { pos: at, button: PointerButton::Primary, pressed, modifiers: Default::default() }; + for events in [vec![], vec![], vec![Event::PointerMoved(at), button(true)], vec![button(false)]] { + let input = + egui::RawInput { screen_rect: Some(Rect::from_min_max(pos2(0.0, 0.0), pos2(200.0, 200.0))), events, ..Default::default() }; + let mut output = ctx.run_ui(input, |ui| { + let info = UiStackInfo::default().with_tag_value(PANEL_FOOTER, Some(200.0_f32)); + ui.scope_builder(UiBuilder::new().ui_stack_info(info), |ui| { + clicked |= ui.allocate_rect(Rect::from_min_max(pos2(0.0, 100.0), pos2(200.0, 200.0)), Sense::click()).clicked(); + let contents = |ui: &mut egui::Ui| { + let _button = ui.button("Footer button"); + }; + if custom { + panel_footer(ui, 31.0, contents); + } else { + bottom_bar(ui, contents); + } + }); + }); + output.textures_delta.clear(); + } + assert!(!clicked, "custom={custom}: a hidden row received the footer click"); + } + } +} diff --git a/docs/gaps.md b/docs/gaps.md index 2910585f6..e9e16036d 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -2,12 +2,16 @@ > **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-10 · **Change:** minor (G5: built-in Map Markers symbol library; estimate 11–21 → 10–20 h; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: G2: Effect Gallery thumbnails and in-dialog preview; estimate 1–2 h → 0; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; G5: built-in UI Frames symbol library with default 9-slice; estimate 11–21 h; previously minor: G5: dynamic symbol part on-canvas takes opacity, blend, gradient and pattern; docs Overlap/auto-corners sync; estimate 12–22 h; previously minor: interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; previously minor: Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; previously minor: G5 docs: Art brush Overlap, Pattern brush auto corners and Global Edit Shift-click leave-out moved from Missing to Done (code already shipped); previously minor: custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; G9: EPS gradients drawn as abutting strips import as one gradient, without seams; Tools stays beside the canvas with left-side panels; new gradients follow document color mode; Selection QoL: marquee modes and visible feedback, #5313; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; Indian-language hyphenation review and license allowlist, #4947; opening a slow file shows a progress window, part of #5930; Hindi catalog and contributor loanword preferences; agent wording pass complete; wider native proofreading welcome; an image zoomed in on is separated, colorized or proofed only where it shows (#4155); scores and estimates unchanged; a document's first frame renders on the render worker, so opening a heavy file no longer freezes the window; scores and estimates unchanged; large images in CMYK documents are shrunk before they are separated into ink planes (#4155); scores and estimates unchanged; `.ai` paths with a Calligraphic brush open brushed, with the 6D Art Pen's channels, and Windows reads the pen's tilt and barrel rotation, #372; scores and estimates unchanged; Compound Shapes add-on, beyond Illustrator; scores and estimates unchanged; Contour add-on, beyond Illustrator; scores and estimates unchanged; a slow first frame is waited for instead of restarting without the graphics adapter, and the menu bar no longer waits for the installed fonts' scan; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) +> **Flyout update:** 2026-11-06 — resizable icon-strip flyouts and bottom-aligned panel footers (#5713); scores and estimates unchanged. + ## Feature submission evidence **Feature evidence last updated:** 2026-11-02. | 2026-11-06 | minor | Opening a file that takes longer than 0.4 s shows a progress window: the file name, a moving bar (reading reports no steps), how many more files wait, Cancel (Esc; the file doesn't open) and Continue in Background (`file.cancelOpen`, `file.openInBackground`); it never opens over another dialog or progress window (part of #5930); scores and estimates unchanged | Raster rendering fits and preflattens curved stroke outlines more tightly only below four times the raster tolerance, retaining thin-band coverage without changing stroke weight or vector export geometry. Current cached character/object outlines, cropped long strokes, gradient clips and effect rasters share the helper; cropped-region reuse and guarded flattening remain. The unchanged `fractional_curved_strokes_retain_coverage_between_notches` regression covers fractional weights, rotation, phase, opacity and f32-rounded endpoints. This is a quality/cost tradeoff, not a speed claim. Scores and estimates unchanged. +Icon-strip flyouts resize from the canvas-side edge and bottom, keeping the icon-column edge anchored (#5713). Independent group heights and flyout widths persist in workspaces; saved heights override content fitting while untouched heights still fit content. Shared panel button bars and the Glyphs font row stay at the bottom after enlargement; the Artboards list grows into the extra space. Regressions: `panel_docking::tests::glyphs_and_artboards_flyouts_keep_user_sizes` and `expanded_panel_footers_stay_at_the_bottom_with_short_and_long_lists`. Scores and estimates unchanged. + **PR #3273 revision — 2026-11-04:** Thin curved stroke coverage ported to the current raster-only cache, cropped-long-stroke and effect paths with guarded flattening; vector export geometry unchanged. Scores and estimates unchanged. Group Selection keeps an already selected component selected during a drag; a completed click still adds its next enclosing group. The current Direct Selection click/release machinery is shared, preserving compound-member picks, Shift deselection, isolation and masked-art dragging. Outline Stroke continues to preserve all original fills, including open contours; the change does not remove fills. Regressions: `group_selection_distinguishes_selected_leaf_drags_from_clicks` and `outline_fill_and_stroke_move_independently_with_group_selection` cover tool events, independent component movement and Undo/Redo. Scores and estimates unchanged. @@ -51,6 +55,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard | 2026-11-05 | minor | G15: Selection skips building corner and pie widgets when the pointer is well outside a large selection; empty-canvas hover with 20,000 selected paths took 28.72 ms before and 16.41 ms after in local median runs. Other cursor targets remain active; estimate unchanged. | | 2026-11-05 | minor | G15: Direct Selection reuses one document hit result when choosing the cursor over a path; after the path hit-test improvement, a 200,000-anchor path's segment cursor took 3.72 ms before and 2.66 ms after in local median runs. Cursor behavior is unchanged; estimate unchanged. | | 2026-11-05 | minor | G15: large-selection transforms reuse object locations found during command validation; a 20,000-object nudge took 58.07 ms before and 51.26 ms after in paired median runs. Estimate unchanged. | +| 2026-11-06 | minor | Icon-strip flyouts resize on their free edges and retain sizes with workspaces (#5713); saved heights beat auto-fit; panel footers stay at the bottom after enlargement. Scores and estimates unchanged. | | 2026-11-05 | minor | G9, G14: `.ai` paths with a Calligraphic brush open brushed (the brush on the stroke, at its weight) where the object's settings are the brush's own, and the brush's Tilt, Bearing and Rotation codes come in as those channels; Windows reads the pen's tilt and barrel rotation from Windows Ink (#372). Checked against a user's file of 6D Art Pen strokes, used locally; not tried with a pen. Scores and estimates unchanged. | | 2026-11-04 | minor | Contour add-on (beyond Illustrator): drag away from a selected shape's edge to grow a live copy of it, or toward the inside to shrink it; the copy carries a live Offset Path effect, so it stays editable until Bake (Expand Appearance), a second drag changes the same Contour instead of nesting one, and a floating bar sets the distance and the Sharp, Round or Square corners. `addon.contour.set` is the command behind it. It is a built-in add-on: Object › Plug-ins › Contour, and its own heading in All Tools, with no default shortcut or toolbar slot. A VectorCraft design built on Offset Path; no other application was inspected. Scores and estimates unchanged. | | 2026-11-04 | minor | Contour tool (beyond Illustrator): drag away from a selected shape's edge to grow a live copy of it, or toward the inside to shrink it; the copy carries a live Offset Path effect, so it stays editable until Bake (Expand Appearance), a second drag changes the same Contour instead of nesting one, and a floating bar sets the distance and the Sharp, Round or Square corners. `object.contour` is the command behind it. A VectorCraft design built on Offset Path; no other application was inspected. Scores and estimates unchanged. | diff --git a/docs/roadmap.md b/docs/roadmap.md index 7204ed34b..7f2600384 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,8 @@ # Roadmap: milestones and what's next -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-10 · **Change:** minor (M10/G5: Map Markers built-in symbol library; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: G2: Effect Gallery thumbnails and in-dialog preview are in; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; M4/M7: `.ai`/EPS type keeps its tab stops, and right-aligned lines with tabs lay out as the file's page, #6147; M4/M7: `.ai`/EPS type from the text document lays its lines out closer to the file's page, within limits, #6137; numeric fields step on wheel hover without taking keyboard focus, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Selection QoL: marquee modes and visible feedback, #5313; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; Indian-language hyphenation review and license allowlist, #4947; Hindi catalog and contributor loanword preferences; full wording review in progress; Effect dialogs: a colour swatch beside a hex colour opens the Color Picker, #5365; shape tools move the selection from its centre point and turn it from the zone outside a box corner, #5477; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-10 · **Change:** minor (resizable icon-strip flyouts and bottom-aligned panel footers, #5713; previously minor: M10/G5: Map Markers built-in symbol library; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: G2: Effect Gallery thumbnails and in-dialog preview are in; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; M4/M7: `.ai`/EPS type keeps its tab stops, and right-aligned lines with tabs lay out as the file's page, #6147; M4/M7: `.ai`/EPS type from the text document lays its lines out closer to the file's page, within limits, #6137; numeric fields step on wheel hover without taking keyboard focus, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Selection QoL: marquee modes and visible feedback, #5313; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; Indian-language hyphenation review and license allowlist, #4947; Hindi catalog and contributor loanword preferences; full wording review in progress; Effect dialogs: a colour swatch beside a hex colour opens the Color Picker, #5365; shape tools move the selection from its centre point and turn it from the zone outside a box corner, #5477; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) + +> **Flyout update:** 2026-11-06 — resizable icon-strip flyouts and bottom-aligned panel footers (#5713); scores and estimates unchanged. > Numeric spinner update (2026-11-10): held mouse arrows repeat past the click timeout; scores and estimates unchanged (#6307). @@ -10,6 +12,8 @@ Numeric fields step on wheel hover without taking keyboard focus (#5949). Arrow Raster rendering fits and preflattens curved stroke outlines more tightly only below four times the raster tolerance, retaining thin-band coverage without changing stroke weight or vector export geometry. Current cached character/object outlines, cropped long strokes, gradient clips and effect rasters share the helper; cropped-region reuse and guarded flattening remain. The unchanged `fractional_curved_strokes_retain_coverage_between_notches` regression covers fractional weights, rotation, phase, opacity and f32-rounded endpoints. This is a quality/cost tradeoff, not a speed claim. Scores and estimates unchanged. +Icon-strip flyouts resize from the canvas-side edge and bottom, keeping the icon-column edge anchored (#5713). Independent group heights and flyout widths persist in workspaces; saved heights override content fitting while untouched heights still fit content. Shared panel button bars and the Glyphs font row stay at the bottom after enlargement; the Artboards list grows into the extra space. Regressions: `panel_docking::tests::glyphs_and_artboards_flyouts_keep_user_sizes` and `expanded_panel_footers_stay_at_the_bottom_with_short_and_long_lists`. Scores and estimates unchanged. + **PR #3273 revision — 2026-11-04:** Thin curved stroke coverage ported to the current raster-only cache, cropped-long-stroke and effect paths with guarded flattening; vector export geometry unchanged. Scores and estimates unchanged. Group Selection keeps an already selected component selected during a drag; a completed click still adds its next enclosing group. The current Direct Selection click/release machinery is shared, preserving compound-member picks, Shift deselection, isolation and masked-art dragging. Outline Stroke continues to preserve all original fills, including open contours; the change does not remove fills. Regressions: `group_selection_distinguishes_selected_leaf_drags_from_clicks` and `outline_fill_and_stroke_move_independently_with_group_selection` cover tool events, independent component movement and Undo/Redo. Scores and estimates unchanged. @@ -50,6 +54,7 @@ M7 type: Touch Type moves a right-to-left character along its line and up from i | 2026-11-02 | minor | Selection QoL: saved marquee modes, screen drag direction with a 3 px dead zone, and translated mode/E feedback (#5313); VectorCraft addition, scores and estimates unchanged. | | 2026-11-07 | minor | Indian-language hyphenation (#4947): eleven MIT-licensed pattern sets, with grapheme and shaping-cluster protection in both composers and existing paragraph limits. Assamese and Sanskrit are selectable; Sanskrit automatic patterns remain pending an allowlisted source. Scores and estimates unchanged. | | 2026-11-05 | minor | Effect dialogs: a colour swatch beside a hex colour field follows typed values, and a double-click opens the Color Picker over the dialog, whose OK fills the field (#5365); scores and estimates unchanged. | +| 2026-11-06 | minor | Icon-strip flyouts resize on their free edges and retain sizes with workspaces (#5713); saved heights beat auto-fit; panel footers stay at the bottom after enlargement. Scores and estimates unchanged. | | 2026-11-05 | minor | Selection Tools add-on: Select Linked, Embedded, Missing and Modified select the images and placed documents with that link status (Object › Plug-ins › Selection Tools and the Links panel menu; Shift adds), and Select by Link Status… lists them by name, status and format to pick from, optionally zooming to them; what is hidden, locked, on a template layer or outside the isolated group is left out and the status bar says how many (#5526, a VectorCraft addition); scores and estimates unchanged (estimated). | | 2026-11-05 | minor | Pencil and Paintbrush: a new stroke with Keep selected off leaves nothing selected, the earlier selection included, while Keep selected on selects only the new path (#5752); scores and estimates unchanged. | | 2026-11-05 | minor | Layers panel: a layer whose Layer Options › Print is off shows its name italic, as the reference application shows non-printing layers, and keeps it while the row is dragged (#5739); scores and estimates unchanged. | @@ -155,7 +160,7 @@ native wording review, layout checks and a web Devanagari font remain. [Notes](l | M12 | Advanced color & art (CMYK/ICC, separations, Gradient Mesh, Live Paint, Image Trace, Graphs) | 🟡 Gradient Mesh, Live Paint, Image Trace (junction-returning pieces retain their positioned boundaries; a simpler contour walk with identical output; an edge's sub-pixel shift is the same from either side; notches read only their own piece; collapsed loops keep few segments; large images trace with a warning; Auto Color keeps rare distinct colours; quick presets trace at once and the Image Trace button opens the adjustment controls; Properties reopens them; tiny fitted contours retain islands and holes; Create Strokes: exact crop EDT, Guo–Hall thinning and maximal-disc graph simplification; 15 presets, the 14 documented ones each with an enhanced version; Ignore Color, Shapes, Auto Grouping and Gradients options; measured in `image-trace-quality.md`; closed outlines retain their interior after straightening; the Flat Logo mode places edges to a fraction of a pixel, restores sharp corners, snaps lines and circles, and refuses gradients; live trace result bounds and selection highlights exclude the retained bitmap except in source-image views), Recolor Artwork, colour management (ICC, soft proofing, separations preview), Graphs (all 9 tools, Graph Data/Type, group-selected series colours persist through regenerate/save/open) done; Graph Design and Marker designs, Transpose and Switch x/y, `\|` label line breaks, the pie options, drop shadows, the legend across the top, overlapping widths, First Row/Column in Front, filled lines, kept label formatting and the Graph Data worksheet done; graph Column designs pending | 6–10 | | M13 | Automation (Actions ✅ record/playback, persisted; variables, scripting, batch) | 🟡 MCP `add_text` exposes existing vertical point, area and path type; `script.run` and File › Scripts › Other Script… run JavaScript files; the script document model and console and batch work remain | 18–27 | | M13 | Automation (Actions ✅ record/playback, persisted; variables, scripting, batch) | 🟡 | 18–27 | -| M14 | 1.0 polish (preferences, shortcut editor, workspaces, accessibility, packaging for all OSes) (status messages and readouts use stable responsive allocations sized for localized labels and ordinary coordinates; sample widths cached by language/unit/DPI; regression-tested and contributor-confirmed 2026-10-19; estimates unchanged) | 🟡 Interim macOS cursor delivery (#2292) uses existing artwork and host-owned AppKit cursors; headless coverage only, native menu/focus/handback and display validation pending. Dock groups collapse to their tabs by a double-click on a tab (UI.8); Preferences › General › Constrain Angle acts for the arrow keys, Shift drags and new objects (#394, 2026-10-21); numeric-field pointer selection and focus regression-tested (#1326, in part; short movements still select all); RPM runtime capabilities and glibc floor checked (#1275; real-distro install/start-up pending), Eyedropper cursor with precise-setting checks (#1269), Preferences, shortcut editor (chords with Shift and a punctuation key fire as typed; other apps' text shortcut sets imported), workspaces (each keeps its layout changes until Reset , UI.7), Window › Help Bar shown or hidden apart from the status bar and kept in saved workspaces, panels docked by dragging into any dock group (as tabs or a top/bottom split) or into the icon column, and closed from a dock tab's context menu (UI.6), a custom title bar on Windows/Linux with a Home button, content-sized dialogs, menus that scroll when longer than the window and macOS native menu dismissal done (M14.8), and native macOS save sheets with a Format menu (#866); titled PDF exports set `DisplayDocTitle` for viewers and accessibility checkers (#3646); accessibility and broader Windows/Linux packaging/QA pending; floating tab groups collapse into the side icon rail and opened rail panels fold back into it (#5925, in part), with pointer and native-window regression coverage; estimates unchanged; numeric spinner arrows keep repeating while held with the mouse (#6307) | 18–28 | +| M14 | 1.0 polish (preferences, shortcut editor, workspaces, accessibility, packaging for all OSes) (status messages and readouts use stable responsive allocations sized for localized labels and ordinary coordinates; sample widths cached by language/unit/DPI; regression-tested and contributor-confirmed 2026-10-19; estimates unchanged) | 🟡 Icon-strip flyouts resize on their free edges and retain sizes (#5713); interim macOS cursor delivery (#2292) uses existing artwork and host-owned AppKit cursors; headless coverage only, native menu/focus/handback and display validation pending. Dock groups collapse to their tabs by a double-click on a tab (UI.8); Preferences › General › Constrain Angle acts for the arrow keys, Shift drags and new objects (#394, 2026-10-21); numeric-field pointer selection and focus regression-tested (#1326, in part; short movements still select all); RPM runtime capabilities and glibc floor checked (#1275; real-distro install/start-up pending), Eyedropper cursor with precise-setting checks (#1269), Preferences, shortcut editor (chords with Shift and a punctuation key fire as typed; other apps' text shortcut sets imported), workspaces (each keeps its layout changes until Reset , UI.7), Window › Help Bar shown or hidden apart from the status bar and kept in saved workspaces, panels docked by dragging into any dock group (as tabs or a top/bottom split) or into the icon column, and closed from a dock tab's context menu (UI.6), a custom title bar on Windows/Linux with a Home button, content-sized dialogs, menus that scroll when longer than the window and macOS native menu dismissal done (M14.8), and native macOS save sheets with a Format menu (#866); titled PDF exports set `DisplayDocTitle` for viewers and accessibility checkers (#3646); accessibility and broader Windows/Linux packaging/QA pending; floating tab groups collapse into the side icon rail and opened rail panels fold back into it (#5925, in part), with pointer and native-window regression coverage; estimates unchanged; numeric spinner arrows keep repeating while held with the mouse (#6307) | 18–28 | | — | Interaction fidelity pass (every tool's modifiers, Properties panel per context, isolation, nuance; width-point creation and adjoining adjustment regression-tested; dialog layout reviewed) | ⬜ (new ruler-guide Alt orientation implemented; existing-guide Escape cancellation regression-tested; broader pass remains) | 60–90 | | — | Hardening at scale (big-file corpus, fuzzing, cross-platform + browser QA) | 🟡 PngSuite integrity and bounded fetching verified (#1729); real-world corpus/manual QA remain | 50–80 | | M14 follow-up | Color Picker layout (#2008) | ✅ Compact controls, centered header, stacked actions, aligned fields and screen sampling; narrow windows scroll vertically | Included above | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index dd37113d7..73fad19c2 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -2,6 +2,8 @@ > **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-10 · **Change:** minor (G5: Map Markers built-in symbol library; bundled content ~32% → ~34%; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: Canvas: a 3D effect changed on type or a group redraws where its solid was; Fit to Artwork Bounds and per-artboard SVG export build each solid once for all artboards; previously minor: Effect Gallery: a thumbnail for each filter and a preview area inside the dialog, on an original drawing; Raster effects 52% → 55%, hours 1–2 → none; headline numbers unchanged; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; G5: UI Frames built-in symbol library with default 9-slice; bundled content ~32%; previously minor: G5: dynamic symbol part on-canvas opacity/blend/gradient/pattern; strokes remaining no longer lists Overlap/auto corners; symbols ~88%; previously minor: interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; previously minor: harakat paint fallback clarified; previously minor: current Guides feature-area rows consolidated using existing ruler evidence; scores and estimates unchanged; previously minor: Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; previously minor: G5 docs: Art brush Overlap and Pattern brush auto corners removed from strokes remaining; symbols remaining lists dynamic on-canvas beyond solid and guides outside the art; previously minor: custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; `.ai`/EPS type keeps its tab stops, and right-aligned lines with tabs lay out as the file's page, #6147; `.ai`/EPS type from the text document lays its lines out closer to the file's page, within limits, #6137; `.ai` and EPS import: older Art brush objects read; numeric fields step on wheel hover without taking keyboard focus, and a panel wheel turn cannot seed a canvas zoom glide, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Tools stays beside the canvas with left-side panels; new gradients follow document color mode; Selection QoL: marquee modes and visible feedback, #5313; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; the canvas keeps selection outlines and anchors between frames; Indian-language hyphenation review and license allowlist, #4947; Stroke gradients: an arrowhead aligned to the tip is painted to the corners of its base, #5964; Drop Shadow and glow dialogs: spinner arrows, #5367, #5368; Stroke Weight Up and Down shortcuts, #5797; Hindi catalog and contributor loanword preferences; full wording review in progress; Properties in Edit Artboards: a Transform section with the reference point above the Artboards section, #5219; the Control bar's Stroke: and Opacity: links scrub their fields, #4088; an image zoomed in on is separated, colorized or proofed only where it shows (#4155); a document's first frame renders on the render worker, so opening a heavy file no longer freezes the window, #4155; Control bar Paragraph section with type selected, #5372; translating unchanged type reuses its glyph and character-stroke outlines; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026; Units › East Asian Type acts, with Q and Ha units; scores unchanged) +> **Flyout update:** 2026-11-06 — resizable icon-strip flyouts and bottom-aligned panel footers (#5713); scores and estimates unchanged. + ## Feature submission evidence EPS embedded fonts, 2026-11-09 (#6023–#6067): the fonts an EPS carries draw their glyphs instead of being set as type in an installed font: composite (Type 0) fonts with the 8/8 mapping, plain Type 42 fonts (characters from their glyph names or character map, widths from the font), CID-keyed fonts over CMaps (Type 0 and Type 2 CIDFonts, CIDFonts in a FontSet's Compact Font Format data, base-font CMaps, named CIDFonts over predefined Unicode CMaps set as type) and embedded Type 1 fonts; the show operators' spacing along the baseline is kept as tracking, `kshow` included. Each step is charged to the interpreter's budgets. Hand-written test files only (no corpus file uses these fonts); scores and estimates unchanged. Evidence: `crates/eps/src/import/tests_composite.rs`, `tests_type42.rs`, `tests_cid.rs`, `tests_type1.rs`; details in [file-format-parity.md](file-format-parity.md). @@ -10,6 +12,8 @@ Numeric fields step on wheel hover without taking keyboard focus (#5949). Arrow Raster rendering fits and preflattens curved stroke outlines more tightly only below four times the raster tolerance, retaining thin-band coverage without changing stroke weight or vector export geometry. Current cached character/object outlines, cropped long strokes, gradient clips and effect rasters share the helper; cropped-region reuse and guarded flattening remain. The unchanged `fractional_curved_strokes_retain_coverage_between_notches` regression covers fractional weights, rotation, phase, opacity and f32-rounded endpoints. This is a quality/cost tradeoff, not a speed claim. Scores and estimates unchanged. +Icon-strip flyouts resize from the canvas-side edge and bottom, keeping the icon-column edge anchored (#5713). Independent group heights and flyout widths persist in workspaces; saved heights override content fitting while untouched heights still fit content. Shared panel button bars stay at the bottom after enlargement. Regressions: `panel_docking::tests::glyphs_and_artboards_flyouts_keep_user_sizes` and `expanded_panel_footers_stay_at_the_bottom_with_short_and_long_lists`. Scores and estimates unchanged. + **PR #3273 revision — 2026-11-04:** Thin curved stroke coverage ported to the current raster-only cache, cropped-long-stroke and effect paths with guarded flattening; vector export geometry unchanged. Scores and estimates unchanged. Group Selection keeps an already selected component selected during a drag; a completed click still adds its next enclosing group. The current Direct Selection click/release machinery is shared, preserving compound-member picks, Shift deselection, isolation and masked-art dragging. Outline Stroke continues to preserve all original fills, including open contours; the change does not remove fills. Regressions: `group_selection_distinguishes_selected_leaf_drags_from_clicks` and `outline_fill_and_stroke_move_independently_with_group_selection` cover tool events, independent component movement and Undo/Redo. Scores and estimates unchanged. @@ -58,6 +62,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard | 2026-11-06 | minor | Stroke gradients: an arrowhead whose tip sits on the end of a long straight leg is painted by the gradient to the corners of its base. The slices that reach across a head now run to the end of its reach instead of narrowing towards the next corner (#5964). Test: `stroke::tests_gradient::slices_cover_an_arrowhead_on_a_long_straight_leg`. Scores and estimates unchanged. | | 2026-11-06 | minor | Width profiles: the width between two width points follows a smooth curve instead of a straight line, so a stroke no longer kinks at each width point (#5714). Each side is a monotone cubic through its points: flat at a widest or narrowest point, never past either point of a span; stacked points still step. Straight runs are sampled between width points to the drawing tolerance. Scores and estimates unchanged. | | 2026-11-05 | minor | A nearby hit on a plain long stroke or outline avoids assembling its whole path. A hit near the middle of a 200,000-anchor path took about 11–13 ms before and 1.0–1.4 ms after in paired runs on a busy Windows machine; a miss inside the path's box fell from 18.7 ms to 1.6 ms, while a distant miss was unchanged. Other stroke styles and fill hits keep their existing checks. Scores and estimates unchanged; idle performance budgets remain unverified. | +| 2026-11-06 | minor | Icon-strip flyouts resize on their free edges and retain sizes with workspaces (#5713); saved heights beat auto-fit; panel footers stay at the bottom after enlargement. Scores and estimates unchanged. | | 2026-11-05 | minor | Translating a type object without changing its text or formatting reuses glyph geometry and expanded character-stroke outlines between frames. The text appears at its new position with unchanged pixels. Scaling the type, editing its content or formatting, or changing installed fonts rebuilds the geometry; scores and estimates unchanged. | | 2026-11-05 | minor | Moving a large selection reuses the object locations found during command validation. A 20,000-object nudge took 58.07 ms before and 51.26 ms after in paired median runs; object positions, selection, and undo behavior stay the same. Scores and estimates unchanged. | | 2026-11-05 | minor | Canvas: an image drawn larger than 1,024 pixels at the size it shows, with its colours made again (separated into ink planes, colorized or shown through Separations Preview), has them made only for the 1,024-pixel tiles in view, each kept for later frames (#4155). Zooming in to 50%, 100% and 200% on the file of #4155 went from 8.6–9.0 s to 0.78 s, 0.05 s and 0.05 s (measured, M3 Pro, one run each), with the same pixels. Scores and estimates unchanged. | @@ -236,7 +241,7 @@ dominate; 3D, graphs and automation are specialist). They sum to 105. Feature de | Export for Screens, Asset Export, slices, Save for Web | 3 | 85% | anti-aliasing ghost borders (#983); TIFF choices and settings are implemented | 2–4 | estimated | | Print, colour management, separations, flattener | 3 | 80% | print preview fidelity | 2–4 | estimated | | Automation (Actions, Variables, scripting, batch) | 3 | 60% | a scripting surface (Illustrator: JavaScript/ExtendScript), Batch, image and graph variables, dataset import/export | 8–14 | estimated; MCP `add_text` exposes existing vertical type; score and hours unchanged | -| UI chrome (panels, Properties, workspaces, preferences) | 6 | 80% | Properties per context, 3 Preferences options stored but unused (#394, audited 2026-10-30: Open Documents As Tabs, Scratch Disks, legacy vector clipboard), SVG Interactivity, context menus beyond the canvas, accessibility | 13–20 | estimated; floating panels in OS windows of their own on the desktop (#4135), score and hours unchanged; macOS save Format menu implemented (#866), score and hours unchanged; Eyedropper cursor and precise setting checked (#1269), OS visual QA unverified; numeric-field focus and normal text drags regression-tested (#1326, in part); movements below the click threshold still select all; artboard navigation synchronizes the tool selection before destructive actions; status messages and readouts use stable responsive allocations sized for localized labels and ordinary coordinates (sample widths cached by language/unit/DPI; regression and contributor original-file evidence below; scores/hours unchanged); effect dialogs' colour fields show a swatch that opens the Color Picker (#5365), score and hours unchanged; floating tab groups collapse into the side icon rail and opened rail panels fold back into it (#5925, in part), scores and hours unchanged ; interim macOS cursor delivery (#2292), headless coverage only; native validation pending, score and hours unchanged | +| UI chrome (panels, Properties, workspaces, preferences) | 6 | 80% | Properties per context, 3 Preferences options stored but unused (#394, audited 2026-10-30: Open Documents As Tabs, Scratch Disks, legacy vector clipboard), SVG Interactivity, context menus beyond the canvas, accessibility | 13–20 | estimated; floating panels in OS windows of their own on the desktop (#4135), score and hours unchanged; macOS save Format menu implemented (#866), score and hours unchanged; Eyedropper cursor and precise setting checked (#1269), OS visual QA unverified; numeric-field focus and normal text drags regression-tested (#1326, in part); movements below the click threshold still select all; artboard navigation synchronizes the tool selection before destructive actions; status messages and readouts use stable responsive allocations sized for localized labels and ordinary coordinates (sample widths cached by language/unit/DPI; regression and contributor original-file evidence below; scores/hours unchanged); effect dialogs' colour fields show a swatch that opens the Color Picker (#5365), score and hours unchanged; icon-strip flyouts retain user sizes (#5713); floating tab groups collapse into the side icon rail and opened rail panels fold back into it (#5925, in part), scores and hours unchanged ; interim macOS cursor delivery (#2292), headless coverage only; native validation pending, score and hours unchanged | | Libraries, Links, Package | 2 | 82% | shared libraries, linked graphics, gradients and patterns as library items (no cloud by design) | 2–3 | estimated | | Generative AI (Text to Vector, Generative Recolor, Shape Fill, Concept to Vector, Prompt to Edit, Rewrite Text) | 2 | 0% | a pluggable provider and the panels; needs an owner decision on models | 30–60 | measured (absent) | | **Feature depth** | **105** | **~75%** | | **~250–420** | | diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 7629084b8..0a3ae90c9 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -4,6 +4,8 @@ > Numeric spinner update (2026-11-10): held mouse arrows repeat past the click timeout; scores and estimates unchanged (#6307). +> **Flyout update:** 2026-11-06 — resizable icon-strip flyouts and bottom-aligned panel footers (#5713); scores and estimates unchanged. + ## Feature submission evidence Numeric fields step on wheel hover without taking keyboard focus (#5949). Arrow keys still require focus; modifiers, units, limits, mixed values and existing rounding stay in the shared stepping path. Headless regressions in `tests/numfields.rs` cover hovered unfocused fields, focused fields, trackpad accumulation and panel scroll exclusion. Scores and estimates unchanged. @@ -11,6 +13,8 @@ Numeric fields step on wheel hover without taking keyboard focus (#5949). Arrow | 2026-11-06 | minor | Opening a file that takes longer than 0.4 s shows a progress window: the file name, a moving bar (reading reports no steps), how many more files wait, Cancel (Esc; the file doesn't open) and Continue in Background (`file.cancelOpen`, `file.openInBackground`); it never opens over another dialog or progress window (part of #5930); scores and estimates unchanged | Raster rendering fits and preflattens curved stroke outlines more tightly only below four times the raster tolerance, retaining thin-band coverage without changing stroke weight or vector export geometry. Current cached character/object outlines, cropped long strokes, gradient clips and effect rasters share the helper; cropped-region reuse and guarded flattening remain. The unchanged `fractional_curved_strokes_retain_coverage_between_notches` regression covers fractional weights, rotation, phase, opacity and f32-rounded endpoints. This is a quality/cost tradeoff, not a speed claim. Scores and estimates unchanged. +Icon-strip flyouts resize from the canvas-side edge and bottom, keeping the icon-column edge anchored (#5713). Independent group heights and flyout widths persist in workspaces; saved heights override content fitting while untouched heights still fit content. Shared panel button bars and the Glyphs font row stay at the bottom after enlargement; the Artboards list grows into the extra space. Regressions: `panel_docking::tests::glyphs_and_artboards_flyouts_keep_user_sizes` and `expanded_panel_footers_stay_at_the_bottom_with_short_and_long_lists`. Scores and estimates unchanged. + **PR #3273 revision — 2026-11-04:** Thin curved stroke coverage ported to the current raster-only cache, cropped-long-stroke and effect paths with guarded flattening; vector export geometry unchanged. Scores and estimates unchanged. Group Selection keeps an already selected component selected during a drag; a completed click still adds its next enclosing group. The current Direct Selection click/release machinery is shared, preserving compound-member picks, Shift deselection, isolation and masked-art dragging. Outline Stroke continues to preserve all original fills, including open contours; the change does not remove fills. Regressions: `group_selection_distinguishes_selected_leaf_drags_from_clicks` and `outline_fill_and_stroke_move_independently_with_group_selection` cover tool events, independent component movement and Undo/Redo. Scores and estimates unchanged. @@ -58,6 +62,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard | 2026-11-06 | minor | Shape tools: a press on the selection's centre point (Show Center) moves the selection, also where no art is painted, and a press in the rotate zone just outside a bounding-box corner turns it about the centre, both as the Selection tool's do (#5477); the Selection tool takes its centre point too, so an unfilled shape moves instead of starting a marquee; scores and estimates unchanged | | 2026-11-06 | minor | Transform fields: one Tab moves from one numeric field to the next in the Transform panel, the Properties panel's Transform section and the Control bar — a field's scrub label was a second, invisible tab stop, and the W/H link between W and H is now made after the fields, so it stays a tab stop after them; scores and estimates unchanged | | 2026-11-06 | minor | Scrubby labels: the Control bar's Stroke: and Opacity: and the Properties panel's Stroke, links to their panels, scrub the field after them as other labels do; a click still opens the panel (#4088); scores and estimates unchanged | +| 2026-11-06 | minor | Icon-strip flyouts resize on their free edges and retain sizes with workspaces (#5713); saved heights beat auto-fit; panel footers stay at the bottom after enlargement. Scores and estimates unchanged. | | 2026-11-05 | minor | Direct Selection: Shift-click picks several points of a blend's spine and dragging one moves them all, with the key objects on them, in one undo step (#5285); scores and estimates unchanged | | 2026-11-05 | minor | Direct Selection: Cmd/Ctrl held lends the Selection tool, whose bounding box shows while the key is down over objects selected whole; its handles scale and rotate, and a Cmd drag moves the whole object with its direct-selected anchors kept (#3901); scores and estimates unchanged | | 2026-11-05 | minor | Join tool: scrubbing over the ends of two crossing open paths that run past the crossing trims those ends and joins the paths there with a corner, instead of drawing a segment between the ends (#5844); scores and estimates unchanged | @@ -154,6 +159,7 @@ tools, never checked side by side) isn't captured row by row. | Busy cursor during long operations | done | Background open, save and export show the system progress cursor (gap G1) | | Each anchor snaps to Smart Guides and construction angles | done | `DrawSnap` | | Curvature tool: click/drag points, double-click or Alt-click for a corner, temporary Direct Selection, point conversion | partial | circular point markers without direction handles or duplicate markers, Anchor Size and Hide Edges regression-tested (#1236, in part); curve geometry: each stretch of three points follows the circle through them (handles along its tangent, chord/(3·cos²(α/2)) long, shorter in a turn tighter than 30°), open ends curving in, the same rule for the rubber band, dragged, inserted and converted points (#1236, in part); neighbours of a point dragged on an existing path keep their tangents, and modifiers requested in #975 remain | +| Icon-strip flyout resizing (#5713) | done | Canvas-side edge and bottom; user size persists after reopening and workspace restore; Glyphs and Artboards pointer/cursor regression | | Layers panel lists a blend's spine (#4820) | done | an open blend shows a `` row below its key objects, where Release leaves the spine; a click selects the blend (Shift/Cmd toggles it) so Direct Selection edits the spine; a locked blend's row selects nothing; no row while searching or filtering (`a_blends_spine_is_a_row_below_its_keys_that_selects_the_blend`) | | Layers panel: a layer whose Layer Options › Print is off shows its name italic (#5739) | done | the panel row and its drag ghost; a printing layer stays upright, and Print on again restores it; `panels::layers` test `a_layer_that_does_not_print_has_its_name_italic` | | Blend spine Anchor Point conversion and handle editing (#2544) | implemented | symmetric handles, independent drag/retraction, visible-handle policy and selection/stacking covered by tool and engine regressions; Windows native canvas checks and screenshots reviewed in PR #2643 |