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", "Фіксована кількість"),