Browse / github.com/storytold/designcraft / patch
Drop shadow: document units in the Effects panel, colour chips in the Effects dialog
Drop shadow: Effects panel reads Distance and Size in points, the Effects dialog in document units; dialog colour menu has no colour chips ### Description The drop shadow can be set in two places, the quick settings in the Effects panel and the Effects dialog (the **fx** button). They edit the same shadow but read numbers differently. And the dialog's colour menu doesn't show the colour of the swatch. **1. Panel and dialog use different units.** The panel always shows and reads Distance and Size in points (`7 pt`, `5 pt`). The dialog uses the document's ruler units. In a document set to picas (the default for Letter), the dialog shows `0p7` and `0p5`, and a bare number typed there is read in picas: 1. New document (Letter, pic
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- PiMinusMax (imported from pull request #1524, open)
- Base
ed8aabd1dc5a88652a076631c2d46886ca60c031- Size
- +104 −12 in 3 files
- Review
- pending
- Shared
- 2026-10-28
- Id
fa94edd0b2c7355c88e64217
How it went
No other operator has reported using it yet.
Diff
raw · crates/ui-egui/src/dialogs.rs crates/ui-egui/src/effects_dialog.rs crates/ui-egui/src/panels/properties.rs
@@ -4535,7 +4535,7 @@ fn para_sides(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, pv: &Value, key: &s /// A swatch menu with a colour chip per swatch (hidden swatches left out); returns the swatch /// picked.-fn swatch_menu(ui: &mut egui::Ui, doc: &designcraft_doc::Document, id: impl egui::AsIdSalt, current: &str, width: f32) -> Option<String> {+pub(crate) fn swatch_menu(ui: &mut egui::Ui, doc: &designcraft_doc::Document, id: impl egui::AsIdSalt, current: &str, width: f32) -> Option<String> { let mut picked = None; egui::ComboBox::from_id_salt(id).selected_text(current).width(width).show_ui(ui, |ui| { for w in doc.swatches.iter().filter(|w| !w.hidden) {@@ -246,15 +246,15 @@ fn swatch(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog, path: &[&str]) { } v.as_str().unwrap_or("[Black]").to_string() };- let names: Vec<String> = app- .session- .active()- .map_or_else(Vec::new, |st| st.doc.swatches.iter().filter(|w| !w.hidden || w.name == cur).map(|w| w.name.clone()).collect());- egui::ComboBox::from_id_salt(("fx_swatch", path.join("."))).selected_text(&cur).width(150.0).show_ui(ui, |ui| {- for n in names {- if ui.selectable_label(n == cur, &n).clicked() {- set(d, path, json!(n));- }+ let Some(doc) = app.session.active().map(|st| &st.doc) else { return };+ // The colour itself beside its name, and a chip per swatch in the menu, as the swatch menus+ // elsewhere: a list of bare names hid what the shadow was coloured with.+ ui.horizontal(|ui| {+ let (c, g) = crate::widgets::swatch_colors(doc, &cur, 1.0);+ let (r, _) = ui.allocate_exact_size(egui::vec2(16.0, 16.0), egui::Sense::hover());+ crate::widgets::paint_chip(ui.painter(), r, c, g);+ if let Some(n) = crate::dialogs::swatch_menu(ui, doc, ("fx_swatch", path.join(".")), &cur, 128.0) {+ set(d, path, json!(n)); } }); }@@ -767,6 +767,44 @@ mod tests { assert!(!fx(&app).inner_shadow.on); } + #[test]+ fn the_shadow_colour_shows_its_swatch_beside_the_name() {+ let (mut app, _) = app_with_frame();+ app.run("swatch.create", json!({"name": "Shade", "color": "#12ab34"})).unwrap();+ let ctx = egui::Context::default();+ crate::theme::install_fonts(&ctx, "en");+ let shade = crate::widgets::swatch_colors(&app.session.doc().unwrap().doc, "Shade", 1.0).0.unwrap();+ // Does the dialog, once faded in, paint a chip of the colour `c`?+ let mut time = 0.0;+ let mut painted = |app: &mut DesignApp, c: egui::Color32| {+ let mut found = false;+ for _ in 0..20 {+ time += 0.1;+ let input = egui::RawInput {+ screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1400.0, 900.0))),+ time: Some(time),+ ..Default::default()+ };+ let mut out = ctx.run_ui(input, |ui| crate::dialogs::show(app, ui.ctx()));+ out.textures_delta.clear();+ fn walk(s: &egui::epaint::Shape, c: egui::Color32) -> bool {+ match s {+ egui::epaint::Shape::Rect(r) => r.fill == c,+ egui::epaint::Shape::Vec(v) => v.iter().any(|s| walk(s, c)),+ _ => false,+ }+ }+ found = out.shapes.iter().any(|s| walk(&s.shape, c));+ }+ found+ };+ open(&mut app, "dropShadow");+ assert!(!painted(&mut app, shade), "not the shadow's colour yet");+ set(app.ui.dialog.as_mut().unwrap(), &["dropShadow", "color"], json!("Shade"));+ assert!(painted(&mut app, shade), "the chosen swatch's colour is shown, not only its name");+ assert_eq!(params(app.ui.dialog.as_ref().unwrap())["effects"]["dropShadow"]["color"], "Shade");+ }+ #[test] fn offsets_follow_distance_and_angle_as_in_indesign() { let (mut app, _) = app_with_frame();@@ -2919,9 +2919,12 @@ pub fn effects_panel(app: &mut DesignApp, ui: &mut egui::Ui) { let _ = app.run("object.dropShadow", json!({"on": on})); } if ds.on {+ // Distance and Size in the document's units, as the Effects dialog shows them: a bare+ // number typed in either means the same.+ let u = units(app); egui::Grid::new("dsg").num_columns(4).spacing(vec2(6.0, 4.0)).show(ui, |ui| { caption(ui, crate::i18n::tr(&app.ui.language, "Distance"));- if let Some(v) = number(ui, "dsd", Some(ds.distance), " pt", 56.0, 1) {+ if let Some(v) = measure(ui, "dsd", Some(ds.distance), u, 56.0) { let _ = app.run("object.dropShadow", json!({"on": true, "distance": v})); } caption(ui, crate::i18n::tr(&app.ui.language, "Angle"));@@ -2948,7 +2951,7 @@ pub fn effects_panel(app: &mut DesignApp, ui: &mut egui::Ui) { let _ = app.run("object.dropShadow", json!({"on": true, "opacity": v / 100.0})); } caption(ui, crate::i18n::tr(&app.ui.language, "Size"));- if let Some(v) = number(ui, "dss", Some(ds.size), " pt", 50.0, 1) {+ if let Some(v) = measure(ui, "dss", Some(ds.size), u, 50.0) { let _ = app.run("object.dropShadow", json!({"on": true, "size": v})); } ui.end_row();@@ -4082,6 +4085,57 @@ mod effects_panel_tests { assert!(!item(&app, a).knockout); } + #[test]+ fn the_effects_panel_shows_and_reads_the_drop_shadow_in_the_document_units() {+ // As the Effects dialog does: a bare number typed in either means the same distance.+ let mut app = DesignApp::new(designcraft_engine::Session::new(), Default::default());+ app.run("file.new", json!({})).unwrap();+ app.run("document.preferences", json!({"horizontalUnits": "millimeters", "verticalUnits": "millimeters"})).unwrap();+ let id = app.run("frame.create", json!({"rect": [72, 72, 144, 144]})).unwrap()["id"].as_u64().unwrap();+ app.run("selection.set", json!({"ids": [id]})).unwrap();+ app.run("object.dropShadow", json!({"on": true, "distance": 7.0, "size": 5.0})).unwrap();+ use egui_kittest::{Harness, kittest::Queryable};+ let mut h = Harness::builder().with_size(vec2(320.0, 600.0)).build_ui_state(+ |ui, app: &mut DesignApp| {+ // Fonts first, as the other panel harnesses: they take effect from the next frame.+ let id = egui::Id::unique("test_fonts");+ if ui.data(|d| d.get_temp::<bool>(id)).is_none() {+ crate::theme::install_fonts(ui.ctx(), "en");+ ui.data_mut(|d| d.insert_temp(id, true));+ return;+ }+ effects_panel(app, ui);+ },+ app,+ );+ h.run_steps(3);+ // Replace the text of the field showing `pt` (in mm) with `typed`.+ fn retype(h: &mut Harness<'static, DesignApp>, pt: f64, typed: &str) {+ let v = designcraft_geom::format_measure(pt, Unit::Millimeters);+ let at = h.get_by(|n| n.role() == egui::accesskit::Role::TextInput && n.value().as_deref() == Some(v.as_str())).rect().center();+ h.hover_at(at);+ h.drag_at(at);+ h.drop_at(at);+ h.run_steps(2);+ h.key_press_modifiers(egui::Modifiers::COMMAND, egui::Key::A);+ h.event(egui::Event::Text(typed.into()));+ h.key_press(egui::Key::Enter);+ h.run_steps(3);+ }+ let shadow = |h: &Harness<'static, DesignApp>| {+ h.state().session.doc().unwrap().doc.item(designcraft_doc::ItemId(id)).unwrap().effects.drop_shadow.clone()+ };+ // Distance and Size show in mm, not as "7 pt" and "5 pt"; a bare number typed is mm.+ retype(&mut h, 7.0, "3");+ let ds = shadow(&h);+ assert!((ds.distance - Unit::Millimeters.to_pt(3.0)).abs() < 1e-6, "Distance 3 is 3 mm: {}", ds.distance);+ assert_eq!(ds.size, 5.0, "Size is left alone");+ retype(&mut h, 5.0, "2");+ let ds = shadow(&h);+ assert!((ds.size - Unit::Millimeters.to_pt(2.0)).abs() < 1e-6, "Size 2 is 2 mm: {}", ds.size);+ assert!((ds.distance - Unit::Millimeters.to_pt(3.0)).abs() < 1e-6, "Distance is left alone: {}", ds.distance);+ }+ #[test] fn pasted_into_frames_keep_object_level_and_disable_group_blending() { let mut app = DesignApp::new(designcraft_engine::Session::new(), Default::default());