1use std::vec::Vec;
43
44use azul_core::{
45 callbacks::{CoreCallbackData, Update},
46 dom::{Dom, IdOrClass, IdOrClass::Class, IdOrClassVec, TabIndex},
47 geom::{CursorNodePosition, LogicalSize},
48 refany::RefAny,
49};
50use azul_css::dynamic_selector::{CssPropertyWithConditions, CssPropertyWithConditionsVec};
51use azul_css::{
52 props::{
53 basic::{color::ColorU, FloatValue, PixelValue},
54 layout::{LayoutFlexGrow, LayoutFlexDirection, LayoutDisplay, LayoutWidth, LayoutHeight, LayoutOverflow, LayoutFlexBasis, LayoutMinWidth, LayoutMinHeight, LayoutFlexShrink},
55 property::{CssProperty, LayoutFlexGrowValue, LayoutWidthValue, LayoutHeightValue, LayoutFlexBasisValue},
56 style::{StyleBackgroundContent, StyleBackgroundContentVec, StyleCursor},
57 },
58 impl_option_inner, AzString,
59};
60
61use crate::callbacks::CallbackInfo;
62
63static SPLIT_PANE_CLASS: &[IdOrClass] =
64 &[Class(AzString::from_const_str("__azul-native-split-pane"))];
65static SPLIT_PANE_FIRST_CLASS: &[IdOrClass] =
66 &[Class(AzString::from_const_str("__azul-native-split-pane-first"))];
67static SPLIT_PANE_DIVIDER_CLASS: &[IdOrClass] =
68 &[Class(AzString::from_const_str("__azul-native-split-pane-divider"))];
69static SPLIT_PANE_SECOND_CLASS: &[IdOrClass] =
70 &[Class(AzString::from_const_str("__azul-native-split-pane-second"))];
71
72#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
74#[repr(C)]
75pub enum SplitDirection {
76 #[default]
78 Horizontal,
79 Vertical,
81}
82
83pub type SplitPaneOnResizeCallbackType =
85 extern "C" fn(RefAny, CallbackInfo, SplitPaneState) -> Update;
86impl_widget_callback!(
87 SplitPaneOnResize,
88 OptionSplitPaneOnResize,
89 SplitPaneOnResizeCallback,
90 SplitPaneOnResizeCallbackType
91);
92
93azul_core::impl_managed_callback! {
94 wrapper: SplitPaneOnResizeCallback,
95 info_ty: CallbackInfo,
96 return_ty: Update,
97 default_ret: Update::DoNothing,
98 invoker_static: SPLIT_PANE_ON_RESIZE_INVOKER,
99 invoker_ty: AzSplitPaneOnResizeCallbackInvoker,
100 thunk_fn: az_split_pane_on_resize_callback_thunk,
101 setter_fn: AzApp_setSplitPaneOnResizeCallbackInvoker,
102 from_handle_fn: AzSplitPaneOnResizeCallback_createFromHostHandle,
103 extra_args: [ state: SplitPaneState ],
104}
105
106#[derive(Debug, Clone)]
108#[repr(C)]
109pub struct SplitPane {
110 pub split_pane_state: SplitPaneStateWrapper,
111 pub first: Dom,
113 pub second: Dom,
115 pub container_style: CssPropertyWithConditionsVec,
117}
118
119#[derive(Debug, Default, Clone, PartialEq)]
120#[repr(C)]
121pub struct SplitPaneStateWrapper {
122 pub inner: SplitPaneState,
124 pub on_resize: OptionSplitPaneOnResize,
126 pub is_dragging: bool,
129 pub drag_start_px: f32,
131 pub ratio_at_drag_start: f32,
133}
134
135#[derive(Debug, Copy, Clone, PartialEq)]
137#[repr(C)]
138pub struct SplitPaneState {
139 pub direction: SplitDirection,
141 pub ratio: f32,
144}
145
146impl Default for SplitPaneState {
147 fn default() -> Self {
148 Self {
149 direction: SplitDirection::Horizontal,
150 ratio: 0.5,
151 }
152 }
153}
154
155const DIVIDER_THICKNESS: isize = 6;
158const GRAB_THRESHOLD: f32 = 9.0;
160const MIN_RATIO: f32 = 0.05;
162const MAX_RATIO: f32 = 0.95;
163
164const DIVIDER_COLOR: ColorU = ColorU { r: 173, g: 181, b: 189, a: 255 };
167
168const DIVIDER_BG_ITEMS: &[StyleBackgroundContent] = &[StyleBackgroundContent::Color(DIVIDER_COLOR)];
169const DIVIDER_BG: StyleBackgroundContentVec =
170 StyleBackgroundContentVec::from_const_slice(DIVIDER_BG_ITEMS);
171
172fn flex_grow_prop(v: f32) -> CssProperty {
174 CssProperty::FlexGrow(LayoutFlexGrowValue::Exact(LayoutFlexGrow {
175 inner: FloatValue::new(v),
176 }))
177}
178
179const fn main_axis(dir: SplitDirection, pos: CursorNodePosition) -> f32 {
181 match dir {
182 SplitDirection::Horizontal => pos.x,
183 SplitDirection::Vertical => pos.y,
184 }
185}
186
187const fn main_size(dir: SplitDirection, size: LogicalSize) -> f32 {
189 match dir {
190 SplitDirection::Horizontal => size.width,
191 SplitDirection::Vertical => size.height,
192 }
193}
194
195fn container_style(dir: SplitDirection) -> CssPropertyWithConditionsVec {
198 let flex_dir = match dir {
199 SplitDirection::Horizontal => LayoutFlexDirection::Row,
200 SplitDirection::Vertical => LayoutFlexDirection::Column,
201 };
202 CssPropertyWithConditionsVec::from_vec(vec![
203 CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
204 CssPropertyWithConditions::simple(CssProperty::const_flex_direction(flex_dir)),
205 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(1))),
206 CssPropertyWithConditions::simple(CssProperty::Width(LayoutWidthValue::Exact(
207 LayoutWidth::Px(PixelValue::percent(100.0)),
208 ))),
209 CssPropertyWithConditions::simple(CssProperty::Height(LayoutHeightValue::Exact(
210 LayoutHeight::Px(PixelValue::percent(100.0)),
211 ))),
212 CssPropertyWithConditions::simple(CssProperty::const_overflow_x(LayoutOverflow::Hidden)),
213 CssPropertyWithConditions::simple(CssProperty::const_overflow_y(LayoutOverflow::Hidden)),
214 ])
215}
216
217fn pane_style(grow: f32) -> CssPropertyWithConditionsVec {
222 CssPropertyWithConditionsVec::from_vec(vec![
223 CssPropertyWithConditions::simple(flex_grow_prop(grow)),
224 CssPropertyWithConditions::simple(CssProperty::FlexBasis(LayoutFlexBasisValue::Exact(
225 LayoutFlexBasis::Exact(PixelValue::const_px(0)),
226 ))),
227 CssPropertyWithConditions::simple(CssProperty::const_min_width(LayoutMinWidth::const_px(0))),
228 CssPropertyWithConditions::simple(CssProperty::const_min_height(LayoutMinHeight::const_px(
229 0,
230 ))),
231 CssPropertyWithConditions::simple(CssProperty::const_overflow_x(LayoutOverflow::Hidden)),
232 CssPropertyWithConditions::simple(CssProperty::const_overflow_y(LayoutOverflow::Hidden)),
233 ])
234}
235
236fn divider_style(dir: SplitDirection) -> CssPropertyWithConditionsVec {
240 let (size_prop, cursor) = match dir {
241 SplitDirection::Horizontal => (
242 CssProperty::const_width(LayoutWidth::const_px(DIVIDER_THICKNESS)),
243 StyleCursor::ColResize,
244 ),
245 SplitDirection::Vertical => (
246 CssProperty::const_height(LayoutHeight::const_px(DIVIDER_THICKNESS)),
247 StyleCursor::RowResize,
248 ),
249 };
250 CssPropertyWithConditionsVec::from_vec(vec![
251 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
252 CssPropertyWithConditions::simple(CssProperty::const_flex_shrink(LayoutFlexShrink {
253 inner: FloatValue::const_new(0),
254 })),
255 CssPropertyWithConditions::simple(size_prop),
256 CssPropertyWithConditions::simple(CssProperty::const_cursor(cursor)),
257 CssPropertyWithConditions::simple(CssProperty::const_background_content(DIVIDER_BG)),
258 ])
259}
260
261impl SplitPane {
262 #[must_use] pub fn create(direction: SplitDirection, first: Dom, second: Dom) -> Self {
264 Self {
265 split_pane_state: SplitPaneStateWrapper {
266 inner: SplitPaneState {
267 direction,
268 ratio: 0.5,
269 },
270 ..Default::default()
271 },
272 first,
273 second,
274 container_style: container_style(direction),
275 }
276 }
277
278 #[inline]
280 pub const fn set_ratio(&mut self, ratio: f32) {
281 self.split_pane_state.inner.ratio = ratio.clamp(MIN_RATIO, MAX_RATIO);
282 }
283
284 #[inline]
286 #[must_use] pub const fn with_ratio(mut self, ratio: f32) -> Self {
287 self.set_ratio(ratio);
288 self
289 }
290
291 #[inline]
293 pub fn set_direction(&mut self, direction: SplitDirection) {
294 self.split_pane_state.inner.direction = direction;
295 self.container_style = container_style(direction);
296 }
297
298 #[inline]
300 #[must_use] pub fn with_direction(mut self, direction: SplitDirection) -> Self {
301 self.set_direction(direction);
302 self
303 }
304
305 #[inline]
307 #[must_use] pub fn with_container_style(mut self, css: CssPropertyWithConditionsVec) -> Self {
308 self.container_style = css;
309 self
310 }
311
312 #[inline]
313 #[must_use] pub fn swap_with_default(&mut self) -> Self {
314 let mut s = Self::create(
315 SplitDirection::Horizontal,
316 Dom::create_div(),
317 Dom::create_div(),
318 );
319 core::mem::swap(&mut s, self);
320 s
321 }
322
323 #[inline]
324 pub fn set_on_resize<C: Into<SplitPaneOnResizeCallback>>(&mut self, data: RefAny, on_resize: C) {
325 self.split_pane_state.on_resize = Some(SplitPaneOnResize {
326 callback: on_resize.into(),
327 refany: data,
328 })
329 .into();
330 }
331
332 #[inline]
333 #[must_use] pub fn with_on_resize<C: Into<SplitPaneOnResizeCallback>>(
334 mut self,
335 data: RefAny,
336 on_resize: C,
337 ) -> Self {
338 self.set_on_resize(data, on_resize);
339 self
340 }
341
342 #[must_use] pub fn dom(self) -> Dom {
343 use azul_core::{
344 callbacks::CoreCallback,
345 dom::{EventFilter, HoverEventFilter},
346 refany::OptionRefAny,
347 };
348
349 let direction = self.split_pane_state.inner.direction;
350 let ratio = self.split_pane_state.inner.ratio;
351
352 let state = RefAny::new(self.split_pane_state);
356 let mk = |event: EventFilter, cb: usize| CoreCallbackData {
357 event,
358 callback: CoreCallback {
359 cb,
360 ctx: OptionRefAny::None,
361 },
362 refany: state.clone(),
363 };
364 let callbacks = vec![
365 mk(
366 EventFilter::Hover(HoverEventFilter::MouseDown),
367 on_split_pointer_down as usize,
368 ),
369 mk(
370 EventFilter::Hover(HoverEventFilter::MouseOver),
371 on_split_pointer_move as usize,
372 ),
373 mk(
374 EventFilter::Hover(HoverEventFilter::MouseUp),
375 on_split_pointer_up as usize,
376 ),
377 mk(
378 EventFilter::Hover(HoverEventFilter::MouseLeave),
379 on_split_pointer_up as usize,
380 ),
381 mk(
382 EventFilter::Hover(HoverEventFilter::TouchStart),
383 on_split_pointer_down as usize,
384 ),
385 mk(
386 EventFilter::Hover(HoverEventFilter::TouchMove),
387 on_split_pointer_move as usize,
388 ),
389 mk(
390 EventFilter::Hover(HoverEventFilter::TouchEnd),
391 on_split_pointer_up as usize,
392 ),
393 ];
394
395 let first_pane = Dom::create_div()
398 .with_ids_and_classes(IdOrClassVec::from_const_slice(SPLIT_PANE_FIRST_CLASS))
399 .with_css_props(pane_style(ratio))
400 .with_children(vec![self.first].into());
401
402 let divider = Dom::create_div()
403 .with_ids_and_classes(IdOrClassVec::from_const_slice(SPLIT_PANE_DIVIDER_CLASS))
404 .with_css_props(divider_style(direction));
405
406 let second_pane = Dom::create_div()
407 .with_ids_and_classes(IdOrClassVec::from_const_slice(SPLIT_PANE_SECOND_CLASS))
408 .with_css_props(pane_style(1.0 - ratio))
409 .with_children(vec![self.second].into());
410
411 Dom::create_div()
412 .with_ids_and_classes(IdOrClassVec::from_const_slice(SPLIT_PANE_CLASS))
413 .with_css_props(self.container_style)
414 .with_callbacks(callbacks.into())
415 .with_tab_index(TabIndex::Auto)
416 .with_children(vec![first_pane, divider, second_pane].into())
417 }
418}
419
420impl Default for SplitPane {
421 fn default() -> Self {
422 Self::create(
423 SplitDirection::Horizontal,
424 Dom::create_div(),
425 Dom::create_div(),
426 )
427 }
428}
429
430extern "C" fn on_split_pointer_down(mut data: RefAny, info: CallbackInfo) -> Update {
434 let Some(pos) = info.get_cursor_relative_to_node().into_option() else {
435 return Update::DoNothing;
436 };
437 let size = match info.get_hit_node_rect() {
438 Some(r) => r.size,
439 None => return Update::DoNothing,
440 };
441 let Some(mut sp) = data.downcast_mut::<SplitPaneStateWrapper>() else {
442 return Update::DoNothing;
443 };
444 let dir = sp.inner.direction;
445 let msize = main_size(dir, size);
446 if msize <= 0.0 {
447 return Update::DoNothing;
448 }
449 let main = main_axis(dir, pos);
450 let divider_center = sp.inner.ratio * msize;
451 if (main - divider_center).abs() <= GRAB_THRESHOLD {
452 sp.is_dragging = true;
453 sp.drag_start_px = main;
454 sp.ratio_at_drag_start = sp.inner.ratio;
455 }
456 Update::DoNothing
457}
458
459extern "C" fn on_split_pointer_move(mut data: RefAny, mut info: CallbackInfo) -> Update {
462 let Some(mut sp) = data.downcast_mut::<SplitPaneStateWrapper>() else {
463 return Update::DoNothing;
464 };
465 if !sp.is_dragging {
466 return Update::DoNothing;
467 }
468 let dir = sp.inner.direction;
469 let Some(pos) = info.get_cursor_relative_to_node().into_option() else {
470 return Update::DoNothing;
471 };
472 let size = match info.get_hit_node_rect() {
473 Some(r) => r.size,
474 None => return Update::DoNothing,
475 };
476 let msize = main_size(dir, size);
477 if msize <= 0.0 {
478 return Update::DoNothing;
479 }
480 let main = main_axis(dir, pos);
481 let delta = main - sp.drag_start_px;
482 let new_ratio = (sp.ratio_at_drag_start + delta / msize).clamp(MIN_RATIO, MAX_RATIO);
483 sp.inner.ratio = new_ratio;
484
485 let container = info.get_hit_node();
488 if let Some(pane0) = info.get_first_child(container) {
489 info.set_css_property(pane0, flex_grow_prop(new_ratio));
490 if let Some(divider) = info.get_next_sibling(pane0) {
491 if let Some(pane1) = info.get_next_sibling(divider) {
492 info.set_css_property(pane1, flex_grow_prop(1.0 - new_ratio));
493 }
494 }
495 }
496
497 let inner = sp.inner;
498 match sp.on_resize.as_mut() {
499 Some(SplitPaneOnResize { callback, refany }) => (callback.cb)(refany.clone(), info, inner),
500 None => Update::DoNothing,
501 }
502}
503
504extern "C" fn on_split_pointer_up(mut data: RefAny, _info: CallbackInfo) -> Update {
506 if let Some(mut sp) = data.downcast_mut::<SplitPaneStateWrapper>() {
507 sp.is_dragging = false;
508 }
509 Update::DoNothing
510}
511
512impl From<SplitPane> for Dom {
513 fn from(s: SplitPane) -> Self {
514 s.dom()
515 }
516}