forked from 1b-agent/opencadstudio-upstream
Merge pull request #1702 from MarcelP102/Pallet-placement
[UX] Pallets you can dock, group and float (including LAYERS panel)
This commit is contained in:
commit
7ca457a6cc
54 files changed
+4245
-1098
No files matched your search
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><g transform="rotate(30 12 12)"><path d="M8 3 H16 L15 9 L18 12 V14 H6 V12 L9 9 Z"/><path d="M12 14 V21"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 270 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#B4B6B9" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9 L15 21 M3 15 L9 21 M3 21 L4 21"/></svg>
|
||||||
|
After Width: | Height: | Size: 208 B |
@@ -6176,3 +6176,12 @@ count-feature =
|
|||||||
.invalid-boundary = كائن حد منطقة العد غير صالح. حدد خطًا متعددًا مغلقًا يتكون من قطع مستقيمة ولا يتقاطع مع نفسه.
|
.invalid-boundary = كائن حد منطقة العد غير صالح. حدد خطًا متعددًا مغلقًا يتكون من قطع مستقيمة ولا يتقاطع مع نفسه.
|
||||||
.block-not-found = تعذر العثور على الكتلة "__ocs_fmt_0__".
|
.block-not-found = تعذر العثور على الكتلة "__ocs_fmt_0__".
|
||||||
.invalid-point = نقطة غير صالحة.
|
.invalid-point = نقطة غير صالحة.
|
||||||
|
|
||||||
|
dock =
|
||||||
|
.allow-docking = السماح بالإرساء
|
||||||
|
.auto-hide = إخفاء تلقائي
|
||||||
|
.dock-left = إرساء إلى اليسار
|
||||||
|
.dock-right = إرساء إلى اليمين
|
||||||
|
.new-group = مجموعة جديدة
|
||||||
|
.browser = المستعرض
|
||||||
|
.node-graph = مخطط العقد
|
||||||
@@ -6164,3 +6164,12 @@ count-feature =
|
|||||||
.invalid-boundary = Невалиден обект за граница на областта за преброяване. Изберете затворена полилиния от отсечки, която не се самопресича.
|
.invalid-boundary = Невалиден обект за граница на областта за преброяване. Изберете затворена полилиния от отсечки, която не се самопресича.
|
||||||
.block-not-found = Блокът "__ocs_fmt_0__" не може да бъде намерен.
|
.block-not-found = Блокът "__ocs_fmt_0__" не може да бъде намерен.
|
||||||
.invalid-point = Невалидна точка.
|
.invalid-point = Невалидна точка.
|
||||||
|
|
||||||
|
dock =
|
||||||
|
.allow-docking = Разреши закачане
|
||||||
|
.auto-hide = Автоматично скриване
|
||||||
|
.dock-left = Закачи вляво
|
||||||
|
.dock-right = Закачи вдясно
|
||||||
|
.new-group = Нова група
|
||||||
|
.browser = Браузър
|
||||||
|
.node-graph = Граф на възлите
|
||||||
@@ -6161,3 +6161,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.
|
.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.
|
.block-not-found = Blok "__ocs_fmt_0__" nelze najít.
|
||||||
.invalid-point = Neplatný bod.
|
.invalid-point = Neplatný bod.
|
||||||
|
|
||||||
|
dock =
|
||||||
|
.allow-docking = Povolit ukotvení
|
||||||
|
.auto-hide = Automaticky skrýt
|
||||||
|
.dock-left = Ukotvit vlevo
|
||||||
|
.dock-right = Ukotvit vpravo
|
||||||
|
.new-group = Nová skupina
|
||||||
|
.browser = Prohlížeč
|
||||||
|
.node-graph = Graf uzlů
|
||||||
@@ -6162,3 +6162,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.
|
.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.
|
.block-not-found = Block "__ocs_fmt_0__" wurde nicht gefunden.
|
||||||
.invalid-point = Ungültiger Punkt.
|
.invalid-point = Ungültiger Punkt.
|
||||||
|
|
||||||
|
dock =
|
||||||
|
.allow-docking = Andocken zulassen
|
||||||
|
.auto-hide = Automatisch ausblenden
|
||||||
|
.dock-left = Links andocken
|
||||||
|
.dock-right = Rechts andocken
|
||||||
|
.new-group = Neue Gruppe
|
||||||
|
.browser = Browser
|
||||||
|
.node-graph = Knotengraph
|
||||||
@@ -6164,3 +6164,12 @@ count-feature =
|
|||||||
.invalid-boundary = Μη έγκυρο αντικείμενο ορίου περιοχής καταμέτρησης. Επιλέξτε μια κλειστή πολυγραμμή από ευθύγραμμα τμήματα που δεν τέμνει τον εαυτό της.
|
.invalid-boundary = Μη έγκυρο αντικείμενο ορίου περιοχής καταμέτρησης. Επιλέξτε μια κλειστή πολυγραμμή από ευθύγραμμα τμήματα που δεν τέμνει τον εαυτό της.
|
||||||
.block-not-found = Το μπλοκ "__ocs_fmt_0__" δεν βρέθηκε.
|
.block-not-found = Το μπλοκ "__ocs_fmt_0__" δεν βρέθηκε.
|
||||||
.invalid-point = Μη έγκυρο σημείο.
|
.invalid-point = Μη έγκυρο σημείο.
|
||||||
|
|
||||||
|
dock =
|
||||||
|
.allow-docking = Να επιτρέπεται η πρόσδεση
|
||||||
|
.auto-hide = Αυτόματη απόκρυψη
|
||||||
|
.dock-left = Πρόσδεση αριστερά
|
||||||
|
.dock-right = Πρόσδεση δεξιά
|
||||||
|
.new-group = Νέα ομάδα
|
||||||
|
.browser = Περιηγητής
|
||||||
|
.node-graph = Γράφος κόμβων
|
||||||
@@ -6165,3 +6165,12 @@ count-feature =
|
|||||||
.invalid-boundary = Invalid count area boundary object. Select a closed polyline consisting of line segments and does not intersect itself.
|
.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.
|
.block-not-found = Block "__ocs_fmt_0__" cannot be found.
|
||||||
.invalid-point = Invalid point.
|
.invalid-point = Invalid point.
|
||||||
|
|
||||||
|
dock =
|
||||||
|
.allow-docking = Allow docking
|
||||||
|
.auto-hide = Auto-hide
|
||||||
|
.dock-left = Dock left
|
||||||
|
.dock-right = Dock right
|
||||||
|
.new-group = New group
|
||||||
|
.browser = Browser
|
||||||
|
.node-graph = Node Graph
|
||||||
@@ -6164,3 +6164,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.
|
.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__".
|
.block-not-found = No se encuentra el bloque "__ocs_fmt_0__".
|
||||||
.invalid-point = Punto no válido.
|
.invalid-point = Punto no válido.
|
||||||
|
|
||||||
|
dock =
|
||||||
|
.allow-docking = Permitir acoplar
|
||||||
|
.auto-hide = Ocultar automáticamente
|
||||||
|
.dock-left = Acoplar a la izquierda
|
||||||
|
.dock-right = Acoplar a la derecha
|
||||||
|
.new-group = Nuevo grupo
|
||||||
|
.browser = Explorador
|
||||||
|
.node-graph = Grafo de nodos
|
||||||
@@ -6161,3 +6161,12 @@ count-feature =
|
|||||||
.invalid-boundary = Virheellinen laskenta-alueen rajaobjekti. Valitse suljettu, janoista koostuva moniviiva, joka ei leikkaa itseään.
|
.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.
|
.block-not-found = Lohkoa "__ocs_fmt_0__" ei löydy.
|
||||||
.invalid-point = Virheellinen piste.
|
.invalid-point = Virheellinen piste.
|
||||||
|
|
||||||
|
dock =
|
||||||
|
.allow-docking = Salli telakointi
|
||||||
|
.auto-hide = Piilota automaattisesti
|
||||||
|
.dock-left = Telakoi vasemmalle
|
||||||
|
.dock-right = Telakoi oikealle
|
||||||
|
.new-group = Uusi ryhmä
|
||||||
|
.browser = Selain
|
||||||
|
.node-graph = Solmukaavio
|
||||||
@@ -6163,3 +6163,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.
|
.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.
|
.block-not-found = Le bloc "__ocs_fmt_0__" est introuvable.
|
||||||
.invalid-point = Point non valide.
|
.invalid-point = Point non valide.
|
||||||
|
|
||||||
|
dock =
|
||||||
|
.allow-docking = Autoriser l'ancrage
|
||||||
|
.auto-hide = Masquage automatique
|
||||||
|
.dock-left = Ancrer à gauche
|
||||||
|
.dock-right = Ancrer à droite
|
||||||
|
.new-group = Nouveau groupe
|
||||||
|
.browser = Navigateur
|
||||||
|
.node-graph = Graphe de nœuds
|
||||||
@@ -6152,3 +6152,12 @@ count-feature =
|
|||||||
.invalid-boundary = अमान्य गणना क्षेत्र सीमा ऑब्जेक्ट। रेखा खंडों से बनी ऐसी बंद पॉलीलाइन चुनें जो स्वयं को प्रतिच्छेद न करे।
|
.invalid-boundary = अमान्य गणना क्षेत्र सीमा ऑब्जेक्ट। रेखा खंडों से बनी ऐसी बंद पॉलीलाइन चुनें जो स्वयं को प्रतिच्छेद न करे।
|
||||||
.block-not-found = ब्लॉक "__ocs_fmt_0__" नहीं मिला।
|
.block-not-found = ब्लॉक "__ocs_fmt_0__" नहीं मिला।
|
||||||
.invalid-point = अमान्य बिंदु।
|
.invalid-point = अमान्य बिंदु।
|
||||||
|
|
||||||
|
dock =
|
||||||
|
.allow-docking = डॉकिंग की अनुमति दें
|
||||||
|
.auto-hide = स्वतः छिपाएँ
|
||||||
|
.dock-left = बाएँ डॉक करें
|
||||||
|
.dock-right = दाएँ डॉक करें
|
||||||
|
.new-group = नया समूह
|
||||||
|
.browser = ब्राउज़र
|
||||||
|
.node-graph = नोड ग्राफ़
|
||||||
@@ -6150,3 +6150,12 @@ count-feature =
|
|||||||
.invalid-boundary = Érvénytelen számlálási kontúrobjektum. Válasszon szakaszokból álló, zárt, önmagát nem metsző vonalláncot.
|
.invalid-boundary = Érvénytelen számlálási kontúrobjektum. Válasszon szakaszokból álló, zárt, önmagát nem metsző vonalláncot.
|
||||||
.block-not-found = A(z) "__ocs_fmt_0__" blokk nem található.
|
.block-not-found = A(z) "__ocs_fmt_0__" blokk nem található.
|
||||||
.invalid-point = Érvénytelen pont.
|
.invalid-point = Érvénytelen pont.
|
||||||
|
|
||||||
|
dock =
|
||||||
|
.allow-docking = Dokkolás engedélyezése
|
||||||
|
.auto-hide = Automatikus elrejtés
|
||||||
|
.dock-left = Dokkolás balra
|
||||||
|
.dock-right = Dokkolás jobbra
|
||||||
|
.new-group = Új csoport
|
||||||
|
.browser = Böngésző
|
||||||
|
.node-graph = Csomópontgráf
|
||||||
@@ -6163,3 +6163,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.
|
.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__".
|
.block-not-found = Impossibile trovare il blocco "__ocs_fmt_0__".
|
||||||
.invalid-point = Punto non valido.
|
.invalid-point = Punto non valido.
|
||||||
|
|
||||||
|
dock =
|
||||||
|
.allow-docking = Consenti ancoraggio
|
||||||
|
.auto-hide = Nascondi automaticamente
|
||||||
|
.dock-left = Ancora a sinistra
|
||||||
|
.dock-right = Ancora a destra
|
||||||
|
.new-group = Nuovo gruppo
|
||||||
|
.browser = Browser
|
||||||
|
.node-graph = Grafo dei nodi
|
||||||
@@ -6152,3 +6152,12 @@ count-feature =
|
|||||||
.invalid-boundary = カウント領域の境界オブジェクトが無効です。線分で構成され、自己交差しない閉じたポリラインを選択してください。
|
.invalid-boundary = カウント領域の境界オブジェクトが無効です。線分で構成され、自己交差しない閉じたポリラインを選択してください。
|
||||||
.block-not-found = ブロック "__ocs_fmt_0__" が見つかりません。
|
.block-not-found = ブロック "__ocs_fmt_0__" が見つかりません。
|
||||||
.invalid-point = 点が無効です。
|
.invalid-point = 点が無効です。
|
||||||
|
|
||||||
|
dock =
|
||||||
|
.allow-docking = ドッキングを許可
|
||||||
|
.auto-hide = 自動的に隠す
|
||||||
|
.dock-left = 左にドッキング
|
||||||
|
.dock-right = 右にドッキング
|
||||||
|
.new-group = 新しいグループ
|
||||||
|
.browser = ブラウザー
|
||||||
|
.node-graph = ノードグラフ
|
||||||
@@ -6163,3 +6163,12 @@ count-feature =
|
|||||||
.invalid-boundary = 개수 영역 경계 객체가 잘못되었습니다. 선분으로 구성되고 자체 교차하지 않는 닫힌 폴리선을 선택하십시오.
|
.invalid-boundary = 개수 영역 경계 객체가 잘못되었습니다. 선분으로 구성되고 자체 교차하지 않는 닫힌 폴리선을 선택하십시오.
|
||||||
.block-not-found = 블록 "__ocs_fmt_0__"을(를) 찾을 수 없습니다.
|
.block-not-found = 블록 "__ocs_fmt_0__"을(를) 찾을 수 없습니다.
|
||||||
.invalid-point = 점이 잘못되었습니다.
|
.invalid-point = 점이 잘못되었습니다.
|
||||||
|
|
||||||
|
dock =
|
||||||
|
.allow-docking = 도킹 허용
|
||||||
|
.auto-hide = 자동 숨기기
|
||||||
|
.dock-left = 왼쪽에 도킹
|
||||||
|
.dock-right = 오른쪽에 도킹
|
||||||
|
.new-group = 새 그룹
|
||||||
|
.browser = 브라우저
|
||||||
|
.node-graph = 노드 그래프
|
||||||
@@ -6165,3 +6165,12 @@ count-feature =
|
|||||||
.invalid-boundary = ວັດຖຸຂອບເຂດພື້ນທີ່ການນັບບໍ່ຖືກຕ້ອງ. ກະລຸນາເລືອກເສັ້ນ polyline ແບບປິດທີ່ປະກອບດ້ວຍສ່ວນເສັ້ນຊື່ ແລະ ບໍ່ຕັດກັນເອງ.
|
.invalid-boundary = ວັດຖຸຂອບເຂດພື້ນທີ່ການນັບບໍ່ຖືກຕ້ອງ. ກະລຸນາເລືອກເສັ້ນ polyline ແບບປິດທີ່ປະກອບດ້ວຍສ່ວນເສັ້ນຊື່ ແລະ ບໍ່ຕັດກັນເອງ.
|
||||||
.block-not-found = ບໍ່ພົບເຫັນບລັອກ "__ocs_fmt_0__".
|
.block-not-found = ບໍ່ພົບເຫັນບລັອກ "__ocs_fmt_0__".
|
||||||
.invalid-point = ຈຸດບໍ່ຖືກຕ້ອງ.
|
.invalid-point = ຈຸດບໍ່ຖືກຕ້ອງ.
|
||||||
|
|
||||||
|
dock =
|
||||||
|
.allow-docking = ອະນຸຍາດການຕິດຂອບ
|
||||||
|
.auto-hide = ເຊື່ອງອັດຕະໂນມັດ
|
||||||
|
.dock-left = ຕິດຂອບຊ້າຍ
|
||||||
|
.dock-right = ຕິດຂອບຂວາ
|
||||||
|
.new-group = ກຸ່ມໃໝ່
|
||||||
|
.browser = ບຣາວເຊີ
|
||||||
|
.node-graph = ກຣາຟໂນດ
|
||||||
@@ -6162,3 +6162,12 @@ count-feature =
|
|||||||
.invalid-boundary = Ongeldig grensobject voor het telgebied. Selecteer een gesloten polylijn die uit lijnsegmenten bestaat en zichzelf niet snijdt.
|
.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.
|
.block-not-found = Blok "__ocs_fmt_0__" kan niet worden gevonden.
|
||||||
.invalid-point = Ongeldig punt.
|
.invalid-point = Ongeldig punt.
|
||||||
|
|
||||||
|
dock =
|
||||||
|
.allow-docking = Koppelen toestaan
|
||||||
|
.auto-hide = Automatisch verbergen
|
||||||
|
.dock-left = Links koppelen
|
||||||
|
.dock-right = Rechts koppelen
|
||||||
|
.new-group = Nieuwe groep
|
||||||
|
.browser = Browser
|
||||||
|
.node-graph = Knooppuntgrafiek
|
||||||
@@ -6163,3 +6163,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.
|
.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__".
|
.block-not-found = Nie można znaleźć bloku "__ocs_fmt_0__".
|
||||||
.invalid-point = Nieprawidłowy punkt.
|
.invalid-point = Nieprawidłowy punkt.
|
||||||
|
|
||||||
|
dock =
|
||||||
|
.allow-docking = Zezwalaj na dokowanie
|
||||||
|
.auto-hide = Autoukrywanie
|
||||||
|
.dock-left = Zadokuj po lewej
|
||||||
|
.dock-right = Zadokuj po prawej
|
||||||
|
.new-group = Nowa grupa
|
||||||
|
.browser = Przeglądarka
|
||||||
|
.node-graph = Graf węzłów
|
||||||
@@ -6164,3 +6164,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.
|
.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.
|
.block-not-found = O bloco "__ocs_fmt_0__" não foi encontrado.
|
||||||
.invalid-point = Ponto inválido.
|
.invalid-point = Ponto inválido.
|
||||||
|
|
||||||
|
dock =
|
||||||
|
.allow-docking = Permitir encaixe
|
||||||
|
.auto-hide = Ocultar automaticamente
|
||||||
|
.dock-left = Encaixar à esquerda
|
||||||
|
.dock-right = Encaixar à direita
|
||||||
|
.new-group = Novo grupo
|
||||||
|
.browser = Navegador
|
||||||
|
.node-graph = Grafo de nós
|
||||||
@@ -6165,3 +6165,12 @@ count-feature =
|
|||||||
.invalid-boundary = Недопустимый объект границы области подсчёта. Выберите замкнутую полилинию из отрезков, которая не пересекает саму себя.
|
.invalid-boundary = Недопустимый объект границы области подсчёта. Выберите замкнутую полилинию из отрезков, которая не пересекает саму себя.
|
||||||
.block-not-found = Блок "__ocs_fmt_0__" не найден.
|
.block-not-found = Блок "__ocs_fmt_0__" не найден.
|
||||||
.invalid-point = Недопустимая точка.
|
.invalid-point = Недопустимая точка.
|
||||||
|
|
||||||
|
dock =
|
||||||
|
.allow-docking = Разрешить закрепление
|
||||||
|
.auto-hide = Автоскрытие
|
||||||
|
.dock-left = Закрепить слева
|
||||||
|
.dock-right = Закрепить справа
|
||||||
|
.new-group = Новая группа
|
||||||
|
.browser = Обозреватель
|
||||||
|
.node-graph = Граф узлов
|
||||||
@@ -6087,3 +6087,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.
|
.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ı.
|
.block-not-found = "__ocs_fmt_0__" bloğu bulunamadı.
|
||||||
.invalid-point = Geçersiz nokta.
|
.invalid-point = Geçersiz nokta.
|
||||||
|
|
||||||
|
dock =
|
||||||
|
.allow-docking = Yerleştirmeye izin ver
|
||||||
|
.auto-hide = Otomatik gizle
|
||||||
|
.dock-left = Sola yerleştir
|
||||||
|
.dock-right = Sağa yerleştir
|
||||||
|
.new-group = Yeni grup
|
||||||
|
.browser = Tarayıcı
|
||||||
|
.node-graph = Düğüm Grafiği
|
||||||
@@ -6147,3 +6147,12 @@ count-feature =
|
|||||||
.invalid-boundary = 无效的计数区域边界对象。请选择由直线段组成且不自交的闭合多段线。
|
.invalid-boundary = 无效的计数区域边界对象。请选择由直线段组成且不自交的闭合多段线。
|
||||||
.block-not-found = 找不到块 "__ocs_fmt_0__"。
|
.block-not-found = 找不到块 "__ocs_fmt_0__"。
|
||||||
.invalid-point = 无效的点。
|
.invalid-point = 无效的点。
|
||||||
|
|
||||||
|
dock =
|
||||||
|
.allow-docking = 允许停靠
|
||||||
|
.auto-hide = 自动隐藏
|
||||||
|
.dock-left = 停靠到左侧
|
||||||
|
.dock-right = 停靠到右侧
|
||||||
|
.new-group = 新建组
|
||||||
|
.browser = 浏览器
|
||||||
|
.node-graph = 节点图
|
||||||
@@ -6159,3 +6159,12 @@ count-feature =
|
|||||||
.invalid-boundary = 無效的計數區域邊界物件。請選取由線段組成且不自我相交的封閉聚合線。
|
.invalid-boundary = 無效的計數區域邊界物件。請選取由線段組成且不自我相交的封閉聚合線。
|
||||||
.block-not-found = 找不到圖塊 "__ocs_fmt_0__"。
|
.block-not-found = 找不到圖塊 "__ocs_fmt_0__"。
|
||||||
.invalid-point = 無效的點。
|
.invalid-point = 無效的點。
|
||||||
|
|
||||||
|
dock =
|
||||||
|
.allow-docking = 允許停駐
|
||||||
|
.auto-hide = 自動隱藏
|
||||||
|
.dock-left = 停駐到左側
|
||||||
|
.dock-right = 停駐到右側
|
||||||
|
.new-group = 新增群組
|
||||||
|
.browser = 瀏覽器
|
||||||
|
.node-graph = 節點圖
|
||||||
@@ -929,18 +929,7 @@ impl OpenCADStudio {
|
|||||||
if self.show_external_references {
|
if self.show_external_references {
|
||||||
// Always open expanded so the panel is immediately usable;
|
// Always open expanded so the panel is immediately usable;
|
||||||
// dock it on the right if the user closed it from the layout.
|
// dock it on the right if the user closed it from the layout.
|
||||||
if self
|
self.dock_open_at_default(crate::ui::dock::PanelId::ExternalReferences);
|
||||||
.dock
|
|
||||||
.location(crate::ui::dock::PanelId::ExternalReferences)
|
|
||||||
.is_none()
|
|
||||||
{
|
|
||||||
self.dock.dock(
|
|
||||||
crate::ui::dock::PanelId::ExternalReferences,
|
|
||||||
crate::app::config::DockSide::Right,
|
|
||||||
usize::MAX,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
self.dock_expanded = Some(crate::ui::dock::PanelId::ExternalReferences);
|
|
||||||
self.refresh_xref_manager();
|
self.refresh_xref_manager();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1784,7 +1773,7 @@ mod tests {
|
|||||||
let _ = app.run_command_line("BLOCKPALETTE");
|
let _ = app.run_command_line("BLOCKPALETTE");
|
||||||
assert!(app.show_block_palette);
|
assert!(app.show_block_palette);
|
||||||
assert!(
|
assert!(
|
||||||
app.dock_expanded == Some(crate::ui::dock::PanelId::BlockPalette),
|
app.dock_peek == Some(crate::ui::dock::PanelId::BlockPalette),
|
||||||
"palette must open expanded, not as the collapsed bar"
|
"palette must open expanded, not as the collapsed bar"
|
||||||
);
|
);
|
||||||
// BLOCKSPALETTE only opens; BLOCKSPALETTECLOSE closes.
|
// BLOCKSPALETTE only opens; BLOCKSPALETTECLOSE closes.
|
||||||
|
|||||||
@@ -259,13 +259,9 @@ impl OpenCADStudio {
|
|||||||
self.count_palette.show = open;
|
self.count_palette.show = open;
|
||||||
self.ribbon.set_count_palette(open);
|
self.ribbon.set_count_palette(open);
|
||||||
if open {
|
if open {
|
||||||
if self.dock.location(id).is_none() {
|
self.dock_open_at_default(id);
|
||||||
self.dock
|
} else {
|
||||||
.dock(id, crate::app::config::DockSide::Right, usize::MAX);
|
self.dock_unpeek(id);
|
||||||
}
|
|
||||||
self.dock_expanded = Some(id);
|
|
||||||
} else if self.dock_expanded == Some(id) {
|
|
||||||
self.dock_expanded = None;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -384,7 +384,9 @@ impl OpenCADStudio {
|
|||||||
"ORTHO" => return Some(Task::done(Message::ToggleOrtho)),
|
"ORTHO" => return Some(Task::done(Message::ToggleOrtho)),
|
||||||
"PARALLEL" => return Some(Task::done(Message::SetProjection(true))),
|
"PARALLEL" => return Some(Task::done(Message::SetProjection(true))),
|
||||||
"PERSP" => return Some(Task::done(Message::SetProjection(false))),
|
"PERSP" => return Some(Task::done(Message::SetProjection(false))),
|
||||||
"LAYERS" => return Some(Task::done(Message::ToggleLayers)),
|
// LAYERS opens the Layer Manager pallet (its close button or
|
||||||
|
// the toggle message close it).
|
||||||
|
"LAYERS" => self.open_layers_pallet(),
|
||||||
|
|
||||||
// SCRIPT <path> — run a command script: each non-comment
|
// SCRIPT <path> — run a command script: each non-comment
|
||||||
// line is fed through the same command path the `--script` startup
|
// line is fed through the same command path the `--script` startup
|
||||||
|
|||||||
@@ -704,15 +704,7 @@ impl OpenCADStudio {
|
|||||||
// rearrange themselves on upgrade for a panel they have
|
// rearrange themselves on upgrade for a panel they have
|
||||||
// not asked for.
|
// not asked for.
|
||||||
let id = crate::ui::dock::PanelId::Browser;
|
let id = crate::ui::dock::PanelId::Browser;
|
||||||
if self.dock.location(id).is_none() {
|
self.dock_open_at_default(id);
|
||||||
// dock() clamps an out-of-range index to the end of
|
|
||||||
// the edge, so this appends below whatever is there.
|
|
||||||
self.dock
|
|
||||||
.dock(id, crate::app::config::DockSide::Right, usize::MAX);
|
|
||||||
}
|
|
||||||
// Open expanded so it is usable straight away; the pin
|
|
||||||
// button still collapses it.
|
|
||||||
self.dock_expanded = Some(id);
|
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_output(crate::t!("Browser opened.").as_ref());
|
.push_output(crate::t!("Browser opened.").as_ref());
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -444,17 +444,11 @@ impl OpenCADStudio {
|
|||||||
"POINTCLOUDMANAGER" => {
|
"POINTCLOUDMANAGER" => {
|
||||||
let id = crate::ui::dock::PanelId::PointCloudManager;
|
let id = crate::ui::dock::PanelId::PointCloudManager;
|
||||||
self.pc_manager.show = true;
|
self.pc_manager.show = true;
|
||||||
if self.dock.location(id).is_none() {
|
self.dock_open_at_default(id);
|
||||||
self.dock
|
|
||||||
.dock(id, crate::app::config::DockSide::Right, usize::MAX);
|
|
||||||
}
|
|
||||||
self.dock_expanded = Some(id);
|
|
||||||
}
|
}
|
||||||
"POINTCLOUDMANAGERCLOSE" => {
|
"POINTCLOUDMANAGERCLOSE" => {
|
||||||
self.pc_manager.show = false;
|
self.pc_manager.show = false;
|
||||||
if self.dock_expanded == Some(crate::ui::dock::PanelId::PointCloudManager) {
|
self.dock_unpeek(crate::ui::dock::PanelId::PointCloudManager);
|
||||||
self.dock_expanded = None;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
"_PCUNCROP" => self
|
"_PCUNCROP" => self
|
||||||
.edit_selected_point_clouds(i, "POINTCLOUDUNCROP", |data| data.croppings.clear()),
|
.edit_selected_point_clouds(i, "POINTCLOUDUNCROP", |data| data.croppings.clear()),
|
||||||
|
|||||||
@@ -660,13 +660,9 @@ impl OpenCADStudio {
|
|||||||
self.sheet_set.show = on;
|
self.sheet_set.show = on;
|
||||||
self.ribbon.set_sheet_set(on);
|
self.ribbon.set_sheet_set(on);
|
||||||
if on {
|
if on {
|
||||||
if self.dock.location(id).is_none() {
|
self.dock_open_at_default(id);
|
||||||
self.dock
|
} else {
|
||||||
.dock(id, crate::app::config::DockSide::Right, usize::MAX);
|
self.dock_unpeek(id);
|
||||||
}
|
|
||||||
self.dock_expanded = Some(id);
|
|
||||||
} else if self.dock_expanded == Some(id) {
|
|
||||||
self.dock_expanded = None;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
//! The app side of docking: pallets docked on the drawing view's edges (one
|
||||||
|
//! group shown per edge, listed in an icon strip) or floating over it.
|
||||||
|
//!
|
||||||
|
//! The persisted layout and its pure geometry live in [`crate::ui::dock`];
|
||||||
|
//! this module holds the transient state handling (`update`) and drawing
|
||||||
|
//! (`view`).
|
||||||
|
|
||||||
|
mod update;
|
||||||
|
mod view;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
@@ -0,0 +1,670 @@
|
|||||||
|
//! Dock behaviour through the app: drags, hover, menus, and the real view.
|
||||||
|
|
||||||
|
use crate::app::{Message, OpenCADStudio};
|
||||||
|
|
||||||
|
fn fresh() -> OpenCADStudio {
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
app.automation_op(r#"{"op":"new"}"#);
|
||||||
|
app
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn blockpalette_pin_toggles_autocollapse_and_close_hides() {
|
||||||
|
let mut app = fresh();
|
||||||
|
app.show_block_palette = true;
|
||||||
|
let id = crate::ui::dock::PanelId::BlockPalette;
|
||||||
|
let _ = app.on_dock(crate::ui::dock::DockMsg::AutoCollapseToggle(id));
|
||||||
|
assert!(app.dock.auto_hides(id), "pin enables auto-hide");
|
||||||
|
let _ = app.on_dock(crate::ui::dock::DockMsg::AutoCollapseToggle(id));
|
||||||
|
assert!(!app.dock.auto_hides(id), "second pin disables auto-hide");
|
||||||
|
let _ = app.on_dock(crate::ui::dock::DockMsg::Close(id));
|
||||||
|
assert!(!app.show_block_palette, "close dismisses the sidebar");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A hermetic app with a 1600×900 workspace and the default dock layout
|
||||||
|
/// (Properties left, block palette right), both shown.
|
||||||
|
fn dock_app() -> OpenCADStudio {
|
||||||
|
let mut app = fresh();
|
||||||
|
// Tests load the user's persisted config; reset the dock to a known
|
||||||
|
// state so this stays hermetic.
|
||||||
|
app.dock = Default::default();
|
||||||
|
app.dock.ensure_settings();
|
||||||
|
app.show_properties = true;
|
||||||
|
app.show_block_palette = true;
|
||||||
|
let i = app.active_tab;
|
||||||
|
app.tabs[i].scene.selection.borrow_mut().view.vp_size = (1600.0, 900.0);
|
||||||
|
app.win_size = (1600.0, 900.0).into();
|
||||||
|
app
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Where the current dock drag would land, if it has started.
|
||||||
|
fn drag_target(app: &OpenCADStudio) -> Option<crate::ui::dock::DropTarget> {
|
||||||
|
app.dock_drag
|
||||||
|
.and_then(|d| d.movement())
|
||||||
|
.and_then(|(_, _, target)| target)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Press `id`'s title bar at `from`, drag to `to`, and return the target
|
||||||
|
/// shown just before release.
|
||||||
|
fn drag_panel(
|
||||||
|
app: &mut OpenCADStudio,
|
||||||
|
id: crate::ui::dock::PanelId,
|
||||||
|
from: iced::Point,
|
||||||
|
to: iced::Point,
|
||||||
|
) -> Option<crate::ui::dock::DropTarget> {
|
||||||
|
use crate::ui::dock::DockMsg;
|
||||||
|
let _ = app.on_dock(DockMsg::DockGrab(id));
|
||||||
|
let _ = app.on_dock(DockMsg::DragMove(from));
|
||||||
|
let _ = app.on_dock(DockMsg::DragMove(to));
|
||||||
|
let target = drag_target(app);
|
||||||
|
let _ = app.on_dock(DockMsg::DragRelease);
|
||||||
|
target
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dropping_on_a_pallets_top_or_bottom_half_stacks_above_or_below_it() {
|
||||||
|
use crate::app::config::DockSide;
|
||||||
|
use crate::ui::dock::{DropTarget, PanelId};
|
||||||
|
let mut app = dock_app();
|
||||||
|
let id = PanelId::BlockPalette;
|
||||||
|
assert_eq!(app.dock.location(id), Some((DockSide::Right, 0)));
|
||||||
|
let target = drag_panel(
|
||||||
|
&mut app,
|
||||||
|
id,
|
||||||
|
iced::Point::new(1500.0, 10.0),
|
||||||
|
iced::Point::new(150.0, 300.0),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
target,
|
||||||
|
Some(DropTarget::Join {
|
||||||
|
side: DockSide::Left,
|
||||||
|
group: 0,
|
||||||
|
index: 0
|
||||||
|
})
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
app.dock.left[0].panels,
|
||||||
|
vec![PanelId::BlockPalette, PanelId::Properties]
|
||||||
|
);
|
||||||
|
assert!(app.dock.right.is_empty());
|
||||||
|
// The bottom half of a pallet stacks below it.
|
||||||
|
let target = drag_panel(
|
||||||
|
&mut app,
|
||||||
|
id,
|
||||||
|
iced::Point::new(150.0, 100.0),
|
||||||
|
iced::Point::new(150.0, 800.0),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
target,
|
||||||
|
Some(DropTarget::Join {
|
||||||
|
side: DockSide::Left,
|
||||||
|
group: 0,
|
||||||
|
index: 2
|
||||||
|
})
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
app.dock.left[0].panels,
|
||||||
|
vec![PanelId::Properties, PanelId::BlockPalette]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dock_dragging_a_lone_pallet_over_its_own_group_is_a_no_op() {
|
||||||
|
use crate::app::config::DockSide;
|
||||||
|
use crate::ui::dock::{DropTarget, PanelId};
|
||||||
|
let mut app = dock_app();
|
||||||
|
let before = app.dock.clone();
|
||||||
|
// The right edge's only group holds just the block palette; drag it
|
||||||
|
// around over itself.
|
||||||
|
let target = drag_panel(
|
||||||
|
&mut app,
|
||||||
|
PanelId::BlockPalette,
|
||||||
|
iced::Point::new(1500.0, 10.0),
|
||||||
|
iced::Point::new(1450.0, 600.0),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
target,
|
||||||
|
Some(DropTarget::Join {
|
||||||
|
side: DockSide::Right,
|
||||||
|
group: 0,
|
||||||
|
index: 1
|
||||||
|
})
|
||||||
|
);
|
||||||
|
assert_eq!(app.dock, before);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dock_click_without_drag_changes_nothing() {
|
||||||
|
let mut app = dock_app();
|
||||||
|
let before = app.dock.clone();
|
||||||
|
let id = crate::ui::dock::PanelId::BlockPalette;
|
||||||
|
let p = iced::Point::new(1500.0, 10.0);
|
||||||
|
let target = drag_panel(&mut app, id, p, iced::Point::new(1502.0, 11.0));
|
||||||
|
assert_eq!(target, None, "movement under the threshold is a click");
|
||||||
|
assert_eq!(app.dock, before);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dock_drop_below_the_strip_groups_starts_a_new_shown_group() {
|
||||||
|
use crate::app::config::DockSide;
|
||||||
|
use crate::ui::dock::{DropTarget, PanelId};
|
||||||
|
let mut app = dock_app();
|
||||||
|
let (_, plus_top) = app.dock_strip_layout(DockSide::Left);
|
||||||
|
let target = drag_panel(
|
||||||
|
&mut app,
|
||||||
|
PanelId::BlockPalette,
|
||||||
|
iced::Point::new(1500.0, 10.0),
|
||||||
|
iced::Point::new(18.0, plus_top + 10.0),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
target,
|
||||||
|
Some(DropTarget::Edge {
|
||||||
|
side: DockSide::Left,
|
||||||
|
index: 1
|
||||||
|
})
|
||||||
|
);
|
||||||
|
assert_eq!(app.dock.left.len(), 2);
|
||||||
|
assert_eq!(app.dock.left[1].panels, vec![PanelId::BlockPalette]);
|
||||||
|
// The new group is the one shown: the column holds only the palette.
|
||||||
|
assert_eq!(app.dock_shown_group(DockSide::Left), Some(1));
|
||||||
|
assert_eq!(app.dock_slot_spans(DockSide::Left), vec![(0, 0.0, 900.0)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dock_icon_press_switches_the_shown_group() {
|
||||||
|
use crate::app::config::DockSide;
|
||||||
|
use crate::ui::dock::{DockGroup, DockMsg, PanelId};
|
||||||
|
let mut app = dock_app();
|
||||||
|
app.show_browser = true;
|
||||||
|
app.dock.left = vec![
|
||||||
|
DockGroup::stack(vec![PanelId::Properties, PanelId::BlockPalette]),
|
||||||
|
DockGroup::single(PanelId::Browser),
|
||||||
|
];
|
||||||
|
app.dock.right.clear();
|
||||||
|
// Group 0 shows both stacked pallets.
|
||||||
|
assert_eq!(app.dock_slot_spans(DockSide::Left).len(), 2);
|
||||||
|
let _ = app.on_dock(DockMsg::IconPress(PanelId::Browser));
|
||||||
|
let _ = app.on_dock(DockMsg::DragRelease);
|
||||||
|
assert_eq!(app.dock_shown_group(DockSide::Left), Some(1));
|
||||||
|
assert_eq!(app.dock_slot_spans(DockSide::Left), vec![(0, 0.0, 900.0)]);
|
||||||
|
// Each group keeps its own width.
|
||||||
|
app.dock.set_group_width(DockSide::Left, 1, 400.0);
|
||||||
|
assert_eq!(app.dock_column_width(DockSide::Left), 400.0);
|
||||||
|
let _ = app.on_dock(DockMsg::IconPress(PanelId::BlockPalette));
|
||||||
|
let _ = app.on_dock(DockMsg::DragRelease);
|
||||||
|
assert_eq!(app.dock_column_width(DockSide::Left), 260.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dock_drop_over_the_viewport_floats_the_panel() {
|
||||||
|
use crate::ui::dock::{DropTarget, PanelId};
|
||||||
|
let mut app = dock_app();
|
||||||
|
let target = drag_panel(
|
||||||
|
&mut app,
|
||||||
|
PanelId::BlockPalette,
|
||||||
|
iced::Point::new(1500.0, 10.0),
|
||||||
|
iced::Point::new(800.0, 400.0),
|
||||||
|
);
|
||||||
|
assert!(matches!(target, Some(DropTarget::Float { .. })));
|
||||||
|
assert!(app.dock.right.is_empty());
|
||||||
|
let f = app
|
||||||
|
.dock
|
||||||
|
.float_rect(PanelId::BlockPalette)
|
||||||
|
.expect("floating");
|
||||||
|
assert!(f.x > 600.0 && f.x < 800.0 && f.y > 350.0 && f.y < 400.0);
|
||||||
|
// Dragging a floating panel keeps the grab point under the pointer.
|
||||||
|
let target = drag_panel(
|
||||||
|
&mut app,
|
||||||
|
PanelId::BlockPalette,
|
||||||
|
iced::Point::new(f.x + 20.0, f.y + 10.0),
|
||||||
|
iced::Point::new(f.x + 120.0, f.y + 60.0),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
target,
|
||||||
|
Some(DropTarget::Float {
|
||||||
|
x: f.x + 100.0,
|
||||||
|
y: f.y + 50.0
|
||||||
|
})
|
||||||
|
);
|
||||||
|
// Opening a floating panel must not re-dock it.
|
||||||
|
assert!(app.dock.is_placed(PanelId::BlockPalette));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dock_dragging_an_icon_into_another_group_lands_between_its_icons() {
|
||||||
|
use crate::app::config::DockSide;
|
||||||
|
use crate::ui::dock::{DockGroup, DockMsg, DropTarget, PanelId, STRIP_CELL_H};
|
||||||
|
let mut app = dock_app();
|
||||||
|
app.show_browser = true;
|
||||||
|
app.dock.left = vec![
|
||||||
|
DockGroup::stack(vec![PanelId::Properties, PanelId::Browser]),
|
||||||
|
DockGroup::single(PanelId::BlockPalette),
|
||||||
|
];
|
||||||
|
app.dock.right.clear();
|
||||||
|
let (layout, _) = app.dock_strip_layout(DockSide::Left);
|
||||||
|
// Drag the Blocks icon (group 1) to between Properties and Browser.
|
||||||
|
let _ = app.on_dock(DockMsg::IconPress(PanelId::BlockPalette));
|
||||||
|
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(
|
||||||
|
20.0,
|
||||||
|
layout[1].icons_top + 10.0,
|
||||||
|
)));
|
||||||
|
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(
|
||||||
|
20.0,
|
||||||
|
layout[0].icons_top + STRIP_CELL_H + 2.0,
|
||||||
|
)));
|
||||||
|
assert_eq!(
|
||||||
|
drag_target(&app),
|
||||||
|
Some(DropTarget::Join {
|
||||||
|
side: DockSide::Left,
|
||||||
|
group: 0,
|
||||||
|
index: 1
|
||||||
|
})
|
||||||
|
);
|
||||||
|
let _ = app.on_dock(DockMsg::DragRelease);
|
||||||
|
assert_eq!(app.dock.left.len(), 1, "the emptied group goes away");
|
||||||
|
assert_eq!(
|
||||||
|
app.dock.left[0].panels,
|
||||||
|
vec![PanelId::Properties, PanelId::BlockPalette, PanelId::Browser]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dock_grip_drag_moves_a_whole_group() {
|
||||||
|
use crate::app::config::DockSide;
|
||||||
|
use crate::ui::dock::{DockGroup, DockMsg, PanelId};
|
||||||
|
let mut app = dock_app();
|
||||||
|
app.show_browser = true;
|
||||||
|
app.dock.left = vec![
|
||||||
|
DockGroup::stack(vec![PanelId::Properties, PanelId::Browser]),
|
||||||
|
DockGroup::single(PanelId::BlockPalette),
|
||||||
|
];
|
||||||
|
app.dock.right.clear();
|
||||||
|
let (layout, plus_top) = app.dock_strip_layout(DockSide::Left);
|
||||||
|
let _ = app.on_dock(DockMsg::GroupGrab(DockSide::Left, 0));
|
||||||
|
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(
|
||||||
|
18.0,
|
||||||
|
layout[0].top + 2.0,
|
||||||
|
)));
|
||||||
|
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(18.0, plus_top + 10.0)));
|
||||||
|
let _ = app.on_dock(DockMsg::DragRelease);
|
||||||
|
assert_eq!(app.dock.left[0].panels, vec![PanelId::BlockPalette]);
|
||||||
|
assert_eq!(
|
||||||
|
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();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dock_closed_pallets_take_no_room_and_empty_groups_hide() {
|
||||||
|
use crate::app::config::DockSide;
|
||||||
|
use crate::ui::dock::{DockGroup, PanelId};
|
||||||
|
let mut app = dock_app();
|
||||||
|
app.dock.left = vec![
|
||||||
|
DockGroup::stack(vec![PanelId::BlockPalette, PanelId::Properties]),
|
||||||
|
DockGroup::single(PanelId::Browser),
|
||||||
|
];
|
||||||
|
app.show_block_palette = false;
|
||||||
|
app.show_browser = false;
|
||||||
|
// Only Properties is open: one listed group, full height.
|
||||||
|
assert_eq!(app.dock_visible_groups(DockSide::Left), vec![0]);
|
||||||
|
assert_eq!(app.dock_slot_spans(DockSide::Left), vec![(1, 0.0, 900.0)]);
|
||||||
|
// A shown group with nothing open falls back to one that has.
|
||||||
|
app.dock.show_group(DockSide::Left, 1);
|
||||||
|
assert_eq!(app.dock_shown_group(DockSide::Left), Some(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dock_splitter_drag_changes_pallet_heights() {
|
||||||
|
use crate::app::config::DockSide;
|
||||||
|
use crate::ui::dock::{DockGroup, DockMsg, PanelId};
|
||||||
|
let mut app = dock_app();
|
||||||
|
app.dock.left = vec![DockGroup::stack(vec![
|
||||||
|
PanelId::BlockPalette,
|
||||||
|
PanelId::Properties,
|
||||||
|
])];
|
||||||
|
app.dock.right.clear();
|
||||||
|
let _ = app.on_dock(DockMsg::SplitGrab(DockSide::Left, 0, 0, 1));
|
||||||
|
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(100.0, 450.0)));
|
||||||
|
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(100.0, 675.0)));
|
||||||
|
let _ = app.on_dock(DockMsg::DragRelease);
|
||||||
|
// 225 px of a 900 px edge holding 2 weight units = 0.5 units.
|
||||||
|
let w = &app.dock.left[0].weights;
|
||||||
|
assert!((w[0] - 1.5).abs() < 1e-4 && (w[1] - 0.5).abs() < 1e-4);
|
||||||
|
let _ = app.on_dock(DockMsg::SplitReset(DockSide::Left, 0));
|
||||||
|
assert_eq!(app.dock.left[0].weights, vec![1.0, 1.0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dock_hover_reveals_groups_only_on_an_auto_hiding_edge() {
|
||||||
|
use crate::app::config::DockSide;
|
||||||
|
use crate::ui::dock::{DockGroup, DockMsg, PanelId};
|
||||||
|
let mut app = dock_app();
|
||||||
|
app.show_browser = true;
|
||||||
|
app.dock.left.push(DockGroup::single(PanelId::Browser));
|
||||||
|
// The pin on any docked pallet turns auto-hide on for its edge.
|
||||||
|
let _ = app.on_dock(DockMsg::AutoCollapseToggle(PanelId::Properties));
|
||||||
|
assert!(app.dock.edge_auto_hide(DockSide::Left));
|
||||||
|
assert!(!app.dock.edge_auto_hide(DockSide::Right));
|
||||||
|
assert!(!app.dock_edge_expanded(DockSide::Left));
|
||||||
|
// Hovering an icon flies its group out.
|
||||||
|
let _ = app.on_dock(DockMsg::Hover(PanelId::Browser));
|
||||||
|
// ...but only once the pointer rests: the reveal waits for the delay.
|
||||||
|
assert!(!app.dock_edge_expanded(DockSide::Left));
|
||||||
|
let _ = app.on_dock(DockMsg::HoverSettled(app.dock_hover_gen));
|
||||||
|
assert!(app.dock_edge_expanded(DockSide::Left));
|
||||||
|
assert_eq!(app.dock_shown_group(DockSide::Left), Some(1));
|
||||||
|
// Leaving and coming back within the delay keeps it open.
|
||||||
|
let _ = app.on_dock(DockMsg::HoverExit);
|
||||||
|
let stale = app.dock_hover_gen;
|
||||||
|
let _ = app.on_dock(DockMsg::HoverStay);
|
||||||
|
let _ = app.on_dock(DockMsg::HoverSettled(stale));
|
||||||
|
assert!(app.dock_edge_expanded(DockSide::Left));
|
||||||
|
let _ = app.on_dock(DockMsg::HoverExit);
|
||||||
|
let _ = app.on_dock(DockMsg::HoverSettled(app.dock_hover_gen));
|
||||||
|
assert!(!app.dock_edge_expanded(DockSide::Left));
|
||||||
|
// Without auto-hide, hovering does not switch groups (clicks do).
|
||||||
|
let _ = app.on_dock(DockMsg::AutoCollapseToggle(PanelId::Properties));
|
||||||
|
let _ = app.on_dock(DockMsg::Hover(PanelId::Properties));
|
||||||
|
assert_eq!(app.dock_shown_group(DockSide::Left), Some(1));
|
||||||
|
assert!(app.dock_edge_expanded(DockSide::Left));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dock_float_resize_grows_the_panel() {
|
||||||
|
use crate::ui::dock::{DockMsg, FloatPanel, PanelId};
|
||||||
|
let mut app = dock_app();
|
||||||
|
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();
|
||||||
|
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));
|
||||||
|
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(100.0, 450.0)));
|
||||||
|
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(60.0, 450.0)));
|
||||||
|
let _ = app.on_dock(DockMsg::DragRelease);
|
||||||
|
let g = app.dock.float_rect(PanelId::BlockPalette).unwrap();
|
||||||
|
assert_eq!((g.x, g.w, g.h), (f.x - 40.0, 340.0, 350.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dock_edge_menu_adds_and_hides_pallets() {
|
||||||
|
use crate::app::config::DockSide;
|
||||||
|
use crate::ui::dock::{DockMsg, PanelId};
|
||||||
|
let mut app = dock_app();
|
||||||
|
app.show_browser = false;
|
||||||
|
let _ = app.on_dock(DockMsg::EdgeMenu(Some(DockSide::Left)));
|
||||||
|
// Checking a closed pallet opens it as a new, shown group here.
|
||||||
|
let _ = app.on_dock(DockMsg::EdgeMenuToggle(DockSide::Left, PanelId::Browser));
|
||||||
|
assert!(app.show_browser);
|
||||||
|
assert_eq!(app.dock.left.len(), 2);
|
||||||
|
assert_eq!(app.dock.left[1].panels, vec![PanelId::Browser]);
|
||||||
|
assert_eq!(app.dock_shown_group(DockSide::Left), Some(1));
|
||||||
|
assert_eq!(app.dock_edge_menu, None, "the menu closes after adding");
|
||||||
|
let _ = app.on_dock(DockMsg::EdgeMenu(Some(DockSide::Left)));
|
||||||
|
// Unchecking hides it but keeps its place.
|
||||||
|
let _ = app.on_dock(DockMsg::EdgeMenuToggle(DockSide::Left, PanelId::Browser));
|
||||||
|
assert!(!app.show_browser);
|
||||||
|
assert_eq!(
|
||||||
|
app.dock.location(PanelId::Browser),
|
||||||
|
Some((DockSide::Left, 1))
|
||||||
|
);
|
||||||
|
// Opening it again from the same edge reuses that place.
|
||||||
|
let _ = app.on_dock(DockMsg::EdgeMenuToggle(DockSide::Left, PanelId::Browser));
|
||||||
|
assert_eq!(app.dock.left.len(), 2);
|
||||||
|
// A pallet open on the other edge is not checked here: picking it
|
||||||
|
// moves it over instead of hiding it.
|
||||||
|
let _ = app.on_dock(DockMsg::EdgeMenuToggle(
|
||||||
|
DockSide::Left,
|
||||||
|
PanelId::BlockPalette,
|
||||||
|
));
|
||||||
|
assert!(app.show_block_palette);
|
||||||
|
assert!(app.dock.right.is_empty());
|
||||||
|
assert_eq!(
|
||||||
|
app.dock.location(PanelId::BlockPalette),
|
||||||
|
Some((DockSide::Left, 2))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dock_divider_resizes_only_its_group() {
|
||||||
|
use crate::app::config::DockSide;
|
||||||
|
use crate::ui::dock::{DockGroup, DockMsg, PanelId};
|
||||||
|
let mut app = dock_app();
|
||||||
|
app.dock.left = vec![
|
||||||
|
DockGroup::stack(vec![PanelId::Properties, PanelId::BlockPalette]),
|
||||||
|
DockGroup::single(PanelId::Count),
|
||||||
|
];
|
||||||
|
app.dock.right.clear();
|
||||||
|
let _ = app.on_dock(DockMsg::ResizeGrab(PanelId::Properties));
|
||||||
|
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(300.0, 100.0)));
|
||||||
|
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(330.0, 100.0)));
|
||||||
|
let _ = app.on_dock(DockMsg::DragRelease);
|
||||||
|
// The group was 260 wide (its widest pallet); it is now 290, while
|
||||||
|
// the other group and the pallets' own widths are untouched.
|
||||||
|
assert_eq!(app.dock_group_width(DockSide::Left, 0), 290.0);
|
||||||
|
assert_eq!(app.dock_group_width(DockSide::Left, 1), 280.0);
|
||||||
|
assert_eq!(app.dock.settings(PanelId::Properties).width, 250.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn layers_command_opens_a_floating_pallet_and_toggles_it_closed() {
|
||||||
|
use crate::ui::dock::PanelId;
|
||||||
|
let mut app = dock_app();
|
||||||
|
let _ = app.update(Message::ToggleLayers);
|
||||||
|
assert!(app.show_layers);
|
||||||
|
assert!(app.active_modal.is_none(), "no modal backdrop");
|
||||||
|
let f = app
|
||||||
|
.dock
|
||||||
|
.float_rect(PanelId::Layers)
|
||||||
|
.expect("floats on first use");
|
||||||
|
assert!(f.x > 0.0 && f.y >= 0.0);
|
||||||
|
let _ = app.update(Message::ToggleLayers);
|
||||||
|
assert!(!app.show_layers);
|
||||||
|
// Its place is kept for next time.
|
||||||
|
assert!(app.dock.float_rect(PanelId::Layers).is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A headless renderer for driving the real view.
|
||||||
|
fn headless_renderer() -> iced::Renderer {
|
||||||
|
iced_test::futures::futures::executor::block_on(
|
||||||
|
<iced::Renderer as iced_test::core::renderer::Headless>::new(
|
||||||
|
iced_test::core::renderer::Settings::default(),
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.expect("headless renderer")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Size the real view is laid out at.
|
||||||
|
const VIEW_SIZE: iced::Size = iced::Size::new(1600.0, 900.0);
|
||||||
|
|
||||||
|
/// Centre of the first text `label` in the real view.
|
||||||
|
fn find_in_view(app: &OpenCADStudio, label: &str) -> iced::Point {
|
||||||
|
use iced_test::core::widget;
|
||||||
|
use iced_test::runtime::user_interface::{Cache, UserInterface};
|
||||||
|
use iced_test::Selector;
|
||||||
|
let mut renderer = headless_renderer();
|
||||||
|
let mut ui = UserInterface::build(app.view_main(), VIEW_SIZE, Cache::default(), &mut renderer);
|
||||||
|
let mut find = Selector::find(label);
|
||||||
|
ui.operate(&renderer, &mut widget::operation::black_box(&mut find));
|
||||||
|
match widget::Operation::finish(&find) {
|
||||||
|
widget::operation::Outcome::Some(Some(target)) => {
|
||||||
|
target.visible_bounds().expect("visible").center()
|
||||||
|
}
|
||||||
|
_ => panic!("{label} not found"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drive the real view through iced's `UserInterface` with the pointer at
|
||||||
|
/// `at`: feed `events` one at a time, and between events hand the
|
||||||
|
/// published messages to the app and rebuild the view while keeping the
|
||||||
|
/// widget-tree cache, as the runtime does.
|
||||||
|
fn drive_view(app: &mut OpenCADStudio, at: iced::Point, events: &[iced::Event]) {
|
||||||
|
use iced_test::core::{mouse, shell, window};
|
||||||
|
use iced_test::runtime::user_interface::{Cache, UserInterface};
|
||||||
|
let mut renderer = headless_renderer();
|
||||||
|
let mut cache = Cache::default();
|
||||||
|
let cursor = mouse::Cursor::Available(at);
|
||||||
|
for event in events {
|
||||||
|
let mut messages = Vec::new();
|
||||||
|
{
|
||||||
|
let mut ui = UserInterface::build(app.view_main(), VIEW_SIZE, cache, &mut renderer);
|
||||||
|
let _ = ui.update(
|
||||||
|
&window::Headless,
|
||||||
|
&shell::Waker::noop(),
|
||||||
|
std::slice::from_ref(event),
|
||||||
|
cursor,
|
||||||
|
&mut renderer,
|
||||||
|
&mut messages,
|
||||||
|
);
|
||||||
|
cache = ui.into_cache();
|
||||||
|
}
|
||||||
|
for message in messages {
|
||||||
|
let _ = app.update(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn docked_title_bar_double_click_floats_the_pallet_in_the_real_view() {
|
||||||
|
use crate::ui::dock::PanelId;
|
||||||
|
let mut app = dock_app();
|
||||||
|
let press = iced::Event::Mouse(iced::mouse::Event::ButtonPressed(iced::mouse::Button::Left));
|
||||||
|
let release = iced::Event::Mouse(iced::mouse::Event::ButtonReleased(
|
||||||
|
iced::mouse::Button::Left,
|
||||||
|
));
|
||||||
|
// The block palette's title text sits in its docked title bar.
|
||||||
|
let title = find_in_view(&app, "Blocks");
|
||||||
|
drive_view(
|
||||||
|
&mut app,
|
||||||
|
title,
|
||||||
|
&[press.clone(), release.clone(), press, release],
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
app.dock.float_rect(PanelId::BlockPalette).is_some(),
|
||||||
|
"a double-click on a docked title bar floats the pallet"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hovering_a_strip_icon_reveals_an_auto_hiding_edge_in_the_real_view() {
|
||||||
|
use crate::app::config::DockSide;
|
||||||
|
use crate::ui::dock::{DockMsg, PanelId, STRIP_CELL_H, STRIP_PAD};
|
||||||
|
let mut app = dock_app();
|
||||||
|
// Mirror the layout onto the left so the block palette's title marks
|
||||||
|
// where the workspace starts, then auto-hide the right edge.
|
||||||
|
let title = find_in_view(&app, "Blocks");
|
||||||
|
let _ = app.on_dock(DockMsg::AutoCollapseToggle(PanelId::BlockPalette));
|
||||||
|
assert!(!app.dock_edge_expanded(DockSide::Right));
|
||||||
|
// The title bar text sits ~13 px below the workspace top; the strip's
|
||||||
|
// first icon is centred STRIP_PAD + half a cell below it.
|
||||||
|
let workspace_top = title.y - 13.0;
|
||||||
|
let icon = iced::Point::new(
|
||||||
|
VIEW_SIZE.width - crate::ui::dock::DOCK_STRIP_W / 2.0,
|
||||||
|
workspace_top + STRIP_PAD + STRIP_CELL_H / 2.0,
|
||||||
|
);
|
||||||
|
drive_view(
|
||||||
|
&mut app,
|
||||||
|
icon,
|
||||||
|
&[iced::Event::Mouse(iced::mouse::Event::CursorMoved {
|
||||||
|
position: icon,
|
||||||
|
})],
|
||||||
|
);
|
||||||
|
let _ = app.on_dock(DockMsg::HoverSettled(app.dock_hover_gen));
|
||||||
|
assert!(
|
||||||
|
app.dock_edge_expanded(DockSide::Right),
|
||||||
|
"hovering the icon reveals the auto-hiding edge"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn layers_ribbon_button_opens_the_pallet_without_staying_highlighted() {
|
||||||
|
let mut app = dock_app();
|
||||||
|
// The ribbon's LAYERS button runs the LAYERS command.
|
||||||
|
let click = || Message::RibbonToolClick {
|
||||||
|
tool_id: "LAYERS".to_string(),
|
||||||
|
event: crate::modules::ModuleEvent::Command("LAYERS".to_string()),
|
||||||
|
};
|
||||||
|
let _ = app.update(click());
|
||||||
|
assert!(app.show_layers);
|
||||||
|
assert_eq!(app.ribbon.active_tool(), None);
|
||||||
|
// Pressing it again keeps it open, like the Blocks palette's button.
|
||||||
|
let _ = app.update(click());
|
||||||
|
assert!(app.show_layers);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dock_double_clicks_float_and_dock_panels() {
|
||||||
|
use crate::app::config::DockSide;
|
||||||
|
use crate::ui::dock::{DockMsg, PanelId};
|
||||||
|
let mut app = dock_app();
|
||||||
|
let _ = app.on_dock(DockMsg::FloatOut(PanelId::Properties));
|
||||||
|
assert!(app.dock.left.is_empty());
|
||||||
|
let f = app.dock.float_rect(PanelId::Properties).expect("floating");
|
||||||
|
assert!(f.x > 0.0, "floats clear of the left edge");
|
||||||
|
let _ = app.on_dock(DockMsg::DockTo(PanelId::Properties, DockSide::Right));
|
||||||
|
assert!(app.dock.floating.is_empty());
|
||||||
|
assert_eq!(
|
||||||
|
app.dock.location(PanelId::Properties),
|
||||||
|
Some((DockSide::Right, 1))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn blockpalette_width_reset() {
|
||||||
|
use crate::app::config::DockSide;
|
||||||
|
let mut app = dock_app();
|
||||||
|
let id = crate::ui::dock::PanelId::BlockPalette;
|
||||||
|
app.dock.set_group_width(DockSide::Right, 0, 500.0);
|
||||||
|
let _ = app.on_dock(crate::ui::dock::DockMsg::WidthReset(id));
|
||||||
|
assert_eq!(app.dock.group_width(DockSide::Right, 0), 260.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn floating_menu_docks_and_allow_docking_keeps_a_drag_floating() {
|
||||||
|
use crate::app::config::DockSide;
|
||||||
|
use crate::ui::dock::{DockMsg, DropTarget, PanelId};
|
||||||
|
let mut app = dock_app();
|
||||||
|
let _ = app.on_dock(DockMsg::FloatOut(PanelId::BlockPalette));
|
||||||
|
// With Allow docking off, a drag onto the left edge's strip stays
|
||||||
|
// floating.
|
||||||
|
let _ = app.on_dock(DockMsg::FloatMenu(Some(PanelId::BlockPalette)));
|
||||||
|
let _ = app.on_dock(DockMsg::FloatDockingToggle(PanelId::BlockPalette));
|
||||||
|
assert_eq!(app.dock_float_menu, None);
|
||||||
|
assert!(!app.dock.float_rect(PanelId::BlockPalette).unwrap().docking);
|
||||||
|
let _ = app.on_dock(DockMsg::DockGrab(PanelId::BlockPalette));
|
||||||
|
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(500.0, 300.0)));
|
||||||
|
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(10.0, 60.0)));
|
||||||
|
assert!(matches!(drag_target(&app), Some(DropTarget::Float { .. })));
|
||||||
|
let _ = app.on_dock(DockMsg::DragRelease);
|
||||||
|
let f = app
|
||||||
|
.dock
|
||||||
|
.float_rect(PanelId::BlockPalette)
|
||||||
|
.expect("still floating");
|
||||||
|
assert!(!f.docking, "moving it keeps the choice");
|
||||||
|
// The menu's Dock left still docks it.
|
||||||
|
let _ = app.on_dock(DockMsg::FloatMenu(Some(PanelId::BlockPalette)));
|
||||||
|
let _ = app.on_dock(DockMsg::DockTo(PanelId::BlockPalette, DockSide::Left));
|
||||||
|
assert_eq!(app.dock_float_menu, None);
|
||||||
|
assert_eq!(
|
||||||
|
app.dock.location(PanelId::BlockPalette).map(|(s, _)| s),
|
||||||
|
Some(DockSide::Left)
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,788 @@
|
|||||||
|
//! Dock state changes: drags, hover, open / close, and the queries the view
|
||||||
|
//! and drop targeting share.
|
||||||
|
|
||||||
|
use crate::app::{Message, OpenCADStudio};
|
||||||
|
|
||||||
|
/// Pointer travel (px) before pressing a title bar or icon becomes a drag,
|
||||||
|
/// so a plain click never moves anything.
|
||||||
|
const DOCK_DRAG_THRESHOLD: f32 = 5.0;
|
||||||
|
|
||||||
|
/// Resolves once the dock hover delay has passed (a sleeping helper thread;
|
||||||
|
/// the web build has none and resolves at once).
|
||||||
|
fn hover_delay() -> impl std::future::Future<Output = ()> {
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
let rx = {
|
||||||
|
let (tx, rx) = iced::futures::channel::oneshot::channel::<()>();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
std::thread::sleep(crate::ui::dock::DOCK_HOVER_DELAY);
|
||||||
|
let _ = tx.send(());
|
||||||
|
});
|
||||||
|
rx
|
||||||
|
};
|
||||||
|
async move {
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
let _ = rx.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OpenCADStudio {
|
||||||
|
/// Dock chrome interaction (grab / pin / resize / hover / move) applied to
|
||||||
|
/// whichever panel the message names.
|
||||||
|
pub(crate) fn on_dock(&mut self, m: crate::ui::dock::DockMsg) -> iced::Task<Message> {
|
||||||
|
use crate::app::config::DockSide;
|
||||||
|
use crate::ui::dock::{DockDrag, DockMsg, FloatPanel};
|
||||||
|
match m {
|
||||||
|
DockMsg::DockGrab(id) => self.dock_begin(DockDrag::moving(id, None), Some(id)),
|
||||||
|
DockMsg::IconPress(id) => {
|
||||||
|
// An icon press shows the icon's group and may also start
|
||||||
|
// dragging the pallet; a plain click never passes the drag
|
||||||
|
// threshold.
|
||||||
|
if self.dock.show_group_of(id) {
|
||||||
|
self.save_config();
|
||||||
|
}
|
||||||
|
self.dock_begin(DockDrag::moving(id, None), Some(id));
|
||||||
|
}
|
||||||
|
DockMsg::GroupGrab(side, gi) => {
|
||||||
|
if let Some(&id) = self.dock_group_visible(side, gi).first() {
|
||||||
|
self.dock_begin(DockDrag::moving(id, Some((side, gi))), Some(id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DockMsg::ResizeGrab(id) => self.dock_begin(DockDrag::Width(id), Some(id)),
|
||||||
|
DockMsg::SplitGrab(side, group, upper, lower) => self.dock_begin(
|
||||||
|
DockDrag::Split {
|
||||||
|
side,
|
||||||
|
group,
|
||||||
|
upper,
|
||||||
|
lower,
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
DockMsg::FloatResizeGrab(panel, from_left) => {
|
||||||
|
self.dock.raise_float(panel);
|
||||||
|
self.dock_begin(DockDrag::FloatSize { panel, from_left }, None);
|
||||||
|
}
|
||||||
|
DockMsg::SplitReset(side, gi) => {
|
||||||
|
self.dock.reset_splits(side, gi);
|
||||||
|
self.save_config();
|
||||||
|
}
|
||||||
|
DockMsg::WidthReset(id) => {
|
||||||
|
// Only docked pallets have the divider that sends this.
|
||||||
|
if let Some((side, gi)) = self.dock.location(id) {
|
||||||
|
self.dock.reset_group_width(side, gi);
|
||||||
|
self.save_config();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DockMsg::TitleHover(id) => self.dock_title_hover = Some(id),
|
||||||
|
DockMsg::TitleHoverEnd(id) => {
|
||||||
|
if self.dock_title_hover == Some(id) {
|
||||||
|
self.dock_title_hover = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DockMsg::GripHover(side, gi) => self.dock_grip_hover = Some((side, gi)),
|
||||||
|
DockMsg::GripHoverEnd(side, gi) => {
|
||||||
|
if self.dock_grip_hover == Some((side, gi)) {
|
||||||
|
self.dock_grip_hover = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DockMsg::FloatRaise(id) => self.dock.raise_float(id),
|
||||||
|
DockMsg::FloatOut(id) => {
|
||||||
|
// Float beside the edge it was docked on, near the top.
|
||||||
|
let (ww, _) = self.dock_workspace_size();
|
||||||
|
let (w, h) = self.dock_float_size(id);
|
||||||
|
let strip = crate::ui::dock::DOCK_STRIP_W;
|
||||||
|
let x = match self.dock.location(id) {
|
||||||
|
Some((DockSide::Right, _)) => {
|
||||||
|
ww - strip - self.dock_column_width(DockSide::Right) - w - 60.0
|
||||||
|
}
|
||||||
|
_ => strip + self.dock_column_width(DockSide::Left) + 40.0,
|
||||||
|
};
|
||||||
|
self.dock_drag = None;
|
||||||
|
if self.dock.float(FloatPanel::new(id, x.max(0.0), 40.0, w, h)) {
|
||||||
|
self.save_config();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DockMsg::DockTo(id, side) => {
|
||||||
|
self.dock_drag = None;
|
||||||
|
self.dock_float_menu = None;
|
||||||
|
if self.dock.dock(id, side, usize::MAX) {
|
||||||
|
self.save_config();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DockMsg::FloatMenu(id) => {
|
||||||
|
self.dock_float_menu = id;
|
||||||
|
self.dock_float_menu_at = self.dock_float_pointer;
|
||||||
|
if let Some(id) = id {
|
||||||
|
self.dock.raise_float(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DockMsg::FloatBarPointer(p) => self.dock_float_pointer = p,
|
||||||
|
DockMsg::FloatDockingToggle(id) => {
|
||||||
|
self.dock_float_menu = None;
|
||||||
|
if self.dock.toggle_float_docking(id) {
|
||||||
|
self.save_config();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DockMsg::EdgeMenu(side) => self.dock_edge_menu = side,
|
||||||
|
DockMsg::EdgeMenuToggle(side, id) => {
|
||||||
|
// Checked = open and docked on this edge: unchecking hides it.
|
||||||
|
// Anything else (closed, floating, on the other edge) moves
|
||||||
|
// here and opens, in its own group here if it has one, else
|
||||||
|
// as a new group; adding a pallet ends the menu's job.
|
||||||
|
let here = self.dock.location(id).map(|(s, _)| s) == Some(side);
|
||||||
|
if !here {
|
||||||
|
self.dock.dock(id, side, usize::MAX);
|
||||||
|
} else if self.dock_panel_visible(id) {
|
||||||
|
let task = self.dock_set_open(id, false);
|
||||||
|
self.save_config();
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
self.dock_edge_menu = None;
|
||||||
|
let task = self.dock_set_open(id, true);
|
||||||
|
self.save_config();
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
DockMsg::AutoCollapseToggle(id) => {
|
||||||
|
// Docked: auto-hide for the whole edge; floating: this pallet.
|
||||||
|
if self.dock.toggle_auto_hide(id) {
|
||||||
|
self.dock_peek = None;
|
||||||
|
}
|
||||||
|
self.save_config();
|
||||||
|
}
|
||||||
|
DockMsg::Close(id) => return self.dock_set_open(id, false),
|
||||||
|
DockMsg::Hover(_)
|
||||||
|
| DockMsg::HoverEnd(_)
|
||||||
|
| DockMsg::HoverExit
|
||||||
|
| DockMsg::HoverStay
|
||||||
|
| DockMsg::HoverSettled(_) => return self.dock_hover(m),
|
||||||
|
DockMsg::DragMove(point) => self.dock_drag_move(point),
|
||||||
|
DockMsg::DragRelease => self.dock_drag_release(),
|
||||||
|
}
|
||||||
|
iced::Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pointer entering / leaving strip icons, edge columns and floating
|
||||||
|
/// frames, and the delayed auto-hide reveal / hide that follows.
|
||||||
|
fn dock_hover(&mut self, m: crate::ui::dock::DockMsg) -> iced::Task<Message> {
|
||||||
|
use crate::ui::dock::{DockMsg, HoverPending};
|
||||||
|
match m {
|
||||||
|
DockMsg::Hover(id) => {
|
||||||
|
self.dock_icon_hover = Some(id);
|
||||||
|
// Only an auto-hiding edge (or floating pallet) reacts to
|
||||||
|
// hover: after a short rest it reveals the hovered pallet's
|
||||||
|
// group. Otherwise groups switch on click. Ignored mid-drag,
|
||||||
|
// when the pointer is over the preview rather than the strip.
|
||||||
|
if self.dock_drag.is_none() && self.dock.auto_hides(id) {
|
||||||
|
if self.dock_peek == Some(id) {
|
||||||
|
self.dock_hover_cancel();
|
||||||
|
} else {
|
||||||
|
return self.dock_hover_arm(HoverPending::Reveal(id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DockMsg::HoverEnd(id) => {
|
||||||
|
if self.dock_icon_hover == Some(id) {
|
||||||
|
self.dock_icon_hover = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DockMsg::HoverExit => {
|
||||||
|
if self.dock_drag.is_none()
|
||||||
|
&& self.dock_peek.is_some_and(|id| self.dock.auto_hides(id))
|
||||||
|
{
|
||||||
|
return self.dock_hover_arm(HoverPending::Hide);
|
||||||
|
}
|
||||||
|
// Left before a pending reveal ran out: it never happens.
|
||||||
|
self.dock_hover_cancel();
|
||||||
|
}
|
||||||
|
DockMsg::HoverStay => {
|
||||||
|
if self.dock_hover_pending == Some(HoverPending::Hide) {
|
||||||
|
self.dock_hover_cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DockMsg::HoverSettled(gen) => {
|
||||||
|
if gen == self.dock_hover_gen && self.dock_drag.is_none() {
|
||||||
|
match self.dock_hover_pending.take() {
|
||||||
|
Some(HoverPending::Reveal(id)) if self.dock.auto_hides(id) => {
|
||||||
|
self.dock_reveal(id)
|
||||||
|
}
|
||||||
|
Some(HoverPending::Hide)
|
||||||
|
if self.dock_peek.is_some_and(|id| self.dock.auto_hides(id)) =>
|
||||||
|
{
|
||||||
|
self.dock_peek = None;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
iced::Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pointer released: land what was dragged and save a changed layout.
|
||||||
|
fn dock_drag_release(&mut self) {
|
||||||
|
use crate::ui::dock::{DockDrag, DropTarget, FloatPanel};
|
||||||
|
let mut changed = false;
|
||||||
|
match self.dock_drag.take() {
|
||||||
|
Some(DockDrag::Move {
|
||||||
|
group: Some((side, gi)),
|
||||||
|
target: Some(DropTarget::Edge { side: to, index }),
|
||||||
|
..
|
||||||
|
}) => changed = self.dock.move_group(side, gi, to, index),
|
||||||
|
Some(DockDrag::Move {
|
||||||
|
panel,
|
||||||
|
group: None,
|
||||||
|
target: Some(target),
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
changed = match target {
|
||||||
|
DropTarget::Edge { side, index } => self.dock.dock(panel, side, index),
|
||||||
|
DropTarget::Join { side, group, index } => {
|
||||||
|
self.dock.join_group(panel, side, group, index)
|
||||||
|
}
|
||||||
|
DropTarget::Float { x, y } => {
|
||||||
|
let (w, h) = self.dock_float_size(panel);
|
||||||
|
// A floating panel keeps its docking choice.
|
||||||
|
let docking = self.dock.float_rect(panel).is_none_or(|f| f.docking);
|
||||||
|
self.dock.float(FloatPanel {
|
||||||
|
docking,
|
||||||
|
..FloatPanel::new(panel, x, y, w, h)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Some(DockDrag::Move { .. } | DockDrag::LayerColumn) | None => {}
|
||||||
|
Some(DockDrag::Width(_) | DockDrag::Split { .. } | DockDrag::FloatSize { .. }) => {
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if changed {
|
||||||
|
self.save_config();
|
||||||
|
}
|
||||||
|
self.dock_drag_last = None;
|
||||||
|
self.xref_col_drag = None;
|
||||||
|
self.xref_col_last = None;
|
||||||
|
self.xref_split_drag = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pointer motion while something in the dock is dragged.
|
||||||
|
fn dock_drag_move(&mut self, point: iced::Point) {
|
||||||
|
// NOTE: xref column drags intentionally do NOT ride this path: its
|
||||||
|
// points live in workspace space while the header tracker reports
|
||||||
|
// header-local points, and mixing the two produced a one-time jump
|
||||||
|
// plus a stuck drag. Column moves arrive via Message::XrefColMove
|
||||||
|
// only.
|
||||||
|
use crate::app::config::DockSide;
|
||||||
|
use crate::ui::dock::{DockDrag, DropTarget};
|
||||||
|
let last = self.dock_drag_last.replace(point);
|
||||||
|
let Some(drag) = self.dock_drag else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
match drag {
|
||||||
|
DockDrag::Move {
|
||||||
|
panel,
|
||||||
|
group,
|
||||||
|
origin,
|
||||||
|
target,
|
||||||
|
grab,
|
||||||
|
} => {
|
||||||
|
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.
|
||||||
|
let grab = if target.is_none() && started {
|
||||||
|
match self.dock.float_rect(panel) {
|
||||||
|
Some(f) => iced::Vector::new(origin.x - f.x, origin.y - f.y),
|
||||||
|
None => iced::Vector::new(
|
||||||
|
(self.dock.width(panel, self.win_size.0) * 0.5).min(120.0),
|
||||||
|
14.0,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
grab
|
||||||
|
};
|
||||||
|
let target = if started {
|
||||||
|
// A floating panel that may not dock only floats.
|
||||||
|
let found = match self.dock.float_rect(panel) {
|
||||||
|
Some(f) if !f.docking => self.dock_float_target(point, grab),
|
||||||
|
_ => self.dock_drop_target(point, grab),
|
||||||
|
};
|
||||||
|
// A whole group only moves between group positions: it
|
||||||
|
// cannot join another group or float.
|
||||||
|
match (group, found) {
|
||||||
|
(Some(_), found @ DropTarget::Edge { .. }) | (None, found) => Some(found),
|
||||||
|
(Some(_), _) => None,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
target
|
||||||
|
};
|
||||||
|
self.dock_drag = Some(DockDrag::Move {
|
||||||
|
panel,
|
||||||
|
group,
|
||||||
|
origin: Some(origin),
|
||||||
|
target,
|
||||||
|
grab,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
DockDrag::Split {
|
||||||
|
side,
|
||||||
|
group,
|
||||||
|
upper,
|
||||||
|
lower,
|
||||||
|
} => {
|
||||||
|
let Some(last) = last else { return };
|
||||||
|
let Some(weights) = self.dock.groups(side).get(group).map(|g| &g.weights) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let (_, avail) = self.dock_workspace_size();
|
||||||
|
let total: f32 = self
|
||||||
|
.dock_slot_spans(side)
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(i, _, _)| weights.get(*i))
|
||||||
|
.sum();
|
||||||
|
if avail > 0.0 && total > 0.0 {
|
||||||
|
let per_px = total / avail;
|
||||||
|
self.dock.shift_split(
|
||||||
|
side,
|
||||||
|
group,
|
||||||
|
upper,
|
||||||
|
lower,
|
||||||
|
(point.y - last.y) * per_px,
|
||||||
|
crate::ui::dock::GROUP_MIN_H * per_px,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DockDrag::FloatSize { panel, from_left } => {
|
||||||
|
let (Some(last), Some(f)) = (last, self.dock.float_rect(panel)) else {
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
DockDrag::LayerColumn => {
|
||||||
|
let Some(last) = last else { return };
|
||||||
|
self.layer_name_col_w =
|
||||||
|
(self.layer_name_col_w + point.x - last.x).clamp(60.0, 640.0);
|
||||||
|
}
|
||||||
|
DockDrag::Width(id) => {
|
||||||
|
let Some(last) = last else { return };
|
||||||
|
// The divider sizes the docked pallet's group.
|
||||||
|
if let Some((side, gi)) = self.dock.location(id) {
|
||||||
|
let dx = point.x - last.x;
|
||||||
|
let delta = if side == DockSide::Left { dx } else { -dx };
|
||||||
|
let cur = self.dock_group_width(side, gi) + delta;
|
||||||
|
self.dock.set_group_width(side, gi, cur);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start `drag`, peeking `peek` (a pallet being dragged or resized stays
|
||||||
|
/// shown on an auto-hiding edge).
|
||||||
|
fn dock_begin(
|
||||||
|
&mut self,
|
||||||
|
drag: crate::ui::dock::DockDrag,
|
||||||
|
peek: Option<crate::ui::dock::PanelId>,
|
||||||
|
) {
|
||||||
|
if let crate::ui::dock::DockDrag::Move { panel, .. } = drag {
|
||||||
|
self.dock.raise_float(panel);
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Arm a delayed reveal or hide; it applies when its timer reports back
|
||||||
|
/// unless a newer hover superseded it.
|
||||||
|
fn dock_hover_arm(&mut self, target: crate::ui::dock::HoverPending) -> iced::Task<Message> {
|
||||||
|
self.dock_hover_gen = self.dock_hover_gen.wrapping_add(1);
|
||||||
|
self.dock_hover_pending = Some(target);
|
||||||
|
let gen = self.dock_hover_gen;
|
||||||
|
iced::Task::perform(hover_delay(), move |()| {
|
||||||
|
Message::Dock(crate::ui::dock::DockMsg::HoverSettled(gen))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drop any pending delayed reveal / hide.
|
||||||
|
fn dock_hover_cancel(&mut self) {
|
||||||
|
self.dock_hover_gen = self.dock_hover_gen.wrapping_add(1);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Open or close pallet `id`, running its usual open path (refreshes,
|
||||||
|
/// ribbon highlight) or close path. An opened pallet is revealed where
|
||||||
|
/// the layout has it.
|
||||||
|
pub(crate) fn dock_set_open(
|
||||||
|
&mut self,
|
||||||
|
id: crate::ui::dock::PanelId,
|
||||||
|
open: bool,
|
||||||
|
) -> iced::Task<Message> {
|
||||||
|
use crate::ui::dock::{DockDrag, PanelId};
|
||||||
|
let mut task = iced::Task::none();
|
||||||
|
if open {
|
||||||
|
if !self.dock_panel_visible(id) {
|
||||||
|
match id {
|
||||||
|
PanelId::Properties => {
|
||||||
|
self.show_properties = true;
|
||||||
|
self.ribbon.set_properties(true);
|
||||||
|
}
|
||||||
|
PanelId::BlockPalette => self.open_blocks_palette(None),
|
||||||
|
PanelId::ExternalReferences => {
|
||||||
|
self.show_external_references = true;
|
||||||
|
self.refresh_xref_manager();
|
||||||
|
}
|
||||||
|
PanelId::Browser => self.show_browser = true,
|
||||||
|
PanelId::NodeGraph => {
|
||||||
|
task = self.on_graph(crate::ui::node_graph::GraphMsg::Toggle);
|
||||||
|
}
|
||||||
|
PanelId::PointCloudManager => self.pc_manager.show = true,
|
||||||
|
PanelId::Count => self.set_count_palette(true),
|
||||||
|
PanelId::SheetSetManager => self.show_sheet_set_manager(true),
|
||||||
|
PanelId::Layers => {
|
||||||
|
self.sync_ribbon_layers();
|
||||||
|
self.show_layers = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.dock_reveal(id);
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
match id {
|
||||||
|
PanelId::BlockPalette => {
|
||||||
|
self.show_block_palette = false;
|
||||||
|
self.block_palette.placing = None;
|
||||||
|
}
|
||||||
|
PanelId::ExternalReferences => self.show_external_references = false,
|
||||||
|
PanelId::Browser => self.show_browser = false,
|
||||||
|
PanelId::NodeGraph => self.show_node_graph = false,
|
||||||
|
PanelId::PointCloudManager => self.pc_manager.show = false,
|
||||||
|
PanelId::Count => {
|
||||||
|
self.count_palette.show = false;
|
||||||
|
self.ribbon.set_count_palette(false);
|
||||||
|
}
|
||||||
|
PanelId::SheetSetManager => {
|
||||||
|
self.sheet_set.show = false;
|
||||||
|
self.ribbon.set_sheet_set(false);
|
||||||
|
}
|
||||||
|
PanelId::Properties => {
|
||||||
|
self.show_properties = false;
|
||||||
|
self.ribbon.set_properties(false);
|
||||||
|
}
|
||||||
|
PanelId::Layers => {
|
||||||
|
self.show_layers = false;
|
||||||
|
self.ribbon.deactivate_tool_if("LAYERS");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.dock_unpeek(id);
|
||||||
|
// A closed pallet cannot stay held by the pointer.
|
||||||
|
let held = match self.dock_drag {
|
||||||
|
Some(DockDrag::Move { panel, .. } | DockDrag::Width(panel)) => panel == id,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
if held {
|
||||||
|
self.dock_drag = None;
|
||||||
|
}
|
||||||
|
task
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show `id` where the layout has it, first docking it on the right edge
|
||||||
|
/// when it has no place yet (opened for the first time, or never moved).
|
||||||
|
pub(crate) fn dock_open_at_default(&mut self, id: crate::ui::dock::PanelId) {
|
||||||
|
if !self.dock.is_placed(id) {
|
||||||
|
self.dock
|
||||||
|
.dock(id, crate::app::config::DockSide::Right, usize::MAX);
|
||||||
|
}
|
||||||
|
self.dock_reveal(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Open the Layer Manager pallet (LAYERS). The first time it floats over
|
||||||
|
/// the middle of the drawing, where the dialog it replaced used to open;
|
||||||
|
/// after that it opens wherever the user left it.
|
||||||
|
pub(crate) fn open_layers_pallet(&mut self) {
|
||||||
|
use crate::ui::dock::{FloatPanel, PanelId};
|
||||||
|
self.sync_ribbon_layers();
|
||||||
|
self.show_layers = true;
|
||||||
|
if !self.dock.is_placed(PanelId::Layers) {
|
||||||
|
let (ww, wh) = self.dock_workspace_size();
|
||||||
|
let (w, h) = self.dock_float_size(PanelId::Layers);
|
||||||
|
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);
|
||||||
|
self.dock_reveal(PanelId::Layers);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop revealing `id` (it closed).
|
||||||
|
pub(crate) fn dock_unpeek(&mut self, id: crate::ui::dock::PanelId) {
|
||||||
|
if self.dock_peek == Some(id) {
|
||||||
|
self.dock_peek = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether `id` is currently rendered (not closed, not on the start screen /
|
||||||
|
/// clean-screen viewport). Mirrors the visibility filter the edge column
|
||||||
|
/// renderer applies before building each side's stack.
|
||||||
|
pub(crate) fn dock_panel_visible(&self, id: crate::ui::dock::PanelId) -> bool {
|
||||||
|
use crate::ui::dock::PanelId;
|
||||||
|
if self.tabs[self.active_tab].is_start || self.clean_screen {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
match id {
|
||||||
|
PanelId::Properties => self.show_properties,
|
||||||
|
PanelId::BlockPalette => self.show_block_palette,
|
||||||
|
PanelId::ExternalReferences => self.show_external_references,
|
||||||
|
PanelId::Browser => self.show_browser,
|
||||||
|
PanelId::NodeGraph => self.show_node_graph,
|
||||||
|
PanelId::PointCloudManager => self.pc_manager.show,
|
||||||
|
PanelId::Count => self.count_palette.show,
|
||||||
|
PanelId::SheetSetManager => self.sheet_set.show,
|
||||||
|
PanelId::Layers => self.show_layers,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Size of the workspace the dock lays out in: window width by viewport
|
||||||
|
/// height.
|
||||||
|
pub(crate) fn dock_workspace_size(&self) -> (f32, f32) {
|
||||||
|
(
|
||||||
|
self.win_size.0,
|
||||||
|
self.tabs[self.active_tab]
|
||||||
|
.scene
|
||||||
|
.selection
|
||||||
|
.borrow()
|
||||||
|
.view
|
||||||
|
.vp_size
|
||||||
|
.1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The open pallets of group `gi` on `side`, top → bottom. Closed
|
||||||
|
/// pallets keep their place but take no space, so a group with none open
|
||||||
|
/// is not drawn at all.
|
||||||
|
pub(crate) fn dock_group_visible(
|
||||||
|
&self,
|
||||||
|
side: crate::app::config::DockSide,
|
||||||
|
gi: usize,
|
||||||
|
) -> Vec<crate::ui::dock::PanelId> {
|
||||||
|
// A stale index (a message from before the layout changed) reads as
|
||||||
|
// an empty group rather than a crash.
|
||||||
|
self.dock
|
||||||
|
.groups(side)
|
||||||
|
.get(gi)
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|g| g.panels.iter().copied())
|
||||||
|
.filter(|id| self.dock_panel_visible(*id))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Indices of the groups on `side` with an open pallet (listed in the
|
||||||
|
/// icon strip).
|
||||||
|
pub(crate) fn dock_visible_groups(&self, side: crate::app::config::DockSide) -> Vec<usize> {
|
||||||
|
(0..self.dock.groups(side).len())
|
||||||
|
.filter(|gi| !self.dock_group_visible(side, *gi).is_empty())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The group `side` draws: the one it shows, or the first with an open
|
||||||
|
/// pallet when that one has none.
|
||||||
|
pub(crate) fn dock_shown_group(&self, side: crate::app::config::DockSide) -> Option<usize> {
|
||||||
|
let visible = self.dock_visible_groups(side);
|
||||||
|
let shown = self.dock.shown(side);
|
||||||
|
if visible.contains(&shown) {
|
||||||
|
Some(shown)
|
||||||
|
} else {
|
||||||
|
visible.first().copied()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether `side` draws its shown group beside the icon strip: always,
|
||||||
|
/// unless the edge auto-hides and none of the group's icons is hovered.
|
||||||
|
pub(crate) fn dock_edge_expanded(&self, side: crate::app::config::DockSide) -> bool {
|
||||||
|
let Some(gi) = self.dock_shown_group(side) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
!self.dock.edge_auto_hide(side)
|
||||||
|
|| self
|
||||||
|
.dock_peek
|
||||||
|
.is_some_and(|e| self.dock_group_visible(side, gi).contains(&e))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// On-screen width of group `gi` on `side`.
|
||||||
|
pub(crate) fn dock_group_width(&self, side: crate::app::config::DockSide, gi: usize) -> f32 {
|
||||||
|
self.dock.group_width_px(side, gi, self.win_size.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Width of the pallet column beside `side`'s icon strip: the shown
|
||||||
|
/// group's width, or nothing when it is hidden.
|
||||||
|
pub(crate) fn dock_column_width(&self, side: crate::app::config::DockSide) -> f32 {
|
||||||
|
match self.dock_shown_group(side) {
|
||||||
|
Some(gi) if self.dock_edge_expanded(side) => self.dock_group_width(side, gi),
|
||||||
|
_ => 0.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The icon strip of `side`: each listed group's place, and the top of
|
||||||
|
/// the + button.
|
||||||
|
pub(crate) fn dock_strip_layout(
|
||||||
|
&self,
|
||||||
|
side: crate::app::config::DockSide,
|
||||||
|
) -> (Vec<crate::ui::dock::StripGroup>, f32) {
|
||||||
|
let groups: Vec<(usize, usize)> = self
|
||||||
|
.dock_visible_groups(side)
|
||||||
|
.into_iter()
|
||||||
|
.map(|gi| (gi, self.dock_group_visible(side, gi).len()))
|
||||||
|
.collect();
|
||||||
|
crate::ui::dock::strip_layout(&groups)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The open pallets of `side`'s shown group, each as (position in the
|
||||||
|
/// group, top, bottom) in the workspace.
|
||||||
|
pub(crate) fn dock_slot_spans(
|
||||||
|
&self,
|
||||||
|
side: crate::app::config::DockSide,
|
||||||
|
) -> Vec<(usize, f32, f32)> {
|
||||||
|
match self.dock_shown_group(side) {
|
||||||
|
Some(gi) => self.dock_open_spans(&self.dock.groups(side)[gi], None),
|
||||||
|
None => Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The open pallets of `group` (plus `extra`, a pallet about to land
|
||||||
|
/// there), each as (position in the group, top, bottom) down the
|
||||||
|
/// workspace height.
|
||||||
|
pub(crate) fn dock_open_spans(
|
||||||
|
&self,
|
||||||
|
group: &crate::ui::dock::DockGroup,
|
||||||
|
extra: Option<crate::ui::dock::PanelId>,
|
||||||
|
) -> Vec<(usize, f32, f32)> {
|
||||||
|
let (_, avail) = self.dock_workspace_size();
|
||||||
|
let open: Vec<usize> = (0..group.panels.len())
|
||||||
|
.filter(|i| {
|
||||||
|
let id = group.panels[*i];
|
||||||
|
Some(id) == extra || self.dock_panel_visible(id)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let weights: Vec<f32> = open.iter().map(|i| group.weights[*i]).collect();
|
||||||
|
open.into_iter()
|
||||||
|
.zip(crate::ui::dock::slot_spans(&weights, avail))
|
||||||
|
.map(|(i, (top, bottom))| (i, top, bottom))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Size a panel takes when floated: its current floating size, or its
|
||||||
|
/// docked width by a comfortable share of the workspace height.
|
||||||
|
pub(crate) fn dock_float_size(&self, id: crate::ui::dock::PanelId) -> (f32, f32) {
|
||||||
|
if let Some(f) = self.dock.float_rect(id) {
|
||||||
|
return (f.w, f.h);
|
||||||
|
}
|
||||||
|
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),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Where a dragged pallet lands if released at workspace point `p`:
|
||||||
|
/// on an edge's icon strip it joins a group between its icons or starts a
|
||||||
|
/// new group; over the shown group's pallets it stacks above / below the
|
||||||
|
/// one under the pointer; on an empty edge it starts a group; anywhere
|
||||||
|
/// else it floats, held at `grab` from its top-left corner.
|
||||||
|
pub(crate) fn dock_drop_target(
|
||||||
|
&self,
|
||||||
|
p: iced::Point,
|
||||||
|
grab: iced::Vector,
|
||||||
|
) -> crate::ui::dock::DropTarget {
|
||||||
|
use crate::app::config::DockSide;
|
||||||
|
use crate::ui::dock::{DropTarget, StripHit, DOCK_EDGE_ZONE, DOCK_STRIP_W};
|
||||||
|
let (ww, _) = self.dock_workspace_size();
|
||||||
|
for side in [DockSide::Left, DockSide::Right] {
|
||||||
|
let listed = !self.dock_visible_groups(side).is_empty();
|
||||||
|
let from_edge = match side {
|
||||||
|
DockSide::Left => p.x,
|
||||||
|
DockSide::Right => ww - p.x,
|
||||||
|
};
|
||||||
|
if !listed {
|
||||||
|
if from_edge <= DOCK_EDGE_ZONE {
|
||||||
|
return DropTarget::Edge {
|
||||||
|
side,
|
||||||
|
index: self.dock.groups(side).len(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Over the icon strip.
|
||||||
|
if from_edge <= DOCK_STRIP_W {
|
||||||
|
let (layout, _) = self.dock_strip_layout(side);
|
||||||
|
return match crate::ui::dock::strip_hit(p.y, &layout) {
|
||||||
|
StripHit::NewGroup(at) => DropTarget::Edge {
|
||||||
|
side,
|
||||||
|
index: at.unwrap_or(self.dock.groups(side).len()),
|
||||||
|
},
|
||||||
|
StripHit::Join { group, index } => {
|
||||||
|
// Map the gap among open pallets to a position among
|
||||||
|
// all of the group's pallets.
|
||||||
|
let open = self.dock_group_visible(side, group);
|
||||||
|
let all = &self.dock.groups(side)[group].panels;
|
||||||
|
let index = open
|
||||||
|
.get(index)
|
||||||
|
.and_then(|t| all.iter().position(|a| a == t))
|
||||||
|
.unwrap_or(all.len());
|
||||||
|
DropTarget::Join { side, group, index }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Over the shown group's pallets: above or below the one there.
|
||||||
|
if from_edge <= DOCK_STRIP_W + self.dock_column_width(side) {
|
||||||
|
let gi = self.dock_shown_group(side).expect("listed edge");
|
||||||
|
let spans = self.dock_slot_spans(side);
|
||||||
|
if let Some(&(pos, top, bottom)) = spans
|
||||||
|
.iter()
|
||||||
|
.find(|(_, _, bottom)| p.y < *bottom)
|
||||||
|
.or(spans.last())
|
||||||
|
{
|
||||||
|
let index = if p.y < (top + bottom) * 0.5 {
|
||||||
|
pos
|
||||||
|
} else {
|
||||||
|
pos + 1
|
||||||
|
};
|
||||||
|
return DropTarget::Join {
|
||||||
|
side,
|
||||||
|
group: gi,
|
||||||
|
index,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.dock_float_target(p, grab)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Floating at workspace point `p`, held at `grab` from its top-left
|
||||||
|
/// corner (kept on the workspace).
|
||||||
|
pub(crate) fn dock_float_target(
|
||||||
|
&self,
|
||||||
|
p: iced::Point,
|
||||||
|
grab: iced::Vector,
|
||||||
|
) -> crate::ui::dock::DropTarget {
|
||||||
|
let (ww, wh) = self.dock_workspace_size();
|
||||||
|
let x = (p.x - grab.x).clamp(0.0, (ww - 60.0).max(0.0));
|
||||||
|
let y = (p.y - grab.y).clamp(0.0, (wh - 30.0).max(0.0));
|
||||||
|
crate::ui::dock::DropTarget::Float { x, y }
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
+41
-19
@@ -14,6 +14,7 @@ pub(crate) mod commands;
|
|||||||
pub(crate) mod dim_viewport;
|
pub(crate) mod dim_viewport;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod dimension_preview_tests;
|
mod dimension_preview_tests;
|
||||||
|
mod dock;
|
||||||
mod document;
|
mod document;
|
||||||
mod drafting_settings;
|
mod drafting_settings;
|
||||||
pub(crate) mod expr_eval;
|
pub(crate) mod expr_eval;
|
||||||
@@ -814,6 +815,8 @@ pub(super) struct OpenCADStudio {
|
|||||||
render_mode_preview: Option<codec::entities::ViewportRenderMode>,
|
render_mode_preview: Option<codec::entities::ViewportRenderMode>,
|
||||||
/// Whether the Properties panel is shown on the left (PROPERTIES).
|
/// Whether the Properties panel is shown on the left (PROPERTIES).
|
||||||
show_properties: bool,
|
show_properties: bool,
|
||||||
|
/// The Layer Manager pallet (LAYERS) is open.
|
||||||
|
pub(crate) show_layers: bool,
|
||||||
/// Docked Insert Block panel visibility.
|
/// Docked Insert Block panel visibility.
|
||||||
pub(crate) show_block_palette: bool,
|
pub(crate) show_block_palette: bool,
|
||||||
/// Node graph overlay over the viewport.
|
/// Node graph overlay over the viewport.
|
||||||
@@ -843,17 +846,33 @@ pub(super) struct OpenCADStudio {
|
|||||||
pub(crate) bg_picker: Option<BgTarget>,
|
pub(crate) bg_picker: Option<BgTarget>,
|
||||||
/// General edge-stack dock layout for the side panels.
|
/// General edge-stack dock layout for the side panels.
|
||||||
pub(crate) dock: crate::ui::dock::DockState,
|
pub(crate) dock: crate::ui::dock::DockState,
|
||||||
/// Which panel is currently floated at full height (hovered, or a pinned
|
/// Pallet revealed although it auto-hides (its edge's group, or its
|
||||||
/// panel on top).
|
/// floating frame): hovered, being dragged, or just opened. Cleared when
|
||||||
pub(crate) dock_expanded: Option<crate::ui::dock::PanelId>,
|
/// the pointer leaves.
|
||||||
/// Panel currently being dragged between sides / reordered.
|
pub(crate) dock_peek: Option<crate::ui::dock::PanelId>,
|
||||||
pub(crate) dock_dragging: Option<crate::ui::dock::PanelId>,
|
/// Reveal or hide waiting out the hover delay, with the generation its
|
||||||
/// Panel currently being width-resized.
|
/// timer carries; any newer hover supersedes it.
|
||||||
pub(crate) dock_resizing: Option<crate::ui::dock::PanelId>,
|
pub(crate) dock_hover_pending: Option<crate::ui::dock::HoverPending>,
|
||||||
/// Last pointer position during a drag / resize.
|
pub(crate) dock_hover_gen: u64,
|
||||||
|
/// What the pointer is dragging in the dock (pallet, group, width,
|
||||||
|
/// splitter, floating size, Layer Manager column).
|
||||||
|
pub(crate) dock_drag: Option<crate::ui::dock::DockDrag>,
|
||||||
|
/// Pallet whose strip icon (or floating frame) the pointer is over.
|
||||||
|
pub(crate) dock_icon_hover: Option<crate::ui::dock::PanelId>,
|
||||||
|
/// Last pointer position during a dock drag.
|
||||||
pub(crate) dock_drag_last: Option<iced::Point>,
|
pub(crate) dock_drag_last: Option<iced::Point>,
|
||||||
/// Live drag target (side + index), shown as a highlight while dragging.
|
/// Docked panel whose title bar the pointer is over (shows pin / close).
|
||||||
pub(crate) dock_drag_target: Option<(crate::app::config::DockSide, usize)>,
|
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 pallet whose title-bar right-click menu is open.
|
||||||
|
pub(crate) dock_float_menu: Option<crate::ui::dock::PanelId>,
|
||||||
|
/// Pointer position over a floating title bar (bar-local), and where
|
||||||
|
/// its open menu was asked for.
|
||||||
|
pub(crate) dock_float_pointer: iced::Point,
|
||||||
|
pub(crate) dock_float_menu_at: iced::Point,
|
||||||
|
/// 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),
|
/// Reference-table column currently being width-resized (column index),
|
||||||
/// with the last pointer position. Mirrors the Layers Name-column drag.
|
/// with the last pointer position. Mirrors the Layers Name-column drag.
|
||||||
pub(crate) xref_col_drag: Option<usize>,
|
pub(crate) xref_col_drag: Option<usize>,
|
||||||
@@ -942,9 +961,6 @@ pub(super) struct OpenCADStudio {
|
|||||||
modal_drag_last: Option<Point>,
|
modal_drag_last: Option<Point>,
|
||||||
/// True while the modal title bar is held (a drag is in progress).
|
/// True while the modal title bar is held (a drag is in progress).
|
||||||
modal_dragging: bool,
|
modal_dragging: bool,
|
||||||
/// Layer Manager: dragging the Name-column divider (width follows the
|
|
||||||
/// shared ModalDragMove flow).
|
|
||||||
layer_col_dragging: bool,
|
|
||||||
/// Layer Manager Name column width in px, adjusted by the divider drag.
|
/// Layer Manager Name column width in px, adjusted by the divider drag.
|
||||||
layer_name_col_w: f32,
|
layer_name_col_w: f32,
|
||||||
/// How far the user has dragged the modal's corner resize grip from the
|
/// How far the user has dragged the modal's corner resize grip from the
|
||||||
@@ -1951,7 +1967,6 @@ pub enum ModalKind {
|
|||||||
PluginManager,
|
PluginManager,
|
||||||
UpdateNotice,
|
UpdateNotice,
|
||||||
DonationPrompt,
|
DonationPrompt,
|
||||||
Layers,
|
|
||||||
LayerStateManager,
|
LayerStateManager,
|
||||||
LayerTranslator,
|
LayerTranslator,
|
||||||
DrawingUnits,
|
DrawingUnits,
|
||||||
@@ -4293,6 +4308,7 @@ impl OpenCADStudio {
|
|||||||
render_mode_menu_open: false,
|
render_mode_menu_open: false,
|
||||||
render_mode_preview: None,
|
render_mode_preview: None,
|
||||||
show_properties: true,
|
show_properties: true,
|
||||||
|
show_layers: false,
|
||||||
show_block_palette: false,
|
show_block_palette: false,
|
||||||
show_node_graph: false,
|
show_node_graph: false,
|
||||||
property_target_override: None,
|
property_target_override: None,
|
||||||
@@ -4308,11 +4324,18 @@ impl OpenCADStudio {
|
|||||||
block_palette: Default::default(),
|
block_palette: Default::default(),
|
||||||
xref_manager: Default::default(),
|
xref_manager: Default::default(),
|
||||||
dock: Default::default(),
|
dock: Default::default(),
|
||||||
dock_expanded: None,
|
dock_peek: None,
|
||||||
dock_dragging: None,
|
dock_hover_pending: None,
|
||||||
dock_resizing: None,
|
dock_hover_gen: 0,
|
||||||
|
dock_float_menu: None,
|
||||||
|
dock_float_pointer: iced::Point::ORIGIN,
|
||||||
|
dock_float_menu_at: iced::Point::ORIGIN,
|
||||||
|
dock_drag: None,
|
||||||
dock_drag_last: None,
|
dock_drag_last: None,
|
||||||
dock_drag_target: None,
|
dock_title_hover: None,
|
||||||
|
dock_edge_menu: None,
|
||||||
|
dock_grip_hover: None,
|
||||||
|
dock_icon_hover: None,
|
||||||
xref_col_drag: None,
|
xref_col_drag: None,
|
||||||
xref_col_last: None,
|
xref_col_last: None,
|
||||||
xref_split_drag: false,
|
xref_split_drag: false,
|
||||||
@@ -4349,7 +4372,6 @@ impl OpenCADStudio {
|
|||||||
modal_offset: iced::Vector::ZERO,
|
modal_offset: iced::Vector::ZERO,
|
||||||
modal_drag_last: None,
|
modal_drag_last: None,
|
||||||
modal_dragging: false,
|
modal_dragging: false,
|
||||||
layer_col_dragging: false,
|
|
||||||
layer_name_col_w: 130.0,
|
layer_name_col_w: 130.0,
|
||||||
modal_resize: iced::Vector::ZERO,
|
modal_resize: iced::Vector::ZERO,
|
||||||
modal_content_size: None,
|
modal_content_size: None,
|
||||||
|
|||||||
@@ -188,11 +188,7 @@ impl OpenCADStudio {
|
|||||||
// The node library docks on the right on first use; after
|
// The node library docks on the right on first use; after
|
||||||
// that it stays wherever the user moved it.
|
// that it stays wherever the user moved it.
|
||||||
let id = crate::ui::dock::PanelId::NodeGraph;
|
let id = crate::ui::dock::PanelId::NodeGraph;
|
||||||
if self.dock.location(id).is_none() {
|
self.dock_open_at_default(id);
|
||||||
self.dock
|
|
||||||
.dock(id, crate::app::config::DockSide::Right, usize::MAX);
|
|
||||||
}
|
|
||||||
self.dock_expanded = Some(id);
|
|
||||||
return self.graph_run(i);
|
return self.graph_run(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ impl OpenCADStudio {
|
|||||||
/// BLOCKSPALETTE: open (expanded) on `tab`, or on the tab it was left on.
|
/// BLOCKSPALETTE: open (expanded) on `tab`, or on the tab it was left on.
|
||||||
pub(crate) fn open_blocks_palette(&mut self, tab: Option<Tab>) {
|
pub(crate) fn open_blocks_palette(&mut self, tab: Option<Tab>) {
|
||||||
self.show_block_palette = true;
|
self.show_block_palette = true;
|
||||||
self.dock_expanded = Some(crate::ui::dock::PanelId::BlockPalette);
|
self.dock_reveal(crate::ui::dock::PanelId::BlockPalette);
|
||||||
self.show_block_palette_tab(tab.unwrap_or(self.block_palette.tab));
|
self.show_block_palette_tab(tab.unwrap_or(self.block_palette.tab));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -542,195 +542,6 @@ impl OpenCADStudio {
|
|||||||
Ok(name)
|
Ok(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Dock chrome interaction (grab / pin / resize / hover / move) applied to
|
|
||||||
/// whichever panel the message names.
|
|
||||||
pub(super) fn on_dock(&mut self, m: crate::ui::dock::DockMsg) -> iced::Task<Message> {
|
|
||||||
use crate::app::config::DockSide;
|
|
||||||
use crate::ui::dock::{DockMsg, PanelId};
|
|
||||||
match m {
|
|
||||||
DockMsg::DockGrab(id) => {
|
|
||||||
self.dock_dragging = Some(id);
|
|
||||||
self.dock_resizing = None;
|
|
||||||
self.dock_drag_last = None;
|
|
||||||
self.dock_drag_target = self.dock.location(id);
|
|
||||||
self.dock_expanded = Some(id);
|
|
||||||
iced::Task::none()
|
|
||||||
}
|
|
||||||
DockMsg::ResizeGrab(id) => {
|
|
||||||
self.dock_resizing = Some(id);
|
|
||||||
self.dock_dragging = None;
|
|
||||||
self.dock_drag_last = None;
|
|
||||||
self.dock_drag_target = None;
|
|
||||||
self.dock_expanded = Some(id);
|
|
||||||
iced::Task::none()
|
|
||||||
}
|
|
||||||
DockMsg::WidthReset(id) => {
|
|
||||||
self.dock.reset_width(id);
|
|
||||||
self.save_config();
|
|
||||||
iced::Task::none()
|
|
||||||
}
|
|
||||||
DockMsg::AutoCollapseToggle(id) => {
|
|
||||||
let on = !self.dock.auto_collapse(id);
|
|
||||||
self.dock.set_auto_collapse(id, on);
|
|
||||||
self.dock_expanded = if on { None } else { Some(id) };
|
|
||||||
self.save_config();
|
|
||||||
iced::Task::none()
|
|
||||||
}
|
|
||||||
DockMsg::Close(id) => {
|
|
||||||
match id {
|
|
||||||
PanelId::BlockPalette => {
|
|
||||||
self.show_block_palette = false;
|
|
||||||
self.block_palette.placing = None;
|
|
||||||
}
|
|
||||||
PanelId::ExternalReferences => {
|
|
||||||
self.show_external_references = false;
|
|
||||||
}
|
|
||||||
PanelId::Browser => {
|
|
||||||
self.show_browser = false;
|
|
||||||
}
|
|
||||||
PanelId::NodeGraph => {
|
|
||||||
self.show_node_graph = false;
|
|
||||||
}
|
|
||||||
PanelId::PointCloudManager => {
|
|
||||||
self.pc_manager.show = false;
|
|
||||||
}
|
|
||||||
PanelId::Count => {
|
|
||||||
self.count_palette.show = false;
|
|
||||||
self.ribbon.set_count_palette(false);
|
|
||||||
}
|
|
||||||
PanelId::SheetSetManager => {
|
|
||||||
self.sheet_set.show = false;
|
|
||||||
self.ribbon.set_sheet_set(false);
|
|
||||||
}
|
|
||||||
PanelId::Properties => {
|
|
||||||
self.show_properties = false;
|
|
||||||
self.ribbon.set_properties(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if self.dock_expanded == Some(id) {
|
|
||||||
self.dock_expanded = None;
|
|
||||||
}
|
|
||||||
if self.dock_dragging == Some(id) {
|
|
||||||
self.dock_dragging = None;
|
|
||||||
}
|
|
||||||
if self.dock_resizing == Some(id) {
|
|
||||||
self.dock_resizing = None;
|
|
||||||
}
|
|
||||||
iced::Task::none()
|
|
||||||
}
|
|
||||||
DockMsg::Hover(id) => {
|
|
||||||
// Ignored while dragging/resizing: the pointer is over the
|
|
||||||
// drag preview, not a rail, so a hover must not collapse the
|
|
||||||
// panel being dragged or repoint the resize target.
|
|
||||||
if self.dock_dragging.is_none() && self.dock_resizing.is_none() {
|
|
||||||
self.dock_expanded = Some(id);
|
|
||||||
}
|
|
||||||
iced::Task::none()
|
|
||||||
}
|
|
||||||
DockMsg::HoverExit => {
|
|
||||||
if self.dock_dragging.is_none() && self.dock_resizing.is_none() {
|
|
||||||
if let Some(id) = self.dock_expanded {
|
|
||||||
if self.dock.auto_collapse(id) {
|
|
||||||
self.dock_expanded = None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
iced::Task::none()
|
|
||||||
}
|
|
||||||
DockMsg::DragMove(point) => {
|
|
||||||
// NOTE: xref column drags intentionally do NOT ride this
|
|
||||||
// path: its points live in workspace space while the header
|
|
||||||
// tracker reports header-local points, and mixing the two
|
|
||||||
// produced a one-time jump plus a stuck drag. Column moves
|
|
||||||
// arrive via Message::XrefColMove only.
|
|
||||||
if self.dock_dragging.is_some() {
|
|
||||||
let avail = self.tabs[self.active_tab]
|
|
||||||
.scene
|
|
||||||
.selection
|
|
||||||
.borrow()
|
|
||||||
.view
|
|
||||||
.vp_size
|
|
||||||
.1;
|
|
||||||
let side = if point.x < self.win_size.0 * 0.5 {
|
|
||||||
DockSide::Left
|
|
||||||
} else {
|
|
||||||
DockSide::Right
|
|
||||||
};
|
|
||||||
let index = crate::ui::dock::drop_index(
|
|
||||||
point.y,
|
|
||||||
std::cmp::max(self.dock_visible_len(side), 1),
|
|
||||||
avail,
|
|
||||||
);
|
|
||||||
self.dock_drag_target = Some((side, index));
|
|
||||||
} else if let Some(id) = self.dock_resizing {
|
|
||||||
if let Some(last) = self.dock_drag_last {
|
|
||||||
let dx = point.x - last.x;
|
|
||||||
let delta = match self.dock.location(id) {
|
|
||||||
Some((DockSide::Left, _)) => dx,
|
|
||||||
_ => -dx,
|
|
||||||
};
|
|
||||||
let cur = self.dock.settings(id).width + delta;
|
|
||||||
self.dock.set_width(id, cur);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if self.dock_dragging.is_some() || self.dock_resizing.is_some() {
|
|
||||||
self.dock_drag_last = Some(point);
|
|
||||||
}
|
|
||||||
iced::Task::none()
|
|
||||||
}
|
|
||||||
DockMsg::DragRelease => {
|
|
||||||
if let Some(id) = self.dock_dragging {
|
|
||||||
if let Some((side, index)) = self.dock_drag_target {
|
|
||||||
if self.dock.dock(id, side, index) {
|
|
||||||
self.save_config();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.dock_dragging = None;
|
|
||||||
self.dock_resizing = None;
|
|
||||||
self.dock_drag_last = None;
|
|
||||||
self.dock_drag_target = None;
|
|
||||||
self.xref_col_drag = None;
|
|
||||||
self.xref_col_last = None;
|
|
||||||
self.xref_split_drag = false;
|
|
||||||
iced::Task::none()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether `id` is currently rendered (not closed, not on the start screen /
|
|
||||||
/// clean-screen viewport). Mirrors the visibility filter the edge column
|
|
||||||
/// renderer applies before building each side's stack.
|
|
||||||
pub(crate) fn dock_panel_visible(&self, id: crate::ui::dock::PanelId) -> bool {
|
|
||||||
use crate::ui::dock::PanelId;
|
|
||||||
if self.tabs[self.active_tab].is_start || self.clean_screen {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
match id {
|
|
||||||
PanelId::Properties => self.show_properties,
|
|
||||||
PanelId::BlockPalette => self.show_block_palette,
|
|
||||||
PanelId::ExternalReferences => self.show_external_references,
|
|
||||||
PanelId::Browser => self.show_browser,
|
|
||||||
PanelId::NodeGraph => self.show_node_graph,
|
|
||||||
PanelId::PointCloudManager => self.pc_manager.show,
|
|
||||||
PanelId::Count => self.count_palette.show,
|
|
||||||
PanelId::SheetSetManager => self.sheet_set.show,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Number of panels currently rendered on `side`. Closed panels keep their
|
|
||||||
/// stack slot but take no vertical space, so drag preview geometry must
|
|
||||||
/// size slots against only the panels that are actually visible.
|
|
||||||
pub(crate) fn dock_visible_len(&self, side: crate::app::config::DockSide) -> usize {
|
|
||||||
let ids: &[crate::ui::dock::PanelId] = match side {
|
|
||||||
crate::app::config::DockSide::Left => &self.dock.left,
|
|
||||||
crate::app::config::DockSide::Right => &self.dock.right,
|
|
||||||
};
|
|
||||||
ids.iter()
|
|
||||||
.filter(|id| self.dock_panel_visible(**id))
|
|
||||||
.count()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Rebuild the Reference Manager's entry list from the active drawing.
|
/// Rebuild the Reference Manager's entry list from the active drawing.
|
||||||
/// Uses the tab's session sets (unloaded + stat baseline) so CLI and
|
/// Uses the tab's session sets (unloaded + stat baseline) so CLI and
|
||||||
/// palette agree; writes fresh load-time mtimes back into the tab's stat
|
/// palette agree; writes fresh load-time mtimes back into the tab's stat
|
||||||
@@ -1605,134 +1416,6 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn blockpalette_pin_toggles_autocollapse_and_close_hides() {
|
|
||||||
let mut app = fresh();
|
|
||||||
app.show_block_palette = true;
|
|
||||||
let id = crate::ui::dock::PanelId::BlockPalette;
|
|
||||||
let _ = app.on_dock(crate::ui::dock::DockMsg::AutoCollapseToggle(id));
|
|
||||||
assert!(app.dock.auto_collapse(id), "pin enables auto-collapse");
|
|
||||||
let _ = app.on_dock(crate::ui::dock::DockMsg::AutoCollapseToggle(id));
|
|
||||||
assert!(
|
|
||||||
!app.dock.auto_collapse(id),
|
|
||||||
"second pin disables auto-collapse"
|
|
||||||
);
|
|
||||||
let _ = app.on_dock(crate::ui::dock::DockMsg::Close(id));
|
|
||||||
assert!(!app.show_block_palette, "close dismisses the sidebar");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn blockpalette_dock_moves_to_other_side_and_persists() {
|
|
||||||
let mut app = fresh();
|
|
||||||
// Tests load the user's persisted config; reset the dock to a known
|
|
||||||
// state so this stays hermetic.
|
|
||||||
app.dock = Default::default();
|
|
||||||
let id = crate::ui::dock::PanelId::BlockPalette;
|
|
||||||
assert_eq!(
|
|
||||||
app.dock.location(id),
|
|
||||||
Some((crate::app::config::DockSide::Right, 0))
|
|
||||||
);
|
|
||||||
let _ = app.on_dock(crate::ui::dock::DockMsg::DockGrab(id));
|
|
||||||
app.win_size = (1600.0, 900.0).into();
|
|
||||||
let _ = app.on_dock(crate::ui::dock::DockMsg::DragMove(iced::Point::new(
|
|
||||||
100.0, 100.0,
|
|
||||||
)));
|
|
||||||
assert_eq!(
|
|
||||||
app.dock_drag_target,
|
|
||||||
Some((crate::app::config::DockSide::Left, 0))
|
|
||||||
);
|
|
||||||
let _ = app.on_dock(crate::ui::dock::DockMsg::DragRelease);
|
|
||||||
assert_eq!(
|
|
||||||
app.dock.location(id),
|
|
||||||
Some((crate::app::config::DockSide::Left, 0))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn dock_visible_len_counts_only_rendered_panels() {
|
|
||||||
let mut app = fresh();
|
|
||||||
app.dock = Default::default();
|
|
||||||
app.dock.left = vec![
|
|
||||||
crate::ui::dock::PanelId::BlockPalette,
|
|
||||||
crate::ui::dock::PanelId::Properties,
|
|
||||||
];
|
|
||||||
// Left: block palette hidden, properties shown -> 1 visible panel.
|
|
||||||
app.show_block_palette = false;
|
|
||||||
app.show_properties = true;
|
|
||||||
assert_eq!(app.dock_visible_len(crate::app::config::DockSide::Left), 1);
|
|
||||||
// Reveal the block palette -> both count.
|
|
||||||
app.show_block_palette = true;
|
|
||||||
assert_eq!(app.dock_visible_len(crate::app::config::DockSide::Left), 2);
|
|
||||||
// A hidden (closed) panel counts for nothing even when stacked.
|
|
||||||
app.show_block_palette = false;
|
|
||||||
assert_eq!(app.dock_visible_len(crate::app::config::DockSide::Left), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn dock_drag_target_ignores_hidden_panels_on_the_side() {
|
|
||||||
// Reproduce the persisted layout that exposed a half-height ghost: two
|
|
||||||
// panels live in the left stack but the block palette is hidden
|
|
||||||
// (show_block_palette=false), so only Properties renders. Drag geometry
|
|
||||||
// must count only panels that are actually visible.
|
|
||||||
let mut app = fresh();
|
|
||||||
app.dock = Default::default();
|
|
||||||
app.dock.left = vec![
|
|
||||||
crate::ui::dock::PanelId::BlockPalette,
|
|
||||||
crate::ui::dock::PanelId::Properties,
|
|
||||||
];
|
|
||||||
app.show_block_palette = false;
|
|
||||||
app.show_properties = true;
|
|
||||||
let i = app.active_tab;
|
|
||||||
app.tabs[i].scene.selection.borrow_mut().view.vp_size = (1600.0, 900.0);
|
|
||||||
app.win_size = (1600.0, 900.0).into();
|
|
||||||
let id = crate::ui::dock::PanelId::Properties;
|
|
||||||
let _ = app.on_dock(crate::ui::dock::DockMsg::DockGrab(id));
|
|
||||||
// Pointer near the bottom of the left edge: one visible panel means a
|
|
||||||
// single slot, so every y maps to index 0 (no top/bottom split).
|
|
||||||
let _ = app.on_dock(crate::ui::dock::DockMsg::DragMove(iced::Point::new(
|
|
||||||
100.0, 850.0,
|
|
||||||
)));
|
|
||||||
assert_eq!(
|
|
||||||
app.dock_drag_target,
|
|
||||||
Some((crate::app::config::DockSide::Left, 0))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn dock_drag_target_counts_all_visible_panels() {
|
|
||||||
let mut app = fresh();
|
|
||||||
app.dock = Default::default();
|
|
||||||
app.dock.left = vec![
|
|
||||||
crate::ui::dock::PanelId::BlockPalette,
|
|
||||||
crate::ui::dock::PanelId::Properties,
|
|
||||||
];
|
|
||||||
// Both panels shown: two real slots on the left edge. A pointer near
|
|
||||||
// the bottom maps to the append slot (index == 2).
|
|
||||||
app.show_block_palette = true;
|
|
||||||
app.show_properties = true;
|
|
||||||
let i = app.active_tab;
|
|
||||||
app.tabs[i].scene.selection.borrow_mut().view.vp_size = (1600.0, 900.0);
|
|
||||||
app.win_size = (1600.0, 900.0).into();
|
|
||||||
let id = crate::ui::dock::PanelId::Properties;
|
|
||||||
let _ = app.on_dock(crate::ui::dock::DockMsg::DockGrab(id));
|
|
||||||
let _ = app.on_dock(crate::ui::dock::DockMsg::DragMove(iced::Point::new(
|
|
||||||
100.0, 850.0,
|
|
||||||
)));
|
|
||||||
assert_eq!(
|
|
||||||
app.dock_drag_target,
|
|
||||||
Some((crate::app::config::DockSide::Left, 2))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn blockpalette_width_reset() {
|
|
||||||
let mut app = fresh();
|
|
||||||
let id = crate::ui::dock::PanelId::BlockPalette;
|
|
||||||
app.dock.set_width(id, 500.0);
|
|
||||||
let _ = app.on_dock(crate::ui::dock::DockMsg::WidthReset(id));
|
|
||||||
assert_eq!(app.dock.settings(id).width, 260.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn block_name_from_file_avoids_collisions() {
|
fn block_name_from_file_avoids_collisions() {
|
||||||
let mut app = fresh();
|
let mut app = fresh();
|
||||||
|
|||||||
+17
-24
@@ -2608,27 +2608,19 @@ impl OpenCADStudio {
|
|||||||
Message::ScriptLine(line) => self.feed_script_line(&line),
|
Message::ScriptLine(line) => self.feed_script_line(&line),
|
||||||
|
|
||||||
Message::ToggleLayers => {
|
Message::ToggleLayers => {
|
||||||
if self.active_modal == Some(super::ModalKind::Layers) {
|
if self.show_layers {
|
||||||
self.ribbon.deactivate_tool_if("LAYERS");
|
return self.on_dock(crate::ui::dock::DockMsg::Close(
|
||||||
self.active_modal = None;
|
crate::ui::dock::PanelId::Layers,
|
||||||
self.reset_modal_geometry();
|
));
|
||||||
} else {
|
|
||||||
self.sync_ribbon_layers();
|
|
||||||
self.active_modal = Some(super::ModalKind::Layers);
|
|
||||||
}
|
}
|
||||||
|
self.open_layers_pallet();
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::ToggleXrefManager => {
|
Message::ToggleXrefManager => {
|
||||||
self.show_external_references ^= true;
|
self.show_external_references ^= true;
|
||||||
if self.show_external_references {
|
if self.show_external_references {
|
||||||
use crate::app::config::DockSide;
|
self.dock_open_at_default(crate::ui::dock::PanelId::ExternalReferences);
|
||||||
use crate::ui::dock::PanelId;
|
|
||||||
if self.dock.location(PanelId::ExternalReferences).is_none() {
|
|
||||||
self.dock
|
|
||||||
.dock(PanelId::ExternalReferences, DockSide::Right, usize::MAX);
|
|
||||||
}
|
|
||||||
self.dock_expanded = Some(PanelId::ExternalReferences);
|
|
||||||
self.refresh_xref_manager();
|
self.refresh_xref_manager();
|
||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
@@ -6362,8 +6354,11 @@ impl OpenCADStudio {
|
|||||||
// Ctrl+A in the MText editor selects all of its text.
|
// Ctrl+A in the MText editor selects all of its text.
|
||||||
self.mtext_select_all();
|
self.mtext_select_all();
|
||||||
Task::none()
|
Task::none()
|
||||||
} else if self.active_modal == Some(super::ModalKind::Layers) {
|
} else if self.dock_panel_visible(crate::ui::dock::PanelId::Layers)
|
||||||
// Select every row in the Layer Manager (#236).
|
&& self.tabs[i].layers.selected.is_some()
|
||||||
|
{
|
||||||
|
// Select every row in the Layer Manager (#236) while the
|
||||||
|
// user is working in it (a layer row is selected).
|
||||||
let n = self.tabs[i].layers.layers.len();
|
let n = self.tabs[i].layers.layers.len();
|
||||||
self.tabs[i].layers.selected_multi = (0..n).collect();
|
self.tabs[i].layers.selected_multi = (0..n).collect();
|
||||||
self.tabs[i].layers.selected = (n > 0).then_some(0);
|
self.tabs[i].layers.selected = (n > 0).then_some(0);
|
||||||
@@ -9144,17 +9139,16 @@ impl OpenCADStudio {
|
|||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::LayerNameColGrab => {
|
Message::LayerNameColGrab => {
|
||||||
// Start a Name-column divider drag; rides ModalDragMove.
|
// Start a Name-column divider drag; the dock's pointer capture
|
||||||
self.layer_col_dragging = true;
|
// feeds it (see `on_dock`).
|
||||||
self.modal_drag_last = None;
|
self.dock_drag = Some(crate::ui::dock::DockDrag::LayerColumn);
|
||||||
|
self.dock_drag_last = None;
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::ModalDragMove(p) => {
|
Message::ModalDragMove(p) => {
|
||||||
if let Some(last) = self.modal_drag_last {
|
if let Some(last) = self.modal_drag_last {
|
||||||
let (dx, dy) = (p.x - last.x, p.y - last.y);
|
let (dx, dy) = (p.x - last.x, p.y - last.y);
|
||||||
if self.layer_col_dragging {
|
if self.modal_resizing {
|
||||||
self.layer_name_col_w = (self.layer_name_col_w + dx).clamp(60.0, 640.0);
|
|
||||||
} else if self.modal_resizing {
|
|
||||||
// The grip sits bottom-right, so dragging out grows the
|
// The grip sits bottom-right, so dragging out grows the
|
||||||
// box. The delta is added to each dialog's natural size,
|
// box. The delta is added to each dialog's natural size,
|
||||||
// so clamp it at zero — dragging in past the natural size
|
// so clamp it at zero — dragging in past the natural size
|
||||||
@@ -9188,7 +9182,7 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if self.modal_dragging || self.modal_resizing || self.layer_col_dragging {
|
if self.modal_dragging || self.modal_resizing {
|
||||||
self.modal_drag_last = Some(p);
|
self.modal_drag_last = Some(p);
|
||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
@@ -9196,7 +9190,6 @@ impl OpenCADStudio {
|
|||||||
Message::ModalDragRelease => {
|
Message::ModalDragRelease => {
|
||||||
self.modal_dragging = false;
|
self.modal_dragging = false;
|
||||||
self.modal_resizing = false;
|
self.modal_resizing = false;
|
||||||
self.layer_col_dragging = false;
|
|
||||||
self.modal_drag_last = None;
|
self.modal_drag_last = None;
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-432
@@ -9,11 +9,11 @@ use crate::t;
|
|||||||
use crate::ui::wrap_bar::DensitySwap;
|
use crate::ui::wrap_bar::DensitySwap;
|
||||||
use crate::ui::wrap_bar::WrapFlow;
|
use crate::ui::wrap_bar::WrapFlow;
|
||||||
use iced::widget::{
|
use iced::widget::{
|
||||||
button, canvas, column, container, mouse_area, pane_grid, responsive, row, scrollable, shader,
|
button, column, container, mouse_area, pane_grid, responsive, row, scrollable, shader, stack,
|
||||||
stack, text, Row, Space,
|
text, Row, Space,
|
||||||
};
|
};
|
||||||
use iced::window;
|
use iced::window;
|
||||||
use iced::{keyboard, Background, Border, Color, Element, Fill, Length, Subscription, Task, Theme};
|
use iced::{keyboard, Background, Border, Color, Element, Fill, Subscription, Task, Theme};
|
||||||
use iced_aw::ContextMenu;
|
use iced_aw::ContextMenu;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
@@ -1889,29 +1889,10 @@ bg={bg_ms:.1}ms n={view_count}"
|
|||||||
viewport_stack = viewport_stack.push(tab.graph.view(sections));
|
viewport_stack = viewport_stack.push(tab.graph.view(sections));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Docked side panels (Properties, block palette, future palettes) live
|
// Docked pallets: each edge's icon strip and shown group (see
|
||||||
// in an ordered vertical stack on the left/right edge of the drawing
|
// `app::dock`).
|
||||||
// view. Auto-collapsing (pinned) panels that aren't being hovered
|
let left_edge = self.build_edge_stack(crate::app::config::DockSide::Left, tab);
|
||||||
// reduce to a rail sharing 1/N of the edge column's height; the
|
let right_edge = self.build_edge_stack(crate::app::config::DockSide::Right, tab);
|
||||||
// hovered or unpinned panel expands to the full column height on top.
|
|
||||||
// Single source of truth with dock_visible_len (#57): one new panel
|
|
||||||
// updated in only one place used to break drag math vs rendering.
|
|
||||||
let visible_panel = |id: crate::ui::dock::PanelId| -> bool { self.dock_panel_visible(id) };
|
|
||||||
let edge_stack = |side: crate::app::config::DockSide| -> Option<Element<'_, Message>> {
|
|
||||||
let ids: Vec<crate::ui::dock::PanelId> = match side {
|
|
||||||
crate::app::config::DockSide::Left => self.dock.left.clone(),
|
|
||||||
crate::app::config::DockSide::Right => self.dock.right.clone(),
|
|
||||||
}
|
|
||||||
.into_iter()
|
|
||||||
.filter(|id| visible_panel(*id))
|
|
||||||
.collect();
|
|
||||||
if ids.is_empty() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
Some(self.build_edge_stack(side, &ids, tab))
|
|
||||||
};
|
|
||||||
let left_edge = edge_stack(crate::app::config::DockSide::Left);
|
|
||||||
let right_edge = edge_stack(crate::app::config::DockSide::Right);
|
|
||||||
|
|
||||||
// Command-line sits as a bottom-centre overlay on top of the
|
// Command-line sits as a bottom-centre overlay on top of the
|
||||||
// viewport stack rather than as a separate row in the main
|
// viewport stack rather than as a separate row in the main
|
||||||
@@ -1957,158 +1938,7 @@ bg={bg_ms:.1}ms n={view_count}"
|
|||||||
}
|
}
|
||||||
let workspace: Element<'_, Message> = row(parts).width(Fill).height(Fill).into();
|
let workspace: Element<'_, Message> = row(parts).width(Fill).height(Fill).into();
|
||||||
|
|
||||||
let any_dragging = self.dock_dragging.is_some();
|
let workspace = self.dock_overlay(workspace, tab);
|
||||||
let any_resizing =
|
|
||||||
self.dock_resizing.is_some() || self.xref_col_drag.is_some() || self.xref_split_drag;
|
|
||||||
let workspace = if any_dragging {
|
|
||||||
let id = self.dock_dragging.expect("guarded by any_dragging");
|
|
||||||
let side = self.dock_drag_target.map(|(s, _)| s).unwrap_or(
|
|
||||||
self.dock
|
|
||||||
.location(id)
|
|
||||||
.map(|(s, _)| s)
|
|
||||||
.unwrap_or(crate::app::config::DockSide::Right),
|
|
||||||
);
|
|
||||||
// Dropping onto an edge joins that edge's stack, whose column is as
|
|
||||||
// wide as its widest currently-shown panel (the dragged panel
|
|
||||||
// included). Hidden (closed) panels don't render, so they can't
|
|
||||||
// widen the column being previewed.
|
|
||||||
let preview_width = {
|
|
||||||
let mut widths: Vec<f32> = match side {
|
|
||||||
crate::app::config::DockSide::Left => &self.dock.left,
|
|
||||||
crate::app::config::DockSide::Right => &self.dock.right,
|
|
||||||
}
|
|
||||||
.iter()
|
|
||||||
.copied()
|
|
||||||
.filter(|&pid| self.dock_panel_visible(pid))
|
|
||||||
.map(|pid| self.dock.width(pid, self.win_size.0))
|
|
||||||
.collect();
|
|
||||||
widths.push(self.dock.width(id, self.win_size.0));
|
|
||||||
widths.into_iter().fold(0.0, f32::max)
|
|
||||||
};
|
|
||||||
// Faint tint over the whole edge column so the target edge reads
|
|
||||||
// even before the panel-sized ghost snaps to a slot.
|
|
||||||
let edge_tint = container(Space::new())
|
|
||||||
.width(Length::Fixed(preview_width))
|
|
||||||
.height(Fill)
|
|
||||||
.style(|theme: &Theme| {
|
|
||||||
let palette = theme.palette();
|
|
||||||
container::Style {
|
|
||||||
background: Some(Background::Color(
|
|
||||||
palette.primary.weak.color.scale_alpha(0.35),
|
|
||||||
)),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// The ghost is the dragged panel at its real size: its own saved
|
|
||||||
// width and its 1/N share of the edge (N = number of panels shown
|
|
||||||
// on the edge after the drop), with a header naming which panel is
|
|
||||||
// being moved. Only visibly-docked panels share the column height:
|
|
||||||
// a hidden (closed) panel keeps its stack slot but no screen space,
|
|
||||||
// so it must not split the preview.
|
|
||||||
let was_here = self.dock.location(id).map(|(s, _)| s) == Some(side);
|
|
||||||
let visible = self.dock_visible_len(side);
|
|
||||||
let final_count = if was_here {
|
|
||||||
std::cmp::max(visible, 1)
|
|
||||||
} else {
|
|
||||||
visible + 1
|
|
||||||
};
|
|
||||||
let edge_h = tab.scene.selection.borrow().view.vp_size.1;
|
|
||||||
let slot_h = edge_h / final_count as f32;
|
|
||||||
let index = self.dock_drag_target.map(|(_, i)| i).unwrap_or(0);
|
|
||||||
let slot = index.min(final_count.saturating_sub(1));
|
|
||||||
let ghost_top = slot as f32 * slot_h;
|
|
||||||
let ghost_header = container(text(id.title()).size(12))
|
|
||||||
.width(Fill)
|
|
||||||
.padding(iced::Padding {
|
|
||||||
top: 5.0,
|
|
||||||
right: 8.0,
|
|
||||||
bottom: 5.0,
|
|
||||||
left: 8.0,
|
|
||||||
})
|
|
||||||
.style(|theme: &Theme| {
|
|
||||||
let palette = theme.palette();
|
|
||||||
container::Style {
|
|
||||||
background: Some(Background::Color(palette.primary.base.color)),
|
|
||||||
text_color: Some(palette.primary.base.text),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let ghost_panel = container(column![ghost_header, Space::new()])
|
|
||||||
.width(Length::Fixed(self.dock.width(id, self.win_size.0)))
|
|
||||||
.height(Length::Fixed(slot_h))
|
|
||||||
.style(|theme: &Theme| {
|
|
||||||
let palette = theme.palette();
|
|
||||||
container::Style {
|
|
||||||
background: Some(Background::Color(palette.background.base.color)),
|
|
||||||
border: Border {
|
|
||||||
color: palette.primary.base.color,
|
|
||||||
width: 2.0,
|
|
||||||
radius: 0.0.into(),
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Position the ghost at its drop slot, flush against the edge.
|
|
||||||
let ghost = container(ghost_panel)
|
|
||||||
.width(Fill)
|
|
||||||
.height(Fill)
|
|
||||||
.align_x(match side {
|
|
||||||
crate::app::config::DockSide::Left => iced::alignment::Horizontal::Left,
|
|
||||||
crate::app::config::DockSide::Right => iced::alignment::Horizontal::Right,
|
|
||||||
})
|
|
||||||
.align_y(iced::alignment::Vertical::Top)
|
|
||||||
.padding(iced::Padding {
|
|
||||||
top: ghost_top,
|
|
||||||
right: 0.0,
|
|
||||||
bottom: 0.0,
|
|
||||||
left: 0.0,
|
|
||||||
});
|
|
||||||
// Thin line across the column at the drop slot's top boundary.
|
|
||||||
let drop_line = container(
|
|
||||||
container(Space::new())
|
|
||||||
.width(Fill)
|
|
||||||
.height(Length::Fixed(3.0))
|
|
||||||
.style(|theme: &Theme| container::Style {
|
|
||||||
background: Some(Background::Color(theme.palette().primary.base.color)),
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.width(Fill)
|
|
||||||
.height(Fill)
|
|
||||||
.align_y(iced::alignment::Vertical::Top)
|
|
||||||
.padding(iced::Padding {
|
|
||||||
top: ghost_top,
|
|
||||||
right: 0.0,
|
|
||||||
bottom: 0.0,
|
|
||||||
left: 0.0,
|
|
||||||
});
|
|
||||||
let preview = iced::widget::stack![edge_tint, ghost, drop_line]
|
|
||||||
.width(Length::Fixed(preview_width))
|
|
||||||
.height(Fill);
|
|
||||||
let preview = container(preview)
|
|
||||||
.width(Fill)
|
|
||||||
.height(Fill)
|
|
||||||
.align_x(match side {
|
|
||||||
crate::app::config::DockSide::Left => iced::alignment::Horizontal::Left,
|
|
||||||
crate::app::config::DockSide::Right => iced::alignment::Horizontal::Right,
|
|
||||||
});
|
|
||||||
stack![workspace, preview].width(Fill).height(Fill).into()
|
|
||||||
} else {
|
|
||||||
workspace
|
|
||||||
};
|
|
||||||
let workspace: Element<'_, Message> = if any_dragging || any_resizing {
|
|
||||||
mouse_area(workspace)
|
|
||||||
.on_move(move |p| Message::Dock(crate::ui::dock::DockMsg::DragMove(p)))
|
|
||||||
.on_release(Message::Dock(crate::ui::dock::DockMsg::DragRelease))
|
|
||||||
.interaction(if any_resizing {
|
|
||||||
iced::mouse::Interaction::ResizingHorizontally
|
|
||||||
} else {
|
|
||||||
iced::mouse::Interaction::Grabbing
|
|
||||||
})
|
|
||||||
.into()
|
|
||||||
} else {
|
|
||||||
workspace
|
|
||||||
};
|
|
||||||
let command_line = self.command_line.view(
|
let command_line = self.command_line.view(
|
||||||
allow_autocomplete,
|
allow_autocomplete,
|
||||||
dyn_capturing,
|
dyn_capturing,
|
||||||
@@ -2900,198 +2730,6 @@ impl OpenCADStudio {
|
|||||||
pub(super) fn unfocus_widgets(&self) -> Task<Message> {
|
pub(super) fn unfocus_widgets(&self) -> Task<Message> {
|
||||||
iced::advanced::widget::operate(iced::advanced::widget::operation::focusable::unfocus())
|
iced::advanced::widget::operate(iced::advanced::widget::operation::focusable::unfocus())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build one edge: a narrow tab strip at the very edge plus the expanded
|
|
||||||
/// panels sliding out beside it. Auto-collapsing (pinned) panels are always
|
|
||||||
/// visible as equal-height 1/N tabs in the strip (via `Fill` distribution);
|
|
||||||
/// hovering a tab raises its panel to full column height beside the strip,
|
|
||||||
/// so switching between stacked panels is a direct tab-to-tab hover with no
|
|
||||||
/// width-jumping collapse dance. Unpinned panels are always expanded. The
|
|
||||||
/// whole edge is one hover region: leaving it collapses any auto-collapsing
|
|
||||||
/// panel, but moving between the tabs and the expanded panel does not.
|
|
||||||
fn build_edge_stack<'a>(
|
|
||||||
&'a self,
|
|
||||||
side: crate::app::config::DockSide,
|
|
||||||
ids: &[crate::ui::dock::PanelId],
|
|
||||||
tab: &'a DocumentTab,
|
|
||||||
) -> Element<'a, Message> {
|
|
||||||
let pinned: Vec<crate::ui::dock::PanelId> = ids
|
|
||||||
.iter()
|
|
||||||
.copied()
|
|
||||||
.filter(|id| self.dock.auto_collapse(*id))
|
|
||||||
.collect();
|
|
||||||
// Expanded = unpinned panels (always open) in stack order, then the
|
|
||||||
// hovered auto-collapsing one last so it floats on top.
|
|
||||||
let mut expanded: Vec<crate::ui::dock::PanelId> = ids
|
|
||||||
.iter()
|
|
||||||
.copied()
|
|
||||||
.filter(|id| !self.dock.auto_collapse(*id))
|
|
||||||
.collect();
|
|
||||||
if let Some(id) = self.dock_expanded {
|
|
||||||
if ids.contains(&id) && self.dock.auto_collapse(id) {
|
|
||||||
expanded.push(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Each expanded panel renders at its own saved width; the column is as
|
|
||||||
// wide as the widest one currently showing.
|
|
||||||
let col_w = expanded
|
|
||||||
.iter()
|
|
||||||
.map(|id| self.dock.width(*id, self.win_size.0))
|
|
||||||
.fold(0.0, f32::max);
|
|
||||||
|
|
||||||
let tab_strip = (!pinned.is_empty()).then(|| {
|
|
||||||
let tabs: Vec<Element<'_, Message>> = pinned
|
|
||||||
.iter()
|
|
||||||
.map(|id| {
|
|
||||||
self.rail_slice(
|
|
||||||
*id,
|
|
||||||
side,
|
|
||||||
self.dock_expanded == Some(*id),
|
|
||||||
self.dock_dragging == Some(*id),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
column(tabs).width(DOCK_RAIL_W).height(Fill).into()
|
|
||||||
});
|
|
||||||
let expanded_stack = (!expanded.is_empty()).then(|| {
|
|
||||||
// Multiple simultaneously-expanded panels (e.g. two unpinned panels
|
|
||||||
// on the same edge) stack one below the other, each taking 1/N of
|
|
||||||
// the column height via equal `Fill` shares — never overlapping.
|
|
||||||
let layers: Vec<Element<'_, Message>> = expanded
|
|
||||||
.iter()
|
|
||||||
.map(|id| self.expanded_panel(*id, side, col_w, tab))
|
|
||||||
.collect();
|
|
||||||
column(layers).height(Fill).into()
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut children: Vec<Element<'_, Message>> = Vec::new();
|
|
||||||
match side {
|
|
||||||
crate::app::config::DockSide::Left => {
|
|
||||||
if let Some(t) = tab_strip {
|
|
||||||
children.push(t);
|
|
||||||
}
|
|
||||||
if let Some(e) = expanded_stack {
|
|
||||||
children.push(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
crate::app::config::DockSide::Right => {
|
|
||||||
if let Some(e) = expanded_stack {
|
|
||||||
children.push(e);
|
|
||||||
}
|
|
||||||
if let Some(t) = tab_strip {
|
|
||||||
children.push(t);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mouse_area(row(children).height(Fill))
|
|
||||||
.on_exit(Message::Dock(crate::ui::dock::DockMsg::HoverExit))
|
|
||||||
.into()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// One narrow tab in the edge strip. Equal `Fill` heights across the tabs
|
|
||||||
/// give each 1/N of the strip; hovering or clicking it raises its panel.
|
|
||||||
fn rail_slice(
|
|
||||||
&self,
|
|
||||||
id: crate::ui::dock::PanelId,
|
|
||||||
side: crate::app::config::DockSide,
|
|
||||||
is_active: bool,
|
|
||||||
is_dragging: bool,
|
|
||||||
) -> Element<'_, Message> {
|
|
||||||
let label = canvas(VBarLabel {
|
|
||||||
text: id.title().to_string(),
|
|
||||||
clockwise: side == crate::app::config::DockSide::Left,
|
|
||||||
})
|
|
||||||
.width(Fill)
|
|
||||||
.height(Fill);
|
|
||||||
let bg = move |theme: &Theme| {
|
|
||||||
let palette = theme.palette();
|
|
||||||
if is_active {
|
|
||||||
palette.primary.weak.color
|
|
||||||
} else if is_dragging {
|
|
||||||
palette.primary.weak.color.scale_alpha(0.55)
|
|
||||||
} else {
|
|
||||||
palette.background.base.color
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let tab = container(label)
|
|
||||||
.width(Length::Fixed(DOCK_RAIL_W))
|
|
||||||
.height(Fill)
|
|
||||||
.style(move |theme: &Theme| container::Style {
|
|
||||||
background: Some(Background::Color(bg(theme))),
|
|
||||||
border: Border {
|
|
||||||
color: if is_active {
|
|
||||||
theme.palette().primary.base.color
|
|
||||||
} else {
|
|
||||||
theme.palette().background.neutral.color
|
|
||||||
},
|
|
||||||
width: 1.0,
|
|
||||||
radius: 0.0.into(),
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
mouse_area(tab)
|
|
||||||
.interaction(iced::mouse::Interaction::Pointer)
|
|
||||||
.on_press(Message::Dock(crate::ui::dock::DockMsg::DockGrab(id)))
|
|
||||||
.on_enter(Message::Dock(crate::ui::dock::DockMsg::Hover(id)))
|
|
||||||
.into()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A panel expanded to the full edge column: the panel body plus a
|
|
||||||
/// grabbable divider against the viewport. Hovering is handled by the
|
|
||||||
/// enclosing edge region (see `build_edge_stack`), so the body stays fully
|
|
||||||
/// interactive without fighting the region's hover tracking.
|
|
||||||
fn expanded_panel<'a>(
|
|
||||||
&'a self,
|
|
||||||
id: crate::ui::dock::PanelId,
|
|
||||||
side: crate::app::config::DockSide,
|
|
||||||
width: f32,
|
|
||||||
tab: &'a DocumentTab,
|
|
||||||
) -> Element<'a, Message> {
|
|
||||||
let auto_collapse = self.dock.auto_collapse(id);
|
|
||||||
let panel: Element<'_, Message> = match id {
|
|
||||||
crate::ui::dock::PanelId::Properties => tab.properties.view(width, auto_collapse),
|
|
||||||
crate::ui::dock::PanelId::BlockPalette => {
|
|
||||||
crate::ui::window::block_palette::view(&self.block_palette, width, auto_collapse)
|
|
||||||
}
|
|
||||||
crate::ui::dock::PanelId::ExternalReferences => {
|
|
||||||
self.xref_manager
|
|
||||||
.view(width, auto_collapse, tab.xref_missing, &tab.scene.document)
|
|
||||||
}
|
|
||||||
crate::ui::dock::PanelId::Browser => crate::ui::window::browser::view(
|
|
||||||
&tab.scene.document,
|
|
||||||
tab.sketch_session
|
|
||||||
.as_ref()
|
|
||||||
.map(|session| session.name.as_str()),
|
|
||||||
width,
|
|
||||||
auto_collapse,
|
|
||||||
),
|
|
||||||
crate::ui::dock::PanelId::NodeGraph => tab.graph.panel(width, auto_collapse),
|
|
||||||
crate::ui::dock::PanelId::Count => crate::ui::window::count_palette::view(
|
|
||||||
&self.count_palette,
|
|
||||||
tab.count.as_ref(),
|
|
||||||
&tab.scene.document,
|
|
||||||
tab.id,
|
|
||||||
tab.scene.geometry_epoch,
|
|
||||||
width,
|
|
||||||
auto_collapse,
|
|
||||||
),
|
|
||||||
crate::ui::dock::PanelId::PointCloudManager => crate::ui::window::pc_manager::view(
|
|
||||||
&self.pc_manager,
|
|
||||||
&tab.scene.document,
|
|
||||||
&tab.scene.selected_handles_in_order(),
|
|
||||||
width,
|
|
||||||
auto_collapse,
|
|
||||||
),
|
|
||||||
crate::ui::dock::PanelId::SheetSetManager => {
|
|
||||||
crate::ui::window::sheet_set::view(&self.sheet_set, width, auto_collapse)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let divider = dock_divider(id);
|
|
||||||
match side {
|
|
||||||
crate::app::config::DockSide::Left => row![panel, divider].height(Fill).into(),
|
|
||||||
crate::app::config::DockSide::Right => row![divider, panel].height(Fill).into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Document tab bar ───────────────────────────────────────────────────────
|
// ── Document tab bar ───────────────────────────────────────────────────────
|
||||||
@@ -3420,68 +3058,6 @@ fn pane_mouse_area<'a>(idx: usize) -> Element<'a, Message> {
|
|||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Canvas that draws a label rotated 90° (for a collapsed panel's bar).
|
|
||||||
struct VBarLabel {
|
|
||||||
text: String,
|
|
||||||
clockwise: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl canvas::Program<Message> for VBarLabel {
|
|
||||||
type State = ();
|
|
||||||
|
|
||||||
fn draw(
|
|
||||||
&self,
|
|
||||||
_state: &(),
|
|
||||||
renderer: &iced::Renderer,
|
|
||||||
theme: &Theme,
|
|
||||||
bounds: iced::Rectangle,
|
|
||||||
_cursor: iced::advanced::mouse::Cursor,
|
|
||||||
) -> Vec<canvas::Geometry> {
|
|
||||||
let mut frame = canvas::Frame::new(renderer, bounds.size());
|
|
||||||
frame.with_save(|frame| {
|
|
||||||
frame.translate(iced::Vector::new(bounds.width / 2.0, bounds.height / 2.0));
|
|
||||||
frame.rotate(iced::Radians(if self.clockwise {
|
|
||||||
std::f32::consts::FRAC_PI_2
|
|
||||||
} else {
|
|
||||||
-std::f32::consts::FRAC_PI_2
|
|
||||||
}));
|
|
||||||
frame.fill_text(canvas::Text {
|
|
||||||
content: self.text.clone(),
|
|
||||||
position: iced::Point::ORIGIN,
|
|
||||||
color: theme.palette().background.base.text.scale_alpha(0.72),
|
|
||||||
size: iced::Pixels(13.0),
|
|
||||||
align_x: iced::advanced::text::Alignment::Center,
|
|
||||||
align_y: iced::alignment::Vertical::Center,
|
|
||||||
shaping: iced::advanced::text::Shaping::Advanced,
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
});
|
|
||||||
vec![frame.into_geometry()]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Width of a collapsed (auto-collapsing) panel's tab in the edge strip.
|
|
||||||
const DOCK_RAIL_W: f32 = 28.0;
|
|
||||||
|
|
||||||
/// Grabbable separator for a docked panel managed by the general dock. Same
|
|
||||||
/// visual as the previous per-panel divider but emits generic dock messages.
|
|
||||||
fn dock_divider(id: crate::ui::dock::PanelId) -> Element<'static, Message> {
|
|
||||||
let line = container(Space::new())
|
|
||||||
.width(Length::Fixed(5.0))
|
|
||||||
.height(Fill)
|
|
||||||
.style(|theme: &Theme| container::Style {
|
|
||||||
background: Some(Background::Color(theme.palette().background.neutral.color)),
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
let grab = Message::Dock(crate::ui::dock::DockMsg::ResizeGrab(id));
|
|
||||||
let reset = Message::Dock(crate::ui::dock::DockMsg::WidthReset(id));
|
|
||||||
mouse_area(line)
|
|
||||||
.on_press(grab)
|
|
||||||
.on_double_click(reset)
|
|
||||||
.interaction(iced::mouse::Interaction::ResizingHorizontally)
|
|
||||||
.into()
|
|
||||||
}
|
|
||||||
|
|
||||||
const START_ACTION_RADIUS: f32 = 6.0;
|
const START_ACTION_RADIUS: f32 = 6.0;
|
||||||
|
|
||||||
fn start_action_shape(mut style: button::Style) -> button::Style {
|
fn start_action_shape(mut style: button::Style) -> button::Style {
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ impl OpenCADStudio {
|
|||||||
Some(K::PluginManager) => crate::tr!("modal", "plugin-manager"),
|
Some(K::PluginManager) => crate::tr!("modal", "plugin-manager"),
|
||||||
Some(K::UpdateNotice) => crate::tr!("modal", "update-available"),
|
Some(K::UpdateNotice) => crate::tr!("modal", "update-available"),
|
||||||
Some(K::DonationPrompt) => crate::tr!("donation", "title"),
|
Some(K::DonationPrompt) => crate::tr!("donation", "title"),
|
||||||
Some(K::Layers) => crate::tr!("modal", "layer-manager"),
|
|
||||||
Some(K::LayerStateManager) => crate::tr!("modal", "layer-state-manager"),
|
Some(K::LayerStateManager) => crate::tr!("modal", "layer-state-manager"),
|
||||||
Some(K::LayerTranslator) => crate::t!("Layer Translator").into_owned(),
|
Some(K::LayerTranslator) => crate::t!("Layer Translator").into_owned(),
|
||||||
Some(K::DrawingUnits) => crate::t!("Drawing Units").into_owned(),
|
Some(K::DrawingUnits) => crate::t!("Drawing Units").into_owned(),
|
||||||
@@ -465,12 +464,6 @@ impl OpenCADStudio {
|
|||||||
crate::ui::window::update_notice::view_window(latest, body, flow)
|
crate::ui::window::update_notice::view_window(latest, body, flow)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
super::super::ModalKind::Layers => {
|
|
||||||
let tab = &self.tabs[self.active_tab];
|
|
||||||
sized_flow(ex, 900, 360, |flow| {
|
|
||||||
tab.layers.view_window(self.layer_name_col_w, flow)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
super::super::ModalKind::LayerTranslator => {
|
super::super::ModalKind::LayerTranslator => {
|
||||||
use crate::modules::draw::layers::laytrans;
|
use crate::modules::draw::layers::laytrans;
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
|
|||||||
@@ -5765,6 +5765,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")),
|
"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")),
|
"Block \"{part}\" cannot be found." => Some(("count-feature", "block-not-found")),
|
||||||
"Invalid point." => Some(("count-feature", "invalid-point")),
|
"Invalid point." => Some(("count-feature", "invalid-point")),
|
||||||
|
"Auto-hide" => Some(("dock", "auto-hide")),
|
||||||
|
"Allow docking" => Some(("dock", "allow-docking")),
|
||||||
|
"Dock left" => Some(("dock", "dock-left")),
|
||||||
|
"Dock right" => Some(("dock", "dock-right")),
|
||||||
|
"New group" => Some(("dock", "new-group")),
|
||||||
|
"Browser" => Some(("dock", "browser")),
|
||||||
|
"Node Graph" => Some(("dock", "node-graph")),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// Layers panel toggle — ribbon definition.
|
// Layer Manager pallet — ribbon definition.
|
||||||
|
|
||||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||||
|
|
||||||
@@ -7,6 +7,8 @@ pub fn tool() -> ToolDef {
|
|||||||
id: "LAYERS",
|
id: "LAYERS",
|
||||||
label: "Layers",
|
label: "Layers",
|
||||||
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/layers/panel.svg")),
|
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/layers/panel.svg")),
|
||||||
event: ModuleEvent::ToggleLayers,
|
// A command like the Blocks palette's: it opens the pallet, and the
|
||||||
|
// ribbon drops the button's highlight once it has run.
|
||||||
|
event: ModuleEvent::Command("LAYERS".to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+1211
-181
File diff suppressed because it is too large.
Load diff
@@ -72,7 +72,11 @@ pub static COPY: &[u8] = include_bytes!("../../assets/icons/ui/copy.svg");
|
|||||||
pub static MENU: &[u8] = include_bytes!("../../assets/icons/ui/menu.svg");
|
pub static MENU: &[u8] = include_bytes!("../../assets/icons/ui/menu.svg");
|
||||||
pub static MOVE: &[u8] = include_bytes!("../../assets/icons/ui/move.svg");
|
pub static MOVE: &[u8] = include_bytes!("../../assets/icons/ui/move.svg");
|
||||||
pub static RESIZE: &[u8] = include_bytes!("../../assets/icons/ui/resize.svg");
|
pub static RESIZE: &[u8] = include_bytes!("../../assets/icons/ui/resize.svg");
|
||||||
|
/// `RESIZE` mirrored for a bottom-left corner grip.
|
||||||
|
pub static RESIZE_LEFT: &[u8] = include_bytes!("../../assets/icons/ui/resize_left.svg");
|
||||||
pub static PIN: &[u8] = include_bytes!("../../assets/icons/ui/pin.svg");
|
pub static PIN: &[u8] = include_bytes!("../../assets/icons/ui/pin.svg");
|
||||||
|
/// `PIN` tilted 30°: the pin while auto-hide is on.
|
||||||
|
pub static PIN_ACTIVE: &[u8] = include_bytes!("../../assets/icons/ui/pin_active.svg");
|
||||||
pub static SPLIT_V: &[u8] = include_bytes!("../../assets/icons/ui/split_v.svg");
|
pub static SPLIT_V: &[u8] = include_bytes!("../../assets/icons/ui/split_v.svg");
|
||||||
pub static SPLIT_H: &[u8] = include_bytes!("../../assets/icons/ui/split_h.svg");
|
pub static SPLIT_H: &[u8] = include_bytes!("../../assets/icons/ui/split_h.svg");
|
||||||
pub static GRID: &[u8] = include_bytes!("../../assets/icons/ui/grid.svg");
|
pub static GRID: &[u8] = include_bytes!("../../assets/icons/ui/grid.svg");
|
||||||
|
|||||||
+3
-11
@@ -658,7 +658,7 @@ impl Graph {
|
|||||||
/// The node library as a docked panel: title bar, a search box with the
|
/// The node library as a docked panel: title bar, a search box with the
|
||||||
/// graph's New / Open / Save beside it, then the categories. Entries are
|
/// graph's New / Open / Save beside it, then the categories. Entries are
|
||||||
/// clicked or dragged onto the canvas.
|
/// clicked or dragged onto the canvas.
|
||||||
pub fn panel(&self, width: f32, auto_collapse: bool) -> Element<'_, Message> {
|
pub fn panel(&self, width: f32, chrome: crate::ui::dock::Chrome) -> Element<'_, Message> {
|
||||||
use crate::ui::dock::{self, PanelId};
|
use crate::ui::dock::{self, PanelId};
|
||||||
let tool = |icon: &'static [u8], tip: &str, message: GraphMsg| {
|
let tool = |icon: &'static [u8], tip: &str, message: GraphMsg| {
|
||||||
dock::tool_button(
|
dock::tool_button(
|
||||||
@@ -740,16 +740,8 @@ impl Graph {
|
|||||||
.height(Length::Fill);
|
.height(Length::Fill);
|
||||||
|
|
||||||
dock::frame(
|
dock::frame(
|
||||||
column![
|
dock::title_bar(PanelId::NodeGraph, t!("Node Graph").into_owned(), chrome),
|
||||||
dock::title_bar(
|
column![toolbar, body].spacing(6),
|
||||||
PanelId::NodeGraph,
|
|
||||||
t!("Node Graph").into_owned(),
|
|
||||||
auto_collapse
|
|
||||||
),
|
|
||||||
toolbar,
|
|
||||||
body
|
|
||||||
]
|
|
||||||
.spacing(6),
|
|
||||||
width,
|
width,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -544,14 +544,11 @@ impl PropertiesPanel {
|
|||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn view(&self, width: f32, auto_collapse: bool) -> Element<'_, Message> {
|
pub fn view(&self, width: f32, chrome: crate::ui::dock::Chrome) -> Element<'_, Message> {
|
||||||
use crate::ui::dock::PanelId;
|
use crate::ui::dock::PanelId;
|
||||||
// ── Header ──────────────────────────────────────────────────────────
|
// ── Header ──────────────────────────────────────────────────────────
|
||||||
let header = crate::ui::dock::title_bar(
|
let header =
|
||||||
PanelId::Properties,
|
crate::ui::dock::title_bar(PanelId::Properties, t!("Properties").into_owned(), chrome);
|
||||||
t!("Properties").into_owned(),
|
|
||||||
auto_collapse,
|
|
||||||
);
|
|
||||||
|
|
||||||
// ── Title bar (entity type / "No Selection") ─────────────────────
|
// ── Title bar (entity type / "No Selection") ─────────────────────
|
||||||
let title_content: Element<'_, Message> = if self.selection_groups.is_empty() {
|
let title_content: Element<'_, Message> = if self.selection_groups.is_empty() {
|
||||||
|
|||||||
@@ -800,11 +800,15 @@ fn options_card<'a>(palette: &'a BlockPalette) -> Element<'a, Message> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Build the docked panel element from the palette state.
|
/// Build the docked panel element from the palette state.
|
||||||
pub fn view(palette: &BlockPalette, width: f32, auto_collapse: bool) -> Element<'_, Message> {
|
pub fn view(
|
||||||
|
palette: &BlockPalette,
|
||||||
|
width: f32,
|
||||||
|
chrome: crate::ui::dock::Chrome,
|
||||||
|
) -> Element<'_, Message> {
|
||||||
let title_bar = crate::ui::dock::title_bar(
|
let title_bar = crate::ui::dock::title_bar(
|
||||||
PanelId::BlockPalette,
|
PanelId::BlockPalette,
|
||||||
crate::t!("Blocks").into_owned(),
|
crate::t!("Blocks").into_owned(),
|
||||||
auto_collapse,
|
chrome,
|
||||||
);
|
);
|
||||||
|
|
||||||
let tabs = {
|
let tabs = {
|
||||||
@@ -991,7 +995,8 @@ pub fn view(palette: &BlockPalette, width: f32, auto_collapse: bool) -> Element<
|
|||||||
};
|
};
|
||||||
|
|
||||||
crate::ui::dock::frame(
|
crate::ui::dock::frame(
|
||||||
column![title_bar, top, body, options_card(palette)].spacing(6),
|
title_bar,
|
||||||
|
column![top, body, options_card(palette)].spacing(6),
|
||||||
width,
|
width,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,13 +100,10 @@ pub fn view<'a>(
|
|||||||
document: &'a CadDocument,
|
document: &'a CadDocument,
|
||||||
open_sketch: Option<&'a str>,
|
open_sketch: Option<&'a str>,
|
||||||
width: f32,
|
width: f32,
|
||||||
auto_collapse: bool,
|
chrome: crate::ui::dock::Chrome,
|
||||||
) -> Element<'a, Message> {
|
) -> Element<'a, Message> {
|
||||||
let title_bar = crate::ui::dock::title_bar(
|
let title_bar =
|
||||||
PanelId::Browser,
|
crate::ui::dock::title_bar(PanelId::Browser, crate::t!("Browser").into_owned(), chrome);
|
||||||
crate::t!("Browser").into_owned(),
|
|
||||||
auto_collapse,
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut tree = column![].spacing(1);
|
let mut tree = column![].spacing(1);
|
||||||
|
|
||||||
@@ -151,5 +148,5 @@ pub fn view<'a>(
|
|||||||
.width(Fill)
|
.width(Fill)
|
||||||
.height(Fill);
|
.height(Fill);
|
||||||
|
|
||||||
crate::ui::dock::frame(column![title_bar, body].spacing(6), width)
|
crate::ui::dock::frame(title_bar, column![body].spacing(6), width)
|
||||||
}
|
}
|
||||||
@@ -660,13 +660,10 @@ pub fn view<'a>(
|
|||||||
doc_id: u64,
|
doc_id: u64,
|
||||||
epoch: u64,
|
epoch: u64,
|
||||||
width: f32,
|
width: f32,
|
||||||
auto_collapse: bool,
|
chrome: crate::ui::dock::Chrome,
|
||||||
) -> Element<'a, Message> {
|
) -> Element<'a, Message> {
|
||||||
let title_bar = crate::ui::dock::title_bar(
|
let title_bar =
|
||||||
PanelId::Count,
|
crate::ui::dock::title_bar(PanelId::Count, crate::t!("Count").into_owned(), chrome);
|
||||||
crate::t!("Count").into_owned(),
|
|
||||||
auto_collapse,
|
|
||||||
);
|
|
||||||
let body = match mode {
|
let body = match mode {
|
||||||
Some(mode) if mode.target.is_some() => mode_view(palette, mode, doc),
|
Some(mode) if mode.target.is_some() => mode_view(palette, mode, doc),
|
||||||
_ => {
|
_ => {
|
||||||
@@ -685,7 +682,7 @@ pub fn view<'a>(
|
|||||||
list_view(palette, &instances)
|
list_view(palette, &instances)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
crate::ui::dock::frame(column![title_bar, body].spacing(6), width)
|
crate::ui::dock::frame(title_bar, column![body].spacing(6), width)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bar_button<'a>(
|
fn bar_button<'a>(
|
||||||
|
|||||||
+53
-23
@@ -337,20 +337,40 @@ impl LayerPanel {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Render the layer panel as the full content of its own OS window.
|
/// The Layer Manager as a dock pallet: its title bar, the toolbar across
|
||||||
pub fn view_window(
|
/// the pallet (search box on the right), then the table, at least as wide
|
||||||
|
/// as the pallet and scrolling sideways when the pallet is narrower.
|
||||||
|
pub fn view_panel(
|
||||||
&self,
|
&self,
|
||||||
name_col_w: f32,
|
name_col_w: f32,
|
||||||
sizing: crate::ui::modal::ModalSizing,
|
width: f32,
|
||||||
|
chrome: crate::ui::dock::Chrome,
|
||||||
) -> Element<'_, Message> {
|
) -> Element<'_, Message> {
|
||||||
self.view_content(name_col_w, sizing)
|
use crate::ui::dock::PanelId;
|
||||||
|
let title_bar = crate::ui::dock::title_bar(
|
||||||
|
PanelId::Layers,
|
||||||
|
crate::tr!("modal", "layer-manager"),
|
||||||
|
chrome,
|
||||||
|
);
|
||||||
|
// Inside the pallet's 1 px border on either side.
|
||||||
|
let body = self.view_content(name_col_w, (width - 2.0).max(0.0));
|
||||||
|
container(column![title_bar, body])
|
||||||
|
.width(Length::Fixed(width))
|
||||||
|
.height(Length::Fill)
|
||||||
|
.style(|theme: &Theme| container::Style {
|
||||||
|
background: Some(Background::Color(theme.palette().background.base.color)),
|
||||||
|
border: Border {
|
||||||
|
color: theme.palette().background.neutral.color,
|
||||||
|
width: 1.0,
|
||||||
|
radius: 0.0.into(),
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view_content(
|
/// Toolbar and table for a pallet `inner_w` wide.
|
||||||
&self,
|
fn view_content(&self, name_col_w: f32, inner_w: f32) -> Element<'_, Message> {
|
||||||
name_col_w: f32,
|
|
||||||
sizing: crate::ui::modal::ModalSizing,
|
|
||||||
) -> Element<'_, Message> {
|
|
||||||
let has_sel = self.selected.is_some();
|
let has_sel = self.selected.is_some();
|
||||||
let sel_is_zero = self
|
let sel_is_zero = self
|
||||||
.selected
|
.selected
|
||||||
@@ -377,7 +397,7 @@ impl LayerPanel {
|
|||||||
Message::LayerSetCurrent,
|
Message::LayerSetCurrent,
|
||||||
can_set_current,
|
can_set_current,
|
||||||
),
|
),
|
||||||
iced::widget::Space::new().width(sizing.width),
|
iced::widget::Space::new().width(Length::Fill),
|
||||||
// Search box: filters rows by name as the user types (#343).
|
// Search box: filters rows by name as the user types (#343).
|
||||||
text_input(t!("Search…").as_ref(), &self.filter)
|
text_input(t!("Search…").as_ref(), &self.filter)
|
||||||
.on_input(Message::LayerManagerFilterChanged)
|
.on_input(Message::LayerManagerFilterChanged)
|
||||||
@@ -393,7 +413,7 @@ impl LayerPanel {
|
|||||||
background: Some(Background::Color(theme.palette().background.weak.color)),
|
background: Some(Background::Color(theme.palette().background.weak.color)),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
.width(sizing.width)
|
.width(Length::Fill)
|
||||||
.padding([4, 8]);
|
.padding([4, 8]);
|
||||||
|
|
||||||
// ── Column header ─────────────────────────────────────────────────
|
// ── Column header ─────────────────────────────────────────────────
|
||||||
@@ -473,7 +493,6 @@ impl LayerPanel {
|
|||||||
),
|
),
|
||||||
]
|
]
|
||||||
.spacing(4)
|
.spacing(4)
|
||||||
.width(sizing.width)
|
|
||||||
.align_y(iced::Center);
|
.align_y(iced::Center);
|
||||||
|
|
||||||
for vp in &self.vp_cols {
|
for vp in &self.vp_cols {
|
||||||
@@ -499,7 +518,7 @@ impl LayerPanel {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.padding([4, 8])
|
.padding([4, 8])
|
||||||
.width(sizing.width);
|
.width(Length::Fill);
|
||||||
|
|
||||||
// ── Layer rows ────────────────────────────────────────────────────
|
// ── Layer rows ────────────────────────────────────────────────────
|
||||||
let mut rows_col = column![].spacing(0);
|
let mut rows_col = column![].spacing(0);
|
||||||
@@ -536,17 +555,28 @@ impl LayerPanel {
|
|||||||
|
|
||||||
let table = scrollable(rows_col)
|
let table = scrollable(rows_col)
|
||||||
.id(iced::advanced::widget::Id::new(LAYER_TABLE_SCROLL_ID))
|
.id(iced::advanced::widget::Id::new(LAYER_TABLE_SCROLL_ID))
|
||||||
.height(sizing.height.min(240.0));
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill);
|
||||||
|
|
||||||
// ── Full-window frame ─────────────────────────────────────────────
|
// The header and table stretch to the wider of their columns and the
|
||||||
container(column![toolbar, col_header, table].spacing(0))
|
// pallet (the zero-height spacer), so the header's grey band and
|
||||||
.style(|theme: &Theme| container::Style {
|
// borders always run to the pallet's edge; a narrower pallet scrolls
|
||||||
background: Some(Background::Color(theme.palette().background.base.color)),
|
// the table sideways while the toolbar stays put.
|
||||||
..Default::default()
|
let grid = column![
|
||||||
})
|
col_header,
|
||||||
.width(sizing.width)
|
table,
|
||||||
.height(sizing.height)
|
iced::widget::Space::new()
|
||||||
.into()
|
.width(Length::Fixed(inner_w))
|
||||||
|
.height(0),
|
||||||
|
]
|
||||||
|
.height(Length::Fill);
|
||||||
|
let grid = scrollable(grid)
|
||||||
|
.direction(iced::widget::scrollable::Direction::Horizontal(
|
||||||
|
iced::widget::scrollable::Scrollbar::new(),
|
||||||
|
))
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill);
|
||||||
|
column![toolbar, grid].height(Length::Fill).into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -277,12 +277,12 @@ pub fn view<'a>(
|
|||||||
document: &'a CadDocument,
|
document: &'a CadDocument,
|
||||||
selected: &[Handle],
|
selected: &[Handle],
|
||||||
width: f32,
|
width: f32,
|
||||||
auto_collapse: bool,
|
chrome: crate::ui::dock::Chrome,
|
||||||
) -> Element<'a, Message> {
|
) -> Element<'a, Message> {
|
||||||
let title_bar = crate::ui::dock::title_bar(
|
let title_bar = crate::ui::dock::title_bar(
|
||||||
PanelId::PointCloudManager,
|
PanelId::PointCloudManager,
|
||||||
crate::t!("Point Cloud Manager").into_owned(),
|
crate::t!("Point Cloud Manager").into_owned(),
|
||||||
auto_collapse,
|
chrome,
|
||||||
);
|
);
|
||||||
let query = state.search.trim().to_lowercase();
|
let query = state.search.trim().to_lowercase();
|
||||||
let matches = |label: &str| query.is_empty() || label.to_lowercase().contains(&query);
|
let matches = |label: &str| query.is_empty() || label.to_lowercase().contains(&query);
|
||||||
@@ -416,5 +416,5 @@ pub fn view<'a>(
|
|||||||
.padding([4, 8])
|
.padding([4, 8])
|
||||||
.size(12);
|
.size(12);
|
||||||
|
|
||||||
crate::ui::dock::frame(column![title_bar, card, search].spacing(6), width)
|
crate::ui::dock::frame(title_bar, column![card, search].spacing(6), width)
|
||||||
}
|
}
|
||||||
@@ -1153,12 +1153,12 @@ fn heading(label: &str) -> Element<'static, Message> {
|
|||||||
pub fn view<'a>(
|
pub fn view<'a>(
|
||||||
state: &'a SheetSetManager,
|
state: &'a SheetSetManager,
|
||||||
width: f32,
|
width: f32,
|
||||||
auto_collapse: bool,
|
chrome: crate::ui::dock::Chrome,
|
||||||
) -> Element<'a, Message> {
|
) -> Element<'a, Message> {
|
||||||
let title_bar = crate::ui::dock::title_bar(
|
let title_bar = crate::ui::dock::title_bar(
|
||||||
PanelId::SheetSetManager,
|
PanelId::SheetSetManager,
|
||||||
t!("Sheet Set Manager").into_owned(),
|
t!("Sheet Set Manager").into_owned(),
|
||||||
auto_collapse,
|
chrome,
|
||||||
);
|
);
|
||||||
let mut picks: Vec<Pick> = state
|
let mut picks: Vec<Pick> = state
|
||||||
.sets
|
.sets
|
||||||
@@ -1305,7 +1305,8 @@ pub fn view<'a>(
|
|||||||
.size(10)
|
.size(10)
|
||||||
.style(muted_style);
|
.style(muted_style);
|
||||||
crate::ui::dock::frame(
|
crate::ui::dock::frame(
|
||||||
column![title_bar, combo, tabs, card, hint].spacing(6),
|
title_bar,
|
||||||
|
column![combo, tabs, card, hint].spacing(6),
|
||||||
width,
|
width,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -590,7 +590,7 @@ impl XrefManagerPanel {
|
|||||||
pub fn view<'a>(
|
pub fn view<'a>(
|
||||||
&'a self,
|
&'a self,
|
||||||
width: f32,
|
width: f32,
|
||||||
auto_collapse: bool,
|
chrome: crate::ui::dock::Chrome,
|
||||||
missing: usize,
|
missing: usize,
|
||||||
doc: &'a CadDocument,
|
doc: &'a CadDocument,
|
||||||
) -> Element<'a, Message> {
|
) -> Element<'a, Message> {
|
||||||
@@ -602,7 +602,7 @@ impl XrefManagerPanel {
|
|||||||
crate::t!("External References").as_ref(),
|
crate::t!("External References").as_ref(),
|
||||||
self.entries.len()
|
self.entries.len()
|
||||||
),
|
),
|
||||||
auto_collapse,
|
chrome,
|
||||||
);
|
);
|
||||||
// Table content width: column widths plus gutters, stretched to the
|
// Table content width: column widths plus gutters, stretched to the
|
||||||
// dock when wider so rows fill the panel; narrower docks sidescroll.
|
// dock when wider so rows fill the panel; narrower docks sidescroll.
|
||||||
|
|||||||
Reference in new issue
Block a user