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::*;
10pub use resize_handle::{
11 HandleEdge, ResizeHandle, ResizeHandleContext, ResizeHandleRenderer, ResizeHandleState,
12 resize_handle,
13};
14
15#[doc(hidden)]
16pub const PANEL_MIN_SIZE: Pixels = px(100.);
17
18pub fn h_resizable(id: impl Into<ElementId>) -> ResizablePanelGroup {
20 ResizablePanelGroup::new(id).axis(Axis::Horizontal)
21}
22
23pub fn v_resizable(id: impl Into<ElementId>) -> ResizablePanelGroup {
25 ResizablePanelGroup::new(id).axis(Axis::Vertical)
26}
27
28pub fn resizable_panel() -> ResizablePanel {
30 ResizablePanel::new()
31}
32
33#[derive(Debug, Clone)]
35pub struct ResizableState {
36 axis: Axis,
38 panels: Vec<ResizablePanelState>,
39 sizes: Vec<Pixels>,
40 resizing_panel_ix: Option<usize>,
41 bounds: Bounds<Pixels>,
42}
43
44impl Default for ResizableState {
45 fn default() -> Self {
46 Self {
47 axis: Axis::Horizontal,
48 panels: vec![],
49 sizes: vec![],
50 resizing_panel_ix: None,
51 bounds: Bounds::default(),
52 }
53 }
54}
55
56impl ResizableState {
57 pub fn sizes(&self) -> &Vec<Pixels> {
59 &self.sizes
60 }
61
62 pub fn resize_panel(
72 &mut self,
73 ix: usize,
74 size: Pixels,
75 window: &mut Window,
76 cx: &mut Context<Self>,
77 ) {
78 if ix >= self.sizes.len() {
79 return;
80 }
81 if ix + 1 < self.sizes.len() {
82 self.resize_panel_at_handle(ix, size, window, cx);
83 } else if ix > 0 {
84 let delta = self.sizes[ix] - size;
87 let prev = self.sizes[ix - 1];
88 self.resize_panel_at_handle(ix - 1, prev + delta, window, cx);
89 }
90 self.done_resizing(cx);
91 }
92
93 pub fn insert_panel(
98 &mut self,
99 size: Option<Pixels>,
100 ix: Option<usize>,
101 cx: &mut Context<Self>,
102 ) {
103 let panel_state = ResizablePanelState {
104 size,
105 ..Default::default()
106 };
107
108 let size = size.unwrap_or(PANEL_MIN_SIZE);
109
110 let container_size = self.container_size().max(px(1.));
113 let total_leftover_size = (container_size - size).max(px(1.));
114
115 for (i, panel) in self.panels.iter_mut().enumerate() {
116 let ratio = self.sizes[i] / container_size;
117 self.sizes[i] = total_leftover_size * ratio;
118 panel.size = Some(self.sizes[i]);
119 }
120
121 if let Some(ix) = ix {
122 self.panels.insert(ix, panel_state);
123 self.sizes.insert(ix, size);
124 } else {
125 self.panels.push(panel_state);
126 self.sizes.push(size);
127 };
128
129 cx.notify();
130 }
131
132 pub(crate) fn adopt_sizes(&mut self, sizes: &[Option<Pixels>], cx: &mut Context<Self>) {
140 let mut changed = false;
141 for (ix, size) in sizes.iter().enumerate() {
142 if let Some(panel) = self.panels.get_mut(ix) {
150 if panel.size != *size {
151 panel.size = *size;
152 changed = true;
153 }
154 }
155
156 let Some(size) = size else { continue };
160 if let Some(slot) = self.sizes.get_mut(ix) {
161 if *slot != *size {
162 *slot = *size;
163 changed = true;
164 }
165 }
166 }
167
168 if changed {
169 cx.notify();
170 }
171 }
172
173 pub(crate) fn sync_panels_count(
174 &mut self,
175 axis: Axis,
176 panels_count: usize,
177 cx: &mut Context<Self>,
178 ) {
179 let mut changed = self.axis != axis;
180 self.axis = axis;
181
182 if panels_count > self.panels.len() {
183 let diff = panels_count - self.panels.len();
184 self.panels
185 .extend(vec![ResizablePanelState::default(); diff]);
186 self.sizes.extend(vec![PANEL_MIN_SIZE; diff]);
187 changed = true;
188 }
189
190 if panels_count < self.panels.len() {
191 self.panels.truncate(panels_count);
192 self.sizes.truncate(panels_count);
193 changed = true;
194 }
195
196 if changed {
197 self.adjust_to_container_size(cx);
199 }
200 }
201
202 pub(crate) fn panel_size_changes(
207 &self,
208 panel_ix: usize,
209 bounds: Bounds<Pixels>,
210 size_range: &Range<Pixels>,
211 fixed: bool,
212 ) -> bool {
213 let Some(panel) = self.panels.get(panel_ix) else {
214 return true;
215 };
216 self.sizes.get(panel_ix).map(|size| size.as_f32()) == Some(PANEL_MIN_SIZE.as_f32())
217 || panel.bounds != bounds
218 || panel.size_range != *size_range
219 || panel.fixed != fixed
220 }
221
222 pub(crate) fn update_panel_size(
223 &mut self,
224 panel_ix: usize,
225 bounds: Bounds<Pixels>,
226 size_range: Range<Pixels>,
227 fixed: bool,
228 cx: &mut Context<Self>,
229 ) {
230 self.panels[panel_ix].fixed = fixed;
231 let size = bounds.size.along(self.axis);
232 if self.sizes[panel_ix].as_f32() == PANEL_MIN_SIZE.as_f32() {
236 self.sizes[panel_ix] = size;
237 self.panels[panel_ix].size = Some(size);
238 }
239 self.panels[panel_ix].bounds = bounds;
240 self.panels[panel_ix].size_range = size_range;
241 cx.notify();
242 }
243
244 pub fn remove_panel(&mut self, panel_ix: usize, cx: &mut Context<Self>) {
246 self.panels.remove(panel_ix);
247 self.sizes.remove(panel_ix);
248 if let Some(resizing_panel_ix) = self.resizing_panel_ix {
249 if resizing_panel_ix > panel_ix {
250 self.resizing_panel_ix = Some(resizing_panel_ix - 1);
251 }
252 }
253 self.adjust_to_container_size(cx);
254 }
255
256 pub fn reset_panel(&mut self, panel_ix: usize, cx: &mut Context<Self>) {
258 let old_size = self.sizes[panel_ix];
259
260 self.panels[panel_ix] = ResizablePanelState::default();
261 self.sizes[panel_ix] = old_size;
262 self.adjust_to_container_size(cx);
263 }
264
265 pub fn clear(&mut self) {
267 self.panels.clear();
268 self.sizes.clear();
269 }
270
271 #[inline]
272 pub fn container_size(&self) -> Pixels {
274 self.bounds.size.along(self.axis)
275 }
276
277 pub(crate) fn done_resizing(&mut self, cx: &mut Context<Self>) {
278 self.resizing_panel_ix = None;
279 cx.emit(ResizablePanelEvent::Resized);
280 }
281
282 fn panel_size_range(&self, ix: usize) -> Range<Pixels> {
283 let Some(panel) = self.panels.get(ix) else {
284 return PANEL_MIN_SIZE..Pixels::MAX;
285 };
286
287 panel.size_range.clone()
288 }
289
290 fn sync_real_panel_sizes(&mut self, _: &App) {
291 for (i, panel) in self.panels.iter().enumerate() {
292 self.sizes[i] = panel.bounds.size.along(self.axis);
293 }
294 }
295
296 fn resize_panel_at_handle(
303 &mut self,
304 ix: usize,
305 size: Pixels,
306 _: &mut Window,
307 cx: &mut Context<Self>,
308 ) {
309 let old_sizes = self.sizes.clone();
310
311 let mut ix = ix;
312 if ix >= old_sizes.len() - 1 {
314 return;
315 }
316 let container_size = self.container_size();
317 self.sync_real_panel_sizes(cx);
318
319 let move_changed = size - old_sizes[ix];
320 if move_changed == px(0.) {
321 return;
322 }
323
324 let size_range = self.panel_size_range(ix);
325 let new_size = size.clamp(size_range.start, size_range.end);
326 let is_expand = move_changed > px(0.);
327
328 let main_ix = ix;
329 let mut new_sizes = old_sizes.clone();
330
331 if is_expand {
332 let mut changed = new_size - old_sizes[ix];
333 new_sizes[ix] = new_size;
334
335 while changed > px(0.) && ix < old_sizes.len() - 1 {
336 ix += 1;
337 if self.panels[ix].fixed && ix != main_ix + 1 {
339 continue;
340 }
341 let size_range = self.panel_size_range(ix);
342 let available_size = (new_sizes[ix] - size_range.start).max(px(0.));
343 let to_reduce = changed.min(available_size);
344 new_sizes[ix] -= to_reduce;
345 changed -= to_reduce;
346 }
347 } else {
348 let mut changed = new_size - size;
349 new_sizes[ix] = new_size;
350
351 while changed > px(0.) && ix > 0 {
352 ix -= 1;
353 if self.panels[ix].fixed {
354 continue;
355 }
356 let size_range = self.panel_size_range(ix);
357 let available_size = (new_sizes[ix] - size_range.start).max(px(0.));
358 let to_reduce = changed.min(available_size);
359 changed -= to_reduce;
360 new_sizes[ix] -= to_reduce;
361 }
362
363 new_sizes[main_ix + 1] += old_sizes[main_ix] - size - changed;
364 }
365
366 let total_size: Pixels = new_sizes.iter().map(|s| s.as_f32()).sum::<f32>().into();
368 if total_size > container_size {
369 let overflow = total_size - container_size;
370 new_sizes[main_ix] = (new_sizes[main_ix] - overflow).max(size_range.start);
371 }
372
373 for (i, _) in old_sizes.iter().enumerate() {
374 let size = new_sizes[i];
375 self.panels[i].size = Some(size);
376 }
377 self.sizes = new_sizes;
378 cx.notify();
379 }
380
381 fn adjust_to_container_size(&mut self, cx: &mut Context<Self>) {
385 if self.container_size().is_zero() {
386 return;
387 }
388
389 if self.panels.iter().any(|panel| panel.size.is_none()) {
397 return;
398 }
399
400 let sum = |fixed: bool| {
405 self.panels
406 .iter()
407 .zip(&self.sizes)
408 .filter(|(panel, _)| panel.fixed == fixed)
409 .map(|(_, size)| size.as_f32())
410 .sum::<f32>()
411 };
412 let (fixed_size, total) = (px(sum(true)), sum(false));
413 if !total.is_finite() || total <= 0. {
414 return;
415 }
416 let total_size = px(total);
417 let available = self.container_size() - fixed_size;
418 if available <= px(0.) {
419 return;
420 }
421
422 for i in 0..self.panels.len() {
423 if self.panels[i].fixed {
424 self.panels[i].size = Some(self.sizes[i]);
425 continue;
426 }
427 let size = self.sizes[i];
428 let ratio = size / total_size;
429 let new_size = available * ratio;
430
431 self.sizes[i] = new_size;
432 self.panels[i].size = Some(new_size);
433 }
434 cx.notify();
435 }
436}
437
438impl EventEmitter<ResizablePanelEvent> for ResizableState {}
439
440#[derive(Debug, Clone, Default)]
441pub(crate) struct ResizablePanelState {
442 pub size: Option<Pixels>,
443 pub size_range: Range<Pixels>,
444 bounds: Bounds<Pixels>,
445 pub(crate) fixed: bool,
447}
448
449#[cfg(test)]
450mod tests {
451 use std::{
452 cell::{Cell, RefCell},
453 rc::Rc,
454 };
455
456 use gpui::{
457 App, AppContext as _, Context, InteractiveElement as _, IntoElement, Modifiers,
458 MouseButton, ParentElement as _, Pixels, Render, Styled as _, TestAppContext,
459 VisualTestContext, Window, div, point, prelude::FluentBuilder as _, px, size,
460 };
461
462 use super::{
463 ResizableState, ResizeHandleContext, ResizeHandleState, h_resizable, resizable_panel,
464 };
465
466 struct MixedSizingHarness {
467 width: Pixels,
468 }
469
470 impl Render for MixedSizingHarness {
471 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
472 div().w(self.width).h(px(100.)).child(
473 h_resizable("mixed-sizing")
474 .child(
475 resizable_panel()
476 .size(px(240.))
477 .child(div().size_full().debug_selector(|| "fixed-sidebar".into())),
478 )
479 .child(
480 resizable_panel().child(
481 div()
482 .size_full()
483 .debug_selector(|| "flexible-content".into()),
484 ),
485 ),
486 )
487 }
488 }
489
490 #[gpui::test]
491 fn mixed_sizing_is_stable_between_resize_and_followup_frame(cx: &mut TestAppContext) {
492 let (view, cx) = cx.add_window_view(|_, _| MixedSizingHarness { width: px(800.) });
493 cx.update(|window, cx| {
494 window.draw(cx).clear(cx);
495 window.draw(cx).clear(cx);
496 });
497 let before = cx.debug_bounds("fixed-sidebar").unwrap().size.width;
498
499 view.update(cx, |view, cx| {
500 view.width = px(1200.);
501 cx.notify();
502 });
503 cx.run_until_parked();
504 let settled_frame = cx.debug_bounds("fixed-sidebar").unwrap().size.width;
505 cx.update(|window, cx| window.draw(cx).clear(cx));
506 let followup_frame = cx.debug_bounds("fixed-sidebar").unwrap().size.width;
507
508 assert_ne!(settled_frame, before);
512 assert_eq!(followup_frame, settled_frame);
513 }
514
515 struct FixedPanelHarness {
516 width: Pixels,
517 fixed: bool,
518 state: gpui::Entity<ResizableState>,
519 }
520
521 impl Render for FixedPanelHarness {
522 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
523 div().w(self.width).h(px(100.)).child(
524 h_resizable("fixed-panel")
525 .with_state(&self.state)
526 .child(
527 resizable_panel()
528 .size(px(240.))
529 .size_range(px(100.)..px(480.))
530 .when(self.fixed, |panel| panel.fixed())
531 .child(div().size_full().debug_selector(|| "fp-sidebar".into())),
532 )
533 .child(
534 resizable_panel()
535 .size(px(400.))
536 .child(div().size_full().debug_selector(|| "fp-main".into())),
537 )
538 .child(
539 resizable_panel()
540 .child(div().size_full().debug_selector(|| "fp-aside".into())),
541 ),
542 )
543 }
544 }
545
546 #[gpui::test]
551 fn fixed_panel_keeps_its_size_across_a_container_resize(cx: &mut TestAppContext) {
552 let state = cx.update(|cx| cx.new(|_| ResizableState::default()));
553 let (view, cx) = cx.add_window_view({
554 let state = state.clone();
555 move |_, _| FixedPanelHarness {
556 width: px(840.),
557 fixed: true,
558 state,
559 }
560 });
561 cx.update(|window, cx| {
562 window.draw(cx).clear(cx);
563 window.draw(cx).clear(cx);
564 });
565 let widths = |cx: &mut VisualTestContext| {
566 ["fp-sidebar", "fp-main", "fp-aside"].map(|selector| {
567 cx.debug_bounds(selector)
568 .expect("every panel must lay out")
569 .size
570 .width
571 })
572 };
573 assert_eq!(widths(cx), [px(240.), px(400.), px(200.)]);
574
575 for (width, expected) in [
576 (px(1140.), Some([px(240.), px(600.), px(300.)])),
577 (px(200.), None),
578 (px(600.), Some([px(240.), px(240.), px(120.)])),
579 ] {
580 view.update(cx, |view, cx| {
581 view.width = width;
582 cx.notify();
583 });
584 cx.run_until_parked();
585 cx.update(|window, cx| window.draw(cx).clear(cx));
586 assert_eq!(widths(cx)[0], px(240.), "{width:?}");
587 if let Some(expected) = expected {
588 assert_eq!(widths(cx), expected, "{width:?}");
589 }
590 }
591
592 cx.update(|window, cx| {
595 state.update(cx, |state, cx| state.resize_panel(1, px(50.), window, cx));
596 window.draw(cx).clear(cx);
597 });
598 assert_eq!(widths(cx), [px(240.), px(100.), px(260.)]);
599
600 cx.update(|window, cx| {
603 state.update(cx, |state, cx| state.resize_panel(0, px(300.), window, cx));
604 window.draw(cx).clear(cx);
605 });
606 assert_eq!(widths(cx), [px(300.), px(100.), px(200.)]);
607 view.update(cx, |view, cx| {
608 view.width = px(900.);
609 cx.notify();
610 });
611 cx.run_until_parked();
612 cx.update(|window, cx| window.draw(cx).clear(cx));
613 assert_eq!(widths(cx)[0], px(300.));
614 }
615
616 #[gpui::test]
619 fn panel_made_fixed_in_place_keeps_its_size_across_a_container_resize(cx: &mut TestAppContext) {
620 let state = cx.update(|cx| cx.new(|_| ResizableState::default()));
621 let (view, cx) = cx.add_window_view({
622 let state = state.clone();
623 move |_, _| FixedPanelHarness {
624 width: px(840.),
625 fixed: false,
626 state,
627 }
628 });
629 let draw = |cx: &mut VisualTestContext, update: fn(&mut FixedPanelHarness)| {
630 view.update(cx, |view, cx| {
631 update(view);
632 cx.notify();
633 });
634 cx.run_until_parked();
635 cx.update(|window, cx| window.draw(cx).clear(cx));
636 };
637 draw(cx, |_| {});
638 draw(cx, |view| view.fixed = true);
639 draw(cx, |view| view.width = px(1140.));
640
641 let sidebar = cx
642 .debug_bounds("fp-sidebar")
643 .expect("the sidebar must lay out");
644 assert_eq!(sidebar.size.width, px(240.));
645 }
646
647 struct CallerStateHarness {
648 width: Pixels,
649 state: gpui::Entity<ResizableState>,
650 }
651
652 impl Render for CallerStateHarness {
653 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
654 div().w(self.width).h(px(100.)).child(
655 h_resizable("caller-state")
656 .with_state(&self.state)
657 .child(
658 resizable_panel()
659 .size(px(240.))
660 .child(div().size_full().debug_selector(|| "cs-sidebar".into())),
661 )
662 .child(resizable_panel().child(div().size_full())),
663 )
664 }
665 }
666
667 #[gpui::test]
671 fn caller_owned_state_settles_on_the_same_frame(cx: &mut TestAppContext) {
672 let state = cx.update(|cx| cx.new(|_| ResizableState::default()));
673 let (view, cx) = cx.add_window_view({
674 let state = state.clone();
675 move |_, _| CallerStateHarness {
676 width: px(800.),
677 state,
678 }
679 });
680 cx.update(|window, cx| {
681 window.draw(cx).clear(cx);
682 window.draw(cx).clear(cx);
683 });
684
685 view.update(cx, |view, cx| {
686 view.width = px(1200.);
687 cx.notify();
688 });
689 cx.run_until_parked();
690 let settled = cx.debug_bounds("cs-sidebar").unwrap().size.width;
691 cx.update(|window, cx| window.draw(cx).clear(cx));
692 let followup = cx.debug_bounds("cs-sidebar").unwrap().size.width;
693
694 assert_eq!(followup, settled, "settling frame must not be pending");
695 }
696
697 struct ResizableHarness {
698 state: gpui::Entity<ResizableState>,
699 resizes: Rc<Cell<usize>>,
700 }
701
702 impl Render for ResizableHarness {
703 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
704 div().w(px(400.)).h(px(100.)).child(
705 h_resizable("resizable")
706 .with_state(&self.state)
707 .on_resize({
708 let resizes = self.resizes.clone();
709 move |_, _, _| resizes.set(resizes.get() + 1)
710 })
711 .child(
712 resizable_panel()
713 .size(px(150.))
714 .child(div().size_full().debug_selector(|| "first-panel".into())),
715 )
716 .child(
717 resizable_panel()
718 .size(px(250.))
719 .child(div().size_full().debug_selector(|| "second-panel".into())),
720 ),
721 )
722 }
723 }
724
725 fn harness(
726 cx: &mut TestAppContext,
727 ) -> (
728 &mut VisualTestContext,
729 gpui::Entity<ResizableState>,
730 Rc<Cell<usize>>,
731 ) {
732 let state = cx.update(|cx| cx.new(|_| ResizableState::default()));
733 let resizes = Rc::new(Cell::new(0));
734 let (_, cx) = cx.add_window_view({
735 let state = state.clone();
736 let resizes = resizes.clone();
737 move |_, _| ResizableHarness { state, resizes }
738 });
739 cx.update(|window, cx| window.draw(cx).clear(cx));
740 cx.update(|window, cx| window.draw(cx).clear(cx));
741 (cx, state, resizes)
742 }
743
744 #[cfg(feature = "gpui-fast")]
746 struct StateReader {
747 state: gpui::Entity<ResizableState>,
748 renders: Rc<Cell<usize>>,
749 }
750
751 #[cfg(feature = "gpui-fast")]
752 impl Render for StateReader {
753 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
754 self.renders.set(self.renders.get() + 1);
755 let _ = self.state.read(cx).sizes.len();
756 div()
757 }
758 }
759
760 #[cfg(feature = "gpui-fast")]
762 struct ReaderBesidePanels {
763 state: gpui::Entity<ResizableState>,
764 reader: gpui::Entity<StateReader>,
765 }
766
767 #[cfg(feature = "gpui-fast")]
768 impl Render for ReaderBesidePanels {
769 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
770 div()
771 .w(px(800.))
772 .h(px(100.))
773 .child(self.reader.clone())
774 .child(
775 h_resizable("reader-panels")
776 .with_state(&self.state)
777 .child(resizable_panel().size(px(240.)).child(div().size_full()))
778 .child(resizable_panel().child(div().size_full())),
779 )
780 }
781 }
782
783 #[cfg(feature = "gpui-fast")]
787 #[gpui::test]
788 fn redrawing_panels_where_they_were_leaves_their_readers_retained(cx: &mut TestAppContext) {
789 let renders = Rc::new(Cell::new(0));
790 let state = cx.update(|cx| cx.new(|_| ResizableState::default()));
791 let (view, cx) = cx.add_window_view({
792 let state = state.clone();
793 let renders = renders.clone();
794 move |_, cx| ReaderBesidePanels {
795 state: state.clone(),
796 reader: cx.new(|_| StateReader { state, renders }),
797 }
798 });
799 for _ in 0..3 {
800 cx.update(|window, cx| window.draw(cx).clear(cx));
801 cx.run_until_parked();
802 }
803 let before = renders.get();
804 for _ in 0..3 {
805 view.update(cx, |_, cx| cx.notify());
806 cx.update(|window, cx| window.draw(cx).clear(cx));
807 cx.run_until_parked();
808 }
809 assert_eq!(renders.get(), before, "the reader rendered again");
810 }
811
812 #[gpui::test]
813 fn dynamic_panel_lifecycle_is_owned_by_resizable_state(cx: &mut TestAppContext) {
814 let state = cx.update(|cx| cx.new(|_| ResizableState::default()));
815
816 cx.update(|cx| {
817 state.update(cx, |state, cx| {
818 state.bounds.size = size(px(400.), px(100.));
819 state.panels.push(Default::default());
820 state.sizes.push(px(400.));
821 state.insert_panel(Some(px(200.)), None, cx);
822 assert_eq!(state.sizes(), &vec![px(200.), px(200.)]);
823
824 state.reset_panel(0, cx);
825 assert_eq!(state.sizes(), &vec![px(200.), px(200.)]);
826
827 state.remove_panel(0, cx);
828 assert_eq!(state.sizes(), &vec![px(400.)]);
829
830 state.clear();
831 assert!(state.sizes().is_empty());
832 });
833 });
834 }
835
836 #[gpui::test]
837 fn group_measures_panels_and_programmatic_resize_uses_drag_rules(cx: &mut TestAppContext) {
838 let (cx, state, _) = harness(cx);
839 let first = cx.debug_bounds("first-panel").unwrap();
840 let second = cx.debug_bounds("second-panel").unwrap();
841 assert_eq!(first.size.width + second.size.width, px(400.));
842
843 cx.update(|window, cx| {
844 state.update(cx, |state, cx| {
845 state.resize_panel(0, px(220.), window, cx);
846 });
847 window.draw(cx).clear(cx);
848 });
849
850 state.read_with(cx, |state, _| {
851 assert_eq!(state.sizes(), &vec![px(220.), px(180.)]);
852 });
853 }
854
855 #[gpui::test]
856 fn dragging_the_handle_resizes_and_emits_once(cx: &mut TestAppContext) {
857 let (cx, state, resizes) = harness(cx);
858 let boundary = cx.debug_bounds("second-panel").unwrap().left();
859
860 cx.simulate_mouse_down(
861 point(boundary - px(2.), px(50.)),
862 MouseButton::Left,
863 Modifiers::default(),
864 );
865 cx.simulate_mouse_move(
866 point(boundary + px(10.), px(50.)),
867 Some(MouseButton::Left),
868 Modifiers::default(),
869 );
870 cx.simulate_mouse_move(
871 point(px(220.), px(50.)),
872 Some(MouseButton::Left),
873 Modifiers::default(),
874 );
875 cx.simulate_mouse_up(
876 point(px(220.), px(50.)),
877 MouseButton::Left,
878 Modifiers::default(),
879 );
880
881 state.read_with(cx, |state, _| {
882 assert_eq!(state.sizes(), &vec![px(220.), px(180.)]);
883 });
884 assert_eq!(resizes.get(), 1);
885 }
886
887 struct HandleStateHarness {
893 seen: Rc<RefCell<Vec<ResizeHandleState>>>,
894 covered: bool,
895 }
896
897 impl Render for HandleStateHarness {
898 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
899 let seen = self.seen.clone();
900 div()
901 .relative()
902 .w(px(400.))
903 .h(px(100.))
904 .child(
905 h_resizable("handle-state")
906 .with_handle_appearance(Rc::new(
907 move |handle: &ResizeHandleContext, _: &mut Window, _: &mut App| {
908 let mut seen = seen.borrow_mut();
909 if seen.last() != Some(&handle.state()) {
910 seen.push(handle.state());
911 }
912 None
914 },
915 ))
916 .child(resizable_panel().size(px(150.)).child(div().size_full()))
917 .child(
918 resizable_panel()
919 .size(px(250.))
920 .child(div().size_full().debug_selector(|| "hs-second".into())),
921 ),
922 )
923 .when(self.covered, |this| {
924 this.child(div().absolute().inset_0().occlude())
925 })
926 }
927 }
928
929 #[gpui::test]
937 fn a_covered_handle_stays_idle(cx: &mut TestAppContext) {
938 let seen: Rc<RefCell<Vec<ResizeHandleState>>> = Rc::new(RefCell::new(Vec::new()));
939 let (_, cx) = cx.add_window_view({
940 let seen = seen.clone();
941 move |_, _| HandleStateHarness {
942 seen,
943 covered: true,
944 }
945 });
946 cx.update(|window, cx| window.draw(cx).clear(cx));
947 cx.update(|window, cx| window.draw(cx).clear(cx));
948
949 let boundary = cx.debug_bounds("hs-second").unwrap().left();
950 let on_handle = point(boundary - px(2.), px(50.));
951 let draw = |cx: &mut VisualTestContext| cx.update(|window, cx| window.draw(cx).clear(cx));
952
953 cx.simulate_mouse_move(on_handle, None, Modifiers::default());
954 draw(cx);
955 cx.simulate_mouse_down(on_handle, MouseButton::Left, Modifiers::default());
956 draw(cx);
957 cx.simulate_mouse_up(on_handle, MouseButton::Left, Modifiers::default());
958 draw(cx);
959
960 assert_eq!(*seen.borrow(), vec![ResizeHandleState::Idle]);
961 }
962
963 #[gpui::test]
964 fn a_handle_reports_the_press_and_the_drag_to_its_renderer(cx: &mut TestAppContext) {
965 let seen: Rc<RefCell<Vec<ResizeHandleState>>> = Rc::new(RefCell::new(Vec::new()));
966 let (_, cx) = cx.add_window_view({
967 let seen = seen.clone();
968 move |_, _| HandleStateHarness {
969 seen,
970 covered: false,
971 }
972 });
973 cx.update(|window, cx| window.draw(cx).clear(cx));
974 cx.update(|window, cx| window.draw(cx).clear(cx));
975
976 let boundary = cx.debug_bounds("hs-second").unwrap().left();
977 let on_handle = point(boundary - px(2.), px(50.));
978 let draw = |cx: &mut VisualTestContext| cx.update(|window, cx| window.draw(cx).clear(cx));
979
980 cx.simulate_mouse_move(on_handle, None, Modifiers::default());
981 draw(cx);
982 cx.simulate_mouse_down(on_handle, MouseButton::Left, Modifiers::default());
983 draw(cx);
984 cx.simulate_mouse_move(
985 point(px(260.), px(50.)),
986 Some(MouseButton::Left),
987 Modifiers::default(),
988 );
989 draw(cx);
990 cx.simulate_mouse_up(
991 point(px(390.), px(90.)),
992 MouseButton::Left,
993 Modifiers::default(),
994 );
995 draw(cx);
996
997 assert_eq!(
1001 *seen.borrow(),
1002 vec![
1003 ResizeHandleState::Idle,
1004 ResizeHandleState::Hovered,
1005 ResizeHandleState::Pressed,
1006 ResizeHandleState::Dragging,
1007 ResizeHandleState::Idle,
1008 ]
1009 );
1010 }
1011
1012 struct SizedGroupHarness;
1013
1014 impl Render for SizedGroupHarness {
1015 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1016 div()
1017 .w(px(400.))
1018 .h(px(100.))
1019 .child(
1020 h_resizable("sized-resizable").size(px(40.)).child(
1021 resizable_panel()
1022 .child(div().size_full().debug_selector(|| "sized-panel".into())),
1023 ),
1024 )
1025 }
1026 }
1027
1028 #[gpui::test]
1029 fn a_group_size_binds_the_cross_axis(cx: &mut TestAppContext) {
1030 let (_, cx) = cx.add_window_view(|_, _| SizedGroupHarness);
1031 cx.update(|window, cx| window.draw(cx).clear(cx));
1032 cx.update(|window, cx| window.draw(cx).clear(cx));
1033
1034 let panel = cx.debug_bounds("sized-panel").unwrap();
1035 assert_eq!(panel.size.width, px(400.));
1036 assert_eq!(panel.size.height, px(40.));
1037 }
1038}