Shell: a control in a drag region occludes it, so it takes its press on Windows, where gpui answers the hit test with the caption over any Drag hitbox left uncovered: window buttons and title bar slots occlude, and a column header's item row does while staying the scroll container, its title a drag region of its own; the mini window drags by its title alone, keeping its hover
This commit is contained in:
5 files changed
+168
-44
No files matched your search
+41
-35
@@ -102,11 +102,13 @@ impl WindowControls {
|
||||
|
||||
impl RenderOnce for WindowControls {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
match self.platform.unwrap_or(cx.theme().platform) {
|
||||
let controls = match self.platform.unwrap_or(cx.theme().platform) {
|
||||
Platform::Mac => traffic_lights(self.id, self.on_close, window, cx),
|
||||
Platform::Windows => caption_buttons(self.id, self.on_close, window, cx),
|
||||
Platform::Linux => round_buttons(self.id, self.on_close, window, cx),
|
||||
}
|
||||
};
|
||||
// Windows takes a drag region under controls as caption.
|
||||
controls.occlude()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +117,7 @@ fn traffic_lights(
|
||||
on_close: Option<OnClose>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> AnyElement {
|
||||
) -> Stateful<Div> {
|
||||
let theme = cx.theme();
|
||||
let colors = &theme.colors;
|
||||
let active = window.is_window_active();
|
||||
@@ -154,7 +156,6 @@ fn traffic_lights(
|
||||
.on_click(move |_, window, cx| run(control, on_close.as_ref(), window, cx))
|
||||
.child(glyph)
|
||||
}))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn caption_buttons(
|
||||
@@ -162,7 +163,7 @@ fn caption_buttons(
|
||||
on_close: Option<OnClose>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> AnyElement {
|
||||
) -> Stateful<Div> {
|
||||
let theme = cx.theme();
|
||||
let colors = &theme.colors;
|
||||
let maximize = if window.is_maximized() {
|
||||
@@ -207,7 +208,6 @@ fn caption_buttons(
|
||||
.group_hover_color(group, glyph),
|
||||
)
|
||||
}))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn round_buttons(
|
||||
@@ -215,7 +215,7 @@ fn round_buttons(
|
||||
on_close: Option<OnClose>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> AnyElement {
|
||||
) -> Stateful<Div> {
|
||||
let theme = cx.theme();
|
||||
let colors = &theme.colors;
|
||||
let offered = window.window_controls();
|
||||
@@ -224,32 +224,38 @@ fn round_buttons(
|
||||
(Control::Maximize, IconName::Square, offered.maximize),
|
||||
(Control::Close, IconName::X, true),
|
||||
];
|
||||
div()
|
||||
.id(id)
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.children(
|
||||
buttons
|
||||
.into_iter()
|
||||
.filter(|(_, _, shown)| *shown)
|
||||
.map(|(control, icon, _)| {
|
||||
let on_close = on_close.clone();
|
||||
div()
|
||||
.id(SharedString::from(format!("{control:?}")))
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.size(theme.control_height(ControlSize::Sm))
|
||||
.rounded_full()
|
||||
.bg(colors.hover)
|
||||
.hover(|style| style.bg(colors.active))
|
||||
.on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default())
|
||||
.on_click(move |_, window, cx| run(control, on_close.as_ref(), window, cx))
|
||||
.child(Icon::new(icon).size(IconSize::Xs).color(colors.fg))
|
||||
}),
|
||||
)
|
||||
.into_any_element()
|
||||
div().id(id).flex().items_center().gap_2().children(
|
||||
buttons
|
||||
.into_iter()
|
||||
.filter(|(_, _, shown)| *shown)
|
||||
.map(|(control, icon, _)| {
|
||||
let on_close = on_close.clone();
|
||||
div()
|
||||
.id(SharedString::from(format!("{control:?}")))
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.size(theme.control_height(ControlSize::Sm))
|
||||
.rounded_full()
|
||||
.bg(colors.hover)
|
||||
.hover(|style| style.bg(colors.active))
|
||||
.on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default())
|
||||
.on_click(move |_, window, cx| run(control, on_close.as_ref(), window, cx))
|
||||
.child(Icon::new(icon).size(IconSize::Xs).color(colors.fg))
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/// A slot's controls, occluding the drag region beneath.
|
||||
fn slot(items: SmallVec<[AnyElement; 2]>) -> Option<Div> {
|
||||
(!items.is_empty()).then(|| {
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.occlude()
|
||||
.children(items)
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether the system draws the window controls, and whose manner they take.
|
||||
@@ -404,7 +410,7 @@ impl RenderOnce for TitleBar {
|
||||
false => start.pl_3(),
|
||||
})
|
||||
.children(leading)
|
||||
.children(self.leading),
|
||||
.children(slot(self.leading)),
|
||||
);
|
||||
let end = div().flex().justify_end().child(
|
||||
div()
|
||||
@@ -412,7 +418,7 @@ impl RenderOnce for TitleBar {
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.when(centered, |end| end.pr_2())
|
||||
.children(self.actions)
|
||||
.children(slot(self.actions))
|
||||
.children(trailing.map(|controls| {
|
||||
div()
|
||||
.flex_none()
|
||||
|
||||
+13
-5
@@ -149,7 +149,7 @@ impl RenderOnce for ColumnShell {
|
||||
items(
|
||||
column.id.clone(),
|
||||
column.leading,
|
||||
title(column.title, cx),
|
||||
title(column.id.clone(), column.title, window, cx),
|
||||
column.actions,
|
||||
)
|
||||
.min_w(cx.theme().control_height(ControlSize::Lg))
|
||||
@@ -212,7 +212,7 @@ fn header(id: ElementId, window: &mut Window, cx: &mut App) -> gpui::Stateful<gp
|
||||
fn items(
|
||||
id: ElementId,
|
||||
leading: SmallVec<[AnyElement; 2]>,
|
||||
title: gpui::Div,
|
||||
title: gpui::Stateful<gpui::Div>,
|
||||
actions: SmallVec<[AnyElement; 2]>,
|
||||
) -> gpui::Div {
|
||||
let group = || div().flex_none().flex().items_center().gap_2();
|
||||
@@ -224,16 +224,24 @@ fn items(
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.overflow_x_scroll()
|
||||
// Windows takes a drag region under controls as caption.
|
||||
.occlude()
|
||||
.child(group().children(leading))
|
||||
.child(title)
|
||||
.child(group().children(actions)),
|
||||
)
|
||||
}
|
||||
|
||||
/// The title, adding nothing to the least width; a spacer without one.
|
||||
fn title(title: Option<SharedString>, cx: &App) -> gpui::Div {
|
||||
/// Title or spacer: drags the window, adds no width.
|
||||
fn title(
|
||||
id: ElementId,
|
||||
title: Option<SharedString>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> gpui::Stateful<gpui::Div> {
|
||||
let region = drag_region((id, "title"), window, cx);
|
||||
let theme = cx.theme();
|
||||
div().flex_1().min_w_0().h_full().relative().child(
|
||||
region.flex_1().min_w_0().h_full().relative().child(
|
||||
div()
|
||||
.absolute()
|
||||
.inset_0()
|
||||
|
||||
+102
-1
@@ -3,7 +3,7 @@ use gpui::{
|
||||
ParentElement, Point, Render, TestAppContext, Window, div, point, prelude::*, px,
|
||||
};
|
||||
|
||||
use super::TitleBar;
|
||||
use super::{ColumnShell, ShellColumn, TitleBar, WindowControls};
|
||||
use crate::theme::{Platform, Theme};
|
||||
|
||||
struct Bar {
|
||||
@@ -71,3 +71,104 @@ fn a_wobbling_press_on_the_bar_moves_the_window(cx: &mut TestAppContext) {
|
||||
.expect("drawn controls");
|
||||
wobble(point(controls.left() - px(200.0), controls.center().y), cx);
|
||||
}
|
||||
|
||||
/// Counts presses that reach the box under the controls.
|
||||
struct Under {
|
||||
platform: Platform,
|
||||
presses: usize,
|
||||
}
|
||||
|
||||
impl Render for Under {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let view = cx.entity();
|
||||
div()
|
||||
.id("under")
|
||||
.flex()
|
||||
.items_start()
|
||||
.w_full()
|
||||
.on_mouse_down(MouseButton::Left, move |_, _, cx| {
|
||||
view.update(cx, |under, _| under.presses += 1)
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.debug_selector(|| "controls".into())
|
||||
.child(WindowControls::new("controls").platform(self.platform)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Each style's controls hide what lies under them.
|
||||
#[gpui::test]
|
||||
fn window_controls_occlude_what_lies_under(cx: &mut TestAppContext) {
|
||||
cx.update(Theme::init);
|
||||
for platform in [Platform::Windows, Platform::Mac, Platform::Linux] {
|
||||
let (view, cx) = cx.add_window_view(move |_, _| Under {
|
||||
platform,
|
||||
presses: 0,
|
||||
});
|
||||
cx.run_until_parked();
|
||||
let controls = cx.debug_bounds("controls").expect("drawn controls");
|
||||
cx.simulate_event(MouseDownEvent {
|
||||
button: MouseButton::Left,
|
||||
position: controls.center(),
|
||||
modifiers: Modifiers::none(),
|
||||
click_count: 1,
|
||||
first_mouse: false,
|
||||
});
|
||||
assert_eq!(
|
||||
view.read_with(cx, |under, _| under.presses),
|
||||
0,
|
||||
"{platform:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Title bar and column header with inert slot items.
|
||||
struct Slotted;
|
||||
|
||||
impl Render for Slotted {
|
||||
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||
let item = |name: &'static str| div().debug_selector(move || name.into()).size_6();
|
||||
let column = ShellColumn::new("main")
|
||||
.leading(item("shell-lead"))
|
||||
.action(item("shell-act"));
|
||||
div()
|
||||
.size_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.child(
|
||||
TitleBar::new("bar")
|
||||
.platform(Platform::Windows)
|
||||
.child(item("bar-lead"))
|
||||
.action(item("bar-act")),
|
||||
)
|
||||
.child(
|
||||
ColumnShell::new("shell")
|
||||
.platform(Platform::Windows)
|
||||
.column(column),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// A press on a slot item never moves the window.
|
||||
#[gpui::test]
|
||||
fn a_wobbling_press_on_a_slot_moves_no_window(cx: &mut TestAppContext) {
|
||||
cx.update(Theme::init);
|
||||
let (_, cx) = cx.add_window_view(|_, _| Slotted);
|
||||
cx.run_until_parked();
|
||||
for name in ["bar-lead", "bar-act", "shell-lead", "shell-act"] {
|
||||
let item = cx.debug_bounds(name).expect("a slot item");
|
||||
wobble(item.center(), cx);
|
||||
}
|
||||
}
|
||||
|
||||
/// A column's title still moves the window; a move panics.
|
||||
#[gpui::test]
|
||||
#[should_panic(expected = "not implemented")]
|
||||
fn a_wobbling_press_on_a_column_title_moves_the_window(cx: &mut TestAppContext) {
|
||||
cx.update(Theme::init);
|
||||
let (_, cx) = cx.add_window_view(|_, _| Slotted);
|
||||
cx.run_until_parked();
|
||||
let items = cx.debug_bounds("column-shell-items-0").expect("the items");
|
||||
wobble(items.center(), cx);
|
||||
}
|
||||
+11
-3
@@ -212,7 +212,7 @@ impl MiniWindow {
|
||||
|
||||
impl Render for MiniWindow {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let bar = drag_region("mini-bar", window, cx);
|
||||
let drag = drag_region("mini-bar", window, cx);
|
||||
let theme = cx.theme();
|
||||
let back = self.back;
|
||||
div()
|
||||
@@ -225,7 +225,8 @@ impl Render for MiniWindow {
|
||||
.font_family(theme.font_family.clone())
|
||||
.child(self.view.clone())
|
||||
.child(
|
||||
bar.absolute()
|
||||
div()
|
||||
.absolute()
|
||||
.top_0()
|
||||
.left_0()
|
||||
.right_0()
|
||||
@@ -241,7 +242,14 @@ impl Render for MiniWindow {
|
||||
.text_color(theme.colors.fg_muted)
|
||||
.invisible()
|
||||
.group_hover("mini-window", |style| style.visible())
|
||||
.child(div().flex_1().child(self.title.clone()))
|
||||
// Only the title drags: Windows takes the region as caption.
|
||||
.child(
|
||||
drag.flex_1()
|
||||
.h_full()
|
||||
.flex()
|
||||
.items_center()
|
||||
.child(self.title.clone()),
|
||||
)
|
||||
.when_some(back, |bar, back| {
|
||||
bar.child(
|
||||
IconButton::new("mini-expand", IconName::Maximize2)
|
||||
|
||||
Reference in new issue
Block a user