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
27pub struct WindowImpl {
29 weak_self: Weak<WindowImpl>,
30 base: WindowBaseImpl,
31 opts: FerroNativePlatformOptions,
32 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 pub fn native(&self) -> Option<ComPtr<IFrnWindow>> {
83 self.native.borrow().clone()
84 }
85
86 #[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 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 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 }
344
345 fn show_taskbar_icon(&self, _value: bool) {
346 }
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 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}