1use std::{ops::Range, rc::Rc};
2
3use gpui::{
4 Along, AnyElement, App, AppContext, Axis, Bounds, Context, Element, ElementId, Empty, Entity,
5 EventEmitter, InteractiveElement as _, IntoElement, IsZero as _, MouseMoveEvent, MouseUpEvent,
6 ParentElement, Pixels, Render, RenderOnce, Style, StyleRefinement, Styled, Window, div,
7 prelude::FluentBuilder,
8};
9
10use crate::{AxisExt, ElementExt, StyledExt as _, h_flex, resizable::PANEL_MIN_SIZE, v_flex};
11
12use super::{ResizableState, ResizeHandleRenderer, resizable_panel, resize_handle};
13
14pub enum ResizablePanelEvent {
15 Resized,
16}
17
18#[derive(Clone)]
19pub(crate) struct DragPanel;
20impl Render for DragPanel {
21 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
22 Empty
23 }
24}
25
26#[derive(IntoElement)]
28pub struct ResizablePanelGroup {
29 id: ElementId,
30 state: Option<Entity<ResizableState>>,
31 axis: Axis,
32 size: Option<Pixels>,
33 children: Vec<ResizablePanel>,
34 on_resize: Rc<dyn Fn(&Entity<ResizableState>, &mut Window, &mut App)>,
35 handle_appearance: Option<ResizeHandleRenderer>,
36}
37
38impl ResizablePanelGroup {
39 pub fn new(id: impl Into<ElementId>) -> Self {
41 Self {
42 id: id.into(),
43 axis: Axis::Horizontal,
44 children: vec![],
45 state: None,
46 size: None,
47 on_resize: Rc::new(|_, _, _| {}),
48 handle_appearance: None,
49 }
50 }
51
52 pub fn with_handle_appearance(mut self, appearance: ResizeHandleRenderer) -> Self {
57 self.handle_appearance = Some(appearance);
58 self
59 }
60
61 pub fn with_state(mut self, state: &Entity<ResizableState>) -> Self {
65 self.state = Some(state.clone());
66 self
67 }
68
69 pub fn axis(mut self, axis: Axis) -> Self {
71 self.axis = axis;
72 self
73 }
74
75 pub fn child(mut self, panel: impl Into<ResizablePanel>) -> Self {
81 self.children.push(panel.into());
82 self
83 }
84
85 pub fn children<I>(mut self, panels: impl IntoIterator<Item = I>) -> Self
87 where
88 I: Into<ResizablePanel>,
89 {
90 self.children = panels.into_iter().map(|panel| panel.into()).collect();
91 self
92 }
93
94 pub fn size(mut self, size: Pixels) -> Self {
99 self.size = Some(size);
100 self
101 }
102
103 pub fn on_resize(
109 mut self,
110 on_resize: impl Fn(&Entity<ResizableState>, &mut Window, &mut App) + 'static,
111 ) -> Self {
112 self.on_resize = Rc::new(on_resize);
113 self
114 }
115}
116
117impl<T> From<T> for ResizablePanel
118where
119 T: Into<AnyElement>,
120{
121 fn from(value: T) -> Self {
122 resizable_panel().child(value.into())
123 }
124}
125
126impl From<ResizablePanelGroup> for ResizablePanel {
127 fn from(value: ResizablePanelGroup) -> Self {
128 resizable_panel().child(value)
129 }
130}
131
132impl EventEmitter<ResizablePanelEvent> for ResizablePanelGroup {}
133
134impl RenderOnce for ResizablePanelGroup {
135 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
136 let state = self.state.unwrap_or(
137 window.use_keyed_state(self.id.clone(), cx, |_, _| ResizableState::default()),
138 );
139 let container = if self.axis.is_horizontal() {
140 h_flex()
141 } else {
142 v_flex()
143 };
144
145 let panels_count = self.children.len();
147 state.update(cx, |state, cx| {
148 state.sync_panels_count(self.axis, panels_count, cx);
149 });
150
151 container
152 .id(self.id)
153 .size_full()
154 .when_some(self.size, |this, size| match self.axis {
157 Axis::Horizontal => this.h(size),
158 Axis::Vertical => this.w(size),
159 })
160 .children(
161 self.children
162 .into_iter()
163 .enumerate()
164 .map(|(ix, mut panel)| {
165 panel.panel_ix = ix;
166 panel.axis = self.axis;
167 panel.state = Some(state.clone());
168 panel.handle_appearance = self.handle_appearance.clone();
169 panel
170 }),
171 )
172 .on_prepaint({
173 let state = state.clone();
174 move |bounds, window, cx| {
175 if state.read(cx).bounds == bounds {
178 return;
179 }
180 state.update(cx, |state, cx| {
181 let size_changed =
182 state.bounds.size.along(self.axis) != bounds.size.along(self.axis);
183
184 state.bounds = bounds;
185
186 if size_changed {
187 state.adjust_to_container_size(cx);
188 let state = cx.entity();
197 window.defer(cx, move |_, cx| {
198 state.update(cx, |_, cx| cx.notify());
199 });
200 }
201 })
202 }
203 })
204 .child(ResizePanelGroupElement {
205 state: state.clone(),
206 axis: self.axis,
207 on_resize: self.on_resize.clone(),
208 })
209 }
210}
211
212#[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 fixed: bool,
256}
257
258impl ResizablePanel {
259 pub(super) fn new() -> Self {
261 Self {
262 panel_ix: 0,
263 initial_size: None,
264 state: None,
265 size_range: (PANEL_MIN_SIZE..Pixels::MAX),
266 axis: Axis::Horizontal,
267 children: vec![],
268 visible: true,
269 style: StyleRefinement::default(),
270 handle_appearance: None,
271 fixed: false,
272 }
273 }
274
275 pub fn visible(mut self, visible: bool) -> Self {
277 self.visible = visible;
278 self
279 }
280
281 pub fn size(mut self, size: impl Into<Pixels>) -> Self {
283 self.initial_size = Some(size.into());
284 self
285 }
286
287 pub fn size_range(mut self, range: impl Into<Range<Pixels>>) -> Self {
291 self.size_range = range.into();
292 self
293 }
294
295 pub fn fixed(mut self) -> Self {
303 self.fixed = true;
304 self
305 }
306}
307
308impl Styled for ResizablePanel {
309 fn style(&mut self) -> &mut StyleRefinement {
310 &mut self.style
311 }
312}
313
314impl ParentElement for ResizablePanel {
315 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
316 self.children.extend(elements);
317 }
318}
319
320impl RenderOnce for ResizablePanel {
321 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
322 if !self.visible {
323 return div().id(("resizable-panel", self.panel_ix));
324 }
325
326 let state = self
327 .state
328 .expect("BUG: The `state` in ResizablePanel should be present.");
329 let panel_state = state
330 .read(cx)
331 .panels
332 .get(self.panel_ix)
333 .expect("BUG: The `index` of ResizablePanel should be one of in `state`.");
334 let size_range = self.size_range.clone();
335 let fixed = self.fixed && self.initial_size.is_some();
337
338 div()
339 .id(("resizable-panel", self.panel_ix))
340 .flex()
341 .flex_grow_1()
342 .size_full()
343 .relative()
344 .refine_style(&self.style)
352 .when(self.axis.is_vertical(), |this| {
353 this.min_h(size_range.start).max_h(size_range.end)
354 })
355 .when(self.axis.is_horizontal(), |this| {
356 this.min_w(size_range.start).max_w(size_range.end)
357 })
358 .when(self.initial_size.is_none(), |this| this.flex_shrink_1())
362 .when_some(self.initial_size, |this, initial_size| {
363 this.when(
366 panel_state.size.is_none() && !initial_size.is_zero(),
367 |this| this.flex_none(),
368 )
369 .flex_basis(initial_size)
370 })
371 .map(|this| match panel_state.size {
372 Some(size) => this.flex_basis(size.min(size_range.end).max(size_range.start)),
373 None => this,
374 })
375 .when(fixed, |this| this.flex_grow_0().flex_shrink_0())
379 .on_prepaint({
380 let state = state.clone();
381 move |bounds, _, cx| {
382 if !state.read(cx).panel_size_changes(
383 self.panel_ix,
384 bounds,
385 &self.size_range,
386 fixed,
387 ) {
388 return;
389 }
390 state.update(cx, |state, cx| {
391 state.update_panel_size(self.panel_ix, bounds, self.size_range, fixed, cx)
392 })
393 }
394 })
395 .children(self.children)
396 .when(self.panel_ix > 0, |this| {
397 let ix = self.panel_ix - 1;
398 this.child(
399 resize_handle(("resizable-handle", ix), self.axis)
400 .when_some(self.handle_appearance.clone(), |handle, appearance| {
401 handle.with_appearance(appearance)
402 })
403 .on_drag(DragPanel, move |drag_panel, _, _, cx| {
404 cx.stop_propagation();
405 state.update(cx, |state, _| {
407 state.resizing_panel_ix = Some(ix);
408 });
409 cx.new(|_| drag_panel.clone())
410 }),
411 )
412 })
413 }
414}
415
416struct ResizePanelGroupElement {
417 state: Entity<ResizableState>,
418 on_resize: Rc<dyn Fn(&Entity<ResizableState>, &mut Window, &mut App)>,
419 axis: Axis,
420}
421
422impl IntoElement for ResizePanelGroupElement {
423 type Element = Self;
424
425 fn into_element(self) -> Self::Element {
426 self
427 }
428}
429
430impl Element for ResizePanelGroupElement {
431 type RequestLayoutState = ();
432 type PrepaintState = ();
433
434 fn id(&self) -> Option<gpui::ElementId> {
435 None
436 }
437
438 fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
439 None
440 }
441
442 fn request_layout(
443 &mut self,
444 _: Option<&gpui::GlobalElementId>,
445 _: Option<&gpui::InspectorElementId>,
446 window: &mut Window,
447 cx: &mut App,
448 ) -> (gpui::LayoutId, Self::RequestLayoutState) {
449 (window.request_layout(Style::default(), None, cx), ())
450 }
451
452 fn prepaint(
453 &mut self,
454 _: Option<&gpui::GlobalElementId>,
455 _: Option<&gpui::InspectorElementId>,
456 _: Bounds<Pixels>,
457 _: &mut Self::RequestLayoutState,
458 _window: &mut Window,
459 _cx: &mut App,
460 ) -> Self::PrepaintState {
461 ()
462 }
463
464 fn paint(
465 &mut self,
466 _: Option<&gpui::GlobalElementId>,
467 _: Option<&gpui::InspectorElementId>,
468 _: Bounds<Pixels>,
469 _: &mut Self::RequestLayoutState,
470 _: &mut Self::PrepaintState,
471 window: &mut Window,
472 cx: &mut App,
473 ) {
474 window.on_mouse_event({
475 let state = self.state.clone();
476 let axis = self.axis;
477 let current_ix = state.read(cx).resizing_panel_ix;
478 move |e: &MouseMoveEvent, phase, window, cx| {
479 if !phase.bubble() {
480 return;
481 }
482 let Some(ix) = current_ix else { return };
483
484 state.update(cx, |state, cx| {
485 let panel = state.panels.get(ix).expect("BUG: invalid panel index");
486
487 match axis {
488 Axis::Horizontal => state.resize_panel_at_handle(
489 ix,
490 e.position.x - panel.bounds.left(),
491 window,
492 cx,
493 ),
494 Axis::Vertical => state.resize_panel_at_handle(
495 ix,
496 e.position.y - panel.bounds.top(),
497 window,
498 cx,
499 ),
500 }
501 cx.notify();
502 })
503 }
504 });
505
506 window.on_mouse_event({
508 let state = self.state.clone();
509 let current_ix = state.read(cx).resizing_panel_ix;
510 let on_resize = self.on_resize.clone();
511 move |_: &MouseUpEvent, phase, window, cx| {
512 if current_ix.is_none() {
513 return;
514 }
515 if phase.bubble() {
516 state.update(cx, |state, cx| state.done_resizing(cx));
517 on_resize(&state, window, cx);
518 }
519 }
520 })
521 }
522}