From 750c34c2470f2805f2127966cfbebbcea68516ff Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Mon, 2 Nov 2026 04:35:56 -0800 Subject: [PATCH] Recolor Artwork: the Edit tab shows the colors on a smooth wheel, a segmented wheel or as color bars Buttons beside the Edit tab's wheel switch between the smooth color wheel, a segmented wheel of solid patches (24 hues by 5 saturations, its markers moving as freely as on the smooth wheel) and color bars: one solid bar per recolored color in row order, where a click selects a bar's row for the color sliders and a drag moves the row without changing a color. The dialog opens on the smooth wheel and keeps the choice while it is open; the dialog field is `display` (`smooth`, `segmented`, `bars`). --- crates/ui-egui/src/dialogs/recolor.rs | 116 ++++++++++++- crates/ui-egui/src/i18n/bg.tsv | 4 + crates/ui-egui/src/i18n/bs.tsv | 4 + crates/ui-egui/src/i18n/ca.tsv | 4 + crates/ui-egui/src/i18n/cs.tsv | 4 + crates/ui-egui/src/i18n/da.tsv | 4 + crates/ui-egui/src/i18n/de.tsv | 4 + crates/ui-egui/src/i18n/el.tsv | 4 + crates/ui-egui/src/i18n/es.tsv | 4 + crates/ui-egui/src/i18n/fi.tsv | 4 + crates/ui-egui/src/i18n/fr.tsv | 4 + crates/ui-egui/src/i18n/hu.tsv | 4 + crates/ui-egui/src/i18n/id.tsv | 4 + crates/ui-egui/src/i18n/it.tsv | 4 + crates/ui-egui/src/i18n/ja.tsv | 4 + crates/ui-egui/src/i18n/ka.tsv | 4 + crates/ui-egui/src/i18n/ko.tsv | 4 + crates/ui-egui/src/i18n/nl.tsv | 4 + crates/ui-egui/src/i18n/pl.tsv | 4 + crates/ui-egui/src/i18n/pt-br.tsv | 4 + crates/ui-egui/src/i18n/ru.tsv | 4 + crates/ui-egui/src/i18n/sv.tsv | 4 + crates/ui-egui/src/i18n/tr.tsv | 4 + crates/ui-egui/src/i18n/uk.tsv | 4 + crates/ui-egui/src/i18n/vi.tsv | 4 + crates/ui-egui/src/i18n/zh-hans.tsv | 4 + crates/ui-egui/src/i18n/zh-hant.tsv | 4 + crates/ui-egui/src/tests/recolor.rs | 233 ++++++++++++++++++++++++++ crates/ui-egui/src/widgets.rs | 3 +- crates/ui-egui/src/widgets/color.rs | 59 ++++++- docs/control-protocol.md | 5 +- docs/target-app-parity.md | 1 + 32 files changed, 513 insertions(+), 8 deletions(-) diff --git a/crates/ui-egui/src/dialogs/recolor.rs b/crates/ui-egui/src/dialogs/recolor.rs index 70e69a78c..5efa3ef6e 100644 --- a/crates/ui-egui/src/dialogs/recolor.rs +++ b/crates/ui-egui/src/dialogs/recolor.rs @@ -15,7 +15,11 @@ //! saturation wheel with a marker per new colour (drag one; with linked handles they all turn and //! scale together), a brightness slider, the harmony rules, and the color sliders with their mode //! menu, whose Global Adjust shows Saturation, Brightness, Temperature and Luminosity (-100% to -//! 100%, 0% changes nothing), which change every new color at once. The preset menu sets up a 1, 2 or 3 +//! 100%, 0% changes nothing), which change every new color at once. The buttons beside the wheel +//! show the new colors on the smooth wheel, on a segmented wheel (solid patches of 24 hues by 5 +//! saturations; its markers move as freely) or as color bars (one per new color, in row order: click +//! one to select it for the sliders, drag one to move its row, which changes no color); the dialog +//! opens on the smooth wheel. The preset menu sets up a 1, 2 or 3 //! colour job or a library. Opened on a colour group (Edit or Apply Color Group), OK also rewrites //! the group with the new colours (renamed by `groupName`); without art only the group changes. //! Opened on colours alone (the Color Guide's Edit or Apply Colors without art), OK saves the new @@ -28,8 +32,8 @@ //! `sliderMode` (`hsb`, `rgb`, `cmyk`, `lab` or `tint`; unset: the row color's own model, HSB for //! a gray and Tint for a spot color; `global`: Global Adjust, on the Edit tab), `globalAdjust` //! (`{saturation, brightness, temperature, luminosity}` in percent, sent as `recolor.apply`'s -//! `globalAdjust` in every slider mode). Changing `colors`, a preserve flag or `limitTo` reduces -//! the rows again. +//! `globalAdjust` in every slider mode), `display` (the Edit tab's `smooth`, `segmented` or `bars`; +//! unset: `smooth`). Changing `colors`, a preserve flag or `limitTo` reduces the rows again. use egui::{Rect, Sense, Ui, pos2, vec2}; use serde_json::{Value, json}; @@ -65,6 +69,9 @@ const CMD: &str = "recolor.apply"; const CURRENT_W: f32 = 250.0; /// Diameter of the Edit tab's colour wheel. const WHEEL: f32 = 220.0; +/// Height of the Edit tab's color bars area (the wheel with its Brightness slider), and of a bar. +const BARS_H: f32 = WHEEL + 31.0; +const BAR_H: f32 = 22.0; /// Height of the rows table, and of either tab (so switching tabs doesn't resize the dialog). const TABLE_H: f32 = 182.0; const TAB_H: f32 = 400.0; @@ -971,7 +978,29 @@ fn edit_tab(app: &VectorcraftApp, ui: &mut Ui, d: &mut Dialog, rows: &mut [Value }); ui.add_space(8.0); let colors: Vec = active.iter().filter_map(|&i| to_color(&rows[i])).collect(); - let w = widgets::harmony_wheel(ui, "recolor-wheel", WHEEL, &colors, base.and_then(|b| active.iter().position(|&i| i == b))); + let marked = base.and_then(|b| active.iter().position(|&i| i == b)); + let display = d.str("display"); + let mut w = widgets::WheelResponse::default(); + ui.horizontal(|ui| { + // The display buttons, beside the wheel so the tab keeps its height. + ui.vertical(|ui| { + for (id, icon, tip) in [ + ("smooth", "circle", tl!("Show smooth color wheel")), + ("segmented", "dc-polar-grid", tl!("Show segmented color wheel")), + ("bars", "dc-list-view", tl!("Show color bars")), + ] { + let on = display == id || (id == "smooth" && !matches!(display.as_str(), "segmented" | "bars")); + if widgets::icon_button(ui, icon, tip, on, 24.0).clicked() { + d.fields.insert("display".into(), json!(id)); + } + } + }); + ui.vertical(|ui| match display.as_str() { + "segmented" => w = widgets::segmented_wheel(ui, "recolor-wheel", WHEEL, &colors, marked), + "bars" => changed |= color_bars(ui, d, rows, &active, base), + _ => w = widgets::harmony_wheel(ui, "recolor-wheel", WHEEL, &colors, marked), + }); + }); if let Some(k) = w.pressed { d.fields.insert("__sel".into(), json!([active[k]])); } @@ -1023,6 +1052,85 @@ fn edit_tab(app: &VectorcraftApp, ui: &mut Ui, d: &mut Dialog, rows: &mut [Value changed } +/// The Edit tab's color bars: a solid bar per row of `active` with its new color, in row order, the +/// base row's framed, scrolling past the wheel's height. A click on a bar selects its row (the +/// sliders under it edit that row's new color); a drag moves the bar's row to where the line under +/// the pointer shows, keeping every new color. Returns true when the rows moved. +fn color_bars(ui: &mut Ui, d: &mut Dialog, rows: &mut [Value], active: &[usize], base: Option) -> bool { + let t = Tokens::get(ui.ctx()); + let mut rects: Vec = Vec::with_capacity(active.len()); + let (mut dragging, mut released, mut dropped) = (None, None, None); + // The area is given its full height first: in the row beside the display buttons a scroll area + // alone is only as tall as the room the row has. + let area = vec2(ui.available_width(), BARS_H); + ui.allocate_ui_with_layout(area, egui::Layout::top_down(egui::Align::Min), |ui| { + ui.set_min_size(area); + egui::ScrollArea::vertical().id_salt("recolor-bars").max_height(BARS_H).show(ui, |ui| { + ui.set_min_height(BARS_H); + ui.set_width(ui.available_width()); + if active.is_empty() { + ui.add_space(8.0); + widgets::dim_label(ui, tl!("No colors to recolor (preserved colors are left out).")); + } + for (n, &i) in active.iter().enumerate() { + let (r, resp) = ui.allocate_exact_size(vec2(ui.available_width() - 4.0, BAR_H), Sense::click_and_drag()); + chip(ui, r, to_color(&rows[i]).unwrap_or_default(), false); + if Some(i) == base { + ui.painter().rect_stroke(r, 2.0, egui::Stroke::new(2.0, t.accent), egui::StrokeKind::Inside); + } + let name = format!("{} {}", tl!("Color"), i + 1); + resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, ui.is_enabled(), &name)); + if resp.clicked() || resp.drag_started() { + d.fields.insert("__sel".into(), json!([i])); + } + if resp.dragged() { + dragging = Some(n); + } + if resp.drag_stopped() { + released = Some(n); + } + rects.push(r); + resp.on_hover_text(rows[i]["to"].as_str().unwrap_or_default()); + } + // The place a dragged bar goes: before the first bar whose middle is below the pointer. + let at = ui.input(|i| i.pointer.latest_pos()).map(|p| rects.iter().filter(|r| r.center().y < p.y).count()); + if let (Some(n), Some(to)) = (dragging, at) + && to != n + && to != n + 1 + { + let y = rects.get(to).map_or_else(|| rects.last().map_or(0.0, |r| r.bottom() + 1.5), |r| r.top() - 1.5); + let x = rects.first().map_or(ui.max_rect().x_range(), |r| r.x_range()); + ui.painter().hline(x, y, egui::Stroke::new(2.0, t.accent)); + } + dropped = released.zip(at); + }); + }); + let Some(moved) = dropped.and_then(|(n, to)| move_bar(rows, active, n, to)) else { return false }; + d.fields.insert("__sel".into(), json!([moved])); + // The slider values held are those of a row index that may now hold another row. + d.fields.remove(COMPS); + true +} + +/// Move the row of the `from`th active row (of `active`, ascending row indexes) to before the +/// `to`th, or after the last for `active.len()`; the rows between shift by one. Returns the row's +/// new index, or `None` when it stays where it is. +fn move_bar(rows: &mut [Value], active: &[usize], from: usize, to: usize) -> Option { + let &src = active.get(from)?; + if to == from || to == from + 1 || to > active.len() { + return None; + } + if to > from { + let dst = active[to - 1]; + rows[src..=dst].rotate_left(1); + Some(dst) + } else { + let dst = active[to]; + rows[dst..=src].rotate_right(1); + Some(dst) + } +} + /// Give the rows `active` the colours of `rule` from the base colour `b` of row `base` (the base row /// first, then the others in order, the rule's colours repeating), each in its row's model. A row /// whose new color changes loses its tint. diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 073f57715..643a878e7 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -1340,6 +1340,10 @@ font-technology Variable Променливи Hard Light Твърда светлина Hard Mix Твърдо смесване Harmony Rules: Правила за хармония: + Segmented color wheel Сегментирано цветово колело + Show smooth color wheel Показване на плавно цветово колело + Show segmented color wheel Показване на сегментирано цветово колело + Show color bars Показване на цветни ленти Harmony Sets Набори от хармонии Height Височина Height: Височина: diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index 80e02fbe4..3f58c09f9 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -2625,6 +2625,10 @@ digits Malayalam Malajalam Current Colors Trenutne boje Current Colors (0) Trenutne boje (0) Harmony Rules: Pravila harmonije: + Segmented color wheel Segmentirani kotač boja + Show smooth color wheel Prikaži glatki kotač boja + Show segmented color wheel Prikaži segmentirani kotač boja + Show color bars Prikaži trake boja Harmony Rules Pravila harmonije Keep this row's colors (exclude) Zadrži boje ovog reda (isključi) Limit to Library: Ograniči na biblioteku: diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index b6985fb13..7cfebef47 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -1804,6 +1804,10 @@ digits Malayalam Malaiàlam Hard Mix Mescla dura Harmony Rules Regles d’harmonia Harmony Rules: Regles d’harmonia: + Segmented color wheel Roda de color segmentada + Show smooth color wheel Mostra la roda de color contínua + Show segmented color wheel Mostra la roda de color segmentada + Show color bars Mostra les barres de color Harmony Sets Conjunts d’harmonia HDTV 1080p HDTV 1080p HDTV 720p HDTV 720p diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 367be32b2..7e228a046 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -2656,6 +2656,10 @@ digits Malayalam Malajálamské Current Colors Aktuální barvy Current Colors (0) Aktuální barvy (0) Harmony Rules: Pravidla harmonie: + Segmented color wheel Segmentované barevné kolo + Show smooth color wheel Zobrazit plynulé barevné kolo + Show segmented color wheel Zobrazit segmentované barevné kolo + Show color bars Zobrazit barevné pruhy Harmony Rules Pravidla harmonie Keep this row's colors (exclude) Ponechat barvy tohoto řádku (vyloučit) Limit to Library: Omezit na knihovnu: diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index 7b077a50a..ded431751 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -2443,6 +2443,10 @@ digits Malayalam Malayalam Current Colors Aktuelle farver Current Colors (0) Aktuelle farver (0) Harmony Rules: Harmoniregler: + Segmented color wheel Segmenteret farvehjul + Show smooth color wheel Vis glat farvehjul + Show segmented color wheel Vis segmenteret farvehjul + Show color bars Vis farvebjælker Harmony Rules Harmoniregler Keep this row's colors (exclude) Behold farverne i denne række (ekskludér) Limit to Library: Begræns til bibliotek: diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index c87b45d22..885b6b3d1 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -1371,6 +1371,10 @@ file-filter Templates Vorlagen Hard Light Hartes Licht Hard Mix Hart mischen Harmony Rules: Harmonieregeln: + Segmented color wheel Segmentierter Farbkreis + Show smooth color wheel Stufenlosen Farbkreis anzeigen + Show segmented color wheel Segmentierten Farbkreis anzeigen + Show color bars Farbbalken anzeigen Harmony Sets Harmoniesätze Height Höhe Height: Höhe: diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 615e0e6ee..8b199fdf1 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -1310,6 +1310,10 @@ font-technology Variable Μεταβλητές Hard Light Έντονο φως Hard Mix Έντονη ανάμειξη Harmony Rules: Κανόνες χρωματικής αρμονίας: + Segmented color wheel Τμηματοποιημένος χρωματικός τροχός + Show smooth color wheel Εμφάνιση ομαλού χρωματικού τροχού + Show segmented color wheel Εμφάνιση τμηματοποιημένου χρωματικού τροχού + Show color bars Εμφάνιση χρωματικών ράβδων Harmony Sets Σύνολα χρωματικής αρμονίας Height Ύψος Height: Ύψος: diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index c4d4a196b..464a253e7 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -1389,6 +1389,10 @@ font-technology Variable Variables Hard Light Luz fuerte Hard Mix Mezcla definida Harmony Rules: Reglas de armonía: + Segmented color wheel Rueda de color segmentada + Show smooth color wheel Mostrar rueda de color continua + Show segmented color wheel Mostrar rueda de color segmentada + Show color bars Mostrar barras de color Harmony Sets Conjuntos de armonía Height Altura Height: Altura: diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 7d571168f..ece930077 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -2600,6 +2600,10 @@ digits Malayalam Malajalam Current Colors Nykyiset värit Current Colors (0) Nykyiset värit (0) Harmony Rules: Harmoniasäännöt: + Segmented color wheel Segmentoitu väriympyrä + Show smooth color wheel Näytä liukuva väriympyrä + Show segmented color wheel Näytä segmentoitu väriympyrä + Show color bars Näytä väripalkit Harmony Rules Harmoniasäännöt Keep this row's colors (exclude) Säilytä tämän rivin värit (jätä pois) Limit to Library: Rajoita kirjastoon: diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 4372280db..742efb538 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -1382,6 +1382,10 @@ font-technology Variable Variables Handles Poignées Hard Light Lumière crue Hard Mix Mélange maximal + Segmented color wheel Roue chromatique segmentée + Show smooth color wheel Afficher la roue chromatique continue + Show segmented color wheel Afficher la roue chromatique segmentée + Show color bars Afficher les barres de couleur Harmony Rules: Règles d’harmonie : Harmony Sets Ensembles d’harmonies Height Hauteur diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 9890ecf5a..e45e37775 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -1771,6 +1771,10 @@ file-filter Templates Sablonok Hard Mix Kemény keverés Harmony Rules Harmóniaszabályok Harmony Rules: Harmóniaszabályok: + Segmented color wheel Szegmentált színkör + Show smooth color wheel Folytonos színkör megjelenítése + Show segmented color wheel Szegmentált színkör megjelenítése + Show color bars Színsávok megjelenítése Harmony Sets Harmóniakészletek HDTV 1080p HDTV 1080p HDTV 720p HDTV 720p diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index d8ceeacad..c7ec63e0b 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -2443,6 +2443,10 @@ digits Malayalam Malayalam Current Colors Warna saat ini Current Colors (0) Warna saat ini (0) Harmony Rules: Aturan harmoni: + Segmented color wheel Roda warna bersegmen + Show smooth color wheel Tampilkan roda warna halus + Show segmented color wheel Tampilkan roda warna bersegmen + Show color bars Tampilkan bilah warna Harmony Rules Aturan harmoni Keep this row's colors (exclude) Pertahankan warna baris ini (kecualikan) Limit to Library: Batasi ke pustaka: diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 1f6ffb4cf..152801c72 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -1401,6 +1401,10 @@ file-filter Templates Modelli Hard Light Luce intensa Hard Mix Miscela intensa Harmony Rules: Regole di armonia: + Segmented color wheel Ruota dei colori segmentata + Show smooth color wheel Mostra ruota dei colori continua + Show segmented color wheel Mostra ruota dei colori segmentata + Show color bars Mostra barre di colore Harmony Sets Gruppi di armonia Height Altezza Height: Altezza: diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 3d6849e46..980727883 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -1725,6 +1725,10 @@ digits Malayalam マラヤーラム数字 Hard Light ハードライト Hard Mix ハードミックス Harmony Rules: ハーモニールール: + Segmented color wheel 分割カラーホイール + Show smooth color wheel 滑らかなカラーホイールを表示 + Show segmented color wheel 分割カラーホイールを表示 + Show color bars カラーバーを表示 Height 高さ Height: 高さ: Help › Search Commands… ヘルプ › コマンドを検索… diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index 1a01580e2..ac304ea5a 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -2334,6 +2334,10 @@ digits Malayalam მალაიალამური Colors: ფერები: Current Colors ({total}) მიმდინარე ფერები ({total}) Harmony Rules: ჰარმონიის წესები: + Segmented color wheel სეგმენტირებული ფერთა წრე + Show smooth color wheel გლუვი ფერთა წრის ჩვენება + Show segmented color wheel სეგმენტირებული ფერთა წრის ჩვენება + Show color bars ფერთა ზოლების ჩვენება Keep this row's colors (exclude) ამ სტრიქონის ფერების შენარჩუნება (გამორიცხვა) Limit to Library: ბიბლიოთეკით შეზღუდვა: Link harmony colors ჰარმონიის ფერების დაკავშირება diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 5f2441972..2a2d4dc90 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -1715,6 +1715,10 @@ digits Malayalam 말라얄람어 Hard Light 하드 라이트 Hard Mix 하드 믹스 Harmony Rules: 하모니 규칙: + Segmented color wheel 분할 색상환 + Show smooth color wheel 부드러운 색상환 표시 + Show segmented color wheel 분할 색상환 표시 + Show color bars 색상 막대 표시 Height 높이 Height: 높이: Help › Search Commands… 도움말 › 명령 검색… diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index d7ccca211..65cb8331d 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -2457,6 +2457,10 @@ digits Malayalam Malayalam Current Colors Huidige kleuren Current Colors (0) Huidige kleuren (0) Harmony Rules: Harmonieregels: + Segmented color wheel Gesegmenteerd kleurenwiel + Show smooth color wheel Vloeiend kleurenwiel tonen + Show segmented color wheel Gesegmenteerd kleurenwiel tonen + Show color bars Kleurbalken tonen Harmony Rules Harmonieregels Keep this row's colors (exclude) De kleuren van deze rij behouden (uitsluiten) Limit to Library: Beperken tot bibliotheek: diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index be0a2a24b..43d40f88c 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -2566,6 +2566,10 @@ digits Malayalam Malajalam Colors: Kolory: Current Colors ({total}) Bieżące kolory ({total}) Harmony Rules: Reguły harmonii: + Segmented color wheel Segmentowe koło kolorów + Show smooth color wheel Pokaż płynne koło kolorów + Show segmented color wheel Pokaż segmentowe koło kolorów + Show color bars Pokaż paski kolorów Keep this row's colors (exclude) Zachowaj kolory tego wiersza (wyklucz) Limit to Library: Ogranicz do biblioteki: Link harmony colors Połącz kolory harmonii diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 2f07e5309..c25bee4cd 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -1215,6 +1215,10 @@ Hard Light Luz Forte Hard Mix Mistura definida Harmony Rules: Regras de Harmonia: + Segmented color wheel Roda de cores segmentada + Show smooth color wheel Mostrar roda de cores contínua + Show segmented color wheel Mostrar roda de cores segmentada + Show color bars Mostrar barras de cores Harmony Sets Conjuntos de Harmonia Height Altura Height: Altura: diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index cb6cbd10b..d639566a7 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -1398,6 +1398,10 @@ file-filter Templates Шаблоны Hard Light Жёсткий свет Hard Mix Жёсткое наложение Harmony Rules: Правила гармонии: + Segmented color wheel Сегментированный цветовой круг + Show smooth color wheel Показать плавный цветовой круг + Show segmented color wheel Показать сегментированный цветовой круг + Show color bars Показать цветовые полосы Harmony Sets Наборы гармонии Height Высота Height: Высота: diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index b8cc54933..e5a0720bc 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -1348,6 +1348,10 @@ font-technology Variable Variabla Hard Light Hårt ljus Hard Mix Hård blandning Harmony Rules: Harmoniregler: + Segmented color wheel Segmenterat färghjul + Show smooth color wheel Visa steglöst färghjul + Show segmented color wheel Visa segmenterat färghjul + Show color bars Visa färgstaplar Harmony Sets Harmoniuppsättningar Height Höjd Height: Höjd: diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index 9009af8ae..a10a72c99 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -1311,6 +1311,10 @@ font-technology Variable Değişken Handles Kollar Hard Light Sert Işık Harmony Rules: Uyum Kuralları: + Segmented color wheel Bölümlü renk tekerleği + Show smooth color wheel Kesintisiz renk tekerleğini göster + Show segmented color wheel Bölümlü renk tekerleğini göster + Show color bars Renk çubuklarını göster Harmony Sets Armoni Setleri Height Yükseklik Height: Yükseklik: diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 7535b5357..83484d9d4 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -1395,6 +1395,10 @@ file-filter Templates Шаблони Hard Light Жорстке світло Hard Mix Жорстке накладання Harmony Rules: Правила гармонії: + Segmented color wheel Сегментоване колірне коло + Show smooth color wheel Показати плавне колірне коло + Show segmented color wheel Показати сегментоване колірне коло + Show color bars Показати колірні смуги Harmony Sets Набори гармонії Height Висота Height: Висота: diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 07852ac9f..dd0026558 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -1610,6 +1610,10 @@ file-filter Templates Mẫu Hard Light Ánh sáng mạnh Hard Mix Trộn cứng Harmony Rules: Quy tắc hài hòa màu: + Segmented color wheel Bánh xe màu phân đoạn + Show smooth color wheel Hiện bánh xe màu liền mạch + Show segmented color wheel Hiện bánh xe màu phân đoạn + Show color bars Hiện các thanh màu Harmony Sets Bộ hài hòa màu Height Chiều cao Height: Chiều cao: diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index d609725c4..56cccd884 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -1338,6 +1338,10 @@ file-filter Templates 模板 Hard Light 强光 Hard Mix 实色混合 Harmony Rules: 协调规则: + Segmented color wheel 分段色轮 + Show smooth color wheel 显示平滑色轮 + Show segmented color wheel 显示分段色轮 + Show color bars 显示颜色条 Harmony Sets 色彩调和组 Height 高度 Height: 高度: diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 32dbb3ba2..19e4baad3 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -1366,6 +1366,10 @@ file-filter Templates 範本 Hard Light 實光 Hard Mix 實色混合 Harmony Rules: 色彩調和規則: + Segmented color wheel 分段色輪 + Show smooth color wheel 顯示平滑色輪 + Show segmented color wheel 顯示分段色輪 + Show color bars 顯示顏色條 Harmony Sets 色彩調和組 Height 高度 Height: 高度: diff --git a/crates/ui-egui/src/tests/recolor.rs b/crates/ui-egui/src/tests/recolor.rs index 0e7fe849b..cb44318cf 100644 --- a/crates/ui-egui/src/tests/recolor.rs +++ b/crates/ui-egui/src/tests/recolor.rs @@ -576,7 +576,10 @@ fn switching_tabs_or_slider_modes_keeps_the_dialog_height() { ("sliderMode", json!("hsb")), ("sliderMode", json!("lab")), ("tab", json!("edit")), + ("display", json!("segmented")), + ("display", json!("bars")), ("sliderMode", json!("global")), + ("display", json!("smooth")), ("sliderMode", json!("rgb")), ("tab", json!("assign")), ] { @@ -939,3 +942,233 @@ fn the_last_edit_color_can_be_selected_edited_and_undone() { assert_eq!(h.state().session.doc().unwrap().doc, before); } } + +/// The height of the open Recolor Artwork dialog. +fn dialog_height(h: &egui_kittest::Harness<'_, VectorcraftApp>) -> f32 { + h.ctx.memory(|m| m.area_rect(egui::Id::unique(("dialog", "recolor")))).unwrap().height() +} + +/// Clicks the Edit tab's display button named `name` and lets the dialog settle. +fn show_display(h: &mut egui_kittest::Harness<'_, VectorcraftApp>, name: &str) { + h.get_by_role_and_label(egui::accesskit::Role::Button, name).click(); + h.run_steps(6); +} + +/// The Edit tab opens on the smooth color wheel and its buttons switch to the segmented wheel and to +/// the color bars and back: the dialog keeps its size, and the rows and the selected row stay. The +/// choice stays while the dialog is open; the next opening starts on the smooth wheel again. +#[test] +fn the_edit_tab_shows_a_smooth_or_segmented_wheel_or_color_bars() { + let mut h = edit_harness(&["#ff0000", "#0000ff", "#00ff00"]); + set(h.state_mut(), "__sel", json!([1])); + h.run_steps(4); + let (rows, height) = (field(h.state(), "rows"), dialog_height(&h)); + let slider = |h: &egui_kittest::Harness<'_, VectorcraftApp>, name: &str| h.query_by_role_and_label(egui::accesskit::Role::Slider, name).is_some(); + let bar = |h: &egui_kittest::Harness<'_, VectorcraftApp>, name: &str| h.query_by_role_and_label(egui::accesskit::Role::Button, name).is_some(); + assert!(slider(&h, "Color wheel") && !slider(&h, "Segmented color wheel"), "the smooth wheel first"); + assert!(!bar(&h, "Color 1")); + show_display(&mut h, "Show segmented color wheel"); + assert_eq!(field(h.state(), "display"), json!("segmented")); + assert!(slider(&h, "Segmented color wheel") && !slider(&h, "Color wheel")); + assert_eq!(widget_of_size(&h, vec2(220.0, 220.0)).size(), vec2(220.0, 220.0), "the same size of wheel"); + assert!(slider(&h, "Brightness")); + show_display(&mut h, "Show color bars"); + assert_eq!(field(h.state(), "display"), json!("bars")); + assert!(!slider(&h, "Color wheel") && !slider(&h, "Segmented color wheel")); + assert!(["Color 1", "Color 2", "Color 3"].iter().all(|n| bar(&h, n)), "a bar per color"); + assert_eq!(dialog_height(&h), height, "the bars keep the dialog's height"); + show_display(&mut h, "Show smooth color wheel"); + assert!(slider(&h, "Color wheel")); + assert_eq!(dialog_height(&h), height); + assert_eq!(field(h.state(), "rows"), rows, "switching changes no color"); + assert_eq!(field(h.state(), "__sel"), json!([1]), "and keeps the selected row"); + show_display(&mut h, "Show color bars"); + set(h.state_mut(), "tab", json!("assign")); + h.run_steps(3); + set(h.state_mut(), "tab", json!("edit")); + h.run_steps(3); + assert!(bar(&h, "Color 1"), "the choice stays while the dialog is open"); + let app = h.state_mut(); + dialogs::cancel(app); + app.run("select.all", json!({})).unwrap(); + app.run("ui.recolorDialog", json!({})).unwrap(); + assert_eq!(field(app, "display"), Value::Null, "a new opening starts on the smooth wheel"); +} + +/// The segmented wheel's patches: 24 hues, centered on red, by 5 saturations, at the brightness asked. +#[test] +fn the_segmented_wheel_has_24_hues_by_5_saturations() { + let seg = |h, s, v| crate::widgets::segment_color(h, s, v).to_hsb(); + let near = |a: [f32; 3], b: [f32; 3]| a.iter().zip(b).all(|(x, y)| (x - y).abs() < 1e-3); + assert!(near(seg(7.0, 0.95, 1.0), [0.0, 0.9, 1.0]), "{:?}", seg(7.0, 0.95, 1.0)); + assert!(near(seg(8.0, 0.0, 0.5), [15.0, 0.1, 0.5]), "{:?}", seg(8.0, 0.0, 0.5)); + assert!(near(seg(355.0, 1.0, 1.0), [0.0, 0.9, 1.0]), "{:?}", seg(355.0, 1.0, 1.0)); + assert!(near(seg(120.0, 0.5, 1.0), [120.0, 0.5, 1.0]), "{:?}", seg(120.0, 0.5, 1.0)); + let mut patches = std::collections::BTreeSet::new(); + for h in 0..360 { + for s in 0..=100 { + patches.insert(ColorKey::of(&crate::widgets::segment_color(h as f32, s as f32 / 100.0, 1.0)).to_string()); + } + } + assert_eq!(patches.len(), 24 * 5); +} + +/// On the segmented wheel a marker moves as freely as on the smooth one, past patch edges and +/// without snapping to a patch; linked colors turn with it from where the drag started, an excluded +/// row keeps its color, and the smooth wheel shows the result. Its Brightness slider works too. +#[test] +fn a_segmented_wheel_marker_moves_freely() { + let mut h = edit_harness(&["rgb 200 100 100", "rgb 100 200 200", "rgb 20 160 20"]); + let mut rows = field(h.state(), "rows"); + let green = (0..3).find(|i| rows[*i]["from"] == json!(["rgb 20 160 20"])).unwrap(); + let red = (0..3).find(|i| rows[*i]["from"] == json!(["rgb 200 100 100"])).unwrap(); + rows[green]["exclude"] = json!(true); + set(h.state_mut(), "rows", rows); + set(h.state_mut(), "__sel", json!([red])); + h.run_steps(4); + show_display(&mut h, "Show segmented color wheel"); + let start = new_hsb(h.state()); + let wheel = widget_of_size(&h, vec2(220.0, 220.0)); + let (c, r) = (wheel.center(), 100.0); + let at = |hue: f32, s: f32| c + vec2(hue.to_radians().cos(), -hue.to_radians().sin()) * r * s; + let [hue, s, _] = start[red]; + h.hover_at(at(hue, s)); + h.run_steps(1); + h.drag_at(at(hue, s)); + h.run_steps(1); + // 37° on in small steps, across two patch edges, ending inside a patch away from its center. + for k in 1..=10 { + h.hover_at(at(hue + 3.7 * k as f32, s)); + h.run_steps(1); + } + h.drop_at(at(hue + 37.0, s)); + h.run_steps(2); + let moved = new_hsb(h.state()); + let gap = |a: f32, b: f32| (a - b).rem_euclid(360.0).min((b - a).rem_euclid(360.0)); + let want = (hue + 37.0).rem_euclid(360.0); + assert!(gap(moved[red][0], want) < 1.0, "the marker follows the pointer: {} -> {}", hue, moved[red][0]); + assert!(gap(moved[red][0], (want / 15.0).round() * 15.0) > 2.0, "not snapped to a patch center: {}", moved[red][0]); + let other = 3 - red - green; + assert!((gap(moved[other][0], start[other][0]) - 37.0).abs() < 1.5, "linked: {:?} -> {:?}", start[other], moved[other]); + assert_eq!(field(h.state(), "rows")[green]["to"], json!("rgb 20 160 20"), "the excluded row stays"); + let rows = field(h.state(), "rows"); + show_display(&mut h, "Show smooth color wheel"); + assert_eq!(field(h.state(), "rows"), rows, "the smooth wheel shows the same colors"); + show_display(&mut h, "Show segmented color wheel"); + click_slider(&mut h, "Brightness", 0.25); + assert!((new_hsb(h.state())[red][2] - 0.25).abs() < 0.02, "the Brightness slider: {:?}", new_hsb(h.state())); +} + +/// The rows as `(current colors, new color)` pairs, sorted. +fn row_pairs(app: &VectorcraftApp) -> Vec { + let mut v: Vec = field(app, "rows").as_array().unwrap().iter().map(|r| format!("{} {}", r["from"], r["to"])).collect(); + v.sort(); + v +} + +/// The color bars: one per recolored row (none for an excluded row), the last one selected with a +/// click and edited with the H slider, then dragged to the top, which moves its row and changes no +/// color. Preview shows the edit, Cancel restores the art and OK keeps it as one undo step. +#[test] +fn color_bars_select_edit_and_reorder_the_rows() { + for confirm in [false, true] { + let mut h = edit_harness(&["#ff0000", "#0000ff", "#00ff00", "#ffff00"]); + let before = h.state().session.doc().unwrap().doc.clone(); + let undo = h.state().session.doc().unwrap().history.undo.len(); + let mut rows = field(h.state(), "rows"); + rows[1]["exclude"] = json!(true); + set(h.state_mut(), "rows", rows); + set(h.state_mut(), "linked", json!(false)); + set(h.state_mut(), "sliderMode", json!("hsb")); + show_display(&mut h, "Show color bars"); + let bar = |h: &egui_kittest::Harness<'_, VectorcraftApp>, name: &str| { + h.query_by_role_and_label(egui::accesskit::Role::Button, name).map(|n| n.rect()) + }; + assert!(bar(&h, "Color 2").is_none(), "no bar for the excluded row"); + let first = bar(&h, "Color 1").expect("a bar per recolored row"); + let last = bar(&h, "Color 4").expect("a bar per recolored row"); + assert!(bar(&h, "Color 3").is_some_and(|r| r.top() > first.bottom() && r.bottom() < last.top()), "in row order"); + h.input_mut().events.extend(press(last.center(), 1)); + h.run_steps(3); + assert_eq!(field(h.state(), "__sel"), json!([3]), "a click selects the bar's row"); + let unedited = field(h.state(), "rows"); + click_slider(&mut h, "H", 0.5); + let edited = field(h.state(), "rows"); + assert_ne!(edited[3]["to"], unedited[3]["to"], "the H slider edits the selected bar's color"); + assert_eq!(&edited.as_array().unwrap()[..3], &unedited.as_array().unwrap()[..3], "and only that one"); + let previewed = h.state().session.doc().unwrap().doc.clone(); + assert_ne!(previewed, before, "the art previews the edit"); + let pairs = row_pairs(h.state()); + let last = bar(&h, "Color 4").unwrap(); + h.hover_at(last.center()); + h.run_steps(1); + h.drag_at(last.center()); + h.run_steps(1); + let top = egui::pos2(last.center().x, first.top() + 2.0); + for k in 1..=8 { + h.hover_at(last.center().lerp(top, k as f32 / 8.0)); + h.run_steps(1); + } + h.drop_at(top); + h.run_steps(3); + let rows = field(h.state(), "rows"); + assert_eq!(rows[0], edited[3], "the dragged row moved to the top: {rows}"); + assert_eq!(rows.as_array().unwrap()[1..], [edited[0].clone(), edited[1].clone(), edited[2].clone()], "the others shift down"); + assert_eq!(row_pairs(h.state()), pairs, "moving a bar changes no color"); + assert_eq!(field(h.state(), "__sel"), json!([0]), "the moved row stays selected"); + assert_eq!(h.state().session.doc().unwrap().doc, previewed, "and the art keeps the same colors"); + h.get_by_label(if confirm { "OK" } else { "Cancel" }).click(); + h.run_steps(2); + assert!(h.state().ui.dialog.is_none()); + let app = h.state(); + if confirm { + assert_eq!(app.session.doc().unwrap().doc, previewed, "OK keeps the edit"); + assert_eq!(app.session.doc().unwrap().history.undo.len(), undo + 1, "as one undo step"); + } else { + assert_eq!(app.session.doc().unwrap().doc, before, "Cancel restores the art"); + assert_eq!(app.session.doc().unwrap().history.undo.len(), undo); + } + } +} + +/// Many color bars scroll within the wheel's height, the dialog keeps its height, and the last bar +/// can be reached and selected; with every row excluded the bars area says there is nothing to +/// recolor. +#[test] +fn color_bars_scroll_and_show_when_nothing_is_recolored() { + let colors: Vec = (1..=40).map(|i| format!("#{:06x}", i * 9973)).collect(); + let refs: Vec<&str> = colors.iter().map(String::as_str).collect(); + let mut h = edit_harness(&refs); + let height = dialog_height(&h); + show_display(&mut h, "Show color bars"); + assert_eq!(dialog_height(&h), height, "40 bars keep the dialog's height"); + let first = h.get_by_role_and_label(egui::accesskit::Role::Button, "Color 1").rect(); + let wheel_bottom = first.top() + 251.0; + let last = |h: &egui_kittest::Harness<'_, VectorcraftApp>| h.get_by_role_and_label(egui::accesskit::Role::Button, "Color 40").rect(); + assert!(last(&h).top() > wheel_bottom, "the last bar starts out of view: {:?}", last(&h)); + h.input_mut().events.extend([ + Event::PointerMoved(first.center()), + Event::MouseWheel { + unit: egui::MouseWheelUnit::Point, + delta: vec2(0.0, -10000.0), + phase: egui::TouchPhase::Move, + modifiers: Modifiers::NONE, + source: egui::MouseWheelSource::Unknown, + }, + ]); + h.run_steps(12); + let shown = last(&h); + assert!(shown.bottom() <= wheel_bottom + 1.0, "the last bar scrolls into view: {shown:?}"); + assert_eq!(dialog_height(&h), height); + h.input_mut().events.extend(press(shown.center(), 1)); + h.run_steps(3); + assert_eq!(field(h.state(), "__sel"), json!([39])); + let mut rows = field(h.state(), "rows"); + for r in rows.as_array_mut().unwrap() { + r["exclude"] = json!(true); + } + set(h.state_mut(), "rows", rows); + h.run_steps(4); + assert!(h.query_by_role_and_label(egui::accesskit::Role::Button, "Color 1").is_none()); + let _ = h.get_by_label("No colors to recolor (preserved colors are left out)."); +} diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index b03788da3..e8da10df3 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -35,7 +35,8 @@ pub use chips::{ProxyClicks, chip_button, chip_button_named, fill_stroke_proxy, pub(crate) use color::pointer_phase; pub use color::{ Live, SliderValue, WheelResponse, channel_slider, channel_slider_named, channel_slider_valued, color_field, color_mesh, color_slider, - color_slider_named, color_slider_stepped, color_slider_valued, harmony_wheel, hex_field, hex_field_named, region_texture, stepped_area, + color_slider_named, color_slider_stepped, color_slider_valued, harmony_wheel, hex_field, hex_field_named, region_texture, segment_color, + segmented_wheel, stepped_area, }; pub use drag::{ PanelDrag, RowClick, SwatchRows, apply_row_click, art_drop, doc_preview, drag_ghost, drag_source, drop_target_outline, ghost_row, row_click, diff --git a/crates/ui-egui/src/widgets/color.rs b/crates/ui-egui/src/widgets/color.rs index 1433cbd4c..bd40e174d 100644 --- a/crates/ui-egui/src/widgets/color.rs +++ b/crates/ui-egui/src/widgets/color.rs @@ -486,6 +486,48 @@ fn wheel_shape(c: Pos2, r: f32, v: f32) -> egui::Shape { egui::Shape::mesh(mesh) } +/// Hue sectors and saturation rings of the [`segmented_wheel`]: patches 15° of hue by 20% +/// saturation wide, the sectors centered on red at 0°. +const WHEEL_SECTORS: u32 = 24; +const WHEEL_RINGS: u32 = 5; + +/// The color of the [`segmented_wheel`] patch that holds hue `h` (degrees) and saturation `s` (0 to +/// 1), at brightness `v`: the hue and saturation at the patch's center. +pub fn segment_color(h: f32, s: f32, v: f32) -> Color { + let step = 360.0 / WHEEL_SECTORS as f32; + let sector = (h.rem_euclid(360.0) / step).round() % WHEEL_SECTORS as f32; + let ring = (s.clamp(0.0, 1.0) * WHEEL_RINGS as f32).floor().min((WHEEL_RINGS - 1) as f32); + Color::from_hsb(sector * step, (ring + 0.5) / WHEEL_RINGS as f32, v) +} + +/// The segmented hue and saturation wheel at brightness `v`: a solid patch per hue sector and +/// saturation ring, in its [`segment_color`]. +fn segmented_wheel_shape(c: Pos2, r: f32, v: f32) -> egui::Shape { + // Points along each patch's arcs. + const ARC: u32 = 4; + let step = 360.0 / WHEEL_SECTORS as f32; + let mut mesh = egui::Mesh::default(); + for ring in 0..WHEEL_RINGS { + let (s0, s1) = (ring as f32 / WHEEL_RINGS as f32, (ring + 1) as f32 / WHEEL_RINGS as f32); + for k in 0..WHEEL_SECTORS { + let h = k as f32 * step; + let col = crate::panels::c32(&segment_color(h, (s0 + s1) / 2.0, v)); + let first = mesh.vertices.len() as u32; + for j in 0..=ARC { + let a = h - step / 2.0 + step * j as f32 / ARC as f32; + mesh.colored_vertex(wheel_pos(c, r, a, s0), col); + mesh.colored_vertex(wheel_pos(c, r, a, s1), col); + } + for j in 0..ARC { + let at = first + 2 * j; + mesh.add_triangle(at, at + 1, at + 3); + mesh.add_triangle(at, at + 3, at + 2); + } + } + } + egui::Shape::mesh(mesh) +} + /// What a [`harmony_wheel`] did this frame. #[derive(Default)] pub struct WheelResponse { @@ -501,6 +543,18 @@ pub struct WheelResponse { /// marker per colour joined to the centre (the selected colour, `base`, larger) and a Brightness /// slider for the selected colour below it. pub fn harmony_wheel(ui: &mut Ui, id: &str, size: f32, colors: &[Color], base: Option) -> WheelResponse { + wheel(ui, id, size, colors, base, false) +} + +/// [`harmony_wheel`] drawn as solid patches (24 hues by 5 saturations, each in its +/// [`segment_color`]). The markers sit at their colors' own hue and +/// saturation, and a drag moves them as freely as on the smooth wheel. +pub fn segmented_wheel(ui: &mut Ui, id: &str, size: f32, colors: &[Color], base: Option) -> WheelResponse { + wheel(ui, id, size, colors, base, true) +} + +/// [`harmony_wheel`], or with `segmented` [`segmented_wheel`]. +fn wheel(ui: &mut Ui, id: &str, size: f32, colors: &[Color], base: Option, segmented: bool) -> WheelResponse { let t = Tokens::get(ui.ctx()); let mut out = WheelResponse::default(); let base_hsb = base.and_then(|b| colors.get(b)).map(Color::to_hsb); @@ -510,7 +564,7 @@ pub fn harmony_wheel(ui: &mut Ui, id: &str, size: f32, colors: &[Color], base: O let (rect, resp) = ui.allocate_exact_size(vec2(size, size), Sense::click_and_drag()); let (c, r) = (rect.center(), size / 2.0 - 10.0); let painter = ui.painter_at(rect); - painter.add(wheel_shape(c, r, v.max(0.15))); + painter.add(if segmented { segmented_wheel_shape(c, r, v.max(0.15)) } else { wheel_shape(c, r, v.max(0.15)) }); painter.circle_stroke(c, r, Stroke::new(1.0, t.border)); let at = |col: &Color| { let [h, s, _] = col.to_hsb(); @@ -539,7 +593,8 @@ pub fn harmony_wheel(ui: &mut Ui, id: &str, size: f32, colors: &[Color], base: O let h = (-dv.y).atan2(dv.x).to_degrees().rem_euclid(360.0); out.moved = Some((i, [Some(h), Some((dv.length() / r).clamp(0.0, 1.0)), None])); } - resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Slider, ui.is_enabled(), tl!("Color wheel"))); + let name = if segmented { tl!("Segmented color wheel") } else { tl!("Color wheel") }; + resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Slider, ui.is_enabled(), name)); }); ui.add_space(6.0); ui.horizontal(|ui| { diff --git a/docs/control-protocol.md b/docs/control-protocol.md index 720bfeed3..3da08ad0f 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -264,7 +264,10 @@ same sliders under the wheel, and its mode menu also has Global Adjust (`sliderM Brightness, Temperature and Luminosity sliders that set `globalAdjust` (`{saturation, brightness, temperature, luminosity}`, percent from -100 to 100, 0 by default). The dialog sends a non-zero `globalAdjust` as `recolor.apply`'s `globalAdjust` in every slider mode, so one slider changes every recolored color at once and the rows keep their new -colors; colors opened alone are saved as a group with the change. Setting +colors; colors opened alone are saved as a group with the change. `display` sets how the Edit tab shows the new +colors: `smooth` (the smooth wheel, also when unset; each opening starts there), `segmented` (a wheel of solid patches, +24 hues by 5 saturations, whose markers move as freely) or `bars` (a solid bar per recolored color in row order; a click +selects its row for the sliders and a drag moves the row without changing a color). Setting `colors`, a preserve flag or `limitTo` reduces the rows again (also on `ui.dialog.confirm`). The dialog previews `recolor.apply` on the canvas; confirm keeps it as one undo step (with a group, the group is rewritten in the same step; with no art selected only the group changes). In the Swatches panel, double-clicking a colour group's folder, diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 7e3241865..878c1d455 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -984,6 +984,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur The Separations Preview panel's Gamut Check button reports one color outside the CMYK gamut in the singular and more in the plural, in English and in each language's plural forms (`gamut_check_status_takes_the_plural_form` in `crates/ui-egui/src/i18n/tests.rs`). - **Document Color Mode outside the layers:** File › Document Color Mode (in one undo step) and `document.open {colorMode}` also convert the colors of graph designs, graphic styles, opacity-mask art, the tile that pattern editing's Cancel puts back and the art of brushes (with a Hue Shift brush's key color); a tint of a global swatch there becomes the converted swatch's color at its tint, as in the art. - **Recolor Artwork layout (#3416):** the Edit chip list uses the modal scroll helper and its actual remaining height; the tab's minimum-height space scrolls with the chips. `tests_recolor` covers 5/20/200/400 colors, window resizing, 100/125/150/200% UI scaling, scrolling to and editing the final chip, Preview, Cancel, Confirm and Undo, alongside the existing #1733 drag regressions. Scores and estimates unchanged. + - **Recolor Artwork Edit displays:** buttons beside the Edit tab's wheel show the colors on a smooth color wheel, a segmented color wheel or as color bars ([Edit colors using Recolor Artwork](https://helpx.adobe.com/illustrator/desktop/manage-colors/modify-colors/edit-colors-using-edit-colors-dialog.html): "Select a smooth color wheel, a segmented color wheel, or color bars to display and edit colors"). The segmented wheel draws 24 hues by 5 saturations as solid patches, and its markers move freely as on the smooth wheel. The bars show one solid bar per recolored color in row order: a click selects a bar for the color sliders, and a drag moves its row without changing a color ([Advanced recolor options overview](https://helpx.adobe.com/illustrator/desktop/manage-colors/learn-color-basics/edit-colors-dialog-overview.html)). The dialog opens on the smooth wheel; `display` (`smooth`, `segmented`, `bars`) is the dialog field. `tests_recolor` covers switching, the dialog height, segmented dragging and Brightness, and bar selection, editing, reordering, scrolling, Preview, Cancel and OK. Scores and estimates unchanged. - **Edit Colors parameters:** `edit.colors.saturate` needs a number `intensity`, the value from -100% to 100% that Saturate takes ([Adjusting colors](https://web.archive.org/web/20230128232631/https://helpx.adobe.com/illustrator/using/adjusting-colors.html)), and answers "missing number `intensity`" without one; a number outside the range counts as its nearest end. `edit.colors.adjustBalance` refuses a channel and `color.harmony` refuses `steps` or `amount` that is not a number; both read `null` as a value left out. `tests::editcolors` and `tests::colorguide` cover them. - **Color Picker layout (#2008):** a centered draggable header, equal-width actions above aligned HSB/RGB/Hex and Lab/CMYK fields, compact field and slider, and warnings below the preview. No horizontal scrolling; narrow windows stack the sections with vertical scrolling. Tests in `dialogs/color_picker.rs` cover actual pointer/key input, typed values before OK/Enter, Cancel/Escape rollback, one Undo step, mode-size stability, warnings and layout at two UI scales. The screen sampler sits above the field and the upstream two-grid Tab order is retained. AccessKit regressions check the action names and each channel slider's displayed value, units and range. German and enlarged English UI renders were reviewed locally; no reference images are shipped. This is a layout improvement, with no change to scores or remaining-hour estimates. - **Eyedropper sources (#1251):** ordinary and Shift clicks, Shift+Alt appearance append, raster pixels and gradient-stop sampling can read visible locked/template art and sources outside isolation. Sampling leaves the source and selection intact; Alt reverse still uses editable hits, and hidden art stays excluded. Engine/tool/hit regressions cover the editing boundary, color models and one-step Undo/Redo. The general object/image workflow is described in [public HelpX](https://helpx.adobe.com/illustrator/using/tool-techniques/eyedropper-tool.html); the lock/isolation expectations come from the user report and these regressions, without a new full-parity measurement.