From a055ebcbc73cb7f78034ab876d983ca31346ce00 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sun, 1 Nov 2026 13:30:34 -0800 Subject: [PATCH 1/2] Fix #3818: the chosen brightness holds when the system theme differs The theme's colors went into egui's style for the current system theme only. When the system's light or dark setting differed from it or changed while the app ran, egui switched to its other style, which held egui's defaults: tool tips became a white box with gray text, or with the theme's light text. Applying a brightness now sets egui's theme to match, starting from the current style so settings such as the tool tip delay carry over. --- crates/ui-egui/src/theme.rs | 60 +++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/crates/ui-egui/src/theme.rs b/crates/ui-egui/src/theme.rs index e6c06e5dd..3b1a0524d 100644 --- a/crates/ui-egui/src/theme.rs +++ b/crates/ui-egui/src/theme.rs @@ -587,6 +587,15 @@ pub fn apply(ctx: &egui::Context, b: Brightness) { pub fn apply_with(ctx: &egui::Context, b: Brightness, accent: Accent, scheme: Scheme) { let t = Tokens::for_scheme(scheme, b, accent); ctx.data_mut(|d| d.insert_temp(egui::Id::NULL, t)); + // egui keeps a style for each system theme and by default follows the system's setting. The + // chosen brightness sets egui's theme, so these colors stay in force when the system's setting + // differs or changes. A change of theme starts from the current style, which keeps settings + // such as the tool tip delay. + let theme = if t.dark { egui::Theme::Dark } else { egui::Theme::Light }; + if ctx.theme() != theme { + ctx.set_style_of(theme, ctx.global_style()); + } + ctx.set_theme(theme); let mut v = if t.dark { Visuals::dark() } else { Visuals::light() }; v.panel_fill = t.panel; v.window_fill = t.panel; @@ -669,4 +678,55 @@ mod tests { assert_eq!(Scheme::parse(Scheme::Classic.id()), Some(Scheme::Classic)); } } + + /// #3818: the brightness chosen in Preferences stays in force when the system's light or dark + /// setting differs from it or changes while the app runs. Tool tips keep the theme's panel fill + /// and text color instead of egui's own white box and gray text. + #[test] + fn the_theme_holds_whatever_the_system_theme() { + let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(400.0, 400.0)); + for b in Brightness::ALL { + for scheme in Scheme::ALL { + for (first, then) in [(egui::Theme::Dark, egui::Theme::Light), (egui::Theme::Light, egui::Theme::Dark)] { + let t = Tokens::for_scheme(scheme, b, Accent::default()); + let ctx = egui::Context::default(); + let mut time = 0.0; + let mut tip = None; + for i in 0..5 { + time += 0.5; + let system_theme = Some(if i == 0 { first } else { then }); + let events = if i == 1 { vec![egui::Event::PointerMoved(egui::pos2(30.0, 15.0))] } else { vec![] }; + let input = egui::RawInput { time: Some(time), screen_rect: Some(screen), events, system_theme, ..Default::default() }; + let mut out = ctx.run_ui(input, |ui| { + if i == 0 { + apply_with(ui.ctx(), b, Accent::default(), scheme); + ui.ctx().global_style_mut(|s| s.interaction.tooltip_delay = 0.0); + } + ui.button("Hover me").on_hover_text("Tip text"); + }); + out.textures_delta.clear(); + tip = out.shapes.iter().find_map(|c| match &c.shape { + egui::Shape::Text(s) if s.galley.text() == "Tip text" => Some(s.fallback_color), + _ => None, + }); + } + let v = ctx.global_style().visuals.clone(); + assert_eq!((v.window_fill, v.override_text_color), (t.panel, Some(t.text)), "{scheme:?} {b:?}, system {first:?} then {then:?}"); + assert_eq!(tip, Some(t.text), "{scheme:?} {b:?}, system {first:?} then {then:?}: the tool tip's text"); + } + } + } + } + + /// Changing brightness keeps the interface settings made after the theme was applied, such as + /// General › Show Tool Tips' delay. + #[test] + fn changing_brightness_keeps_other_interface_settings() { + let ctx = egui::Context::default(); + apply(&ctx, Brightness::Dark); + ctx.global_style_mut(|s| s.interaction.tooltip_delay = f32::INFINITY); + apply(&ctx, Brightness::Light); + assert_eq!(ctx.global_style().interaction.tooltip_delay, f32::INFINITY); + assert_eq!(ctx.global_style().visuals.window_fill, Tokens::for_brightness(Brightness::Light).panel); + } } From 0067aee519d9b447b99f112a9c3f6330b34e42ca Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sun, 1 Nov 2026 14:45:35 -0800 Subject: [PATCH 2/2] Tests: the tool tip's box keeps the theme's fill and contrast The test for #3818 checked the tool tip's text color and the style's window fill. It now also checks the fill of the box drawn behind the tool tip's text, and that the text and the box have a contrast ratio of at least 4.5, for every brightness and scheme. --- crates/ui-egui/src/theme.rs | 39 +++++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/crates/ui-egui/src/theme.rs b/crates/ui-egui/src/theme.rs index 3b1a0524d..0ca446b4d 100644 --- a/crates/ui-egui/src/theme.rs +++ b/crates/ui-egui/src/theme.rs @@ -705,19 +705,50 @@ mod tests { ui.button("Hover me").on_hover_text("Tip text"); }); out.textures_delta.clear(); - tip = out.shapes.iter().find_map(|c| match &c.shape { - egui::Shape::Text(s) if s.galley.text() == "Tip text" => Some(s.fallback_color), + // The tool tip's text color, and the fill of the box drawn behind it. + let mut shapes: Vec<&egui::Shape> = Vec::new(); + let mut open: Vec<&egui::Shape> = out.shapes.iter().rev().map(|c| &c.shape).collect(); + while let Some(shape) = open.pop() { + match shape { + egui::Shape::Vec(inner) => open.extend(inner.iter().rev()), + shape => shapes.push(shape), + } + } + tip = shapes.iter().enumerate().find_map(|(n, shape)| match shape { + egui::Shape::Text(s) if s.galley.text() == "Tip text" => { + let fill = shapes[..n].iter().rev().find_map(|behind| match behind { + egui::Shape::Rect(r) if r.rect.contains(s.pos) && r.fill.a() > 0 => Some(r.fill), + _ => None, + }); + Some((s.fallback_color, fill)) + } _ => None, }); } let v = ctx.global_style().visuals.clone(); - assert_eq!((v.window_fill, v.override_text_color), (t.panel, Some(t.text)), "{scheme:?} {b:?}, system {first:?} then {then:?}"); - assert_eq!(tip, Some(t.text), "{scheme:?} {b:?}, system {first:?} then {then:?}: the tool tip's text"); + let what = format!("{scheme:?} {b:?}, system {first:?} then {then:?}"); + assert_eq!((v.window_fill, v.override_text_color), (t.panel, Some(t.text)), "{what}"); + assert_eq!(tip, Some((t.text, Some(t.panel))), "{what}: the tool tip's text and box"); + assert!(contrast(t.text, t.panel) >= 4.5, "{what}: the tool tip's text against its box, {}", contrast(t.text, t.panel)); } } } } + /// The contrast ratio of two opaque colors, from 1 (the same) to 21 (black on white), as WCAG 2 + /// defines it. + fn contrast(a: egui::Color32, b: egui::Color32) -> f32 { + let luminance = |c: egui::Color32| { + let channel = |v: u8| { + let v = f32::from(v) / 255.0; + if v <= 0.040_45 { v / 12.92 } else { ((v + 0.055) / 1.055).powf(2.4) } + }; + 0.2126 * channel(c.r()) + 0.7152 * channel(c.g()) + 0.0722 * channel(c.b()) + }; + let (a, b) = (luminance(a), luminance(b)); + (a.max(b) + 0.05) / (a.min(b) + 0.05) + } + /// Changing brightness keeps the interface settings made after the theme was applied, such as /// General › Show Tool Tips' delay. #[test]