Merge pull request #5249 from dotCooCoo/feat/recolor-wheel-display

Recolor Artwork: the Edit tab shows the colors on a smooth wheel, a segmented wheel or as color bars
This commit is contained in:
Brandon Thomas
2026-11-03 00:19:40 -05:00
committed by GitHub
32 changed files with 513 additions and 8 deletions
+112 -4
View File
@@ -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<Color> = 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<usize>) -> bool {
let t = Tokens::get(ui.ctx());
let mut rects: Vec<Rect> = 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<usize> {
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.
+4
View File
@@ -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: Височина:
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -2626,6 +2626,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:
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -1805,6 +1805,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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -2657,6 +2657,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:
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -2444,6 +2444,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:
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -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:
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -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: Ύψος:
Can't render this file because it contains an unexpected character in line 163 and column 5.
+4
View File
@@ -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:
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -2601,6 +2601,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:
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -2444,6 +2444,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:
Can't render this file because it contains an unexpected character in line 2210 and column 5.
+4
View File
@@ -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:
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -1726,6 +1726,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… ヘルプ › コマンドを検索…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -2335,6 +2335,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 ჰარმონიის ფერების დაკავშირება
Can't render this file because it is too large.
+4
View File
@@ -1716,6 +1716,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… 도움말 › 명령 검색…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -2458,6 +2458,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:
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -2567,6 +2567,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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -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:
Can't render this file because it contains an unexpected character in line 6 and column 47.
+4
View File
@@ -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: Высота:
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -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:
Can't render this file because it contains an unexpected character in line 5 and column 25.
+4
View File
@@ -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:
Can't render this file because it contains an unexpected character in line 5 and column 25.
+4
View File
@@ -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: Висота:
Can't render this file because it contains an unexpected character in line 177 and column 5.
+4
View File
@@ -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:
Can't render this file because it contains an unexpected character in line 199 and column 5.
+4
View File
@@ -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: 高度:
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -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: 高度:
Can't render this file because it contains an unexpected character in line 6 and column 47.
+233
View File
@@ -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<String> {
let mut v: Vec<String> = 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<String> = (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).");
}
+2 -1
View File
@@ -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,
+57 -2
View File
@@ -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<usize>) -> 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<usize>) -> 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<usize>, 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| {
+4 -1
View File
@@ -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,
+1
View File
@@ -988,6 +988,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.