diff --git a/locales/ar-SA/opencadstudio.ftl b/locales/ar-SA/opencadstudio.ftl index 729a484b7..75ac31b28 100644 --- a/locales/ar-SA/opencadstudio.ftl +++ b/locales/ar-SA/opencadstudio.ftl @@ -6171,6 +6171,5 @@ dock = .dock-left = إرساء إلى اليسار .dock-right = إرساء إلى اليمين .new-group = مجموعة جديدة - .pallets = اللوحات .browser = المستعرض .node-graph = مخطط العقد diff --git a/locales/bg-BG/opencadstudio.ftl b/locales/bg-BG/opencadstudio.ftl index 59d517521..408ae4873 100644 --- a/locales/bg-BG/opencadstudio.ftl +++ b/locales/bg-BG/opencadstudio.ftl @@ -6159,6 +6159,5 @@ dock = .dock-left = Закачи вляво .dock-right = Закачи вдясно .new-group = Нова група - .pallets = Палитри .browser = Браузър .node-graph = Граф на възлите diff --git a/locales/cs-CZ/opencadstudio.ftl b/locales/cs-CZ/opencadstudio.ftl index a209e1049..2c1ca44bd 100644 --- a/locales/cs-CZ/opencadstudio.ftl +++ b/locales/cs-CZ/opencadstudio.ftl @@ -6156,6 +6156,5 @@ dock = .dock-left = Ukotvit vlevo .dock-right = Ukotvit vpravo .new-group = Nová skupina - .pallets = Palety .browser = Prohlížeč .node-graph = Graf uzlů diff --git a/locales/de-DE/opencadstudio.ftl b/locales/de-DE/opencadstudio.ftl index a697eca8b..cace0f53f 100644 --- a/locales/de-DE/opencadstudio.ftl +++ b/locales/de-DE/opencadstudio.ftl @@ -6157,6 +6157,5 @@ dock = .dock-left = Links andocken .dock-right = Rechts andocken .new-group = Neue Gruppe - .pallets = Paletten .browser = Browser .node-graph = Knotengraph diff --git a/locales/el-GR/opencadstudio.ftl b/locales/el-GR/opencadstudio.ftl index 82058463b..32480969e 100644 --- a/locales/el-GR/opencadstudio.ftl +++ b/locales/el-GR/opencadstudio.ftl @@ -6159,6 +6159,5 @@ dock = .dock-left = Πρόσδεση αριστερά .dock-right = Πρόσδεση δεξιά .new-group = Νέα ομάδα - .pallets = Παλέτες .browser = Περιηγητής .node-graph = Γράφος κόμβων diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index b1a57b60a..2cbfb7d96 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -6160,6 +6160,5 @@ dock = .dock-left = Dock left .dock-right = Dock right .new-group = New group - .pallets = Pallets .browser = Browser .node-graph = Node Graph diff --git a/locales/es-ES/opencadstudio.ftl b/locales/es-ES/opencadstudio.ftl index 4947517a1..429d931ce 100644 --- a/locales/es-ES/opencadstudio.ftl +++ b/locales/es-ES/opencadstudio.ftl @@ -6159,6 +6159,5 @@ dock = .dock-left = Acoplar a la izquierda .dock-right = Acoplar a la derecha .new-group = Nuevo grupo - .pallets = Paletas .browser = Explorador .node-graph = Grafo de nodos diff --git a/locales/fi-FI/opencadstudio.ftl b/locales/fi-FI/opencadstudio.ftl index 35de0cf29..3dae8fa03 100644 --- a/locales/fi-FI/opencadstudio.ftl +++ b/locales/fi-FI/opencadstudio.ftl @@ -6156,6 +6156,5 @@ dock = .dock-left = Telakoi vasemmalle .dock-right = Telakoi oikealle .new-group = Uusi ryhmä - .pallets = Paletit .browser = Selain .node-graph = Solmukaavio diff --git a/locales/fr-FR/opencadstudio.ftl b/locales/fr-FR/opencadstudio.ftl index de078bafb..9e6c00967 100644 --- a/locales/fr-FR/opencadstudio.ftl +++ b/locales/fr-FR/opencadstudio.ftl @@ -6158,6 +6158,5 @@ dock = .dock-left = Ancrer à gauche .dock-right = Ancrer à droite .new-group = Nouveau groupe - .pallets = Palettes .browser = Navigateur .node-graph = Graphe de nœuds diff --git a/locales/hi-IN/opencadstudio.ftl b/locales/hi-IN/opencadstudio.ftl index cdd91b104..f825d5103 100644 --- a/locales/hi-IN/opencadstudio.ftl +++ b/locales/hi-IN/opencadstudio.ftl @@ -6147,6 +6147,5 @@ dock = .dock-left = बाएँ डॉक करें .dock-right = दाएँ डॉक करें .new-group = नया समूह - .pallets = पैलेट .browser = ब्राउज़र .node-graph = नोड ग्राफ़ diff --git a/locales/hu-HU/opencadstudio.ftl b/locales/hu-HU/opencadstudio.ftl index a1a260f5c..6c00d4e97 100644 --- a/locales/hu-HU/opencadstudio.ftl +++ b/locales/hu-HU/opencadstudio.ftl @@ -6158,6 +6158,5 @@ dock = .dock-left = Dokkolás balra .dock-right = Dokkolás jobbra .new-group = Új csoport - .pallets = Paletták .browser = Böngésző .node-graph = Csomópontgráf diff --git a/locales/it-IT/opencadstudio.ftl b/locales/it-IT/opencadstudio.ftl index 2e36787be..af2b1d7c4 100644 --- a/locales/it-IT/opencadstudio.ftl +++ b/locales/it-IT/opencadstudio.ftl @@ -6158,6 +6158,5 @@ dock = .dock-left = Ancora a sinistra .dock-right = Ancora a destra .new-group = Nuovo gruppo - .pallets = Tavolozze .browser = Browser .node-graph = Grafo dei nodi diff --git a/locales/ja-JP/opencadstudio.ftl b/locales/ja-JP/opencadstudio.ftl index b0ea367e4..ff7fdc061 100644 --- a/locales/ja-JP/opencadstudio.ftl +++ b/locales/ja-JP/opencadstudio.ftl @@ -6147,6 +6147,5 @@ dock = .dock-left = 左にドッキング .dock-right = 右にドッキング .new-group = 新しいグループ - .pallets = パレット .browser = ブラウザー .node-graph = ノードグラフ diff --git a/locales/ko-KR/opencadstudio.ftl b/locales/ko-KR/opencadstudio.ftl index 067ddd8f2..d270ca2e6 100644 --- a/locales/ko-KR/opencadstudio.ftl +++ b/locales/ko-KR/opencadstudio.ftl @@ -6158,6 +6158,5 @@ dock = .dock-left = 왼쪽에 도킹 .dock-right = 오른쪽에 도킹 .new-group = 새 그룹 - .pallets = 팔레트 .browser = 브라우저 .node-graph = 노드 그래프 diff --git a/locales/nl-NL/opencadstudio.ftl b/locales/nl-NL/opencadstudio.ftl index 08632e9c1..b485167b3 100644 --- a/locales/nl-NL/opencadstudio.ftl +++ b/locales/nl-NL/opencadstudio.ftl @@ -6157,6 +6157,5 @@ dock = .dock-left = Links koppelen .dock-right = Rechts koppelen .new-group = Nieuwe groep - .pallets = Paletten .browser = Browser .node-graph = Knooppuntgrafiek diff --git a/locales/pl-PL/opencadstudio.ftl b/locales/pl-PL/opencadstudio.ftl index 0d0319030..092e1134d 100644 --- a/locales/pl-PL/opencadstudio.ftl +++ b/locales/pl-PL/opencadstudio.ftl @@ -6158,6 +6158,5 @@ dock = .dock-left = Zadokuj po lewej .dock-right = Zadokuj po prawej .new-group = Nowa grupa - .pallets = Palety .browser = Przeglądarka .node-graph = Graf węzłów diff --git a/locales/pt-BR/opencadstudio.ftl b/locales/pt-BR/opencadstudio.ftl index 550f62bf6..8f7d24909 100644 --- a/locales/pt-BR/opencadstudio.ftl +++ b/locales/pt-BR/opencadstudio.ftl @@ -6159,6 +6159,5 @@ dock = .dock-left = Encaixar à esquerda .dock-right = Encaixar à direita .new-group = Novo grupo - .pallets = Paletas .browser = Navegador .node-graph = Grafo de nós diff --git a/locales/ru-RU/opencadstudio.ftl b/locales/ru-RU/opencadstudio.ftl index db6cadab3..9e522dff5 100644 --- a/locales/ru-RU/opencadstudio.ftl +++ b/locales/ru-RU/opencadstudio.ftl @@ -6160,6 +6160,5 @@ dock = .dock-left = Закрепить слева .dock-right = Закрепить справа .new-group = Новая группа - .pallets = Палитры .browser = Обозреватель .node-graph = Граф узлов diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl index 266869dfa..1423b863b 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -6082,6 +6082,5 @@ dock = .dock-left = Sola yerleştir .dock-right = Sağa yerleştir .new-group = Yeni grup - .pallets = Paletler .browser = Tarayıcı .node-graph = Düğüm Grafiği diff --git a/locales/zh-CN/opencadstudio.ftl b/locales/zh-CN/opencadstudio.ftl index 2b2118aac..33f5a53ee 100644 --- a/locales/zh-CN/opencadstudio.ftl +++ b/locales/zh-CN/opencadstudio.ftl @@ -6142,6 +6142,5 @@ dock = .dock-left = 停靠到左侧 .dock-right = 停靠到右侧 .new-group = 新建组 - .pallets = 选项板 .browser = 浏览器 .node-graph = 节点图 diff --git a/locales/zh-TW/opencadstudio.ftl b/locales/zh-TW/opencadstudio.ftl index fb5d8d65f..95d417cd5 100644 --- a/locales/zh-TW/opencadstudio.ftl +++ b/locales/zh-TW/opencadstudio.ftl @@ -6154,6 +6154,5 @@ dock = .dock-left = 停駐到左側 .dock-right = 停駐到右側 .new-group = 新增群組 - .pallets = 選項板 .browser = 瀏覽器 .node-graph = 節點圖 diff --git a/src/app/dock/tests.rs b/src/app/dock/tests.rs index 8c84ac664..3f97ec975 100644 --- a/src/app/dock/tests.rs +++ b/src/app/dock/tests.rs @@ -205,7 +205,7 @@ fn dock_drop_over_the_viewport_floats_the_panel() { ); assert!(matches!(target, Some(DropTarget::Float { .. }))); assert!(app.dock.right.is_empty()); - let f = app.dock.float_rect(PanelId::BlockPalette).expect("floating").clone(); + let f = app.dock.float_rect(PanelId::BlockPalette).expect("floating"); assert!(f.x > 600.0 && f.x < 800.0 && f.y > 350.0 && f.y < 400.0); // Dragging a floating panel keeps the grab point under the pointer. let target = drag_panel( @@ -275,53 +275,14 @@ fn dock_grip_drag_moves_a_whole_group() { app.dock.left[1].panels, vec![PanelId::Properties, PanelId::Browser] ); - // A group dropped over the viewport floats as one window. + // A group dropped over the viewport does not float or split up. + let before = app.dock.clone(); let _ = app.on_dock(DockMsg::GroupGrab(DockSide::Left, 1)); let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(18.0, 300.0))); let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(800.0, 400.0))); + assert_eq!(drag_target(&app), None); let _ = app.on_dock(DockMsg::DragRelease); - assert_eq!(app.dock.left.len(), 1); - let f = app.dock.float_rect(PanelId::Browser).expect("floats"); - assert_eq!(f.group.panels, vec![PanelId::Properties, PanelId::Browser]); - // Dragging the window's title bar onto the strip docks it whole again. - let _ = app.on_dock(DockMsg::FloatGrab(PanelId::Properties)); - let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(800.0, 400.0))); - let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(18.0, plus_top + 10.0))); - let _ = app.on_dock(DockMsg::DragRelease); - assert!(app.dock.floating.is_empty()); - assert_eq!( - app.dock.left[1].panels, - vec![PanelId::Properties, PanelId::Browser] - ); -} - -#[test] -fn floating_menu_adds_pallets_and_docks_the_window() { - use crate::app::config::DockSide; - use crate::ui::dock::{DockMsg, PanelId}; - let mut app = dock_app(); - let _ = app.on_dock(DockMsg::FloatOut(PanelId::BlockPalette)); - let _ = app.on_dock(DockMsg::FloatMenu(Some(PanelId::BlockPalette))); - let _ = app.on_dock(DockMsg::FloatMenuPallets(true)); - // Picking a pallet adds it to the window, opens it and closes the menu. - let _ = app.on_dock(DockMsg::FloatMenuToggle(PanelId::BlockPalette, PanelId::Browser)); - assert!(app.show_browser); - assert_eq!(app.dock_float_menu, None); - let f = app.dock.float_rect(PanelId::BlockPalette).unwrap(); - assert_eq!(f.group.panels, vec![PanelId::BlockPalette, PanelId::Browser]); - // Picking a checked one hides it but keeps its place. - let _ = app.on_dock(DockMsg::FloatMenuToggle(PanelId::BlockPalette, PanelId::Browser)); - assert!(!app.show_browser); - assert!(app.dock.float_rect(PanelId::Browser).is_some()); - // Dock left takes the whole window to the left edge. - let _ = app.on_dock(DockMsg::DockTo(PanelId::BlockPalette, DockSide::Left)); - assert!(app.dock.floating.is_empty()); - let (side, gi) = app.dock.location(PanelId::Browser).unwrap(); - assert_eq!(side, DockSide::Left); - assert_eq!( - app.dock.groups(side)[gi].panels, - vec![PanelId::BlockPalette, PanelId::Browser] - ); + assert_eq!(app.dock, before); } #[test] @@ -405,7 +366,7 @@ fn dock_float_resize_grows_the_panel() { let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(360.0, 400.0))); let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(400.0, 450.0))); let _ = app.on_dock(DockMsg::DragRelease); - let f = app.dock.float_rect(PanelId::BlockPalette).unwrap().clone(); + let f = app.dock.float_rect(PanelId::BlockPalette).unwrap(); assert_eq!((f.w, f.h), (300.0, 350.0)); // The bottom-left grip grows the panel leftward, keeping its right edge. let _ = app.on_dock(DockMsg::FloatResizeGrab(PanelId::BlockPalette, true)); @@ -630,18 +591,27 @@ fn blockpalette_width_reset() { } #[test] -fn a_window_without_allow_docking_only_floats_when_dragged() { +fn floating_menu_docks_and_allow_docking_keeps_a_drag_floating() { + use crate::app::config::DockSide; use crate::ui::dock::{DockMsg, DropTarget, PanelId}; let mut app = dock_app(); let _ = app.on_dock(DockMsg::FloatOut(PanelId::BlockPalette)); + // With Allow docking off, a drag onto the left edge's strip stays + // floating. + let _ = app.on_dock(DockMsg::FloatMenu(Some(PanelId::BlockPalette))); let _ = app.on_dock(DockMsg::FloatDockingToggle(PanelId::BlockPalette)); + assert_eq!(app.dock_float_menu, None); assert!(!app.dock.float_rect(PanelId::BlockPalette).unwrap().docking); - // Dragged onto the left edge's strip, it stays a floating window. - let _ = app.on_dock(DockMsg::FloatGrab(PanelId::BlockPalette)); + let _ = app.on_dock(DockMsg::DockGrab(PanelId::BlockPalette)); let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(500.0, 300.0))); let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(10.0, 60.0))); assert!(matches!(drag_target(&app), Some(DropTarget::Float { .. }))); let _ = app.on_dock(DockMsg::DragRelease); - assert!(app.dock.float_rect(PanelId::BlockPalette).is_some()); - assert_eq!(app.dock.location(PanelId::BlockPalette), None); + let f = app.dock.float_rect(PanelId::BlockPalette).expect("still floating"); + assert!(!f.docking, "moving it keeps the choice"); + // The menu's Dock left still docks it. + let _ = app.on_dock(DockMsg::FloatMenu(Some(PanelId::BlockPalette))); + let _ = app.on_dock(DockMsg::DockTo(PanelId::BlockPalette, DockSide::Left)); + assert_eq!(app.dock_float_menu, None); + assert_eq!(app.dock.location(PanelId::BlockPalette).map(|(s, _)| s), Some(DockSide::Left)); } diff --git a/src/app/dock/update.rs b/src/app/dock/update.rs index 9601846fd..9f0c6eec2 100644 --- a/src/app/dock/update.rs +++ b/src/app/dock/update.rs @@ -102,60 +102,20 @@ impl OpenCADStudio { self.save_config(); } } - DockMsg::GroupFloat(side, gi) => { - // Float beside the edge it was docked on, near the top. - let (ww, _) = self.dock_workspace_size(); - let w = self.dock_group_width(side, gi); - let strip = crate::ui::dock::DOCK_STRIP_W; - let x = match side { - DockSide::Right => ww - strip - self.dock_column_width(side) - w - 60.0, - DockSide::Left => strip + self.dock_column_width(side) + 40.0, - }; - self.dock_drag = None; - let h = self.dock_default_float_h(); - if self.dock.float_group(side, gi, x.max(0.0), 40.0, w, h) { - self.save_config(); - } - } DockMsg::DockTo(id, side) => { self.dock_drag = None; self.dock_float_menu = None; - if self.dock.dock_window(id, side, usize::MAX) { + if self.dock.dock(id, side, usize::MAX) { self.save_config(); } } - DockMsg::FloatGrab(id) => self.dock_begin(DockDrag::moving_window(id), Some(id)), - DockMsg::FloatClose(id) => { - let panels = self - .dock - .float_rect(id) - .map(|f| f.group.panels.clone()) - .unwrap_or_default(); - let mut tasks = Vec::new(); - for p in panels { - if self.dock_panel_visible(p) { - tasks.push(self.dock_set_open(p, false)); - } - } - return iced::Task::batch(tasks); - } - DockMsg::FloatSplitGrab(panel, upper, lower) => { - self.dock.raise_float(panel); - self.dock_begin(DockDrag::FloatSplit { panel, upper, lower }, None); - } - DockMsg::FloatSplitReset(id) => { - self.dock.reset_float_splits(id); - self.save_config(); - } DockMsg::FloatMenu(id) => { self.dock_float_menu = id; self.dock_float_menu_at = self.dock_float_pointer; - self.dock_float_menu_pallets = false; if let Some(id) = id { self.dock.raise_float(id); } } - DockMsg::FloatMenuPallets(open) => self.dock_float_menu_pallets = open, DockMsg::FloatBarPointer(p) => self.dock_float_pointer = p, DockMsg::FloatDockingToggle(id) => { self.dock_float_menu = None; @@ -163,22 +123,6 @@ impl OpenCADStudio { self.save_config(); } } - DockMsg::FloatMenuToggle(window, id) => { - // Checked = open in this window: unchecking hides it. - // Anything else (closed, docked, in another window) moves - // into the window and opens; adding ends the menu's job. - let here = self.dock.float_rect(window).is_some_and(|f| f.contains(id)); - if here && self.dock_panel_visible(id) { - let task = self.dock_set_open(id, false); - self.save_config(); - return task; - } - self.dock.add_to_float(id, window); - self.dock_float_menu = None; - let task = self.dock_set_open(id, true); - self.save_config(); - return task; - } DockMsg::EdgeMenu(side) => self.dock_edge_menu = side, DockMsg::EdgeMenuToggle(side, id) => { // Checked = open and docked on this edge: unchecking hides it. @@ -259,31 +203,6 @@ impl OpenCADStudio { target: Some(DropTarget::Edge { side: to, index }), .. }) => changed = self.dock.move_group(side, gi, to, index), - Some(DockDrag::Move { - group: Some((side, gi)), - target: Some(DropTarget::Float { x, y }), - .. - }) => { - let w = self.dock_group_width(side, gi); - let h = self.dock_default_float_h(); - changed = self.dock.float_group(side, gi, x, y, w, h); - } - Some(DockDrag::Move { - panel, - window: true, - target: Some(target), - .. - }) => { - changed = match target { - DropTarget::Edge { side, index } => { - self.dock.dock_window(panel, side, index) - } - DropTarget::Join { side, group, index } => { - self.dock.join_window(panel, side, group, index) - } - DropTarget::Float { x, y } => self.dock.move_float(panel, x, y), - }; - } Some(DockDrag::Move { panel, group: None, @@ -297,8 +216,7 @@ impl OpenCADStudio { } DropTarget::Float { x, y } => { let (w, h) = self.dock_float_size(panel); - // Pulled out of a window, it keeps the - // window's docking choice. + // A floating panel keeps its docking choice. let docking = self.dock.float_rect(panel).is_none_or(|f| f.docking); self.dock.float(FloatPanel { docking, @@ -308,12 +226,7 @@ impl OpenCADStudio { }; } Some(DockDrag::Move { .. } | DockDrag::LayerColumn) | None => {} - Some( - DockDrag::Width(_) - | DockDrag::Split { .. } - | DockDrag::FloatSize { .. } - | DockDrag::FloatSplit { .. }, - ) => { + Some(DockDrag::Width(_) | DockDrag::Split { .. } | DockDrag::FloatSize { .. }) => { changed = true; } } @@ -346,7 +259,6 @@ impl OpenCADStudio { DockDrag::Move { panel, group, - window, origin, target, grab, @@ -354,12 +266,10 @@ impl OpenCADStudio { let origin = origin.unwrap_or(point); let moved = (point.x - origin.x).hypot(point.y - origin.y); let started = target.is_some() || moved >= DOCK_DRAG_THRESHOLD; - // Hold a floating window where it was grabbed; a docked - // pallet (or one pulled out of a floating group) by its - // title bar. + // Hold a floating panel where it was grabbed; a docked one by + // its title bar. let grab = if target.is_none() && started { - let whole = |f: &&crate::ui::dock::FloatPanel| window || f.group.panels.len() == 1; - match self.dock.float_rect(panel).filter(whole) { + match self.dock.float_rect(panel) { Some(f) => iced::Vector::new(origin.x - f.x, origin.y - f.y), None => iced::Vector::new( (self.dock.width(panel, self.win_size.0) * 0.5).min(120.0), @@ -370,20 +280,16 @@ impl OpenCADStudio { grab }; let target = if started { - // A window that may not dock (or a pallet pulled out of - // one) only floats. - let floats_only = group.is_none() - && self.dock.float_rect(panel).is_some_and(|f| !f.docking); - let found = if floats_only { - self.dock_float_target(point, grab) - } else { - self.dock_drop_target(point, grab) + // A whole group only moves between group positions: it + // cannot join another group or float. + // A floating panel that may not dock only floats. + let found = match self.dock.float_rect(panel) { + Some(f) if !f.docking => self.dock_float_target(point, grab), + _ => self.dock_drop_target(point, grab), }; - // A whole docked group moves between group positions or - // floats; it cannot join another group. match (group, found) { - (Some(_), DropTarget::Join { .. }) => None, - (_, found) => Some(found), + (Some(_), found @ DropTarget::Edge { .. }) | (None, found) => Some(found), + (Some(_), _) => None, } } else { target @@ -391,7 +297,6 @@ impl OpenCADStudio { self.dock_drag = Some(DockDrag::Move { panel, group, - window, origin: Some(origin), target, grab, @@ -430,30 +335,12 @@ impl OpenCADStudio { return; }; let dx = point.x - last.x; - let (w, h) = (if from_left { f.w - dx } else { f.w + dx }, f.h + point.y - last.y); - self.dock.resize_float(panel, w, h, from_left); - } - DockDrag::FloatSplit { panel, upper, lower } => { - let (Some(last), Some(f)) = (last, self.dock.float_rect(panel)) else { - return; - }; - let total: f32 = self - .dock_float_visible(f) - .iter() - .filter_map(|p| f.group.panels.iter().position(|q| q == p)) - .filter_map(|i| f.group.weights.get(i)) - .sum(); - let avail = f.h; - if avail > 0.0 && total > 0.0 { - let per_px = total / avail; - self.dock.shift_float_split( - panel, - upper, - lower, - (point.y - last.y) * per_px, - crate::ui::dock::GROUP_MIN_H * per_px, - ); - } + self.dock.resize_float( + panel, + if from_left { f.w - dx } else { f.w + dx }, + f.h + point.y - last.y, + from_left, + ); } DockDrag::LayerColumn => { let Some(last) = last else { return }; @@ -494,6 +381,8 @@ impl OpenCADStudio { } } + /// Bring `id` into view: show its group on its edge and, where it + /// auto-hides, keep it revealed until the pointer leaves. /// Arm a delayed reveal (`Some`) or hide (`None`); it applies when its /// timer reports back unless a newer hover superseded it. fn dock_hover_arm(&mut self, target: Option) -> iced::Task { @@ -511,8 +400,6 @@ impl OpenCADStudio { self.dock_hover_pending = None; } - /// Bring `id` into view: show its group on its edge and, where it - /// auto-hides, keep it revealed until the pointer leaves. pub(crate) fn dock_reveal(&mut self, id: crate::ui::dock::PanelId) { self.dock.show_group_of(id); self.dock_peek = Some(id); @@ -764,37 +651,11 @@ impl OpenCADStudio { if let Some(f) = self.dock.float_rect(id) { return (f.w, f.h); } - (self.dock.width(id, self.win_size.0), self.dock_default_float_h()) - } - - /// Height a pallet or group floats at first: a comfortable share of the - /// workspace height. - pub(crate) fn dock_default_float_h(&self) -> f32 { let (_, avail) = self.dock_workspace_size(); - (avail * 0.6).clamp(crate::ui::dock::FLOAT_MIN_H, 520.0) - } - - /// The pallets floating window `f` draws, top → bottom: its open ones, - /// less one being dragged out of it. - pub(crate) fn dock_float_visible( - &self, - f: &crate::ui::dock::FloatPanel, - ) -> Vec { - let pulled = match self.dock_drag { - Some(crate::ui::dock::DockDrag::Move { - panel, - window: false, - target: Some(_), - .. - }) => Some(panel), - _ => None, - }; - f.group - .panels - .iter() - .copied() - .filter(|p| self.dock_panel_visible(*p) && Some(*p) != pulled) - .collect() + ( + self.dock.width(id, self.win_size.0), + (avail * 0.6).clamp(crate::ui::dock::FLOAT_MIN_H, 520.0), + ) } /// Where a dragged pallet lands if released at workspace point `p`: diff --git a/src/app/dock/view.rs b/src/app/dock/view.rs index bea980338..91ba479ca 100644 --- a/src/app/dock/view.rs +++ b/src/app/dock/view.rs @@ -17,19 +17,16 @@ impl OpenCADStudio { workspace: Element<'a, Message>, tab: &'a DocumentTab, ) -> Element<'a, Message> { - use crate::ui::dock::DockMsg; // Floating panels sit over the workspace, back to front. let mut layers: Vec> = vec![workspace]; for f in &self.dock.floating { - // Once a window drag is under way only its preview shows it. - let dragged = matches!( - self.dock_drag, - Some(crate::ui::dock::DockDrag::Move { panel, window: true, target: Some(_), .. }) - if f.contains(panel) - ); - let visible = self.dock_float_visible(f); - if !visible.is_empty() && !dragged { - layers.push(self.floating_panel(f, visible, tab)); + // Once a drag is under way only its preview shows the panel. + let dragged = self + .dock_drag + .and_then(|d| d.movement()) + .is_some_and(|(panel, _, target)| panel == f.id && target.is_some()); + if self.dock_panel_visible(f.id) && !dragged { + layers.push(self.floating_panel(*f, tab)); } } // An open pallet menu hangs beside its edge's + button, over a @@ -54,19 +51,20 @@ impl OpenCADStudio { }; layers.push( mouse_area(Space::new().width(Fill).height(Fill)) - .on_press(Message::Dock(DockMsg::EdgeMenu(None))) - .on_right_press(Message::Dock(DockMsg::EdgeMenu(None))) + .on_press(Message::Dock(crate::ui::dock::DockMsg::EdgeMenu(None))) + .on_right_press(Message::Dock(crate::ui::dock::DockMsg::EdgeMenu(None))) .into(), ); - let toggle = move |id| Message::Dock(DockMsg::EdgeMenuToggle(side, id)); - layers.push(place_at(dock_pallet_menu(&checked, toggle), x.max(0.0), plus_top)); + layers.push(place_at(dock_edge_menu(side, &checked), x.max(0.0), plus_top)); } } + // An open floating title-bar menu, over its own click catcher. if let Some(menu) = self.dock_float_menu_overlay() { + let close = Message::Dock(crate::ui::dock::DockMsg::FloatMenu(None)); layers.push( mouse_area(Space::new().width(Fill).height(Fill)) - .on_press(Message::Dock(DockMsg::FloatMenu(None))) - .on_right_press(Message::Dock(DockMsg::FloatMenu(None))) + .on_press(close.clone()) + .on_right_press(close) .into(), ); layers.push(menu); @@ -288,7 +286,6 @@ impl OpenCADStudio { }), ) .on_press(Message::Dock(DockMsg::GroupGrab(side, gi))) - .on_double_click(Message::Dock(DockMsg::GroupFloat(side, gi))) .on_enter(Message::Dock(DockMsg::GripHover(side, gi))) .on_exit(Message::Dock(DockMsg::GripHoverEnd(side, gi))) .interaction(iced::mouse::Interaction::Grab); @@ -437,46 +434,19 @@ impl OpenCADStudio { } } - /// Where floating window `f` draws: its top-left corner, whether its - /// title bar is on the left, and whether it is hidden down to that bar - /// (auto-hide, not hovered). - fn dock_float_frame(&self, f: &crate::ui::dock::FloatPanel) -> (f32, f32, bool, bool) { - let (ww, wh) = self.dock_workspace_size(); - // The bar faces the nearer workspace edge; while resizing, it stays - // opposite the grip being dragged so it cannot flip mid-drag. - let bar_left = match self.dock_drag { - Some(crate::ui::dock::DockDrag::FloatSize { panel, from_left }) if f.contains(panel) => { - !from_left - } - _ => f.x + (f.w + DOCK_FLOAT_BAR_W) * 0.5 < ww * 0.5, - }; - let hidden = self.dock.auto_hides(f.id()) && !self.dock_peek.is_some_and(|p| f.contains(p)); - let total_w = if hidden { DOCK_FLOAT_BAR_W } else { f.w + DOCK_FLOAT_BAR_W }; - // A hidden window keeps its title bar where it sits when shown. - let x = if hidden && !bar_left { f.x + f.w } else { f.x }; - // Keep the window on screen when the workspace shrank since it was - // placed. - let x = x.min(ww - total_w).max(0.0); - let y = f.y.min(wh - f.h).max(0.0); - (x, y, bar_left, hidden) - } - - /// A floating window at its saved place: a vertical title bar on the - /// side facing the nearer workspace edge, its open pallets (`visible`) - /// stacked beside it, and a corner grip to resize it. With its pin on, - /// the window hides down to the title bar until hovered. + /// A floating panel at its saved place: a vertical title bar on the side + /// facing the nearer workspace edge, the panel beside it, and a corner + /// grip to resize it. With its pin on, the panel hides down to the title + /// bar until hovered. fn floating_panel<'a>( &'a self, - f: &'a crate::ui::dock::FloatPanel, - visible: Vec, + f: crate::ui::dock::FloatPanel, tab: &'a DocumentTab, ) -> Element<'a, Message> { use crate::ui::dock::DockMsg; let (x, y, bar_left, hidden) = self.dock_float_frame(f); - // Messages name the window by its first open pallet. - let anchor = visible[0]; - let hovered = self.dock_title_hover.is_some_and(|p| f.contains(p)); - let bar = floating_title_bar(&visible, self.dock.auto_hides(anchor), bar_left, hovered); + let hovered = self.dock_title_hover == Some(f.id); + let bar = floating_title_bar(f.id, self.dock.auto_collapse(f.id), bar_left, hovered); let content: Element<'_, Message> = if hidden { bar } else { @@ -495,31 +465,14 @@ impl OpenCADStudio { .center_x(Length::Fixed(14.0)) .center_y(Length::Fixed(14.0)), ) - .on_press(Message::Dock(DockMsg::FloatResizeGrab(anchor, grip_left))) + .on_press(Message::Dock(DockMsg::FloatResizeGrab(f.id, grip_left))) .interaction(if grip_left { iced::mouse::Interaction::ResizingDiagonallyUp } else { iced::mouse::Interaction::ResizingDiagonallyDown }); - // The pallets stack like a docked group, with splitters between. - let mut pallets: Vec> = Vec::new(); - let mut prev: Option = None; - for id in &visible { - let pos = f.group.panels.iter().position(|p| p == id).unwrap_or(0); - if let Some(upper) = prev { - pallets.push(float_splitter(anchor, upper, pos, f.w)); - } - let weight = f.group.weights.get(pos).copied().unwrap_or(1.0); - pallets.push( - container(self.panel_body(*id, f.w, true, tab)) - .height(Length::FillPortion(crate::ui::dock::portion(weight))) - .clip(true) - .into(), - ); - prev = Some(pos); - } let body = stack![ - column(pallets).height(Fill), + self.panel_body(f.id, f.w, true, tab), container(grip) .width(Fill) .height(Fill) @@ -538,8 +491,8 @@ impl OpenCADStudio { row![body, bar].into() } }; - // Clipped, so a pallet squeezed below its content's height never - // draws outside the window. + // Clipped, so a panel squeezed below its content's height never + // draws outside its frame. let framed = container(content) .height(Length::Fixed(f.h)) .clip(true) @@ -556,25 +509,48 @@ impl OpenCADStudio { }, ..Default::default() }); - // The idle cursor marks the window as opaque to the pointer, so - // clicks on its empty areas don't fall through to the drawing below. + // The idle cursor marks the panel as opaque to the pointer, so clicks + // on its empty areas don't fall through to the drawing below. let panel = mouse_area(framed) - .on_press(Message::Dock(DockMsg::FloatRaise(anchor))) - .on_enter(Message::Dock(DockMsg::Hover(anchor))) + .on_press(Message::Dock(DockMsg::FloatRaise(f.id))) + .on_enter(Message::Dock(DockMsg::Hover(f.id))) .on_exit(Message::Dock(DockMsg::HoverExit)) .interaction(iced::mouse::Interaction::Idle); place_at(panel.into(), x, y) } - /// The open right-click menu of a floating window: Pallets (a submenu - /// to add or hide pallets in the window), Allow docking, Dock left and - /// Dock right. It opens at the pointer, flipped to stay on screen. + /// Where floating panel `f` draws: its top-left corner, whether its + /// title bar is on the left, and whether it is hidden down to that bar + /// (auto-hide, not hovered). + fn dock_float_frame(&self, f: crate::ui::dock::FloatPanel) -> (f32, f32, bool, bool) { + let (ww, wh) = self.dock_workspace_size(); + // The bar faces the nearer workspace edge; while resizing, it stays + // opposite the grip being dragged so it cannot flip mid-drag. + let bar_left = match self.dock_drag { + Some(crate::ui::dock::DockDrag::FloatSize { panel, from_left }) if panel == f.id => { + !from_left + } + _ => f.x + (f.w + DOCK_FLOAT_BAR_W) * 0.5 < ww * 0.5, + }; + let hidden = self.dock.auto_collapse(f.id) && self.dock_peek != Some(f.id); + let total_w = if hidden { DOCK_FLOAT_BAR_W } else { f.w + DOCK_FLOAT_BAR_W }; + // A hidden panel keeps its title bar where it sits when shown. + let x = if hidden && !bar_left { f.x + f.w } else { f.x }; + // Keep the panel on screen when the window shrank since it was placed. + let x = x.min(ww - total_w).max(0.0); + let y = f.y.min(wh - f.h).max(0.0); + (x, y, bar_left, hidden) + } + + /// The open right-click menu of a floating title bar: Allow docking, + /// Dock left and Dock right. It opens at the pointer, flipped to stay on + /// screen. fn dock_float_menu_overlay(&self) -> Option> { use crate::app::config::DockSide; - use crate::ui::dock::{DockMsg, PanelId}; - let anchor = self.dock_float_menu?; - let f = self.dock.float_rect(anchor)?; - if self.dock_float_visible(f).is_empty() { + use crate::ui::dock::DockMsg; + let id = self.dock_float_menu?; + let f = self.dock.float_rect(id)?; + if !self.dock_panel_visible(id) { return None; } let (ww, wh) = self.dock_workspace_size(); @@ -583,35 +559,17 @@ impl OpenCADStudio { let bar_x = if bar_left { x } else { x + total_w - DOCK_FLOAT_BAR_W }; // Where the right-click happened, in the workspace. let at = iced::Point::new(bar_x + self.dock_float_menu_at.x, y + self.dock_float_menu_at.y); - let max_x = (ww - DOCK_MENU_W).max(0.0); let menu_x = if at.x + DOCK_MENU_W <= ww { at.x } else { at.x - DOCK_MENU_W }; - let menu_x = menu_x.clamp(0.0, max_x); + let menu_x = menu_x.clamp(0.0, (ww - DOCK_MENU_W).max(0.0)); let menu_y = at.y.min(wh - FLOAT_MENU_H).max(0.0); - let open = self.dock_float_menu_pallets; - // Hovering a row opens (Pallets) or closes (the others) the submenu. - let item = |label: Element<'static, Message>, press: DockMsg, sub: bool| -> Element<'static, Message> { - mouse_area( - button(label) - .on_press(Message::Dock(press)) - .style(menu_row_style) - .padding([5, 8]) - .width(Fill), - ) - .on_enter(Message::Dock(DockMsg::FloatMenuPallets(sub))) - .into() + let item = |label: Element<'static, Message>, press: DockMsg| -> Element<'static, Message> { + button(label) + .on_press(Message::Dock(press)) + .style(menu_row_style) + .padding([5, 8]) + .width(Fill) + .into() }; - let pallets = row![ - text(t!("Pallets")).size(12).width(Fill), - text("\u{203A}").size(13), - ] - .align_y(iced::Center); - let separator = container(Space::new()) - .width(Fill) - .height(Length::Fixed(1.0)) - .style(|theme: &Theme| container::Style { - background: Some(Background::Color(theme.palette().background.neutral.color)), - ..Default::default() - }); let check: Element<'static, Message> = if f.docking { crate::ui::icons::themed(crate::ui::icons::CHECK, 12.0) } else { @@ -619,46 +577,22 @@ impl OpenCADStudio { }; let docking = row![text(t!("Allow docking")).size(12).width(Fill), check] .align_y(iced::Center); - let main = column![ - item(pallets.into(), DockMsg::FloatMenuPallets(!open), true), + let separator = container(Space::new()) + .width(Fill) + .height(Length::Fixed(1.0)) + .style(|theme: &Theme| container::Style { + background: Some(Background::Color(theme.palette().background.neutral.color)), + ..Default::default() + }); + let menu = column![ + item(docking.into(), DockMsg::FloatDockingToggle(id)), container(separator).padding([3, 4]), - item(docking.into(), DockMsg::FloatDockingToggle(anchor), false), - item( - text(t!("Dock left")).size(12).into(), - DockMsg::DockTo(anchor, DockSide::Left), - false - ), - item( - text(t!("Dock right")).size(12).into(), - DockMsg::DockTo(anchor, DockSide::Right), - false - ), + item(text(t!("Dock left")).size(12).into(), DockMsg::DockTo(id, DockSide::Left)), + item(text(t!("Dock right")).size(12).into(), DockMsg::DockTo(id, DockSide::Right)), ] .spacing(1) .padding(4); - let mut layers = vec![place_at(menu_frame(main.into()), menu_x, menu_y)]; - if open { - let checked: Vec<_> = PanelId::ALL - .iter() - .map(|p| (*p, f.contains(*p) && self.dock_panel_visible(*p))) - .collect(); - let toggle = move |id| Message::Dock(DockMsg::FloatMenuToggle(anchor, id)); - // To the right of the menu, or its left when there is no room. - let sub_x = if menu_x + 2.0 * DOCK_MENU_W + 2.0 <= ww { - menu_x + DOCK_MENU_W + 2.0 - } else { - menu_x - DOCK_MENU_W - 2.0 - } - .clamp(0.0, max_x); - let sub_y = menu_y.min(wh - PALLET_MENU_H).max(0.0); - layers.push(place_at(dock_pallet_menu(&checked, toggle), sub_x, sub_y)); - } - Some( - iced::widget::Stack::with_children(layers) - .width(Fill) - .height(Fill) - .into(), - ) + Some(place_at(menu_frame(menu.into()), menu_x, menu_y)) } /// The live drop preview for dragged panel `id`: a tint over the target @@ -695,32 +629,18 @@ impl OpenCADStudio { }; let mut layers: Vec> = Vec::new(); let group_drag = self.dock_drag.and_then(|d| d.movement()).and_then(|(_, g, _)| g); - // The pallets that move: a docked group's, a floating window's, or - // just `id`. - let window_drag = matches!( - self.dock_drag, - Some(crate::ui::dock::DockDrag::Move { window: true, .. }) - ); - let moving = match (group_drag, self.dock.float_rect(id)) { - (Some((s, g)), _) => self.dock_group_visible(s, g), - (None, Some(f)) if window_drag => self.dock_float_visible(f), - _ => vec![id], - }; let marks_side = match target { DropTarget::Edge { side, .. } | DropTarget::Join { side, .. } => Some(side), DropTarget::Float { x, y } => { - let (w, h) = match group_drag { - Some((s, g)) => (self.dock_group_width(s, g), self.dock_default_float_h()), - None => self.dock_float_size(id), - }; + let (w, h) = self.dock_float_size(id); layers.push(place_at(dock_ghost(id, w + DOCK_FLOAT_BAR_W, h), x, y)); None } }; - // A single pallet also gets a ghost where it lands in the column; - // several show only the strip feedback, as the column keeps showing - // what is there. - if let (Some(side), None, [_]) = (marks_side, group_drag, moving.as_slice()) { + // A single pallet also gets a ghost where it lands in the column; a + // whole group shows only the strip feedback, as the column keeps + // showing what is there. + if let (Some(side), None) = (marks_side, group_drag) { if let Some((top, bottom, w)) = self.dock_landing(id, target) { let x = match side { DockSide::Left => strip, @@ -735,7 +655,11 @@ impl OpenCADStudio { if let Some(side) = marks_side { layers.extend(self.dock_strip_drop_marks(side, target)); if let Some(p) = self.dock_drag_last { - layers.push(place_at(dock_icon_ghost(&moving), p.x - 16.0, p.y - 15.0)); + let icons = match group_drag { + Some((s, g)) => self.dock_group_visible(s, g), + None => vec![id], + }; + layers.push(place_at(dock_icon_ghost(&icons), p.x - 16.0, p.y - 15.0)); } } iced::widget::Stack::with_children(layers) @@ -956,73 +880,53 @@ impl canvas::Program for VBarLabel { /// Width of a floating panel's vertical title bar. const DOCK_FLOAT_BAR_W: f32 = 28.0; -/// A floating window's vertical title bar: close then the hide (auto-hide) -/// pin at the top; at the bottom the pallet's icon with its title reading -/// upward, or for a group each pallet's icon (press one to drag that pallet -/// out). Pressing the bar drags the window, double-clicking docks it against -/// the edge the bar faces, right-clicking opens its menu. +/// A floating panel's vertical title bar: close then the hide (auto-collapse) +/// pin at the top, the panel's icon at the bottom with its title reading +/// upward above it. Pressing the bar's free area drags the panel. fn floating_title_bar( - visible: &[crate::ui::dock::PanelId], + id: crate::ui::dock::PanelId, auto_collapse: bool, on_left: bool, hovered: bool, ) -> Element<'static, Message> { use crate::ui::dock::DockMsg; - let anchor = visible[0]; let tip = if on_left { iced::widget::tooltip::Position::Left } else { iced::widget::tooltip::Position::Right }; + let title = canvas(VBarLabel { + text: t!(id.title()).into_owned(), + }) + .width(Fill) + .height(Fill); // Close and pin only show while the pointer is over the bar, as on a // docked title bar. let mut bar = column![].spacing(2); if hovered { bar = bar - .push(crate::ui::dock::close_button(DockMsg::FloatClose(anchor), tip)) - .push(crate::ui::dock::pin_button(anchor, auto_collapse, tip)); - } - if let [id] = visible { - let title = canvas(VBarLabel { - text: t!(id.title()).into_owned(), - }) - .width(Fill) - .height(Fill); - bar = bar - .push(container(title).width(Fill).height(Fill).padding([6, 0])) - .push(container(crate::ui::icons::themed(id.icon(), 16.0)).center_x(Fill)); - } else { - bar = bar.push(Space::new().height(Fill)); - for id in visible { - let icon = mouse_area( - container(crate::ui::icons::themed(id.icon(), 16.0)) - .center_x(Fill) - .center_y(Length::Fixed(24.0)), - ) - .on_press(Message::Dock(DockMsg::IconPress(*id))) - .interaction(iced::mouse::Interaction::Grab); - bar = bar.push( - iced::widget::tooltip(icon, text(t!(id.title())).size(10), tip).gap(4), - ); - } + .push(crate::ui::dock::close_button(id, tip)) + .push(crate::ui::dock::pin_button(id, auto_collapse, tip)); } let bar = bar - .padding([4, 0]) - .align_x(iced::Center) - .width(Length::Fixed(DOCK_FLOAT_BAR_W)) - .height(Fill); + .push(container(title).width(Fill).height(Fill).padding([6, 0])) + .push(container(crate::ui::icons::themed(id.icon(), 16.0)).center_x(Fill)) + .padding([4, 0]) + .align_x(iced::Center) + .width(Length::Fixed(DOCK_FLOAT_BAR_W)) + .height(Fill); mouse_area(container(bar).style(|theme: &Theme| container::Style { background: Some(Background::Color(theme.palette().background.weak.color)), ..Default::default() })) - .on_press(Message::Dock(DockMsg::FloatGrab(anchor))) - .on_right_press(Message::Dock(DockMsg::FloatMenu(Some(anchor)))) - .on_enter(Message::Dock(DockMsg::TitleHover(anchor))) + .on_press(Message::Dock(DockMsg::DockGrab(id))) + .on_right_press(Message::Dock(DockMsg::FloatMenu(Some(id)))) + .on_enter(Message::Dock(DockMsg::TitleHover(id))) .on_move(|p| Message::Dock(DockMsg::FloatBarPointer(p))) - .on_exit(Message::Dock(DockMsg::TitleHoverEnd(anchor))) + .on_exit(Message::Dock(DockMsg::TitleHoverEnd(id))) // Double-click docks it against the edge its title bar faces. .on_double_click(Message::Dock(DockMsg::DockTo( - anchor, + id, if on_left { crate::app::config::DockSide::Left } else { @@ -1033,29 +937,6 @@ fn floating_title_bar( .into() } -/// Draggable bar between floating pallets `upper` and `lower` of the window -/// holding `anchor`; double-click evens their heights. -fn float_splitter( - anchor: crate::ui::dock::PanelId, - upper: usize, - lower: usize, - w: f32, -) -> Element<'static, Message> { - use crate::ui::dock::DockMsg; - let line = container(Space::new()) - .width(Length::Fixed(w)) - .height(Length::Fixed(5.0)) - .style(|theme: &Theme| container::Style { - background: Some(Background::Color(theme.palette().background.neutral.color)), - ..Default::default() - }); - mouse_area(line) - .on_press(Message::Dock(DockMsg::FloatSplitGrab(anchor, upper, lower))) - .on_double_click(Message::Dock(DockMsg::FloatSplitReset(anchor))) - .interaction(iced::mouse::Interaction::ResizingVertically) - .into() -} - /// Grabbable separator between a docked pallet and the viewport: drag to /// size its group, double-click to reset the width. fn dock_divider(id: crate::ui::dock::PanelId) -> Element<'static, Message> { @@ -1078,19 +959,17 @@ fn dock_divider(id: crate::ui::dock::PanelId) -> Element<'static, Message> { /// Width of the icon strip's pallet menu. const DOCK_MENU_W: f32 = 230.0; -/// Rough heights of a floating window's menu and of a pallet menu, to keep -/// them on screen. -const FLOAT_MENU_H: f32 = 150.0; -const PALLET_MENU_H: f32 = 270.0; +/// Rough height of a floating title bar's menu, to keep it on screen. +const FLOAT_MENU_H: f32 = 110.0; -/// A pallet menu (an edge's + button, a floating window's Pallets -/// submenu), styled like the right-click menu: one row per pallet with its -/// icon, its name and a check mark when it is open there. `checked` holds -/// each pallet with that state; picking one sends `toggle`. -fn dock_pallet_menu( +/// The pallet menu of an edge's + button, styled like the right-click menu: +/// one row per pallet with its icon, its name and a check mark when it is +/// open on this edge. `checked` holds each pallet with that state. +fn dock_edge_menu( + side: crate::app::config::DockSide, checked: &[(crate::ui::dock::PanelId, bool)], - toggle: impl Fn(crate::ui::dock::PanelId) -> Message, ) -> Element<'static, Message> { + use crate::ui::dock::DockMsg; let rows: Vec> = checked .iter() .map(|(id, checked)| { @@ -1109,7 +988,7 @@ fn dock_pallet_menu( .spacing(8) .align_y(iced::Center), ) - .on_press(toggle(id)) + .on_press(Message::Dock(DockMsg::EdgeMenuToggle(side, id))) .style(menu_row_style) .padding([5, 8]) .width(Fill) diff --git a/src/app/mod.rs b/src/app/mod.rs index bb4190dfe..adbfcb285 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -865,10 +865,8 @@ pub(super) struct OpenCADStudio { pub(crate) dock_title_hover: Option, /// Edge whose icon-strip pallet menu (+) is open. pub(crate) dock_edge_menu: Option, - /// Floating window (named by one of its pallets) whose right-click menu - /// is open, and whether its Pallets submenu shows. + /// Floating pallet whose title-bar right-click menu is open. pub(crate) dock_float_menu: Option, - pub(crate) dock_float_menu_pallets: bool, /// Pointer position over a floating title bar (bar-local), and where /// its open menu was asked for. pub(crate) dock_float_pointer: iced::Point, @@ -4306,7 +4304,6 @@ impl OpenCADStudio { dock_hover_pending: None, dock_hover_gen: 0, dock_float_menu: None, - dock_float_menu_pallets: false, dock_float_pointer: iced::Point::ORIGIN, dock_float_menu_at: iced::Point::ORIGIN, dock_drag: None, diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs index 91bb128bf..058177a4f 100644 --- a/src/locale_catalog.rs +++ b/src/locale_catalog.rs @@ -5759,7 +5759,6 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "Dock left" => Some(("dock", "dock-left")), "Dock right" => Some(("dock", "dock-right")), "New group" => Some(("dock", "new-group")), - "Pallets" => Some(("dock", "pallets")), "Browser" => Some(("dock", "browser")), "Node Graph" => Some(("dock", "node-graph")), _ => None, diff --git a/src/ui/dock.rs b/src/ui/dock.rs index 3b8c21621..6015b694e 100644 --- a/src/ui/dock.rs +++ b/src/ui/dock.rs @@ -65,35 +65,17 @@ pub enum DockMsg { GripHoverEnd(DockSide, usize), /// Double-click on a docked title bar: float the panel. FloatOut(PanelId), - /// Double-click on a group's edge band: float the whole group. - GroupFloat(DockSide, usize), /// Double-click on a floating title bar (or its menu's Dock left / - /// right): dock the window holding the panel on that side. + /// right): dock the panel on that side. DockTo(PanelId, DockSide), - /// Begin dragging the floating window holding `panel` by its title bar. - FloatGrab(PanelId), - /// Close every pallet of the floating window holding `panel`. - FloatClose(PanelId), - /// Begin dragging the splitter between pallets `upper` and `lower` of - /// the floating window holding `panel`. - FloatSplitGrab(PanelId, usize, usize), - /// Give every pallet of the floating window holding `panel` the same - /// height again. - FloatSplitReset(PanelId), - /// Open (`Some`) the right-click menu of the floating window holding the - /// pallet, or close it (`None`). + /// Open (`Some`) the right-click menu of a floating title bar, or close + /// it (`None`). FloatMenu(Option), - /// Show (`true`) or hide the floating menu's Pallets submenu. - FloatMenuPallets(bool), - /// Floating menu's Allow docking check: flip it for the window holding - /// the pallet. + /// Floating menu's Allow docking check: flip it for the panel. FloatDockingToggle(PanelId), /// The pointer moved over a floating title bar, at this point in it /// (where its right-click menu opens). FloatBarPointer(iced::Point), - /// Pallets submenu pick (window, pallet): hide the pallet when it shows - /// in the window, else add it to the window and open it. - FloatMenuToggle(PanelId, PanelId), /// Bring floating `panel` to the front. FloatRaise(PanelId), /// The pointer left the edge column; collapse any auto-collapsing panel. @@ -335,34 +317,27 @@ where Ok(groups) } -/// A floating window over the workspace, in workspace-local pixels: one -/// pallet, or a group of them stacked like a docked group. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(from = "FloatRepr")] +/// A panel floating over the workspace, in workspace-local pixels. +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +#[serde(try_from = "FloatRepr")] pub struct FloatPanel { - /// Pallets top → bottom with their heights (`width` is unused: the - /// window has its own). - #[serde(flatten)] - pub group: DockGroup, + pub id: PanelId, pub x: f32, pub y: f32, pub w: f32, pub h: f32, - /// Dragging the window (or a pallet out of it) may dock it on an edge; - /// off, it only ever floats. + /// Dragging the panel may dock it on an edge; off, it only ever floats. pub docking: bool, } -/// On-disk form of a floating window. Configs from before floating groups -/// stored one pallet as `id`. +/// On-disk form of a floating panel. A short-lived build floated groups, +/// stored as `panels`: its first pallet keeps floating, so such a config +/// still loads (a config that fails to load falls back to all defaults). #[derive(Deserialize)] struct FloatRepr { - #[serde(default)] id: Option, #[serde(default)] panels: Vec, - #[serde(default)] - weights: Vec, x: f32, y: f32, w: f32, @@ -375,29 +350,23 @@ fn docking_default() -> bool { true } -impl From for FloatPanel { - fn from(r: FloatRepr) -> Self { - let mut group = DockGroup::stack(r.id.into_iter().chain(r.panels).collect()); - if !r.weights.is_empty() { - group.weights = r.weights; - } - group.heal(); - Self { - group, - x: r.x, - y: r.y, - w: r.w, - h: r.h, +impl TryFrom for FloatPanel { + type Error = &'static str; + + fn try_from(r: FloatRepr) -> Result { + let id = r.id.or(r.panels.first().copied()).ok_or("floating panel without id")?; + Ok(Self { docking: r.docking, - } + ..Self::new(id, r.x, r.y, r.w, r.h) + }) } } impl FloatPanel { - /// Pallet `id` floating on its own. + /// Panel `id` floating at `x`, `y`, sized `w` × `h`, free to dock. pub fn new(id: PanelId, x: f32, y: f32, w: f32, h: f32) -> Self { Self { - group: DockGroup::single(id), + id, x, y, w, @@ -406,33 +375,13 @@ impl FloatPanel { } } - /// The window's first pallet, which names it in messages. - pub fn id(&self) -> PanelId { - self.group.panels[0] - } - - pub fn contains(&self, id: PanelId) -> bool { - self.group.panels.contains(&id) - } - - /// The widest the window may be: the most any of its pallets allows. - fn max_width(&self) -> f32 { - self.group - .panels - .iter() - .map(|id| id.max_width()) - .fold(DOCK_MIN_W, f32::max) - } - /// Keep a placement read from disk usable: finite, on the workspace's - /// positive side, and at least the minimum size. The window holds at - /// least one pallet. + /// positive side, and at least the minimum size. fn heal(&mut self) { - self.group.heal(); let finite_or = |v: f32, fallback: f32| if v.is_finite() { v } else { fallback }; self.x = finite_or(self.x, 0.0).max(0.0); self.y = finite_or(self.y, 0.0).max(0.0); - self.w = finite_or(self.w, self.id().default_width()).clamp(DOCK_MIN_W, self.max_width()); + self.w = finite_or(self.w, self.id.default_width()).clamp(DOCK_MIN_W, self.id.max_width()); self.h = finite_or(self.h, FLOAT_MIN_H).max(FLOAT_MIN_H); } } @@ -462,7 +411,7 @@ pub struct DockState { /// Auto-hide per edge (left, right): the edge shows only its icon strip /// until an icon is hovered. pub auto_hide: (bool, bool), - /// Floating windows, back → front. + /// Floating panels, back → front. pub floating: Vec, /// Per-panel settings, keyed by `PanelId`: the width a pallet floats at /// (and starts a new group with) and its floating auto-hide. @@ -552,18 +501,7 @@ impl DockState { let shown = self.shown(side).min(len.saturating_sub(1)); self.show_group(side, shown); } - for f in &mut self.floating { - f.group.heal(); - (f.group.panels, f.group.weights) = f - .group - .panels - .iter() - .zip(&f.group.weights) - .filter(|(id, _)| seen.insert(**id)) - .map(|(id, w)| (*id, *w)) - .unzip(); - } - self.floating.retain(|f| !f.group.panels.is_empty()); + self.floating.retain(|f| seen.insert(f.id)); self.floating.iter_mut().for_each(FloatPanel::heal); } @@ -577,13 +515,9 @@ impl DockState { None } - /// The floating window holding `id`, if it floats. - pub fn float_rect(&self, id: PanelId) -> Option<&FloatPanel> { - self.floating.iter().find(|f| f.contains(id)) - } - - fn float_index(&self, id: PanelId) -> Option { - self.floating.iter().position(|f| f.contains(id)) + /// The floating placement of `id`, if it floats. + pub fn float_rect(&self, id: PanelId) -> Option { + self.floating.iter().find(|f| f.id == id).copied() } /// Whether `id` has a place in the layout (docked or floating). A panel @@ -605,7 +539,7 @@ impl DockState { } /// The floating auto-hide flag of `id` (a docked pallet follows its - /// edge, a floating one its window; see [`Self::auto_hides`]). + /// edge instead; see [`Self::auto_hides`]). pub fn auto_collapse(&self, id: PanelId) -> bool { self.settings(id).auto_collapse } @@ -619,31 +553,22 @@ impl DockState { } /// Whether `id` auto-hides where it is: by its edge's setting when - /// docked, by its window's (its first pallet's flag) when floating. + /// docked, by its own when floating. pub fn auto_hides(&self, id: PanelId) -> bool { - match (self.location(id), self.float_rect(id)) { - (Some((side, _)), _) => self.edge_auto_hide(side), - (None, Some(f)) => self.auto_collapse(f.id()), - (None, None) => self.auto_collapse(id), + match self.location(id) { + Some((side, _)) => self.edge_auto_hide(side), + None => self.auto_collapse(id), } } - /// Flip auto-hide where `id` is: its whole edge when docked, its whole - /// window when floating. Returns the new state. + /// Flip auto-hide where `id` is: its whole edge when docked, the pallet + /// itself when floating. Returns the new state. pub fn toggle_auto_hide(&mut self, id: PanelId) -> bool { let on = !self.auto_hides(id); match self.location(id) { Some((DockSide::Left, _)) => self.auto_hide.0 = on, Some((DockSide::Right, _)) => self.auto_hide.1 = on, - None => { - let panels = match self.float_rect(id) { - Some(f) => f.group.panels.clone(), - None => vec![id], - }; - for p in panels { - self.set_auto_collapse(p, on); - } - } + None => self.set_auto_collapse(id, on), } on } @@ -725,13 +650,7 @@ impl DockState { /// and weight there, and whether the group disappeared (it held nothing /// else). fn detach(&mut self, id: PanelId) -> Option<(DockSide, usize, usize, f32, bool)> { - if let Some(i) = self.float_index(id) { - self.floating[i].group.remove(id); - if self.floating[i].group.panels.is_empty() { - self.floating.remove(i); - } - return None; - } + self.floating.retain(|f| f.id != id); let (side, gi) = self.location(id)?; let (pos, weight) = self.groups_mut(side)[gi].remove(id).expect("located"); if !self.groups(side)[gi].panels.is_empty() { @@ -829,127 +748,38 @@ impl DockState { true } - /// Float `rect`'s pallets in it, on top of the other floating windows. + /// Float `id` at `rect`, on top of the other floating panels. pub fn float(&mut self, rect: FloatPanel) -> bool { let before = self.clone(); - for id in &rect.group.panels { - self.detach(*id); - } + self.detach(rect.id); self.floating.push(rect); *self != before } - /// Float the whole group `group` of `side` as one window at `x`, `y` - /// sized `w` × `h`. Returns whether the layout changed. - pub fn float_group(&mut self, side: DockSide, group: usize, x: f32, y: f32, w: f32, h: f32) -> bool { - if group >= self.groups(side).len() { - return false; - } - let g = self.groups_mut(side).remove(group); - self.after_group_removed(side, group); - let mut f = FloatPanel { - group: g, - x, - y, - w, - h, - docking: true, - }; - f.heal(); - self.floating.push(f); - true - } - - /// Move the floating window holding `id` to `x`, `y`. - pub fn move_float(&mut self, id: PanelId, x: f32, y: f32) -> bool { - let Some(i) = self.float_index(id) else { - return false; - }; - let f = &mut self.floating[i]; - let changed = (f.x, f.y) != (x, y); - (f.x, f.y) = (x, y); - changed - } - - /// Dock the floating window holding `id` as a new group on `side` at - /// insertion `index`, keeping its pallets, their heights and its width, - /// and show it. A docked `id` docks alone (see [`Self::dock`]). - pub fn dock_window(&mut self, id: PanelId, side: DockSide, index: usize) -> bool { - let Some(i) = self.float_index(id) else { - return self.dock(id, side, index); - }; - let f = self.floating.remove(i); - let mut g = f.group; - g.width = f.w; - let groups = self.groups_mut(side); - let index = index.min(groups.len()); - groups.insert(index, g); - self.show_group(side, index); - true - } - - /// Stack every pallet of the floating window holding `id` into group - /// `group` on `side` from position `index` on, and show the group. A - /// docked `id` joins alone (see [`Self::join_group`]). - pub fn join_window(&mut self, id: PanelId, side: DockSide, group: usize, index: usize) -> bool { - let Some(i) = self.float_index(id) else { - return self.join_group(id, side, group, index); - }; - if group >= self.groups(side).len() { - return false; - } - let f = self.floating.remove(i); - let target = &mut self.groups_mut(side)[group]; - let weight = target.mean_weight(); - for (k, p) in f.group.panels.into_iter().enumerate() { - target.insert(index.saturating_add(k), p, weight); - } - self.show_group(side, group); - true - } - - /// Add pallet `id` to the bottom of the floating window holding `into`. - /// Returns whether the layout changed. - pub fn add_to_float(&mut self, id: PanelId, into: PanelId) -> bool { - match self.float_rect(into) { - Some(f) if !f.contains(id) => {} - _ => return false, - } - self.detach(id); - // Detaching may have emptied (and removed) another window. - let Some(i) = self.float_index(into) else { - return false; - }; - let g = &mut self.floating[i].group; - let weight = g.mean_weight(); - g.insert(usize::MAX, id, weight); - true - } - - /// Flip whether dragging the floating window holding `id` may dock it. + /// Flip whether dragging floating panel `id` may dock it. pub fn toggle_float_docking(&mut self, id: PanelId) -> bool { - let Some(i) = self.float_index(id) else { - return false; - }; - self.floating[i].docking = !self.floating[i].docking; - true + match self.floating.iter_mut().find(|f| f.id == id) { + Some(f) => { + f.docking = !f.docking; + true + } + None => false, + } } - /// Bring the floating window holding `id` to the front. + /// Bring a floating panel to the front. pub fn raise_float(&mut self, id: PanelId) { - if let Some(i) = self.float_index(id) { + if let Some(i) = self.floating.iter().position(|f| f.id == id) { let f = self.floating.remove(i); self.floating.push(f); } } - /// Resize the floating window holding `id`, keeping it at least a usable - /// size. With `keep_right` the right edge stays put (resizing from the - /// left corner). + /// Resize a floating panel, keeping it at least a usable size. With + /// `keep_right` the right edge stays put (resizing from the left corner). pub fn resize_float(&mut self, id: PanelId, w: f32, h: f32, keep_right: bool) { - if let Some(i) = self.float_index(id) { - let f = &mut self.floating[i]; - let new_w = w.clamp(DOCK_MIN_W, f.max_width()); + if let Some(f) = self.floating.iter_mut().find(|f| f.id == id) { + let new_w = w.clamp(DOCK_MIN_W, id.max_width()); if keep_right { f.x += f.w - new_w; } @@ -970,16 +800,18 @@ impl DockState { delta: f32, min_weight: f32, ) { - if let Some(g) = self.groups_mut(side).get_mut(group) { - shift_weights(&mut g.weights, upper, lower, delta, min_weight); - } - } - - /// [`Self::shift_split`] for the floating window holding `id`. - pub fn shift_float_split(&mut self, id: PanelId, upper: usize, lower: usize, delta: f32, min_weight: f32) { - if let Some(i) = self.float_index(id) { - shift_weights(&mut self.floating[i].group.weights, upper, lower, delta, min_weight); + let Some(g) = self.groups_mut(side).get_mut(group) else { + return; + }; + let w = &mut g.weights; + if upper >= w.len() || lower >= w.len() || upper == lower { + return; } + let total = w[upper] + w[lower]; + let min = min_weight.clamp(MIN_WEIGHT, total * 0.5); + let up = (w[upper] + delta).clamp(min, total - min); + w[upper] = up; + w[lower] = total - up; } /// Give every pallet of group `group` on `side` the same height again. @@ -988,26 +820,6 @@ impl DockState { g.weights.iter_mut().for_each(|w| *w = 1.0); } } - - /// [`Self::reset_splits`] for the floating window holding `id`. - pub fn reset_float_splits(&mut self, id: PanelId) { - if let Some(i) = self.float_index(id) { - self.floating[i].group.weights.iter_mut().for_each(|w| *w = 1.0); - } - } -} - -/// Move the boundary between stacked pallets `upper` and `lower` by `delta` -/// weight units, keeping their combined share and each above `min_weight`. -fn shift_weights(w: &mut [f32], upper: usize, lower: usize, delta: f32, min_weight: f32) { - if upper >= w.len() || lower >= w.len() || upper == lower { - return; - } - let total = w[upper] + w[lower]; - let min = min_weight.clamp(MIN_WEIGHT, total * 0.5); - let up = (w[upper] + delta).clamp(min, total - min); - w[upper] = up; - w[lower] = total - up; } /// What the pointer is dragging in the dock, if anything. @@ -1021,8 +833,6 @@ pub enum DockDrag { Move { panel: PanelId, group: Option<(DockSide, usize)>, - /// The whole floating window holding `panel` moves. - window: bool, origin: Option, target: Option, grab: iced::Vector, @@ -1039,9 +849,6 @@ pub enum DockDrag { /// Sizing floating `panel` from its bottom-left (`from_left`) or /// bottom-right corner. FloatSize { panel: PanelId, from_left: bool }, - /// Moving the splitter between pallets `upper` and `lower` of the - /// floating window holding `panel`. - FloatSplit { panel: PanelId, upper: usize, lower: usize }, /// Sizing the Layer Manager's Name column. LayerColumn, } @@ -1052,19 +859,6 @@ impl DockDrag { DockDrag::Move { panel, group, - window: false, - origin: None, - target: None, - grab: iced::Vector::new(0.0, 0.0), - } - } - - /// Start moving the floating window holding `panel`. - pub fn moving_window(panel: PanelId) -> Self { - DockDrag::Move { - panel, - group: None, - window: true, origin: None, target: None, grab: iced::Vector::new(0.0, 0.0), @@ -1265,7 +1059,7 @@ pub fn title_bar<'a>(id: PanelId, title: String, chrome: Chrome) -> Element<'a, if chrome.title_hovered { bar = bar .push(pin_button(id, chrome.auto_collapse, tooltip::Position::Bottom)) - .push(close_button(DockMsg::Close(id), tooltip::Position::Bottom)); + .push(close_button(id, tooltip::Position::Bottom)); } mouse_area( container(bar) @@ -1307,10 +1101,10 @@ pub fn pin_button<'a>(id: PanelId, auto_collapse: bool, tip: tooltip::Position) .into() } -/// The close button of a panel's title bar, sending `close`. -pub fn close_button<'a>(close: DockMsg, tip: tooltip::Position) -> Element<'a, Message> { +/// The close button of a panel's title bar. +pub fn close_button<'a>(id: PanelId, tip: tooltip::Position) -> Element<'a, Message> { let close = button(crate::ui::icons::themed_secondary(crate::ui::icons::CLOSE, 12.0)) - .on_press(Message::Dock(close)) + .on_press(Message::Dock(DockMsg::Close(id))) .style(button::subtle) .padding([3, 5]); tooltip(close, text(crate::t!("Close")).size(10), tip) @@ -1676,10 +1470,10 @@ mod tests { fn float_detaches_and_dock_brings_it_back() { let mut state = DockState::default(); let rect = FloatPanel::new(PanelId::Properties, 100.0, 50.0, 250.0, 300.0); - assert!(state.float(rect.clone())); + assert!(state.float(rect)); assert!(state.left.is_empty()); assert_eq!(state.location(PanelId::Properties), None); - assert_eq!(state.float_rect(PanelId::Properties), Some(&rect)); + assert_eq!(state.float_rect(PanelId::Properties), Some(rect)); assert!(state.is_placed(PanelId::Properties)); assert!(state.dock(PanelId::Properties, DockSide::Right, 0)); assert!(state.floating.is_empty()); @@ -1700,41 +1494,14 @@ mod tests { } #[test] - fn legacy_single_float_loads_as_a_one_pallet_window() { - let json = r#"{"floating":[{"id":"count","x":1.0,"y":2.0,"w":300.0,"h":400.0}]}"#; + fn floating_groups_from_an_older_build_load_as_their_first_pallet() { + let json = r#"{"floating":[{"panels":["count","browser"],"weights":[1.0,1.0],"width":0.0,"x":1.0,"y":2.0,"w":300.0,"h":400.0,"docking":false}]}"#; let state: DockState = serde_json::from_str(json).unwrap(); - assert_eq!(state.floating, vec![FloatPanel::new(PanelId::Count, 1.0, 2.0, 300.0, 400.0)]); - } - - #[test] - fn a_group_floats_docks_and_takes_pallets_as_one_window() { - let mut state = DockState::default(); - state.join_group(PanelId::Browser, DockSide::Left, 0, 1); - state.dock(PanelId::Count, DockSide::Left, 1); - // Floating group 0 takes both its pallets into one window. - assert!(state.float_group(DockSide::Left, 0, 10.0, 20.0, 300.0, 400.0)); - let f = state.float_rect(PanelId::Browser).unwrap(); - assert_eq!(f.group.panels, vec![PanelId::Properties, PanelId::Browser]); - assert_eq!(state.left.len(), 1); - // Adding a pallet stacks it at the bottom; floating one out leaves - // the rest together. - assert!(state.add_to_float(PanelId::Count, PanelId::Properties)); - assert!(state.left.is_empty()); - assert_eq!(state.float_rect(PanelId::Count).unwrap().group.panels.len(), 3); - state.float(FloatPanel::new(PanelId::Count, 0.0, 0.0, 250.0, 300.0)); - assert_eq!(state.floating.len(), 2); - assert_eq!(state.float_rect(PanelId::Properties).unwrap().group.panels.len(), 2); - // The window docks as one group with its width. - assert!(state.dock_window(PanelId::Browser, DockSide::Right, 0)); - assert_eq!(state.right[0].panels, vec![PanelId::Properties, PanelId::Browser]); - assert_eq!(state.right[0].width, 300.0); - // And a window joins a docked group as a whole. - assert!(state.join_window(PanelId::Count, DockSide::Right, 0, 1)); - assert_eq!( - state.right[0].panels, - vec![PanelId::Properties, PanelId::Count, PanelId::Browser] - ); - assert!(state.floating.is_empty()); + let expected = FloatPanel { + docking: false, + ..FloatPanel::new(PanelId::Count, 1.0, 2.0, 300.0, 400.0) + }; + assert_eq!(state.floating, vec![expected]); } #[test]