collective

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

diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs--- a/crates/ui-egui/src/dialogs.rs+++ b/crates/ui-egui/src/dialogs.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) {diff --git a/crates/ui-egui/src/effects_dialog.rs b/crates/ui-egui/src/effects_dialog.rs--- a/crates/ui-egui/src/effects_dialog.rs+++ b/crates/ui-egui/src/effects_dialog.rs@@ -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();diff --git a/crates/ui-egui/src/panels/properties.rs b/crates/ui-egui/src/panels/properties.rs--- a/crates/ui-egui/src/panels/properties.rs+++ b/crates/ui-egui/src/panels/properties.rs@@ -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());