1use ferroui_base::reactive::{Disposable, IDisposable};
5use ferroui_base::utilities::HandlerList;
6use ferroui_base::{Rect, Size, Thickness};
7use std::cell::Cell;
8use std::rc::Rc;
9
10#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
12#[repr(isize)]
13pub enum StatusBarStyle {
14 #[default]
16 Default = 0,
17 LightContent = 1,
19 DarkContent = 3,
21}
22
23pub trait IFerroViewController {
25 fn preferred_status_bar_style(&self) -> StatusBarStyle;
27
28 fn set_preferred_status_bar_style(&self, value: StatusBarStyle);
30
31 fn prefers_status_bar_hidden(&self) -> bool;
33
34 fn set_prefers_status_bar_hidden(&self, value: bool);
36
37 fn safe_area_padding(&self) -> Thickness;
39
40 fn safe_area_padding_changed(&self, handler: Rc<dyn Fn()>) -> Rc<dyn IDisposable>;
42}
43
44pub fn safe_area_padding_of(size: Size, frame: Rect) -> Thickness {
47 Thickness::new(frame.left(), frame.top(), size.width - frame.right(), size.height - frame.bottom())
48}
49
50pub struct ViewControllerState {
53 preferred_status_bar_style: Cell<Option<StatusBarStyle>>,
54 prefers_status_bar_hidden: Cell<Option<bool>>,
55 safe_area_padding: Cell<Thickness>,
56 safe_area_padding_changed: Rc<HandlerList<dyn Fn()>>,
57 needs_status_bar_appearance_update: Box<dyn Fn()>,
59}
60
61impl ViewControllerState {
62 pub fn new(needs_status_bar_appearance_update: Box<dyn Fn()>) -> Rc<Self> {
65 Rc::new(Self {
66 preferred_status_bar_style: Cell::new(None),
67 prefers_status_bar_hidden: Cell::new(None),
68 safe_area_padding: Cell::new(Thickness::default()),
69 safe_area_padding_changed: Rc::new(HandlerList::new()),
70 needs_status_bar_appearance_update,
71 })
72 }
73
74 pub fn preferred_status_bar_style_override(&self) -> Option<StatusBarStyle> {
76 self.preferred_status_bar_style.get()
77 }
78
79 pub fn prefers_status_bar_hidden_or(&self, base: impl FnOnce() -> bool) -> bool {
82 match self.prefers_status_bar_hidden.get() {
83 Some(value) => value,
84 None => {
85 let value = base();
86 self.prefers_status_bar_hidden.set(Some(value));
87 value
88 }
89 }
90 }
91
92 pub fn view_did_layout_subviews(&self, size: Size, frame: Rect) {
95 let safe_area = safe_area_padding_of(size, frame);
96 if self.safe_area_padding.get() != safe_area {
97 self.safe_area_padding.set(safe_area);
98 for (_, handler) in self.safe_area_padding_changed.snapshot().iter() {
99 handler();
100 }
101 }
102 }
103}
104
105impl IFerroViewController for ViewControllerState {
106 fn preferred_status_bar_style(&self) -> StatusBarStyle {
107 self.preferred_status_bar_style.get().unwrap_or_default()
108 }
109
110 fn set_preferred_status_bar_style(&self, value: StatusBarStyle) {
111 self.preferred_status_bar_style.set(Some(value));
112 (self.needs_status_bar_appearance_update)();
113 }
114
115 fn prefers_status_bar_hidden(&self) -> bool {
116 self.prefers_status_bar_hidden.get().unwrap_or(false)
118 }
119
120 fn set_prefers_status_bar_hidden(&self, value: bool) {
121 self.prefers_status_bar_hidden.set(Some(value));
122 (self.needs_status_bar_appearance_update)();
123 }
124
125 fn safe_area_padding(&self) -> Thickness {
126 self.safe_area_padding.get()
127 }
128
129 fn safe_area_padding_changed(&self, handler: Rc<dyn Fn()>) -> Rc<dyn IDisposable> {
130 let token = self.safe_area_padding_changed.add(handler);
131 let handlers = self.safe_area_padding_changed.clone();
132 Disposable::create(move || {
133 handlers.remove(token);
134 })
135 }
136}
137
138#[cfg(target_os = "ios")]
139pub use uikit::DefaultFerroViewController;
140
141#[cfg(target_os = "ios")]
142mod uikit {
143 use super::{IFerroViewController, StatusBarStyle, ViewControllerState};
144 use crate::extensions::{to_rect, to_size};
145 use ferroui_base::reactive::IDisposable;
146 use ferroui_base::{Rect, Size, Thickness};
147 use objc2::rc::{Retained, Weak};
148 use objc2::{define_class, msg_send, DefinedClass, MainThreadMarker, MainThreadOnly};
149 use objc2_foundation::NSObjectProtocol;
150 use objc2_ui_kit::{UIStatusBarStyle, UIViewController};
151 use std::cell::OnceCell;
152 use std::rc::Rc;
153
154 #[derive(Default)]
156 pub struct DefaultFerroViewControllerIvars {
157 state: OnceCell<Rc<ViewControllerState>>,
158 }
159
160 define_class!(
161 #[unsafe(super(UIViewController))]
165 #[thread_kind = MainThreadOnly]
166 #[name = "DefaultFerroViewController"]
167 #[ivars = DefaultFerroViewControllerIvars]
168 pub struct DefaultFerroViewController;
169
170 impl DefaultFerroViewController {
171 #[unsafe(method(viewDidLayoutSubviews))]
172 fn view_did_layout_subviews(&self) {
173 let _: () = unsafe { msg_send![super(self), viewDidLayoutSubviews] };
175 let (size, frame) = match self.viewIfLoaded() {
176 Some(view) => (to_size(view.frame().size), to_rect(view.safeAreaLayoutGuide().layoutFrame())),
177 None => (Size::default(), Rect::default()),
178 };
179 self.state().view_did_layout_subviews(size, frame);
180 }
181
182 #[unsafe(method(prefersStatusBarHidden))]
183 fn prefers_status_bar_hidden(&self) -> bool {
184 self.state().prefers_status_bar_hidden_or(|| {
185 unsafe { msg_send![super(self), prefersStatusBarHidden] }
187 })
188 }
189
190 #[unsafe(method(preferredStatusBarStyle))]
191 fn preferred_status_bar_style(&self) -> UIStatusBarStyle {
192 match self.state().preferred_status_bar_style_override() {
195 Some(style) => UIStatusBarStyle(style as isize),
196 None => unsafe { msg_send![super(self), preferredStatusBarStyle] },
199 }
200 }
201 }
202
203 unsafe impl NSObjectProtocol for DefaultFerroViewController {}
204 );
205
206 impl DefaultFerroViewController {
207 pub fn new(mtm: MainThreadMarker) -> Retained<Self> {
209 let this = mtm.alloc::<Self>().set_ivars(DefaultFerroViewControllerIvars::default());
210 let this: Retained<Self> = unsafe { msg_send![super(this), init] };
213
214 let weak = Weak::from_retained(&this);
215 let state = ViewControllerState::new(Box::new(move || {
216 if let Some(controller) = weak.load() {
217 controller.setNeedsStatusBarAppearanceUpdate();
218 }
219 }));
220 let _ = this.ivars().state.set(state);
221 this
222 }
223
224 fn state(&self) -> &Rc<ViewControllerState> {
225 match self.ivars().state.get() {
226 Some(state) => state,
227 None => panic!("The view controller was not created by DefaultFerroViewController::new."),
228 }
229 }
230
231 pub fn controller(&self) -> Rc<dyn IFerroViewController> {
233 self.state().clone()
234 }
235 }
236
237 impl IFerroViewController for DefaultFerroViewController {
244 fn preferred_status_bar_style(&self) -> StatusBarStyle {
245 self.state().preferred_status_bar_style()
246 }
247
248 fn set_preferred_status_bar_style(&self, value: StatusBarStyle) {
249 self.state().set_preferred_status_bar_style(value);
250 }
251
252 fn prefers_status_bar_hidden(&self) -> bool {
253 IFerroViewController::prefers_status_bar_hidden(&**self.state())
254 }
255
256 fn set_prefers_status_bar_hidden(&self, value: bool) {
257 self.state().set_prefers_status_bar_hidden(value);
258 }
259
260 fn safe_area_padding(&self) -> Thickness {
261 self.state().safe_area_padding()
262 }
263
264 fn safe_area_padding_changed(&self, handler: Rc<dyn Fn()>) -> Rc<dyn IDisposable> {
265 self.state().safe_area_padding_changed(handler)
266 }
267 }
268
269 const _: () = {
270 assert!(StatusBarStyle::Default as isize == UIStatusBarStyle::Default.0);
272 assert!(StatusBarStyle::LightContent as isize == UIStatusBarStyle::LightContent.0);
273 assert!(StatusBarStyle::DarkContent as isize == UIStatusBarStyle::DarkContent.0);
274 };
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
281 use std::cell::Cell;
282
283 fn state() -> (Rc<ViewControllerState>, Rc<Cell<u32>>) {
284 let updates = Rc::new(Cell::new(0));
285 let state = ViewControllerState::new({
286 let updates = updates.clone();
287 Box::new(move || updates.set(updates.get() + 1))
288 });
289 (state, updates)
290 }
291
292 #[test]
293 fn the_safe_area_padding_is_what_the_layout_guide_leaves_free() {
294 let padding = safe_area_padding_of(Size::new(393.0, 852.0), Rect::new(0.0, 59.0, 393.0, 759.0));
297 assert_eq!(Thickness::new(0.0, 59.0, 0.0, 34.0), padding);
298
299 let padding = safe_area_padding_of(Size::new(852.0, 393.0), Rect::new(59.0, 0.0, 734.0, 372.0));
301 assert_eq!(Thickness::new(59.0, 0.0, 59.0, 21.0), padding);
302 }
303
304 #[test]
305 fn a_layout_raises_the_change_only_when_the_padding_changed() {
306 let (state, _) = state();
307 let changes = Rc::new(Cell::new(0));
308 let subscription = state.safe_area_padding_changed({
309 let changes = changes.clone();
310 Rc::new(move || changes.set(changes.get() + 1))
311 });
312
313 state.view_did_layout_subviews(Size::new(393.0, 852.0), Rect::new(0.0, 59.0, 393.0, 759.0));
314 assert_eq!(1, changes.get());
315 assert_eq!(Thickness::new(0.0, 59.0, 0.0, 34.0), state.safe_area_padding());
316
317 state.view_did_layout_subviews(Size::new(393.0, 852.0), Rect::new(0.0, 59.0, 393.0, 759.0));
318 assert_eq!(1, changes.get());
319
320 subscription.dispose();
321 state.view_did_layout_subviews(Size::new(852.0, 393.0), Rect::new(59.0, 0.0, 734.0, 372.0));
322 assert_eq!(1, changes.get());
323 }
324
325 #[test]
326 fn the_status_bar_properties_have_the_defaults_of_the_system_until_set() {
327 let (state, updates) = state();
328
329 assert_eq!(StatusBarStyle::Default, state.preferred_status_bar_style());
330 assert_eq!(None, state.preferred_status_bar_style_override());
331 assert!(!state.prefers_status_bar_hidden());
332
333 state.set_preferred_status_bar_style(StatusBarStyle::LightContent);
334 assert_eq!(Some(StatusBarStyle::LightContent), state.preferred_status_bar_style_override());
335 state.set_prefers_status_bar_hidden(true);
336 assert!(state.prefers_status_bar_hidden());
337 assert_eq!(2, updates.get());
338 }
339
340 #[test]
341 fn the_answer_of_the_system_for_the_hidden_status_bar_is_kept() {
342 let (state, _) = state();
343
344 assert!(state.prefers_status_bar_hidden_or(|| true));
345 assert!(state.prefers_status_bar_hidden_or(|| false));
347 assert!(state.prefers_status_bar_hidden());
348 }
349}