1use std::ops::Range;
2
3use gpui::{
4 Along, App, Axis, Bounds, Context, ElementId, EventEmitter, IsZero, Pixels, Window, px,
5};
6
7mod panel;
8mod resize_handle;
9pub use panel::*;
10#[doc(hidden)]
11pub use resize_handle::*;
12
13#[doc(hidden)]
14pub const PANEL_MIN_SIZE: Pixels = px(100.);
15
16pub fn h_resizable(id: impl Into<ElementId>) -> ResizablePanelGroup {
18 ResizablePanelGroup::new(id).axis(Axis::Horizontal)
19}
20
21pub fn v_resizable(id: impl Into<ElementId>) -> ResizablePanelGroup {
23 ResizablePanelGroup::new(id).axis(Axis::Vertical)
24}
25
26pub fn resizable_panel() -> ResizablePanel {
28 ResizablePanel::new()
29}
30
31#[derive(Debug, Clone)]
33pub struct ResizableState {
34 axis: Axis,
36 panels: Vec<ResizablePanelState>,
37 sizes: Vec<Pixels>,
38 resizing_panel_ix: Option<usize>,
39 bounds: Bounds<Pixels>,
40}
41
42impl Default for ResizableState {
43 fn default() -> Self {
44 Self {
45 axis: Axis::Horizontal,
46 panels: vec![],
47 sizes: vec![],
48 resizing_panel_ix: None,
49 bounds: Bounds::default(),
50 }
51 }
52}
53
54impl ResizableState {
55 pub fn sizes(&self) -> &Vec<Pixels> {
57 &self.sizes
58 }
59
60 pub fn resize_panel(
70 &mut self,
71 ix: usize,
72 size: Pixels,
73 window: &mut Window,
74 cx: &mut Context<Self>,
75 ) {
76 if ix >= self.sizes.len() {
77 return;
78 }
79 if ix + 1 < self.sizes.len() {
80 self.resize_panel_at_handle(ix, size, window, cx);
81 } else if ix > 0 {
82 let delta = self.sizes[ix] - size;
85 let prev = self.sizes[ix - 1];
86 self.resize_panel_at_handle(ix - 1, prev + delta, window, cx);
87 }
88 self.done_resizing(cx);
89 }
90
91 pub fn insert_panel(
96 &mut self,
97 size: Option<Pixels>,
98 ix: Option<usize>,
99 cx: &mut Context<Self>,
100 ) {
101 let panel_state = ResizablePanelState {
102 size,
103 ..Default::default()
104 };
105
106 let size = size.unwrap_or(PANEL_MIN_SIZE);
107
108 let container_size = self.container_size().max(px(1.));
111 let total_leftover_size = (container_size - size).max(px(1.));
112
113 for (i, panel) in self.panels.iter_mut().enumerate() {
114 let ratio = self.sizes[i] / container_size;
115 self.sizes[i] = total_leftover_size * ratio;
116 panel.size = Some(self.sizes[i]);
117 }
118
119 if let Some(ix) = ix {
120 self.panels.insert(ix, panel_state);
121 self.sizes.insert(ix, size);
122 } else {
123 self.panels.push(panel_state);
124 self.sizes.push(size);
125 };
126
127 cx.notify();
128 }
129
130 pub(crate) fn adopt_sizes(&mut self, sizes: &[Option<Pixels>], cx: &mut Context<Self>) {
138 let mut changed = false;
139 for (ix, size) in sizes.iter().enumerate() {
140 if let Some(panel) = self.panels.get_mut(ix) {
148 if panel.size != *size {
149 panel.size = *size;
150 changed = true;
151 }
152 }
153
154 let Some(size) = size else { continue };
158 if let Some(slot) = self.sizes.get_mut(ix) {
159 if *slot != *size {
160 *slot = *size;
161 changed = true;
162 }
163 }
164 }
165
166 if changed {
167 cx.notify();
168 }
169 }
170
171 pub(crate) fn sync_panels_count(
172 &mut self,
173 axis: Axis,
174 panels_count: usize,
175 cx: &mut Context<Self>,
176 ) {
177 let mut changed = self.axis != axis;
178 self.axis = axis;
179
180 if panels_count > self.panels.len() {
181 let diff = panels_count - self.panels.len();
182 self.panels
183 .extend(vec![ResizablePanelState::default(); diff]);
184 self.sizes.extend(vec![PANEL_MIN_SIZE; diff]);
185 changed = true;
186 }
187
188 if panels_count < self.panels.len() {
189 self.panels.truncate(panels_count);
190 self.sizes.truncate(panels_count);
191 changed = true;
192 }
193
194 if changed {
195 self.adjust_to_container_size(cx);
197 }
198 }
199
200 pub(crate) fn panel_size_changes(
205 &self,
206 panel_ix: usize,
207 bounds: Bounds<Pixels>,
208 size_range: &Range<Pixels>,
209 ) -> bool {
210 let Some(panel) = self.panels.get(panel_ix) else {
211 return true;
212 };
213 self.sizes.get(panel_ix).map(|size| size.as_f32()) == Some(PANEL_MIN_SIZE.as_f32())
214 || panel.bounds != bounds
215 || panel.size_range != *size_range
216 }
217
218 pub(crate) fn update_panel_size(
219 &mut self,
220 panel_ix: usize,
221 bounds: Bounds<Pixels>,
222 size_range: Range<Pixels>,
223 cx: &mut Context<Self>,
224 ) {
225 let size = bounds.size.along(self.axis);
226 if self.sizes[panel_ix].as_f32() == PANEL_MIN_SIZE.as_f32() {
230 self.sizes[panel_ix] = size;
231 self.panels[panel_ix].size = Some(size);
232 }
233 self.panels[panel_ix].bounds = bounds;
234 self.panels[panel_ix].size_range = size_range;
235 cx.notify();
236 }
237
238 pub fn remove_panel(&mut self, panel_ix: usize, cx: &mut Context<Self>) {
240 self.panels.remove(panel_ix);
241 self.sizes.remove(panel_ix);
242 if let Some(resizing_panel_ix) = self.resizing_panel_ix {
243 if resizing_panel_ix > panel_ix {
244 self.resizing_panel_ix = Some(resizing_panel_ix - 1);
245 }
246 }
247 self.adjust_to_container_size(cx);
248 }
249
250 pub fn reset_panel(&mut self, panel_ix: usize, cx: &mut Context<Self>) {
252 let old_size = self.sizes[panel_ix];
253
254 self.panels[panel_ix] = ResizablePanelState::default();
255 self.sizes[panel_ix] = old_size;
256 self.adjust_to_container_size(cx);
257 }
258
259 pub fn clear(&mut self) {
261 self.panels.clear();
262 self.sizes.clear();
263 }
264
265 #[inline]
266 pub fn container_size(&self) -> Pixels {
268 self.bounds.size.along(self.axis)
269 }
270
271 pub(crate) fn done_resizing(&mut self, cx: &mut Context<Self>) {
272 self.resizing_panel_ix = None;
273 cx.emit(ResizablePanelEvent::Resized);
274 }
275
276 fn panel_size_range(&self, ix: usize) -> Range<Pixels> {
277 let Some(panel) = self.panels.get(ix) else {
278 return PANEL_MIN_SIZE..Pixels::MAX;
279 };
280
281 panel.size_range.clone()
282 }
283
284 fn sync_real_panel_sizes(&mut self, _: &App) {
285 for (i, panel) in self.panels.iter().enumerate() {
286 self.sizes[i] = panel.bounds.size.along(self.axis);
287 }
288 }
289
290 fn resize_panel_at_handle(
297 &mut self,
298 ix: usize,
299 size: Pixels,
300 _: &mut Window,
301 cx: &mut Context<Self>,
302 ) {
303 let old_sizes = self.sizes.clone();
304
305 let mut ix = ix;
306 if ix >= old_sizes.len() - 1 {
308 return;
309 }
310 let container_size = self.container_size();
311 self.sync_real_panel_sizes(cx);
312
313 let move_changed = size - old_sizes[ix];
314 if move_changed == px(0.) {
315 return;
316 }
317
318 let size_range = self.panel_size_range(ix);
319 let new_size = size.clamp(size_range.start, size_range.end);
320 let is_expand = move_changed > px(0.);
321
322 let main_ix = ix;
323 let mut new_sizes = old_sizes.clone();
324
325 if is_expand {
326 let mut changed = new_size - old_sizes[ix];
327 new_sizes[ix] = new_size;
328
329 while changed > px(0.) && ix < old_sizes.len() - 1 {
330 ix += 1;
331 let size_range = self.panel_size_range(ix);
332 let available_size = (new_sizes[ix] - size_range.start).max(px(0.));
333 let to_reduce = changed.min(available_size);
334 new_sizes[ix] -= to_reduce;
335 changed -= to_reduce;
336 }
337 } else {
338 let mut changed = new_size - size;
339 new_sizes[ix] = new_size;
340
341 while changed > px(0.) && ix > 0 {
342 ix -= 1;
343 let size_range = self.panel_size_range(ix);
344 let available_size = (new_sizes[ix] - size_range.start).max(px(0.));
345 let to_reduce = changed.min(available_size);
346 changed -= to_reduce;
347 new_sizes[ix] -= to_reduce;
348 }
349
350 new_sizes[main_ix + 1] += old_sizes[main_ix] - size - changed;
351 }
352
353 let total_size: Pixels = new_sizes.iter().map(|s| s.as_f32()).sum::<f32>().into();
355 if total_size > container_size {
356 let overflow = total_size - container_size;
357 new_sizes[main_ix] = (new_sizes[main_ix] - overflow).max(size_range.start);
358 }
359
360 for (i, _) in old_sizes.iter().enumerate() {
361 let size = new_sizes[i];
362 self.panels[i].size = Some(size);
363 }
364 self.sizes = new_sizes;
365 cx.notify();
366 }
367
368 fn adjust_to_container_size(&mut self, cx: &mut Context<Self>) {
372 if self.container_size().is_zero() {
373 return;
374 }
375
376 if self.panels.iter().any(|panel| panel.size.is_none()) {
384 return;
385 }
386
387 let container_size = self.container_size();
388 let total = self.sizes.iter().map(|s| s.as_f32()).sum::<f32>();
389 if !total.is_finite() || total <= 0. {
390 return;
391 }
392 let total_size = px(total);
393
394 for i in 0..self.panels.len() {
395 let size = self.sizes[i];
396 let ratio = size / total_size;
397 let new_size = container_size * ratio;
398
399 self.sizes[i] = new_size;
400 self.panels[i].size = Some(new_size);
401 }
402 cx.notify();
403 }
404}
405
406impl EventEmitter<ResizablePanelEvent> for ResizableState {}
407
408#[derive(Debug, Clone, Default)]
409pub(crate) struct ResizablePanelState {
410 pub size: Option<Pixels>,
411 pub size_range: Range<Pixels>,
412 bounds: Bounds<Pixels>,
413}
414
415#[cfg(test)]
416mod tests {
417 use std::{
418 cell::{Cell, RefCell},
419 rc::Rc,
420 };
421
422 use gpui::{
423 App, AppContext as _, Context, InteractiveElement as _, IntoElement, Modifiers,
424 MouseButton, ParentElement as _, Pixels, Render, Styled as _, TestAppContext,
425 VisualTestContext, Window, div, point, prelude::FluentBuilder as _, px, size,
426 };
427
428 use super::{
429 ResizableState, ResizeHandleContext, ResizeHandleState, h_resizable, resizable_panel,
430 };
431
432 struct MixedSizingHarness {
433 width: Pixels,
434 }
435
436 impl Render for MixedSizingHarness {
437 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
438 div().w(self.width).h(px(100.)).child(
439 h_resizable("mixed-sizing")
440 .child(
441 resizable_panel()
442 .size(px(240.))
443 .child(div().size_full().debug_selector(|| "fixed-sidebar".into())),
444 )
445 .child(
446 resizable_panel().child(
447 div()
448 .size_full()
449 .debug_selector(|| "flexible-content".into()),
450 ),
451 ),
452 )
453 }
454 }
455
456 #[gpui::test]
457 fn mixed_sizing_is_stable_between_resize_and_followup_frame(cx: &mut TestAppContext) {
458 let (view, cx) = cx.add_window_view(|_, _| MixedSizingHarness { width: px(800.) });
459 cx.update(|window, cx| {
460 window.draw(cx).clear(cx);
461 window.draw(cx).clear(cx);
462 });
463 let before = cx.debug_bounds("fixed-sidebar").unwrap().size.width;
464
465 view.update(cx, |view, cx| {
466 view.width = px(1200.);
467 cx.notify();
468 });
469 cx.run_until_parked();
470 let settled_frame = cx.debug_bounds("fixed-sidebar").unwrap().size.width;
471 cx.update(|window, cx| window.draw(cx).clear(cx));
472 let followup_frame = cx.debug_bounds("fixed-sidebar").unwrap().size.width;
473
474 assert_ne!(settled_frame, before);
478 assert_eq!(followup_frame, settled_frame);
479 }
480
481 struct CallerStateHarness {
482 width: Pixels,
483 state: gpui::Entity<ResizableState>,
484 }
485
486 impl Render for CallerStateHarness {
487 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
488 div().w(self.width).h(px(100.)).child(
489 h_resizable("caller-state")
490 .with_state(&self.state)
491 .child(
492 resizable_panel()
493 .size(px(240.))
494 .child(div().size_full().debug_selector(|| "cs-sidebar".into())),
495 )
496 .child(resizable_panel().child(div().size_full())),
497 )
498 }
499 }
500
501 #[gpui::test]
505 fn caller_owned_state_settles_on_the_same_frame(cx: &mut TestAppContext) {
506 let state = cx.update(|cx| cx.new(|_| ResizableState::default()));
507 let (view, cx) = cx.add_window_view({
508 let state = state.clone();
509 move |_, _| CallerStateHarness {
510 width: px(800.),
511 state,
512 }
513 });
514 cx.update(|window, cx| {
515 window.draw(cx).clear(cx);
516 window.draw(cx).clear(cx);
517 });
518
519 view.update(cx, |view, cx| {
520 view.width = px(1200.);
521 cx.notify();
522 });
523 cx.run_until_parked();
524 let settled = cx.debug_bounds("cs-sidebar").unwrap().size.width;
525 cx.update(|window, cx| window.draw(cx).clear(cx));
526 let followup = cx.debug_bounds("cs-sidebar").unwrap().size.width;
527
528 assert_eq!(followup, settled, "settling frame must not be pending");
529 }
530
531 struct ResizableHarness {
532 state: gpui::Entity<ResizableState>,
533 resizes: Rc<Cell<usize>>,
534 }
535
536 impl Render for ResizableHarness {
537 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
538 div().w(px(400.)).h(px(100.)).child(
539 h_resizable("resizable")
540 .with_state(&self.state)
541 .on_resize({
542 let resizes = self.resizes.clone();
543 move |_, _, _| resizes.set(resizes.get() + 1)
544 })
545 .child(
546 resizable_panel()
547 .size(px(150.))
548 .child(div().size_full().debug_selector(|| "first-panel".into())),
549 )
550 .child(
551 resizable_panel()
552 .size(px(250.))
553 .child(div().size_full().debug_selector(|| "second-panel".into())),
554 ),
555 )
556 }
557 }
558
559 fn harness(
560 cx: &mut TestAppContext,
561 ) -> (
562 &mut VisualTestContext,
563 gpui::Entity<ResizableState>,
564 Rc<Cell<usize>>,
565 ) {
566 let state = cx.update(|cx| cx.new(|_| ResizableState::default()));
567 let resizes = Rc::new(Cell::new(0));
568 let (_, cx) = cx.add_window_view({
569 let state = state.clone();
570 let resizes = resizes.clone();
571 move |_, _| ResizableHarness { state, resizes }
572 });
573 cx.update(|window, cx| window.draw(cx).clear(cx));
574 cx.update(|window, cx| window.draw(cx).clear(cx));
575 (cx, state, resizes)
576 }
577
578 #[cfg(feature = "gpui-fast")]
580 struct StateReader {
581 state: gpui::Entity<ResizableState>,
582 renders: Rc<Cell<usize>>,
583 }
584
585 #[cfg(feature = "gpui-fast")]
586 impl Render for StateReader {
587 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
588 self.renders.set(self.renders.get() + 1);
589 let _ = self.state.read(cx).sizes.len();
590 div()
591 }
592 }
593
594 #[cfg(feature = "gpui-fast")]
596 struct ReaderBesidePanels {
597 state: gpui::Entity<ResizableState>,
598 reader: gpui::Entity<StateReader>,
599 }
600
601 #[cfg(feature = "gpui-fast")]
602 impl Render for ReaderBesidePanels {
603 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
604 div()
605 .w(px(800.))
606 .h(px(100.))
607 .child(self.reader.clone())
608 .child(
609 h_resizable("reader-panels")
610 .with_state(&self.state)
611 .child(resizable_panel().size(px(240.)).child(div().size_full()))
612 .child(resizable_panel().child(div().size_full())),
613 )
614 }
615 }
616
617 #[cfg(feature = "gpui-fast")]
621 #[gpui::test]
622 fn redrawing_panels_where_they_were_leaves_their_readers_retained(cx: &mut TestAppContext) {
623 let renders = Rc::new(Cell::new(0));
624 let state = cx.update(|cx| cx.new(|_| ResizableState::default()));
625 let (view, cx) = cx.add_window_view({
626 let state = state.clone();
627 let renders = renders.clone();
628 move |_, cx| ReaderBesidePanels {
629 state: state.clone(),
630 reader: cx.new(|_| StateReader { state, renders }),
631 }
632 });
633 for _ in 0..3 {
634 cx.update(|window, cx| window.draw(cx).clear(cx));
635 cx.run_until_parked();
636 }
637 let before = renders.get();
638 for _ in 0..3 {
639 view.update(cx, |_, cx| cx.notify());
640 cx.update(|window, cx| window.draw(cx).clear(cx));
641 cx.run_until_parked();
642 }
643 assert_eq!(renders.get(), before, "the reader rendered again");
644 }
645
646 #[gpui::test]
647 fn dynamic_panel_lifecycle_is_owned_by_resizable_state(cx: &mut TestAppContext) {
648 let state = cx.update(|cx| cx.new(|_| ResizableState::default()));
649
650 cx.update(|cx| {
651 state.update(cx, |state, cx| {
652 state.bounds.size = size(px(400.), px(100.));
653 state.panels.push(Default::default());
654 state.sizes.push(px(400.));
655 state.insert_panel(Some(px(200.)), None, cx);
656 assert_eq!(state.sizes(), &vec![px(200.), px(200.)]);
657
658 state.reset_panel(0, cx);
659 assert_eq!(state.sizes(), &vec![px(200.), px(200.)]);
660
661 state.remove_panel(0, cx);
662 assert_eq!(state.sizes(), &vec![px(400.)]);
663
664 state.clear();
665 assert!(state.sizes().is_empty());
666 });
667 });
668 }
669
670 #[gpui::test]
671 fn group_measures_panels_and_programmatic_resize_uses_drag_rules(cx: &mut TestAppContext) {
672 let (cx, state, _) = harness(cx);
673 let first = cx.debug_bounds("first-panel").unwrap();
674 let second = cx.debug_bounds("second-panel").unwrap();
675 assert_eq!(first.size.width + second.size.width, px(400.));
676
677 cx.update(|window, cx| {
678 state.update(cx, |state, cx| {
679 state.resize_panel(0, px(220.), window, cx);
680 });
681 window.draw(cx).clear(cx);
682 });
683
684 state.read_with(cx, |state, _| {
685 assert_eq!(state.sizes(), &vec![px(220.), px(180.)]);
686 });
687 }
688
689 #[gpui::test]
690 fn dragging_the_handle_resizes_and_emits_once(cx: &mut TestAppContext) {
691 let (cx, state, resizes) = harness(cx);
692 let boundary = cx.debug_bounds("second-panel").unwrap().left();
693
694 cx.simulate_mouse_down(
695 point(boundary - px(2.), px(50.)),
696 MouseButton::Left,
697 Modifiers::default(),
698 );
699 cx.simulate_mouse_move(
700 point(boundary + px(10.), px(50.)),
701 Some(MouseButton::Left),
702 Modifiers::default(),
703 );
704 cx.simulate_mouse_move(
705 point(px(220.), px(50.)),
706 Some(MouseButton::Left),
707 Modifiers::default(),
708 );
709 cx.simulate_mouse_up(
710 point(px(220.), px(50.)),
711 MouseButton::Left,
712 Modifiers::default(),
713 );
714
715 state.read_with(cx, |state, _| {
716 assert_eq!(state.sizes(), &vec![px(220.), px(180.)]);
717 });
718 assert_eq!(resizes.get(), 1);
719 }
720
721 struct HandleStateHarness {
727 seen: Rc<RefCell<Vec<ResizeHandleState>>>,
728 covered: bool,
729 }
730
731 impl Render for HandleStateHarness {
732 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
733 let seen = self.seen.clone();
734 div()
735 .relative()
736 .w(px(400.))
737 .h(px(100.))
738 .child(
739 h_resizable("handle-state")
740 .with_handle_appearance(Rc::new(
741 move |handle: &ResizeHandleContext, _: &mut Window, _: &mut App| {
742 let mut seen = seen.borrow_mut();
743 if seen.last() != Some(&handle.state()) {
744 seen.push(handle.state());
745 }
746 None
748 },
749 ))
750 .child(resizable_panel().size(px(150.)).child(div().size_full()))
751 .child(
752 resizable_panel()
753 .size(px(250.))
754 .child(div().size_full().debug_selector(|| "hs-second".into())),
755 ),
756 )
757 .when(self.covered, |this| {
758 this.child(div().absolute().inset_0().occlude())
759 })
760 }
761 }
762
763 #[gpui::test]
771 fn a_covered_handle_stays_idle(cx: &mut TestAppContext) {
772 let seen: Rc<RefCell<Vec<ResizeHandleState>>> = Rc::new(RefCell::new(Vec::new()));
773 let (_, cx) = cx.add_window_view({
774 let seen = seen.clone();
775 move |_, _| HandleStateHarness {
776 seen,
777 covered: true,
778 }
779 });
780 cx.update(|window, cx| window.draw(cx).clear(cx));
781 cx.update(|window, cx| window.draw(cx).clear(cx));
782
783 let boundary = cx.debug_bounds("hs-second").unwrap().left();
784 let on_handle = point(boundary - px(2.), px(50.));
785 let draw = |cx: &mut VisualTestContext| cx.update(|window, cx| window.draw(cx).clear(cx));
786
787 cx.simulate_mouse_move(on_handle, None, Modifiers::default());
788 draw(cx);
789 cx.simulate_mouse_down(on_handle, MouseButton::Left, Modifiers::default());
790 draw(cx);
791 cx.simulate_mouse_up(on_handle, MouseButton::Left, Modifiers::default());
792 draw(cx);
793
794 assert_eq!(*seen.borrow(), vec![ResizeHandleState::Idle]);
795 }
796
797 #[gpui::test]
798 fn a_handle_reports_the_press_and_the_drag_to_its_renderer(cx: &mut TestAppContext) {
799 let seen: Rc<RefCell<Vec<ResizeHandleState>>> = Rc::new(RefCell::new(Vec::new()));
800 let (_, cx) = cx.add_window_view({
801 let seen = seen.clone();
802 move |_, _| HandleStateHarness {
803 seen,
804 covered: false,
805 }
806 });
807 cx.update(|window, cx| window.draw(cx).clear(cx));
808 cx.update(|window, cx| window.draw(cx).clear(cx));
809
810 let boundary = cx.debug_bounds("hs-second").unwrap().left();
811 let on_handle = point(boundary - px(2.), px(50.));
812 let draw = |cx: &mut VisualTestContext| cx.update(|window, cx| window.draw(cx).clear(cx));
813
814 cx.simulate_mouse_move(on_handle, None, Modifiers::default());
815 draw(cx);
816 cx.simulate_mouse_down(on_handle, MouseButton::Left, Modifiers::default());
817 draw(cx);
818 cx.simulate_mouse_move(
819 point(px(260.), px(50.)),
820 Some(MouseButton::Left),
821 Modifiers::default(),
822 );
823 draw(cx);
824 cx.simulate_mouse_up(
825 point(px(390.), px(90.)),
826 MouseButton::Left,
827 Modifiers::default(),
828 );
829 draw(cx);
830
831 assert_eq!(
835 *seen.borrow(),
836 vec![
837 ResizeHandleState::Idle,
838 ResizeHandleState::Hovered,
839 ResizeHandleState::Pressed,
840 ResizeHandleState::Dragging,
841 ResizeHandleState::Idle,
842 ]
843 );
844 }
845
846 struct SizedGroupHarness;
847
848 impl Render for SizedGroupHarness {
849 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
850 div()
851 .w(px(400.))
852 .h(px(100.))
853 .child(
854 h_resizable("sized-resizable").size(px(40.)).child(
855 resizable_panel()
856 .child(div().size_full().debug_selector(|| "sized-panel".into())),
857 ),
858 )
859 }
860 }
861
862 #[gpui::test]
863 fn a_group_size_binds_the_cross_axis(cx: &mut TestAppContext) {
864 let (_, cx) = cx.add_window_view(|_, _| SizedGroupHarness);
865 cx.update(|window, cx| window.draw(cx).clear(cx));
866 cx.update(|window, cx| window.draw(cx).clear(cx));
867
868 let panel = cx.debug_bounds("sized-panel").unwrap();
869 assert_eq!(panel.size.width, px(400.));
870 assert_eq!(panel.size.height, px(40.));
871 }
872}