From 59b7247ea71b3473913bff09e2bf297cdf0fc6ec Mon Sep 17 00:00:00 2001 From: insideTheFlux Date: Mon, 2 Nov 2026 18:27:36 -0800 Subject: [PATCH 1/2] LR-BEHAV-TITLEBAR: the Windows title bar takes LightCraft's theme at launch (#3381) When LightCraft's appearance differs from Windows' (its dark default on a light Windows), the first theme egui sends the just-shown, focused window changed the title's colour but not the caption's: white on a light caption, which looks blank, until focus moved. The same command sent once the window is up repaints the caption, so the UI phase sends it again the first frame the window has focus (Windows only). With a test, which fails without the send, and a What's New entry. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01CvoftKWm1wYSXdzRBwvFR3 --- crates/ui-egui/src/app/frame.rs | 1 + crates/ui-egui/src/theme.rs | 41 +++++++++++++++++++++++++++++++++ docs/whats-new.md | 7 ++++++ 3 files changed, 49 insertions(+) diff --git a/crates/ui-egui/src/app/frame.rs b/crates/ui-egui/src/app/frame.rs index a434881ae5..a757af1aef 100644 --- a/crates/ui-egui/src/app/frame.rs +++ b/crates/ui-egui/src/app/frame.rs @@ -263,6 +263,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..9f1873521b 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,28 @@ mod tests { } } + /// #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| { + let raw = egui::RawInput { focused, ..Default::default() }; + let mut out = ctx.run_ui(raw, |ui| 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() + }; + 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"); + } + /// 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 98a0ac7157..5289b03b66 100644 --- a/docs/whats-new.md +++ b/docs/whats-new.md @@ -1,5 +1,12 @@ # What's new in LightCraft +## 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-10-30 ### The Flatpak may talk to the accessibility bus From bd2f8e9670970b3f54393829b0f5af6a1fe3f49f Mon Sep 17 00:00:00 2001 From: insideTheFlux Date: Mon, 2 Nov 2026 18:58:42 -0800 Subject: [PATCH 2/2] LR-BEHAV-TITLEBAR: test a window that has focus from its first frame (#3381) A fresh context whose first frame is focused sends the theme once more than egui's own command in that frame, compared with the same frame without the refresh rather than with a fixed count (CodeRabbit). Both tests share a helper that counts the frame's SetTheme(Light) commands. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01CvoftKWm1wYSXdzRBwvFR3 --- crates/ui-egui/src/theme.rs | 37 ++++++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/crates/ui-egui/src/theme.rs b/crates/ui-egui/src/theme.rs index 9f1873521b..de2be96a6c 100644 --- a/crates/ui-egui/src/theme.rs +++ b/crates/ui-egui/src/theme.rs @@ -525,6 +525,21 @@ 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")] @@ -532,13 +547,7 @@ mod tests { 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| { - let raw = egui::RawInput { focused, ..Default::default() }; - let mut out = ctx.run_ui(raw, |ui| 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() - }; + 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"); @@ -547,6 +556,20 @@ mod tests { 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]