use super::events::*;
use super::text_input::TextInputView;
use super::{CallbackSlot, IosDisplay, platform::IosPlatformState};
use gpui::{
AnyWindowHandle, BackGestureEvent, BackGesturePhase, Bounds, Capslock, Corners, DevicePixels,
DispatchEventResult, DisplayId, Edges, GpuSpecs, Modifiers, Pixels, PlatformAtlas,
PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point, PromptButton,
PromptLevel, RequestFrameOptions, Scene, Size, TextInputConfiguration, TextInputStateChange,
TouchEvent, TouchId, TouchPhase, WindowAppearance, WindowBackgroundAppearance, WindowBounds,
WindowControlArea, WindowInsets, WindowParams, WindowVisibility, px, size,
};
use gpui_apple::{
main_thread_marker,
metal_renderer::{Context as MetalContext, MetalRenderer},
};
use objc2::rc::Retained;
use objc2::runtime::{AnyClass, AnyObject, ProtocolObject};
use objc2::{
ClassType, DefinedClass, MainThreadMarker, MainThreadOnly, define_class, msg_send, sel,
};
use objc2_core_foundation::{CGPoint, CGRect, CGSize};
use objc2_foundation::{
NSArray, NSNotification, NSNotificationCenter, NSObjectProtocol, NSSet, NSValue,
};
use objc2_quartz_core::CAMetalLayer;
use objc2_ui_kit::{
NSValueUIGeometryExtensions, UIKeyboardFrameEndUserInfoKey,
UIKeyboardWillChangeFrameNotification, UIKeyboardWillHideNotification, UIStatusBarStyle,
};
use objc2_ui_kit::{
UICornerConfiguration, UICornerRadius, UIEvent, UIGestureRecognizer,
UIGestureRecognizerDelegate, UIPanGestureRecognizer, UIRectCorner, UIRectEdge, UIScreen,
UIScreenEdgePanGestureRecognizer, UIScrollTypeMask, UITouch, UITraitCollection,
UITraitEnvironment, UIUserInterfaceLayoutDirection, UIUserInterfaceStyle, UIView,
UIViewAutoresizing, UIViewController, UIWindow, UIWindowScene,
};
use parking_lot::Mutex;
use raw_window_handle::{HasDisplayHandle, HasWindowHandle, UiKitDisplayHandle, UiKitWindowHandle};
use std::{
cell::{Cell, RefCell},
ffi::c_void,
ptr::NonNull,
rc::{Rc, Weak},
sync::Arc,
};
const KEYBOARD_DISMISS_DISTANCE: Pixels = px(24.);
#[derive(Clone, Copy)]
struct KeyboardDismissTouch {
id: TouchId,
start_position: Point<Pixels>,
}
static STATUS_BAR_STYLE: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
#[derive(Default)]
pub(super) struct WindowReference(pub(super) RefCell<Weak<IosWindowState>>);
impl WindowReference {
pub(super) fn with_window<R>(&self, callback: impl FnOnce(&IosWindowState) -> R) -> Option<R> {
let window = self.0.borrow().upgrade()?;
Some(callback(&window))
}
}
define_class!(
#[unsafe(super = UIViewController)]
#[thread_kind = MainThreadOnly]
#[name = "GPUIViewController"]
#[ivars = WindowReference]
struct ViewController;
impl ViewController {
#[unsafe(method(preferredStatusBarStyle))]
fn preferred_status_bar_style(&self) -> UIStatusBarStyle {
let style = STATUS_BAR_STYLE.load(std::sync::atomic::Ordering::Relaxed);
if style == 1 {
UIStatusBarStyle::LightContent
} else {
UIStatusBarStyle::DarkContent
}
}
#[unsafe(method(viewDidLayoutSubviews))]
fn view_did_layout_subviews(&self) {
unsafe {
let _: () = msg_send![super(self), viewDidLayoutSubviews];
}
self.ivars().with_window(IosWindowState::handle_layout_change);
}
}
);
impl ViewController {
fn new(main_thread: MainThreadMarker) -> Retained<Self> {
let this = Self::alloc(main_thread).set_ivars(WindowReference::default());
unsafe { msg_send![super(this), init] }
}
}
pub fn set_status_bar_style(style: crate::StatusBarContentStyle) {
use crate::StatusBarContentStyle;
let value = match style {
StatusBarContentStyle::Light => 1,
StatusBarContentStyle::Dark => 0,
};
STATUS_BAR_STYLE.store(value, std::sync::atomic::Ordering::Relaxed);
if let Some(platform) = super::application::current_platform() {
platform.application.with_windows(|window| {
window.view_controller.setNeedsStatusBarAppearanceUpdate();
});
}
}
define_class!(
#[unsafe(super = UIView)]
#[thread_kind = MainThreadOnly]
#[name = "GPUIMetalView"]
#[ivars = WindowReference]
struct MetalView;
unsafe impl NSObjectProtocol for MetalView {}
impl MetalView {
#[unsafe(method(layerClass))]
fn layer_class() -> &'static AnyClass {
CAMetalLayer::class()
}
#[unsafe(method(traitCollectionDidChange:))]
fn trait_collection_did_change(&self, previous: Option<&UITraitCollection>) {
unsafe {
let _: () = msg_send![super(self), traitCollectionDidChange: previous];
if self
.traitCollection()
.hasDifferentColorAppearanceComparedToTraitCollection(previous)
{
self.ivars()
.with_window(IosWindowState::notify_appearance_changed);
}
}
}
#[unsafe(method(touchesBegan:withEvent:))]
fn touches_began(&self, touches: &NSSet<UITouch>, event: Option<&UIEvent>) {
self.handle_touches(touches, event);
}
#[unsafe(method(touchesMoved:withEvent:))]
fn touches_moved(&self, touches: &NSSet<UITouch>, event: Option<&UIEvent>) {
self.handle_touches(touches, event);
}
#[unsafe(method(touchesEnded:withEvent:))]
fn touches_ended(&self, touches: &NSSet<UITouch>, event: Option<&UIEvent>) {
self.handle_touches(touches, event);
}
#[unsafe(method(touchesCancelled:withEvent:))]
fn touches_cancelled(&self, touches: &NSSet<UITouch>, event: Option<&UIEvent>) {
self.handle_touches(touches, event);
}
#[unsafe(method(handleScroll:))]
fn handle_scroll(&self, recognizer: &UIPanGestureRecognizer) {
self.ivars()
.with_window(|window| window.handle_scroll(recognizer));
}
#[unsafe(method(handleBackGesture:))]
fn handle_back_gesture(&self, recognizer: &UIScreenEdgePanGestureRecognizer) {
self.ivars()
.with_window(|window| window.handle_back_gesture(recognizer));
}
}
unsafe impl UIGestureRecognizerDelegate for MetalView {
#[unsafe(method(gestureRecognizerShouldBegin:))]
fn gesture_recognizer_should_begin(&self, recognizer: &UIGestureRecognizer) -> bool {
self.ivars()
.with_window(|window| window.begin_back_gesture(recognizer))
.unwrap_or(false)
}
}
);
impl MetalView {
fn new(frame: CGRect, main_thread: MainThreadMarker) -> Retained<Self> {
let this = Self::alloc(main_thread).set_ivars(WindowReference::default());
unsafe { msg_send![super(this), initWithFrame: frame] }
}
fn handle_touches(&self, touches: &NSSet<UITouch>, event: Option<&UIEvent>) {
for touch in touches {
self.ivars()
.with_window(|window| window.handle_touch(&touch, event));
}
}
}
pub(crate) struct IosWindow {
state: Rc<IosWindowState>,
}
impl std::ops::Deref for IosWindow {
type Target = IosWindowState;
fn deref(&self) -> &Self::Target {
&self.state
}
}
#[allow(clippy::type_complexity)]
pub(crate) struct IosWindowState {
platform: Weak<IosPlatformState>,
keyboard_observers:
RefCell<Vec<Retained<objc2::runtime::ProtocolObject<dyn NSObjectProtocol>>>>,
window: Retained<UIWindow>,
view_controller: Retained<ViewController>,
view: Retained<MetalView>,
text_input_view: Retained<TextInputView>,
bounds: Cell<Bounds<Pixels>>,
scale_factor: Cell<f32>,
input_handler: CallbackSlot<PlatformInputHandler>,
request_frame_callback: CallbackSlot<Box<dyn FnMut(RequestFrameOptions)>>,
force_next_frame: Cell<bool>,
input_callback: CallbackSlot<Box<dyn FnMut(PlatformInput) -> DispatchEventResult>>,
active_status_callback: CallbackSlot<Box<dyn FnMut(bool)>>,
visibility: Cell<WindowVisibility>,
visibility_callback: CallbackSlot<Box<dyn FnMut(WindowVisibility)>>,
hover_status_callback: CallbackSlot<Box<dyn FnMut(bool)>>,
resize_callback: CallbackSlot<Box<dyn FnMut(Size<Pixels>, f32)>>,
display_id: Cell<DisplayId>,
display_changed_callback: CallbackSlot<Box<dyn FnMut()>>,
moved_callback: CallbackSlot<Box<dyn FnMut()>>,
should_close_callback: CallbackSlot<Box<dyn FnMut() -> bool>>,
hit_test_callback: CallbackSlot<Box<dyn FnMut() -> Option<WindowControlArea>>>,
close_callback: CallbackSlot<Box<dyn FnOnce()>>,
appearance_changed_callback: CallbackSlot<Box<dyn FnMut()>>,
insets_changed_callback: CallbackSlot<Box<dyn FnMut(WindowInsets)>>,
keyboard_dismiss_touch: Cell<Option<KeyboardDismissTouch>>,
back_gesture_start: Cell<Option<Point<Pixels>>>,
keyboard_height: Cell<f32>,
mouse_position: Cell<Point<Pixels>>,
modifiers: Cell<Modifiers>,
renderer: Mutex<MetalRenderer>,
}
impl IosWindow {
#[allow(deprecated)] pub fn new(
_handle: AnyWindowHandle,
_params: WindowParams,
platform: &Rc<IosPlatformState>,
) -> anyhow::Result<Self> {
let screen = IosDisplay::main();
let screen_bounds = screen.bounds();
let scale_factor = screen.scale();
let main_thread = main_thread_marker().expect("UIKit requires the main thread");
let window_scene = platform.application.window_scene();
let window_scene = window_scene.as_deref();
let (window, screen_bounds_cg, scale) = if let Some(scene) = window_scene {
let screen = scene.screen();
let bounds = screen.bounds();
let window = UIWindow::initWithWindowScene(UIWindow::alloc(main_thread), scene);
window.setFrame(bounds);
(window, bounds, screen.scale())
} else {
let screen = UIScreen::mainScreen(main_thread);
let bounds = screen.bounds();
let window = UIWindow::initWithFrame(UIWindow::alloc(main_thread), bounds);
(window, bounds, screen.scale())
};
let metal_frame = CGRect::new(CGPoint::ZERO, screen_bounds_cg.size);
let view = MetalView::new(metal_frame, main_thread);
let layer = view.layer();
layer.setContentsScale(scale);
let mut renderer = MetalRenderer::from_layer(
MetalContext::default(),
layer
.downcast_ref::<CAMetalLayer>()
.ok_or_else(|| anyhow::anyhow!("UIKit view must have a CAMetalLayer"))?,
false,
)?;
let pixel_w = (screen_bounds_cg.size.width * scale) as i32;
let pixel_h = (screen_bounds_cg.size.height * scale) as i32;
renderer.update_drawable_size(size(DevicePixels(pixel_w), DevicePixels(pixel_h)));
view.setAutoresizingMask(
UIViewAutoresizing::FlexibleWidth | UIViewAutoresizing::FlexibleHeight,
);
view.setUserInteractionEnabled(true);
view.setMultipleTouchEnabled(true);
if reports_corner_radii(&view) {
view.setCornerConfiguration(&UICornerConfiguration::configurationWithRadius(
&UICornerRadius::containerConcentricRadius(),
));
}
let scroll_target: &AnyObject = &view;
let scroll_recognizer = unsafe {
UIPanGestureRecognizer::initWithTarget_action(
UIPanGestureRecognizer::alloc(main_thread),
Some(scroll_target),
Some(sel!(handleScroll:)),
)
};
scroll_recognizer.setAllowedScrollTypesMask(UIScrollTypeMask::All);
scroll_recognizer.setAllowedTouchTypes(&NSArray::new());
scroll_recognizer.setCancelsTouchesInView(false);
view.addGestureRecognizer(&scroll_recognizer);
let back_target: &AnyObject = &view;
let back_recognizer = unsafe {
UIScreenEdgePanGestureRecognizer::initWithTarget_action(
UIScreenEdgePanGestureRecognizer::alloc(main_thread),
Some(back_target),
Some(sel!(handleBackGesture:)),
)
};
back_recognizer.setEdges(
if view.effectiveUserInterfaceLayoutDirection()
== UIUserInterfaceLayoutDirection::RightToLeft
{
UIRectEdge::Right
} else {
UIRectEdge::Left
},
);
back_recognizer.setDelegate(Some(ProtocolObject::from_ref(&*view)));
view.addGestureRecognizer(&back_recognizer);
let view_controller = ViewController::new(main_thread);
view_controller.setView(Some(&view));
window.setRootViewController(Some(&view_controller));
window.makeKeyAndVisible();
let text_input_frame = CGRect::new(CGPoint::ZERO, CGSize::new(1.0, 1.0));
let text_input_view = TextInputView::new(text_input_frame, main_thread);
text_input_view.setAlpha(0.01);
text_input_view.setUserInteractionEnabled(true);
view.addSubview(&text_input_view);
let state = IosWindowState {
platform: Rc::downgrade(platform),
keyboard_observers: RefCell::default(),
display_id: Cell::new(IosDisplay::from_screen(window.screen()).id()),
display_changed_callback: CallbackSlot::default(),
window,
view_controller,
view,
text_input_view,
bounds: Cell::new(screen_bounds),
scale_factor: Cell::new(scale_factor),
input_handler: CallbackSlot::default(),
request_frame_callback: CallbackSlot::default(),
force_next_frame: Cell::new(true),
input_callback: CallbackSlot::default(),
active_status_callback: CallbackSlot::default(),
visibility: Cell::new(WindowVisibility::Visible),
visibility_callback: CallbackSlot::default(),
hover_status_callback: CallbackSlot::default(),
resize_callback: CallbackSlot::default(),
moved_callback: CallbackSlot::default(),
should_close_callback: CallbackSlot::default(),
hit_test_callback: CallbackSlot::default(),
close_callback: CallbackSlot::default(),
appearance_changed_callback: CallbackSlot::default(),
insets_changed_callback: CallbackSlot::default(),
keyboard_dismiss_touch: Cell::new(None),
back_gesture_start: Cell::new(None),
keyboard_height: Cell::new(0.),
mouse_position: Cell::new(Point::default()),
modifiers: Cell::new(Modifiers::default()),
renderer: Mutex::new(renderer),
};
Ok(Self {
state: Rc::new(state),
})
}
pub(crate) fn register(&self) {
if let Some(platform) = self.platform.upgrade() {
platform.application.register_window(&self.state);
}
let window = Rc::downgrade(&self.state);
*self.view_controller.ivars().0.borrow_mut() = window.clone();
*self.view.ivars().0.borrow_mut() = window.clone();
self.text_input_view.set_window(window);
self.state.register_keyboard_observers();
}
}
impl IosWindowState {
pub(super) fn attach_to_scene(&self, scene: &UIWindowScene) {
self.window.setWindowScene(Some(scene));
self.window.makeKeyAndVisible();
self.handle_layout_change();
}
fn register_keyboard_observers(self: &Rc<Self>) {
unsafe {
let notification_center = NSNotificationCenter::defaultCenter();
let window = Rc::downgrade(self);
let frame_change_block =
block2::RcBlock::new(move |notification: NonNull<NSNotification>| {
let Some(user_info) = notification.as_ref().userInfo() else {
return;
};
let Some(frame_value) = user_info.objectForKey(UIKeyboardFrameEndUserInfoKey)
else {
return;
};
let Some(frame_value) = frame_value.downcast_ref::<NSValue>() else {
return;
};
let frame = frame_value.CGRectValue();
if let Some(window) = window.upgrade() {
window.set_keyboard_height(frame.size.height as f32);
}
});
let window = Rc::downgrade(self);
let hide_block = block2::RcBlock::new(move |_notification: NonNull<NSNotification>| {
if let Some(window) = window.upgrade() {
window.set_keyboard_height(0.);
}
});
let frame_observer = notification_center.addObserverForName_object_queue_usingBlock(
Some(UIKeyboardWillChangeFrameNotification),
None,
None,
&frame_change_block,
);
let hide_observer = notification_center.addObserverForName_object_queue_usingBlock(
Some(UIKeyboardWillHideNotification),
None,
None,
&hide_block,
);
self.keyboard_observers
.borrow_mut()
.extend([frame_observer, hide_observer]);
}
}
pub fn handle_touch(&self, touch: &UITouch, _event: Option<&UIEvent>) {
let id = touch_id(touch);
self.handle_touch_sample(touch, id);
}
fn handle_touch_sample(&self, touch: &UITouch, id: TouchId) {
let position = touch_location_in_view(touch, &self.view);
self.mouse_position.set(position);
let event = TouchEvent {
id,
phase: touch_phase(touch),
position,
predicted_position: None,
force: touch_force(touch),
};
self.handle_keyboard_dismiss_touch(&event);
self.input_callback
.with(|callback| callback(PlatformInput::Touch(event)));
}
fn handle_scroll(&self, recognizer: &UIPanGestureRecognizer) {
let Some(event) = scroll_event(recognizer, &self.view) else {
return;
};
self.mouse_position.set(event.position);
self.modifiers.set(event.modifiers);
self.input_callback
.with(|callback| callback(PlatformInput::Scroll(event)));
}
fn begin_back_gesture(&self, recognizer: &UIGestureRecognizer) -> bool {
let location = recognizer.locationInView(Some(&self.view));
let start_position = Point::new(px(location.x as f32), px(location.y as f32));
let claimed = self
.input_callback
.with(|callback| {
callback(PlatformInput::BackGesture(BackGestureEvent {
phase: BackGesturePhase::Started,
start_position,
progress: 0.,
}))
})
.is_some_and(|result| result.default_prevented);
self.back_gesture_start
.set(claimed.then_some(start_position));
claimed
}
fn handle_back_gesture(&self, recognizer: &UIScreenEdgePanGestureRecognizer) {
let Some(start_position) = self.back_gesture_start.get() else {
return;
};
let Some(event) = back_event(recognizer, &self.view, start_position) else {
return;
};
if matches!(
event.phase,
BackGesturePhase::Committed | BackGesturePhase::Cancelled
) {
self.back_gesture_start.set(None);
}
self.input_callback
.with(|callback| callback(PlatformInput::BackGesture(event)));
}
fn handle_keyboard_dismiss_touch(&self, event: &TouchEvent) {
if self.keyboard_height.get() <= 0. {
self.keyboard_dismiss_touch.set(None);
return;
}
match event.phase {
TouchPhase::Started if self.keyboard_dismiss_touch.get().is_none() => {
self.keyboard_dismiss_touch.set(Some(KeyboardDismissTouch {
id: event.id,
start_position: event.position,
}));
}
TouchPhase::Moved => {
let Some(touch) = self
.keyboard_dismiss_touch
.get()
.filter(|touch| touch.id == event.id)
else {
return;
};
let delta = event.position - touch.start_position;
if delta.y >= KEYBOARD_DISMISS_DISTANCE && delta.y.abs() > delta.x.abs() * 1.25 {
self.keyboard_dismiss_touch.set(None);
self.hide_keyboard();
}
}
TouchPhase::Ended | TouchPhase::Cancelled => {
if self
.keyboard_dismiss_touch
.get()
.is_some_and(|touch| touch.id == event.id)
{
self.keyboard_dismiss_touch.set(None);
}
}
TouchPhase::Started => {}
}
}
pub(super) fn request_frame(&self) {
self.request_frame_callback.with(|callback| {
let force_render = self.force_next_frame.replace(false);
callback(RequestFrameOptions {
force_render,
..Default::default()
});
});
}
fn safe_area_insets(&self) -> (f32, f32, f32, f32) {
let insets = self.view.safeAreaInsets();
(
insets.top as f32,
insets.bottom as f32,
insets.left as f32,
insets.right as f32,
)
}
fn current_insets(&self) -> WindowInsets {
let (top, bottom, left, right) = self.safe_area_insets();
WindowInsets {
safe_area: Edges {
top: px(top),
right: px(right),
bottom: px(bottom),
left: px(left),
},
ime: Edges {
bottom: px(self.keyboard_height.get()),
..Default::default()
},
corner_radii: self.display_corner_radii(),
}
}
fn display_corner_radii(&self) -> Corners<Pixels> {
if !reports_corner_radii(&self.view) {
return Corners::default();
}
let radius = |corner| px(self.view.effectiveRadiusForCorner(corner) as f32);
Corners {
top_left: radius(UIRectCorner::TopLeft),
top_right: radius(UIRectCorner::TopRight),
bottom_right: radius(UIRectCorner::BottomRight),
bottom_left: radius(UIRectCorner::BottomLeft),
}
}
fn notify_insets_changed(&self) {
self.insets_changed_callback
.with(|callback| callback(self.current_insets()));
}
fn notify_appearance_changed(&self) {
self.appearance_changed_callback.with(|callback| callback());
}
fn set_keyboard_height(&self, height: f32) {
let height = height.max(0.);
if (self.keyboard_height.get() - height).abs() <= 0.5 {
return;
}
self.keyboard_height.set(height);
if height <= 0. {
self.keyboard_dismiss_touch.set(None);
}
self.notify_insets_changed();
}
pub fn show_keyboard(&self) {
self.text_input_view.set_keyboard_visible(true);
}
pub fn hide_keyboard(&self) {
self.text_input_view.set_keyboard_visible(false);
}
pub(super) fn with_input_handler<R>(
&self,
callback: impl FnOnce(&mut PlatformInputHandler) -> R,
) -> Option<R> {
self.input_handler.with(callback)
}
pub fn notify_active_status_change(&self, is_active: bool) {
log::info!("GPUI iOS: Window active status changed to: {}", is_active);
self.active_status_callback
.with(|callback| callback(is_active));
}
pub(crate) fn notify_visibility_change(&self, visibility: WindowVisibility) {
if self.visibility.replace(visibility) != visibility {
self.visibility_callback
.with(|callback| callback(visibility));
}
}
pub fn handle_layout_change(&self) {
let view_bounds = self.view.bounds();
let display = IosDisplay::from_screen(self.window.screen());
if self.display_id.replace(display.id()) != display.id() {
self.display_changed_callback.with(|callback| callback());
}
let scale = display.scale() as f64;
let width = view_bounds.size.width as f32;
let height = view_bounds.size.height as f32;
let new_scale = scale as f32;
let old_bounds = self.bounds.get();
let old_scale = self.scale_factor.get();
let new_size = size(px(width), px(height));
self.notify_insets_changed();
if old_bounds.size == new_size && (old_scale - new_scale).abs() < 0.01 {
return;
}
log::info!(
"GPUI iOS: Layout changed — {:?} @{:.1}x → {:?} @{:.1}x",
old_bounds.size,
old_scale,
new_size,
new_scale,
);
let new_bounds = Bounds {
origin: Default::default(),
size: new_size,
};
self.bounds.set(new_bounds);
self.scale_factor.set(new_scale);
self.view.layer().setContentsScale(scale);
let pixel_width = (width * new_scale) as i32;
let pixel_height = (height * new_scale) as i32;
self.renderer
.lock()
.update_drawable_size(size(DevicePixels(pixel_width), DevicePixels(pixel_height)));
self.resize_callback
.with(|callback| callback(new_size, new_scale));
}
}
fn reports_corner_radii(view: &UIView) -> bool {
view.respondsToSelector(sel!(effectiveRadiusForCorner:))
}
impl Drop for IosWindow {
fn drop(&mut self) {
if let Some(platform) = self.platform.upgrade() {
platform.application.unregister_window(&self.state);
}
let observers = std::mem::take(&mut *self.keyboard_observers.borrow_mut());
for observer in observers {
unsafe { NSNotificationCenter::defaultCenter().removeObserver((&*observer).as_ref()) };
}
*self.view_controller.ivars().0.borrow_mut() = Weak::new();
*self.view.ivars().0.borrow_mut() = Weak::new();
self.text_input_view.set_window(Weak::new());
self.text_input_view.removeFromSuperview();
}
}
impl HasWindowHandle for IosWindow {
fn window_handle(
&self,
) -> std::result::Result<raw_window_handle::WindowHandle<'_>, raw_window_handle::HandleError>
{
let view = NonNull::new(Retained::as_ptr(&self.view).cast_mut().cast::<c_void>())
.ok_or(raw_window_handle::HandleError::Unavailable)?;
let handle = UiKitWindowHandle::new(view);
Ok(unsafe { raw_window_handle::WindowHandle::borrow_raw(handle.into()) })
}
}
impl HasDisplayHandle for IosWindow {
fn display_handle(
&self,
) -> std::result::Result<raw_window_handle::DisplayHandle<'_>, raw_window_handle::HandleError>
{
let handle = UiKitDisplayHandle::new();
Ok(unsafe { raw_window_handle::DisplayHandle::borrow_raw(handle.into()) })
}
}
impl PlatformWindow for IosWindow {
fn bounds(&self) -> Bounds<Pixels> {
self.bounds.get()
}
fn is_maximized(&self) -> bool {
true }
fn window_bounds(&self) -> WindowBounds {
WindowBounds::Fullscreen(self.bounds.get())
}
fn content_size(&self) -> Size<Pixels> {
self.bounds.get().size
}
fn resize(&mut self, _size: Size<Pixels>) {
}
fn scale_factor(&self) -> f32 {
self.scale_factor.get()
}
fn appearance(&self) -> WindowAppearance {
match unsafe { self.view.traitCollection().userInterfaceStyle() } {
UIUserInterfaceStyle::Dark => WindowAppearance::Dark,
_ => WindowAppearance::Light,
}
}
fn display(&self) -> Option<Rc<dyn PlatformDisplay>> {
Some(Rc::new(IosDisplay::from_screen(self.window.screen())))
}
fn on_display_changed(&self, callback: Box<dyn FnMut()>) {
self.display_changed_callback.set(callback);
}
fn mouse_position(&self) -> Point<Pixels> {
self.mouse_position.get()
}
fn modifiers(&self) -> Modifiers {
self.modifiers.get()
}
fn capslock(&self) -> Capslock {
Capslock { on: false }
}
fn set_input_handler(&mut self, input_handler: PlatformInputHandler) {
self.text_input_view
.set_input_field(input_handler.focus_id());
self.input_handler.set(input_handler);
self.text_input_view.refresh_keyboard();
}
fn take_input_handler(&mut self) -> Option<PlatformInputHandler> {
let handler = self.input_handler.take();
self.text_input_view.refresh_keyboard();
handler
}
fn prompt(
&self,
_level: PromptLevel,
_msg: &str,
_detail: Option<&str>,
_answers: &[PromptButton],
) -> Option<futures::channel::oneshot::Receiver<usize>> {
None
}
fn activate(&self) {
self.window.makeKeyAndVisible();
}
fn is_active(&self) -> bool {
self.window.isKeyWindow()
}
fn is_hovered(&self) -> bool {
false
}
fn set_title(&mut self, _title: &str) {
}
fn background_appearance(&self) -> WindowBackgroundAppearance {
WindowBackgroundAppearance::Opaque
}
fn set_background_appearance(&self, _background_appearance: WindowBackgroundAppearance) {
}
fn minimize(&self) {
}
fn zoom(&self) {
}
fn toggle_fullscreen(&self) {
}
fn is_fullscreen(&self) -> bool {
true
}
fn on_request_frame(&self, callback: Box<dyn FnMut(RequestFrameOptions)>) {
self.request_frame_callback.set(callback);
}
fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> DispatchEventResult>) {
self.input_callback.set(callback);
}
fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>) {
self.active_status_callback.set(callback);
}
fn visibility(&self) -> WindowVisibility {
self.visibility.get()
}
fn on_visibility_change(&self, callback: Box<dyn FnMut(WindowVisibility)>) {
self.visibility_callback.set(callback);
}
fn on_hover_status_change(&self, callback: Box<dyn FnMut(bool)>) {
self.hover_status_callback.set(callback);
}
fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>) {
self.resize_callback.set(callback);
}
fn on_moved(&self, callback: Box<dyn FnMut()>) {
self.moved_callback.set(callback);
}
fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>) {
self.should_close_callback.set(callback);
}
fn on_hit_test_window_control(&self, callback: Box<dyn FnMut() -> Option<WindowControlArea>>) {
self.hit_test_callback.set(callback);
}
fn on_close(&self, callback: Box<dyn FnOnce()>) {
self.close_callback.set(callback);
}
fn on_appearance_changed(&self, callback: Box<dyn FnMut()>) {
self.appearance_changed_callback.set(callback);
}
fn draw(&self, scene: &Scene) {
self.renderer.lock().draw(scene);
}
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas> {
self.renderer.lock().sprite_atlas().clone()
}
fn is_subpixel_rendering_supported(&self) -> bool {
false
}
fn gpu_specs(&self) -> Option<GpuSpecs> {
None
}
fn update_ime_position(&self, _bounds: Bounds<Pixels>) {
}
fn insets(&self) -> WindowInsets {
self.current_insets()
}
fn on_insets_changed(&self, callback: Box<dyn FnMut(WindowInsets)>) {
self.insets_changed_callback.set(callback);
}
fn show_soft_keyboard(&self) {
self.show_keyboard();
}
fn hide_soft_keyboard(&self) {
self.hide_keyboard();
}
fn text_input_state_changed(&self, change: TextInputStateChange) {
self.text_input_view.state_changed(change);
}
fn set_text_input_configuration(&mut self, configuration: TextInputConfiguration) {
self.text_input_view.set_configuration(&configuration);
}
}
#[cfg(test)]
mod tests {
use super::CallbackSlot;
#[test]
fn callback_is_unavailable_during_dispatch_and_restored_afterward() {
let slot = CallbackSlot::default();
slot.set(1);
assert_eq!(
slot.with(|value| {
*value += 1;
assert!(
slot.with(|_| panic!("reentered the same handler"))
.is_none()
);
*value
}),
Some(2)
);
assert_eq!(slot.with(|value| *value), Some(2));
}
#[test]
fn callback_replacement_during_dispatch_is_preserved() {
let slot = CallbackSlot::default();
slot.set(1);
slot.with(|_| {
slot.set(2);
slot.with(|value| *value += 1);
});
assert_eq!(slot.take(), Some(3));
}
#[test]
fn withdrawing_an_in_flight_handler_does_not_restore_it() {
let slot = CallbackSlot::default();
slot.set(1);
slot.with(|_| assert!(slot.take().is_none()));
assert!(
slot.with(|_| panic!("restored a withdrawn handler"))
.is_none()
);
}
#[test]
fn removing_a_replacement_does_not_resurrect_the_previous_handler() {
let slot = CallbackSlot::default();
slot.set(1);
slot.with(|_| {
slot.set(2);
assert_eq!(slot.take(), Some(2));
});
assert!(slot.take().is_none());
}
}