use crate::presenters::ContentPresenter;
use crate::primitives::{AdornerLayer, TemplatedControlImpl, Thumb, ThumbImpl, ThumbImplExt};
use crate::templates::ITemplateOf;
use crate::{
ColumnDefinition, Control, ControlImpl, Decorator, DefinitionBase, Grid, GridLength, GridUnitType, RowDefinition,
};
use ferroui_base::input::navigation::XYFocusHelpers;
use ferroui_base::input::{
Cursor, FocusChangedEventArgs, InputElementImpl, InputElementImplExt, Key, KeyEventArgs, PointerEventArgs,
StandardCursorType, VectorEventArgs,
};
use ferroui_base::interactivity::InteractiveImpl;
use ferroui_base::layout::{HorizontalAlignment, LayoutHelper, LayoutableImpl, LayoutableImplExt, VerticalAlignment};
use ferroui_base::media::{Geometry, ITransform, TranslateTransform};
use ferroui_base::utilities::MathUtilities;
use ferroui_base::{
ferro_class, ferro_impl_classes, ferro_property, instantiate, FerroObjectImpl, FerroProperty, Ref, Size,
StyledElement, StyledElementImpl, StyledProperty, VisualImpl,
};
use std::cell::{Cell, RefCell};
use std::rc::Rc;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[repr(i32)]
pub enum GridResizeDirection {
Auto = 0,
Columns = 1,
Rows = 2,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[repr(i32)]
pub enum GridResizeBehavior {
BasedOnAlignment = 0,
CurrentAndNext = 1,
PreviousAndCurrent = 2,
PreviousAndNext = 3,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum SplitBehavior {
Split,
Resize1,
Resize2,
}
#[derive(Clone)]
struct ResizeData {
shows_preview: bool,
adorner: Option<Ref<PreviewAdorner>>,
min_change: f64,
max_change: f64,
grid: Ref<Grid>,
resize_direction: GridResizeDirection,
resize_behavior: GridResizeBehavior,
definition1: Option<Ref<DefinitionBase>>,
definition2: Option<Ref<DefinitionBase>>,
split_behavior: SplitBehavior,
splitter_index: i32,
definition1_index: i32,
definition2_index: i32,
original_definition1_length: GridLength,
original_definition2_length: GridLength,
original_definition1_actual_length: f64,
original_definition2_actual_length: f64,
splitter_length: f64,
scaling: f64,
}
ferroui_base::ferro_thread_local! {
static COLUMN_SPLITTER_CURSOR: RefCell<Option<Rc<Cursor>>> = const { RefCell::new(None) };
static ROW_SPLITTER_CURSOR: RefCell<Option<Rc<Cursor>>> = const { RefCell::new(None) };
}
#[repr(C)]
pub struct GridSplitter {
base: Thumb,
resize_data: RefCell<Option<ResizeData>>,
is_focus_engaged: Cell<bool>,
}
ferro_class! {
GridSplitter: Thumb, virtuals GridSplitterImpl: ThumbImpl {
fn get_parent_grid(this) -> Option<Ref<Grid>>;
fn get_properties_value_source(this) -> Ref<StyledElement>;
}
}
ferroui_base::ferro_class_info!(GridSplitter { new: GridSplitter::new });
ferro_impl_classes!(
GridSplitter: FerroObjectImpl,
StyledElementImpl,
VisualImpl,
LayoutableImpl,
InteractiveImpl,
ControlImpl,
TemplatedControlImpl
);
impl InputElementImpl for GridSplitter {
fn on_pointer_entered(this: &Self, e: &PointerEventArgs) {
Self::parent_on_pointer_entered(this, e);
let direction = this.get_effective_resize_direction();
match direction {
GridResizeDirection::Columns => {
this.set_cursor(Some(Self::splitter_cursor(&COLUMN_SPLITTER_CURSOR, StandardCursorType::SizeWestEast)));
}
GridResizeDirection::Rows => {
this.set_cursor(Some(Self::splitter_cursor(&ROW_SPLITTER_CURSOR, StandardCursorType::SizeNorthSouth)));
}
GridResizeDirection::Auto => {}
}
}
fn on_lost_focus(this: &Self, e: &FocusChangedEventArgs) {
Self::parent_on_lost_focus(this, e);
if this.has_resize_data() {
this.cancel_resize();
}
}
fn on_key_down(this: &Self, e: &KeyEventArgs) {
let using_xy_navigation = XYFocusHelpers::is_allowed_xy_navigation_mode(this, Some(e.key_device_type));
let allow_arrow_keys = this.is_focus_engaged.get() || !using_xy_navigation;
match e.key {
Key::Enter if using_xy_navigation => {
this.is_focus_engaged.set(!this.is_focus_engaged.get());
e.set_handled(true);
}
Key::Escape => {
this.is_focus_engaged.set(false);
if this.has_resize_data() {
this.cancel_resize();
e.set_handled(true);
}
}
Key::Left if allow_arrow_keys => {
e.set_handled(this.keyboard_move_splitter(-this.keyboard_increment(), 0.0));
}
Key::Right if allow_arrow_keys => {
e.set_handled(this.keyboard_move_splitter(this.keyboard_increment(), 0.0));
}
Key::Up if allow_arrow_keys => {
e.set_handled(this.keyboard_move_splitter(0.0, -this.keyboard_increment()));
}
Key::Down if allow_arrow_keys => {
e.set_handled(this.keyboard_move_splitter(0.0, this.keyboard_increment()));
}
_ => {}
}
}
}
impl ThumbImpl for GridSplitter {
fn on_drag_started(this: &Self, e: &VectorEventArgs) {
Self::parent_on_drag_started(this, e);
if this.has_resize_data() {
return;
}
this.initialize_data(this.shows_preview());
}
fn on_drag_delta(this: &Self, e: &VectorEventArgs) {
Self::parent_on_drag_delta(this, e);
let Some(resize_data) = this.resize_data.borrow().clone() else {
return;
};
let mut horizontal_change = e.vector.x;
let mut vertical_change = e.vector.y;
let drag_increment = this.drag_increment();
horizontal_change = (horizontal_change / drag_increment).round_ties_even() * drag_increment;
vertical_change = (vertical_change / drag_increment).round_ties_even() * drag_increment;
if resize_data.shows_preview {
if let Some(adorner) = &resize_data.adorner {
if resize_data.resize_direction == GridResizeDirection::Columns {
adorner.set_offset_x(f64::min(
f64::max(horizontal_change, resize_data.min_change),
resize_data.max_change,
));
} else {
adorner.set_offset_y(f64::min(
f64::max(vertical_change, resize_data.min_change),
resize_data.max_change,
));
}
}
} else {
this.move_splitter(horizontal_change, vertical_change);
}
}
fn on_drag_completed(this: &Self, e: &VectorEventArgs) {
Self::parent_on_drag_completed(this, e);
let Some(resize_data) = this.resize_data.borrow().clone() else {
return;
};
if resize_data.shows_preview {
if let Some(adorner) = &resize_data.adorner {
this.move_splitter(adorner.offset_x(), adorner.offset_y());
this.remove_preview_adorner();
}
}
*this.resize_data.borrow_mut() = None;
}
}
impl GridSplitterImpl for GridSplitter {
fn get_parent_grid(this: &Self) -> Option<Ref<Grid>> {
let parent = this.parent()?;
if let Some(grid) = parent.clone().cast::<Grid>() {
return Some(grid);
}
let items_control = match parent.clone().cast::<crate::ItemsControl>() {
Some(items_control) => items_control,
None => {
parent.cast::<ContentPresenter>()?;
this.parent()?.parent()?.cast::<crate::ItemsControl>()?
}
};
items_control.items_panel_root()?.cast::<Grid>()
}
fn get_properties_value_source(this: &Self) -> Ref<StyledElement> {
match this.parent() {
Some(parent) if parent.is::<ContentPresenter>() => parent,
_ => this.to_ref().upcast(),
}
}
}
ferroui_base::ferro_properties! { impl GridSplitter {
ferro_property!(
pub fn resize_direction_property() -> StyledProperty<GridResizeDirection> {
FerroProperty::register::<GridSplitter, _>("ResizeDirection", GridResizeDirection::Auto)
}
);
ferro_property!(
pub fn resize_behavior_property() -> StyledProperty<GridResizeBehavior> {
FerroProperty::register::<GridSplitter, _>("ResizeBehavior", GridResizeBehavior::BasedOnAlignment)
}
);
ferro_property!(
pub fn shows_preview_property() -> StyledProperty<bool> {
FerroProperty::register::<GridSplitter, _>("ShowsPreview", false)
}
);
ferro_property!(
pub fn keyboard_increment_property() -> StyledProperty<f64> {
FerroProperty::register::<GridSplitter, _>("KeyboardIncrement", 10.0)
}
);
ferro_property!(
pub fn drag_increment_property() -> StyledProperty<f64> {
FerroProperty::register::<GridSplitter, _>("DragIncrement", 1.0)
}
);
ferro_property!(
pub fn preview_content_property() -> StyledProperty<Option<Rc<dyn ITemplateOf<Ref<Control>>>>> {
FerroProperty::register::<GridSplitter, _>("PreviewContent", None)
}
);
} }
impl GridSplitter {
pub fn construct() -> Self {
Self { base: Thumb::construct(), resize_data: RefCell::new(None), is_focus_engaged: Cell::new(false) }
}
pub fn new() -> Ref<Self> {
instantiate(Self::construct())
}
pub fn resize_direction(&self) -> GridResizeDirection {
self.get_value(Self::resize_direction_property())
}
pub fn set_resize_direction(&self, value: GridResizeDirection) {
self.set_value(Self::resize_direction_property(), value)
}
pub fn resize_behavior(&self) -> GridResizeBehavior {
self.get_value(Self::resize_behavior_property())
}
pub fn set_resize_behavior(&self, value: GridResizeBehavior) {
self.set_value(Self::resize_behavior_property(), value)
}
pub fn shows_preview(&self) -> bool {
self.get_value(Self::shows_preview_property())
}
pub fn set_shows_preview(&self, value: bool) {
self.set_value(Self::shows_preview_property(), value)
}
pub fn keyboard_increment(&self) -> f64 {
self.get_value(Self::keyboard_increment_property())
}
pub fn set_keyboard_increment(&self, value: f64) {
self.set_value(Self::keyboard_increment_property(), value)
}
pub fn drag_increment(&self) -> f64 {
self.get_value(Self::drag_increment_property())
}
pub fn set_drag_increment(&self, value: f64) {
self.set_value(Self::drag_increment_property(), value)
}
pub fn preview_content(&self) -> Option<Rc<dyn ITemplateOf<Ref<Control>>>> {
self.get_value(Self::preview_content_property())
}
pub fn set_preview_content(&self, value: Option<Rc<dyn ITemplateOf<Ref<Control>>>>) {
self.set_value(Self::preview_content_property(), value)
}
fn splitter_cursor(
slot: &'static ferroui_base::ferro_thread_local::LocalKey<RefCell<Option<Rc<Cursor>>>>,
cursor_type: StandardCursorType,
) -> Rc<Cursor> {
if let Some(cursor) = slot.with(|slot| slot.borrow().clone()) {
return cursor;
}
let cursor = Cursor::new(cursor_type);
slot.with(|slot| *slot.borrow_mut() = Some(cursor.clone()));
cursor
}
fn remove_preview_adorner(&self) {
let adorner = self.resize_data.borrow().as_ref().and_then(|resize_data| resize_data.adorner.clone());
if let Some(adorner) = adorner {
if let Some(layer) = AdornerLayer::get_adorner_layer(self) {
layer.children().remove(&adorner.upcast::<Control>());
}
}
}
fn has_resize_data(&self) -> bool {
self.resize_data.borrow().is_some()
}
pub(crate) fn get_effective_resize_direction(&self) -> GridResizeDirection {
let mut direction = self.resize_direction();
if direction != GridResizeDirection::Auto {
return direction;
}
if self.horizontal_alignment() != HorizontalAlignment::Stretch {
direction = GridResizeDirection::Columns;
} else if self.vertical_alignment() != VerticalAlignment::Stretch {
direction = GridResizeDirection::Rows;
} else if self.bounds().width <= self.bounds().height {
direction = GridResizeDirection::Columns;
} else {
direction = GridResizeDirection::Rows;
}
direction
}
fn get_effective_resize_behavior(&self, direction: GridResizeDirection) -> GridResizeBehavior {
let mut resize_behavior = self.resize_behavior();
if resize_behavior == GridResizeBehavior::BasedOnAlignment {
if direction == GridResizeDirection::Columns {
resize_behavior = match self.horizontal_alignment() {
HorizontalAlignment::Left => GridResizeBehavior::PreviousAndCurrent,
HorizontalAlignment::Right => GridResizeBehavior::CurrentAndNext,
_ => GridResizeBehavior::PreviousAndNext,
};
} else {
resize_behavior = match self.vertical_alignment() {
VerticalAlignment::Top => GridResizeBehavior::PreviousAndCurrent,
VerticalAlignment::Bottom => GridResizeBehavior::CurrentAndNext,
_ => GridResizeBehavior::PreviousAndNext,
};
}
}
resize_behavior
}
fn initialize_data(&self, shows_preview: bool) {
let Some(grid) = self.get_parent_grid() else {
return;
};
let resize_direction = self.get_effective_resize_direction();
let bounds = self.bounds();
let resize_data = self.setup_definitions_to_resize(ResizeData {
grid,
shows_preview,
adorner: None,
min_change: 0.0,
max_change: 0.0,
resize_direction,
splitter_length: f64::min(bounds.width, bounds.height),
resize_behavior: self.get_effective_resize_behavior(resize_direction),
scaling: self.get_layout_root().map_or(1.0, |root| root.layout_scaling()),
definition1: None,
definition2: None,
split_behavior: SplitBehavior::Split,
splitter_index: 0,
definition1_index: 0,
definition2_index: 0,
original_definition1_length: GridLength::default(),
original_definition2_length: GridLength::default(),
original_definition1_actual_length: 0.0,
original_definition2_actual_length: 0.0,
});
let can_resize = resize_data.is_some();
*self.resize_data.borrow_mut() = resize_data;
if can_resize {
self.setup_preview_adorner();
}
}
fn setup_preview_adorner(&self) {
let Some(resize_data) = self.resize_data.borrow().clone() else {
return;
};
if resize_data.shows_preview {
let adorner_layer = AdornerLayer::get_adorner_layer(&resize_data.grid);
let preview_content = self.preview_content();
let Some(adorner_layer) = adorner_layer else {
return;
};
let built_preview_content = preview_content.map(|preview_content| preview_content.build_typed());
let adorner = PreviewAdorner::new(built_preview_content);
AdornerLayer::set_adorned_element(&adorner, self.to_ref().upcast::<ferroui_base::Visual>());
AdornerLayer::set_is_clip_enabled(&adorner, false);
adorner_layer.children().add(adorner.clone().upcast::<Control>());
let (min_change, max_change) = Self::get_delta_constraints(&resize_data);
if let Some(resize_data) = self.resize_data.borrow_mut().as_mut() {
resize_data.adorner = Some(adorner);
resize_data.min_change = min_change;
resize_data.max_change = max_change;
}
}
}
fn setup_definitions_to_resize(&self, mut resize_data: ResizeData) -> Option<ResizeData> {
let source_control = self.get_properties_value_source();
let is_columns = resize_data.resize_direction == GridResizeDirection::Columns;
let grid_span = source_control.get_value(if is_columns {
Grid::column_span_property()
} else {
Grid::row_span_property()
});
if grid_span == 1 {
let splitter_index =
source_control.get_value(if is_columns { Grid::column_property() } else { Grid::row_property() });
let (index1, index2) = match resize_data.resize_behavior {
GridResizeBehavior::PreviousAndCurrent => (splitter_index - 1, splitter_index),
GridResizeBehavior::CurrentAndNext => (splitter_index, splitter_index + 1),
_ => (splitter_index - 1, splitter_index + 1),
};
let count = if is_columns {
resize_data.grid.column_definitions().count()
} else {
resize_data.grid.row_definitions().count()
} as i32;
if index1 >= 0 && index2 < count {
resize_data.splitter_index = splitter_index;
let definition1 = Self::get_grid_definition(&resize_data.grid, index1, resize_data.resize_direction);
resize_data.definition1_index = index1;
resize_data.original_definition1_length = definition1.user_size_value_cache();
resize_data.original_definition1_actual_length = Self::get_actual_length(&definition1);
let definition2 = Self::get_grid_definition(&resize_data.grid, index2, resize_data.resize_direction);
resize_data.definition2_index = index2;
resize_data.original_definition2_length = definition2.user_size_value_cache();
resize_data.original_definition2_actual_length = Self::get_actual_length(&definition2);
let is_star1 = Self::is_star(&definition1);
let is_star2 = Self::is_star(&definition2);
resize_data.split_behavior = if is_star1 && is_star2 {
SplitBehavior::Split
} else if !is_star1 {
SplitBehavior::Resize1
} else {
SplitBehavior::Resize2
};
resize_data.definition1 = Some(definition1);
resize_data.definition2 = Some(definition2);
return Some(resize_data);
}
}
None
}
fn cancel_resize(&self) {
let Some(resize_data) = self.resize_data.borrow().clone() else {
return;
};
if resize_data.shows_preview {
self.remove_preview_adorner();
} else {
if let Some(definition1) = &resize_data.definition1 {
Self::set_definition_length(definition1, resize_data.original_definition1_length);
}
if let Some(definition2) = &resize_data.definition2 {
Self::set_definition_length(definition2, resize_data.original_definition2_length);
}
}
*self.resize_data.borrow_mut() = None;
}
fn is_star(definition: &DefinitionBase) -> bool {
definition.user_size_value_cache().is_star()
}
fn get_grid_definition(grid: &Grid, index: i32, direction: GridResizeDirection) -> Ref<DefinitionBase> {
if direction == GridResizeDirection::Columns {
grid.column_definitions().get(index as usize).upcast()
} else {
grid.row_definitions().get(index as usize).upcast()
}
}
fn get_actual_length(definition: &Ref<DefinitionBase>) -> f64 {
if let Some(column) = definition.cast::<ColumnDefinition>() {
return column.actual_width();
}
definition.cast::<RowDefinition>().map_or(0.0, |row| row.actual_height())
}
fn set_definition_length(definition: &Ref<DefinitionBase>, length: GridLength) {
definition.set_value(
if definition.is::<ColumnDefinition>() {
ColumnDefinition::width_property()
} else {
RowDefinition::height_property()
},
length,
);
}
fn get_delta_constraints(resize_data: &ResizeData) -> (f64, f64) {
let (Some(definition1), Some(definition2)) = (&resize_data.definition1, &resize_data.definition2) else {
return (0.0, 0.0);
};
let definition1_len = Self::get_actual_length(definition1);
let mut definition1_min = definition1.user_min_size_value_cache();
let definition1_max = definition1.user_max_size_value_cache();
let definition2_len = Self::get_actual_length(definition2);
let mut definition2_min = definition2.user_min_size_value_cache();
let definition2_max = definition2.user_max_size_value_cache();
if resize_data.splitter_index == resize_data.definition1_index {
definition1_min = f64::max(definition1_min, resize_data.splitter_length);
} else if resize_data.splitter_index == resize_data.definition2_index {
definition2_min = f64::max(definition2_min, resize_data.splitter_length);
}
let min_delta = -f64::min(definition1_len - definition1_min, definition2_max - definition2_len);
let max_delta = f64::min(definition1_max - definition1_len, definition2_len - definition2_min);
(min_delta, max_delta)
}
fn set_lengths(resize_data: &ResizeData, definition1_pixels: f64, definition2_pixels: f64) {
if resize_data.split_behavior == SplitBehavior::Split {
let definitions: Vec<Ref<DefinitionBase>> = if resize_data.resize_direction == GridResizeDirection::Columns
{
resize_data.grid.column_definitions().snapshot().iter().map(|d| d.clone().upcast()).collect()
} else {
resize_data.grid.row_definitions().snapshot().iter().map(|d| d.clone().upcast()).collect()
};
for (i, definition) in definitions.iter().enumerate() {
let i = i as i32;
if i == resize_data.definition1_index {
Self::set_definition_length(definition, GridLength::new(definition1_pixels, GridUnitType::Star));
} else if i == resize_data.definition2_index {
Self::set_definition_length(definition, GridLength::new(definition2_pixels, GridUnitType::Star));
} else if Self::is_star(definition) {
Self::set_definition_length(
definition,
GridLength::new(Self::get_actual_length(definition), GridUnitType::Star),
);
}
}
} else if resize_data.split_behavior == SplitBehavior::Resize1 {
if let Some(definition1) = &resize_data.definition1 {
Self::set_definition_length(definition1, GridLength::from_pixels(definition1_pixels));
}
} else if let Some(definition2) = &resize_data.definition2 {
Self::set_definition_length(definition2, GridLength::from_pixels(definition2_pixels));
}
}
fn move_splitter(&self, horizontal_change: f64, vertical_change: f64) {
let Some(resize_data) = self.resize_data.borrow().clone() else {
return;
};
let mut delta = if resize_data.resize_direction == GridResizeDirection::Columns {
horizontal_change
} else {
vertical_change
};
if self.use_layout_rounding() {
delta = LayoutHelper::round_layout_value(delta, LayoutHelper::get_layout_scale(self));
}
if let (Some(definition1), Some(definition2)) = (&resize_data.definition1, &resize_data.definition2) {
let actual_length1 = Self::get_actual_length(definition1);
let actual_length2 = Self::get_actual_length(definition2);
let pixel_length = 1.0 / resize_data.scaling;
let epsilon = pixel_length + LayoutHelper::LAYOUT_EPSILON;
if resize_data.split_behavior == SplitBehavior::Split
&& !MathUtilities::are_close_eps(
actual_length1 + actual_length2,
resize_data.original_definition1_actual_length + resize_data.original_definition2_actual_length,
epsilon,
)
{
self.cancel_resize();
return;
}
let (min, max) = Self::get_delta_constraints(&resize_data);
delta = f64::min(f64::max(delta, min), max);
let definition1_length_new = actual_length1 + delta;
let definition2_length_new = actual_length1 + actual_length2 - definition1_length_new;
Self::set_lengths(&resize_data, definition1_length_new, definition2_length_new);
}
}
fn keyboard_move_splitter(&self, horizontal_change: f64, vertical_change: f64) -> bool {
if self.has_resize_data() {
return false; }
self.initialize_data(false);
if !self.has_resize_data() {
return false; }
self.move_splitter(horizontal_change, vertical_change);
*self.resize_data.borrow_mut() = None;
true
}
}
#[repr(C)]
pub(crate) struct PreviewAdorner {
base: Decorator,
translation: Ref<TranslateTransform>,
}
ferro_class!(PreviewAdorner: Decorator);
ferro_impl_classes!(
PreviewAdorner: FerroObjectImpl,
StyledElementImpl,
VisualImpl,
InteractiveImpl,
InputElementImpl,
ControlImpl
);
impl LayoutableImpl for PreviewAdorner {
fn arrange_override(this: &Self, final_size: Size) -> Size {
this.set_clip(None::<Ref<Geometry>>);
Self::parent_arrange_override(this, final_size)
}
}
impl PreviewAdorner {
fn new(preview_control: Option<Ref<Control>>) -> Ref<Self> {
let translation = TranslateTransform::new();
let this = instantiate(Self { base: Decorator::construct(), translation: translation.clone() });
let decorator = Decorator::new();
decorator.set_child(preview_control);
let transform: Rc<dyn ITransform> = translation.into();
decorator.set_render_transform(Some(transform));
this.set_child(decorator);
this
}
fn offset_x(&self) -> f64 {
self.translation.x()
}
fn set_offset_x(&self, value: f64) {
self.translation.set_x(value)
}
fn offset_y(&self) -> f64 {
self.translation.y()
}
fn set_offset_y(&self, value: f64) {
self.translation.set_y(value)
}
}