layout: make strips the titlebar with window controls
This commit is contained in:
@@ -40,7 +40,7 @@ jobs:
|
||||
run: |
|
||||
cargo check -p gpui-native-layout --no-default-features
|
||||
cargo check -p gpui-native-layout --all-features
|
||||
for f in dock resize_handle shell; do
|
||||
for f in dock resize_handle shell window_controls; do
|
||||
cargo check -p gpui-native-layout --no-default-features --features "$f"
|
||||
done
|
||||
- name: Feature checks (facade)
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect width="14" height="14" x="8" y="8" rx="2" ry="2" />
|
||||
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 339 B |
@@ -0,0 +1,13 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect width="18" height="18" x="3" y="3" rx="2" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 261 B |
@@ -31,6 +31,9 @@ pub enum IconRole {
|
||||
EyeOff,
|
||||
PanelLeft,
|
||||
PanelRight,
|
||||
WindowMinimize,
|
||||
WindowMaximize,
|
||||
WindowRestore,
|
||||
}
|
||||
|
||||
impl IconRole {
|
||||
@@ -52,6 +55,9 @@ impl IconRole {
|
||||
Self::EyeOff,
|
||||
Self::PanelLeft,
|
||||
Self::PanelRight,
|
||||
Self::WindowMinimize,
|
||||
Self::WindowMaximize,
|
||||
Self::WindowRestore,
|
||||
]
|
||||
}
|
||||
|
||||
@@ -73,6 +79,9 @@ impl IconRole {
|
||||
Self::EyeOff => "icons/eye-off.svg",
|
||||
Self::PanelLeft => "icons/panel-left.svg",
|
||||
Self::PanelRight => "icons/panel-right.svg",
|
||||
Self::WindowMinimize => "icons/minus.svg",
|
||||
Self::WindowMaximize => "icons/square.svg",
|
||||
Self::WindowRestore => "icons/copy.svg",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +103,9 @@ impl IconRole {
|
||||
Self::EyeOff => "eye-off",
|
||||
Self::PanelLeft => "panel-left",
|
||||
Self::PanelRight => "panel-right",
|
||||
Self::WindowMinimize => "window-minimize",
|
||||
Self::WindowMaximize => "window-maximize",
|
||||
Self::WindowRestore => "window-restore",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,6 +177,9 @@ impl IconRole {
|
||||
"panel-right",
|
||||
"sidebar-right",
|
||||
],
|
||||
Self::WindowMinimize => &["window-minimize-symbolic", "window-minimize"],
|
||||
Self::WindowMaximize => &["window-maximize-symbolic", "window-maximize"],
|
||||
Self::WindowRestore => &["window-restore-symbolic", "window-restore"],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -263,6 +278,8 @@ fn asset_bytes(path: &str) -> Option<&'static [u8]> {
|
||||
match path {
|
||||
"icons/check.svg" => Some(include_bytes!("../assets/check.svg")),
|
||||
"icons/x.svg" => Some(include_bytes!("../assets/x.svg")),
|
||||
"icons/square.svg" => Some(include_bytes!("../assets/square.svg")),
|
||||
"icons/copy.svg" => Some(include_bytes!("../assets/copy.svg")),
|
||||
"icons/chevron-down.svg" => Some(include_bytes!("../assets/chevron-down.svg")),
|
||||
"icons/chevron-left.svg" => Some(include_bytes!("../assets/chevron-left.svg")),
|
||||
"icons/chevron-right.svg" => Some(include_bytes!("../assets/chevron-right.svg")),
|
||||
|
||||
+19
-16
@@ -6,22 +6,25 @@ use crate::IconRole;
|
||||
/// The Segoe Fluent Icons codepoint for the role.
|
||||
pub(crate) fn glyph(role: IconRole) -> Option<char> {
|
||||
let codepoint = match role {
|
||||
IconRole::Check => 0xE73E, // CheckMark
|
||||
IconRole::Close => 0xE8BB, // ChromeClose
|
||||
IconRole::ChevronDown => 0xE70D, // ChevronDown
|
||||
IconRole::ChevronLeft => 0xE76B, // ChevronLeft
|
||||
IconRole::ChevronRight => 0xE76C, // ChevronRight
|
||||
IconRole::ChevronUp => 0xE70E, // ChevronUp
|
||||
IconRole::Plus => 0xE710, // Add
|
||||
IconRole::Minus => 0xE738, // Remove
|
||||
IconRole::Search => 0xE721, // Search
|
||||
IconRole::Folder => 0xE8B7, // Folder
|
||||
IconRole::Warning => 0xE7BA, // Warning
|
||||
IconRole::Info => 0xE946, // Info
|
||||
IconRole::Eye => 0xE7B3, // RedEye
|
||||
IconRole::EyeOff => 0xED1A, // Hide
|
||||
IconRole::PanelLeft => 0xE90C, // DockLeft
|
||||
IconRole::PanelRight => 0xE90D, // DockRight
|
||||
IconRole::Check => 0xE73E, // CheckMark
|
||||
IconRole::Close => 0xE8BB, // ChromeClose
|
||||
IconRole::ChevronDown => 0xE70D, // ChevronDown
|
||||
IconRole::ChevronLeft => 0xE76B, // ChevronLeft
|
||||
IconRole::ChevronRight => 0xE76C, // ChevronRight
|
||||
IconRole::ChevronUp => 0xE70E, // ChevronUp
|
||||
IconRole::Plus => 0xE710, // Add
|
||||
IconRole::Minus => 0xE738, // Remove
|
||||
IconRole::Search => 0xE721, // Search
|
||||
IconRole::Folder => 0xE8B7, // Folder
|
||||
IconRole::Warning => 0xE7BA, // Warning
|
||||
IconRole::Info => 0xE946, // Info
|
||||
IconRole::Eye => 0xE7B3, // RedEye
|
||||
IconRole::EyeOff => 0xED1A, // Hide
|
||||
IconRole::PanelLeft => 0xE90C, // DockLeft
|
||||
IconRole::PanelRight => 0xE90D, // DockRight
|
||||
IconRole::WindowMinimize => 0xE921, // ChromeMinimize
|
||||
IconRole::WindowMaximize => 0xE922, // ChromeMaximize
|
||||
IconRole::WindowRestore => 0xE923, // ChromeRestore
|
||||
};
|
||||
char::from_u32(codepoint)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ gpui = { workspace = true, features = ["test-support"] }
|
||||
|
||||
[features]
|
||||
default = ["full"]
|
||||
full = ["dock", "resize_handle", "shell"]
|
||||
full = ["dock", "resize_handle", "shell", "window_controls"]
|
||||
dock = [
|
||||
"resize_handle",
|
||||
"dep:gpui-native-core",
|
||||
@@ -25,11 +25,14 @@ dock = [
|
||||
"dep:gpui-native-icons",
|
||||
"gpui-native-core/context_menu",
|
||||
"gpui-native-data/tabs",
|
||||
"window_controls",
|
||||
]
|
||||
resize_handle = []
|
||||
shell = [
|
||||
"resize_handle",
|
||||
"window_controls",
|
||||
"dep:gpui-native-core",
|
||||
"dep:gpui-native-data",
|
||||
"dep:gpui-native-icons",
|
||||
]
|
||||
window_controls = ["dep:gpui-native-icons"]
|
||||
|
||||
@@ -7,8 +7,8 @@ use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
AnyElement, AnyView, App, AppContext as _, Axis, Context, ElementId, Entity,
|
||||
InteractiveElement as _, IntoElement, ParentElement as _, Render, RenderOnce, Role,
|
||||
SharedString, StatefulInteractiveElement as _, StyleRefinement, Styled as _, Window, canvas,
|
||||
div, px, relative, size,
|
||||
SharedString, StatefulInteractiveElement as _, StyleRefinement, Styled as _, Window,
|
||||
WindowControlArea, canvas, div, px, relative, size,
|
||||
};
|
||||
use gpui_native_core::common::{h_flex, v_flex};
|
||||
use gpui_native_core::context_menu::ContextMenu;
|
||||
@@ -18,6 +18,7 @@ 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, maximize_on_double_click};
|
||||
|
||||
/// The narrowest either side of a split can get, as a fraction of the split.
|
||||
const MIN_SPLIT_RATIO: f32 = 0.1;
|
||||
@@ -680,6 +681,10 @@ enum TreeView {
|
||||
pub struct Dock {
|
||||
id: ElementId,
|
||||
state: Entity<DockState>,
|
||||
/// The pill bar is the window's titlebar: trailing window controls.
|
||||
window_controls: bool,
|
||||
/// The pill bar leads the window: pad for the macOS traffic lights.
|
||||
traffic_light_pad: bool,
|
||||
}
|
||||
|
||||
impl Dock {
|
||||
@@ -691,14 +696,32 @@ impl Dock {
|
||||
Self {
|
||||
id: id.into(),
|
||||
state,
|
||||
window_controls: false,
|
||||
traffic_light_pad: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders the window controls in the pill bar's trailing end.
|
||||
pub fn window_controls(mut self, window_controls: bool) -> Self {
|
||||
self.window_controls = window_controls;
|
||||
self
|
||||
}
|
||||
|
||||
/// Pads the pill bar's leading edge for the macOS traffic lights.
|
||||
pub fn traffic_light_pad(mut self, traffic_light_pad: bool) -> Self {
|
||||
self.traffic_light_pad = traffic_light_pad;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Dock {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let tree = self.state.read(cx).snapshot();
|
||||
let root = render_node(&self.id, &self.state, &tree, window, cx);
|
||||
let titlebar = Titlebar {
|
||||
window_controls: self.window_controls,
|
||||
traffic_light_pad: self.traffic_light_pad,
|
||||
};
|
||||
let root = render_node(&self.id, &self.state, &tree, &titlebar, window, cx);
|
||||
|
||||
div()
|
||||
.id(self.id.clone())
|
||||
@@ -714,16 +737,24 @@ impl RenderOnce for Dock {
|
||||
}
|
||||
}
|
||||
|
||||
/// Which titlebar duties the pane's pill bar carries.
|
||||
#[derive(Clone, Copy, Default)]
|
||||
struct Titlebar {
|
||||
window_controls: bool,
|
||||
traffic_light_pad: bool,
|
||||
}
|
||||
|
||||
fn render_node(
|
||||
dock_id: &ElementId,
|
||||
state: &Entity<DockState>,
|
||||
node: &TreeView,
|
||||
titlebar: &Titlebar,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> AnyElement {
|
||||
match node {
|
||||
TreeView::Pane(pane) => render_pane(dock_id, state, pane, cx),
|
||||
TreeView::Split(split) => render_split(dock_id, state, split, window, cx),
|
||||
TreeView::Pane(pane) => render_pane(dock_id, state, pane, titlebar, cx),
|
||||
TreeView::Split(split) => render_split(dock_id, state, split, titlebar, window, cx),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -731,6 +762,7 @@ fn render_split(
|
||||
dock_id: &ElementId,
|
||||
state: &Entity<DockState>,
|
||||
split: &SplitView,
|
||||
titlebar: &Titlebar,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> AnyElement {
|
||||
@@ -790,7 +822,7 @@ fn render_split(
|
||||
.min_w_0()
|
||||
.min_h_0()
|
||||
.overflow_hidden()
|
||||
.child(render_node(dock_id, state, &split.a, window, cx)),
|
||||
.child(render_node(dock_id, state, &split.a, titlebar, window, cx)),
|
||||
)
|
||||
.child(seam)
|
||||
.child(
|
||||
@@ -800,7 +832,7 @@ fn render_split(
|
||||
.min_w_0()
|
||||
.min_h_0()
|
||||
.overflow_hidden()
|
||||
.child(render_node(dock_id, state, &split.b, window, cx)),
|
||||
.child(render_node(dock_id, state, &split.b, titlebar, window, cx)),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
@@ -809,6 +841,7 @@ fn render_pane(
|
||||
dock_id: &ElementId,
|
||||
state: &Entity<DockState>,
|
||||
pane: &PaneView,
|
||||
titlebar: &Titlebar,
|
||||
cx: &App,
|
||||
) -> AnyElement {
|
||||
if pane.tabs.is_empty() {
|
||||
@@ -854,6 +887,7 @@ fn render_pane(
|
||||
let dock_id = dock_id.clone();
|
||||
let state = state.clone();
|
||||
let pane = pane.clone();
|
||||
let titlebar = *titlebar;
|
||||
let padding_x = metrics.dense_padding_x;
|
||||
move |_, _| {
|
||||
let selected = pane
|
||||
@@ -921,13 +955,32 @@ fn render_pane(
|
||||
}
|
||||
});
|
||||
|
||||
// Chrome-less like the future titlebar: the bar wraps the pills.
|
||||
h_flex()
|
||||
.id(SharedString::from(format!(
|
||||
"{dock_id}-pane-{}-bar",
|
||||
pane.key
|
||||
)))
|
||||
.debug_selector({
|
||||
let id = SharedString::from(format!("{dock_id}-pane-{}-bar", pane.key));
|
||||
move || id.to_string()
|
||||
})
|
||||
.flex_none()
|
||||
.w_full()
|
||||
.items_center()
|
||||
.window_control_area(WindowControlArea::Drag)
|
||||
.on_click(maximize_on_double_click())
|
||||
.when(titlebar.traffic_light_pad, |bar| {
|
||||
bar.pl(px(TRAFFIC_LIGHT_PAD))
|
||||
})
|
||||
.pr(padding_x)
|
||||
.child(tabs)
|
||||
.when(titlebar.window_controls, |bar| {
|
||||
bar.child(div().flex_1())
|
||||
.child(WindowControls::new(SharedString::from(format!(
|
||||
"{dock_id}-pane-{}-controls",
|
||||
pane.key
|
||||
))))
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -9,4 +9,7 @@ pub mod resize_handle;
|
||||
#[cfg(feature = "shell")]
|
||||
pub mod shell;
|
||||
|
||||
#[cfg(feature = "window_controls")]
|
||||
pub mod window_controls;
|
||||
|
||||
mod window_frame;
|
||||
|
||||
+43
-29
@@ -3,8 +3,9 @@
|
||||
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
AnyElement, App, AppContext as _, Context, ElementId, Entity, IntoElement, ParentElement as _,
|
||||
Pixels, RenderOnce, SharedString, Styled as _, Window, div, px,
|
||||
AnyElement, App, AppContext as _, Context, ElementId, Entity, InteractiveElement as _,
|
||||
IntoElement, ParentElement as _, Pixels, RenderOnce, SharedString,
|
||||
StatefulInteractiveElement as _, Styled as _, Window, WindowControlArea, div, px,
|
||||
};
|
||||
use gpui_native_core::button::{Button, ButtonVariant};
|
||||
use gpui_native_core::common::{Sizable as _, h_flex, v_flex};
|
||||
@@ -13,6 +14,7 @@ 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, maximize_on_double_click};
|
||||
use crate::window_frame::frame;
|
||||
|
||||
/// The width a sidebar opens at.
|
||||
@@ -254,37 +256,49 @@ fn sidebar_region(
|
||||
strip_height: Pixels,
|
||||
padding_x: Pixels,
|
||||
) -> 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()
|
||||
.window_control_area(WindowControlArea::Drag)
|
||||
.on_click(maximize_on_double_click())
|
||||
.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(
|
||||
h_flex()
|
||||
.flex_none()
|
||||
.h(strip_height)
|
||||
.items_center()
|
||||
.justify_end()
|
||||
.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)),
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(strip)
|
||||
.child(div().flex_1().min_h_0().overflow_hidden().child(content))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,274 @@
|
||||
//! The window controls: minimize, maximize/restore, close — in the platform's
|
||||
//! own dress and behavior. The titlebar strips drag the window themselves via
|
||||
//! `WindowControlArea::Drag`; macOS draws its traffic lights in the shell's
|
||||
//! reserved pad and needs no controls of ours.
|
||||
|
||||
use gpui::{App, ClickEvent, ElementId, IntoElement, RenderOnce, Window, div};
|
||||
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
use gpui::{
|
||||
AnyElement, InteractiveElement as _, ParentElement as _, StatefulInteractiveElement as _,
|
||||
Styled as _,
|
||||
};
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
use gpui_native_icons::{IconRole, icon};
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
use gpui_native_theme::ActiveTheme as _;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
use gpui::Decorations;
|
||||
#[cfg(target_os = "windows")]
|
||||
use gpui::{WindowControlArea, px, rgba};
|
||||
|
||||
/// macOS traffic-lights zone: the pad a leading titlebar strip reserves.
|
||||
pub const TRAFFIC_LIGHT_PAD: f32 = 78.0;
|
||||
/// The width of a Windows caption button.
|
||||
#[cfg(target_os = "windows")]
|
||||
const CAPTION_WIDTH: f32 = 36.0;
|
||||
/// Windows paints its caption close hover red in every theme.
|
||||
#[cfg(target_os = "windows")]
|
||||
const WINDOWS_CLOSE_HOVER: (f32, f32, f32) = (232.0 / 255.0, 17.0 / 255.0, 32.0 / 255.0);
|
||||
|
||||
/// The window controls for the trailing end of a titlebar strip.
|
||||
#[derive(IntoElement)]
|
||||
pub struct WindowControls {
|
||||
id: ElementId,
|
||||
}
|
||||
|
||||
impl WindowControls {
|
||||
/// Creates the controls row.
|
||||
pub fn new<I>(id: I) -> Self
|
||||
where
|
||||
I: Into<ElementId>,
|
||||
{
|
||||
Self { id: id.into() }
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for WindowControls {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let id = self.id.clone();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let _ = (id, window, cx);
|
||||
div()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let controls = window.window_controls();
|
||||
div()
|
||||
.flex_none()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.h_full()
|
||||
.when(controls.minimize, |row| {
|
||||
row.child(caption_button(
|
||||
cx,
|
||||
SharedString::from(format!("{id}-minimize")),
|
||||
IconRole::WindowMinimize,
|
||||
WindowControlArea::Min,
|
||||
window.is_minimizable(),
|
||||
false,
|
||||
))
|
||||
})
|
||||
.when(controls.maximize, |row| {
|
||||
row.child(caption_button(
|
||||
cx,
|
||||
SharedString::from(format!("{id}-maximize")),
|
||||
if window.is_maximized() {
|
||||
IconRole::WindowRestore
|
||||
} else {
|
||||
IconRole::WindowMaximize
|
||||
},
|
||||
WindowControlArea::Max,
|
||||
window.is_resizable(),
|
||||
false,
|
||||
))
|
||||
})
|
||||
.child(caption_button(
|
||||
cx,
|
||||
SharedString::from(format!("{id}-close")),
|
||||
IconRole::Close,
|
||||
WindowControlArea::Close,
|
||||
true,
|
||||
true,
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let _ = id;
|
||||
let controls = window.window_controls();
|
||||
let client_decorated =
|
||||
matches!(window.window_decorations(), Decorations::Client { .. });
|
||||
|
||||
div()
|
||||
.flex_none()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.when(client_decorated && controls.minimize, |row| {
|
||||
row.child(circle_button(
|
||||
cx,
|
||||
"window-minimize",
|
||||
IconRole::WindowMinimize,
|
||||
window.is_minimizable(),
|
||||
|window, _cx| window.minimize_window(),
|
||||
))
|
||||
})
|
||||
.when(client_decorated && controls.maximize, |row| {
|
||||
row.child(circle_button(
|
||||
cx,
|
||||
"window-maximize",
|
||||
if window.is_maximized() {
|
||||
IconRole::WindowRestore
|
||||
} else {
|
||||
IconRole::WindowMaximize
|
||||
},
|
||||
window.is_resizable(),
|
||||
|window, _cx| window.zoom_window(),
|
||||
))
|
||||
})
|
||||
.when(client_decorated, |row| {
|
||||
row.child(circle_button(
|
||||
cx,
|
||||
"window-close",
|
||||
IconRole::Close,
|
||||
true,
|
||||
|window, _cx| window.remove_window(),
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Double-click on a titlebar strip maximizes where the platform does not do it.
|
||||
pub fn maximize_on_double_click() -> impl Fn(&ClickEvent, &mut Window, &mut App) + 'static {
|
||||
move |event, window, _cx| {
|
||||
if event.click_count() != 2 {
|
||||
return;
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
window.titlebar_double_click();
|
||||
#[cfg(target_os = "linux")]
|
||||
window.zoom_window();
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let _ = (event, window);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One Windows caption button.
|
||||
#[cfg(target_os = "windows")]
|
||||
fn caption_button(
|
||||
cx: &App,
|
||||
id: ElementId,
|
||||
role: IconRole,
|
||||
area: WindowControlArea,
|
||||
enabled: bool,
|
||||
close: bool,
|
||||
) -> AnyElement {
|
||||
let theme = cx.theme();
|
||||
let glyph_size = theme.sizes.small.caption_size;
|
||||
let (close_r, close_g, close_b) = WINDOWS_CLOSE_HOVER;
|
||||
|
||||
let close_hover = rgba((
|
||||
(close_r * 255.0) as u8,
|
||||
(close_g * 255.0) as u8,
|
||||
(close_b * 255.0) as u8,
|
||||
0xff,
|
||||
));
|
||||
let hover = if close {
|
||||
close_hover
|
||||
} else {
|
||||
theme.colors.muted_hover
|
||||
};
|
||||
|
||||
div()
|
||||
.id(id)
|
||||
.debug_selector({
|
||||
let id = SharedString::from(format!("window-control-{}", role.slug()));
|
||||
move || id.to_string()
|
||||
})
|
||||
.occlude()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.w(px(CAPTION_WIDTH))
|
||||
.h_full()
|
||||
.window_control_area(area)
|
||||
.text_color(if enabled {
|
||||
theme.colors.foreground
|
||||
} else {
|
||||
theme.colors.muted_foreground
|
||||
})
|
||||
.when(enabled, |button| {
|
||||
button
|
||||
.cursor_pointer()
|
||||
.hover(|hovered| {
|
||||
hovered.bg(hover).text_color(if close {
|
||||
gpui::white()
|
||||
} else {
|
||||
theme.colors.foreground
|
||||
})
|
||||
})
|
||||
.active(|pressed| {
|
||||
pressed
|
||||
.bg(if close {
|
||||
hover.opacity(0.8)
|
||||
} else {
|
||||
theme.ramps.secondary.pressed
|
||||
})
|
||||
.text_color(theme.colors.foreground)
|
||||
})
|
||||
})
|
||||
.child(icon(role).size(glyph_size))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
/// 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
|
||||
where
|
||||
F: Fn(&mut Window, &mut App) + 'static,
|
||||
{
|
||||
let theme = cx.theme();
|
||||
let small = theme.sizes.small;
|
||||
|
||||
div()
|
||||
.id(id)
|
||||
.debug_selector(move || format!("window-control-{}", role.slug()))
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.size(small.control_height)
|
||||
.rounded(theme.radius.full)
|
||||
.text_color(if enabled {
|
||||
theme.colors.foreground
|
||||
} else {
|
||||
theme.colors.muted_foreground
|
||||
})
|
||||
.when(enabled, |button| {
|
||||
button
|
||||
.cursor_pointer()
|
||||
.hover(|hovered| hovered.bg(theme.colors.muted_hover))
|
||||
.on_click(move |_, window, cx| {
|
||||
cx.stop_propagation();
|
||||
action(window, cx);
|
||||
})
|
||||
})
|
||||
.child(icon(role).size(small.dense_icon_size))
|
||||
.into_any_element()
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
//! The titlebar duties: traffic-light pads and window controls, asserted per
|
||||
//! platform — macOS draws only pads, Windows/Linux only draw their controls.
|
||||
|
||||
use gpui::{
|
||||
AppContext as _, Entity, IntoElement, ParentElement as _, Pixels, Render, Styled as _,
|
||||
TestAppContext, VisualTestContext, Window, div, px,
|
||||
};
|
||||
use gpui_native_layout::dock::{Dock, DockState, PanelDescriptor};
|
||||
use gpui_native_layout::window_controls::TRAFFIC_LIGHT_PAD;
|
||||
use gpui_native_theme::Theme;
|
||||
|
||||
struct Panel {
|
||||
tag: &'static str,
|
||||
}
|
||||
|
||||
impl Render for Panel {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut gpui::Context<Self>) -> impl IntoElement {
|
||||
div().size_full().child(self.tag)
|
||||
}
|
||||
}
|
||||
|
||||
fn descriptor(tag: &'static str) -> PanelDescriptor {
|
||||
PanelDescriptor::new(tag, tag, move |_, cx| cx.new(|_| Panel { tag }).into())
|
||||
}
|
||||
|
||||
struct Harness {
|
||||
dock: Entity<DockState>,
|
||||
window_controls: bool,
|
||||
traffic_light_pad: bool,
|
||||
}
|
||||
|
||||
impl Render for Harness {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut gpui::Context<Self>) -> impl IntoElement {
|
||||
div().size_full().child(
|
||||
Dock::new("test-dock", self.dock.clone())
|
||||
.window_controls(self.window_controls)
|
||||
.traffic_light_pad(self.traffic_light_pad),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn dock_window(
|
||||
window_controls: bool,
|
||||
traffic_light_pad: bool,
|
||||
cx: &mut TestAppContext,
|
||||
) -> (Entity<DockState>, VisualTestContext) {
|
||||
cx.update(Theme::init);
|
||||
let dock = cx.update(DockState::new);
|
||||
let dock_for_window = dock.clone();
|
||||
let window = cx.add_window(move |_, _| Harness {
|
||||
dock: dock_for_window,
|
||||
window_controls,
|
||||
traffic_light_pad,
|
||||
});
|
||||
let mut visual = VisualTestContext::from_window(window.into(), cx);
|
||||
visual.update(|window, cx| window.draw(cx).clear(cx));
|
||||
(dock, visual)
|
||||
}
|
||||
|
||||
fn bounds(visual: &mut VisualTestContext, selector: &str) -> Option<gpui::Bounds<Pixels>> {
|
||||
visual.debug_bounds(Box::leak(selector.to_string().into_boxed_str()))
|
||||
}
|
||||
|
||||
fn open(visual: &mut VisualTestContext, dock: &Entity<DockState>, tag: &'static str) {
|
||||
visual.update(|window, cx| {
|
||||
dock.update(cx, |dock, cx| dock.open_panel(tag, window, cx));
|
||||
});
|
||||
visual.update(|window, cx| window.draw(cx).clear(cx));
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn the_pill_bar_pads_for_traffic_lights_when_it_leads_the_window() {
|
||||
let mut cx = TestAppContext::single();
|
||||
let (dock, mut visual) = dock_window(false, false, &mut cx);
|
||||
cx.update(|cx| {
|
||||
dock.update(cx, |dock, cx| {
|
||||
dock.register_panel(descriptor("overview"), cx)
|
||||
});
|
||||
});
|
||||
open(&mut visual, &dock, "overview");
|
||||
|
||||
let bar = bounds(&mut visual, "test-dock-pane-0-bar").expect("pill bar missing");
|
||||
let tab = bounds(&mut visual, "test-dock-pane-0-tabs-tab-overview").expect("tab missing");
|
||||
let bare_inset = tab.left() - bar.left();
|
||||
|
||||
let (dock, mut visual) = dock_window(false, true, &mut cx);
|
||||
let _ = &dock;
|
||||
cx.update(|cx| {
|
||||
dock.update(cx, |dock, cx| {
|
||||
dock.register_panel(descriptor("overview"), cx)
|
||||
});
|
||||
});
|
||||
open(&mut visual, &dock, "overview");
|
||||
|
||||
let padded_bar = bounds(&mut visual, "test-dock-pane-0-bar").expect("padded bar missing");
|
||||
let padded_tab =
|
||||
bounds(&mut visual, "test-dock-pane-0-tabs-tab-overview").expect("tab missing");
|
||||
let padded_inset = padded_tab.left() - padded_bar.left();
|
||||
|
||||
assert!(
|
||||
padded_inset - bare_inset >= px(TRAFFIC_LIGHT_PAD) - px(1.),
|
||||
"the pad must clear the traffic lights (bare {bare_inset:?}, padded {padded_inset:?})"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn window_controls_render_only_when_the_flag_is_set() {
|
||||
let mut cx = TestAppContext::single();
|
||||
let (dock, mut visual) = dock_window(false, false, &mut cx);
|
||||
cx.update(|cx| {
|
||||
dock.update(cx, |dock, cx| {
|
||||
dock.register_panel(descriptor("overview"), cx)
|
||||
});
|
||||
});
|
||||
open(&mut visual, &dock, "overview");
|
||||
|
||||
// macOS: the system traffic lights own the controls — nothing renders;
|
||||
// the pad test above covers that mode.
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
assert!(
|
||||
bounds(&mut visual, "window-control-close").is_none(),
|
||||
"controls must not render without the flag"
|
||||
);
|
||||
#[cfg(target_os = "macos")]
|
||||
let _ = (&dock, &mut visual);
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
#[test]
|
||||
fn the_flag_hosts_the_platform_controls_in_the_bar() {
|
||||
let mut cx = TestAppContext::single();
|
||||
let (dock, mut visual) = dock_window(true, false, &mut cx);
|
||||
cx.update(|cx| {
|
||||
dock.update(cx, |dock, cx| {
|
||||
dock.register_panel(descriptor("overview"), cx)
|
||||
});
|
||||
});
|
||||
open(&mut visual, &dock, "overview");
|
||||
|
||||
// The maximize control exists once; its restore flip follows the
|
||||
// window's maximized state, not a separate control.
|
||||
assert!(bounds(&mut visual, "window-control-close").is_some());
|
||||
assert!(bounds(&mut visual, "window-control-maximize").is_some());
|
||||
}
|
||||
+2
-2
@@ -290,7 +290,7 @@ Units, each through the review gate:
|
||||
| L3 | layout | Dock ✅ Landed (2026-11-02) | The center pane tree, controlled: `DockState` holds the `Pane { tabs, active }` / `Split { axis, ratio, a, b }` tree plus a registry of `PanelDescriptor`s (`id` + title + optional icon + a view builder called once per open; the built view is retained, so panel state survives tab switches). `open` / `activate` / `close` / `close_others` / `close_pane` / `split` / `move` are plain state ops keyed by panel id — opening an open panel re-activates it, one instance per descriptor for now, and `active_panel()` reports what the active pane shows. Any op emptying a pane collapses it: the pane detaches and single-child splits are replaced by the surviving subtree, so only the root can sit empty. Split seams are `ResizeHandle`s (arrow keys move them for free); the ratio lives in the state and the seam turns pixel deltas into fractions against a per-split span that a canvas captures into dock-namespaced keyed state each frame — double-click resets to 0.5, both sides clamp at 10% of the split. Panes render a `strip_height` header row composing the data crate's `Tabs` (revised in L4 — see below), `TabList`/`Tab`/`TabPanel` roles, and a core `ContextMenu` on the header — Close / Close Others / Close Pane; the active tab's view fills the pane. Feature `dock` pulls core `button` + `context_menu` and re-uses `resize_handle`; 7 headless tests in `crates/layout/tests/dock.rs` (open/activate, tab clicks, neighbor-on-close, split + collapse, seam drag/reset/clamp with a 1 px flex-rounding tolerance, merge-by-move, close-others/close-pane + reopen); the gallery's shell center hosts a dock with demo panels and open/split buttons |
|
||||
| L4 | layout | Dock drag-to-split ✅ Landed (2026-11-02) | Dragging a tab uses gpui's own drag system: the dock attaches `on_drag` through data `Tabs`' `Tab::wrap` seam with a `PanelDrag { panel }` payload (ghost = a small bordered title card). While `App::has_active_drag()`, each pane's content mounts five absolute drop zones — four edge bands that call the new `DockState::split_panel_at(panel, anchor, direction)` (a split beside the *target* pane, not the dragged tab's own; the empty source still collapses) and a center zone that calls `move_panel`. Zones paint a `selection` wash via `drag_over::<PanelDrag>` while hovered and are mounted only during a drag, so content clicks are never blocked; gpui cancels the drag on a mouse-up over nothing, which unmounts them. Dropped tabs land active in their new pane. Same pass: pane content renders through `AnyView::cached` (`StyleRefinement` absolute + size full, the gpui-kit pattern) so an un-notified panel never re-renders on dock frames; pane headers moved from hand-rolled tabs to data `Tabs` (`Pill` variant — renamed from `Card` and reshaped to signed's exact tab: leading element via `Tab::lead`, wide title-to-controls gap, `Tab::busy` loading slot standing in with a quiet ring until the Spinner control lands, a ghost core `Button` close control (the first data → core dependency edge, feature-gated: `tabs` pulls `core/button`), and the pill radius is the button radius, not `full`; the pill bar is chrome-less because the dock's tab strip doubles as the L4.5 titlebar), which now ships the two designs (default stays `Underline`); tab strips carry `{tabs-id}-tab-{label}` / `{tabs-id}-close-{label}` debug selectors for headless tests. 10 headless tests in `crates/layout/tests/dock.rs` — the new ones cover a real simulated drag (mouse down → threshold move → drag starts → drop on a right edge band → split) and `split_panel_at`'s target-side placement; the rest carried over with the new tab selectors |
|
||||
| L2.5 | layout | window frame ✅ Landed (2026-11-01) | `window_frame::frame(child, window, cx)` — the client-side window frame for windows the OS does not decorate: under `Decorations::Server` (macOS, Windows, KDE, X11) it passes the child through untouched; under `Decorations::Client` (Wayland/GNOME) it insets the content, draws the hairline border and `Radius::overlay` corners (tiled sides stay square and lose their band), casts the dialog elevation's shadow dimmed to 0.7× when the window is inactive, and turns a 4 px band inside each live edge into resize hits (`start_window_resize`, edge cursors; a maximized window counts as flush everywhere). Requires a transparent window background, and must be the window's root — the bands hit-test against `Window::window_bounds`. **The `Shell` applies it by default**, so the module ships ungated — no separate feature. Adapted from bezel's `window::frame` + gpui-kit's `WindowBorder` (D15/D20), simplified to one inset band and theme-owned tokens |
|
||||
| L4.5 | layout | Titlebar + window controls | The strips across the shell's top become the window's titlebar: they drag the window (`WindowControlArea::Drag` on the strip) and host the controls where the platform puts them. A `WindowControls` element — minimize, maximize/restore, close, capability-gated by `Window::window_controls()` — renders per platform: **macOS** draws nothing where the system traffic lights sit (they are system-drawn; `TitlebarOptions.appears_transparent` + `traffic_light_position` is app window config), and the shell reserves the pad in the left sidebar's header leading edge — or across the center's strip when the left sidebar is hidden; **Windows** draws native-glyph buttons (`ChromeMinimize`/`ChromeMaximize`/`ChromeClose`, Segoe E921/E922/E8BB verified against the MDL2 table) marked `WindowControlArea::Min/Max/Close` so Windows handles the clicks and snap layouts natively, maximize flipping to Restore via `is_maximized`; **Linux** draws the same ops as click handlers (`minimize_window` / `zoom_window` / `remove_window`) in the platform's own dress — Adwaita's small gray circle wrappers around symbolic glyphs, resolved through the freedesktop cascade with the bundled fallback (`Close` already leads with `window-close-symbolic`; new `WindowMinimize`/`WindowMaximize` roles land in the icons crate). Placement follows the shell's shape: Windows/Linux put the controls at the trailing end of the right-hand strip — the right sidebar's strip when visible, else the center region's titlebar strip, which is now the dock's `Pill` tab bar (the pill strip ships chrome-less for exactly this — in signed the tab bar doubles as the titlebar, 44 px, with 34 px-wide controls and a danger hover on close, which the pill tab already matches apart from the metrics). Reference: signed's `crates/dock/src/window_controls.rs` |
|
||||
| L4.5 | layout | Titlebar + window controls ✅ Landed (2026-11-02) | The strips are the window's titlebar: every strip (shell sidebars, dock pill bar) marks itself `WindowControlArea::Drag` and double-click-maximizes (`titlebar_double_click` on macOS, `zoom_window` on Linux; Windows does it natively through the caption hit test). New `window_controls` module (feature `window_controls`, pulled by `shell` and `dock`): a `WindowControls` element rendered at the trailing end of the right-hand strip — the right sidebar's strip when visible, else the dock pill bar via `Dock::window_controls(bool)`. **Windows**: caption buttons (36 px wide, Segoe codepoints through the icons crate — new `WindowMinimize`/`WindowMaximize`/`WindowRestore` roles, E921/E922/E923 verified against zed's own table) marked `WindowControlArea::Min/Max/Close` so the OS owns the clicks and snap layouts; maximize flips to restore via `is_maximized`; capability-gated by `Window::window_controls()` + `is_minimizable`/`is_resizable`; close hovers the platform red (a platform convention const, like the Segoe codepoints). **Linux**: Adwaita circular buttons (the platform dress, hand-rolled like zed's) under `Decorations::Client` only, clicking `minimize_window`/`zoom_window`/`remove_window`. **macOS** draws nothing — system traffic lights; the shell reserves `TRAFFIC_LIGHT_PAD` (78 px, Tahoe SDK) in the left sidebar strip's leading edge, and `Dock::traffic_light_pad(bool)` pads the pill bar when no left sidebar sits over the lights. The gpui dispatch fact that makes this safe: Windows forwards `WM_NCLBUTTONDOWN` into gpui's normal event stream, so buttons inside a Drag-marked strip still get their clicks, and only unconsumed presses fall through to native caption behavior. Adapted from zed's `platform_title_bar` (D15/D20); the gallery wires both dock flags from `LayoutState`. 2 platform-gated headless tests (macOS pad shifts the pill bar's tab by the pad width; controls render only under the flag on their platforms); CI feature loop gained `window_controls` |
|
||||
| L5 | layout | Sidebar auto-open | Panels declare `needs_right_sidebar` on their descriptor; activating such a panel opens the right sidebar; closing it leaves visibility to the user. The gallery gains a workspace-style shell page — the bootstrap example apps copy. |
|
||||
|
||||
Deferred until a real need: layout persistence/serialization, pane zoom (maximize one pane), a bottom dock, collapse-to-edge docks, floating windows, drag between windows.
|
||||
@@ -377,6 +377,6 @@ Dropped from rev 3: D2, D5, D9 (port-era decisions); the review gate returns in
|
||||
6. ~~Unit 14 — motion, per component (§6 row 14, D21)~~ ✅ Landed in five passes (2026-10-30): (a) theme `reduce_motion` → `cx.set_reduce_motion` in `reapply`; (b) Switch thumb `with_spring`; (c) Toast 8 px slide-in; (d) Dialog scrim enters with the panel; (e) Progress 30 fps cap. Exits and hover washes stay deferred (§6 deferred list).
|
||||
7. ~~Phase F, data first~~ ✅ data complete (2026-10-30) — `crates/data` opened with List, then Tree, Tabs, Sidebar (the dead `sheet` feature dropped from core along the way; Table dropped from the roadmap). Also extracted `crates/utils` (2026-10-30): `h_flex`/`v_flex` (core's `common` re-exports them), `focus_handle` (the namespaced keyed handle, 8 call sites), `focus_ring` (7 sites), `adjacent_selectable` + `nav_key` (4 sites), and the selectable-row paint helpers `selectable_label_color`/`paint_selectable_row` (3 sites, reading `&Theme` directly — no intermediate color struct), plus the `StyledExt` trait (`refine_style` — adapted from gpui-kit `base`, D15 — replacing the per-component `style().refine` pattern; 5 sites) — `core` and `data` depend on it; the facade doesn't re-export it. **Linux theme sources landed (2026-10-30)**: Omarchy (colors.toml parse + state-dir detection + inotify on `current`), KDE (kdeglobals `[Colors:*]` + AccentColor + `~/.config` inotify), GNOME/generic (portal `ReadAll` + `SettingChanged` via zbus 5 blocking on its own thread, gsettings fallback with the libadwaita accent table) — `SourceKind::Omarchy/Kde/Gnome`, detection cascade Omarchy → KDE → portal → fallback, all parsers fixture-tested on every OS and the Linux build compile-checked via `cargo check --target x86_64-unknown-linux-gnu`. **Windows source + icon resolvers landed (2026-10-30)**: `SourceKind::Windows` (WinRT `UISettings` + `Personalize` fallback + `ColorValuesChanged`/`HighContrastChanged`, §5.3), the Segoe Fluent codepoint renderer, and the freedesktop theme walker (§4.4); the WinRT surface was verified against the real `windows` 0.62 bindings via a standalone cross-check (gpui's own manifest build needs llvm-rc from macOS). Remaining Phase F: **side-by-side gallery screenshots per platform** — Omarchy/KDE/GNOME need a Linux run, Windows needs its first hardware build. NumberInput stays deferred until needed; multiline landed as §6.3 M7 (2026-11-01).
|
||||
8. Then — Phase G is the migration additions (§6.3): Badge ✅, Select ✅, Segmented control ✅, Context menu ✅, Empty state ✅, password input ✅, multiline textarea ✅, then Avatar image (M8) — each unblocks a concrete signed screen. Pickers (Calendar/DatePicker, TimeField, ColorPicker) stay decided-at-unit-time. `crates/editor` starts when file viewing (M15) is reached.
|
||||
9. Then — the layout crate (§6.4): ResizeHandle ✅, Shell ✅, window frame ✅, Dock ✅, drag-to-split ✅, titlebar + window controls, and panel-declared sidebar auto-open — each through the review gate, with signed's `crates/dock` as the usage reference and the gallery workspace page as the example. Order against M9+ is decided when the crate starts.
|
||||
9. Then — the layout crate (§6.4): ResizeHandle ✅, Shell ✅, window frame ✅, Dock ✅, drag-to-split ✅, titlebar + window controls ✅, and panel-declared sidebar auto-open — each through the review gate, with signed's `crates/dock` as the usage reference and the gallery workspace page as the example. Order against M9+ is decided when the crate starts.
|
||||
|
||||
Batched polish candidates between units: scrollbar auto-hide + horizontal thumb, `Undo`/`Redo` menu events.
|
||||
|
||||
@@ -245,11 +245,13 @@ impl Gallery {
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.min_h_0()
|
||||
.overflow_hidden()
|
||||
.child(Dock::new("gallery-dock", self.dock_state.clone())),
|
||||
div().flex_1().min_h_0().overflow_hidden().child(
|
||||
Dock::new("gallery-dock", self.dock_state.clone())
|
||||
// The pill bar is the titlebar strip whenever no
|
||||
// right sidebar sits to its right.
|
||||
.window_controls(!state.read(cx).is_right_sidebar_visible())
|
||||
.traffic_light_pad(!state.read(cx).is_left_sidebar_visible()),
|
||||
),
|
||||
);
|
||||
|
||||
Shell::new("gallery-shell", state)
|
||||
|
||||
Reference in New Issue
Block a user