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,
};
use crate::devtools::Probed;
type RenderItem = Rc<dyn Fn(ItemId, &mut Window, &mut App) -> AnyElement>;
type ItemTitle = Rc<dyn Fn(ItemId, &App) -> SharedString>;
type ItemDot = Rc<dyn Fn(ItemId, &App) -> Option<gpui::Hsla>>;
#[derive(Clone, Debug)]
pub enum PaneGroupEvent {
Activated(ItemId),
CloseRequested(ItemId),
NewRequested(PaneId),
FocusChanged(PaneId),
TearOff(ItemId),
ContextMenu {
item: ItemId,
position: gpui::Point<gpui::Pixels>,
},
}
#[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>,
item_dot: Option<ItemDot>,
drag_over: Option<(PaneId, Option<DropEdge>)>,
dragging: Option<ItemId>,
zoomed: bool,
tab_height: f32,
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,
item_dot: None,
drag_over: None,
dragging: None,
tab_height: 28.0,
zoomed: false,
titlebar: None,
}
}
pub fn tab_height(mut self, px: f32) -> Self {
self.tab_height = px;
self
}
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 on_item_dot(mut self, f: impl Fn(ItemId, &App) -> Option<gpui::Hsla> + 'static) -> Self {
self.item_dot = 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 panes_with_items(&self) -> Vec<(PaneId, Vec<ItemId>, ItemId)> {
self.tree
.panes()
.into_iter()
.filter_map(|p| {
self.panes
.get(&p)
.map(|pane| (p, pane.items().to_vec(), pane.active()))
})
.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 {
let idx = self.tree.panes().iter().position(|&p| p == pane);
self.panes.remove(&pane);
if self.tree.remove(pane) && self.focused == pane {
let order = self.tree.panes();
let next = idx
.and_then(|i| order.get(i.saturating_sub(1)).or_else(|| order.last()))
.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;
self.dragging = 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>) {
self.drag_over = None;
self.dragging = None;
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>) {
self.drag_over = None;
self.dragging = None;
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 snapshot(&self) -> super::LayoutSnapshot {
fn walk(node: &Node, panes: &HashMap<PaneId, Pane>) -> super::LayoutSnapshot {
match node {
Node::Leaf(id) => {
let pane = &panes[id];
super::LayoutSnapshot::Pane {
items: pane.items().iter().map(|item| item.0).collect(),
active: pane.active_index(),
}
}
Node::Split {
axis,
ratio,
first,
second,
..
} => super::LayoutSnapshot::Split {
axis: *axis,
ratio: *ratio,
first: Box::new(walk(first, panes)),
second: Box::new(walk(second, panes)),
},
}
}
walk(self.tree.root(), &self.panes)
}
pub fn restore(&mut self, snapshot: &super::LayoutSnapshot, cx: &mut Context<Self>) -> bool {
let all_items = snapshot.item_ids();
if all_items.is_empty() {
return false;
}
let mut seen = std::collections::HashSet::new();
if !all_items.iter().all(|id| seen.insert(*id)) {
return false;
}
fn build(
snap: &super::LayoutSnapshot,
ids: &mut PaneIds,
panes: &mut HashMap<PaneId, Pane>,
splits: &mut u64,
) -> Option<Node> {
match snap {
super::LayoutSnapshot::Pane { items, active } => {
let (first, rest) = items.split_first()?;
let pane_id = ids.next();
let mut pane = Pane::new(ItemId(*first));
for item in rest {
pane.add(ItemId(*item), None);
}
pane.activate((*active).min(items.len() - 1));
panes.insert(pane_id, pane);
Some(Node::Leaf(pane_id))
}
super::LayoutSnapshot::Split {
axis,
ratio,
first,
second,
} => {
*splits += 1;
let id = SplitId(*splits);
let first = build(first, ids, panes, splits)?;
let second = build(second, ids, panes, splits)?;
Some(Node::Split {
id,
axis: *axis,
ratio: clamp_ratio(*ratio),
first: Box::new(first),
second: Box::new(second),
})
}
}
}
let mut ids = PaneIds::new();
let mut panes = HashMap::new();
let mut splits = 0;
let Some(root) = build(snapshot, &mut ids, &mut panes, &mut splits) else {
return false;
};
let first_pane = {
let mut leaves = Vec::new();
fn collect(node: &Node, out: &mut Vec<PaneId>) {
match node {
Node::Leaf(id) => out.push(*id),
Node::Split { first, second, .. } => {
collect(first, out);
collect(second, out);
}
}
}
collect(&root, &mut leaves);
leaves[0]
};
self.tree = PaneTree::from_parts(root, splits);
self.panes = panes;
self.ids = ids;
self.focused = first_pane;
self.zoomed = false;
self.drag_over = None;
self.dragging = None;
cx.notify();
true
}
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_edge(node: &Node, out: &mut Vec<PaneId>) {
match node {
Node::Leaf(p) => out.push(*p),
Node::Split {
axis: SplitDirection::Horizontal,
first,
second,
..
} => {
top_edge(first, out);
top_edge(second, out);
}
Node::Split { first, .. } => top_edge(first, out),
}
}
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 item_dot = self.item_dot.clone();
let inner = if self.zoomed {
self.pane_el(
self.focused,
&render_item,
&item_title,
&item_dot,
window,
cx,
)
} else {
self.node_el(&root, &render_item, &item_title, &item_dot, window, cx)
};
div()
.size_full()
.track_focus(&self.focus)
.on_mouse_up_out(
MouseButton::Left,
cx.listener(|this, _ev, _window, cx| {
let dragged = this.dragging.take();
if this.drag_over.take().is_some() {
cx.notify();
}
if let Some(item) = dragged {
this.tear_off(item, cx);
}
}),
)
.child(inner)
.probe("PaneGroup")
}
}
impl PaneGroup {
fn node_el(
&self,
node: &Node,
render_item: &Option<RenderItem>,
item_title: &Option<ItemTitle>,
item_dot: &Option<ItemDot>,
window: &mut Window,
cx: &mut Context<Self>,
) -> AnyElement {
match node {
Node::Leaf(pane) => self.pane_el(*pane, render_item, item_title, item_dot, 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, item_dot, window, cx);
let s = self.node_el(second, render_item, item_title, item_dot, 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>,
item_dot: &Option<ItemDot>,
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 tab_h = self.tab_height;
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 dot = item_dot.as_ref().and_then(|f| f(item, cx));
let is_active = item == active;
div()
.id(("pg-tab", (pane.0 as usize) << 20 | i))
.flex()
.items_center()
.gap_1()
.px_2()
.h(px(tab_h))
.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_mouse_down(
MouseButton::Right,
cx.listener(move |this, ev: &gpui::MouseDownEvent, _w, cx| {
this.activate(pane, item, cx);
cx.emit(PaneGroupEvent::ContextMenu {
item,
position: ev.position,
});
}),
)
.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);
}))
.when_some(dot, |d, color| {
d.child(
div()
.flex_none()
.w(px(6.0))
.h(px(6.0))
.rounded_full()
.bg(color),
)
})
.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 is_top_edge = self.titlebar.is_some() && {
let mut edge = Vec::new();
top_edge(self.tree.root(), &mut edge);
edge.contains(&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(tab_h))
.bg(surface)
.border_b_1()
.border_color(border)
.when(is_top_right, |d| d.pr(px(trailing)))
.when(is_top_left, |d| {
d.child(
div()
.id(("pg-titleinset", pane.0 as usize))
.flex_none()
.w(px(leading))
.h_full()
.window_control_area(WindowControlArea::Drag)
.on_mouse_down(MouseButton::Left, |_, window, _| {
window.start_window_move()
}),
)
})
.children(tabs)
.child(
div()
.id(("pg-newtab", pane.0 as usize))
.px_2()
.h(px(tab_h))
.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_edge {
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| {
this.dragging = Some(ev.drag(cx).item);
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()
}
}