Watch for OS theme changes at runtime
This commit is contained in:
Generated
+3
@@ -5699,6 +5699,7 @@ dependencies = [
|
||||
"fontdb",
|
||||
"inventory",
|
||||
"native-theme-derive",
|
||||
"notify",
|
||||
"objc2 0.6.5",
|
||||
"objc2-app-kit 0.3.2",
|
||||
"objc2-core-foundation",
|
||||
@@ -8337,11 +8338,13 @@ version = "0.2.0-alpha"
|
||||
dependencies = [
|
||||
"assets",
|
||||
"dock",
|
||||
"flume 0.11.1",
|
||||
"gpui-component",
|
||||
"gpui-pre",
|
||||
"gpui-pre-platform",
|
||||
"gpui-pre-reqwest-client",
|
||||
"metadata",
|
||||
"native-theme",
|
||||
"native-theme-gpui",
|
||||
"paths",
|
||||
"settings",
|
||||
|
||||
@@ -18,6 +18,7 @@ reqwest_client = { package = "gpui-pre-reqwest-client", version = "0.3.8" }
|
||||
gpui-component = { version = "=0.7.1", features = ["tree-sitter-languages"] }
|
||||
gpui-base = "=0.7.1"
|
||||
|
||||
native-theme = { version = "0.6.1", features = ["watch"] }
|
||||
native-theme-gpui = { version = "0.6.1", default-features = false, features = ["system-fonts"] }
|
||||
|
||||
dock = { path = "crates/dock" }
|
||||
|
||||
@@ -38,7 +38,9 @@ gpui.workspace = true
|
||||
gpui_platform.workspace = true
|
||||
dock = { workspace = true }
|
||||
gpui-component.workspace = true
|
||||
native-theme.workspace = true
|
||||
native-theme-gpui.workspace = true
|
||||
flume.workspace = true
|
||||
reqwest_client.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
|
||||
+76
-21
@@ -5,8 +5,9 @@ use dock::TAB_BAR_HEIGHT;
|
||||
use gpui::*;
|
||||
use gpui_component::{Theme, ThemeMode, ThemeRegistry, theme};
|
||||
use gpui_platform::application;
|
||||
use native_theme::watch::ThemeSubscription;
|
||||
use native_theme_gpui::{SystemTheme, apply_system_theme};
|
||||
use settings::{AppearanceMode, SettingsStore};
|
||||
use settings::{AppearanceMode, Settings, SettingsStore};
|
||||
|
||||
actions!(signed, [Quit]);
|
||||
|
||||
@@ -28,26 +29,21 @@ fn main() {
|
||||
SettingsStore::set_global(store.clone(), cx);
|
||||
|
||||
let settings = store.read(cx).settings().clone();
|
||||
let registry = ThemeRegistry::global_mut(cx);
|
||||
|
||||
for (name, content) in Assets.themes() {
|
||||
if let Err(err) = registry.load_themes_from_str(&content) {
|
||||
tracing::error!("Failed to load theme {name}: {err}");
|
||||
{
|
||||
let registry = ThemeRegistry::global_mut(cx);
|
||||
for (name, content) in Assets.themes() {
|
||||
if let Err(err) = registry.load_themes_from_str(&content) {
|
||||
tracing::error!("Failed to load theme {name}: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match SystemTheme::from_system() {
|
||||
Ok(mut system) => {
|
||||
let native = system.light.defaults.font.size;
|
||||
if native > 0.0 {
|
||||
let scaling = settings.theme.font_size / native;
|
||||
system.accessibility.text_scaling_factor *= scaling;
|
||||
}
|
||||
apply_system_theme(&system, cx);
|
||||
}
|
||||
match apply_native_theme(&settings, cx) {
|
||||
Ok(()) => start_theme_watcher(cx),
|
||||
Err(error) => {
|
||||
tracing::warn!("native theme unavailable ({error})");
|
||||
|
||||
let registry = ThemeRegistry::global_mut(cx);
|
||||
let light_theme = registry
|
||||
.themes()
|
||||
.get(settings.theme.light_theme.as_str())
|
||||
@@ -77,13 +73,13 @@ fn main() {
|
||||
} else {
|
||||
tracing::warn!("{} theme is missing", settings.theme.dark_theme);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match settings.appearance {
|
||||
AppearanceMode::System => Theme::sync_system_appearance(None, cx),
|
||||
AppearanceMode::Light => Theme::change(ThemeMode::Light, None, cx),
|
||||
AppearanceMode::Dark => Theme::change(ThemeMode::Dark, None, cx),
|
||||
match settings.appearance {
|
||||
AppearanceMode::System => Theme::sync_system_appearance(None, cx),
|
||||
AppearanceMode::Light => Theme::change(ThemeMode::Light, None, cx),
|
||||
AppearanceMode::Dark => Theme::change(ThemeMode::Dark, None, cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::fs::create_dir_all(paths::nostr_dir()).ok();
|
||||
@@ -128,3 +124,62 @@ fn main() {
|
||||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
|
||||
/// Apply the OS theme to the app, scaling the font size to match the system font size.
|
||||
fn apply_native_theme(settings: &Settings, cx: &mut App) -> native_theme::Result<()> {
|
||||
let mut system = SystemTheme::from_system()?;
|
||||
let native = system.light.defaults.font.size;
|
||||
if native > 0.0 {
|
||||
let scaling = settings.theme.font_size / native;
|
||||
system.accessibility.text_scaling_factor *= scaling;
|
||||
}
|
||||
|
||||
apply_system_theme(&system, cx);
|
||||
|
||||
match settings.appearance {
|
||||
AppearanceMode::System => Theme::sync_system_appearance(None, cx),
|
||||
AppearanceMode::Light => Theme::change(ThemeMode::Light, None, cx),
|
||||
AppearanceMode::Dark => Theme::change(ThemeMode::Dark, None, cx),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Holds the watcher subscription so it lives as long as the app.
|
||||
struct ThemeWatcher {
|
||||
_subscription: ThemeSubscription,
|
||||
}
|
||||
|
||||
impl Global for ThemeWatcher {}
|
||||
|
||||
/// Re-apply the OS theme whenever it changes at runtime.
|
||||
fn start_theme_watcher(cx: &mut App) {
|
||||
let (sender, receiver) = flume::unbounded();
|
||||
match native_theme::watch::on_theme_change(move |event| {
|
||||
if let Err(error) = sender.send(event) {
|
||||
tracing::warn!("theme change dropped ({error})");
|
||||
}
|
||||
}) {
|
||||
Ok(subscription) => {
|
||||
cx.set_global(ThemeWatcher {
|
||||
_subscription: subscription,
|
||||
});
|
||||
cx.spawn(async move |cx| {
|
||||
while receiver.recv_async().await.is_ok() {
|
||||
tracing::info!("OS theme changed; re-applying");
|
||||
cx.update(|cx| {
|
||||
let store = SettingsStore::global(cx);
|
||||
let settings = store.read(cx).settings().clone();
|
||||
if let Err(error) = apply_native_theme(&settings, cx) {
|
||||
tracing::warn!("failed to apply updated OS theme ({error})");
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::warn!("OS theme watching unavailable ({error})")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,11 +169,14 @@ and overwriting them fights its observer.
|
||||
- `Theme::change(mode, None, cx)` reproduces the native palette for the mode
|
||||
because the connector installed a `ThemeConfig` per variant.
|
||||
|
||||
Known gap (not addressed here): gpui-component's system-appearance follow is
|
||||
driven by window-activation events. A live OS theme change while the window
|
||||
is focused may not propagate on Linux until the window is re-focused;
|
||||
native-theme's `watch` feature (a theme-change watcher) is a possible
|
||||
follow-up, out of scope.
|
||||
Known gap (addressed since): gpui-component's system-appearance follow is
|
||||
driven by window-activation events. The app now enables native-theme's
|
||||
`watch` feature and re-applies the theme on its signal (`start_theme_watcher`
|
||||
in `desktop/src/main.rs`), so live OS changes apply without re-focusing or
|
||||
restarting. macOS reports appearance changes only (`AppleInterfaceTheme-
|
||||
ChangedNotification`); an accent-only change posts a different notification
|
||||
the watcher does not observe yet — Windows (`UISettings::ColorValuesChanged`),
|
||||
KDE (inotify on `kdeglobals`) and GNOME (portal `SettingChanged`) cover accent.
|
||||
|
||||
### 4.4 Settings dialog (`settings_dialog.rs`)
|
||||
|
||||
@@ -278,6 +281,5 @@ Unchanged:
|
||||
SVG assets.
|
||||
- theme-drawn widgets (`widgets` feature).
|
||||
- a preset-picker UI (`from_preset`) in settings.
|
||||
- runtime OS-theme watching (`watch` feature).
|
||||
- per-field user overrides layered on the native theme.
|
||||
- migrating `signed.json` into a native-theme `Theme` definition.
|
||||
|
||||
Reference in New Issue
Block a user