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 <noreply@anthropic.com>
This commit is contained in:
paaspaas00andClaude Opus 5.5 committed 2026-10-21 18:54:54 +02:00
1 parent 2a14be0dfa
commit 3430a88cbc
11 files changed
+123 -7

No files matched your search

+29
View File
@@ -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<designcraft_render::Placed> = 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,
+76 -7
View File
@@ -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<f64> = 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());
+10
View File
@@ -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", "栏"]),
+1
View File
@@ -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", "عدد ثابت"),
+1
View File
@@ -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"),
+1
View File
@@ -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"),
+1
View File
@@ -31,6 +31,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Cut", "ამოჭრა"),
("Copy", "კოპირება"),
("Paste", "ჩასმა"),
("Eyedropper: drag it onto the page", "პიპეტი: გადაიტანეთ გვერდზე"),
("Inches Decimal", "ინჩები (ათობითი)"),
("Back", "უკან"),
("Warning", "გაფრთხილება"),
+1
View File
@@ -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"),
+1
View File
@@ -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"),
+1
View File
@@ -33,6 +33,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Cut", "Вырезать"),
("Copy", "Копировать"),
("Paste", "Вставить"),
("Eyedropper: drag it onto the page", "Пипетка: перетащите её на страницу"),
("Clear", "Очистить"),
("Duplicate", "Дублировать"),
("Select All", "Выделить все"),
+1
View File
@@ -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", "Фіксована кількість"),