1use std::cell::Cell;
63use std::rc::Rc;
64use std::sync::Arc;
65
66use gpui::{
67 div, prelude::FluentBuilder as _, px, AnyElement, App, Bounds, ElementId, InteractiveElement,
68 IntoElement, MouseButton, ParentElement, Pixels, Point, RenderOnce, SharedString, Styled,
69 Window,
70};
71use herogpui_core::{element_id, Orientation};
72use herogpui_theme::ActiveTheme;
73
74use crate::a11y::{self, A11y as _};
75
76type ResizeCallback = Arc<dyn Fn(&[f32], &mut Window, &mut App) + 'static>;
77
78pub const RESIZABLE_HANDLE_SIZE: Pixels = px(8.);
81
82#[must_use = "builder methods return a new value; pass it on to its component"]
85pub struct ResizablePanel {
86 default_size: Option<f32>,
87 min_size: f32,
88 max_size: f32,
89 min_size_px: Option<Pixels>,
90 max_size_px: Option<Pixels>,
91 collapsible: bool,
92 collapsed_size: f32,
93 children: Vec<AnyElement>,
94}
95
96impl Default for ResizablePanel {
97 fn default() -> Self {
98 Self::new()
99 }
100}
101
102impl ResizablePanel {
103 pub fn new() -> Self {
106 Self {
107 default_size: None,
108 min_size: 0.,
109 max_size: 100.,
110 min_size_px: None,
111 max_size_px: None,
112 collapsible: false,
113 collapsed_size: 0.,
114 children: Vec::new(),
115 }
116 }
117
118 pub fn default_size(mut self, percent: f32) -> Self {
121 self.default_size = Some(percent.max(0.));
122 self
123 }
124
125 pub fn min_size(mut self, percent: f32) -> Self {
128 self.min_size = percent.clamp(0., 100.);
129 self
130 }
131
132 pub fn max_size(mut self, percent: f32) -> Self {
135 self.max_size = percent.clamp(0., 100.);
136 self
137 }
138
139 pub fn min_size_px(mut self, min: impl Into<Pixels>) -> Self {
142 self.min_size_px = Some(min.into().max(px(0.)));
143 self
144 }
145
146 pub fn max_size_px(mut self, max: impl Into<Pixels>) -> Self {
150 self.max_size_px = Some(max.into().max(px(0.)));
151 self
152 }
153
154 pub fn collapsible(mut self, collapsible: bool) -> Self {
158 self.collapsible = collapsible;
159 self
160 }
161
162 pub fn collapsed_size(mut self, percent: f32) -> Self {
166 self.collapsed_size = percent.clamp(0., 100.);
167 self
168 }
169
170 fn limits(&self, available: Option<f32>) -> Limit {
174 let mut min = self.min_size;
175 let mut max = self.max_size;
176 if let Some(available) = available.filter(|a| *a > 0.) {
177 if let Some(px_min) = self.min_size_px {
178 min = min.max(f32::from(px_min) / available * 100.);
179 }
180 if let Some(px_max) = self.max_size_px {
181 max = max.min(f32::from(px_max) / available * 100.);
182 }
183 }
184 let min = min.clamp(0., 100.);
185 let max = max.clamp(0., 100.).max(min);
186 let collapsed = self
187 .collapsible
188 .then_some(self.collapsed_size.min(min))
189 .filter(|collapsed| *collapsed < min - EPSILON);
190 Limit {
191 min,
192 max,
193 collapsed,
194 }
195 }
196
197 fn has_pixel_limits(&self) -> bool {
198 self.min_size_px.is_some() || self.max_size_px.is_some()
199 }
200}
201
202const EPSILON: f32 = 0.0001;
204
205#[derive(Clone, Copy, Debug, PartialEq)]
208struct Limit {
209 min: f32,
210 max: f32,
211 collapsed: Option<f32>,
212}
213
214impl Limit {
215 #[cfg(test)]
216 fn range(min: f32, max: f32) -> Self {
217 Self {
218 min,
219 max,
220 collapsed: None,
221 }
222 }
223
224 fn pieces(self) -> impl Iterator<Item = (f32, f32)> {
226 std::iter::once((self.min, self.max)).chain(self.collapsed.map(|c| (c, c)))
227 }
228
229 fn is_collapsed(self, size: f32) -> bool {
230 self.collapsed
231 .is_some_and(|collapsed| (size - collapsed).abs() <= 0.01)
232 }
233}
234
235impl ParentElement for ResizablePanel {
236 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
237 self.children.extend(elements);
238 }
239}
240
241#[must_use = "a component does nothing until it is rendered: add it as a child or return it from `render`"]
244#[derive(IntoElement)]
245pub struct ResizablePanelGroup {
246 id: ElementId,
247 orientation: Orientation,
248 panels: Vec<ResizablePanel>,
249 sizes: Option<Vec<f32>>,
251 keyboard_step: f32,
252 is_disabled: bool,
253 on_resize: Option<ResizeCallback>,
254 on_resize_end: Option<ResizeCallback>,
255}
256
257impl ResizablePanelGroup {
258 pub fn new(id: impl Into<ElementId>) -> Self {
261 Self {
262 id: id.into(),
263 orientation: Orientation::Horizontal,
264 panels: Vec::new(),
265 sizes: None,
266 keyboard_step: 5.,
267 is_disabled: false,
268 on_resize: None,
269 on_resize_end: None,
270 }
271 }
272
273 pub fn orientation(mut self, orientation: Orientation) -> Self {
275 self.orientation = orientation;
276 self
277 }
278
279 pub fn panel(mut self, panel: ResizablePanel) -> Self {
281 self.panels.push(panel);
282 self
283 }
284
285 pub fn panels(mut self, panels: impl IntoIterator<Item = ResizablePanel>) -> Self {
287 self.panels.extend(panels);
288 self
289 }
290
291 pub fn sizes(mut self, sizes: impl IntoIterator<Item = f32>) -> Self {
298 self.sizes = Some(sizes.into_iter().collect());
299 self
300 }
301
302 pub fn keyboard_step(mut self, percent: f32) -> Self {
305 self.keyboard_step = percent.max(0.);
306 self
307 }
308
309 pub fn is_disabled(mut self, disabled: bool) -> Self {
312 self.is_disabled = disabled;
313 self
314 }
315
316 pub fn on_resize(mut self, f: impl Fn(&[f32], &mut Window, &mut App) + 'static) -> Self {
319 self.on_resize = Some(Arc::new(f));
320 self
321 }
322
323 pub fn on_resize_end(mut self, f: impl Fn(&[f32], &mut Window, &mut App) + 'static) -> Self {
328 self.on_resize_end = Some(Arc::new(f));
329 self
330 }
331}
332
333fn default_sizes(panels: &[ResizablePanel], limits: &[Limit]) -> Vec<f32> {
336 let fixed: f32 = panels
337 .iter()
338 .filter_map(|p| p.default_size)
339 .filter(|size| size.is_finite())
340 .sum();
341 let free = panels.iter().filter(|p| p.default_size.is_none()).count();
342 let share = if free > 0 {
343 (100. - fixed).max(0.) / free as f32
344 } else {
345 0.
346 };
347 let sizes: Vec<f32> = panels
348 .iter()
349 .map(|p| p.default_size.unwrap_or(share))
350 .collect();
351 normalize(&sizes, limits)
352}
353
354fn normalize(sizes: &[f32], limits: &[Limit]) -> Vec<f32> {
363 let n = sizes.len();
364 if n == 0 {
365 return Vec::new();
366 }
367 let mut out: Vec<f32> = sizes
368 .iter()
369 .map(|&v| if v.is_finite() && v > 0. { v } else { 0. })
370 .collect();
371 let total: f32 = out.iter().sum();
372 if !total.is_finite() || total <= 0. {
373 out = vec![100. / n as f32; n];
374 } else if (total - 100.).abs() > 0.001 {
375 for size in &mut out {
376 *size *= 100. / total;
377 }
378 }
379 if limits.len() != n {
380 return out;
381 }
382 let limits: Vec<(f32, f32)> = out
383 .iter()
384 .zip(limits)
385 .map(|(&size, limit)| match limit.collapsed {
386 Some(collapsed) if size < (collapsed + limit.min) / 2. => (collapsed, collapsed),
387 _ => (limit.min, limit.max),
388 })
389 .collect();
390 let limits = &limits[..];
391 let min_total: f32 = limits.iter().map(|l| l.0).sum();
392 let max_total: f32 = limits.iter().map(|l| l.1).sum();
393 if min_total > 100. + 0.001 || max_total < 100. - 0.001 {
394 return out;
395 }
396 for (size, &(min, max)) in out.iter_mut().zip(limits) {
397 *size = size.clamp(min, max);
398 }
399 let diff = 100. - out.iter().sum::<f32>();
400 if diff.abs() > 0.0001 {
401 let room: Vec<f32> = out
402 .iter()
403 .zip(limits)
404 .map(|(&size, &(min, max))| if diff > 0. { max - size } else { size - min })
405 .collect();
406 let total_room: f32 = room.iter().sum();
407 if total_room > 0. {
408 for (size, room) in out.iter_mut().zip(room) {
409 *size += diff * room / total_room;
410 }
411 }
412 }
413 out
414}
415
416fn pair_pieces(sizes: &[f32], handle: usize, limits: &[Limit]) -> Vec<(f32, f32)> {
420 let total = sizes[handle] + sizes[handle + 1];
421 let mut pieces = Vec::new();
422 for (min_a, max_a) in limits[handle].pieces() {
423 for (min_b, max_b) in limits[handle + 1].pieces() {
424 let lower = min_a.max(total - max_b);
425 let upper = max_a.min(total - min_b);
426 if lower <= upper + EPSILON {
427 pieces.push((lower, upper.max(lower)));
428 }
429 }
430 }
431 pieces
432}
433
434fn pair_range(sizes: &[f32], handle: usize, limits: &[Limit]) -> Option<(f32, f32)> {
437 let pieces = pair_pieces(sizes, handle, limits);
438 let lower = pieces.iter().map(|p| p.0).reduce(f32::min)?;
439 let upper = pieces.iter().map(|p| p.1).reduce(f32::max)?;
440 Some((lower, upper))
441}
442
443fn nearest(pieces: &[(f32, f32)], target: f32) -> Option<f32> {
446 pieces
447 .iter()
448 .map(|&(lower, upper)| target.clamp(lower, upper))
449 .min_by(|a, b| (a - target).abs().total_cmp(&(b - target).abs()))
450}
451
452fn resize_pair(sizes: &[f32], handle: usize, target: f32, limits: &[Limit]) -> Vec<f32> {
455 let mut next = sizes.to_vec();
456 let Some(first) = nearest(&pair_pieces(sizes, handle, limits), target) else {
457 return next;
458 };
459 let total = sizes[handle] + sizes[handle + 1];
460 next[handle] = first;
461 next[handle + 1] = total - first;
462 next
463}
464
465fn step_pair(sizes: &[f32], handle: usize, target: f32, limits: &[Limit]) -> Vec<f32> {
470 let current = sizes[handle];
471 let mut next = resize_pair(sizes, handle, target, limits);
472 let pieces = pair_pieces(sizes, handle, limits);
473 let jump = if target > current + EPSILON && next[handle] <= current + EPSILON {
474 pieces
475 .iter()
476 .map(|p| p.0)
477 .filter(|lower| *lower > current + EPSILON)
478 .reduce(f32::min)
479 } else if target < current - EPSILON && next[handle] >= current - EPSILON {
480 pieces
481 .iter()
482 .map(|p| p.1)
483 .filter(|upper| *upper < current - EPSILON)
484 .reduce(f32::max)
485 } else {
486 None
487 };
488 if let Some(first) = jump {
489 let total = sizes[handle] + sizes[handle + 1];
490 next[handle] = first;
491 next[handle + 1] = total - first;
492 }
493 next
494}
495
496fn toggle_collapse(
501 sizes: &[f32],
502 handle: usize,
503 limits: &[Limit],
504 restore: &[Option<f32>],
505) -> Option<Vec<f32>> {
506 let panel = if limits[handle].collapsed.is_some() {
507 handle
508 } else if limits[handle + 1].collapsed.is_some() {
509 handle + 1
510 } else {
511 return None;
512 };
513 let limit = limits[panel];
514 let collapsed = limit.collapsed?;
515 let size = if limit.is_collapsed(sizes[panel]) {
516 restore
517 .get(panel)
518 .copied()
519 .flatten()
520 .unwrap_or(limit.min)
521 .max(limit.min)
522 } else {
523 collapsed
524 };
525 let total = sizes[handle] + sizes[handle + 1];
526 let first = if panel == handle { size } else { total - size };
527 Some(resize_pair(sizes, handle, first, limits))
528}
529
530#[derive(Clone)]
537struct Drag {
538 handle: usize,
539 origin: f32,
540 start: Vec<f32>,
541 last: Vec<f32>,
542}
543
544#[derive(Clone)]
546struct GroupState {
547 store: gpui::Entity<Vec<f32>>,
548 controlled: bool,
549 limits: Rc<Vec<Limit>>,
550 restore: gpui::Entity<Vec<Option<f32>>>,
552 on_resize: Option<ResizeCallback>,
553 on_resize_end: Option<ResizeCallback>,
554}
555
556impl GroupState {
557 fn apply(&self, now: &[f32], next: Vec<f32>, window: &mut Window, cx: &mut App) -> bool {
561 if next.len() != now.len()
562 || next
563 .iter()
564 .zip(now)
565 .all(|(a, b)| (a - b).abs() <= f32::EPSILON)
566 {
567 return false;
568 }
569 let collapsing: Vec<(usize, f32)> = now
571 .iter()
572 .zip(&next)
573 .zip(self.limits.iter())
574 .enumerate()
575 .filter(|(_, ((was, is), limit))| {
576 !limit.is_collapsed(**was) && limit.is_collapsed(**is)
577 })
578 .map(|(ix, ((was, _), _))| (ix, *was))
579 .collect();
580 if !collapsing.is_empty() {
581 self.restore.update(cx, |restore, _| {
582 for (ix, size) in collapsing {
583 if let Some(slot) = restore.get_mut(ix) {
584 *slot = Some(size);
585 }
586 }
587 });
588 }
589 if !self.controlled {
590 self.store.update(cx, |sizes, cx| {
591 sizes.clone_from(&next);
592 cx.notify();
593 });
594 }
595 if let Some(cb) = &self.on_resize {
596 cb(&next, window, cx);
597 }
598 true
599 }
600
601 fn resize_ended(&self, sizes: &[f32], window: &mut Window, cx: &mut App) {
602 if let Some(cb) = &self.on_resize_end {
603 cb(sizes, window, cx);
604 }
605 }
606}
607
608fn selector_base(id: &ElementId) -> String {
610 match id {
611 ElementId::Name(name) => name.to_string(),
612 other => format!("{other:?}"),
613 }
614}
615
616impl RenderOnce for ResizablePanelGroup {
617 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
618 let base = self.id.clone();
619 let selector = selector_base(&base);
620 let count = self.panels.len();
621 let horizontal = self.orientation == Orientation::Horizontal;
622 let group_bounds = window
623 .use_keyed_state(element_id::scoped(&base, "bounds"), cx, |_, _| {
624 Rc::new(Cell::new(None::<Bounds<Pixels>>))
625 })
626 .read(cx)
627 .clone();
628 let handle_total = f32::from(RESIZABLE_HANDLE_SIZE) * count.saturating_sub(1) as f32;
629 let available = group_bounds.get().map(|bounds| {
632 f32::from(if horizontal {
633 bounds.size.width
634 } else {
635 bounds.size.height
636 }) - handle_total
637 });
638 let has_pixel_limits = self.panels.iter().any(ResizablePanel::has_pixel_limits);
639 let limits: Rc<Vec<Limit>> = Rc::new(
640 self.panels
641 .iter()
642 .map(|panel| panel.limits(available))
643 .collect(),
644 );
645 let defaults = default_sizes(&self.panels, &limits);
646
647 let store = window.use_keyed_state(element_id::scoped(&base, "sizes"), cx, {
648 let defaults = defaults.clone();
649 move |_, _| defaults
650 });
651 if store.read(cx).len() != count {
652 store.update(cx, |sizes, _| sizes.clone_from(&defaults));
653 }
654 let controlled = self.sizes.as_ref().is_some_and(|s| s.len() == count);
655 let sizes: Vec<f32> = match &self.sizes {
656 Some(sizes) if controlled => normalize(sizes, &limits),
657 _ => {
658 let stored = store.read(cx).clone();
661 let normal = normalize(&stored, &limits);
662 if normal != stored {
663 store.update(cx, |sizes, _| sizes.clone_from(&normal));
664 }
665 normal
666 }
667 };
668 let drag =
669 window.use_keyed_state(element_id::scoped(&base, "drag"), cx, |_, _| None::<Drag>);
670 if drag
672 .read(cx)
673 .as_ref()
674 .is_some_and(|d| d.handle + 1 >= count || d.start.len() != count)
675 {
676 drag.update(cx, |value, _| *value = None);
677 }
678 let restore = window.use_keyed_state(element_id::scoped(&base, "restore"), cx, |_, _| {
679 Vec::<Option<f32>>::new()
680 });
681 if restore.read(cx).len() != count {
682 restore.update(cx, |restore, _| *restore = vec![None; count]);
683 }
684 let handles: Vec<gpui::FocusHandle> = (0..count.saturating_sub(1))
685 .map(|ix| {
686 window
687 .use_keyed_state(
688 element_id::indexed(&base, "handle-focus", ix),
689 cx,
690 |_, cx| cx.focus_handle(),
691 )
692 .read(cx)
693 .clone()
694 .tab_stop(!self.is_disabled)
695 })
696 .collect();
697
698 let state = GroupState {
699 store,
700 controlled,
701 limits: limits.clone(),
702 restore,
703 on_resize: self.on_resize.clone(),
704 on_resize_end: self.on_resize_end.clone(),
705 };
706 let dragging = drag.read(cx).as_ref().map(|d| d.handle);
707
708 let colors = cx.colors().clone();
709 let focus_visible = crate::util::focus_visible(cx);
710 let cursor_style = if horizontal {
711 gpui::CursorStyle::ResizeColumn
712 } else {
713 gpui::CursorStyle::ResizeRow
714 };
715 let separator_orientation = if horizontal {
718 Orientation::Vertical
719 } else {
720 Orientation::Horizontal
721 };
722 let axis = move |point: Point<Pixels>| -> f32 {
723 f32::from(if horizontal { point.x } else { point.y })
724 };
725
726 let mut root = div()
727 .id(base.clone())
728 .relative()
729 .flex()
730 .when(horizontal, |el| el.flex_row())
731 .when(!horizontal, |el| el.flex_col())
732 .size_full()
733 .overflow_hidden()
734 .debug_selector({
735 let name = format!("{selector}-group");
736 move || name
737 });
738
739 let panels = self.panels;
740 for (ix, panel) in panels.into_iter().enumerate() {
741 let size = sizes.get(ix).copied().unwrap_or(0.);
742 root = root.child(
743 div()
744 .flex()
745 .flex_col()
746 .flex_basis(px(0.))
747 .flex_grow(size)
748 .flex_shrink(1.)
749 .min_w_0()
750 .min_h_0()
751 .overflow_hidden()
752 .debug_selector({
753 let name = format!("{selector}-panel-{ix}");
754 move || name
755 })
756 .children(panel.children),
757 );
758 if ix + 1 >= count {
759 continue;
760 }
761
762 let handle = handles[ix].clone();
763 let focused = handle.is_focused(window);
764 let active = dragging == Some(ix) || (focused && focus_visible);
765 let (lower, upper) = pair_range(&sizes, ix, &limits).unwrap_or((size, size));
766 let group_name = SharedString::from(format!("{selector}-handle-{ix}"));
767 let mut handle_el = div()
768 .id(element_id::indexed(&base, "handle", ix))
769 .a11y(a11y::Role::Splitter)
774 .a11y_orientation(separator_orientation)
775 .a11y_range(
776 &a11y::Range::new(lower as f64, upper as f64, size as f64)
777 .step(self.keyboard_step as f64),
778 )
779 .group(group_name.clone())
780 .relative()
781 .flex()
782 .items_center()
783 .justify_center()
784 .flex_shrink_0()
785 .when(horizontal, |el| el.w(RESIZABLE_HANDLE_SIZE).h_full())
786 .when(!horizontal, |el| el.h(RESIZABLE_HANDLE_SIZE).w_full())
787 .debug_selector({
788 let name = group_name.to_string();
789 move || name
790 })
791 .child(
792 div()
793 .when(horizontal, |el| el.w(px(1.)).h_full())
794 .when(!horizontal, |el| el.h(px(1.)).w_full())
795 .bg(if active {
796 colors.accent.color
797 } else {
798 colors.separator
799 })
800 .when(!self.is_disabled, |line| {
801 line.group_hover(group_name.clone(), |s| s.bg(colors.muted))
802 }),
803 );
804 if !self.is_disabled {
805 handle_el = handle_el
806 .track_focus(&handle)
807 .cursor(cursor_style)
808 .when(focused && focus_visible, |el| {
809 el.child(crate::util::focus_ring_overlay(px(2.), false, cx))
810 });
811
812 let is_dragging = dragging == Some(ix);
817 handle_el = handle_el.child(
818 gpui::canvas(
819 |bounds, window, _| {
820 window.insert_hitbox(bounds, gpui::HitboxBehavior::Normal)
821 },
822 move |_, hitbox, window, _| {
823 if is_dragging {
824 window.capture_pointer(hitbox.id);
825 window.set_cursor_style(cursor_style, &hitbox);
826 }
827 },
828 )
829 .absolute()
830 .inset_0(),
831 );
832
833 let press_drag = drag.clone();
834 let press_focus = handle.clone();
835 let press_sizes = sizes.clone();
836 handle_el = handle_el.on_mouse_down(MouseButton::Left, move |event, window, cx| {
837 crate::util::set_focus_visible(false, cx);
838 window.focus(&press_focus, cx);
839 press_drag.update(cx, |value, cx| {
840 *value = Some(Drag {
841 handle: ix,
842 origin: axis(event.position),
843 start: press_sizes.clone(),
844 last: press_sizes.clone(),
845 });
846 cx.notify();
847 });
848 cx.stop_propagation();
849 });
850
851 let keys = state.clone();
852 let key_focus = handle.clone();
853 let key_sizes = sizes.clone();
854 let step = self.keyboard_step;
855 handle_el = handle_el.on_key_down(move |event, window, cx| {
856 if !key_focus.is_focused(window) {
857 return;
858 }
859 let m = &event.keystroke.modifiers;
860 if m.control || m.alt || m.platform || m.function {
861 return;
862 }
863 let (decrease, increase) = if horizontal {
864 ("left", "right")
865 } else {
866 ("up", "down")
867 };
868 let delta = if m.shift { step * 4. } else { step };
869 let current = key_sizes[ix];
870 let next = match event.keystroke.key.as_str() {
871 k if k == decrease => {
872 step_pair(&key_sizes, ix, current - delta, &keys.limits)
873 }
874 k if k == increase => {
875 step_pair(&key_sizes, ix, current + delta, &keys.limits)
876 }
877 "home" if !m.shift => resize_pair(&key_sizes, ix, -1., &keys.limits),
879 "end" if !m.shift => resize_pair(&key_sizes, ix, 101., &keys.limits),
880 "enter" if !m.shift => {
881 let Some(next) = toggle_collapse(
882 &key_sizes,
883 ix,
884 &keys.limits,
885 keys.restore.read(cx),
886 ) else {
887 return;
888 };
889 next
890 }
891 _ => return,
892 };
893 crate::util::set_focus_visible(true, cx);
894 if keys.apply(&key_sizes, next.clone(), window, cx) {
895 keys.resize_ended(&next, window, cx);
896 }
897 cx.stop_propagation();
898 });
899 }
900 if !self.is_disabled {
901 handle_el = crate::util::record_focus_bounds(handle_el, &handle, window, cx);
902 }
903 root = root.child(handle_el);
904 }
905
906 let probe = group_bounds.clone();
909 let measure = move |bounds: Bounds<Pixels>, window: &mut Window, _: &mut App| {
910 let changed = probe.get().map(|b| b.size) != Some(bounds.size);
911 probe.set(Some(bounds));
912 if changed && has_pixel_limits {
913 window.request_animation_frame();
914 }
915 };
916 if self.is_disabled || count <= 1 {
919 root = root.child(gpui::canvas(measure, |_, _, _, _| {}).absolute().inset_0());
920 } else {
921 let move_drag = drag.clone();
922 let up_drag = drag;
923 let up_state = state.clone();
924 let move_state = state;
925 root = root.child(
926 gpui::canvas(measure, move |_, _, window, _| {
927 let bounds = group_bounds.clone();
928 let held = move_drag.clone();
929 let state = move_state.clone();
930 window.on_mouse_event(
931 move |event: &gpui::MouseMoveEvent, phase, window, cx| {
932 if phase != gpui::DispatchPhase::Capture {
933 return;
934 }
935 let Some(drag) = held.read(cx).clone() else {
936 return;
937 };
938 if event.pressed_button != Some(MouseButton::Left) {
939 held.update(cx, |value, cx| {
942 *value = None;
943 cx.notify();
944 });
945 return;
946 }
947 if drag.handle + 1 >= state.limits.len()
951 || drag.start.len() != state.limits.len()
952 {
953 held.update(cx, |value, cx| {
954 *value = None;
955 cx.notify();
956 });
957 return;
958 }
959 let Some(bounds) = bounds.get() else {
960 return;
961 };
962 let length = f32::from(if horizontal {
963 bounds.size.width
964 } else {
965 bounds.size.height
966 });
967 let available = length - handle_total;
968 if available <= 0. {
969 return;
970 }
971 let delta = (axis(event.position) - drag.origin) / available * 100.;
972 let target = drag.start[drag.handle] + delta;
973 let next = resize_pair(&drag.start, drag.handle, target, &state.limits);
974 let now = if state.controlled {
980 drag.last
981 } else {
982 state.store.read(cx).clone()
983 };
984 if state.apply(&now, next.clone(), window, cx) {
985 held.update(cx, |value, _| {
986 if let Some(value) = value {
987 value.last = next;
988 }
989 });
990 }
991 },
992 );
993 let held = up_drag.clone();
994 let state = up_state.clone();
995 window.on_mouse_event(move |event: &gpui::MouseUpEvent, phase, window, cx| {
996 if phase != gpui::DispatchPhase::Capture
997 || event.button != MouseButton::Left
998 {
999 return;
1000 }
1001 let Some(drag) = held.read(cx).clone() else {
1002 return;
1003 };
1004 held.update(cx, |value, cx| {
1005 *value = None;
1006 cx.notify();
1007 });
1008 if drag.last != drag.start {
1009 state.resize_ended(&drag.last, window, cx);
1010 }
1011 });
1012 })
1013 .absolute()
1014 .inset_0(),
1015 );
1016 }
1017 root
1018 }
1019}
1020
1021#[cfg(test)]
1022mod tests {
1023 use super::*;
1024
1025 fn panel(default: Option<f32>) -> ResizablePanel {
1026 let p = ResizablePanel::new();
1027 match default {
1028 Some(size) => p.default_size(size),
1029 None => p,
1030 }
1031 }
1032
1033 #[test]
1034 fn defaults_share_the_remainder_and_scale_to_100() {
1035 let defaults = |panels: &[ResizablePanel]| {
1036 let limits: Vec<Limit> = panels.iter().map(|p| p.limits(None)).collect();
1037 default_sizes(panels, &limits)
1038 };
1039 assert_eq!(
1040 defaults(&[panel(Some(20.)), panel(None), panel(None)]),
1041 vec![20., 40., 40.]
1042 );
1043 assert_eq!(
1044 defaults(&[panel(Some(30.)), panel(Some(10.))]),
1045 vec![75., 25.]
1046 );
1047 assert_eq!(defaults(&[panel(None), panel(None)]), vec![50., 50.]);
1048 }
1049
1050 #[test]
1051 fn a_pair_resize_respects_both_panels_limits() {
1052 let limits = [
1053 Limit::range(10., 60.),
1054 Limit::range(0., 100.),
1055 Limit::range(0., 100.),
1056 ];
1057 let sizes = [40., 40., 20.];
1058 assert_eq!(resize_pair(&sizes, 0, 70., &limits), vec![60., 20., 20.]);
1060 let tight = [
1063 Limit::range(10., 90.),
1064 Limit::range(30., 100.),
1065 Limit::range(0., 100.),
1066 ];
1067 assert_eq!(resize_pair(&sizes, 0, 70., &tight), vec![50., 30., 20.]);
1068 assert_eq!(resize_pair(&sizes, 0, -5., &limits), vec![10., 70., 20.]);
1070 assert_eq!(resize_pair(&sizes, 1, 50., &limits), vec![40., 50., 10.]);
1072 }
1073
1074 #[test]
1075 fn normalize_scales_splits_and_clamps() {
1076 let free = [Limit::range(0., 100.), Limit::range(0., 100.)];
1077 assert_eq!(normalize(&[1., 3.], &free), vec![25., 75.]);
1078 assert_eq!(normalize(&[0., 0.], &free), vec![50., 50.]);
1079 assert_eq!(normalize(&[f32::NAN, -5.], &free), vec![50., 50.]);
1080 assert_eq!(normalize(&[f32::INFINITY, 1.], &free), vec![0., 100.]);
1081 let limits = [
1084 Limit::range(20., 100.),
1085 Limit::range(0., 100.),
1086 Limit::range(0., 100.),
1087 ];
1088 assert_eq!(normalize(&[10., 45., 45.], &limits), vec![20., 40., 40.]);
1089 let capped = [Limit::range(0., 50.), Limit::range(0., 100.)];
1091 assert_eq!(normalize(&[80., 20.], &capped), vec![50., 50.]);
1092 let impossible = [Limit::range(70., 100.), Limit::range(70., 100.)];
1094 assert_eq!(normalize(&[50., 50.], &impossible), vec![50., 50.]);
1095 }
1096
1097 #[test]
1098 fn contradictory_limits_leave_the_sizes_alone() {
1099 let limits = [Limit::range(70., 100.), Limit::range(70., 100.)];
1100 assert_eq!(resize_pair(&[50., 50.], 0, 60., &limits), vec![50., 50.]);
1101 assert!(pair_range(&[50., 50.], 0, &limits).is_none());
1102 }
1103
1104 fn collapsible(min: f32) -> Limit {
1105 Limit {
1106 min,
1107 max: 100.,
1108 collapsed: Some(0.),
1109 }
1110 }
1111
1112 #[test]
1113 fn pixel_limits_convert_against_the_measured_length() {
1114 let panel = ResizablePanel::new()
1115 .min_size(10.)
1116 .min_size_px(px(100.))
1117 .max_size(90.)
1118 .max_size_px(px(300.));
1119 let percent = |limit: Limit| {
1120 assert_eq!(limit.collapsed, None);
1121 (
1122 (limit.min * 1000.).round() / 1000.,
1123 (limit.max * 1000.).round() / 1000.,
1124 )
1125 };
1126 assert_eq!(percent(panel.limits(None)), (10., 90.));
1128 assert_eq!(percent(panel.limits(Some(500.))), (20., 60.));
1130 assert_eq!(percent(panel.limits(Some(2000.))), (10., 15.));
1132 let squeezed = ResizablePanel::new().min_size(50.).max_size_px(px(10.));
1134 assert_eq!(percent(squeezed.limits(Some(100.))), (50., 50.));
1135 }
1136
1137 #[test]
1138 fn a_collapsible_pair_snaps_at_halfway_to_the_minimum() {
1139 let limits = [collapsible(20.), Limit::range(0., 100.)];
1140 let sizes = [30., 70.];
1141 assert_eq!(resize_pair(&sizes, 0, 11., &limits), vec![20., 80.]);
1143 assert_eq!(resize_pair(&sizes, 0, 9., &limits), vec![0., 100.]);
1144 assert_eq!(pair_range(&sizes, 0, &limits), Some((0., 100.)));
1145 let after = [Limit::range(0., 100.), collapsible(20.)];
1147 assert_eq!(resize_pair(&sizes, 0, 95., &after), vec![100., 0.]);
1148 assert_eq!(resize_pair(&sizes, 0, 88., &after), vec![80., 20.]);
1149 }
1150
1151 #[test]
1152 fn a_step_crosses_the_collapse_gap() {
1153 let limits = [collapsible(20.), Limit::range(0., 100.)];
1154 assert_eq!(step_pair(&[20., 80.], 0, 15., &limits), vec![0., 100.]);
1156 assert_eq!(step_pair(&[0., 100.], 0, 5., &limits), vec![20., 80.]);
1159 assert_eq!(step_pair(&[30., 70.], 0, 35., &limits), vec![35., 65.]);
1161 }
1162
1163 #[test]
1164 fn enter_collapses_and_restores() {
1165 let limits = [collapsible(20.), Limit::range(0., 100.)];
1166 let restore = [Some(35.), None];
1167 assert_eq!(
1168 toggle_collapse(&[40., 60.], 0, &limits, &restore),
1169 Some(vec![0., 100.])
1170 );
1171 assert_eq!(
1172 toggle_collapse(&[0., 100.], 0, &limits, &restore),
1173 Some(vec![35., 65.])
1174 );
1175 assert_eq!(
1177 toggle_collapse(&[0., 100.], 0, &limits, &[None, None]),
1178 Some(vec![20., 80.])
1179 );
1180 let after = [Limit::range(0., 100.), collapsible(20.)];
1182 assert_eq!(
1183 toggle_collapse(&[40., 60.], 0, &after, &[None, None]),
1184 Some(vec![100., 0.])
1185 );
1186 let neither = [Limit::range(0., 100.), Limit::range(0., 100.)];
1187 assert_eq!(
1188 toggle_collapse(&[40., 60.], 0, &neither, &[None, None]),
1189 None
1190 );
1191 }
1192
1193 #[test]
1194 fn normalize_keeps_a_collapsed_panel_collapsed() {
1195 let limits = [collapsible(20.), Limit::range(0., 100.)];
1196 assert_eq!(normalize(&[0., 100.], &limits), vec![0., 100.]);
1197 assert_eq!(normalize(&[5., 95.], &limits), vec![0., 100.]);
1199 assert_eq!(normalize(&[15., 85.], &limits), vec![20., 80.]);
1200 }
1201}