diff --git a/crates/ui-egui/src/app/frame.rs b/crates/ui-egui/src/app/frame.rs index 98593a8587..798101a7ef 100644 --- a/crates/ui-egui/src/app/frame.rs +++ b/crates/ui-egui/src/app/frame.rs @@ -265,6 +265,7 @@ impl LightcraftApp { self.import_dropped_files(ctx); } self.check_external_editor(ctx); + theme::refresh_window_on_first_focus(ctx); // Alt (Option on macOS) while a slider is dragged shows its temporary preview self.ui.alt_held = ctx.input(|i| i.modifiers.alt); // Recent-library checks and launch-list keys read focus/time/keys: they belong to the UI diff --git a/crates/ui-egui/src/theme.rs b/crates/ui-egui/src/theme.rs index 0c562f451f..de2be96a6c 100644 --- a/crates/ui-egui/src/theme.rs +++ b/crates/ui-egui/src/theme.rs @@ -412,6 +412,25 @@ pub fn current(ctx: &egui::Context) -> Option { ctx.data(|d| d.get_temp::(theme_id())) } +/// Windows only: tell the window its theme again, the first time it has focus. A first theme that +/// reaches a focused window as it comes up changes the title's colour but not the caption's, so +/// with LightCraft dark on a light system the title is white on a light caption (it looks blank), +/// and light on dark is dark on dark, until focus next moves (#3381). The same command repaints +/// the caption once the window is up. A window that opens without focus shows the right colours +/// from the start. +pub fn refresh_window_on_first_focus(ctx: &egui::Context) { + let done = egui::Id::unique("lightcraft-window-theme-refreshed"); + if !cfg!(target_os = "windows") || !ctx.input(|i| i.focused) || ctx.data(|d| d.get_temp::(done)).is_some() { + return; + } + ctx.data_mut(|d| d.insert_temp(done, true)); + let theme = match ctx.theme() { + egui::Theme::Dark => egui::SystemTheme::Dark, + egui::Theme::Light => egui::SystemTheme::Light, + }; + ctx.send_viewport_cmd(egui::ViewportCommand::SetTheme(theme)); +} + /// Style `ctx` with the default theme. pub fn apply(ctx: &egui::Context) { apply_kind(ctx, ThemeKind::default()); @@ -506,6 +525,51 @@ mod tests { } } + /// The `SetTheme(Light)` commands one frame of `ctx` sends the window, with or without + /// [`refresh_window_on_first_focus`] in it. + #[cfg(target_os = "windows")] + fn light_theme_commands(ctx: &egui::Context, focused: bool, refresh: bool) -> usize { + let raw = egui::RawInput { focused, ..Default::default() }; + let mut out = ctx.run_ui(raw, |ui| { + if refresh { + refresh_window_on_first_focus(ui.ctx()); + } + }); + out.textures_delta.clear(); + let commands = out.viewport_output.remove(&egui::ViewportId::ROOT).map(|v| v.commands).unwrap_or_default(); + commands.iter().filter(|c| matches!(c, egui::ViewportCommand::SetTheme(egui::SystemTheme::Light))).count() + } + + /// #3381: egui sends the theme with the first frame, and the window hears it once more the + /// first time it has focus, not on every later focus. + #[cfg(target_os = "windows")] + #[test] + fn the_window_hears_its_theme_again_the_first_time_it_has_focus() { + let ctx = egui::Context::default(); + apply_kind(&ctx, ThemeKind::Silver); + let sent = |focused: bool| light_theme_commands(&ctx, focused, true); + assert_eq!(sent(false), 1, "egui's own, with the first frame"); + assert_eq!(sent(false), 0, "no focus yet"); + assert_eq!(sent(true), 1, "the first frame with focus"); + assert_eq!(sent(true), 0, "still focused"); + assert_eq!(sent(false), 0, "focus lost"); + assert_eq!(sent(true), 0, "focus back: Windows repaints this by itself"); + } + + /// #3381: a window that has focus from its very first frame hears the theme once more in that + /// frame, on top of egui's own command. (In the app the first frame never gets here: `ui` + /// returns before the fonts are ready, so egui's command always goes a frame earlier.) + #[cfg(target_os = "windows")] + #[test] + fn a_window_focused_from_its_first_frame_hears_its_theme_again_in_that_frame() { + let first_frame = |refresh: bool| { + let ctx = egui::Context::default(); + apply_kind(&ctx, ThemeKind::Silver); + light_theme_commands(&ctx, true, refresh) + }; + assert_eq!(first_frame(true), first_frame(false) + 1); + } + /// Every theme scrolls 100 points a wheel notch, and a zoom notch stays egui's stock ~1.22x /// (`exp(40 / 200)`) instead of growing with the longer scroll. #[test] diff --git a/docs/whats-new.md b/docs/whats-new.md index f11c8f9e4d..98b998b59d 100644 --- a/docs/whats-new.md +++ b/docs/whats-new.md @@ -12,6 +12,13 @@ hot pixels on a sensor. Stars and other points of light that are kept don't count, so a dense star field still loses its hot pixels. Settings ▸ Performance ▸ Hot pixel limit sets the share (0.05 % to 1 %). +## 2026-11-02 + +### The Windows title bar is readable from the moment the window opens +- When LightCraft's appearance differs from the one set in Windows (the dark default on a light Windows, for one), the + title bar kept the system's colour with the title in LightCraft's, so the title looked blank until focus moved to + another window. It now takes LightCraft's colours as soon as the window has focus. + ## 2026-11-01 ### Korean and Vietnamese text in exports draws whole