Integration: #1503 eyedropper

The merge of PR #1503 took main's and #1080's merged picker as is; this applies the eyedropper's own delta on top of upr/1080 (canvas::color_at, drag-from-picker in dialogs.rs, tests, picker strings). Conflicts: dialogs.rs kept the single-fire Enter/Esc handling with the eyedropper's trimmed OK key; the i18n tables keep both sides' entries.
This commit is contained in:
aiongg
2026-10-30 23:25:49 +00:00
parent 208deb495b
commit 79d09395f5
13 changed files with 230 additions and 33 deletions
+1 -1
View File
@@ -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;
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;
+40 -3
View File
@@ -1,9 +1,40 @@
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])
}
/// 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 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()?;
// 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])])
}
/// 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 {
@@ -268,10 +299,16 @@ fn render_worker_spawn(ctx: &egui::Context) -> Option<crate::render_worker::Work
crate::render_worker::Worker::spawn(ctx.clone())
}
fn upload(app: &mut DesignApp, ctx: &egui::Context, mut img: designcraft_render::Rendered) {
/// What the canvas does to a render before showing it: Proof Colors. The texture and the
/// eyedropper's sample both go through it.
fn screen_post(app: &DesignApp, img: &mut designcraft_render::Rendered) {
if app.ui.proof_colors {
designcraft_render::proof_view(&mut img, &app.ui.proof_setup);
designcraft_render::proof_view(img, &app.ui.proof_setup);
}
}
fn upload(app: &mut DesignApp, ctx: &egui::Context, mut img: designcraft_render::Rendered) {
screen_post(app, &mut img);
let ci = egui::ColorImage::from_rgba_premultiplied([img.width as usize, img.height as usize], &img.pixels);
let ci = crate::widgets::fit_texture(ci, ctx.input(|i| i.max_texture_side));
match &mut app.canvas.texture {
+36 -16
View File
@@ -8262,6 +8262,32 @@ fn nums<const N: usize>(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
@@ -8374,7 +8400,8 @@ fn color_picker(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialog) {
// The picker draws its own buttons, so the generic footer's Enter/Esc handling is skipped.
// The keys are consumed here so nothing else reacts to them as well, and a click on a pill
// wins over a key pressed in the same frame, so the dialog closes once.
let ok = pill(ui, 0, &tr("OK"), true);
// The footer's padded OK key, which every catalog has.
let ok = pill(ui, 0, tr(" OK ").trim(), true);
let cancel = pill(ui, 1, &tr("Cancel"), false);
let enter = ui.input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::Enter));
let esc = ui.input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::Escape));
@@ -8472,7 +8499,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));
@@ -8489,20 +8516,14 @@ 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,
);
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]);
}
if p != before {
store_picked(d, &p);
}
// Typed C/M/Y/K are kept as typed, even when the colour shown stays the same (at 100% K, a
// device conversion is black whatever C, M and Y are).
if let Some(c) = exact_cmyk {
d.fields.insert("cmyk".into(), json!(c));
d.fields.insert("edited".into(), json!(true));
if let Some(c) = exact_cmyk {
d.fields.insert("cmyk".into(), json!(c));
}
}
}
@@ -8890,8 +8911,7 @@ mod picker_layout {
pub const RIGHT: f32 = 457.0;
}
/// A value field, named `name` (its caption): the text typed stays while it has the focus; a
/// number that parses applies.
/// 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, name: &str, value: f32, range: (f32, f32)) -> Option<f32> {
let id = egui::Id::unique(("color_picker_field", key));
let editing = ui.memory(|m| m.has_focus(id));
+103 -13
View File
@@ -3487,24 +3487,114 @@ fn guide_counts(app: &DesignApp) -> Vec<usize> {
/// 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_color_picker_keeps_a_typed_cmyk_value_the_colour_shown_does_not_change_with() {
use egui_kittest::kittest::Queryable;
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.ui.dialog = Some(Dialog::new("colorPicker", json!({"target": "fill", "cmyk": [0.0, 0.0, 0.0, 100.0], "text": false})));
let mut h = crate::test_window::open(app, egui::vec2(1400.0, 900.0));
h.run_steps(3);
// A C typed at 100% K is kept, whether or not the colour shown changes with it.
let rgb = h.state().app.ui.dialog.as_ref().unwrap().fields.get("rgb").cloned();
h.get_by_label("C").focus();
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);
h.run_steps(1);
h.event(egui::Event::Text("50".into()));
h.run_steps(2);
let d = h.state().app.ui.dialog.as_ref().unwrap();
assert_eq!(d.fields["cmyk"], json!([50.0, 0.0, 0.0, 100.0]), "rgb {:?} (was {rgb:?})", d.fields.get("rgb"));
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);
// 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]
+5
View File
@@ -66,6 +66,11 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("New Section", "قسم جديد"),
("Clear Frame Fitting Options", "مسح خيارات ملاءمة الإطار"),
("Cannot be edited", "غير قابل للتحرير"),
("RGB Color Space View", "عرض فضاء ألوان RGB"),
("Lab Color Space View", "عرض فضاء ألوان Lab"),
("Add CMYK Swatch", "إضافة عينة لون CMYK"),
("Added swatch", "تمت إضافة عينة اللون"),
("Eyedropper: drag it onto the page", "القطارة: اسحبها إلى الصفحة"),
("Restart Numbering", "إعادة الترقيم"),
("Continue Numbering", "متابعة الترقيم"),
("Keep Options…", "خيارات الإبقاء…"),
+5
View File
@@ -71,6 +71,11 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("New Section", "Nový oddíl"),
("Clear Frame Fitting Options", "Vymazat volby přizpůsobení rámečku"),
("Cannot be edited", "Nelze upravit"),
("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"),
("Restart Numbering", "Začít číslování znovu"),
("Continue Numbering", "Pokračovat v číslování"),
("Keep Options…", "Volby zachování…"),
+5
View File
@@ -65,6 +65,11 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("New Section", "Nuova sezione"),
("Clear Frame Fitting Options", "Cancella opzioni di adattamento cornice"),
("Cannot be edited", "Non modificabile"),
("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"),
("Restart Numbering", "Riavvia numerazione"),
("Continue Numbering", "Continua numerazione"),
("Keep Options…", "Opzioni di separazione…"),
+5
View File
@@ -65,6 +65,11 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("New Section", "ახალი სექცია"),
("Clear Frame Fitting Options", "ჩარჩოს მორგების პარამეტრების გასუფთავება"),
("Cannot be edited", "რედაქტირება შეუძლებელია"),
("RGB Color Space View", "RGB ფერთა სივრცის ხედი"),
("Lab Color Space View", "Lab ფერთა სივრცის ხედი"),
("Add CMYK Swatch", "CMYK ფერის ნიმუშის დამატება"),
("Added swatch", "ფერის ნიმუში დამატებულია"),
("Eyedropper: drag it onto the page", "პიპეტი: გადაიტანეთ გვერდზე"),
("Restart Numbering", "ნუმერაციის თავიდან დაწყება"),
("Continue Numbering", "ნუმერაციის გაგრძელება"),
("Keep Options…", "შენარჩუნების პარამეტრები…"),
+5
View File
@@ -69,6 +69,11 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("New Section", "Nowa sekcja"),
("Clear Frame Fitting Options", "Wyczyść opcje dopasowania ramki"),
("Cannot be edited", "Nie można edytować"),
("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ę"),
("Restart Numbering", "Zacznij numerowanie od nowa"),
("Continue Numbering", "Kontynuuj numerowanie"),
("Keep Options…", "Opcje utrzymywania…"),
+5
View File
@@ -68,6 +68,11 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("New Section", "Nova seção"),
("Clear Frame Fitting Options", "Limpar opções de ajuste do quadro"),
("Cannot be edited", "Não editável"),
("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"),
("Restart Numbering", "Reiniciar numeração"),
("Continue Numbering", "Continuar numeração"),
("Keep Options…", "Opções de manutenção…"),
+5
View File
@@ -73,6 +73,11 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("New Section", "Новый раздел"),
("Clear Frame Fitting Options", "Очистить параметры подгонки фрейма"),
("Cannot be edited", "Нельзя изменить"),
("RGB Color Space View", "Просмотр цветового пространства RGB"),
("Lab Color Space View", "Просмотр цветового пространства Lab"),
("Add CMYK Swatch", "Добавить образец CMYK"),
("Added swatch", "Образец добавлен"),
("Eyedropper: drag it onto the page", "Пипетка: перетащите её на страницу"),
("Restart Numbering", "Начать нумерацию заново"),
("Continue Numbering", "Продолжить нумерацию"),
("Keep Options…", "Параметры удержания…"),
+10
View File
@@ -1053,6 +1053,16 @@ pub(super) const TABLE: &[(&str, [&str; 5])] = &[
("Class:", ["Class:", "Classe :", "Class:", "Class:", "类别:"]),
("Color Picker", ["Color Picker", "Sélecteur de couleurs", "Color Picker", "Color Picker", "拾色器"]),
("Color spectrum", ["Farbspektrum", "Spectre de couleurs", "Espectro de color", "カラースペクトル", "色谱"]),
(
"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", "栏"]),
+5
View File
@@ -66,6 +66,11 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("New Section", "Новий розділ"),
("Clear Frame Fitting Options", "Очистити параметри припасування фрейму"),
("Cannot be edited", "Не можна змінити"),
("RGB Color Space View", "Перегляд колірного простору RGB"),
("Lab Color Space View", "Перегляд колірного простору Lab"),
("Add CMYK Swatch", "Додати зразок CMYK"),
("Added swatch", "Зразок додано"),
("Eyedropper: drag it onto the page", "Піпетка: перетягніть її на сторінку"),
("Restart Numbering", "Почати нумерацію знову"),
("Continue Numbering", "Продовжити нумерацію"),
("Keep Options…", "Параметри утримання…"),