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:
keiya sasaki 2022-10-19 03:34:36 +09:00 committed by GitHub
parent 4f06cfcf5b
commit 92fdf5ba85
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 256 additions and 60 deletions

View file

@ -24,7 +24,7 @@ use crate::{
error,
event::{self, VirtualKeyCode},
event_loop::{self, ControlFlow},
window::{self, CursorGrabMode},
window::{self, CursorGrabMode, Theme},
};
static CONFIG: Lazy<RwLock<Configuration>> = Lazy::new(|| {
@ -852,6 +852,10 @@ impl Window {
pub fn content_rect(&self) -> Rect {
ndk_glue::content_rect()
}
pub fn theme(&self) -> Option<Theme> {
None
}
}
#[derive(Default, Clone, Debug)]

View file

@ -23,7 +23,8 @@ use crate::{
monitor, view, EventLoopWindowTarget, Fullscreen, MonitorHandle,
},
window::{
CursorGrabMode, CursorIcon, UserAttentionType, WindowAttributes, WindowId as RootWindowId,
CursorGrabMode, CursorIcon, Theme, UserAttentionType, WindowAttributes,
WindowId as RootWindowId,
},
};
@ -341,6 +342,11 @@ impl Inner {
pub fn raw_display_handle(&self) -> RawDisplayHandle {
RawDisplayHandle::UiKit(UiKitDisplayHandle::empty())
}
pub fn theme(&self) -> Option<Theme> {
warn!("`Window::theme` is ignored on iOS");
None
}
}
pub struct Window {

View file

@ -31,8 +31,6 @@ pub use self::x11::XNotSupported;
use self::x11::{ffi::XVisualInfo, util::WindowType as XWindowType, XConnection, XError};
#[cfg(feature = "x11")]
use crate::platform::x11::XlibErrorHook;
#[cfg(feature = "wayland")]
use crate::window::Theme;
use crate::{
dpi::{PhysicalPosition, PhysicalSize, Position, Size},
error::{ExternalError, NotSupportedError, OsError as RootOsError},
@ -41,7 +39,7 @@ use crate::{
ControlFlow, DeviceEventFilter, EventLoopClosed, EventLoopWindowTarget as RootELW,
},
icon::Icon,
window::{CursorGrabMode, CursorIcon, UserAttentionType, WindowAttributes},
window::{CursorGrabMode, CursorIcon, Theme, UserAttentionType, WindowAttributes},
};
pub(crate) use crate::icon::RgbaIcon as PlatformIcon;
@ -104,8 +102,6 @@ pub struct PlatformSpecificWindowBuilderAttributes {
pub x11_window_types: Vec<XWindowType>,
#[cfg(feature = "x11")]
pub gtk_theme_variant: Option<String>,
#[cfg(feature = "wayland")]
pub csd_theme: Option<Theme>,
}
impl Default for PlatformSpecificWindowBuilderAttributes {
@ -126,8 +122,6 @@ impl Default for PlatformSpecificWindowBuilderAttributes {
x11_window_types: vec![XWindowType::Normal],
#[cfg(feature = "x11")]
gtk_theme_variant: None,
#[cfg(feature = "wayland")]
csd_theme: None,
}
}
}
@ -590,6 +584,11 @@ impl Window {
pub fn raw_display_handle(&self) -> RawDisplayHandle {
x11_or_wayland!(match self; Window(window) => window.raw_display_handle())
}
#[inline]
pub fn theme(&self) -> Option<Theme> {
x11_or_wayland!(match self; Window(window) => window.theme())
}
}
/// Hooks for X11 errors.

View file

@ -170,7 +170,7 @@ impl Window {
// Set CSD frame config from theme if specified,
// otherwise use upstream automatic selection.
#[cfg(feature = "sctk-adwaita")]
if let Some(theme) = platform_attributes.csd_theme.or_else(|| {
if let Some(theme) = attributes.preferred_theme.or_else(|| {
std::env::var(WAYLAND_CSD_THEME_ENV_VAR)
.ok()
.and_then(|s| s.as_str().try_into().ok())
@ -619,6 +619,11 @@ impl Window {
self.window_requests.lock().unwrap().push(request);
self.event_loop_awakener.ping();
}
#[inline]
pub fn theme(&self) -> Option<Theme> {
None
}
}
impl Drop for Window {

View file

@ -20,7 +20,7 @@ use crate::{
Fullscreen, MonitorHandle as PlatformMonitorHandle, OsError,
PlatformSpecificWindowBuilderAttributes, VideoMode as PlatformVideoMode,
},
window::{CursorGrabMode, CursorIcon, Icon, UserAttentionType, WindowAttributes},
window::{CursorGrabMode, CursorIcon, Icon, Theme, UserAttentionType, WindowAttributes},
};
use super::{
@ -1546,4 +1546,9 @@ impl UnownedWindow {
display_handle.screen = self.screen_id;
RawDisplayHandle::Xlib(display_handle)
}
#[inline]
pub fn theme(&self) -> Option<Theme> {
None
}
}

View 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,] }
}
}
);

View file

@ -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);
}

View file

@ -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,

View file

@ -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);
}
}

View file

@ -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![
&notification_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));
}
}
}
);

View file

@ -3,7 +3,7 @@ use crate::error::{ExternalError, NotSupportedError, OsError as RootOE};
use crate::event;
use crate::icon::Icon;
use crate::window::{
CursorGrabMode, CursorIcon, UserAttentionType, WindowAttributes, WindowId as RootWI,
CursorGrabMode, CursorIcon, Theme, UserAttentionType, WindowAttributes, WindowId as RootWI,
};
use raw_window_handle::{RawDisplayHandle, RawWindowHandle, WebDisplayHandle, WebWindowHandle};
@ -373,6 +373,11 @@ impl Window {
pub fn raw_display_handle(&self) -> RawDisplayHandle {
RawDisplayHandle::Web(WebDisplayHandle::empty())
}
#[inline]
pub fn theme(&self) -> Option<Theme> {
None
}
}
impl Drop for Window {

View file

@ -19,7 +19,6 @@ pub(self) use crate::platform_impl::Fullscreen;
use crate::event::DeviceId as RootDeviceId;
use crate::icon::Icon;
use crate::window::Theme;
#[derive(Clone)]
pub enum Parent {
@ -35,7 +34,6 @@ pub struct PlatformSpecificWindowBuilderAttributes {
pub taskbar_icon: Option<Icon>,
pub no_redirection_bitmap: bool,
pub drag_and_drop: bool,
pub preferred_theme: Option<Theme>,
pub skip_taskbar: bool,
pub decoration_shadow: bool,
}
@ -48,7 +46,6 @@ impl Default for PlatformSpecificWindowBuilderAttributes {
taskbar_icon: None,
no_redirection_bitmap: false,
drag_and_drop: true,
preferred_theme: None,
skip_taskbar: false,
decoration_shadow: false,
}

View file

@ -694,8 +694,8 @@ impl Window {
}
#[inline]
pub fn theme(&self) -> Theme {
self.window_state_lock().current_theme
pub fn theme(&self) -> Option<Theme> {
Some(self.window_state_lock().current_theme)
}
#[inline]
@ -781,7 +781,7 @@ impl<'a, T: 'static> InitData<'a, T> {
// If the system theme is dark, we need to set the window theme now
// before we update the window flags (and possibly show the
// window for the first time).
let current_theme = try_theme(window, self.pl_attribs.preferred_theme);
let current_theme = try_theme(window, self.attributes.preferred_theme);
let window_state = {
let window_state = WindowState::new(
@ -789,7 +789,7 @@ impl<'a, T: 'static> InitData<'a, T> {
self.pl_attribs.taskbar_icon.clone(),
scale_factor,
current_theme,
self.pl_attribs.preferred_theme,
self.attributes.preferred_theme,
);
let window_state = Arc::new(Mutex::new(window_state));
WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| {