Skip to main content

ferroui_controls/
grid_splitter.rs

1// This source file is adapted from the Windows Presentation Foundation project.
2// (https://github.com/dotnet/wpf/)
3
4use crate::presenters::ContentPresenter;
5use crate::primitives::{AdornerLayer, TemplatedControlImpl, Thumb, ThumbImpl, ThumbImplExt};
6use crate::templates::ITemplateOf;
7use crate::{
8    ColumnDefinition, Control, ControlImpl, Decorator, DefinitionBase, Grid, GridLength, GridUnitType, RowDefinition,
9};
10use ferroui_base::input::navigation::XYFocusHelpers;
11use ferroui_base::input::{
12    Cursor, FocusChangedEventArgs, InputElementImpl, InputElementImplExt, Key, KeyEventArgs, PointerEventArgs,
13    StandardCursorType, VectorEventArgs,
14};
15use ferroui_base::interactivity::InteractiveImpl;
16use ferroui_base::layout::{HorizontalAlignment, LayoutHelper, LayoutableImpl, LayoutableImplExt, VerticalAlignment};
17use ferroui_base::media::{Geometry, ITransform, TranslateTransform};
18use ferroui_base::utilities::MathUtilities;
19use ferroui_base::{
20    ferro_class, ferro_impl_classes, ferro_property, instantiate, FerroObjectImpl, FerroProperty, Ref, Size,
21    StyledElement, StyledElementImpl, StyledProperty, VisualImpl,
22};
23use std::cell::{Cell, RefCell};
24use std::rc::Rc;
25
26/// Indicates whether a [`GridSplitter`] resizes columns or rows.
27#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
28#[repr(i32)]
29pub enum GridResizeDirection {
30    /// Determines whether to resize rows or columns based on its alignment
31    /// and width compared to height.
32    Auto = 0,
33    /// Resize columns when dragging the splitter.
34    Columns = 1,
35    /// Resize rows when dragging the splitter.
36    Rows = 2,
37}
38
39/// Indicates what columns or rows a [`GridSplitter`] resizes.
40#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
41#[repr(i32)]
42pub enum GridResizeBehavior {
43    /// Determine which columns or rows to resize based on its alignment.
44    BasedOnAlignment = 0,
45    /// Resize the current and next columns or rows.
46    CurrentAndNext = 1,
47    /// Resize the previous and current columns or rows.
48    PreviousAndCurrent = 2,
49    /// Resize the previous and next columns or rows.
50    PreviousAndNext = 3,
51}
52
53/// The splitter has special behavior when columns are fixed. If the left
54/// column is fixed, the splitter will only resize that column. Else if the
55/// right column is fixed, the splitter will only resize the right column.
56#[derive(Clone, Copy, Debug, PartialEq, Eq)]
57enum SplitBehavior {
58    /// Both columns/rows are star lengths.
59    Split,
60    /// Resize 1 only.
61    Resize1,
62    /// Resize 2 only.
63    Resize2,
64}
65
66/// Stores data during the resizing operation.
67#[derive(Clone)]
68struct ResizeData {
69    shows_preview: bool,
70    adorner: Option<Ref<PreviewAdorner>>,
71
72    /// The constraints to keep the preview within valid ranges.
73    min_change: f64,
74    max_change: f64,
75
76    /// The grid to resize.
77    grid: Ref<Grid>,
78
79    /// Cache of the resize direction and behavior.
80    resize_direction: GridResizeDirection,
81    resize_behavior: GridResizeBehavior,
82
83    /// The columns/rows to resize.
84    definition1: Option<Ref<DefinitionBase>>,
85    definition2: Option<Ref<DefinitionBase>>,
86
87    /// Are the columns/rows star lengths.
88    split_behavior: SplitBehavior,
89
90    /// The index of the splitter.
91    splitter_index: i32,
92
93    /// The indices of the columns/rows.
94    definition1_index: i32,
95    definition2_index: i32,
96
97    /// The original lengths of the definitions (to restore the lengths if
98    /// the user cancels the resize).
99    original_definition1_length: GridLength,
100    original_definition2_length: GridLength,
101    original_definition1_actual_length: f64,
102    original_definition2_actual_length: f64,
103
104    /// The minimum of the width/height of the splitter. Used to ensure the
105    /// splitter is not hidden by resizing a row/column smaller than the
106    /// splitter.
107    splitter_length: f64,
108
109    /// The current layout scaling factor.
110    scaling: f64,
111}
112
113ferroui_base::ferro_thread_local! {
114    static COLUMN_SPLITTER_CURSOR: RefCell<Option<Rc<Cursor>>> = const { RefCell::new(None) };
115    static ROW_SPLITTER_CURSOR: RefCell<Option<Rc<Cursor>>> = const { RefCell::new(None) };
116}
117
118/// Represents the control that redistributes space between columns or rows
119/// of a [`Grid`] control.
120#[repr(C)]
121pub struct GridSplitter {
122    base: Thumb,
123    resize_data: RefCell<Option<ResizeData>>,
124    is_focus_engaged: Cell<bool>,
125}
126
127ferro_class! {
128    GridSplitter: Thumb, virtuals GridSplitterImpl: ThumbImpl {
129        /// Retrieves the [`Grid`] that ultimately hosts this splitter in the
130        /// visual/logical tree.
131        ///
132        /// A splitter can be placed directly inside a grid or indirectly
133        /// inside an items control that uses a grid as its items panel. In
134        /// the latter case the first logical parent is usually a content
135        /// presenter (or the items control itself), so the method walks
136        /// these intermediate containers to locate the underlying grid.
137        ///
138        /// Returns the containing grid if one is found.
139        fn get_parent_grid(this) -> Option<Ref<Grid>>;
140        /// Returns the element that carries the grid-attached properties
141        /// (`Grid.Row`, `Grid.Column`, etc.) relevant to this splitter.
142        ///
143        /// When the splitter is generated as part of an items control
144        /// template, the attached properties are set on the surrounding
145        /// content presenter rather than on the splitter itself. This helper
146        /// selects that presenter when appropriate so subsequent property
147        /// look-ups read the correct values; otherwise it simply returns
148        /// the splitter.
149        fn get_properties_value_source(this) -> Ref<StyledElement>;
150    }
151}
152ferroui_base::ferro_class_info!(GridSplitter { new: GridSplitter::new });
153
154ferro_impl_classes!(
155    GridSplitter: FerroObjectImpl,
156    StyledElementImpl,
157    VisualImpl,
158    LayoutableImpl,
159    InteractiveImpl,
160    ControlImpl,
161    TemplatedControlImpl
162);
163
164impl InputElementImpl for GridSplitter {
165    fn on_pointer_entered(this: &Self, e: &PointerEventArgs) {
166        Self::parent_on_pointer_entered(this, e);
167
168        let direction = this.get_effective_resize_direction();
169
170        match direction {
171            GridResizeDirection::Columns => {
172                this.set_cursor(Some(Self::splitter_cursor(&COLUMN_SPLITTER_CURSOR, StandardCursorType::SizeWestEast)));
173            }
174            GridResizeDirection::Rows => {
175                this.set_cursor(Some(Self::splitter_cursor(&ROW_SPLITTER_CURSOR, StandardCursorType::SizeNorthSouth)));
176            }
177            GridResizeDirection::Auto => {}
178        }
179    }
180
181    fn on_lost_focus(this: &Self, e: &FocusChangedEventArgs) {
182        Self::parent_on_lost_focus(this, e);
183
184        if this.has_resize_data() {
185            this.cancel_resize();
186        }
187    }
188
189    fn on_key_down(this: &Self, e: &KeyEventArgs) {
190        let using_xy_navigation = XYFocusHelpers::is_allowed_xy_navigation_mode(this, Some(e.key_device_type));
191        let allow_arrow_keys = this.is_focus_engaged.get() || !using_xy_navigation;
192
193        match e.key {
194            Key::Enter if using_xy_navigation => {
195                this.is_focus_engaged.set(!this.is_focus_engaged.get());
196                e.set_handled(true);
197            }
198            Key::Escape => {
199                this.is_focus_engaged.set(false);
200                if this.has_resize_data() {
201                    this.cancel_resize();
202                    e.set_handled(true);
203                }
204            }
205            Key::Left if allow_arrow_keys => {
206                e.set_handled(this.keyboard_move_splitter(-this.keyboard_increment(), 0.0));
207            }
208            Key::Right if allow_arrow_keys => {
209                e.set_handled(this.keyboard_move_splitter(this.keyboard_increment(), 0.0));
210            }
211            Key::Up if allow_arrow_keys => {
212                e.set_handled(this.keyboard_move_splitter(0.0, -this.keyboard_increment()));
213            }
214            Key::Down if allow_arrow_keys => {
215                e.set_handled(this.keyboard_move_splitter(0.0, this.keyboard_increment()));
216            }
217            _ => {}
218        }
219    }
220}
221
222impl ThumbImpl for GridSplitter {
223    fn on_drag_started(this: &Self, e: &VectorEventArgs) {
224        Self::parent_on_drag_started(this, e);
225
226        // The thumb sometimes raises multiple drag started events.
227        if this.has_resize_data() {
228            return;
229        }
230
231        this.initialize_data(this.shows_preview());
232    }
233
234    fn on_drag_delta(this: &Self, e: &VectorEventArgs) {
235        Self::parent_on_drag_delta(this, e);
236
237        let Some(resize_data) = this.resize_data.borrow().clone() else {
238            return;
239        };
240
241        let mut horizontal_change = e.vector.x;
242        let mut vertical_change = e.vector.y;
243
244        // Round change to nearest multiple of DragIncrement.
245        let drag_increment = this.drag_increment();
246        horizontal_change = (horizontal_change / drag_increment).round_ties_even() * drag_increment;
247        vertical_change = (vertical_change / drag_increment).round_ties_even() * drag_increment;
248
249        if resize_data.shows_preview {
250            // Set the translation of the adorner to the distance from the
251            // thumb. There is no adorner when the grid has no adorner layer.
252            if let Some(adorner) = &resize_data.adorner {
253                if resize_data.resize_direction == GridResizeDirection::Columns {
254                    adorner.set_offset_x(f64::min(
255                        f64::max(horizontal_change, resize_data.min_change),
256                        resize_data.max_change,
257                    ));
258                } else {
259                    adorner.set_offset_y(f64::min(
260                        f64::max(vertical_change, resize_data.min_change),
261                        resize_data.max_change,
262                    ));
263                }
264            }
265        } else {
266            // Directly update the grid.
267            this.move_splitter(horizontal_change, vertical_change);
268        }
269    }
270
271    fn on_drag_completed(this: &Self, e: &VectorEventArgs) {
272        Self::parent_on_drag_completed(this, e);
273
274        let Some(resize_data) = this.resize_data.borrow().clone() else {
275            return;
276        };
277
278        if resize_data.shows_preview {
279            // Update the grid. There is no adorner when the grid has no
280            // adorner layer.
281            if let Some(adorner) = &resize_data.adorner {
282                this.move_splitter(adorner.offset_x(), adorner.offset_y());
283                this.remove_preview_adorner();
284            }
285        }
286
287        *this.resize_data.borrow_mut() = None;
288    }
289}
290
291impl GridSplitterImpl for GridSplitter {
292    fn get_parent_grid(this: &Self) -> Option<Ref<Grid>> {
293        // When the splitter is used inside an items control with a grid as
294        // its items panel, its immediate parent is usually the items control
295        // or a content presenter.
296        let parent = this.parent()?;
297        if let Some(grid) = parent.clone().cast::<Grid>() {
298            return Some(grid);
299        }
300
301        let items_control = match parent.clone().cast::<crate::ItemsControl>() {
302            Some(items_control) => items_control,
303            None => {
304                parent.cast::<ContentPresenter>()?;
305                this.parent()?.parent()?.cast::<crate::ItemsControl>()?
306            }
307        };
308        items_control.items_panel_root()?.cast::<Grid>()
309    }
310
311    fn get_properties_value_source(this: &Self) -> Ref<StyledElement> {
312        match this.parent() {
313            Some(parent) if parent.is::<ContentPresenter>() => parent,
314            _ => this.to_ref().upcast(),
315        }
316    }
317}
318
319ferroui_base::ferro_properties! { impl GridSplitter {
320    ferro_property!(
321        /// Defines the `ResizeDirection` property.
322        pub fn resize_direction_property() -> StyledProperty<GridResizeDirection> {
323            FerroProperty::register::<GridSplitter, _>("ResizeDirection", GridResizeDirection::Auto)
324        }
325    );
326
327    ferro_property!(
328        /// Defines the `ResizeBehavior` property.
329        pub fn resize_behavior_property() -> StyledProperty<GridResizeBehavior> {
330            FerroProperty::register::<GridSplitter, _>("ResizeBehavior", GridResizeBehavior::BasedOnAlignment)
331        }
332    );
333
334    ferro_property!(
335        /// Defines the `ShowsPreview` property.
336        pub fn shows_preview_property() -> StyledProperty<bool> {
337            FerroProperty::register::<GridSplitter, _>("ShowsPreview", false)
338        }
339    );
340
341    ferro_property!(
342        /// Defines the `KeyboardIncrement` property.
343        pub fn keyboard_increment_property() -> StyledProperty<f64> {
344            FerroProperty::register::<GridSplitter, _>("KeyboardIncrement", 10.0)
345        }
346    );
347
348    ferro_property!(
349        /// Defines the `DragIncrement` property.
350        pub fn drag_increment_property() -> StyledProperty<f64> {
351            FerroProperty::register::<GridSplitter, _>("DragIncrement", 1.0)
352        }
353    );
354
355    ferro_property!(
356        /// Defines the `PreviewContent` property.
357        pub fn preview_content_property() -> StyledProperty<Option<Rc<dyn ITemplateOf<Ref<Control>>>>> {
358            FerroProperty::register::<GridSplitter, _>("PreviewContent", None)
359        }
360    );
361} }
362
363impl GridSplitter {
364    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
365    pub fn construct() -> Self {
366        Self { base: Thumb::construct(), resize_data: RefCell::new(None), is_focus_engaged: Cell::new(false) }
367    }
368
369    pub fn new() -> Ref<Self> {
370        instantiate(Self::construct())
371    }
372
373    /// Indicates whether the splitter resizes the columns, rows, or both.
374    pub fn resize_direction(&self) -> GridResizeDirection {
375        self.get_value(Self::resize_direction_property())
376    }
377
378    pub fn set_resize_direction(&self, value: GridResizeDirection) {
379        self.set_value(Self::resize_direction_property(), value)
380    }
381
382    /// Indicates which columns or rows the splitter resizes.
383    pub fn resize_behavior(&self) -> GridResizeBehavior {
384        self.get_value(Self::resize_behavior_property())
385    }
386
387    pub fn set_resize_behavior(&self, value: GridResizeBehavior) {
388        self.set_value(Self::resize_behavior_property(), value)
389    }
390
391    /// Indicates whether to preview the column resizing without updating
392    /// layout.
393    pub fn shows_preview(&self) -> bool {
394        self.get_value(Self::shows_preview_property())
395    }
396
397    pub fn set_shows_preview(&self, value: bool) {
398        self.set_value(Self::shows_preview_property(), value)
399    }
400
401    /// The distance to move the splitter when pressing the keyboard arrow
402    /// keys.
403    pub fn keyboard_increment(&self) -> f64 {
404        self.get_value(Self::keyboard_increment_property())
405    }
406
407    pub fn set_keyboard_increment(&self, value: f64) {
408        self.set_value(Self::keyboard_increment_property(), value)
409    }
410
411    /// Restricts the splitter to move a multiple of the specified units.
412    pub fn drag_increment(&self) -> f64 {
413        self.get_value(Self::drag_increment_property())
414    }
415
416    pub fn set_drag_increment(&self, value: f64) {
417        self.set_value(Self::drag_increment_property(), value)
418    }
419
420    /// The content that will be shown when `ShowsPreview` is enabled and the
421    /// user starts a resize operation.
422    pub fn preview_content(&self) -> Option<Rc<dyn ITemplateOf<Ref<Control>>>> {
423        self.get_value(Self::preview_content_property())
424    }
425
426    pub fn set_preview_content(&self, value: Option<Rc<dyn ITemplateOf<Ref<Control>>>>) {
427        self.set_value(Self::preview_content_property(), value)
428    }
429
430    fn splitter_cursor(
431        slot: &'static ferroui_base::ferro_thread_local::LocalKey<RefCell<Option<Rc<Cursor>>>>,
432        cursor_type: StandardCursorType,
433    ) -> Rc<Cursor> {
434        if let Some(cursor) = slot.with(|slot| slot.borrow().clone()) {
435            return cursor;
436        }
437        let cursor = Cursor::new(cursor_type);
438        slot.with(|slot| *slot.borrow_mut() = Some(cursor.clone()));
439        cursor
440    }
441
442    fn remove_preview_adorner(&self) {
443        let adorner = self.resize_data.borrow().as_ref().and_then(|resize_data| resize_data.adorner.clone());
444        if let Some(adorner) = adorner {
445            if let Some(layer) = AdornerLayer::get_adorner_layer(self) {
446                layer.children().remove(&adorner.upcast::<Control>());
447            }
448        }
449    }
450
451    fn has_resize_data(&self) -> bool {
452        self.resize_data.borrow().is_some()
453    }
454
455    /// Converts the `Auto` direction to rows or columns depending on the
456    /// alignment and the width/height of the splitter.
457    pub(crate) fn get_effective_resize_direction(&self) -> GridResizeDirection {
458        let mut direction = self.resize_direction();
459
460        if direction != GridResizeDirection::Auto {
461            return direction;
462        }
463
464        // When HorizontalAlignment is Left, Right or Center, resize Columns.
465        if self.horizontal_alignment() != HorizontalAlignment::Stretch {
466            direction = GridResizeDirection::Columns;
467        } else if self.vertical_alignment() != VerticalAlignment::Stretch {
468            direction = GridResizeDirection::Rows;
469        } else if self.bounds().width <= self.bounds().height {
470            // Fall back to Width vs Height.
471            direction = GridResizeDirection::Columns;
472        } else {
473            direction = GridResizeDirection::Rows;
474        }
475
476        direction
477    }
478
479    /// Converts `BasedOnAlignment` to next/previous/both depending on the
480    /// alignment and the direction.
481    fn get_effective_resize_behavior(&self, direction: GridResizeDirection) -> GridResizeBehavior {
482        let mut resize_behavior = self.resize_behavior();
483
484        if resize_behavior == GridResizeBehavior::BasedOnAlignment {
485            if direction == GridResizeDirection::Columns {
486                resize_behavior = match self.horizontal_alignment() {
487                    HorizontalAlignment::Left => GridResizeBehavior::PreviousAndCurrent,
488                    HorizontalAlignment::Right => GridResizeBehavior::CurrentAndNext,
489                    _ => GridResizeBehavior::PreviousAndNext,
490                };
491            } else {
492                resize_behavior = match self.vertical_alignment() {
493                    VerticalAlignment::Top => GridResizeBehavior::PreviousAndCurrent,
494                    VerticalAlignment::Bottom => GridResizeBehavior::CurrentAndNext,
495                    _ => GridResizeBehavior::PreviousAndNext,
496                };
497            }
498        }
499
500        resize_behavior
501    }
502
503    /// Initializes the data needed for resizing.
504    fn initialize_data(&self, shows_preview: bool) {
505        // If not in a grid or can't resize, do nothing.
506        let Some(grid) = self.get_parent_grid() else {
507            return;
508        };
509
510        let resize_direction = self.get_effective_resize_direction();
511
512        // Setup data used for resizing; store the rows and columns to
513        // resize on drag events. When unable to resize, no data is kept.
514        let bounds = self.bounds();
515        let resize_data = self.setup_definitions_to_resize(ResizeData {
516            grid,
517            shows_preview,
518            adorner: None,
519            min_change: 0.0,
520            max_change: 0.0,
521            resize_direction,
522            splitter_length: f64::min(bounds.width, bounds.height),
523            resize_behavior: self.get_effective_resize_behavior(resize_direction),
524            scaling: self.get_layout_root().map_or(1.0, |root| root.layout_scaling()),
525            definition1: None,
526            definition2: None,
527            split_behavior: SplitBehavior::Split,
528            splitter_index: 0,
529            definition1_index: 0,
530            definition2_index: 0,
531            original_definition1_length: GridLength::default(),
532            original_definition2_length: GridLength::default(),
533            original_definition1_actual_length: 0.0,
534            original_definition2_actual_length: 0.0,
535        });
536
537        // Unable to resize: no data is kept.
538        let can_resize = resize_data.is_some();
539        *self.resize_data.borrow_mut() = resize_data;
540
541        if can_resize {
542            // Setup the preview in the adorner if ShowsPreview is true.
543            self.setup_preview_adorner();
544        }
545    }
546
547    /// Creates the preview adorner and adds it to the adorner layer.
548    fn setup_preview_adorner(&self) {
549        let Some(resize_data) = self.resize_data.borrow().clone() else {
550            return;
551        };
552
553        if resize_data.shows_preview {
554            // Get the adorner layer and add an adorner to it.
555            let adorner_layer = AdornerLayer::get_adorner_layer(&resize_data.grid);
556
557            let preview_content = self.preview_content();
558
559            // Can't display the preview.
560            let Some(adorner_layer) = adorner_layer else {
561                return;
562            };
563
564            let built_preview_content = preview_content.map(|preview_content| preview_content.build_typed());
565
566            let adorner = PreviewAdorner::new(built_preview_content);
567
568            AdornerLayer::set_adorned_element(&adorner, self.to_ref().upcast::<ferroui_base::Visual>());
569            AdornerLayer::set_is_clip_enabled(&adorner, false);
570
571            adorner_layer.children().add(adorner.clone().upcast::<Control>());
572
573            // Get constraints on the translation of the preview.
574            let (min_change, max_change) = Self::get_delta_constraints(&resize_data);
575
576            if let Some(resize_data) = self.resize_data.borrow_mut().as_mut() {
577                resize_data.adorner = Some(adorner);
578                resize_data.min_change = min_change;
579                resize_data.max_change = max_change;
580            }
581        }
582    }
583
584    /// Completes the resize data with the definitions to resize. Returns
585    /// `None` if the splitter cannot resize rows/columns.
586    fn setup_definitions_to_resize(&self, mut resize_data: ResizeData) -> Option<ResizeData> {
587        // Get the property values from the content presenter if the grid is
588        // used in an items control as the items panel, otherwise directly
589        // from the splitter.
590        let source_control = self.get_properties_value_source();
591        let is_columns = resize_data.resize_direction == GridResizeDirection::Columns;
592        let grid_span = source_control.get_value(if is_columns {
593            Grid::column_span_property()
594        } else {
595            Grid::row_span_property()
596        });
597
598        if grid_span == 1 {
599            let splitter_index =
600                source_control.get_value(if is_columns { Grid::column_property() } else { Grid::row_property() });
601
602            // Select the columns based on behavior.
603            let (index1, index2) = match resize_data.resize_behavior {
604                // Get current and previous.
605                GridResizeBehavior::PreviousAndCurrent => (splitter_index - 1, splitter_index),
606                // Get current and next.
607                GridResizeBehavior::CurrentAndNext => (splitter_index, splitter_index + 1),
608                // GridResizeBehavior::PreviousAndNext: get previous and next.
609                _ => (splitter_index - 1, splitter_index + 1),
610            };
611
612            // Get count of rows/columns in the resize direction.
613            let count = if is_columns {
614                resize_data.grid.column_definitions().count()
615            } else {
616                resize_data.grid.row_definitions().count()
617            } as i32;
618
619            if index1 >= 0 && index2 < count {
620                resize_data.splitter_index = splitter_index;
621
622                let definition1 = Self::get_grid_definition(&resize_data.grid, index1, resize_data.resize_direction);
623                resize_data.definition1_index = index1;
624                // Save the size in case the user cancels.
625                resize_data.original_definition1_length = definition1.user_size_value_cache();
626                resize_data.original_definition1_actual_length = Self::get_actual_length(&definition1);
627
628                let definition2 = Self::get_grid_definition(&resize_data.grid, index2, resize_data.resize_direction);
629                resize_data.definition2_index = index2;
630                // Save the size in case the user cancels.
631                resize_data.original_definition2_length = definition2.user_size_value_cache();
632                resize_data.original_definition2_actual_length = Self::get_actual_length(&definition2);
633
634                // Determine how to resize the columns.
635                let is_star1 = Self::is_star(&definition1);
636                let is_star2 = Self::is_star(&definition2);
637
638                resize_data.split_behavior = if is_star1 && is_star2 {
639                    // If they are both stars, resize both.
640                    SplitBehavior::Split
641                } else if !is_star1 {
642                    // One column is fixed width, resize the first one that
643                    // is fixed.
644                    SplitBehavior::Resize1
645                } else {
646                    SplitBehavior::Resize2
647                };
648
649                resize_data.definition1 = Some(definition1);
650                resize_data.definition2 = Some(definition2);
651
652                return Some(resize_data);
653            }
654        }
655
656        None
657    }
658
659    /// Cancels the resize operation.
660    fn cancel_resize(&self) {
661        let Some(resize_data) = self.resize_data.borrow().clone() else {
662            return;
663        };
664
665        // Restore the original column/row lengths.
666        if resize_data.shows_preview {
667            self.remove_preview_adorner();
668        } else {
669            // Reset the columns/rows lengths to the saved values.
670            if let Some(definition1) = &resize_data.definition1 {
671                Self::set_definition_length(definition1, resize_data.original_definition1_length);
672            }
673            if let Some(definition2) = &resize_data.definition2 {
674                Self::set_definition_length(definition2, resize_data.original_definition2_length);
675            }
676        }
677
678        *self.resize_data.borrow_mut() = None;
679    }
680
681    /// Returns true if the row/column has a star length.
682    fn is_star(definition: &DefinitionBase) -> bool {
683        definition.user_size_value_cache().is_star()
684    }
685
686    /// Gets the column or row definition at an index from the grid based on
687    /// the resize direction.
688    fn get_grid_definition(grid: &Grid, index: i32, direction: GridResizeDirection) -> Ref<DefinitionBase> {
689        if direction == GridResizeDirection::Columns {
690            grid.column_definitions().get(index as usize).upcast()
691        } else {
692            grid.row_definitions().get(index as usize).upcast()
693        }
694    }
695
696    /// Retrieves the actual width or actual height of the definition
697    /// depending on whether it is a column or a row.
698    fn get_actual_length(definition: &Ref<DefinitionBase>) -> f64 {
699        if let Some(column) = definition.cast::<ColumnDefinition>() {
700            return column.actual_width();
701        }
702
703        definition.cast::<RowDefinition>().map_or(0.0, |row| row.actual_height())
704    }
705
706    /// Sets the length of a column or row definition.
707    fn set_definition_length(definition: &Ref<DefinitionBase>, length: GridLength) {
708        definition.set_value(
709            if definition.is::<ColumnDefinition>() {
710                ColumnDefinition::width_property()
711            } else {
712                RowDefinition::height_property()
713            },
714            length,
715        );
716    }
717
718    /// Gets the minimum and maximum delta given the definition constraints
719    /// (MinWidth/MaxWidth).
720    fn get_delta_constraints(resize_data: &ResizeData) -> (f64, f64) {
721        let (Some(definition1), Some(definition2)) = (&resize_data.definition1, &resize_data.definition2) else {
722            return (0.0, 0.0);
723        };
724
725        let definition1_len = Self::get_actual_length(definition1);
726        let mut definition1_min = definition1.user_min_size_value_cache();
727        let definition1_max = definition1.user_max_size_value_cache();
728
729        let definition2_len = Self::get_actual_length(definition2);
730        let mut definition2_min = definition2.user_min_size_value_cache();
731        let definition2_max = definition2.user_max_size_value_cache();
732
733        // Set MinWidths to be greater than width of splitter.
734        if resize_data.splitter_index == resize_data.definition1_index {
735            definition1_min = f64::max(definition1_min, resize_data.splitter_length);
736        } else if resize_data.splitter_index == resize_data.definition2_index {
737            definition2_min = f64::max(definition2_min, resize_data.splitter_length);
738        }
739
740        // Determine the minimum and maximum the columns can be resized.
741        let min_delta = -f64::min(definition1_len - definition1_min, definition2_max - definition2_len);
742        let max_delta = f64::min(definition1_max - definition1_len, definition2_len - definition2_min);
743
744        (min_delta, max_delta)
745    }
746
747    /// Sets the length of definition1 and definition2.
748    fn set_lengths(resize_data: &ResizeData, definition1_pixels: f64, definition2_pixels: f64) {
749        // For the case where both definition1 and 2 are stars, update all
750        // star values to match their current pixel values.
751        if resize_data.split_behavior == SplitBehavior::Split {
752            let definitions: Vec<Ref<DefinitionBase>> = if resize_data.resize_direction == GridResizeDirection::Columns
753            {
754                resize_data.grid.column_definitions().snapshot().iter().map(|d| d.clone().upcast()).collect()
755            } else {
756                resize_data.grid.row_definitions().snapshot().iter().map(|d| d.clone().upcast()).collect()
757            };
758
759            for (i, definition) in definitions.iter().enumerate() {
760                let i = i as i32;
761
762                // For each definition, if it is a star, set its value to
763                // the actual length in stars. This makes 1 star == 1 pixel
764                // in length.
765                if i == resize_data.definition1_index {
766                    Self::set_definition_length(definition, GridLength::new(definition1_pixels, GridUnitType::Star));
767                } else if i == resize_data.definition2_index {
768                    Self::set_definition_length(definition, GridLength::new(definition2_pixels, GridUnitType::Star));
769                } else if Self::is_star(definition) {
770                    Self::set_definition_length(
771                        definition,
772                        GridLength::new(Self::get_actual_length(definition), GridUnitType::Star),
773                    );
774                }
775            }
776        } else if resize_data.split_behavior == SplitBehavior::Resize1 {
777            if let Some(definition1) = &resize_data.definition1 {
778                Self::set_definition_length(definition1, GridLength::from_pixels(definition1_pixels));
779            }
780        } else if let Some(definition2) = &resize_data.definition2 {
781            Self::set_definition_length(definition2, GridLength::from_pixels(definition2_pixels));
782        }
783    }
784
785    /// Moves the splitter by the given deltas in the horizontal and vertical
786    /// directions.
787    fn move_splitter(&self, horizontal_change: f64, vertical_change: f64) {
788        let Some(resize_data) = self.resize_data.borrow().clone() else {
789            return;
790        };
791
792        // Calculate the offset to adjust the splitter. If layout rounding is
793        // enabled, we need to round to an integer physical pixel value to
794        // avoid round-ups of children that expand the bounds of the grid. In
795        // practice this only happens in high dpi because horizontal/vertical
796        // offsets here are never fractional (they correspond to mouse
797        // movement across logical pixels). Rounding error only creeps in
798        // when converting to a physical display with something other than
799        // the logical 96 dpi.
800        let mut delta = if resize_data.resize_direction == GridResizeDirection::Columns {
801            horizontal_change
802        } else {
803            vertical_change
804        };
805
806        if self.use_layout_rounding() {
807            delta = LayoutHelper::round_layout_value(delta, LayoutHelper::get_layout_scale(self));
808        }
809
810        if let (Some(definition1), Some(definition2)) = (&resize_data.definition1, &resize_data.definition2) {
811            let actual_length1 = Self::get_actual_length(definition1);
812            let actual_length2 = Self::get_actual_length(definition2);
813            let pixel_length = 1.0 / resize_data.scaling;
814            let epsilon = pixel_length + LayoutHelper::LAYOUT_EPSILON;
815
816            // When splitting, check to see if the total pixels spanned by
817            // the definitions is the same as before starting the resize. If
818            // not, cancel the drag. We need to account for layout rounding
819            // here, so ignore differences of less than a device pixel.
820            if resize_data.split_behavior == SplitBehavior::Split
821                && !MathUtilities::are_close_eps(
822                    actual_length1 + actual_length2,
823                    resize_data.original_definition1_actual_length + resize_data.original_definition2_actual_length,
824                    epsilon,
825                )
826            {
827                self.cancel_resize();
828
829                return;
830            }
831
832            let (min, max) = Self::get_delta_constraints(&resize_data);
833
834            // Constrain the delta to the Min/MaxWidth of the columns.
835            delta = f64::min(f64::max(delta, min), max);
836
837            let definition1_length_new = actual_length1 + delta;
838            let definition2_length_new = actual_length1 + actual_length2 - definition1_length_new;
839
840            Self::set_lengths(&resize_data, definition1_length_new, definition2_length_new);
841        }
842    }
843
844    /// Moves the splitter using the keyboard (does not show the preview).
845    fn keyboard_move_splitter(&self, horizontal_change: f64, vertical_change: f64) -> bool {
846        // If moving with the mouse, ignore keyboard motion.
847        if self.has_resize_data() {
848            return false; // Don't handle the event.
849        }
850
851        // Don't show preview.
852        self.initialize_data(false);
853
854        // Check that we are actually able to resize.
855        if !self.has_resize_data() {
856            return false; // Don't handle the event.
857        }
858
859        self.move_splitter(horizontal_change, vertical_change);
860
861        *self.resize_data.borrow_mut() = None;
862
863        true
864    }
865}
866
867/// The adorner that previews the position of the splitter while it is
868/// dragged with `ShowsPreview` enabled.
869#[repr(C)]
870pub(crate) struct PreviewAdorner {
871    base: Decorator,
872    translation: Ref<TranslateTransform>,
873}
874
875ferro_class!(PreviewAdorner: Decorator);
876ferro_impl_classes!(
877    PreviewAdorner: FerroObjectImpl,
878    StyledElementImpl,
879    VisualImpl,
880    InteractiveImpl,
881    InputElementImpl,
882    ControlImpl
883);
884
885impl LayoutableImpl for PreviewAdorner {
886    fn arrange_override(this: &Self, final_size: Size) -> Size {
887        // Adorners always get clipped to the owner control. In this case we
888        // want to constrain the size to the splitter size but draw on top
889        // of the parent grid.
890        this.set_clip(None::<Ref<Geometry>>);
891
892        Self::parent_arrange_override(this, final_size)
893    }
894}
895
896impl PreviewAdorner {
897    fn new(preview_control: Option<Ref<Control>>) -> Ref<Self> {
898        // Add a decorator to perform translations.
899        let translation = TranslateTransform::new();
900        let this = instantiate(Self { base: Decorator::construct(), translation: translation.clone() });
901
902        let decorator = Decorator::new();
903        decorator.set_child(preview_control);
904        let transform: Rc<dyn ITransform> = translation.into();
905        decorator.set_render_transform(Some(transform));
906
907        this.set_child(decorator);
908        this
909    }
910
911    /// The offset of the preview in the X direction from the splitter.
912    fn offset_x(&self) -> f64 {
913        self.translation.x()
914    }
915
916    fn set_offset_x(&self, value: f64) {
917        self.translation.set_x(value)
918    }
919
920    /// The offset of the preview in the Y direction from the splitter.
921    fn offset_y(&self) -> f64 {
922        self.translation.y()
923    }
924
925    fn set_offset_y(&self, value: f64) {
926        self.translation.set_y(value)
927    }
928}