use std::vec::Vec;
use azul_core::{
callbacks::{CoreCallbackData, Update},
dom::{Dom, IdOrClass, IdOrClass::Class, IdOrClassVec, TabIndex},
geom::{CursorNodePosition, LogicalSize},
refany::RefAny,
};
use azul_css::dynamic_selector::{CssPropertyWithConditions, CssPropertyWithConditionsVec};
use azul_css::{
props::{
basic::{color::ColorU, FloatValue, PixelValue},
layout::{LayoutFlexGrow, LayoutFlexDirection, LayoutDisplay, LayoutWidth, LayoutHeight, LayoutOverflow, LayoutFlexBasis, LayoutMinWidth, LayoutMinHeight, LayoutFlexShrink},
property::{CssProperty, LayoutFlexGrowValue, LayoutWidthValue, LayoutHeightValue, LayoutFlexBasisValue},
style::{StyleBackgroundContent, StyleBackgroundContentVec, StyleCursor},
},
impl_option_inner, AzString,
};
use crate::callbacks::CallbackInfo;
static SPLIT_PANE_CLASS: &[IdOrClass] =
&[Class(AzString::from_const_str("__azul-native-split-pane"))];
static SPLIT_PANE_FIRST_CLASS: &[IdOrClass] =
&[Class(AzString::from_const_str("__azul-native-split-pane-first"))];
static SPLIT_PANE_DIVIDER_CLASS: &[IdOrClass] =
&[Class(AzString::from_const_str("__azul-native-split-pane-divider"))];
static SPLIT_PANE_SECOND_CLASS: &[IdOrClass] =
&[Class(AzString::from_const_str("__azul-native-split-pane-second"))];
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
#[repr(C)]
pub enum SplitDirection {
#[default]
Horizontal,
Vertical,
}
pub type SplitPaneOnResizeCallbackType =
extern "C" fn(RefAny, CallbackInfo, SplitPaneState) -> Update;
impl_widget_callback!(
SplitPaneOnResize,
OptionSplitPaneOnResize,
SplitPaneOnResizeCallback,
SplitPaneOnResizeCallbackType
);
azul_core::impl_managed_callback! {
wrapper: SplitPaneOnResizeCallback,
info_ty: CallbackInfo,
return_ty: Update,
default_ret: Update::DoNothing,
invoker_static: SPLIT_PANE_ON_RESIZE_INVOKER,
invoker_ty: AzSplitPaneOnResizeCallbackInvoker,
thunk_fn: az_split_pane_on_resize_callback_thunk,
setter_fn: AzApp_setSplitPaneOnResizeCallbackInvoker,
from_handle_fn: AzSplitPaneOnResizeCallback_createFromHostHandle,
extra_args: [ state: SplitPaneState ],
}
#[derive(Debug, Clone)]
#[repr(C)]
pub struct SplitPane {
pub split_pane_state: SplitPaneStateWrapper,
pub first: Dom,
pub second: Dom,
pub container_style: CssPropertyWithConditionsVec,
}
#[derive(Debug, Default, Clone, PartialEq)]
#[repr(C)]
pub struct SplitPaneStateWrapper {
pub inner: SplitPaneState,
pub on_resize: OptionSplitPaneOnResize,
pub is_dragging: bool,
pub drag_start_px: f32,
pub ratio_at_drag_start: f32,
}
#[derive(Debug, Copy, Clone, PartialEq)]
#[repr(C)]
pub struct SplitPaneState {
pub direction: SplitDirection,
pub ratio: f32,
}
impl Default for SplitPaneState {
fn default() -> Self {
Self {
direction: SplitDirection::Horizontal,
ratio: 0.5,
}
}
}
const DIVIDER_THICKNESS: isize = 6;
const GRAB_THRESHOLD: f32 = 9.0;
const MIN_RATIO: f32 = 0.05;
const MAX_RATIO: f32 = 0.95;
const DIVIDER_COLOR: ColorU = ColorU { r: 173, g: 181, b: 189, a: 255 };
const DIVIDER_BG_ITEMS: &[StyleBackgroundContent] = &[StyleBackgroundContent::Color(DIVIDER_COLOR)];
const DIVIDER_BG: StyleBackgroundContentVec =
StyleBackgroundContentVec::from_const_slice(DIVIDER_BG_ITEMS);
fn flex_grow_prop(v: f32) -> CssProperty {
CssProperty::FlexGrow(LayoutFlexGrowValue::Exact(LayoutFlexGrow {
inner: FloatValue::new(v),
}))
}
const fn main_axis(dir: SplitDirection, pos: CursorNodePosition) -> f32 {
match dir {
SplitDirection::Horizontal => pos.x,
SplitDirection::Vertical => pos.y,
}
}
const fn main_size(dir: SplitDirection, size: LogicalSize) -> f32 {
match dir {
SplitDirection::Horizontal => size.width,
SplitDirection::Vertical => size.height,
}
}
fn container_style(dir: SplitDirection) -> CssPropertyWithConditionsVec {
let flex_dir = match dir {
SplitDirection::Horizontal => LayoutFlexDirection::Row,
SplitDirection::Vertical => LayoutFlexDirection::Column,
};
CssPropertyWithConditionsVec::from_vec(vec![
CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
CssPropertyWithConditions::simple(CssProperty::const_flex_direction(flex_dir)),
CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(1))),
CssPropertyWithConditions::simple(CssProperty::Width(LayoutWidthValue::Exact(
LayoutWidth::Px(PixelValue::percent(100.0)),
))),
CssPropertyWithConditions::simple(CssProperty::Height(LayoutHeightValue::Exact(
LayoutHeight::Px(PixelValue::percent(100.0)),
))),
CssPropertyWithConditions::simple(CssProperty::const_overflow_x(LayoutOverflow::Hidden)),
CssPropertyWithConditions::simple(CssProperty::const_overflow_y(LayoutOverflow::Hidden)),
])
}
fn pane_style(grow: f32) -> CssPropertyWithConditionsVec {
CssPropertyWithConditionsVec::from_vec(vec![
CssPropertyWithConditions::simple(flex_grow_prop(grow)),
CssPropertyWithConditions::simple(CssProperty::FlexBasis(LayoutFlexBasisValue::Exact(
LayoutFlexBasis::Exact(PixelValue::const_px(0)),
))),
CssPropertyWithConditions::simple(CssProperty::const_min_width(LayoutMinWidth::const_px(0))),
CssPropertyWithConditions::simple(CssProperty::const_min_height(LayoutMinHeight::const_px(
0,
))),
CssPropertyWithConditions::simple(CssProperty::const_overflow_x(LayoutOverflow::Hidden)),
CssPropertyWithConditions::simple(CssProperty::const_overflow_y(LayoutOverflow::Hidden)),
])
}
fn divider_style(dir: SplitDirection) -> CssPropertyWithConditionsVec {
let (size_prop, cursor) = match dir {
SplitDirection::Horizontal => (
CssProperty::const_width(LayoutWidth::const_px(DIVIDER_THICKNESS)),
StyleCursor::ColResize,
),
SplitDirection::Vertical => (
CssProperty::const_height(LayoutHeight::const_px(DIVIDER_THICKNESS)),
StyleCursor::RowResize,
),
};
CssPropertyWithConditionsVec::from_vec(vec![
CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
CssPropertyWithConditions::simple(CssProperty::const_flex_shrink(LayoutFlexShrink {
inner: FloatValue::const_new(0),
})),
CssPropertyWithConditions::simple(size_prop),
CssPropertyWithConditions::simple(CssProperty::const_cursor(cursor)),
CssPropertyWithConditions::simple(CssProperty::const_background_content(DIVIDER_BG)),
])
}
impl SplitPane {
#[must_use] pub fn create(direction: SplitDirection, first: Dom, second: Dom) -> Self {
Self {
split_pane_state: SplitPaneStateWrapper {
inner: SplitPaneState {
direction,
ratio: 0.5,
},
..Default::default()
},
first,
second,
container_style: container_style(direction),
}
}
#[inline]
pub const fn set_ratio(&mut self, ratio: f32) {
self.split_pane_state.inner.ratio = ratio.clamp(MIN_RATIO, MAX_RATIO);
}
#[inline]
#[must_use] pub const fn with_ratio(mut self, ratio: f32) -> Self {
self.set_ratio(ratio);
self
}
#[inline]
pub fn set_direction(&mut self, direction: SplitDirection) {
self.split_pane_state.inner.direction = direction;
self.container_style = container_style(direction);
}
#[inline]
#[must_use] pub fn with_direction(mut self, direction: SplitDirection) -> Self {
self.set_direction(direction);
self
}
#[inline]
#[must_use] pub fn with_container_style(mut self, css: CssPropertyWithConditionsVec) -> Self {
self.container_style = css;
self
}
#[inline]
#[must_use] pub fn swap_with_default(&mut self) -> Self {
let mut s = Self::create(
SplitDirection::Horizontal,
Dom::create_div(),
Dom::create_div(),
);
core::mem::swap(&mut s, self);
s
}
#[inline]
pub fn set_on_resize<C: Into<SplitPaneOnResizeCallback>>(&mut self, data: RefAny, on_resize: C) {
self.split_pane_state.on_resize = Some(SplitPaneOnResize {
callback: on_resize.into(),
refany: data,
})
.into();
}
#[inline]
#[must_use] pub fn with_on_resize<C: Into<SplitPaneOnResizeCallback>>(
mut self,
data: RefAny,
on_resize: C,
) -> Self {
self.set_on_resize(data, on_resize);
self
}
#[must_use] pub fn dom(self) -> Dom {
use azul_core::{
callbacks::CoreCallback,
dom::{EventFilter, HoverEventFilter},
refany::OptionRefAny,
};
let direction = self.split_pane_state.inner.direction;
let ratio = self.split_pane_state.inner.ratio;
let state = RefAny::new(self.split_pane_state);
let mk = |event: EventFilter, cb: usize| CoreCallbackData {
event,
callback: CoreCallback {
cb,
ctx: OptionRefAny::None,
},
refany: state.clone(),
};
let callbacks = vec![
mk(
EventFilter::Hover(HoverEventFilter::MouseDown),
on_split_pointer_down as usize,
),
mk(
EventFilter::Hover(HoverEventFilter::MouseOver),
on_split_pointer_move as usize,
),
mk(
EventFilter::Hover(HoverEventFilter::MouseUp),
on_split_pointer_up as usize,
),
mk(
EventFilter::Hover(HoverEventFilter::MouseLeave),
on_split_pointer_up as usize,
),
mk(
EventFilter::Hover(HoverEventFilter::TouchStart),
on_split_pointer_down as usize,
),
mk(
EventFilter::Hover(HoverEventFilter::TouchMove),
on_split_pointer_move as usize,
),
mk(
EventFilter::Hover(HoverEventFilter::TouchEnd),
on_split_pointer_up as usize,
),
];
let first_pane = Dom::create_div()
.with_ids_and_classes(IdOrClassVec::from_const_slice(SPLIT_PANE_FIRST_CLASS))
.with_css_props(pane_style(ratio))
.with_children(vec![self.first].into());
let divider = Dom::create_div()
.with_ids_and_classes(IdOrClassVec::from_const_slice(SPLIT_PANE_DIVIDER_CLASS))
.with_css_props(divider_style(direction));
let second_pane = Dom::create_div()
.with_ids_and_classes(IdOrClassVec::from_const_slice(SPLIT_PANE_SECOND_CLASS))
.with_css_props(pane_style(1.0 - ratio))
.with_children(vec![self.second].into());
Dom::create_div()
.with_ids_and_classes(IdOrClassVec::from_const_slice(SPLIT_PANE_CLASS))
.with_css_props(self.container_style)
.with_callbacks(callbacks.into())
.with_tab_index(TabIndex::Auto)
.with_children(vec![first_pane, divider, second_pane].into())
}
}
impl Default for SplitPane {
fn default() -> Self {
Self::create(
SplitDirection::Horizontal,
Dom::create_div(),
Dom::create_div(),
)
}
}
extern "C" fn on_split_pointer_down(mut data: RefAny, info: CallbackInfo) -> Update {
let Some(pos) = info.get_cursor_relative_to_node().into_option() else {
return Update::DoNothing;
};
let size = match info.get_hit_node_rect() {
Some(r) => r.size,
None => return Update::DoNothing,
};
let Some(mut sp) = data.downcast_mut::<SplitPaneStateWrapper>() else {
return Update::DoNothing;
};
let dir = sp.inner.direction;
let msize = main_size(dir, size);
if msize <= 0.0 {
return Update::DoNothing;
}
let main = main_axis(dir, pos);
let divider_center = sp.inner.ratio * msize;
if (main - divider_center).abs() <= GRAB_THRESHOLD {
sp.is_dragging = true;
sp.drag_start_px = main;
sp.ratio_at_drag_start = sp.inner.ratio;
}
Update::DoNothing
}
extern "C" fn on_split_pointer_move(mut data: RefAny, mut info: CallbackInfo) -> Update {
let Some(mut sp) = data.downcast_mut::<SplitPaneStateWrapper>() else {
return Update::DoNothing;
};
if !sp.is_dragging {
return Update::DoNothing;
}
let dir = sp.inner.direction;
let Some(pos) = info.get_cursor_relative_to_node().into_option() else {
return Update::DoNothing;
};
let size = match info.get_hit_node_rect() {
Some(r) => r.size,
None => return Update::DoNothing,
};
let msize = main_size(dir, size);
if msize <= 0.0 {
return Update::DoNothing;
}
let main = main_axis(dir, pos);
let delta = main - sp.drag_start_px;
let new_ratio = (sp.ratio_at_drag_start + delta / msize).clamp(MIN_RATIO, MAX_RATIO);
sp.inner.ratio = new_ratio;
let container = info.get_hit_node();
if let Some(pane0) = info.get_first_child(container) {
info.set_css_property(pane0, flex_grow_prop(new_ratio));
if let Some(divider) = info.get_next_sibling(pane0) {
if let Some(pane1) = info.get_next_sibling(divider) {
info.set_css_property(pane1, flex_grow_prop(1.0 - new_ratio));
}
}
}
let inner = sp.inner;
match sp.on_resize.as_mut() {
Some(SplitPaneOnResize { callback, refany }) => (callback.cb)(refany.clone(), info, inner),
None => Update::DoNothing,
}
}
extern "C" fn on_split_pointer_up(mut data: RefAny, _info: CallbackInfo) -> Update {
if let Some(mut sp) = data.downcast_mut::<SplitPaneStateWrapper>() {
sp.is_dragging = false;
}
Update::DoNothing
}
impl From<SplitPane> for Dom {
fn from(s: SplitPane) -> Self {
s.dom()
}
}