Theme: text fields show in the light theme (#1204)

In the light theme a text field had the same white fill as the dialog,
pane or ribbon it sits on and no outline (nor a visible focus outline),
so only its label or hint showed. Fill text fields with a light grey, as
the dark theme already sets them apart with a darker well. The File tab
keeps its outlined white fields.
This commit is contained in:
xc4pr
2026-10-23 02:00:59 +02:00
parent b47195feec
commit b61898bba5
4 changed files with 95 additions and 0 deletions
+1
View File
@@ -388,6 +388,7 @@ pub fn show(app: &mut SlideApp, ui: &mut Ui) {
});
egui::CentralPanel::default().frame(egui::Frame::NONE.fill(page_bg)).show(ui, |ui| {
// Fields, boxes and buttons sit on the page's own colour: outline them, as PowerPoint does.
ui.visuals_mut().text_edit_bg_color = Some(t.field_bg);
let w = &mut ui.visuals_mut().widgets;
w.inactive.bg_fill = t.field_bg;
w.inactive.weak_bg_fill = t.field_bg;
+87
View File
@@ -0,0 +1,87 @@
//! Text fields must show where they are: in the light theme the field was as white as the dialog,
//! pane or ribbon it sits on and had no outline, so only its label (or hint) was visible (#1204).
#![allow(clippy::unwrap_used, clippy::expect_used)]
use deckcraft_engine::Session;
use egui::accesskit::Role;
use egui::epaint::Shape;
use egui::{Color32, Rect};
use egui_kittest::Harness;
use egui_kittest::kittest::Queryable;
use serde_json::json;
use crate::{Services, SlideApp, theme};
fn harness(app: SlideApp) -> Harness<'static, SlideApp> {
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).with_step_dt(1.0 / 60.0).build_ui_state(
|ui, app: &mut SlideApp| {
app.logic(ui.ctx());
app.ui(ui);
},
app,
);
// The app keeps asking for frames, so step instead of run().
h.run_steps(3);
h
}
/// The largest per-channel difference between two colours.
fn distance(a: Color32, b: Color32) -> u8 {
a.r().abs_diff(b.r()).max(a.g().abs_diff(b.g())).max(a.b().abs_diff(b.b()))
}
/// 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) {
h.output()
.shapes
.iter()
.find_map(|c| match &c.shape {
Shape::Rect(r) if r.rect.min.distance(field.min) < 2.0 && r.rect.max.distance(field.max) < 2.0 => Some((r.fill, r.stroke)),
_ => None,
})
.unwrap_or_else(|| panic!("no frame painted for the field at {field:?}"))
}
/// 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,
"{what}: the field at {field:?} (fill {fill:?}, outline {stroke:?}) can't be told from the {surface:?} behind it"
);
}
}
#[test]
fn light_text_fields_show_in_dialogs_panes_and_the_ribbon() {
let light = theme::Tokens::light();
// Home › Find: "Find what:" in the dialog card.
let mut h = harness(SlideApp::new(Session::with_new(), Services::default()));
h.state_mut().run("app.find", json!({})).unwrap();
h.run_steps(3);
assert_visible(&h, "Find and Replace", h.ctx.global_style().visuals.window_fill);
// Still visible with the keyboard in it.
let field = h.get_by_role(Role::TextInput).rect();
h.get_by_role(Role::TextInput).click();
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:?}");
// Review › Thesaurus: the search box on the task pane.
let mut h = harness(SlideApp::new(Session::with_new(), Services::default()));
h.state_mut().ui.pane = Some("thesaurus".into());
h.run_steps(3);
assert_visible(&h, "Thesaurus pane", light.card);
// Transitions › Duration: the spinner on the ribbon.
let mut app = SlideApp::new(Session::with_new(), Services::default());
app.ui.tab = "Transitions".into();
let h = harness(app);
assert_visible(&h, "Transitions ribbon", light.card);
}
+2
View File
@@ -2052,6 +2052,8 @@ mod compare_tests;
#[cfg(test)]
mod eyedropper_tests;
#[cfg(test)]
mod field_frame_tests;
#[cfg(test)]
mod huge_deck_tests;
#[cfg(test)]
mod ime_tests;
+5
View File
@@ -20,6 +20,8 @@ pub struct Tokens {
/// The ribbon card and popovers.
pub card: Color32,
pub card_border: Color32,
/// Text fields. They have no outline, so the fill alone must set them apart from the card.
pub text_field: Color32,
/// Canvas around the slide.
pub canvas: Color32,
pub text: Color32,
@@ -51,6 +53,7 @@ impl Tokens {
chrome: Color32::from_rgb(0xF5, 0xF5, 0xF5),
card: Color32::WHITE,
card_border: Color32::from_rgb(0xE3, 0xE3, 0xE3),
text_field: Color32::from_rgb(0xEB, 0xEB, 0xEB),
canvas: Color32::from_rgb(0xF0, 0xF0, 0xF0),
text: Color32::from_rgb(0x24, 0x24, 0x24),
text_dim: Color32::from_rgb(0x5C, 0x5C, 0x5C),
@@ -79,6 +82,7 @@ impl Tokens {
chrome: Color32::from_rgb(0x26, 0x26, 0x26),
card: Color32::from_rgb(0x33, 0x33, 0x33),
card_border: Color32::from_rgb(0x44, 0x44, 0x44),
text_field: Color32::from_rgb(0x2A, 0x2A, 0x2A),
canvas: Color32::from_rgb(0x1E, 0x1E, 0x1E),
text: Color32::from_rgb(0xEE, 0xEE, 0xEE),
text_dim: Color32::from_rgb(0xBB, 0xBB, 0xBB),
@@ -118,6 +122,7 @@ pub fn apply(ctx: &egui::Context, t: &Tokens) {
v.panel_fill = t.chrome;
v.window_fill = t.card;
v.extreme_bg_color = t.field_bg;
v.text_edit_bg_color = Some(t.text_field);
v.faint_bg_color = t.hover;
v.window_stroke = Stroke::new(1.0, t.card_border);
v.window_corner_radius = CornerRadius::same(10);