diff --git a/crates/ui-egui/src/field_frame_tests.rs b/crates/ui-egui/src/field_frame_tests.rs index 60ac4b74..14fdee3d 100644 --- a/crates/ui-egui/src/field_frame_tests.rs +++ b/crates/ui-egui/src/field_frame_tests.rs @@ -5,7 +5,7 @@ use deckcraft_engine::Session; use egui::accesskit::Role; use egui::epaint::Shape; -use egui::{Color32, Rect}; +use egui::{Color32, Rect, Stroke}; use egui_kittest::Harness; use egui_kittest::kittest::Queryable; use serde_json::json; @@ -31,7 +31,7 @@ fn distance(a: Color32, b: Color32) -> u8 { } /// The frame egui painted for the text field at `field` (its fill and outline). -fn frame_at(h: &Harness<'_, SlideApp>, field: Rect) -> (Color32, egui::Stroke) { +fn frame_at(h: &Harness<'_, SlideApp>, field: Rect) -> (Color32, Stroke) { h.output() .shapes .iter() @@ -42,20 +42,49 @@ fn frame_at(h: &Harness<'_, SlideApp>, field: Rect) -> (Color32, egui::Stroke) { .unwrap_or_else(|| panic!("no frame painted for the field at {field:?}")) } +/// What `c` shows when painted over the opaque `surface`. `Color32` is premultiplied, so each +/// channel is `c + surface * (255 - alpha) / 255`. +fn over(c: Color32, surface: Color32) -> Color32 { + let behind = |s: u8| (u16::from(s) * u16::from(255 - c.a()) / 255) as u8; + Color32::from_rgb(c.r().saturating_add(behind(surface.r())), c.g().saturating_add(behind(surface.g())), c.b().saturating_add(behind(surface.b()))) +} + +/// A frame with this `fill` and `stroke` stands apart from the opaque `surface`: it paints a fill +/// or an outline (at least 1 px wide) that, as actually shown over the surface, differs from it. +fn stands_apart(fill: Color32, stroke: Stroke, surface: Color32) -> bool { + let outlined = stroke.width >= 1.0 && distance(over(stroke.color, surface), surface) >= 24; + outlined || distance(over(fill, surface), surface) >= 16 +} + /// The field stands apart from `surface` by its fill or by its outline. fn assert_visible(h: &Harness<'_, SlideApp>, what: &str, surface: Color32) { let fields: Vec = h.query_all_by_role(Role::TextInput).map(|n| n.rect()).collect(); assert!(!fields.is_empty(), "{what}: no text field"); for field in fields { let (fill, stroke) = frame_at(h, field); - let outlined = stroke.width >= 1.0 && distance(stroke.color, surface) >= 24; assert!( - outlined || distance(fill, surface) >= 16, + stands_apart(fill, stroke, surface), "{what}: the field at {field:?} (fill {fill:?}, outline {stroke:?}) can't be told from the {surface:?} behind it" ); } } +#[test] +fn only_a_painted_fill_or_outline_stands_apart() { + let surface = Color32::WHITE; + // Stroke::NONE is transparent black: nothing is painted, whatever its RGB. + assert!(!stands_apart(surface, Stroke::NONE, surface)); + assert!(!stands_apart(Color32::WHITE, Stroke::NONE, Color32::from_rgb(250, 250, 250))); + // A fully transparent fill shows the surface. + assert!(!stands_apart(Color32::TRANSPARENT, Stroke::NONE, surface)); + assert!(!stands_apart(Color32::TRANSPARENT, Stroke::new(0.5, Color32::BLACK), surface)); + // A transparent outline paints nothing either. + assert!(!stands_apart(surface, Stroke::new(1.0, Color32::TRANSPARENT), surface)); + // A real 1 px dark outline, or a clearly different opaque fill, does. + assert!(stands_apart(surface, Stroke::new(1.0, Color32::from_gray(120)), surface)); + assert!(stands_apart(Color32::from_gray(200), Stroke::NONE, surface)); +} + #[test] fn light_text_fields_show_in_dialogs_panes_and_the_ribbon() { let light = theme::Tokens::light(); @@ -71,7 +100,9 @@ fn light_text_fields_show_in_dialogs_panes_and_the_ribbon() { h.run_steps(2); assert!(h.ctx.memory(|m| m.focused().is_some()), "the click puts the keyboard in the field"); let (fill, stroke) = frame_at(&h, field); - assert!(distance(fill, light.card) >= 16 || distance(stroke.color, light.card) >= 24, "focused field: fill {fill:?}, outline {stroke:?}"); + // Same surface as the dialog check above: the field sits on the dialog's window fill. + let surface = h.ctx.global_style().visuals.window_fill; + assert!(stands_apart(fill, stroke, surface), "focused field: fill {fill:?}, outline {stroke:?}"); // Review › Thesaurus: the search box on the task pane. let mut h = harness(SlideApp::new(Session::with_new(), Services::default()));