Theme: the field test only counts a fill or outline that is actually painted (#1204)

This commit is contained in:
xc4pr
2026-10-23 18:52:55 +02:00
parent b61898bba5
commit d86f7aed75
+36 -5
View File
@@ -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<Rect> = 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()));