mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-22 19:08:39 +00:00
1 parent
d6d91c0954
commit
9cda8452ab
33 files changed
+635
-52
No files matched your search
@@ -123,7 +123,7 @@ fn panel_icon(app: &mut VectorcraftApp, ui: &mut Ui, id: &'static str, label: &s
|
||||
if resp.clicked() {
|
||||
app.ui.open_panel = if open { None } else { Some(id.to_string()) };
|
||||
}
|
||||
crate::panel_docking::legacy_tab(app, ui, id, &resp);
|
||||
crate::panel_docking::legacy_tab(app, ui, id, &resp, false);
|
||||
}
|
||||
|
||||
pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
@@ -133,6 +133,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
apply_pending_dock_width(app, ui.ctx(), LEGACY_DOCK_PANEL, clamp_legacy_dock_width);
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let shown = floating::shown_tab(&app.ui);
|
||||
ui.ctx().data_mut(|d| d.insert_temp(floating::root_groups_id(), Vec::<floating::RootGroup>::new()));
|
||||
// Main tabbed group, unless it is collapsed to icons or all its panels float.
|
||||
let group = shown.filter(|_| !app.ui.dock_collapsed).map(|active| {
|
||||
egui::Panel::right(LEGACY_DOCK_PANEL)
|
||||
@@ -145,6 +146,15 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
if collapse_header(ui, false) {
|
||||
toggle(app, true);
|
||||
}
|
||||
// What a group dropped on it joins ([`floating::Drop::Root`]).
|
||||
let rect = ui.available_rect_before_wrap();
|
||||
let group = floating::RootGroup {
|
||||
panels: floating::docked_tabs(&app.ui).map(|tab| tab.info().0).collect(),
|
||||
shown: active.info().0,
|
||||
rect,
|
||||
strip: Rect::from_min_size(rect.min, vec2(rect.width(), 33.0)),
|
||||
};
|
||||
ui.ctx().data_mut(|d| d.insert_temp(floating::root_groups_id(), vec![group]));
|
||||
if tab_strip(app, ui, active) {
|
||||
// It floats from this frame on.
|
||||
return;
|
||||
@@ -251,7 +261,7 @@ fn tab_strip(app: &mut VectorcraftApp, ui: &mut Ui, active: DockTab) -> bool {
|
||||
} else if resp.clicked() {
|
||||
app.ui.dock_tab = tab;
|
||||
}
|
||||
crate::panel_docking::legacy_tab_in_strip(app, ui, id, resp, Some(strip));
|
||||
crate::panel_docking::legacy_tab_in_strip(app, ui, id, resp, true, Some(strip));
|
||||
}
|
||||
if let Some(tab) = out.picked.and_then(|i| tabs.get(i)).copied() {
|
||||
app.ui.dock_tab = tab;
|
||||
@@ -398,8 +408,10 @@ pub fn floating_panel(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
return;
|
||||
};
|
||||
let tab = DockTab::from_id(id);
|
||||
// Expanded, the group shows its panels in the dock itself; a floating panel shows in its group.
|
||||
if tab.is_some() && !app.ui.dock_collapsed || floating::group_of(&app.ui, id).is_some() {
|
||||
// Expanded, the group shows its panels in the dock itself, unless one was dropped on the icon
|
||||
// column; a floating panel shows in its group.
|
||||
let in_rail = app.ui.docking_icons.contains(id) && app.ui.docking.as_ref().is_some_and(|layout| !layout.contains(&id.to_string()));
|
||||
if tab.is_some() && !app.ui.dock_collapsed && !in_rail || floating::group_of(&app.ui, id).is_some() {
|
||||
return;
|
||||
}
|
||||
// A library panel (in a collapsed dock) is titled by its library's name.
|
||||
|
||||
+134
-13
@@ -3,10 +3,12 @@
|
||||
//! A dock tab, an icon of the icon column or a popped-out panel's title dragged out of the dock
|
||||
//! floats that panel; the strip right of the dock's tabs floats the whole tabbed group. A floating
|
||||
//! group moves by its title bar (or the strip right of its tabs, or the tab of a lone panel), and a
|
||||
//! tab dragged out of a group floats on its own. Dropped on the dock, which lights up while the
|
||||
//! pointer is over it, a group's panels go back to where they live in the dock (the tabbed group or
|
||||
//! the icon column); dropped on another group's title bar or tabs, they stack with that group. The
|
||||
//! × of a group puts its panels back in the dock too. The Tools panel floats the same way by its
|
||||
//! tab dragged out of a group floats on its own. Dropped on the tabs of one of the dock's groups
|
||||
//! (lit up), a group's panels join that group as tabs; dropped on the upper or lower half of it,
|
||||
//! they make a new group above or below it in the dock. Dropped on the icon column, which lights
|
||||
//! up, they become icons there, the tabbed group's panels too; dropped anywhere else on the dock,
|
||||
//! they go back to where they live in it. Dropped on another floating group's title bar or tabs,
|
||||
//! they stack with that group. The × of a group puts its panels back in the dock too. The Tools panel floats the same way by its
|
||||
//! title bar ([`crate::toolbar`]) and docks again on the window's left edge.
|
||||
//!
|
||||
//! Dropped on the upper or lower half of another group (a line shows where), a group joins it in
|
||||
@@ -80,6 +82,47 @@ pub(crate) enum Drop {
|
||||
Above(&'static str),
|
||||
/// In the dock as a new column at this place (1: next to the icon column, counting left).
|
||||
Column(u32),
|
||||
/// In the dock's group showing this panel: as tabs, or as a new group above or below it.
|
||||
Root(&'static str, RootZone),
|
||||
/// In the icon column, as icons (`window.panel.dock {icon: true}`).
|
||||
Icons,
|
||||
}
|
||||
|
||||
/// Where a drop on one of the dock's groups puts the moved panels.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum RootZone {
|
||||
/// With the group, as tabs.
|
||||
Tab,
|
||||
/// In a new group right above it.
|
||||
Top,
|
||||
/// In a new group right below it.
|
||||
Bottom,
|
||||
}
|
||||
|
||||
impl RootZone {
|
||||
/// The `zone` of `window.panel.dock` / `window.panel.move`.
|
||||
pub(crate) fn id(self) -> &'static str {
|
||||
match self {
|
||||
RootZone::Tab => "tab",
|
||||
RootZone::Top => "top",
|
||||
RootZone::Bottom => "bottom",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One of the dock's groups as last drawn (egui temp memory under [`root_groups_id`]): its panels,
|
||||
/// the one it shows, its bounds and its tab strip. A group dropped on it joins it ([`Drop::Root`]).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(crate) struct RootGroup {
|
||||
pub panels: Vec<&'static str>,
|
||||
pub shown: &'static str,
|
||||
pub rect: Rect,
|
||||
pub strip: Rect,
|
||||
}
|
||||
|
||||
/// The dock's groups as last drawn, top to bottom.
|
||||
pub(crate) fn root_groups_id() -> Id {
|
||||
Id::new("floating-root-groups")
|
||||
}
|
||||
|
||||
fn move_id() -> Id {
|
||||
@@ -459,7 +502,31 @@ pub(crate) fn drop_target(app: &VectorcraftApp, ctx: &egui::Context, what: Movin
|
||||
let y = ctx.data(|d| d.get_temp::<Rect>(group_rect_id(last.0))).map_or(col.bottom(), |r| r.bottom()).min(col.bottom() - 2.0);
|
||||
return Some((Drop::Below(last.0), Rect::from_x_y_ranges(col.x_range(), (y - 2.0)..=(y + 2.0))));
|
||||
}
|
||||
let dock = zone(dock_rect_id()).filter(|_| app.ui.dock && app.ui.screen_mode < 3)?;
|
||||
let shown = app.ui.dock && app.ui.screen_mode < 3;
|
||||
// The icon column takes the panels as icons. (Mid-frame the dock's rect may not take it in yet.)
|
||||
if let Some(icons) = zone(icons_rect_id()).filter(|_| shown) {
|
||||
return Some((Drop::Icons, icons));
|
||||
}
|
||||
let dock = zone(dock_rect_id()).filter(|_| shown)?;
|
||||
// One of the dock's groups: its tabs take the panels as tabs, its upper or lower half puts them
|
||||
// in a new group above or below it. The group they came from (being dragged by a tab) takes
|
||||
// nothing: the shared dock handles a tab moved within it.
|
||||
let moved: Vec<&str> = std::iter::once(id).chain(ids_in(&app.ui, &moving)).collect();
|
||||
let groups = ctx.data(|d| d.get_temp::<Vec<RootGroup>>(root_groups_id())).unwrap_or_default();
|
||||
if let Some(group) = groups.iter().find(|g| g.rect.contains(at)) {
|
||||
let anchor = Some(group.shown).filter(|p| !moved.contains(p)).or_else(|| group.panels.iter().copied().find(|p| !moved.contains(p)));
|
||||
if let Some(anchor) = anchor {
|
||||
if group.strip.contains(at) {
|
||||
return Some((Drop::Root(anchor, RootZone::Tab), group.strip));
|
||||
}
|
||||
let mid = group.rect.center().y;
|
||||
return Some(if at.y < mid {
|
||||
(Drop::Root(anchor, RootZone::Top), Rect::from_x_y_ranges(group.rect.x_range(), group.rect.top()..=mid))
|
||||
} else {
|
||||
(Drop::Root(anchor, RootZone::Bottom), Rect::from_x_y_ranges(group.rect.x_range(), mid..=group.rect.bottom()))
|
||||
});
|
||||
}
|
||||
}
|
||||
// Panels that aren't tabs of the tabbed group (or all of them, with it collapsed) go back to
|
||||
// their icons: light up the icon column, where they land, rather than the tabs.
|
||||
let tabbed = !app.ui.dock_collapsed
|
||||
@@ -492,8 +559,8 @@ fn insertion_line(ctx: &egui::Context, line: Rect) {
|
||||
pub(crate) fn preview(ctx: &egui::Context, target: Drop, rect: Rect) {
|
||||
match target {
|
||||
Drop::Below(_) | Drop::Above(_) | Drop::Column(_) => insertion_line(ctx, rect),
|
||||
Drop::Stack(_) => tab_zone(ctx, rect),
|
||||
Drop::Dock => highlight(ctx, rect),
|
||||
Drop::Stack(_) | Drop::Root(..) => tab_zone(ctx, rect),
|
||||
Drop::Dock | Drop::Icons => highlight(ctx, rect),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -548,7 +615,7 @@ pub fn track(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
}
|
||||
match target {
|
||||
Some((Drop::Below(_) | Drop::Above(_) | Drop::Column(_), line)) => insertion_line(ctx, line),
|
||||
Some((Drop::Stack(_), tabs)) => tab_zone(ctx, tabs),
|
||||
Some((Drop::Stack(_) | Drop::Root(..), tabs)) => tab_zone(ctx, tabs),
|
||||
Some((_, zone)) => highlight(ctx, zone),
|
||||
None => {}
|
||||
}
|
||||
@@ -565,7 +632,30 @@ pub fn track(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
let set = set_of(&app.ui, gi);
|
||||
let ids = ids_in(&app.ui, &set);
|
||||
let active = set.first().and_then(|&i| shown(&app.ui, i)).unwrap_or(id);
|
||||
// Icon panels dropped on the icon column go back to their icons, as on the rest of the dock.
|
||||
let drop = match drop {
|
||||
Drop::Icons if ids.iter().all(|p| crate::state::ICON_PANELS.iter().any(|entry| entry.0 == *p)) => Drop::Dock,
|
||||
drop => drop,
|
||||
};
|
||||
if matches!(drop, Drop::Root(..) | Drop::Icons) {
|
||||
// The shared layout puts them in the dock's tree, group by group: the first where it was
|
||||
// dropped, the others with it as tabs.
|
||||
let leads: Vec<&'static str> = set.iter().filter_map(|&i| shown(&app.ui, i)).collect();
|
||||
for (k, lead) in leads.iter().enumerate() {
|
||||
let params = match (drop, leads.first()) {
|
||||
(Drop::Root(anchor, zone), _) if k == 0 => json!({"panel": lead, "group": true, "anchor": anchor, "zone": zone.id()}),
|
||||
(Drop::Root(..), Some(first)) => json!({"panel": lead, "group": true, "anchor": first, "zone": "tab"}),
|
||||
_ => json!({"panel": lead, "group": true, "icon": true}),
|
||||
};
|
||||
if let Err(error) = app.run("window.panel.dock", params) {
|
||||
app.ui.status = error;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
match drop {
|
||||
Drop::Root(..) | Drop::Icons => {}
|
||||
Drop::Dock => {
|
||||
dock(&mut app.ui, &ids, active);
|
||||
// One that went back to its icon pops out of it, so it doesn't seem to vanish.
|
||||
@@ -1384,24 +1474,55 @@ mod tests {
|
||||
assert_eq!(h.dock_tabs().len(), 4);
|
||||
}
|
||||
|
||||
/// A floating icon panel dropped on the icon column lights it up, goes back and pops out of its icon.
|
||||
#[test]
|
||||
fn a_panel_dropped_on_the_dock_lights_up_and_pops_out_of_its_icon() {
|
||||
fn a_panel_dropped_on_the_icon_column_lights_it_up_and_pops_out_of_its_icon() {
|
||||
let mut h = Harness::new();
|
||||
let count = h.icons().len();
|
||||
h.drag(h.icons()[0].center(), pos2(500.0, 400.0));
|
||||
assert_eq!(h.groups(), [["color"]]);
|
||||
// Over the tabbed group: Color has no tab there, so the icon column lights up, not the tabs.
|
||||
let (dock, column) = (h.temp_rect(dock_rect_id()), h.temp_rect(icons_rect_id()));
|
||||
let over = h.dock_tabs()[1].center();
|
||||
let over = column.center();
|
||||
h.hold(h.title("color"), over);
|
||||
assert_eq!(drop_target(&h.app, &h.ctx, Moving::Panel("color"), over), Some((Drop::Dock, column)));
|
||||
assert_eq!(drop_target(&h.app, &h.ctx, Moving::Panel("color"), over), Some((Drop::Icons, column)));
|
||||
assert!(column.width() < dock.width(), "the column, not the whole dock: {column:?} {dock:?}");
|
||||
// Dropped, it's back in the column and pops out of its icon rather than vanishing.
|
||||
h.release(over);
|
||||
assert!(h.groups().is_empty());
|
||||
assert!(h.groups().is_empty(), "{}", h.app.ui.status);
|
||||
assert_eq!((h.icons().len(), h.app.ui.open_panel.as_deref()), (count, Some("color")));
|
||||
}
|
||||
|
||||
/// A floating panel dropped on the dock's tabs joins them; on the group's lower half it makes a group below.
|
||||
#[test]
|
||||
fn a_panel_dropped_on_the_dock_tabs_joins_them_and_on_its_lower_half_makes_a_group_below() {
|
||||
let mut h = Harness::new();
|
||||
h.drag(h.icons()[0].center(), pos2(500.0, 400.0));
|
||||
assert_eq!(h.groups(), [["color"]]);
|
||||
// Over the tabbed group's tabs: they light up, and Color joins them.
|
||||
let over = h.dock_tabs()[1].center();
|
||||
h.hold(h.title("color"), over);
|
||||
assert!(matches!(drop_target(&h.app, &h.ctx, Moving::Panel("color"), over), Some((Drop::Root(_, RootZone::Tab), _))));
|
||||
h.release(over);
|
||||
assert!(h.groups().is_empty());
|
||||
assert_eq!(h.dock_tabs().len(), 4);
|
||||
assert!(h.app.ui.open_panel.is_none());
|
||||
// Color Guide on the group's lower half: a group of its own below the tabbed group.
|
||||
h.drag(h.icons()[0].center(), pos2(500.0, 400.0));
|
||||
assert_eq!(h.groups(), [["colorGuide"]]);
|
||||
let dock = h.ctx.data(|d| d.get_temp::<Vec<RootGroup>>(root_groups_id())).unwrap()[0].rect;
|
||||
let lower = pos2(dock.center().x, dock.bottom() - 60.0);
|
||||
h.hold(h.title("colorGuide"), lower);
|
||||
assert!(matches!(drop_target(&h.app, &h.ctx, Moving::Panel("colorGuide"), lower), Some((Drop::Root(_, RootZone::Bottom), _))));
|
||||
h.release(lower);
|
||||
assert!(h.groups().is_empty());
|
||||
let groups = h.ctx.data(|d| d.get_temp::<Vec<RootGroup>>(root_groups_id())).unwrap();
|
||||
assert_eq!(
|
||||
groups.iter().map(|g| g.panels.clone()).collect::<Vec<_>>(),
|
||||
[vec!["properties", "layers", "libraries", "color"], vec!["colorGuide"]]
|
||||
);
|
||||
assert!(groups[0].rect.bottom() <= groups[1].rect.top() + 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_popped_out_panel_dragged_by_its_title_floats() {
|
||||
let mut h = Harness::new();
|
||||
|
||||
@@ -4484,6 +4484,9 @@ effect Soft Меко
|
||||
@msg before must name a panel параметърът „преди“ трябва да указва панел
|
||||
@msg docking action contains an unknown panel or an excessive split path действието по закачане съдържа непознат панел или прекомерен път на разделяне
|
||||
@msg group must be true or false групата трябва да е истина или лъжа
|
||||
@msg icon must be true or false icon трябва да е истина или лъжа
|
||||
@msg icon only docks a panel in the icon column, with no anchor icon закачва панел само в колоната с икони, без anchor
|
||||
@msg column docks a set as a dock column, with no anchor or icon column закачва набор като колона на дока, без anchor или icon
|
||||
@msg move requires a destination panel преместването изисква целеви панел
|
||||
@msg move requires an existing destination panel преместването изисква съществуващ целеви панел
|
||||
@msg onto must name a floating panel параметърът „върху“ трябва да указва плаващ панел
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4752,6 +4752,9 @@ imageTrace Outline Obrys
|
||||
@msg before must name a panel before musí určovat panel
|
||||
@msg docking action contains an unknown panel or an excessive split path akce ukotvení obsahuje neznámý panel nebo příliš dlouhou cestu rozdělení
|
||||
@msg group must be true or false group musí být true nebo false
|
||||
@msg icon must be true or false icon musí být true nebo false
|
||||
@msg icon only docks a panel in the icon column, with no anchor icon jen ukotví panel ve sloupci ikon, bez anchor
|
||||
@msg column docks a set as a dock column, with no anchor or icon column ukotví sadu jako sloupec doku, bez anchor nebo icon
|
||||
@msg move requires a destination panel přesun vyžaduje cílový panel
|
||||
@msg move requires an existing destination panel přesun vyžaduje existující cílový panel
|
||||
@msg onto must name a floating panel onto musí určovat plovoucí panel
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4450,6 +4450,9 @@ imageTrace Outline Kontur
|
||||
@msg before must name a panel before skal angive et panel
|
||||
@msg docking action contains an unknown panel or an excessive split path dokningshandlingen indeholder et ukendt panel eller en overdreven opdelingssti
|
||||
@msg group must be true or false group skal være true eller false
|
||||
@msg icon must be true or false icon skal være true eller false
|
||||
@msg icon only docks a panel in the icon column, with no anchor icon docker kun et panel i ikonkolonnen, uden anchor
|
||||
@msg column docks a set as a dock column, with no anchor or icon column docker et sæt som en dockkolonne, uden anchor eller icon
|
||||
@msg move requires a destination panel move kræver et destinationspanel
|
||||
@msg move requires an existing destination panel move kræver et eksisterende destinationspanel
|
||||
@msg onto must name a floating panel onto skal angive et flydende panel
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4564,6 +4564,9 @@ theme Appearance Erscheinungsbild
|
||||
@msg before must name a panel before muss ein Bedienfeld benennen
|
||||
@msg docking action contains an unknown panel or an excessive split path Die Docking-Aktion enthält ein unbekanntes Bedienfeld oder einen zu langen Teilungspfad
|
||||
@msg group must be true or false group muss true oder false sein
|
||||
@msg icon must be true or false icon muss true oder false sein
|
||||
@msg icon only docks a panel in the icon column, with no anchor icon dockt ein Bedienfeld nur in der Symbolspalte an, ohne anchor
|
||||
@msg column docks a set as a dock column, with no anchor or icon column dockt einen Satz als Dock-Spalte an, ohne anchor oder icon
|
||||
@msg move requires a destination panel Das Verschieben erfordert ein Zielbedienfeld
|
||||
@msg move requires an existing destination panel Das Verschieben erfordert ein vorhandenes Zielbedienfeld
|
||||
@msg onto must name a floating panel onto muss ein schwebendes Bedienfeld benennen
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4469,6 +4469,9 @@ theme Appearance Εμφάνιση
|
||||
@msg before must name a panel το before πρέπει να ορίζει πίνακα
|
||||
@msg docking action contains an unknown panel or an excessive split path η ενέργεια σύνδεσης περιέχει άγνωστο πίνακα ή υπερβολικά βαθιά διαδρομή διαίρεσης
|
||||
@msg group must be true or false το group πρέπει να είναι true ή false
|
||||
@msg icon must be true or false το icon πρέπει να είναι true ή false
|
||||
@msg icon only docks a panel in the icon column, with no anchor το icon αγκυρώνει πάνελ μόνο στη στήλη εικονιδίων, χωρίς anchor
|
||||
@msg column docks a set as a dock column, with no anchor or icon το column αγκυρώνει ένα σύνολο ως στήλη αγκύρωσης, χωρίς anchor ή icon
|
||||
@msg move requires a destination panel η μετακίνηση απαιτεί πίνακα προορισμού
|
||||
@msg move requires an existing destination panel η μετακίνηση απαιτεί υπάρχοντα πίνακα προορισμού
|
||||
@msg onto must name a floating panel το onto πρέπει να ορίζει αιωρούμενο πίνακα
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2039 and column 19.
|
@@ -4567,6 +4567,9 @@ axis Both Ambos
|
||||
@msg before must name a panel before debe indicar un panel
|
||||
@msg docking action contains an unknown panel or an excessive split path La acción de anclaje contiene un panel desconocido o una ruta de divisiones demasiado larga
|
||||
@msg group must be true or false group debe ser true o false
|
||||
@msg icon must be true or false icon debe ser true o false
|
||||
@msg icon only docks a panel in the icon column, with no anchor icon solo acopla un panel en la columna de iconos, sin anchor
|
||||
@msg column docks a set as a dock column, with no anchor or icon column acopla un conjunto como columna del acoplamiento, sin anchor ni icon
|
||||
@msg move requires a destination panel Para mover se requiere un panel de destino
|
||||
@msg move requires an existing destination panel Para mover se requiere un panel de destino existente
|
||||
@msg onto must name a floating panel onto debe indicar un panel flotante
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4661,6 +4661,9 @@ imageTrace Outline Ääriviiva
|
||||
@msg before must name a panel before-arvon on nimettävä paneeli
|
||||
@msg docking action contains an unknown panel or an excessive split path telakointitoiminto sisältää tuntemattoman paneelin tai liian pitkän jakopolun
|
||||
@msg group must be true or false group-arvon on oltava true tai false
|
||||
@msg icon must be true or false icon-arvon on oltava true tai false
|
||||
@msg icon only docks a panel in the icon column, with no anchor icon telakoi paneelin vain kuvakesarakkeeseen, ilman anchor-arvoa
|
||||
@msg column docks a set as a dock column, with no anchor or icon column telakoi joukon telakkasarakkeeksi, ilman anchor- tai icon-arvoa
|
||||
@msg move requires a destination panel siirto vaatii kohdepaneelin
|
||||
@msg move requires an existing destination panel siirto vaatii olemassa olevan kohdepaneelin
|
||||
@msg onto must name a floating panel onto-arvon on nimettävä kelluva paneeli
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4579,6 +4579,9 @@ effect Soft Doux
|
||||
@msg before must name a panel before doit désigner un panneau
|
||||
@msg docking action contains an unknown panel or an excessive split path L’action d’ancrage contient un panneau inconnu ou un chemin de divisions trop long
|
||||
@msg group must be true or false group doit être true ou false
|
||||
@msg icon must be true or false icon doit être true ou false
|
||||
@msg icon only docks a panel in the icon column, with no anchor icon ne fait qu’ancrer un panneau dans la colonne d’icônes, sans anchor
|
||||
@msg column docks a set as a dock column, with no anchor or icon column ancre un ensemble comme colonne du dock, sans anchor ni icon
|
||||
@msg move requires a destination panel Le déplacement nécessite un panneau de destination
|
||||
@msg move requires an existing destination panel Le déplacement nécessite un panneau de destination existant
|
||||
@msg onto must name a floating panel onto doit désigner un panneau flottant
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4595,6 +4595,9 @@ imageTrace Outline Körvonal
|
||||
@msg give the path `id` and the width point `index` add meg az útvonal `id` és a szélességi pont `index` értékét
|
||||
@msg gradient “{name}” isn't defined in the file: a plain colour stands in for it a színátmenet nincs megadva a fájlban: „{name}”; helyette egyszínű kitöltés áll
|
||||
@msg group must be true or false a group értéke true vagy false lehet
|
||||
@msg icon must be true or false az icon értéke true vagy false lehet
|
||||
@msg icon only docks a panel in the icon column, with no anchor az icon csak az ikonoszlopba dokkol egy panelt, anchor nélkül
|
||||
@msg column docks a set as a dock column, with no anchor or icon a column egy készletet dokkoszlopként dokkol, anchor és icon nélkül
|
||||
@msg guides are locked a segédvonalak zárolva vannak
|
||||
@msg handle must be in or out handle: in vagy out kell
|
||||
@msg image '{key}' has no pixels and was left out a kép kimaradt, mert nincsenek pixelei: '{key}'
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4450,6 +4450,9 @@ imageTrace Outline Kontur
|
||||
@msg before must name a panel before harus menyebutkan panel
|
||||
@msg docking action contains an unknown panel or an excessive split path tindakan docking berisi panel tidak dikenal atau jalur pemisahan berlebihan
|
||||
@msg group must be true or false group harus true atau false
|
||||
@msg icon must be true or false icon harus true atau false
|
||||
@msg icon only docks a panel in the icon column, with no anchor icon hanya menambatkan panel di kolom ikon, tanpa anchor
|
||||
@msg column docks a set as a dock column, with no anchor or icon column menambatkan satu set sebagai kolom dok, tanpa anchor atau icon
|
||||
@msg move requires a destination panel move memerlukan panel tujuan
|
||||
@msg move requires an existing destination panel move memerlukan panel tujuan yang sudah ada
|
||||
@msg onto must name a floating panel onto harus menyebutkan panel mengambang
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 3809 and column 19.
|
@@ -4609,6 +4609,9 @@ effect Regular Normale
|
||||
@msg before must name a panel before deve indicare un pannello
|
||||
@msg docking action contains an unknown panel or an excessive split path L’azione di ancoraggio contiene un pannello sconosciuto o un percorso di divisioni troppo lungo
|
||||
@msg group must be true or false group deve essere true o false
|
||||
@msg icon must be true or false icon deve essere true o false
|
||||
@msg icon only docks a panel in the icon column, with no anchor icon ancora un pannello solo nella colonna delle icone, senza anchor
|
||||
@msg column docks a set as a dock column, with no anchor or icon column ancora un insieme come colonna del dock, senza anchor né icon
|
||||
@msg move requires a destination panel Lo spostamento richiede un pannello di destinazione
|
||||
@msg move requires an existing destination panel Lo spostamento richiede un pannello di destinazione esistente
|
||||
@msg onto must name a floating panel onto deve indicare un pannello mobile
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4649,6 +4649,9 @@ effect Vertical 垂直
|
||||
@msg before must name a panel before にパネルを指定してください
|
||||
@msg docking action contains an unknown panel or an excessive split path ドッキング操作に不明なパネルまたは長すぎる分割パスが含まれています
|
||||
@msg group must be true or false group は true または false にしてください
|
||||
@msg icon must be true or false icon は true または false である必要があります
|
||||
@msg icon only docks a panel in the icon column, with no anchor icon はパネルをアイコン列にのみドッキングします(anchor なし)
|
||||
@msg column docks a set as a dock column, with no anchor or icon column はセットをドック列としてドッキングします(anchor・icon なし)
|
||||
@msg move requires a destination panel 移動先のパネルを指定してください
|
||||
@msg move requires an existing destination panel 移動先には既存のパネルを指定してください
|
||||
@msg onto must name a floating panel onto にフローティングパネルを指定してください
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4201,6 +4201,9 @@ effect Regular ჩვეულებრივი
|
||||
@msg before must name a panel before უნდა ასახელებდეს პანელს
|
||||
@msg docking action contains an unknown panel or an excessive split path მიმაგრების მოქმედება შეიცავს უცნობ პანელს ან ზედმეტად გრძელ გაყოფის გზას
|
||||
@msg group must be true or false group უნდა იყოს true ან false
|
||||
@msg icon must be true or false icon უნდა იყოს true ან false
|
||||
@msg icon only docks a panel in the icon column, with no anchor icon პანელს მხოლოდ ხატულების სვეტში ამაგრებს, anchor-ის გარეშე
|
||||
@msg column docks a set as a dock column, with no anchor or icon column ნაკრებს დოკის სვეტად ამაგრებს, anchor-ის ან icon-ის გარეშე
|
||||
@msg move requires a destination panel move-ს სჭირდება სამიზნე პანელი
|
||||
@msg move requires an existing destination panel move-ს სჭირდება არსებული სამიზნე პანელი
|
||||
@msg onto must name a floating panel onto უნდა ასახელებდეს მცურავ პანელს
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4579,6 +4579,9 @@ effect Vertical 세로
|
||||
@msg before must name a panel before는 패널을 지정해야 합니다
|
||||
@msg docking action contains an unknown panel or an excessive split path 도킹 동작에 알 수 없는 패널이나 지나치게 많은 분할 경로가 있습니다
|
||||
@msg group must be true or false group은 true 또는 false여야 합니다
|
||||
@msg icon must be true or false icon은 true 또는 false여야 합니다
|
||||
@msg icon only docks a panel in the icon column, with no anchor icon은 anchor 없이 패널을 아이콘 열에만 도킹합니다
|
||||
@msg column docks a set as a dock column, with no anchor or icon column은 anchor나 icon 없이 세트를 도크 열로 도킹합니다
|
||||
@msg move requires a destination panel move에는 대상 패널이 필요합니다
|
||||
@msg move requires an existing destination panel move에는 존재하는 대상 패널이 필요합니다
|
||||
@msg onto must name a floating panel onto는 떠 있는 패널을 지정해야 합니다
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4462,6 +4462,9 @@ imageTrace Outline Contour
|
||||
@msg before must name a panel before moet een paneel benoemen
|
||||
@msg docking action contains an unknown panel or an excessive split path dokactie bevat een onbekend paneel of een overmatig splitspad
|
||||
@msg group must be true or false group moet true of false zijn
|
||||
@msg icon must be true or false icon moet true of false zijn
|
||||
@msg icon only docks a panel in the icon column, with no anchor icon dockt een paneel alleen in de pictogramkolom, zonder anchor
|
||||
@msg column docks a set as a dock column, with no anchor or icon column dockt een set als dockkolom, zonder anchor of icon
|
||||
@msg move requires a destination panel move vereist een doelpaneel
|
||||
@msg move requires an existing destination panel move vereist een bestaand doelpaneel
|
||||
@msg onto must name a floating panel onto moet een zwevend paneel benoemen
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4577,6 +4577,9 @@ imageTrace Outline Kontur
|
||||
@msg before must name a panel before musi wskazywać panel
|
||||
@msg docking action contains an unknown panel or an excessive split path operacja dokowania zawiera nieznany panel lub zbyt długą ścieżkę podziału
|
||||
@msg group must be true or false group musi mieć wartość true lub false
|
||||
@msg icon must be true or false icon musi mieć wartość true lub false
|
||||
@msg icon only docks a panel in the icon column, with no anchor icon tylko dokuje panel w kolumnie ikon, bez anchor
|
||||
@msg column docks a set as a dock column, with no anchor or icon column dokuje zestaw jako kolumnę doku, bez anchor ani icon
|
||||
@msg move requires a destination panel przeniesienie wymaga panelu docelowego
|
||||
@msg move requires an existing destination panel przeniesienie wymaga istniejącego panelu docelowego
|
||||
@msg onto must name a floating panel onto musi wskazywać panel pływający
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4021,6 +4021,9 @@ axis Both Ambos
|
||||
@msg before must name a panel before deve nomear um painel
|
||||
@msg docking action contains an unknown panel or an excessive split path a ação de encaixe contém um painel desconhecido ou um caminho de divisão excessivo
|
||||
@msg group must be true or false group deve ser true ou false
|
||||
@msg icon must be true or false icon deve ser true ou false
|
||||
@msg icon only docks a panel in the icon column, with no anchor icon apenas encaixa um painel na coluna de ícones, sem anchor
|
||||
@msg column docks a set as a dock column, with no anchor or icon column encaixa um conjunto como coluna do encaixe, sem anchor nem icon
|
||||
@msg move requires a destination panel mover exige um painel de destino
|
||||
@msg move requires an existing destination panel mover exige um painel de destino existente
|
||||
@msg onto must name a floating panel onto deve nomear um painel flutuante
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -4605,6 +4605,9 @@ axis Both Оба
|
||||
@msg before must name a panel before должен указывать панель
|
||||
@msg docking action contains an unknown panel or an excessive split path Действие прикрепления содержит неизвестную панель или слишком длинный путь разделений
|
||||
@msg group must be true or false group должен иметь значение true или false
|
||||
@msg icon must be true or false icon должен быть true или false
|
||||
@msg icon only docks a panel in the icon column, with no anchor icon только закрепляет панель в столбце значков, без anchor
|
||||
@msg column docks a set as a dock column, with no anchor or icon column закрепляет набор как столбец дока, без anchor и icon
|
||||
@msg move requires a destination panel Для перемещения требуется целевая панель
|
||||
@msg move requires an existing destination panel Для перемещения требуется существующая целевая панель
|
||||
@msg onto must name a floating panel onto должен указывать плавающую панель
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4490,6 +4490,9 @@ effect Soft Mjuk
|
||||
@msg before must name a panel before måste ange en panel
|
||||
@msg docking action contains an unknown panel or an excessive split path dockningsåtgärden innehåller en okänd panel eller en alltför djup delningssökväg
|
||||
@msg group must be true or false group måste vara true eller false
|
||||
@msg icon must be true or false icon måste vara true eller false
|
||||
@msg icon only docks a panel in the icon column, with no anchor icon dockar bara en panel i ikonkolumnen, utan anchor
|
||||
@msg column docks a set as a dock column, with no anchor or icon column dockar en uppsättning som en dockningskolumn, utan anchor eller icon
|
||||
@msg move requires a destination panel flyttning kräver en målpanel
|
||||
@msg move requires an existing destination panel flyttning kräver en befintlig målpanel
|
||||
@msg onto must name a floating panel onto måste ange en flytande panel
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -4591,6 +4591,9 @@ axis Both Обидві
|
||||
@msg before must name a panel before має вказувати панель
|
||||
@msg docking action contains an unknown panel or an excessive split path Дія прикріплення містить невідому панель або надто довгий шлях поділів
|
||||
@msg group must be true or false group має мати значення true або false
|
||||
@msg icon must be true or false icon має бути true або false
|
||||
@msg icon only docks a panel in the icon column, with no anchor icon лише закріплює панель у стовпці значків, без anchor
|
||||
@msg column docks a set as a dock column, with no anchor or icon column закріплює набір як стовпець дока, без anchor та icon
|
||||
@msg move requires a destination panel Для переміщення потрібна цільова панель
|
||||
@msg move requires an existing destination panel Для переміщення потрібна наявна цільова панель
|
||||
@msg onto must name a floating panel onto має вказувати плаваючу панель
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 733 and column 40.
|
@@ -4241,6 +4241,9 @@ theme Appearance Giao diện
|
||||
@msg give the path `id` and the width point `index` hãy cung cấp `id` đường dẫn và `index` điểm chiều rộng
|
||||
@msg gradient “{name}” isn't defined in the file: a plain colour stands in for it chuyển sắc “{name}” không được định nghĩa trong tệp: một màu đơn thay thế
|
||||
@msg group must be true or false group phải là true hoặc false
|
||||
@msg icon must be true or false icon phải là true hoặc false
|
||||
@msg icon only docks a panel in the icon column, with no anchor icon chỉ neo bảng vào cột biểu tượng, không có anchor
|
||||
@msg column docks a set as a dock column, with no anchor or icon column neo một bộ thành cột neo, không có anchor hay icon
|
||||
@msg guides are locked đường căn bị khóa
|
||||
@msg handle must be in or out handle phải là in hoặc out
|
||||
@msg image '{key}' has no pixels and was left out ảnh '{key}' không có pixel và bị bỏ qua
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 4021 and column 10.
|
@@ -4274,6 +4274,9 @@ imageTrace Outline 轮廓
|
||||
@msg before must name a panel before 必须指定面板
|
||||
@msg docking action contains an unknown panel or an excessive split path 停靠操作包含未知面板或过长的拆分路径
|
||||
@msg group must be true or false group 必须是 true 或 false
|
||||
@msg icon must be true or false icon 必须为 true 或 false
|
||||
@msg icon only docks a panel in the icon column, with no anchor icon 只会把面板停放到图标栏中,不能带 anchor
|
||||
@msg column docks a set as a dock column, with no anchor or icon column 将一组面板停放为停放栏,不能带 anchor 或 icon
|
||||
@msg move requires a destination panel 移动需要目标面板
|
||||
@msg move requires an existing destination panel 移动需要现有的目标面板
|
||||
@msg onto must name a floating panel onto 必须指定浮动面板
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2258,6 +2258,9 @@ file-filter Templates 範本
|
||||
@msg before must name a panel before 必須指定面板
|
||||
@msg docking action contains an unknown panel or an excessive split path 停駐動作包含未知的面板或過長的分割路徑
|
||||
@msg group must be true or false group 必須是 true 或 false
|
||||
@msg icon must be true or false icon 必須為 true 或 false
|
||||
@msg icon only docks a panel in the icon column, with no anchor icon 只會將面板停駐到圖示欄中,不能帶 anchor
|
||||
@msg column docks a set as a dock column, with no anchor or icon column 會將一組面板停駐為停駐欄,不能帶 anchor 或 icon
|
||||
@msg move requires a destination panel 移動需要目的地面板
|
||||
@msg move requires an existing destination panel 移動需要現有的目的地面板
|
||||
@msg onto must name a floating panel onto 必須指定浮動面板
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -469,7 +469,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
||||
"window.panel.dock",
|
||||
"Dock Panel",
|
||||
"",
|
||||
"{panel: id or \"tools\", group?: bool, column?: true or n} put a floating panel (with `group`, its whole group) back in the dock, as dropping it on the dock or its group's × does: Properties, Layers and Libraries in the tabbed group, the other panels in the icon column; with `column`, dock its group and the rest of its set as a dock column instead, as dropping it on the left edge of the icon column or of a dock column does: `true` at the dock's left, n at that place (1: next to the icon column, counting left); \"tools\" docks the Tools panel at the window's left edge. Returns {panel, floating, column}",
|
||||
"{panel: id or \"tools\", group?: bool, column?: true or n, anchor?: id, zone?: \"tab\" | \"top\" | \"bottom\" | \"left\" | \"right\" | \"center\", before?: id, icon?: bool} put a floating panel (with `group`, its whole group) back in the dock, as dropping it on the dock or its group's × does: Properties, Layers and Libraries in the tabbed group, the other panels in the icon column; with `anchor`, in the dock group holding that panel instead: `zone` \"tab\" (the default) as a tab of it (before the `before` panel's tab), as dropping it on that group's tabs does, \"top\" / \"bottom\" as a new group above / below it, as dropping it on that group's upper / lower half does (\"left\", \"right\" and \"center\" split or join it likewise); with `icon: true` (no `anchor`), as icons in the icon column, the tabbed group's panels too, as dropping it on the icon column does: it lives there from then on and pops out of its icon; with `column`, dock its group and the rest of its set as a dock column instead, as dropping it on the left edge of the icon column or of a dock column does: `true` at the dock's left, n at that place (1: next to the icon column, counting left); \"tools\" docks the Tools panel at the window's left edge. Returns {panel, floating, column}, or with `anchor` or `icon: true` (the shared layout) {panel, floating, layout} and no `column`",
|
||||
),
|
||||
(
|
||||
"window.collapseDock",
|
||||
|
||||
@@ -7,6 +7,7 @@ use crate::VectorcraftApp;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct LegacyDrag {
|
||||
panel: String,
|
||||
layout: Option<Layout<String>>,
|
||||
open_panel: Option<String>,
|
||||
floating: Vec<crate::state::FloatingPanels>,
|
||||
@@ -18,6 +19,7 @@ struct RootGroupDrag {
|
||||
panel: String,
|
||||
layout: Layout<String>,
|
||||
hidden: std::collections::BTreeMap<String, craft_ui::docking::Location<String>>,
|
||||
open_panel: Option<String>,
|
||||
generation: u64,
|
||||
}
|
||||
|
||||
@@ -231,7 +233,15 @@ fn placement(params: &Value) -> Result<Placement<String>, String> {
|
||||
}
|
||||
|
||||
pub(crate) fn command(app: &mut VectorcraftApp, id: &str, params: &Value) -> Option<Result<Value, String>> {
|
||||
// Docking into the dock's tree or its icon column is the shared layout's, whatever set the
|
||||
// panel floats in. A dock column is the native sets', so it doesn't combine with either.
|
||||
let to_icons = params.get("icon").is_some_and(|icon| icon != &Value::Bool(false));
|
||||
let into_tree = id == "window.panel.dock" && (params.get("anchor").is_some() || to_icons);
|
||||
if into_tree && params.get("column").is_some() {
|
||||
return Some(Err("column docks a set as a dock column, with no anchor or icon".into()));
|
||||
}
|
||||
if matches!(id, "window.panel.float" | "window.panel.dock")
|
||||
&& !into_tree
|
||||
&& (params.get("above").is_some()
|
||||
|| params.get("below").is_some()
|
||||
|| params.get("column").is_some()
|
||||
@@ -386,11 +396,24 @@ fn change(app: &mut VectorcraftApp, operation: &str, params: &Value) -> Result<V
|
||||
Some(Value::Bool(value)) => *value,
|
||||
Some(_) => return Err("group must be true or false".into()),
|
||||
};
|
||||
let to_icons = match params.get("icon") {
|
||||
None => false,
|
||||
Some(Value::Bool(value)) => *value,
|
||||
Some(_) => return Err("icon must be true or false".into()),
|
||||
};
|
||||
if to_icons && (operation != "dock" || params.get("anchor").or_else(|| params.get("onto")).is_some()) {
|
||||
return Err("icon only docks a panel in the icon column, with no anchor".into());
|
||||
}
|
||||
let members = if whole_group { group_members(&layout, panel) } else { vec![panel.to_string()] };
|
||||
let mut hidden: std::collections::BTreeMap<_, _> =
|
||||
app.ui.docking_hidden.iter().filter(|(id, _)| normalize(id).is_some()).take(64).map(|(id, at)| (id.clone(), at.clone())).collect();
|
||||
let mut icons = icon_origins(app);
|
||||
let icon_panel = crate::state::ICON_PANELS.iter().any(|entry| entry.0 == panel);
|
||||
if to_icons {
|
||||
// Any panel the icon column can show lives there once dropped on it, the tabbed group's
|
||||
// too; the others (library panels) dock as they would without `icon`.
|
||||
icons.extend(members.iter().filter(|id| crate::state::rail_panel(id).is_some()).cloned());
|
||||
}
|
||||
let icon_panel = crate::state::ICON_PANELS.iter().any(|entry| entry.0 == panel) || icons.contains(panel);
|
||||
if icon_panel && !layout.contains(&panel.to_string()) {
|
||||
icons.insert(panel.to_string());
|
||||
}
|
||||
@@ -631,6 +654,7 @@ impl craft_ui::docking::DockContent<String> for PanelContent<'_> {
|
||||
panel: active.clone(),
|
||||
layout: self.app.ui.docking.clone().unwrap_or_else(|| legacy(self.app)),
|
||||
hidden: self.app.ui.docking_hidden.clone(),
|
||||
open_panel: self.app.ui.open_panel.clone(),
|
||||
generation: self.app.ui.docking_generation,
|
||||
},
|
||||
)
|
||||
@@ -642,6 +666,74 @@ impl craft_ui::docking::DockContent<String> for PanelContent<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The command that puts `panel` (with its group when `whole_group`) where a pointer drop on
|
||||
/// `target` puts it.
|
||||
fn drop_command(panel: &str, target: crate::floating::Drop, whole_group: bool) -> (&'static str, Value) {
|
||||
use crate::floating::Drop;
|
||||
match target {
|
||||
Drop::Dock => ("window.panel.dock", json!({"panel": panel, "group": whole_group})),
|
||||
Drop::Stack(onto) => ("window.panel.float", json!({"panel": panel, "group": whole_group, "onto": onto})),
|
||||
Drop::Below(onto) => ("window.panel.float", json!({"panel": panel, "group": whole_group, "below": onto})),
|
||||
Drop::Above(onto) => ("window.panel.float", json!({"panel": panel, "group": whole_group, "above": onto})),
|
||||
Drop::Column(slot) => ("window.panel.dock", json!({"panel": panel, "group": whole_group, "column": slot})),
|
||||
Drop::Root(anchor, zone) => ("window.panel.dock", json!({"panel": panel, "group": whole_group, "anchor": anchor, "zone": zone.id()})),
|
||||
Drop::Icons => ("window.panel.dock", json!({"panel": panel, "group": whole_group, "icon": true})),
|
||||
}
|
||||
}
|
||||
|
||||
/// The groups of the shared dock's tree as drawn in `content` (the rect its tree was laid out in),
|
||||
/// from where its tabs were drawn: a group's tabs start at its top-left corner, and the groups of
|
||||
/// a split meet where the second one's tabs start.
|
||||
fn root_groups(ctx: &egui::Context, layout: &Layout<String>, content: egui::Rect, tab_height: f32) -> Vec<crate::floating::RootGroup> {
|
||||
let area = egui::Id::new("vectorcraft-panel-docking");
|
||||
let tab = |panel: &String| ctx.read_response(area.with(("tab", panel))).map(|response| response.rect).filter(|r| r.is_positive());
|
||||
/// The top-left corner of a node's first group, from where its tabs were drawn.
|
||||
fn corner(node: &Node<String>, tab: &dyn Fn(&String) -> Option<egui::Rect>) -> Option<egui::Pos2> {
|
||||
match node {
|
||||
Node::Tabs { panels, .. } => panels.iter().filter_map(tab).map(|r| r.min).reduce(|a, b| a.min(b)),
|
||||
Node::Stack { entries } => entries.first().and_then(|entry| tab(&entry.panel)).map(|r| r.min),
|
||||
Node::Split { first, .. } => corner(first, tab),
|
||||
}
|
||||
}
|
||||
let mut out = Vec::new();
|
||||
let mut pending: Vec<(&Node<String>, egui::Rect)> = layout.root.iter().map(|root| (root, content)).collect();
|
||||
while let Some((node, rect)) = pending.pop() {
|
||||
if out.len() >= craft_ui::docking::MAX_PANELS || !rect.is_positive() {
|
||||
break;
|
||||
}
|
||||
match node {
|
||||
Node::Tabs { panels, active } => {
|
||||
let ids: Vec<&'static str> = panels.iter().filter_map(|id| normalize(id)).collect();
|
||||
let Some(shown) = panels.get(*active).and_then(|id| normalize(id)).or_else(|| ids.first().copied()) else { continue };
|
||||
let strip = egui::Rect::from_min_max(rect.min, egui::pos2(rect.right(), (rect.top() + tab_height).min(rect.bottom())));
|
||||
out.push(crate::floating::RootGroup { panels: ids, shown, rect, strip });
|
||||
}
|
||||
Node::Stack { entries } => {
|
||||
let ids: Vec<&'static str> = entries.iter().filter_map(|entry| normalize(&entry.panel)).collect();
|
||||
let Some(shown) = ids.first().copied() else { continue };
|
||||
let strip = egui::Rect::from_min_max(rect.min, egui::pos2(rect.right(), (rect.top() + tab_height).min(rect.bottom())));
|
||||
out.push(crate::floating::RootGroup { panels: ids, shown, rect, strip });
|
||||
}
|
||||
Node::Split { axis, first, second, .. } => {
|
||||
let Some(at) = corner(second, &tab) else { continue };
|
||||
let (a, b) = match axis {
|
||||
craft_ui::layout::SplitAxis::Vertical => {
|
||||
let y = at.y.clamp(rect.top(), rect.bottom());
|
||||
(egui::Rect::from_x_y_ranges(rect.x_range(), rect.top()..=y), egui::Rect::from_x_y_ranges(rect.x_range(), y..=rect.bottom()))
|
||||
}
|
||||
craft_ui::layout::SplitAxis::Horizontal => {
|
||||
let x = at.x.clamp(rect.left(), rect.right());
|
||||
(egui::Rect::from_x_y_ranges(rect.left()..=x, rect.y_range()), egui::Rect::from_x_y_ranges(x..=rect.right(), rect.y_range()))
|
||||
}
|
||||
};
|
||||
pending.push((second, b));
|
||||
pending.push((first, a));
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub(crate) fn show(app: &mut VectorcraftApp, ui: &mut egui::Ui) -> bool {
|
||||
let root_key = egui::Id::new("vectorcraft-root-group-drag");
|
||||
if let Some(drag) = ui.ctx().data(|data| data.get_temp::<RootGroupDrag>(root_key)) {
|
||||
@@ -650,6 +742,7 @@ pub(crate) fn show(app: &mut VectorcraftApp, ui: &mut egui::Ui) -> bool {
|
||||
} else if ui.input(|input| !input.focused || input.key_pressed(egui::Key::Escape)) {
|
||||
app.ui.docking = Some(drag.layout);
|
||||
app.ui.docking_hidden = drag.hidden;
|
||||
app.ui.open_panel = drag.open_panel;
|
||||
sync_legacy(app);
|
||||
ui.ctx().data_mut(|data| data.remove::<RootGroupDrag>(root_key));
|
||||
} else if let Some(pos) = ui.input(|input| input.pointer.interact_pos()) {
|
||||
@@ -666,13 +759,7 @@ pub(crate) fn show(app: &mut VectorcraftApp, ui: &mut egui::Ui) -> bool {
|
||||
if let Some(panel) = normalize(&drag.panel)
|
||||
&& let Some((target, _)) = crate::floating::drop_target(app, ui.ctx(), crate::floating::Moving::Panel(panel), pos)
|
||||
{
|
||||
let (id, params) = match target {
|
||||
crate::floating::Drop::Dock => ("window.panel.dock", json!({"panel":panel,"group":true})),
|
||||
crate::floating::Drop::Stack(onto) => ("window.panel.float", json!({"panel":panel,"group":true,"onto":onto})),
|
||||
crate::floating::Drop::Below(onto) => ("window.panel.float", json!({"panel":panel,"group":true,"below":onto})),
|
||||
crate::floating::Drop::Above(onto) => ("window.panel.float", json!({"panel":panel,"group":true,"above":onto})),
|
||||
crate::floating::Drop::Column(slot) => ("window.panel.dock", json!({"panel":panel,"group":true,"column":slot})),
|
||||
};
|
||||
let (id, params) = drop_command(panel, target, true);
|
||||
if let Err(error) = app.run(id, params) {
|
||||
app.ui.status = error;
|
||||
}
|
||||
@@ -775,6 +862,7 @@ pub(crate) fn show(app: &mut VectorcraftApp, ui: &mut egui::Ui) -> bool {
|
||||
style.panels_label = tl!("Panels").to_string();
|
||||
style.resize_label = tl!("Resize panels").to_string();
|
||||
style.resize_window_label = tl!("Resize panel window").to_string();
|
||||
let tree = ui.available_rect_before_wrap();
|
||||
let mut content = PanelContent { app, commands: Vec::new() };
|
||||
let output = craft_ui::docking::DockArea::new(area).show_customized(
|
||||
ui,
|
||||
@@ -791,6 +879,8 @@ pub(crate) fn show(app: &mut VectorcraftApp, ui: &mut egui::Ui) -> bool {
|
||||
|_| craft_ui::docking::PanelLimits { min: egui::vec2(220.0, 80.0), ..Default::default() },
|
||||
&mut content,
|
||||
);
|
||||
let groups = if collapsed { Vec::new() } else { root_groups(ui.ctx(), &display, tree, style.tab_height) };
|
||||
ui.ctx().data_mut(|data| data.insert_temp(crate::floating::root_groups_id(), groups));
|
||||
for (id, params) in content.commands {
|
||||
if let Err(error) = app.run(id, params) {
|
||||
app.ui.status = error;
|
||||
@@ -818,6 +908,16 @@ pub(crate) fn show(app: &mut VectorcraftApp, ui: &mut egui::Ui) -> bool {
|
||||
{
|
||||
crate::floating::preview(ui.ctx(), target, rect);
|
||||
}
|
||||
// A tab dragged over the icon column becomes an icon there: light the column up (the shared
|
||||
// dock shows the other drops).
|
||||
if ui.input(|input| input.pointer.primary_down())
|
||||
&& let Some(pos) = ui.input(|input| input.pointer.interact_pos())
|
||||
&& let Some(panel) = dragged_tab(ui.ctx(), &layout).and_then(|panel| normalize(&panel))
|
||||
&& let Some((target @ crate::floating::Drop::Icons, rect)) =
|
||||
crate::floating::drop_target(app, ui.ctx(), crate::floating::Moving::Panel(panel), pos)
|
||||
{
|
||||
crate::floating::preview(ui.ctx(), target, rect);
|
||||
}
|
||||
if let Some(error) = output.error {
|
||||
app.ui.status = error.to_string();
|
||||
}
|
||||
@@ -831,7 +931,7 @@ pub(crate) fn show(app: &mut VectorcraftApp, ui: &mut egui::Ui) -> bool {
|
||||
_ => return None,
|
||||
};
|
||||
crate::floating::drop_target(app, ui.ctx(), crate::floating::Moving::Panel(panel), pos)
|
||||
.filter(|(target, _)| !matches!(target, crate::floating::Drop::Dock))
|
||||
.filter(|(target, _)| !matches!(target, crate::floating::Drop::Dock | crate::floating::Drop::Root(..)))
|
||||
.map(|(target, _)| (panel, target))
|
||||
})
|
||||
});
|
||||
@@ -864,13 +964,7 @@ pub(crate) fn show(app: &mut VectorcraftApp, ui: &mut egui::Ui) -> bool {
|
||||
crate::floating::drop_target(app, ui.ctx(), crate::floating::Moving::Panel(panel), pos).map(|(target, _)| (panel, target, true))
|
||||
});
|
||||
if let Some((panel, target, whole_group)) = group_drop.or_else(|| tab_drop.map(|(panel, target)| (panel, target, false))) {
|
||||
let (command, params) = match target {
|
||||
crate::floating::Drop::Dock => ("window.panel.dock", json!({"panel": panel, "group": whole_group})),
|
||||
crate::floating::Drop::Stack(onto) => ("window.panel.float", json!({"panel": panel, "group": whole_group, "onto": onto})),
|
||||
crate::floating::Drop::Below(onto) => ("window.panel.float", json!({"panel":panel,"group":whole_group,"below":onto})),
|
||||
crate::floating::Drop::Above(onto) => ("window.panel.float", json!({"panel":panel,"group":whole_group,"above":onto})),
|
||||
crate::floating::Drop::Column(slot) => ("window.panel.dock", json!({"panel":panel,"group":whole_group,"column":slot})),
|
||||
};
|
||||
let (command, params) = drop_command(panel, target, whole_group);
|
||||
// Shared and native windows have already chosen their render ownership for this
|
||||
// frame. Switch representations only after both renderers finish, so a panel's
|
||||
// widgets never appear in two layers in the same frame.
|
||||
@@ -894,6 +988,17 @@ pub(crate) fn show(app: &mut VectorcraftApp, ui: &mut egui::Ui) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// The panel whose tab (of the shared dock, or of the legacy dock or icon column on the drag that
|
||||
/// began the shared layout) is being dragged.
|
||||
fn dragged_tab(ctx: &egui::Context, layout: &Layout<String>) -> Option<String> {
|
||||
let area = egui::Id::new("vectorcraft-panel-docking");
|
||||
if let Some(drag) = ctx.data(|data| data.get_temp::<LegacyDrag>(area.with("legacy-origin"))) {
|
||||
return Some(drag.panel);
|
||||
}
|
||||
let dragged = ctx.dragged_id()?;
|
||||
layout.panels().into_iter().find(|panel| area.with(("tab", *panel)) == dragged).cloned()
|
||||
}
|
||||
|
||||
fn icon_rail(app: &mut VectorcraftApp, ui: &mut egui::Ui) {
|
||||
let t = crate::theme::Tokens::get(ui.ctx());
|
||||
let column = egui::Panel::right("shared-panel-icons")
|
||||
@@ -904,21 +1009,41 @@ fn icon_rail(app: &mut VectorcraftApp, ui: &mut egui::Ui) {
|
||||
crate::theme::card(ui, &t, egui::Margin { left: 3, right: 3, top: 6, bottom: 6 }, |ui| {
|
||||
egui::ScrollArea::vertical().id_salt("shared-panel-icons-scroll").show(ui, |ui| {
|
||||
ui.spacing_mut().item_spacing.y = 2.0;
|
||||
for &(id, label, icon) in crate::state::ICON_PANEL_GROUPS
|
||||
// The tabbed group's panels dropped on the column head it, then the icon panels,
|
||||
// then the other panels dropped on it (those the column has no place for).
|
||||
let tabs: Vec<_> = crate::state::DockTab::ALL
|
||||
.into_iter()
|
||||
.map(crate::state::DockTab::info)
|
||||
.filter(|info| app.ui.docking_icons.contains(info.0))
|
||||
.collect();
|
||||
let grouped = |id: &str| crate::state::ICON_PANEL_GROUPS.iter().any(|group| group.contains(&id));
|
||||
let dropped: Vec<_> = crate::state::ICON_PANELS
|
||||
.iter()
|
||||
.flat_map(|group| group.iter())
|
||||
.filter_map(|id| crate::state::ICON_PANELS.iter().find(|entry| entry.0 == *id))
|
||||
.filter(|entry| !grouped(entry.0) && app.ui.docking_icons.contains(entry.0))
|
||||
.copied()
|
||||
.collect();
|
||||
for (id, label, icon) in tabs
|
||||
.into_iter()
|
||||
.chain(
|
||||
crate::state::ICON_PANEL_GROUPS
|
||||
.iter()
|
||||
.flat_map(|group| group.iter())
|
||||
.filter_map(|id| crate::state::ICON_PANELS.iter().find(|entry| entry.0 == *id).copied()),
|
||||
)
|
||||
.chain(dropped)
|
||||
{
|
||||
if app.ui.docking.as_ref().is_some_and(|layout| layout.contains(&id.to_string())) {
|
||||
continue;
|
||||
}
|
||||
let (_, rect) = ui.allocate_space(egui::vec2(30.0, 30.0));
|
||||
let response = ui.interact(rect, ui.id().with(("panel-icon", id)), egui::Sense::click_and_drag());
|
||||
let response = crate::widgets::paint_icon_button(ui, response, icon, tl!(label), app.ui.open_panel.as_deref() == Some(id));
|
||||
let open = app.ui.open_panel.as_deref() == Some(id);
|
||||
let response = crate::widgets::paint_icon_button(ui, response, icon, tl!(label), open);
|
||||
response.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::SelectableLabel, ui.is_enabled(), open, tl!(label)));
|
||||
if response.clicked() {
|
||||
app.ui.open_panel = if app.ui.open_panel.as_deref() == Some(id) { None } else { Some(id.into()) };
|
||||
}
|
||||
legacy_tab(app, ui, id, &response);
|
||||
legacy_tab(app, ui, id, &response, false);
|
||||
}
|
||||
});
|
||||
})
|
||||
@@ -937,11 +1062,21 @@ fn icon_rail(app: &mut VectorcraftApp, ui: &mut egui::Ui) {
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn legacy_tab(app: &mut VectorcraftApp, ui: &mut egui::Ui, panel: &str, response: &egui::Response) {
|
||||
legacy_tab_in_strip(app, ui, panel, response, None);
|
||||
/// A tab of the dock's tabbed group (`closable`) or an icon of the icon column: its context menu
|
||||
/// floats, groups or (a tab) closes its panel, and a drag moves it in the shared layout.
|
||||
pub(crate) fn legacy_tab(app: &mut VectorcraftApp, ui: &mut egui::Ui, panel: &str, response: &egui::Response, closable: bool) {
|
||||
legacy_tab_in_strip(app, ui, panel, response, closable, None);
|
||||
}
|
||||
|
||||
pub(crate) fn legacy_tab_in_strip(app: &mut VectorcraftApp, ui: &mut egui::Ui, panel: &str, response: &egui::Response, strip: Option<egui::Rect>) {
|
||||
/// [`legacy_tab`] for a tab of the tab strip `strip`, where a drag reorders the tabs.
|
||||
pub(crate) fn legacy_tab_in_strip(
|
||||
app: &mut VectorcraftApp,
|
||||
ui: &mut egui::Ui,
|
||||
panel: &str,
|
||||
response: &egui::Response,
|
||||
closable: bool,
|
||||
strip: Option<egui::Rect>,
|
||||
) {
|
||||
let Some(panel) = normalize(panel) else { return };
|
||||
response.context_menu(|ui| {
|
||||
if ui.button(tl!("Float Panel")).clicked() {
|
||||
@@ -964,6 +1099,12 @@ pub(crate) fn legacy_tab_in_strip(app: &mut VectorcraftApp, ui: &mut egui::Ui, p
|
||||
}
|
||||
}
|
||||
});
|
||||
if closable && ui.button(tl!("Close Panel")).clicked() {
|
||||
if let Err(error) = app.run("window.panel.close", json!({"panel": panel})) {
|
||||
app.ui.status = error;
|
||||
}
|
||||
ui.close();
|
||||
}
|
||||
});
|
||||
let inside_strip = strip.is_some_and(|strip| ui.input(|i| i.pointer.interact_pos()).is_some_and(|p| strip.expand(4.0).contains(p)));
|
||||
let area = egui::Id::new("vectorcraft-panel-docking");
|
||||
@@ -983,6 +1124,7 @@ pub(crate) fn legacy_tab_in_strip(app: &mut VectorcraftApp, ui: &mut egui::Ui, p
|
||||
data.insert_temp(
|
||||
egui::Id::new("vectorcraft-panel-docking").with("legacy-origin"),
|
||||
LegacyDrag {
|
||||
panel: id.to_string(),
|
||||
layout: app.ui.docking.clone(),
|
||||
open_panel: app.ui.open_panel.clone(),
|
||||
floating: app.ui.floating_panels.clone(),
|
||||
|
||||
@@ -693,3 +693,229 @@ fn library_panels_float_with_their_names_and_a_persistent_menu_item() {
|
||||
let persistent: Vec<&str> = h.state().ui.library_panels.iter().filter(|p| p.persistent).map(|p| p.id.as_str()).collect();
|
||||
assert_eq!(persistent, ["pastels"]);
|
||||
}
|
||||
|
||||
/// The dock groups the last frame registered as drop targets.
|
||||
fn root_groups(h: &Harness<'static, VectorcraftApp>) -> Vec<crate::floating::RootGroup> {
|
||||
h.ctx.data(|d| d.get_temp::<Vec<crate::floating::RootGroup>>(crate::floating::root_groups_id())).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// The dock's group holding `panel`, as panel ids.
|
||||
fn root_group_of(app: &VectorcraftApp, panel: &str) -> Vec<String> {
|
||||
let layout = app.ui.docking.as_ref().unwrap();
|
||||
assert!(Layout { root: layout.root.clone(), floating: Vec::new() }.contains(&panel.to_string()), "{panel} is in the dock: {layout:?}");
|
||||
group_members(layout, panel)
|
||||
}
|
||||
|
||||
/// A legacy (native) floating group holding `panel`, as the Window menu or an icon tear leaves it
|
||||
/// before the shared layout takes over, and a point on its title bar.
|
||||
fn native_float(h: &mut Harness<'static, VectorcraftApp>, panel: &'static str) -> Pos2 {
|
||||
crate::floating::float(&mut h.state_mut().ui, &[panel], panel, egui::pos2(300.0, 300.0));
|
||||
h.run_steps(3);
|
||||
assert!(h.state().ui.docking.is_none(), "still the legacy dock");
|
||||
let r = h.ctx.memory(|m| m.area_rect(crate::floating::area_id(panel))).unwrap();
|
||||
egui::pos2(r.left() + 30.0, r.top() + 1.0 + crate::floating::TITLE / 2.0)
|
||||
}
|
||||
|
||||
/// A floating group dropped on the dock's tab strip joins the tabbed group as tabs.
|
||||
#[test]
|
||||
fn a_floating_group_dropped_on_the_dock_tabs_joins_the_tabbed_group() {
|
||||
let mut h = harness(app(), vec2(1280.0, 800.0));
|
||||
let title = native_float(&mut h, "pathfinder");
|
||||
let groups = root_groups(&h);
|
||||
assert_eq!(groups.len(), 1);
|
||||
assert_eq!(groups[0].panels, ["properties", "layers", "libraries"]);
|
||||
let onto = groups[0].strip.right_center() - vec2(60.0, 0.0);
|
||||
drag(&mut h, title, onto);
|
||||
assert!(h.state().ui.floating_panels.is_empty(), "{:?}", h.state().ui.floating_panels);
|
||||
assert_eq!(root_group_of(h.state(), "pathfinder"), ["properties", "layers", "libraries", "pathfinder"]);
|
||||
assert!(!h.state().ui.docking_icons.contains("pathfinder"), "its home is the tabbed group now");
|
||||
assert!(valid(h.state().ui.docking.as_ref().unwrap()));
|
||||
}
|
||||
|
||||
/// A floating group dropped on a dock group's lower half becomes a new group below it.
|
||||
#[test]
|
||||
fn a_floating_group_dropped_on_the_lower_half_of_the_dock_splits_it() {
|
||||
let mut h = harness(app(), vec2(1280.0, 800.0));
|
||||
let title = native_float(&mut h, "pathfinder");
|
||||
let group = root_groups(&h)[0].rect;
|
||||
drag(&mut h, title, egui::pos2(group.center().x, group.bottom() - 80.0));
|
||||
assert!(h.state().ui.floating_panels.is_empty());
|
||||
let layout = h.state().ui.docking.clone().unwrap();
|
||||
let Some(Node::Split { axis: craft_ui::layout::SplitAxis::Vertical, second, .. }) = &layout.root else {
|
||||
panic!("a top/bottom split: {layout:?}")
|
||||
};
|
||||
assert_eq!(**second, Node::Tabs { panels: vec!["pathfinder".into()], active: 0 });
|
||||
assert_eq!(root_group_of(h.state(), "layers"), ["properties", "layers", "libraries"]);
|
||||
// The two groups are drawn one above the other, and another group dropped on the upper half of
|
||||
// the lower one goes between them.
|
||||
h.run_steps(3);
|
||||
let groups = root_groups(&h);
|
||||
assert_eq!(groups.len(), 2, "{groups:?}");
|
||||
assert!(groups[0].rect.bottom() <= groups[1].rect.top() + 1.0, "{groups:?}");
|
||||
assert_eq!(groups[1].panels, ["pathfinder"]);
|
||||
let lower = groups[1].rect;
|
||||
h.state_mut().run("window.panel.float", json!({"panel":"align","x":300,"y":300})).unwrap();
|
||||
h.run_steps(3);
|
||||
let title =
|
||||
h.ctx.memory(|m| m.area_rect(egui::Id::new("vectorcraft-panel-docking").with(("floating", &"align".to_string())))).unwrap().left_top()
|
||||
+ vec2(6.0, 16.0);
|
||||
drag(&mut h, title, egui::pos2(lower.center().x, lower.top() + 50.0));
|
||||
assert!(h.state().ui.docking.as_ref().unwrap().floating.is_empty());
|
||||
h.run_steps(3);
|
||||
let panels: Vec<_> = root_groups(&h).into_iter().map(|g| g.panels).collect();
|
||||
assert_eq!(panels, [vec!["properties", "layers", "libraries"], vec!["align"], vec!["pathfinder"]]);
|
||||
}
|
||||
|
||||
/// A floating group dropped on the icon column becomes icons there.
|
||||
#[test]
|
||||
fn a_floating_group_dropped_on_the_icon_column_becomes_icons() {
|
||||
let mut h = harness(app(), vec2(1280.0, 1400.0));
|
||||
h.state_mut().run("window.panel.float", json!({"panel":"layers","x":300,"y":300})).unwrap();
|
||||
h.run_steps(3);
|
||||
let title =
|
||||
h.ctx.memory(|m| m.area_rect(egui::Id::new("vectorcraft-panel-docking").with(("floating", &"layers".to_string())))).unwrap().left_top()
|
||||
+ vec2(6.0, 16.0);
|
||||
let column = h.ctx.data(|d| d.get_temp::<egui::Rect>(crate::floating::icons_rect_id())).unwrap();
|
||||
drag(&mut h, title, column.center());
|
||||
let ui = &h.state().ui;
|
||||
assert!(ui.docking_icons.contains("layers"));
|
||||
assert!(!ui.docking.as_ref().unwrap().contains(&"layers".to_string()));
|
||||
assert_eq!(ui.open_panel.as_deref(), Some("layers"), "it pops out of its new icon");
|
||||
}
|
||||
|
||||
/// A dock tab dragged onto the icon column becomes an icon whose flyout shows the panel.
|
||||
#[test]
|
||||
fn a_dock_tab_dragged_onto_the_icon_column_becomes_an_icon_and_its_flyout_shows() {
|
||||
let mut h = harness(app(), vec2(1280.0, 1400.0));
|
||||
let tab = h.query_all_by_label("Layers").find(|node| node.rect().height() <= 45.0).unwrap().rect().center();
|
||||
let column = h.ctx.data(|d| d.get_temp::<egui::Rect>(crate::floating::icons_rect_id())).unwrap();
|
||||
drag(&mut h, tab, column.center());
|
||||
let ui = &h.state().ui;
|
||||
assert!(ui.docking_icons.contains("layers"), "{:?}", ui.docking_icons);
|
||||
assert_eq!(root_group_of(h.state(), "properties"), ["properties", "libraries"]);
|
||||
// Its icon heads the column; a click puts the popped-out panel away and another shows it again.
|
||||
h.state_mut().ui.open_panel = None;
|
||||
h.run_steps(3);
|
||||
let column = h.ctx.data(|d| d.get_temp::<egui::Rect>(crate::floating::icons_rect_id())).unwrap();
|
||||
let icon = h.query_all_by_label("Layers").map(|node| node.rect()).find(|r| column.contains_rect(*r)).expect("a Layers icon in the column");
|
||||
h.event(Event::PointerMoved(icon.center()));
|
||||
h.event(Event::PointerButton { pos: icon.center(), button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE });
|
||||
h.event(Event::PointerButton { pos: icon.center(), button: PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE });
|
||||
h.run_steps(3);
|
||||
assert_eq!(h.state().ui.open_panel.as_deref(), Some("layers"));
|
||||
// The preferences keep it in the column.
|
||||
let saved = serde_json::to_value(&h.state().ui).unwrap();
|
||||
let restored: crate::state::UiState = serde_json::from_value(saved).unwrap();
|
||||
assert!(restored.sanitized().docking_icons.contains("layers"));
|
||||
// Dragged from its icon back onto the tabbed group's tabs, it is a tab again.
|
||||
let strip = root_groups(&h)[0].strip;
|
||||
drag(&mut h, icon.center(), strip.right_center() - vec2(30.0, 0.0));
|
||||
assert_eq!(root_group_of(h.state(), "layers"), ["properties", "libraries", "layers"]);
|
||||
}
|
||||
|
||||
/// A dock tab's context menu closes its panel, and the Window menu brings it back.
|
||||
#[test]
|
||||
fn a_dock_tab_context_menu_closes_its_panel_and_the_window_menu_brings_it_back() {
|
||||
let mut h = harness(app(), vec2(1280.0, 800.0));
|
||||
assert!(h.state().ui.docking.is_none());
|
||||
let tab = h.query_all_by_label("Libraries").find(|node| node.rect().height() <= 45.0).unwrap().rect().center();
|
||||
h.event(Event::PointerMoved(tab));
|
||||
h.event(Event::PointerButton { pos: tab, button: PointerButton::Secondary, pressed: true, modifiers: Modifiers::NONE });
|
||||
h.event(Event::PointerButton { pos: tab, button: PointerButton::Secondary, pressed: false, modifiers: Modifiers::NONE });
|
||||
h.run_steps(3);
|
||||
h.get_by_label("Close Panel").click();
|
||||
h.run_steps(3);
|
||||
let layout = h.state().ui.docking.clone().expect("closing starts the shared layout");
|
||||
assert!(!layout.contains(&"libraries".to_string()));
|
||||
assert_eq!(root_group_of(h.state(), "layers"), ["properties", "layers"]);
|
||||
// In the shared dock the tabs' context menu closes them too.
|
||||
let tab = h.ctx.read_response(egui::Id::new("vectorcraft-panel-docking").with(("tab", &"layers".to_string()))).unwrap().rect.center();
|
||||
h.event(Event::PointerMoved(tab));
|
||||
h.event(Event::PointerButton { pos: tab, button: PointerButton::Secondary, pressed: true, modifiers: Modifiers::NONE });
|
||||
h.event(Event::PointerButton { pos: tab, button: PointerButton::Secondary, pressed: false, modifiers: Modifiers::NONE });
|
||||
h.run_steps(3);
|
||||
h.get_by_label("Close Panel").click();
|
||||
h.run_steps(3);
|
||||
assert_eq!(root_group_of(h.state(), "properties"), ["properties"]);
|
||||
h.state_mut().run("window.panel", json!({"panel":"libraries"})).unwrap();
|
||||
h.state_mut().run("window.panel", json!({"panel":"layers"})).unwrap();
|
||||
let mut panels = root_group_of(h.state(), "properties");
|
||||
panels.sort();
|
||||
assert_eq!(panels, ["layers", "libraries", "properties"]);
|
||||
}
|
||||
|
||||
/// `window.panel.dock {icon}` and `{anchor, zone}` check their parameters and survive a workspace switch.
|
||||
#[test]
|
||||
fn icon_docking_commands_validate_and_round_trip_through_workspaces() {
|
||||
let mut app = app();
|
||||
for params in [json!({"panel":"layers","icon":3}), json!({"panel":"layers","icon":true,"anchor":"properties"})] {
|
||||
assert!(app.run("window.panel.dock", params).is_err());
|
||||
assert!(app.ui.docking.is_none());
|
||||
}
|
||||
assert!(app.run("window.panel.move", json!({"panel":"layers","anchor":"properties","icon":true})).is_err());
|
||||
let r = app.run("window.panel.dock", json!({"panel":"layers","icon":true})).unwrap();
|
||||
assert!(r.get("layout").is_some() && r.get("column").is_none(), "the shared layout's result: {r}");
|
||||
assert!(app.ui.docking_icons.contains("layers"));
|
||||
assert!(!app.ui.docking.as_ref().unwrap().contains(&"layers".to_string()));
|
||||
let r = app.run("window.panel.dock", json!({"panel":"pathfinder","anchor":"properties","zone":"bottom"})).unwrap();
|
||||
assert!(r.get("layout").is_some() && r.get("column").is_none(), "the shared layout's result: {r}");
|
||||
assert!(matches!(&app.ui.docking.as_ref().unwrap().root, Some(Node::Split { axis: craft_ui::layout::SplitAxis::Vertical, .. })));
|
||||
app.run("window.workspace.new", json!({"name":"Icons"})).unwrap();
|
||||
app.run("window.workspace", json!({"name":"Essentials"})).unwrap();
|
||||
assert!(!app.ui.docking_icons.contains("layers"));
|
||||
app.run("window.workspace", json!({"name":"Icons"})).unwrap();
|
||||
assert!(app.ui.docking_icons.contains("layers"));
|
||||
assert_eq!(root_group_of(&app, "pathfinder"), ["pathfinder"]);
|
||||
// From the Window menu it pops out of its icon.
|
||||
app.run("window.panel", json!({"panel":"layers"})).unwrap();
|
||||
assert_eq!(app.ui.open_panel.as_deref(), Some("layers"));
|
||||
}
|
||||
|
||||
/// `column` goes with neither `anchor` nor `icon: true`, so it is never silently dropped;
|
||||
/// `icon: false` is the same as no `icon`, so a dock column still takes the panel.
|
||||
#[test]
|
||||
fn a_dock_column_is_refused_with_an_anchor_or_icon_and_icon_false_is_no_icon() {
|
||||
let mut app = app();
|
||||
for params in [json!({"panel":"color","column":1,"anchor":"properties"}), json!({"panel":"color","column":true,"icon":true})] {
|
||||
assert!(app.run("window.panel.dock", params.clone()).is_err(), "{params}");
|
||||
assert!(app.ui.docking.is_none() && app.ui.floating_panels.is_empty(), "{params} changed nothing");
|
||||
}
|
||||
let r = app.run("window.panel.dock", json!({"panel":"color","column":true,"icon":false})).unwrap();
|
||||
assert_eq!((r["column"].clone(), r["floating"].clone()), (json!(1), json!(false)));
|
||||
}
|
||||
|
||||
/// A group dropped on the icon column leaves what the column can't show (a library panel) in the
|
||||
/// dock instead of losing it.
|
||||
#[test]
|
||||
fn a_library_panel_dropped_on_the_icon_column_with_its_group_stays_in_the_dock() {
|
||||
let mut app = app();
|
||||
app.run("file.new", json!({})).unwrap();
|
||||
app.run("window.swatchLibrary", json!({"library": "pastels"})).unwrap();
|
||||
let library = "library:swatches:pastels".to_string();
|
||||
app.run("window.panel.float", json!({"panel": library, "x": 300, "y": 100})).unwrap();
|
||||
app.run("window.panel.move", json!({"panel":"color", "anchor": library})).unwrap();
|
||||
assert_eq!(group_members(app.ui.docking.as_ref().unwrap(), &library).len(), 2, "one floating group");
|
||||
app.run("window.panel.dock", json!({"panel": library, "group": true, "icon": true})).unwrap();
|
||||
assert!(app.ui.docking_icons.contains("color"));
|
||||
assert!(!app.ui.docking_icons.contains(&library));
|
||||
assert!(Layout { root: app.ui.docking.as_ref().unwrap().root.clone(), floating: Vec::new() }.contains(&library), "in the dock");
|
||||
}
|
||||
|
||||
/// A panel outside the icon groups still gets an icon once dropped on the column.
|
||||
#[test]
|
||||
fn a_panel_the_icon_column_has_no_place_for_still_shows_there_once_dropped_on_it() {
|
||||
let mut h = harness(app(), vec2(1280.0, 1600.0));
|
||||
assert!(!crate::state::ICON_PANEL_GROUPS.iter().any(|group| group.contains(&"charStyles")));
|
||||
h.state_mut().run("window.panel.float", json!({"panel":"charStyles","x":300,"y":300})).unwrap();
|
||||
h.run_steps(3);
|
||||
let title =
|
||||
h.ctx.memory(|m| m.area_rect(egui::Id::new("vectorcraft-panel-docking").with(("floating", &"charStyles".to_string())))).unwrap().left_top()
|
||||
+ vec2(6.0, 16.0);
|
||||
let column = h.ctx.data(|d| d.get_temp::<egui::Rect>(crate::floating::icons_rect_id())).unwrap();
|
||||
drag(&mut h, title, column.center());
|
||||
assert!(h.state().ui.docking_icons.contains("charStyles"));
|
||||
h.state_mut().ui.open_panel = None;
|
||||
h.run_steps(3);
|
||||
let column = h.ctx.data(|d| d.get_temp::<egui::Rect>(crate::floating::icons_rect_id())).unwrap();
|
||||
assert!(h.query_all_by_label("Character Styles").any(|node| column.contains_rect(node.rect())), "its icon is in the column");
|
||||
}
|
||||
@@ -145,6 +145,12 @@ pub fn all_panels() -> impl Iterator<Item = (&'static str, &'static str)> {
|
||||
tabs.chain(ICON_PANELS.iter().map(|&(id, label, _)| (id, label)))
|
||||
}
|
||||
|
||||
/// Panel `id`'s id, English label and icon if it can live in the icon column: an icon panel, or
|
||||
/// one of the tabbed group's panels dropped there.
|
||||
pub fn rail_panel(id: &str) -> Option<(&'static str, &'static str, &'static str)> {
|
||||
ICON_PANELS.iter().find(|entry| entry.0 == id).copied().or_else(|| DockTab::from_id(id).map(DockTab::info))
|
||||
}
|
||||
|
||||
/// Panels that live as collapsed icons in the dock (Essentials Classic).
|
||||
pub const ICON_PANELS: &[(&str, &str, &str)] = &[
|
||||
("color", "Color", "palette"),
|
||||
@@ -774,7 +780,8 @@ impl UiState {
|
||||
FloatingPanels::sanitize(&mut self.floating_panels, &mut self.toolbar_pos);
|
||||
crate::edit_toolbar::sanitize(&mut self.toolbar_edits);
|
||||
self.docking = self.docking.filter(crate::panel_docking::valid);
|
||||
self.docking_icons.retain(|id| ICON_PANELS.iter().any(|entry| entry.0 == id));
|
||||
// The tabbed group's panels live in the icon column too once dropped there.
|
||||
self.docking_icons.retain(|id| rail_panel(id).is_some());
|
||||
// Changes kept for workspaces that still exist, under their saved spelling and one per
|
||||
// workspace whatever the case (a hand-edited file).
|
||||
let mut seen = std::collections::BTreeSet::new();
|
||||
|
||||
@@ -204,8 +204,7 @@ pub fn apply(ui: &mut UiState, w: &Workspace) {
|
||||
ui.docking_generation = ui.docking_generation.wrapping_add(1);
|
||||
ui.docking = w.docking.clone().filter(crate::panel_docking::valid);
|
||||
ui.docking_hidden = w.docking_hidden.clone();
|
||||
ui.docking_icons =
|
||||
w.docking_icons.iter().filter(|id| crate::state::ICON_PANELS.iter().any(|entry| entry.0 == id.as_str())).take(64).cloned().collect();
|
||||
ui.docking_icons = w.docking_icons.iter().filter(|id| crate::state::rail_panel(id).is_some()).take(64).cloned().collect();
|
||||
ui.toolbar_pos = w.toolbar_pos;
|
||||
ui.dock_width = w.dock_width;
|
||||
ui.dock_width_pending = true;
|
||||
|
||||
+4
-2
@@ -1,6 +1,6 @@
|
||||
# Roadmap: milestones and what's next
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-21 · **Change:** minor (M14: workspaces keep their layout changes (UI.7); estimates unchanged; Shape Builder Gap Detection shares Live Paint’s spatial gap finder and temporary merge bridges (#2526); Custom is capped at 72 pt in the existing options dialog; Recolor model selection is available through the engine and MCP only (#2520); the dialog retains its existing controls; 3D: original Standard, Diffuse, Top Left and Right lighting presets; command access, effective-setting recognition and preview/history; scores and estimates unchanged; Polygon and Star exact-size placement remembers the chosen count for later drags and dialog openings; Cancel preserves the previous count; M12 duplicate milestone rows consolidated without losing distinct details; scores and estimates unchanged; live-trace bounds note placed under Image Trace rather than Graphs; Live trace result bounds and selection highlights exclude the retained bitmap except in source-image views; M3.57: CSS swatch libraries retain fractional gradient stop positions; scores and hours unchanged; G2: Effect › Artistic › Smudge Stick, Sponge and Watercolor; Sketch › Bas Relief, Charcoal, Photocopy and Stamp, #2533; Photoshop filters 45/57, 12 remaining (8–14 h); interactive Mirror Repeat axis; bounding-box handles take priority over the axis body while the center and end handles retain their move and rotation actions; status-readout sample widths cached by language, document unit and pixels per point; status messages and readouts use stable responsive allocations sized for localized labels and ordinary coordinates; DXF circular cubic paths export as native CIRCLE/ARC with R12 weighted strokes kept as POLYLINE and closing-chord safeguards (#1299); cached canvas dimensions match the renderer limit before cache-key creation, view construction and worker submission; cached canvas reprojection preserves scale during resize, rotation and DPI changes; interactive Mirror Repeat axis; bounding-box handles take priority over the axis body while the center and end handles retain their move and rotation actions; 3D lighting: one to ten colored lights, smooth colored Inflate, legacy white-light appearance preserved; surface batching and restored seam minimum; scores and estimates unchanged; Blend spine Anchor Point conversions, selection/stacking priority and Shift regression coverage (#2544); scores and hours unchanged; Image Trace: Auto Color keeps rare but distinct colours; Image Trace: large images trace with a warning; anchor limit raised to 8,000,000; Image Trace: Raster::new returns an error for a buffer of the wrong length; M12 graph pruning and junction merging measured; M12 exact crop EDT and Guo–Hall thinning measured; M12 centre-lines baseline measured; scores and hours unchanged; Image Trace: a higher Paths never gives fewer segments on collapsed loops; Image Trace: the notch rule reads only its own piece; Image Trace: an edge's sub-pixel shift is the same from either side; Image Trace: a simpler contour walk with no precomputed direction table; transparency grid visibility saved in native documents, #1968; EPS editing data: legacy mesh corners, vector opacity masks and compound member paint retained; raster page comparison filters minification without relaxing limits; scores and hours unchanged; correction: M12's Image Trace quick presets leave the panel as it was, the Image Trace button opens it; batch exporting, #1935; G2: Effect › Sketch › Halftone Pattern; Photoshop filters 45/57, 12 remaining (8–14 h); Warp Options controls verified by focused checks and original-image review; estimates unchanged; existing-guide Escape cancellation validated 2026-10-14; scores and hours unchanged; open-path anchor Delete keeps surviving runs separate; estimates unchanged; PDF.R1: shared PDF reader with windowed content scanning and explicit incomplete-content errors; scores and estimates unchanged; PDF.W1: guarded shared PDF writer; output behavior, scores and estimates unchanged; Artboard tab-switch follow-up historically validated on `a0671777`; scores and hours unchanged; Artboard clean active-tab close/Delete follow-up validated 2026-10-15; later 2026-10-14 merges: per-corner radius, #1682; Width Point Edit from the Width tool, #1814, #1630; guides, #1782, #1836; anchor X/Y/W/H, #1824; toolbar and All Tools, #1751; macOS save sheets, #1855; rendering caches, #1667–#1792; untrusted files, #1852, #1862–#1873; failed-save dialog, #1685; Dim Images, #1683; estimates unchanged; M12 row: Image Trace's enhanced presets; verified PngSuite downloads, #1729; synthetic control-input budgets, #1709; 2026-10-14 merges: anchor Align and Distribute, #1636; Direct Selection and Delete fixes, #1674, #1666, #1743; Expand on repeats, #1756; file and input hardening, #1776–#1783, #1786, #1807; MCP limits and envelope, #1727, #1728; Image Trace #1647, #1657; estimates unchanged; later 2026-10-13 mLine truncated
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-21 · **Change:** minor (M14: panels dock into any dock group or the icon column by dragging (UI.6), workspaces keep their layout changes (UI.7); estimates unchanged; Shape Builder Gap Detection shares Live Paint’s spatial gap finder and temporary merge bridges (#2526); Custom is capped at 72 pt in the existing options dialog; Recolor model selection is available through the engine and MCP only (#2520); the dialog retains its existing controls; 3D: original Standard, Diffuse, Top Left and Right lighting presets; command access, effective-setting recognition and preview/history; scores and estimates unchanged; Polygon and Star exact-size placement remembers the chosen count for later drags and dialog openings; Cancel preserves the previous count; M12 duplicate milestone rows consolidated without losing distinct details; scores and estimates unchanged; live-trace bounds note placed under Image Trace rather than Graphs; Live trace result bounds and selection highlights exclude the retained bitmap except in source-image views; M3.57: CSS swatch libraries retain fractional gradient stop positions; scores and hours unchanged; G2: Effect › Artistic › Smudge Stick, Sponge and Watercolor; Sketch › Bas Relief, Charcoal, Photocopy and Stamp, #2533; Photoshop filters 45/57, 12 remaining (8–14 h); interactive Mirror Repeat axis; bounding-box handles take priority over the axis body while the center and end handles retain their move and rotation actions; status-readout sample widths cached by language, document unit and pixels per point; status messages and readouts use stable responsive allocations sized for localized labels and ordinary coordinates; DXF circular cubic paths export as native CIRCLE/ARC with R12 weighted strokes kept as POLYLINE and closing-chord safeguards (#1299); cached canvas dimensions match the renderer limit before cache-key creation, view construction and worker submission; cached canvas reprojection preserves scale during resize, rotation and DPI changes; interactive Mirror Repeat axis; bounding-box handles take priority over the axis body while the center and end handles retain their move and rotation actions; 3D lighting: one to ten colored lights, smooth colored Inflate, legacy white-light appearance preserved; surface batching and restored seam minimum; scores and estimates unchanged; Blend spine Anchor Point conversions, selection/stacking priority and Shift regression coverage (#2544); scores and hours unchanged; Image Trace: Auto Color keeps rare but distinct colours; Image Trace: large images trace with a warning; anchor limit raised to 8,000,000; Image Trace: Raster::new returns an error for a buffer of the wrong length; M12 graph pruning and junction merging measured; M12 exact crop EDT and Guo–Hall thinning measured; M12 centre-lines baseline measured; scores and hours unchanged; Image Trace: a higher Paths never gives fewer segments on collapsed loops; Image Trace: the notch rule reads only its own piece; Image Trace: an edge's sub-pixel shift is the same from either side; Image Trace: a simpler contour walk with no precomputed direction table; transparency grid visibility saved in native documents, #1968; EPS editing data: legacy mesh corners, vector opacity masks and compound member paint retained; raster page comparison filters minification without relaxing limits; scores and hours unchanged; correction: M12's Image Trace quick presets leave the panel as it was, the Image Trace button opens it; batch exporting, #1935; G2: Effect › Sketch › Halftone Pattern; Photoshop filters 45/57, 12 remaining (8–14 h); Warp Options controls verified by focused checks and original-image review; estimates unchanged; existing-guide Escape cancellation validated 2026-10-14; scores and hours unchanged; open-path anchor Delete keeps surviving runs separate; estimates unchanged; PDF.R1: shared PDF reader with windowed content scanning and explicit incomplete-content errors; scores and estimates unchanged; PDF.W1: guarded shared PDF writer; output behavior, scores and estimates unchanged; Artboard tab-switch follow-up historically validated on `a0671777`; scores and hours unchanged; Artboard clean active-tab close/Delete follow-up validated 2026-10-15; later 2026-10-14 merges: per-corner radius, #1682; Width Point Edit from the Width tool, #1814, #1630; guides, #1782, #1836; anchor X/Y/W/H, #1824; toolbar and All Tools, #1751; macOS save sheets, #1855; rendering caches, #1667–#1792; untrusted files, #1852, #1862–#1873; failed-save dialog, #1685; Dim Images, #1683; estimates unchanged; M12 row: Image Trace's enhanced presets; verified PngSuite downloads, #1729; synthetic control-input budgets, #1709; 2026-10-14 merges: anchor Align and Distribute, #1636; Direct Selection and Delete fixes, #1674, #1666, #1743; Expand on repeats, #1756; file and input hardening, #1776–#1783, #1786, #1807; MCP limits and envelope, #1727, #Line truncated
|
||||
|
||||
Forward-looking plan. Where VectorCraft stands is in [`ROADMAP.md`](../ROADMAP.md) and
|
||||
[target-app-parity.md](target-app-parity.md); the ranked work list is [gaps.md](gaps.md). Hours are Opus 5.5
|
||||
@@ -66,7 +66,7 @@ Checked 2026-10-10 from the code, tests and open issues; no workflow fails, so V
|
||||
| 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; broader scripting and batch work remains | 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) | 🟡 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 <name>, UI.7), 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); an experimental, unsupported Windows 7 x64 portable OpenGL build (not verified by maintainers); accessibility and broader Windows/Linux packaging/QA pending | 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) | 🟡 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 <name>, UI.7), 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); an experimental, unsupported Windows 7 x64 portable OpenGL build (not verified by maintainers); accessibility and broader Windows/Linux packaging/QA pending | 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 |
|
||||
@@ -95,6 +95,8 @@ sampling coexistence. Scores and estimates unchanged. See [development.md](devel
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-21 | minor | M14: workspaces keep the layout changes made in them across switches and restarts until Reset <name> (UI.7); estimates unchanged |
|
||||
| 2026-10-21 | minor | M14: panels dock by dragging into any dock group (tabs or a top/bottom split) or the icon column; dock tabs close from their context menu (UI.6); estimates unchanged |
|
||||
| 2026-10-20 | minor | M14: workspaces keep the layout changes made in them across switches and restarts until Reset <name> (UI.7); estimates unchanged |
|
||||
| 2026-10-21 | minor | M3: highlighted-text Eyedropper sampling preserves ranges and rejects unsupported source/target transparency atomically; regression-tested; scores and estimates unchanged. |
|
||||
| 2026-10-21 | minor | Shape Builder Gap Detection shares Live Paint’s spatial gap finder and temporary merge bridges (#2526); Custom is capped at 72 pt in the existing options dialog. Scores and estimates unchanged. |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Parity with Adobe Illustrator
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-22 · **Change:** minor (UI chrome: workspaces keep their layout changes (UI.7); scores and hours unchanged; Shape Builder Gap Detection shares Live Paint’s spatial gap finder and temporary merge bridges (#2526); Custom is capped at 72 pt in the existing options dialog; Recolor model selection is available through the engine and MCP only (#2520); the dialog retains its existing controls; filter plug-ins read and set effect records and stroke joins, caps and miter limits, G17; the Print dialog's Media list has the Japanese papers: JIS B4 257 × 364 mm, JIS B5 182 × 257 mm and JIS B6 beside the ISO B4/B5, which keep their ISO 216 sizes, and はがき, 往復はがき, 封筒 長形3号/長形4号/角形2号/角形3号 and 四六判; a paper is named by its JSON id or its dialog label, any case and spacing; imported PDF type that is letter-spaced keeps its word spaces and no others; Fractional Widths: Character panel menu, per type object, on by default; System Layout remains a panel stub; scores unchanged; G4: manual Tate-chu-yoko; G4: No Break; scores unchanged; the selection tools take a clipping path on its outline, G1; Polygon and Star exact-size placement remembers the chosen count for later drags and dialog openings; Cancel preserves the previous count; Live trace result bounds and selection highlights exclude the retained bitmap except in source-image views; CSS swatch libraries retain fractional gradient stop positions; scores and hours unchanged; live-effect plug-ins given their object's fills and strokes and the item they sit on, G17; plug-in parameters: labels, units, groups, help and conditions in their dialogs, text and number-list parameters, up to 256 of them, G17; exporter plug-ins: plug-in file formats in Export As and document.export, G17; G2: Effect › Artistic › Smudge Stick, Sponge and Watercolor; Sketch › Bas Relief, Charcoal, Photocopy and Stamp, #2533; Photoshop filters 45/57, 12 remaining (8–14 h); interactive Mirror Repeat axis; bounding-box handles take priority over the axis body while the center and end handles retain their move and rotation actions; Libraries panel: the list scrolls, items and groups reorder by dragging, items drag into and out of groups (`library.moveItem` before/after, `library.moveGroup`); scores unchanged; PDF invisible text opens as type with no fill or stroke and is written back invisible, #2675; status-readout sample widths cached by language, document unit and pixels per point; status messages and readouts use stable responsive allocations sized for localized labels and ordinary coordinates; cached canvas dimensions match the renderer limit before cache-key creation, view construction and worker submission; cached canvas reprojection preserves scale during resize, rotation and DPI changes; interactive Mirror Repeat axis; bounding-box handles take priority over the axis body while the center and end handles retain their move and rotation actions; 3D lighting: one to ten colored lights, smooth colored Inflate, legacy white-light appearance preserved; surface batching and restored seam minimum; scores and estimates unchanged; Blend spine Anchor Point conversions, selection/stacking priority and Shift regression coverage (#2544); scores and hours unchanged; Artboard Options' X and Y with a reference point, #1876; Image Trace: Auto Color keeps rare but distinct colours; Image Trace: large images trace with a warning; anchor limit raised to 8,000,000; Image Trace: Raster::new returns an error for a buffer of the wrong length; M12 maximal-disc graph pruning and junction merging measured; M12 crop EDT and Guo–Hall thinning measured; M12 centre-lines baseline measured; scores and hours unchanged; installed plug-ins stay installed after a restart, #2541; Group Selection picks and moves one path of a compound path, #2441 in part; the Move dialog opens on the last move and its last checkboxes; Image Trace: a higher Paths never gives fewer segments on collapsed loops; Image Trace: the notch rule reads only its own piece; Image Trace: an edge's sub-pixel shift is the same from either side; Image Trace Flat Logo traces line art on white paper instead of a blank result; score and hours unchanged; Image Trace: a simpler contour walk with no precomputed direction table; transparency grid visibility saved in native documents, #1968; MCP cancellation, progress and busy answers during long exports, #2165, #2171, #2172; EPS editing data: legacy mesh corners, vector opacity masks and compound member paint retained; raster page comparison filters minification without relaxing limits; scores and hours unchanged; native family-name fallback from English-only typographic names, #2208; scores unchanged; batch exporting, #1935; G2: Effect › Sketch › Halftone Pattern; Photoshop filters 45/57, 12 remaining (8–14 h); G5: Symbol Options Export Type (Graphic / Movie Clip); Localization: Brazilian Portuguese coLine truncated
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-22 · **Change:** minor (UI chrome: panels dock into any dock group or the icon column by dragging (UI.6), workspaces keep their layout changes (UI.7); scores and hours unchanged; Shape Builder Gap Detection shares Live Paint’s spatial gap finder and temporary merge bridges (#2526); Custom is capped at 72 pt in the existing options dialog; Recolor model selection is available through the engine and MCP only (#2520); the dialog retains its existing controls; filter plug-ins read and set effect records and stroke joins, caps and miter limits, G17; the Print dialog's Media list has the Japanese papers: JIS B4 257 × 364 mm, JIS B5 182 × 257 mm and JIS B6 beside the ISO B4/B5, which keep their ISO 216 sizes, and はがき, 往復はがき, 封筒 長形3号/長形4号/角形2号/角形3号 and 四六判; a paper is named by its JSON id or its dialog label, any case and spacing; imported PDF type that is letter-spaced keeps its word spaces and no others; Fractional Widths: Character panel menu, per type object, on by default; System Layout remains a panel stub; scores unchanged; G4: manual Tate-chu-yoko; G4: No Break; scores unchanged; the selection tools take a clipping path on its outline, G1; Polygon and Star exact-size placement remembers the chosen count for later drags and dialog openings; Cancel preserves the previous count; Live trace result bounds and selection highlights exclude the retained bitmap except in source-image views; CSS swatch libraries retain fractional gradient stop positions; scores and hours unchanged; live-effect plug-ins given their object's fills and strokes and the item they sit on, G17; plug-in parameters: labels, units, groups, help and conditions in their dialogs, text and number-list parameters, up to 256 of them, G17; exporter plug-ins: plug-in file formats in Export As and document.export, G17; G2: Effect › Artistic › Smudge Stick, Sponge and Watercolor; Sketch › Bas Relief, Charcoal, Photocopy and Stamp, #2533; Photoshop filters 45/57, 12 remaining (8–14 h); interactive Mirror Repeat axis; bounding-box handles take priority over the axis body while the center and end handles retain their move and rotation actions; Libraries panel: the list scrolls, items and groups reorder by dragging, items drag into and out of groups (`library.moveItem` before/after, `library.moveGroup`); scores unchanged; PDF invisible text opens as type with no fill or stroke and is written back invisible, #2675; status-readout sample widths cached by language, document unit and pixels per point; status messages and readouts use stable responsive allocations sized for localized labels and ordinary coordinates; cached canvas dimensions match the renderer limit before cache-key creation, view construction and worker submission; cached canvas reprojection preserves scale during resize, rotation and DPI changes; interactive Mirror Repeat axis; bounding-box handles take priority over the axis body while the center and end handles retain their move and rotation actions; 3D lighting: one to ten colored lights, smooth colored Inflate, legacy white-light appearance preserved; surface batching and restored seam minimum; scores and estimates unchanged; Blend spine Anchor Point conversions, selection/stacking priority and Shift regression coverage (#2544); scores and hours unchanged; Artboard Options' X and Y with a reference point, #1876; Image Trace: Auto Color keeps rare but distinct colours; Image Trace: large images trace with a warning; anchor limit raised to 8,000,000; Image Trace: Raster::new returns an error for a buffer of the wrong length; M12 maximal-disc graph pruning and junction merging measured; M12 crop EDT and Guo–Hall thinning measured; M12 centre-lines baseline measured; scores and hours unchanged; installed plug-ins stay installed after a restart, #2541; Group Selection picks and moves one path of a compound path, #2441 in part; the Move dialog opens on the last move and its last checkboxes; Image Trace: a higher Paths never gives fewer segments on collapsed loops; Image Trace: the notch rule reads only its own piece; Image Trace: an edge's sub-pixel shift is the same from either side; Image Trace Flat Logo traces line art on white paper instead of a blank result; score and hours unchanged; Image Trace: a simpler contour walk with no precomputed direction table; transparency grid visibility saved in native documents, #1968; MCP cancellation, progress and busy answers during long exports, #2165, #2171, #2172; EPS editing data: legacy mesh corners, vector opacity masks and compound member paint retained; raster page comparison filters minification without relaxing limits; scores and hours unchanged; native family-name fallback from English-only typographic names, #2208; scores unchanged; batch exporting, #1935; G2: Effect › Sketch › Halftone Pattern; Photoshop filters 45/57, 12 remaining (8–14 h); G5: Symbol Options ExLine truncated
|
||||
|
||||
The authoritative parity assessment. [`ROADMAP.md`](../ROADMAP.md) summarizes it; [`gaps.md`](gaps.md) is
|
||||
the work list made from it. Deep areas have their own checklists: [UI and interaction](ui-parity.md),
|
||||
@@ -350,8 +350,8 @@ about 1–1.5 agent-hours each, so the other rows (estimated on the older scale)
|
||||
| Export for Screens, Asset Export, slices, Save for Web | 3 | 85% | polish only (all four are done) | 2–4 |
|
||||
| Print, colour management, separations, flattener | 3 | 80% | print preview fidelity (flattener presets in PDF and print, overprint in PDF and composite print, Print dialog, presets, PostScript, marks, separations, Print Tiling, print overprint/flattener/bitmap options and printer profile are done, as are the Japanese papers in its Media list: JIS B4/B5/B6, はがき, 往復はがき, 封筒 長形3号/長形4号/角形2号/角形3号 and 四六判, beside the ISO B4/B5 that settings saved before them keep) | 1–2 |
|
||||
| Automation | 3 | 65% | scripting surface, batch (MCP drops malformed app replies without replay; Variables: text/visibility variables bound to objects, an object may carry several, dataset rows applied in one undo step, panel and Window › Variables with dialogs; image/graph kinds and dataset import/export open) | 6–10 |
|
||||
| UI chrome (panels, contextual Properties, workspaces, prefs) | 6 | 80% | macOS native menu dismissal fixed (M14.8); Variables, Links, Asset Export, CSS Properties, Attributes and the canvas context menu are done; remaining: SVG Interactivity, Properties per context, Consolidate All Windows, floating panels outside the app window and docked into any group, context menus beyond the canvas, about 13 Preferences options stored but not yet acted on (#394: Open Documents As Tabs, Real-time Drawing, hyphenation language, devices…) | 13–20 |
|
||||
| UI chrome (panels, contextual Properties, workspaces, prefs) | 6 | 80% | macOS native menu dismissal fixed (M14.8); Variables, Links, Asset Export, CSS Properties, Attributes and the canvas context menu are done; remaining: SVG Interactivity, Properties per context, Consolidate All Windows, floating panels outside the app window and docked into any group, context menus beyond the canvas, about 14 Preferences options stored but not yet acted on (#394: Highlight Alternate Glyphs, Open Documents As Tabs, Real-time Drawing, hyphenation language, devices…) | 13–20 |
|
||||
| UI chrome (panels, contextual Properties, workspaces, prefs) | 6 | 80% | macOS native menu dismissal fixed (M14.8); Variables, Links, Asset Export, CSS Properties, Attributes, the canvas context menu and docking panels into any dock group by dragging (as tabs, a top/bottom split or icons) are done; remaining: SVG Interactivity, Properties per context, Consolidate All Windows, floating panels outside the app window, context menus beyond the canvas, about 13 Preferences options stored but not yet acted on (#394: Open Documents As Tabs, Real-time Drawing, hyphenation language, devices…) | 13–20 |
|
||||
| UI chrome (panels, contextual Properties, workspaces, prefs) | 6 | 80% | macOS native menu dismissal fixed (M14.8); Variables, Links, Asset Export, CSS Properties, Attributes, the canvas context menu and docking panels into any dock group by dragging (as tabs, a top/bottom split or icons) are done; remaining: SVG Interactivity, Properties per context, Consolidate All Windows, floating panels outside the app window, context menus beyond the canvas, about 14 Preferences options stored but not yet acted on (#394: Highlight Alternate Glyphs, Open Documents As Tabs, Real-time Drawing, hyphenation language, devices…) | 13–20 |
|
||||
| Libraries, Links, Package | 2 | 82% | the Libraries panel is local only (no cloud by design: libraries are files on this machine) and has no shared libraries, linked graphics or gradients and patterns as items yet; its graphics, colours and text styles (#745), its search, groups and library export/import as `.vclibrary` files (#926), and a list that scrolls, with items and groups reordered by dragging and items dragged into and out of groups, are done; Links and Package are done; placed VectorCraft documents are done (linked by default, vectors in every output, previews, Break Link, Expand, Edit Original, refresh on save) | 2–3 |
|
||||
| **Feature parity** | **103** | **~75%** | | **~210–330** |
|
||||
| Interaction-fidelity pass (side by side with Illustrator: every tool modifier, cursor, dialog, Properties context) | | | | 60–90 |
|
||||
@@ -486,7 +486,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- Screen readers get a name for the Tools panel's tool buttons, flyout rows, floating strips and tear-off bars, and for the icon buttons, text buttons, toggles, checkboxes, radio buttons, tabs, chips, sliders, angle dials and combo boxes that panels and dialogs draw with `crates/ui-egui/src/widgets.rs`: the text, label or tool tip each one shows, or a name of its own where it shows none (the angle dial, the spinner, the preset arrow, the color wheel and the Reference Point's squares). The tool buttons, toggles, checkboxes, radio buttons, tabs and the Reference Point's nine squares also report whether they are on, an icon button that is lit, such as the chosen Align To button, reports that it is pressed, and the angle dial reports its angle. The status bar's First, Previous, Next and Last Artboard buttons have tool tips.
|
||||
- These dialog sliders, each beside a number field, respond to the keyboard: those of the effect, tool option, brush and transform dialogs, the Perspective Grid, the 3D dialogs, and the flattener options, which the Flattener Preview panel also shows. A click, or Tab, gives a slider the keyboard focus, and an outline shows it. Then → and ↑ step the field's number up to the next whole number and ← and ↓ step it down to the next whole number, Shift with an arrow and Page Up and Page Down step it by ten, and Home and End set the slider's first and last value; no step goes past either end. Screen readers' increment and decrement actions step the slider as → and ← do. The Color panel's, Swatch Options', the Navigator's and the Character panel's sliders, and the Color Picker's channel slider, respond only to the pointer. The keys follow the WAI-ARIA slider pattern (<https://www.w3.org/WAI/ARIA/apg/patterns/slider/>); the reference app's help does not describe keys on a dialog's slider, so the step sizes are VectorCraft's own.
|
||||
- Screen readers get the value of the color and channel sliders that panels and dialogs draw (the Color panel, the Color Picker, Swatch Options, the Navigator's Zoom, the Character panel's variable font axes, the Perspective Grid and flattener options, and the sliders of the effect, tool option and transform dialogs): the number and text of the field beside each one, such as 128 for G or 50% for an opacity, with the slider's range, which extends to include a number past the slider's end. The harmony wheel's Brightness slider, which has no field, reports its brightness in percent.
|
||||
- Panels undock (#495): a dock tab, a panel icon or a popped-out panel's title dragged out of the dock floats that panel inside the window, and the strip right of the dock's tabs floats the whole tabbed group. A floating group moves by its title bar, a tab dragged out of it floats on its own, and dropped on another group's title bar or tabs it stacks with that group; dropped on the dock, which lights up under the pointer, its panels go back where they live (the tabbed group or the icon column), as its × and the panel menus' Float Panel / Dock Panel do (`window.panel.float`, `window.panel.dock`). The Tools panel floats by its title bar and docks again on the window's left edge. Floating groups stack into sets: dropped on the upper or lower half of another group (a line shows where), a group joins it right above or below it, and the set's groups stack top to bottom, each showing one panel, moving together by the top group's title bar, whose × docks them all; a group dragged by its tab strip leaves the set (`window.panel.float {below, above}`). A double-click on a tab collapses a floating group to its tabs (`collapsed`). Sets dropped on the left edge of the icon column or of another dock column dock as dock columns (a multi-column dock, `window.panel.dock {column}`), whose groups stack, tab and tear out as a floating set's do, and whose top strip floats the column again. Floating panels, their sets and the Tools panel's position are kept with the preferences and in user workspaces, and clamped into a smaller window. Not done: panels in separate OS windows, docking a panel into the default tabbed group it doesn't belong to, reordering tabs by dragging, collapsing a set or dock column to icons, dock columns right of the default dock, resizing floating panels and the groups of a set.
|
||||
- Panels undock (#495): a dock tab, a panel icon or a popped-out panel's title dragged out of the dock floats that panel inside the window, and the strip right of the dock's tabs floats the whole tabbed group. A floating group moves by its title bar, a tab dragged out of it floats on its own, and dropped on another group's title bar or tabs it stacks with that group; dropped on a dock group's tabs, which light up under the pointer, its panels join that group, and on the group's upper or lower half they make a new group above or below it (`window.panel.dock {anchor, zone}`); dropped on the icon column they become icons there, the tabbed group's panels too (`window.panel.dock {icon: true}`); dropped anywhere else on the dock, which lights up, they go back where they live (the tabbed group or the icon column), as its × and the panel menus' Float Panel / Dock Panel do (`window.panel.float`, `window.panel.dock`). The Tools panel floats by its title bar and docks again on the window's left edge. Floating groups stack into sets: dropped on the upper or lower half of another group (a line shows where), a group joins it right above or below it, and the set's groups stack top to bottom, each showing one panel, moving together by the top group's title bar, whose × docks them all; a group dragged by its tab strip leaves the set (`window.panel.float {below, above}`). A double-click on a tab collapses a floating group to its tabs (`collapsed`). Sets dropped on the left edge of the icon column or of another dock column dock as dock columns (a multi-column dock, `window.panel.dock {column}`), whose groups stack, tab and tear out as a floating set's do, and whose top strip floats the column again. Floating panels, their sets and the Tools panel's position are kept with the preferences and in user workspaces, and clamped into a smaller window. Not done: panels in separate OS windows, reordering tabs by dragging, collapsing a set or dock column to icons, dock columns right of the default dock, resizing floating panels and the groups of a set.
|
||||
- The dock's double arrow collapses Properties, Layers and Libraries to icons at the top of the icon column, popping out like the other icon panels, and expands them again (`window.collapseDock`, kept with the preferences and in saved workspaces). The icon column's own arrow (expanding the icon panels into a full column) is not done.
|
||||
- Czech render-site labels: desktop/web file filters, diagnostics, suggested preset names and contributor tooltips/plurals now use catalog lookups; gradients have Czech adjective agreement and printer marks have a separate context (PDF/Print and Summary). Search, Home, toolbar/panel tooltips and Preferences already look up their labels. Every complete catalog has rows for these labels and for the Roman and Japanese printer mark styles. Evidence: `i18n::tests::czech_render_site_follow_up`, `i18n::tests::complete_catalogs_translate_render_site_labels`, `dialogs::tests_modal::printer_mark_labels_are_distinct_from_language_names`; see [localization-parity.md](localization-parity.md).
|
||||
- The History panel, Edit › Undo and Edit › Redo translate the names of the steps that apply or redefine a character or paragraph style and of the step that redefines a graphic style: each such step keeps its template (`Apply {name}`, `Redefine {name}`, `Redefine Graphic Style “{name}”`) and the style's name, every complete catalog has a row for each template, and the panel and the menu show the style's name as the document holds it, except the Normal styles' names, which they show in the interface language (`steps_named_after_a_style_list_the_template_around_the_name`, `undo_and_redo_name_a_style_step_around_the_style_name`, `every_undo_label_is_translated`).
|
||||
@@ -931,6 +931,8 @@ sampling coexistence. Scores and estimates unchanged. See [development.md](devel
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-21 | minor | UI chrome: workspaces keep the layout changes made in them when switching away and back, and across restarts; Reset <name> returns to the saved layout (UI.7); score and hours unchanged (estimated) |
|
||||
| 2026-10-21 | minor | UI chrome: a panel group dragged onto a dock group's tabs joins it, onto its upper or lower half makes a new group above or below it, onto the icon column becomes icons (the tabbed group's panels too); dock tabs close from their context menu (UI.6); score and hours unchanged (estimated) |
|
||||
| 2026-10-20 | minor | UI chrome: workspaces keep the layout changes made in them when switching away and back, and across restarts; Reset <name> returns to the saved layout (UI.7); score and hours unchanged (estimated) |
|
||||
| 2026-10-21 | minor | M3: highlighted-text Eyedropper sampling preserves ranges and rejects unsupported source/target transparency atomically; regression-tested; scores and estimates unchanged. |
|
||||
| 2026-10-21 | minor | Shape Builder Gap Detection shares Live Paint’s spatial gap finder and temporary merge bridges (#2526); Custom is capped at 72 pt in the existing options dialog. Scores and estimates unchanged. |
|
||||
|
||||
+4
-1
@@ -1,6 +1,6 @@
|
||||
# UI and interaction parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-21 · **Change:** minor (workspaces keep their layout changes (UI.7); scores unchanged; Shape Builder Gap Detection shares Live Paint’s spatial gap finder and temporary merge bridges (#2526); Custom is capped at 72 pt in the existing options dialog; 3D: original Standard, Diffuse, Top Left and Right lighting presets; command access, effective-setting recognition and preview/history; scores and estimates unchanged; one selected clip group's Properties panel has Edit Clipping Path and Edit Contents at its top, G1; Distribute Objects and Distribute Spacing space from a key guide (#3165) with the Distribute Spacing value; Distribute spaces selected ruler guides with the objects, #1597 in part; the selection tools take a clipping path on its outline, over the art it clips; Polygon and Star exact-size placement remembers the chosen count for later drags and dialog openings; Cancel preserves the previous count; Live trace result bounds and selection highlights exclude the retained bitmap except in source-image views; a ruler guide can be the key object Align aligns to, and selected guides align to a key object, #2143; Pencil: Alt key toggles to Smooth Tool, #2001; status-readout sample widths cached by language, document unit and pixels per point; status messages and readouts use stable responsive allocations sized for localized labels and ordinary coordinates; cached canvas dimensions match the renderer limit before cache-key creation, view construction and worker submission; cached canvas reprojection preserves scale during resize, rotation and DPI changes; on-canvas Mirror Repeat axis, #2877; Background Export progress dialog with Cancel, #2564; 3D lighting: one to ten colored lights, smooth colored Inflate, legacy white-light appearance preserved; surface batching and restored seam minimum; scores and estimates unchanged; Blend spine Anchor Point conversions, selection/stacking priority and Shift regression coverage (#2544); scores and hours unchanged; Artboard Options' X and Y with a reference point, #1876; scores unchanged; library panels: one per library, dockable and tabbable, with Persistent, #2603; the Selection tool threads text from the out port, #2572 in part; scores unchanged; Group Selection picks and moves one path of a compound path, #2441 in part; the Move dialog opens on the last move and its last checkboxes; scores unchanged; transparency grid visibility saved in native documents, #1968; Live Corners radius up to the neighbouring cuts, #2332; Image Trace quick presets: wording corrected to the shipped behaviour, the Image Trace button opens the controls; batch exporting, #1935; document window scroll bars, #2322; Anchor Point tool: a click on a handle end retracts that handle only, #2267; scores unchanged; Smart Guides: the dragged end of a straight segment keeps to its extension at any angle, #2120; Warp Options controls verified by focused checks and original-image review; estimates unchanged; existing-guide Escape cancellation validated 2026-10-14; scores and hours unchanged; open-path anchor Delete keeps surviving runs separate; estimates unchanged; Artboard tab-switch follow-up historically validated on `a0671777`; scores and hours unchanged; Artboard clean active-tab close/Delete follow-up validated 2026-10-15; Curvature curve geometry, #1236 in part; Width-point finite-arithmetic follow-up validated; scores and hours unchanged; Image Trace adjustment discovery; later 2026-10-14 merges: macOS save sheet Format menu back to missing, #1855, #866 reopened; anchor X/Y/W/H in the Transform panel, Properties and Control bar, #1824; Snap to Grid over Snap to Pixel, #1816; toolbar labels, columns and the All Tools drawer, #1751; Actions panel selection, #1629 in part; scores unchanged; 2026-10-14 merges: anchor Align and Distribute, #1636; Delete on closed paths and whole objects, #1674; selection inside isolation, #1666; Escape in a Direct Selection drag and while typing, #1743, #1686; Pen clicks under 3 px, #1663; [ and ] on more tools, #1661 in part; fields commit on click away, #1787; command search keys, #1757; a guide's position in the Transform panel is a convenience with no parity claim, #1659; scores unchanged; intersect Smart Guides for drawn points and dragged anchors, #1602 in part; a ruler guide's position in the Transform panel, #1597 in part; Symbol Options before New Symbol, #1560; tool flyout widths, #1616; Pathfinder Trim, Merge and Crop strokes, #1570; scores unchanged; G1/M11: Artboard deletion and history selection normalization; history regressions and full workspace CI pass; scores and hours unchanged; Width Point Edit: Shift-double-click enables adjoining adjustment; plain double-click keeps it off; scores and hours unchanged; per-corner radius fields with a link toggle, split from #1594; Join merges anchors lying on one another, #1533; correction: Average and Join with a corner or smooth choice is missing, noLine truncated
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-21 · **Change:** minor (Panels dock into any dock group or the icon column by dragging (UI.6), workspaces keep their layout changes (UI.7); scores unchanged; Shape Builder Gap Detection shares Live Paint’s spatial gap finder and temporary merge bridges (#2526); Custom is capped at 72 pt in the existing options dialog; 3D: original Standard, Diffuse, Top Left and Right lighting presets; command access, effective-setting recognition and preview/history; scores and estimates unchanged; one selected clip group's Properties panel has Edit Clipping Path and Edit Contents at its top, G1; Distribute Objects and Distribute Spacing space from a key guide (#3165) with the Distribute Spacing value; Distribute spaces selected ruler guides with the objects, #1597 in part; the selection tools take a clipping path on its outline, over the art it clips; Polygon and Star exact-size placement remembers the chosen count for later drags and dialog openings; Cancel preserves the previous count; Live trace result bounds and selection highlights exclude the retained bitmap except in source-image views; a ruler guide can be the key object Align aligns to, and selected guides align to a key object, #2143; Pencil: Alt key toggles to Smooth Tool, #2001; status-readout sample widths cached by language, document unit and pixels per point; status messages and readouts use stable responsive allocations sized for localized labels and ordinary coordinates; cached canvas dimensions match the renderer limit before cache-key creation, view construction and worker submission; cached canvas reprojection preserves scale during resize, rotation and DPI changes; on-canvas Mirror Repeat axis, #2877; Background Export progress dialog with Cancel, #2564; 3D lighting: one to ten colored lights, smooth colored Inflate, legacy white-light appearance preserved; surface batching and restored seam minimum; scores and estimates unchanged; Blend spine Anchor Point conversions, selection/stacking priority and Shift regression coverage (#2544); scores and hours unchanged; Artboard Options' X and Y with a reference point, #1876; scores unchanged; library panels: one per library, dockable and tabbable, with Persistent, #2603; the Selection tool threads text from the out port, #2572 in part; scores unchanged; Group Selection picks and moves one path of a compound path, #2441 in part; the Move dialog opens on the last move and its last checkboxes; scores unchanged; transparency grid visibility saved in native documents, #1968; Live Corners radius up to the neighbouring cuts, #2332; Image Trace quick presets: wording corrected to the shipped behaviour, the Image Trace button opens the controls; batch exporting, #1935; document window scroll bars, #2322; Anchor Point tool: a click on a handle end retracts that handle only, #2267; scores unchanged; Smart Guides: the dragged end of a straight segment keeps to its extension at any angle, #2120; Warp Options controls verified by focused checks and original-image review; estimates unchanged; existing-guide Escape cancellation validated 2026-10-14; scores and hours unchanged; open-path anchor Delete keeps surviving runs separate; estimates unchanged; Artboard tab-switch follow-up historically validated on `a0671777`; scores and hours unchanged; Artboard clean active-tab close/Delete follow-up validated 2026-10-15; Curvature curve geometry, #1236 in part; Width-point finite-arithmetic follow-up validated; scores and hours unchanged; Image Trace adjustment discovery; later 2026-10-14 merges: macOS save sheet Format menu back to missing, #1855, #866 reopened; anchor X/Y/W/H in the Transform panel, Properties and Control bar, #1824; Snap to Grid over Snap to Pixel, #1816; toolbar labels, columns and the All Tools drawer, #1751; Actions panel selection, #1629 in part; scores unchanged; 2026-10-14 merges: anchor Align and Distribute, #1636; Delete on closed paths and whole objects, #1674; selection inside isolation, #1666; Escape in a Direct Selection drag and while typing, #1743, #1686; Pen clicks under 3 px, #1663; [ and ] on more tools, #1661 in part; fields commit on click away, #1787; command search keys, #1757; a guide's position in the Transform panel is a convenience with no parity claim, #1659; scores unchanged; intersect Smart Guides for drawn points and dragged anchors, #1602 in part; a ruler guide's position in the Transform panel, #1597 in part; Symbol Options before New Symbol, #1560; tool flyout widths, #1616; Pathfinder Trim, Merge and Crop strokes, #1570; scores unchanged; G1/M11: Artboard deletion and history selection normalization; history regressions and full workspace CI pass; scores and hours unchanged; Width Point Edit: Shift-double-click enables adjoining adjustment; plain double-click keeps it off; scores and hours unchanged; per-corner radius fields with a link toggle, split from #1594; Join merges anchors lying on one another, #1533; corLine truncated
|
||||
|
||||
How VectorCraft's tools, handles, snapping and panels behave next to Illustrator's. A vector illustrator's
|
||||
trust is in the details: where a handle lands, what Shift and Alt do mid-drag, what a smart guide snaps to,
|
||||
@@ -223,6 +223,7 @@ empty), without opening the dialog.
|
||||
| ~40 panels with ≡ menus; dock, icon column, collapse, tabs | done | 51 panel modules incl. tests |
|
||||
| Panels float inside the window, stack, dock back | done | #495 |
|
||||
| Floating groups stack into sets; sets dock as extra dock columns (a multi-column dock); groups drop anywhere in a set or column, or onto its tabs; a column taller than the window scrolls | done | `window.panel.float {below, above}`, `window.panel.dock {column}` |
|
||||
| Panels dragged onto a dock group's tabs join it; onto its upper or lower half they make a new group above or below it (a top/bottom split, resizable); onto the icon column they become icons, the tabbed group's panels too; a dock tab's context menu closes it (Window menu reopens it) | done | `window.panel.dock {anchor, zone}`, `window.panel.dock {icon: true}`, `window.panel.close` |
|
||||
| Panels outside the app window (another monitor) | missing | single OS window |
|
||||
| Edit Toolbar (•••): the All Tools drawer opens beside the toolbar by its button (Escape or a click outside closes it), dims the tools already in the toolbar; while it is open, tools drag from it onto the toolbar (between two tools, or onto one to join its group), from the toolbar into it, and within the toolbar; each toolbar keeps its edits | done | #1018; `window.editToolbar`, `toolbar.addTool`/`removeTool`/`reset`; the drawer's Reset (Illustrator has it in a drawer menu); dragging a toolbar button moves the tool it shows, not its whole group; New/Manage Toolbars missing; tool flyouts are as wide as their longest translated name and shortcut need (#1616) |
|
||||
| Quick Image Trace presets trace at once and leave the panel as it was; the Image Trace button (Control bar, Properties) opens the editable controls | done | Per the public documentation, choosing a preset traces the image with it straight away, and the panel is opened with the Image Trace button in the Control panel or in Properties' Image Trace section (<https://helpx.adobe.com/illustrator/using/image-trace-results-optimization.html>). The controls show Threshold, Paths, Corners and Noise with Advanced open; `panels/image_trace.rs` headless tests (`a_quick_trace_leaves_the_panel_as_it_was`, `sketch_quick_trace_reveals_all_adjustments_and_keeps_the_source` for the button's route). No algorithm parity claim. |
|
||||
@@ -280,6 +281,8 @@ sampling coexistence. Scores and estimates unchanged. See [development.md](devel
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-21 | minor | Workspaces keep the layout changes made in them when switching away and back, and across restarts; Reset <name> returns to the saved layout; score unchanged (estimated) |
|
||||
| 2026-10-21 | minor | Panels dragged by the mouse join a dock group as tabs, split it top/bottom or become icons in the icon column; dock tabs close from their context menu; score unchanged (estimated) |
|
||||
| 2026-10-20 | minor | Workspaces keep the layout changes made in them when switching away and back, and across restarts; Reset <name> returns to the saved layout; score unchanged (estimated) |
|
||||
| 2026-10-21 | minor | M3: highlighted-text Eyedropper sampling preserves ranges and rejects unsupported source/target transparency atomically; regression-tested; scores and estimates unchanged. |
|
||||
| 2026-10-21 | minor | Shape Builder Gap Detection shares Live Paint’s spatial gap finder and temporary merge bridges (#2526); Custom is capped at 72 pt in the existing options dialog. Scores and estimates unchanged. |
|
||||
|
||||
Reference in new issue
Block a user