diff --git a/crates/engine/src/cmd/mod.rs b/crates/engine/src/cmd/mod.rs index fa3f6459..fcda1413 100644 --- a/crates/engine/src/cmd/mod.rs +++ b/crates/engine/src/cmd/mod.rs @@ -371,6 +371,8 @@ pub(crate) fn ok() -> Result { } /// Parse a colour param: `"#RRGGBB"`, `"accent1"`…, or `{"scheme":"accent2","lumMod":75000}`. +/// A preset or system colour keeps its base: `{"preset":"red"}`, +/// `{"system":"windowText","last":"#000000"}`. pub(crate) fn color_param(p: &Value, key: &str) -> Option { let v = p.get(key)?; color_value(v) @@ -391,6 +393,11 @@ pub(crate) fn color_value(v: &Value) -> Option { Value::Object(o) => { let mut c = if let Some(s) = o.get("scheme").and_then(Value::as_str) { ColorRef::scheme(deckcraft_color::SchemeSlot::from_xml(s)?) + } else if let Some(name) = o.get("preset").and_then(Value::as_str) { + ColorRef { base: deckcraft_model::ColorBase::Preset { name: name.to_string() }, mods: vec![] } + } else if let Some(name) = o.get("system").and_then(Value::as_str) { + let last = deckcraft_color::Rgba::from_hex(o.get("last").and_then(Value::as_str)?)?; + ColorRef { base: deckcraft_model::ColorBase::System { name: name.to_string(), last }, mods: vec![] } } else { ColorRef::rgb(deckcraft_color::Rgba::from_hex(o.get("rgb").and_then(Value::as_str)?)?) }; @@ -639,4 +646,16 @@ mod tests { assert!(!needs_arguments(save.params)); assert!(needs_arguments(audio.params)); } + + #[test] + fn preset_and_system_colors_keep_their_base() { + use deckcraft_color::{ColorTransform, Rgba}; + use deckcraft_model::{ColorBase, ColorRef}; + use serde_json::json; + let preset = color_value(&json!({"preset": "red", "lumMod": 50_000})); + assert_eq!(preset, Some(ColorRef { base: ColorBase::Preset { name: "red".into() }, mods: vec![ColorTransform::LumMod(50_000)] })); + let system = color_value(&json!({"system": "windowText", "last": "#123456"})); + assert_eq!(system, Some(ColorRef { base: ColorBase::System { name: "windowText".into(), last: Rgba::rgb(0x12, 0x34, 0x56) }, mods: vec![] })); + assert_eq!(color_value(&json!({"system": "windowText"})), None, "a system color needs its last value"); + } } diff --git a/crates/ui-egui/src/fillui.rs b/crates/ui-egui/src/fillui.rs index f2b2bf9f..47fcfa89 100644 --- a/crates/ui-egui/src/fillui.rs +++ b/crates/ui-egui/src/fillui.rs @@ -420,7 +420,7 @@ pub fn pattern_editor(ui: &mut Ui, pt: &PatternFill, sc: &ColorScheme) -> Option let on = pt.preset == name; let (r, resp) = ui.allocate_exact_size(vec2(22.0, 22.0), Sense::click()); paint_pattern(ui.painter(), r.shrink(2.0), name, fg, bg); - let stroke = if on || resp.hovered() { Stroke::new(2.0, t.accent) } else { Stroke::new(1.0, t.border) }; + let stroke = if on || resp.hovered() || resp.has_focus() { Stroke::new(2.0, t.accent) } else { Stroke::new(1.0, t.border) }; ui.painter().rect_stroke(r.shrink(1.0), CornerRadius::same(2), stroke, egui::StrokeKind::Inside); let label = pattern_label(name); resp.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::SelectableLabel, true, on, label)); diff --git a/crates/ui-egui/src/pattern_fill_tests.rs b/crates/ui-egui/src/pattern_fill_tests.rs index 3b4a2d71..065b03f5 100644 --- a/crates/ui-egui/src/pattern_fill_tests.rs +++ b/crates/ui-egui/src/pattern_fill_tests.rs @@ -4,7 +4,7 @@ use deckcraft_color::Rgba; use deckcraft_engine::Session; use deckcraft_model::style::PatternFill; -use deckcraft_model::{Background, ColorRef, Fill, ShapeId}; +use deckcraft_model::{Background, ColorBase, ColorRef, Fill, ShapeId}; use egui::accesskit::Role; use egui::{Event, Key, Modifiers, PointerButton}; use egui_kittest::Harness; @@ -93,6 +93,45 @@ fn shape_pattern_fill_picks_a_preset_from_the_gallery() { assert_eq!(h.state().session.active().unwrap().history.undo.len(), undo + 1, "one undo step"); } +#[test] +fn shape_pattern_fill_keeps_preset_and_system_colors() { + let (mut s, id) = patterned_shape(); + let fg = json!({"preset": "red", "lumMod": 50000}); + let bg = json!({"system": "window", "last": "#F0F0F0"}); + s.execute("shape.fill", &json!({"pattern": {"preset": "pct25", "fg": fg, "bg": bg}})).unwrap(); + let mut h = harness(SlideApp::new(s, Services::default()), "format"); + let before = shape_pattern(&h, id); + assert!(matches!(before.fg.base, ColorBase::Preset { .. }) && matches!(before.bg.base, ColorBase::System { .. })); + h.get_by_label("Zig zag").click(); + h.run_steps(2); + let pt = shape_pattern(&h, id); + assert_eq!(pt.preset, "zigZag"); + assert_eq!((&pt.fg, &pt.bg), (&before.fg, &before.bg), "a preset change keeps both colors"); + pick_hex(&mut h, "Foreground color", "00FF00"); + assert_eq!(shape_pattern(&h, id).bg, before.bg, "a foreground change keeps the background"); +} + +#[test] +fn pattern_gallery_shows_which_swatch_has_keyboard_focus() { + let (s, _) = patterned_shape(); + let mut h = harness(SlideApp::new(s, Services::default()), "format"); + let r = h.get_by_label("Zig zag").rect(); + let accent = crate::theme::Tokens::get(&h.ctx).accent; + let outlined = |h: &Harness<'_, SlideApp>| { + h.output().shapes.iter().any(|c| match &c.shape { + egui::Shape::Rect(rs) => { + rs.stroke.width == 2.0 && rs.stroke.color == accent && (rs.rect.center() - r.center()).length() < 0.5 && rs.rect.width() < r.width() + } + _ => false, + }) + }; + assert!(!outlined(&h), "an unselected swatch has a plain border"); + h.get_by_label("Zig zag").focus(); + h.run_steps(2); + assert!(h.get_by_label("Zig zag").is_focused()); + assert!(outlined(&h), "the focused swatch is outlined"); +} + #[test] fn shape_solid_fill_keeps_its_transparency_without_a_pattern_gallery() { let (mut s, _) = patterned_shape(); @@ -129,3 +168,17 @@ fn background_pattern_fill_offers_every_preset_and_both_colors() { let pt = background_pattern(&h); assert_eq!((pt.preset.as_str(), pt.bg), ("zigZag", ColorRef::rgb(Rgba::from_hex("FFEE00").unwrap()))); } + +#[test] +fn background_pattern_fill_keeps_a_system_color() { + let mut s = Session::with_new(); + let bg = json!({"system": "window", "last": "#F0F0F0"}); + s.execute("design.background", &json!({"pattern": {"preset": "pct20", "fg": "accent1", "bg": bg}})).unwrap(); + let mut h = harness(SlideApp::new(s, Services::default()), "background"); + let before = background_pattern(&h).bg; + assert!(matches!(before.base, ColorBase::System { .. })); + h.get_by_label("Zig zag").click(); + h.run_steps(2); + let pt = background_pattern(&h); + assert_eq!((pt.preset.as_str(), pt.bg), ("zigZag", before)); +} diff --git a/crates/ui-egui/src/ribbon.rs b/crates/ui-egui/src/ribbon.rs index 340ca99f..f0118543 100644 --- a/crates/ui-egui/src/ribbon.rs +++ b/crates/ui-egui/src/ribbon.rs @@ -604,7 +604,27 @@ pub fn cref_param(c: &deckcraft_model::ColorRef) -> Value { } Value::Object(o) } - _ => json!("#000000"), + deckcraft_model::ColorBase::Preset { name } => { + let mut o = serde_json::Map::new(); + o.insert("preset".into(), json!(name)); + for m in &c.mods { + if let Some(v) = m.value() { + o.insert(m.xml_name().into(), json!(v)); + } + } + Value::Object(o) + } + deckcraft_model::ColorBase::System { name, last } => { + let mut o = serde_json::Map::new(); + o.insert("system".into(), json!(name)); + o.insert("last".into(), json!(format!("#{}", last.hex()))); + for m in &c.mods { + if let Some(v) = m.value() { + o.insert(m.xml_name().into(), json!(v)); + } + } + Value::Object(o) + } } }