diff --git a/locales/ar-SA/opencadstudio.ftl b/locales/ar-SA/opencadstudio.ftl index c6a75500f..9e78732e8 100644 --- a/locales/ar-SA/opencadstudio.ftl +++ b/locales/ar-SA/opencadstudio.ftl @@ -6164,3 +6164,12 @@ count-feature = .invalid-boundary = كائن حد منطقة العد غير صالح. حدد خطًا متعددًا مغلقًا يتكون من قطع مستقيمة ولا يتقاطع مع نفسه. .block-not-found = تعذر العثور على الكتلة "__ocs_fmt_0__". .invalid-point = نقطة غير صالحة. + +dock = + .auto-hide = إخفاء تلقائي + .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 83f0c2d02..0de11349e 100644 --- a/locales/bg-BG/opencadstudio.ftl +++ b/locales/bg-BG/opencadstudio.ftl @@ -6152,3 +6152,12 @@ count-feature = .invalid-boundary = Невалиден обект за граница на областта за преброяване. Изберете затворена полилиния от отсечки, която не се самопресича. .block-not-found = Блокът "__ocs_fmt_0__" не може да бъде намерен. .invalid-point = Невалидна точка. + +dock = + .auto-hide = Автоматично скриване + .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 7aec030d7..b77e80e31 100644 --- a/locales/cs-CZ/opencadstudio.ftl +++ b/locales/cs-CZ/opencadstudio.ftl @@ -6149,3 +6149,12 @@ count-feature = .invalid-boundary = Neplatný objekt hranice oblasti počítání. Vyberte uzavřenou křivku složenou z úseček, která neprotíná sama sebe. .block-not-found = Blok "__ocs_fmt_0__" nelze najít. .invalid-point = Neplatný bod. + +dock = + .auto-hide = Automaticky skrýt + .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 d22c55b6c..59a0369b7 100644 --- a/locales/de-DE/opencadstudio.ftl +++ b/locales/de-DE/opencadstudio.ftl @@ -6150,3 +6150,12 @@ count-feature = .invalid-boundary = Ungültiges Begrenzungsobjekt für den Zählbereich. Wählen Sie eine geschlossene Polylinie aus Liniensegmenten, die sich nicht selbst schneidet. .block-not-found = Block "__ocs_fmt_0__" wurde nicht gefunden. .invalid-point = Ungültiger Punkt. + +dock = + .auto-hide = Automatisch ausblenden + .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 a085923a0..eee2a7975 100644 --- a/locales/el-GR/opencadstudio.ftl +++ b/locales/el-GR/opencadstudio.ftl @@ -6152,3 +6152,12 @@ count-feature = .invalid-boundary = Μη έγκυρο αντικείμενο ορίου περιοχής καταμέτρησης. Επιλέξτε μια κλειστή πολυγραμμή από ευθύγραμμα τμήματα που δεν τέμνει τον εαυτό της. .block-not-found = Το μπλοκ "__ocs_fmt_0__" δεν βρέθηκε. .invalid-point = Μη έγκυρο σημείο. + +dock = + .auto-hide = Αυτόματη απόκρυψη + .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 2f8ced110..6ebc848e4 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -6153,3 +6153,12 @@ count-feature = .invalid-boundary = Invalid count area boundary object. Select a closed polyline consisting of line segments and does not intersect itself. .block-not-found = Block "__ocs_fmt_0__" cannot be found. .invalid-point = Invalid point. + +dock = + .auto-hide = Auto-hide + .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 a61b1c8b4..1f75e7eb7 100644 --- a/locales/es-ES/opencadstudio.ftl +++ b/locales/es-ES/opencadstudio.ftl @@ -6152,3 +6152,12 @@ count-feature = .invalid-boundary = Objeto de contorno de área de recuento no válido. Seleccione una polilínea cerrada formada por segmentos de línea que no se corte a sí misma. .block-not-found = No se encuentra el bloque "__ocs_fmt_0__". .invalid-point = Punto no válido. + +dock = + .auto-hide = Ocultar automáticamente + .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 82cf03e99..85f83d9f2 100644 --- a/locales/fi-FI/opencadstudio.ftl +++ b/locales/fi-FI/opencadstudio.ftl @@ -6149,3 +6149,12 @@ count-feature = .invalid-boundary = Virheellinen laskenta-alueen rajaobjekti. Valitse suljettu, janoista koostuva moniviiva, joka ei leikkaa itseään. .block-not-found = Lohkoa "__ocs_fmt_0__" ei löydy. .invalid-point = Virheellinen piste. + +dock = + .auto-hide = Piilota automaattisesti + .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 afb43870f..7507461fd 100644 --- a/locales/fr-FR/opencadstudio.ftl +++ b/locales/fr-FR/opencadstudio.ftl @@ -6151,3 +6151,12 @@ count-feature = .invalid-boundary = Objet de contour de zone de comptage non valide. Sélectionnez une polyligne fermée composée de segments de ligne et qui ne se recoupe pas. .block-not-found = Le bloc "__ocs_fmt_0__" est introuvable. .invalid-point = Point non valide. + +dock = + .auto-hide = Masquage automatique + .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 2c4c06cee..cd1686ba4 100644 --- a/locales/hi-IN/opencadstudio.ftl +++ b/locales/hi-IN/opencadstudio.ftl @@ -6140,3 +6140,12 @@ count-feature = .invalid-boundary = अमान्य गणना क्षेत्र सीमा ऑब्जेक्ट। रेखा खंडों से बनी ऐसी बंद पॉलीलाइन चुनें जो स्वयं को प्रतिच्छेद न करे। .block-not-found = ब्लॉक "__ocs_fmt_0__" नहीं मिला। .invalid-point = अमान्य बिंदु। + +dock = + .auto-hide = स्वतः छिपाएँ + .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 7393d69a1..a7ba41b25 100644 --- a/locales/hu-HU/opencadstudio.ftl +++ b/locales/hu-HU/opencadstudio.ftl @@ -6151,3 +6151,12 @@ count-feature = .invalid-boundary = Érvénytelen számlálási terület határobjektum. Válasszon egy zárt, vonalszakaszokból álló, önmagát nem metsző vonalláncot. .block-not-found = A(z) "__ocs_fmt_0__" blokk nem található. .invalid-point = Érvénytelen pont. + +dock = + .auto-hide = Automatikus elrejtés + .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 212e19a64..90cc6785e 100644 --- a/locales/it-IT/opencadstudio.ftl +++ b/locales/it-IT/opencadstudio.ftl @@ -6151,3 +6151,12 @@ count-feature = .invalid-boundary = Oggetto di contorno dell'area di conteggio non valido. Selezionare una polilinea chiusa composta da segmenti di linea che non si interseca. .block-not-found = Impossibile trovare il blocco "__ocs_fmt_0__". .invalid-point = Punto non valido. + +dock = + .auto-hide = Nascondi automaticamente + .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 cf930fc9b..b8ac215be 100644 --- a/locales/ja-JP/opencadstudio.ftl +++ b/locales/ja-JP/opencadstudio.ftl @@ -6140,3 +6140,12 @@ count-feature = .invalid-boundary = カウント領域の境界オブジェクトが無効です。線分で構成され、自己交差しない閉じたポリラインを選択してください。 .block-not-found = ブロック "__ocs_fmt_0__" が見つかりません。 .invalid-point = 点が無効です。 + +dock = + .auto-hide = 自動的に隠す + .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 1cbff7eaf..28ac8e053 100644 --- a/locales/ko-KR/opencadstudio.ftl +++ b/locales/ko-KR/opencadstudio.ftl @@ -6151,3 +6151,12 @@ count-feature = .invalid-boundary = 개수 영역 경계 객체가 잘못되었습니다. 선분으로 구성되고 자체 교차하지 않는 닫힌 폴리선을 선택하십시오. .block-not-found = 블록 "__ocs_fmt_0__"을(를) 찾을 수 없습니다. .invalid-point = 점이 잘못되었습니다. + +dock = + .auto-hide = 자동 숨기기 + .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 496c8f95d..91b99f044 100644 --- a/locales/nl-NL/opencadstudio.ftl +++ b/locales/nl-NL/opencadstudio.ftl @@ -6150,3 +6150,12 @@ count-feature = .invalid-boundary = Ongeldig grensobject voor het telgebied. Selecteer een gesloten polylijn die uit lijnsegmenten bestaat en zichzelf niet snijdt. .block-not-found = Blok "__ocs_fmt_0__" kan niet worden gevonden. .invalid-point = Ongeldig punt. + +dock = + .auto-hide = Automatisch verbergen + .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 380533d4a..4ba9b0a52 100644 --- a/locales/pl-PL/opencadstudio.ftl +++ b/locales/pl-PL/opencadstudio.ftl @@ -6151,3 +6151,12 @@ count-feature = .invalid-boundary = Nieprawidłowy obiekt obwiedni obszaru liczenia. Wybierz zamkniętą polilinię złożoną z odcinków, która nie przecina samej siebie. .block-not-found = Nie można znaleźć bloku "__ocs_fmt_0__". .invalid-point = Nieprawidłowy punkt. + +dock = + .auto-hide = Autoukrywanie + .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 4f32592b7..8c1274a9a 100644 --- a/locales/pt-BR/opencadstudio.ftl +++ b/locales/pt-BR/opencadstudio.ftl @@ -6152,3 +6152,12 @@ count-feature = .invalid-boundary = Objeto de contorno da área de contagem inválido. Selecione uma polilinha fechada composta de segmentos de linha e que não se cruze. .block-not-found = O bloco "__ocs_fmt_0__" não foi encontrado. .invalid-point = Ponto inválido. + +dock = + .auto-hide = Ocultar automaticamente + .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 8d980fc7f..a3e79f208 100644 --- a/locales/ru-RU/opencadstudio.ftl +++ b/locales/ru-RU/opencadstudio.ftl @@ -6153,3 +6153,12 @@ count-feature = .invalid-boundary = Недопустимый объект границы области подсчёта. Выберите замкнутую полилинию из отрезков, которая не пересекает саму себя. .block-not-found = Блок "__ocs_fmt_0__" не найден. .invalid-point = Недопустимая точка. + +dock = + .auto-hide = Автоскрытие + .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 e7a2d568d..4683551d7 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -6075,3 +6075,12 @@ count-feature = .invalid-boundary = Geçersiz sayım alanı sınır nesnesi. Doğru parçalarından oluşan, kendisiyle kesişmeyen kapalı bir çoklu çizgi seçin. .block-not-found = "__ocs_fmt_0__" bloğu bulunamadı. .invalid-point = Geçersiz nokta. + +dock = + .auto-hide = Otomatik gizle + .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 6d36da043..ac07a0696 100644 --- a/locales/zh-CN/opencadstudio.ftl +++ b/locales/zh-CN/opencadstudio.ftl @@ -6135,3 +6135,12 @@ count-feature = .invalid-boundary = 无效的计数区域边界对象。请选择由直线段组成且不自交的闭合多段线。 .block-not-found = 找不到块 "__ocs_fmt_0__"。 .invalid-point = 无效的点。 + +dock = + .auto-hide = 自动隐藏 + .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 cd3e2a1f1..aedf9a45f 100644 --- a/locales/zh-TW/opencadstudio.ftl +++ b/locales/zh-TW/opencadstudio.ftl @@ -6147,3 +6147,12 @@ count-feature = .invalid-boundary = 無效的計數區域邊界物件。請選取由線段組成且不自我相交的封閉聚合線。 .block-not-found = 找不到圖塊 "__ocs_fmt_0__"。 .invalid-point = 無效的點。 + +dock = + .auto-hide = 自動隱藏 + .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 6220927bb..30522c614 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"); + let f = app.dock.float_rect(PanelId::BlockPalette).expect("floating").clone(); 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,14 +275,53 @@ fn dock_grip_drag_moves_a_whole_group() { app.dock.left[1].panels, vec![PanelId::Properties, PanelId::Browser] ); - // A group dropped over the viewport does not float or split up. - let before = app.dock.clone(); + // A group dropped over the viewport floats as one window. 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, before); + 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] + ); } #[test] @@ -361,18 +400,12 @@ fn dock_hover_reveals_groups_only_on_an_auto_hiding_edge() { fn dock_float_resize_grows_the_panel() { use crate::ui::dock::{DockMsg, FloatPanel, PanelId}; let mut app = dock_app(); - app.dock.float(FloatPanel { - id: PanelId::BlockPalette, - x: 100.0, - y: 100.0, - w: 260.0, - h: 300.0, - }); + app.dock.float(FloatPanel::new(PanelId::BlockPalette, 100.0, 100.0, 260.0, 300.0)); let _ = app.on_dock(DockMsg::FloatResizeGrab(PanelId::BlockPalette, false)); 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(); + let f = app.dock.float_rect(PanelId::BlockPalette).unwrap().clone(); 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)); diff --git a/src/app/dock/update.rs b/src/app/dock/update.rs index 5f3a90618..3b43df3ec 100644 --- a/src/app/dock/update.rs +++ b/src/app/dock/update.rs @@ -98,22 +98,79 @@ impl OpenCADStudio { _ => strip + self.dock_column_width(DockSide::Left) + 40.0, }; self.dock_drag = None; - if self.dock.float(FloatPanel { - id, - x: x.max(0.0), - y: 40.0, - w, - h, - }) { + if self.dock.float(FloatPanel::new(id, x.max(0.0), 40.0, w, h)) { + 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; - if self.dock.dock(id, side, usize::MAX) { + self.dock_float_menu = None; + if self.dock.dock_window(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_pallets = false; + if let Some(id) = id { + self.dock.raise_float(id); + } + } + DockMsg::FloatMenuPallets(open) => self.dock_float_menu_pallets = open, + 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. @@ -194,6 +251,31 @@ 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, @@ -207,18 +289,17 @@ impl OpenCADStudio { } DropTarget::Float { x, y } => { let (w, h) = self.dock_float_size(panel); - self.dock.float(FloatPanel { - id: panel, - x, - y, - w, - h, - }) + self.dock.float(FloatPanel::new(panel, x, y, w, h)) } }; } Some(DockDrag::Move { .. } | DockDrag::LayerColumn) | None => {} - Some(DockDrag::Width(_) | DockDrag::Split { .. } | DockDrag::FloatSize { .. }) => { + Some( + DockDrag::Width(_) + | DockDrag::Split { .. } + | DockDrag::FloatSize { .. } + | DockDrag::FloatSplit { .. }, + ) => { changed = true; } } @@ -251,6 +332,7 @@ impl OpenCADStudio { DockDrag::Move { panel, group, + window, origin, target, grab, @@ -258,10 +340,12 @@ 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 panel where it was grabbed; a docked one by - // its title bar. + // Hold a floating window where it was grabbed; a docked + // pallet (or one pulled out of a floating group) by its + // title bar. let grab = if target.is_none() && started { - match self.dock.float_rect(panel) { + let whole = |f: &&crate::ui::dock::FloatPanel| window || f.group.panels.len() == 1; + match self.dock.float_rect(panel).filter(whole) { 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), @@ -272,11 +356,11 @@ impl OpenCADStudio { grab }; let target = if started { - // A whole group only moves between group positions: it - // cannot join another group or float. + // A whole docked group moves between group positions or + // floats; it cannot join another group. match (group, self.dock_drop_target(point, grab)) { - (Some(_), found @ DropTarget::Edge { .. }) | (None, found) => Some(found), - (Some(_), _) => None, + (Some(_), DropTarget::Join { .. }) => None, + (_, found) => Some(found), } } else { target @@ -284,6 +368,7 @@ impl OpenCADStudio { self.dock_drag = Some(DockDrag::Move { panel, group, + window, origin: Some(origin), target, grab, @@ -322,12 +407,30 @@ impl OpenCADStudio { return; }; let dx = point.x - last.x; - self.dock.resize_float( - panel, - if from_left { f.w - dx } else { f.w + dx }, - f.h + point.y - last.y, - from_left, - ); + 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, + ); + } } DockDrag::LayerColumn => { let Some(last) = last else { return }; @@ -362,13 +465,12 @@ impl OpenCADStudio { self.dock_drag = Some(drag); self.dock_drag_last = None; self.dock_edge_menu = None; + self.dock_float_menu = None; if peek.is_some() { self.dock_peek = peek; } } - /// 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 { @@ -386,6 +488,8 @@ 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); @@ -486,13 +590,8 @@ impl OpenCADStudio { if !self.dock.is_placed(PanelId::Layers) { let (ww, wh) = self.dock_workspace_size(); let (w, h) = self.dock_float_size(PanelId::Layers); - self.dock.float(FloatPanel { - id: PanelId::Layers, - x: ((ww - w) * 0.5).max(0.0), - y: ((wh - h) * 0.5).max(0.0), - w, - h, - }); + let (x, y) = (((ww - w) * 0.5).max(0.0), ((wh - h) * 0.5).max(0.0)); + self.dock.float(FloatPanel::new(PanelId::Layers, x, y, w, h)); self.save_config(); } self.dock.raise_float(PanelId::Layers); @@ -642,11 +741,37 @@ 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(); - ( - self.dock.width(id, self.win_size.0), - (avail * 0.6).clamp(crate::ui::dock::FLOAT_MIN_H, 520.0), - ) + (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() } /// 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 769d062e9..994b55464 100644 --- a/src/app/dock/view.rs +++ b/src/app/dock/view.rs @@ -17,16 +17,19 @@ 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 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)); + // 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)); } } // An open pallet menu hangs beside its edge's + button, over a @@ -51,13 +54,23 @@ impl OpenCADStudio { }; layers.push( mouse_area(Space::new().width(Fill).height(Fill)) - .on_press(Message::Dock(crate::ui::dock::DockMsg::EdgeMenu(None))) - .on_right_press(Message::Dock(crate::ui::dock::DockMsg::EdgeMenu(None))) + .on_press(Message::Dock(DockMsg::EdgeMenu(None))) + .on_right_press(Message::Dock(DockMsg::EdgeMenu(None))) .into(), ); - layers.push(place_at(dock_edge_menu(side, &checked), x.max(0.0), plus_top)); + 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)); } } + if let Some(menu) = self.dock_float_menu_overlay() { + 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))) + .into(), + ); + layers.push(menu); + } // While a panel is dragged, preview where it lands. if let Some((id, _, Some(target))) = self.dock_drag.and_then(|d| d.movement()) { layers.push(self.dock_drop_preview(id, target)); @@ -274,6 +287,7 @@ 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); @@ -422,27 +436,45 @@ impl OpenCADStudio { } } - /// 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: crate::ui::dock::FloatPanel, - tab: &'a DocumentTab, - ) -> Element<'a, Message> { - use crate::ui::dock::DockMsg; + /// 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 panel == f.id => { + 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_collapse(f.id) && self.dock_peek != Some(f.id); - let bar = floating_title_bar(f.id, self.dock.auto_collapse(f.id), bar_left); + 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. + fn floating_panel<'a>( + &'a self, + f: &'a crate::ui::dock::FloatPanel, + visible: Vec, + 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 bar = floating_title_bar(&visible, self.dock.auto_hides(anchor), bar_left); let content: Element<'_, Message> = if hidden { bar } else { @@ -461,14 +493,30 @@ impl OpenCADStudio { .center_x(Length::Fixed(14.0)) .center_y(Length::Fixed(14.0)), ) - .on_press(Message::Dock(DockMsg::FloatResizeGrab(f.id, grip_left))) + .on_press(Message::Dock(DockMsg::FloatResizeGrab(anchor, 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))) + .into(), + ); + prev = Some(pos); + } let body = stack![ - self.panel_body(f.id, f.w, true, tab), + column(pallets).height(Fill), container(grip) .width(Fill) .height(Fill) @@ -487,7 +535,6 @@ impl OpenCADStudio { row![body, bar].into() } }; - let total_w = if hidden { DOCK_FLOAT_BAR_W } else { f.w + DOCK_FLOAT_BAR_W }; let framed = container(content) .height(Length::Fixed(f.h)) .style(|theme: &Theme| container::Style { @@ -503,21 +550,106 @@ impl OpenCADStudio { }, ..Default::default() }); - // 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. + // 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. let panel = mouse_area(framed) - .on_press(Message::Dock(DockMsg::FloatRaise(f.id))) - .on_enter(Message::Dock(DockMsg::Hover(f.id))) + .on_press(Message::Dock(DockMsg::FloatRaise(anchor))) + .on_enter(Message::Dock(DockMsg::Hover(anchor))) .on_exit(Message::Dock(DockMsg::HoverExit)) .interaction(iced::mouse::Interaction::Idle); - // 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); 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), Dock left and Dock right. It + /// hangs beside the title bar, toward the window's inside. + 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() { + return None; + } + let (ww, _) = self.dock_workspace_size(); + let (x, y, bar_left, hidden) = self.dock_float_frame(f); + let total_w = if hidden { DOCK_FLOAT_BAR_W } else { f.w + DOCK_FLOAT_BAR_W }; + let bar_x = if bar_left { x } else { x + total_w - DOCK_FLOAT_BAR_W }; + let max_x = (ww - DOCK_MENU_W).max(0.0); + let menu_x = if bar_left { + bar_x + DOCK_FLOAT_BAR_W + 2.0 + } else { + bar_x - DOCK_MENU_W - 2.0 + } + .clamp(0.0, max_x); + 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 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 main = column![ + item(pallets.into(), DockMsg::FloatMenuPallets(!open), true), + container(separator).padding([3, 4]), + 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 + ), + ] + .spacing(1) + .padding(4); + let mut layers = vec![place_at(menu_frame(main.into()), menu_x, y + 4.0)]; + 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)); + // Beside the menu, on the side it opened toward, unless there is + // no room there. + let right = menu_x + 2.0 * DOCK_MENU_W + 2.0 <= ww; + let left = menu_x - DOCK_MENU_W - 2.0 >= 0.0; + let sub_x = if (bar_left && right) || !left { + menu_x + DOCK_MENU_W + 2.0 + } else { + menu_x - DOCK_MENU_W - 2.0 + } + .clamp(0.0, max_x); + layers.push(place_at(dock_pallet_menu(&checked, toggle), sub_x, y + 4.0)); + } + Some( + iced::widget::Stack::with_children(layers) + .width(Fill) + .height(Fill) + .into(), + ) + } + /// The live drop preview for dragged panel `id`: a tint over the target /// edge plus a panel-sized ghost where it lands — stacked into a group /// (sized as that group will be), as a new group of its own (full @@ -552,18 +684,32 @@ 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) = self.dock_float_size(id); + 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), + }; 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; 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) { + // 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()) { if let Some((top, bottom, w)) = self.dock_landing(id, target) { let x = match side { DockSide::Left => strip, @@ -578,11 +724,7 @@ 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 { - 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)); + layers.push(place_at(dock_icon_ghost(&moving), p.x - 16.0, p.y - 15.0)); } } iced::widget::Stack::with_children(layers) @@ -803,46 +945,64 @@ impl canvas::Program for VBarLabel { /// Width of a floating panel's vertical title bar. const DOCK_FLOAT_BAR_W: f32 = 28.0; -/// 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. +/// 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. fn floating_title_bar( - id: crate::ui::dock::PanelId, + visible: &[crate::ui::dock::PanelId], auto_collapse: bool, on_left: 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 close = crate::ui::dock::close_button(id, tip); - let pin = crate::ui::dock::pin_button(id, auto_collapse, tip); - let title = canvas(VBarLabel { - text: t!(id.title()).into_owned(), - }) - .width(Fill) - .height(Fill); - let bar = column![ - close, - pin, - container(title).width(Fill).height(Fill).padding([6, 0]), - container(crate::ui::icons::themed(id.icon(), 16.0)).center_x(Fill), - ] - .spacing(2) - .padding([4, 0]) - .align_x(iced::Center) - .width(Length::Fixed(DOCK_FLOAT_BAR_W)) - .height(Fill); + let close = crate::ui::dock::close_button(DockMsg::FloatClose(anchor), tip); + let pin = crate::ui::dock::pin_button(anchor, auto_collapse, tip); + let mut bar = column![close, pin].spacing(2); + 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), + ); + } + } + let bar = bar + .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::DockGrab(id))) + .on_press(Message::Dock(DockMsg::FloatGrab(anchor))) + .on_right_press(Message::Dock(DockMsg::FloatMenu(Some(anchor)))) // Double-click docks it against the edge its title bar faces. .on_double_click(Message::Dock(DockMsg::DockTo( - id, + anchor, if on_left { crate::app::config::DockSide::Left } else { @@ -853,6 +1013,29 @@ 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> { @@ -876,14 +1059,14 @@ 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; -/// 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, +/// 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( 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)| { @@ -902,26 +1085,34 @@ fn dock_edge_menu( .spacing(8) .align_y(iced::Center), ) - .on_press(Message::Dock(DockMsg::EdgeMenuToggle(side, id))) - .style(|theme: &Theme, status| { - let palette = theme.palette(); - button::Style { - background: matches!(status, button::Status::Hovered | button::Status::Pressed) - .then(|| Background::Color(palette.background.weak.color)), - text_color: palette.background.base.text, - border: Border { - radius: 3.0.into(), - ..Default::default() - }, - ..Default::default() - } - }) + .on_press(toggle(id)) + .style(menu_row_style) .padding([5, 8]) .width(Fill) .into() }) .collect(); - container(column(rows).spacing(1).padding(4)) + menu_frame(column(rows).spacing(1).padding(4).into()) +} + +/// A menu row button: transparent until hovered. +fn menu_row_style(theme: &Theme, status: button::Status) -> button::Style { + let palette = theme.palette(); + button::Style { + background: matches!(status, button::Status::Hovered | button::Status::Pressed) + .then(|| Background::Color(palette.background.weak.color)), + text_color: palette.background.base.text, + border: Border { + radius: 3.0.into(), + ..Default::default() + }, + ..Default::default() + } +} + +/// The raised box a dock menu sits in. +fn menu_frame(rows: Element<'static, Message>) -> Element<'static, Message> { + container(rows) .width(Length::Fixed(DOCK_MENU_W)) .style(|theme: &Theme| container::Style { background: Some(Background::Color(theme.palette().background.base.color)), diff --git a/src/app/mod.rs b/src/app/mod.rs index 53bb69fb7..0887f0718 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -865,6 +865,10 @@ 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. + pub(crate) dock_float_menu: Option, + pub(crate) dock_float_menu_pallets: bool, /// Group whose edge band (group grip) the pointer is over. pub(crate) dock_grip_hover: Option<(crate::app::config::DockSide, usize)>, /// Reference-table column currently being width-resized (column index), @@ -4297,6 +4301,8 @@ impl OpenCADStudio { dock_peek: None, dock_hover_pending: None, dock_hover_gen: 0, + dock_float_menu: None, + dock_float_menu_pallets: false, dock_drag: None, dock_drag_last: None, dock_title_hover: None, diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs index 8a8759359..730c2fe0c 100644 --- a/src/locale_catalog.rs +++ b/src/locale_catalog.rs @@ -5754,6 +5754,13 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "Invalid count area boundary object. Select a closed polyline consisting of line segments and does not intersect itself." => Some(("count-feature", "invalid-boundary")), "Block \"{part}\" cannot be found." => Some(("count-feature", "block-not-found")), "Invalid point." => Some(("count-feature", "invalid-point")), + "Auto-hide" => Some(("dock", "auto-hide")), + "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 4697811bc..d1fb5dd67 100644 --- a/src/ui/dock.rs +++ b/src/ui/dock.rs @@ -65,8 +65,29 @@ pub enum DockMsg { GripHoverEnd(DockSide, usize), /// Double-click on a docked title bar: float the panel. FloatOut(PanelId), - /// Double-click on a floating title bar: dock the panel on that side. + /// 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. 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`). + FloatMenu(Option), + /// Show (`true`) or hide the floating menu's Pallets submenu. + FloatMenuPallets(bool), + /// 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. @@ -308,24 +329,93 @@ where Ok(groups) } -/// A panel floating over the workspace, in workspace-local pixels. -#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +/// 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")] pub struct FloatPanel { - pub id: PanelId, + /// Pallets top → bottom with their heights (`width` is unused: the + /// window has its own). + #[serde(flatten)] + pub group: DockGroup, pub x: f32, pub y: f32, pub w: f32, pub h: f32, } +/// On-disk form of a floating window. Configs from before floating groups +/// stored one pallet as `id`. +#[derive(Deserialize)] +struct FloatRepr { + #[serde(default)] + id: Option, + #[serde(default)] + panels: Vec, + #[serde(default)] + weights: Vec, + x: f32, + y: f32, + w: f32, + h: f32, +} + +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 FloatPanel { + /// Pallet `id` floating on its own. + pub fn new(id: PanelId, x: f32, y: f32, w: f32, h: f32) -> Self { + Self { + group: DockGroup::single(id), + x, + y, + w, + h, + } + } + + /// 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. + /// positive side, and at least the minimum size. The window holds at + /// least one pallet. 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.id.max_width()); + self.w = finite_or(self.w, self.id().default_width()).clamp(DOCK_MIN_W, self.max_width()); self.h = finite_or(self.h, FLOAT_MIN_H).max(FLOAT_MIN_H); } } @@ -355,7 +445,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 panels, back → front. + /// Floating windows, 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. @@ -445,7 +535,18 @@ impl DockState { let shown = self.shown(side).min(len.saturating_sub(1)); self.show_group(side, shown); } - self.floating.retain(|f| seen.insert(f.id)); + 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.iter_mut().for_each(FloatPanel::heal); } @@ -459,9 +560,13 @@ impl DockState { None } - /// 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() + /// 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)) } /// Whether `id` has a place in the layout (docked or floating). A panel @@ -483,7 +588,7 @@ impl DockState { } /// The floating auto-hide flag of `id` (a docked pallet follows its - /// edge instead; see [`Self::auto_hides`]). + /// edge, a floating one its window; see [`Self::auto_hides`]). pub fn auto_collapse(&self, id: PanelId) -> bool { self.settings(id).auto_collapse } @@ -497,22 +602,31 @@ impl DockState { } /// Whether `id` auto-hides where it is: by its edge's setting when - /// docked, by its own when floating. + /// docked, by its window's (its first pallet's flag) when floating. pub fn auto_hides(&self, id: PanelId) -> bool { - match self.location(id) { - Some((side, _)) => self.edge_auto_hide(side), - None => self.auto_collapse(id), + 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), } } - /// Flip auto-hide where `id` is: its whole edge when docked, the pallet - /// itself when floating. Returns the new state. + /// Flip auto-hide where `id` is: its whole edge when docked, its whole + /// window 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 => self.set_auto_collapse(id, 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); + } + } } on } @@ -594,7 +708,13 @@ 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)> { - self.floating.retain(|f| f.id != id); + 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; + } 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() { @@ -692,27 +812,111 @@ impl DockState { true } - /// Float `id` at `rect`, on top of the other floating panels. + /// Float `rect`'s pallets in it, on top of the other floating windows. pub fn float(&mut self, rect: FloatPanel) -> bool { let before = self.clone(); - self.detach(rect.id); + for id in &rect.group.panels { + self.detach(*id); + } self.floating.push(rect); *self != before } - /// Bring a floating panel to the front. + /// 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 }; + 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 + } + + /// Bring the floating window holding `id` to the front. pub fn raise_float(&mut self, id: PanelId) { - if let Some(i) = self.floating.iter().position(|f| f.id == id) { + if let Some(i) = self.float_index(id) { let f = self.floating.remove(i); self.floating.push(f); } } - /// Resize a floating panel, keeping it at least a usable size. With - /// `keep_right` the right edge stays put (resizing from the left corner). + /// 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). pub fn resize_float(&mut self, id: PanelId, w: f32, h: f32, keep_right: bool) { - 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 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 keep_right { f.x += f.w - new_w; } @@ -733,18 +937,16 @@ impl DockState { delta: f32, min_weight: f32, ) { - 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; + 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 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. @@ -753,6 +955,26 @@ 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. @@ -766,6 +988,8 @@ 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, @@ -782,6 +1006,9 @@ 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, } @@ -792,6 +1019,19 @@ 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), @@ -992,7 +1232,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(id, tooltip::Position::Bottom)); + .push(close_button(DockMsg::Close(id), tooltip::Position::Bottom)); } mouse_area( container(bar) @@ -1034,10 +1274,10 @@ pub fn pin_button<'a>(id: PanelId, auto_collapse: bool, tip: tooltip::Position) .into() } -/// The close button of a panel's title bar. -pub fn close_button<'a>(id: PanelId, tip: tooltip::Position) -> Element<'a, Message> { +/// The close button of a panel's title bar, sending `close`. +pub fn close_button<'a>(close: DockMsg, tip: tooltip::Position) -> Element<'a, Message> { let close = button(crate::ui::icons::themed_secondary(crate::ui::icons::CLOSE, 12.0)) - .on_press(Message::Dock(DockMsg::Close(id))) + .on_press(Message::Dock(close)) .style(button::subtle) .padding([3, 5]); tooltip(close, text(crate::t!("Close")).size(10), tip) @@ -1218,13 +1458,7 @@ mod tests { let mut state = DockState::default(); state.join_group(PanelId::Browser, DockSide::Left, 0, 1); state.dock(PanelId::Count, DockSide::Left, 1); - state.float(FloatPanel { - id: PanelId::SheetSetManager, - x: 10.0, - y: 20.0, - w: 300.0, - h: 400.0, - }); + state.float(FloatPanel::new(PanelId::SheetSetManager, 10.0, 20.0, 300.0, 400.0)); let json = serde_json::to_string(&state).unwrap(); let back: DockState = serde_json::from_str(&json).unwrap(); assert_eq!(back, state); @@ -1294,13 +1528,7 @@ mod tests { state.dock(PanelId::Browser, DockSide::Left, 1); assert_eq!(state.shown(DockSide::Left), 1); // Floating Browser removes the last group; the edge shows group 0. - state.float(FloatPanel { - id: PanelId::Browser, - x: 0.0, - y: 0.0, - w: 250.0, - h: 300.0, - }); + state.float(FloatPanel::new(PanelId::Browser, 0.0, 0.0, 250.0, 300.0)); assert_eq!(state.left.len(), 1); assert_eq!(state.shown(DockSide::Left), 0); } @@ -1361,13 +1589,7 @@ mod tests { assert!(state.auto_hides(PanelId::Browser)); assert!(!state.auto_hides(PanelId::BlockPalette)); // A floating pallet keeps its own setting. - state.float(FloatPanel { - id: PanelId::Count, - x: 0.0, - y: 0.0, - w: 250.0, - h: 300.0, - }); + state.float(FloatPanel::new(PanelId::Count, 0.0, 0.0, 250.0, 300.0)); assert!(!state.auto_hides(PanelId::Count)); assert!(state.toggle_auto_hide(PanelId::Count)); assert!(state.auto_hides(PanelId::Count)); @@ -1420,17 +1642,11 @@ mod tests { #[test] fn float_detaches_and_dock_brings_it_back() { let mut state = DockState::default(); - let rect = FloatPanel { - id: PanelId::Properties, - x: 100.0, - y: 50.0, - w: 250.0, - h: 300.0, - }; - assert!(state.float(rect)); + let rect = FloatPanel::new(PanelId::Properties, 100.0, 50.0, 250.0, 300.0); + assert!(state.float(rect.clone())); 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()); @@ -1440,13 +1656,7 @@ mod tests { #[test] fn float_resize_respects_minimums() { let mut state = DockState::default(); - state.float(FloatPanel { - id: PanelId::Count, - x: 0.0, - y: 0.0, - w: 300.0, - h: 300.0, - }); + state.float(FloatPanel::new(PanelId::Count, 0.0, 0.0, 300.0, 300.0)); state.resize_float(PanelId::Count, 10.0, 10.0, false); let f = state.float_rect(PanelId::Count).unwrap(); assert_eq!((f.w, f.h), (DOCK_MIN_W, FLOAT_MIN_H)); @@ -1456,6 +1666,44 @@ mod tests { assert_eq!((f.x, f.w), (DOCK_MIN_W - 350.0, 350.0)); } + #[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}]}"#; + 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()); + } + #[test] fn every_panel_has_its_own_icon() { let ids = PanelId::ALL;