Files
gpui-native-kit/crates/layout/src/shell.rs
T
2026-11-09 10:09:56 +00:00

322 lines
10 KiB
Rust

//! The application shell: a left sidebar (on by default), a right sidebar
//! (off by default), and the center region that hosts the dock.
use gpui::prelude::FluentBuilder as _;
use gpui::{
AnyElement, App, AppContext as _, Context, ElementId, Entity, InteractiveElement as _,
IntoElement, ParentElement as _, Pixels, RenderOnce, Styled as _, Window, div, px,
};
use gpui_native_core::button::{Button, ButtonVariant};
use gpui_native_core::common::{Sizable as _, h_flex, v_flex};
use gpui_native_data::sidebar::Sidebar;
use gpui_native_icons::IconRole;
use gpui_native_theme::{ActiveTheme as _, Size};
use crate::resize_handle::ResizeHandle;
use crate::window_controls::{TRAFFIC_LIGHT_PAD, WindowControls, titlebar_strip};
use crate::window_frame::frame;
/// The width a sidebar opens at.
const DEFAULT_SIDEBAR_WIDTH: f32 = 240.0;
/// The narrowest a sidebar can be dragged to.
const MIN_SIDEBAR_WIDTH: f32 = 160.0;
/// The widest a sidebar can be dragged to.
const MAX_SIDEBAR_WIDTH: f32 = 480.0;
/// The layout the application owns.
pub struct LayoutState {
left_visible: bool,
right_visible: bool,
left_width: Pixels,
right_width: Pixels,
}
impl LayoutState {
/// Creates the state: left sidebar visible, right sidebar hidden.
pub fn new(cx: &mut App) -> Entity<Self> {
cx.new(|_| Self {
left_visible: true,
right_visible: false,
left_width: px(DEFAULT_SIDEBAR_WIDTH),
right_width: px(DEFAULT_SIDEBAR_WIDTH),
})
}
/// Shows or hides the left sidebar.
pub fn set_left_sidebar_visible(&mut self, visible: bool, cx: &mut Context<Self>) {
if self.left_visible != visible {
self.left_visible = visible;
cx.notify();
}
}
/// Shows or hides the right sidebar.
pub fn set_right_sidebar_visible(&mut self, visible: bool, cx: &mut Context<Self>) {
if self.right_visible != visible {
self.right_visible = visible;
cx.notify();
}
}
/// Whether the left sidebar is visible.
pub fn is_left_sidebar_visible(&self) -> bool {
self.left_visible
}
/// Whether the right sidebar is visible.
pub fn is_right_sidebar_visible(&self) -> bool {
self.right_visible
}
/// The left sidebar's current width.
pub fn left_sidebar_width(&self) -> Pixels {
self.left_width
}
/// The right sidebar's current width.
pub fn right_sidebar_width(&self) -> Pixels {
self.right_width
}
/// Applies a drag delta to the left sidebar's width, clamped.
fn resize_left(&mut self, delta: Pixels, cx: &mut Context<Self>) {
self.left_width =
(self.left_width + delta).clamp(px(MIN_SIDEBAR_WIDTH), px(MAX_SIDEBAR_WIDTH));
cx.notify();
}
/// Applies a drag delta to the right sidebar's width, clamped.
fn resize_right(&mut self, delta: Pixels, cx: &mut Context<Self>) {
self.right_width =
(self.right_width - delta).clamp(px(MIN_SIDEBAR_WIDTH), px(MAX_SIDEBAR_WIDTH));
cx.notify();
}
}
/// Which sidebar a region or toggle belongs to.
#[derive(Clone, Copy, PartialEq, Eq)]
enum Side {
Left,
Right,
}
impl Side {
fn name(self) -> &'static str {
match self {
Side::Left => "left",
Side::Right => "right",
}
}
}
/// The application shell.
#[derive(IntoElement)]
pub struct Shell {
id: ElementId,
state: Entity<LayoutState>,
left_sidebar: Option<Sidebar>,
right_sidebar: Option<AnyElement>,
center: Option<AnyElement>,
}
impl Shell {
/// Creates the shell bound to `state`.
pub fn new<I>(id: I, state: Entity<LayoutState>) -> Self
where
I: Into<ElementId>,
{
Self {
id: id.into(),
state,
left_sidebar: None,
right_sidebar: None,
center: None,
}
}
/// Sets the left sidebar's navigation list.
pub fn left_sidebar(mut self, sidebar: Sidebar) -> Self {
self.left_sidebar = Some(sidebar);
self
}
/// Sets the right sidebar's content: a details or inspector pane.
pub fn right_sidebar<E>(mut self, content: E) -> Self
where
E: IntoElement,
{
self.right_sidebar = Some(content.into_any_element());
self
}
/// Sets the center region's content, which hosts the dock.
pub fn center<E>(mut self, content: E) -> Self
where
E: IntoElement,
{
self.center = Some(content.into_any_element());
self
}
}
impl RenderOnce for Shell {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme();
let metrics = theme.sizes.resolve(Size::default());
let background = theme.colors.background;
let titlebar_height = theme.titlebar_height;
let id = self.id.clone();
let state = self.state;
let mut left = self.left_sidebar;
let mut right = self.right_sidebar;
let mut center = self.center;
let left_visible = state.read(cx).is_left_sidebar_visible();
let right_visible = state.read(cx).is_right_sidebar_visible();
let left_material = left
.as_ref()
.is_some_and(|sidebar| sidebar.is_transparent());
let layout = div()
.flex()
.flex_row()
.w_full()
.h_full()
.overflow_hidden()
.when(left_visible && left.is_some(), |layout| {
let width = state.read(cx).left_sidebar_width();
layout
.child(sidebar_region(
(id.clone(), "left"),
width,
left.take()
.map(|sidebar| sidebar.into_any_element())
.unwrap_or_else(|| div().into_any_element()),
Side::Left,
left_material,
&state,
titlebar_height,
metrics.dense_padding_x,
window,
cx,
))
.child(ResizeHandle::new((id.clone(), "left-resize")).on_resize({
let state = state.clone();
move |delta, _, cx| {
state.update(cx, |state, cx| state.resize_left(delta, cx));
}
}))
})
.child(
div()
.flex_1()
.h_full()
.min_w_0()
.overflow_hidden()
.bg(background)
.child(center.take().unwrap_or_else(|| div().into_any_element())),
)
.when(right_visible && right.is_some(), |layout| {
let width = state.read(cx).right_sidebar_width();
layout
.child(ResizeHandle::new((id.clone(), "right-resize")).on_resize({
let state = state.clone();
move |delta, _, cx| {
state.update(cx, |state, cx| state.resize_right(delta, cx));
}
}))
.child(sidebar_region(
(id.clone(), "right"),
width,
right.take().unwrap_or_else(|| div().into_any_element()),
Side::Right,
false,
&state,
titlebar_height,
metrics.dense_padding_x,
window,
cx,
))
});
// Wraps the layout in a window frame.
frame(layout, window, cx)
}
}
/// One sidebar: a header strip with the hide toggle over the app's content.
#[allow(clippy::too_many_arguments)]
fn sidebar_region(
id: impl Into<ElementId>,
width: Pixels,
content: AnyElement,
side: Side,
material: bool,
state: &Entity<LayoutState>,
strip_height: Pixels,
padding_x: Pixels,
window: &mut Window,
cx: &mut App,
) -> AnyElement {
let id = id.into();
let theme = cx.theme();
let background = theme.colors.background;
let strip_background = match (side, material) {
(Side::Left, true) => theme.material.background,
_ => theme.colors.background,
};
let strip = h_flex()
.id((id.clone(), "strip"))
.debug_selector(move || format!("sidebar-strip-{}", side.name()))
.flex_none()
.h(strip_height)
.bg(strip_background)
.items_center()
.map(|strip| match side {
Side::Left => strip.when(cfg!(target_os = "macos"), |strip| {
strip.pl(TRAFFIC_LIGHT_PAD)
}),
Side::Right => strip,
})
.px(padding_x)
.child(titlebar_strip(
div().id((id.clone(), "strip-drag")).flex_1().self_stretch(),
window,
cx,
))
.map(|strip| match side {
Side::Left => strip.child(
Button::new((id, "sidebar-toggle"))
.icon(IconRole::PanelLeft)
.variant(ButtonVariant::Ghost)
.small()
.accessibility_label("Hide left sidebar")
.on_click({
let state = state.clone();
move |_, _, cx| {
state.update(cx, |state, cx| state.set_left_sidebar_visible(false, cx))
}
}),
),
Side::Right => strip.child(WindowControls::new((id, "window-controls"))),
});
v_flex()
.w(width)
.h_full()
.overflow_hidden()
.child(strip)
.child(
div()
.flex_1()
.min_h_0()
.overflow_hidden()
.when(!material, |content| content.bg(background))
.child(content),
)
.into_any_element()
}