mirror of
https://github.com/rust-windowing/winit.git
synced 2026-06-26 14:49:07 -04:00
WIP on making the safe area on iOS track the keyboard as well
This commit is contained in:
@@ -752,10 +752,11 @@ pub trait Window: AsAny + Send + Sync + fmt::Debug {
|
|||||||
/// The inset area of the surface that is unobstructed.
|
/// The inset area of the surface that is unobstructed.
|
||||||
///
|
///
|
||||||
/// On some devices, especially mobile devices, the screen is not a perfect rectangle, and may
|
/// 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
|
/// have rounded corners, notches, bezels, and so on. Additionally, a soft keyboard may be open.
|
||||||
/// want to draw your background and other such unimportant content on the entire surface, while
|
/// When drawing your content, you usually want to draw your background and other such
|
||||||
/// you will want to restrict important content such as text, interactable or visual indicators
|
/// unimportant content on the entire surface, while you will want to restrict important content
|
||||||
/// to the part of the screen that is actually visible; for this, you use the safe area.
|
/// 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
|
/// 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
|
/// of the surface, and the size extending downwards to the right. The area will not extend
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ objc2-core-foundation = { workspace = true, features = [
|
|||||||
"CFRunLoop",
|
"CFRunLoop",
|
||||||
"CFString",
|
"CFString",
|
||||||
] }
|
] }
|
||||||
|
objc2-core-graphics = { workspace = true, features = ["std", "CGGeometry"] }
|
||||||
objc2-foundation = { workspace = true, features = [
|
objc2-foundation = { workspace = true, features = [
|
||||||
"std",
|
"std",
|
||||||
"block2",
|
"block2",
|
||||||
@@ -47,14 +48,13 @@ objc2-foundation = { workspace = true, features = [
|
|||||||
] }
|
] }
|
||||||
objc2-ui-kit = { workspace = true, features = [
|
objc2-ui-kit = { workspace = true, features = [
|
||||||
"std",
|
"std",
|
||||||
|
"block2",
|
||||||
"objc2-core-foundation",
|
"objc2-core-foundation",
|
||||||
"UIApplication",
|
"UIApplication",
|
||||||
"UIDevice",
|
"UIDevice",
|
||||||
"UIEvent",
|
"UIEvent",
|
||||||
"UIGeometry",
|
"UIGeometry",
|
||||||
"UIGestureRecognizer",
|
"UIGestureRecognizer",
|
||||||
"UITextInput",
|
|
||||||
"UITextInputTraits",
|
|
||||||
"UIOrientation",
|
"UIOrientation",
|
||||||
"UIPanGestureRecognizer",
|
"UIPanGestureRecognizer",
|
||||||
"UIPinchGestureRecognizer",
|
"UIPinchGestureRecognizer",
|
||||||
@@ -63,10 +63,14 @@ objc2-ui-kit = { workspace = true, features = [
|
|||||||
"UIScreen",
|
"UIScreen",
|
||||||
"UIScreenMode",
|
"UIScreenMode",
|
||||||
"UITapGestureRecognizer",
|
"UITapGestureRecognizer",
|
||||||
|
"UITextInput",
|
||||||
|
"UITextInputTraits",
|
||||||
"UITouch",
|
"UITouch",
|
||||||
"UITraitCollection",
|
"UITraitCollection",
|
||||||
"UIView",
|
"UIView",
|
||||||
|
"UIViewAnimating",
|
||||||
"UIViewController",
|
"UIViewController",
|
||||||
|
"UIViewPropertyAnimator",
|
||||||
"UIWindow",
|
"UIWindow",
|
||||||
] }
|
] }
|
||||||
winit-common = { workspace = true, features = ["core-foundation", "event-handler"] }
|
winit-common = { workspace = true, features = ["core-foundation", "event-handler"] }
|
||||||
|
|||||||
@@ -309,7 +309,7 @@ pub(crate) fn handle_nonuser_events<I: IntoIterator<Item = EventWrapper>>(
|
|||||||
|
|
||||||
for event in events {
|
for event in events {
|
||||||
if !processing_redraws && event.is_redraw() {
|
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() {
|
} else if processing_redraws && !event.is_redraw() {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
"processing non `RedrawRequested` event after the main event loop: {:#?}",
|
"processing non `RedrawRequested` event after the main event loop: {:#?}",
|
||||||
@@ -327,7 +327,7 @@ pub(crate) fn handle_nonuser_events<I: IntoIterator<Item = EventWrapper>>(
|
|||||||
|
|
||||||
for event in queued_events {
|
for event in queued_events {
|
||||||
if !processing_redraws && event.is_redraw() {
|
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() {
|
} else if processing_redraws && !event.is_redraw() {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
"processing non-`RedrawRequested` event after the main event loop: {:#?}",
|
"processing non-`RedrawRequested` event after the main event loop: {:#?}",
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ define_class!(
|
|||||||
|
|
||||||
#[unsafe(method(safeAreaInsetsDidChange))]
|
#[unsafe(method(safeAreaInsetsDidChange))]
|
||||||
fn safe_area_changed(&self) {
|
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
|
// When the safe area changes we want to make sure to emit a redraw event
|
||||||
self.setNeedsDisplay();
|
self.setNeedsDisplay();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,26 @@
|
|||||||
use std::cell::Cell;
|
use std::cell::Cell;
|
||||||
|
|
||||||
use objc2::rc::Retained;
|
use block2::RcBlock;
|
||||||
use objc2::{DefinedClass, MainThreadMarker, available, define_class, msg_send};
|
use objc2::rc::{Retained, Weak};
|
||||||
use objc2_foundation::NSObject;
|
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::{
|
use objc2_ui_kit::{
|
||||||
UIDevice, UIInterfaceOrientationMask, UIRectEdge, UIResponder, UIStatusBarStyle,
|
UICoordinateSpace, UIDevice, UIEdgeInsets, UIInterfaceOrientationMask,
|
||||||
UIUserInterfaceIdiom, UIView, UIViewController,
|
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};
|
use crate::{ScreenEdge, StatusBarStyle, ValidOrientations, WindowAttributesIos};
|
||||||
|
|
||||||
pub struct ViewControllerState {
|
pub struct ViewControllerState {
|
||||||
@@ -16,6 +29,9 @@ pub struct ViewControllerState {
|
|||||||
prefers_home_indicator_auto_hidden: Cell<bool>,
|
prefers_home_indicator_auto_hidden: Cell<bool>,
|
||||||
supported_orientations: Cell<UIInterfaceOrientationMask>,
|
supported_orientations: Cell<UIInterfaceOrientationMask>,
|
||||||
preferred_screen_edges_deferring_system_gestures: Cell<UIRectEdge>,
|
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!(
|
define_class!(
|
||||||
@@ -138,6 +154,7 @@ impl WinitViewController {
|
|||||||
prefers_home_indicator_auto_hidden: Cell::new(false),
|
prefers_home_indicator_auto_hidden: Cell::new(false),
|
||||||
supported_orientations: Cell::new(UIInterfaceOrientationMask::All),
|
supported_orientations: Cell::new(UIInterfaceOrientationMask::All),
|
||||||
preferred_screen_edges_deferring_system_gestures: Cell::new(UIRectEdge::empty()),
|
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] };
|
let this: Retained<Self> = unsafe { msg_send![super(this), init] };
|
||||||
|
|
||||||
@@ -153,8 +170,115 @@ impl WinitViewController {
|
|||||||
ios_attributes.preferred_screen_edges_deferring_system_gestures,
|
ios_attributes.preferred_screen_edges_deferring_system_gestures,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let center = NSNotificationCenter::defaultCenter();
|
||||||
|
|
||||||
this.setView(Some(view));
|
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(
|
||||||
|
¢er,
|
||||||
|
unsafe { UIKeyboardWillChangeFrameNotification },
|
||||||
|
move |notification| {
|
||||||
|
eprintln!("UIKeyboardWillChangeFrameNotification");
|
||||||
|
if let Some(controller) = controller_weak.load() {
|
||||||
|
keyboard_will_change_frame(&controller, notification);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)));
|
||||||
|
|
||||||
this
|
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,
|
||||||
|
// );
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ use dpi::{
|
|||||||
use objc2::rc::Retained;
|
use objc2::rc::Retained;
|
||||||
use objc2::{MainThreadMarker, available, class, define_class, msg_send};
|
use objc2::{MainThreadMarker, available, class, define_class, msg_send};
|
||||||
use objc2_core_foundation::{CGFloat, CGPoint, CGRect, CGSize};
|
use objc2_core_foundation::{CGFloat, CGPoint, CGRect, CGSize};
|
||||||
|
use objc2_core_graphics::CGRectIntersection;
|
||||||
use objc2_foundation::{NSObject, NSObjectProtocol};
|
use objc2_foundation::{NSObject, NSObjectProtocol};
|
||||||
use objc2_ui_kit::{
|
use objc2_ui_kit::{
|
||||||
UIApplication, UICoordinateSpace, UIEdgeInsets, UIResponder, UIScreen,
|
UIApplication, UICoordinateSpace, UIEdgeInsets, UIResponder, UIScreen,
|
||||||
@@ -202,7 +203,7 @@ impl Inner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn safe_area(&self) -> PhysicalInsets<u32> {
|
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()
|
self.view.safeAreaInsets()
|
||||||
} else {
|
} else {
|
||||||
// Assume the status bar frame is the only thing that obscures the view
|
// Assume the status bar frame is the only thing that obscures the view
|
||||||
@@ -211,7 +212,17 @@ impl Inner {
|
|||||||
let status_bar_frame = app.statusBarFrame();
|
let status_bar_frame = app.statusBarFrame();
|
||||||
UIEdgeInsets { top: status_bar_frame.size.height, left: 0.0, bottom: 0.0, right: 0.0 }
|
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())
|
insets.to_physical(self.scale_factor())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,16 @@
|
|||||||
|
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
|
||||||
|
use dpi::PhysicalInsets;
|
||||||
use winit::application::ApplicationHandler;
|
use winit::application::ApplicationHandler;
|
||||||
use winit::event::WindowEvent;
|
use winit::event::WindowEvent;
|
||||||
use winit::event_loop::{ActiveEventLoop, EventLoop};
|
use winit::event_loop::{ActiveEventLoop, EventLoop};
|
||||||
#[cfg(web_platform)]
|
#[cfg(web_platform)]
|
||||||
use winit::platform::web::WindowAttributesWeb;
|
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"]
|
#[path = "util/fill.rs"]
|
||||||
mod fill;
|
mod fill;
|
||||||
@@ -17,6 +21,7 @@ mod tracing;
|
|||||||
#[derive(Default, Debug)]
|
#[derive(Default, Debug)]
|
||||||
struct App {
|
struct App {
|
||||||
window: Option<Box<dyn Window>>,
|
window: Option<Box<dyn Window>>,
|
||||||
|
prev_safe_area: PhysicalInsets<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ApplicationHandler for App {
|
impl ApplicationHandler for App {
|
||||||
@@ -33,11 +38,24 @@ impl ApplicationHandler for App {
|
|||||||
event_loop.exit();
|
event_loop.exit();
|
||||||
return;
|
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) {
|
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 {
|
match event {
|
||||||
WindowEvent::CloseRequested => {
|
WindowEvent::CloseRequested => {
|
||||||
println!("Close was requested; stopping");
|
println!("Close was requested; stopping");
|
||||||
@@ -47,6 +65,7 @@ impl ApplicationHandler for App {
|
|||||||
self.window.as_ref().expect("resize event without a window").request_redraw();
|
self.window.as_ref().expect("resize event without a window").request_redraw();
|
||||||
},
|
},
|
||||||
WindowEvent::RedrawRequested => {
|
WindowEvent::RedrawRequested => {
|
||||||
|
println!("redraw");
|
||||||
// Redraw the application.
|
// Redraw the application.
|
||||||
//
|
//
|
||||||
// It's preferable for applications that do not render continuously to render in
|
// It's preferable for applications that do not render continuously to render in
|
||||||
@@ -64,9 +83,16 @@ impl ApplicationHandler for App {
|
|||||||
// For contiguous redraw loop you can request a redraw from here.
|
// For contiguous redraw loop you can request a redraw from here.
|
||||||
// window.request_redraw();
|
// 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>> {
|
fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
|||||||
Reference in New Issue
Block a user