1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
use core::ffi::c_void;
use objc2::rc::{Retained, Weak};
use objc2::runtime::{AnyClass, AnyObject};
use objc2::{define_class, msg_send, AllocAnyThread, ClassType, DefinedClass};
use objc2_foundation::{
ns_string, NSDictionary, NSKeyValueChangeKey, NSKeyValueChangeNewKey,
NSKeyValueObservingOptions, NSNumber, NSObjectNSKeyValueObserverRegistration, NSString,
NSValue,
};
use objc2_quartz_core::{CALayer, CAMetalLayer};
define_class!(
/// A `CAMetalLayer` layer that will automatically update its bounds and scale factor to match
/// its super layer.
///
/// We do this by subclassing, to allow the user to just store the layer as
/// `Retained<CAMetalLayer>`, and still have our observers work.
///
/// See the documentation on Key-Value Observing for details on how this works in general:
/// <https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/KeyValueObserving/KeyValueObserving.html>
//
// SAFETY:
// - The superclass CAMetalLayer does not have any subclassing requirements.
// - CustomLayer implements `Drop` and ensures that:
// - It does not call an overridden method.
// - It does not `retain` itself.
#[unsafe(super(CAMetalLayer))]
#[name = "RawWindowMetalLayer"]
#[ivars = Weak<CALayer>]
pub(crate) struct ObserverLayer;
// `NSKeyValueObserving` category.
//
// SAFETY: The method is correctly defined.
impl ObserverLayer {
#[unsafe(method(observeValueForKeyPath:ofObject:change:context:))]
fn _observe_value(
&self,
key_path: Option<&NSString>,
object: Option<&AnyObject>,
change: Option<&NSDictionary<NSKeyValueChangeKey, AnyObject>>,
context: *mut c_void,
) {
self.observe_value(key_path, object, change, context);
}
}
);
impl Drop for ObserverLayer {
fn drop(&mut self) {
// It is possible for the root layer to be de-allocated before the custom layer.
//
// In that case, the observer is already de-registered, and so we don't need to do anything.
//
// We use a weak variable here to avoid issues if the layer was removed from the super
// layer, and then later de-allocated, without de-registering these observers.
if let Some(root_layer) = self.ivars().load() {
// SAFETY: The observer is registered for these key paths in `new`.
unsafe {
root_layer.removeObserver_forKeyPath(self, ns_string!("contentsScale"));
root_layer.removeObserver_forKeyPath(self, ns_string!("bounds"));
}
}
}
}
impl ObserverLayer {
/// The context pointer, to differentiate between key-value observing registered by this class,
/// and the superclass.
fn context() -> *mut c_void {
ObserverLayer::class() as *const AnyClass as *mut c_void
}
/// Create a new custom layer that tracks parameters from the given super layer.
pub fn new(root_layer: &CALayer) -> Retained<Self> {
let this = Self::alloc().set_ivars(Weak::new(root_layer));
// SAFETY: Initializing `CAMetalLayer` is safe.
let this: Retained<Self> = unsafe { msg_send![super(this), init] };
// Add the layer as a sublayer of the root layer.
root_layer.addSublayer(&this);
// Do not use auto-resizing mask.
//
// This is done to work around a bug in macOS 14 and above, where views using auto layout
// may end up setting fractional values as the bounds, and that in turn doesn't propagate
// properly through the auto-resizing mask and with contents gravity.
//
// Instead, we keep the bounds of the layer in sync with the root layer using an observer,
// see below.
//
// this.setAutoresizingMask(kCALayerHeightSizable | kCALayerWidthSizable);
// AppKit / UIKit automatically sets the correct scale factor and bounds for layers attached
// to a view. Our layer, however, is not directly attached to a view, and so we need to
// observe changes to the root layer's parameters, and apply them to our layer.
//
// Note the use of `NSKeyValueObservingOptionInitial` to also set the initial values here.
//
// Note that for AppKit, we _could_ make the layer match the window by adding a delegate on
// the layer with the `layer:shouldInheritContentsScale:fromWindow:` method returning `true`
// - this tells the system to automatically update the scale factor when it changes on a
// window. But this wouldn't support headless rendering very well, and doesn't work on UIKit
// anyhow, so we might as well just always use the observer technique.
//
// SAFETY: Observer deregistered in `Drop` before the observer object is deallocated.
unsafe {
root_layer.addObserver_forKeyPath_options_context(
&this,
ns_string!("contentsScale"),
NSKeyValueObservingOptions::New | NSKeyValueObservingOptions::Initial,
ObserverLayer::context(),
);
root_layer.addObserver_forKeyPath_options_context(
&this,
ns_string!("bounds"),
NSKeyValueObservingOptions::New | NSKeyValueObservingOptions::Initial,
ObserverLayer::context(),
);
}
// The default content gravity (`kCAGravityResize`) is a fine choice for most applications,
// as it masks / alleviates issues with resizing and behaves better when moving the window
// between monitors, so we won't modify that.
//
// Unfortunately, it may also make it harder to debug resize issues, swap this for
// `kCAGravityTopLeft` instead when doing so.
//
// this.setContentsGravity(unsafe { kCAGravityResize });
this
}
fn observe_value(
&self,
key_path: Option<&NSString>,
object: Option<&AnyObject>,
change: Option<&NSDictionary<NSKeyValueChangeKey, AnyObject>>,
context: *mut c_void,
) {
// An unrecognized context must belong to the super class.
if context != ObserverLayer::context() {
// SAFETY: The signature is correct, and it's safe to forward to the superclass' method
// when we're overriding the method.
return unsafe {
msg_send![
super(self),
observeValueForKeyPath: key_path,
ofObject: object,
change: change,
context: context,
]
};
}
let change =
change.expect("requested a change dictionary in `addObserver`, but none was provided");
// SAFETY: The static is declared with the correct type in `objc2`.
let key = unsafe { NSKeyValueChangeNewKey };
let new = change
.objectForKey(key)
.expect("requested change dictionary did not contain `NSKeyValueChangeNewKey`");
// NOTE: Setting these values usually causes a quarter second animation to occur, which is
// undesirable.
//
// However, since we're setting them inside an observer, there already is a transaction
// ongoing, and as such we don't need to wrap this in a `CATransaction` ourselves.
if key_path == Some(ns_string!("contentsScale")) {
// `contentsScale` is a CGFloat, and so the observed value is always a NSNumber.
let new = new.downcast::<NSNumber>().unwrap();
let scale_factor = new.as_cgfloat();
// Set the scale factor of the layer to match the root layer when it changes (e.g. if
// moved to a different monitor, or monitor settings changed).
self.setContentsScale(scale_factor);
} else if key_path == Some(ns_string!("bounds")) {
// `bounds` is a CGRect, and so the observed value is always a NSNumber.
let new = new.downcast::<NSValue>().unwrap();
let bounds = new.get_rect().expect("new bounds value was not CGRect");
// Set `bounds` and `position` so that the new layer is inside the superlayer.
//
// This differs from just setting the `bounds`, as it also takes into account any
// translation that the superlayer may have that we'd want to preserve.
self.setFrame(bounds);
} else {
panic!("unknown observed keypath {key_path:?}");
}
}
}
#[cfg(test)]
mod tests {
use objc2_core_foundation::{CGPoint, CGRect, CGSize};
use super::*;
#[test]
fn release_order_does_not_matter() {
let root_layer = CALayer::new();
let layer = ObserverLayer::new(&root_layer);
drop(root_layer);
drop(layer);
let root_layer = CALayer::new();
let layer = ObserverLayer::new(&root_layer);
drop(layer);
drop(root_layer);
}
#[test]
fn scale_factor_propagates() {
let root_layer = CALayer::new();
let layer = ObserverLayer::new(&root_layer);
root_layer.setContentsScale(3.0);
assert_eq!(layer.contentsScale(), 3.0);
}
#[test]
fn bounds_propagates() {
let root_layer = CALayer::new();
let layer = ObserverLayer::new(&root_layer);
root_layer.setBounds(CGRect::new(
CGPoint::new(10.0, 20.0),
CGSize::new(30.0, 40.0),
));
assert_eq!(layer.position(), CGPoint::new(25.0, 40.0));
assert_eq!(
layer.bounds(),
CGRect::new(CGPoint::new(0.0, 0.0), CGSize::new(30.0, 40.0),)
);
}
#[test]
fn superlayer_can_remove_all_sublayers() {
let root_layer = CALayer::new();
let layer = ObserverLayer::new(&root_layer);
layer.removeFromSuperlayer();
drop(layer);
root_layer.setContentsScale(3.0);
}
}