Files
Ely-GPUI-Components/src/layout/dock/view.rs
T

317 lines
11 KiB
Rust

use gpui::{
AnyElement, Axis, Context, DragMoveEvent, EmptyView, EntityId, FontWeight, InteractiveElement,
IntoElement, MouseButton, ParentElement, Pixels, Point, Render, SharedString,
StatefulInteractiveElement, Styled, Window, div, point, prelude::*, relative,
};
use super::{Dock, DockSide, Spot};
use crate::{
layout::{FloatingPanel, resize_handle},
primitives::{DragGhost, Icon},
theme::{ActiveTheme, ControlSize, IconSize, Radius, TextSize},
};
struct DraggedPanel {
dock: EntityId,
id: SharedString,
}
struct DockResize {
dock: EntityId,
side: DockSide,
}
impl Dock {
fn tabs(&self, side: DockSide, cx: &mut Context<Self>) -> AnyElement {
let theme = cx.theme();
let active = self.active[side.index()];
div()
.flex()
.items_center()
.gap_0p5()
.h(theme.control_height(ControlSize::Lg))
.px_1()
.border_b_1()
.border_color(theme.colors.border)
.children(self.on_side(side).into_iter().map(|ix| {
let panel = &self.panels[ix];
let selected = active == Some(ix);
let (id, title, icon) = (panel.id.clone(), panel.title.clone(), panel.icon);
let drag = DraggedPanel {
dock: cx.entity_id(),
id: id.clone(),
};
div()
.id(SharedString::from(format!("tab-{id}")))
.flex()
.items_center()
.gap_1p5()
.px_2()
.h(theme.control_height(ControlSize::Sm))
.rounded(theme.radius(Radius::Md))
.cursor_pointer()
.text_size(theme.text_size(TextSize::Sm))
.text_color(if selected {
theme.colors.fg
} else {
theme.colors.fg_subtle
})
.when(selected, |tab| {
tab.bg(theme.colors.hover).font_weight(FontWeight::MEDIUM)
})
.hover(|style| style.text_color(theme.colors.fg))
.child(Icon::new(icon).size(IconSize::Sm).color(if selected {
theme.colors.fg
} else {
theme.colors.fg_subtle
}))
.child(title.clone())
.on_click(cx.listener(move |dock, _, _, cx| dock.activate(&id, cx)))
.on_drag(drag, move |_, _, _, cx| {
DragGhost::new(title.clone(), Some(icon), cx)
})
}))
.into_any_element()
}
fn side(
&self,
side: DockSide,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<AnyElement> {
let active = self.active[side.index()]?;
let size = self.sizes[side.index()];
let panel_id = self.panels[active].id.clone();
let body = (self.content)(&panel_id, window, cx);
let theme = cx.theme();
let pane = div()
.flex()
.flex_col()
.overflow_hidden()
.bg(theme.colors.surface)
.child(self.tabs(side, cx))
.child(div().flex_1().min_h_0().child(body));
let axis = match side {
DockSide::Left | DockSide::Right => Axis::Horizontal,
DockSide::Top | DockSide::Bottom => Axis::Vertical,
};
let resize = DockResize {
dock: cx.entity_id(),
side,
};
let handle = resize_handle(
SharedString::from(format!("dock-handle-{side:?}")),
axis,
cx,
)
.on_drag(resize, |_, _, _, cx| cx.new(|_| EmptyView));
let pane = match axis {
Axis::Horizontal => pane.w(size).h_full(),
Axis::Vertical => pane.h(size).w_full(),
};
let row = div().flex().flex_none();
Some(
match side {
DockSide::Left => row.h_full().child(pane).child(handle),
DockSide::Right => row.h_full().child(handle).child(pane),
DockSide::Top => row.w_full().flex_col().child(pane).child(handle),
DockSide::Bottom => row.w_full().flex_col().child(handle).child(pane),
}
.into_any_element(),
)
}
fn zones(&self, cx: &mut Context<Self>) -> AnyElement {
let theme = cx.theme();
let (lit, edge, idle) = (
theme.colors.focus.opacity(0.12),
theme.colors.focus.opacity(0.7),
theme.colors.border_strong,
);
let radius = theme.radius(Radius::Lg);
let zone = |id: &'static str, spot: Option<DockSide>, cx: &mut Context<Self>| {
div()
.id(id)
.absolute()
.rounded(radius)
.border_2()
.border_dashed()
.border_color(idle)
.drag_over::<DraggedPanel>(move |style, _, _, _| style.bg(lit).border_color(edge))
.on_drop(
cx.listener(move |dock, dragged: &DraggedPanel, window, cx| {
if dragged.dock != cx.entity_id() {
return;
}
let target = match spot {
Some(side) => Spot::Docked(side),
None => {
let at = window.mouse_position();
Spot::Floating {
x: f32::from(at.x),
y: f32::from(at.y),
}
}
};
dock.dragging = false;
dock.move_panel(&dragged.id, target, cx);
}),
)
};
div()
.absolute()
.top_0()
.left_0()
.size_full()
.p_2()
.child(
zone("zone-left", Some(DockSide::Left), cx)
.top_2()
.bottom_2()
.left_2()
.w(relative(0.2)),
)
.child(
zone("zone-right", Some(DockSide::Right), cx)
.top_2()
.bottom_2()
.right_2()
.w(relative(0.2)),
)
.child(
zone("zone-top", Some(DockSide::Top), cx)
.top_2()
.left(relative(0.22))
.right(relative(0.22))
.h(relative(0.25)),
)
.child(
zone("zone-bottom", Some(DockSide::Bottom), cx)
.bottom_2()
.left(relative(0.22))
.right(relative(0.22))
.h(relative(0.25)),
)
.child(
zone("zone-float", None, cx)
.top(relative(0.3))
.left(relative(0.3))
.right(relative(0.3))
.h(relative(0.4)),
)
.into_any_element()
}
}
impl Render for Dock {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let left = self.side(DockSide::Left, window, cx);
let right = self.side(DockSide::Right, window, cx);
let top = self.side(DockSide::Top, window, cx);
let bottom = self.side(DockSide::Bottom, window, cx);
let center = (self.center)(window, cx);
let floating: Vec<AnyElement> = (0..self.panels.len())
.filter_map(|ix| match self.spots[ix] {
Spot::Floating { x, y } => Some((ix, point(Pixels::from(x), Pixels::from(y)))),
Spot::Docked(_) => None,
})
.map(|(ix, start)| self.floating(ix, start, window, cx))
.collect();
let showing_zones = self.dragging && cx.has_active_drag();
let rem = window.rem_size();
let min = cx.theme().pane_min();
let own = cx.entity_id();
div()
.id("dock")
.relative()
.size_full()
.flex()
.on_drag_move(
cx.listener(move |dock, event: &DragMoveEvent<DockResize>, _, cx| {
let resize = event.drag(cx);
if resize.dock != own {
return;
}
let side = resize.side;
let bounds = event.bounds;
let pointer = event.event.position;
let span = match side {
DockSide::Left => pointer.x - bounds.left(),
DockSide::Right => bounds.right() - pointer.x,
DockSide::Top => pointer.y - bounds.top(),
DockSide::Bottom => bounds.bottom() - pointer.y,
};
let limit = match side {
DockSide::Left | DockSide::Right => bounds.size.width * 0.45,
DockSide::Top | DockSide::Bottom => bounds.size.height * 0.45,
};
let size = span.max(min.to_pixels(rem)).min(limit);
dock.sizes[side.index()] = gpui::rems(f32::from(size) / f32::from(rem));
cx.notify();
}),
)
.on_drag_move(
cx.listener(|dock, event: &DragMoveEvent<DraggedPanel>, _, cx| {
if event.drag(cx).dock == cx.entity_id() && !dock.dragging {
dock.dragging = true;
cx.notify();
}
}),
)
.on_mouse_up(
MouseButton::Left,
cx.listener(|dock, _, _, _| dock.dragging = false),
)
.on_mouse_up_out(
MouseButton::Left,
cx.listener(|dock, _, _, _| dock.dragging = false),
)
.children(left)
.child(
div()
.flex_1()
.min_w_0()
.flex()
.flex_col()
.children(top)
.child(div().flex_1().min_h_0().child(center))
.children(bottom),
)
.children(right)
.children(floating)
.when(showing_zones, |dock| dock.child(self.zones(cx)))
}
}
impl Dock {
fn floating(
&self,
ix: usize,
start: Point<Pixels>,
window: &mut Window,
cx: &mut Context<Self>,
) -> AnyElement {
let panel = &self.panels[ix];
let id = panel.id.clone();
let body = (self.content)(&id, window, cx);
let dock_id = id.clone();
FloatingPanel::new(
SharedString::from(format!("float-{id}")),
panel.title.clone(),
start,
)
.w(cx.theme().sidebar_width(false))
.h(cx.theme().sheet_size())
.on_move(cx.listener(move |dock, at: &Point<Pixels>, _, _| {
let ix = dock.find(&dock_id).expect("dock panels are never removed");
dock.spots[ix] = Spot::Floating {
x: f32::from(at.x),
y: f32::from(at.y),
};
}))
.child(body)
.into_any_element()
}
}