Skip to main content

ferroui_native/
window_impl.rs

1use crate::double_click_helper::DoubleClickHelper;
2use crate::ferro_native_menu_exporter::FerroNativeMenuExporter;
3use crate::ferro_native_platform_extensions::FerroNativePlatformOptions;
4use crate::frn_string::to_c_string;
5use crate::helpers::*;
6use crate::interop::*;
7use crate::popup_impl::PopupImpl;
8use crate::top_level_impl::{
9    callback_property, impl_top_level_contract, MacOSTopLevelHandle, TopLevelEvents, TopLevelImpl, TopLevelParent,
10};
11use crate::window_impl_base::{impl_window_base_contract, WindowBaseImpl, WindowBaseParent, WindowEventsParent};
12use ferroui_base::input::raw::{RawPointerEventArgs, RawPointerEventType};
13use ferroui_base::input::{InputElement, PointerPressedEventArgs, WindowDecorationsElementRole};
14use ferroui_base::media::Color;
15use ferroui_base::{PixelPoint, Ref, Size, Thickness, Visual};
16use ferroui_controls::chrome::WindowDecorationProperties;
17use ferroui_controls::platform::{
18    ITopLevelNativeMenuExporter, IPopupImpl, ITopLevelImpl, IWindowBaseImpl, IWindowIconImpl, IWindowImpl, PlatformRequestedDrawnDecoration,
19    PlatformThemeVariant,
20};
21use ferroui_controls::{WindowCloseReason, WindowDecorations, WindowEdge, WindowResizeReason, WindowState};
22use ferroui_microcom::ComPtr;
23use std::any::{Any, TypeId};
24use std::cell::{Cell, RefCell};
25use std::rc::{Rc, Weak};
26
27/// A macOS window.
28pub struct WindowImpl {
29    weak_self: Weak<WindowImpl>,
30    base: WindowBaseImpl,
31    opts: FerroNativePlatformOptions,
32    /// The native window; `None` until it is created and once it is
33    /// disposed.
34    native: RefCell<Option<ComPtr<IFrnWindow>>>,
35    extend_title_bar_height: Cell<f64>,
36    double_click_helper: DoubleClickHelper,
37    can_resize: Cell<bool>,
38    can_maximize: Cell<bool>,
39    decorations: Cell<WindowDecorations>,
40    native_menu_exporter: RefCell<Option<Rc<FerroNativeMenuExporter>>>,
41    is_extended: Cell<bool>,
42    extended_margins: Cell<Thickness>,
43
44    window_state_changed: RefCell<Option<Rc<dyn Fn(WindowState)>>>,
45    extend_client_area_to_decorations_changed: RefCell<Option<Rc<dyn Fn(bool)>>>,
46    closing: RefCell<Option<Rc<dyn Fn(WindowCloseReason) -> bool>>>,
47    got_input_when_disabled: RefCell<Option<Rc<dyn Fn()>>>,
48}
49
50impl WindowImpl {
51    pub(crate) fn new(factory: ComPtr<IFerroNativeFactory>, opts: FerroNativePlatformOptions) -> Rc<WindowImpl> {
52        let this = Rc::new_cyclic(|weak_self| WindowImpl {
53            weak_self: weak_self.clone(),
54            base: WindowBaseImpl::new(factory.clone()),
55            opts,
56            native: RefCell::new(None),
57            extend_title_bar_height: Cell::new(-1.0),
58            double_click_helper: DoubleClickHelper::new(),
59            can_resize: Cell::new(true),
60            can_maximize: Cell::new(true),
61            decorations: Cell::new(WindowDecorations::Full),
62            native_menu_exporter: RefCell::new(None),
63            is_extended: Cell::new(false),
64            extended_margins: Cell::new(Thickness::default()),
65            window_state_changed: RefCell::new(None),
66            extend_client_area_to_decorations_changed: RefCell::new(None),
67            closing: RefCell::new(None),
68            got_input_when_disabled: RefCell::new(None),
69        });
70
71        let e = IFrnWindowEvents::from_impl(TopLevelEvents(this.clone()));
72        let native = factory.create_window(Some(&e)).check().expect("the native window");
73        *this.native.borrow_mut() = Some(native.clone());
74        this.base.init(MacOSTopLevelHandle::from_window_base(ComPtr::<IFrnWindowBase>::from_ref(&native)));
75
76        *this.native_menu_exporter.borrow_mut() = Some(FerroNativeMenuExporter::for_window(native, &factory));
77
78        this
79    }
80
81    /// The native window; `None` once the window is disposed.
82    pub fn native(&self) -> Option<ComPtr<IFrnWindow>> {
83        self.native.borrow().clone()
84    }
85
86    /// The native window of a live window.
87    ///
88    /// # Panics
89    /// Panics when the window is disposed, like every use of a disposed
90    /// native object in the reference implementation.
91    #[track_caller]
92    fn live_native(&self) -> ComPtr<IFrnWindow> {
93        match self.native() {
94            Some(native) => native,
95            None => panic!("Cannot access a disposed object: the native window"),
96        }
97    }
98
99    pub(crate) fn top_level(&self) -> &Rc<TopLevelImpl> {
100        self.base.top_level()
101    }
102
103    pub fn set_title_bar_color(&self, color: Color) {
104        self.live_native()
105            .set_title_bar_color(FrnColor { alpha: color.a, red: color.r, green: color.g, blue: color.b })
106            .check();
107    }
108
109    pub fn z_order(&self) -> Option<isize> {
110        Some(self.live_native().get_window_z_order().check() as isize)
111    }
112
113    fn invalidate_extended_margins(&self) {
114        let Some(native) = self.native() else {
115            return;
116        };
117
118        if self.window_state() == WindowState::FullScreen
119            || !self.is_extended.get()
120            || self.decorations.get() != WindowDecorations::Full
121        {
122            self.extended_margins.set(Thickness::default());
123        } else {
124            let top = if self.extend_title_bar_height.get() == -1.0 {
125                native.get_extend_title_bar_height().check()
126            } else {
127                self.extend_title_bar_height.get()
128            };
129            self.extended_margins.set(Thickness::new(0.0, top, 0.0, 0.0));
130        }
131
132        let changed = self.extend_client_area_to_decorations_changed.borrow().clone();
133        if let Some(changed) = changed {
134            changed(self.is_extended.get());
135        }
136    }
137
138    callback_property!(window_state_changed_cb, set_window_state_changed_cb, window_state_changed, dyn Fn(WindowState));
139    callback_property!(
140        extend_client_area_to_decorations_changed_cb,
141        set_extend_client_area_to_decorations_changed_cb,
142        extend_client_area_to_decorations_changed,
143        dyn Fn(bool)
144    );
145    callback_property!(closing_cb, set_closing_cb, closing, dyn Fn(WindowCloseReason) -> bool);
146    callback_property!(got_input_when_disabled_cb, set_got_input_when_disabled_cb, got_input_when_disabled, dyn Fn());
147}
148
149impl TopLevelParent for WindowImpl {
150    fn top_level(&self) -> &Rc<TopLevelImpl> {
151        self.base.top_level()
152    }
153
154    fn dispose_top_level(&self) {
155        self.base.dispose();
156        let exporter = self.native_menu_exporter.borrow_mut().take();
157        drop(exporter);
158        let native = self.native.borrow_mut().take();
159        drop(native);
160    }
161
162    fn chrome_hit_test(&self, e: &RawPointerEventArgs) -> bool {
163        if self.is_extended.get() && e.type_() == RawPointerEventType::LeftButtonDown {
164            // The presentation source behind the input root hit-tests its
165            // visual tree.
166            let root: Option<Ref<Visual>> = self.top_level().input_root().map(|root| root.root_element().upcast());
167            let source = root.as_ref().and_then(|root| root.presentation_source());
168            let visual = match (&source, &root) {
169                (Some(source), Some(root)) => source.hit_tester().hit_test_first(
170                    e.position(),
171                    root,
172                    Some(&|x: &Visual| {
173                        if let Some(ie) = x.to_ref().cast::<InputElement>() {
174                            if !ie.is_hit_test_visible() || !ie.is_effectively_visible() {
175                                return false;
176                            }
177                        }
178                        true
179                    }),
180                ),
181                _ => None,
182            };
183
184            if visual.is_none_or(|visual| {
185                WindowDecorationProperties::get_element_role(&visual) == WindowDecorationsElementRole::TitleBar
186            }) {
187                if self.double_click_helper.is_double_click(e.timestamp(), e.position()) {
188                    match self.window_state() {
189                        WindowState::Maximized | WindowState::FullScreen if self.can_resize.get() => {
190                            self.set_window_state(WindowState::Normal);
191                        }
192                        WindowState::Normal if self.can_maximize.get() => {
193                            self.set_window_state(WindowState::Maximized);
194                        }
195                        _ => {}
196                    }
197                } else {
198                    self.live_native().begin_move_drag().check();
199                }
200            }
201        }
202
203        false
204    }
205
206    fn create_popup_core(&self) -> Option<Rc<dyn IPopupImpl>> {
207        if self.opts.overlay_popups {
208            return None;
209        }
210        let this: Rc<dyn ITopLevelImpl> = self.weak_self.upgrade()?;
211        Some(PopupImpl::new(self.top_level().factory().clone(), this))
212    }
213
214    fn set_frame_theme_variant_core(&self, theme_variant: Option<PlatformThemeVariant>) {
215        self.base.set_frame_theme_variant(theme_variant);
216    }
217
218    fn try_get_feature_core(&self, feature_type: TypeId) -> Option<Rc<dyn Any>> {
219        if feature_type == TypeId::of::<dyn ITopLevelNativeMenuExporter>() {
220            let exporter: Rc<dyn ITopLevelNativeMenuExporter> = self.native_menu_exporter.borrow().clone()?;
221            return Some(Rc::new(exporter));
222        }
223
224        self.top_level().try_get_feature(feature_type)
225    }
226}
227
228impl WindowBaseParent for WindowImpl {
229    fn window_base(&self) -> &WindowBaseImpl {
230        &self.base
231    }
232
233    fn show_core(&self, activate: bool, is_dialog: bool) {
234        self.base.show(activate, is_dialog);
235
236        self.invalidate_extended_margins();
237    }
238}
239
240impl WindowEventsParent for WindowImpl {
241    fn on_closing(&self) -> bool {
242        let closing = self.closing.borrow().clone();
243        if let Some(closing) = closing {
244            return closing(WindowCloseReason::WindowClosing);
245        }
246
247        true
248    }
249
250    fn on_window_state_changed(&self, state: FrnWindowState) {
251        self.invalidate_extended_margins();
252
253        let changed = self.window_state_changed.borrow().clone();
254        if let Some(changed) = changed {
255            changed(to_window_state(state));
256        }
257    }
258
259    fn on_got_input_when_disabled(&self) {
260        let callback = self.got_input_when_disabled.borrow().clone();
261        if let Some(callback) = callback {
262            callback();
263        }
264    }
265}
266
267impl_top_level_contract!(WindowImpl {
268    fn as_window_base_impl(&self) -> Option<&dyn IWindowBaseImpl> {
269        Some(self)
270    }
271
272    fn as_window_impl(&self) -> Option<&dyn IWindowImpl> {
273        Some(self)
274    }
275});
276
277impl_window_base_contract!(WindowImpl);
278
279impl IWindowImpl for WindowImpl {
280    fn window_state(&self) -> WindowState {
281        to_window_state(self.live_native().get_window_state().check())
282    }
283
284    fn set_window_state(&self, value: WindowState) {
285        self.live_native().set_window_state(to_frn_window_state(value)).check();
286    }
287
288    fn window_state_getter_is_usable(&self) -> bool {
289        false
290    }
291
292    fn window_state_changed(&self) -> Option<Rc<dyn Fn(WindowState)>> {
293        self.window_state_changed_cb()
294    }
295
296    fn set_window_state_changed(&self, value: Option<Rc<dyn Fn(WindowState)>>) {
297        self.set_window_state_changed_cb(value)
298    }
299
300    fn set_title(&self, title: Option<&str>) {
301        self.live_native().set_title(Some(&to_c_string(title.unwrap_or("")))).check();
302    }
303
304    fn set_parent(&self, parent: Option<Rc<dyn IWindowImpl>>) {
305        let parent_native = parent.as_ref().map(|parent| {
306            let Some(parent) = parent.as_any().downcast_ref::<WindowImpl>() else {
307                panic!("The parent window belongs to a different windowing platform.");
308            };
309            parent.live_native()
310        });
311        let parent_base: Option<&IFrnWindowBase> = parent_native.as_deref().map(|native| &**native);
312        self.live_native().set_parent(parent_base).check();
313    }
314
315    fn set_enabled(&self, enable: bool) {
316        self.live_native().set_enabled(enable).check();
317
318        // Showing a dialog should result in mouse capture being lost. macOS doesn't have the concept of mouse
319        // capture, so no we have no OS-level event to hook into. Instead, release the mouse capture when the
320        // owner window is disabled. This behavior matches win32, which sends a WM_CANCELMODE message when
321        // EnableWindow(hWnd, false) is called from SetEnabled.
322        if !enable {
323            self.top_level().mouse().platform_capture_lost();
324        }
325    }
326
327    fn got_input_when_disabled(&self) -> Option<Rc<dyn Fn()>> {
328        self.got_input_when_disabled_cb()
329    }
330
331    fn set_got_input_when_disabled(&self, value: Option<Rc<dyn Fn()>>) {
332        self.set_got_input_when_disabled_cb(value)
333    }
334
335    fn set_window_decorations(&self, enabled: WindowDecorations) {
336        self.decorations.set(enabled);
337        self.live_native().set_decorations(to_frn_decorations(enabled)).check();
338        self.invalidate_extended_margins();
339    }
340
341    fn set_icon(&self, _icon: Option<Rc<dyn IWindowIconImpl>>) {
342        // NO OP on OSX
343    }
344
345    fn show_taskbar_icon(&self, _value: bool) {
346        // NO OP On OSX
347    }
348
349    fn can_resize(&self, value: bool) {
350        self.can_resize.set(value);
351        self.live_native().set_can_resize(value).check();
352    }
353
354    fn set_can_minimize(&self, value: bool) {
355        self.live_native().set_can_minimize(value).check();
356    }
357
358    fn set_can_maximize(&self, value: bool) {
359        self.can_maximize.set(value);
360        self.live_native().set_can_maximize(value).check();
361    }
362
363    fn closing(&self) -> Option<Rc<dyn Fn(WindowCloseReason) -> bool>> {
364        self.closing_cb()
365    }
366
367    fn set_closing(&self, value: Option<Rc<dyn Fn(WindowCloseReason) -> bool>>) {
368        self.set_closing_cb(value)
369    }
370
371    fn is_client_area_extended_to_decorations(&self) -> bool {
372        self.is_extended.get()
373    }
374
375    fn extend_client_area_to_decorations_changed(&self) -> Option<Rc<dyn Fn(bool)>> {
376        self.extend_client_area_to_decorations_changed_cb()
377    }
378
379    fn set_extend_client_area_to_decorations_changed(&self, value: Option<Rc<dyn Fn(bool)>>) {
380        self.set_extend_client_area_to_decorations_changed_cb(value)
381    }
382
383    fn needs_managed_decorations(&self) -> bool {
384        false
385    }
386
387    // Extension is handled by native backend
388    fn requested_drawn_decorations(&self) -> PlatformRequestedDrawnDecoration {
389        PlatformRequestedDrawnDecoration::NONE
390    }
391
392    fn extended_margins(&self) -> Thickness {
393        self.extended_margins.get()
394    }
395
396    fn off_screen_margin(&self) -> Thickness {
397        Thickness::default()
398    }
399
400    fn begin_move_drag(&self, _e: &PointerPressedEventArgs) {
401        self.base.begin_move_drag();
402    }
403
404    fn begin_resize_drag(&self, edge: WindowEdge, _e: &PointerPressedEventArgs) {
405        self.base.begin_resize_drag(edge);
406    }
407
408    fn resize(&self, client_size: Size, reason: WindowResizeReason) {
409        self.base.resize(client_size, reason);
410    }
411
412    fn move_(&self, point: PixelPoint) {
413        self.base.set_position(point);
414    }
415
416    fn set_min_max_size(&self, min_size: Size, max_size: Size) {
417        self.base.set_min_max_size(min_size, max_size);
418    }
419
420    fn set_extend_client_area_to_decorations_hint(&self, extend_into_client_area_hint: bool) {
421        self.is_extended.set(extend_into_client_area_hint);
422
423        self.live_native().set_extend_client_area(extend_into_client_area_hint).check();
424
425        self.invalidate_extended_margins();
426    }
427
428    fn set_extend_client_area_title_bar_height_hint(&self, title_bar_height: f64) {
429        self.extend_title_bar_height.set(title_bar_height);
430        self.live_native().set_extend_title_bar_height(title_bar_height).check();
431
432        self.invalidate_extended_margins();
433    }
434}