chore: audit components
This commit is contained in:
@@ -8,7 +8,7 @@ description = "Application layout bootstrap: sidebars and a center dock, one car
|
||||
gpui = { workspace = true }
|
||||
gpui-native-theme = { workspace = true }
|
||||
gpui-native-utils = { workspace = true }
|
||||
gpui-native-core = { workspace = true, optional = true, features = ["button"] }
|
||||
gpui-native-core = { workspace = true, optional = true }
|
||||
gpui-native-data = { workspace = true, optional = true, features = ["sidebar"] }
|
||||
gpui-native-icons = { workspace = true, optional = true }
|
||||
|
||||
@@ -33,6 +33,7 @@ shell = [
|
||||
"resize_handle",
|
||||
"window_controls",
|
||||
"dep:gpui-native-core",
|
||||
"gpui-native-core/button",
|
||||
"dep:gpui-native-data",
|
||||
"dep:gpui-native-icons",
|
||||
]
|
||||
|
||||
+48
-56
@@ -32,6 +32,7 @@ type PanelBuilder = Box<dyn Fn(&mut Window, &mut App) -> AnyView>;
|
||||
type OpenRightSidebarHandler = Rc<dyn Fn(&mut App)>;
|
||||
|
||||
/// A registered panel: its identity, chrome, and how its view is built.
|
||||
#[must_use]
|
||||
pub struct PanelDescriptor {
|
||||
id: SharedString,
|
||||
title: SharedString,
|
||||
@@ -237,7 +238,7 @@ impl DockState {
|
||||
|
||||
/// Points the right sidebar at the panel's own view and asks the app to open it.
|
||||
fn show_right_view_of(&mut self, id: &str, cx: &mut Context<Self>) {
|
||||
let right_view = tab_in(&self.root, id).and_then(|tab| tab.right_view.clone());
|
||||
let right_view = Self::tab_in(&self.root, id).and_then(|tab| tab.right_view.clone());
|
||||
let Some(right_view) = right_view else {
|
||||
return;
|
||||
};
|
||||
@@ -300,7 +301,7 @@ impl DockState {
|
||||
|
||||
// The sidebar showed the closed panel's view.
|
||||
if self.right_view.as_ref() == closed_right_view.as_ref() {
|
||||
self.right_view = first_right_view(&self.root);
|
||||
self.right_view = Self::first_right_view(&self.root);
|
||||
}
|
||||
|
||||
if emptied {
|
||||
@@ -341,7 +342,7 @@ impl DockState {
|
||||
})
|
||||
})
|
||||
.and_then(|tab| tab.right_view.clone())
|
||||
.or_else(|| first_right_view(&self.root));
|
||||
.or_else(|| Self::first_right_view(&self.root));
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
@@ -372,7 +373,7 @@ impl DockState {
|
||||
}
|
||||
|
||||
// The closed pane's views are gone; show the first remaining one.
|
||||
self.right_view = first_right_view(&self.root);
|
||||
self.right_view = Self::first_right_view(&self.root);
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
@@ -495,7 +496,7 @@ impl DockState {
|
||||
|
||||
/// The active tab of the pane new panels open into.
|
||||
pub fn active_panel(&self) -> Option<SharedString> {
|
||||
let pane = pane_in(&self.root, self.target_pane_key())?;
|
||||
let pane = Self::pane_in(&self.root, self.target_pane_key())?;
|
||||
pane.active.clone()
|
||||
}
|
||||
|
||||
@@ -522,7 +523,7 @@ impl DockState {
|
||||
}
|
||||
|
||||
fn target_pane_key(&self) -> u64 {
|
||||
if pane_in(&self.root, self.active_pane).is_some() {
|
||||
if Self::pane_in(&self.root, self.active_pane).is_some() {
|
||||
self.active_pane
|
||||
} else {
|
||||
self.first_pane_key().unwrap_or(0)
|
||||
@@ -613,7 +614,7 @@ impl DockState {
|
||||
) {
|
||||
match node {
|
||||
Node::Pane(pane) if pane.key == key => {
|
||||
let old_pane = take_node(node);
|
||||
let old_pane = Self::take_node(node);
|
||||
let split = if direction.leads() {
|
||||
Split {
|
||||
key: split_key,
|
||||
@@ -635,9 +636,9 @@ impl DockState {
|
||||
}
|
||||
Node::Pane(_) => {}
|
||||
Node::Split(split) => {
|
||||
if pane_in(&split.a, key).is_some() {
|
||||
if Self::pane_in(&split.a, key).is_some() {
|
||||
Self::wrap_in(&mut split.a, key, new_pane, direction, split_key);
|
||||
} else if pane_in(&split.b, key).is_some() {
|
||||
} else if Self::pane_in(&split.b, key).is_some() {
|
||||
Self::wrap_in(&mut split.b, key, new_pane, direction, split_key);
|
||||
}
|
||||
}
|
||||
@@ -654,14 +655,14 @@ impl DockState {
|
||||
return;
|
||||
};
|
||||
|
||||
let a_direct = direct_pane_key(&split.a) == Some(key);
|
||||
let b_direct = direct_pane_key(&split.b) == Some(key);
|
||||
let a_direct = Self::direct_pane_key(&split.a) == Some(key);
|
||||
let b_direct = Self::direct_pane_key(&split.b) == Some(key);
|
||||
|
||||
if a_direct != b_direct {
|
||||
let survivor = if a_direct {
|
||||
take_node(&mut split.b)
|
||||
Self::take_node(&mut split.b)
|
||||
} else {
|
||||
take_node(&mut split.a)
|
||||
Self::take_node(&mut split.a)
|
||||
};
|
||||
*node = survivor;
|
||||
} else {
|
||||
@@ -699,52 +700,46 @@ impl DockState {
|
||||
}
|
||||
snap(&self.root)
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds a pane by key anywhere in the tree.
|
||||
fn pane_in(node: &Node, key: u64) -> Option<&Pane> {
|
||||
match node {
|
||||
Node::Pane(pane) => (pane.key == key).then_some(pane),
|
||||
Node::Split(split) => pane_in(&split.a, key).or_else(|| pane_in(&split.b, key)),
|
||||
/// Finds an open tab by panel id, anywhere in the tree.
|
||||
fn tab_in<'a>(node: &'a Node, id: &str) -> Option<&'a PanelTab> {
|
||||
match node {
|
||||
Node::Pane(pane) => pane.tabs.iter().find(|tab| tab.panel.as_ref() == id),
|
||||
Node::Split(split) => Self::tab_in(&split.a, id).or_else(|| Self::tab_in(&split.b, id)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds an open tab by panel id, anywhere in the tree.
|
||||
fn tab_in<'a>(node: &'a Node, id: &str) -> Option<&'a PanelTab> {
|
||||
match node {
|
||||
Node::Pane(pane) => pane.tabs.iter().find(|tab| tab.panel.as_ref() == id),
|
||||
Node::Split(split) => tab_in(&split.a, id).or_else(|| tab_in(&split.b, id)),
|
||||
/// The first open tab's right sidebar view, in display order.
|
||||
fn first_right_view(node: &Node) -> Option<AnyView> {
|
||||
match node {
|
||||
Node::Pane(pane) => pane.tabs.iter().find_map(|tab| tab.right_view.clone()),
|
||||
Node::Split(split) => {
|
||||
Self::first_right_view(&split.a).or_else(|| Self::first_right_view(&split.b))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The first open tab's right sidebar view, in display order.
|
||||
fn first_right_view(node: &Node) -> Option<AnyView> {
|
||||
match node {
|
||||
Node::Pane(pane) => pane.tabs.iter().find_map(|tab| tab.right_view.clone()),
|
||||
Node::Split(split) => first_right_view(&split.a).or_else(|| first_right_view(&split.b)),
|
||||
/// The pane key of a node that is a pane, for seam-level tree edits.
|
||||
fn direct_pane_key(node: &Node) -> Option<u64> {
|
||||
match node {
|
||||
Node::Pane(pane) => Some(pane.key),
|
||||
Node::Split(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The pane key of a node that is a pane, for seam-level tree edits.
|
||||
fn direct_pane_key(node: &Node) -> Option<u64> {
|
||||
match node {
|
||||
Node::Pane(pane) => Some(pane.key),
|
||||
Node::Split(_) => None,
|
||||
/// Takes a node out, leaving a placeholder that is never read again.
|
||||
fn take_node(node: &mut Node) -> Node {
|
||||
std::mem::replace(
|
||||
node,
|
||||
Node::Pane(Pane {
|
||||
key: u64::MAX,
|
||||
tabs: Vec::new(),
|
||||
active: None,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Takes a node out, leaving a placeholder that is never read again.
|
||||
fn take_node(node: &mut Node) -> Node {
|
||||
std::mem::replace(
|
||||
node,
|
||||
Node::Pane(Pane {
|
||||
key: u64::MAX,
|
||||
tabs: Vec::new(),
|
||||
active: None,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/// One open panel view, per frame.
|
||||
#[derive(Clone)]
|
||||
struct TabView {
|
||||
@@ -776,6 +771,7 @@ enum TreeView {
|
||||
|
||||
/// A dock: renders the pane tree of a [`DockState`].
|
||||
#[derive(IntoElement)]
|
||||
#[must_use]
|
||||
pub struct Dock {
|
||||
id: ElementId,
|
||||
state: Entity<DockState>,
|
||||
@@ -1095,6 +1091,7 @@ fn render_pane(
|
||||
});
|
||||
|
||||
let bar_id = SharedString::from(format!("{dock_id}-pane-{}-bar", pane.key));
|
||||
let strip_id = SharedString::from(format!("{dock_id}-pane-{}-strip", pane.key));
|
||||
|
||||
h_flex()
|
||||
.id(bar_id.clone())
|
||||
@@ -1111,13 +1108,8 @@ fn render_pane(
|
||||
.pr(padding_x)
|
||||
.child(tabs)
|
||||
.child(titlebar_strip(
|
||||
div()
|
||||
.id(SharedString::from(format!(
|
||||
"{dock_id}-pane-{}-strip",
|
||||
pane.key
|
||||
)))
|
||||
.flex_1()
|
||||
.self_stretch(),
|
||||
div().id(strip_id.clone()).flex_1().self_stretch(),
|
||||
strip_id,
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
|
||||
@@ -6,15 +6,11 @@ use gpui::{
|
||||
InteractiveElement as _, IntoElement, ParentElement as _, Pixels, RenderOnce, SharedString,
|
||||
StyleRefinement, Styled, Window, canvas, div, px,
|
||||
};
|
||||
use gpui_native_theme::{PANE_DEFAULT_SIZE, PANE_MIN_SIZE};
|
||||
use gpui_native_utils::StyledExt as _;
|
||||
|
||||
use crate::resize_handle::ResizeHandle;
|
||||
|
||||
/// The size a pane starts at before the group has measured its extent.
|
||||
const DEFAULT_PANE_SIZE: f32 = 200.0;
|
||||
/// The floor a pane keeps while dragging unless the caller lowers it.
|
||||
const DEFAULT_PANE_MIN: f32 = 48.0;
|
||||
|
||||
type DebugSelector = std::rc::Rc<dyn Fn() -> String>;
|
||||
|
||||
/// The retained pane sizes of one resizable group.
|
||||
@@ -56,11 +52,12 @@ impl ResizableState {
|
||||
}
|
||||
}
|
||||
|
||||
/// The equal split of the measured extent.
|
||||
/// The equal split of the measured extent, or the default pane size unmeasured.
|
||||
fn equal_split(&self, pane_count: usize) -> Pixels {
|
||||
match self.measured {
|
||||
Some(extent) => extent / pane_count as f32,
|
||||
None => px(DEFAULT_PANE_SIZE),
|
||||
// usize can exceed f32's exact integer range; bound it via u32 before casting.
|
||||
Some(extent) => extent / u32::try_from(pane_count).unwrap_or(u32::MAX) as f32,
|
||||
None => PANE_DEFAULT_SIZE,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,8 +90,11 @@ impl ResizableState {
|
||||
size = size.min(ceiling);
|
||||
}
|
||||
|
||||
// Earlier panes hold the equal split they already render at, so the
|
||||
// first drag does not snap them to a backfilled default.
|
||||
let backfill = self.equal_split(pane_count);
|
||||
while self.sizes.len() <= ix {
|
||||
self.sizes.push(px(DEFAULT_PANE_SIZE));
|
||||
self.sizes.push(backfill);
|
||||
}
|
||||
|
||||
self.sizes[ix] = size;
|
||||
@@ -105,7 +105,7 @@ impl ResizableState {
|
||||
fn reset_pane(&mut self, ix: usize, pane_count: usize, cx: &mut Context<Self>) {
|
||||
let split = self.equal_split(pane_count);
|
||||
while self.sizes.len() <= ix {
|
||||
self.sizes.push(px(DEFAULT_PANE_SIZE));
|
||||
self.sizes.push(split);
|
||||
}
|
||||
self.sizes[ix] = split;
|
||||
cx.notify();
|
||||
@@ -113,6 +113,7 @@ impl ResizableState {
|
||||
}
|
||||
|
||||
/// One pane of a resizable group: its content and its drag floor.
|
||||
#[must_use]
|
||||
pub struct ResizablePane {
|
||||
content: AnyElement,
|
||||
min: Pixels,
|
||||
@@ -120,7 +121,7 @@ pub struct ResizablePane {
|
||||
|
||||
impl ResizablePane {
|
||||
/// The floor a pane keeps while dragging unless the caller lowers it.
|
||||
pub const DEFAULT_MIN: Pixels = px(DEFAULT_PANE_MIN);
|
||||
pub const DEFAULT_MIN: Pixels = PANE_MIN_SIZE;
|
||||
|
||||
/// Wraps this element as a pane.
|
||||
pub fn new<E>(content: E) -> Self
|
||||
@@ -142,6 +143,7 @@ impl ResizablePane {
|
||||
|
||||
/// A group of panes stacked along one axis, separated by drag dividers.
|
||||
#[derive(IntoElement)]
|
||||
#[must_use]
|
||||
pub struct Resizable {
|
||||
id: ElementId,
|
||||
state: Entity<ResizableState>,
|
||||
@@ -250,6 +252,7 @@ impl RenderOnce for Resizable {
|
||||
.into_any_element();
|
||||
|
||||
let divider = (!remainder).then(|| {
|
||||
// The positional index is a stable id: panes are append-only, never reordered.
|
||||
ResizeHandle::new(SharedString::from(format!("{}-divider-{ix}", self.id)))
|
||||
.axis(axis)
|
||||
.on_resize({
|
||||
|
||||
@@ -10,7 +10,9 @@ use gpui::{
|
||||
SharedString, StatefulInteractiveElement as _, Styled as _, Window, canvas, deferred, div,
|
||||
fill, point, px,
|
||||
};
|
||||
use gpui_native_theme::{ActiveTheme as _, RESIZE_KEYBOARD_STEP, Size};
|
||||
use gpui_native_theme::{
|
||||
ActiveTheme as _, BORDER_WIDTH, HAIRLINE_WIDTH, RESIZE_KEYBOARD_STEP, Size,
|
||||
};
|
||||
use gpui_native_utils::{focus_handle, focus_ring};
|
||||
|
||||
/// How far one keyboard arrow step moves the seam.
|
||||
@@ -28,12 +30,13 @@ struct DragState {
|
||||
|
||||
/// A thin drag divider that resizes the adjacent region.
|
||||
#[derive(IntoElement)]
|
||||
#[must_use]
|
||||
pub struct ResizeHandle {
|
||||
id: ElementId,
|
||||
/// The axis the drag moves along: a vertical seam drags horizontally.
|
||||
axis: Axis,
|
||||
disabled: bool,
|
||||
a11y_label: Option<SharedString>,
|
||||
accessibility_label: Option<SharedString>,
|
||||
on_resize: Option<ResizeHandler>,
|
||||
on_reset: Option<ResetHandler>,
|
||||
}
|
||||
@@ -48,7 +51,7 @@ impl ResizeHandle {
|
||||
id: id.into(),
|
||||
axis: Axis::Horizontal,
|
||||
disabled: false,
|
||||
a11y_label: None,
|
||||
accessibility_label: None,
|
||||
on_resize: None,
|
||||
on_reset: None,
|
||||
}
|
||||
@@ -71,7 +74,7 @@ impl ResizeHandle {
|
||||
where
|
||||
L: Into<SharedString>,
|
||||
{
|
||||
self.a11y_label = Some(label.into());
|
||||
self.accessibility_label = Some(label.into());
|
||||
self
|
||||
}
|
||||
|
||||
@@ -105,7 +108,7 @@ impl RenderOnce for ResizeHandle {
|
||||
let straddle = -thickness / 2.0;
|
||||
|
||||
let label = self
|
||||
.a11y_label
|
||||
.accessibility_label
|
||||
.clone()
|
||||
.unwrap_or_else(|| SharedString::from("Resize"));
|
||||
|
||||
@@ -221,17 +224,17 @@ impl RenderOnce for ResizeHandle {
|
||||
|
||||
let line = match axis {
|
||||
Axis::Horizontal => {
|
||||
let x = bounds.center().x - px(0.5);
|
||||
let x = bounds.center().x - HAIRLINE_WIDTH;
|
||||
Bounds::from_corners(
|
||||
point(x, bounds.top()),
|
||||
point(x + px(1.), bounds.bottom()),
|
||||
point(x + BORDER_WIDTH, bounds.bottom()),
|
||||
)
|
||||
}
|
||||
Axis::Vertical => {
|
||||
let y = bounds.center().y - px(0.5);
|
||||
let y = bounds.center().y - HAIRLINE_WIDTH;
|
||||
Bounds::from_corners(
|
||||
point(bounds.left(), y),
|
||||
point(bounds.right(), y + px(1.)),
|
||||
point(bounds.right(), y + BORDER_WIDTH),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
+53
-40
@@ -10,19 +10,14 @@ 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 gpui_native_theme::{
|
||||
ActiveTheme as _, SIDEBAR_DEFAULT_WIDTH, SIDEBAR_MAX_WIDTH, SIDEBAR_MIN_WIDTH, 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,
|
||||
@@ -37,8 +32,8 @@ impl LayoutState {
|
||||
cx.new(|_| Self {
|
||||
left_visible: true,
|
||||
right_visible: false,
|
||||
left_width: px(DEFAULT_SIDEBAR_WIDTH),
|
||||
right_width: px(DEFAULT_SIDEBAR_WIDTH),
|
||||
left_width: SIDEBAR_DEFAULT_WIDTH,
|
||||
right_width: SIDEBAR_DEFAULT_WIDTH,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -80,16 +75,20 @@ impl LayoutState {
|
||||
|
||||
/// 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();
|
||||
let width = (self.left_width + delta).clamp(SIDEBAR_MIN_WIDTH, SIDEBAR_MAX_WIDTH);
|
||||
if (self.left_width - width).abs() > px(f32::EPSILON) {
|
||||
self.left_width = 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();
|
||||
let width = (self.right_width - delta).clamp(SIDEBAR_MIN_WIDTH, SIDEBAR_MAX_WIDTH);
|
||||
if (self.right_width - width).abs() > px(f32::EPSILON) {
|
||||
self.right_width = width;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,6 +110,7 @@ impl Side {
|
||||
|
||||
/// The application shell.
|
||||
#[derive(IntoElement)]
|
||||
#[must_use]
|
||||
pub struct Shell {
|
||||
id: ElementId,
|
||||
state: Entity<LayoutState>,
|
||||
@@ -194,11 +194,13 @@ impl RenderOnce for Shell {
|
||||
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,
|
||||
Titlebar {
|
||||
side: Side::Left,
|
||||
material: left_material,
|
||||
height: titlebar_height,
|
||||
padding_x: metrics.dense_padding_x,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
@@ -231,11 +233,13 @@ impl RenderOnce for Shell {
|
||||
(id.clone(), "right"),
|
||||
width,
|
||||
right.take().unwrap_or_else(|| div().into_any_element()),
|
||||
Side::Right,
|
||||
false,
|
||||
&state,
|
||||
titlebar_height,
|
||||
metrics.dense_padding_x,
|
||||
Titlebar {
|
||||
side: Side::Right,
|
||||
material: false,
|
||||
height: titlebar_height,
|
||||
padding_x: metrics.dense_padding_x,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
@@ -246,48 +250,57 @@ impl RenderOnce for Shell {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
/// Which titlebar duties a sidebar's header strip carries.
|
||||
#[derive(Clone, Copy)]
|
||||
struct Titlebar {
|
||||
side: Side,
|
||||
material: bool,
|
||||
state: &Entity<LayoutState>,
|
||||
strip_height: Pixels,
|
||||
height: Pixels,
|
||||
padding_x: Pixels,
|
||||
}
|
||||
|
||||
/// One sidebar: a header strip with the hide toggle over the app's content.
|
||||
fn sidebar_region<I>(
|
||||
id: I,
|
||||
width: Pixels,
|
||||
content: AnyElement,
|
||||
state: &Entity<LayoutState>,
|
||||
titlebar: Titlebar,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> AnyElement {
|
||||
) -> AnyElement
|
||||
where
|
||||
I: Into<ElementId>,
|
||||
{
|
||||
let id = id.into();
|
||||
let theme = cx.theme();
|
||||
let background = theme.colors.background;
|
||||
let strip_background = match (side, material) {
|
||||
let strip_background = match (titlebar.side, titlebar.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()))
|
||||
.debug_selector(move || format!("sidebar-strip-{}", titlebar.side.name()))
|
||||
.flex_none()
|
||||
.h(strip_height)
|
||||
.h(titlebar.height)
|
||||
.bg(strip_background)
|
||||
.items_center()
|
||||
.map(|strip| match side {
|
||||
.map(|strip| match titlebar.side {
|
||||
Side::Left => strip.when(cfg!(target_os = "macos"), |strip| {
|
||||
strip.pl(TRAFFIC_LIGHT_PAD)
|
||||
}),
|
||||
Side::Right => strip,
|
||||
})
|
||||
.px(padding_x)
|
||||
.px(titlebar.padding_x)
|
||||
.child(titlebar_strip(
|
||||
div().id((id.clone(), "strip-drag")).flex_1().self_stretch(),
|
||||
(id.clone(), "strip-drag"),
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
.map(|strip| match side {
|
||||
.map(|strip| match titlebar.side {
|
||||
Side::Left => strip.child(
|
||||
Button::new((id, "sidebar-toggle"))
|
||||
.icon(IconRole::PanelLeft)
|
||||
@@ -314,7 +327,7 @@ fn sidebar_region(
|
||||
.flex_1()
|
||||
.min_h_0()
|
||||
.overflow_hidden()
|
||||
.when(!material, |content| content.bg(background))
|
||||
.when(!titlebar.material, |content| content.bg(background))
|
||||
.child(content),
|
||||
)
|
||||
.into_any_element()
|
||||
|
||||
@@ -8,6 +8,8 @@ use gpui::WindowControlArea;
|
||||
use gpui::{App, ClickEvent, Div, ElementId, IntoElement, RenderOnce, Stateful, Window, div};
|
||||
use gpui::{InteractiveElement as _, MouseButton, StatefulInteractiveElement as _};
|
||||
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
use gpui::Styled as _;
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
@@ -19,13 +21,18 @@ use gpui_native_theme::ActiveTheme as _;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
use gpui::Decorations;
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
use gpui::SharedString;
|
||||
#[cfg(target_os = "windows")]
|
||||
use gpui::{SharedString, px, rgba};
|
||||
use gpui::px;
|
||||
|
||||
/// macOS traffic-lights zone: the pad a leading titlebar strip reserves.
|
||||
pub use gpui_native_theme::TRAFFIC_LIGHT_PAD;
|
||||
#[cfg(target_os = "windows")]
|
||||
use gpui_native_theme::{WINDOWS_CAPTION_WIDTH as CAPTION_WIDTH, WINDOWS_CLOSE_HOVER_HEX};
|
||||
use gpui_native_theme::{
|
||||
WINDOWS_CAPTION_WIDTH as CAPTION_WIDTH, WINDOWS_CLOSE_FOREGROUND, WINDOWS_CLOSE_HOVER,
|
||||
WINDOWS_CLOSE_PRESSED,
|
||||
};
|
||||
|
||||
/// The window controls for the trailing end of a titlebar strip.
|
||||
#[derive(IntoElement)]
|
||||
@@ -49,6 +56,7 @@ impl RenderOnce for WindowControls {
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let controls = {
|
||||
// macOS draws native traffic lights in the shell's reserved pad; nothing to render.
|
||||
let _ = (id, window, cx);
|
||||
div()
|
||||
};
|
||||
@@ -97,7 +105,6 @@ impl RenderOnce for WindowControls {
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
let controls = {
|
||||
let _ = id;
|
||||
let controls = window.window_controls();
|
||||
let client_decorated =
|
||||
matches!(window.window_decorations(), Decorations::Client { .. });
|
||||
@@ -111,7 +118,7 @@ impl RenderOnce for WindowControls {
|
||||
.when(client_decorated && controls.minimize, |row| {
|
||||
row.child(circle_button(
|
||||
cx,
|
||||
"window-minimize",
|
||||
SharedString::from(format!("{id}-minimize")),
|
||||
IconRole::WindowMinimize,
|
||||
window.is_minimizable(),
|
||||
|window, _cx| window.minimize_window(),
|
||||
@@ -120,7 +127,7 @@ impl RenderOnce for WindowControls {
|
||||
.when(client_decorated && controls.maximize, |row| {
|
||||
row.child(circle_button(
|
||||
cx,
|
||||
"window-maximize",
|
||||
SharedString::from(format!("{id}-maximize")),
|
||||
if window.is_maximized() {
|
||||
IconRole::WindowRestore
|
||||
} else {
|
||||
@@ -133,7 +140,7 @@ impl RenderOnce for WindowControls {
|
||||
.when(client_decorated, |row| {
|
||||
row.child(circle_button(
|
||||
cx,
|
||||
"window-close",
|
||||
SharedString::from(format!("{id}-close")),
|
||||
IconRole::Close,
|
||||
true,
|
||||
|window, _cx| window.remove_window(),
|
||||
@@ -155,6 +162,7 @@ fn maximize_on_double_click() -> impl Fn(&ClickEvent, &mut Window, &mut App) + '
|
||||
window.titlebar_double_click();
|
||||
#[cfg(target_os = "linux")]
|
||||
window.zoom_window();
|
||||
// Windows handles titlebar double-click natively via WindowControlArea::Drag.
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let _ = (event, window);
|
||||
@@ -162,9 +170,18 @@ fn maximize_on_double_click() -> impl Fn(&ClickEvent, &mut Window, &mut App) + '
|
||||
}
|
||||
}
|
||||
|
||||
/// Makes an element a titlebar strip.
|
||||
pub fn titlebar_strip(this: Stateful<Div>, window: &mut Window, cx: &mut App) -> Stateful<Div> {
|
||||
let should_move = window.use_state(cx, |_, _| false);
|
||||
/// Makes an element a titlebar strip; `id` keys its drag state, which
|
||||
/// `use_state` would otherwise share among every strip in the window.
|
||||
pub fn titlebar_strip<I>(
|
||||
this: Stateful<Div>,
|
||||
id: I,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Stateful<Div>
|
||||
where
|
||||
I: Into<ElementId>,
|
||||
{
|
||||
let should_move = window.use_keyed_state(id.into(), cx, |_, _| false);
|
||||
this.window_control_area(WindowControlArea::Drag)
|
||||
.on_mouse_down_out(window.listener_for(&should_move, |should_move, _, _, _| {
|
||||
*should_move = false;
|
||||
@@ -208,7 +225,7 @@ fn caption_button(
|
||||
let glyph_size = theme.sizes.small.caption_size;
|
||||
|
||||
let hover = if close {
|
||||
rgba(WINDOWS_CLOSE_HOVER_HEX)
|
||||
WINDOWS_CLOSE_HOVER
|
||||
} else {
|
||||
theme.colors.muted_hover
|
||||
};
|
||||
@@ -236,7 +253,7 @@ fn caption_button(
|
||||
.cursor_pointer()
|
||||
.hover(|hovered| {
|
||||
hovered.bg(hover).text_color(if close {
|
||||
gpui::white()
|
||||
WINDOWS_CLOSE_FOREGROUND
|
||||
} else {
|
||||
theme.colors.foreground
|
||||
})
|
||||
@@ -244,7 +261,7 @@ fn caption_button(
|
||||
.active(|pressed| {
|
||||
pressed
|
||||
.bg(if close {
|
||||
hover.opacity(0.8)
|
||||
WINDOWS_CLOSE_PRESSED
|
||||
} else {
|
||||
theme.ramps.secondary.pressed
|
||||
})
|
||||
@@ -257,14 +274,9 @@ fn caption_button(
|
||||
|
||||
/// Adwaita's headerbar control: a small circle around a symbolic glyph.
|
||||
#[cfg(target_os = "linux")]
|
||||
fn circle_button<F>(
|
||||
cx: &App,
|
||||
id: &'static str,
|
||||
role: IconRole,
|
||||
enabled: bool,
|
||||
action: F,
|
||||
) -> AnyElement
|
||||
fn circle_button<I, F>(cx: &App, id: I, role: IconRole, enabled: bool, action: F) -> AnyElement
|
||||
where
|
||||
I: Into<ElementId>,
|
||||
F: Fn(&mut Window, &mut App) + 'static,
|
||||
{
|
||||
let theme = cx.theme();
|
||||
|
||||
@@ -7,13 +7,15 @@ use gpui::{
|
||||
IntoElement, MouseButton, ParentElement as _, Pixels, Point, ResizeEdge, Size, Styled as _,
|
||||
Tiling, Window, canvas, div, point, px, size,
|
||||
};
|
||||
use gpui_native_theme::{ActiveTheme as _, BORDER_WIDTH, WINDOW_RESIZE_INSET};
|
||||
|
||||
/// How much of the shadow's strength survives losing window focus.
|
||||
const INACTIVE_SHADOW_OPACITY: f32 = 0.7;
|
||||
use gpui_native_theme::{
|
||||
ActiveTheme as _, BORDER_WIDTH, INACTIVE_SHADOW_OPACITY, WINDOW_RESIZE_INSET,
|
||||
};
|
||||
|
||||
/// Wraps a window's root in the frame it owes.
|
||||
pub fn frame(child: impl IntoElement, window: &mut Window, cx: &App) -> Div {
|
||||
pub fn frame<E>(child: E, window: &mut Window, cx: &App) -> Div
|
||||
where
|
||||
E: IntoElement,
|
||||
{
|
||||
let Decorations::Client { tiling } = window.window_decorations() else {
|
||||
return div().size_full().child(child);
|
||||
};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![cfg(feature = "dock")]
|
||||
|
||||
use gpui::{
|
||||
AppContext as _, Context, Entity, InteractiveElement as _, IntoElement, Modifiers, MouseButton,
|
||||
MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Render, Styled as _,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![cfg(feature = "resizable")]
|
||||
|
||||
use gpui::{
|
||||
AppContext as _, Context, Entity, IntoElement, Modifiers, MouseButton, MouseDownEvent,
|
||||
MouseMoveEvent, ParentElement as _, Pixels, Render, Styled as _, Subscription, TestAppContext,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![cfg(feature = "resize_handle")]
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![cfg(feature = "shell")]
|
||||
|
||||
use gpui::{
|
||||
Entity, InteractiveElement as _, Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent,
|
||||
MouseUpEvent, ParentElement as _, Pixels, Render, Styled as _, TestAppContext,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
//! The titlebar duties: traffic-light pads and window controls, asserted per
|
||||
//! platform — macOS draws only pads, Windows/Linux only draw their controls.
|
||||
|
||||
#![cfg(feature = "dock")]
|
||||
|
||||
use gpui::{
|
||||
AppContext as _, Entity, IntoElement, ParentElement as _, Pixels, Render, Styled as _,
|
||||
TestAppContext, VisualTestContext, Window, div, px,
|
||||
|
||||
Reference in New Issue
Block a user