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}
517
518#[cfg(test)]
519#[allow(
520 clippy::float_cmp,
521 clippy::too_many_lines,
522 clippy::cast_precision_loss,
523 clippy::unreadable_literal
524)]
525mod autotest_generated {
526 use std::{
527 collections::{BTreeMap, HashMap},
528 string::{String, ToString},
529 sync::{Arc, Mutex},
530 };
531
532 use azul_core::{
533 dom::{
534 DomId, DomNodeId, EventFilter, FormattingContext, HoverEventFilter, NodeId,
535 },
536 geom::{LogicalPosition, LogicalRect, OptionLogicalPosition},
537 gl::OptionGlContextPtr,
538 hit_test::ScrollPosition,
539 refany::OptionRefAny,
540 resources::RendererResources,
541 styled_dom::{NodeHierarchyItemId, StyledDom},
542 window::{MonitorVec, RawWindowHandle},
543 };
544 use azul_css::{props::basic::length::SizeMetric, system::SystemStyle};
545 use rust_fontconfig::FcFontCache;
546
547 use super::*;
548 #[cfg(feature = "icu")]
549 use crate::icu::IcuLocalizerHandle;
550 use crate::{
551 callbacks::{CallbackChange, CallbackInfoRefData, ExternalSystemCallbacks},
552 solver3::{
553 display_list::DisplayList,
554 geometry::PackedBoxProps,
555 layout_tree::{LayoutNodeHot, LayoutTree},
556 },
557 window::{DomLayoutResult, LayoutWindow},
558 window_state::FullWindowState,
559 };
560
561 const BOTH_DIRECTIONS: [SplitDirection; 2] =
566 [SplitDirection::Horizontal, SplitDirection::Vertical];
567
568 const IN_RANGE_RATIOS: [f32; 7] = [0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95];
571
572 fn size(width: f32, height: f32) -> LogicalSize {
573 LogicalSize::new(width, height)
574 }
575
576 fn cursor(x: f32, y: f32) -> OptionLogicalPosition {
577 OptionLogicalPosition::Some(LogicalPosition::new(x, y))
578 }
579
580 fn pos(x: f32, y: f32) -> CursorNodePosition {
581 CursorNodePosition::new(x, y)
582 }
583
584 fn pane(first: Dom, second: Dom) -> SplitPane {
585 SplitPane::create(SplitDirection::Horizontal, first, second)
586 }
587
588 fn plain(direction: SplitDirection) -> SplitPane {
589 SplitPane::create(direction, Dom::create_div(), Dom::create_div())
590 }
591
592 fn div_with_class(class: &str) -> Dom {
593 Dom::create_div().with_ids_and_classes(vec![Class(AzString::from(class))].into())
594 }
595
596 fn properties(v: &CssPropertyWithConditionsVec) -> Vec<CssProperty> {
598 v.as_ref().iter().map(|p| p.property.clone()).collect()
599 }
600
601 fn kinds(v: &CssPropertyWithConditionsVec) -> Vec<core::mem::Discriminant<CssProperty>> {
603 v.as_ref()
604 .iter()
605 .map(|p| core::mem::discriminant(&p.property))
606 .collect()
607 }
608
609 fn find<T>(
610 v: &CssPropertyWithConditionsVec,
611 f: impl Fn(&CssProperty) -> Option<T>,
612 ) -> Option<T> {
613 v.as_ref().iter().find_map(|p| f(&p.property))
614 }
615
616 fn flex_grow_of(p: &CssProperty) -> Option<f32> {
617 match p {
618 CssProperty::FlexGrow(g) => g.get_property().map(|g| g.inner.get()),
619 _ => None,
620 }
621 }
622
623 fn flex_grow_raw(p: &CssProperty) -> Option<isize> {
626 match p {
627 CssProperty::FlexGrow(g) => g.get_property().map(|g| g.inner.number()),
628 _ => None,
629 }
630 }
631
632 fn grow(v: &CssPropertyWithConditionsVec) -> Option<f32> {
633 find(v, flex_grow_of)
634 }
635
636 fn shrink(v: &CssPropertyWithConditionsVec) -> Option<f32> {
637 find(v, |p| match p {
638 CssProperty::FlexShrink(s) => s.get_property().map(|s| s.inner.get()),
639 _ => None,
640 })
641 }
642
643 fn width_pv(v: &CssPropertyWithConditionsVec) -> Option<PixelValue> {
644 find(v, |p| match p {
645 CssProperty::Width(w) => match w.get_property() {
646 Some(LayoutWidth::Px(pv)) => Some(*pv),
647 _ => None,
648 },
649 _ => None,
650 })
651 }
652
653 fn height_pv(v: &CssPropertyWithConditionsVec) -> Option<PixelValue> {
654 find(v, |p| match p {
655 CssProperty::Height(h) => match h.get_property() {
656 Some(LayoutHeight::Px(pv)) => Some(*pv),
657 _ => None,
658 },
659 _ => None,
660 })
661 }
662
663 fn flex_basis_pv(v: &CssPropertyWithConditionsVec) -> Option<PixelValue> {
664 find(v, |p| match p {
665 CssProperty::FlexBasis(b) => match b.get_property() {
666 Some(LayoutFlexBasis::Exact(pv)) => Some(*pv),
667 _ => None,
668 },
669 _ => None,
670 })
671 }
672
673 fn min_width_pv(v: &CssPropertyWithConditionsVec) -> Option<PixelValue> {
674 find(v, |p| match p {
675 CssProperty::MinWidth(w) => w.get_property().map(|w| w.inner),
676 _ => None,
677 })
678 }
679
680 fn min_height_pv(v: &CssPropertyWithConditionsVec) -> Option<PixelValue> {
681 find(v, |p| match p {
682 CssProperty::MinHeight(h) => h.get_property().map(|h| h.inner),
683 _ => None,
684 })
685 }
686
687 fn display(v: &CssPropertyWithConditionsVec) -> Option<LayoutDisplay> {
688 find(v, |p| match p {
689 CssProperty::Display(d) => d.get_property().copied(),
690 _ => None,
691 })
692 }
693
694 fn flex_direction(v: &CssPropertyWithConditionsVec) -> Option<LayoutFlexDirection> {
695 find(v, |p| match p {
696 CssProperty::FlexDirection(d) => d.get_property().copied(),
697 _ => None,
698 })
699 }
700
701 fn cursor_style(v: &CssPropertyWithConditionsVec) -> Option<StyleCursor> {
702 find(v, |p| match p {
703 CssProperty::Cursor(c) => c.get_property().copied(),
704 _ => None,
705 })
706 }
707
708 fn background_color(v: &CssPropertyWithConditionsVec) -> Option<ColorU> {
709 v.as_ref().iter().find_map(|p| match &p.property {
710 CssProperty::BackgroundContent(b) => match b.get_property()?.as_ref().first()? {
711 StyleBackgroundContent::Color(c) => Some(*c),
712 _ => None,
713 },
714 _ => None,
715 })
716 }
717
718 fn overflows(v: &CssPropertyWithConditionsVec) -> (Option<LayoutOverflow>, Option<LayoutOverflow>) {
720 (
721 find(v, |p| match p {
722 CssProperty::OverflowX(o) => o.get_property().copied(),
723 _ => None,
724 }),
725 find(v, |p| match p {
726 CssProperty::OverflowY(o) => o.get_property().copied(),
727 _ => None,
728 }),
729 )
730 }
731
732 fn px(pv: PixelValue) -> f32 {
736 assert_eq!(pv.metric, SizeMetric::Px, "expected an absolute px length");
737 pv.number.get()
738 }
739
740 fn percent(pv: PixelValue) -> f32 {
741 assert_eq!(pv.metric, SizeMetric::Percent, "expected a percentage");
742 pv.number.get()
743 }
744
745 fn dom_classes(d: &Dom) -> Vec<String> {
748 d.root
749 .get_ids_and_classes()
750 .as_ref()
751 .iter()
752 .filter_map(|c| match c {
753 IdOrClass::Class(s) => Some(s.as_str().to_string()),
754 IdOrClass::Id(_) => None,
755 })
756 .collect()
757 }
758
759 fn inline_properties(d: &Dom) -> Vec<CssProperty> {
761 d.root
762 .style
763 .iter_inline_properties()
764 .map(|(p, _)| p.clone())
765 .collect()
766 }
767
768 fn inline_grow(d: &Dom) -> Option<f32> {
769 inline_properties(d).iter().find_map(flex_grow_of)
770 }
771
772 fn child(d: &Dom, idx: usize) -> &Dom {
773 &d.children.as_ref()[idx]
774 }
775
776 fn node(idx: usize) -> DomNodeId {
779 DomNodeId {
780 dom: DomId::ROOT_ID,
781 node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(idx))),
782 }
783 }
784
785 fn node_none() -> DomNodeId {
786 DomNodeId {
787 dom: DomId::ROOT_ID,
788 node: NodeHierarchyItemId::NONE,
789 }
790 }
791
792 fn empty_layout_tree() -> LayoutTree {
793 LayoutTree {
794 nodes: Vec::new(),
795 warm: Vec::new(),
796 cold: Vec::new(),
797 root: 0,
798 dom_to_layout: BTreeMap::new(),
799 children_arena: Vec::new(),
800 children_offsets: Vec::new(),
801 subtree_needs_intrinsic: Vec::new(),
802 }
803 }
804
805 fn layout_result(styled_dom: StyledDom, boxes: &[(usize, LogicalSize)]) -> DomLayoutResult {
810 let mut lr = DomLayoutResult {
811 styled_dom,
812 layout_tree: empty_layout_tree(),
813 calculated_positions: Vec::new(),
814 viewport: LogicalRect::zero(),
815 display_list: DisplayList::default(),
816 scroll_ids: HashMap::new(),
817 scroll_id_to_node_id: HashMap::new(),
818 };
819 for (layout_index, (node_index, used)) in boxes.iter().enumerate() {
820 lr.layout_tree
821 .dom_to_layout
822 .insert(NodeId::new(*node_index), vec![layout_index]);
823 lr.layout_tree.nodes.push(LayoutNodeHot {
824 box_props: PackedBoxProps::default(),
825 dom_node_id: Some(NodeId::new(*node_index)),
826 used_size: Some(*used),
827 formatting_context: FormattingContext::Flex,
828 parent: None,
829 });
830 lr.calculated_positions.push(LogicalPosition::zero());
831 }
832 lr
833 }
834
835 fn drive<R>(
839 styled_dom: StyledDom,
840 boxes: &[(usize, LogicalSize)],
841 hit: DomNodeId,
842 cur: OptionLogicalPosition,
843 f: impl FnOnce(CallbackInfo) -> R,
844 ) -> (R, Vec<CallbackChange>) {
845 let mut layout_window =
846 LayoutWindow::new(FcFontCache::default()).expect("LayoutWindow::new failed");
847 layout_window
848 .layout_results
849 .insert(DomId::ROOT_ID, layout_result(styled_dom, boxes));
850
851 let renderer_resources = RendererResources::default();
852 let previous_window_state: Option<FullWindowState> = None;
853 let current_window_state = FullWindowState::default();
854 let gl_context = OptionGlContextPtr::None;
855 let scroll_states: BTreeMap<DomId, BTreeMap<NodeHierarchyItemId, ScrollPosition>> =
856 BTreeMap::new();
857 let window_handle = RawWindowHandle::Unsupported;
858 let system_callbacks = ExternalSystemCallbacks::rust_internal();
859
860 let ref_data = CallbackInfoRefData {
861 layout_window: &layout_window,
862 renderer_resources: &renderer_resources,
863 previous_window_state: &previous_window_state,
864 current_window_state: ¤t_window_state,
865 gl_context: &gl_context,
866 current_scroll_manager: &scroll_states,
867 current_window_handle: &window_handle,
868 system_callbacks: &system_callbacks,
869 system_style: Arc::new(SystemStyle::default()),
870 monitors: Arc::new(Mutex::new(MonitorVec::from_const_slice(&[]))),
871 #[cfg(feature = "icu")]
872 icu_localizer: IcuLocalizerHandle::default(),
873 ctx: OptionRefAny::None,
874 };
875
876 let changes: Arc<Mutex<Vec<CallbackChange>>> = Arc::new(Mutex::new(Vec::new()));
877 let info = CallbackInfo::new(
878 &ref_data,
879 &changes,
880 hit,
881 cur,
882 OptionLogicalPosition::None,
883 );
884
885 let out = f(info);
886 let recorded = core::mem::take(&mut *changes.lock().expect("change log poisoned"));
887 (out, recorded)
888 }
889
890 fn laid_out(sp: SplitPane) -> (StyledDom, RefAny) {
896 let dom = sp.dom();
897 let state = dom.root.callbacks.as_ref()[0].refany.clone();
898 (StyledDom::create_from_dom(dom), state)
899 }
900
901 fn wrapper(state: &mut RefAny) -> SplitPaneStateWrapper {
902 let guard = state
903 .downcast_ref::<SplitPaneStateWrapper>()
904 .expect("the widget state changed type");
905 (*guard).clone()
906 }
907
908 fn css_changes(changes: &[CallbackChange]) -> Vec<(NodeId, f32)> {
910 changes
911 .iter()
912 .filter_map(|c| match c {
913 CallbackChange::ChangeNodeCssProperties {
914 node_id, properties, ..
915 } => properties
916 .as_ref()
917 .first()
918 .and_then(flex_grow_of)
919 .map(|g| (*node_id, g)),
920 _ => None,
921 })
922 .collect()
923 }
924
925 #[derive(Debug, Default)]
927 struct ResizeLog {
928 seen: Vec<SplitPaneState>,
929 }
930
931 extern "C" fn record_resize(
932 mut data: RefAny,
933 _info: CallbackInfo,
934 state: SplitPaneState,
935 ) -> Update {
936 if let Some(mut log) = data.downcast_mut::<ResizeLog>() {
937 log.seen.push(state);
938 }
939 Update::RefreshDom
940 }
941
942 extern "C" fn resize_do_nothing(
943 _data: RefAny,
944 _info: CallbackInfo,
945 _state: SplitPaneState,
946 ) -> Update {
947 Update::DoNothing
948 }
949
950 fn logged(log: &mut RefAny) -> Vec<SplitPaneState> {
951 let guard = log.downcast_ref::<ResizeLog>().expect("payload type");
952 guard.seen.clone()
953 }
954
955 #[test]
960 fn flex_grow_prop_round_trips_the_representative_ratios() {
961 for v in IN_RANGE_RATIOS {
962 let got = flex_grow_of(&flex_grow_prop(v)).expect("flex-grow declaration");
963 assert!((got - v).abs() < 1e-6, "flex-grow({v}) decoded as {got}");
964 }
965 }
966
967 #[test]
968 fn flex_grow_prop_zero_is_exactly_zero() {
969 assert_eq!(flex_grow_raw(&flex_grow_prop(0.0)), Some(0));
970 assert_eq!(flex_grow_of(&flex_grow_prop(0.0)), Some(0.0));
971 assert_eq!(flex_grow_raw(&flex_grow_prop(-0.0)), Some(0));
973 }
974
975 #[test]
976 fn flex_grow_prop_quantizes_to_three_decimals_by_truncation() {
977 assert_eq!(flex_grow_raw(&flex_grow_prop(1.0 / 3.0)), Some(333));
979 assert_eq!(flex_grow_raw(&flex_grow_prop(2.0 / 3.0)), Some(666));
980 assert_eq!(flex_grow_raw(&flex_grow_prop(0.0009)), Some(0));
983 assert_eq!(flex_grow_of(&flex_grow_prop(0.0009)), Some(0.0));
984 }
985
986 #[test]
987 fn flex_grow_prop_nan_silently_becomes_zero_not_a_panic() {
988 assert_eq!(flex_grow_raw(&flex_grow_prop(f32::NAN)), Some(0));
991 assert_eq!(flex_grow_of(&flex_grow_prop(f32::NAN)), Some(0.0));
992 }
993
994 #[test]
995 fn flex_grow_prop_infinities_saturate_to_the_isize_bounds() {
996 assert_eq!(flex_grow_raw(&flex_grow_prop(f32::INFINITY)), Some(isize::MAX));
997 assert_eq!(
998 flex_grow_raw(&flex_grow_prop(f32::NEG_INFINITY)),
999 Some(isize::MIN)
1000 );
1001 for v in [f32::INFINITY, f32::NEG_INFINITY] {
1003 let got = flex_grow_of(&flex_grow_prop(v)).expect("flex-grow declaration");
1004 assert!(got.is_finite(), "flex-grow({v}) decoded as {got}");
1005 }
1006 }
1007
1008 #[test]
1009 fn flex_grow_prop_f32_extremes_do_not_panic() {
1010 for v in [
1011 f32::MAX,
1012 f32::MIN,
1013 f32::MIN_POSITIVE,
1014 -f32::MIN_POSITIVE,
1015 1.0e30,
1016 -1.0e30,
1017 f32::EPSILON,
1018 ] {
1019 let got = flex_grow_of(&flex_grow_prop(v)).expect("flex-grow declaration");
1020 assert!(!got.is_nan(), "flex-grow({v}) decoded as NaN");
1021 }
1022 assert_eq!(flex_grow_raw(&flex_grow_prop(f32::MAX)), Some(isize::MAX));
1025 assert_eq!(flex_grow_raw(&flex_grow_prop(f32::MIN)), Some(isize::MIN));
1026 }
1027
1028 #[test]
1029 fn flex_grow_prop_passes_negative_values_through_unclamped() {
1030 assert_eq!(flex_grow_raw(&flex_grow_prop(-1.0)), Some(-1000));
1032 assert_eq!(flex_grow_of(&flex_grow_prop(-1.0)), Some(-1.0));
1033 assert_eq!(flex_grow_of(&flex_grow_prop(-0.5)), Some(-0.5));
1034 }
1035
1036 #[test]
1037 fn flex_grow_prop_is_deterministic() {
1038 for v in [0.0, 0.5, -1.0, f32::NAN, f32::INFINITY] {
1039 assert_eq!(
1040 flex_grow_raw(&flex_grow_prop(v)),
1041 flex_grow_raw(&flex_grow_prop(v))
1042 );
1043 }
1044 }
1045
1046 #[test]
1051 fn main_axis_picks_x_for_horizontal_and_y_for_vertical() {
1052 let p = pos(3.0, 7.0);
1054 assert_eq!(main_axis(SplitDirection::Horizontal, p), 3.0);
1055 assert_eq!(main_axis(SplitDirection::Vertical, p), 7.0);
1056 }
1057
1058 #[test]
1059 fn main_axis_propagates_nan_and_infinities_verbatim() {
1060 assert!(main_axis(SplitDirection::Horizontal, pos(f32::NAN, 0.0)).is_nan());
1061 assert!(main_axis(SplitDirection::Vertical, pos(0.0, f32::NAN)).is_nan());
1062 assert_eq!(
1063 main_axis(SplitDirection::Horizontal, pos(f32::INFINITY, 0.0)),
1064 f32::INFINITY
1065 );
1066 assert_eq!(
1067 main_axis(SplitDirection::Vertical, pos(0.0, f32::NEG_INFINITY)),
1068 f32::NEG_INFINITY
1069 );
1070 assert_eq!(main_axis(SplitDirection::Horizontal, pos(5.0, f32::NAN)), 5.0);
1072 assert_eq!(main_axis(SplitDirection::Vertical, pos(f32::NAN, 5.0)), 5.0);
1073 }
1074
1075 #[test]
1076 fn main_axis_extreme_finite_values_are_exact() {
1077 for v in [0.0, -0.0, f32::MAX, f32::MIN, 1.0e30, -1.0e30, f32::EPSILON] {
1078 assert_eq!(main_axis(SplitDirection::Horizontal, pos(v, 1.0)), v);
1079 assert_eq!(main_axis(SplitDirection::Vertical, pos(1.0, v)), v);
1080 }
1081 }
1082
1083 #[test]
1088 fn main_size_picks_width_for_horizontal_and_height_for_vertical() {
1089 let s = size(11.0, 22.0);
1090 assert_eq!(main_size(SplitDirection::Horizontal, s), 11.0);
1091 assert_eq!(main_size(SplitDirection::Vertical, s), 22.0);
1092 }
1093
1094 #[test]
1095 fn main_size_zero_and_negative_pass_straight_through() {
1096 assert_eq!(main_size(SplitDirection::Horizontal, size(0.0, 5.0)), 0.0);
1098 assert_eq!(main_size(SplitDirection::Vertical, size(5.0, 0.0)), 0.0);
1099 assert_eq!(main_size(SplitDirection::Horizontal, size(-40.0, 5.0)), -40.0);
1100 assert_eq!(main_size(SplitDirection::Vertical, size(5.0, -40.0)), -40.0);
1101 }
1102
1103 #[test]
1104 fn main_size_nan_and_infinities_do_not_panic() {
1105 assert!(main_size(SplitDirection::Horizontal, size(f32::NAN, 1.0)).is_nan());
1106 assert!(main_size(SplitDirection::Vertical, size(1.0, f32::NAN)).is_nan());
1107 assert_eq!(
1108 main_size(SplitDirection::Horizontal, size(f32::INFINITY, 1.0)),
1109 f32::INFINITY
1110 );
1111 assert_eq!(
1112 main_size(SplitDirection::Vertical, size(1.0, f32::NEG_INFINITY)),
1113 f32::NEG_INFINITY
1114 );
1115 }
1116
1117 #[test]
1118 fn main_size_nan_slips_past_the_non_positive_guard_the_callers_use() {
1119 let msize = main_size(SplitDirection::Horizontal, size(f32::NAN, 1.0));
1123 let caught_by_the_guard = msize <= 0.0;
1126 assert!(
1127 !caught_by_the_guard,
1128 "NaN must not be caught by the <= 0 guard"
1129 );
1130 }
1131
1132 #[test]
1133 fn main_size_extreme_finite_values_are_exact() {
1134 for v in [f32::MAX, f32::MIN, f32::MIN_POSITIVE, 1.0e30, -1.0e30] {
1135 assert_eq!(main_size(SplitDirection::Horizontal, size(v, 1.0)), v);
1136 assert_eq!(main_size(SplitDirection::Vertical, size(1.0, v)), v);
1137 }
1138 }
1139
1140 #[test]
1145 fn container_style_declares_a_full_size_flex_box() {
1146 for dir in BOTH_DIRECTIONS {
1147 let s = container_style(dir);
1148 assert_eq!(properties(&s).len(), 7, "{dir:?}");
1149 assert_eq!(display(&s), Some(LayoutDisplay::Flex), "{dir:?}");
1150 assert_eq!(grow(&s), Some(1.0), "{dir:?}");
1151 assert_eq!(percent(width_pv(&s).expect("width")), 100.0, "{dir:?}");
1152 assert_eq!(percent(height_pv(&s).expect("height")), 100.0, "{dir:?}");
1153 assert_eq!(
1154 overflows(&s),
1155 (Some(LayoutOverflow::Hidden), Some(LayoutOverflow::Hidden)),
1156 "{dir:?}"
1157 );
1158 }
1159 }
1160
1161 #[test]
1162 fn container_style_direction_only_changes_the_flex_direction() {
1163 let h = container_style(SplitDirection::Horizontal);
1164 let v = container_style(SplitDirection::Vertical);
1165 assert_eq!(
1166 flex_direction(&h),
1167 Some(LayoutFlexDirection::Row),
1168 "horizontal splits lay the panes out left/right"
1169 );
1170 assert_eq!(
1171 flex_direction(&v),
1172 Some(LayoutFlexDirection::Column),
1173 "vertical splits lay the panes out top/bottom"
1174 );
1175 assert_eq!(kinds(&h), kinds(&v));
1177 let (ph, pv) = (properties(&h), properties(&v));
1178 let differing = ph
1179 .iter()
1180 .zip(pv.iter())
1181 .filter(|(a, b)| a != b)
1182 .count();
1183 assert_eq!(differing, 1, "exactly one declaration may depend on the axis");
1184 }
1185
1186 #[test]
1187 fn container_style_is_deterministic() {
1188 for dir in BOTH_DIRECTIONS {
1189 assert_eq!(properties(&container_style(dir)), properties(&container_style(dir)));
1190 }
1191 }
1192
1193 #[test]
1198 fn pane_style_declares_the_same_six_properties_for_every_grow() {
1199 let reference = kinds(&pane_style(0.5));
1200 assert_eq!(reference.len(), 6);
1201 for g in [
1202 0.0,
1203 1.0,
1204 -1.0,
1205 f32::NAN,
1206 f32::INFINITY,
1207 f32::NEG_INFINITY,
1208 f32::MAX,
1209 f32::MIN,
1210 1.0e30,
1211 ] {
1212 assert_eq!(kinds(&pane_style(g)), reference, "grow = {g}");
1213 }
1214 }
1215
1216 #[test]
1217 fn pane_style_basis_and_minimums_are_zero_px() {
1218 let s = pane_style(0.5);
1219 assert_eq!(px(flex_basis_pv(&s).expect("flex-basis")), 0.0);
1222 assert_eq!(px(min_width_pv(&s).expect("min-width")), 0.0);
1223 assert_eq!(px(min_height_pv(&s).expect("min-height")), 0.0);
1224 assert_eq!(
1225 overflows(&s),
1226 (Some(LayoutOverflow::Hidden), Some(LayoutOverflow::Hidden))
1227 );
1228 }
1229
1230 #[test]
1231 fn pane_style_carries_the_grow_through_verbatim() {
1232 for g in IN_RANGE_RATIOS {
1233 let got = grow(&pane_style(g)).expect("flex-grow");
1234 assert!((got - g).abs() < 1e-6, "pane_style({g}) declared {got}");
1235 }
1236 assert_eq!(grow(&pane_style(0.0)), Some(0.0));
1237 assert_eq!(grow(&pane_style(1.0)), Some(1.0));
1238 }
1239
1240 #[test]
1241 fn pane_style_complementary_grows_sum_to_one() {
1242 for r in IN_RANGE_RATIOS {
1245 let a = grow(&pane_style(r)).expect("first pane grow");
1246 let b = grow(&pane_style(1.0 - r)).expect("second pane grow");
1247 assert!(
1248 (a + b - 1.0).abs() < 3e-3,
1249 "ratio {r}: {a} + {b} does not fill the container"
1250 );
1251 }
1252 }
1253
1254 #[test]
1255 fn pane_style_nan_grow_collapses_the_pane_instead_of_panicking() {
1256 assert_eq!(grow(&pane_style(f32::NAN)), Some(0.0));
1260 assert_eq!(grow(&pane_style(1.0 - f32::NAN)), Some(0.0));
1261 }
1262
1263 #[test]
1264 fn pane_style_extreme_grows_do_not_panic() {
1265 for g in [f32::INFINITY, f32::NEG_INFINITY, f32::MAX, f32::MIN, -1.0e30] {
1266 let got = grow(&pane_style(g)).expect("flex-grow");
1267 assert!(got.is_finite(), "pane_style({g}) declared {got}");
1268 }
1269 }
1270
1271 #[test]
1276 fn divider_style_horizontal_is_a_fixed_width_col_resize_bar() {
1277 let s = divider_style(SplitDirection::Horizontal);
1278 assert_eq!(px(width_pv(&s).expect("width")), DIVIDER_THICKNESS as f32);
1279 assert_eq!(cursor_style(&s), Some(StyleCursor::ColResize));
1280 assert!(height_pv(&s).is_none(), "vertical size must stay unset");
1282 }
1283
1284 #[test]
1285 fn divider_style_vertical_is_a_fixed_height_row_resize_bar() {
1286 let s = divider_style(SplitDirection::Vertical);
1287 assert_eq!(px(height_pv(&s).expect("height")), DIVIDER_THICKNESS as f32);
1288 assert_eq!(cursor_style(&s), Some(StyleCursor::RowResize));
1289 assert!(width_pv(&s).is_none(), "horizontal size must stay unset");
1290 }
1291
1292 #[test]
1293 fn divider_style_never_grows_or_shrinks_and_is_visible() {
1294 for dir in BOTH_DIRECTIONS {
1295 let s = divider_style(dir);
1296 assert_eq!(properties(&s).len(), 5, "{dir:?}");
1297 assert_eq!(grow(&s), Some(0.0), "{dir:?}");
1300 assert_eq!(shrink(&s), Some(0.0), "{dir:?}");
1301 assert_eq!(background_color(&s), Some(DIVIDER_COLOR), "{dir:?}");
1302 }
1303 }
1304
1305 #[test]
1306 fn divider_style_axes_never_agree() {
1307 let h = divider_style(SplitDirection::Horizontal);
1308 let v = divider_style(SplitDirection::Vertical);
1309 assert_ne!(cursor_style(&h), cursor_style(&v));
1310 assert_ne!(properties(&h), properties(&v));
1311 }
1312
1313 #[test]
1318 fn create_starts_centred_idle_and_hookless() {
1319 for dir in BOTH_DIRECTIONS {
1320 let sp = plain(dir);
1321 assert_eq!(sp.split_pane_state.inner.direction, dir);
1322 assert_eq!(sp.split_pane_state.inner.ratio, 0.5);
1323 assert!(!sp.split_pane_state.is_dragging, "{dir:?}");
1324 assert_eq!(sp.split_pane_state.drag_start_px, 0.0, "{dir:?}");
1325 assert_eq!(sp.split_pane_state.ratio_at_drag_start, 0.0, "{dir:?}");
1326 assert!(sp.split_pane_state.on_resize.is_none(), "{dir:?}");
1327 assert_eq!(
1328 properties(&sp.container_style),
1329 properties(&container_style(dir)),
1330 "{dir:?}"
1331 );
1332 }
1333 }
1334
1335 #[test]
1336 fn default_split_pane_matches_a_horizontal_create() {
1337 let d = SplitPane::default();
1338 let c = plain(SplitDirection::Horizontal);
1339 assert_eq!(d.split_pane_state, c.split_pane_state);
1340 assert_eq!(properties(&d.container_style), properties(&c.container_style));
1341 }
1342
1343 #[test]
1344 fn create_keeps_the_children_in_first_second_order() {
1345 let sp = pane(div_with_class("alpha"), div_with_class("beta"));
1346 assert_eq!(dom_classes(&sp.first), vec!["alpha".to_string()]);
1347 assert_eq!(dom_classes(&sp.second), vec!["beta".to_string()]);
1348 }
1349
1350 #[test]
1355 fn set_ratio_keeps_in_range_values_verbatim() {
1356 for r in IN_RANGE_RATIOS {
1357 let mut sp = plain(SplitDirection::Horizontal);
1358 sp.set_ratio(r);
1359 assert_eq!(sp.split_pane_state.inner.ratio, r);
1360 }
1361 }
1362
1363 #[test]
1364 fn set_ratio_clamps_everything_out_of_range_into_min_max() {
1365 let below = [
1366 0.0_f32,
1367 -0.0,
1368 -1.0,
1369 0.049_999,
1370 f32::MIN,
1371 f32::NEG_INFINITY,
1372 -1.0e30,
1373 ];
1374 let above = [1.0_f32, 2.0, 0.950_001, f32::MAX, f32::INFINITY, 1.0e30];
1375 for r in below {
1376 let mut sp = plain(SplitDirection::Horizontal);
1377 sp.set_ratio(r);
1378 assert_eq!(
1379 sp.split_pane_state.inner.ratio, MIN_RATIO,
1380 "{r} must clamp up to MIN_RATIO"
1381 );
1382 }
1383 for r in above {
1384 let mut sp = plain(SplitDirection::Horizontal);
1385 sp.set_ratio(r);
1386 assert_eq!(
1387 sp.split_pane_state.inner.ratio, MAX_RATIO,
1388 "{r} must clamp down to MAX_RATIO"
1389 );
1390 }
1391 }
1392
1393 #[test]
1394 fn set_ratio_nan_escapes_the_documented_clamp() {
1395 let mut sp = plain(SplitDirection::Horizontal);
1402 sp.set_ratio(f32::NAN);
1403 assert!(
1404 sp.split_pane_state.inner.ratio.is_nan(),
1405 "NaN is currently stored as-is"
1406 );
1407 }
1408
1409 #[test]
1410 fn set_ratio_is_idempotent_and_a_projection() {
1411 for r in [-5.0_f32, 0.0, 0.3, 0.5, 0.95, 7.0, f32::INFINITY] {
1412 let mut once = plain(SplitDirection::Horizontal);
1413 once.set_ratio(r);
1414 let settled = once.split_pane_state.inner.ratio;
1415 once.set_ratio(settled);
1416 assert_eq!(once.split_pane_state.inner.ratio, settled, "input {r}");
1417 assert!(
1418 (MIN_RATIO..=MAX_RATIO).contains(&settled),
1419 "input {r} settled outside the documented range at {settled}"
1420 );
1421 }
1422 }
1423
1424 #[test]
1425 fn set_ratio_touches_nothing_but_the_ratio() {
1426 let mut sp = plain(SplitDirection::Vertical);
1427 let before = properties(&sp.container_style);
1428 sp.set_ratio(0.2);
1429 assert_eq!(sp.split_pane_state.inner.direction, SplitDirection::Vertical);
1430 assert!(!sp.split_pane_state.is_dragging);
1431 assert_eq!(properties(&sp.container_style), before);
1432 }
1433
1434 #[test]
1435 fn with_ratio_agrees_with_set_ratio_on_every_input() {
1436 for r in [
1437 -1.0_f32,
1438 0.0,
1439 0.05,
1440 0.5,
1441 0.95,
1442 1.0,
1443 f32::MAX,
1444 f32::NEG_INFINITY,
1445 ] {
1446 let built = plain(SplitDirection::Horizontal).with_ratio(r);
1447 let mut mutated = plain(SplitDirection::Horizontal);
1448 mutated.set_ratio(r);
1449 assert_eq!(
1450 built.split_pane_state.inner.ratio,
1451 mutated.split_pane_state.inner.ratio,
1452 "input {r}"
1453 );
1454 }
1455 }
1456
1457 #[test]
1458 fn with_ratio_preserves_the_rest_of_the_widget() {
1459 let sp = SplitPane::create(
1460 SplitDirection::Vertical,
1461 div_with_class("alpha"),
1462 div_with_class("beta"),
1463 )
1464 .with_ratio(0.25);
1465 assert_eq!(sp.split_pane_state.inner.ratio, 0.25);
1466 assert_eq!(sp.split_pane_state.inner.direction, SplitDirection::Vertical);
1467 assert_eq!(dom_classes(&sp.first), vec!["alpha".to_string()]);
1468 assert_eq!(dom_classes(&sp.second), vec!["beta".to_string()]);
1469 assert_eq!(
1470 properties(&sp.container_style),
1471 properties(&container_style(SplitDirection::Vertical))
1472 );
1473 }
1474
1475 #[test]
1480 fn set_direction_updates_both_the_state_and_the_container_style() {
1481 let mut sp = plain(SplitDirection::Horizontal);
1482 sp.set_direction(SplitDirection::Vertical);
1483 assert_eq!(sp.split_pane_state.inner.direction, SplitDirection::Vertical);
1484 assert_eq!(
1485 flex_direction(&sp.container_style),
1486 Some(LayoutFlexDirection::Column),
1487 "a stale Row here would lay a vertical split out sideways"
1488 );
1489 }
1490
1491 #[test]
1492 fn set_direction_is_idempotent_and_round_trips() {
1493 let mut sp = plain(SplitDirection::Horizontal);
1494 let original = properties(&sp.container_style);
1495 sp.set_direction(SplitDirection::Vertical);
1496 sp.set_direction(SplitDirection::Vertical);
1497 assert_eq!(
1498 properties(&sp.container_style),
1499 properties(&container_style(SplitDirection::Vertical))
1500 );
1501 sp.set_direction(SplitDirection::Horizontal);
1502 assert_eq!(properties(&sp.container_style), original);
1503 }
1504
1505 #[test]
1506 fn set_direction_discards_a_custom_container_style() {
1507 let sp = plain(SplitDirection::Horizontal)
1510 .with_container_style(CssPropertyWithConditionsVec::from_vec(vec![]))
1511 .with_direction(SplitDirection::Vertical);
1512 assert_eq!(
1513 properties(&sp.container_style),
1514 properties(&container_style(SplitDirection::Vertical)),
1515 "set_direction overwrites, it does not merge"
1516 );
1517 }
1518
1519 #[test]
1520 fn with_direction_leaves_the_ratio_and_children_alone() {
1521 let sp = SplitPane::create(
1522 SplitDirection::Horizontal,
1523 div_with_class("alpha"),
1524 div_with_class("beta"),
1525 )
1526 .with_ratio(0.3)
1527 .with_direction(SplitDirection::Vertical);
1528 assert_eq!(sp.split_pane_state.inner.ratio, 0.3);
1529 assert_eq!(dom_classes(&sp.first), vec!["alpha".to_string()]);
1530 assert_eq!(dom_classes(&sp.second), vec!["beta".to_string()]);
1531 }
1532
1533 #[test]
1538 fn with_container_style_replaces_the_default_verbatim() {
1539 let custom = CssPropertyWithConditionsVec::from_vec(vec![
1540 CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Block)),
1541 ]);
1542 let sp = plain(SplitDirection::Horizontal).with_container_style(custom.clone());
1543 assert_eq!(properties(&sp.container_style), properties(&custom));
1544 assert_eq!(display(&sp.container_style), Some(LayoutDisplay::Block));
1545 }
1546
1547 #[test]
1548 fn with_container_style_accepts_an_empty_vec() {
1549 let sp = plain(SplitDirection::Horizontal)
1550 .with_container_style(CssPropertyWithConditionsVec::from_vec(vec![]));
1551 assert!(properties(&sp.container_style).is_empty());
1552 assert_eq!(sp.split_pane_state.inner.ratio, 0.5);
1555 let dom = sp.dom();
1556 assert_eq!(dom.children.as_ref().len(), 3);
1557 }
1558
1559 #[test]
1564 fn swap_with_default_returns_the_old_value_and_leaves_a_default() {
1565 let mut sp = SplitPane::create(
1566 SplitDirection::Vertical,
1567 div_with_class("alpha"),
1568 div_with_class("beta"),
1569 )
1570 .with_ratio(0.8);
1571 let old = sp.swap_with_default();
1572
1573 assert_eq!(old.split_pane_state.inner.ratio, 0.8);
1574 assert_eq!(old.split_pane_state.inner.direction, SplitDirection::Vertical);
1575 assert_eq!(dom_classes(&old.first), vec!["alpha".to_string()]);
1576 assert_eq!(dom_classes(&old.second), vec!["beta".to_string()]);
1577
1578 assert_eq!(sp.split_pane_state, SplitPaneStateWrapper::default());
1579 assert!(dom_classes(&sp.first).is_empty());
1580 assert_eq!(
1581 properties(&sp.container_style),
1582 properties(&container_style(SplitDirection::Horizontal))
1583 );
1584 }
1585
1586 #[test]
1587 fn swap_with_default_moves_the_on_resize_hook_out() {
1588 let log = RefAny::new(ResizeLog::default());
1589 let mut sp = plain(SplitDirection::Horizontal)
1590 .with_on_resize(log, record_resize as SplitPaneOnResizeCallbackType);
1591 let old = sp.swap_with_default();
1592 assert!(old.split_pane_state.on_resize.is_some());
1593 assert!(sp.split_pane_state.on_resize.is_none());
1594 }
1595
1596 #[test]
1597 fn swap_with_default_twice_is_stable() {
1598 let mut sp = plain(SplitDirection::Vertical).with_ratio(0.1);
1599 let _first = sp.swap_with_default();
1600 let second = sp.swap_with_default();
1601 assert_eq!(second.split_pane_state, SplitPaneStateWrapper::default());
1602 assert_eq!(sp.split_pane_state, SplitPaneStateWrapper::default());
1603 }
1604
1605 #[test]
1610 fn set_on_resize_installs_the_hook_and_keeps_the_payload() {
1611 let mut log = RefAny::new(ResizeLog::default());
1612 let mut sp = plain(SplitDirection::Horizontal);
1613 sp.set_on_resize(log.clone(), record_resize as SplitPaneOnResizeCallbackType);
1614 let hook = sp.split_pane_state.on_resize.as_ref().expect("hook");
1615 assert_eq!(hook.callback.cb as usize, record_resize as usize);
1616 assert!(logged(&mut log).is_empty());
1618 }
1619
1620 #[test]
1621 fn set_on_resize_replaces_a_previous_hook() {
1622 let mut sp = plain(SplitDirection::Horizontal);
1623 sp.set_on_resize(
1624 RefAny::new(ResizeLog::default()),
1625 record_resize as SplitPaneOnResizeCallbackType,
1626 );
1627 sp.set_on_resize(
1628 RefAny::new(ResizeLog::default()),
1629 resize_do_nothing as SplitPaneOnResizeCallbackType,
1630 );
1631 let hook = sp.split_pane_state.on_resize.as_ref().expect("hook");
1632 assert_eq!(hook.callback.cb as usize, resize_do_nothing as usize);
1633 }
1634
1635 #[test]
1636 fn with_on_resize_matches_set_on_resize() {
1637 let built = plain(SplitDirection::Horizontal).with_on_resize(
1638 RefAny::new(ResizeLog::default()),
1639 record_resize as SplitPaneOnResizeCallbackType,
1640 );
1641 let mut mutated = plain(SplitDirection::Horizontal);
1642 mutated.set_on_resize(
1643 RefAny::new(ResizeLog::default()),
1644 record_resize as SplitPaneOnResizeCallbackType,
1645 );
1646 assert_eq!(
1647 built.split_pane_state.on_resize.as_ref().map(|h| h.callback.cb as usize),
1648 mutated.split_pane_state.on_resize.as_ref().map(|h| h.callback.cb as usize),
1649 );
1650 assert_eq!(built.split_pane_state.inner, mutated.split_pane_state.inner);
1652 }
1653
1654 #[test]
1659 fn dom_is_first_pane_divider_second_pane_in_that_order() {
1660 let dom = plain(SplitDirection::Horizontal).dom();
1661 assert_eq!(dom_classes(&dom), vec!["__azul-native-split-pane".to_string()]);
1662 assert_eq!(dom.root.get_tab_index(), Some(TabIndex::Auto));
1663 let children = dom.children.as_ref();
1664 assert_eq!(children.len(), 3);
1665 assert_eq!(
1666 dom_classes(&children[0]),
1667 vec!["__azul-native-split-pane-first".to_string()]
1668 );
1669 assert_eq!(
1670 dom_classes(&children[1]),
1671 vec!["__azul-native-split-pane-divider".to_string()]
1672 );
1673 assert_eq!(
1674 dom_classes(&children[2]),
1675 vec!["__azul-native-split-pane-second".to_string()]
1676 );
1677 }
1678
1679 #[test]
1680 fn dom_wraps_the_user_children_one_per_pane() {
1681 let dom = pane(div_with_class("alpha"), div_with_class("beta")).dom();
1682 let first = child(&dom, 0);
1683 let second = child(&dom, 2);
1684 assert_eq!(first.children.as_ref().len(), 1);
1685 assert_eq!(second.children.as_ref().len(), 1);
1686 assert_eq!(dom_classes(child(first, 0)), vec!["alpha".to_string()]);
1687 assert_eq!(dom_classes(child(second, 0)), vec!["beta".to_string()]);
1688 assert!(child(&dom, 1).children.as_ref().is_empty());
1691 }
1692
1693 #[test]
1694 fn dom_pane_grows_are_complementary_for_every_ratio() {
1695 for r in IN_RANGE_RATIOS {
1696 let dom = plain(SplitDirection::Horizontal).with_ratio(r).dom();
1697 let a = inline_grow(child(&dom, 0)).expect("first pane grow");
1698 let b = inline_grow(child(&dom, 2)).expect("second pane grow");
1699 assert!((a - r).abs() < 2e-3, "ratio {r}: first pane got {a}");
1700 assert!((a + b - 1.0).abs() < 3e-3, "ratio {r}: {a} + {b} != 1");
1701 }
1702 }
1703
1704 #[test]
1705 fn dom_clamped_extremes_still_leave_both_panes_visible() {
1706 for r in [-100.0_f32, 0.0, 1.0, f32::INFINITY] {
1707 let dom = plain(SplitDirection::Horizontal).with_ratio(r).dom();
1708 let a = inline_grow(child(&dom, 0)).expect("first pane grow");
1709 let b = inline_grow(child(&dom, 2)).expect("second pane grow");
1710 assert!(a > 0.0 && b > 0.0, "ratio {r} collapsed a pane ({a}, {b})");
1711 assert!((a + b - 1.0).abs() < 3e-3, "ratio {r}: {a} + {b} != 1");
1712 }
1713 }
1714
1715 #[test]
1716 fn dom_with_a_nan_ratio_collapses_both_panes() {
1717 let dom = plain(SplitDirection::Horizontal).with_ratio(f32::NAN).dom();
1720 assert_eq!(inline_grow(child(&dom, 0)), Some(0.0));
1721 assert_eq!(inline_grow(child(&dom, 2)), Some(0.0));
1722 }
1723
1724 #[test]
1725 fn dom_divider_matches_divider_style_for_the_direction() {
1726 for dir in BOTH_DIRECTIONS {
1727 let dom = plain(dir).dom();
1728 assert_eq!(
1729 inline_properties(child(&dom, 1)),
1730 properties(÷r_style(dir)),
1731 "{dir:?}"
1732 );
1733 }
1734 }
1735
1736 #[test]
1737 fn dom_container_keeps_the_configured_style() {
1738 for dir in BOTH_DIRECTIONS {
1739 let dom = plain(dir).dom();
1740 assert_eq!(
1741 inline_properties(&dom),
1742 properties(&container_style(dir)),
1743 "{dir:?}"
1744 );
1745 }
1746 }
1747
1748 #[test]
1749 fn dom_registers_every_pointer_event_on_the_container() {
1750 let dom = plain(SplitDirection::Horizontal).dom();
1751 let wired: Vec<(EventFilter, usize)> = dom
1752 .root
1753 .callbacks
1754 .as_ref()
1755 .iter()
1756 .map(|c| (c.event, c.callback.cb))
1757 .collect();
1758 let expected: Vec<(EventFilter, usize)> = vec![
1759 (
1760 EventFilter::Hover(HoverEventFilter::MouseDown),
1761 on_split_pointer_down as usize,
1762 ),
1763 (
1764 EventFilter::Hover(HoverEventFilter::MouseOver),
1765 on_split_pointer_move as usize,
1766 ),
1767 (
1768 EventFilter::Hover(HoverEventFilter::MouseUp),
1769 on_split_pointer_up as usize,
1770 ),
1771 (
1772 EventFilter::Hover(HoverEventFilter::MouseLeave),
1773 on_split_pointer_up as usize,
1774 ),
1775 (
1776 EventFilter::Hover(HoverEventFilter::TouchStart),
1777 on_split_pointer_down as usize,
1778 ),
1779 (
1780 EventFilter::Hover(HoverEventFilter::TouchMove),
1781 on_split_pointer_move as usize,
1782 ),
1783 (
1784 EventFilter::Hover(HoverEventFilter::TouchEnd),
1785 on_split_pointer_up as usize,
1786 ),
1787 ];
1788 assert_eq!(wired, expected);
1789 for i in 0..3 {
1792 assert!(
1793 child(&dom, i).root.callbacks.as_ref().is_empty(),
1794 "child {i} must not carry pointer callbacks"
1795 );
1796 }
1797 }
1798
1799 #[test]
1800 fn dom_shares_one_state_refany_across_all_callbacks() {
1801 let dom = plain(SplitDirection::Horizontal).dom();
1802 let mut first = dom.root.callbacks.as_ref()[0].refany.clone();
1803 let mut last = dom.root.callbacks.as_ref()[6].refany.clone();
1804 {
1805 let mut sp = first
1806 .downcast_mut::<SplitPaneStateWrapper>()
1807 .expect("state type");
1808 sp.is_dragging = true;
1809 sp.drag_start_px = 42.0;
1810 }
1811 let seen = wrapper(&mut last);
1812 assert!(
1813 seen.is_dragging && seen.drag_start_px == 42.0,
1814 "press/move/release must observe one shared drag state"
1815 );
1816 }
1817
1818 #[test]
1819 fn dom_carries_the_state_into_the_callback_payload() {
1820 let mut sp = plain(SplitDirection::Vertical).with_ratio(0.25);
1821 sp.set_on_resize(
1822 RefAny::new(ResizeLog::default()),
1823 record_resize as SplitPaneOnResizeCallbackType,
1824 );
1825 let (_sd, mut state) = laid_out(sp);
1826 let w = wrapper(&mut state);
1827 assert_eq!(w.inner.direction, SplitDirection::Vertical);
1828 assert_eq!(w.inner.ratio, 0.25);
1829 assert!(!w.is_dragging);
1830 assert!(w.on_resize.is_some());
1831 }
1832
1833 #[test]
1834 fn dom_of_deeply_nested_children_does_not_panic() {
1835 let mut deep = Dom::create_div();
1838 for _ in 0..200 {
1839 deep = Dom::create_div().with_children(vec![deep].into());
1840 }
1841 let (sd, _state) = laid_out(pane(deep, Dom::create_div()));
1842 assert!(sd.node_count() > 200);
1843 }
1844
1845 #[test]
1846 fn from_split_pane_for_dom_matches_dom() {
1847 let via_into: Dom = plain(SplitDirection::Vertical).with_ratio(0.3).into();
1848 let direct = plain(SplitDirection::Vertical).with_ratio(0.3).dom();
1849 assert_eq!(dom_classes(&via_into), dom_classes(&direct));
1850 assert_eq!(via_into.children.as_ref().len(), direct.children.as_ref().len());
1851 assert_eq!(inline_grow(child(&via_into, 0)), inline_grow(child(&direct, 0)));
1852 }
1853
1854 #[test]
1859 fn pointer_down_without_a_cursor_is_a_no_op() {
1860 let (sd, state) = laid_out(plain(SplitDirection::Horizontal));
1861 let (update, changes) = drive(
1862 sd,
1863 &[(0, size(200.0, 100.0))],
1864 node(0),
1865 OptionLogicalPosition::None,
1866 |info| on_split_pointer_down(state.clone(), info),
1867 );
1868 assert_eq!(update, Update::DoNothing);
1869 assert!(changes.is_empty());
1870 let mut state = state;
1871 assert!(!wrapper(&mut state).is_dragging);
1872 }
1873
1874 #[test]
1875 fn pointer_down_without_a_laid_out_rect_is_a_no_op() {
1876 let (sd, state) = laid_out(plain(SplitDirection::Horizontal));
1877 let (update, changes) = drive(sd, &[], node(0), cursor(100.0, 50.0), |info| {
1878 on_split_pointer_down(state.clone(), info)
1879 });
1880 assert_eq!(update, Update::DoNothing);
1881 assert!(changes.is_empty());
1882 let mut state = state;
1883 assert!(!wrapper(&mut state).is_dragging);
1884 }
1885
1886 #[test]
1887 fn pointer_down_on_a_zero_sized_container_is_a_no_op() {
1888 let (sd, state) = laid_out(plain(SplitDirection::Horizontal));
1889 let (update, _) = drive(
1890 sd,
1891 &[(0, size(0.0, 0.0))],
1892 node(0),
1893 cursor(0.0, 0.0),
1894 |info| on_split_pointer_down(state.clone(), info),
1895 );
1896 assert_eq!(update, Update::DoNothing);
1897 let mut state = state;
1898 assert!(
1899 !wrapper(&mut state).is_dragging,
1900 "a 0-wide container has no divider to grab"
1901 );
1902 }
1903
1904 #[test]
1905 fn pointer_down_on_a_negative_sized_container_is_a_no_op() {
1906 let (sd, state) = laid_out(plain(SplitDirection::Horizontal));
1907 let (update, _) = drive(
1908 sd,
1909 &[(0, size(-200.0, -100.0))],
1910 node(0),
1911 cursor(-100.0, -50.0),
1912 |info| on_split_pointer_down(state.clone(), info),
1913 );
1914 assert_eq!(update, Update::DoNothing);
1915 let mut state = state;
1916 assert!(!wrapper(&mut state).is_dragging);
1917 }
1918
1919 #[test]
1920 fn pointer_down_records_the_anchor_when_it_lands_on_the_divider() {
1921 let (sd, state) = laid_out(plain(SplitDirection::Horizontal).with_ratio(0.25));
1922 let (update, changes) = drive(
1924 sd,
1925 &[(0, size(200.0, 100.0))],
1926 node(0),
1927 cursor(52.0, 10.0),
1928 |info| on_split_pointer_down(state.clone(), info),
1929 );
1930 assert_eq!(update, Update::DoNothing, "the press itself never redraws");
1931 assert!(changes.is_empty(), "the press must not touch the DOM");
1932 let mut state = state;
1933 let w = wrapper(&mut state);
1934 assert!(w.is_dragging);
1935 assert_eq!(w.drag_start_px, 52.0);
1936 assert_eq!(w.ratio_at_drag_start, 0.25);
1937 assert_eq!(w.inner.ratio, 0.25, "the press must not move the divider");
1938 }
1939
1940 #[test]
1941 fn pointer_down_far_from_the_divider_leaves_the_press_to_the_pane() {
1942 let (sd, state) = laid_out(plain(SplitDirection::Horizontal));
1943 let (update, _) = drive(
1944 sd,
1945 &[(0, size(200.0, 100.0))],
1946 node(0),
1947 cursor(10.0, 50.0),
1948 |info| on_split_pointer_down(state.clone(), info),
1949 );
1950 assert_eq!(update, Update::DoNothing);
1951 let mut state = state;
1952 assert!(!wrapper(&mut state).is_dragging);
1953 }
1954
1955 #[test]
1956 fn pointer_down_grabs_exactly_within_the_threshold() {
1957 for (x, expected) in [
1960 (100.0_f32, true),
1961 (109.0, true),
1962 (91.0, true),
1963 (109.5, false),
1964 (90.5, false),
1965 (0.0, false),
1966 (200.0, false),
1967 ] {
1968 let (sd, state) = laid_out(plain(SplitDirection::Horizontal));
1969 let (_, _) = drive(
1970 sd,
1971 &[(0, size(200.0, 100.0))],
1972 node(0),
1973 cursor(x, 50.0),
1974 |info| on_split_pointer_down(state.clone(), info),
1975 );
1976 let mut state = state;
1977 assert_eq!(
1978 wrapper(&mut state).is_dragging,
1979 expected,
1980 "press at x = {x} (|{x} - 100| vs {GRAB_THRESHOLD})"
1981 );
1982 }
1983 }
1984
1985 #[test]
1986 fn pointer_down_uses_the_axis_that_matches_the_direction() {
1987 for (dir, cur, expected) in [
1990 (SplitDirection::Horizontal, (100.0, 5.0), true),
1991 (SplitDirection::Vertical, (100.0, 5.0), false),
1992 (SplitDirection::Horizontal, (5.0, 50.0), false),
1993 (SplitDirection::Vertical, (5.0, 50.0), true),
1994 ] {
1995 let (sd, state) = laid_out(plain(dir));
1996 let (_, _) = drive(
1997 sd,
1998 &[(0, size(200.0, 100.0))],
1999 node(0),
2000 cursor(cur.0, cur.1),
2001 |info| on_split_pointer_down(state.clone(), info),
2002 );
2003 let mut state = state;
2004 assert_eq!(
2005 wrapper(&mut state).is_dragging,
2006 expected,
2007 "{dir:?} press at {cur:?}"
2008 );
2009 }
2010 }
2011
2012 #[test]
2013 fn pointer_down_with_a_nan_cursor_or_size_never_grabs() {
2014 for (s, cur) in [
2015 (size(200.0, 100.0), (f32::NAN, 50.0)),
2016 (size(f32::NAN, 100.0), (100.0, 50.0)),
2017 (size(f32::INFINITY, 100.0), (100.0, 50.0)),
2018 (size(200.0, 100.0), (f32::INFINITY, 50.0)),
2019 ] {
2020 let (sd, state) = laid_out(plain(SplitDirection::Horizontal));
2021 let (update, _) = drive(sd, &[(0, s)], node(0), cursor(cur.0, cur.1), |info| {
2022 on_split_pointer_down(state.clone(), info)
2023 });
2024 assert_eq!(update, Update::DoNothing);
2025 let mut state = state;
2026 assert!(
2027 !wrapper(&mut state).is_dragging,
2028 "a non-finite comparison must fail closed, not grab"
2029 );
2030 }
2031 }
2032
2033 #[test]
2034 fn pointer_down_with_a_wrong_typed_payload_is_a_no_op() {
2035 let (sd, _state) = laid_out(plain(SplitDirection::Horizontal));
2036 let stranger = RefAny::new(0u16);
2037 let (update, changes) = drive(
2038 sd,
2039 &[(0, size(200.0, 100.0))],
2040 node(0),
2041 cursor(100.0, 50.0),
2042 |info| on_split_pointer_down(stranger.clone(), info),
2043 );
2044 assert_eq!(update, Update::DoNothing);
2045 assert!(changes.is_empty());
2046 }
2047
2048 #[test]
2049 fn pointer_down_re_anchors_on_every_press() {
2050 let (sd, state) = laid_out(plain(SplitDirection::Horizontal));
2051 let boxes = [(0, size(200.0, 100.0))];
2052 for x in [95.0_f32, 104.0] {
2053 let sd_clone = sd.clone();
2054 let (_, _) = drive(sd_clone, &boxes, node(0), cursor(x, 50.0), |info| {
2055 on_split_pointer_down(state.clone(), info)
2056 });
2057 }
2058 let mut state = state;
2059 let w = wrapper(&mut state);
2060 assert!(w.is_dragging);
2061 assert_eq!(w.drag_start_px, 104.0, "the newest press wins");
2062 }
2063
2064 fn press_then_move(
2071 sp: SplitPane,
2072 boxes: &[(usize, LogicalSize)],
2073 press: (f32, f32),
2074 to: (f32, f32),
2075 ) -> (Update, Vec<CallbackChange>, RefAny) {
2076 let (sd, state) = laid_out(sp);
2077 let down_sd = sd.clone();
2078 let (_, _) = drive(
2079 down_sd,
2080 boxes,
2081 node(0),
2082 cursor(press.0, press.1),
2083 |info| on_split_pointer_down(state.clone(), info),
2084 );
2085 let (update, changes) = drive(sd, boxes, node(0), cursor(to.0, to.1), |info| {
2086 on_split_pointer_move(state.clone(), info)
2087 });
2088 (update, changes, state)
2089 }
2090
2091 #[test]
2092 fn pointer_move_without_a_drag_changes_nothing() {
2093 let (sd, state) = laid_out(plain(SplitDirection::Horizontal));
2094 let (update, changes) = drive(
2095 sd,
2096 &[(0, size(200.0, 100.0))],
2097 node(0),
2098 cursor(180.0, 50.0),
2099 |info| on_split_pointer_move(state.clone(), info),
2100 );
2101 assert_eq!(update, Update::DoNothing);
2102 assert!(changes.is_empty(), "a hover must not resize the panes");
2103 let mut state = state;
2104 assert_eq!(wrapper(&mut state).inner.ratio, 0.5);
2105 }
2106
2107 #[test]
2108 fn pointer_move_applies_the_cursor_delta_and_resizes_both_panes() {
2109 let boxes = [(0, size(200.0, 100.0))];
2110 let (update, changes, mut state) = press_then_move(
2111 plain(SplitDirection::Horizontal),
2112 &boxes,
2113 (100.0, 50.0),
2114 (150.0, 50.0),
2115 );
2116 assert_eq!(wrapper(&mut state).inner.ratio, 0.75);
2118 assert_eq!(update, Update::DoNothing, "no hook installed");
2119
2120 let writes = css_changes(&changes);
2121 assert_eq!(writes.len(), 2, "exactly one flex-grow per pane");
2122 assert_eq!(writes[0].1, 0.75);
2123 assert_eq!(writes[1].1, 0.25);
2124 assert_ne!(writes[0].0, writes[1].0, "the two panes must be distinct nodes");
2125 }
2126
2127 #[test]
2128 fn pointer_move_writes_to_the_first_and_third_children() {
2129 let boxes = [(0, size(200.0, 100.0))];
2132 let (sd, state) = laid_out(plain(SplitDirection::Horizontal));
2133 let classes: Vec<Vec<String>> = sd
2134 .node_data
2135 .as_ref()
2136 .iter()
2137 .map(|n| {
2138 n.get_ids_and_classes()
2139 .as_ref()
2140 .iter()
2141 .filter_map(|c| match c {
2142 IdOrClass::Class(s) => Some(s.as_str().to_string()),
2143 IdOrClass::Id(_) => None,
2144 })
2145 .collect()
2146 })
2147 .collect();
2148 let down_sd = sd.clone();
2149 let (_, _) = drive(down_sd, &boxes, node(0), cursor(100.0, 50.0), |info| {
2150 on_split_pointer_down(state.clone(), info)
2151 });
2152 let (_, changes) = drive(sd, &boxes, node(0), cursor(150.0, 50.0), |info| {
2153 on_split_pointer_move(state.clone(), info)
2154 });
2155 let writes = css_changes(&changes);
2156 assert_eq!(writes.len(), 2);
2157 assert_eq!(
2158 classes[writes[0].0.index()],
2159 vec!["__azul-native-split-pane-first".to_string()],
2160 "the larger grow must land on the first pane"
2161 );
2162 assert_eq!(
2163 classes[writes[1].0.index()],
2164 vec!["__azul-native-split-pane-second".to_string()]
2165 );
2166 }
2167
2168 #[test]
2169 fn pointer_move_clamps_at_both_ends_and_never_collapses_a_pane() {
2170 let boxes = [(0, size(200.0, 100.0))];
2171 for (to_x, expected) in [(-10_000.0_f32, MIN_RATIO), (10_000.0, MAX_RATIO)] {
2172 let (_, changes, mut state) = press_then_move(
2173 plain(SplitDirection::Horizontal),
2174 &boxes,
2175 (100.0, 50.0),
2176 (to_x, 50.0),
2177 );
2178 assert_eq!(wrapper(&mut state).inner.ratio, expected, "moved to {to_x}");
2179 let writes = css_changes(&changes);
2180 assert_eq!(writes.len(), 2);
2181 assert!(writes[0].1 > 0.0 && writes[1].1 > 0.0, "moved to {to_x}");
2182 assert!((writes[0].1 + writes[1].1 - 1.0).abs() < 3e-3, "moved to {to_x}");
2183 }
2184 }
2185
2186 #[test]
2187 fn pointer_move_grows_always_sum_to_one() {
2188 let boxes = [(0, size(400.0, 100.0))];
2189 for to_x in [0.0_f32, 40.0, 133.0, 200.0, 267.0, 360.0, 400.0] {
2190 let (_, changes, _) = press_then_move(
2191 plain(SplitDirection::Horizontal),
2192 &boxes,
2193 (200.0, 50.0),
2194 (to_x, 50.0),
2195 );
2196 let writes = css_changes(&changes);
2197 assert_eq!(writes.len(), 2, "moved to {to_x}");
2198 assert!(
2200 (writes[0].1 + writes[1].1 - 1.0).abs() < 3e-3,
2201 "moved to {to_x}: {} + {} != 1",
2202 writes[0].1,
2203 writes[1].1
2204 );
2205 }
2206 }
2207
2208 #[test]
2209 fn pointer_move_is_anchor_relative_not_cursor_absolute() {
2210 let boxes = [(0, size(200.0, 100.0))];
2213 let (_, _, mut state) = press_then_move(
2214 plain(SplitDirection::Horizontal),
2215 &boxes,
2216 (108.0, 50.0),
2217 (128.0, 50.0),
2218 );
2219 let r = wrapper(&mut state).inner.ratio;
2221 assert!((r - 0.6).abs() < 1e-6, "expected 0.5 + 20/200, got {r}");
2222 }
2223
2224 #[test]
2225 fn pointer_move_back_to_the_press_point_restores_the_ratio() {
2226 let boxes = [(0, size(200.0, 100.0))];
2227 let (sd, state) = laid_out(plain(SplitDirection::Horizontal).with_ratio(0.4));
2228 let a = sd.clone();
2230 let (_, _) = drive(a, &boxes, node(0), cursor(80.0, 50.0), |info| {
2231 on_split_pointer_down(state.clone(), info)
2232 });
2233 let b = sd.clone();
2234 let (_, _) = drive(b, &boxes, node(0), cursor(160.0, 50.0), |info| {
2235 on_split_pointer_move(state.clone(), info)
2236 });
2237 let (_, changes) = drive(sd, &boxes, node(0), cursor(80.0, 50.0), |info| {
2238 on_split_pointer_move(state.clone(), info)
2239 });
2240 let mut state = state;
2241 assert_eq!(wrapper(&mut state).inner.ratio, 0.4, "the drag is not cumulative");
2242 assert_eq!(css_changes(&changes)[0].1, 0.4);
2243 }
2244
2245 #[test]
2246 fn pointer_move_uses_the_axis_that_matches_the_direction() {
2247 let boxes = [(0, size(200.0, 100.0))];
2248 let (_, _, mut state) = press_then_move(
2250 plain(SplitDirection::Vertical),
2251 &boxes,
2252 (10.0, 50.0),
2253 (999.0, 75.0),
2254 );
2255 assert_eq!(
2256 wrapper(&mut state).inner.ratio,
2257 0.75,
2258 "a vertical split must ignore horizontal cursor motion"
2259 );
2260 }
2261
2262 #[test]
2263 fn pointer_move_fires_the_hook_with_the_new_state_and_returns_its_update() {
2264 let boxes = [(0, size(200.0, 100.0))];
2265 let mut log = RefAny::new(ResizeLog::default());
2266 let sp = plain(SplitDirection::Horizontal)
2267 .with_on_resize(log.clone(), record_resize as SplitPaneOnResizeCallbackType);
2268 let (update, _, _) = press_then_move(sp, &boxes, (100.0, 50.0), (150.0, 50.0));
2269 assert_eq!(update, Update::RefreshDom, "the hook's Update is returned verbatim");
2270 let seen = logged(&mut log);
2271 assert_eq!(seen.len(), 1);
2272 assert_eq!(seen[0].ratio, 0.75);
2273 assert_eq!(seen[0].direction, SplitDirection::Horizontal);
2274 }
2275
2276 #[test]
2277 fn pointer_move_returns_the_hooks_do_nothing_too() {
2278 let boxes = [(0, size(200.0, 100.0))];
2279 let sp = plain(SplitDirection::Horizontal).with_on_resize(
2280 RefAny::new(ResizeLog::default()),
2281 resize_do_nothing as SplitPaneOnResizeCallbackType,
2282 );
2283 let (update, changes, _) = press_then_move(sp, &boxes, (100.0, 50.0), (150.0, 50.0));
2284 assert_eq!(update, Update::DoNothing);
2285 assert_eq!(css_changes(&changes).len(), 2, "the panes still resize");
2286 }
2287
2288 #[test]
2289 fn pointer_move_fires_the_hook_even_when_the_ratio_did_not_change() {
2290 let boxes = [(0, size(200.0, 100.0))];
2291 let mut log = RefAny::new(ResizeLog::default());
2292 let sp = plain(SplitDirection::Horizontal)
2293 .with_on_resize(log.clone(), record_resize as SplitPaneOnResizeCallbackType);
2294 let (_, changes, mut state) = press_then_move(sp, &boxes, (100.0, 50.0), (100.0, 50.0));
2295 assert_eq!(wrapper(&mut state).inner.ratio, 0.5);
2296 assert_eq!(logged(&mut log).len(), 1, "a zero-delta move still notifies");
2297 assert_eq!(css_changes(&changes).len(), 2);
2298 }
2299
2300 #[test]
2301 fn pointer_move_without_a_cursor_or_rect_keeps_the_drag_alive() {
2302 let boxes = [(0, size(200.0, 100.0))];
2303 let (sd, state) = laid_out(plain(SplitDirection::Horizontal));
2304 let a = sd.clone();
2305 let (_, _) = drive(a, &boxes, node(0), cursor(100.0, 50.0), |info| {
2306 on_split_pointer_down(state.clone(), info)
2307 });
2308 let b = sd.clone();
2310 let (u1, c1) = drive(b, &boxes, node(0), OptionLogicalPosition::None, |info| {
2311 on_split_pointer_move(state.clone(), info)
2312 });
2313 let (u2, c2) = drive(sd, &[], node(0), cursor(150.0, 50.0), |info| {
2315 on_split_pointer_move(state.clone(), info)
2316 });
2317 assert_eq!((u1, u2), (Update::DoNothing, Update::DoNothing));
2318 assert!(c1.is_empty() && c2.is_empty());
2319 let mut state = state;
2320 let w = wrapper(&mut state);
2321 assert!(w.is_dragging, "a dropped move event must not cancel the drag");
2322 assert_eq!(w.inner.ratio, 0.5);
2323 }
2324
2325 #[test]
2326 fn pointer_move_on_a_zero_sized_container_is_a_no_op() {
2327 let boxes = [(0, size(200.0, 100.0))];
2328 let (sd, state) = laid_out(plain(SplitDirection::Horizontal));
2329 let a = sd.clone();
2330 let (_, _) = drive(a, &boxes, node(0), cursor(100.0, 50.0), |info| {
2331 on_split_pointer_down(state.clone(), info)
2332 });
2333 let (update, changes) = drive(
2334 sd,
2335 &[(0, size(0.0, 0.0))],
2336 node(0),
2337 cursor(0.0, 0.0),
2338 |info| on_split_pointer_move(state.clone(), info),
2339 );
2340 assert_eq!(update, Update::DoNothing);
2341 assert!(changes.is_empty(), "no division by a zero main size");
2342 let mut state = state;
2343 assert_eq!(wrapper(&mut state).inner.ratio, 0.5);
2344 }
2345
2346 #[test]
2347 fn pointer_move_with_a_wrong_typed_payload_is_a_no_op() {
2348 let (sd, _state) = laid_out(plain(SplitDirection::Horizontal));
2349 let stranger = RefAny::new([0u8; 3]);
2350 let (update, changes) = drive(
2351 sd,
2352 &[(0, size(200.0, 100.0))],
2353 node(0),
2354 cursor(150.0, 50.0),
2355 |info| on_split_pointer_move(stranger.clone(), info),
2356 );
2357 assert_eq!(update, Update::DoNothing);
2358 assert!(changes.is_empty());
2359 }
2360
2361 #[test]
2362 fn pointer_move_on_a_childless_hit_node_still_tracks_the_ratio() {
2363 let boxes = [(0, size(200.0, 100.0)), (3, size(6.0, 100.0))];
2367 let (sd, state) = laid_out(plain(SplitDirection::Horizontal));
2368 let a = sd.clone();
2369 let (_, _) = drive(a, &boxes, node(0), cursor(100.0, 50.0), |info| {
2370 on_split_pointer_down(state.clone(), info)
2371 });
2372 let (update, changes) = drive(sd, &boxes, node(3), cursor(3.0, 50.0), |info| {
2373 on_split_pointer_move(state.clone(), info)
2374 });
2375 assert_eq!(update, Update::DoNothing);
2376 assert!(
2377 css_changes(&changes).len() <= 2,
2378 "at most the two panes may be written"
2379 );
2380 let mut state = state;
2381 assert!(wrapper(&mut state).inner.ratio.is_finite());
2382 }
2383
2384 #[test]
2385 fn pointer_move_with_a_nan_cursor_poisons_the_ratio() {
2386 let boxes = [(0, size(200.0, 100.0))];
2391 let (_, changes, mut state) = press_then_move(
2392 plain(SplitDirection::Horizontal),
2393 &boxes,
2394 (100.0, 50.0),
2395 (f32::NAN, 50.0),
2396 );
2397 assert!(wrapper(&mut state).inner.ratio.is_nan());
2398 let writes = css_changes(&changes);
2399 assert_eq!(writes.len(), 2);
2400 assert_eq!(writes[0].1, 0.0);
2401 assert_eq!(writes[1].1, 0.0);
2402 }
2403
2404 #[test]
2405 fn pointer_move_with_a_nan_container_size_poisons_the_ratio() {
2406 let (sd, state) = laid_out(plain(SplitDirection::Horizontal));
2409 let a = sd.clone();
2410 let (_, _) = drive(
2411 a,
2412 &[(0, size(200.0, 100.0))],
2413 node(0),
2414 cursor(100.0, 50.0),
2415 |info| on_split_pointer_down(state.clone(), info),
2416 );
2417 let (update, _) = drive(
2418 sd,
2419 &[(0, size(f32::NAN, 100.0))],
2420 node(0),
2421 cursor(150.0, 50.0),
2422 |info| on_split_pointer_move(state.clone(), info),
2423 );
2424 assert_eq!(update, Update::DoNothing);
2425 let mut state = state;
2426 assert!(wrapper(&mut state).inner.ratio.is_nan());
2427 }
2428
2429 #[test]
2430 fn pointer_move_with_extreme_cursors_stays_inside_the_clamp() {
2431 let boxes = [(0, size(200.0, 100.0))];
2432 for x in [f32::MAX, f32::MIN, 1.0e30, -1.0e30, f32::INFINITY, f32::NEG_INFINITY] {
2433 let (_, _, mut state) = press_then_move(
2434 plain(SplitDirection::Horizontal),
2435 &boxes,
2436 (100.0, 50.0),
2437 (x, 50.0),
2438 );
2439 let r = wrapper(&mut state).inner.ratio;
2440 assert!(
2441 (MIN_RATIO..=MAX_RATIO).contains(&r),
2442 "cursor {x} produced ratio {r}"
2443 );
2444 }
2445 }
2446
2447 #[test]
2448 fn pointer_move_on_a_huge_container_is_still_a_proportion() {
2449 let boxes = [(0, size(1.0e9, 100.0))];
2450 let (_, _, mut state) = press_then_move(
2451 plain(SplitDirection::Horizontal),
2452 &boxes,
2453 (5.0e8, 50.0),
2454 (7.5e8, 50.0),
2455 );
2456 let r = wrapper(&mut state).inner.ratio;
2457 assert!((r - 0.75).abs() < 1e-4, "expected ~0.75, got {r}");
2458 }
2459
2460 #[test]
2461 fn pointer_move_on_a_sub_pixel_container_does_not_explode() {
2462 let boxes = [(0, size(f32::MIN_POSITIVE, 100.0))];
2463 let (_, _, mut state) = press_then_move(
2464 plain(SplitDirection::Horizontal),
2465 &boxes,
2466 (0.0, 50.0),
2467 (1.0, 50.0),
2468 );
2469 assert_eq!(wrapper(&mut state).inner.ratio, MAX_RATIO);
2472 }
2473
2474 #[test]
2479 fn pointer_up_ends_the_drag_and_keeps_the_ratio() {
2480 let boxes = [(0, size(200.0, 100.0))];
2481 let (sd, state) = laid_out(plain(SplitDirection::Horizontal));
2482 let a = sd.clone();
2483 let (_, _) = drive(a, &boxes, node(0), cursor(100.0, 50.0), |info| {
2484 on_split_pointer_down(state.clone(), info)
2485 });
2486 let b = sd.clone();
2487 let (_, _) = drive(b, &boxes, node(0), cursor(150.0, 50.0), |info| {
2488 on_split_pointer_move(state.clone(), info)
2489 });
2490 let (update, changes) = drive(sd, &boxes, node(0), cursor(150.0, 50.0), |info| {
2491 on_split_pointer_up(state.clone(), info)
2492 });
2493 assert_eq!(update, Update::DoNothing);
2494 assert!(changes.is_empty(), "release must not rewrite the panes");
2495 let mut state = state;
2496 let w = wrapper(&mut state);
2497 assert!(!w.is_dragging);
2498 assert_eq!(w.inner.ratio, 0.75, "the drag result survives the release");
2499 }
2500
2501 #[test]
2502 fn pointer_up_is_idempotent_and_safe_without_a_drag() {
2503 let (sd, state) = laid_out(plain(SplitDirection::Horizontal));
2504 let a = sd.clone();
2505 let (u1, _) = drive(a, &[], node_none(), OptionLogicalPosition::None, |info| {
2506 on_split_pointer_up(state.clone(), info)
2507 });
2508 let (u2, _) = drive(sd, &[], node_none(), OptionLogicalPosition::None, |info| {
2509 on_split_pointer_up(state.clone(), info)
2510 });
2511 assert_eq!((u1, u2), (Update::DoNothing, Update::DoNothing));
2512 let mut state = state;
2513 let w = wrapper(&mut state);
2514 assert!(!w.is_dragging);
2515 assert_eq!(w.inner.ratio, 0.5);
2516 }
2517
2518 #[test]
2519 fn pointer_up_with_a_wrong_typed_payload_is_a_no_op() {
2520 let (sd, _state) = laid_out(plain(SplitDirection::Horizontal));
2521 let stranger = RefAny::new("not a split pane".to_string());
2522 let (update, changes) = drive(sd, &[], node_none(), OptionLogicalPosition::None, |info| {
2523 on_split_pointer_up(stranger.clone(), info)
2524 });
2525 assert_eq!(update, Update::DoNothing);
2526 assert!(changes.is_empty());
2527 }
2528
2529 #[test]
2530 fn a_released_drag_ignores_further_motion() {
2531 let boxes = [(0, size(200.0, 100.0))];
2532 let (sd, state) = laid_out(plain(SplitDirection::Horizontal));
2533 let a = sd.clone();
2534 let (_, _) = drive(a, &boxes, node(0), cursor(100.0, 50.0), |info| {
2535 on_split_pointer_down(state.clone(), info)
2536 });
2537 let b = sd.clone();
2538 let (_, _) = drive(b, &boxes, node(0), cursor(150.0, 50.0), |info| {
2539 on_split_pointer_move(state.clone(), info)
2540 });
2541 let c = sd.clone();
2542 let (_, _) = drive(c, &boxes, node(0), cursor(150.0, 50.0), |info| {
2543 on_split_pointer_up(state.clone(), info)
2544 });
2545 let (update, changes) = drive(sd, &boxes, node(0), cursor(20.0, 50.0), |info| {
2546 on_split_pointer_move(state.clone(), info)
2547 });
2548 assert_eq!(update, Update::DoNothing);
2549 assert!(changes.is_empty(), "post-release motion must not resize");
2550 let mut state = state;
2551 assert_eq!(wrapper(&mut state).inner.ratio, 0.75);
2552 }
2553}