use std::collections::HashMap;
use std::rc::Rc;
use gpui::prelude::*;
use gpui::{
div, px, AnyElement, App, Context, DragMoveEvent, Empty, EntityId, EventEmitter, FocusHandle,
IntoElement, MouseButton, SharedString, Window, WindowControlArea,
};
use crate::style::FlexExt;
use crate::theme::theme;
use crate::SplitDirection;
use super::drag::{drop_edge, drop_overlay, DropEdge, TabDrag};
use super::tree::clamp_ratio;
use super::{compute_layout, neighbor, Direction, ItemId, Node, Pane, PaneId, PaneIds, PaneTree, Rect, SplitId};
type RenderItem = Rc<dyn Fn(ItemId, &mut Window, &mut App) -> AnyElement>;
type ItemTitle = Rc<dyn Fn(ItemId, &App) -> SharedString>;
#[derive(Clone, Debug)]
pub enum PaneGroupEvent {
Activated(ItemId),
CloseRequested(ItemId),
NewRequested(PaneId),
FocusChanged(PaneId),
TearOff(ItemId),
}
#[derive(Clone, Copy)]
struct DividerDrag {
group: EntityId,
split: SplitId,
}
pub struct PaneGroup {
tree: PaneTree,
panes: HashMap<PaneId, Pane>,
ids: PaneIds,
focused: PaneId,
focus: FocusHandle,
render_item: Option<RenderItem>,
item_title: Option<ItemTitle>,
drag_over: Option<(PaneId, Option<DropEdge>)>,
zoomed: bool,
titlebar: Option<(f32, f32)>,
}
impl EventEmitter<PaneGroupEvent> for PaneGroup {}
impl PaneGroup {
pub fn new(first: ItemId, cx: &mut Context<Self>) -> Self {
let mut ids = PaneIds::new();
let root = ids.next();
let mut panes = HashMap::new();
panes.insert(root, Pane::new(first));
Self {
tree: PaneTree::new(root),
panes,
ids,
focused: root,
focus: cx.focus_handle(),
render_item: None,
item_title: None,
drag_over: None,
zoomed: false,
titlebar: None,
}
}
pub fn titlebar(mut self, leading: f32, trailing: f32) -> Self {
self.titlebar = Some((leading, trailing));
self
}
pub fn on_render_item(
mut self,
f: impl Fn(ItemId, &mut Window, &mut App) -> AnyElement + 'static,
) -> Self {
self.render_item = Some(Rc::new(f));
self
}
pub fn on_item_title(mut self, f: impl Fn(ItemId, &App) -> SharedString + 'static) -> Self {
self.item_title = Some(Rc::new(f));
self
}
pub fn focused_pane(&self) -> PaneId {
self.focused
}
pub fn active_item(&self) -> ItemId {
self.panes[&self.focused].active()
}
pub fn items(&self) -> Vec<ItemId> {
self.tree
.panes()
.into_iter()
.filter_map(|p| self.panes.get(&p))
.flat_map(|p| p.items().iter().copied())
.collect()
}
pub fn pane_of(&self, item: ItemId) -> Option<PaneId> {
self.panes
.iter()
.find(|(_, p)| p.contains(item))
.map(|(&id, _)| id)
}
pub fn add_item(&mut self, pane: PaneId, item: ItemId, cx: &mut Context<Self>) {
if let Some(p) = self.panes.get_mut(&pane) {
p.add(item, None);
self.set_focus(pane, cx);
}
}
pub fn add_to_focused(&mut self, item: ItemId, cx: &mut Context<Self>) {
let pane = self.focused;
self.add_item(pane, item, cx);
}
pub fn split(
&mut self,
pane: PaneId,
dir: SplitDirection,
first: bool,
item: ItemId,
cx: &mut Context<Self>,
) -> PaneId {
let new_pane = self.ids.next();
if self.tree.split(pane, dir, new_pane, first).is_some() {
self.panes.insert(new_pane, Pane::new(item));
self.set_focus(new_pane, cx);
}
new_pane
}
pub fn activate(&mut self, pane: PaneId, item: ItemId, cx: &mut Context<Self>) {
if let Some(p) = self.panes.get_mut(&pane) {
if p.activate_item(item) {
self.set_focus(pane, cx);
cx.emit(PaneGroupEvent::Activated(item));
}
}
}
pub fn close_item(&mut self, item: ItemId, cx: &mut Context<Self>) {
let Some(pane) = self.pane_of(item) else {
return;
};
let emptied = self.panes.get_mut(&pane).map(|p| p.remove(item)).unwrap_or(false);
if emptied {
self.panes.remove(&pane);
if self.tree.remove(pane) && self.focused == pane {
let next = self.tree.panes().first().copied().unwrap_or(pane);
self.set_focus(next, cx);
}
}
cx.notify();
}
pub fn move_item(
&mut self,
item: ItemId,
to_pane: PaneId,
edge: Option<DropEdge>,
cx: &mut Context<Self>,
) {
self.drag_over = None;
let Some(from) = self.pane_of(item) else {
cx.notify();
return;
};
let from_single = self.panes.get(&from).is_some_and(|p| p.len() == 1);
if from == to_pane && (edge.is_none() || from_single) {
cx.notify();
return;
}
self.detach(from, item);
match edge {
Some(edge) => {
let (axis, first) = edge.split();
let new_pane = self.ids.next();
if self.tree.split(to_pane, axis, new_pane, first).is_some() {
self.panes.insert(new_pane, Pane::new(item));
self.set_focus(new_pane, cx);
return;
}
self.reattach_somewhere(item, cx);
}
None => {
if let Some(p) = self.panes.get_mut(&to_pane) {
p.add(item, None);
self.set_focus(to_pane, cx);
} else {
self.reattach_somewhere(item, cx);
}
}
}
cx.notify();
}
pub fn reorder_in_pane(&mut self, item: ItemId, index: usize, cx: &mut Context<Self>) {
if let Some(pane) = self.pane_of(item) {
if let Some(p) = self.panes.get_mut(&pane) {
if let Some(from) = p.index_of(item) {
p.reorder(from, index);
cx.notify();
}
}
}
}
fn detach(&mut self, pane: PaneId, item: ItemId) {
if let Some(p) = self.panes.get_mut(&pane) {
if p.remove(item) {
self.panes.remove(&pane);
self.tree.remove(pane);
}
}
}
fn reattach_somewhere(&mut self, item: ItemId, cx: &mut Context<Self>) {
if let Some(&pane) = self.tree.panes().first() {
if let Some(p) = self.panes.get_mut(&pane) {
p.add(item, None);
self.set_focus(pane, cx);
}
}
}
pub fn set_ratio(&mut self, split: SplitId, ratio: f32, cx: &mut Context<Self>) {
if self.tree.set_ratio(split, clamp_ratio(ratio)) {
cx.notify();
}
}
fn set_focus(&mut self, pane: PaneId, cx: &mut Context<Self>) {
let changed = self.focused != pane;
self.focused = pane;
if changed {
cx.emit(PaneGroupEvent::FocusChanged(pane));
}
cx.notify();
}
pub fn focus_direction(&mut self, dir: Direction, cx: &mut Context<Self>) {
let layout = compute_layout(&self.tree, Rect::new(0.0, 0.0, 1000.0, 1000.0), 0.0);
if let Some(pane) = neighbor(&layout, self.focused, dir) {
self.set_focus(pane, cx);
}
}
pub fn activate_next(&mut self, cx: &mut Context<Self>) {
self.cycle_focused(true, cx);
}
pub fn activate_prev(&mut self, cx: &mut Context<Self>) {
self.cycle_focused(false, cx);
}
fn cycle_focused(&mut self, next: bool, cx: &mut Context<Self>) {
if let Some(p) = self.panes.get_mut(&self.focused) {
if next {
p.activate_next();
} else {
p.activate_prev();
}
let item = p.active();
cx.emit(PaneGroupEvent::Activated(item));
cx.notify();
}
}
pub fn equalize(&mut self, cx: &mut Context<Self>) {
for (split, _) in self.tree.list_dividers() {
self.tree.set_ratio(split, 0.5);
}
cx.notify();
}
pub fn resize_focused(&mut self, dir: Direction, step: f32, cx: &mut Context<Self>) {
let (axis, delta) = match dir {
Direction::Left => (SplitDirection::Horizontal, -step),
Direction::Right => (SplitDirection::Horizontal, step),
Direction::Up => (SplitDirection::Vertical, -step),
Direction::Down => (SplitDirection::Vertical, step),
};
if let Some(split) = self.tree.nearest_split(self.focused, axis) {
if let Some(r) = self.tree.ratio(split) {
self.tree.set_ratio(split, r + delta);
cx.notify();
}
}
}
pub fn toggle_zoom(&mut self, cx: &mut Context<Self>) {
self.zoomed = !self.zoomed;
cx.notify();
}
pub fn is_zoomed(&self) -> bool {
self.zoomed
}
pub fn close_focused(&mut self, cx: &mut Context<Self>) {
if let Some(p) = self.panes.get(&self.focused) {
cx.emit(PaneGroupEvent::CloseRequested(p.active()));
}
}
pub fn tear_off(&mut self, item: ItemId, cx: &mut Context<Self>) {
if let Some(pane) = self.pane_of(item) {
if self.tree.panes().len() == 1
&& self.panes.get(&pane).is_some_and(|p| p.len() == 1)
{
return;
}
self.detach(pane, item);
if !self.tree.contains(self.focused) {
if let Some(&p) = self.tree.panes().first() {
self.focused = p;
}
}
}
cx.emit(PaneGroupEvent::TearOff(item));
cx.notify();
}
pub fn tree(&self) -> &PaneTree {
&self.tree
}
pub fn pane_items(&self, pane: PaneId) -> Option<&[ItemId]> {
self.panes.get(&pane).map(|p| p.items())
}
}
fn top_left(node: &Node) -> PaneId {
match node {
Node::Leaf(p) => *p,
Node::Split { first, .. } => top_left(first),
}
}
fn top_right(node: &Node) -> PaneId {
match node {
Node::Leaf(p) => *p,
Node::Split {
axis: SplitDirection::Horizontal,
second,
..
} => top_right(second),
Node::Split { first, .. } => top_right(first),
}
}
impl gpui::Focusable for PaneGroup {
fn focus_handle(&self, _cx: &App) -> FocusHandle {
self.focus.clone()
}
}
impl Render for PaneGroup {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let root = self.tree.root().clone();
let render_item = self.render_item.clone();
let item_title = self.item_title.clone();
let inner = if self.zoomed {
self.pane_el(self.focused, &render_item, &item_title, window, cx)
} else {
self.node_el(&root, &render_item, &item_title, window, cx)
};
div().size_full().track_focus(&self.focus).child(inner)
}
}
impl PaneGroup {
fn node_el(
&self,
node: &Node,
render_item: &Option<RenderItem>,
item_title: &Option<ItemTitle>,
window: &mut Window,
cx: &mut Context<Self>,
) -> AnyElement {
match node {
Node::Leaf(pane) => self.pane_el(*pane, render_item, item_title, window, cx),
Node::Split {
id,
axis,
ratio,
first,
second,
} => {
let horizontal = matches!(axis, SplitDirection::Horizontal);
let ratio = *ratio;
let split = *id;
let group = cx.entity().entity_id();
let f = self.node_el(first, render_item, item_title, window, cx);
let s = self.node_el(second, render_item, item_title, window, cx);
let line = theme(cx).border().hsla();
let grip = theme(cx).primary().alpha(0.35);
let first_pane = div()
.flex_basis(px(0.0))
.grow(ratio)
.overflow_hidden()
.child(f);
let second_pane = div()
.flex_basis(px(0.0))
.grow(1.0 - ratio)
.overflow_hidden()
.child(s);
let mut divider = div()
.id(("pg-divider", split.0 as usize))
.flex_none()
.flex()
.items_center()
.justify_center()
.hover(move |st| st.bg(grip))
.on_drag(DividerDrag { group, split }, |_, _off, _w, cx| cx.new(|_| Empty));
divider = if horizontal {
divider
.w(px(6.0))
.h_full()
.cursor_col_resize()
.child(div().w(px(1.0)).h_full().bg(line))
} else {
divider
.h(px(6.0))
.w_full()
.cursor_row_resize()
.child(div().h(px(1.0)).w_full().bg(line))
};
let mut container = div().size_full().flex().on_drag_move(cx.listener(
move |this, ev: &DragMoveEvent<DividerDrag>, _w, cx| {
let d = ev.drag(cx);
if d.group != group || d.split != split {
return;
}
let b = ev.bounds;
let (pos, extent) = if horizontal {
(f32::from(ev.event.position.x - b.left()), f32::from(b.size.width))
} else {
(f32::from(ev.event.position.y - b.top()), f32::from(b.size.height))
};
if extent > 0.0 {
this.set_ratio(split, pos / extent, cx);
}
},
));
container = if horizontal {
container.flex_row()
} else {
container.flex_col()
};
container
.child(first_pane)
.child(divider)
.child(second_pane)
.into_any_element()
}
}
}
fn pane_el(
&self,
pane: PaneId,
render_item: &Option<RenderItem>,
item_title: &Option<ItemTitle>,
window: &mut Window,
cx: &mut Context<Self>,
) -> AnyElement {
let Some(p) = self.panes.get(&pane) else {
return div().into_any_element();
};
let t = theme(cx);
let surface = t.surface().hsla();
let text = t.text().hsla();
let border = t.border().hsla();
let active_bg = t.surface_hover().hsla();
let active = p.active();
let group = cx.entity().entity_id();
let tabs = p.items().iter().copied().enumerate().map(|(i, item)| {
let title = item_title
.as_ref()
.map(|f| f(item, cx))
.unwrap_or_else(|| SharedString::from("untitled"));
let is_active = item == active;
div()
.id(("pg-tab", (pane.0 as usize) << 20 | i))
.flex()
.items_center()
.gap_1()
.px_2()
.h(px(28.0))
.when(is_active, |d| d.bg(active_bg))
.text_color(text)
.hover(|s| s.bg(active_bg))
.on_click(cx.listener(move |this, _ev, _w, cx| this.activate(pane, item, cx)))
.on_drag(
TabDrag {
group,
item,
from_pane: pane,
label: title.clone(),
},
|d, _off, _w, cx| cx.new(|_| d.clone()),
)
.on_drop(cx.listener(move |this, d: &TabDrag, _w, cx| {
if d.from_pane != pane {
this.move_item(d.item, pane, None, cx);
}
this.reorder_in_pane(d.item, i, cx);
}))
.child(div().text_size(px(12.0)).child(title))
.child(
div()
.id(("pg-tabclose", (pane.0 as usize) << 20 | i))
.text_size(px(12.0))
.text_color(text)
.hover(|s| s.text_color(text))
.child("\u{00d7}")
.on_click(cx.listener(move |_this, _ev, _w, cx| {
cx.emit(PaneGroupEvent::CloseRequested(item));
})),
)
});
let is_top_left = self.titlebar.is_some() && top_left(self.tree.root()) == pane;
let is_top_right = self.titlebar.is_some() && top_right(self.tree.root()) == pane;
let (leading, trailing) = self.titlebar.unwrap_or((0.0, 0.0));
let mut tab_bar = div()
.flex()
.flex_row()
.items_center()
.w_full()
.h(px(28.0))
.bg(surface)
.border_b_1()
.border_color(border)
.when(is_top_left, |d| d.pl(px(leading)))
.when(is_top_right, |d| d.pr(px(trailing)))
.children(tabs)
.child(
div()
.id(("pg-newtab", pane.0 as usize))
.px_2()
.h(px(28.0))
.flex()
.items_center()
.text_color(text)
.hover(|s| s.bg(active_bg))
.child("+")
.on_click(cx.listener(move |_this, _ev, _w, cx| {
cx.emit(PaneGroupEvent::NewRequested(pane));
})),
);
if is_top_right {
tab_bar = tab_bar.child(
div()
.id(("pg-titledrag", pane.0 as usize))
.flex_1()
.h_full()
.window_control_area(WindowControlArea::Drag)
.on_mouse_down(MouseButton::Left, |_, window, _| window.start_window_move()),
);
}
let content = render_item
.as_ref()
.map(|f| f(active, window, cx))
.unwrap_or_else(|| div().into_any_element());
let over = match self.drag_over {
Some((p, edge)) if p == pane => Some(edge),
_ => None,
};
let body = div()
.relative()
.flex_1()
.overflow_hidden()
.on_drag_move::<TabDrag>(cx.listener(
move |this, ev: &DragMoveEvent<TabDrag>, _w, cx| {
let edge = drop_edge(ev.bounds, ev.event.position);
if this.drag_over != Some((pane, edge)) {
this.drag_over = Some((pane, edge));
cx.notify();
}
},
))
.on_drop(cx.listener(move |this, d: &TabDrag, _w, cx| {
let edge = match this.drag_over {
Some((p, e)) if p == pane => e,
_ => None,
};
this.move_item(d.item, pane, edge, cx);
}))
.child(content)
.when_some(over, |el, edge| el.child(drop_overlay(edge)));
div()
.flex()
.flex_col()
.size_full()
.child(tab_bar)
.child(body)
.into_any_element()
}
}