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, window, 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 let state = cx.entity();
195 window.defer(cx, move |_, cx| {
196 state.update(cx, |_, cx| cx.notify());
197 });
198 }
199 })
200 }
201 })
202 .child(ResizePanelGroupElement {
203 state: state.clone(),
204 axis: self.axis,
205 on_resize: self.on_resize.clone(),
206 })
207 }
208}
209
210#[derive(IntoElement)]
238pub struct ResizablePanel {
239 axis: Axis,
240 panel_ix: usize,
241 state: Option<Entity<ResizableState>>,
242 initial_size: Option<Pixels>,
244 size_range: Range<Pixels>,
246 children: Vec<AnyElement>,
247 visible: bool,
248 style: StyleRefinement,
249 handle_appearance: Option<ResizeHandleRenderer>,
250}
251
252impl ResizablePanel {
253 pub(super) fn new() -> Self {
255 Self {
256 panel_ix: 0,
257 initial_size: None,
258 state: None,
259 size_range: (PANEL_MIN_SIZE..Pixels::MAX),
260 axis: Axis::Horizontal,
261 children: vec![],
262 visible: true,
263 style: StyleRefinement::default(),
264 handle_appearance: None,
265 }
266 }
267
268 pub fn visible(mut self, visible: bool) -> Self {
270 self.visible = visible;
271 self
272 }
273
274 pub fn size(mut self, size: impl Into<Pixels>) -> Self {
276 self.initial_size = Some(size.into());
277 self
278 }
279
280 pub fn size_range(mut self, range: impl Into<Range<Pixels>>) -> Self {
284 self.size_range = range.into();
285 self
286 }
287}
288
289impl Styled for ResizablePanel {
290 fn style(&mut self) -> &mut StyleRefinement {
291 &mut self.style
292 }
293}
294
295impl ParentElement for ResizablePanel {
296 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
297 self.children.extend(elements);
298 }
299}
300
301impl RenderOnce for ResizablePanel {
302 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
303 if !self.visible {
304 return div().id(("resizable-panel", self.panel_ix));
305 }
306
307 let state = self
308 .state
309 .expect("BUG: The `state` in ResizablePanel should be present.");
310 let panel_state = state
311 .read(cx)
312 .panels
313 .get(self.panel_ix)
314 .expect("BUG: The `index` of ResizablePanel should be one of in `state`.");
315 let size_range = self.size_range.clone();
316
317 div()
318 .id(("resizable-panel", self.panel_ix))
319 .flex()
320 .flex_grow_1()
321 .size_full()
322 .relative()
323 .refine_style(&self.style)
331 .when(self.axis.is_vertical(), |this| {
332 this.min_h(size_range.start).max_h(size_range.end)
333 })
334 .when(self.axis.is_horizontal(), |this| {
335 this.min_w(size_range.start).max_w(size_range.end)
336 })
337 .when(self.initial_size.is_none(), |this| this.flex_shrink_1())
341 .when_some(self.initial_size, |this, initial_size| {
342 this.when(
345 panel_state.size.is_none() && !initial_size.is_zero(),
346 |this| this.flex_none(),
347 )
348 .flex_basis(initial_size)
349 })
350 .map(|this| match panel_state.size {
351 Some(size) => this.flex_basis(size.min(size_range.end).max(size_range.start)),
352 None => this,
353 })
354 .on_prepaint({
355 let state = state.clone();
356 move |bounds, _, cx| {
357 state.update(cx, |state, cx| {
358 state.update_panel_size(self.panel_ix, bounds, self.size_range, cx)
359 })
360 }
361 })
362 .children(self.children)
363 .when(self.panel_ix > 0, |this| {
364 let ix = self.panel_ix - 1;
365 this.child(
366 resize_handle(("resizable-handle", ix), self.axis)
367 .when_some(self.handle_appearance.clone(), |handle, appearance| {
368 handle.with_appearance(appearance)
369 })
370 .on_drag(DragPanel, move |drag_panel, _, _, cx| {
371 cx.stop_propagation();
372 state.update(cx, |state, _| {
374 state.resizing_panel_ix = Some(ix);
375 });
376 cx.new(|_| drag_panel.deref().clone())
377 }),
378 )
379 })
380 }
381}
382
383struct ResizePanelGroupElement {
384 state: Entity<ResizableState>,
385 on_resize: Rc<dyn Fn(&Entity<ResizableState>, &mut Window, &mut App)>,
386 axis: Axis,
387}
388
389impl IntoElement for ResizePanelGroupElement {
390 type Element = Self;
391
392 fn into_element(self) -> Self::Element {
393 self
394 }
395}
396
397impl Element for ResizePanelGroupElement {
398 type RequestLayoutState = ();
399 type PrepaintState = ();
400
401 fn id(&self) -> Option<gpui::ElementId> {
402 None
403 }
404
405 fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
406 None
407 }
408
409 fn request_layout(
410 &mut self,
411 _: Option<&gpui::GlobalElementId>,
412 _: Option<&gpui::InspectorElementId>,
413 window: &mut Window,
414 cx: &mut App,
415 ) -> (gpui::LayoutId, Self::RequestLayoutState) {
416 (window.request_layout(Style::default(), None, cx), ())
417 }
418
419 fn prepaint(
420 &mut self,
421 _: Option<&gpui::GlobalElementId>,
422 _: Option<&gpui::InspectorElementId>,
423 _: Bounds<Pixels>,
424 _: &mut Self::RequestLayoutState,
425 _window: &mut Window,
426 _cx: &mut App,
427 ) -> Self::PrepaintState {
428 ()
429 }
430
431 fn paint(
432 &mut self,
433 _: Option<&gpui::GlobalElementId>,
434 _: Option<&gpui::InspectorElementId>,
435 _: Bounds<Pixels>,
436 _: &mut Self::RequestLayoutState,
437 _: &mut Self::PrepaintState,
438 window: &mut Window,
439 cx: &mut App,
440 ) {
441 window.on_mouse_event({
442 let state = self.state.clone();
443 let axis = self.axis;
444 let current_ix = state.read(cx).resizing_panel_ix;
445 move |e: &MouseMoveEvent, phase, window, cx| {
446 if !phase.bubble() {
447 return;
448 }
449 let Some(ix) = current_ix else { return };
450
451 state.update(cx, |state, cx| {
452 let panel = state.panels.get(ix).expect("BUG: invalid panel index");
453
454 match axis {
455 Axis::Horizontal => state.resize_panel_at_handle(
456 ix,
457 e.position.x - panel.bounds.left(),
458 window,
459 cx,
460 ),
461 Axis::Vertical => state.resize_panel_at_handle(
462 ix,
463 e.position.y - panel.bounds.top(),
464 window,
465 cx,
466 ),
467 }
468 cx.notify();
469 })
470 }
471 });
472
473 window.on_mouse_event({
475 let state = self.state.clone();
476 let current_ix = state.read(cx).resizing_panel_ix;
477 let on_resize = self.on_resize.clone();
478 move |_: &MouseUpEvent, phase, window, cx| {
479 if current_ix.is_none() {
480 return;
481 }
482 if phase.bubble() {
483 state.update(cx, |state, cx| state.done_resizing(cx));
484 on_resize(&state, window, cx);
485 }
486 }
487 })
488 }
489}