Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions winit-core/src/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -752,10 +752,11 @@ pub trait Window: AsAny + Send + Sync + fmt::Debug {
/// The inset area of the surface that is unobstructed.
///
/// On some devices, especially mobile devices, the screen is not a perfect rectangle, and may
/// have rounded corners, notches, bezels, and so on. When drawing your content, you usually
/// want to draw your background and other such unimportant content on the entire surface, while
/// you will want to restrict important content such as text, interactable or visual indicators
/// to the part of the screen that is actually visible; for this, you use the safe area.
/// have rounded corners, notches, bezels, and so on. Additionally, a soft keyboard may be open.
/// When drawing your content, you usually want to draw your background and other such
/// unimportant content on the entire surface, while you will want to restrict important content
/// such as text, interactable or visual indicators to the part of the screen that is actually
/// visible; for this, you use the safe area.
///
/// The safe area is a rectangle that is defined relative to the origin at the top-left corner
/// of the surface, and the size extending downwards to the right. The area will not extend
Expand Down
8 changes: 6 additions & 2 deletions winit-uikit/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ objc2-core-foundation = { workspace = true, features = [
"CFRunLoop",
"CFString",
] }
objc2-core-graphics = { workspace = true, features = ["std", "CGGeometry"] }
objc2-foundation = { workspace = true, features = [
"std",
"block2",
Expand All @@ -47,14 +48,13 @@ objc2-foundation = { workspace = true, features = [
] }
objc2-ui-kit = { workspace = true, features = [
"std",
"block2",
"objc2-core-foundation",
"UIApplication",
"UIDevice",
"UIEvent",
"UIGeometry",
"UIGestureRecognizer",
"UITextInput",
"UITextInputTraits",
"UIOrientation",
"UIPanGestureRecognizer",
"UIPinchGestureRecognizer",
Expand All @@ -63,10 +63,14 @@ objc2-ui-kit = { workspace = true, features = [
"UIScreen",
"UIScreenMode",
"UITapGestureRecognizer",
"UITextInput",
"UITextInputTraits",
"UITouch",
"UITraitCollection",
"UIView",
"UIViewAnimating",
"UIViewController",
"UIViewPropertyAnimator",
"UIWindow",
] }
winit-common = { workspace = true, features = ["core-foundation", "event-handler"] }
Expand Down
4 changes: 2 additions & 2 deletions winit-uikit/src/app_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ pub(crate) fn handle_nonuser_events<I: IntoIterator<Item = EventWrapper>>(

for event in events {
if !processing_redraws && event.is_redraw() {
tracing::info!("processing `RedrawRequested` during the main event loop");
// tracing::info!("processing `RedrawRequested` during the main event loop");
} else if processing_redraws && !event.is_redraw() {
tracing::warn!(
"processing non `RedrawRequested` event after the main event loop: {:#?}",
Expand All @@ -327,7 +327,7 @@ pub(crate) fn handle_nonuser_events<I: IntoIterator<Item = EventWrapper>>(

for event in queued_events {
if !processing_redraws && event.is_redraw() {
tracing::info!("processing `RedrawRequested` during the main event loop");
// tracing::info!("processing `RedrawRequested` during the main event loop");
} else if processing_redraws && !event.is_redraw() {
tracing::warn!(
"processing non-`RedrawRequested` event after the main event loop: {:#?}",
Expand Down
2 changes: 1 addition & 1 deletion winit-uikit/src/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ define_class!(

#[unsafe(method(safeAreaInsetsDidChange))]
fn safe_area_changed(&self) {
debug!("safeAreaInsetsDidChange was called, requesting redraw");
println!("safeAreaInsetsDidChange was called, requesting redraw");
// When the safe area changes we want to make sure to emit a redraw event
self.setNeedsDisplay();
}
Expand Down
134 changes: 129 additions & 5 deletions winit-uikit/src/view_controller.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,26 @@
use std::cell::Cell;

use objc2::rc::Retained;
use objc2::{DefinedClass, MainThreadMarker, available, define_class, msg_send};
use objc2_foundation::NSObject;
use block2::RcBlock;
use objc2::rc::{Retained, Weak};
use objc2::runtime::ProtocolObject;
use objc2::{
DefinedClass, MainThreadMarker, MainThreadOnly, Message, available, define_class, msg_send,
};
use objc2_core_foundation::{CFTimeInterval, CGRect};
use objc2_core_graphics::CGRectIntersection;
use objc2_foundation::{
NSNotification, NSNotificationCenter, NSNumber, NSObject, NSObjectProtocol, NSValue,
};
use objc2_ui_kit::{
UIDevice, UIInterfaceOrientationMask, UIRectEdge, UIResponder, UIStatusBarStyle,
UIUserInterfaceIdiom, UIView, UIViewController,
UICoordinateSpace, UIDevice, UIEdgeInsets, UIInterfaceOrientationMask,
UIKeyboardAnimationCurveUserInfoKey, UIKeyboardAnimationDurationUserInfoKey,
UIKeyboardFrameBeginUserInfoKey, UIKeyboardFrameEndUserInfoKey,
UIKeyboardWillChangeFrameNotification, UIRectEdge, UIResponder, UIScreen, UIStatusBarStyle,
UIUserInterfaceIdiom, UIView, UIViewAnimating, UIViewAnimationCurve, UIViewAnimationOptions,
UIViewController, UIViewPropertyAnimator,
};

use crate::notification_center::create_observer;
use crate::{ScreenEdge, StatusBarStyle, ValidOrientations, WindowAttributesIos};

pub struct ViewControllerState {
Expand All @@ -16,6 +29,9 @@ pub struct ViewControllerState {
prefers_home_indicator_auto_hidden: Cell<bool>,
supported_orientations: Cell<UIInterfaceOrientationMask>,
preferred_screen_edges_deferring_system_gestures: Cell<UIRectEdge>,
// Keep observer around (deallocating it stops notifications being posted).
keyboard_will_change_frame_observer:
Cell<Option<Retained<ProtocolObject<dyn NSObjectProtocol>>>>,
}

define_class!(
Expand Down Expand Up @@ -138,6 +154,7 @@ impl WinitViewController {
prefers_home_indicator_auto_hidden: Cell::new(false),
supported_orientations: Cell::new(UIInterfaceOrientationMask::All),
preferred_screen_edges_deferring_system_gestures: Cell::new(UIRectEdge::empty()),
keyboard_will_change_frame_observer: Cell::new(None),
});
let this: Retained<Self> = unsafe { msg_send![super(this), init] };

Expand All @@ -153,8 +170,115 @@ impl WinitViewController {
ios_attributes.preferred_screen_edges_deferring_system_gestures,
);

let center = NSNotificationCenter::defaultCenter();

this.setView(Some(view));

// Set up an observer that will make the `safeAreaRect` of the view update based on the soft
// keyboard's presence (in addition to everything else that the safe area depends on).
let controller_weak = Weak::from_retained(&this);
this.ivars().keyboard_will_change_frame_observer.set(Some(create_observer(
&center,
unsafe { UIKeyboardWillChangeFrameNotification },
move |notification| {
eprintln!("UIKeyboardWillChangeFrameNotification");
if let Some(controller) = controller_weak.load() {
keyboard_will_change_frame(&controller, notification);
}
},
)));

this
}

/// The current keyboard frame, in the view's coordinate space.
pub(crate) fn current_keyboard_frame(&self) -> CGRect {
// TODO: Combine start_frame and end_frame with `animator.fractionComplete()` to produce
// current frame

// Convert keyboard frame to view coordinates.
let keyboard_frame = self
.view()
.unwrap()
.convertRect_fromCoordinateSpace(frame, &keyboard_screen.coordinateSpace());
todo!()
}
}

fn keyboard_will_change_frame(controller: &WinitViewController, notification: &NSNotification) {
let mtm = controller.mtm();
let controller = controller.retain();
let view = controller.view().unwrap();

// The notification's object is the screen the keyboard appears on (since iOS 16).
let keyboard_screen = notification
.object()
.map(|s| s.downcast::<UIScreen>().unwrap())
.unwrap_or_else(|| view.window().unwrap().screen());

let user_info = notification.userInfo().unwrap();
let begin_frame = user_info
.objectForKey(unsafe { UIKeyboardFrameBeginUserInfoKey })
.unwrap()
.downcast::<NSValue>()
.unwrap()
.get_rect()
.unwrap();
let end_frame = user_info
.objectForKey(unsafe { UIKeyboardFrameEndUserInfoKey })
.unwrap()
.downcast::<NSValue>()
.unwrap()
.get_rect()
.unwrap();
let duration: CFTimeInterval = user_info
.objectForKey(unsafe { UIKeyboardAnimationDurationUserInfoKey })
.unwrap()
.downcast::<NSNumber>()
.unwrap()
.doubleValue();
let curve_raw = user_info
.objectForKey(unsafe { UIKeyboardAnimationCurveUserInfoKey })
.unwrap()
.downcast::<NSNumber>()
.unwrap()
.integerValue();
let curve = UIViewAnimationCurve(curve_raw);

// If OS version is high enough, set up a `UIViewPropertyAnimator` to track the position of the
// keyboard.
if available!(ios = 10.0, tvos = 10.0, visionos = 1.0) {
let animator = UIViewPropertyAnimator::initWithDuration_curve_animations(
UIViewPropertyAnimator::alloc(mtm),
duration,
curve,
None,
);

animator.addCompletion(&RcBlock::new(move |_| {
controller.setAdditionalSafeAreaInsets(todo!());
// TODO: Might need to do further work to update the safe area when we
// move the view?

view.layoutIfNeeded();
// Safe area changed -> request redraw.
view.setNeedsDisplay();
}));

animator.startAnimation();
} else {
// Update immediately.
todo!()
}

// Not sufficient, `setAdditionalSafeAreaInsets` only updates at the start, it doesn't change
// `safeAreaInsets` continously during the keyboard open animation.
//
// UIView::animateWithDuration_delay_options_animations_completion(
// duration,
// 0.0,
// options,
// None,
// mtm,
// );
}
15 changes: 13 additions & 2 deletions winit-uikit/src/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use dpi::{
use objc2::rc::Retained;
use objc2::{MainThreadMarker, available, class, define_class, msg_send};
use objc2_core_foundation::{CGFloat, CGPoint, CGRect, CGSize};
use objc2_core_graphics::CGRectIntersection;
use objc2_foundation::{NSObject, NSObjectProtocol};
use objc2_ui_kit::{
UIApplication, UICoordinateSpace, UIEdgeInsets, UIResponder, UIScreen,
Expand Down Expand Up @@ -202,7 +203,7 @@ impl Inner {
}

pub fn safe_area(&self) -> PhysicalInsets<u32> {
let insets = if available!(ios = 11.0, tvos = 11.0, visionos = 1.0) {
let device_insets = if available!(ios = 11.0, tvos = 11.0, visionos = 1.0) {
self.view.safeAreaInsets()
} else {
// Assume the status bar frame is the only thing that obscures the view
Expand All @@ -211,7 +212,17 @@ impl Inner {
let status_bar_frame = app.statusBarFrame();
UIEdgeInsets { top: status_bar_frame.size.height, left: 0.0, bottom: 0.0, right: 0.0 }
};
let insets = LogicalInsets::new(insets.top, insets.left, insets.bottom, insets.right);

let keyboard_frame = self.view_controller.current_keyboard_frame();
let intersection = CGRectIntersection(self.view.bounds(), keyboard_frame);

let insets = LogicalInsets::new(
device_insets.top,
device_insets.left,
// Assume that the keyboard appears from the bottom.
device_insets.bottom + intersection.size.height,
device_insets.right,
);
insets.to_physical(self.scale_factor())
}

Expand Down
34 changes: 30 additions & 4 deletions winit/examples/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@

use std::error::Error;

use dpi::PhysicalInsets;
use winit::application::ApplicationHandler;
use winit::event::WindowEvent;
use winit::event_loop::{ActiveEventLoop, EventLoop};
#[cfg(web_platform)]
use winit::platform::web::WindowAttributesWeb;
use winit::window::{Window, WindowAttributes, WindowId};
use winit::window::{
ImeCapabilities, ImeEnableRequest, ImeRequest, ImeRequestData, Window, WindowAttributes,
WindowId,
};

#[path = "util/fill.rs"]
mod fill;
Expand All @@ -17,6 +21,7 @@ mod tracing;
#[derive(Default, Debug)]
struct App {
window: Option<Box<dyn Window>>,
prev_safe_area: PhysicalInsets<u32>,
}

impl ApplicationHandler for App {
Expand All @@ -33,11 +38,24 @@ impl ApplicationHandler for App {
event_loop.exit();
return;
},
}
};

// Allow IME out of the box.
let enable_request =
ImeEnableRequest::new(ImeCapabilities::new(), ImeRequestData::default()).unwrap();
let enable_ime = ImeRequest::Enable(enable_request);

// Initial update
self.window.as_ref().unwrap().request_ime_update(enable_ime).unwrap();
}

fn window_event(&mut self, event_loop: &dyn ActiveEventLoop, _: WindowId, event: WindowEvent) {
println!("{event:?}");
let current_safe_area = self.window.as_ref().unwrap().safe_area();
if self.prev_safe_area != current_safe_area {
println!("safe area changed from {:?} to {:?}", self.prev_safe_area, current_safe_area);
self.prev_safe_area = current_safe_area;
}

match event {
WindowEvent::CloseRequested => {
println!("Close was requested; stopping");
Expand All @@ -47,6 +65,7 @@ impl ApplicationHandler for App {
self.window.as_ref().expect("resize event without a window").request_redraw();
},
WindowEvent::RedrawRequested => {
println!("redraw");
// Redraw the application.
//
// It's preferable for applications that do not render continuously to render in
Expand All @@ -64,9 +83,16 @@ impl ApplicationHandler for App {
// For contiguous redraw loop you can request a redraw from here.
// window.request_redraw();
},
_ => (),
_ => {
println!("{event:?}");
},
}
}

fn about_to_wait(&mut self, event_loop: &dyn ActiveEventLoop) {
let window = self.window.as_ref().expect("redraw request without a window");
// window.request_redraw();
}
}

fn main() -> Result<(), Box<dyn Error>> {
Expand Down
Loading