Watch for OS theme changes at runtime
This commit is contained in:
Generated
+3
@@ -5699,6 +5699,7 @@ dependencies = [
|
|||||||
"fontdb",
|
"fontdb",
|
||||||
"inventory",
|
"inventory",
|
||||||
"native-theme-derive",
|
"native-theme-derive",
|
||||||
|
"notify",
|
||||||
"objc2 0.6.5",
|
"objc2 0.6.5",
|
||||||
"objc2-app-kit 0.3.2",
|
"objc2-app-kit 0.3.2",
|
||||||
"objc2-core-foundation",
|
"objc2-core-foundation",
|
||||||
@@ -8337,11 +8338,13 @@ version = "0.2.0-alpha"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"assets",
|
"assets",
|
||||||
"dock",
|
"dock",
|
||||||
|
"flume 0.11.1",
|
||||||
"gpui-component",
|
"gpui-component",
|
||||||
"gpui-pre",
|
"gpui-pre",
|
||||||
"gpui-pre-platform",
|
"gpui-pre-platform",
|
||||||
"gpui-pre-reqwest-client",
|
"gpui-pre-reqwest-client",
|
||||||
"metadata",
|
"metadata",
|
||||||
|
"native-theme",
|
||||||
"native-theme-gpui",
|
"native-theme-gpui",
|
||||||
"paths",
|
"paths",
|
||||||
"settings",
|
"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-component = { version = "=0.7.1", features = ["tree-sitter-languages"] }
|
||||||
gpui-base = "=0.7.1"
|
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"] }
|
native-theme-gpui = { version = "0.6.1", default-features = false, features = ["system-fonts"] }
|
||||||
|
|
||||||
dock = { path = "crates/dock" }
|
dock = { path = "crates/dock" }
|
||||||
|
|||||||
@@ -38,7 +38,9 @@ gpui.workspace = true
|
|||||||
gpui_platform.workspace = true
|
gpui_platform.workspace = true
|
||||||
dock = { workspace = true }
|
dock = { workspace = true }
|
||||||
gpui-component.workspace = true
|
gpui-component.workspace = true
|
||||||
|
native-theme.workspace = true
|
||||||
native-theme-gpui.workspace = true
|
native-theme-gpui.workspace = true
|
||||||
|
flume.workspace = true
|
||||||
reqwest_client.workspace = true
|
reqwest_client.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
tracing-subscriber.workspace = true
|
tracing-subscriber.workspace = true
|
||||||
|
|||||||
+76
-21
@@ -5,8 +5,9 @@ use dock::TAB_BAR_HEIGHT;
|
|||||||
use gpui::*;
|
use gpui::*;
|
||||||
use gpui_component::{Theme, ThemeMode, ThemeRegistry, theme};
|
use gpui_component::{Theme, ThemeMode, ThemeRegistry, theme};
|
||||||
use gpui_platform::application;
|
use gpui_platform::application;
|
||||||
|
use native_theme::watch::ThemeSubscription;
|
||||||
use native_theme_gpui::{SystemTheme, apply_system_theme};
|
use native_theme_gpui::{SystemTheme, apply_system_theme};
|
||||||
use settings::{AppearanceMode, SettingsStore};
|
use settings::{AppearanceMode, Settings, SettingsStore};
|
||||||
|
|
||||||
actions!(signed, [Quit]);
|
actions!(signed, [Quit]);
|
||||||
|
|
||||||
@@ -28,26 +29,21 @@ fn main() {
|
|||||||
SettingsStore::set_global(store.clone(), cx);
|
SettingsStore::set_global(store.clone(), cx);
|
||||||
|
|
||||||
let settings = store.read(cx).settings().clone();
|
let settings = store.read(cx).settings().clone();
|
||||||
let registry = ThemeRegistry::global_mut(cx);
|
{
|
||||||
|
let registry = ThemeRegistry::global_mut(cx);
|
||||||
for (name, content) in Assets.themes() {
|
for (name, content) in Assets.themes() {
|
||||||
if let Err(err) = registry.load_themes_from_str(&content) {
|
if let Err(err) = registry.load_themes_from_str(&content) {
|
||||||
tracing::error!("Failed to load theme {name}: {err}");
|
tracing::error!("Failed to load theme {name}: {err}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
match SystemTheme::from_system() {
|
match apply_native_theme(&settings, cx) {
|
||||||
Ok(mut system) => {
|
Ok(()) => start_theme_watcher(cx),
|
||||||
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);
|
|
||||||
}
|
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
tracing::warn!("native theme unavailable ({error})");
|
tracing::warn!("native theme unavailable ({error})");
|
||||||
|
|
||||||
|
let registry = ThemeRegistry::global_mut(cx);
|
||||||
let light_theme = registry
|
let light_theme = registry
|
||||||
.themes()
|
.themes()
|
||||||
.get(settings.theme.light_theme.as_str())
|
.get(settings.theme.light_theme.as_str())
|
||||||
@@ -77,13 +73,13 @@ fn main() {
|
|||||||
} else {
|
} else {
|
||||||
tracing::warn!("{} theme is missing", settings.theme.dark_theme);
|
tracing::warn!("{} theme is missing", settings.theme.dark_theme);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
match settings.appearance {
|
match settings.appearance {
|
||||||
AppearanceMode::System => Theme::sync_system_appearance(None, cx),
|
AppearanceMode::System => Theme::sync_system_appearance(None, cx),
|
||||||
AppearanceMode::Light => Theme::change(ThemeMode::Light, None, cx),
|
AppearanceMode::Light => Theme::change(ThemeMode::Light, None, cx),
|
||||||
AppearanceMode::Dark => Theme::change(ThemeMode::Dark, None, cx),
|
AppearanceMode::Dark => Theme::change(ThemeMode::Dark, None, cx),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::fs::create_dir_all(paths::nostr_dir()).ok();
|
std::fs::create_dir_all(paths::nostr_dir()).ok();
|
||||||
@@ -128,3 +124,62 @@ fn main() {
|
|||||||
cx.activate(true);
|
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
|
- `Theme::change(mode, None, cx)` reproduces the native palette for the mode
|
||||||
because the connector installed a `ThemeConfig` per variant.
|
because the connector installed a `ThemeConfig` per variant.
|
||||||
|
|
||||||
Known gap (not addressed here): gpui-component's system-appearance follow is
|
Known gap (addressed since): gpui-component's system-appearance follow is
|
||||||
driven by window-activation events. A live OS theme change while the window
|
driven by window-activation events. The app now enables native-theme's
|
||||||
is focused may not propagate on Linux until the window is re-focused;
|
`watch` feature and re-applies the theme on its signal (`start_theme_watcher`
|
||||||
native-theme's `watch` feature (a theme-change watcher) is a possible
|
in `desktop/src/main.rs`), so live OS changes apply without re-focusing or
|
||||||
follow-up, out of scope.
|
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`)
|
### 4.4 Settings dialog (`settings_dialog.rs`)
|
||||||
|
|
||||||
@@ -278,6 +281,5 @@ Unchanged:
|
|||||||
SVG assets.
|
SVG assets.
|
||||||
- theme-drawn widgets (`widgets` feature).
|
- theme-drawn widgets (`widgets` feature).
|
||||||
- a preset-picker UI (`from_preset`) in settings.
|
- a preset-picker UI (`from_preset`) in settings.
|
||||||
- runtime OS-theme watching (`watch` feature).
|
|
||||||
- per-field user overrides layered on the native theme.
|
- per-field user overrides layered on the native theme.
|
||||||
- migrating `signed.json` into a native-theme `Theme` definition.
|
- migrating `signed.json` into a native-theme `Theme` definition.
|
||||||
|
|||||||
Reference in New Issue
Block a user