1use std::collections::HashMap;
9use std::rc::Rc;
10
11use gpui::prelude::*;
12use gpui::{
13 div, px, AnyElement, App, Context, DragMoveEvent, Empty, EntityId, EventEmitter, FocusHandle,
14 IntoElement, MouseButton, SharedString, Window, WindowControlArea,
15};
16
17use crate::style::FlexExt;
18use crate::theme::theme;
19use crate::SplitDirection;
20
21use super::drag::{drop_edge, drop_overlay, DropEdge, TabDrag};
22use super::tree::clamp_ratio;
23use super::{compute_layout, neighbor, Direction, ItemId, Node, Pane, PaneId, PaneIds, PaneTree, Rect, SplitId};
24
25type RenderItem = Rc<dyn Fn(ItemId, &mut Window, &mut App) -> AnyElement>;
27type ItemTitle = Rc<dyn Fn(ItemId, &App) -> SharedString>;
29
30#[derive(Clone, Debug)]
33pub enum PaneGroupEvent {
34 Activated(ItemId),
36 CloseRequested(ItemId),
39 NewRequested(PaneId),
42 FocusChanged(PaneId),
44 TearOff(ItemId),
47}
48
49#[derive(Clone, Copy)]
51struct DividerDrag {
52 group: EntityId,
53 split: SplitId,
54}
55
56pub struct PaneGroup {
60 tree: PaneTree,
61 panes: HashMap<PaneId, Pane>,
62 ids: PaneIds,
63 focused: PaneId,
64 focus: FocusHandle,
65 render_item: Option<RenderItem>,
66 item_title: Option<ItemTitle>,
67 drag_over: Option<(PaneId, Option<DropEdge>)>,
70 zoomed: bool,
72 titlebar: Option<(f32, f32)>,
79}
80
81impl EventEmitter<PaneGroupEvent> for PaneGroup {}
82
83impl PaneGroup {
84 pub fn new(first: ItemId, cx: &mut Context<Self>) -> Self {
86 let mut ids = PaneIds::new();
87 let root = ids.next();
88 let mut panes = HashMap::new();
89 panes.insert(root, Pane::new(first));
90 Self {
91 tree: PaneTree::new(root),
92 panes,
93 ids,
94 focused: root,
95 focus: cx.focus_handle(),
96 render_item: None,
97 item_title: None,
98 drag_over: None,
99 zoomed: false,
100 titlebar: None,
101 }
102 }
103
104 pub fn titlebar(mut self, leading: f32, trailing: f32) -> Self {
109 self.titlebar = Some((leading, trailing));
110 self
111 }
112
113 pub fn on_render_item(
115 mut self,
116 f: impl Fn(ItemId, &mut Window, &mut App) -> AnyElement + 'static,
117 ) -> Self {
118 self.render_item = Some(Rc::new(f));
119 self
120 }
121
122 pub fn on_item_title(mut self, f: impl Fn(ItemId, &App) -> SharedString + 'static) -> Self {
124 self.item_title = Some(Rc::new(f));
125 self
126 }
127
128 pub fn focused_pane(&self) -> PaneId {
131 self.focused
132 }
133
134 pub fn active_item(&self) -> ItemId {
136 self.panes[&self.focused].active()
137 }
138
139 pub fn items(&self) -> Vec<ItemId> {
141 self.tree
142 .panes()
143 .into_iter()
144 .filter_map(|p| self.panes.get(&p))
145 .flat_map(|p| p.items().iter().copied())
146 .collect()
147 }
148
149 pub fn pane_of(&self, item: ItemId) -> Option<PaneId> {
150 self.panes
151 .iter()
152 .find(|(_, p)| p.contains(item))
153 .map(|(&id, _)| id)
154 }
155
156 pub fn add_item(&mut self, pane: PaneId, item: ItemId, cx: &mut Context<Self>) {
160 if let Some(p) = self.panes.get_mut(&pane) {
161 p.add(item, None);
162 self.set_focus(pane, cx);
163 }
164 }
165
166 pub fn add_to_focused(&mut self, item: ItemId, cx: &mut Context<Self>) {
168 let pane = self.focused;
169 self.add_item(pane, item, cx);
170 }
171
172 pub fn split(
175 &mut self,
176 pane: PaneId,
177 dir: SplitDirection,
178 first: bool,
179 item: ItemId,
180 cx: &mut Context<Self>,
181 ) -> PaneId {
182 let new_pane = self.ids.next();
183 if self.tree.split(pane, dir, new_pane, first).is_some() {
184 self.panes.insert(new_pane, Pane::new(item));
185 self.set_focus(new_pane, cx);
186 }
187 new_pane
188 }
189
190 pub fn activate(&mut self, pane: PaneId, item: ItemId, cx: &mut Context<Self>) {
192 if let Some(p) = self.panes.get_mut(&pane) {
193 if p.activate_item(item) {
194 self.set_focus(pane, cx);
195 cx.emit(PaneGroupEvent::Activated(item));
196 }
197 }
198 }
199
200 pub fn close_item(&mut self, item: ItemId, cx: &mut Context<Self>) {
202 let Some(pane) = self.pane_of(item) else {
203 return;
204 };
205 let emptied = self.panes.get_mut(&pane).map(|p| p.remove(item)).unwrap_or(false);
206 if emptied {
207 self.panes.remove(&pane);
208 if self.tree.remove(pane) && self.focused == pane {
211 let next = self.tree.panes().first().copied().unwrap_or(pane);
212 self.set_focus(next, cx);
213 }
214 }
215 cx.notify();
216 }
217
218 pub fn move_item(
223 &mut self,
224 item: ItemId,
225 to_pane: PaneId,
226 edge: Option<DropEdge>,
227 cx: &mut Context<Self>,
228 ) {
229 self.drag_over = None;
230 let Some(from) = self.pane_of(item) else {
231 cx.notify();
232 return;
233 };
234 let from_single = self.panes.get(&from).is_some_and(|p| p.len() == 1);
235 if from == to_pane && (edge.is_none() || from_single) {
237 cx.notify();
238 return;
239 }
240 self.detach(from, item);
241 match edge {
242 Some(edge) => {
243 let (axis, first) = edge.split();
244 let new_pane = self.ids.next();
245 if self.tree.split(to_pane, axis, new_pane, first).is_some() {
246 self.panes.insert(new_pane, Pane::new(item));
247 self.set_focus(new_pane, cx);
248 return;
249 }
250 self.reattach_somewhere(item, cx);
252 }
253 None => {
254 if let Some(p) = self.panes.get_mut(&to_pane) {
255 p.add(item, None);
256 self.set_focus(to_pane, cx);
257 } else {
258 self.reattach_somewhere(item, cx);
259 }
260 }
261 }
262 cx.notify();
263 }
264
265 pub fn reorder_in_pane(&mut self, item: ItemId, index: usize, cx: &mut Context<Self>) {
267 if let Some(pane) = self.pane_of(item) {
268 if let Some(p) = self.panes.get_mut(&pane) {
269 if let Some(from) = p.index_of(item) {
270 p.reorder(from, index);
271 cx.notify();
272 }
273 }
274 }
275 }
276
277 fn detach(&mut self, pane: PaneId, item: ItemId) {
279 if let Some(p) = self.panes.get_mut(&pane) {
280 if p.remove(item) {
281 self.panes.remove(&pane);
282 self.tree.remove(pane);
283 }
284 }
285 }
286
287 fn reattach_somewhere(&mut self, item: ItemId, cx: &mut Context<Self>) {
289 if let Some(&pane) = self.tree.panes().first() {
290 if let Some(p) = self.panes.get_mut(&pane) {
291 p.add(item, None);
292 self.set_focus(pane, cx);
293 }
294 }
295 }
296
297 pub fn set_ratio(&mut self, split: SplitId, ratio: f32, cx: &mut Context<Self>) {
299 if self.tree.set_ratio(split, clamp_ratio(ratio)) {
300 cx.notify();
301 }
302 }
303
304 fn set_focus(&mut self, pane: PaneId, cx: &mut Context<Self>) {
305 let changed = self.focused != pane;
306 self.focused = pane;
307 if changed {
308 cx.emit(PaneGroupEvent::FocusChanged(pane));
309 }
310 cx.notify();
311 }
312
313 pub fn focus_direction(&mut self, dir: Direction, cx: &mut Context<Self>) {
317 let layout = compute_layout(&self.tree, Rect::new(0.0, 0.0, 1000.0, 1000.0), 0.0);
318 if let Some(pane) = neighbor(&layout, self.focused, dir) {
319 self.set_focus(pane, cx);
320 }
321 }
322
323 pub fn activate_next(&mut self, cx: &mut Context<Self>) {
325 self.cycle_focused(true, cx);
326 }
327
328 pub fn activate_prev(&mut self, cx: &mut Context<Self>) {
329 self.cycle_focused(false, cx);
330 }
331
332 fn cycle_focused(&mut self, next: bool, cx: &mut Context<Self>) {
333 if let Some(p) = self.panes.get_mut(&self.focused) {
334 if next {
335 p.activate_next();
336 } else {
337 p.activate_prev();
338 }
339 let item = p.active();
340 cx.emit(PaneGroupEvent::Activated(item));
341 cx.notify();
342 }
343 }
344
345 pub fn equalize(&mut self, cx: &mut Context<Self>) {
349 for (split, _) in self.tree.list_dividers() {
350 self.tree.set_ratio(split, 0.5);
351 }
352 cx.notify();
353 }
354
355 pub fn resize_focused(&mut self, dir: Direction, step: f32, cx: &mut Context<Self>) {
357 let (axis, delta) = match dir {
358 Direction::Left => (SplitDirection::Horizontal, -step),
359 Direction::Right => (SplitDirection::Horizontal, step),
360 Direction::Up => (SplitDirection::Vertical, -step),
361 Direction::Down => (SplitDirection::Vertical, step),
362 };
363 if let Some(split) = self.tree.nearest_split(self.focused, axis) {
364 if let Some(r) = self.tree.ratio(split) {
365 self.tree.set_ratio(split, r + delta);
366 cx.notify();
367 }
368 }
369 }
370
371 pub fn toggle_zoom(&mut self, cx: &mut Context<Self>) {
373 self.zoomed = !self.zoomed;
374 cx.notify();
375 }
376
377 pub fn is_zoomed(&self) -> bool {
378 self.zoomed
379 }
380
381 pub fn close_focused(&mut self, cx: &mut Context<Self>) {
386 if let Some(p) = self.panes.get(&self.focused) {
387 cx.emit(PaneGroupEvent::CloseRequested(p.active()));
388 }
389 }
390
391 pub fn tear_off(&mut self, item: ItemId, cx: &mut Context<Self>) {
395 if let Some(pane) = self.pane_of(item) {
396 if self.tree.panes().len() == 1
398 && self.panes.get(&pane).is_some_and(|p| p.len() == 1)
399 {
400 return;
401 }
402 self.detach(pane, item);
403 if !self.tree.contains(self.focused) {
404 if let Some(&p) = self.tree.panes().first() {
405 self.focused = p;
406 }
407 }
408 }
409 cx.emit(PaneGroupEvent::TearOff(item));
410 cx.notify();
411 }
412
413 pub fn tree(&self) -> &PaneTree {
417 &self.tree
418 }
419
420 pub fn pane_items(&self, pane: PaneId) -> Option<&[ItemId]> {
422 self.panes.get(&pane).map(|p| p.items())
423 }
424}
425
426fn top_left(node: &Node) -> PaneId {
428 match node {
429 Node::Leaf(p) => *p,
430 Node::Split { first, .. } => top_left(first),
431 }
432}
433
434fn top_right(node: &Node) -> PaneId {
437 match node {
438 Node::Leaf(p) => *p,
439 Node::Split {
440 axis: SplitDirection::Horizontal,
441 second,
442 ..
443 } => top_right(second),
444 Node::Split { first, .. } => top_right(first),
445 }
446}
447
448impl gpui::Focusable for PaneGroup {
449 fn focus_handle(&self, _cx: &App) -> FocusHandle {
450 self.focus.clone()
451 }
452}
453
454impl Render for PaneGroup {
455 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
456 let root = self.tree.root().clone();
457 let render_item = self.render_item.clone();
458 let item_title = self.item_title.clone();
459 let inner = if self.zoomed {
461 self.pane_el(self.focused, &render_item, &item_title, window, cx)
462 } else {
463 self.node_el(&root, &render_item, &item_title, window, cx)
464 };
465 div().size_full().track_focus(&self.focus).child(inner)
466 }
467}
468
469impl PaneGroup {
470 fn node_el(
472 &self,
473 node: &Node,
474 render_item: &Option<RenderItem>,
475 item_title: &Option<ItemTitle>,
476 window: &mut Window,
477 cx: &mut Context<Self>,
478 ) -> AnyElement {
479 match node {
480 Node::Leaf(pane) => self.pane_el(*pane, render_item, item_title, window, cx),
481 Node::Split {
482 id,
483 axis,
484 ratio,
485 first,
486 second,
487 } => {
488 let horizontal = matches!(axis, SplitDirection::Horizontal);
489 let ratio = *ratio;
490 let split = *id;
491 let group = cx.entity().entity_id();
492 let f = self.node_el(first, render_item, item_title, window, cx);
493 let s = self.node_el(second, render_item, item_title, window, cx);
494
495 let line = theme(cx).border().hsla();
496 let grip = theme(cx).primary().alpha(0.35);
497
498 let first_pane = div()
499 .flex_basis(px(0.0))
500 .grow(ratio)
501 .overflow_hidden()
502 .child(f);
503 let second_pane = div()
504 .flex_basis(px(0.0))
505 .grow(1.0 - ratio)
506 .overflow_hidden()
507 .child(s);
508
509 let mut divider = div()
510 .id(("pg-divider", split.0 as usize))
511 .flex_none()
512 .flex()
513 .items_center()
514 .justify_center()
515 .hover(move |st| st.bg(grip))
516 .on_drag(DividerDrag { group, split }, |_, _off, _w, cx| cx.new(|_| Empty));
517 divider = if horizontal {
518 divider
519 .w(px(6.0))
520 .h_full()
521 .cursor_col_resize()
522 .child(div().w(px(1.0)).h_full().bg(line))
523 } else {
524 divider
525 .h(px(6.0))
526 .w_full()
527 .cursor_row_resize()
528 .child(div().h(px(1.0)).w_full().bg(line))
529 };
530
531 let mut container = div().size_full().flex().on_drag_move(cx.listener(
532 move |this, ev: &DragMoveEvent<DividerDrag>, _w, cx| {
533 let d = ev.drag(cx);
534 if d.group != group || d.split != split {
535 return;
536 }
537 let b = ev.bounds;
538 let (pos, extent) = if horizontal {
539 (f32::from(ev.event.position.x - b.left()), f32::from(b.size.width))
540 } else {
541 (f32::from(ev.event.position.y - b.top()), f32::from(b.size.height))
542 };
543 if extent > 0.0 {
544 this.set_ratio(split, pos / extent, cx);
545 }
546 },
547 ));
548 container = if horizontal {
549 container.flex_row()
550 } else {
551 container.flex_col()
552 };
553 container
554 .child(first_pane)
555 .child(divider)
556 .child(second_pane)
557 .into_any_element()
558 }
559 }
560 }
561
562 fn pane_el(
564 &self,
565 pane: PaneId,
566 render_item: &Option<RenderItem>,
567 item_title: &Option<ItemTitle>,
568 window: &mut Window,
569 cx: &mut Context<Self>,
570 ) -> AnyElement {
571 let Some(p) = self.panes.get(&pane) else {
572 return div().into_any_element();
573 };
574 let t = theme(cx);
575 let surface = t.surface().hsla();
576 let text = t.text().hsla();
577 let border = t.border().hsla();
578 let active_bg = t.surface_hover().hsla();
579
580 let active = p.active();
581 let group = cx.entity().entity_id();
582 let tabs = p.items().iter().copied().enumerate().map(|(i, item)| {
583 let title = item_title
584 .as_ref()
585 .map(|f| f(item, cx))
586 .unwrap_or_else(|| SharedString::from("untitled"));
587 let is_active = item == active;
588 div()
589 .id(("pg-tab", (pane.0 as usize) << 20 | i))
590 .flex()
591 .items_center()
592 .gap_1()
593 .px_2()
594 .h(px(28.0))
595 .when(is_active, |d| d.bg(active_bg))
596 .text_color(text)
597 .hover(|s| s.bg(active_bg))
598 .on_click(cx.listener(move |this, _ev, _w, cx| this.activate(pane, item, cx)))
599 .on_drag(
602 TabDrag {
603 group,
604 item,
605 from_pane: pane,
606 label: title.clone(),
607 },
608 |d, _off, _w, cx| cx.new(|_| d.clone()),
609 )
610 .on_drop(cx.listener(move |this, d: &TabDrag, _w, cx| {
611 if d.from_pane != pane {
612 this.move_item(d.item, pane, None, cx);
613 }
614 this.reorder_in_pane(d.item, i, cx);
615 }))
616 .child(div().text_size(px(12.0)).child(title))
617 .child(
618 div()
619 .id(("pg-tabclose", (pane.0 as usize) << 20 | i))
620 .text_size(px(12.0))
621 .text_color(text)
622 .hover(|s| s.text_color(text))
623 .child("\u{00d7}")
624 .on_click(cx.listener(move |_this, _ev, _w, cx| {
625 cx.emit(PaneGroupEvent::CloseRequested(item));
626 })),
627 )
628 });
629
630 let is_top_left = self.titlebar.is_some() && top_left(self.tree.root()) == pane;
633 let is_top_right = self.titlebar.is_some() && top_right(self.tree.root()) == pane;
634 let (leading, trailing) = self.titlebar.unwrap_or((0.0, 0.0));
635
636 let mut tab_bar = div()
637 .flex()
638 .flex_row()
639 .items_center()
640 .w_full()
641 .h(px(28.0))
642 .bg(surface)
643 .border_b_1()
644 .border_color(border)
645 .when(is_top_left, |d| d.pl(px(leading)))
646 .when(is_top_right, |d| d.pr(px(trailing)))
647 .children(tabs)
648 .child(
649 div()
650 .id(("pg-newtab", pane.0 as usize))
651 .px_2()
652 .h(px(28.0))
653 .flex()
654 .items_center()
655 .text_color(text)
656 .hover(|s| s.bg(active_bg))
657 .child("+")
658 .on_click(cx.listener(move |_this, _ev, _w, cx| {
659 cx.emit(PaneGroupEvent::NewRequested(pane));
660 })),
661 );
662 if is_top_right {
665 tab_bar = tab_bar.child(
666 div()
667 .id(("pg-titledrag", pane.0 as usize))
668 .flex_1()
669 .h_full()
670 .window_control_area(WindowControlArea::Drag)
671 .on_mouse_down(MouseButton::Left, |_, window, _| window.start_window_move()),
672 );
673 }
674
675 let content = render_item
676 .as_ref()
677 .map(|f| f(active, window, cx))
678 .unwrap_or_else(|| div().into_any_element());
679
680 let over = match self.drag_over {
682 Some((p, edge)) if p == pane => Some(edge),
683 _ => None,
684 };
685 let body = div()
686 .relative()
687 .flex_1()
688 .overflow_hidden()
689 .on_drag_move::<TabDrag>(cx.listener(
690 move |this, ev: &DragMoveEvent<TabDrag>, _w, cx| {
691 let edge = drop_edge(ev.bounds, ev.event.position);
692 if this.drag_over != Some((pane, edge)) {
693 this.drag_over = Some((pane, edge));
694 cx.notify();
695 }
696 },
697 ))
698 .on_drop(cx.listener(move |this, d: &TabDrag, _w, cx| {
699 let edge = match this.drag_over {
700 Some((p, e)) if p == pane => e,
701 _ => None,
702 };
703 this.move_item(d.item, pane, edge, cx);
704 }))
705 .child(content)
706 .when_some(over, |el, edge| el.child(drop_overlay(edge)));
707
708 div()
709 .flex()
710 .flex_col()
711 .size_full()
712 .child(tab_bar)
713 .child(body)
714 .into_any_element()
715 }
716}