forked from 1b-agent/opencadstudio-upstream
Dock: floating groups, floating title bar menu, translations
- A docked group floats as one window: drag its edge band over the drawing or double-click the band. Floating windows hold a stack of pallets with splitters; the vertical title bar shows each pallet's icon (press one to drag that pallet out). Dragging the bar moves the whole window; dropping it on an edge docks or joins it as a group. - Right-click a floating title bar: Pallets submenu (add or hide pallets in the window), Dock left, Dock right. - Close on a floating bar closes every pallet in the window; its pin sets auto-hide for the whole window. - Older configs with single floating pallets still load. - Translate the new dock texts (Auto-hide, Dock left/right, New group, Pallets, Browser, Node Graph) in all 21 languages. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013RyGiMXcsf9HTKfqDxaQGt
This commit is contained in:
27 files changed
+1024
-225
No files matched your search
@@ -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 = مخطط العقد
|
||||
@@ -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 = Граф на възлите
|
||||
@@ -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ů
|
||||
@@ -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
|
||||
@@ -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 = Γράφος κόμβων
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 = नोड ग्राफ़
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 = ノードグラフ
|
||||
@@ -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 = 노드 그래프
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 = Граф узлов
|
||||
@@ -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
|
||||
@@ -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 = 节点图
|
||||
@@ -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 = 節點圖
|
||||
+46
-13
@@ -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));
|
||||
|
||||
+167
-42
@@ -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<crate::ui::dock::PanelId>) -> iced::Task<Message> {
|
||||
@@ -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<crate::ui::dock::PanelId> {
|
||||
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`:
|
||||
|
||||
+281
-90
@@ -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<Element<'_, Message>> = 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<crate::ui::dock::PanelId>,
|
||||
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<Element<'_, Message>> = Vec::new();
|
||||
let mut prev: Option<usize> = 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<Element<'_, Message>> {
|
||||
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<Element<'_, Message>> = 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<Message> 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<Element<'static, Message>> = 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)),
|
||||
|
||||
@@ -865,6 +865,10 @@ pub(super) struct OpenCADStudio {
|
||||
pub(crate) dock_title_hover: Option<crate::ui::dock::PanelId>,
|
||||
/// Edge whose icon-strip pallet menu (+) is open.
|
||||
pub(crate) dock_edge_menu: Option<crate::app::config::DockSide>,
|
||||
/// 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<crate::ui::dock::PanelId>,
|
||||
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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
+328
-80
@@ -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<PanelId>),
|
||||
/// 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<PanelId>,
|
||||
#[serde(default)]
|
||||
panels: Vec<PanelId>,
|
||||
#[serde(default)]
|
||||
weights: Vec<f32>,
|
||||
x: f32,
|
||||
y: f32,
|
||||
w: f32,
|
||||
h: f32,
|
||||
}
|
||||
|
||||
impl From<FloatRepr> 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<FloatPanel>,
|
||||
/// 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<FloatPanel> {
|
||||
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<usize> {
|
||||
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<iced::Point>,
|
||||
target: Option<DropTarget>,
|
||||
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;
|
||||
|
||||
Reference in new issue
Block a user