Skip to main content

ui/
split.rs

1//! Two panes with a draggable divider. The host owns and persists the ratio.
2
3use crate::widgets::{self, Layout, SplitStyle};
4use gpui::{
5    AnyElement, App, Axis, DragMoveEvent, ElementId, Empty, IntoElement, MouseButton, Pixels,
6    Refineable, RenderOnce, StyleRefinement, Window, div, prelude::*, px,
7};
8use theme::Theme;
9
10#[derive(Clone)]
11struct Drag(ElementId);
12
13#[derive(Default)]
14struct Gesture {
15    held: bool,
16    offset: Pixels,
17}
18
19/// Fill the available space with two panes. Set the new ratio in `on_resize`
20/// and redraw the host. The container must accommodate both pane minimums
21/// plus [`widgets::SPLIT_HANDLE_HIT`].
22#[derive(IntoElement)]
23pub struct Split {
24    id: ElementId,
25    axis: Axis,
26    first: AnyElement,
27    second: AnyElement,
28    ratio: f32,
29    min: Pixels,
30    resized: Option<Box<Resize>>,
31    style: StyleRefinement,
32}
33
34type Resize = dyn Fn(&f32, &mut Window, &mut App);
35
36impl Split {
37    pub fn horizontal(
38        id: impl Into<ElementId>,
39        first: impl IntoElement,
40        second: impl IntoElement,
41    ) -> Self {
42        Self {
43            id: id.into(),
44            axis: Axis::Horizontal,
45            first: first.into_any_element(),
46            second: second.into_any_element(),
47            ratio: 0.5,
48            min: px(0.0),
49            resized: None,
50            style: StyleRefinement::default(),
51        }
52    }
53
54    pub fn vertical(
55        id: impl Into<ElementId>,
56        first: impl IntoElement,
57        second: impl IntoElement,
58    ) -> Self {
59        Self {
60            axis: Axis::Vertical,
61            ..Self::horizontal(id, first, second)
62        }
63    }
64
65    /// Share of pane space assigned to the first pane, excluding the divider.
66    pub fn ratio(mut self, ratio: f32) -> Self {
67        self.ratio = ratio.clamp(0.0, 1.0);
68        self
69    }
70
71    pub fn min(mut self, min: Pixels) -> Self {
72        self.min = min.max(px(0.0));
73        self
74    }
75
76    pub fn on_resize(mut self, resized: impl Fn(&f32, &mut Window, &mut App) + 'static) -> Self {
77        self.resized = Some(Box::new(resized));
78        self
79    }
80}
81
82impl Styled for Split {
83    fn style(&mut self) -> &mut StyleRefinement {
84        &mut self.style
85    }
86}
87
88impl RenderOnce for Split {
89    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
90        let gesture = window.use_keyed_state(self.id.clone(), cx, |_, _| Gesture::default());
91        let held = gesture.read(cx).held;
92        let pressed = gesture.clone();
93        let released = gesture.clone();
94        let axis = self.axis;
95        let min = self.min;
96        let id = self.id.clone();
97        let pane = |child, weight| {
98            div()
99                .flex_grow(weight)
100                .flex_shrink(1.0)
101                .flex_basis(px(0.0))
102                .min_w_0()
103                .min_h_0()
104                .overflow_hidden()
105                .map(|pane| match axis {
106                    Axis::Horizontal => pane.min_w(min).h_full(),
107                    Axis::Vertical => pane.min_h(min).w_full(),
108                })
109                .child(child)
110        };
111        let handle = Theme::of(cx)
112            .split_handle(axis, SplitStyle::Line { dragging: held })
113            .id("divider")
114            .on_drag(Drag(id.clone()), move |_, offset, _, cx| {
115                pressed.update(cx, |gesture, cx| {
116                    gesture.held = true;
117                    gesture.offset = match axis {
118                        Axis::Horizontal => offset.x,
119                        Axis::Vertical => offset.y,
120                    } - px(widgets::SPLIT_HANDLE_HIT / 2.0);
121                    cx.notify();
122                });
123                cx.new(|_| Empty)
124            });
125        let mut frame = div()
126            .id(self.id)
127            .size_full()
128            .flex()
129            .map(|frame| match axis {
130                Axis::Horizontal => frame.flex_row(),
131                Axis::Vertical => frame.flex_col(),
132            })
133            .when_some(self.resized, |frame, resized| {
134                frame.on_drag_move(move |event: &DragMoveEvent<Drag>, window, cx| {
135                    if event.drag(cx).0 != id {
136                        return;
137                    }
138                    let mut bounds = event.bounds;
139                    let half = px(widgets::SPLIT_HANDLE_HIT / 2.0);
140                    let extent = match axis {
141                        Axis::Horizontal => {
142                            bounds.origin.x += half + gesture.read(cx).offset;
143                            bounds.size.width -= half * 2.0;
144                            bounds.size.width
145                        }
146                        Axis::Vertical => {
147                            bounds.origin.y += half + gesture.read(cx).offset;
148                            bounds.size.height -= half * 2.0;
149                            bounds.size.height
150                        }
151                    };
152                    if extent <= px(0.0) {
153                        return;
154                    }
155                    let ratio =
156                        widgets::axis_fraction(event.event.position, bounds, axis, min / extent);
157                    resized(&ratio, window, cx);
158                })
159            })
160            .on_mouse_up(MouseButton::Left, {
161                let released = released.clone();
162                move |_, _, cx| {
163                    released.update(cx, |gesture, cx| {
164                        if std::mem::take(&mut gesture.held) {
165                            cx.notify();
166                        }
167                    })
168                }
169            })
170            .on_mouse_up_out(MouseButton::Left, move |_, _, cx| {
171                released.update(cx, |gesture, cx| {
172                    if std::mem::take(&mut gesture.held) {
173                        cx.notify();
174                    }
175                })
176            })
177            .child(pane(self.first, self.ratio))
178            .child(handle)
179            .child(pane(self.second, 1.0 - self.ratio));
180        frame.style().refine(&self.style);
181        frame
182    }
183}