mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Merge pull request #3280 from gorkematlyn/upstream-ui6
UI.6: dock panels into any dock group
This commit is contained in:
@@ -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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+4
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user