Skip to main content

ferroui_ios/
native_control_host_impl.rs

1//! The native control host of a view: the views of an application become
2//! subviews of the view of a top-level, where the layout of the framework
3//! puts them. And the handle of a `UIView`.
4
5use ferroui_base::{Rect, Size};
6
7fn math_max(val1: f64, val2: f64) -> f64 {
8    if val1.is_nan() || val2.is_nan() {
9        f64::NAN
10    } else {
11        val1.max(val2)
12    }
13}
14
15/// The frame of a hidden native control of a size: at the origin, at
16/// least one point wide and high.
17pub fn hidden_frame(size: Size) -> Rect {
18    Rect::new(0.0, 0.0, math_max(1.0, size.width), math_max(1.0, size.height))
19}
20
21/// The frame of a native control shown in bounds: at least one point
22/// wide and high.
23pub fn shown_frame(bounds: Rect) -> Rect {
24    Rect::new(bounds.x, bounds.y, math_max(1.0, bounds.width), math_max(1.0, bounds.height))
25}
26
27#[cfg(target_os = "ios")]
28pub use uikit::UIViewControlHandle;
29#[cfg(target_os = "ios")]
30pub(crate) use uikit::NativeControlHostImpl;
31
32#[cfg(target_os = "ios")]
33mod uikit {
34    use super::{hidden_frame, shown_frame};
35    use crate::extensions::to_cg_rect;
36    use crate::ferro_view::FerroView;
37    use ferroui_base::reactive::IDisposable;
38    use ferroui_base::{Rect, Size};
39    use ferroui_controls::platform::{
40        INativeControlHostControlTopLevelAttachment, INativeControlHostDestroyableControlHandle,
41        INativeControlHostImpl, IPlatformHandle,
42    };
43    use objc2::rc::{Retained, Weak};
44    use objc2::{MainThreadMarker, MainThreadOnly};
45    use objc2_ui_kit::UIView;
46    use std::any::Any;
47    use std::cell::RefCell;
48    use std::rc::{Rc, Weak as RcWeak};
49
50    /// The native control host of a view.
51    pub(crate) struct NativeControlHostImpl {
52        this: RcWeak<NativeControlHostImpl>,
53        ferro_view: Weak<FerroView>,
54    }
55
56    impl NativeControlHostImpl {
57        pub(crate) fn new(ferro_view: Weak<FerroView>) -> Rc<Self> {
58            Rc::new_cyclic(|this| Self { this: this.clone(), ferro_view })
59        }
60
61        fn rc(&self) -> Rc<NativeControlHostImpl> {
62            match self.this.upgrade() {
63                Some(this) => this,
64                None => panic!("The native control host is used while it is released."),
65            }
66        }
67
68        fn view(&self) -> Retained<FerroView> {
69            match self.ferro_view.load() {
70                Some(view) => view,
71                None => panic!("The view of the native control host was released."),
72            }
73        }
74    }
75
76    impl INativeControlHostImpl for NativeControlHostImpl {
77        fn as_any(&self) -> &dyn Any {
78            self
79        }
80
81        fn create_default_child(
82            &self,
83            _parent: Rc<dyn IPlatformHandle>,
84        ) -> Rc<dyn INativeControlHostDestroyableControlHandle> {
85            let mtm = self.view().mtm();
86            Rc::new(UIViewControlHandle::new(UIView::new(mtm)))
87        }
88
89        fn create_new_attachment_with(
90            &self,
91            create: Rc<dyn Fn(Rc<dyn IPlatformHandle>) -> Rc<dyn IPlatformHandle>>,
92        ) -> Rc<dyn INativeControlHostControlTopLevelAttachment> {
93            let parent = Rc::new(UIViewControlHandle::new(Retained::into_super(self.view())));
94            // The reference disposes the attachment when attaching it
95            // fails; attaching does not fail here with an error that
96            // could be handed on.
97            let child = create(parent);
98            let attachment = Rc::new(NativeControlAttachment::new(child));
99            attachment.set_attached_to(Some(self.rc()));
100            attachment
101        }
102
103        fn create_new_attachment(
104            &self,
105            handle: Rc<dyn IPlatformHandle>,
106        ) -> Rc<dyn INativeControlHostControlTopLevelAttachment> {
107            let attachment = Rc::new(NativeControlAttachment::new(handle));
108            attachment.set_attached_to(Some(self.rc()));
109            attachment
110        }
111
112        fn is_compatible_with(&self, handle: &dyn IPlatformHandle) -> bool {
113            handle.handle_descriptor() == Some(UIViewControlHandle::UI_VIEW_DESCRIPTOR)
114        }
115    }
116
117    /// The view of a handle that is not a handle of this crate: the
118    /// pointer of the handle, which its descriptor says is a `UIView`.
119    fn view_holder(handle: &dyn IPlatformHandle) -> Option<Retained<UIView>> {
120        MainThreadMarker::new()?;
121        if handle.handle_descriptor() != Some(UIViewControlHandle::UI_VIEW_DESCRIPTOR) {
122            return None;
123        }
124        // SAFETY: a handle with the descriptor of a `UIView` holds the
125        // pointer of a live `UIView` (the contract of the descriptor);
126        // the view is retained for as long as the attachment has it. The
127        // attachment is used on the main thread, which was just checked.
128        unsafe { Retained::retain(handle.handle() as *mut UIView) }
129    }
130
131    struct NativeControlAttachment {
132        // Keeps the handle, and with it the view, alive.
133        child: RefCell<Option<Rc<dyn IPlatformHandle>>>,
134        view: RefCell<Option<Retained<UIView>>>,
135        attached_to: RefCell<Option<Rc<NativeControlHostImpl>>>,
136    }
137
138    impl NativeControlAttachment {
139        fn new(child: Rc<dyn IPlatformHandle>) -> Self {
140            let view = match child.as_any().downcast_ref::<UIViewControlHandle>() {
141                Some(handle) => Some(handle.view().clone()),
142                None => view_holder(&*child),
143            };
144            let Some(view) = view else {
145                panic!("The handle of a native control is not the handle of a UIView.");
146            };
147
148            Self { child: RefCell::new(Some(child)), view: RefCell::new(Some(view)), attached_to: RefCell::new(None) }
149        }
150
151        fn check_disposed(&self) -> Retained<UIView> {
152            match self.view.borrow().clone() {
153                Some(view) => view,
154                None => panic!("Cannot access a disposed object. Object name: 'NativeControlAttachment'."),
155            }
156        }
157    }
158
159    impl IDisposable for NativeControlAttachment {
160        fn dispose(&self) {
161            let view = self.view.borrow_mut().take();
162            if let Some(view) = &view {
163                view.removeFromSuperview();
164            }
165            *self.child.borrow_mut() = None;
166            *self.attached_to.borrow_mut() = None;
167        }
168    }
169
170    impl INativeControlHostControlTopLevelAttachment for NativeControlAttachment {
171        fn attached_to(&self) -> Option<Rc<dyn INativeControlHostImpl>> {
172            self.attached_to.borrow().clone().map(|host| host as Rc<dyn INativeControlHostImpl>)
173        }
174
175        fn set_attached_to(&self, value: Option<Rc<dyn INativeControlHostImpl>>) {
176            let view = self.check_disposed();
177
178            let host = value.map(|value| match value.as_any().downcast_ref::<NativeControlHostImpl>() {
179                Some(host) => host.rc(),
180                None => panic!("Unable to cast the native control host to NativeControlHostImpl."),
181            });
182            match &host {
183                None => view.removeFromSuperview(),
184                Some(host) => host.view().addSubview(&view),
185            }
186            *self.attached_to.borrow_mut() = host;
187        }
188
189        fn is_compatible_with(&self, host: &dyn INativeControlHostImpl) -> bool {
190            host.as_any().is::<NativeControlHostImpl>()
191        }
192
193        fn hide_with_size(&self, size: Size) {
194            let view = self.check_disposed();
195            if self.attached_to.borrow().is_none() {
196                return;
197            }
198
199            view.setHidden(true);
200            view.setFrame(to_cg_rect(hidden_frame(size)));
201        }
202
203        fn show_in_bounds(&self, bounds: Rect) {
204            let view = self.check_disposed();
205            if self.attached_to.borrow().is_none() {
206                panic!("The control isn't currently attached to a toplevel");
207            }
208
209            view.setFrame(to_cg_rect(shown_frame(bounds)));
210            view.setHidden(false);
211        }
212    }
213
214    /// The platform handle of a `UIView`.
215    pub struct UIViewControlHandle {
216        view: Retained<UIView>,
217    }
218
219    impl UIViewControlHandle {
220        /// The descriptor of the handle.
221        pub const UI_VIEW_DESCRIPTOR: &'static str = "UIView";
222
223        /// Creates the handle of `view`.
224        pub fn new(view: Retained<UIView>) -> Self {
225            Self { view }
226        }
227
228        /// The view.
229        pub fn view(&self) -> &Retained<UIView> {
230            &self.view
231        }
232    }
233
234    impl IPlatformHandle for UIViewControlHandle {
235        fn handle(&self) -> isize {
236            Retained::as_ptr(&self.view) as isize
237        }
238
239        fn handle_descriptor(&self) -> Option<&str> {
240            Some(Self::UI_VIEW_DESCRIPTOR)
241        }
242
243        fn as_any(&self) -> &dyn Any {
244            self
245        }
246    }
247
248    impl INativeControlHostDestroyableControlHandle for UIViewControlHandle {
249        fn destroy(&self) {
250            // The reference disposes its wrapper of the view, which
251            // releases its reference; the reference of the port is
252            // released with the handle. What the application sees is
253            // that the view leaves the view tree.
254            self.view.removeFromSuperview();
255        }
256    }
257}
258
259#[cfg(test)]
260mod tests {
261    // Not from the reference, which has no tests of this file.
262    use super::*;
263
264    #[test]
265    fn a_hidden_control_keeps_a_size_at_the_origin() {
266        assert_eq!(Rect::new(0.0, 0.0, 120.0, 40.0), hidden_frame(Size::new(120.0, 40.0)));
267        assert_eq!(Rect::new(0.0, 0.0, 1.0, 1.0), hidden_frame(Size::new(0.0, 0.5)));
268    }
269
270    #[test]
271    fn a_shown_control_is_at_least_a_point_wide_and_high() {
272        assert_eq!(Rect::new(10.0, 20.0, 120.0, 40.0), shown_frame(Rect::new(10.0, 20.0, 120.0, 40.0)));
273        assert_eq!(Rect::new(10.0, 20.0, 1.0, 1.0), shown_frame(Rect::new(10.0, 20.0, 0.0, -3.0)));
274    }
275}