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, InputEvent as _, InteractiveElement as _, IntoElement,
458 Modifiers, 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 #[gpui::test]
888 fn dragging_before_the_next_paint_resizes_and_emits_once(cx: &mut TestAppContext) {
889 let (cx, state, resizes) = harness(cx);
890 let boundary = cx.debug_bounds("second-panel").unwrap().left();
891
892 cx.update(|window, cx| {
895 window.dispatch_event(
896 gpui::MouseDownEvent {
897 position: point(boundary - px(2.), px(50.)),
898 button: MouseButton::Left,
899 click_count: 1,
900 ..Default::default()
901 }
902 .to_platform_input(),
903 cx,
904 );
905 for x in [boundary + px(10.), px(220.)] {
906 window.dispatch_event(
907 gpui::MouseMoveEvent {
908 position: point(x, px(50.)),
909 pressed_button: Some(MouseButton::Left),
910 ..Default::default()
911 }
912 .to_platform_input(),
913 cx,
914 );
915 }
916 assert_eq!(state.read(cx).resizing_panel_ix, Some(0));
917 assert_eq!(state.read(cx).sizes(), &vec![px(220.), px(180.)]);
918 assert_eq!(resizes.get(), 0);
919
920 for _ in 0..2 {
923 window.dispatch_event(
924 gpui::MouseUpEvent {
925 position: point(px(220.), px(50.)),
926 button: MouseButton::Left,
927 click_count: 1,
928 ..Default::default()
929 }
930 .to_platform_input(),
931 cx,
932 );
933 }
934 window.dispatch_event(
935 gpui::MouseMoveEvent {
936 position: point(px(260.), px(50.)),
937 ..Default::default()
938 }
939 .to_platform_input(),
940 cx,
941 );
942 assert_eq!(state.read(cx).resizing_panel_ix, None);
943 assert_eq!(state.read(cx).sizes(), &vec![px(220.), px(180.)]);
944 assert_eq!(resizes.get(), 1);
945 });
946 }
947
948 #[gpui::test]
949 fn cancelled_drag_does_not_reuse_the_painted_index(cx: &mut TestAppContext) {
950 let (cx, state, resizes) = harness(cx);
951 let boundary = cx.debug_bounds("second-panel").unwrap().left();
952 cx.simulate_mouse_down(
953 point(boundary - px(2.), px(50.)),
954 MouseButton::Left,
955 Modifiers::default(),
956 );
957 cx.simulate_mouse_move(
958 point(boundary + px(10.), px(50.)),
959 Some(MouseButton::Left),
960 Modifiers::default(),
961 );
962 cx.update(|window, cx| {
963 assert_eq!(state.read(cx).resizing_panel_ix, Some(0));
964 window.draw(cx).clear(cx);
965 state.update(cx, |state, cx| state.resize_panel(0, px(210.), window, cx));
966 cx.stop_active_drag(window);
967 assert_eq!(state.read(cx).resizing_panel_ix, None);
968
969 window.dispatch_event(
972 gpui::MouseMoveEvent {
973 position: point(px(260.), px(50.)),
974 pressed_button: Some(MouseButton::Left),
975 ..Default::default()
976 }
977 .to_platform_input(),
978 cx,
979 );
980 window.dispatch_event(
981 gpui::MouseUpEvent {
982 position: point(px(260.), px(50.)),
983 button: MouseButton::Left,
984 click_count: 1,
985 ..Default::default()
986 }
987 .to_platform_input(),
988 cx,
989 );
990 assert_eq!(state.read(cx).sizes(), &vec![px(210.), px(190.)]);
991 assert_eq!(state.read(cx).resizing_panel_ix, None);
992 assert_eq!(resizes.get(), 0);
993 });
994 }
995
996 struct HandleStateHarness {
1002 seen: Rc<RefCell<Vec<ResizeHandleState>>>,
1003 covered: bool,
1004 }
1005
1006 impl Render for HandleStateHarness {
1007 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1008 let seen = self.seen.clone();
1009 div()
1010 .relative()
1011 .w(px(400.))
1012 .h(px(100.))
1013 .child(
1014 h_resizable("handle-state")
1015 .with_handle_appearance(Rc::new(
1016 move |handle: &ResizeHandleContext, _: &mut Window, _: &mut App| {
1017 let mut seen = seen.borrow_mut();
1018 if seen.last() != Some(&handle.state()) {
1019 seen.push(handle.state());
1020 }
1021 None
1023 },
1024 ))
1025 .child(resizable_panel().size(px(150.)).child(div().size_full()))
1026 .child(
1027 resizable_panel()
1028 .size(px(250.))
1029 .child(div().size_full().debug_selector(|| "hs-second".into())),
1030 ),
1031 )
1032 .when(self.covered, |this| {
1033 this.child(div().absolute().inset_0().occlude())
1034 })
1035 }
1036 }
1037
1038 #[gpui::test]
1046 fn a_covered_handle_stays_idle(cx: &mut TestAppContext) {
1047 let seen: Rc<RefCell<Vec<ResizeHandleState>>> = Rc::new(RefCell::new(Vec::new()));
1048 let (_, cx) = cx.add_window_view({
1049 let seen = seen.clone();
1050 move |_, _| HandleStateHarness {
1051 seen,
1052 covered: true,
1053 }
1054 });
1055 cx.update(|window, cx| window.draw(cx).clear(cx));
1056 cx.update(|window, cx| window.draw(cx).clear(cx));
1057
1058 let boundary = cx.debug_bounds("hs-second").unwrap().left();
1059 let on_handle = point(boundary - px(2.), px(50.));
1060 let draw = |cx: &mut VisualTestContext| cx.update(|window, cx| window.draw(cx).clear(cx));
1061
1062 cx.simulate_mouse_move(on_handle, None, Modifiers::default());
1063 draw(cx);
1064 cx.simulate_mouse_down(on_handle, MouseButton::Left, Modifiers::default());
1065 draw(cx);
1066 cx.simulate_mouse_up(on_handle, MouseButton::Left, Modifiers::default());
1067 draw(cx);
1068
1069 assert_eq!(*seen.borrow(), vec![ResizeHandleState::Idle]);
1070 }
1071
1072 #[gpui::test]
1073 fn a_handle_reports_the_press_and_the_drag_to_its_renderer(cx: &mut TestAppContext) {
1074 let seen: Rc<RefCell<Vec<ResizeHandleState>>> = Rc::new(RefCell::new(Vec::new()));
1075 let (_, cx) = cx.add_window_view({
1076 let seen = seen.clone();
1077 move |_, _| HandleStateHarness {
1078 seen,
1079 covered: false,
1080 }
1081 });
1082 cx.update(|window, cx| window.draw(cx).clear(cx));
1083 cx.update(|window, cx| window.draw(cx).clear(cx));
1084
1085 let boundary = cx.debug_bounds("hs-second").unwrap().left();
1086 let on_handle = point(boundary - px(2.), px(50.));
1087 let draw = |cx: &mut VisualTestContext| cx.update(|window, cx| window.draw(cx).clear(cx));
1088
1089 cx.simulate_mouse_move(on_handle, None, Modifiers::default());
1090 draw(cx);
1091 cx.simulate_mouse_down(on_handle, MouseButton::Left, Modifiers::default());
1092 draw(cx);
1093 cx.simulate_mouse_move(
1094 point(px(260.), px(50.)),
1095 Some(MouseButton::Left),
1096 Modifiers::default(),
1097 );
1098 draw(cx);
1099 cx.simulate_mouse_up(
1100 point(px(390.), px(90.)),
1101 MouseButton::Left,
1102 Modifiers::default(),
1103 );
1104 draw(cx);
1105
1106 assert_eq!(
1110 *seen.borrow(),
1111 vec![
1112 ResizeHandleState::Idle,
1113 ResizeHandleState::Hovered,
1114 ResizeHandleState::Pressed,
1115 ResizeHandleState::Dragging,
1116 ResizeHandleState::Idle,
1117 ]
1118 );
1119 }
1120
1121 struct SizedGroupHarness;
1122
1123 impl Render for SizedGroupHarness {
1124 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1125 div()
1126 .w(px(400.))
1127 .h(px(100.))
1128 .child(
1129 h_resizable("sized-resizable").size(px(40.)).child(
1130 resizable_panel()
1131 .child(div().size_full().debug_selector(|| "sized-panel".into())),
1132 ),
1133 )
1134 }
1135 }
1136
1137 #[gpui::test]
1138 fn a_group_size_binds_the_cross_axis(cx: &mut TestAppContext) {
1139 let (_, cx) = cx.add_window_view(|_, _| SizedGroupHarness);
1140 cx.update(|window, cx| window.draw(cx).clear(cx));
1141 cx.update(|window, cx| window.draw(cx).clear(cx));
1142
1143 let panel = cx.debug_bounds("sized-panel").unwrap();
1144 assert_eq!(panel.size.width, px(400.));
1145 assert_eq!(panel.size.height, px(40.));
1146 }
1147}