use crate::{Appearance, Theme};
use gpui::{App, Global, Subscription, Window};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum AppearanceMode {
#[default]
System,
Light,
Dark,
}
impl AppearanceMode {
pub fn label(self) -> &'static str {
match self {
Self::System => "System",
Self::Light => "Light",
Self::Dark => "Dark",
}
}
pub const ALL: [Self; 3] = [Self::System, Self::Light, Self::Dark];
}
pub struct AppearanceState {
pub mode: AppearanceMode,
pub system: Appearance,
}
impl Global for AppearanceState {}
pub fn resolve(mode: AppearanceMode, system: Appearance) -> Appearance {
match mode {
AppearanceMode::System => system,
AppearanceMode::Light => Appearance::Light,
AppearanceMode::Dark => Appearance::Dark,
}
}
pub fn init(mode: AppearanceMode, cx: &mut App) {
let system = Appearance::from_window(cx.window_appearance());
tracing::debug!(?mode, ?system, "appearance: initial");
cx.set_global(AppearanceState { mode, system });
sync_ns_appearance(mode);
Theme::install(resolve(mode, system), cx);
}
pub fn mode(cx: &App) -> AppearanceMode {
cx.try_global::<AppearanceState>()
.map(|s| s.mode)
.unwrap_or_default()
}
pub fn set_mode(mode: AppearanceMode, cx: &mut App) {
if !cx.has_global::<AppearanceState>() {
return;
}
let state = cx.global_mut::<AppearanceState>();
if state.mode == mode {
return;
}
state.mode = mode;
apply(cx);
}
pub fn observe_window(window: &mut Window, cx: &mut App) -> Subscription {
sync(Appearance::from_window(window.appearance()), cx);
window.observe_window_appearance(|window, cx| {
sync(Appearance::from_window(window.appearance()), cx);
})
}
fn sync(system: Appearance, cx: &mut App) {
if !cx.has_global::<AppearanceState>() {
return;
}
let state = cx.global_mut::<AppearanceState>();
if state.system == system {
return;
}
tracing::debug!(?system, "appearance: system changed");
state.system = system;
apply(cx);
}
pub fn apply(cx: &mut App) {
let Some(state) = cx.try_global::<AppearanceState>() else {
return;
};
sync_ns_appearance(state.mode);
let wanted = resolve(state.mode, state.system);
let changed = !cx
.try_global::<Theme>()
.is_some_and(|t| t.appearance == wanted);
if changed {
tracing::debug!(?wanted, "appearance: installing palette");
Theme::install(wanted, cx);
cx.refresh_windows();
}
reapply_window_background(cx);
}
#[cfg(target_os = "macos")]
fn sync_ns_appearance(mode: AppearanceMode) {
use objc::{class, msg_send, runtime::Object, sel, sel_impl};
let name = match mode {
AppearanceMode::System => None,
AppearanceMode::Light => Some(c"NSAppearanceNameAqua"),
AppearanceMode::Dark => Some(c"NSAppearanceNameDarkAqua"),
};
unsafe {
let appearance: *mut Object = match name {
None => std::ptr::null_mut(),
Some(name) => {
let name: *mut Object =
msg_send![class!(NSString), stringWithUTF8String: name.as_ptr()];
msg_send![class!(NSAppearance), appearanceNamed: name]
}
};
let app: *mut Object = msg_send![class!(NSApplication), sharedApplication];
let _: () = msg_send![app, setAppearance: appearance];
}
}
#[cfg(not(target_os = "macos"))]
fn sync_ns_appearance(_mode: AppearanceMode) {}
pub fn reapply_window_background(cx: &mut App) {
let Some(wanted) = cx
.try_global::<Theme>()
.map(|theme| theme.window_background_appearance())
else {
return;
};
for window in cx.windows() {
window
.update(cx, |_, window, _| {
window.set_background_appearance(wanted);
})
.ok();
}
}