From 3430a88cbca1d269f82f28fa03e5896fc6327352 Mon Sep 17 00:00:00 2001 From: paaspaas00 <151774837+paaspaas00@users.noreply.github.com> Date: Wed, 21 Oct 2026 18:54:54 +0200 Subject: [PATCH 01/17] Color Picker: the eyedropper takes a colour off the page The eyedropper beside the hex code works as InDesign's: pressed and dragged onto the page, it takes the colour shown under the pointer when let go (one device pixel rendered as the canvas renders it, over the paper); off the pages it takes nothing. Found in the Learn Skills Daily course, chapter 23. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/canvas.rs | 29 +++++++++++ crates/ui-egui/src/dialogs.rs | 83 +++++++++++++++++++++++++++++--- crates/ui-egui/src/i18n.rs | 10 ++++ crates/ui-egui/src/i18n/ar.rs | 1 + crates/ui-egui/src/i18n/cs.rs | 1 + crates/ui-egui/src/i18n/it.rs | 1 + crates/ui-egui/src/i18n/ka.rs | 1 + crates/ui-egui/src/i18n/pl.rs | 1 + crates/ui-egui/src/i18n/pt_br.rs | 1 + crates/ui-egui/src/i18n/ru.rs | 1 + crates/ui-egui/src/i18n/uk.rs | 1 + 11 files changed, 123 insertions(+), 7 deletions(-) diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 8d94f099f..74c5284d5 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -639,6 +639,35 @@ fn draw_cursor_badge(painter: &egui::Painter, p: Pos2, c: Cursor, t: &Tokens) { } /// Render options for the canvas texture. +/// The colour the canvas shows at screen point `pos` (the Color Picker's eyedropper): one device +/// pixel rendered as the page is, over its paper. None off the pages. +pub fn color_at(app: &mut DesignApp, ctx: &egui::Context, pos: Pos2) -> Option<[u8; 3]> { + let rect = app.canvas_rect.filter(|r| r.contains(pos))?; + let v = *app.view()?; + let xf = Xf::new(rect, &v); + let at = xf.to_canvas(pos); + let st = app.session.active()?; + let doc = st.doc.clone(); + let layout = CanvasLayout::new(&doc, st.editing_parents); + let on_page = layout + .slots + .iter() + .any(|slot| doc.spread(slot.spread).is_some_and(|sp| sp.pages.iter().any(|page| slot.xf.transform_rect_bbox(page.bounds()).contains(at)))); + if !on_page { + return None; + } + let placed: Vec = layout.slots.iter().map(|s| designcraft_render::Placed { spread: s.spread, xf: s.xf }).collect(); + let ppp = ctx.pixels_per_point() as f64; + let view = Affine::scale(ppp) * Affine::scale(xf.zoom) * Affine::translate((-at.x, -at.y)); + let opts = screen_options(app, &layout, app.ui.screen_mode != ScreenMode::Normal, ppp); + let img = app.canvas.patcher.render(&doc, &app.session.cache, &placed, 1, 1, view, &opts); + let [r, g, b, a] = <[u8; 4]>::try_from(img.pixels.get(..4)?).ok()?; + // Premultiplied over the paper. + let paper = doc.resolve_color(designcraft_color::swatch::PAPER, 1.0).unwrap_or(designcraft_color::Color::WHITE).to_rgba8(1.0); + let over = |c: u8, p: u8| (u32::from(c) + u32::from(p) * (255 - u32::from(a)) / 255).min(255) as u8; + Some([over(r, paper[0]), over(g, paper[1]), over(b, paper[2])]) +} + fn screen_options(app: &DesignApp, layout: &CanvasLayout, preview: bool, ppp: f64) -> designcraft_render::RenderOptions { designcraft_render::RenderOptions { printing_only: preview, diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 443df730e..5b3849243 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -6870,7 +6870,7 @@ fn color_picker(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { if comp_now != comp { d.fields.insert("component".into(), json!(comp_now.key())); } - // The hex code, and the eyedropper's mark beside it. + // The hex code, and the eyedropper beside it. let cy = area.min.y + at::ROWS[7]; text(ui, egui::pos2(area.min.x + 321.0, cy), egui::Align2::LEFT_CENTER, "#:"); let hr = egui::Rect::from_min_size(egui::pos2(area.min.x + 341.0, cy - 10.5), egui::vec2(64.0, 21.0)); @@ -6886,12 +6886,26 @@ fn color_picker(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { p = Picked::from_rgb(c.to_rgb_uncalibrated(), p.hsb[0]); } d.fields.insert("hexEdit".into(), json!(h)); - crate::icons::paint( - ui.painter(), - egui::Rect::from_center_size(egui::pos2(hr.right() + 16.0, cy), egui::vec2(15.0, 15.0)), - "tool-eyedropper", - t.icon, - ); + // The eyedropper, as InDesign's: dragged onto the page, it takes the colour shown under the + // pointer when it is let go. + let tip = tr("Eyedropper: drag it onto the page"); + let er = egui::Rect::from_center_size(egui::pos2(hr.right() + 16.0, cy), egui::vec2(21.0, 21.0)); + let resp = ui.interact(er, egui::Id::new("color_picker_eyedropper"), egui::Sense::click_and_drag()); + resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, &tip)); + if resp.hovered() || resp.dragged() { + ui.painter().rect_filled(er, 3.0, t.hover); + } + crate::icons::paint(ui.painter(), egui::Rect::from_center_size(er.center(), egui::vec2(15.0, 15.0)), "tool-eyedropper", t.icon); + if resp.dragged() { + ui.ctx().set_cursor_icon(egui::CursorIcon::Crosshair); + } + if resp.drag_stopped() + && let Some(pos) = ui.input(|i| i.pointer.latest_pos()) + && let Some(c) = crate::canvas::color_at(app, ui.ctx(), pos) + { + p = Picked::from_rgb(c.map(|v| f32::from(v) / 255.0), p.hsb[0]); + } + resp.on_hover_text(tip); if p != before { store_picked(d, &p); if let Some(c) = exact_cmyk { @@ -10017,8 +10031,63 @@ mod tests { /// 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. + /// The course's chapter 23: the Color Picker's eyedropper, dragged from the picker onto the + /// page and let go there, takes the colour the page shows. #[test] + fn the_color_pickers_eyedropper_takes_a_colour_off_the_page() { + use egui_kittest::{Harness, kittest::Queryable}; + let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); + app.run("file.new", json!({})).unwrap(); + let id = app.run("frame.create", json!({"rect": [36, 36, 560, 700], "content": "unassigned"})).unwrap()["id"].as_u64().unwrap(); + app.run("object.fill", json!({"ids": [id], "swatch": "C=0 M=100 Y=100 K=0"})).unwrap(); + app.ui.dialog = Some(Dialog::new("colorPicker", json!({"target": "fill", "hex": "#000000"}))); + let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_ui_state( + |ui, app: &mut DesignApp| { + let ctx = ui.ctx().clone(); + app.logic(&ctx); + app.ui(ui); + }, + app, + ); + h.run_steps(6); + // Where the frame shows on screen. + let target = { + let app = h.state(); + let doc = &app.session.doc().unwrap().doc; + let layout = designcraft_tools::CanvasLayout::new(doc, false); + let v = *app.view().unwrap(); + crate::canvas::Xf::new(app.canvas_rect.unwrap(), &v).to_screen(layout.slots[0].xf * designcraft_geom::Point::new(300.0, 600.0)) + }; + let from = h.get_by_label("Eyedropper: drag it onto the page").rect().center(); + h.hover_at(from); + h.drag_at(from); + h.run_steps(1); + for k in 1..=4 { + h.hover_at(from + (target - from) * (k as f32 / 4.0)); + h.run_steps(1); + } + h.drop_at(target); + h.run_steps(3); + let d = h.state().ui.dialog.as_ref().expect("the picker stays open"); + let [r, g, b, _] = h.state().session.doc().unwrap().doc.resolve_color("C=0 M=100 Y=100 K=0", 1.0).unwrap().to_rgba8(1.0); + let got: Vec = d.fields["rgb"].as_array().unwrap().iter().map(|v| v.as_f64().unwrap()).collect(); + for (a, e) in got.iter().zip([r, g, b]) { + assert!((a - f64::from(e)).abs() <= 3.0, "{got:?} vs {:?}", [r, g, b]); + } + assert!(d.b("edited")); + // Let go off the page (here, on the dialog's own field area): no colour, nothing changes. + let hex = d.s("hex"); + h.hover_at(from); + h.drag_at(from); + h.run_steps(1); + h.hover_at(from + egui::vec2(-40.0, 0.0)); + h.run_steps(1); + h.drop_at(egui::pos2(2.0, 2.0)); + h.run_steps(3); + assert_eq!(h.state().ui.dialog.as_ref().unwrap().s("hex"), hex); + } + #[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()); diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index 6e9a0b40b..0443399cd 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -848,6 +848,16 @@ const TABLE: &[(&str, [&str; 5])] = &[ ("Choose Image…", ["Choose Image…", "Choisir une image…", "Choose Image…", "Choose Image…", "选择图像…"]), ("Class:", ["Class:", "Classe :", "Class:", "Class:", "类别:"]), ("Color Picker", ["Color Picker", "Sélecteur de couleurs", "Color Picker", "Color Picker", "拾色器"]), + ( + "Eyedropper: drag it onto the page", + [ + "Pipette: auf die Seite ziehen", + "Pipette : faites-la glisser sur la page", + "Cuentagotas: arrástrelo a la página", + "スポイト : ページにドラッグ", + "吸管:将其拖到页面上", + ], + ), ("Color Settings", ["Color Settings", "Réglages des couleurs", "Color Settings", "Color Settings", "颜色设置"]), ("Color:", ["Color:", "Couleur :", "Color:", "Color:", "颜色:"]), ("Columns", ["Columns", "Colonnes", "Columns", "Columns", "栏"]), diff --git a/crates/ui-egui/src/i18n/ar.rs b/crates/ui-egui/src/i18n/ar.rs index b050de38b..b74df5167 100644 --- a/crates/ui-egui/src/i18n/ar.rs +++ b/crates/ui-egui/src/i18n/ar.rs @@ -30,6 +30,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "قص"), ("Copy", "نسخ"), ("Paste", "لصق"), + ("Eyedropper: drag it onto the page", "القطارة: اسحبها إلى الصفحة"), ("Paste Vector Art", "لصق رسم متجه"), ("Baseline Options", "خيارات خط الأساس"), ("Fixed Number", "عدد ثابت"), diff --git a/crates/ui-egui/src/i18n/cs.rs b/crates/ui-egui/src/i18n/cs.rs index e77e72a1c..e16f71a0b 100644 --- a/crates/ui-egui/src/i18n/cs.rs +++ b/crates/ui-egui/src/i18n/cs.rs @@ -31,6 +31,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Vyjmout"), ("Copy", "Kopírovat"), ("Paste", "Vložit"), + ("Eyedropper: drag it onto the page", "Kapátko: přetáhněte ho na stránku"), ("Reverse Path", "Obrátit cestu"), ("Set Anchor", "Nastavit kotvu"), ("Clear", "Vymazat"), diff --git a/crates/ui-egui/src/i18n/it.rs b/crates/ui-egui/src/i18n/it.rs index a918a9829..59caf152a 100644 --- a/crates/ui-egui/src/i18n/it.rs +++ b/crates/ui-egui/src/i18n/it.rs @@ -28,6 +28,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Taglia"), ("Copy", "Copia"), ("Paste", "Incolla"), + ("Eyedropper: drag it onto the page", "Contagocce: trascinalo sulla pagina"), ("Paste Vector Art", "Incolla grafica vettoriale"), ("Baseline Options", "Opzioni linea di base"), ("Fixed Number", "Numero fisso"), diff --git a/crates/ui-egui/src/i18n/ka.rs b/crates/ui-egui/src/i18n/ka.rs index d938c2dc7..99c455519 100644 --- a/crates/ui-egui/src/i18n/ka.rs +++ b/crates/ui-egui/src/i18n/ka.rs @@ -31,6 +31,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "ამოჭრა"), ("Copy", "კოპირება"), ("Paste", "ჩასმა"), + ("Eyedropper: drag it onto the page", "პიპეტი: გადაიტანეთ გვერდზე"), ("Inches Decimal", "ინჩები (ათობითი)"), ("Back", "უკან"), ("Warning", "გაფრთხილება"), diff --git a/crates/ui-egui/src/i18n/pl.rs b/crates/ui-egui/src/i18n/pl.rs index 68d685d99..ec533dbb0 100644 --- a/crates/ui-egui/src/i18n/pl.rs +++ b/crates/ui-egui/src/i18n/pl.rs @@ -29,6 +29,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Wytnij"), ("Copy", "Kopiuj"), ("Paste", "Wklej"), + ("Eyedropper: drag it onto the page", "Kroplomierz: przeciągnij go na stronę"), ("Clear", "Wyczyść"), ("Duplicate", "Powiel"), ("Select All", "Zaznacz wszystko"), diff --git a/crates/ui-egui/src/i18n/pt_br.rs b/crates/ui-egui/src/i18n/pt_br.rs index 8521de594..f8ed98d04 100644 --- a/crates/ui-egui/src/i18n/pt_br.rs +++ b/crates/ui-egui/src/i18n/pt_br.rs @@ -31,6 +31,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Recortar"), ("Copy", "Copiar"), ("Paste", "Colar"), + ("Eyedropper: drag it onto the page", "Conta-gotas: arraste-o para a página"), ("Paste Vector Art", "Colar arte vetorial"), ("Baseline Options", "Opções de linha de base"), ("Fixed Number", "Número fixo"), diff --git a/crates/ui-egui/src/i18n/ru.rs b/crates/ui-egui/src/i18n/ru.rs index 251555ca5..13b74df76 100644 --- a/crates/ui-egui/src/i18n/ru.rs +++ b/crates/ui-egui/src/i18n/ru.rs @@ -33,6 +33,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Вырезать"), ("Copy", "Копировать"), ("Paste", "Вставить"), + ("Eyedropper: drag it onto the page", "Пипетка: перетащите её на страницу"), ("Clear", "Очистить"), ("Duplicate", "Дублировать"), ("Select All", "Выделить все"), diff --git a/crates/ui-egui/src/i18n/uk.rs b/crates/ui-egui/src/i18n/uk.rs index dd4a1434d..a5652335c 100644 --- a/crates/ui-egui/src/i18n/uk.rs +++ b/crates/ui-egui/src/i18n/uk.rs @@ -29,6 +29,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Вирізати"), ("Copy", "Копіювати"), ("Paste", "Вставити"), + ("Eyedropper: drag it onto the page", "Піпетка: перетягніть її на сторінку"), ("Paste Vector Art", "Вставити векторну графіку"), ("Baseline Options", "Параметри базової лінії"), ("Fixed Number", "Фіксована кількість"), From 22bb814c9d80bbed0131aaa72fbce2cb7fc5bfcd Mon Sep 17 00:00:00 2001 From: moppeeer Date: Thu, 22 Oct 2026 00:30:40 +0200 Subject: [PATCH 02/17] Eyedropper: sample the colour as the canvas shows it; picker strings in every catalog - `screen_post` holds what the canvas does to a render before showing it (Proof Colors, the composite ink-limit preview). The texture and the eyedropper's one-pixel sample both go through it, so with either view on, the eyedropper takes the colour on screen. - The picker's OK uses the footer's padded " OK " key, which every catalog has (Chinese showed "OK"). - "RGB Color Space View", "Lab Color Space View", "Add CMYK Swatch" and "Added swatch" are in the ar, cs, it, ka, pl, pt-br, ru and uk catalogs. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/canvas.rs | 16 +++++++--- crates/ui-egui/src/dialogs.rs | 54 +++++++++++++++++++++++++++++++- crates/ui-egui/src/i18n/ar.rs | 4 +++ crates/ui-egui/src/i18n/cs.rs | 4 +++ crates/ui-egui/src/i18n/it.rs | 4 +++ crates/ui-egui/src/i18n/ka.rs | 4 +++ crates/ui-egui/src/i18n/pl.rs | 4 +++ crates/ui-egui/src/i18n/pt_br.rs | 4 +++ crates/ui-egui/src/i18n/ru.rs | 4 +++ crates/ui-egui/src/i18n/uk.rs | 4 +++ 10 files changed, 97 insertions(+), 5 deletions(-) diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 74c5284d5..7f96aac17 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -660,7 +660,9 @@ pub fn color_at(app: &mut DesignApp, ctx: &egui::Context, pos: Pos2) -> Option<[ let ppp = ctx.pixels_per_point() as f64; let view = Affine::scale(ppp) * Affine::scale(xf.zoom) * Affine::translate((-at.x, -at.y)); let opts = screen_options(app, &layout, app.ui.screen_mode != ScreenMode::Normal, ppp); - let img = app.canvas.patcher.render(&doc, &app.session.cache, &placed, 1, 1, view, &opts); + let mut img = app.canvas.patcher.render(&doc, &app.session.cache, &placed, 1, 1, view, &opts); + // The colour as the canvas shows it: proofed, or marked over the ink limit. + screen_post(app, &mut img); let [r, g, b, a] = <[u8; 4]>::try_from(img.pixels.get(..4)?).ok()?; // Premultiplied over the paper. let paper = doc.resolve_color(designcraft_color::swatch::PAPER, 1.0).unwrap_or(designcraft_color::Color::WHITE).to_rgba8(1.0); @@ -934,14 +936,20 @@ fn render_worker_spawn(ctx: &egui::Context) -> Option| crate::canvas::color_at(h.state_mut(), &ctx, target).expect("on the page"); + // What a view makes of the plain colour, as the canvas texture gets it. + let shown = |c: [u8; 3], view: &dyn Fn(&mut designcraft_render::Rendered)| { + let mut img = designcraft_render::Rendered { width: 1, height: 1, pixels: vec![c[0], c[1], c[2], 255] }; + view(&mut img); + [img.pixels[0], img.pixels[1], img.pixels[2]] + }; + let plain = sample(&mut h); + h.state_mut().ui.proof_colors = true; + let setup = h.state().ui.proof_setup.clone(); + let proofed = sample(&mut h); + assert_eq!(proofed, shown(plain, &|img| designcraft_render::proof_view(img, &setup))); + assert_ne!(proofed, plain, "the proof changes this green"); + // The composite ink-limit preview: a blue over a 100% limit shows (and samples) the warning. + h.state_mut().ui.proof_colors = false; + h.state_mut().run("object.fill", json!({"ids": [id], "swatch": "C=100 M=90 Y=10 K=0"})).unwrap(); + h.run_steps(3); + let blue = sample(&mut h); + h.state_mut().ui.ink_limit = Some(1.0); + let limited = sample(&mut h); + assert_eq!(limited, shown(blue, &|img| designcraft_render::separation_view(img, None, Some(1.0)))); + assert_eq!(limited, [230, 40, 40]); + } + #[test] fn the_color_pickers_eyedropper_takes_a_colour_off_the_page() { use egui_kittest::{Harness, kittest::Queryable}; diff --git a/crates/ui-egui/src/i18n/ar.rs b/crates/ui-egui/src/i18n/ar.rs index b74df5167..8ff1269d4 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", "تمت إضافة عينة اللون"), ("Eyedropper: drag it onto the page", "القطارة: اسحبها إلى الصفحة"), ("Paste Vector Art", "لصق رسم متجه"), ("Baseline Options", "خيارات خط الأساس"), diff --git a/crates/ui-egui/src/i18n/cs.rs b/crates/ui-egui/src/i18n/cs.rs index e16f71a0b..f81957415 100644 --- a/crates/ui-egui/src/i18n/cs.rs +++ b/crates/ui-egui/src/i18n/cs.rs @@ -31,6 +31,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Vyjmout"), ("Copy", "Kopírovat"), ("Paste", "Vložit"), + ("RGB Color Space View", "Zobrazení barevného prostoru RGB"), + ("Lab Color Space View", "Zobrazení barevného prostoru Lab"), + ("Add CMYK Swatch", "Přidat vzorek CMYK"), + ("Added swatch", "Vzorek přidán"), ("Eyedropper: drag it onto the page", "Kapátko: přetáhněte ho na stránku"), ("Reverse Path", "Obrátit cestu"), ("Set Anchor", "Nastavit kotvu"), diff --git a/crates/ui-egui/src/i18n/it.rs b/crates/ui-egui/src/i18n/it.rs index 59caf152a..59c44df6e 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"), ("Eyedropper: drag it onto the page", "Contagocce: trascinalo sulla pagina"), ("Paste Vector Art", "Incolla grafica vettoriale"), ("Baseline Options", "Opzioni linea di base"), diff --git a/crates/ui-egui/src/i18n/ka.rs b/crates/ui-egui/src/i18n/ka.rs index 99c455519..99e6e8e54 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", "ფერის ნიმუში დამატებულია"), ("Eyedropper: drag it onto the page", "პიპეტი: გადაიტანეთ გვერდზე"), ("Inches Decimal", "ინჩები (ათობითი)"), ("Back", "უკან"), diff --git a/crates/ui-egui/src/i18n/pl.rs b/crates/ui-egui/src/i18n/pl.rs index ec533dbb0..9546497f4 100644 --- a/crates/ui-egui/src/i18n/pl.rs +++ b/crates/ui-egui/src/i18n/pl.rs @@ -29,6 +29,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Wytnij"), ("Copy", "Kopiuj"), ("Paste", "Wklej"), + ("RGB Color Space View", "Widok przestrzeni barw RGB"), + ("Lab Color Space View", "Widok przestrzeni barw Lab"), + ("Add CMYK Swatch", "Dodaj próbkę CMYK"), + ("Added swatch", "Dodano próbkę"), ("Eyedropper: drag it onto the page", "Kroplomierz: przeciągnij go na stronę"), ("Clear", "Wyczyść"), ("Duplicate", "Powiel"), diff --git a/crates/ui-egui/src/i18n/pt_br.rs b/crates/ui-egui/src/i18n/pt_br.rs index f8ed98d04..2f861e53b 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"), ("Eyedropper: drag it onto the page", "Conta-gotas: arraste-o para a página"), ("Paste Vector Art", "Colar arte vetorial"), ("Baseline Options", "Opções de linha de base"), diff --git a/crates/ui-egui/src/i18n/ru.rs b/crates/ui-egui/src/i18n/ru.rs index 13b74df76..25de858ef 100644 --- a/crates/ui-egui/src/i18n/ru.rs +++ b/crates/ui-egui/src/i18n/ru.rs @@ -33,6 +33,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", "Образец добавлен"), ("Eyedropper: drag it onto the page", "Пипетка: перетащите её на страницу"), ("Clear", "Очистить"), ("Duplicate", "Дублировать"), diff --git a/crates/ui-egui/src/i18n/uk.rs b/crates/ui-egui/src/i18n/uk.rs index a5652335c..562d527a6 100644 --- a/crates/ui-egui/src/i18n/uk.rs +++ b/crates/ui-egui/src/i18n/uk.rs @@ -29,6 +29,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", "Зразок додано"), ("Eyedropper: drag it onto the page", "Піпетка: перетягніть її на сторінку"), ("Paste Vector Art", "Вставити векторну графіку"), ("Baseline Options", "Параметри базової лінії"), From 2de763bb522ce9e2b589a34c29571ffc5c52da57 Mon Sep 17 00:00:00 2001 From: moppeeer Date: Sun, 25 Oct 2026 17:14:51 +0100 Subject: [PATCH 03/17] Color Picker eyedropper: let go over the picker, it takes nothing The picker has no backdrop, so a page can lie under it; color_at only checks the canvas. The drop now samples only when the picker's layer isn't the topmost one there, and the test drops onto its colour field. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/dialogs.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 0f95512a0..ad4de5b9c 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -7115,8 +7115,10 @@ fn color_picker(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { if resp.dragged() { ui.ctx().set_cursor_icon(egui::CursorIcon::Crosshair); } + // Let go over the picker itself it takes nothing, though a page may lie under it. if resp.drag_stopped() && let Some(pos) = ui.input(|i| i.pointer.latest_pos()) + && ui.ctx().layer_id_at(pos) != Some(ui.layer_id()) && let Some(c) = crate::canvas::color_at(app, ui.ctx(), pos) { p = Picked::from_rgb(c.map(|v| f32::from(v) / 255.0), p.hsb[0]); @@ -10523,6 +10525,18 @@ mod tests { h.drop_at(egui::pos2(2.0, 2.0)); h.run_steps(3); assert_eq!(h.state().ui.dialog.as_ref().unwrap().s("hex"), hex); + // Let go over the picker's own colour field, with the page under it: nothing either. + let field = h.get_by_label("Color field").rect().center(); + let ctx = h.ctx.clone(); + assert!(crate::canvas::color_at(h.state_mut(), &ctx, field).is_some(), "the page lies under the field"); + h.hover_at(from); + h.drag_at(from); + h.run_steps(1); + h.hover_at(field); + h.run_steps(1); + h.drop_at(field); + h.run_steps(3); + assert_eq!(h.state().ui.dialog.as_ref().unwrap().s("hex"), hex, "the picker isn't sampled"); } #[test] From 9114b51de258926b6806de348496a6c4e28a5a31 Mon Sep 17 00:00:00 2001 From: moppeeer Date: Thu, 29 Oct 2026 12:13:28 +0100 Subject: [PATCH 04/17] Eyedropper test: the ink limit is the Separations view's on main, not the canvas texture's Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/dialogs/tests.rs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/crates/ui-egui/src/dialogs/tests.rs b/crates/ui-egui/src/dialogs/tests.rs index 71cc94a4a..165727cf8 100644 --- a/crates/ui-egui/src/dialogs/tests.rs +++ b/crates/ui-egui/src/dialogs/tests.rs @@ -3506,15 +3506,6 @@ fn guide_counts(app: &DesignApp) -> Vec { let proofed = sample(&mut h); assert_eq!(proofed, shown(plain, &|img| designcraft_render::proof_view(img, &setup))); assert_ne!(proofed, plain, "the proof changes this green"); - // The composite ink-limit preview: a blue over a 100% limit shows (and samples) the warning. - h.state_mut().ui.proof_colors = false; - h.state_mut().run("object.fill", json!({"ids": [id], "swatch": "C=100 M=90 Y=10 K=0"})).unwrap(); - h.run_steps(3); - let blue = sample(&mut h); - h.state_mut().ui.ink_limit = Some(1.0); - let limited = sample(&mut h); - assert_eq!(limited, shown(blue, &|img| designcraft_render::separation_view(img, None, Some(1.0)))); - assert_eq!(limited, [230, 40, 40]); } #[test] From 329bff534fed36bf4cf1f3e4df0ea302818c6f46 Mon Sep 17 00:00:00 2001 From: moppeeer Date: Thu, 29 Oct 2026 18:47:05 +0100 Subject: [PATCH 05/17] Eyedropper: the canvas colour sampler re-exported from main's split canvas render module Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/canvas.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index de4331c7d..c29cbdfd9 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -44,7 +44,7 @@ use input::{press_count, release_clicks}; #[cfg(test)] use page_decor::{MAX_BASELINES, baseline_grid_lines}; use page_decor::{dashed, draw_flattener_preview, draw_guides, shown_area}; -pub use render::DisplayKey; +pub use render::{DisplayKey, color_at}; use render::render_texture; #[cfg(test)] use render::{c32, invalidate_on_display_change, screen_options}; From 54190bb88612ce19e6a245c63504e5367aa6c8af Mon Sep 17 00:00:00 2001 From: moppeeer Date: Thu, 29 Oct 2026 21:44:29 +0100 Subject: [PATCH 06/17] Color Picker: the remembered colours seeded with map entries Keeps color_picker within clippy's cognitive complexity limit. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/dialogs.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index d345f1658..e13340a25 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -7225,16 +7225,12 @@ fn color_picker(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { use picker_layout as at; 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 old = d.s("hex"); + d.fields.entry("old").or_insert_with(|| json!(old)); let comp = PickComp::from_key(&d.s("component")); let mut p = picked_of(d); let before = p; - if !d.fields.contains_key("rgb") { - d.fields.insert("rgb".into(), json!(p.rgb.map(|x| x * 255.0))); - } + d.fields.entry("rgb").or_insert_with(|| json!(p.rgb.map(|x| x * 255.0))); if !d.fields.contains_key("cmyk") { d.fields.insert( "cmyk".into(), From 0591765d36e828a5e845adcab10f9366ae2b9d8f Mon Sep 17 00:00:00 2001 From: moppeeer Date: Thu, 29 Oct 2026 22:39:54 +0100 Subject: [PATCH 07/17] rustfmt Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/canvas.rs | 2 +- crates/ui-egui/src/dialogs/tests.rs | 374 ++++++++++++++-------------- 2 files changed, 188 insertions(+), 188 deletions(-) diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index c29cbdfd9..a88f2d180 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -44,8 +44,8 @@ use input::{press_count, release_clicks}; #[cfg(test)] use page_decor::{MAX_BASELINES, baseline_grid_lines}; use page_decor::{dashed, draw_flattener_preview, draw_guides, shown_area}; -pub use render::{DisplayKey, color_at}; use render::render_texture; +pub use render::{DisplayKey, color_at}; #[cfg(test)] use render::{c32, invalidate_on_display_change, screen_options}; pub(crate) use rulers::fmt_tick; diff --git a/crates/ui-egui/src/dialogs/tests.rs b/crates/ui-egui/src/dialogs/tests.rs index 165727cf8..a35377864 100644 --- a/crates/ui-egui/src/dialogs/tests.rs +++ b/crates/ui-egui/src/dialogs/tests.rs @@ -592,7 +592,7 @@ fn the_hex_field_keeps_typed_text_and_commits_a_valid_code() { dialog_frame(&mut app, &ctx, screen, vec![select_all_key()]); type_slowly(&mut app, &ctx, screen, "zz"); let labels = dialog_frame(&mut app, &ctx, screen, vec![]); - click_dialog(&mut app, &ctx, screen, visible_label(&labels, screen, "#:").center()); + click_dialog(&mut app, &ctx, screen, visible_label(&labels, screen, "#:").center()); let labels = dialog_frame(&mut app, &ctx, screen, vec![]); visible_label(&labels, screen, "f59019"); assert_eq!(app.ui.dialog.as_ref().unwrap().s("hex"), "#f59019"); @@ -602,13 +602,13 @@ fn the_hex_field_keeps_typed_text_and_commits_a_valid_code() { fn typed_channel_values_are_kept_while_typing() { let (mut app, ctx, screen, id) = picker(json!({"target": "fill", "hex": "#010203", "model": "rgb", "rgb": [1, 2, 3]})); let labels = dialog_frame(&mut app, &ctx, screen, vec![]); - let r = visible_label(&labels, screen, "R:"); - click_dialog(&mut app, &ctx, screen, egui::pos2(r.min.x + 46.0, r.center().y)); + let r = visible_label(&labels, screen, "R:"); + click_dialog(&mut app, &ctx, screen, egui::pos2(r.min.x + 46.0, r.center().y)); dialog_frame(&mut app, &ctx, screen, vec![select_all_key()]); type_slowly(&mut app, &ctx, screen, "12.5"); let labels = dialog_frame(&mut app, &ctx, screen, vec![]); - visible_label(&labels, screen, "12.5"); - assert_eq!(app.ui.dialog.as_ref().unwrap().fields["rgb"][1], json!(2.0)); + visible_label(&labels, screen, "12.5"); + assert_eq!(app.ui.dialog.as_ref().unwrap().fields["rgb"][1], json!(2.0)); assert_eq!(app.ui.dialog.as_ref().unwrap().s("hex"), "#0d0203"); confirm(&mut app).unwrap(); let designcraft_color::Color::Rgb { r, .. } = fill(&app, id) else { panic!("not RGB") }; @@ -630,8 +630,8 @@ fn a_cmyk_colour_stays_cmyk_unless_rgb_values_are_typed() { // A typed C/M/Y/K percent is a percent: M 1 is 1 %. let (mut app, ctx, screen, id) = picker(cmyk.clone()); let labels = dialog_frame(&mut app, &ctx, screen, vec![]); - let r = visible_label(&labels, screen, "M:"); - click_dialog(&mut app, &ctx, screen, egui::pos2(r.max.x + 29.0, r.center().y)); + let r = visible_label(&labels, screen, "M:"); + click_dialog(&mut app, &ctx, screen, egui::pos2(r.max.x + 29.0, r.center().y)); dialog_frame(&mut app, &ctx, screen, vec![select_all_key()]); type_slowly(&mut app, &ctx, screen, "1"); confirm(&mut app).unwrap(); @@ -641,9 +641,9 @@ fn a_cmyk_colour_stays_cmyk_unless_rgb_values_are_typed() { let (mut app, ctx, screen, id) = picker(cmyk); let r = app.ui.dialog.as_ref().unwrap().fields["rgb"][0].as_f64().unwrap(); let labels = dialog_frame(&mut app, &ctx, screen, vec![]); - let _ = r; - let r = visible_label(&labels, screen, "R:"); - click_dialog(&mut app, &ctx, screen, egui::pos2(r.min.x + 46.0, r.center().y)); + let _ = r; + let r = visible_label(&labels, screen, "R:"); + click_dialog(&mut app, &ctx, screen, egui::pos2(r.min.x + 46.0, r.center().y)); dialog_frame(&mut app, &ctx, screen, vec![select_all_key()]); type_slowly(&mut app, &ctx, screen, "10"); confirm(&mut app).unwrap(); @@ -1644,7 +1644,7 @@ fn dialog_frame(app: &mut DesignApp, ctx: &egui::Context, screen: egui::Rect, ev fn visible_label(labels: &[(String, egui::Rect, egui::Rect)], screen: egui::Rect, expected: &str) -> egui::Rect { let (_, rect, clip) = labels.iter().find(|(text, _, _)| text.trim() == expected).unwrap_or_else(|| panic!("{expected} is painted")); assert!(screen.contains_rect(*rect), "{expected} must stay on screen: {rect:?}"); - assert!(clip.expand(0.5).contains_rect(*rect), "{expected} must not be clipped: {rect:?}, {clip:?}"); + assert!(clip.expand(0.5).contains_rect(*rect), "{expected} must not be clipped: {rect:?}, {clip:?}"); *rect } @@ -3462,188 +3462,188 @@ fn guide_counts(app: &DesignApp) -> Vec { (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. - /// The course's chapter 23: the Color Picker's eyedropper, dragged from the picker onto the - /// page and let go there, takes the colour the page shows. - /// The eyedropper takes the colour the canvas shows: with Proof Colors the proofed colour, and - /// with the ink-limit preview the warning colour over the limit. - #[test] - fn the_eyedropper_samples_what_proof_colors_and_the_ink_limit_show() { - use egui_kittest::Harness; - let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); - app.run("file.new", json!({})).unwrap(); - app.run("swatch.create", json!({"name": "Vivid Green", "color": [0, 255, 0]})).unwrap(); - let id = app.run("frame.create", json!({"rect": [36, 36, 560, 700], "content": "unassigned"})).unwrap()["id"].as_u64().unwrap(); - app.run("object.fill", json!({"ids": [id], "swatch": "Vivid Green"})).unwrap(); - let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_ui_state( - |ui, app: &mut DesignApp| { - let ctx = ui.ctx().clone(); - app.logic(&ctx); - app.ui(ui); - }, - app, - ); - h.run_steps(6); - let target = { - let app = h.state(); - let doc = &app.session.doc().unwrap().doc; - let layout = designcraft_tools::CanvasLayout::new(doc, false); - let v = *app.view().unwrap(); - crate::canvas::Xf::new(app.canvas_rect.unwrap(), &v).to_screen(layout.slots[0].xf * designcraft_geom::Point::new(300.0, 600.0)) - }; - let ctx = h.ctx.clone(); - let sample = |h: &mut Harness<'_, DesignApp>| crate::canvas::color_at(h.state_mut(), &ctx, target).expect("on the page"); - // What a view makes of the plain colour, as the canvas texture gets it. - let shown = |c: [u8; 3], view: &dyn Fn(&mut designcraft_render::Rendered)| { - let mut img = designcraft_render::Rendered { width: 1, height: 1, pixels: vec![c[0], c[1], c[2], 255] }; - view(&mut img); - [img.pixels[0], img.pixels[1], img.pixels[2]] - }; - let plain = sample(&mut h); - h.state_mut().ui.proof_colors = true; - let setup = h.state().ui.proof_setup.clone(); - let proofed = sample(&mut h); - assert_eq!(proofed, shown(plain, &|img| designcraft_render::proof_view(img, &setup))); - assert_ne!(proofed, plain, "the proof changes this green"); - } +/// 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. +/// The course's chapter 23: the Color Picker's eyedropper, dragged from the picker onto the +/// page and let go there, takes the colour the page shows. +/// The eyedropper takes the colour the canvas shows: with Proof Colors the proofed colour, and +/// with the ink-limit preview the warning colour over the limit. +#[test] +fn the_eyedropper_samples_what_proof_colors_and_the_ink_limit_show() { + use egui_kittest::Harness; + let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); + app.run("file.new", json!({})).unwrap(); + app.run("swatch.create", json!({"name": "Vivid Green", "color": [0, 255, 0]})).unwrap(); + let id = app.run("frame.create", json!({"rect": [36, 36, 560, 700], "content": "unassigned"})).unwrap()["id"].as_u64().unwrap(); + app.run("object.fill", json!({"ids": [id], "swatch": "Vivid Green"})).unwrap(); + let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_ui_state( + |ui, app: &mut DesignApp| { + let ctx = ui.ctx().clone(); + app.logic(&ctx); + app.ui(ui); + }, + app, + ); + h.run_steps(6); + let target = { + let app = h.state(); + let doc = &app.session.doc().unwrap().doc; + let layout = designcraft_tools::CanvasLayout::new(doc, false); + let v = *app.view().unwrap(); + crate::canvas::Xf::new(app.canvas_rect.unwrap(), &v).to_screen(layout.slots[0].xf * designcraft_geom::Point::new(300.0, 600.0)) + }; + let ctx = h.ctx.clone(); + let sample = |h: &mut Harness<'_, DesignApp>| crate::canvas::color_at(h.state_mut(), &ctx, target).expect("on the page"); + // What a view makes of the plain colour, as the canvas texture gets it. + let shown = |c: [u8; 3], view: &dyn Fn(&mut designcraft_render::Rendered)| { + let mut img = designcraft_render::Rendered { width: 1, height: 1, pixels: vec![c[0], c[1], c[2], 255] }; + view(&mut img); + [img.pixels[0], img.pixels[1], img.pixels[2]] + }; + let plain = sample(&mut h); + h.state_mut().ui.proof_colors = true; + let setup = h.state().ui.proof_setup.clone(); + let proofed = sample(&mut h); + assert_eq!(proofed, shown(plain, &|img| designcraft_render::proof_view(img, &setup))); + assert_ne!(proofed, plain, "the proof changes this green"); +} #[test] - fn the_color_pickers_eyedropper_takes_a_colour_off_the_page() { - use egui_kittest::{Harness, kittest::Queryable}; - let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); - app.run("file.new", json!({})).unwrap(); - let id = app.run("frame.create", json!({"rect": [36, 36, 560, 700], "content": "unassigned"})).unwrap()["id"].as_u64().unwrap(); - app.run("object.fill", json!({"ids": [id], "swatch": "C=0 M=100 Y=100 K=0"})).unwrap(); - app.ui.dialog = Some(Dialog::new("colorPicker", json!({"target": "fill", "hex": "#000000"}))); - let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_ui_state( - |ui, app: &mut DesignApp| { - let ctx = ui.ctx().clone(); - app.logic(&ctx); - app.ui(ui); - }, - app, - ); - h.run_steps(6); - // Where the frame shows on screen. - let target = { - let app = h.state(); - let doc = &app.session.doc().unwrap().doc; - let layout = designcraft_tools::CanvasLayout::new(doc, false); - let v = *app.view().unwrap(); - crate::canvas::Xf::new(app.canvas_rect.unwrap(), &v).to_screen(layout.slots[0].xf * designcraft_geom::Point::new(300.0, 600.0)) - }; - let from = h.get_by_label("Eyedropper: drag it onto the page").rect().center(); - h.hover_at(from); - h.drag_at(from); +fn the_color_pickers_eyedropper_takes_a_colour_off_the_page() { + use egui_kittest::{Harness, kittest::Queryable}; + let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); + app.run("file.new", json!({})).unwrap(); + let id = app.run("frame.create", json!({"rect": [36, 36, 560, 700], "content": "unassigned"})).unwrap()["id"].as_u64().unwrap(); + app.run("object.fill", json!({"ids": [id], "swatch": "C=0 M=100 Y=100 K=0"})).unwrap(); + app.ui.dialog = Some(Dialog::new("colorPicker", json!({"target": "fill", "hex": "#000000"}))); + let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_ui_state( + |ui, app: &mut DesignApp| { + let ctx = ui.ctx().clone(); + app.logic(&ctx); + app.ui(ui); + }, + app, + ); + h.run_steps(6); + // Where the frame shows on screen. + let target = { + let app = h.state(); + let doc = &app.session.doc().unwrap().doc; + let layout = designcraft_tools::CanvasLayout::new(doc, false); + let v = *app.view().unwrap(); + crate::canvas::Xf::new(app.canvas_rect.unwrap(), &v).to_screen(layout.slots[0].xf * designcraft_geom::Point::new(300.0, 600.0)) + }; + let from = h.get_by_label("Eyedropper: drag it onto the page").rect().center(); + h.hover_at(from); + h.drag_at(from); + h.run_steps(1); + for k in 1..=4 { + h.hover_at(from + (target - from) * (k as f32 / 4.0)); h.run_steps(1); - for k in 1..=4 { - h.hover_at(from + (target - from) * (k as f32 / 4.0)); - h.run_steps(1); - } - h.drop_at(target); - h.run_steps(3); - let d = h.state().ui.dialog.as_ref().expect("the picker stays open"); - let [r, g, b, _] = h.state().session.doc().unwrap().doc.resolve_color("C=0 M=100 Y=100 K=0", 1.0).unwrap().to_rgba8(1.0); - let got: Vec = d.fields["rgb"].as_array().unwrap().iter().map(|v| v.as_f64().unwrap()).collect(); - for (a, e) in got.iter().zip([r, g, b]) { - assert!((a - f64::from(e)).abs() <= 3.0, "{got:?} vs {:?}", [r, g, b]); - } - assert!(d.b("edited")); - // Let go off the page (here, on the dialog's own field area): no colour, nothing changes. - let hex = d.s("hex"); - h.hover_at(from); - h.drag_at(from); - h.run_steps(1); - h.hover_at(from + egui::vec2(-40.0, 0.0)); - h.run_steps(1); - h.drop_at(egui::pos2(2.0, 2.0)); - h.run_steps(3); - assert_eq!(h.state().ui.dialog.as_ref().unwrap().s("hex"), hex); - // Let go over the picker's own colour field, with the page under it: nothing either. - let field = h.get_by_label("Color field").rect().center(); - let ctx = h.ctx.clone(); - assert!(crate::canvas::color_at(h.state_mut(), &ctx, field).is_some(), "the page lies under the field"); - h.hover_at(from); - h.drag_at(from); - h.run_steps(1); - h.hover_at(field); - h.run_steps(1); - h.drop_at(field); - h.run_steps(3); - assert_eq!(h.state().ui.dialog.as_ref().unwrap().s("hex"), hex, "the picker isn't sampled"); } - - #[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.run("object.color", json!({"color":designcraft_color::Color::from_hex("#7c1315").unwrap(),"target":"fill"})).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. - store_picked(h.state_mut().app.ui.dialog.as_mut().unwrap(), &Picked::from_rgb([124.0 / 255.0, 19.0 / 255.0, 21.0 / 255.0], 0.0)); - 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. - store_picked(h.state_mut().app.ui.dialog.as_mut().unwrap(), &Picked::from_rgb([124.0 / 255.0, 19.0 / 255.0, 21.0 / 255.0], 0.0)); - 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 { .. }, .. } - )); - // The percentages the C/M/Y/K fields show, exactly (not the colour converted back). - h.state_mut().app.ui.dialog.as_mut().unwrap().fields.insert("cmyk".into(), json!([0.0, 50.0, 100.0, 0.0])); - h.run_steps(2); - h.get_by_label("Add CMYK Swatch").click(); - h.run_steps(3); - let doc = &h.state().app.session.active().unwrap().doc; - let designcraft_color::swatch::SwatchValue::Color { color: designcraft_color::Color::Cmyk { c, m, y, k }, .. } = - doc.swatches.last().unwrap().value - else { - panic!("a CMYK swatch") - }; - assert_eq!([c, m, y, k].map(|v| (v * 100.0).round()), [0.0, 50.0, 100.0, 0.0]); - // Its own OK, at the top right, applies it. - h.get_by_label("OK").click(); - h.run_steps(3); - assert!(h.state().app.ui.dialog.is_none(), "OK closes it"); - 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"); + h.drop_at(target); + h.run_steps(3); + let d = h.state().ui.dialog.as_ref().expect("the picker stays open"); + let [r, g, b, _] = h.state().session.doc().unwrap().doc.resolve_color("C=0 M=100 Y=100 K=0", 1.0).unwrap().to_rgba8(1.0); + let got: Vec = d.fields["rgb"].as_array().unwrap().iter().map(|v| v.as_f64().unwrap()).collect(); + for (a, e) in got.iter().zip([r, g, b]) { + assert!((a - f64::from(e)).abs() <= 3.0, "{got:?} vs {:?}", [r, g, b]); } + assert!(d.b("edited")); + // Let go off the page (here, on the dialog's own field area): no colour, nothing changes. + let hex = d.s("hex"); + h.hover_at(from); + h.drag_at(from); + h.run_steps(1); + h.hover_at(from + egui::vec2(-40.0, 0.0)); + h.run_steps(1); + h.drop_at(egui::pos2(2.0, 2.0)); + h.run_steps(3); + assert_eq!(h.state().ui.dialog.as_ref().unwrap().s("hex"), hex); + // Let go over the picker's own colour field, with the page under it: nothing either. + let field = h.get_by_label("Color field").rect().center(); + let ctx = h.ctx.clone(); + assert!(crate::canvas::color_at(h.state_mut(), &ctx, field).is_some(), "the page lies under the field"); + h.hover_at(from); + h.drag_at(from); + h.run_steps(1); + h.hover_at(field); + h.run_steps(1); + h.drop_at(field); + h.run_steps(3); + assert_eq!(h.state().ui.dialog.as_ref().unwrap().s("hex"), hex, "the picker isn't sampled"); +} - #[test] +#[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.run("object.color", json!({"color":designcraft_color::Color::from_hex("#7c1315").unwrap(),"target":"fill"})).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. + store_picked(h.state_mut().app.ui.dialog.as_mut().unwrap(), &Picked::from_rgb([124.0 / 255.0, 19.0 / 255.0, 21.0 / 255.0], 0.0)); + 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. + store_picked(h.state_mut().app.ui.dialog.as_mut().unwrap(), &Picked::from_rgb([124.0 / 255.0, 19.0 / 255.0, 21.0 / 255.0], 0.0)); + 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 { .. }, .. } + )); + // The percentages the C/M/Y/K fields show, exactly (not the colour converted back). + h.state_mut().app.ui.dialog.as_mut().unwrap().fields.insert("cmyk".into(), json!([0.0, 50.0, 100.0, 0.0])); + h.run_steps(2); + h.get_by_label("Add CMYK Swatch").click(); + h.run_steps(3); + let doc = &h.state().app.session.active().unwrap().doc; + let designcraft_color::swatch::SwatchValue::Color { color: designcraft_color::Color::Cmyk { c, m, y, k }, .. } = + doc.swatches.last().unwrap().value + else { + panic!("a CMYK swatch") + }; + assert_eq!([c, m, y, k].map(|v| (v * 100.0).round()), [0.0, 50.0, 100.0, 0.0]); + // Its own OK, at the top right, applies it. + h.get_by_label("OK").click(); + h.run_steps(3); + assert!(h.state().app.ui.dialog.is_none(), "OK closes it"); + 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); From 7e51494eacd9460e9cc3c56942fe530c2c1a7916 Mon Sep 17 00:00:00 2001 From: moppeeer Date: Thu, 29 Oct 2026 22:42:42 +0100 Subject: [PATCH 08/17] egui 0.37: the renamed APIs in this branch's code Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/dialogs.rs | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index e13340a25..2a7567400 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -7262,11 +7262,11 @@ fn color_picker(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { }, ); let fr = put(at::field()); - let fresp = ui.interact(fr, ui.id().with("cp_field"), egui::Sense::click_and_drag()); + let fresp = ui.interact(fr, ui.scope_id().with("cp_field"), 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); - fresp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Other, true, "Color field")); + fresp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Unknown, 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); @@ -7285,9 +7285,9 @@ fn color_picker(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { } // The slider, with a marker on each side. let sr = put(at::slider()); - let sresp = ui.interact(sr.expand2(egui::vec2(6.0, 0.0)), ui.id().with("cp_slider"), egui::Sense::click_and_drag()); + let sresp = ui.interact(sr.expand2(egui::vec2(6.0, 0.0)), ui.scope_id().with("cp_slider"), egui::Sense::click_and_drag()); gradient_mesh(ui.painter(), sr, 1, 48, |_, y| p.slider_at(comp, y)); - sresp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Slider, true, "Color slider")); + sresp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::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); @@ -7314,7 +7314,7 @@ fn color_picker(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { // OK, Cancel, Add CMYK Swatch: rounded, OK filled. let pill = |ui: &mut egui::Ui, i: usize, label: &str, primary: bool| -> bool { let r = put(at::button(i)); - let resp = ui.interact(r, ui.id().with(("cp_button", i)), egui::Sense::click()); + let resp = ui.interact(r, ui.scope_id().with(("cp_button", i)), egui::Sense::click()); let (fill, fg, stroke) = if primary { (t.accent_strong, egui::Color32::WHITE, egui::Stroke::NONE) } else { @@ -7322,7 +7322,7 @@ fn color_picker(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { }; ui.painter().rect(r, r.height() / 2.0, fill, stroke, egui::StrokeKind::Inside); ui.painter().text(r.center(), egui::Align2::CENTER_CENTER, label, egui::FontId::proportional(12.0), fg); - resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, label)); + resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, label)); resp.clicked() }; // The footer's padded OK key, which every catalog has. @@ -7370,12 +7370,12 @@ fn color_picker(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { match c { Some(c) => { let rr = egui::Rect::from_center_size(egui::pos2(x, cy), egui::vec2(14.0, 14.0)); - let resp = ui.interact(rr, ui.id().with(("cp_radio", key)), egui::Sense::click()); + let resp = ui.interact(rr, ui.scope_id().with(("cp_radio", key)), egui::Sense::click()); ui.painter().circle(rr.center(), 6.0, t.input, egui::Stroke::new(1.0, t.text_dim)); if comp_now == c { ui.painter().circle_filled(rr.center(), 2.6, t.text_strong); } - resp.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::RadioButton, true, comp_now == c, label)); + resp.widget_info(|| egui::WidgetInfo::selected(egui::Role::RadioButton, true, comp_now == c, label)); if resp.clicked() { comp_now = c; } @@ -7425,11 +7425,11 @@ fn color_picker(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { let cy = area.min.y + at::ROWS[7]; text(ui, egui::pos2(area.min.x + 321.0, cy), egui::Align2::LEFT_CENTER, "#:"); let hr = egui::Rect::from_min_size(egui::pos2(area.min.x + 341.0, cy - 10.5), egui::vec2(64.0, 21.0)); - let id = egui::Id::new("color_picker_hex"); + let id = egui::Id::unique("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 - .put(hr, egui::TextEdit::singleline(&mut h).id(id).font(font).vertical_align(egui::Align::Center).margin(egui::Margin::symmetric(4, 2))) + .put(hr, egui::TextEdit::singleline(&mut h).id(id).font(font).align(egui::Align2::LEFT_CENTER).margin(egui::Margin::symmetric(4, 2))) .changed() && (h.len() == 6 || (h.len() == 7 && h.starts_with('#'))) && let Some(c) = designcraft_color::Color::from_hex(&h) @@ -7441,8 +7441,8 @@ fn color_picker(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { // pointer when it is let go. let tip = tr("Eyedropper: drag it onto the page"); let er = egui::Rect::from_center_size(egui::pos2(hr.right() + 16.0, cy), egui::vec2(21.0, 21.0)); - let resp = ui.interact(er, egui::Id::new("color_picker_eyedropper"), egui::Sense::click_and_drag()); - resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, &tip)); + let resp = ui.interact(er, egui::Id::unique("color_picker_eyedropper"), egui::Sense::click_and_drag()); + resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, &tip)); if resp.hovered() || resp.dragged() { ui.painter().rect_filled(er, 3.0, t.hover); } @@ -7853,7 +7853,7 @@ mod picker_layout { /// A value field: the text typed stays while it has the focus; a number that parses applies. fn picker_field(ui: &mut egui::Ui, d: &mut Dialog, r: egui::Rect, key: &str, value: f32, range: (f32, f32)) -> Option { - let id = egui::Id::new(("color_picker_field", key)); + let id = egui::Id::unique(("color_picker_field", key)); let editing = ui.memory(|m| m.has_focus(id)); let shown = format!("{}", value.round() as i64); let edit_key = format!("_edit_{key}"); @@ -7863,7 +7863,7 @@ fn picker_field(ui: &mut egui::Ui, d: &mut Dialog, r: egui::Rect, key: &str, val egui::TextEdit::singleline(&mut text) .id(id) .font(egui::FontId::proportional(11.5)) - .vertical_align(egui::Align::Center) + .align(egui::Align2::LEFT_CENTER) .margin(egui::Margin::symmetric(4, 2)), ); let mut out = None; From 12425aafd7d12868ebafc081ceecc49174aa69f5 Mon Sep 17 00:00:00 2001 From: moppeeer Date: Thu, 29 Oct 2026 22:43:22 +0100 Subject: [PATCH 09/17] Eyedropper: the canvas render module's imports for the colour sampler; screen_options keeps its doc comment Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/canvas/render.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/canvas/render.rs b/crates/ui-egui/src/canvas/render.rs index 691d9c162..5b7358996 100644 --- a/crates/ui-egui/src/canvas/render.rs +++ b/crates/ui-egui/src/canvas/render.rs @@ -1,10 +1,9 @@ -use super::{Affine, CanvasLayout, Color32, DRect, DesignApp, Point, Rect, Xf}; +use super::{Affine, CanvasLayout, Color32, DRect, DesignApp, Point, Pos2, Rect, ScreenMode, Xf}; pub(super) fn c32(c: [u8; 3]) -> Color32 { Color32::from_rgb(c[0], c[1], c[2]) } -/// Render options for the canvas texture. /// The colour the canvas shows at screen point `pos` (the Color Picker's eyedropper): one device /// pixel rendered as the page is, over its paper. None off the pages. pub fn color_at(app: &mut DesignApp, ctx: &egui::Context, pos: Pos2) -> Option<[u8; 3]> { @@ -36,6 +35,7 @@ pub fn color_at(app: &mut DesignApp, ctx: &egui::Context, pos: Pos2) -> Option<[ Some([over(r, paper[0]), over(g, paper[1]), over(b, paper[2])]) } +/// Render options for the canvas texture. pub(super) fn screen_options(app: &DesignApp, layout: &CanvasLayout, preview: bool, ppp: f64) -> designcraft_render::RenderOptions { designcraft_render::RenderOptions { printing_only: preview, From b77a1c29bde398920f978b7491b6d13e95bb30ce Mon Sep 17 00:00:00 2001 From: moppeeer Date: Fri, 30 Oct 2026 15:33:46 +0100 Subject: [PATCH 10/17] Eyedropper: the renderer's size as one (width, height) argument, as main's render takes it Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/canvas/render.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ui-egui/src/canvas/render.rs b/crates/ui-egui/src/canvas/render.rs index 5b7358996..07983944e 100644 --- a/crates/ui-egui/src/canvas/render.rs +++ b/crates/ui-egui/src/canvas/render.rs @@ -25,7 +25,7 @@ pub fn color_at(app: &mut DesignApp, ctx: &egui::Context, pos: Pos2) -> Option<[ let ppp = ctx.pixels_per_point() as f64; let view = Affine::scale(ppp) * Affine::scale(xf.zoom) * Affine::translate((-at.x, -at.y)); let opts = screen_options(app, &layout, app.ui.screen_mode != ScreenMode::Normal, ppp); - let mut img = app.canvas.patcher.render(&doc, &app.session.cache, &placed, 1, 1, view, &opts); + let mut img = app.canvas.patcher.render(&doc, &app.session.cache, &placed, (1, 1), view, &opts); // The colour as the canvas shows it (proofed). screen_post(app, &mut img); let [r, g, b, a] = <[u8; 4]>::try_from(img.pixels.get(..4)?).ok()?; From 8e514a4a074db044e0f8c6db368bda861bb74fbf Mon Sep 17 00:00:00 2001 From: moppeeer Date: Fri, 30 Oct 2026 17:15:02 +0100 Subject: [PATCH 11/17] Color Picker: its number fields and hex code named for screen readers, as on color-picker-layout Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/dialogs.rs | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 2a7567400..fc39a67c0 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -7385,7 +7385,7 @@ fn color_picker(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { None => text(ui, egui::pos2(x + 28.0, cy), egui::Align2::RIGHT_CENTER, label), } let fr = egui::Rect::from_min_size(egui::pos2(x + 35.0, cy - 10.5), egui::vec2(44.0, 21.0)); - if let Some(v) = picker_field(ui, d, fr, key, value, range) { + if let Some(v) = picker_field(ui, d, fr, key, label.trim_end_matches(':'), value, range) { if matches!(key, "r" | "g" | "bl") { d.fields.insert("model".into(), json!("rgb")); } @@ -7430,6 +7430,7 @@ fn color_picker(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { let mut h = if editing { d.s("hexEdit") } else { p.hex().trim_start_matches('#').to_string() }; if ui .put(hr, egui::TextEdit::singleline(&mut h).id(id).font(font).align(egui::Align2::LEFT_CENTER).margin(egui::Margin::symmetric(4, 2))) + .accessible_name("Hex".to_owned()) .changed() && (h.len() == 6 || (h.len() == 7 && h.starts_with('#'))) && let Some(c) = designcraft_color::Color::from_hex(&h) @@ -7852,20 +7853,22 @@ mod picker_layout { } /// A value field: the text typed stays while it has the focus; a number that parses applies. -fn picker_field(ui: &mut egui::Ui, d: &mut Dialog, r: egui::Rect, key: &str, value: f32, range: (f32, f32)) -> Option { +fn picker_field(ui: &mut egui::Ui, d: &mut Dialog, r: egui::Rect, key: &str, name: &str, value: f32, range: (f32, f32)) -> Option { let id = egui::Id::unique(("color_picker_field", key)); let editing = ui.memory(|m| m.has_focus(id)); let shown = format!("{}", value.round() as i64); let edit_key = format!("_edit_{key}"); let mut text = if editing { d.fields.get(&edit_key).and_then(Value::as_str).map(str::to_string).unwrap_or(shown) } else { shown }; - let resp = ui.put( - r, - egui::TextEdit::singleline(&mut text) - .id(id) - .font(egui::FontId::proportional(11.5)) - .align(egui::Align2::LEFT_CENTER) - .margin(egui::Margin::symmetric(4, 2)), - ); + let resp = ui + .put( + r, + egui::TextEdit::singleline(&mut text) + .id(id) + .font(egui::FontId::proportional(11.5)) + .align(egui::Align2::LEFT_CENTER) + .margin(egui::Margin::symmetric(4, 2)), + ) + .accessible_name(name.to_owned()); let mut out = None; if resp.changed() && let Ok(v) = text.trim().parse::() From a9b6bc5753c649e38a6c60290dbbaf81236ded9c Mon Sep 17 00:00:00 2001 From: moppeeer Date: Fri, 30 Oct 2026 20:32:37 +0100 Subject: [PATCH 12/17] Eyedropper test: the drop on a point of the frame outside the picker The picker sits over the page's middle on main's dialog layout, and a drop on the picker takes nothing, as it should. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/dialogs/tests.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/dialogs/tests.rs b/crates/ui-egui/src/dialogs/tests.rs index a35377864..72826aa80 100644 --- a/crates/ui-egui/src/dialogs/tests.rs +++ b/crates/ui-egui/src/dialogs/tests.rs @@ -3525,13 +3525,18 @@ fn the_color_pickers_eyedropper_takes_a_colour_off_the_page() { app, ); h.run_steps(6); - // Where the frame shows on screen. + // Where the frame shows on screen, outside the picker (a drop on the picker takes nothing). let target = { let app = h.state(); let doc = &app.session.doc().unwrap().doc; let layout = designcraft_tools::CanvasLayout::new(doc, false); let v = *app.view().unwrap(); - crate::canvas::Xf::new(app.canvas_rect.unwrap(), &v).to_screen(layout.slots[0].xf * designcraft_geom::Point::new(300.0, 600.0)) + let xf = crate::canvas::Xf::new(app.canvas_rect.unwrap(), &v); + [(60.0, 660.0), (60.0, 80.0), (300.0, 600.0), (540.0, 80.0)] + .into_iter() + .map(|(x, y)| xf.to_screen(layout.slots[0].xf * designcraft_geom::Point::new(x, y))) + .find(|p| h.ctx.layer_id_at(*p).is_none_or(|l| l.order != egui::Order::Foreground)) + .expect("a point of the frame outside the picker") }; let from = h.get_by_label("Eyedropper: drag it onto the page").rect().center(); h.hover_at(from); From 5819e3aea8b298ac25185e8ff6b14f78b893197f Mon Sep 17 00:00:00 2001 From: moppeeer Date: Fri, 30 Oct 2026 20:34:34 +0100 Subject: [PATCH 13/17] Eyedropper test: outside the picker is outside the dialog's own layer Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/dialogs/tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ui-egui/src/dialogs/tests.rs b/crates/ui-egui/src/dialogs/tests.rs index 72826aa80..4a97d6770 100644 --- a/crates/ui-egui/src/dialogs/tests.rs +++ b/crates/ui-egui/src/dialogs/tests.rs @@ -3535,7 +3535,7 @@ fn the_color_pickers_eyedropper_takes_a_colour_off_the_page() { [(60.0, 660.0), (60.0, 80.0), (300.0, 600.0), (540.0, 80.0)] .into_iter() .map(|(x, y)| xf.to_screen(layout.slots[0].xf * designcraft_geom::Point::new(x, y))) - .find(|p| h.ctx.layer_id_at(*p).is_none_or(|l| l.order != egui::Order::Foreground)) + .find(|p| h.ctx.layer_id_at(*p).is_none_or(|l| l.id != egui::Id::unique("dialog"))) .expect("a point of the frame outside the picker") }; let from = h.get_by_label("Eyedropper: drag it onto the page").rect().center(); From aef687906f52393963f3973ccab9c70cdbde7446 Mon Sep 17 00:00:00 2001 From: moppeeer Date: Fri, 30 Oct 2026 20:38:24 +0100 Subject: [PATCH 14/17] Eyedropper test: any point of the frame the picker leaves uncovered Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/dialogs/tests.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/dialogs/tests.rs b/crates/ui-egui/src/dialogs/tests.rs index 4a97d6770..a07d46c17 100644 --- a/crates/ui-egui/src/dialogs/tests.rs +++ b/crates/ui-egui/src/dialogs/tests.rs @@ -3532,8 +3532,9 @@ fn the_color_pickers_eyedropper_takes_a_colour_off_the_page() { let layout = designcraft_tools::CanvasLayout::new(doc, false); let v = *app.view().unwrap(); let xf = crate::canvas::Xf::new(app.canvas_rect.unwrap(), &v); - [(60.0, 660.0), (60.0, 80.0), (300.0, 600.0), (540.0, 80.0)] - .into_iter() + // Any point of the frame (36–560 × 36–700 pt) the picker leaves uncovered. + (0..27) + .flat_map(|i| (0..34).map(move |j| (46.0 + 19.0 * f64::from(i), 46.0 + 19.0 * f64::from(j)))) .map(|(x, y)| xf.to_screen(layout.slots[0].xf * designcraft_geom::Point::new(x, y))) .find(|p| h.ctx.layer_id_at(*p).is_none_or(|l| l.id != egui::Id::unique("dialog"))) .expect("a point of the frame outside the picker") From d31a838aab0dbf7089f1a8a620d9a2ac7a8b973f Mon Sep 17 00:00:00 2001 From: moppeeer Date: Fri, 30 Oct 2026 20:41:11 +0100 Subject: [PATCH 15/17] Eyedropper test: back to dropping on the frame's middle The picker's modal backdrop covers the screen on main, so no point is outside its host layer; the drop check compares with the picker's own layer, which the middle of the frame isn't on. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/dialogs/tests.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/crates/ui-egui/src/dialogs/tests.rs b/crates/ui-egui/src/dialogs/tests.rs index a07d46c17..a35377864 100644 --- a/crates/ui-egui/src/dialogs/tests.rs +++ b/crates/ui-egui/src/dialogs/tests.rs @@ -3525,19 +3525,13 @@ fn the_color_pickers_eyedropper_takes_a_colour_off_the_page() { app, ); h.run_steps(6); - // Where the frame shows on screen, outside the picker (a drop on the picker takes nothing). + // Where the frame shows on screen. let target = { let app = h.state(); let doc = &app.session.doc().unwrap().doc; let layout = designcraft_tools::CanvasLayout::new(doc, false); let v = *app.view().unwrap(); - let xf = crate::canvas::Xf::new(app.canvas_rect.unwrap(), &v); - // Any point of the frame (36–560 × 36–700 pt) the picker leaves uncovered. - (0..27) - .flat_map(|i| (0..34).map(move |j| (46.0 + 19.0 * f64::from(i), 46.0 + 19.0 * f64::from(j)))) - .map(|(x, y)| xf.to_screen(layout.slots[0].xf * designcraft_geom::Point::new(x, y))) - .find(|p| h.ctx.layer_id_at(*p).is_none_or(|l| l.id != egui::Id::unique("dialog"))) - .expect("a point of the frame outside the picker") + crate::canvas::Xf::new(app.canvas_rect.unwrap(), &v).to_screen(layout.slots[0].xf * designcraft_geom::Point::new(300.0, 600.0)) }; let from = h.get_by_label("Eyedropper: drag it onto the page").rect().center(); h.hover_at(from); From 9e51746b63eedba1d7c49f354362c0a44ec0fa63 Mon Sep 17 00:00:00 2001 From: moppeeer Date: Fri, 30 Oct 2026 20:44:50 +0100 Subject: [PATCH 16/17] Color Picker eyedropper: a drop over the picker is one inside where it draws On main the dialog is a modal whose transparent backdrop shares the picker's layer and covers the screen, so comparing layers took every drop for one on the picker and the eyedropper took nothing. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/dialogs.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index fc39a67c0..15ff35e34 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -7451,10 +7451,11 @@ fn color_picker(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { if resp.dragged() { ui.ctx().set_cursor_icon(egui::CursorIcon::Crosshair); } - // Let go over the picker itself it takes nothing, though a page may lie under it. + // Let go over the picker itself it takes nothing, though a page may lie under it. (Its modal + // backdrop shares the picker's layer and covers the screen: the picker is where it draws.) if resp.drag_stopped() && let Some(pos) = ui.input(|i| i.pointer.latest_pos()) - && ui.ctx().layer_id_at(pos) != Some(ui.layer_id()) + && !ui.clip_rect().contains(pos) && let Some(c) = crate::canvas::color_at(app, ui.ctx(), pos) { p = Picked::from_rgb(c.map(|v| f32::from(v) / 255.0), p.hsb[0]); From 4bc752dc66df9ecb88729e85e55740e999fe5c69 Mon Sep 17 00:00:00 2001 From: moppeeer Date: Fri, 30 Oct 2026 20:49:12 +0100 Subject: [PATCH 17/17] Color Picker: the eyedropper in its own function Keeps color_picker within clippy's cognitive complexity limit. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/dialogs.rs | 48 ++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 15ff35e34..06466d4e6 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -7217,6 +7217,32 @@ fn nums(d: &Dialog, k: &str) -> Option<[f64; N]> { Some(out) } +/// The Color Picker's eyedropper at `at`, as InDesign's: dragged onto the page, it takes the colour +/// shown under the pointer when it is let go. The colour taken, if any. +fn eyedropper(app: &mut DesignApp, ui: &mut egui::Ui, at: egui::Pos2, tip: String, t: &crate::theme::Tokens) -> Option<[u8; 3]> { + let er = egui::Rect::from_center_size(at, egui::vec2(21.0, 21.0)); + let resp = ui.interact(er, egui::Id::unique("color_picker_eyedropper"), egui::Sense::click_and_drag()); + resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, &tip)); + if resp.hovered() || resp.dragged() { + ui.painter().rect_filled(er, 3.0, t.hover); + } + crate::icons::paint(ui.painter(), egui::Rect::from_center_size(er.center(), egui::vec2(15.0, 15.0)), "tool-eyedropper", t.icon); + if resp.dragged() { + ui.ctx().set_cursor_icon(egui::CursorIcon::Crosshair); + } + // Let go over the picker itself it takes nothing, though a page may lie under it. (Its modal + // backdrop shares the picker's layer and covers the screen: the picker is where it draws.) + let mut taken = None; + if resp.drag_stopped() + && let Some(pos) = ui.input(|i| i.pointer.latest_pos()) + && !ui.clip_rect().contains(pos) + { + taken = crate::canvas::color_at(app, ui.ctx(), pos); + } + resp.on_hover_text(tip); + taken +} + /// The Color Picker, laid out as InDesign's: the colour space view (a field of two components, /// a slider for the third: the one whose radio button is on, R at first), the new colour over the /// one it replaces, OK / Cancel / Add CMYK Swatch at the top right, H/S/B beside L/a/b, R/G/B @@ -7438,29 +7464,9 @@ fn color_picker(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { p = Picked::from_rgb(c.to_rgb_uncalibrated(), p.hsb[0]); } d.fields.insert("hexEdit".into(), json!(h)); - // The eyedropper, as InDesign's: dragged onto the page, it takes the colour shown under the - // pointer when it is let go. - let tip = tr("Eyedropper: drag it onto the page"); - let er = egui::Rect::from_center_size(egui::pos2(hr.right() + 16.0, cy), egui::vec2(21.0, 21.0)); - let resp = ui.interact(er, egui::Id::unique("color_picker_eyedropper"), egui::Sense::click_and_drag()); - resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, &tip)); - if resp.hovered() || resp.dragged() { - ui.painter().rect_filled(er, 3.0, t.hover); - } - crate::icons::paint(ui.painter(), egui::Rect::from_center_size(er.center(), egui::vec2(15.0, 15.0)), "tool-eyedropper", t.icon); - if resp.dragged() { - ui.ctx().set_cursor_icon(egui::CursorIcon::Crosshair); - } - // Let go over the picker itself it takes nothing, though a page may lie under it. (Its modal - // backdrop shares the picker's layer and covers the screen: the picker is where it draws.) - if resp.drag_stopped() - && let Some(pos) = ui.input(|i| i.pointer.latest_pos()) - && !ui.clip_rect().contains(pos) - && let Some(c) = crate::canvas::color_at(app, ui.ctx(), pos) - { + if let Some(c) = eyedropper(app, ui, egui::pos2(hr.right() + 16.0, cy), tr("Eyedropper: drag it onto the page"), &t) { p = Picked::from_rgb(c.map(|v| f32::from(v) / 255.0), p.hsb[0]); } - resp.on_hover_text(tip); if p != before { store_picked(d, &p); if let Some(c) = exact_cmyk {