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.
|
||||
///
|
||||
/// 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
|
||||
|
||||
@@ -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",
|
||||
@@ -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",
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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: {:#?}",
|
||||
@@ -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: {:#?}",
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -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!(
|
||||
@@ -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] };
|
||||
|
||||
@@ -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(
|
||||
¢er,
|
||||
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,
|
||||
// );
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
@@ -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
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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 {
|
||||
@@ -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");
|
||||
@@ -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
|
||||
@@ -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>> {
|
||||
|
||||
Reference in New Issue
Block a user