forked from 1b-agent/opencadstudio-upstream
Redesign Select Color panel: index and true color tabs
Replace the old index page and the iced_aw ColorPicker popup with one Select Color panel: - Index Color tab: standard colors, ACI 10-249 grid, grays; arrow keys move through the palette. - True Color tab: HSB, RGB and gradient-field modes built on two new widgets (GradientSlider, ColorArea) and iced_aw NumberInput boxes, an editable hex field and a nearest/exact index color button. - Current/New swatch: clicking Current reverts. A mixed selection opens on the color of the most recently drawn object with *VARIES* on top. - Color field accepts an index, a color name, r,g,b or hex. - Enter confirms, Esc cancels, other keys stay in the panel. - New labels translated in all 22 locales. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013rGmDUNr1hUf4zo1X3dak8
This commit is contained in:
36 files changed
+2173
-276
No files matched your search
Generated
+12
@@ -2535,6 +2535,8 @@ dependencies = [
|
||||
"iced_core",
|
||||
"iced_fonts_macros",
|
||||
"iced_widget",
|
||||
"num-format",
|
||||
"num-traits",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
@@ -3732,6 +3734,16 @@ dependencies = [
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-format"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a652d9771a63711fd3c3deb670acfbe5c30a4072e664d7a3bf5a9e1056ac72c3"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"itoa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.46"
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ url = "2"
|
||||
iced = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00b9327cb7b8546ed84db39", features = ["image", "svg", "advanced", "canvas", "smol", "markdown"] }
|
||||
iced_runtime = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00b9327cb7b8546ed84db39" }
|
||||
iced_core = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00b9327cb7b8546ed84db39" }
|
||||
iced_aw = { git = "https://github.com/HakanSeven12/iced_aw.git", branch = "agent/fix-iced-fonts", features = ["menu", "context_menu", "color_picker", "drop_down"] }
|
||||
iced_aw = { git = "https://github.com/HakanSeven12/iced_aw.git", branch = "agent/fix-iced-fonts", features = ["menu", "context_menu", "color_picker", "drop_down", "number_input"] }
|
||||
bytemuck = { version = "1.25", features = ["derive"] }
|
||||
glam = { version = "0.33", features = ["bytemuck"] }
|
||||
rfd = "0.17"
|
||||
|
||||
@@ -1339,6 +1339,9 @@ common =
|
||||
.index-color = { common.color } (ACI)
|
||||
.true-color = { common.color } (RGB)
|
||||
.standard-colors = { common.standard } · { common.color }
|
||||
.color-hex = سداسي عشري
|
||||
.nearest-index-color = أقرب فهرس
|
||||
.exact-index-match = تطابق تام مع الفهرس
|
||||
.cad-color-index-aci-10-249 = فهرس ألوان ACI 10–249
|
||||
.color-family-shade-intensity = { common.color-family } → · { common.shade } / { properties.intensity } ↓
|
||||
.grayscale-250-255 = { common.grayscale } 250–255
|
||||
|
||||
@@ -1326,6 +1326,9 @@ common =
|
||||
.index-color = { common.color } (ACI)
|
||||
.true-color = { common.color } (RGB)
|
||||
.standard-colors = { common.standard } · { common.color }
|
||||
.color-hex = Hex
|
||||
.nearest-index-color = Най-близък индекс
|
||||
.exact-index-match = Точно съвпадение с индекс
|
||||
.cad-color-index-aci-10-249 = ACI Цветови индекс 10–249
|
||||
.color-family-shade-intensity = { common.color-family } → · { common.shade } / { properties.intensity } ↓
|
||||
.grayscale-250-255 = { common.grayscale } 250–255
|
||||
|
||||
@@ -1326,6 +1326,9 @@ common =
|
||||
.index-color = { common.color } (ACI)
|
||||
.true-color = { common.color } (RGB)
|
||||
.standard-colors = { common.standard } · { common.color }
|
||||
.color-hex = Hex
|
||||
.nearest-index-color = Nejbližší index
|
||||
.exact-index-match = Přesná shoda s indexem
|
||||
.cad-color-index-aci-10-249 = Barevný index ACI 10–249
|
||||
.color-family-shade-intensity = { common.color-family } → · { common.shade } / { properties.intensity } ↓
|
||||
.grayscale-250-255 = { common.grayscale } 250–255
|
||||
|
||||
@@ -1327,6 +1327,9 @@ common =
|
||||
.index-color = { common.color } (ACI)
|
||||
.true-color = { common.color } (RGB)
|
||||
.standard-colors = { common.standard } · { common.color }
|
||||
.color-hex = Hex
|
||||
.nearest-index-color = Nächster Index
|
||||
.exact-index-match = Exakter Index-Treffer
|
||||
.cad-color-index-aci-10-249 = ACI Farbindex 10–249
|
||||
.color-family-shade-intensity = { common.color-family } → · { common.shade } / { properties.intensity } ↓
|
||||
.grayscale-250-255 = { common.grayscale } 250–255
|
||||
|
||||
@@ -1328,6 +1328,9 @@ common =
|
||||
.index-color = { common.color } (ACI)
|
||||
.true-color = { common.color } (RGB)
|
||||
.standard-colors = { common.standard } · { common.color }
|
||||
.color-hex = Hex
|
||||
.nearest-index-color = Πλησιέστερος δείκτης
|
||||
.exact-index-match = Ακριβής αντιστοίχιση δείκτη
|
||||
.cad-color-index-aci-10-249 = Δείκτης χρώματος ACI 10–249
|
||||
.color-family-shade-intensity = { common.color-family } → · { common.shade } / { properties.intensity } ↓
|
||||
.grayscale-250-255 = { common.grayscale } 250–255
|
||||
|
||||
@@ -1328,6 +1328,9 @@ common =
|
||||
.index-color = Index Color
|
||||
.true-color = True Color
|
||||
.standard-colors = Standard colors
|
||||
.color-hex = Hex
|
||||
.nearest-index-color = Nearest index
|
||||
.exact-index-match = Exact index match
|
||||
.cad-color-index-aci-10-249 = CAD Color Index (ACI) 10–249
|
||||
.color-family-shade-intensity = Color family → · Shade / intensity ↓
|
||||
.grayscale-250-255 = Grayscale 250–255
|
||||
|
||||
@@ -1327,6 +1327,9 @@ common =
|
||||
.index-color = { common.color } (ACI)
|
||||
.true-color = { common.color } (RGB)
|
||||
.standard-colors = { common.standard } · { common.color }
|
||||
.color-hex = Hex
|
||||
.nearest-index-color = Índice más cercano
|
||||
.exact-index-match = Coincide con un índice
|
||||
.cad-color-index-aci-10-249 = Índice de color ACI 10–249
|
||||
.color-family-shade-intensity = { common.color-family } → · { common.shade } / { properties.intensity } ↓
|
||||
.grayscale-250-255 = { common.grayscale } 250–255
|
||||
|
||||
@@ -1326,6 +1326,9 @@ common =
|
||||
.index-color = { common.color } (ACI)
|
||||
.true-color = { common.color } (RGB)
|
||||
.standard-colors = { common.standard } · { common.color }
|
||||
.color-hex = Hex
|
||||
.nearest-index-color = Lähin indeksi
|
||||
.exact-index-match = Täsmälleen indeksiväri
|
||||
.cad-color-index-aci-10-249 = ACI väriindeksi 10–249
|
||||
.color-family-shade-intensity = { common.color-family } → · { common.shade } / { properties.intensity } ↓
|
||||
.grayscale-250-255 = { common.grayscale } 250–255
|
||||
|
||||
@@ -1327,6 +1327,9 @@ common =
|
||||
.index-color = { common.color } (ACI)
|
||||
.true-color = { common.color } (RGB)
|
||||
.standard-colors = { common.standard } · { common.color }
|
||||
.color-hex = Hex
|
||||
.nearest-index-color = Index le plus proche
|
||||
.exact-index-match = Correspondance exacte d'index
|
||||
.cad-color-index-aci-10-249 = ACI Indice de couleur 10–249
|
||||
.color-family-shade-intensity = { common.color-family } → · { common.shade } / { properties.intensity } ↓
|
||||
.grayscale-250-255 = { common.grayscale } 250–255
|
||||
|
||||
@@ -1315,6 +1315,9 @@ common =
|
||||
.index-color = { common.color } (ACI)
|
||||
.true-color = { common.color } (RGB)
|
||||
.standard-colors = { common.standard } · { common.color }
|
||||
.color-hex = हेक्स
|
||||
.nearest-index-color = निकटतम इंडेक्स
|
||||
.exact-index-match = इंडेक्स से सटीक मेल
|
||||
.cad-color-index-aci-10-249 = ACI रंग सूचकांक 10–249
|
||||
.color-family-shade-intensity = { common.color-family } → · { common.shade } / { properties.intensity } ↓
|
||||
.grayscale-250-255 = { common.grayscale } 250–255
|
||||
|
||||
@@ -1326,6 +1326,9 @@ common =
|
||||
.index-color = { common.color } (ACI)
|
||||
.true-color = { common.color } (RGB)
|
||||
.standard-colors = { common.standard } · { common.color }
|
||||
.color-hex = Hex
|
||||
.nearest-index-color = Legközelebbi index
|
||||
.exact-index-match = Pontos indexegyezés
|
||||
.cad-color-index-aci-10-249 = ACI színindex 10–249
|
||||
.color-family-shade-intensity = { common.color-family } → · { common.shade } / { properties.intensity } ↓
|
||||
.grayscale-250-255 = { common.grayscale } 250–255
|
||||
|
||||
@@ -1326,6 +1326,9 @@ common =
|
||||
.index-color = { common.color } (ACI)
|
||||
.true-color = { common.color } (RGB)
|
||||
.standard-colors = { common.standard } · { common.color }
|
||||
.color-hex = Hex
|
||||
.nearest-index-color = Indice più vicino
|
||||
.exact-index-match = Corrispondenza esatta indice
|
||||
.cad-color-index-aci-10-249 = ACI Indice colori 10–249
|
||||
.color-family-shade-intensity = { common.color-family } → · { common.shade } / { properties.intensity } ↓
|
||||
.grayscale-250-255 = { common.grayscale } 250–255
|
||||
|
||||
@@ -1317,6 +1317,9 @@ common =
|
||||
.index-color = { common.color } (ACI)
|
||||
.true-color = { common.color } (RGB)
|
||||
.standard-colors = { common.standard } · { common.color }
|
||||
.color-hex = 16進
|
||||
.nearest-index-color = 最も近いインデックス
|
||||
.exact-index-match = インデックスと完全一致
|
||||
.cad-color-index-aci-10-249 = ACI カラーインデックス 10–249
|
||||
.color-family-shade-intensity = { common.color-family } → · { common.shade } / { properties.intensity } ↓
|
||||
.grayscale-250-255 = { common.grayscale } 250–255
|
||||
|
||||
@@ -1326,6 +1326,9 @@ common =
|
||||
.index-color = { common.color } (ACI)
|
||||
.true-color = { common.color } (RGB)
|
||||
.standard-colors = { common.standard } · { common.color }
|
||||
.color-hex = 16진수
|
||||
.nearest-index-color = 가장 가까운 인덱스
|
||||
.exact-index-match = 인덱스와 정확히 일치
|
||||
.cad-color-index-aci-10-249 = ACI 컬러 인덱스 10–249
|
||||
.color-family-shade-intensity = { common.color-family } → · { common.shade } / { properties.intensity } ↓
|
||||
.grayscale-250-255 = { common.grayscale } 250–255
|
||||
|
||||
@@ -1328,6 +1328,9 @@ common =
|
||||
.index-color = ສີດັດຊະນີ
|
||||
.true-color = True Color
|
||||
.standard-colors = ສີມາດຕະຖານ
|
||||
.color-hex = Hex
|
||||
.nearest-index-color = ດັດຊະນີທີ່ໃກ້ທີ່ສຸດ
|
||||
.exact-index-match = ກົງກັບດັດຊະນີພໍດີ
|
||||
.cad-color-index-aci-10-249 = CAD Color Index (ACI) 10–249
|
||||
.color-family-shade-intensity = ກຸ່ມສີ → · ລະດັບສີ / ຄວາມເຂັ້ມ ↓
|
||||
.grayscale-250-255 = ລະດັບສີເທົາ 250–255
|
||||
|
||||
@@ -1325,6 +1325,9 @@ common =
|
||||
.index-color = { common.color } (ACI)
|
||||
.true-color = { common.color } (RGB)
|
||||
.standard-colors = { common.standard } · { common.color }
|
||||
.color-hex = Hex
|
||||
.nearest-index-color = Dichtstbijzijnde index
|
||||
.exact-index-match = Exacte indexkleur
|
||||
.cad-color-index-aci-10-249 = ACI Kleurindex 10–249
|
||||
.color-family-shade-intensity = { common.color-family } → · { common.shade } / { properties.intensity } ↓
|
||||
.grayscale-250-255 = { common.grayscale } 250–255
|
||||
|
||||
@@ -1326,6 +1326,9 @@ common =
|
||||
.index-color = { common.color } (ACI)
|
||||
.true-color = { common.color } (RGB)
|
||||
.standard-colors = { common.standard } · { common.color }
|
||||
.color-hex = Hex
|
||||
.nearest-index-color = Najbliższy indeks
|
||||
.exact-index-match = Dokładne dopasowanie indeksu
|
||||
.cad-color-index-aci-10-249 = Indeks kolorów ACI 10–249
|
||||
.color-family-shade-intensity = { common.color-family } → · { common.shade } / { properties.intensity } ↓
|
||||
.grayscale-250-255 = { common.grayscale } 250–255
|
||||
|
||||
@@ -1327,6 +1327,9 @@ common =
|
||||
.index-color = { common.color } (ACI)
|
||||
.true-color = { common.color } (RGB)
|
||||
.standard-colors = { common.standard } · { common.color }
|
||||
.color-hex = Hex
|
||||
.nearest-index-color = Índice mais próximo
|
||||
.exact-index-match = Corresponde exatamente ao índice
|
||||
.cad-color-index-aci-10-249 = Índice de cor ACI 10–249
|
||||
.color-family-shade-intensity = { common.color-family } → · { common.shade } / { properties.intensity } ↓
|
||||
.grayscale-250-255 = { common.grayscale } 250–255
|
||||
|
||||
@@ -1330,6 +1330,9 @@ common =
|
||||
.index-color = { common.color } (ACI)
|
||||
.true-color = { common.color } (RGB)
|
||||
.standard-colors = { common.standard } · { common.color }
|
||||
.color-hex = Hex
|
||||
.nearest-index-color = Ближайший индекс
|
||||
.exact-index-match = Точное совпадение с индексом
|
||||
.cad-color-index-aci-10-249 = Индекс цвета ACI 10–249
|
||||
.color-family-shade-intensity = { common.color-family } → · { common.shade } / { properties.intensity } ↓
|
||||
.grayscale-250-255 = { common.grayscale } 250–255
|
||||
|
||||
@@ -1313,6 +1313,9 @@ common =
|
||||
.index-color = { common.color } (ACI)
|
||||
.true-color = { common.color } (RGB)
|
||||
.standard-colors = { common.standard } · { common.color }
|
||||
.color-hex = Hex
|
||||
.nearest-index-color = En yakın indeks
|
||||
.exact-index-match = Tam indeks eşleşmesi
|
||||
.cad-color-index-aci-10-249 = ACI renk dizini 10–249
|
||||
.color-family-shade-intensity = { common.color-family } → · { common.shade } / { properties.intensity } ↓
|
||||
.grayscale-250-255 = { common.grayscale } 250–255
|
||||
|
||||
@@ -1314,6 +1314,9 @@ common =
|
||||
.index-color = { common.color } (ACI)
|
||||
.true-color = { common.color } (RGB)
|
||||
.standard-colors = { common.standard } · { common.color }
|
||||
.color-hex = 十六进制
|
||||
.nearest-index-color = 最接近的索引色
|
||||
.exact-index-match = 与索引色完全匹配
|
||||
.cad-color-index-aci-10-249 = ACI 颜色索引 10–249
|
||||
.color-family-shade-intensity = { common.color-family } → · { common.shade } / { properties.intensity } ↓
|
||||
.grayscale-250-255 = { common.grayscale } 250–255
|
||||
|
||||
@@ -1326,6 +1326,9 @@ common =
|
||||
.index-color = { common.color } (ACI)
|
||||
.true-color = { common.color } (RGB)
|
||||
.standard-colors = { common.standard } · { common.color }
|
||||
.color-hex = 十六進位
|
||||
.nearest-index-color = 最接近的索引色
|
||||
.exact-index-match = 與索引色完全相符
|
||||
.cad-color-index-aci-10-249 = ACI 顏色索引 10–249
|
||||
.color-family-shade-intensity = { common.color-family } → · { common.shade } / { properties.intensity } ↓
|
||||
.grayscale-250-255 = { common.grayscale } 250–255
|
||||
|
||||
+5
-17
@@ -894,10 +894,8 @@ pub(super) struct OpenCADStudio {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pending_web_thumbnail_save: Option<PendingWebThumbnailSave>,
|
||||
// ── Floating panel windows ────────────────────────────────────────────
|
||||
/// Active `iced_aw` colour picker: destination plus its initial colour.
|
||||
color_pick_target: Option<(ColorPickTarget, AcadColor)>,
|
||||
/// Visible page in the shared CAD colour picker.
|
||||
color_picker_tab: ColorPickerTab,
|
||||
/// The open Select Color panel, if any.
|
||||
color_picker: Option<crate::ui::color_select::ColorPicker>,
|
||||
/// Recently committed real colours, newest first.
|
||||
/// ACI and True Color remain semantically distinct even when their RGB matches.
|
||||
recent_colors: Vec<AcadColor>,
|
||||
@@ -1590,13 +1588,6 @@ pub enum BgTarget {
|
||||
Desk,
|
||||
}
|
||||
|
||||
/// Active page in the shared CAD colour picker.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum ColorPickerTab {
|
||||
#[default]
|
||||
Index,
|
||||
TrueColor,
|
||||
}
|
||||
/// Where a colour chosen in the standalone palette window should be applied.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ColorPickTarget {
|
||||
@@ -3911,10 +3902,8 @@ pub enum Message {
|
||||
DsColorMore(DsField),
|
||||
/// Open the shared CAD colour picker for a field and its current colour.
|
||||
OpenColorWindow(ColorPickTarget, AcadColor),
|
||||
/// Switch between Index Color and True Color.
|
||||
ColorPickerTabChanged(ColorPickerTab),
|
||||
/// Change the pending colour without committing it yet.
|
||||
ColorPickerColorChanged(AcadColor),
|
||||
/// Interaction inside the Select Color panel.
|
||||
ColorPicker(crate::ui::color_select::ColorPickerMsg),
|
||||
/// Close the colour picker without choosing.
|
||||
CloseColorPicker,
|
||||
/// Commit the colour chosen in the shared picker.
|
||||
@@ -4318,8 +4307,7 @@ impl OpenCADStudio {
|
||||
pending_native_thumbnail_save: None,
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pending_web_thumbnail_save: None,
|
||||
color_pick_target: None,
|
||||
color_picker_tab: ColorPickerTab::Index,
|
||||
color_picker: None,
|
||||
recent_colors: Vec::new(),
|
||||
active_modal: None,
|
||||
hyperlink_editor_handles: Vec::new(),
|
||||
|
||||
+83
-14
@@ -377,6 +377,37 @@ impl OpenCADStudio {
|
||||
// closes it and every other keystroke-derived message is swallowed;
|
||||
// the modal's own text fields keep working because they emit their own
|
||||
// (non-blocked) messages. (#126)
|
||||
// The Select Color panel can open on top of another dialog, so it
|
||||
// takes the keyboard first: Esc cancels, Enter confirms, the arrows
|
||||
// walk the index palette, and every other keystroke is swallowed.
|
||||
if self.color_picker.is_some() {
|
||||
use crate::ui::color_select::ColorPickerMsg;
|
||||
let last_key = |key: &str| key.rsplit('+').next().map(str::to_owned);
|
||||
match &msg {
|
||||
Message::CommandEscape => {
|
||||
return self.update(Message::ColorPicker(ColorPickerMsg::Cancel));
|
||||
}
|
||||
Message::CommandFinalize => {
|
||||
return self.update(Message::ColorPicker(ColorPickerMsg::Ok));
|
||||
}
|
||||
Message::ShortcutPressed(key) => match last_key(key).as_deref() {
|
||||
Some("ESCAPE") => {
|
||||
return self.update(Message::ColorPicker(ColorPickerMsg::Cancel));
|
||||
}
|
||||
Some("ENTER" | "RETURN") => {
|
||||
return self.update(Message::ColorPicker(ColorPickerMsg::Ok));
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Message::ArrowKeyPressed { direction, .. } => {
|
||||
return self.update(Message::ColorPicker(ColorPickerMsg::Arrow(*direction)));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if is_modal_blocked_key_msg(&msg) {
|
||||
return Task::none();
|
||||
}
|
||||
}
|
||||
if self.active_modal.is_some() {
|
||||
if matches!(msg, Message::CommandEscape)
|
||||
|| matches!(&msg, Message::ShortcutPressed(key) if key.rsplit('+').next() == Some("ESCAPE"))
|
||||
@@ -3806,7 +3837,7 @@ impl OpenCADStudio {
|
||||
Message::PaneClicked(pane) => self.on_pane_clicked(pane),
|
||||
Message::PaneDragged(ev) => self.on_pane_dragged(ev),
|
||||
Message::PaneMove(idx, local) => {
|
||||
if self.color_pick_target.is_some() {
|
||||
if self.color_picker.is_some() {
|
||||
return Task::none();
|
||||
}
|
||||
let p = self.pane_canvas_point(idx, local);
|
||||
@@ -11005,10 +11036,10 @@ impl OpenCADStudio {
|
||||
Task::none()
|
||||
}
|
||||
Message::OpenColorWindow(target, color) => {
|
||||
self.color_pick_target = Some((target, color));
|
||||
|
||||
// Always open the shared CAD colour picker on the indexed ACI page.
|
||||
self.color_picker_tab = crate::app::ColorPickerTab::Index;
|
||||
let (current, varies) = self.color_picker_current(&target, color);
|
||||
self.color_picker = Some(crate::ui::color_select::ColorPicker::new(
|
||||
target, current, varies,
|
||||
));
|
||||
self.modal_offset = iced::Vector::ZERO;
|
||||
self.ds_color_open = None;
|
||||
self.mls_color_open = None;
|
||||
@@ -11023,18 +11054,23 @@ impl OpenCADStudio {
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::ColorPickerTabChanged(tab) => {
|
||||
self.color_picker_tab = tab;
|
||||
Task::none()
|
||||
}
|
||||
Message::ColorPickerColorChanged(color) => {
|
||||
if let Some((_, current)) = self.color_pick_target.as_mut() {
|
||||
*current = color;
|
||||
Message::ColorPicker(msg) => {
|
||||
let Some(picker) = self.color_picker.as_mut() else {
|
||||
return Task::none();
|
||||
};
|
||||
match picker.update(msg) {
|
||||
Some(crate::ui::color_select::Outcome::Commit(color)) => {
|
||||
self.update(Message::ColorWindowPick(color))
|
||||
}
|
||||
Some(crate::ui::color_select::Outcome::Cancel) => {
|
||||
self.color_picker = None;
|
||||
Task::none()
|
||||
}
|
||||
None => Task::none(),
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::CloseColorPicker => {
|
||||
self.color_pick_target = None;
|
||||
self.color_picker = None;
|
||||
Task::none()
|
||||
}
|
||||
|
||||
@@ -11046,6 +11082,39 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
|
||||
/// The colour the Select Color panel shows as "Current", and whether the
|
||||
/// selection it edits has mixed colours. A mixed selection starts from the
|
||||
/// colour of its most recently drawn object (the highest handle).
|
||||
fn color_picker_current(
|
||||
&self,
|
||||
target: &crate::app::ColorPickTarget,
|
||||
color: codec::types::Color,
|
||||
) -> (codec::types::Color, bool) {
|
||||
if !matches!(target, crate::app::ColorPickTarget::Properties) {
|
||||
return (color, false);
|
||||
}
|
||||
let i = self.active_tab;
|
||||
let document = &self.tabs[i].scene.document;
|
||||
let mut colors = self
|
||||
.property_target_handles(i)
|
||||
.into_iter()
|
||||
.filter_map(|handle| {
|
||||
document
|
||||
.get_entity(handle)
|
||||
.map(|entity| (handle, entity.common().color))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let Some(&(_, first)) = colors.first() else {
|
||||
return (color, false);
|
||||
};
|
||||
if colors.iter().all(|&(_, c)| c == first) {
|
||||
return (first, false);
|
||||
}
|
||||
colors.sort_by_key(|&(handle, _)| handle);
|
||||
let newest = colors.last().map_or(color, |&(_, c)| c);
|
||||
(newest, true)
|
||||
}
|
||||
|
||||
pub(crate) fn note_recent_color(&mut self, color: codec::types::Color) {
|
||||
// Keep only real colours in the recent list. ByLayer / ByBlock / None
|
||||
// are logical CAD states rather than reusable colours.
|
||||
|
||||
@@ -1137,10 +1137,10 @@ impl OpenCADStudio {
|
||||
|
||||
pub(super) fn on_color_window_pick(&mut self, color: codec::types::Color) -> Task<Message> {
|
||||
if matches!(
|
||||
self.color_pick_target.as_ref().map(|(target, _)| target),
|
||||
self.color_picker.as_ref().map(|picker| &picker.target),
|
||||
Some(crate::app::ColorPickTarget::PlotStyle)
|
||||
) {
|
||||
self.color_pick_target = None;
|
||||
self.color_picker = None;
|
||||
|
||||
let rgb = match color {
|
||||
codec::types::Color::Rgb { r, g, b } => Some((r, g, b)),
|
||||
@@ -1155,7 +1155,7 @@ impl OpenCADStudio {
|
||||
return self.on_plot_style_panel_apply();
|
||||
}
|
||||
let s = crate::ui::color_select::color_to_aci_string(color);
|
||||
let edit = match self.color_pick_target.take().map(|(target, _)| target) {
|
||||
let edit = match self.color_picker.take().map(|picker| picker.target) {
|
||||
Some(crate::app::ColorPickTarget::DimStyle(f)) => Some(Message::DsEdit(f, s)),
|
||||
Some(crate::app::ColorPickTarget::MLeader(f)) => {
|
||||
Some(Message::MLeaderStyleEdit { field: f, value: s })
|
||||
|
||||
@@ -1119,7 +1119,7 @@ impl OpenCADStudio {
|
||||
p: Point,
|
||||
expected_viewport: Option<codec::Handle>,
|
||||
) -> Task<Message> {
|
||||
if self.color_pick_target.is_some() {
|
||||
if self.color_picker.is_some() {
|
||||
return Task::none();
|
||||
}
|
||||
|
||||
@@ -1207,7 +1207,7 @@ impl OpenCADStudio {
|
||||
// leaks through the stack to the pane mouse_area beneath and
|
||||
// would track the crosshair over the dropdown's empty areas.
|
||||
// Drop the move here instead. (#227)
|
||||
if self.ribbon.open_dropdown.is_some() || self.color_pick_target.is_some() {
|
||||
if self.ribbon.open_dropdown.is_some() || self.color_picker.is_some() {
|
||||
return Task::none();
|
||||
}
|
||||
let i = self.active_tab;
|
||||
|
||||
+9
-36
@@ -2474,42 +2474,15 @@ bg={bg_ms:.1}ms n={view_count}"
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((_, current)) = self.color_pick_target.as_ref() {
|
||||
match self.color_picker_tab {
|
||||
super::ColorPickerTab::Index => {
|
||||
let content =
|
||||
crate::ui::color_select::index_color_page(*current, &self.recent_colors);
|
||||
|
||||
crate::ui::modal::modal(
|
||||
base,
|
||||
t!("Select Color"),
|
||||
content,
|
||||
Message::CloseColorPicker,
|
||||
self.modal_offset,
|
||||
crate::ui::modal::ModalOptions::NOTICE,
|
||||
)
|
||||
}
|
||||
|
||||
super::ColorPickerTab::TrueColor => {
|
||||
let initial = crate::ui::properties::acad_color_display(*current).0;
|
||||
let modal_base = crate::ui::modal::backdrop(base, Message::CloseColorPicker);
|
||||
|
||||
iced_aw::ColorPicker::new(
|
||||
true,
|
||||
initial,
|
||||
modal_base,
|
||||
// Cancel inside True Color returns to the indexed page instead
|
||||
// of closing the whole CAD colour picker.
|
||||
Message::ColorPickerTabChanged(super::ColorPickerTab::Index),
|
||||
|color| {
|
||||
Message::ColorWindowPick(crate::ui::color_select::iced_to_acad_color(
|
||||
color,
|
||||
))
|
||||
},
|
||||
)
|
||||
.into()
|
||||
}
|
||||
}
|
||||
if let Some(picker) = self.color_picker.as_ref() {
|
||||
crate::ui::modal::modal(
|
||||
base,
|
||||
t!("Select Color"),
|
||||
picker.view(),
|
||||
Message::CloseColorPicker,
|
||||
self.modal_offset,
|
||||
crate::ui::modal::ModalOptions::NOTICE,
|
||||
)
|
||||
} else {
|
||||
base
|
||||
}
|
||||
|
||||
@@ -4694,6 +4694,9 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
||||
"Index Color" => Some(("common", "index-color")),
|
||||
"True Color" => Some(("common", "true-color")),
|
||||
"Standard colors" => Some(("common", "standard-colors")),
|
||||
"Hex" => Some(("common", "color-hex")),
|
||||
"Nearest index" => Some(("common", "nearest-index-color")),
|
||||
"Exact index match" => Some(("common", "exact-index-match")),
|
||||
"CAD Color Index (ACI) 10–249" => Some(("common", "cad-color-index-aci-10-249")),
|
||||
"Color family → · Shade / intensity ↓" => Some(("common", "color-family-shade-intensity")),
|
||||
"Grayscale 250–255" => Some(("common", "grayscale-250-255")),
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
//! Colour maths and text parsing for the Select Color panel.
|
||||
//!
|
||||
//! Everything here is pure so it can be unit tested without a UI: HSB/RGB
|
||||
//! conversion, hex formatting, nearest-index lookup and the parser behind the
|
||||
//! panel's Color field.
|
||||
|
||||
use codec::types::Color as AcadColor;
|
||||
|
||||
/// Hue in degrees (0–360), saturation and brightness in 0–1.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Hsb {
|
||||
pub h: f32,
|
||||
pub s: f32,
|
||||
pub b: f32,
|
||||
}
|
||||
|
||||
impl Hsb {
|
||||
pub fn to_rgb(self) -> (u8, u8, u8) {
|
||||
hsb_to_rgb(self.h, self.s, self.b)
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert hue (degrees), saturation and brightness (0–1) to 8-bit RGB.
|
||||
pub fn hsb_to_rgb(h: f32, s: f32, b: f32) -> (u8, u8, u8) {
|
||||
let h = h.rem_euclid(360.0);
|
||||
let s = s.clamp(0.0, 1.0);
|
||||
let b = b.clamp(0.0, 1.0);
|
||||
let channel = |n: f32| {
|
||||
let k = (n + h / 60.0) % 6.0;
|
||||
let v = b - b * s * k.min(4.0 - k).clamp(0.0, 1.0);
|
||||
(v * 255.0).round() as u8
|
||||
};
|
||||
(channel(5.0), channel(3.0), channel(1.0))
|
||||
}
|
||||
|
||||
/// Convert 8-bit RGB to HSB. Grays have no hue; they report 0.
|
||||
pub fn rgb_to_hsb(r: u8, g: u8, b: u8) -> Hsb {
|
||||
let (r, g, b) = (
|
||||
f32::from(r) / 255.0,
|
||||
f32::from(g) / 255.0,
|
||||
f32::from(b) / 255.0,
|
||||
);
|
||||
let max = r.max(g).max(b);
|
||||
let min = r.min(g).min(b);
|
||||
let delta = max - min;
|
||||
let mut h = if delta == 0.0 {
|
||||
0.0
|
||||
} else if max == r {
|
||||
60.0 * (((g - b) / delta) % 6.0)
|
||||
} else if max == g {
|
||||
60.0 * ((b - r) / delta + 2.0)
|
||||
} else {
|
||||
60.0 * ((r - g) / delta + 4.0)
|
||||
};
|
||||
if h < 0.0 {
|
||||
h += 360.0;
|
||||
}
|
||||
let s = if max == 0.0 { 0.0 } else { delta / max };
|
||||
Hsb { h, s, b: max }
|
||||
}
|
||||
|
||||
/// RGB of an ACI entry, or `None` outside 1–255.
|
||||
pub fn aci_rgb(index: u8) -> Option<(u8, u8, u8)> {
|
||||
codec::types::aci_table::aci_to_rgb(index)
|
||||
}
|
||||
|
||||
/// The RGB a concrete colour displays as. Logical colours have none.
|
||||
pub fn color_rgb(color: AcadColor) -> Option<(u8, u8, u8)> {
|
||||
match color {
|
||||
AcadColor::Index(i) => aci_rgb(i),
|
||||
AcadColor::Rgb { r, g, b } => Some((r, g, b)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The closest index colour to an RGB value, and whether it is an exact match.
|
||||
pub fn nearest_aci_match(r: u8, g: u8, b: u8) -> (u8, bool) {
|
||||
let mut best = 7;
|
||||
let mut best_distance = u32::MAX;
|
||||
for index in 1..=255 {
|
||||
let Some((ar, ag, ab)) = aci_rgb(index) else {
|
||||
continue;
|
||||
};
|
||||
let dr = i32::from(r) - i32::from(ar);
|
||||
let dg = i32::from(g) - i32::from(ag);
|
||||
let db = i32::from(b) - i32::from(ab);
|
||||
let distance = (dr * dr + dg * dg + db * db) as u32;
|
||||
if distance < best_distance {
|
||||
best = index;
|
||||
best_distance = distance;
|
||||
}
|
||||
}
|
||||
(best, best_distance == 0)
|
||||
}
|
||||
|
||||
/// `#RRGGBB`, upper case.
|
||||
pub fn hex_string(r: u8, g: u8, b: u8) -> String {
|
||||
format!("#{r:02X}{g:02X}{b:02X}")
|
||||
}
|
||||
|
||||
/// Parse `#RRGGBB`, `RRGGBB`, `#RGB` or `RGB`.
|
||||
pub fn parse_hex(text: &str) -> Option<(u8, u8, u8)> {
|
||||
let digits = text.trim().trim_start_matches('#');
|
||||
if !digits.chars().all(|c| c.is_ascii_hexdigit()) {
|
||||
return None;
|
||||
}
|
||||
let expanded: String = match digits.len() {
|
||||
3 => digits.chars().flat_map(|c| [c, c]).collect(),
|
||||
6 => digits.to_string(),
|
||||
_ => return None,
|
||||
};
|
||||
let value = u32::from_str_radix(&expanded, 16).ok()?;
|
||||
Some(((value >> 16) as u8, (value >> 8) as u8, value as u8))
|
||||
}
|
||||
|
||||
/// Parse what the user typed in the Color field.
|
||||
///
|
||||
/// Accepts an index (`140`, `aci 140`), a standard colour name (`red`), an
|
||||
/// `r,g,b` triple, or hex (`#39BFFC`). Logical colours are not accepted: the
|
||||
/// panel has no ByLayer / ByBlock entry.
|
||||
pub fn parse_color_input(text: &str) -> Option<AcadColor> {
|
||||
let text = text.trim().to_ascii_lowercase();
|
||||
if text.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let named = match text.as_str() {
|
||||
"red" => Some(1),
|
||||
"yellow" => Some(2),
|
||||
"green" => Some(3),
|
||||
"cyan" => Some(4),
|
||||
"blue" => Some(5),
|
||||
"magenta" => Some(6),
|
||||
"white" | "black" => Some(7),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(index) = named {
|
||||
return Some(AcadColor::Index(index));
|
||||
}
|
||||
let number = text.strip_prefix("aci").map(str::trim).unwrap_or(&text);
|
||||
if !number.is_empty() && number.chars().all(|c| c.is_ascii_digit()) {
|
||||
return match number.parse::<u16>() {
|
||||
Ok(n @ 1..=255) => Some(AcadColor::Index(n as u8)),
|
||||
_ => None,
|
||||
};
|
||||
}
|
||||
if text.contains(',') {
|
||||
let parts: Vec<&str> = text.split(',').map(str::trim).collect();
|
||||
if let [r, g, b] = parts.as_slice() {
|
||||
let (r, g, b) = (r.parse().ok()?, g.parse().ok()?, b.parse().ok()?);
|
||||
return Some(AcadColor::Rgb { r, g, b });
|
||||
}
|
||||
return None;
|
||||
}
|
||||
if text.starts_with('#') || text.len() == 6 {
|
||||
let (r, g, b) = parse_hex(&text)?;
|
||||
return Some(AcadColor::Rgb { r, g, b });
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// The text the Color field shows for a colour.
|
||||
pub fn color_field_text(color: AcadColor) -> String {
|
||||
match color {
|
||||
AcadColor::Index(i) => i.to_string(),
|
||||
AcadColor::Rgb { r, g, b } => format!("{r}, {g}, {b}"),
|
||||
AcadColor::ByLayer => "ByLayer".to_string(),
|
||||
AcadColor::ByBlock => "ByBlock".to_string(),
|
||||
AcadColor::None => "None".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// One-line description for the panel's info line.
|
||||
pub fn describe(color: AcadColor) -> String {
|
||||
match color {
|
||||
AcadColor::Index(i) => {
|
||||
let (r, g, b) = aci_rgb(i).unwrap_or((128, 128, 128));
|
||||
format!("ACI {i} · RGB {r}, {g}, {b}")
|
||||
}
|
||||
AcadColor::Rgb { r, g, b } => format!("RGB {r}, {g}, {b} · {}", hex_string(r, g, b)),
|
||||
other => color_field_text(other),
|
||||
}
|
||||
}
|
||||
|
||||
/// Black or white text, whichever reads better on the given background.
|
||||
pub fn ink_is_dark(r: u8, g: u8, b: u8) -> bool {
|
||||
0.299 * f32::from(r) + 0.587 * f32::from(g) + 0.114 * f32::from(b) > 150.0
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn hsb_round_trips_primary_and_mixed_colours() {
|
||||
for (r, g, b) in [
|
||||
(255, 0, 0),
|
||||
(0, 255, 0),
|
||||
(0, 0, 255),
|
||||
(57, 191, 252),
|
||||
(120, 200, 80),
|
||||
(128, 128, 128),
|
||||
(0, 0, 0),
|
||||
(255, 255, 255),
|
||||
] {
|
||||
let hsb = rgb_to_hsb(r, g, b);
|
||||
assert_eq!(hsb.to_rgb(), (r, g, b), "round trip of {r},{g},{b}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hue_of_pure_colours() {
|
||||
assert_eq!(rgb_to_hsb(255, 0, 0).h, 0.0);
|
||||
assert!((rgb_to_hsb(0, 255, 0).h - 120.0).abs() < 0.01);
|
||||
assert!((rgb_to_hsb(0, 0, 255).h - 240.0).abs() < 0.01);
|
||||
assert_eq!(rgb_to_hsb(80, 80, 80).s, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hue_wraps_around() {
|
||||
assert_eq!(hsb_to_rgb(360.0, 1.0, 1.0), (255, 0, 0));
|
||||
assert_eq!(hsb_to_rgb(-120.0, 1.0, 1.0), hsb_to_rgb(240.0, 1.0, 1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nearest_aci_reports_exact_matches() {
|
||||
assert_eq!(nearest_aci_match(255, 0, 0), (1, true));
|
||||
let (index, exact) = nearest_aci_match(250, 5, 5);
|
||||
assert_eq!(index, 1);
|
||||
assert!(!exact);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hex_formats_and_parses() {
|
||||
assert_eq!(hex_string(57, 191, 252), "#39BFFC");
|
||||
assert_eq!(parse_hex("#39bffc"), Some((57, 191, 252)));
|
||||
assert_eq!(parse_hex("39BFFC"), Some((57, 191, 252)));
|
||||
assert_eq!(parse_hex("#3bf"), Some((0x33, 0xBB, 0xFF)));
|
||||
assert_eq!(parse_hex("#39BFF"), None);
|
||||
assert_eq!(parse_hex("zzzzzz"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn colour_field_accepts_index_name_rgb_and_hex() {
|
||||
assert_eq!(parse_color_input("140"), Some(AcadColor::Index(140)));
|
||||
assert_eq!(parse_color_input(" ACI 7 "), Some(AcadColor::Index(7)));
|
||||
assert_eq!(parse_color_input("Red"), Some(AcadColor::Index(1)));
|
||||
assert_eq!(
|
||||
parse_color_input("57, 191,252"),
|
||||
Some(AcadColor::Rgb {
|
||||
r: 57,
|
||||
g: 191,
|
||||
b: 252
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
parse_color_input("#39BFFC"),
|
||||
Some(AcadColor::Rgb {
|
||||
r: 57,
|
||||
g: 191,
|
||||
b: 252
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn colour_field_rejects_logical_and_out_of_range_values() {
|
||||
assert_eq!(parse_color_input("bylayer"), None);
|
||||
assert_eq!(parse_color_input("byblock"), None);
|
||||
assert_eq!(parse_color_input("0"), None);
|
||||
assert_eq!(parse_color_input("256"), None);
|
||||
assert_eq!(parse_color_input("300,0,0"), None);
|
||||
assert_eq!(parse_color_input("1,2"), None);
|
||||
assert_eq!(parse_color_input(""), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn field_text_matches_parser() {
|
||||
for color in [
|
||||
AcadColor::Index(140),
|
||||
AcadColor::Rgb {
|
||||
r: 57,
|
||||
g: 191,
|
||||
b: 252,
|
||||
},
|
||||
] {
|
||||
assert_eq!(parse_color_input(&color_field_text(color)), Some(color));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,12 @@
|
||||
//! properties panel and every style editor so colour selection looks and
|
||||
//! behaves the same everywhere.
|
||||
|
||||
pub mod math;
|
||||
pub mod picker;
|
||||
pub mod widgets;
|
||||
|
||||
pub use picker::{ColorPicker, ColorPickerMsg, ColorPickerTab, Outcome, TrueColorMode};
|
||||
|
||||
use crate::app::Message;
|
||||
use crate::t;
|
||||
use crate::ui::properties::acad_color_display;
|
||||
@@ -34,40 +40,9 @@ pub fn color_to_aci_string(c: AcadColor) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert an Iced colour chosen by `iced_aw::ColorPicker` into a DWG true
|
||||
/// colour. ACI-only destinations map it to their closest indexed colour later.
|
||||
pub fn iced_to_acad_color(color: Color) -> AcadColor {
|
||||
// Truncate the way the picker labels its channels, so the stored colour
|
||||
// is the one the dialog showed; rounding drifted dragged values by one
|
||||
// (#1621).
|
||||
let byte = |c: f32| (c * 255.0) as u8;
|
||||
AcadColor::Rgb {
|
||||
r: byte(color.r),
|
||||
g: byte(color.g),
|
||||
b: byte(color.b),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the closest CAD Color Index for an RGB colour.
|
||||
pub fn nearest_aci(r: u8, g: u8, b: u8) -> u8 {
|
||||
let mut best = 7;
|
||||
let mut best_distance = u32::MAX;
|
||||
|
||||
for index in 1..=255 {
|
||||
let Some((ar, ag, ab)) = codec::types::aci_table::aci_to_rgb(index) else {
|
||||
continue;
|
||||
};
|
||||
let dr = i32::from(r) - i32::from(ar);
|
||||
let dg = i32::from(g) - i32::from(ag);
|
||||
let db = i32::from(b) - i32::from(ab);
|
||||
let distance = (dr * dr + dg * dg + db * db) as u32;
|
||||
if distance < best_distance {
|
||||
best = index;
|
||||
best_distance = distance;
|
||||
}
|
||||
}
|
||||
|
||||
best
|
||||
math::nearest_aci_match(r, g, b).0
|
||||
}
|
||||
|
||||
/// Decode an ACI integer string back into an `AcadColor`.
|
||||
@@ -350,174 +325,3 @@ pub fn drop_down_below<'a>(
|
||||
}
|
||||
dd.into()
|
||||
}
|
||||
|
||||
/// Full CAD indexed-colour page used by the standalone Select Color dialog.
|
||||
///
|
||||
/// This only edits the pending colour. The caller commits it with
|
||||
/// `Message::ColorWindowPick`, so choosing a swatch does not immediately close
|
||||
/// the dialog.
|
||||
pub fn index_color_page<'a>(
|
||||
current: AcadColor,
|
||||
recent_colors: &'a [AcadColor],
|
||||
) -> Element<'a, Message> {
|
||||
let swatch_button = |color: AcadColor, size: f32| -> Element<'a, Message> {
|
||||
let (bg, _) = acad_color_display(color);
|
||||
let selected = current == color;
|
||||
|
||||
button(
|
||||
text("")
|
||||
.width(Length::Fixed(size))
|
||||
.height(Length::Fixed(size)),
|
||||
)
|
||||
.on_press(Message::ColorPickerColorChanged(color))
|
||||
.style(move |theme: &Theme, status| {
|
||||
let hovered = matches!(status, button::Status::Hovered | button::Status::Pressed);
|
||||
|
||||
button::Style {
|
||||
background: Some(Background::Color(bg)),
|
||||
border: Border {
|
||||
color: if selected || hovered {
|
||||
theme.palette().primary.base.color
|
||||
} else {
|
||||
theme.palette().background.neutral.color
|
||||
},
|
||||
width: if selected {
|
||||
2.5
|
||||
} else if hovered {
|
||||
1.5
|
||||
} else {
|
||||
1.0
|
||||
},
|
||||
radius: 1.0.into(),
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
})
|
||||
.padding(0)
|
||||
.into()
|
||||
};
|
||||
|
||||
// The first standard indexed CAD colours.
|
||||
let mut standard = row![].spacing(4);
|
||||
for idx in 1u8..=9 {
|
||||
standard = standard.push(swatch_button(AcadColor::Index(idx), 22.0));
|
||||
}
|
||||
|
||||
// ACI 10-249.
|
||||
//
|
||||
// Each group of ten indices belongs together. Arrange the 24 colour
|
||||
// families horizontally and their ten shade/intensity variants vertically.
|
||||
// This makes the palette read as a colour spectrum instead of a raw numeric
|
||||
// sequence.
|
||||
let mut palette_rows = column![].spacing(2);
|
||||
|
||||
for variant in 0u8..10 {
|
||||
let mut palette_row = row![].spacing(2);
|
||||
|
||||
for family in 1u8..=24 {
|
||||
let idx = family * 10 + variant;
|
||||
palette_row = palette_row.push(swatch_button(AcadColor::Index(idx), 16.0));
|
||||
}
|
||||
|
||||
palette_rows = palette_rows.push(palette_row);
|
||||
}
|
||||
|
||||
// ACI 250-255 are the grayscale entries and are shown separately.
|
||||
let mut grayscale = row![].spacing(4);
|
||||
|
||||
for idx in 250u8..=255 {
|
||||
grayscale = grayscale.push(swatch_button(AcadColor::Index(idx), 22.0));
|
||||
}
|
||||
|
||||
let recent: Element<'a, Message> = if recent_colors.is_empty() {
|
||||
text(crate::t!("No recent colors yet"))
|
||||
.size(10)
|
||||
.style(|theme: &Theme| iced::widget::text::Style {
|
||||
color: Some(theme.palette().background.base.text.scale_alpha(0.55)),
|
||||
})
|
||||
.into()
|
||||
} else {
|
||||
let mut recent_row = row![].spacing(4);
|
||||
|
||||
for &color in recent_colors.iter().take(12) {
|
||||
recent_row = recent_row.push(swatch_button(color, 22.0));
|
||||
}
|
||||
|
||||
recent_row.into()
|
||||
};
|
||||
|
||||
let selected_text = match current {
|
||||
AcadColor::Index(i) => {
|
||||
let (r, g, b) = codec::types::aci_table::aci_to_rgb(i).unwrap_or((128, 128, 128));
|
||||
format!("ACI {i} RGB {r}, {g}, {b}")
|
||||
}
|
||||
AcadColor::Rgb { r, g, b } => crate::tf!("True Color RGB {r}, {g}, {b}").into_owned(),
|
||||
AcadColor::ByLayer => t!("ByLayer").into_owned(),
|
||||
AcadColor::ByBlock => t!("ByBlock").into_owned(),
|
||||
AcadColor::None => t!("None").into_owned(),
|
||||
};
|
||||
|
||||
let tabs = row![
|
||||
button(text(crate::t!("Index Color")).size(12))
|
||||
.style(button::primary)
|
||||
.padding([5, 14]),
|
||||
button(text(crate::t!("True Color")).size(12))
|
||||
.on_press(Message::ColorPickerTabChanged(
|
||||
crate::app::ColorPickerTab::TrueColor
|
||||
))
|
||||
.style(button::secondary)
|
||||
.padding([5, 14]),
|
||||
]
|
||||
.spacing(4);
|
||||
|
||||
let actions = container(
|
||||
row![
|
||||
button(text(crate::t!("Cancel")).size(11))
|
||||
.on_press(Message::CloseColorPicker)
|
||||
.style(button::secondary)
|
||||
.padding([5, 14]),
|
||||
button(text(crate::t!("OK")).size(11))
|
||||
.on_press(Message::ColorWindowPick(current))
|
||||
.style(button::primary)
|
||||
.padding([5, 18]),
|
||||
]
|
||||
.spacing(8),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.align_x(iced::alignment::Horizontal::Right);
|
||||
|
||||
container(
|
||||
column![
|
||||
tabs,
|
||||
text(crate::t!("Standard colors")).size(11),
|
||||
standard,
|
||||
text(crate::t!("CAD Color Index (ACI) 10–249")).size(11),
|
||||
text(crate::t!("Color family → · Shade / intensity ↓"))
|
||||
.size(9)
|
||||
.style(|theme: &Theme| iced::widget::text::Style {
|
||||
color: Some(theme.palette().background.base.text.scale_alpha(0.65)),
|
||||
}),
|
||||
container(palette_rows)
|
||||
.padding(6)
|
||||
.style(|theme: &Theme| container::Style {
|
||||
background: Some(Background::Color(theme.palette().background.weakest.color)),
|
||||
border: Border {
|
||||
color: theme.palette().background.neutral.color,
|
||||
width: 1.0,
|
||||
radius: 3.0.into(),
|
||||
},
|
||||
..Default::default()
|
||||
}),
|
||||
text(crate::t!("Grayscale 250–255")).size(11),
|
||||
grayscale,
|
||||
text(crate::t!("Recently used")).size(11),
|
||||
recent,
|
||||
text(selected_text).size(11),
|
||||
actions,
|
||||
]
|
||||
.spacing(9),
|
||||
)
|
||||
.padding(10)
|
||||
.width(Length::Fixed(470.0))
|
||||
.into()
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,258 @@
|
||||
//! Saturation/brightness field for one hue.
|
||||
//!
|
||||
//! Drawn as three layered quads instead of pixel by pixel: the solid hue, a
|
||||
//! white-to-transparent wash from left to right, and a transparent-to-black
|
||||
//! wash from top to bottom. Saturation runs left to right, brightness bottom
|
||||
//! to top. Dragging anywhere moves the marker.
|
||||
|
||||
use iced::advanced::layout::{self, Layout};
|
||||
use iced::advanced::widget::{self, tree, Widget};
|
||||
use iced::advanced::{mouse, renderer, Shell};
|
||||
use iced::gradient::Linear;
|
||||
use iced::{
|
||||
Background, Border, Color, Element, Event, Gradient, Length, Point, Radians, Rectangle,
|
||||
Renderer, Size, Theme,
|
||||
};
|
||||
|
||||
const RADIUS: f32 = 5.0;
|
||||
const MARKER: f32 = 16.0;
|
||||
|
||||
pub struct ColorArea<'a, Message> {
|
||||
/// Fully saturated, full-brightness colour of the current hue.
|
||||
hue_color: Color,
|
||||
/// The colour under the marker.
|
||||
marker_color: Color,
|
||||
saturation: f32,
|
||||
brightness: f32,
|
||||
on_change: Box<dyn Fn(f32, f32) -> Message + 'a>,
|
||||
width: Length,
|
||||
height: Length,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct State {
|
||||
dragging: bool,
|
||||
}
|
||||
|
||||
impl<'a, Message> ColorArea<'a, Message> {
|
||||
/// `saturation` and `brightness` are 0–1; `on_change` receives the same.
|
||||
pub fn new(
|
||||
hue_color: Color,
|
||||
marker_color: Color,
|
||||
saturation: f32,
|
||||
brightness: f32,
|
||||
on_change: impl Fn(f32, f32) -> Message + 'a,
|
||||
) -> Self {
|
||||
Self {
|
||||
hue_color,
|
||||
marker_color,
|
||||
saturation: saturation.clamp(0.0, 1.0),
|
||||
brightness: brightness.clamp(0.0, 1.0),
|
||||
on_change: Box::new(on_change),
|
||||
width: Length::Fill,
|
||||
height: Length::Fixed(150.0),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
||||
self.height = height.into();
|
||||
self
|
||||
}
|
||||
|
||||
fn values_at(bounds: Rectangle, cursor: Point) -> (f32, f32) {
|
||||
let s = ((cursor.x - bounds.x) / bounds.width.max(1.0)).clamp(0.0, 1.0);
|
||||
let b = 1.0 - ((cursor.y - bounds.y) / bounds.height.max(1.0)).clamp(0.0, 1.0);
|
||||
(s, b)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Message> Widget<Message, Theme, Renderer> for ColorArea<'_, Message> {
|
||||
fn tag(&self) -> tree::Tag {
|
||||
tree::Tag::of::<State>()
|
||||
}
|
||||
|
||||
fn state(&self) -> tree::State {
|
||||
tree::State::new(State::default())
|
||||
}
|
||||
|
||||
fn size(&self) -> Size<Length> {
|
||||
Size::new(self.width, self.height)
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
_tree: &mut widget::Tree,
|
||||
_renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
layout::atomic(limits, self.width, self.height)
|
||||
}
|
||||
|
||||
fn update(
|
||||
&mut self,
|
||||
tree: &mut widget::Tree,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
_renderer: &Renderer,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
_viewport: &Rectangle,
|
||||
) {
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
let bounds = layout.bounds();
|
||||
let publish = |shell: &mut Shell<'_, Message>, position: Point| {
|
||||
let (s, b) = Self::values_at(bounds, position);
|
||||
shell.publish((self.on_change)(s, b));
|
||||
shell.capture_event();
|
||||
};
|
||||
match event {
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
|
||||
if let Some(position) = cursor.position_over(bounds) {
|
||||
state.dragging = true;
|
||||
publish(shell, position);
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse::Event::CursorMoved { .. }) if state.dragging => {
|
||||
if let Some(position) = cursor.position() {
|
||||
publish(shell, position);
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) if state.dragging => {
|
||||
state.dragging = false;
|
||||
shell.capture_event();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
tree: &widget::Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
_viewport: &Rectangle,
|
||||
_renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
if tree.state.downcast_ref::<State>().dragging || cursor.is_over(layout.bounds()) {
|
||||
mouse::Interaction::Crosshair
|
||||
} else {
|
||||
mouse::Interaction::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
_tree: &widget::Tree,
|
||||
renderer: &mut Renderer,
|
||||
_theme: &Theme,
|
||||
_style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
_cursor: mouse::Cursor,
|
||||
_viewport: &Rectangle,
|
||||
) {
|
||||
use iced::advanced::Renderer as _;
|
||||
|
||||
let bounds = layout.bounds();
|
||||
let rounded = renderer::Quad {
|
||||
bounds,
|
||||
border: Border {
|
||||
radius: RADIUS.into(),
|
||||
..Border::default()
|
||||
},
|
||||
..renderer::Quad::default()
|
||||
};
|
||||
let wash = |angle: f32, from: Color, to: Color| {
|
||||
Background::Gradient(Gradient::Linear(
|
||||
Linear::new(Radians(angle))
|
||||
.add_stop(0.0, from)
|
||||
.add_stop(1.0, to),
|
||||
))
|
||||
};
|
||||
|
||||
renderer.fill_quad(rounded, self.hue_color);
|
||||
renderer.fill_quad(
|
||||
rounded,
|
||||
wash(
|
||||
std::f32::consts::FRAC_PI_2,
|
||||
Color::WHITE,
|
||||
Color {
|
||||
a: 0.0,
|
||||
..Color::WHITE
|
||||
},
|
||||
),
|
||||
);
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
border: Border {
|
||||
color: Color::from_rgba(0.0, 0.0, 0.0, 0.5),
|
||||
width: 1.0,
|
||||
radius: RADIUS.into(),
|
||||
},
|
||||
..rounded
|
||||
},
|
||||
wash(
|
||||
std::f32::consts::PI,
|
||||
Color {
|
||||
a: 0.0,
|
||||
..Color::BLACK
|
||||
},
|
||||
Color::BLACK,
|
||||
),
|
||||
);
|
||||
|
||||
let centre = Point::new(
|
||||
bounds.x + bounds.width * self.saturation,
|
||||
bounds.y + bounds.height * (1.0 - self.brightness),
|
||||
);
|
||||
let marker = |size: f32| Rectangle {
|
||||
x: centre.x - size / 2.0,
|
||||
y: centre.y - size / 2.0,
|
||||
width: size,
|
||||
height: size,
|
||||
};
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: marker(MARKER + 2.0),
|
||||
border: Border {
|
||||
radius: ((MARKER + 2.0) / 2.0).into(),
|
||||
..Border::default()
|
||||
},
|
||||
..renderer::Quad::default()
|
||||
},
|
||||
Color::from_rgba(0.0, 0.0, 0.0, 0.55),
|
||||
);
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: marker(MARKER),
|
||||
border: Border {
|
||||
color: Color::WHITE,
|
||||
width: 2.0,
|
||||
radius: (MARKER / 2.0).into(),
|
||||
},
|
||||
..renderer::Quad::default()
|
||||
},
|
||||
self.marker_color,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message: 'a> From<ColorArea<'a, Message>> for Element<'a, Message> {
|
||||
fn from(area: ColorArea<'a, Message>) -> Self {
|
||||
Element::new(area)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn corners_map_to_saturation_and_brightness() {
|
||||
let bounds = Rectangle::new(Point::new(10.0, 20.0), Size::new(200.0, 100.0));
|
||||
let at = |x, y| ColorArea::<()>::values_at(bounds, Point::new(x, y));
|
||||
assert_eq!(at(10.0, 20.0), (0.0, 1.0));
|
||||
assert_eq!(at(210.0, 120.0), (1.0, 0.0));
|
||||
assert_eq!(at(110.0, 70.0), (0.5, 0.5));
|
||||
assert_eq!(at(-5.0, 500.0), (0.0, 0.0));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
//! A slider whose track is a colour gradient, with a white bar handle.
|
||||
//!
|
||||
//! Used for the HSB and RGB channels of the Select Color panel and, upright,
|
||||
//! for the hue strip beside the saturation/brightness field. The value is
|
||||
//! normalised to 0–1; callers map it to their own range.
|
||||
|
||||
use iced::advanced::layout::{self, Layout};
|
||||
use iced::advanced::widget::{self, tree, Widget};
|
||||
use iced::advanced::{mouse, renderer, Shell};
|
||||
use iced::gradient::Linear;
|
||||
use iced::{
|
||||
Background, Border, Color, Element, Event, Gradient, Length, Point, Radians, Rectangle,
|
||||
Renderer, Shadow, Size, Theme, Vector,
|
||||
};
|
||||
|
||||
const TRACK_THICKNESS: f32 = 18.0;
|
||||
const TRACK_RADIUS: f32 = 5.0;
|
||||
const HANDLE_THICKNESS: f32 = 8.0;
|
||||
const HANDLE_RADIUS: f32 = 3.0;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Orientation {
|
||||
Horizontal,
|
||||
Vertical,
|
||||
}
|
||||
|
||||
pub struct GradientSlider<'a, Message> {
|
||||
value: f32,
|
||||
stops: Vec<(f32, Color)>,
|
||||
on_change: Box<dyn Fn(f32) -> Message + 'a>,
|
||||
orientation: Orientation,
|
||||
/// How far one mouse-wheel notch moves the value (0–1 units).
|
||||
wheel_step: f32,
|
||||
width: Length,
|
||||
height: Length,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct State {
|
||||
dragging: bool,
|
||||
}
|
||||
|
||||
impl<'a, Message> GradientSlider<'a, Message> {
|
||||
/// `value` is 0–1. `stops` are (offset 0–1, colour) pairs, at most eight.
|
||||
pub fn new(
|
||||
value: f32,
|
||||
stops: Vec<(f32, Color)>,
|
||||
on_change: impl Fn(f32) -> Message + 'a,
|
||||
) -> Self {
|
||||
Self {
|
||||
value: value.clamp(0.0, 1.0),
|
||||
stops,
|
||||
on_change: Box::new(on_change),
|
||||
orientation: Orientation::Horizontal,
|
||||
wheel_step: 0.01,
|
||||
width: Length::Fill,
|
||||
height: Length::Fixed(26.0),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn vertical(mut self) -> Self {
|
||||
self.orientation = Orientation::Vertical;
|
||||
self.width = Length::Fixed(22.0);
|
||||
self.height = Length::Fill;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn wheel_step(mut self, step: f32) -> Self {
|
||||
self.wheel_step = step;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||
self.width = width.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
||||
self.height = height.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// The span the handle centre travels along, in the slider's main axis.
|
||||
fn travel(&self, bounds: Rectangle) -> (f32, f32) {
|
||||
let inset = HANDLE_THICKNESS / 2.0;
|
||||
match self.orientation {
|
||||
Orientation::Horizontal => (bounds.x + inset, bounds.x + bounds.width - inset),
|
||||
Orientation::Vertical => (bounds.y + inset, bounds.y + bounds.height - inset),
|
||||
}
|
||||
}
|
||||
|
||||
fn value_at(&self, bounds: Rectangle, cursor: Point) -> f32 {
|
||||
let (start, end) = self.travel(bounds);
|
||||
let along = match self.orientation {
|
||||
Orientation::Horizontal => cursor.x,
|
||||
Orientation::Vertical => cursor.y,
|
||||
};
|
||||
if end <= start {
|
||||
return 0.0;
|
||||
}
|
||||
((along - start) / (end - start)).clamp(0.0, 1.0)
|
||||
}
|
||||
|
||||
fn track_bounds(&self, bounds: Rectangle) -> Rectangle {
|
||||
match self.orientation {
|
||||
Orientation::Horizontal => {
|
||||
let thickness = TRACK_THICKNESS.min(bounds.height);
|
||||
Rectangle {
|
||||
y: bounds.y + (bounds.height - thickness) / 2.0,
|
||||
height: thickness,
|
||||
..bounds
|
||||
}
|
||||
}
|
||||
Orientation::Vertical => bounds,
|
||||
}
|
||||
}
|
||||
|
||||
fn gradient(&self) -> Background {
|
||||
// Gradient angles run clockwise from "bottom to top": a quarter turn
|
||||
// is left to right, a half turn top to bottom.
|
||||
let angle = match self.orientation {
|
||||
Orientation::Horizontal => std::f32::consts::FRAC_PI_2,
|
||||
Orientation::Vertical => std::f32::consts::PI,
|
||||
};
|
||||
let linear = self
|
||||
.stops
|
||||
.iter()
|
||||
.fold(Linear::new(Radians(angle)), |linear, &(offset, color)| {
|
||||
linear.add_stop(offset, color)
|
||||
});
|
||||
Background::Gradient(Gradient::Linear(linear))
|
||||
}
|
||||
}
|
||||
|
||||
impl<Message> Widget<Message, Theme, Renderer> for GradientSlider<'_, Message> {
|
||||
fn tag(&self) -> tree::Tag {
|
||||
tree::Tag::of::<State>()
|
||||
}
|
||||
|
||||
fn state(&self) -> tree::State {
|
||||
tree::State::new(State::default())
|
||||
}
|
||||
|
||||
fn size(&self) -> Size<Length> {
|
||||
Size::new(self.width, self.height)
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
_tree: &mut widget::Tree,
|
||||
_renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
layout::atomic(limits, self.width, self.height)
|
||||
}
|
||||
|
||||
fn update(
|
||||
&mut self,
|
||||
tree: &mut widget::Tree,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
_renderer: &Renderer,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
_viewport: &Rectangle,
|
||||
) {
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
let bounds = layout.bounds();
|
||||
match event {
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
|
||||
if let Some(position) = cursor.position_over(bounds) {
|
||||
state.dragging = true;
|
||||
shell.publish((self.on_change)(self.value_at(bounds, position)));
|
||||
shell.capture_event();
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse::Event::CursorMoved { .. }) if state.dragging => {
|
||||
if let Some(position) = cursor.position() {
|
||||
shell.publish((self.on_change)(self.value_at(bounds, position)));
|
||||
shell.capture_event();
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) if state.dragging => {
|
||||
state.dragging = false;
|
||||
shell.capture_event();
|
||||
}
|
||||
Event::Mouse(mouse::Event::WheelScrolled { delta }) if cursor.is_over(bounds) => {
|
||||
let lines = match delta {
|
||||
mouse::ScrollDelta::Lines { y, .. } => *y,
|
||||
mouse::ScrollDelta::Pixels { y, .. } => y.signum(),
|
||||
};
|
||||
if lines != 0.0 {
|
||||
// Scrolling up raises the value on a horizontal slider and
|
||||
// moves the handle up on an upright one.
|
||||
let direction = match self.orientation {
|
||||
Orientation::Horizontal => lines.signum(),
|
||||
Orientation::Vertical => -lines.signum(),
|
||||
};
|
||||
let next = (self.value + direction * self.wheel_step).clamp(0.0, 1.0);
|
||||
shell.publish((self.on_change)(next));
|
||||
shell.capture_event();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
tree: &widget::Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
_viewport: &Rectangle,
|
||||
_renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
if tree.state.downcast_ref::<State>().dragging {
|
||||
mouse::Interaction::Grabbing
|
||||
} else if cursor.is_over(layout.bounds()) {
|
||||
mouse::Interaction::Pointer
|
||||
} else {
|
||||
mouse::Interaction::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
_tree: &widget::Tree,
|
||||
renderer: &mut Renderer,
|
||||
_theme: &Theme,
|
||||
_style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
_cursor: mouse::Cursor,
|
||||
_viewport: &Rectangle,
|
||||
) {
|
||||
use iced::advanced::Renderer as _;
|
||||
|
||||
let bounds = layout.bounds();
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: self.track_bounds(bounds),
|
||||
border: Border {
|
||||
color: Color::from_rgba(0.0, 0.0, 0.0, 0.35),
|
||||
width: 1.0,
|
||||
radius: TRACK_RADIUS.into(),
|
||||
},
|
||||
..renderer::Quad::default()
|
||||
},
|
||||
self.gradient(),
|
||||
);
|
||||
|
||||
let (start, end) = self.travel(bounds);
|
||||
let centre = start + (end - start) * self.value;
|
||||
let handle = match self.orientation {
|
||||
Orientation::Horizontal => Rectangle {
|
||||
x: centre - HANDLE_THICKNESS / 2.0,
|
||||
y: bounds.y,
|
||||
width: HANDLE_THICKNESS,
|
||||
height: bounds.height,
|
||||
},
|
||||
Orientation::Vertical => Rectangle {
|
||||
x: bounds.x - 3.0,
|
||||
y: centre - HANDLE_THICKNESS / 2.0,
|
||||
width: bounds.width + 6.0,
|
||||
height: HANDLE_THICKNESS,
|
||||
},
|
||||
};
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: handle,
|
||||
border: Border {
|
||||
color: Color::from_rgba(0.0, 0.0, 0.0, 0.4),
|
||||
width: 1.0,
|
||||
radius: HANDLE_RADIUS.into(),
|
||||
},
|
||||
shadow: Shadow {
|
||||
color: Color::from_rgba(0.0, 0.0, 0.0, 0.55),
|
||||
offset: Vector::new(0.0, 1.0),
|
||||
blur_radius: 4.0,
|
||||
},
|
||||
..renderer::Quad::default()
|
||||
},
|
||||
Color::WHITE,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message: 'a> From<GradientSlider<'a, Message>> for Element<'a, Message> {
|
||||
fn from(slider: GradientSlider<'a, Message>) -> Self {
|
||||
Element::new(slider)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn slider() -> GradientSlider<'static, f32> {
|
||||
GradientSlider::new(0.5, vec![(0.0, Color::BLACK), (1.0, Color::WHITE)], |v| v)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cursor_maps_to_value_inside_handle_travel() {
|
||||
let bounds = Rectangle::new(Point::ORIGIN, Size::new(108.0, 26.0));
|
||||
let s = slider();
|
||||
assert_eq!(s.value_at(bounds, Point::new(4.0, 10.0)), 0.0);
|
||||
assert_eq!(s.value_at(bounds, Point::new(104.0, 10.0)), 1.0);
|
||||
assert!((s.value_at(bounds, Point::new(54.0, 10.0)) - 0.5).abs() < 1e-6);
|
||||
assert_eq!(s.value_at(bounds, Point::new(-50.0, 10.0)), 0.0);
|
||||
assert_eq!(s.value_at(bounds, Point::new(500.0, 10.0)), 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vertical_slider_reads_the_y_axis() {
|
||||
let bounds = Rectangle::new(Point::ORIGIN, Size::new(22.0, 108.0));
|
||||
let s = slider().vertical();
|
||||
assert_eq!(s.value_at(bounds, Point::new(11.0, 4.0)), 0.0);
|
||||
assert_eq!(s.value_at(bounds, Point::new(11.0, 104.0)), 1.0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
//! Widgets the Select Color panel needs that iced does not provide.
|
||||
|
||||
mod color_area;
|
||||
mod gradient_slider;
|
||||
|
||||
pub use color_area::ColorArea;
|
||||
pub use gradient_slider::GradientSlider;
|
||||
Reference in new issue
Block a user