Select Color: Document and Color books tabs

Document tab: every concrete colour the drawing uses (objects, block
definitions and layers), found in one pass when the panel opens, as a
grid of swatches sortable by recently used, most used, colour and colour
number. The recently-used list is now kept per drawing, so the ribbon's
Recent row shows this drawing's colours.

Color books tab: load AutoCAD .acb colour books (remembered in the
settings on desktop), browse them page by page with the page strip,
search by name and see the RGB equivalent. A book colour is applied as
RGB plus its BOOK$NAME identity on objects and layers instead of
dropping the name.

Strings translated into all 22 locales.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_013rGmDUNr1hUf4zo1X3dak8
This commit is contained in:
Claude committed 2026-10-12 19:54:24 +00:00
1 parent 0904661192
commit c4597e7ba0
35 files changed
+1684 -129

No files matched your search

+15
View File
@@ -1356,6 +1356,21 @@ common =
.color-hex = سداسي عشري .color-hex = سداسي عشري
.nearest-index-color = أقرب فهرس .nearest-index-color = أقرب فهرس
.exact-index-match = تطابق تام مع الفهرس .exact-index-match = تطابق تام مع الفهرس
.color-recently-used = المستخدمة مؤخرًا
.color-most-used = الأكثر استخدامًا
.color-number = رقم اللون
.color-in-drawing = الألوان في هذا الرسم
.color-sort = فرز
.color-no-drawing-colors = لا توجد ألوان في هذا الرسم بعد
.color-books = كتب الألوان
.color-no-books = لم يتم تحميل كتب ألوان
.color-load-book = تحميل كتاب ألوان (.acb)
.color-book-label = كتاب الألوان
.color-rgb-equivalent = مكافئ RGB
.color-no-match = لا توجد ألوان مطابقة
.color-not-a-book = ليس كتاب ألوان
.color-book-encrypted = كتاب الألوان هذا مشفر ولا يمكن قراءته
.color-book-empty = لا يحتوي كتاب الألوان على ألوان
.cad-color-index-aci-10-249 = فهرس ألوان ACI 10–249 .cad-color-index-aci-10-249 = فهرس ألوان ACI 10–249
.grayscale-250-255 = { common.grayscale } 250–255 .grayscale-250-255 = { common.grayscale } 250–255
.isometric-drafting = الرسم متساوي القياس .isometric-drafting = الرسم متساوي القياس
+15
View File
@@ -1343,6 +1343,21 @@ common =
.color-hex = Hex .color-hex = Hex
.nearest-index-color = Най-близък индекс .nearest-index-color = Най-близък индекс
.exact-index-match = Точно съвпадение с индекс .exact-index-match = Точно съвпадение с индекс
.color-recently-used = Наскоро използвани
.color-most-used = Най-често използвани
.color-number = Номер на цвят
.color-in-drawing = Цветове в този чертеж
.color-sort = Сортиране
.color-no-drawing-colors = Все още няма цветове в този чертеж
.color-books = Цветови каталози
.color-no-books = Няма заредени цветови каталози
.color-load-book = Зареждане на цветови каталог (.acb)
.color-book-label = Цветови каталог
.color-rgb-equivalent = RGB еквивалент
.color-no-match = Няма съвпадащи цветове
.color-not-a-book = Не е цветови каталог
.color-book-encrypted = Този цветови каталог е шифрован и не може да бъде прочетен
.color-book-empty = Цветовият каталог няма цветове
.cad-color-index-aci-10-249 = ACI Цветови индекс 10–249 .cad-color-index-aci-10-249 = ACI Цветови индекс 10–249
.grayscale-250-255 = { common.grayscale } 250–255 .grayscale-250-255 = { common.grayscale } 250–255
.isometric-drafting = Изометрично чертане .isometric-drafting = Изометрично чертане
+15
View File
@@ -1343,6 +1343,21 @@ common =
.color-hex = Hex .color-hex = Hex
.nearest-index-color = Nejbližší index .nearest-index-color = Nejbližší index
.exact-index-match = Přesná shoda s indexem .exact-index-match = Přesná shoda s indexem
.color-recently-used = Naposledy použité
.color-most-used = Nejpoužívanější
.color-number = Číslo barvy
.color-in-drawing = Barvy v tomto výkresu
.color-sort = Řadit
.color-no-drawing-colors = V tomto výkresu zatím nejsou žádné barvy
.color-books = Barevné vzorníky
.color-no-books = Nejsou načteny žádné barevné vzorníky
.color-load-book = Načíst barevný vzorník (.acb)
.color-book-label = Barevný vzorník
.color-rgb-equivalent = Ekvivalent RGB
.color-no-match = Žádné odpovídající barvy
.color-not-a-book = Nejde o barevný vzorník
.color-book-encrypted = Tento barevný vzorník je zašifrovaný a nelze jej přečíst
.color-book-empty = Barevný vzorník neobsahuje žádné barvy
.cad-color-index-aci-10-249 = Barevný index ACI 10–249 .cad-color-index-aci-10-249 = Barevný index ACI 10–249
.grayscale-250-255 = { common.grayscale } 250–255 .grayscale-250-255 = { common.grayscale } 250–255
.isometric-drafting = Izometrické kreslení .isometric-drafting = Izometrické kreslení
+15
View File
@@ -1344,6 +1344,21 @@ common =
.color-hex = Hex .color-hex = Hex
.nearest-index-color = Nächster Index .nearest-index-color = Nächster Index
.exact-index-match = Exakter Index-Treffer .exact-index-match = Exakter Index-Treffer
.color-recently-used = Zuletzt verwendet
.color-most-used = Am häufigsten verwendet
.color-number = Farbnummer
.color-in-drawing = Farben in dieser Zeichnung
.color-sort = Sortieren
.color-no-drawing-colors = Noch keine Farben in dieser Zeichnung
.color-books = Farbbücher
.color-no-books = Keine Farbbücher geladen
.color-load-book = Farbbuch laden (.acb)
.color-book-label = Farbbuch
.color-rgb-equivalent = RGB-Entsprechung
.color-no-match = Keine passenden Farben
.color-not-a-book = Kein Farbbuch
.color-book-encrypted = Dieses Farbbuch ist verschlüsselt und kann nicht gelesen werden
.color-book-empty = Das Farbbuch enthält keine Farben
.cad-color-index-aci-10-249 = ACI Farbindex 10–249 .cad-color-index-aci-10-249 = ACI Farbindex 10–249
.grayscale-250-255 = { common.grayscale } 250–255 .grayscale-250-255 = { common.grayscale } 250–255
.isometric-drafting = Isometrisches Zeichnen .isometric-drafting = Isometrisches Zeichnen
+15
View File
@@ -1345,6 +1345,21 @@ common =
.color-hex = Hex .color-hex = Hex
.nearest-index-color = Πλησιέστερος δείκτης .nearest-index-color = Πλησιέστερος δείκτης
.exact-index-match = Ακριβής αντιστοίχιση δείκτη .exact-index-match = Ακριβής αντιστοίχιση δείκτη
.color-recently-used = Πρόσφατα χρησιμοποιημένα
.color-most-used = Πιο συχνά χρησιμοποιημένα
.color-number = Αριθμός χρώματος
.color-in-drawing = Χρώματα σε αυτό το σχέδιο
.color-sort = Ταξινόμηση
.color-no-drawing-colors = Δεν υπάρχουν ακόμη χρώματα σε αυτό το σχέδιο
.color-books = Βιβλία χρωμάτων
.color-no-books = Δεν έχουν φορτωθεί βιβλία χρωμάτων
.color-load-book = Φόρτωση βιβλίου χρωμάτων (.acb)
.color-book-label = Βιβλίο χρωμάτων
.color-rgb-equivalent = Ισοδύναμο RGB
.color-no-match = Κανένα χρώμα δεν ταιριάζει
.color-not-a-book = Δεν είναι βιβλίο χρωμάτων
.color-book-encrypted = Αυτό το βιβλίο χρωμάτων είναι κρυπτογραφημένο και δεν μπορεί να διαβαστεί
.color-book-empty = Το βιβλίο χρωμάτων δεν έχει χρώματα
.cad-color-index-aci-10-249 = Δείκτης χρώματος ACI 10–249 .cad-color-index-aci-10-249 = Δείκτης χρώματος ACI 10–249
.grayscale-250-255 = { common.grayscale } 250–255 .grayscale-250-255 = { common.grayscale } 250–255
.isometric-drafting = Ισομετρική σχεδίαση .isometric-drafting = Ισομετρική σχεδίαση
+15
View File
@@ -1344,6 +1344,21 @@ common =
.color-hex = Hex .color-hex = Hex
.nearest-index-color = Nearest index .nearest-index-color = Nearest index
.exact-index-match = Exact index match .exact-index-match = Exact index match
.color-recently-used = Recently used
.color-most-used = Most used
.color-number = Color number
.color-in-drawing = Colors in this drawing
.color-sort = Sort
.color-no-drawing-colors = No colors in this drawing yet
.color-books = Color Books
.color-no-books = No color books loaded
.color-load-book = Load color book (.acb)
.color-book-label = Color book
.color-rgb-equivalent = RGB equivalent
.color-no-match = No colors match
.color-not-a-book = Not a color book
.color-book-encrypted = This color book is encrypted and cannot be read
.color-book-empty = The color book has no colors
.cad-color-index-aci-10-249 = CAD Color Index (ACI) 10–249 .cad-color-index-aci-10-249 = CAD Color Index (ACI) 10–249
.grayscale-250-255 = Grayscale 250–255 .grayscale-250-255 = Grayscale 250–255
.isometric-drafting = Isometric drafting .isometric-drafting = Isometric drafting
+15
View File
@@ -1344,6 +1344,21 @@ common =
.color-hex = Hex .color-hex = Hex
.nearest-index-color = Índice más cercano .nearest-index-color = Índice más cercano
.exact-index-match = Coincide con un índice .exact-index-match = Coincide con un índice
.color-recently-used = Usados recientemente
.color-most-used = Más usados
.color-number = Número de color
.color-in-drawing = Colores de este dibujo
.color-sort = Ordenar
.color-no-drawing-colors = Aún no hay colores en este dibujo
.color-books = Libros de colores
.color-no-books = No hay libros de colores cargados
.color-load-book = Cargar libro de colores (.acb)
.color-book-label = Libro de colores
.color-rgb-equivalent = Equivalente RGB
.color-no-match = Ningún color coincide
.color-not-a-book = No es un libro de colores
.color-book-encrypted = Este libro de colores está cifrado y no se puede leer
.color-book-empty = El libro de colores no tiene colores
.cad-color-index-aci-10-249 = Índice de color ACI 10–249 .cad-color-index-aci-10-249 = Índice de color ACI 10–249
.grayscale-250-255 = { common.grayscale } 250–255 .grayscale-250-255 = { common.grayscale } 250–255
.isometric-drafting = Dibujo isométrico .isometric-drafting = Dibujo isométrico
+15
View File
@@ -1343,6 +1343,21 @@ common =
.color-hex = Hex .color-hex = Hex
.nearest-index-color = Lähin indeksi .nearest-index-color = Lähin indeksi
.exact-index-match = Täsmälleen indeksiväri .exact-index-match = Täsmälleen indeksiväri
.color-recently-used = Viimeksi käytetyt
.color-most-used = Eniten käytetyt
.color-number = Värin numero
.color-in-drawing = Tämän piirustuksen värit
.color-sort = Lajittele
.color-no-drawing-colors = Tässä piirustuksessa ei ole vielä värejä
.color-books = Värikirjat
.color-no-books = Värikirjoja ei ole ladattu
.color-load-book = Lataa värikirja (.acb)
.color-book-label = Värikirja
.color-rgb-equivalent = RGB-vastine
.color-no-match = Ei vastaavia värejä
.color-not-a-book = Ei värikirja
.color-book-encrypted = Tämä värikirja on salattu eikä sitä voi lukea
.color-book-empty = Värikirjassa ei ole värejä
.cad-color-index-aci-10-249 = ACI väriindeksi 10–249 .cad-color-index-aci-10-249 = ACI väriindeksi 10–249
.grayscale-250-255 = { common.grayscale } 250–255 .grayscale-250-255 = { common.grayscale } 250–255
.isometric-drafting = Isometrinen piirtäminen .isometric-drafting = Isometrinen piirtäminen
+15
View File
@@ -1344,6 +1344,21 @@ common =
.color-hex = Hex .color-hex = Hex
.nearest-index-color = Index le plus proche .nearest-index-color = Index le plus proche
.exact-index-match = Correspondance exacte d'index .exact-index-match = Correspondance exacte d'index
.color-recently-used = Utilisées récemment
.color-most-used = Les plus utilisées
.color-number = Numéro de couleur
.color-in-drawing = Couleurs de ce dessin
.color-sort = Trier
.color-no-drawing-colors = Aucune couleur dans ce dessin pour l'instant
.color-books = Carnets de couleurs
.color-no-books = Aucun carnet de couleurs chargé
.color-load-book = Charger un carnet de couleurs (.acb)
.color-book-label = Carnet de couleurs
.color-rgb-equivalent = Équivalent RVB
.color-no-match = Aucune couleur correspondante
.color-not-a-book = Ce n'est pas un carnet de couleurs
.color-book-encrypted = Ce carnet de couleurs est chiffré et ne peut pas être lu
.color-book-empty = Le carnet de couleurs ne contient aucune couleur
.cad-color-index-aci-10-249 = ACI Indice de couleur 10–249 .cad-color-index-aci-10-249 = ACI Indice de couleur 10–249
.grayscale-250-255 = { common.grayscale } 250–255 .grayscale-250-255 = { common.grayscale } 250–255
.isometric-drafting = Dessin isométrique .isometric-drafting = Dessin isométrique
+15
View File
@@ -1332,6 +1332,21 @@ common =
.color-hex = हेक्स .color-hex = हेक्स
.nearest-index-color = निकटतम इंडेक्स .nearest-index-color = निकटतम इंडेक्स
.exact-index-match = इंडेक्स से सटीक मेल .exact-index-match = इंडेक्स से सटीक मेल
.color-recently-used = हाल ही में उपयोग किए गए
.color-most-used = सबसे अधिक उपयोग किए गए
.color-number = रंग संख्या
.color-in-drawing = इस ड्रॉइंग के रंग
.color-sort = क्रमबद्ध करें
.color-no-drawing-colors = इस ड्रॉइंग में अभी तक कोई रंग नहीं है
.color-books = रंग पुस्तकें
.color-no-books = कोई रंग पुस्तक लोड नहीं है
.color-load-book = रंग पुस्तक लोड करें (.acb)
.color-book-label = रंग पुस्तक
.color-rgb-equivalent = RGB समतुल्य
.color-no-match = कोई रंग मेल नहीं खाता
.color-not-a-book = यह रंग पुस्तक नहीं है
.color-book-encrypted = यह रंग पुस्तक एन्क्रिप्टेड है और पढ़ी नहीं जा सकती
.color-book-empty = रंग पुस्तक में कोई रंग नहीं है
.cad-color-index-aci-10-249 = ACI रंग सूचकांक 10–249 .cad-color-index-aci-10-249 = ACI रंग सूचकांक 10–249
.grayscale-250-255 = { common.grayscale } 250–255 .grayscale-250-255 = { common.grayscale } 250–255
.isometric-drafting = सममितीय आरेखन .isometric-drafting = सममितीय आरेखन
+15
View File
@@ -1343,6 +1343,21 @@ common =
.color-hex = Hex .color-hex = Hex
.nearest-index-color = Legközelebbi index .nearest-index-color = Legközelebbi index
.exact-index-match = Pontos indexegyezés .exact-index-match = Pontos indexegyezés
.color-recently-used = Legutóbb használt
.color-most-used = Leggyakrabban használt
.color-number = Színszám
.color-in-drawing = Színek ebben a rajzban
.color-sort = Rendezés
.color-no-drawing-colors = Ebben a rajzban még nincsenek színek
.color-books = Színkönyvek
.color-no-books = Nincs betöltött színkönyv
.color-load-book = Színkönyv betöltése (.acb)
.color-book-label = Színkönyv
.color-rgb-equivalent = RGB-megfelelő
.color-no-match = Nincs egyező szín
.color-not-a-book = Nem színkönyv
.color-book-encrypted = Ez a színkönyv titkosított, és nem olvasható
.color-book-empty = A színkönyv nem tartalmaz színeket
.cad-color-index-aci-10-249 = ACI színindex 10–249 .cad-color-index-aci-10-249 = ACI színindex 10–249
.grayscale-250-255 = { common.grayscale } 250–255 .grayscale-250-255 = { common.grayscale } 250–255
.isometric-drafting = Izometrikus rajzolás .isometric-drafting = Izometrikus rajzolás
+15
View File
@@ -1343,6 +1343,21 @@ common =
.color-hex = Hex .color-hex = Hex
.nearest-index-color = Indice più vicino .nearest-index-color = Indice più vicino
.exact-index-match = Corrispondenza esatta indice .exact-index-match = Corrispondenza esatta indice
.color-recently-used = Usati di recente
.color-most-used = Più usati
.color-number = Numero colore
.color-in-drawing = Colori in questo disegno
.color-sort = Ordina
.color-no-drawing-colors = Ancora nessun colore in questo disegno
.color-books = Album colori
.color-no-books = Nessun album colori caricato
.color-load-book = Carica album colori (.acb)
.color-book-label = Album colori
.color-rgb-equivalent = Equivalente RGB
.color-no-match = Nessun colore corrispondente
.color-not-a-book = Non è un album colori
.color-book-encrypted = Questo album colori è crittografato e non può essere letto
.color-book-empty = L'album colori non contiene colori
.cad-color-index-aci-10-249 = ACI Indice colori 10–249 .cad-color-index-aci-10-249 = ACI Indice colori 10–249
.grayscale-250-255 = { common.grayscale } 250–255 .grayscale-250-255 = { common.grayscale } 250–255
.isometric-drafting = Disegno isometrico .isometric-drafting = Disegno isometrico
+15
View File
@@ -1334,6 +1334,21 @@ common =
.color-hex = 16進 .color-hex = 16進
.nearest-index-color = 最も近いインデックス .nearest-index-color = 最も近いインデックス
.exact-index-match = インデックスと完全一致 .exact-index-match = インデックスと完全一致
.color-recently-used = 最近使用した色
.color-most-used = 使用頻度の高い色
.color-number = 色番号
.color-in-drawing = この図面の色
.color-sort = 並べ替え
.color-no-drawing-colors = この図面にはまだ色がありません
.color-books = カラー ブック
.color-no-books = カラー ブックが読み込まれていません
.color-load-book = カラー ブックを読み込む (.acb)
.color-book-label = カラー ブック
.color-rgb-equivalent = RGB 相当値
.color-no-match = 一致する色がありません
.color-not-a-book = カラー ブックではありません
.color-book-encrypted = このカラー ブックは暗号化されているため読み込めません
.color-book-empty = カラー ブックに色がありません
.cad-color-index-aci-10-249 = ACI カラーインデックス 10–249 .cad-color-index-aci-10-249 = ACI カラーインデックス 10–249
.grayscale-250-255 = { common.grayscale } 250–255 .grayscale-250-255 = { common.grayscale } 250–255
.isometric-drafting = アイソメ図作成 .isometric-drafting = アイソメ図作成
+15
View File
@@ -1343,6 +1343,21 @@ common =
.color-hex = 16진수 .color-hex = 16진수
.nearest-index-color = 가장 가까운 인덱스 .nearest-index-color = 가장 가까운 인덱스
.exact-index-match = 인덱스와 정확히 일치 .exact-index-match = 인덱스와 정확히 일치
.color-recently-used = 최근 사용
.color-most-used = 가장 많이 사용
.color-number = 색상 번호
.color-in-drawing = 이 도면의 색상
.color-sort = 정렬
.color-no-drawing-colors = 이 도면에는 아직 색상이 없습니다
.color-books = 색상표
.color-no-books = 로드된 색상표가 없습니다
.color-load-book = 색상표 로드 (.acb)
.color-book-label = 색상표
.color-rgb-equivalent = RGB 등가
.color-no-match = 일치하는 색상이 없습니다
.color-not-a-book = 색상표가 아닙니다
.color-book-encrypted = 이 색상표는 암호화되어 있어 읽을 수 없습니다
.color-book-empty = 색상표에 색상이 없습니다
.cad-color-index-aci-10-249 = ACI 컬러 인덱스 10–249 .cad-color-index-aci-10-249 = ACI 컬러 인덱스 10–249
.grayscale-250-255 = { common.grayscale } 250–255 .grayscale-250-255 = { common.grayscale } 250–255
.isometric-drafting = 등각 제도 .isometric-drafting = 등각 제도
+15
View File
@@ -1349,6 +1349,21 @@ common =
.color-hex = Hex .color-hex = Hex
.nearest-index-color = ດັດຊະນີທີ່ໃກ້ທີ່ສຸດ .nearest-index-color = ດັດຊະນີທີ່ໃກ້ທີ່ສຸດ
.exact-index-match = ກົງກັບດັດຊະນີພໍດີ .exact-index-match = ກົງກັບດັດຊະນີພໍດີ
.color-recently-used = ໃຊ້ຫຼ້າສຸດ
.color-most-used = ໃຊ້ຫຼາຍທີ່ສຸດ
.color-number = ເລກສີ
.color-in-drawing = ສີໃນແບບແຕ້ມນີ້
.color-sort = ຈັດລຽງ
.color-no-drawing-colors = ຍັງບໍ່ມີສີໃນແບບແຕ້ມນີ້
.color-books = ປຶ້ມສີ
.color-no-books = ບໍ່ໄດ້ໂຫຼດປຶ້ມສີ
.color-load-book = ໂຫຼດປຶ້ມສີ (.acb)
.color-book-label = ປຶ້ມສີ
.color-rgb-equivalent = ທຽບເທົ່າ RGB
.color-no-match = ບໍ່ມີສີທີ່ກົງກັນ
.color-not-a-book = ບໍ່ແມ່ນປຶ້ມສີ
.color-book-encrypted = ປຶ້ມສີນີ້ຖືກເຂົ້າລະຫັດ ແລະອ່ານບໍ່ໄດ້
.color-book-empty = ປຶ້ມສີບໍ່ມີສີ
.cad-color-index-aci-10-249 = CAD Color Index (ACI) 10–249 .cad-color-index-aci-10-249 = CAD Color Index (ACI) 10–249
.grayscale-250-255 = ລະດັບສີເທົາ 250–255 .grayscale-250-255 = ລະດັບສີເທົາ 250–255
.isometric-drafting = ການຂຽນແບບໄອໂຊເມຕຣິກ .isometric-drafting = ການຂຽນແບບໄອໂຊເມຕຣິກ
+15
View File
@@ -1342,6 +1342,21 @@ common =
.color-hex = Hex .color-hex = Hex
.nearest-index-color = Dichtstbijzijnde index .nearest-index-color = Dichtstbijzijnde index
.exact-index-match = Exacte indexkleur .exact-index-match = Exacte indexkleur
.color-recently-used = Recent gebruikt
.color-most-used = Meest gebruikt
.color-number = Kleurnummer
.color-in-drawing = Kleuren in deze tekening
.color-sort = Sorteren
.color-no-drawing-colors = Nog geen kleuren in deze tekening
.color-books = Kleurboeken
.color-no-books = Geen kleurboeken geladen
.color-load-book = Kleurboek laden (.acb)
.color-book-label = Kleurboek
.color-rgb-equivalent = RGB-equivalent
.color-no-match = Geen kleuren gevonden
.color-not-a-book = Geen kleurboek
.color-book-encrypted = Dit kleurboek is versleuteld en kan niet worden gelezen
.color-book-empty = Het kleurboek bevat geen kleuren
.cad-color-index-aci-10-249 = ACI Kleurindex 10–249 .cad-color-index-aci-10-249 = ACI Kleurindex 10–249
.grayscale-250-255 = { common.grayscale } 250–255 .grayscale-250-255 = { common.grayscale } 250–255
.isometric-drafting = Isometrisch tekenen .isometric-drafting = Isometrisch tekenen
+15
View File
@@ -1343,6 +1343,21 @@ common =
.color-hex = Hex .color-hex = Hex
.nearest-index-color = Najbliższy indeks .nearest-index-color = Najbliższy indeks
.exact-index-match = Dokładne dopasowanie indeksu .exact-index-match = Dokładne dopasowanie indeksu
.color-recently-used = Ostatnio używane
.color-most-used = Najczęściej używane
.color-number = Numer koloru
.color-in-drawing = Kolory w tym rysunku
.color-sort = Sortuj
.color-no-drawing-colors = Brak kolorów w tym rysunku
.color-books = Wzorniki kolorów
.color-no-books = Nie wczytano wzorników kolorów
.color-load-book = Wczytaj wzornik kolorów (.acb)
.color-book-label = Wzornik kolorów
.color-rgb-equivalent = Odpowiednik RGB
.color-no-match = Brak pasujących kolorów
.color-not-a-book = To nie jest wzornik kolorów
.color-book-encrypted = Ten wzornik kolorów jest zaszyfrowany i nie można go odczytać
.color-book-empty = Wzornik kolorów nie zawiera kolorów
.cad-color-index-aci-10-249 = Indeks kolorów ACI 10–249 .cad-color-index-aci-10-249 = Indeks kolorów ACI 10–249
.grayscale-250-255 = { common.grayscale } 250–255 .grayscale-250-255 = { common.grayscale } 250–255
.isometric-drafting = Rysowanie izometryczne .isometric-drafting = Rysowanie izometryczne
+15
View File
@@ -1344,6 +1344,21 @@ common =
.color-hex = Hex .color-hex = Hex
.nearest-index-color = Índice mais próximo .nearest-index-color = Índice mais próximo
.exact-index-match = Corresponde exatamente ao índice .exact-index-match = Corresponde exatamente ao índice
.color-recently-used = Usadas recentemente
.color-most-used = Mais usadas
.color-number = Número da cor
.color-in-drawing = Cores neste desenho
.color-sort = Ordenar
.color-no-drawing-colors = Ainda não há cores neste desenho
.color-books = Catálogos de cores
.color-no-books = Nenhum catálogo de cores carregado
.color-load-book = Carregar catálogo de cores (.acb)
.color-book-label = Catálogo de cores
.color-rgb-equivalent = Equivalente RGB
.color-no-match = Nenhuma cor corresponde
.color-not-a-book = Não é um catálogo de cores
.color-book-encrypted = Este catálogo de cores é criptografado e não pode ser lido
.color-book-empty = O catálogo de cores não tem cores
.cad-color-index-aci-10-249 = Índice de cor ACI 10–249 .cad-color-index-aci-10-249 = Índice de cor ACI 10–249
.grayscale-250-255 = { common.grayscale } 250–255 .grayscale-250-255 = { common.grayscale } 250–255
.isometric-drafting = Desenho isométrico .isometric-drafting = Desenho isométrico
+15
View File
@@ -1347,6 +1347,21 @@ common =
.color-hex = Hex .color-hex = Hex
.nearest-index-color = Ближайший индекс .nearest-index-color = Ближайший индекс
.exact-index-match = Точное совпадение с индексом .exact-index-match = Точное совпадение с индексом
.color-recently-used = Недавно использованные
.color-most-used = Чаще всего используемые
.color-number = Номер цвета
.color-in-drawing = Цвета в этом чертеже
.color-sort = Сортировка
.color-no-drawing-colors = В этом чертеже пока нет цветов
.color-books = Альбомы цветов
.color-no-books = Альбомы цветов не загружены
.color-load-book = Загрузить альбом цветов (.acb)
.color-book-label = Альбом цветов
.color-rgb-equivalent = Эквивалент RGB
.color-no-match = Нет подходящих цветов
.color-not-a-book = Это не альбом цветов
.color-book-encrypted = Этот альбом цветов зашифрован и не может быть прочитан
.color-book-empty = В альбоме цветов нет цветов
.cad-color-index-aci-10-249 = Индекс цвета ACI 10–249 .cad-color-index-aci-10-249 = Индекс цвета ACI 10–249
.grayscale-250-255 = { common.grayscale } 250–255 .grayscale-250-255 = { common.grayscale } 250–255
.isometric-drafting = Изометрическое черчение .isometric-drafting = Изометрическое черчение
+15
View File
@@ -1330,6 +1330,21 @@ common =
.color-hex = Hex .color-hex = Hex
.nearest-index-color = En yakın indeks .nearest-index-color = En yakın indeks
.exact-index-match = Tam indeks eşleşmesi .exact-index-match = Tam indeks eşleşmesi
.color-recently-used = Son kullanılanlar
.color-most-used = En çok kullanılanlar
.color-number = Renk numarası
.color-in-drawing = Bu çizimdeki renkler
.color-sort = Sırala
.color-no-drawing-colors = Bu çizimde henüz renk yok
.color-books = Renk kitapları
.color-no-books = Yüklenmiş renk kitabı yok
.color-load-book = Renk kitabı yükle (.acb)
.color-book-label = Renk kitabı
.color-rgb-equivalent = RGB karşılığı
.color-no-match = Eşleşen renk yok
.color-not-a-book = Bir renk kitabı değil
.color-book-encrypted = Bu renk kitabı şifreli ve okunamıyor
.color-book-empty = Renk kitabında renk yok
.cad-color-index-aci-10-249 = ACI renk dizini 10–249 .cad-color-index-aci-10-249 = ACI renk dizini 10–249
.grayscale-250-255 = { common.grayscale } 250–255 .grayscale-250-255 = { common.grayscale } 250–255
.isometric-drafting = İzometrik çizim .isometric-drafting = İzometrik çizim
+15
View File
@@ -1331,6 +1331,21 @@ common =
.color-hex = 十六进制 .color-hex = 十六进制
.nearest-index-color = 最接近的索引色 .nearest-index-color = 最接近的索引色
.exact-index-match = 与索引色完全匹配 .exact-index-match = 与索引色完全匹配
.color-recently-used = 最近使用
.color-most-used = 最常使用
.color-number = 颜色编号
.color-in-drawing = 此图形中的颜色
.color-sort = 排序
.color-no-drawing-colors = 此图形中还没有颜色
.color-books = 配色系统
.color-no-books = 未加载配色系统
.color-load-book = 加载配色系统 (.acb)
.color-book-label = 配色系统
.color-rgb-equivalent = RGB 等效值
.color-no-match = 没有匹配的颜色
.color-not-a-book = 不是配色系统
.color-book-encrypted = 此配色系统已加密,无法读取
.color-book-empty = 配色系统中没有颜色
.cad-color-index-aci-10-249 = ACI 颜色索引 10–249 .cad-color-index-aci-10-249 = ACI 颜色索引 10–249
.grayscale-250-255 = { common.grayscale } 250–255 .grayscale-250-255 = { common.grayscale } 250–255
.isometric-drafting = 等轴测绘图 .isometric-drafting = 等轴测绘图
+15
View File
@@ -1343,6 +1343,21 @@ common =
.color-hex = 十六進位 .color-hex = 十六進位
.nearest-index-color = 最接近的索引色 .nearest-index-color = 最接近的索引色
.exact-index-match = 與索引色完全相符 .exact-index-match = 與索引色完全相符
.color-recently-used = 最近使用
.color-most-used = 最常使用
.color-number = 顏色編號
.color-in-drawing = 此圖面中的顏色
.color-sort = 排序
.color-no-drawing-colors = 此圖面中還沒有顏色
.color-books = 配色系統
.color-no-books = 未載入配色系統
.color-load-book = 載入配色系統 (.acb)
.color-book-label = 配色系統
.color-rgb-equivalent = RGB 等效值
.color-no-match = 沒有相符的顏色
.color-not-a-book = 不是配色系統
.color-book-encrypted = 此配色系統已加密,無法讀取
.color-book-empty = 配色系統中沒有顏色
.cad-color-index-aci-10-249 = ACI 顏色索引 10–249 .cad-color-index-aci-10-249 = ACI 顏色索引 10–249
.grayscale-250-255 = { common.grayscale } 250–255 .grayscale-250-255 = { common.grayscale } 250–255
.isometric-drafting = 等角製圖 .isometric-drafting = 等角製圖
+3
View File
@@ -277,6 +277,8 @@ pub(super) struct DocumentTab {
/// Model-space corners of the last crossing window drawn with no command /// Model-space corners of the last crossing window drawn with no command
/// running: STRETCH on that selection stretches through it (#1068). /// running: STRETCH on that selection stretches through it (#1068).
pub(super) last_crossing_window: Option<([f64; 2], [f64; 2])>, pub(super) last_crossing_window: Option<([f64; 2], [f64; 2])>,
/// Colours picked in this drawing this session, newest first. Not saved.
pub(super) recent_colors: Vec<codec::types::Color>,
} }
impl DocumentTab { impl DocumentTab {
@@ -679,6 +681,7 @@ impl DocumentTab {
suspended_cmd: None, suspended_cmd: None,
transparent_resume: false, transparent_resume: false,
last_crossing_window: None, last_crossing_window: None,
recent_colors: Vec::new(),
} }
} }
+12 -4
View File
@@ -965,9 +965,12 @@ pub(super) struct OpenCADStudio {
// ── Floating panel windows ──────────────────────────────────────────── // ── Floating panel windows ────────────────────────────────────────────
/// The open Select Color panel, if any. /// The open Select Color panel, if any.
color_picker: Option<crate::ui::color_select::ColorPicker>, color_picker: Option<crate::ui::color_select::ColorPicker>,
/// Colour books loaded this session, read lazily from `color_book_paths`.
color_books: std::sync::Arc<Vec<crate::ui::color_select::book::ColorBook>>,
color_book_paths: Vec<String>,
color_books_read: bool,
/// Recently committed real colours, newest first. /// Recently committed real colours, newest first.
/// ACI and True Color remain semantically distinct even when their RGB matches. /// ACI and True Color remain semantically distinct even when their RGB matches.
recent_colors: Vec<AcadColor>,
/// The open in-canvas modal dialog, if any (Plan B: shared overlay instead /// The open in-canvas modal dialog, if any (Plan B: shared overlay instead
/// of OS windows). /// of OS windows).
active_modal: Option<ModalKind>, active_modal: Option<ModalKind>,
@@ -4168,8 +4171,11 @@ pub enum Message {
ColorPicker(crate::ui::color_select::ColorPickerMsg), ColorPicker(crate::ui::color_select::ColorPickerMsg),
/// Close the colour picker without choosing. /// Close the colour picker without choosing.
CloseColorPicker, CloseColorPicker,
/// Commit the colour chosen in the shared picker. /// Commit the colour chosen in the shared picker, with its `BOOK$NAME`
ColorWindowPick(codec::types::Color), /// when it comes from a colour book.
ColorWindowPick(codec::types::Color, Option<String>),
/// An `.acb` file was read: the book and, on desktop, where it lives.
ColorBookLoaded(Result<(crate::ui::color_select::book::ColorBook, Option<String>), String>),
/// Set a block/linetype Handle field on the selected dim style from a /// Set a block/linetype Handle field on the selected dim style from a
/// dropdown of available block-records / linetypes (by name). /// dropdown of available block-records / linetypes (by name).
DsSetHandle { DsSetHandle {
@@ -4601,7 +4607,9 @@ impl OpenCADStudio {
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
web_autosave_clear_after: Vec::new(), web_autosave_clear_after: Vec::new(),
color_picker: None, color_picker: None,
recent_colors: Vec::new(), color_books: Default::default(),
color_book_paths: Vec::new(),
color_books_read: false,
active_modal: None, active_modal: None,
hyperlink_editor_handles: Vec::new(), hyperlink_editor_handles: Vec::new(),
hyperlink_editor_url: String::new(), hyperlink_editor_url: String::new(),
+4
View File
@@ -619,6 +619,9 @@ pub struct UserSettings {
/// Blocks palette: library folders or drawings, last used first. /// Blocks palette: library folders or drawings, last used first.
#[serde(default)] #[serde(default)]
pub block_libraries: Vec<String>, pub block_libraries: Vec<String>,
/// Colour book (.acb) files loaded in the Select Color panel.
#[serde(default)]
pub color_books: Vec<String>,
/// Blocks palette view: 0 extra large, 1 large, 2 medium, 3 small icons, /// Blocks palette view: 0 extra large, 1 large, 2 medium, 3 small icons,
/// 4 details, 5 list. /// 4 details, 5 list.
#[serde(default = "default_block_view")] #[serde(default = "default_block_view")]
@@ -901,6 +904,7 @@ impl Default for UserSettings {
block_recent: Vec::new(), block_recent: Vec::new(),
block_favorites: Vec::new(), block_favorites: Vec::new(),
block_libraries: Vec::new(), block_libraries: Vec::new(),
color_books: Vec::new(),
block_palette_view: 1, block_palette_view: 1,
block_insert: BlockInsertOptions::default(), block_insert: BlockInsertOptions::default(),
block_mru_list: 50, block_mru_list: 50,
+2
View File
@@ -860,6 +860,7 @@ impl OpenCADStudio {
block_recent: self.block_palette.recent.clone(), block_recent: self.block_palette.recent.clone(),
block_favorites: self.block_palette.favorites.clone(), block_favorites: self.block_palette.favorites.clone(),
block_libraries: self.block_palette.libraries.clone(), block_libraries: self.block_palette.libraries.clone(),
color_books: self.color_book_paths.clone(),
block_palette_view: self.block_palette.view as u8, block_palette_view: self.block_palette.view as u8,
block_insert: self.block_palette.options.clone(), block_insert: self.block_palette.options.clone(),
block_mru_list: self.block_mru_list, block_mru_list: self.block_mru_list,
@@ -1025,6 +1026,7 @@ impl OpenCADStudio {
self.block_palette.recent = s.block_recent.clone(); self.block_palette.recent = s.block_recent.clone();
self.block_palette.favorites = s.block_favorites.clone(); self.block_palette.favorites = s.block_favorites.clone();
self.block_palette.libraries = s.block_libraries.clone(); self.block_palette.libraries = s.block_libraries.clone();
self.color_book_paths = s.color_books.clone();
self.block_palette.view = self.block_palette.view =
crate::ui::window::block_palette::ViewMode::from_u8(s.block_palette_view); crate::ui::window::block_palette::ViewMode::from_u8(s.block_palette_view);
self.block_palette.set_options(s.block_insert.clone()); self.block_palette.set_options(s.block_insert.clone());
+181 -69
View File
@@ -4180,38 +4180,7 @@ impl OpenCADStudio {
Task::none() Task::none()
} }
Message::LayerColorSet(color) => { Message::LayerColorSet(color) => self.set_layer_color(color, None),
let i = self.active_tab;
// Apply to every selected layer (multi-select), not just one.
let names = self.selected_layer_names(i);
if !names.is_empty() {
let undo = self.begin_layer_undo(i, "LAYER COLOR", &names);
for name in &names {
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) {
dl.color = color;
dl.color_name = None;
dl.book_name = None;
}
}
for pl in self.tabs[i].layers.layers.iter_mut() {
if names.contains(&pl.name) {
pl.color = color;
pl.color_name = None;
pl.book_name = None;
}
}
self.tabs[i].dirty = true;
self.commit_layer_undo(i, undo);
// ByLayer color is baked into the cached wires at
// tessellation time, so bump the geometry epoch to
// invalidate the wire cache and repaint with the new color.
self.tabs[i].scene.invalidate_layer_dependencies(&names);
self.tabs[i].layers.color_picker_row = None;
self.tabs[i].layers.color_full_palette = false;
self.sync_ribbon_layers();
}
Task::none()
}
Message::LayerLinetypeSet(lt) => { Message::LayerLinetypeSet(lt) => {
let i = self.active_tab; let i = self.active_tab;
@@ -7264,22 +7233,7 @@ impl OpenCADStudio {
Task::none() Task::none()
} }
Message::PropColorChanged(color) => { Message::PropColorChanged(color) => self.set_property_color(color, None),
let i = self.active_tab;
let handles = self.property_target_handles(i);
if handles.is_empty() {
let task = self.on_ribbon_color_changed(color);
self.refresh_properties();
return task;
}
self.apply_property_op(i, "CHPROP", &handles, |app, handle| {
if let Some(entity) = app.tabs[i].scene.document.get_entity_mut(handle) {
crate::scene::view::dispatch::apply_color(entity, color);
}
});
self.tabs[i].properties.color_picker_open = false;
Task::none()
}
Message::PropLwChanged(lw) => { Message::PropLwChanged(lw) => {
let i = self.active_tab; let i = self.active_tab;
@@ -11605,10 +11559,19 @@ impl OpenCADStudio {
) { ) {
self.graphic_attributes.open_menu = None; self.graphic_attributes.open_menu = None;
} }
let (current, varies) = self.color_picker_current(&target, color); let (current, varies, name) = self.color_picker_current(&target, color);
self.color_picker = Some(crate::ui::color_select::ColorPicker::new( let mut picker = crate::ui::color_select::ColorPicker::new(target, current, varies);
target, current, varies, if !matches!(picker.target, crate::app::ColorPickTarget::Interface(_)) {
)); self.read_color_books();
let tab = &self.tabs[self.active_tab];
picker = picker.with_drawing(
crate::ui::color_select::document::collect(&tab.scene.document),
&tab.recent_colors,
std::sync::Arc::clone(&self.color_books),
name,
);
}
self.color_picker = Some(picker);
self.modal_offset = iced::Vector::ZERO; self.modal_offset = iced::Vector::ZERO;
self.ds_color_open = None; self.ds_color_open = None;
self.mls_color_open = None; self.mls_color_open = None;
@@ -11628,9 +11591,28 @@ impl OpenCADStudio {
return Task::none(); return Task::none();
}; };
match picker.update(msg) { match picker.update(msg) {
Some(crate::ui::color_select::Outcome::Commit(color)) => { Some(crate::ui::color_select::Outcome::Commit(color, name)) => {
self.update(Message::ColorWindowPick(color)) self.update(Message::ColorWindowPick(color, name))
} }
Some(crate::ui::color_select::Outcome::LoadBook) => Task::perform(
async {
let file = crate::sys::file_dialog()
.add_filter(crate::t!("Color book").as_ref(), &["acb", "ACB"])
.pick_file()
.await
.ok_or_else(|| "Cancelled".to_string())?;
let book = crate::ui::color_select::book::parse(
&String::from_utf8_lossy(&file.read().await),
)?;
// The web build has no paths to read again later.
#[cfg(not(target_arch = "wasm32"))]
let path = Some(crate::sys::handle_path(&file).display().to_string());
#[cfg(target_arch = "wasm32")]
let path = None;
Ok((book, path))
},
Message::ColorBookLoaded,
),
Some(crate::ui::color_select::Outcome::Cancel) => { Some(crate::ui::color_select::Outcome::Cancel) => {
self.color_picker = None; self.color_picker = None;
Task::none() Task::none()
@@ -11656,7 +11638,7 @@ impl OpenCADStudio {
Task::none() Task::none()
} }
Message::ColorWindowPick(color) => { Message::ColorWindowPick(color, name) => {
// Interface colours are not drawing colours: keep them out of // Interface colours are not drawing colours: keep them out of
// the recent list. // the recent list.
if !matches!( if !matches!(
@@ -11665,7 +11647,35 @@ impl OpenCADStudio {
) { ) {
self.note_recent_color(color); self.note_recent_color(color);
} }
self.on_color_window_pick(color) self.on_color_window_pick(color, name)
}
Message::ColorBookLoaded(Ok((book, path))) => {
// A book loaded again replaces the earlier copy.
let books = std::sync::Arc::make_mut(&mut self.color_books);
let index = match books.iter().position(|b| b.name == book.name) {
Some(index) => {
books[index] = book;
index
}
None => {
books.push(book);
books.len() - 1
}
};
if let Some(path) = path.filter(|path| !self.color_book_paths.contains(path)) {
self.color_book_paths.push(path);
self.save_config();
}
if let Some(picker) = self.color_picker.as_mut() {
picker.show_book(std::sync::Arc::clone(&self.color_books), index);
}
Task::none()
}
Message::ColorBookLoaded(Err(error)) => {
if let Some(picker) = self.color_picker.as_mut().filter(|_| error != "Cancelled") {
picker.set_status(error);
}
Task::none()
} }
Message::DsSetHandle { field, value } => self.on_ds_set_handle(field, value), Message::DsSetHandle { field, value } => self.on_ds_set_handle(field, value),
} }
@@ -11674,16 +11684,33 @@ impl OpenCADStudio {
/// The colour the Select Color panel shows as "Current", and whether the /// The colour the Select Color panel shows as "Current", and whether the
/// selection it edits has mixed colours. A mixed selection starts from the /// selection it edits has mixed colours. A mixed selection starts from the
/// colour of its most recently drawn object (the highest handle). /// colour of its most recently drawn object (the highest handle).
/// Also returns the colour-book identity (`BOOK$NAME`) of that colour.
fn color_picker_current( fn color_picker_current(
&self, &self,
target: &crate::app::ColorPickTarget, target: &crate::app::ColorPickTarget,
color: codec::types::Color, color: codec::types::Color,
) -> (codec::types::Color, bool) { ) -> (codec::types::Color, bool, Option<String>) {
if !matches!(target, crate::app::ColorPickTarget::Properties) {
return (color, false);
}
let i = self.active_tab; let i = self.active_tab;
let document = &self.tabs[i].scene.document; let document = &self.tabs[i].scene.document;
let name_of = |handle| {
document
.get_entity(handle)
.and_then(|entity| entity.common().color_name.clone())
};
match target {
crate::app::ColorPickTarget::Properties => {}
crate::app::ColorPickTarget::Layer(layer) => {
let name = document.layers.get(layer).and_then(|layer| {
match (&layer.book_name, &layer.color_name) {
(Some(book), Some(name)) => Some(format!("{book}${name}")),
(None, Some(name)) => Some(name.clone()),
_ => None,
}
});
return (color, false, name);
}
_ => return (color, false, None),
}
let mut colors = self let mut colors = self
.property_target_handles(i) .property_target_handles(i)
.into_iter() .into_iter()
@@ -11693,15 +11720,99 @@ impl OpenCADStudio {
.map(|entity| (handle, entity.common().color)) .map(|entity| (handle, entity.common().color))
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let Some(&(_, first)) = colors.first() else { let Some(&(first_handle, first)) = colors.first() else {
return (color, false); return (color, false, None);
}; };
if colors.iter().all(|&(_, c)| c == first) { if colors.iter().all(|&(_, c)| c == first) {
return (first, false); return (first, false, name_of(first_handle));
} }
colors.sort_by_key(|&(handle, _)| handle); colors.sort_by_key(|&(handle, _)| handle);
let newest = colors.last().map_or(color, |&(_, c)| c); let (newest_handle, newest) = colors.last().copied().unwrap_or((first_handle, color));
(newest, true) (newest, true, name_of(newest_handle))
}
/// Read the colour books remembered in the settings, once per session.
fn read_color_books(&mut self) {
if std::mem::replace(&mut self.color_books_read, true) {
return;
}
#[cfg(not(target_arch = "wasm32"))]
{
let books: Vec<_> = self
.color_book_paths
.iter()
.filter_map(|path| std::fs::read_to_string(path).ok())
.filter_map(|text| crate::ui::color_select::book::parse(&text).ok())
.collect();
self.color_books = std::sync::Arc::new(books);
}
}
/// Colour the selected objects (or set the current colour when nothing
/// is selected). `name` is a colour-book identity, `BOOK$NAME`.
pub(super) fn set_property_color(
&mut self,
color: codec::types::Color,
name: Option<String>,
) -> Task<Message> {
let i = self.active_tab;
let handles = self.property_target_handles(i);
if handles.is_empty() {
let task = self.on_ribbon_color_changed(color);
self.refresh_properties();
return task;
}
self.apply_property_op(i, "CHPROP", &handles, |app, handle| {
if let Some(entity) = app.tabs[i].scene.document.get_entity_mut(handle) {
crate::scene::view::dispatch::apply_named_color(entity, color, name.clone());
}
});
self.tabs[i].properties.color_picker_open = false;
Task::none()
}
/// Colour every selected layer (multi-select), not just one. `name` is a
/// colour-book identity, `BOOK$NAME`.
pub(super) fn set_layer_color(
&mut self,
color: codec::types::Color,
name: Option<&str>,
) -> Task<Message> {
let i = self.active_tab;
// Layers keep the book and the colour name apart.
let (book_name, color_name) = match name.map(|name| name.split_once('$')) {
Some(Some((book, name))) => (Some(book), Some(name)),
Some(None) => (None, name),
None => (None, None),
};
let names = self.selected_layer_names(i);
if !names.is_empty() {
let undo = self.begin_layer_undo(i, "LAYER COLOR", &names);
for name in &names {
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) {
dl.color = color;
dl.color_name = color_name.map(str::to_string);
dl.book_name = book_name.map(str::to_string);
}
}
for pl in self.tabs[i].layers.layers.iter_mut() {
if names.contains(&pl.name) {
pl.color = color;
pl.color_name = color_name.map(str::to_string);
pl.book_name = book_name.map(str::to_string);
}
}
self.tabs[i].dirty = true;
self.commit_layer_undo(i, undo);
// ByLayer color is baked into the cached wires at
// tessellation time, so bump the geometry epoch to
// invalidate the wire cache and repaint with the new color.
self.tabs[i].scene.invalidate_layer_dependencies(&names);
self.tabs[i].layers.color_picker_row = None;
self.tabs[i].layers.color_full_palette = false;
self.sync_ribbon_layers();
}
Task::none()
} }
pub(crate) fn note_recent_color(&mut self, color: codec::types::Color) { pub(crate) fn note_recent_color(&mut self, color: codec::types::Color) {
@@ -11712,12 +11823,13 @@ impl OpenCADStudio {
codec::types::Color::Index(_) | codec::types::Color::Rgb { .. } codec::types::Color::Index(_) | codec::types::Color::Rgb { .. }
) { ) {
// No duplicates: selecting an existing colour moves it to the front. // No duplicates: selecting an existing colour moves it to the front.
if let Some(pos) = self.recent_colors.iter().position(|c| c == &color) { let recent = &mut self.tabs[self.active_tab].recent_colors;
self.recent_colors.remove(pos); if let Some(pos) = recent.iter().position(|c| c == &color) {
recent.remove(pos);
} }
self.recent_colors.insert(0, color); recent.insert(0, color);
self.recent_colors.truncate(12); recent.truncate(12);
} }
} }
+11 -5
View File
@@ -1136,7 +1136,11 @@ impl OpenCADStudio {
Task::none() Task::none()
} }
pub(super) fn on_color_window_pick(&mut self, color: codec::types::Color) -> Task<Message> { pub(super) fn on_color_window_pick(
&mut self,
color: codec::types::Color,
name: Option<String>,
) -> Task<Message> {
if let Some(crate::app::ColorPickTarget::Interface(target)) = self if let Some(crate::app::ColorPickTarget::Interface(target)) = self
.color_picker .color_picker
.as_ref() .as_ref()
@@ -1201,7 +1205,9 @@ impl OpenCADStudio {
field: f, field: f,
value: s, value: s,
}), }),
Some(crate::app::ColorPickTarget::Properties) => Some(Message::PropColorChanged(color)), Some(crate::app::ColorPickTarget::Properties) => {
return self.set_property_color(color, name);
}
Some(crate::app::ColorPickTarget::PropertiesBg) => { Some(crate::app::ColorPickTarget::PropertiesBg) => {
Some(Message::PropBgColorChanged(color)) Some(Message::PropBgColorChanged(color))
} }
@@ -1210,12 +1216,12 @@ impl OpenCADStudio {
} }
Some(crate::app::ColorPickTarget::MText) => Some(Message::MTextColorChanged(color)), Some(crate::app::ColorPickTarget::MText) => Some(Message::MTextColorChanged(color)),
Some(crate::app::ColorPickTarget::Ribbon) => Some(Message::RibbonColorChanged(color)), Some(crate::app::ColorPickTarget::Ribbon) => Some(Message::RibbonColorChanged(color)),
Some(crate::app::ColorPickTarget::Layer(name)) => { Some(crate::app::ColorPickTarget::Layer(layer)) => {
let i = self.active_tab; let i = self.active_tab;
match self.layer_panel_index(i, &name) { match self.layer_panel_index(i, &layer) {
Some(idx) => { Some(idx) => {
self.tabs[i].layers.selected = Some(idx); self.tabs[i].layers.selected = Some(idx);
Some(Message::LayerColorSet(color)) return self.set_layer_color(color, name.as_deref());
} }
None => None, None => None,
} }
+1 -1
View File
@@ -2187,7 +2187,7 @@ bg={bg_ms:.1}ms n={view_count}"
&redo_labels, &redo_labels,
self.win_size, self.win_size,
self.tabs[self.active_tab].is_start, self.tabs[self.active_tab].is_start,
&self.recent_colors, &self.tabs[self.active_tab].recent_colors,
) )
.unwrap_or_else(|| iced::widget::Space::new().width(0).height(0).into()) .unwrap_or_else(|| iced::widget::Space::new().width(0).height(0).into())
}; };
+15
View File
@@ -4720,6 +4720,21 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
"Hex" => Some(("common", "color-hex")), "Hex" => Some(("common", "color-hex")),
"Nearest index" => Some(("common", "nearest-index-color")), "Nearest index" => Some(("common", "nearest-index-color")),
"Exact index match" => Some(("common", "exact-index-match")), "Exact index match" => Some(("common", "exact-index-match")),
"Recently used" => Some(("common", "color-recently-used")),
"Most used" => Some(("common", "color-most-used")),
"Color number" => Some(("common", "color-number")),
"Colors in this drawing" => Some(("common", "color-in-drawing")),
"Sort" => Some(("common", "color-sort")),
"No colors in this drawing yet" => Some(("common", "color-no-drawing-colors")),
"Color Books" => Some(("common", "color-books")),
"No color books loaded" => Some(("common", "color-no-books")),
"Load color book (.acb)" => Some(("common", "color-load-book")),
"Color book" => Some(("common", "color-book-label")),
"RGB equivalent" => Some(("common", "color-rgb-equivalent")),
"No colors match" => Some(("common", "color-no-match")),
"Not a color book" => Some(("common", "color-not-a-book")),
"This color book is encrypted and cannot be read" => Some(("common", "color-book-encrypted")),
"The color book has no colors" => Some(("common", "color-book-empty")),
"CAD Color Index (ACI) 10–249" => Some(("common", "cad-color-index-aci-10-249")), "CAD Color Index (ACI) 10–249" => Some(("common", "cad-color-index-aci-10-249")),
"Grayscale 250–255" => Some(("common", "grayscale-250-255")), "Grayscale 250–255" => Some(("common", "grayscale-250-255")),
"Isometric drafting" => Some(("common", "isometric-drafting")), "Isometric drafting" => Some(("common", "isometric-drafting")),
+6
View File
@@ -322,6 +322,12 @@ pub fn apply_color(entity: &mut EntityType, color: AcadColor) {
common.color_book_handle = None; common.color_book_handle = None;
} }
/// Like [`apply_color`], but keeps a colour-book identity (`BOOK$NAME`).
pub fn apply_named_color(entity: &mut EntityType, color: AcadColor, name: Option<String>) {
apply_color(entity, color);
entity.common_mut().color_name = name;
}
pub fn apply_line_weight(entity: &mut EntityType, lw: LineWeight) { pub fn apply_line_weight(entity: &mut EntityType, lw: LineWeight) {
entity.as_entity_mut().set_line_weight(lw); entity.as_entity_mut().set_line_weight(lw);
} }
+176
View File
@@ -0,0 +1,176 @@
//! Colour books: AutoCAD `.acb` files.
//!
//! An `.acb` is XML: a `bookName` and `colorPage`s of `colorEntry`s, each a
//! `colorName` and an `RGB8` value. Books from some vendors encrypt their
//! values; those cannot be read.
/// One named colour.
#[derive(Debug, Clone, PartialEq)]
pub struct BookColor {
pub name: String,
pub rgb: (u8, u8, u8),
}
#[derive(Debug, Clone, PartialEq)]
pub struct ColorBook {
pub name: String,
/// Every colour, page after page.
pub colors: Vec<BookColor>,
/// Where each page starts in `colors`.
page_starts: Vec<usize>,
}
/// Pages for a book without `colorPage`s, as AutoCAD shows them.
const PAGE_SIZE: usize = 7;
impl ColorBook {
pub fn page_count(&self) -> usize {
self.page_starts.len()
}
/// The colours of one page, and the index of its first colour.
pub fn page(&self, page: usize) -> (usize, &[BookColor]) {
let start = self.page_starts.get(page).copied().unwrap_or(0);
let end = self
.page_starts
.get(page + 1)
.copied()
.unwrap_or(self.colors.len());
(start, &self.colors[start..end])
}
/// The page holding colour `index`.
pub fn page_of(&self, index: usize) -> usize {
self.page_starts
.partition_point(|&start| start <= index)
.saturating_sub(1)
}
/// The colour with this name, as stored after the `$` of `BOOK$NAME`.
pub fn find(&self, name: &str) -> Option<usize> {
self.colors.iter().position(|c| c.name == name)
}
/// The `BOOK$NAME` identity objects and layers store.
pub fn identity(&self, index: usize) -> String {
format!("{}${}", self.name, self.colors[index].name)
}
}
/// Read an `.acb` file's text.
pub fn parse(text: &str) -> Result<ColorBook, String> {
let doc = roxmltree::Document::parse(text.trim_start_matches('\u{feff}'))
.map_err(|e| e.to_string())?;
let root = doc.root_element();
if !root.has_tag_name("colorBook") {
return Err(crate::t!("Not a color book").into_owned());
}
let mut book = ColorBook {
name: text_of(root, "bookName").unwrap_or_default().to_string(),
colors: Vec::new(),
page_starts: Vec::new(),
};
let mut encrypted = false;
let mut read_entries = |parent: roxmltree::Node<'_, '_>, book: &mut ColorBook| {
for entry in parent.children().filter(|n| n.has_tag_name("colorEntry")) {
let rgb = child(entry, "RGB8").and_then(|rgb| {
let channel = |tag| text_of(rgb, tag)?.parse::<u8>().ok();
Some((channel("red")?, channel("green")?, channel("blue")?))
});
match (text_of(entry, "colorName"), rgb) {
(Some(name), Some(rgb)) => book.colors.push(BookColor {
name: name.to_string(),
rgb,
}),
_ => encrypted = true,
}
}
};
let mut pages = root
.children()
.filter(|n| n.has_tag_name("colorPage"))
.peekable();
if pages.peek().is_some() {
for page in pages {
let start = book.colors.len();
read_entries(page, &mut book);
if book.colors.len() > start {
book.page_starts.push(start);
}
}
} else {
read_entries(root, &mut book);
book.page_starts = (0..book.colors.len()).step_by(PAGE_SIZE).collect();
}
if book.colors.is_empty() {
return Err(if encrypted {
crate::t!("This color book is encrypted and cannot be read")
} else {
crate::t!("The color book has no colors")
}
.into_owned());
}
Ok(book)
}
fn child<'a, 'i>(node: roxmltree::Node<'a, 'i>, tag: &str) -> Option<roxmltree::Node<'a, 'i>> {
node.children().find(|n| n.has_tag_name(tag))
}
fn text_of<'a>(node: roxmltree::Node<'a, '_>, tag: &str) -> Option<&'a str> {
child(node, tag).and_then(|n| n.text()).map(str::trim)
}
#[cfg(test)]
mod tests {
use super::*;
fn entry(name: &str, (r, g, b): (u8, u8, u8)) -> String {
format!(
"<colorEntry><colorName>{name}</colorName>\
<RGB8><red>{r}</red><green>{g}</green><blue>{b}</blue></RGB8></colorEntry>"
)
}
#[test]
fn reads_pages_and_colours() {
let text = format!(
"\u{feff}<?xml version=\"1.0\"?><colorBook><bookName>RAL CLASSIC</bookName>\
<colorPage><pageColor><RGB8><red>1</red><green>2</green><blue>3</blue></RGB8></pageColor>{}{}</colorPage>\
<colorPage>{}</colorPage></colorBook>",
entry("RAL 1000", (190, 189, 127)),
entry("RAL 1001", (194, 176, 120)),
entry("RAL 5015", (34, 113, 179)),
);
let book = parse(&text).unwrap();
assert_eq!(book.name, "RAL CLASSIC");
assert_eq!(book.page_count(), 2);
assert_eq!(book.page(1), (2, &book.colors[2..]));
assert_eq!(book.page_of(1), 0);
assert_eq!(book.page_of(2), 1);
let blue = book.find("RAL 5015").unwrap();
assert_eq!(book.colors[blue].rgb, (34, 113, 179));
assert_eq!(book.identity(blue), "RAL CLASSIC$RAL 5015");
}
#[test]
fn a_book_without_pages_gets_pages_of_seven() {
let entries: String = (0..9).map(|i| entry(&format!("C{i}"), (i, i, i))).collect();
let book = parse(&format!(
"<colorBook><bookName>B</bookName>{entries}</colorBook>"
))
.unwrap();
assert_eq!(book.page_count(), 2);
assert_eq!(book.page(1).1.len(), 2);
}
#[test]
fn rejects_unreadable_books() {
assert!(parse("not xml").is_err());
assert!(parse("<palette/>").is_err());
let encrypted = "<colorBook><bookName>P</bookName><colorPage><colorEntry>\
<colorName>X</colorName><RGB8Encrypt>AbC</RGB8Encrypt></colorEntry>\
</colorPage></colorBook>";
assert!(parse(encrypted).is_err());
}
}
+233
View File
@@ -0,0 +1,233 @@
//! The colours a drawing uses, for the Document tab.
//!
//! One pass over every object (block definitions included) and every layer
//! when the panel opens. Keys borrow from the drawing, so the pass allocates
//! only per distinct colour, never per object.
use super::math;
use codec::types::{Color as AcadColor, Handle};
use rustc_hash::FxHashMap;
/// One distinct colour: a concrete colour plus, for a colour-book colour,
/// its `BOOK$NAME` identity.
#[derive(Debug, Clone, PartialEq)]
pub struct DocColor {
pub color: AcadColor,
pub name: Option<String>,
/// Objects and layers using it. Only used for sorting, never shown.
uses: u32,
/// The newest object or layer using it.
newest: Handle,
}
impl DocColor {
/// The few characters drawn on the swatch: the index number, `RGB`, or
/// the first word of the colour book.
pub fn tag(&self) -> String {
match (&self.name, self.color) {
(Some(name), _) => name
.split(['$', ' '])
.next()
.unwrap_or_default()
.chars()
.take(5)
.collect(),
(None, AcadColor::Index(i)) => i.to_string(),
_ => "RGB".to_string(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DocSort {
#[default]
Recent,
Usage,
Hue,
Number,
}
impl DocSort {
pub const ALL: [DocSort; 4] = [
DocSort::Recent,
DocSort::Usage,
DocSort::Hue,
DocSort::Number,
];
}
impl std::fmt::Display for DocSort {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&match self {
DocSort::Recent => crate::t!("Recently used"),
DocSort::Usage => crate::t!("Most used"),
DocSort::Hue => crate::t!("Color"),
DocSort::Number => crate::t!("Color number"),
})
}
}
/// A colour as a hash key: index colours keep their number, true colours
/// sit above 255.
fn key(color: AcadColor) -> Option<u32> {
match color {
AcadColor::Index(i) => Some(u32::from(i)),
AcadColor::Rgb { r, g, b } => {
Some(1 << 24 | u32::from(r) << 16 | u32::from(g) << 8 | u32::from(b))
}
_ => None,
}
}
/// Every concrete colour in the drawing. ByLayer, ByBlock and None are
/// states rather than colours and are skipped.
pub fn collect(document: &codec::CadDocument) -> Vec<DocColor> {
// Layers keep book and colour name apart; join them like objects do.
let layer_names: Vec<Option<String>> = document
.layers
.iter()
.map(|layer| match (&layer.book_name, &layer.color_name) {
(Some(book), Some(name)) => Some(format!("{book}${name}")),
(None, Some(name)) => Some(name.clone()),
_ => None,
})
.collect();
let objects = document.entities().map(|entity| {
let common = entity.common();
(common.color, common.color_name.as_deref(), common.handle)
});
let layers = document
.layers
.iter()
.zip(&layer_names)
.map(|(layer, name)| (layer.color, name.as_deref(), layer.handle));
tally(objects.chain(layers))
}
fn tally<'a>(uses: impl Iterator<Item = (AcadColor, Option<&'a str>, Handle)>) -> Vec<DocColor> {
let mut index: FxHashMap<(u32, Option<&str>), usize> = FxHashMap::default();
let mut colors: Vec<DocColor> = Vec::new();
for (color, name, handle) in uses {
let Some(k) = key(color) else { continue };
let name = name.filter(|name| !name.is_empty());
let slot = *index.entry((k, name)).or_insert_with(|| {
colors.push(DocColor {
color,
name: name.map(str::to_string),
uses: 0,
newest: handle,
});
colors.len() - 1
});
let entry = &mut colors[slot];
entry.uses += 1;
entry.newest = entry.newest.max(handle);
}
colors
}
/// Sort for display. `recent` is this drawing's pick history, newest first;
/// colours not picked this session follow, newest object first.
pub fn sort(colors: &mut [DocColor], by: DocSort, recent: &[AcadColor]) {
let rank = |c: &DocColor| {
recent
.iter()
.position(|&r| r == c.color)
.unwrap_or(usize::MAX)
};
match by {
DocSort::Recent => {
colors.sort_by(|a, b| rank(a).cmp(&rank(b)).then(b.newest.cmp(&a.newest)))
}
DocSort::Usage => colors.sort_by(|a, b| b.uses.cmp(&a.uses).then(b.newest.cmp(&a.newest))),
DocSort::Hue => colors.sort_by_cached_key(|c| {
let (r, g, b) = math::color_rgb(c.color).unwrap_or_default();
let hsb = math::rgb_to_hsb(r, g, b);
// Grays after the colours; within a hue band, light to dark.
let gray = hsb.s < 0.12 || hsb.b < 0.08;
let band = if gray {
0
} else {
(hsb.h / 15.0).round() as u32
};
(gray, band, std::cmp::Reverse((hsb.b * 1000.0) as u32))
}),
DocSort::Number => colors.sort_by(|a, b| {
let kind = |c: &DocColor| match (&c.name, c.color) {
(None, AcadColor::Index(_)) => 0,
(None, _) => 1,
_ => 2,
};
kind(a)
.cmp(&kind(b))
.then_with(|| a.name.cmp(&b.name))
.then_with(|| key(a.color).cmp(&key(b.color)))
}),
}
}
#[cfg(test)]
mod tests {
use super::*;
const RED: AcadColor = AcadColor::Index(1);
const BLUE: AcadColor = AcadColor::Index(5);
const SKY: AcadColor = AcadColor::Rgb {
r: 57,
g: 191,
b: 252,
};
fn sample() -> Vec<DocColor> {
let h = Handle::new;
tally(
[
(RED, None, h(1)),
(RED, None, h(2)),
(RED, None, h(3)),
(AcadColor::ByLayer, None, h(4)),
(SKY, None, h(5)),
(SKY, Some("RAL CLASSIC$RAL 5015"), h(6)),
(BLUE, Some(""), h(7)),
]
.into_iter(),
)
}
#[test]
fn collects_each_concrete_colour_once() {
let colors = sample();
assert_eq!(colors.len(), 4, "ByLayer skipped, book colour kept apart");
assert_eq!(colors[0].uses, 3);
assert_eq!(colors[0].newest, Handle::new(3));
assert_eq!(colors[3].name, None, "an empty name is no name");
assert_eq!(colors[2].tag(), "RAL");
assert_eq!(colors[0].tag(), "1");
assert_eq!(colors[1].tag(), "RGB");
}
#[test]
fn sorts_four_ways() {
let order = |by, recent: &[AcadColor]| {
let mut colors = sample();
sort(&mut colors, by, recent);
colors
.iter()
.map(|c| (c.color, c.name.is_some()))
.collect::<Vec<_>>()
};
// Picked this session first, then newest object first.
assert_eq!(
order(DocSort::Recent, &[RED]),
[(RED, false), (BLUE, false), (SKY, true), (SKY, false)]
);
assert_eq!(order(DocSort::Usage, &[])[0], (RED, false));
assert_eq!(
order(DocSort::Number, &[]),
[(RED, false), (BLUE, false), (SKY, false), (SKY, true)]
);
// Red (hue 0) before sky blue (hue ~199) before blue (hue 240).
assert_eq!(order(DocSort::Hue, &[])[0], (RED, false));
assert_eq!(order(DocSort::Hue, &[])[3], (BLUE, false));
}
}
+2
View File
@@ -3,6 +3,8 @@
//! properties panel and every style editor so colour selection looks and //! properties panel and every style editor so colour selection looks and
//! behaves the same everywhere. //! behaves the same everywhere.
pub mod book;
pub mod document;
pub mod editor; pub mod editor;
pub mod eyedropper; pub mod eyedropper;
pub mod math; pub mod math;
+708 -50
View File
@@ -5,6 +5,8 @@
//! Opened for an interface colour (any colour in Options) it shows only the //! Opened for an interface colour (any colour in Options) it shows only the
//! colour editor: no tabs, no index colours. //! colour editor: no tabs, no index colours.
use super::book::ColorBook;
use super::document::{self, DocColor, DocSort};
use super::editor::{rgb_color, ColorEditor, EditorMsg}; use super::editor::{rgb_color, ColorEditor, EditorMsg};
use super::eyedropper::{Eyedropper, EyedropperMsg}; use super::eyedropper::{Eyedropper, EyedropperMsg};
use super::math; use super::math;
@@ -12,20 +14,29 @@ use crate::app::{ArrowKey, ColorPickTarget, Message};
use crate::t; use crate::t;
use crate::ui::properties::acad_color_display; use crate::ui::properties::acad_color_display;
use codec::types::Color as AcadColor; use codec::types::Color as AcadColor;
use iced::widget::{button, column, container, row, text, Space}; use iced::widget::{
button, column, container, mouse_area, pick_list, row, scrollable, svg, text, text_input, Space,
};
use iced::{Background, Border, Color, Element, Length, Theme}; use iced::{Background, Border, Color, Element, Length, Theme};
use std::sync::Arc;
const PANEL_WIDTH: f32 = 400.0; const PANEL_WIDTH: f32 = 400.0;
/// Scope for Tab / Shift+Tab, so focus cycles through the panel's own /// Scope for Tab / Shift+Tab, so focus cycles through the panel's own
/// fields instead of the dialogs and command line underneath. /// fields instead of the dialogs and command line underneath.
const PANEL_ID: &str = "select-color-panel"; const PANEL_ID: &str = "select-color-panel";
const BODY_HEIGHT: f32 = 352.0; const BODY_HEIGHT: f32 = 352.0;
/// Swatches per row in the Document tab.
const DOC_COLUMNS: usize = 8;
/// Search matches shown at once in the Color books tab.
const PAGE_BARS: usize = 7;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ColorPickerTab { pub enum ColorPickerTab {
Document,
#[default] #[default]
Index, Index,
TrueColor, TrueColor,
Books,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -40,17 +51,32 @@ pub enum ColorPickerMsg {
UseNearest, UseNearest,
Arrow(ArrowKey), Arrow(ArrowKey),
Eyedropper(EyedropperMsg), Eyedropper(EyedropperMsg),
/// A colour of the Document tab, by its place in the list.
PickDocument(usize),
DocSort(DocSort),
/// Show another loaded colour book.
Book(usize),
BookPage(usize),
/// Scrolling over the page: one page per notch.
BookScroll(f32),
BookQuery(String),
/// A colour of the shown book, by its index in the book.
PickBook(usize),
LoadBook,
Ok, Ok,
Cancel, Cancel,
} }
/// What the caller has to do after an update. /// What the caller has to do after an update.
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub enum Outcome { pub enum Outcome {
Commit(AcadColor), /// The colour, and its `BOOK$NAME` when it comes from a colour book.
Commit(AcadColor, Option<String>),
Cancel, Cancel,
/// Capture the window for the eyedropper. /// Capture the window for the eyedropper.
Screenshot, Screenshot,
/// Ask for an `.acb` file.
LoadBook,
} }
pub struct ColorPicker { pub struct ColorPicker {
@@ -60,9 +86,22 @@ pub struct ColorPicker {
current: AcadColor, current: AcadColor,
varies: bool, varies: bool,
pending: AcadColor, pending: AcadColor,
/// `BOOK$NAME` of the current and the new colour, for colour-book colours.
current_name: Option<String>,
pending_name: Option<String>,
tab: ColorPickerTab, tab: ColorPickerTab,
editor: ColorEditor, editor: ColorEditor,
eyedropper: Option<Eyedropper>, eyedropper: Option<Eyedropper>,
doc: Vec<DocColor>,
doc_sort: DocSort,
/// This drawing's colours picked this session, newest first.
recent: Vec<AcadColor>,
books: Arc<Vec<ColorBook>>,
book: usize,
book_page: usize,
book_query: String,
/// Why the last colour book did not load.
status: Option<String>,
} }
impl ColorPicker { impl ColorPicker {
@@ -77,12 +116,85 @@ impl ColorPicker {
current, current,
varies, varies,
pending: current, pending: current,
current_name: None,
pending_name: None,
tab, tab,
editor: ColorEditor::new(math::color_rgb(current).unwrap_or((255, 255, 255))), editor: ColorEditor::new(math::color_rgb(current).unwrap_or((255, 255, 255))),
eyedropper: None, eyedropper: None,
doc: Vec::new(),
doc_sort: DocSort::default(),
recent: Vec::new(),
books: Arc::default(),
book: 0,
book_page: 0,
book_query: String::new(),
status: None,
} }
} }
/// Fill the Document and Color books tabs. `current_name` is the
/// `BOOK$NAME` of the current colour; a colour from a loaded book opens
/// on its page.
pub fn with_drawing(
mut self,
mut doc: Vec<DocColor>,
recent: &[AcadColor],
books: Arc<Vec<ColorBook>>,
current_name: Option<String>,
) -> Self {
document::sort(&mut doc, self.doc_sort, recent);
self.doc = doc;
self.recent = recent.to_vec();
self.books = books;
if let Some(name) = current_name.filter(|name| !name.is_empty()) {
if self.show_identity(&name) {
self.tab = ColorPickerTab::Books;
}
self.current_name = Some(name.clone());
self.pending_name = Some(name);
}
self
}
/// The loaded books changed; show book `index`.
pub fn show_book(&mut self, books: Arc<Vec<ColorBook>>, index: usize) {
self.books = books;
self.book = index;
self.book_page = 0;
self.book_query.clear();
self.status = None;
self.tab = ColorPickerTab::Books;
}
pub fn set_status(&mut self, status: String) {
self.status = Some(status);
}
/// Turn to the page holding `BOOK$NAME`. False when no loaded book has it.
fn show_identity(&mut self, identity: &str) -> bool {
let Some((book_name, name)) = identity.split_once('$') else {
return false;
};
for (i, book) in self.books.iter().enumerate() {
if let Some(color) = book.find(name).filter(|_| book.name == book_name) {
self.book = i;
self.book_page = book.page_of(color);
return true;
}
}
false
}
/// The colour of the shown book that is the new colour, if any.
fn book_selection(&self) -> Option<usize> {
let (book_name, name) = self.pending_name.as_deref()?.split_once('$')?;
let book = self
.books
.get(self.book)
.filter(|book| book.name == book_name)?;
book.find(name)
}
pub fn pending(&self) -> AcadColor { pub fn pending(&self) -> AcadColor {
self.pending self.pending
} }
@@ -98,6 +210,7 @@ impl ColorPicker {
fn set_pending(&mut self, color: AcadColor) { fn set_pending(&mut self, color: AcadColor) {
self.pending = color; self.pending = color;
self.pending_name = None;
if let Some(rgb) = math::color_rgb(color) { if let Some(rgb) = math::color_rgb(color) {
self.editor.set_rgb(rgb); self.editor.set_rgb(rgb);
} }
@@ -131,24 +244,82 @@ impl ColorPicker {
match msg { match msg {
ColorPickerMsg::Tab(tab) => self.tab = tab, ColorPickerMsg::Tab(tab) => self.tab = tab,
ColorPickerMsg::Pick(color) => self.set_pending(color), ColorPickerMsg::Pick(color) => self.set_pending(color),
ColorPickerMsg::Revert => self.set_pending(self.current), ColorPickerMsg::Revert => {
self.set_pending(self.current);
self.pending_name = self.current_name.clone();
}
ColorPickerMsg::Editor(editor) => { ColorPickerMsg::Editor(editor) => {
if self.editor.update(editor) { if self.editor.update(editor) {
let (r, g, b) = self.editor.rgb(); let (r, g, b) = self.editor.rgb();
self.pending = AcadColor::Rgb { r, g, b }; self.pending = AcadColor::Rgb { r, g, b };
self.pending_name = None;
} }
} }
ColorPickerMsg::UseNearest => { ColorPickerMsg::UseNearest => {
let (r, g, b) = self.rgb(); let (r, g, b) = self.rgb();
self.set_pending(AcadColor::Index(math::nearest_aci(r, g, b))); self.set_pending(AcadColor::Index(math::nearest_aci(r, g, b)));
} }
ColorPickerMsg::Arrow(direction) => { ColorPickerMsg::Arrow(direction) => match (self.tab, self.pending) {
if let (ColorPickerTab::Index, AcadColor::Index(index)) = (self.tab, self.pending) { (ColorPickerTab::Index, AcadColor::Index(index)) => {
if let Some(next) = step_index(index, direction) { if let Some(next) = step_index(index, direction) {
self.set_pending(AcadColor::Index(next)); self.set_pending(AcadColor::Index(next));
} }
} }
(ColorPickerTab::Document, _) => {
let at = self
.doc
.iter()
.position(|c| c.color == self.pending && c.name == self.pending_name);
if let Some(next) = step_list(at, self.doc.len(), DOC_COLUMNS, direction) {
self.update(ColorPickerMsg::PickDocument(next));
}
}
(ColorPickerTab::Books, _) => {
let len = self
.books
.get(self.book)
.map_or(0, |book| book.colors.len());
if let Some(next) = step_list(self.book_selection(), len, 1, direction) {
self.pick_book(next);
}
}
_ => {}
},
ColorPickerMsg::PickDocument(i) => {
if let Some(entry) = self.doc.get(i) {
let (color, name) = (entry.color, entry.name.clone());
self.set_pending(color);
self.pending_name = name;
}
} }
ColorPickerMsg::DocSort(by) => {
self.doc_sort = by;
document::sort(&mut self.doc, by, &self.recent);
}
ColorPickerMsg::Book(i) => {
if i < self.books.len() {
self.book = i;
self.book_page = 0;
self.book_query.clear();
}
}
ColorPickerMsg::BookPage(page) => {
if let Some(book) = self.books.get(self.book) {
self.book_page = page.min(book.page_count().saturating_sub(1));
self.book_query.clear();
}
}
ColorPickerMsg::BookScroll(delta) => {
let page = if delta < 0.0 {
self.book_page + 1
} else {
self.book_page.saturating_sub(1)
};
self.update(ColorPickerMsg::BookPage(page));
}
ColorPickerMsg::BookQuery(query) => self.book_query = query,
ColorPickerMsg::PickBook(i) => self.pick_book(i),
ColorPickerMsg::LoadBook => return Some(Outcome::LoadBook),
ColorPickerMsg::Eyedropper(EyedropperMsg::Start) => { ColorPickerMsg::Eyedropper(EyedropperMsg::Start) => {
self.eyedropper = Some(Eyedropper::default()); self.eyedropper = Some(Eyedropper::default());
} }
@@ -156,13 +327,26 @@ impl ColorPicker {
ColorPickerMsg::Ok => { ColorPickerMsg::Ok => {
// Apply a value still being typed, as if Enter had been pressed. // Apply a value still being typed, as if Enter had been pressed.
self.update(ColorPickerMsg::Editor(EditorMsg::HexSubmit)); self.update(ColorPickerMsg::Editor(EditorMsg::HexSubmit));
return Some(Outcome::Commit(self.pending)); return Some(Outcome::Commit(self.pending, self.pending_name.clone()));
} }
ColorPickerMsg::Cancel => return Some(Outcome::Cancel), ColorPickerMsg::Cancel => return Some(Outcome::Cancel),
} }
None None
} }
fn pick_book(&mut self, index: usize) {
let books = Arc::clone(&self.books);
let Some(book) = books.get(self.book) else {
return;
};
if let Some(color) = book.colors.get(index) {
let (r, g, b) = color.rgb;
self.set_pending(AcadColor::Rgb { r, g, b });
self.pending_name = Some(book.identity(index));
self.book_page = book.page_of(index);
}
}
/// The full-window eyedropper layer, while it is active. /// The full-window eyedropper layer, while it is active.
pub fn eyedropper_view(&self) -> Option<Element<'_, Message>> { pub fn eyedropper_view(&self) -> Option<Element<'_, Message>> {
self.eyedropper.as_ref().map(Eyedropper::view) self.eyedropper.as_ref().map(Eyedropper::view)
@@ -170,19 +354,33 @@ impl ColorPicker {
pub fn view(&self) -> Element<'_, Message> { pub fn view(&self) -> Element<'_, Message> {
let editor_only = self.editor_only(); let editor_only = self.editor_only();
let body = if editor_only || self.tab == ColorPickerTab::TrueColor { let body = match self.tab {
self.true_color_tab() _ if editor_only => self.true_color_tab(),
} else { ColorPickerTab::Document => self.document_tab(),
self.index_tab() ColorPickerTab::Index => self.index_tab(),
ColorPickerTab::TrueColor => self.true_color_tab(),
ColorPickerTab::Books => self.books_tab(),
}; };
// The interface picker shrinks to the editor; the CAD panel keeps one // The interface picker shrinks to the editor; the CAD panel keeps one
// height across its tabs, with the info line at the bottom. // height across its tabs, with the info line at the bottom.
let mut body = column![body].spacing(8); let mut body = column![body].spacing(8);
if !editor_only { if !editor_only {
body = body.push(Space::new().height(Length::Fill)); // Document and Color books fill the height themselves.
if matches!(self.tab, ColorPickerTab::Index | ColorPickerTab::TrueColor) {
body = body.push(Space::new().height(Length::Fill));
}
let fill = let fill =
math::color_rgb(self.pending).map_or(Color::from_rgb8(128, 128, 128), rgb_color); math::color_rgb(self.pending).map_or(Color::from_rgb8(128, 128, 128), rgb_color);
let info = match (&self.status, &self.pending_name) {
(Some(status), _) => status.clone(),
(None, Some(name)) => {
let (r, g, b) = math::color_rgb(self.pending).unwrap_or_default();
format!("{name} · RGB {r}, {g}, {b}")
}
(None, None) => math::describe(self.pending),
};
let error = self.status.is_some();
body = body body = body
.push( .push(
container(Space::new()) container(Space::new())
@@ -196,9 +394,11 @@ impl ColorPicker {
.width(12) .width(12)
.height(12) .height(12)
.style(move |_theme: &Theme| swatch_style(fill)), .style(move |_theme: &Theme| swatch_style(fill)),
text(math::describe(self.pending)) text(info).size(11).font(iced::Font::MONOSPACE).style(
.size(11) move |theme: &Theme| text::Style {
.font(iced::Font::MONOSPACE), color: error.then(|| theme.palette().danger.base.color),
}
),
] ]
.spacing(8) .spacing(8)
.align_y(iced::Center), .align_y(iced::Center),
@@ -247,8 +447,10 @@ impl ColorPicker {
}; };
container( container(
row![ row![
tab(t!("Document").into_owned(), ColorPickerTab::Document),
tab(t!("Index Color").into_owned(), ColorPickerTab::Index), tab(t!("Index Color").into_owned(), ColorPickerTab::Index),
tab(t!("True Color").into_owned(), ColorPickerTab::TrueColor), tab(t!("True Color").into_owned(), ColorPickerTab::TrueColor),
tab(t!("Color Books").into_owned(), ColorPickerTab::Books),
] ]
.spacing(2), .spacing(2),
) )
@@ -261,11 +463,7 @@ impl ColorPicker {
/// under the middle of the tab bar. Clicking Current reverts. /// under the middle of the tab bar. Clicking Current reverts.
fn preview_row(&self) -> Element<'_, Message> { fn preview_row(&self) -> Element<'_, Message> {
let half = |label: String, value: String, fill: Color| { let half = |label: String, value: String, fill: Color| {
let ink = if ink_is_dark(fill) { let ink = ink(fill);
Color::from_rgb8(0x11, 0x12, 0x14)
} else {
Color::WHITE
};
column![ column![
text(label).size(11).color(ink.scale_alpha(0.85)), text(label).size(11).color(ink.scale_alpha(0.85)),
Space::new().height(Length::Fill), Space::new().height(Length::Fill),
@@ -277,10 +475,18 @@ impl ColorPicker {
}; };
let (current_bg, _) = acad_color_display(self.current); let (current_bg, _) = acad_color_display(self.current);
let (new_bg, _) = acad_color_display(self.pending); let (new_bg, _) = acad_color_display(self.pending);
// A colour-book colour shows its own name, without the book.
let name = |color: AcadColor, identity: &Option<String>| match identity {
Some(identity) => identity
.split_once('$')
.map_or(identity.as_str(), |(_, name)| name)
.to_string(),
None => math::short_name(color),
};
let current_value = if self.varies { let current_value = if self.varies {
"*VARIES*".to_string() "*VARIES*".to_string()
} else { } else {
math::short_name(self.current) name(self.current, &self.current_name)
}; };
let current = button(half(t!("Current").into_owned(), current_value, current_bg)) let current = button(half(t!("Current").into_owned(), current_value, current_bg))
.on_press(picker_msg(ColorPickerMsg::Revert)) .on_press(picker_msg(ColorPickerMsg::Revert))
@@ -301,7 +507,7 @@ impl ColorPicker {
.style(|_theme: &Theme| filled(Color::from_rgba(0.0, 0.0, 0.0, 0.35))); .style(|_theme: &Theme| filled(Color::from_rgba(0.0, 0.0, 0.0, 0.35)));
let new = container(half( let new = container(half(
t!("New").into_owned(), t!("New").into_owned(),
math::short_name(self.pending), name(self.pending, &self.pending_name),
new_bg, new_bg,
)) ))
.style(move |_theme: &Theme| container::Style { .style(move |_theme: &Theme| container::Style {
@@ -364,29 +570,272 @@ impl ColorPicker {
button(Space::new().width(width).height(height)) button(Space::new().width(width).height(height))
.on_press(picker_msg(ColorPickerMsg::Pick(AcadColor::Index(index)))) .on_press(picker_msg(ColorPickerMsg::Pick(AcadColor::Index(index))))
.padding(0) .padding(0)
.style(move |theme: &Theme, status| { .style(move |theme: &Theme, status| swatch_button_style(theme, status, fill, selected))
let hovered = matches!(status, button::Status::Hovered | button::Status::Pressed); .into()
button::Style { }
background: Some(fill.into()),
fn document_tab(&self) -> Element<'_, Message> {
let header = row![
text(t!("Colors in this drawing")).size(11),
text(format!("· {}", self.doc.len()))
.size(11)
.style(muted_text),
Space::new().width(Length::Fill),
text(t!("Sort")).size(11).style(muted_text),
pick_list(Some(self.doc_sort), &DocSort::ALL[..], DocSort::to_string)
.on_select(|by| picker_msg(ColorPickerMsg::DocSort(by)))
.text_size(12)
.padding([3, 6]),
]
.spacing(6)
.align_y(iced::Center);
if self.doc.is_empty() {
return column![
header,
empty_note(t!("No colors in this drawing yet").into_owned())
]
.spacing(10)
.into();
}
let grid = self.doc.chunks(DOC_COLUMNS).enumerate().fold(
column![].spacing(4),
|grid, (r, chunk)| {
let line = chunk
.iter()
.enumerate()
.fold(row![].spacing(4), |line, (c, entry)| {
line.push(self.doc_swatch(r * DOC_COLUMNS + c, entry))
});
let line = (chunk.len()..DOC_COLUMNS)
.fold(line, |line, _| line.push(Space::new().width(Length::Fill)));
grid.push(line)
},
);
column![
header,
scrollable(grid).height(Length::Fill).spacing(4).direction(
scrollable::Direction::Vertical(
scrollable::Scrollbar::new().width(4).scroller_width(4)
)
),
]
.spacing(10)
.into()
}
fn doc_swatch<'a>(&self, i: usize, entry: &'a DocColor) -> Element<'a, Message> {
let fill = math::color_rgb(entry.color).map_or(Color::from_rgb8(128, 128, 128), rgb_color);
let selected = self.pending == entry.color && self.pending_name == entry.name;
button(
container(
text(entry.tag())
.size(10)
.font(iced::Font::MONOSPACE)
.color(ink(fill)),
)
.align_y(iced::Bottom)
.padding([1, 4])
.height(Length::Fill),
)
.on_press(picker_msg(ColorPickerMsg::PickDocument(i)))
.padding(0)
.width(Length::Fill)
.height(30)
.style(move |theme: &Theme, status| swatch_button_style(theme, status, fill, selected))
.into()
}
fn books_tab(&self) -> Element<'_, Message> {
let load_button = |content: Element<'static, Message>| {
button(content)
.on_press(picker_msg(ColorPickerMsg::LoadBook))
.style(button::secondary)
};
let Some(book) = self.books.get(self.book) else {
return container(
column![
text(t!("No color books loaded")).size(12).style(muted_text),
load_button(text(t!("Load color book (.acb)")).size(12).into())
.padding([6, 14]),
]
.spacing(10)
.align_x(iced::Center),
)
.center(Length::Fill)
.into();
};
let label = |label: std::borrow::Cow<'static, str>| text(label).size(11).style(muted_text);
let books = Arc::clone(&self.books);
let top = row![
column![
label(t!("Color book")),
pick_list(
Some(self.book),
(0..self.books.len()).collect::<Vec<_>>(),
move |&i| books[i].name.clone(),
)
.on_select(|i| picker_msg(ColorPickerMsg::Book(i)))
.text_size(12)
.padding([4, 6])
.width(Length::Fill),
]
.spacing(4)
.width(Length::Fill),
column![
label(t!("Find")),
text_input("", &self.book_query)
.on_input(|query| picker_msg(ColorPickerMsg::BookQuery(query)))
.size(12)
.padding([5, 8])
.font(iced::Font::MONOSPACE)
.width(116),
]
.spacing(4),
load_button(
svg(crate::ui::icons::themed_handle(
crate::ui::icons::FOLDER_OPEN
))
.width(15)
.height(15)
.into()
)
.padding([6, 7]),
]
.spacing(8)
.align_y(iced::Bottom);
// The page's colours, or the first matches while searching.
let selected = self.book_selection();
let query = self.book_query.trim().to_lowercase();
let mut bars = column![].spacing(2).height(Length::Fill);
let mut shown = 0;
if query.is_empty() {
let (start, colors) = book.page(self.book_page);
for (i, color) in colors.iter().enumerate() {
bars = bars.push(book_bar(start + i, color, selected));
shown += 1;
}
} else {
for (i, color) in book
.colors
.iter()
.enumerate()
.filter(|(_, color)| color.name.to_lowercase().contains(&query))
.take(PAGE_BARS)
{
bars = bars.push(book_bar(i, color, selected));
shown += 1;
}
}
let bars: Element<'_, Message> = if shown == 0 {
empty_note(t!("No colors match").into_owned())
} else {
mouse_area(bars)
.on_scroll(|delta| {
let (iced::mouse::ScrollDelta::Lines { y, .. }
| iced::mouse::ScrollDelta::Pixels { y, .. }) = delta;
picker_msg(ColorPickerMsg::BookScroll(y))
})
.into()
};
// Every page as one band of its middle colour; the shown page framed.
let strip = (0..book.page_count()).fold(column![], |strip, page| {
let (_, colors) = book.page(page);
let fill = rgb_color(colors[colors.len() / 2].rgb);
let shown = query.is_empty() && page == self.book_page;
strip.push(
button(Space::new())
.on_press(picker_msg(ColorPickerMsg::BookPage(page)))
.padding(0)
.width(Length::Fill)
.height(Length::Fill)
.style(move |_theme: &Theme, _status| button::Style {
background: Some(fill.into()),
border: Border {
color: Color::WHITE,
width: if shown { 2.0 } else { 0.0 },
radius: 2.0.into(),
},
..Default::default()
}),
)
});
let arrow = |path: &'static str, page: usize| {
button(
svg(svg::Handle::from_memory(
format!(
"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 7 4'>\
<path d='{path}' fill='black'/></svg>"
)
.into_bytes(),
))
.width(9)
.height(5)
.style(|theme: &Theme, _status| svg::Style {
color: Some(theme.palette().background.base.text),
}),
)
.on_press(picker_msg(ColorPickerMsg::BookPage(page)))
.width(Length::Fill)
.height(18)
.padding([6, 8])
.style(button::secondary)
};
let strip = column![
arrow("M0 4L3.5 0L7 4z", self.book_page.saturating_sub(1)),
container(strip)
.height(Length::Fill)
.padding(1)
.style(|theme: &Theme| container::Style {
border: Border { border: Border {
color: if selected || hovered { color: theme.palette().background.strong.color,
theme.palette().primary.base.color width: 1.0,
} else { radius: 3.0.into(),
Color::from_rgba(0.0, 0.0, 0.0, 0.35)
},
width: if selected {
2.5
} else if hovered {
1.5
} else {
1.0
},
radius: 2.0.into(),
}, },
..Default::default() ..Default::default()
} }),
}) arrow("M0 0L7 0L3.5 4z", self.book_page + 1),
.into() ]
.spacing(3)
.width(26);
// RGB equivalent of the selected colour.
let chosen = selected.map(|i| &book.colors[i]);
let channel = |name: std::borrow::Cow<'static, str>, value: Option<u8>| {
row![
text(name).size(12).style(muted_text),
Space::new().width(Length::Fill),
text(value.map_or("—".to_string(), |v| v.to_string()))
.size(12)
.font(iced::Font::MONOSPACE),
]
};
let equivalent = column![
text(t!("RGB equivalent")).size(11),
channel(t!("Red"), chosen.map(|c| c.rgb.0)),
channel(t!("Green"), chosen.map(|c| c.rgb.1)),
channel(t!("Blue"), chosen.map(|c| c.rgb.2)),
container(Space::new())
.width(Length::Fill)
.height(1)
.style(|theme: &Theme| filled(theme.palette().background.weak.color)),
text(format!("{} / {}", self.book_page + 1, book.page_count()))
.size(11)
.style(muted_text),
text(chosen.map_or(String::new(), |c| c.name.clone())).size(12),
]
.spacing(6)
.width(104);
column![
top,
row![bars, strip, equivalent]
.spacing(10)
.height(Length::Fill)
]
.spacing(12)
.into()
} }
fn true_color_tab(&self) -> Element<'_, Message> { fn true_color_tab(&self) -> Element<'_, Message> {
@@ -494,6 +943,21 @@ fn step_index(index: u8, direction: ArrowKey) -> Option<u8> {
} }
} }
/// Arrow keys over a list laid out `columns` wide. Nothing selected yet
/// starts at the first entry.
fn step_list(at: Option<usize>, len: usize, columns: usize, direction: ArrowKey) -> Option<usize> {
let Some(at) = at else {
return (len > 0).then_some(0);
};
let next = match direction {
ArrowKey::Left => at.checked_sub(1),
ArrowKey::Right => Some(at + 1),
ArrowKey::Up => at.checked_sub(columns),
ArrowKey::Down => Some(at + columns),
};
next.filter(|&next| next < len)
}
/// Move keyboard focus to the next (or previous) field inside the panel. /// Move keyboard focus to the next (or previous) field inside the panel.
pub fn focus_step<T>(forward: bool) -> iced::Task<T> { pub fn focus_step<T>(forward: bool) -> iced::Task<T> {
use iced::advanced::widget::operation::{focusable, scope}; use iced::advanced::widget::operation::{focusable, scope};
@@ -514,6 +978,77 @@ fn ink_is_dark(color: Color) -> bool {
math::ink_is_dark(r, g, b) math::ink_is_dark(r, g, b)
} }
/// Text that reads on top of `fill`.
fn ink(fill: Color) -> Color {
if ink_is_dark(fill) {
Color::from_rgb8(0x11, 0x12, 0x14)
} else {
Color::WHITE
}
}
/// One colour of a colour book: a full-width bar with its name.
fn book_bar(
i: usize,
color: &super::book::BookColor,
selected: Option<usize>,
) -> Element<'_, Message> {
let fill = rgb_color(color.rgb);
button(
container(
text(&color.name)
.size(12)
.font(iced::Font::MONOSPACE)
.color(ink(fill)),
)
.center(Length::Fill),
)
.on_press(picker_msg(ColorPickerMsg::PickBook(i)))
.padding(0)
.width(Length::Fill)
.height(Length::Fill)
.style(move |theme: &Theme, status| {
swatch_button_style(theme, status, fill, selected == Some(i))
})
.into()
}
/// A short gray note in the middle of the free space.
fn empty_note<'a>(note: String) -> Element<'a, Message> {
container(text(note).size(12).style(muted_text))
.center(Length::Fill)
.into()
}
/// A clickable colour: ringed in the accent colour when selected or hovered.
fn swatch_button_style(
theme: &Theme,
status: button::Status,
fill: Color,
selected: bool,
) -> button::Style {
let hovered = matches!(status, button::Status::Hovered | button::Status::Pressed);
button::Style {
background: Some(fill.into()),
border: Border {
color: if selected || hovered {
theme.palette().primary.base.color
} else {
Color::from_rgba(0.0, 0.0, 0.0, 0.35)
},
width: if selected {
2.5
} else if hovered {
1.5
} else {
1.0
},
radius: 2.0.into(),
},
..Default::default()
}
}
fn filled(color: Color) -> container::Style { fn filled(color: Color) -> container::Style {
container::Style { container::Style {
background: Some(color.into()), background: Some(color.into()),
@@ -641,7 +1176,7 @@ mod tests {
p.update(ColorPickerMsg::Pick(AcadColor::Index(5))); p.update(ColorPickerMsg::Pick(AcadColor::Index(5)));
assert_eq!( assert_eq!(
p.update(ColorPickerMsg::Ok), p.update(ColorPickerMsg::Ok),
Some(Outcome::Commit(AcadColor::Index(5))) Some(Outcome::Commit(AcadColor::Index(5), None))
); );
assert_eq!(p.update(ColorPickerMsg::Cancel), Some(Outcome::Cancel)); assert_eq!(p.update(ColorPickerMsg::Cancel), Some(Outcome::Cancel));
} }
@@ -652,11 +1187,14 @@ mod tests {
edit(&mut p, EditorMsg::HexInput("#3bf".into())); edit(&mut p, EditorMsg::HexInput("#3bf".into()));
assert_eq!( assert_eq!(
p.update(ColorPickerMsg::Ok), p.update(ColorPickerMsg::Ok),
Some(Outcome::Commit(AcadColor::Rgb { Some(Outcome::Commit(
r: 0x33, AcadColor::Rgb {
g: 0xBB, r: 0x33,
b: 0xFF g: 0xBB,
})) b: 0xFF
},
None
))
); );
} }
@@ -724,6 +1262,89 @@ mod tests {
); );
} }
fn ral() -> Arc<Vec<ColorBook>> {
let entry = |name: &str, (r, g, b): (u8, u8, u8)| {
format!(
"<colorEntry><colorName>{name}</colorName><RGB8><red>{r}</red>\
<green>{g}</green><blue>{b}</blue></RGB8></colorEntry>"
)
};
let text = format!(
"<colorBook><bookName>RAL CLASSIC</bookName><colorPage>{}{}</colorPage>\
<colorPage>{}</colorPage></colorBook>",
entry("RAL 1000", (190, 189, 127)),
entry("RAL 3020", (204, 6, 5)),
entry("RAL 5015", (34, 113, 179)),
);
Arc::new(vec![super::super::book::parse(&text).unwrap()])
}
#[test]
fn book_colours_keep_their_name_until_edited() {
let mut p = picker(AcadColor::Index(30)).with_drawing(Vec::new(), &[], ral(), None);
p.update(ColorPickerMsg::PickBook(2));
assert_eq!(p.book_page, 1, "turns to the colour's page");
assert_eq!(
p.update(ColorPickerMsg::Ok),
Some(Outcome::Commit(
AcadColor::Rgb {
r: 34,
g: 113,
b: 179
},
Some("RAL CLASSIC$RAL 5015".into())
))
);
p.update(ColorPickerMsg::Arrow(ArrowKey::Up));
assert_eq!(p.pending_name.as_deref(), Some("RAL CLASSIC$RAL 3020"));
edit(&mut p, EditorMsg::Hue(120.0));
assert_eq!(
p.pending_name, None,
"an edited colour is no longer the book's"
);
}
#[test]
fn opens_a_book_colour_on_its_page() {
let p = picker(AcadColor::Rgb {
r: 34,
g: 113,
b: 179,
})
.with_drawing(Vec::new(), &[], ral(), Some("RAL CLASSIC$RAL 5015".into()));
assert_eq!(p.tab(), ColorPickerTab::Books);
assert_eq!(p.book_selection(), Some(2));
assert_eq!(p.book_page, 1);
}
#[test]
fn document_colours_pick_and_walk() {
// A new drawing's layer 0 is white.
let doc = document::collect(&codec::CadDocument::new());
assert!(doc.iter().any(|c| c.color == AcadColor::Index(7)));
let mut p = picker(AcadColor::Index(30)).with_drawing(doc, &[], Arc::default(), None);
p.update(ColorPickerMsg::Tab(ColorPickerTab::Document));
p.update(ColorPickerMsg::Arrow(ArrowKey::Right));
assert_eq!(
p.pending(),
p.doc[0].color,
"the first key press starts at the top"
);
assert_eq!(step_list(None, 3, 8, ArrowKey::Down), Some(0));
assert_eq!(step_list(Some(1), 20, 8, ArrowKey::Down), Some(9));
assert_eq!(step_list(Some(1), 5, 8, ArrowKey::Down), None);
assert_eq!(step_list(Some(0), 5, 8, ArrowKey::Left), None);
}
#[test]
fn loading_a_book_is_left_to_the_app() {
let mut p = picker(AcadColor::Index(30));
assert_eq!(p.update(ColorPickerMsg::LoadBook), Some(Outcome::LoadBook));
p.show_book(ral(), 0);
assert_eq!(p.tab(), ColorPickerTab::Books);
}
// ── Headless UI tests ─────────────────────────────────────────────── // ── Headless UI tests ───────────────────────────────────────────────
// //
// These run the real widgets through iced's simulator. Setting // These run the real widgets through iced's simulator. Setting
@@ -809,7 +1430,31 @@ mod tests {
ColorPickerTab, ColorPickerTab,
EditorMode, EditorMode,
bool, bool,
); 7] = [ ); 10] = [
(
"document",
ColorPickTarget::Properties,
AcadColor::Index(7),
ColorPickerTab::Document,
EditorMode::Hsb,
false,
),
(
"books",
ColorPickTarget::Properties,
AcadColor::Index(30),
ColorPickerTab::Books,
EditorMode::Hsb,
false,
),
(
"no-books",
ColorPickTarget::Properties,
AcadColor::Index(30),
ColorPickerTab::Books,
EditorMode::Hsb,
false,
),
( (
"index", "index",
ColorPickTarget::Properties, ColorPickTarget::Properties,
@@ -876,8 +1521,21 @@ mod tests {
crate::ui::style::fusion_theme::fusion_white(), crate::ui::style::fusion_theme::fusion_white(),
] { ] {
for (name, target, color, tab, mode, varies) in views.clone() { for (name, target, color, tab, mode, varies) in views.clone() {
let mut p = ColorPicker::new(target, color, varies); let books = if name == "no-books" {
Arc::default()
} else {
ral()
};
let mut p = ColorPicker::new(target, color, varies).with_drawing(
document::collect(&codec::CadDocument::new()),
&[],
books,
None,
);
p.update(ColorPickerMsg::Tab(tab)); p.update(ColorPickerMsg::Tab(tab));
if name == "books" {
p.update(ColorPickerMsg::PickBook(1));
}
edit(&mut p, EditorMsg::Mode(mode)); edit(&mut p, EditorMsg::Mode(mode));
let mut ui = simulator(&p); let mut ui = simulator(&p);
let snapshot = ui.snapshot(&theme).expect("snapshot"); let snapshot = ui.snapshot(&theme).expect("snapshot");