From 2a7f4d95059006d20f559b841ba4fae8cd276dcb Mon Sep 17 00:00:00 2001 From: moppeeer Date: Thu, 15 Oct 2026 21:58:30 +0200 Subject: [PATCH] Color Picker: laid out as InDesign's, with a component field and slider, every space's values and Add CMYK Swatch Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 1 + crates/ui-egui/src/dialogs.rs | 431 ++++++++++++++++++++++++++++--- crates/ui-egui/src/i18n.rs | 22 ++ crates/ui-egui/src/i18n/ar.rs | 4 + crates/ui-egui/src/i18n/de.rs | 4 + crates/ui-egui/src/i18n/it.rs | 4 + crates/ui-egui/src/i18n/ja.rs | 4 + crates/ui-egui/src/i18n/ka.rs | 4 + crates/ui-egui/src/i18n/pt_br.rs | 4 + crates/ui-egui/src/i18n/uk.rs | 4 + 10 files changed, 450 insertions(+), 32 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index d60a6bf49..87ad24136 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -347,6 +347,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m Newest first. +- 2026-10-15: The Color Picker is laid out as InDesign's: a field of two components and a slider for the third (H, S, B, L, a, b, R, G or B by radio button; RGB or Lab Color Space View), the new colour over the old one, H/S/B, L/a/b, R/G/B and C/M/Y/K values, the hex code, and Add CMYK Swatch. - **2026-10-14** · The Type tool draws a new text frame over a locked empty frame, as InDesign does, instead of turning the locked frame into a text frame. No readiness percentage change. - **2026-10-14** · 100% black shows as pure black on screen by default, as InDesign's Appearance of Black does out of the box (Display All Blacks as Rich Black); Preferences › Appearance of Black › Display All Blacks Accurately still shows it as the dark grey it prints, and a saved choice is kept. No readiness percentage change. - **2026-10-14** · Pathfinder keeps the frontmost object, as InDesign does: a photo placed over shapes and intersected with them shows through the shapes (it was dropped, and the shapes kept their black fill); Add, Exclude Overlap and Minus Back take the front object's fill too, Subtract still keeps the back one. No readiness percentage change. diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 7d02776a2..262a48b60 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -1828,7 +1828,7 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) { } "insertXref" => insert_xref(app, ui, &mut d), "findFont" => find_font(app, ui, &mut d), - "colorPicker" => color_picker(ui, &mut d), + "colorPicker" => color_picker(app, ui, &mut d), "preferences" => preferences(app, ui, &mut d, body_height), "print" => print_dialog(app, ui, &mut d), "pdfImport" => { @@ -4710,44 +4710,356 @@ fn insert_xref(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { } } -/// Color Picker: saturation/brightness field and hue slider, with RGB, CMYK and hex fields. -fn color_picker(ui: &mut egui::Ui, d: &mut Dialog) { - let hex = d.s("hex"); - let mut c = designcraft_color::Color::from_hex(&hex).map(|c| c.to_rgb()).map_or(egui::Color32::BLACK, |[r, g, b]| { - egui::Color32::from_rgb((r * 255.0).round() as u8, (g * 255.0).round() as u8, (b * 255.0).round() as u8) - }); - let before = c; +/// The Color Picker's components: the one picked runs along the slider, the other two of its +/// space across the field (InDesign's, as Photoshop's: R puts B across and G up). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum PickComp { + H, + S, + Br, + L, + A, + Lb, + R, + G, + Bl, +} + +impl PickComp { + const ALL: [PickComp; 9] = + [PickComp::H, PickComp::S, PickComp::Br, PickComp::L, PickComp::A, PickComp::Lb, PickComp::R, PickComp::G, PickComp::Bl]; + + fn key(self) -> &'static str { + match self { + PickComp::H => "h", + PickComp::S => "s", + PickComp::Br => "b", + PickComp::L => "l", + PickComp::A => "la", + PickComp::Lb => "lb", + PickComp::R => "r", + PickComp::G => "g", + PickComp::Bl => "bl", + } + } + + fn from_key(k: &str) -> PickComp { + PickComp::ALL.into_iter().find(|c| c.key() == k).unwrap_or(PickComp::R) + } + + fn lab(self) -> bool { + matches!(self, PickComp::L | PickComp::A | PickComp::Lb) + } +} + +/// The colour the picker shows, in each of its spaces (HSB keeps its hue through greys). +#[derive(Clone, Copy, Debug, PartialEq)] +pub(crate) struct Picked { + pub rgb: [f32; 3], + pub hsb: [f32; 3], + pub lab: [f32; 3], +} + +impl Picked { + pub fn from_rgb(rgb: [f32; 3], hue: f32) -> Picked { + let rgb = rgb.map(|v| v.clamp(0.0, 1.0)); + let c = designcraft_color::Color::Rgb { r: rgb[0], g: rgb[1], b: rgb[2] }; + let mut hsb = c.to_hsb(); + if hsb[1] <= 1e-4 || hsb[2] <= 1e-4 { + hsb[0] = hue; + } + let l = designcraft_color::cms::lab::srgb_to_lab(rgb); + Picked { rgb, hsb, lab: [l.l, l.a, l.b] } + } + + fn from_hsb(hsb: [f32; 3]) -> Picked { + let hsb = [hsb[0].rem_euclid(360.0), hsb[1].clamp(0.0, 1.0), hsb[2].clamp(0.0, 1.0)]; + let mut p = Picked::from_rgb(designcraft_color::Color::from_hsb(hsb[0], hsb[1], hsb[2]).to_rgb_uncalibrated(), hsb[0]); + p.hsb = hsb; + p + } + + fn from_lab(lab: [f32; 3], hue: f32) -> Picked { + let lab = [lab[0].clamp(0.0, 100.0), lab[1].clamp(-128.0, 127.0), lab[2].clamp(-128.0, 127.0)]; + let mut p = Picked::from_rgb(designcraft_color::cms::lab::lab_to_srgb(designcraft_color::cms::Lab::new(lab[0], lab[1], lab[2])), hue); + p.lab = lab; + p + } + + /// (slider, across, up), each 0..1, for component `c`. + pub fn coords(&self, c: PickComp) -> (f32, f32, f32) { + let [r, g, b] = self.rgb; + let [h, s, v] = self.hsb; + let [l, la, lb] = self.lab; + let (a, bb) = ((la + 128.0) / 255.0, (lb + 128.0) / 255.0); + match c { + PickComp::R => (r, b, g), + PickComp::G => (g, b, r), + PickComp::Bl => (b, r, g), + PickComp::H => (h / 360.0, s, v), + PickComp::S => (s, h / 360.0, v), + PickComp::Br => (v, h / 360.0, s), + PickComp::L => (l / 100.0, a, bb), + PickComp::A => (a, bb, l / 100.0), + PickComp::Lb => (bb, a, l / 100.0), + } + } + + /// The colour at (slider `t`, across `u`, up `v`) for component `c`. + pub fn at(&self, c: PickComp, t: f32, u: f32, v: f32) -> Picked { + let hue = self.hsb[0]; + let ab = |x: f32| x * 255.0 - 128.0; + match c { + PickComp::R => Picked::from_rgb([t, v, u], hue), + PickComp::G => Picked::from_rgb([v, t, u], hue), + PickComp::Bl => Picked::from_rgb([u, v, t], hue), + PickComp::H => Picked::from_hsb([t * 360.0, u, v]), + PickComp::S => Picked::from_hsb([u * 360.0, t, v]), + PickComp::Br => Picked::from_hsb([u * 360.0, v, t]), + PickComp::L => Picked::from_lab([t * 100.0, ab(u), ab(v)], hue), + PickComp::A => Picked::from_lab([v * 100.0, ab(t), ab(u)], hue), + PickComp::Lb => Picked::from_lab([v * 100.0, ab(u), ab(t)], hue), + } + } + + /// The slider's colour at `t`: the hue slider shows pure hues, the others this colour with + /// the component at `t`. + fn slider_at(&self, c: PickComp, t: f32) -> [f32; 3] { + if c == PickComp::H { + return designcraft_color::Color::from_hsb(t * 360.0, 1.0, 1.0).to_rgb_uncalibrated(); + } + let (_, u, v) = self.coords(c); + self.at(c, t, u, v).rgb + } + + pub fn hex(&self) -> String { + let q = |x: f32| (x.clamp(0.0, 1.0) * 255.0).round() as u8; + format!("#{:02x}{:02x}{:02x}", q(self.rgb[0]), q(self.rgb[1]), q(self.rgb[2])) + } +} + +fn color32(rgb: [f32; 3]) -> egui::Color32 { + let q = |x: f32| (x.clamp(0.0, 1.0) * 255.0).round() as u8; + egui::Color32::from_rgb(q(rgb[0]), q(rgb[1]), q(rgb[2])) +} + +/// A `cols` × `rows` grid of quads over `r`, coloured at each vertex by `f(x, y)` (0..1, y up). +fn gradient_mesh(painter: &egui::Painter, r: egui::Rect, cols: usize, rows: usize, f: impl Fn(f32, f32) -> [f32; 3]) { + let mut mesh = egui::Mesh::default(); + for j in 0..=rows { + for i in 0..=cols { + let (x, y) = (i as f32 / cols as f32, j as f32 / rows as f32); + mesh.colored_vertex(egui::pos2(r.left() + x * r.width(), r.bottom() - y * r.height()), color32(f(x, y))); + } + } + let w = (cols + 1) as u32; + for j in 0..rows as u32 { + for i in 0..cols as u32 { + let a = j * w + i; + mesh.add_triangle(a, a + 1, a + w); + mesh.add_triangle(a + 1, a + w + 1, a + w); + } + } + painter.add(egui::Shape::mesh(mesh)); +} + +/// `n` numbers from an array field. +fn nums(d: &Dialog, key: &str) -> Option<[f64; N]> { + let a = d.fields.get(key)?.as_array()?; + let mut out = [0.0; N]; + for (o, v) in out.iter_mut().zip(a) { + *o = v.as_f64().filter(|v| v.is_finite())?; + } + (a.len() == N).then_some(out) +} + +/// The colour in the dialog's fields: `hex`, with the HSB hue and the Lab values kept while +/// they round-trip (`_hsb`, `_lab`). +fn picked_of(d: &Dialog) -> Picked { + let rgb = designcraft_color::Color::from_hex(&d.s("hex")).map_or([0.0; 3], |c| c.to_rgb_uncalibrated()); + let hue = nums::<3>(d, "_hsb").map_or(0.0, |h| h[0] as f32); + let mut p = Picked::from_rgb(rgb, hue); + // The exact values the picker last set, while they still give this colour. + if let Some(h) = nums::<3>(d, "_hsb").map(|h| h.map(|v| v as f32)) + && Picked::from_hsb(h).hex() == p.hex() + { + p.hsb = h; + } + if let Some(l) = nums::<3>(d, "_lab").map(|l| l.map(|v| v as f32)) + && Picked::from_lab(l, p.hsb[0]).hex() == p.hex() + { + p.lab = l; + } + p +} + +fn store_picked(d: &mut Dialog, p: &Picked) { + d.fields.insert("hex".into(), json!(p.hex())); + d.fields.insert("_hsb".into(), json!(p.hsb)); + d.fields.insert("_lab".into(), json!(p.lab)); +} + +/// The Color Picker, laid out as InDesign's: the field of two components and the slider of the +/// third (the one whose radio button is on, R at first), the new colour over the one it replaces, +/// H/S/B, L/a/b, R/G/B and C/M/Y/K values, the hex code, and Add CMYK Swatch. +fn color_picker(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { + let lang = app.ui.language.clone(); + let tr = |s: &str| crate::i18n::tr(&lang, s).to_string(); + if !d.fields.contains_key("old") { + let old = d.s("hex"); + d.fields.insert("old".into(), json!(old)); + } + let comp = PickComp::from_key(&d.s("component")); + let mut p = picked_of(d); + let before = p; + let t = crate::theme::Tokens::get(ui.ctx()); ui.horizontal_top(|ui| { - egui::color_picker::color_picker_color32(ui, &mut c, egui::color_picker::Alpha::Opaque); + // The field and the slider. ui.vertical(|ui| { - let mut rgb = [c.r() as f32, c.g() as f32, c.b() as f32]; - egui::Grid::new("cp_rgb").num_columns(2).spacing([6.0, 4.0]).show(ui, |ui| { - for (k, l) in ["R", "G", "B"].iter().enumerate() { - ui.label(*l); - ui.add(egui::DragValue::new(&mut rgb[k]).range(0.0..=255.0)); - ui.end_row(); + crate::rtl::label(ui, egui::RichText::new(tr(if comp.lab() { "Lab Color Space View" } else { "RGB Color Space View" })).size(11.5)); + ui.add_space(4.0); + ui.horizontal_top(|ui| { + let (fr, fresp) = ui.allocate_exact_size(egui::vec2(190.0, 200.0), egui::Sense::click_and_drag()); + let (tv, _, _) = p.coords(comp); + let cells = if matches!(comp, PickComp::R | PickComp::G | PickComp::Bl | PickComp::H) { 2 } else { 24 }; + gradient_mesh(ui.painter(), fr, cells, cells, |x, y| p.at(comp, tv, x, y).rgb); + ui.painter().rect_stroke(fr, 0.0, egui::Stroke::new(1.0, t.field_border), egui::StrokeKind::Outside); + fresp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Other, true, "Color field")); + if let Some(pos) = fresp.interact_pointer_pos().filter(|_| fresp.is_pointer_button_down_on() || fresp.clicked()) { + let u = ((pos.x - fr.left()) / fr.width()).clamp(0.0, 1.0); + let v = ((fr.bottom() - pos.y) / fr.height()).clamp(0.0, 1.0); + p = p.at(comp, tv, u, v); } - }); - c = egui::Color32::from_rgb(rgb[0] as u8, rgb[1] as u8, rgb[2] as u8); - let col = designcraft_color::Color::rgb8(c.r(), c.g(), c.b()); - let [cc, m, y, k] = col.to_cmyk(); - ui.add_space(6.0); - ui.label(format!("C {:.0}% M {:.0}% Y {:.0}% K {:.0}%", cc * 100.0, m * 100.0, y * 100.0, k * 100.0)); - ui.add_space(6.0); - ui.horizontal(|ui| { - ui.label("#"); - let mut h = format!("{:02x}{:02x}{:02x}", c.r(), c.g(), c.b()); - if ui.add(egui::TextEdit::singleline(&mut h).desired_width(70.0)).changed() - && let Some(n) = designcraft_color::Color::from_hex(&h) - { - let [r, g, b] = n.to_rgb(); - c = egui::Color32::from_rgb((r * 255.0).round() as u8, (g * 255.0).round() as u8, (b * 255.0).round() as u8); + let (_, u, v) = p.coords(comp); + let mark = egui::pos2(fr.left() + u * fr.width(), fr.bottom() - v * fr.height()); + ui.painter().circle_stroke(mark, 5.0, egui::Stroke::new(1.5, egui::Color32::WHITE)); + ui.painter().circle_stroke(mark, 6.0, egui::Stroke::new(1.0, egui::Color32::BLACK)); + ui.add_space(6.0); + let (sr, sresp) = ui.allocate_exact_size(egui::vec2(14.0, 200.0), egui::Sense::click_and_drag()); + gradient_mesh(ui.painter(), sr, 1, 48, |_, y| p.slider_at(comp, y)); + ui.painter().rect_stroke(sr, 0.0, egui::Stroke::new(1.0, t.field_border), egui::StrokeKind::Outside); + sresp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Slider, true, "Color slider")); + if let Some(pos) = sresp.interact_pointer_pos().filter(|_| sresp.is_pointer_button_down_on() || sresp.clicked()) { + let tn = ((sr.bottom() - pos.y) / sr.height()).clamp(0.0, 1.0); + let (_, u, v) = p.coords(comp); + p = p.at(comp, tn, u, v); + } + let (tn, _, _) = p.coords(comp); + let y = sr.bottom() - tn * sr.height(); + for (x, dir) in [(sr.left() - 1.0, 1.0), (sr.right() + 1.0, -1.0)] { + ui.painter().add(egui::Shape::convex_polygon( + vec![egui::pos2(x, y), egui::pos2(x - 5.0 * dir, y - 4.0), egui::pos2(x - 5.0 * dir, y + 4.0)], + t.text_strong, + egui::Stroke::NONE, + )); } }); }); + ui.add_space(10.0); + ui.vertical(|ui| { + // The new colour over the one it replaces. + let (pr, _) = ui.allocate_exact_size(egui::vec2(60.0, 60.0), egui::Sense::hover()); + let old = designcraft_color::Color::from_hex(&d.s("old")).map_or([0.0; 3], |c| c.to_rgb_uncalibrated()); + ui.painter().rect_filled(egui::Rect::from_min_max(pr.min, egui::pos2(pr.max.x, pr.center().y)), 0.0, color32(p.rgb)); + ui.painter().rect_filled(egui::Rect::from_min_max(egui::pos2(pr.min.x, pr.center().y), pr.max), 0.0, color32(old)); + ui.painter().rect_stroke(pr, 0.0, egui::Stroke::new(1.0, t.field_border), egui::StrokeKind::Outside); + ui.add_space(10.0); + let mut comp_now = comp; + let mut edited: Option = None; + let [cc, cm, cy, ck] = designcraft_color::Color::Rgb { r: p.rgb[0], g: p.rgb[1], b: p.rgb[2] } + .to_cmyk_managed(designcraft_color::cms::active_settings().intent) + .map(|v| v * 100.0); + let mut cmyk = [cc, cm, cy, ck]; + egui::Grid::new("cp_values").num_columns(6).spacing([4.0, 4.0]).show(ui, |ui| { + let mut field = + |ui: &mut egui::Ui, c: Option, label: &str, v: &mut f32, range: std::ops::RangeInclusive, suffix: &str| -> bool { + match c { + Some(c) => { + ui.radio_value(&mut comp_now, c, ""); + } + None => { + ui.label(""); + } + } + ui.label(label); + ui.add(egui::DragValue::new(v).range(range).max_decimals(0).suffix(suffix)).changed() + }; + let (mut h, mut s, mut b) = (p.hsb[0], p.hsb[1] * 100.0, p.hsb[2] * 100.0); + let (mut l, mut la, mut lb) = (p.lab[0], p.lab[1], p.lab[2]); + let (mut r, mut g, mut bl) = (p.rgb[0] * 255.0, p.rgb[1] * 255.0, p.rgb[2] * 255.0); + // Rows as InDesign's: H S B beside L a b, R G B beside C M Y K. + let hsb_changed = + field(ui, Some(PickComp::H), "H:", &mut h, 0.0..=360.0, "°") | field(ui, Some(PickComp::L), "L:", &mut l, 0.0..=100.0, ""); + ui.end_row(); + let hsb_changed2 = + field(ui, Some(PickComp::S), "S:", &mut s, 0.0..=100.0, "%") | field(ui, Some(PickComp::A), "a:", &mut la, -128.0..=127.0, ""); + ui.end_row(); + let hsb_changed3 = + field(ui, Some(PickComp::Br), "B:", &mut b, 0.0..=100.0, "%") | field(ui, Some(PickComp::Lb), "b:", &mut lb, -128.0..=127.0, ""); + ui.end_row(); + if hsb_changed || hsb_changed2 || hsb_changed3 { + // Whichever space changed wins: HSB if its values moved, else Lab. + edited = Some(if (h, s, b) != (p.hsb[0], p.hsb[1] * 100.0, p.hsb[2] * 100.0) { + Picked::from_hsb([h, s / 100.0, b / 100.0]) + } else { + Picked::from_lab([l, la, lb], p.hsb[0]) + }); + } + let rgb_changed = field(ui, Some(PickComp::R), "R:", &mut r, 0.0..=255.0, ""); + let c1 = field(ui, None, "C:", &mut cmyk[0], 0.0..=100.0, "%"); + ui.end_row(); + let rgb_changed2 = field(ui, Some(PickComp::G), "G:", &mut g, 0.0..=255.0, ""); + let c2 = field(ui, None, "M:", &mut cmyk[1], 0.0..=100.0, "%"); + ui.end_row(); + let rgb_changed3 = field(ui, Some(PickComp::Bl), "B:", &mut bl, 0.0..=255.0, ""); + let c3 = field(ui, None, "Y:", &mut cmyk[2], 0.0..=100.0, "%"); + ui.end_row(); + ui.label(""); + ui.label(""); + ui.label(""); + let c4 = field(ui, None, "K:", &mut cmyk[3], 0.0..=100.0, "%"); + ui.end_row(); + if rgb_changed || rgb_changed2 || rgb_changed3 { + edited = Some(Picked::from_rgb([r / 255.0, g / 255.0, bl / 255.0], p.hsb[0])); + } + if c1 || c2 || c3 || c4 { + let [c, m, y, k] = cmyk.map(|v| v / 100.0); + edited = Some(Picked::from_rgb(designcraft_color::Color::Cmyk { c, m, y, k }.to_rgb(), p.hsb[0])); + } + }); + if let Some(e) = edited { + p = e; + } + if comp_now != comp { + d.fields.insert("component".into(), json!(comp_now.key())); + } + ui.add_space(4.0); + ui.horizontal(|ui| { + ui.label("#"); + let id = egui::Id::new("color_picker_hex"); + let editing = ui.memory(|m| m.has_focus(id)); + let mut h = if editing { d.s("_hexEdit") } else { p.hex().trim_start_matches('#').to_string() }; + if ui.add(egui::TextEdit::singleline(&mut h).id(id).desired_width(70.0)).changed() + && let Some(c) = designcraft_color::Color::from_hex(&h) + { + p = Picked::from_rgb(c.to_rgb_uncalibrated(), p.hsb[0]); + } + d.fields.insert("_hexEdit".into(), json!(h)); + }); + ui.add_space(8.0); + if ui.button(crate::rtl::widget(ui, tr("Add CMYK Swatch"))).clicked() { + let [c, m, y, k] = designcraft_color::Color::Rgb { r: p.rgb[0], g: p.rgb[1], b: p.rgb[2] } + .to_cmyk_managed(designcraft_color::cms::active_settings().intent) + .map(|v| (v * 100.0).round()); + match app.run("swatch.create", json!({"color": {"c": c, "m": m, "y": y, "k": k}})) { + Ok(r) => app.status(format!("{} {}", tr("Added swatch"), r["name"].as_str().unwrap_or(""))), + Err(e) => app.status(e), + } + } + }); }); - if c != before { - d.fields.insert("hex".into(), json!(format!("#{:02x}{:02x}{:02x}", c.r(), c.g(), c.b()))); + if p != before { + store_picked(d, &p); } } @@ -6794,6 +7106,61 @@ mod tests { (0..doc.page_count()).map(|abs| doc.page(abs).unwrap().guides.len()).collect() } + /// The Color Picker laid out as InDesign's: a field and a slider for the component whose radio + /// is on, the new colour over the old one, and the values in every space. + #[test] + fn the_color_picker_picks_on_its_field_and_slider_and_adds_a_cmyk_swatch() { + use egui_kittest::kittest::Queryable; + let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); + app.run("file.new", json!({})).unwrap(); + app.run("frame.create", json!({"rect": [72, 72, 200, 200], "shape": "rectangle"})).unwrap(); + app.ui.dialog = Some(Dialog::new("colorPicker", json!({"target": "fill", "hex": "#7c1315", "text": false}))); + let mut h = crate::test_window::open(app, egui::vec2(1400.0, 900.0)); + h.run_steps(3); + // R at first: R up the slider, B across and G up the field. + let _ = h.get_by_label("RGB Color Space View"); + let field = h.get_by_label("Color field").rect(); + crate::test_window::click_at(&mut h, egui::pos2(field.right() - 1.0, field.top() + 1.0)); + h.run_steps(2); + let p = picked_of(h.state().app.ui.dialog.as_ref().unwrap()); + assert!((p.rgb[0] - 124.0 / 255.0).abs() < 0.01 && p.rgb[1] > 0.97 && p.rgb[2] > 0.97, "{p:?}: R kept, G and B full"); + // The slider sets R alone. + let slider = h.get_by_label("Color slider").rect(); + crate::test_window::click_at(&mut h, egui::pos2(slider.center().x, slider.bottom() - 1.0)); + h.run_steps(2); + let p = picked_of(h.state().app.ui.dialog.as_ref().unwrap()); + assert!(p.rgb[0] < 0.02 && p.rgb[1] > 0.97, "{p:?}"); + // Hue on the slider: the field is saturation across, brightness up. + h.state_mut().app.ui.dialog.as_mut().unwrap().fields.insert("hex".into(), json!("#7c1315")); + h.state_mut().app.ui.dialog.as_mut().unwrap().fields.insert("component".into(), json!("h")); + h.run_steps(2); + crate::test_window::click_at(&mut h, egui::pos2(field.left() + 1.0, field.top() + 1.0)); + h.run_steps(2); + let p = picked_of(h.state().app.ui.dialog.as_ref().unwrap()); + assert!(p.hsb[1] < 0.02 && p.hsb[2] > 0.98 && (p.hsb[0] - 359.0).abs() < 2.0, "{p:?}: no saturation, full brightness, the hue kept"); + // Lab shows its own view. + h.state_mut().app.ui.dialog.as_mut().unwrap().fields.insert("component".into(), json!("l")); + h.run_steps(2); + let _ = h.get_by_label("Lab Color Space View"); + // Add CMYK Swatch adds the colour as a CMYK swatch; OK applies it. + h.state_mut().app.ui.dialog.as_mut().unwrap().fields.insert("hex".into(), json!("#7c1315")); + h.run_steps(2); + let before = h.state().app.session.active().unwrap().doc.swatches.len(); + h.get_by_label("Add CMYK Swatch").click(); + h.run_steps(3); + let doc = &h.state().app.session.active().unwrap().doc; + assert_eq!(doc.swatches.len(), before + 1); + assert!(matches!( + doc.swatches.last().unwrap().value, + designcraft_color::swatch::SwatchValue::Color { color: designcraft_color::Color::Cmyk { .. }, .. } + )); + confirm(&mut h.state_mut().app).unwrap(); + let st = h.state().app.session.active().unwrap(); + let id = st.selection.items[0]; + let fill = st.doc.item(id).unwrap().fill.swatch.clone(); + assert_eq!(st.doc.resolve_color(&fill, 1.0).unwrap().to_hex(), "#7c1315"); + } + #[test] fn create_guides_dialog_targets_the_page_in_view() { let mut app = facing_app(6); diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index 245a6e287..87a3eda7b 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -58,6 +58,28 @@ const TABLE: &[(&str, [&str; 5])] = &[ ("Cut", ["Ausschneiden", "Couper", "Cortar", "カット", "剪切"]), ("Copy", ["Kopieren", "Copier", "Copiar", "コピー", "复制"]), ("Paste", ["Einfügen", "Coller", "Pegar", "ペースト", "粘贴"]), + ( + "RGB Color Space View", + [ + "RGB-Farbraumansicht", + "Affichage de l’espace colorimétrique RVB", + "Vista de espacio de color RGB", + "RGB カラースペース表示", + "RGB 色彩空间视图", + ], + ), + ( + "Lab Color Space View", + [ + "Lab-Farbraumansicht", + "Affichage de l’espace colorimétrique Lab", + "Vista de espacio de color Lab", + "Lab カラースペース表示", + "Lab 色彩空间视图", + ], + ), + ("Add CMYK Swatch", ["CMYK-Farbfeld hinzufügen", "Ajouter une nuance CMJN", "Añadir muestra CMYK", "CMYK スウォッチを追加", "添加 CMYK 色板"]), + ("Added swatch", ["Farbfeld hinzugefügt", "Nuance ajoutée", "Muestra añadida", "スウォッチを追加しました", "已添加色板"]), ( "Bulleted & Numbered Lists", [ diff --git a/crates/ui-egui/src/i18n/ar.rs b/crates/ui-egui/src/i18n/ar.rs index d1dcb7acf..29a703d35 100644 --- a/crates/ui-egui/src/i18n/ar.rs +++ b/crates/ui-egui/src/i18n/ar.rs @@ -30,6 +30,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "قص"), ("Copy", "نسخ"), ("Paste", "لصق"), + ("RGB Color Space View", "عرض مساحة ألوان RGB"), + ("Lab Color Space View", "عرض مساحة ألوان Lab"), + ("Add CMYK Swatch", "إضافة حامل ألوان CMYK"), + ("Added swatch", "تمت إضافة حامل الألوان"), ("Bulleted & Numbered Lists", "القوائم ذات التعداد النقطي والرقمي"), ("Current Page Number", "رقم الصفحة الحالية"), ("Apply Color (,)", "تطبيق اللون (,)"), diff --git a/crates/ui-egui/src/i18n/de.rs b/crates/ui-egui/src/i18n/de.rs index 0f55578b5..d9eb9b272 100644 --- a/crates/ui-egui/src/i18n/de.rs +++ b/crates/ui-egui/src/i18n/de.rs @@ -30,6 +30,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Ausschneiden"), ("Copy", "Kopieren"), ("Paste", "Einfügen"), + ("RGB Color Space View", "RGB-Farbraumansicht"), + ("Lab Color Space View", "Lab-Farbraumansicht"), + ("Add CMYK Swatch", "CMYK-Farbfeld hinzufügen"), + ("Added swatch", "Farbfeld hinzugefügt"), ("Clear", "Löschen"), ("Duplicate", "Duplizieren"), ("Select All", "Alles auswählen"), diff --git a/crates/ui-egui/src/i18n/it.rs b/crates/ui-egui/src/i18n/it.rs index 00328ac9f..1da1642d3 100644 --- a/crates/ui-egui/src/i18n/it.rs +++ b/crates/ui-egui/src/i18n/it.rs @@ -28,6 +28,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Taglia"), ("Copy", "Copia"), ("Paste", "Incolla"), + ("RGB Color Space View", "Vista spazio colore RGB"), + ("Lab Color Space View", "Vista spazio colore Lab"), + ("Add CMYK Swatch", "Aggiungi campione CMYK"), + ("Added swatch", "Campione aggiunto"), ("Bulleted & Numbered Lists", "Elenchi puntati e numerati"), ("Current Page Number", "Numero di pagina corrente"), ("Apply Color (,)", "Applica colore (,)"), diff --git a/crates/ui-egui/src/i18n/ja.rs b/crates/ui-egui/src/i18n/ja.rs index fe9e213e7..dd848f64b 100644 --- a/crates/ui-egui/src/i18n/ja.rs +++ b/crates/ui-egui/src/i18n/ja.rs @@ -30,6 +30,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "カット"), ("Copy", "コピー"), ("Paste", "ペースト"), + ("RGB Color Space View", "RGB カラースペース表示"), + ("Lab Color Space View", "Lab カラースペース表示"), + ("Add CMYK Swatch", "CMYK スウォッチを追加"), + ("Added swatch", "スウォッチを追加しました"), ("Bulleted & Numbered Lists", "箇条書き"), ("Apply Color (,)", "カラーを適用 (,)"), ("Apply Gradient (.)", "グラデーションを適用 (.)"), diff --git a/crates/ui-egui/src/i18n/ka.rs b/crates/ui-egui/src/i18n/ka.rs index b3379c6d7..12d4b0b8a 100644 --- a/crates/ui-egui/src/i18n/ka.rs +++ b/crates/ui-egui/src/i18n/ka.rs @@ -31,6 +31,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "ამოჭრა"), ("Copy", "კოპირება"), ("Paste", "ჩასმა"), + ("RGB Color Space View", "RGB ფერთა სივრცის ხედი"), + ("Lab Color Space View", "Lab ფერთა სივრცის ხედი"), + ("Add CMYK Swatch", "CMYK ნიმუშის დამატება"), + ("Added swatch", "ნიმუში დაემატა"), ("Reverse Path", "ტრაექტორიის შებრუნება"), ("Set Anchor", "საყრდენი წერტილის დაყენება"), ("Clear", "გასუფთავება"), diff --git a/crates/ui-egui/src/i18n/pt_br.rs b/crates/ui-egui/src/i18n/pt_br.rs index 0675a2f66..76ca36985 100644 --- a/crates/ui-egui/src/i18n/pt_br.rs +++ b/crates/ui-egui/src/i18n/pt_br.rs @@ -31,6 +31,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Recortar"), ("Copy", "Copiar"), ("Paste", "Colar"), + ("RGB Color Space View", "Exibição do espaço de cores RGB"), + ("Lab Color Space View", "Exibição do espaço de cores Lab"), + ("Add CMYK Swatch", "Adicionar amostra CMYK"), + ("Added swatch", "Amostra adicionada"), ("Bulleted & Numbered Lists", "Listas com marcadores e numeradas"), ("Show Hidden Characters", "Mostrar caracteres ocultos"), ("Current Page Number", "Número da página atual"), diff --git a/crates/ui-egui/src/i18n/uk.rs b/crates/ui-egui/src/i18n/uk.rs index 8f1d27f84..32d637087 100644 --- a/crates/ui-egui/src/i18n/uk.rs +++ b/crates/ui-egui/src/i18n/uk.rs @@ -31,6 +31,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Вирізати"), ("Copy", "Копіювати"), ("Paste", "Вставити"), + ("RGB Color Space View", "Подання колірного простору RGB"), + ("Lab Color Space View", "Подання колірного простору Lab"), + ("Add CMYK Swatch", "Додати зразок CMYK"), + ("Added swatch", "Додано зразок"), ("Bulleted & Numbered Lists", "Марковані й нумеровані списки"), ("Current Page Number", "Номер поточної сторінки"), ("Apply Color (,)", "Застосувати колір (,)"),