311 lines
9.6 KiB
Rust
311 lines
9.6 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, SharedString, 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",
|
|
}
|
|
}
|
|
|
|
fn toggle_icon(self) -> IconRole {
|
|
match self {
|
|
Side::Left => IconRole::PanelLeft,
|
|
Side::Right => IconRole::PanelRight,
|
|
}
|
|
}
|
|
|
|
fn toggle_label(self) -> SharedString {
|
|
match self {
|
|
Side::Left => SharedString::from("Hide left sidebar"),
|
|
Side::Right => SharedString::from("Hide right sidebar"),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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 state = self.state;
|
|
let id = self.id.clone();
|
|
|
|
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 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(
|
|
width,
|
|
left.take()
|
|
.map(|sidebar| sidebar.into_any_element())
|
|
.unwrap_or_else(|| div().into_any_element()),
|
|
Side::Left,
|
|
&state,
|
|
metrics.strip_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()
|
|
.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(
|
|
width,
|
|
right.take().unwrap_or_else(|| div().into_any_element()),
|
|
Side::Right,
|
|
&state,
|
|
metrics.strip_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(
|
|
width: Pixels,
|
|
content: AnyElement,
|
|
side: Side,
|
|
state: &Entity<LayoutState>,
|
|
strip_height: Pixels,
|
|
padding_x: Pixels,
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
) -> AnyElement {
|
|
let strip = h_flex()
|
|
.id(SharedString::from(format!("sidebar-strip-{}", side.name())))
|
|
.debug_selector(move || format!("sidebar-strip-{}", side.name()))
|
|
.flex_none()
|
|
.h(strip_height)
|
|
.items_center()
|
|
.justify_end()
|
|
.map(|strip| titlebar_strip(strip, window, cx))
|
|
.map(|strip| match side {
|
|
Side::Left => strip.when(cfg!(target_os = "macos"), |strip| {
|
|
strip.pl(px(TRAFFIC_LIGHT_PAD))
|
|
}),
|
|
Side::Right => strip.child(WindowControls::new("sidebar-window-controls")),
|
|
})
|
|
.px(padding_x)
|
|
.child(
|
|
Button::new(SharedString::from(format!(
|
|
"sidebar-toggle-{}",
|
|
side.name()
|
|
)))
|
|
.icon(side.toggle_icon())
|
|
.variant(ButtonVariant::Ghost)
|
|
.small()
|
|
.accessibility_label(side.toggle_label())
|
|
.on_click({
|
|
let state = state.clone();
|
|
move |_, _, cx| match side {
|
|
Side::Left => {
|
|
state.update(cx, |state, cx| state.set_left_sidebar_visible(false, cx))
|
|
}
|
|
Side::Right => {
|
|
state.update(cx, |state, cx| state.set_right_sidebar_visible(false, cx))
|
|
}
|
|
}
|
|
}),
|
|
);
|
|
|
|
v_flex()
|
|
.w(width)
|
|
.h_full()
|
|
.overflow_hidden()
|
|
.child(strip)
|
|
.child(div().flex_1().min_h_0().overflow_hidden().child(content))
|
|
.into_any_element()
|
|
}
|