1use super::{SplitViewDisplayMode, SplitViewPanePlacement, SplitViewTemplateSettings};
2use crate::presenters::ContentPresenter;
3use crate::primitives::{PopupRoot, TemplateAppliedEventArgs, TemplatedControlImpl, TemplatedControlImplExt};
4use crate::templates::IDataTemplate;
5use crate::{
6 ContentControl, ContentControlImpl, ContentControlImplExt, Control, ControlImpl, GridLength, GridUnitType, Panel, TopLevel,
7};
8use ferroui_base::input::{InputElement, InputElementImpl, InputElementImplExt, Key, KeyEventArgs, PointerReleasedEventArgs};
9use ferroui_base::interactivity::{
10 CancelRoutedEventArgs, Interactive, InteractiveImpl, RoutedEvent, RoutedEventArgs, RoutedEventHandlerToken,
11 RoutingStrategies,
12};
13use ferroui_base::layout::LayoutableImpl;
14use ferroui_base::media::IBrush;
15use ferroui_base::reactive::{Disposable, IDisposable};
16use ferroui_base::{
17 ferro_class, ferro_class_info, ferro_impl_classes, ferro_properties, ferro_routed_event, instantiate, BoxedValue,
18 DirectProperty, DirectPropertyMetadata, FerroObject, FerroObjectImpl, FerroObjectImplExt, FerroProperty,
19 FerroPropertyChangedEventArgs, Ref, StyledElement, StyledElementImpl, StyledProperty, StyledPropertyOptions,
20 Visual, VisualImpl, VisualImplExt, VisualTreeAttachmentEventArgs,
21};
22use std::cell::{Cell, RefCell};
23use std::rc::Rc;
24
25const PC_OPEN: &str = ":open";
26const PC_CLOSED: &str = ":closed";
27const PC_COMPACT_OVERLAY: &str = ":compactoverlay";
28const PC_COMPACT_INLINE: &str = ":compactinline";
29const PC_OVERLAY: &str = ":overlay";
30const PC_INLINE: &str = ":inline";
31const PC_LEFT: &str = ":left";
32const PC_RIGHT: &str = ":right";
33const PC_TOP: &str = ":top";
34const PC_BOTTOM: &str = ":bottom";
35const PC_LIGHT_DISMISS: &str = ":lightDismiss";
36
37#[repr(C)]
39pub struct SplitView {
40 base: ContentControl,
41 pane: RefCell<Option<Ref<Panel>>>,
42 pointer_disposable: RefCell<Option<Rc<dyn IDisposable>>>,
43 template_settings: RefCell<Ref<SplitViewTemplateSettings>>,
44 last_display_mode_pseudoclass: Cell<Option<&'static str>>,
45 last_placement_pseudoclass: Cell<Option<&'static str>>,
46}
47
48ferro_class! {
49 SplitView: ContentControl, virtuals SplitViewImpl: ContentControlImpl {
50 fn on_pane_opening(this, args: &CancelRoutedEventArgs);
52 fn on_pane_opened(this, args: &RoutedEventArgs);
54 fn on_pane_closing(this, args: &CancelRoutedEventArgs);
56 fn on_pane_closed(this, args: &RoutedEventArgs);
58 fn on_coerce_is_pane_open(this, value: bool) -> bool;
60 }
61}
62
63ferro_class_info!(SplitView {
64 new: SplitView::new,
65 markup: {
66 property_attributes: [Pane: [DependsOn("PaneTemplate")]],
67 attributes: [
68 TemplatePart("PART_PaneRoot", type(Ref<Panel>)),
69 PseudoClasses(":open", ":closed"),
70 PseudoClasses(":compactoverlay", ":compactinline", ":overlay", ":inline"),
71 PseudoClasses(":left", ":right", ":top", ":bottom"),
72 PseudoClasses(":lightDismiss"),
73 ],
74 },
75});
76
77ferro_impl_classes!(SplitView: StyledElementImpl, LayoutableImpl, InteractiveImpl, ControlImpl);
78
79impl FerroObjectImpl for SplitView {
80 fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
81 Self::parent_on_property_changed(this, change);
82
83 if change.property() == Self::compact_pane_length_property().as_property() {
84 this.update_visual_state_for_compact_pane_length(change.get_new_value::<f64>());
85 } else if change.property() == Self::display_mode_property().as_property() {
86 this.update_visual_state_for_display_mode(change.get_new_value::<SplitViewDisplayMode>());
87 } else if change.property() == Self::is_pane_open_property().as_property() {
88 let is_pane_open = change.get_new_value::<bool>();
89 this.update_pane_state_pseudo_class(is_pane_open);
90 if is_pane_open {
91 this.on_pane_opened(&RoutedEventArgs::with_event_and_source(Self::pane_opened_event(), this.to_ref()));
92 } else {
93 this.on_pane_closed(&RoutedEventArgs::with_event_and_source(Self::pane_closed_event(), this.to_ref()));
94 }
95 } else if change.property() == Self::pane_property().as_property() {
96 let (old_value, new_value) = change.get_old_and_new_value::<Option<BoxedValue>>();
97
98 if let Some(old_child) = old_value.as_ref().and_then(Control::logical_from_boxed) {
99 StyledElement::logical_children(this).remove(&old_child);
100 }
101
102 if let Some(new_child) = new_value.as_ref().and_then(Control::logical_from_boxed) {
103 StyledElement::logical_children(this).add(new_child);
104 }
105 } else if change.property() == Self::pane_placement_property().as_property() {
106 this.update_visual_state_for_pane_placement_property(change.get_new_value::<SplitViewPanePlacement>());
107 } else if change.property() == Self::use_light_dismiss_overlay_mode_property().as_property() {
108 let mode = change.get_new_value::<bool>();
109 this.pseudo_classes().set(PC_LIGHT_DISMISS, mode);
110 }
111 }
112}
113
114impl VisualImpl for SplitView {
115 fn on_attached_to_visual_tree(this: &Self, e: &VisualTreeAttachmentEventArgs) {
116 Self::parent_on_attached_to_visual_tree(this, e);
117
118 this.update_visual_state_for_pane_placement_property(this.pane_placement());
122 }
123
124 fn on_detached_from_visual_tree(this: &Self, e: &VisualTreeAttachmentEventArgs) {
125 Self::parent_on_detached_from_visual_tree(this, e);
126 this.dispose_pointer_disposable();
127 }
128}
129
130impl InputElementImpl for SplitView {
131 fn on_key_down(this: &Self, e: &KeyEventArgs) {
132 if !e.handled() && e.key == Key::Escape && this.is_pane_open() && this.is_in_overlay_mode() {
133 this.set_current_value(Self::is_pane_open_property(), false);
134 e.set_handled(true);
135 }
136
137 Self::parent_on_key_down(this, e);
138 }
139}
140
141impl TemplatedControlImpl for SplitView {
142 fn on_apply_template(this: &Self, e: &TemplateAppliedEventArgs) {
143 Self::parent_on_apply_template(this, e);
144 let pane = e.name_scope().find_as::<Panel>("PART_PaneRoot");
145 *this.pane.borrow_mut() = pane;
146
147 this.update_visual_state_for_display_mode(this.display_mode());
148 this.update_pane_state_pseudo_class(this.is_pane_open());
149 }
150}
151
152impl ContentControlImpl for SplitView {
153 fn register_content_presenter(this: &Self, presenter: &ContentPresenter) -> bool {
154 let result = Self::parent_register_content_presenter(this, presenter);
155
156 if presenter.name().as_deref() == Some("PART_PanePresenter") {
157 return true;
158 }
159
160 result
161 }
162}
163
164impl SplitViewImpl for SplitView {
165 fn on_pane_opening(this: &Self, args: &CancelRoutedEventArgs) {
166 this.raise_event(args);
167 }
168
169 fn on_pane_opened(this: &Self, args: &RoutedEventArgs) {
170 this.invalidate_light_dismiss_subscription();
171 this.raise_event(args);
172 }
173
174 fn on_pane_closing(this: &Self, args: &CancelRoutedEventArgs) {
175 this.raise_event(args);
176 }
177
178 fn on_pane_closed(this: &Self, args: &RoutedEventArgs) {
179 this.dispose_pointer_disposable();
180 this.raise_event(args);
181 }
182
183 fn on_coerce_is_pane_open(this: &Self, value: bool) -> bool {
184 let event_args = if value {
185 let event_args = CancelRoutedEventArgs::with_event_and_source(Self::pane_opening_event(), this.to_ref());
186 this.on_pane_opening(&event_args);
187 event_args
188 } else {
189 let event_args = CancelRoutedEventArgs::with_event_and_source(Self::pane_closing_event(), this.to_ref());
190 this.on_pane_closing(&event_args);
191 event_args
192 };
193
194 if event_args.cancel() {
195 return !value;
196 }
197
198 value
199 }
200}
201
202ferro_properties! {
203 impl SplitView {
204 pub fn compact_pane_length_property() -> StyledProperty<f64> {
206 FerroProperty::register::<SplitView, _>("CompactPaneLength", 48.0)
207 }
208
209 pub fn display_mode_property() -> StyledProperty<SplitViewDisplayMode> {
211 FerroProperty::register::<SplitView, _>("DisplayMode", SplitViewDisplayMode::Overlay)
212 }
213
214 pub fn is_pane_open_property() -> StyledProperty<bool> {
216 FerroProperty::register_with::<SplitView, _>(
217 "IsPaneOpen",
218 StyledPropertyOptions::new(false).coerce(SplitView::coerce_is_pane_open),
219 )
220 }
221
222 pub fn open_pane_length_property() -> StyledProperty<f64> {
224 FerroProperty::register::<SplitView, _>("OpenPaneLength", 320.0)
225 }
226
227 pub fn pane_background_property() -> StyledProperty<Option<Rc<dyn IBrush>>> {
229 FerroProperty::register::<SplitView, _>("PaneBackground", None)
230 }
231
232 pub fn pane_placement_property() -> StyledProperty<SplitViewPanePlacement> {
234 FerroProperty::register::<SplitView, _>("PanePlacement", SplitViewPanePlacement::Left)
235 }
236
237 pub fn pane_property() -> StyledProperty<Option<BoxedValue>> {
239 FerroProperty::register::<SplitView, _>("Pane", None)
240 }
241
242 pub fn pane_template_property() -> StyledProperty<Option<Rc<dyn IDataTemplate>>> {
244 FerroProperty::register::<SplitView, _>("PaneTemplate", None)
245 }
246
247 pub fn use_light_dismiss_overlay_mode_property() -> StyledProperty<bool> {
249 FerroProperty::register::<SplitView, _>("UseLightDismissOverlayMode", false)
250 }
251
252 pub fn template_settings_property() -> DirectProperty<SplitView, Ref<SplitViewTemplateSettings>> {
254 FerroProperty::register_direct_with::<SplitView, _>(
255 "TemplateSettings",
256 |x| x.template_settings(),
257 None,
258 DirectPropertyMetadata::new(None),
259 )
260 }
261 }
262}
263
264impl SplitView {
265 ferro_routed_event!(
266 pub fn pane_closed_event() -> RoutedEvent<RoutedEventArgs> {
268 RoutedEvent::register::<SplitView, _>("PaneClosed", RoutingStrategies::BUBBLE)
269 }
270 );
271
272 ferro_routed_event!(
273 pub fn pane_closing_event() -> RoutedEvent<CancelRoutedEventArgs> {
275 RoutedEvent::register::<SplitView, _>("PaneClosing", RoutingStrategies::BUBBLE)
276 }
277 );
278
279 ferro_routed_event!(
280 pub fn pane_opened_event() -> RoutedEvent<RoutedEventArgs> {
282 RoutedEvent::register::<SplitView, _>("PaneOpened", RoutingStrategies::BUBBLE)
283 }
284 );
285
286 ferro_routed_event!(
287 pub fn pane_opening_event() -> RoutedEvent<CancelRoutedEventArgs> {
289 RoutedEvent::register::<SplitView, _>("PaneOpening", RoutingStrategies::BUBBLE)
290 }
291 );
292
293 pub fn construct() -> Self {
295 Self {
296 base: ContentControl::construct(),
297 pane: RefCell::new(None),
298 pointer_disposable: RefCell::new(None),
299 template_settings: RefCell::new(SplitViewTemplateSettings::new()),
300 last_display_mode_pseudoclass: Cell::new(None),
301 last_placement_pseudoclass: Cell::new(None),
302 }
303 }
304
305 pub fn new() -> Ref<Self> {
306 instantiate(Self::construct())
307 }
308
309 pub fn compact_pane_length(&self) -> f64 {
312 self.get_value(Self::compact_pane_length_property())
313 }
314
315 pub fn set_compact_pane_length(&self, value: f64) {
316 self.set_value(Self::compact_pane_length_property(), value)
317 }
318
319 pub fn display_mode(&self) -> SplitViewDisplayMode {
321 self.get_value(Self::display_mode_property())
322 }
323
324 pub fn set_display_mode(&self, value: SplitViewDisplayMode) {
325 self.set_value(Self::display_mode_property(), value)
326 }
327
328 pub fn is_pane_open(&self) -> bool {
330 self.get_value(Self::is_pane_open_property())
331 }
332
333 pub fn set_is_pane_open(&self, value: bool) {
334 self.set_value(Self::is_pane_open_property(), value)
335 }
336
337 pub fn open_pane_length(&self) -> f64 {
339 self.get_value(Self::open_pane_length_property())
340 }
341
342 pub fn set_open_pane_length(&self, value: f64) {
343 self.set_value(Self::open_pane_length_property(), value)
344 }
345
346 pub fn pane_background(&self) -> Option<Rc<dyn IBrush>> {
348 self.get_value(Self::pane_background_property())
349 }
350
351 pub fn set_pane_background(&self, value: Option<Rc<dyn IBrush>>) {
352 self.set_value(Self::pane_background_property(), value)
353 }
354
355 pub fn pane_placement(&self) -> SplitViewPanePlacement {
357 self.get_value(Self::pane_placement_property())
358 }
359
360 pub fn set_pane_placement(&self, value: SplitViewPanePlacement) {
361 self.set_value(Self::pane_placement_property(), value)
362 }
363
364 pub fn pane(&self) -> Option<BoxedValue> {
366 self.get_value(Self::pane_property())
367 }
368
369 pub fn set_pane(&self, value: Option<BoxedValue>) {
370 self.set_value(Self::pane_property(), value)
371 }
372
373 pub fn pane_template(&self) -> Option<Rc<dyn IDataTemplate>> {
375 self.get_value(Self::pane_template_property())
376 }
377
378 pub fn set_pane_template(&self, value: Option<Rc<dyn IDataTemplate>>) {
379 self.set_value(Self::pane_template_property(), value)
380 }
381
382 pub fn use_light_dismiss_overlay_mode(&self) -> bool {
388 self.get_value(Self::use_light_dismiss_overlay_mode_property())
389 }
390
391 pub fn set_use_light_dismiss_overlay_mode(&self, value: bool) {
392 self.set_value(Self::use_light_dismiss_overlay_mode_property(), value)
393 }
394
395 pub fn template_settings(&self) -> Ref<SplitViewTemplateSettings> {
397 self.template_settings.borrow().clone()
398 }
399
400 pub fn pane_closed(&self, handler: impl Fn(&Interactive, &RoutedEventArgs) + 'static) -> RoutedEventHandlerToken {
402 self.add_handler(Self::pane_closed_event(), handler)
403 }
404
405 pub fn pane_closing(
410 &self,
411 handler: impl Fn(&Interactive, &CancelRoutedEventArgs) + 'static,
412 ) -> RoutedEventHandlerToken {
413 self.add_handler(Self::pane_closing_event(), handler)
414 }
415
416 pub fn pane_opened(&self, handler: impl Fn(&Interactive, &RoutedEventArgs) + 'static) -> RoutedEventHandlerToken {
418 self.add_handler(Self::pane_opened_event(), handler)
419 }
420
421 pub fn pane_opening(
426 &self,
427 handler: impl Fn(&Interactive, &CancelRoutedEventArgs) + 'static,
428 ) -> RoutedEventHandlerToken {
429 self.add_handler(Self::pane_opening_event(), handler)
430 }
431
432 fn dispose_pointer_disposable(&self) {
433 let disposable = self.pointer_disposable.borrow_mut().take();
434 if let Some(disposable) = disposable {
435 disposable.dispose();
436 }
437 }
438
439 fn pointer_released_outside(&self, e: &PointerReleasedEventArgs) {
440 let Some(pane) = self.pane.borrow().clone().map(Ref::upcast::<Visual>) else {
441 return;
442 };
443 if !self.is_pane_open() {
444 return;
445 }
446
447 let mut close_pane = true;
448 let mut src = e.source().and_then(|source| source.cast::<Visual>());
449 while let Some(current) = src {
450 if current.ptr_eq(&pane) || current.is::<PopupRoot>() {
455 close_pane = false;
456 break;
457 }
458
459 src = current.visual_parent();
460 }
461
462 if close_pane {
463 self.set_current_value(Self::is_pane_open_property(), false);
464 e.set_handled(true);
465 }
466 }
467
468 fn is_in_overlay_mode(&self) -> bool {
469 let display_mode = self.display_mode();
470 display_mode == SplitViewDisplayMode::CompactOverlay || display_mode == SplitViewDisplayMode::Overlay
471 }
472
473 fn display_mode_pseudo_class(mode: SplitViewDisplayMode) -> &'static str {
475 match mode {
476 SplitViewDisplayMode::Inline => PC_INLINE,
477 SplitViewDisplayMode::CompactInline => PC_COMPACT_INLINE,
478 SplitViewDisplayMode::Overlay => PC_OVERLAY,
479 SplitViewDisplayMode::CompactOverlay => PC_COMPACT_OVERLAY,
480 }
481 }
482
483 fn placement_pseudo_class(placement: SplitViewPanePlacement) -> &'static str {
485 match placement {
486 SplitViewPanePlacement::Left => PC_LEFT,
487 SplitViewPanePlacement::Right => PC_RIGHT,
488 SplitViewPanePlacement::Top => PC_TOP,
489 SplitViewPanePlacement::Bottom => PC_BOTTOM,
490 }
491 }
492
493 fn update_visual_state_for_compact_pane_length(&self, new_len: f64) {
494 let display_mode = self.display_mode();
495 let template_settings = self.template_settings();
496 if display_mode == SplitViewDisplayMode::CompactInline {
497 template_settings.set_closed_pane_width(new_len);
498 template_settings.set_closed_pane_height(new_len);
499 } else if display_mode == SplitViewDisplayMode::CompactOverlay {
500 template_settings.set_closed_pane_width(new_len);
501 template_settings.set_pane_column_grid_length(GridLength::new(new_len, GridUnitType::Pixel));
502 template_settings.set_closed_pane_height(new_len);
503 template_settings.set_pane_row_grid_length(GridLength::new(new_len, GridUnitType::Pixel));
504 }
505 }
506
507 fn update_visual_state_for_display_mode(&self, new_value: SplitViewDisplayMode) {
508 if let Some(last) = self.last_display_mode_pseudoclass.get() {
509 self.pseudo_classes().remove_pseudo(last);
510 }
511
512 let pseudo_class = Self::display_mode_pseudo_class(new_value);
513 self.last_display_mode_pseudoclass.set(Some(pseudo_class));
514 self.pseudo_classes().add_pseudo(pseudo_class);
515
516 let template_settings = self.template_settings();
517
518 let (closed_pane_width, pane_column_grid_length) = match new_value {
519 SplitViewDisplayMode::Overlay => (0.0, GridLength::new(0.0, GridUnitType::Pixel)),
520 SplitViewDisplayMode::CompactOverlay => {
521 (self.compact_pane_length(), GridLength::new(self.compact_pane_length(), GridUnitType::Pixel))
522 }
523 SplitViewDisplayMode::Inline => (0.0, GridLength::new(0.0, GridUnitType::Auto)),
524 SplitViewDisplayMode::CompactInline => {
525 (self.compact_pane_length(), GridLength::new(0.0, GridUnitType::Auto))
526 }
527 };
528 template_settings.set_closed_pane_width(closed_pane_width);
529 template_settings.set_pane_column_grid_length(pane_column_grid_length);
530
531 let (closed_pane_height, pane_row_grid_length) = match new_value {
532 SplitViewDisplayMode::Overlay => (0.0, GridLength::new(0.0, GridUnitType::Pixel)),
533 SplitViewDisplayMode::CompactOverlay => {
534 (self.compact_pane_length(), GridLength::new(self.compact_pane_length(), GridUnitType::Pixel))
535 }
536 SplitViewDisplayMode::Inline => (0.0, GridLength::new(0.0, GridUnitType::Auto)),
537 SplitViewDisplayMode::CompactInline => {
538 (self.compact_pane_length(), GridLength::new(0.0, GridUnitType::Auto))
539 }
540 };
541 template_settings.set_closed_pane_height(closed_pane_height);
542 template_settings.set_pane_row_grid_length(pane_row_grid_length);
543
544 self.invalidate_light_dismiss_subscription();
545 }
546
547 fn update_visual_state_for_pane_placement_property(&self, new_value: SplitViewPanePlacement) {
548 if let Some(last) = self.last_placement_pseudoclass.get() {
549 self.pseudo_classes().remove_pseudo(last);
550 }
551
552 let pseudo_class = Self::placement_pseudo_class(new_value);
553 self.last_placement_pseudoclass.set(Some(pseudo_class));
554 self.pseudo_classes().add_pseudo(pseudo_class);
555 }
556
557 fn invalidate_light_dismiss_subscription(&self) {
558 if self.pane.borrow().is_none() {
559 return;
560 }
561
562 if !self.is_in_overlay_mode() {
565 self.dispose_pointer_disposable();
566 return;
567 }
568
569 if self.pointer_disposable.borrow().is_some() {
570 return;
571 }
572
573 if let Some(top_level) = TopLevel::get_top_level(Some(self)) {
574 let weak = self.to_ref().downgrade();
577 let pointer_released = top_level.add_handler(InputElement::pointer_released_event(), {
578 let weak = weak.clone();
579 move |_, e: &PointerReleasedEventArgs| {
580 if let Some(this) = weak.upgrade() {
581 this.pointer_released_outside(e);
582 }
583 }
584 });
585 let back_requested = top_level.back_requested(move |_, e| {
586 if let Some(this) = weak.upgrade() {
587 this.top_level_back_requested(e);
588 }
589 });
590
591 let top_level = top_level.downgrade();
592 let disposable = Disposable::create(move || {
593 if let Some(top_level) = top_level.upgrade() {
594 top_level.remove_handler(InputElement::pointer_released_event(), pointer_released);
595 top_level.remove_handler(TopLevel::back_requested_event(), back_requested);
596 }
597 });
598 *self.pointer_disposable.borrow_mut() = Some(disposable);
599 }
600 }
601
602 fn top_level_back_requested(&self, e: &RoutedEventArgs) {
603 if !self.is_in_overlay_mode() {
604 return;
605 }
606
607 if !self.is_pane_open() {
608 return;
609 }
610
611 self.set_current_value(Self::is_pane_open_property(), false);
612 e.set_handled(true);
613 }
614
615 fn update_pane_state_pseudo_class(&self, is_pane_open: bool) {
616 if is_pane_open {
617 self.pseudo_classes().add_pseudo(PC_OPEN);
618 self.pseudo_classes().remove_pseudo(PC_CLOSED);
619 } else {
620 self.pseudo_classes().add_pseudo(PC_CLOSED);
621 self.pseudo_classes().remove_pseudo(PC_OPEN);
622 }
623 }
624
625 fn coerce_is_pane_open(instance: &FerroObject, value: bool) -> bool {
627 if let Some(split_view) = instance.downcast_ref::<SplitView>() {
628 return split_view.on_coerce_is_pane_open(value);
629 }
630
631 value
632 }
633}