diff --git a/internal/backends/winit/Cargo.toml b/internal/backends/winit/Cargo.toml index 8cc8a617396..30b09d38f61 100644 --- a/internal/backends/winit/Cargo.toml +++ b/internal/backends/winit/Cargo.toml @@ -130,7 +130,7 @@ i-slint-renderer-skia = { workspace = true, features = ["default"] } [target.'cfg(target_os = "ios")'.dependencies] objc2 = { workspace = true } objc2-foundation = { version = "0.3.2", default-features = false, features = ["std", "block2", "NSString", "NSNotification", "NSOperation", "NSGeometry", "objc2-core-foundation"] } -objc2-ui-kit = { version = "0.3.2", default-features = false, features = ["UIScreen", "UIWindow", "UIView", "UIViewAnimating", "UIViewPropertyAnimator", "UIResponder", "objc2-core-foundation", "objc2-quartz-core", "block2"] } +objc2-ui-kit = { version = "0.3.2", default-features = false, features = ["UIScreen", "UIWindow", "UIView", "UIViewAnimating", "UIViewPropertyAnimator", "UIResponder", "UIInterface", "UITrait", "UITraitCollection", "objc2-core-foundation", "objc2-quartz-core", "block2"] } objc2-quartz-core = { version = "0.3.2", default-features = false, features = ["CADisplayLink", "CATransaction"] } # Match version in objc2 block2 = "0.6.2" diff --git a/internal/backends/winit/ios.rs b/internal/backends/winit/ios.rs index ff7a1812dad..c9278705d45 100644 --- a/internal/backends/winit/ios.rs +++ b/internal/backends/winit/ios.rs @@ -1,8 +1,12 @@ // Copyright © SixtyFPS GmbH // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 +mod color_scheme; mod keyboard_animator; mod virtual_keyboard; +pub(crate) use color_scheme::{ + ColorSchemeObserver, current_color_scheme, install_color_scheme_observer, +}; pub(crate) use keyboard_animator::KeyboardCurveSampler; pub(crate) use virtual_keyboard::{KeyboardNotifications, register_keyboard_notifications}; diff --git a/internal/backends/winit/ios/color_scheme.rs b/internal/backends/winit/ios/color_scheme.rs new file mode 100644 index 00000000000..44d2f5a71ca --- /dev/null +++ b/internal/backends/winit/ios/color_scheme.rs @@ -0,0 +1,84 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + +// Stopgap until winit ships iOS appearance support (rust-windowing/winit#4570). +// Once that lands, `winit_window.theme()` and `WindowEvent::ThemeChanged` will +// work on iOS and this whole module — plus the `ios_color_scheme_observer` +// wiring in winitwindowadapter.rs — can be deleted. + +use std::ptr::NonNull; +use std::rc::Weak; + +use block2::RcBlock; +use objc2::{ + ClassType, Message, available, msg_send, + rc::Retained, + runtime::{AnyClass, ProtocolObject}, +}; +use objc2_foundation::NSArray; +use objc2_ui_kit::{ + UITraitChangeObservable, UITraitChangeRegistration, UITraitCollection, UITraitEnvironment, + UITraitUserInterfaceStyle, UIUserInterfaceStyle, UIView, +}; + +use i_slint_core::items::ColorScheme; + +use crate::winitwindowadapter::WinitWindowAdapter; + +fn style_to_color_scheme(style: UIUserInterfaceStyle) -> ColorScheme { + match style { + UIUserInterfaceStyle::Dark => ColorScheme::Dark, + UIUserInterfaceStyle::Light => ColorScheme::Light, + _ => ColorScheme::Unknown, + } +} + +pub(crate) fn current_color_scheme(view: &UIView) -> ColorScheme { + style_to_color_scheme(unsafe { view.traitCollection().userInterfaceStyle() }) +} + +pub(crate) struct ColorSchemeObserver { + view: Retained, + registration: Retained>, +} + +impl Drop for ColorSchemeObserver { + fn drop(&mut self) { + self.view.unregisterForTraitChanges(&self.registration); + } +} + +pub(crate) fn install_color_scheme_observer( + view: &UIView, + adapter: Weak, +) -> Option { + // `registerForTraitChanges:withHandler:` is iOS 17+. Older iOS still gets the + // initial scheme via `current_color_scheme`, but no live updates. + if !available!(ios = 17.0) { + return None; + } + + let handler = RcBlock::new( + move |env: NonNull>, + _prev: NonNull| { + let Some(adapter) = adapter.upgrade() else { return }; + let env = unsafe { env.as_ref() }; + let scheme = + style_to_color_scheme(unsafe { env.traitCollection().userInterfaceStyle() }); + adapter.set_color_scheme(scheme); + }, + ); + + let traits: Retained> = + NSArray::from_slice(&[UITraitUserInterfaceStyle::class()]); + + let registration: Retained> = unsafe { + msg_send![ + view, + registerForTraitChanges: &*traits, + withHandler: &*handler, + ] + }; + + Some(ColorSchemeObserver { view: view.retain(), registration }) +} diff --git a/internal/backends/winit/winitwindowadapter.rs b/internal/backends/winit/winitwindowadapter.rs index b5f6f2545cb..32088f0b52d 100644 --- a/internal/backends/winit/winitwindowadapter.rs +++ b/internal/backends/winit/winitwindowadapter.rs @@ -173,6 +173,8 @@ enum WinitWindowOrNone { context_menu_muda_adapter: RefCell>, #[cfg(target_os = "ios")] keyboard_curve_sampler: super::ios::KeyboardCurveSampler, + #[cfg(target_os = "ios")] + _color_scheme_observer: Option, }, None(RefCell), } @@ -516,6 +518,12 @@ impl WinitWindowAdapter { (view, self.self_weak.clone()) }; + // winit doesn't surface iOS appearance, so query the view's trait + // collection directly; the matching live observer is installed below as + // part of the `HasWindow` variant so its lifetime is tied to the window. + #[cfg(target_os = "ios")] + self.set_color_scheme(crate::ios::current_color_scheme(&content_view)); + let frame_throttle = crate::frame_throttle::create_frame_throttle( self.self_weak.clone(), &winit_window, @@ -552,6 +560,11 @@ impl WinitWindowAdapter { } }, ), + #[cfg(target_os = "ios")] + _color_scheme_observer: crate::ios::install_color_scheme_observer( + &content_view, + self.self_weak.clone(), + ), }; #[cfg(muda)]