forked from reya/gpui-native-kit
322 lines
10 KiB
Rust
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()
|
|
}
|