From 9ad147b31074e73de9ad81477898ea0d6c81fad6 Mon Sep 17 00:00:00 2001 From: Harrison Booth <73434116+Harrison-Booth@users.noreply.github.com> Date: Mon, 26 Oct 2026 21:51:54 -0500 Subject: [PATCH 1/4] Format Shape Effects: drawn Shadow, Glow and Soft Edges preset tiles, and a shadow Size slider --- crates/engine/src/cmd/shape.rs | 2 +- crates/engine/src/cmd/shape/appearance.rs | 6 + crates/engine/src/tests.rs | 9 + crates/ui-egui/src/panels.rs | 4 + crates/ui-egui/src/panels/effect_presets.rs | 202 ++++++++++++++++++ .../src/panels/effect_presets_tests.rs | 73 +++++++ crates/ui-egui/src/panels/format_shape.rs | 25 +-- 7 files changed, 306 insertions(+), 15 deletions(-) create mode 100644 crates/ui-egui/src/panels/effect_presets.rs create mode 100644 crates/ui-egui/src/panels/effect_presets_tests.rs diff --git a/crates/engine/src/cmd/shape.rs b/crates/engine/src/cmd/shape.rs index f3d41640..d52a0320 100644 --- a/crates/engine/src/cmd/shape.rs +++ b/crates/engine/src/cmd/shape.rs @@ -121,7 +121,7 @@ pub fn specs() -> Vec { "Shape Effects", ["Shape Format", "Shape Styles"], None, - "{shadow?: none|outer|inner|perspective|{color, blur, dist, dir}, glow?: none|{color, radius}, softEdges?: pt, reflection?: none|tight|half|full, bevel?: none|circle|…, threeD?: {camera?: preset, rotX?, rotY?, rotZ?: deg, z?: pt, bevelTop?, bevelBottom?: none|circle|…, bevelTopW?, bevelTopH?, bevelBottomW?, bevelBottomH?, depth?, contour?: pt, material?, lighting?, reset?: true}, shadowProps?: {color?, transparency?: 0..1, blur?: pt, dist?: pt, dir?: deg}, glowProps?: {color?, transparency?, radius?: pt}, reflectionProps?: {transparency?, size?: 0..1, dist?: pt, blur?: pt}, reset?, ids?} (the *Props keys adjust the current effect, adding a default one when there is none)", + "{shadow?: none|outer|inner|perspective|{color, blur, dist, dir}, glow?: none|{color, radius}, softEdges?: pt, reflection?: none|tight|half|full, bevel?: none|circle|…, threeD?: {camera?: preset, rotX?, rotY?, rotZ?: deg, z?: pt, bevelTop?, bevelBottom?: none|circle|…, bevelTopW?, bevelTopH?, bevelBottomW?, bevelBottomH?, depth?, contour?: pt, material?, lighting?, reset?: true}, shadowProps?: {color?, transparency?: 0..1, size?: 0.01..2 (1 = 100%), blur?: pt, dist?: pt, dir?: deg}, glowProps?: {color?, transparency?, radius?: pt}, reflectionProps?: {transparency?, size?: 0..1, dist?: pt, blur?: pt}, reset?, ids?} (the *Props keys adjust the current effect, adding a default one when there is none)", has_selection, effects ), diff --git a/crates/engine/src/cmd/shape/appearance.rs b/crates/engine/src/cmd/shape/appearance.rs index b1f33e62..70825561 100644 --- a/crates/engine/src/cmd/shape/appearance.rs +++ b/crates/engine/src/cmd/shape/appearance.rs @@ -397,6 +397,12 @@ pub(super) fn effects(s: &mut Session, p: &Value) -> Result { if let Some(d) = num(v, "dir") { x.dir = d.rem_euclid(360.0); } + // PowerPoint's Size: 0–200%, both axes; a zero scale would collapse the shadow's matrix. + if let Some(z) = num(v, "size") { + let z = z.clamp(0.01, 2.0); + x.sx = z; + x.sy = z; + } *slot = Some(x); } if let Some(v) = &glow_props { diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 4dfc21f4..9e0694b7 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -2320,6 +2320,15 @@ fn shape_effects_props_adjust_shadow_glow_and_reflection() { let f = fx(&s); assert!(f.outer_shadow.is_none()); assert_eq!(f.inner_shadow.unwrap().blur, 3.0); + // Size scales both axes (PowerPoint's 0–200%); out-of-range values clamp. + s.execute("shape.effects", &json!({"shadow": "inner"})).unwrap(); + s.execute("shape.effects", &json!({"shadowProps": {"size": 1.5}})).unwrap(); + let sw = fx(&s).inner_shadow.unwrap(); + assert_eq!((sw.sx, sw.sy), (1.5, 1.5)); + s.execute("shape.effects", &json!({"shadowProps": {"size": 9}})).unwrap(); + assert_eq!(fx(&s).inner_shadow.unwrap().sx, 2.0); + s.execute("shape.effects", &json!({"shadowProps": {"size": 0}})).unwrap(); + assert_eq!(fx(&s).inner_shadow.unwrap().sy, 0.01); // Glow: size, colour, transparency; size 0 removes it. s.execute("shape.effects", &json!({"glowProps": {"radius": 12, "transparency": 0.5}})).unwrap(); let g = fx(&s).glow.unwrap(); diff --git a/crates/ui-egui/src/panels.rs b/crates/ui-egui/src/panels.rs index e31e5298..482fb95d 100644 --- a/crates/ui-egui/src/panels.rs +++ b/crates/ui-egui/src/panels.rs @@ -14,6 +14,7 @@ mod animation; mod background; mod comments; mod designer; +mod effect_presets; mod format_shape; mod smartart; mod thesaurus; @@ -121,6 +122,9 @@ pub mod thumbs; #[cfg(test)] mod background_tests; +#[cfg(test)] +mod effect_presets_tests; + #[cfg(test)] mod pattern_fill_tests; diff --git a/crates/ui-egui/src/panels/effect_presets.rs b/crates/ui-egui/src/panels/effect_presets.rs new file mode 100644 index 00000000..3d7b8f7a --- /dev/null +++ b/crates/ui-egui/src/panels/effect_presets.rs @@ -0,0 +1,202 @@ +//! Format Shape ▸ Effects: preset tiles that draw what each Shadow, Glow and Soft Edges preset does +//! to a small card, so the choice is made by looking rather than by reading names. + +use deckcraft_model::style::Effects; +use egui::{Color32, CornerRadius, Painter, Rect, Role, Sense, Stroke, StrokeKind, Ui, WidgetInfo, pos2, vec2}; + +use crate::theme::Tokens; + +const TILE: egui::Vec2 = vec2(46.0, 38.0); +const SHADOWS: [(&str, &str); 6] = [ + ("none", "No shadow"), + ("outer", "Outer shadow"), + ("bottom", "Bottom shadow"), + ("center", "Centered shadow"), + ("inner", "Inner shadow"), + ("perspective", "Perspective shadow"), +]; +/// The glow radii (pt) the ribbon's Glow menu offers. +pub(super) const GLOWS: [f64; 4] = [5.0, 8.0, 11.0, 18.0]; +/// The soft-edge radii (pt) the ribbon's Soft Edges menu offers; 0 is "none". +pub(super) const SOFT_EDGES: [f64; 6] = [0.0, 1.0, 2.5, 5.0, 10.0, 25.0]; + +/// The `shape.effects` shadow preset id that `fx`'s current shadow came from. +pub(super) fn shadow_preset_id(fx: &Effects) -> &'static str { + if fx.inner_shadow.is_some() { + return "inner"; + } + match &fx.outer_shadow { + None => "none", + Some(s) if s.align == "bl" => "perspective", + Some(s) if s.dist == 0.0 => "center", + Some(s) if s.dir == 90.0 => "bottom", + Some(_) => "outer", + } +} + +/// A row of shadow preset tiles; returns the id of the one clicked. +pub(super) fn shadow_row(ui: &mut Ui, current: &str) -> Option<&'static str> { + let mut picked = None; + ui.horizontal_wrapped(|ui| { + for (id, label) in SHADOWS { + if tile(ui, label, id == current, |p, r| paint_shadow(p, r, id)).clicked() { + picked = Some(id); + } + } + }); + picked +} + +/// A row of glow tiles (the first clears it); returns the radius clicked, 0 for none. +pub(super) fn glow_row(ui: &mut Ui, current: f64) -> Option { + let mut picked = None; + ui.horizontal_wrapped(|ui| { + for r in std::iter::once(0.0).chain(GLOWS) { + let label = if r == 0.0 { "No glow".to_string() } else { format!("{r} pt glow") }; + if tile(ui, &label, r == current, |p, rect| paint_glow(p, rect, r)).clicked() { + picked = Some(r); + } + } + }); + picked +} + +/// A row of soft-edge tiles (the first clears it); returns the radius clicked, 0 for none. +pub(super) fn soft_edge_row(ui: &mut Ui, current: f64) -> Option { + let mut picked = None; + ui.horizontal_wrapped(|ui| { + for r in SOFT_EDGES { + let label = if r == 0.0 { "No soft edges".to_string() } else { format!("{r} pt soft edges") }; + if tile(ui, &label, r == current, |p, rect| paint_soft(p, rect, r)).clicked() { + picked = Some(r); + } + } + }); + picked +} + +fn tile(ui: &mut Ui, label: &str, selected: bool, paint: impl FnOnce(&Painter, Rect)) -> egui::Response { + let t = Tokens::get(ui.ctx()); + let (rect, resp) = ui.allocate_exact_size(TILE, Sense::click()); + resp.widget_info(|| WidgetInfo::selected(Role::Button, true, selected, label)); + let p = ui.painter(); + let round = CornerRadius::same(5); + p.rect_filled(rect, round, if resp.hovered() { t.hover } else { t.field_bg }); + paint(&p.with_clip_rect(rect.shrink(1.0)), rect); + let stroke = if selected { Stroke::new(2.0, t.accent) } else { Stroke::new(1.0, t.border) }; + p.rect_stroke(rect, round, stroke, StrokeKind::Inside); + resp.on_hover_text(label) +} + +/// The sample card every preview draws on, centred in `tile`. +fn card(tile: Rect) -> Rect { + Rect::from_center_size(tile.center(), vec2(22.0, 16.0)) +} + +fn card_fill(p: &Painter, r: Rect) { + p.rect_filled(r, CornerRadius::same(2), Tokens::get(p.ctx()).accent); +} + +/// `layers` rounded rects growing from `r` by up to `spread`, each faint, so the edge falls off. +fn halo(p: &Painter, r: Rect, spread: f32, color: Color32, layers: u32) { + for i in (1..=layers).rev() { + let k = i as f32 / layers as f32; + let a = (f32::from(color.a()) * (1.0 - k * 0.8) / layers as f32 * 2.0).min(255.0) as u8; + p.rect_filled( + r.expand(spread * k), + CornerRadius::same((2.0 + spread * k) as u8), + Color32::from_rgba_unmultiplied(color.r(), color.g(), color.b(), a), + ); + } +} + +fn paint_shadow(p: &Painter, tile: Rect, id: &str) { + let r = card(tile); + let ink = Color32::from_rgba_unmultiplied(0, 0, 0, 120); + match id { + "outer" => halo(p, r.translate(vec2(3.0, 3.0)), 3.0, ink, 4), + "bottom" => halo(p, r.translate(vec2(0.0, 4.0)), 3.0, ink, 4), + "center" => halo(p, r, 5.0, ink, 5), + "perspective" => { + let base = r.max.y + 1.0; + let slant = [pos2(r.min.x - 6.0, base), pos2(r.max.x - 8.0, base), pos2(r.max.x - 2.0, base + 5.0), pos2(r.min.x - 12.0, base + 5.0)]; + p.add(egui::Shape::convex_polygon(slant.to_vec(), Color32::from_rgba_unmultiplied(0, 0, 0, 90), Stroke::NONE)); + } + _ => {} + } + card_fill(p, r); + if id == "inner" { + for i in 0..4 { + let a = 110 - i * 25; + p.rect_stroke( + r.shrink(i as f32), + CornerRadius::same(2), + Stroke::new(1.0, Color32::from_rgba_unmultiplied(0, 0, 0, a as u8)), + StrokeKind::Inside, + ); + } + } + if id == "none" { + p.line_segment([tile.left_bottom() + vec2(6.0, -6.0), tile.right_top() + vec2(-6.0, 6.0)], Stroke::new(1.5, Tokens::get(p.ctx()).text_faint)); + } +} + +fn paint_glow(p: &Painter, tile: Rect, radius: f64) { + let r = card(tile); + if radius > 0.0 { + // 18 pt fills the tile's margin; smaller radii scale down. + halo(p, r, (radius as f32 / 18.0 * 9.0).max(2.0), Tokens::get(p.ctx()).accent.gamma_multiply(0.9), 6); + } + card_fill(p, r); + if radius == 0.0 { + p.line_segment([tile.left_bottom() + vec2(6.0, -6.0), tile.right_top() + vec2(-6.0, 6.0)], Stroke::new(1.5, Tokens::get(p.ctx()).text_faint)); + } +} + +fn paint_soft(p: &Painter, tile: Rect, radius: f64) { + let t = Tokens::get(p.ctx()); + let r = card(tile).expand(2.0); + card_fill(p, r); + // Rings of the tile's own background, opaque at the edge and clear inward, feather the rim. + let depth = (radius as f32 / 25.0 * 7.0).round().max(if radius > 0.0 { 1.0 } else { 0.0 }) as u32; + let bg = t.field_bg; + for i in 0..depth { + let a = (255.0 * (1.0 - i as f32 / depth as f32)) as u8; + let c = Color32::from_rgba_unmultiplied(bg.r(), bg.g(), bg.b(), a); + p.rect_stroke(r.shrink(i as f32), CornerRadius::same(2), Stroke::new(1.0, c), StrokeKind::Inside); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use deckcraft_model::style::Shadow; + use deckcraft_model::{ColorRef, SchemeSlot}; + + fn shadow(dist: f64, dir: f64, align: &str, inner: bool) -> Shadow { + Shadow { + color: ColorRef::scheme(SchemeSlot::Dk1), + blur: 4.0, + dist, + dir, + inner, + sx: 1.0, + sy: 1.0, + kx: 0.0, + ky: 0.0, + align: align.into(), + rotate_with_shape: false, + } + } + + #[test] + fn shadow_preset_id_names_each_preset() { + let with = |o: Option, i: Option| Effects { outer_shadow: o, inner_shadow: i, ..Effects::default() }; + assert_eq!(shadow_preset_id(&with(None, None)), "none"); + assert_eq!(shadow_preset_id(&with(Some(shadow(3.0, 45.0, "tl", false)), None)), "outer"); + assert_eq!(shadow_preset_id(&with(Some(shadow(3.0, 90.0, "tl", false)), None)), "bottom"); + assert_eq!(shadow_preset_id(&with(Some(shadow(0.0, 45.0, "tl", false)), None)), "center"); + assert_eq!(shadow_preset_id(&with(Some(shadow(0.0, 90.0, "bl", false)), None)), "perspective"); + assert_eq!(shadow_preset_id(&with(None, Some(shadow(3.0, 45.0, "tl", true)))), "inner"); + } +} diff --git a/crates/ui-egui/src/panels/effect_presets_tests.rs b/crates/ui-egui/src/panels/effect_presets_tests.rs new file mode 100644 index 00000000..166a4477 --- /dev/null +++ b/crates/ui-egui/src/panels/effect_presets_tests.rs @@ -0,0 +1,73 @@ +//! Format Shape ▸ Effects: the Shadow, Glow and Soft Edges preset tiles. + +use deckcraft_engine::Session; +use deckcraft_model::style::Effects; +use egui::accesskit::Toggled; +use egui_kittest::Harness; +use egui_kittest::kittest::{NodeT, Queryable}; +use serde_json::json; + +use crate::{Services, SlideApp}; + +fn harness() -> Harness<'static, SlideApp> { + let mut s = Session::with_new(); + let r = s.execute("shape.insert", &json!({"preset": "rect", "rect": [80.0, 80.0, 200.0, 120.0]})).unwrap(); + let id = r.get("id").and_then(|v| v.as_u64()).unwrap(); + s.execute("edit.select", &json!({"ids": [id]})).unwrap(); + let mut app = SlideApp::new(s, Services::default()); + app.ui.pane = Some("format".into()); + app.ui.format_tab = "effects".into(); + let mut h = Harness::builder().with_size(egui::vec2(1800.0, 1100.0)).build_ui_state( + |ui, app: &mut SlideApp| { + app.logic(ui.ctx()); + app.ui(ui); + }, + app, + ); + h.run_steps(3); + h +} + +fn effects(h: &Harness<'_, SlideApp>) -> Effects { + h.state().session.active().unwrap().selected_shapes()[0].effects.clone().unwrap_or_default() +} + +fn click(h: &mut Harness<'_, SlideApp>, label: &str) { + h.get_by_label(label).click(); + h.run_steps(3); +} + +#[test] +fn preset_tiles_set_shadow_glow_and_soft_edges() { + let mut h = harness(); + click(&mut h, "Bottom shadow"); + let sw = effects(&h).outer_shadow.unwrap(); + assert_eq!((sw.dir, sw.dist), (90.0, 3.0)); + // The Size slider appears once there is a shadow. + assert!(h.query_all_by_label("Size").count() >= 2); + + click(&mut h, "Inner shadow"); + let fx = effects(&h); + assert!(fx.outer_shadow.is_none() && fx.inner_shadow.is_some()); + click(&mut h, "No shadow"); + let fx = effects(&h); + assert!(fx.outer_shadow.is_none() && fx.inner_shadow.is_none()); + + click(&mut h, "18 pt glow"); + assert_eq!(effects(&h).glow.unwrap().radius, 18.0); + click(&mut h, "No glow"); + assert!(effects(&h).glow.is_none()); + + click(&mut h, "10 pt soft edges"); + assert_eq!(effects(&h).soft_edge, Some(10.0)); + click(&mut h, "No soft edges"); + assert_eq!(effects(&h).soft_edge, None); +} + +#[test] +fn the_current_preset_tile_is_marked_selected() { + let mut h = harness(); + click(&mut h, "5 pt glow"); + assert_eq!(h.get_by_label("5 pt glow").accesskit_node().toggled(), Some(Toggled::True)); + assert_eq!(h.get_by_label("11 pt glow").accesskit_node().toggled(), Some(Toggled::False)); +} diff --git a/crates/ui-egui/src/panels/format_shape.rs b/crates/ui-egui/src/panels/format_shape.rs index 77b26bf4..c527ea8c 100644 --- a/crates/ui-egui/src/panels/format_shape.rs +++ b/crates/ui-egui/src/panels/format_shape.rs @@ -1,3 +1,4 @@ +use super::effect_presets; use super::*; /// Percent transparency (0 = opaque) of a solid fill, the same number `shape.fill`'s @@ -56,21 +57,10 @@ pub(super) fn format_shape(app: &mut SlideApp, ui: &mut Ui) { match app.ui.format_tab.as_str() { "effects" => { ui.label(egui::RichText::new("Shadow").font(theme::bold(13.0))); - ui.horizontal_wrapped(|ui| { - for (l, v) in [ - ("None", "none"), - ("Outer", "outer"), - ("Bottom", "bottom"), - ("Center", "center"), - ("Inner", "inner"), - ("Perspective", "perspective"), - ] { - if ui.button(l).clicked() { - run(app, "shape.effects", json!({"shadow": v})); - } - } - }); let fx = sh.effects.clone().unwrap_or_default(); + if let Some(v) = effect_presets::shadow_row(ui, effect_presets::shadow_preset_id(&fx)) { + run(app, "shape.effects", json!({"shadow": v})); + } let shadow = fx.outer_shadow.as_ref().or(fx.inner_shadow.as_ref()); if let Some(sw) = shadow { color_button(app, ui, "Color", None, |c| ("shape.effects".into(), json!({"shadowProps": {"color": c.as_ref().map(cref_param)}}))); @@ -86,6 +76,7 @@ pub(super) fn format_shape(app: &mut SlideApp, ui: &mut Ui) { "transparency", 0.01, ); + fx_slider(app, ui, ("shadow", sh.id.0, "s"), "Size", sw.sx * 100.0, 1.0..=200.0, "%", "shadowProps", "size", 0.01); fx_slider(app, ui, ("shadow", sh.id.0, "b"), "Blur", sw.blur, 0.0..=100.0, " pt", "shadowProps", "blur", 1.0); fx_slider(app, ui, ("shadow", sh.id.0, "a"), "Angle", sw.dir, 0.0..=359.0, "°", "shadowProps", "dir", 1.0); fx_slider(app, ui, ("shadow", sh.id.0, "d"), "Distance", sw.dist, 0.0..=200.0, " pt", "shadowProps", "dist", 1.0); @@ -119,6 +110,9 @@ pub(super) fn format_shape(app: &mut SlideApp, ui: &mut Ui) { ui.add_space(6.0); ui.label(egui::RichText::new("Glow").font(theme::bold(13.0))); let radius = fx.glow.as_ref().map_or(0.0, |g| g.radius); + if let Some(r) = effect_presets::glow_row(ui, radius) { + run(app, "shape.effects", json!({"glow": if r > 0.0 { json!(r) } else { Value::Null }})); + } fx_slider(app, ui, ("glow", sh.id.0, "r"), "Size", radius, 0.0..=150.0, " pt", "glowProps", "radius", 1.0); if let Some(g) = &fx.glow { color_button(app, ui, "Color", None, |c| ("shape.effects".into(), json!({"glowProps": {"color": c.as_ref().map(cref_param)}}))); @@ -137,6 +131,9 @@ pub(super) fn format_shape(app: &mut SlideApp, ui: &mut Ui) { } ui.label(egui::RichText::new("Soft Edges").font(theme::bold(13.0))); let mut s = sh.effects.as_ref().and_then(|e| e.soft_edge).unwrap_or(0.0); + if let Some(r) = effect_presets::soft_edge_row(ui, s) { + run(app, "shape.effects", json!({"softEdges": r})); + } if ui.add(egui::Slider::new(&mut s, 0.0..=50.0).text("size (pt)")).drag_stopped() { run(app, "shape.effects", json!({"softEdges": s})); } From 322be04e7bff2be9ba9d573bc3fe124ceb852fe7 Mon Sep 17 00:00:00 2001 From: Harrison Booth <73434116+Harrison-Booth@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:55:03 -0500 Subject: [PATCH 2/4] Format Shape: a glow size tile keeps the glow's colour and transparency --- crates/ui-egui/src/panels/format_shape.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ui-egui/src/panels/format_shape.rs b/crates/ui-egui/src/panels/format_shape.rs index c527ea8c..de54c831 100644 --- a/crates/ui-egui/src/panels/format_shape.rs +++ b/crates/ui-egui/src/panels/format_shape.rs @@ -111,7 +111,7 @@ pub(super) fn format_shape(app: &mut SlideApp, ui: &mut Ui) { ui.label(egui::RichText::new("Glow").font(theme::bold(13.0))); let radius = fx.glow.as_ref().map_or(0.0, |g| g.radius); if let Some(r) = effect_presets::glow_row(ui, radius) { - run(app, "shape.effects", json!({"glow": if r > 0.0 { json!(r) } else { Value::Null }})); + run(app, "shape.effects", json!({"glowProps": {"radius": r}})); } fx_slider(app, ui, ("glow", sh.id.0, "r"), "Size", radius, 0.0..=150.0, " pt", "glowProps", "radius", 1.0); if let Some(g) = &fx.glow { From 5638b0ca099fb760594dd2b384ceff029a8bfab6 Mon Sep 17 00:00:00 2001 From: Harrison Booth <73434116+Harrison-Booth@users.noreply.github.com> Date: Tue, 27 Oct 2026 21:11:54 -0500 Subject: [PATCH 3/4] Format Shape: the glow size test checks the colour and transparency are kept --- crates/ui-egui/src/panels/effect_presets_tests.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/panels/effect_presets_tests.rs b/crates/ui-egui/src/panels/effect_presets_tests.rs index 166a4477..d23651f2 100644 --- a/crates/ui-egui/src/panels/effect_presets_tests.rs +++ b/crates/ui-egui/src/panels/effect_presets_tests.rs @@ -53,8 +53,14 @@ fn preset_tiles_set_shadow_glow_and_soft_edges() { let fx = effects(&h); assert!(fx.outer_shadow.is_none() && fx.inner_shadow.is_none()); + // A radius tile keeps the glow's colour and transparency. + h.state_mut().session.execute("shape.effects", &json!({"glowProps": {"color": "accent2", "transparency": 0.35, "radius": 8.0}})).unwrap(); + h.run_steps(3); + let colour = effects(&h).glow.unwrap().color; click(&mut h, "18 pt glow"); - assert_eq!(effects(&h).glow.unwrap().radius, 18.0); + let glow = effects(&h).glow.unwrap(); + assert_eq!(glow.radius, 18.0); + assert_eq!(glow.color, colour); click(&mut h, "No glow"); assert!(effects(&h).glow.is_none()); From 9c24a63d1ef599c6223a6e48d02eca4f99690b16 Mon Sep 17 00:00:00 2001 From: Harrison Booth <73434116+Harrison-Booth@users.noreply.github.com> Date: Tue, 27 Oct 2026 21:28:23 -0500 Subject: [PATCH 4/4] Format Shape: the shadow detail controls read the inner shadow first, as the preset row does --- crates/ui-egui/src/panels/format_shape.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ui-egui/src/panels/format_shape.rs b/crates/ui-egui/src/panels/format_shape.rs index de54c831..ba4c4b69 100644 --- a/crates/ui-egui/src/panels/format_shape.rs +++ b/crates/ui-egui/src/panels/format_shape.rs @@ -61,7 +61,7 @@ pub(super) fn format_shape(app: &mut SlideApp, ui: &mut Ui) { if let Some(v) = effect_presets::shadow_row(ui, effect_presets::shadow_preset_id(&fx)) { run(app, "shape.effects", json!({"shadow": v})); } - let shadow = fx.outer_shadow.as_ref().or(fx.inner_shadow.as_ref()); + let shadow = fx.inner_shadow.as_ref().or(fx.outer_shadow.as_ref()); if let Some(sw) = shadow { color_button(app, ui, "Color", None, |c| ("shape.effects".into(), json!({"shadowProps": {"color": c.as_ref().map(cref_param)}}))); fx_slider(