Rework theme API
This commit adds support for theming on macOS and also unifies the system theme handling across platforms.
This commit is contained in:
parent
4f06cfcf5b
commit
92fdf5ba85
20 changed files with 256 additions and 60 deletions
29
src/platform_impl/macos/appkit/appearance.rs
Normal file
29
src/platform_impl/macos/appkit/appearance.rs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
use objc2::foundation::{NSArray, NSObject, NSString};
|
||||
use objc2::rc::{Id, Shared};
|
||||
use objc2::{extern_class, extern_methods, msg_send_id, ClassType};
|
||||
|
||||
extern_class!(
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub(crate) struct NSAppearance;
|
||||
|
||||
unsafe impl ClassType for NSAppearance {
|
||||
type Super = NSObject;
|
||||
}
|
||||
);
|
||||
|
||||
type NSAppearanceName = NSString;
|
||||
|
||||
extern_methods!(
|
||||
unsafe impl NSAppearance {
|
||||
pub fn appearanceNamed(name: &NSAppearanceName) -> Id<Self, Shared> {
|
||||
unsafe { msg_send_id![Self::class(), appearanceNamed: name] }
|
||||
}
|
||||
|
||||
pub fn bestMatchFromAppearancesWithNames(
|
||||
&self,
|
||||
appearances: &NSArray<NSAppearanceName>,
|
||||
) -> Id<NSAppearanceName, Shared> {
|
||||
unsafe { msg_send_id![self, bestMatchFromAppearancesWithNames: appearances,] }
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
@ -4,7 +4,7 @@ use objc2::runtime::Object;
|
|||
use objc2::{extern_class, extern_methods, msg_send_id, ClassType};
|
||||
use objc2::{Encode, Encoding};
|
||||
|
||||
use super::{NSEvent, NSMenu, NSResponder, NSWindow};
|
||||
use super::{NSAppearance, NSEvent, NSMenu, NSResponder, NSWindow};
|
||||
|
||||
extern_class!(
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
|
|
@ -82,6 +82,13 @@ extern_methods!(
|
|||
#[sel(setMainMenu:)]
|
||||
pub fn setMainMenu(&self, menu: &NSMenu);
|
||||
|
||||
pub fn effectiveAppearance(&self) -> Id<NSAppearance, Shared> {
|
||||
unsafe { msg_send_id![self, effectiveAppearance] }
|
||||
}
|
||||
|
||||
#[sel(setAppearance:)]
|
||||
pub fn setAppearance(&self, appearance: &NSAppearance);
|
||||
|
||||
#[sel(run)]
|
||||
pub unsafe fn run(&self);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
#![allow(clippy::enum_variant_names)]
|
||||
#![allow(non_upper_case_globals)]
|
||||
|
||||
mod appearance;
|
||||
mod application;
|
||||
mod button;
|
||||
mod color;
|
||||
|
|
@ -28,6 +29,7 @@ mod version;
|
|||
mod view;
|
||||
mod window;
|
||||
|
||||
pub(crate) use self::appearance::NSAppearance;
|
||||
pub(crate) use self::application::{
|
||||
NSApp, NSApplication, NSApplicationActivationPolicy, NSApplicationPresentationOptions,
|
||||
NSRequestUserAttentionType,
|
||||
|
|
|
|||
|
|
@ -29,7 +29,8 @@ use crate::{
|
|||
Fullscreen, OsError,
|
||||
},
|
||||
window::{
|
||||
CursorGrabMode, CursorIcon, UserAttentionType, WindowAttributes, WindowId as RootWindowId,
|
||||
CursorGrabMode, CursorIcon, Theme, UserAttentionType, WindowAttributes,
|
||||
WindowId as RootWindowId,
|
||||
},
|
||||
};
|
||||
use core_graphics::display::{CGDisplay, CGPoint};
|
||||
|
|
@ -38,12 +39,12 @@ use objc2::foundation::{
|
|||
is_main_thread, CGFloat, NSArray, NSCopying, NSObject, NSPoint, NSRect, NSSize, NSString,
|
||||
};
|
||||
use objc2::rc::{autoreleasepool, Id, Owned, Shared};
|
||||
use objc2::{declare_class, msg_send, msg_send_id, ClassType};
|
||||
use objc2::{declare_class, msg_send, msg_send_id, sel, ClassType};
|
||||
|
||||
use super::appkit::{
|
||||
NSApp, NSAppKitVersion, NSApplicationPresentationOptions, NSBackingStoreType, NSColor,
|
||||
NSCursor, NSFilenamesPboardType, NSRequestUserAttentionType, NSResponder, NSScreen, NSWindow,
|
||||
NSWindowButton, NSWindowLevel, NSWindowStyleMask, NSWindowTitleVisibility,
|
||||
NSApp, NSAppKitVersion, NSAppearance, NSApplicationPresentationOptions, NSBackingStoreType,
|
||||
NSColor, NSCursor, NSFilenamesPboardType, NSRequestUserAttentionType, NSResponder, NSScreen,
|
||||
NSWindow, NSWindowButton, NSWindowLevel, NSWindowStyleMask, NSWindowTitleVisibility,
|
||||
};
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
|
|
@ -149,6 +150,7 @@ pub struct SharedState {
|
|||
/// bar in exclusive fullscreen but want to restore the original options when
|
||||
/// transitioning back to borderless fullscreen.
|
||||
save_presentation_opts: Option<NSApplicationPresentationOptions>,
|
||||
pub current_theme: Option<Theme>,
|
||||
}
|
||||
|
||||
impl SharedState {
|
||||
|
|
@ -391,6 +393,18 @@ impl WinitWindow {
|
|||
unsafe { NSFilenamesPboardType }.copy()
|
||||
]));
|
||||
|
||||
match attrs.preferred_theme {
|
||||
Some(theme) => {
|
||||
set_ns_theme(theme);
|
||||
let mut state = this.shared_state.lock().unwrap();
|
||||
state.current_theme = Some(theme);
|
||||
}
|
||||
None => {
|
||||
let mut state = this.shared_state.lock().unwrap();
|
||||
state.current_theme = Some(get_ns_theme());
|
||||
}
|
||||
}
|
||||
|
||||
let delegate = WinitWindowDelegate::new(&this, attrs.fullscreen.is_some());
|
||||
|
||||
// Set fullscreen mode after we setup everything
|
||||
|
|
@ -1095,6 +1109,12 @@ impl WinitWindow {
|
|||
util::set_style_mask_sync(self, current_style_mask & (!mask));
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn theme(&self) -> Option<Theme> {
|
||||
let state = self.shared_state.lock().unwrap();
|
||||
state.current_theme
|
||||
}
|
||||
}
|
||||
|
||||
impl WindowExtMacOS for WinitWindow {
|
||||
|
|
@ -1186,3 +1206,33 @@ impl WindowExtMacOS for WinitWindow {
|
|||
self.setHasShadow(has_shadow)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn get_ns_theme() -> Theme {
|
||||
let app = NSApp();
|
||||
let has_theme: bool = unsafe { msg_send![&app, respondsToSelector: sel!(effectiveAppearance)] };
|
||||
if !has_theme {
|
||||
return Theme::Light;
|
||||
}
|
||||
let appearance = app.effectiveAppearance();
|
||||
let name = appearance.bestMatchFromAppearancesWithNames(&NSArray::from_slice(&[
|
||||
NSString::from_str("NSAppearanceNameAqua"),
|
||||
NSString::from_str("NSAppearanceNameDarkAqua"),
|
||||
]));
|
||||
match &*name.to_string() {
|
||||
"NSAppearanceNameDarkAqua" => Theme::Dark,
|
||||
_ => Theme::Light,
|
||||
}
|
||||
}
|
||||
|
||||
fn set_ns_theme(theme: Theme) {
|
||||
let app = NSApp();
|
||||
let has_theme: bool = unsafe { msg_send![&app, respondsToSelector: sel!(effectiveAppearance)] };
|
||||
if has_theme {
|
||||
let name = match theme {
|
||||
Theme::Dark => NSString::from_str("NSAppearanceNameDarkAqua"),
|
||||
Theme::Light => NSString::from_str("NSAppearanceNameAqua"),
|
||||
};
|
||||
let appearance = NSAppearance::appearanceNamed(&name);
|
||||
app.setAppearance(&appearance);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use objc2::declare::{Ivar, IvarDrop};
|
|||
use objc2::foundation::{NSArray, NSObject, NSString};
|
||||
use objc2::rc::{autoreleasepool, Id, Shared};
|
||||
use objc2::runtime::Object;
|
||||
use objc2::{declare_class, msg_send, msg_send_id, sel, ClassType};
|
||||
use objc2::{class, declare_class, msg_send, msg_send_id, sel, ClassType};
|
||||
|
||||
use super::appkit::{
|
||||
NSApplicationPresentationOptions, NSFilenamesPboardType, NSPasteboard, NSWindowOcclusionState,
|
||||
|
|
@ -16,7 +16,7 @@ use crate::{
|
|||
app_state::AppState,
|
||||
event::{EventProxy, EventWrapper},
|
||||
util,
|
||||
window::WinitWindow,
|
||||
window::{get_ns_theme, WinitWindow},
|
||||
Fullscreen,
|
||||
},
|
||||
window::WindowId,
|
||||
|
|
@ -69,6 +69,22 @@ declare_class!(
|
|||
this.emit_static_scale_factor_changed_event();
|
||||
}
|
||||
this.window.setDelegate(Some(this));
|
||||
|
||||
// Enable theme change event
|
||||
let notification_center: Id<Object, Shared> =
|
||||
unsafe { msg_send_id![class!(NSDistributedNotificationCenter), defaultCenter] };
|
||||
let notification_name =
|
||||
NSString::from_str("AppleInterfaceThemeChangedNotification");
|
||||
let _: () = unsafe {
|
||||
msg_send![
|
||||
¬ification_center,
|
||||
addObserver: &*this
|
||||
selector: sel!(effectiveAppearanceDidChange:)
|
||||
name: &*notification_name
|
||||
object: ptr::null::<Object>()
|
||||
]
|
||||
};
|
||||
|
||||
this
|
||||
})
|
||||
}
|
||||
|
|
@ -349,6 +365,34 @@ declare_class!(
|
|||
.contains(NSWindowOcclusionState::NSWindowOcclusionStateVisible),
|
||||
))
|
||||
}
|
||||
|
||||
// Observe theme change
|
||||
#[sel(effectiveAppearanceDidChange:)]
|
||||
fn effective_appearance_did_change(&self, sender: Option<&Object>) {
|
||||
trace_scope!("Triggered `effectiveAppearanceDidChange:`");
|
||||
unsafe {
|
||||
msg_send![
|
||||
self,
|
||||
performSelectorOnMainThread: sel!(effectiveAppearanceDidChangedOnMainThread:),
|
||||
withObject: sender,
|
||||
waitUntilDone: false,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
#[sel(effectiveAppearanceDidChangedOnMainThread:)]
|
||||
fn effective_appearance_did_changed_on_main_thread(&self, _: Option<&Object>) {
|
||||
let theme = get_ns_theme();
|
||||
let mut shared_state = self
|
||||
.window
|
||||
.lock_shared_state("effective_appearance_did_change");
|
||||
let current_theme = shared_state.current_theme;
|
||||
shared_state.current_theme = Some(theme);
|
||||
drop(shared_state);
|
||||
if current_theme != Some(theme) {
|
||||
self.emit_event(WindowEvent::ThemeChanged(theme));
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue