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