1use std::{
2 ops::{Deref, Range},
3 rc::Rc,
4};
5
6use gpui::{
7 Along, AnyElement, App, AppContext, Axis, Bounds, Context, Element, ElementId, Empty, Entity,
8 EventEmitter, InteractiveElement as _, IntoElement, IsZero as _, MouseMoveEvent, MouseUpEvent,
9 ParentElement, Pixels, Render, RenderOnce, Style, StyleRefinement, Styled, Window, div,
10 prelude::FluentBuilder,
11};
12
13use crate::{AxisExt, ElementExt, StyledExt as _, h_flex, resizable::PANEL_MIN_SIZE, v_flex};
14
15use super::{ResizableState, ResizeHandleRenderer, resizable_panel, resize_handle};
16
17pub enum ResizablePanelEvent {
18 Resized,
19}
20
21#[derive(Clone)]
22pub(crate) struct DragPanel;
23impl Render for DragPanel {
24 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
25 Empty
26 }
27}
28
29#[derive(IntoElement)]
31pub struct ResizablePanelGroup {
32 id: ElementId,
33 state: Option<Entity<ResizableState>>,
34 axis: Axis,
35 size: Option<Pixels>,
36 children: Vec<ResizablePanel>,
37 on_resize: Rc<dyn Fn(&Entity<ResizableState>, &mut Window, &mut App)>,
38 handle_appearance: Option<ResizeHandleRenderer>,
39}
40
41impl ResizablePanelGroup {
42 pub fn new(id: impl Into<ElementId>) -> Self {
44 Self {
45 id: id.into(),
46 axis: Axis::Horizontal,
47 children: vec![],
48 state: None,
49 size: None,
50 on_resize: Rc::new(|_, _, _| {}),
51 handle_appearance: None,
52 }
53 }
54
55 pub fn with_handle_appearance(mut self, appearance: ResizeHandleRenderer) -> Self {
60 self.handle_appearance = Some(appearance);
61 self
62 }
63
64 pub fn with_state(mut self, state: &Entity<ResizableState>) -> Self {
68 self.state = Some(state.clone());
69 self
70 }
71
72 pub fn axis(mut self, axis: Axis) -> Self {
74 self.axis = axis;
75 self
76 }
77
78 pub fn child(mut self, panel: impl Into<ResizablePanel>) -> Self {
84 self.children.push(panel.into());
85 self
86 }
87
88 pub fn children<I>(mut self, panels: impl IntoIterator<Item = I>) -> Self
90 where
91 I: Into<ResizablePanel>,
92 {
93 self.children = panels.into_iter().map(|panel| panel.into()).collect();
94 self
95 }
96
97 pub fn size(mut self, size: Pixels) -> Self {
102 self.size = Some(size);
103 self
104 }
105
106 pub fn on_resize(
112 mut self,
113 on_resize: impl Fn(&Entity<ResizableState>, &mut Window, &mut App) + 'static,
114 ) -> Self {
115 self.on_resize = Rc::new(on_resize);
116 self
117 }
118}
119
120impl<T> From<T> for ResizablePanel
121where
122 T: Into<AnyElement>,
123{
124 fn from(value: T) -> Self {
125 resizable_panel().child(value.into())
126 }
127}
128
129impl From<ResizablePanelGroup> for ResizablePanel {
130 fn from(value: ResizablePanelGroup) -> Self {
131 resizable_panel().child(value)
132 }
133}
134
135impl EventEmitter<ResizablePanelEvent> for ResizablePanelGroup {}
136
137impl RenderOnce for ResizablePanelGroup {
138 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
139 let state = self.state.unwrap_or(
140 window.use_keyed_state(self.id.clone(), cx, |_, _| ResizableState::default()),
141 );
142 let container = if self.axis.is_horizontal() {
143 h_flex()
144 } else {
145 v_flex()
146 };
147
148 let panels_count = self.children.len();
150 state.update(cx, |state, cx| {
151 state.sync_panels_count(self.axis, panels_count, cx);
152 });
153
154 container
155 .id(self.id)
156 .size_full()
157 .when_some(self.size, |this, size| match self.axis {
160 Axis::Horizontal => this.h(size),
161 Axis::Vertical => this.w(size),
162 })
163 .children(
164 self.children
165 .into_iter()
166 .enumerate()
167 .map(|(ix, mut panel)| {
168 panel.panel_ix = ix;
169 panel.axis = self.axis;
170 panel.state = Some(state.clone());
171 panel.handle_appearance = self.handle_appearance.clone();
172 panel
173 }),
174 )
175 .on_prepaint({
176 let state = state.clone();
177 move |bounds, _, cx| {
178 state.update(cx, |state, cx| {
179 let size_changed =
180 state.bounds.size.along(self.axis) != bounds.size.along(self.axis);
181
182 state.bounds = bounds;
183
184 if size_changed {
185 state.adjust_to_container_size(cx);
186 }
187 })
188 }
189 })
190 .child(ResizePanelGroupElement {
191 state: state.clone(),
192 axis: self.axis,
193 on_resize: self.on_resize.clone(),
194 })
195 }
196}
197
198#[derive(IntoElement)]
226pub struct ResizablePanel {
227 axis: Axis,
228 panel_ix: usize,
229 state: Option<Entity<ResizableState>>,
230 initial_size: Option<Pixels>,
232 size_range: Range<Pixels>,
234 children: Vec<AnyElement>,
235 visible: bool,
236 style: StyleRefinement,
237 handle_appearance: Option<ResizeHandleRenderer>,
238}
239
240impl ResizablePanel {
241 pub(super) fn new() -> Self {
243 Self {
244 panel_ix: 0,
245 initial_size: None,
246 state: None,
247 size_range: (PANEL_MIN_SIZE..Pixels::MAX),
248 axis: Axis::Horizontal,
249 children: vec![],
250 visible: true,
251 style: StyleRefinement::default(),
252 handle_appearance: None,
253 }
254 }
255
256 pub fn visible(mut self, visible: bool) -> Self {
258 self.visible = visible;
259 self
260 }
261
262 pub fn size(mut self, size: impl Into<Pixels>) -> Self {
264 self.initial_size = Some(size.into());
265 self
266 }
267
268 pub fn size_range(mut self, range: impl Into<Range<Pixels>>) -> Self {
272 self.size_range = range.into();
273 self
274 }
275}
276
277impl Styled for ResizablePanel {
278 fn style(&mut self) -> &mut StyleRefinement {
279 &mut self.style
280 }
281}
282
283impl ParentElement for ResizablePanel {
284 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
285 self.children.extend(elements);
286 }
287}
288
289impl RenderOnce for ResizablePanel {
290 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
291 if !self.visible {
292 return div().id(("resizable-panel", self.panel_ix));
293 }
294
295 let state = self
296 .state
297 .expect("BUG: The `state` in ResizablePanel should be present.");
298 let panel_state = state
299 .read(cx)
300 .panels
301 .get(self.panel_ix)
302 .expect("BUG: The `index` of ResizablePanel should be one of in `state`.");
303 let size_range = self.size_range.clone();
304
305 div()
306 .id(("resizable-panel", self.panel_ix))
307 .flex()
308 .flex_grow_1()
309 .size_full()
310 .relative()
311 .refine_style(&self.style)
319 .when(self.axis.is_vertical(), |this| {
320 this.min_h(size_range.start).max_h(size_range.end)
321 })
322 .when(self.axis.is_horizontal(), |this| {
323 this.min_w(size_range.start).max_w(size_range.end)
324 })
325 .when(self.initial_size.is_none(), |this| this.flex_shrink_1())
329 .when_some(self.initial_size, |this, initial_size| {
330 this.when(
333 panel_state.size.is_none() && !initial_size.is_zero(),
334 |this| this.flex_none(),
335 )
336 .flex_basis(initial_size)
337 })
338 .map(|this| match panel_state.size {
339 Some(size) => this.flex_basis(size.min(size_range.end).max(size_range.start)),
340 None => this,
341 })
342 .on_prepaint({
343 let state = state.clone();
344 move |bounds, _, cx| {
345 state.update(cx, |state, cx| {
346 state.update_panel_size(self.panel_ix, bounds, self.size_range, cx)
347 })
348 }
349 })
350 .children(self.children)
351 .when(self.panel_ix > 0, |this| {
352 let ix = self.panel_ix - 1;
353 this.child(
354 resize_handle(("resizable-handle", ix), self.axis)
355 .when_some(self.handle_appearance.clone(), |handle, appearance| {
356 handle.with_appearance(appearance)
357 })
358 .on_drag(DragPanel, move |drag_panel, _, _, cx| {
359 cx.stop_propagation();
360 state.update(cx, |state, _| {
362 state.resizing_panel_ix = Some(ix);
363 });
364 cx.new(|_| drag_panel.deref().clone())
365 }),
366 )
367 })
368 }
369}
370
371struct ResizePanelGroupElement {
372 state: Entity<ResizableState>,
373 on_resize: Rc<dyn Fn(&Entity<ResizableState>, &mut Window, &mut App)>,
374 axis: Axis,
375}
376
377impl IntoElement for ResizePanelGroupElement {
378 type Element = Self;
379
380 fn into_element(self) -> Self::Element {
381 self
382 }
383}
384
385impl Element for ResizePanelGroupElement {
386 type RequestLayoutState = ();
387 type PrepaintState = ();
388
389 fn id(&self) -> Option<gpui::ElementId> {
390 None
391 }
392
393 fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
394 None
395 }
396
397 fn request_layout(
398 &mut self,
399 _: Option<&gpui::GlobalElementId>,
400 _: Option<&gpui::InspectorElementId>,
401 window: &mut Window,
402 cx: &mut App,
403 ) -> (gpui::LayoutId, Self::RequestLayoutState) {
404 (window.request_layout(Style::default(), None, cx), ())
405 }
406
407 fn prepaint(
408 &mut self,
409 _: Option<&gpui::GlobalElementId>,
410 _: Option<&gpui::InspectorElementId>,
411 _: Bounds<Pixels>,
412 _: &mut Self::RequestLayoutState,
413 _window: &mut Window,
414 _cx: &mut App,
415 ) -> Self::PrepaintState {
416 ()
417 }
418
419 fn paint(
420 &mut self,
421 _: Option<&gpui::GlobalElementId>,
422 _: Option<&gpui::InspectorElementId>,
423 _: Bounds<Pixels>,
424 _: &mut Self::RequestLayoutState,
425 _: &mut Self::PrepaintState,
426 window: &mut Window,
427 cx: &mut App,
428 ) {
429 window.on_mouse_event({
430 let state = self.state.clone();
431 let axis = self.axis;
432 let current_ix = state.read(cx).resizing_panel_ix;
433 move |e: &MouseMoveEvent, phase, window, cx| {
434 if !phase.bubble() {
435 return;
436 }
437 let Some(ix) = current_ix else { return };
438
439 state.update(cx, |state, cx| {
440 let panel = state.panels.get(ix).expect("BUG: invalid panel index");
441
442 match axis {
443 Axis::Horizontal => state.resize_panel_at_handle(
444 ix,
445 e.position.x - panel.bounds.left(),
446 window,
447 cx,
448 ),
449 Axis::Vertical => state.resize_panel_at_handle(
450 ix,
451 e.position.y - panel.bounds.top(),
452 window,
453 cx,
454 ),
455 }
456 cx.notify();
457 })
458 }
459 });
460
461 window.on_mouse_event({
463 let state = self.state.clone();
464 let current_ix = state.read(cx).resizing_panel_ix;
465 let on_resize = self.on_resize.clone();
466 move |_: &MouseUpEvent, phase, window, cx| {
467 if current_ix.is_none() {
468 return;
469 }
470 if phase.bubble() {
471 state.update(cx, |state, cx| state.done_resizing(cx));
472 on_resize(&state, window, cx);
473 }
474 }
475 })
476 }
477}