Column shell: a drag that comes back where it set off gives the width the owner had, where the last width it reported stayed; the width a drag sets off from lives with its press, so each press starts afresh
This commit is contained in:
3 files changed
+111
-34
No files matched your search
@@ -221,7 +221,7 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
|
|||||||
- `typography::Ellipsis` shapes its line with the text style's own run, so a strike or an underline set on its box shows (`lists::ListItem::struck`, a closed task's title).
|
- `typography::Ellipsis` shapes its line with the text style's own run, so a strike or an underline set on its box shows (`lists::ListItem::struck`, a closed task's title).
|
||||||
- Focus that must follow an item to another parent is taken by the owner and keyed by the item: a handle gpui makes for an element dies with its path (`project::KanbanBoard`, whose cards keep focus across columns).
|
- Focus that must follow an item to another parent is taken by the owner and keyed by the item: a handle gpui makes for an element dies with its path (`project::KanbanBoard`, whose cards keep focus across columns).
|
||||||
- The theme's `platform`, the system's to start, decides the window buttons Ely draws, the title bar's layout and key caps, `secondary` included (`typography::keys::keystroke`). A title bar whose buttons the OS draws follows the OS.
|
- The theme's `platform`, the system's to start, decides the window buttons Ely draws, the title bar's layout and key caps, `secondary` included (`typography::keys::keystroke`). A title bar whose buttons the OS draws follows the OS.
|
||||||
- A window whose sidebar runs to its top is `shell::ColumnShell`: each column's header drags the window; the first leaves the traffic lights room, and drawn controls lead the first header in the macOS manner or end the last. A set width is a flex basis, not a width, so it holds its header's own parts, which a width let run past it, and yields when the shell runs short; the host's header items keep one control's width and scroll when short. A resizable column drags by its border toward the filling column, its right before it and its left after it; the handle draws that border and occludes the header under it, and a drag sets off from the wider of the width drawn and the owner's, since a header's parts may hold a column wider and a short shell squeeze it narrower; a drag that has not moved sideways changes nothing.
|
- A window whose sidebar runs to its top is `shell::ColumnShell`: each column's header drags the window; the first leaves the traffic lights room, and drawn controls lead the first header in the macOS manner or end the last. A set width is a flex basis, not a width, so it holds its header's own parts, which a width let run past it, and yields when the shell runs short; the host's header items keep one control's width and scroll when short. A resizable column drags by its border toward the filling column, its right before it and its left after it; the handle draws that border and occludes the header under it, and a drag sets off from the wider of the width drawn and the owner's, since a header's parts may hold a column wider and a short shell squeeze it narrower; a drag that has not moved sideways changes nothing, and one that comes back where it set off gives the width the owner had.
|
||||||
- Taffy counts a flex item's text in its least width whatever width the item sets. A box that must add none lays its content absolutely inside a filling box (`shell::ColumnShell`'s title and body).
|
- Taffy counts a flex item's text in its least width whatever width the item sets. A box that must add none lays its content absolutely inside a filling box (`shell::ColumnShell`'s title and body).
|
||||||
- A centered title sits between a start and an end that grow alike from a zero basis, their padding on a box inside them, since taffy counts padding in the flex base; their content is their least width, so the title gives way before either end (`shell::TitleBar`).
|
- A centered title sits between a start and an end that grow alike from a zero basis, their padding on a box inside them, since taffy counts padding in the flex base; their content is their least width, so the title gives way before either end (`shell::TitleBar`).
|
||||||
- A pane the window's blur shows through is `shell::Vibrancy`, filled with the palette's `glass`, in a window opened `WindowBackgroundAppearance::Blurred`: vibrancy on macOS, acrylic on Windows. gpui's `MicaBackdrop` and `MicaAltBackdrop` are DWM backdrops from Windows 11 build 22621; older builds get none, in silence, and macOS plain transparency.
|
- A pane the window's blur shows through is `shell::Vibrancy`, filled with the palette's `glass`, in a window opened `WindowBackgroundAppearance::Blurred`: vibrancy on macOS, acrylic on Windows. gpui's `MicaBackdrop` and `MicaAltBackdrop` are DWM backdrops from Windows 11 build 22621; older builds get none, in silence, and macOS plain transparency.
|
||||||
|
|||||||
+45
-28
@@ -1,4 +1,4 @@
|
|||||||
use std::{cell::Cell, rc::Rc};
|
use std::rc::Rc;
|
||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, Axis, DefiniteLength, Div, DragMoveEvent, ElementId, EmptyView, EntityId,
|
AnyElement, App, Axis, DefiniteLength, Div, DragMoveEvent, ElementId, EmptyView, EntityId,
|
||||||
@@ -31,11 +31,17 @@ enum Side {
|
|||||||
Right,
|
Right,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
struct BorderDrag {
|
struct BorderDrag {
|
||||||
owner: EntityId,
|
owner: EntityId,
|
||||||
side: Side,
|
side: Side,
|
||||||
/// The width drawn when the drag set off.
|
}
|
||||||
start: Rc<Cell<Option<Pixels>>>,
|
|
||||||
|
/// Where a press on a border landed; once it moves sideways, the width it sets off from and the owner's then.
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct Press {
|
||||||
|
at: Pixels,
|
||||||
|
start: Option<(Pixels, Pixels)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One column of a `ColumnShell`: a header that drags the window, then its body.
|
/// One column of a `ColumnShell`: a header that drags the window, then its body.
|
||||||
@@ -292,13 +298,9 @@ fn resizing(
|
|||||||
max,
|
max,
|
||||||
on_resize,
|
on_resize,
|
||||||
} = resize;
|
} = resize;
|
||||||
let pressed = window.use_keyed_state((id.clone(), "press"), cx, |_, _| None::<Pixels>);
|
let pressed = window.use_keyed_state((id.clone(), "press"), cx, |_, _| None::<Press>);
|
||||||
let owner = pressed.entity_id();
|
let owner = pressed.entity_id();
|
||||||
let drag = BorderDrag {
|
let drag = BorderDrag { owner, side };
|
||||||
owner,
|
|
||||||
side,
|
|
||||||
start: Rc::default(),
|
|
||||||
};
|
|
||||||
let at = pressed.clone();
|
let at = pressed.clone();
|
||||||
let handle = resize_handle((id, "border"), Axis::Horizontal, cx)
|
let handle = resize_handle((id, "border"), Axis::Horizontal, cx)
|
||||||
.absolute()
|
.absolute()
|
||||||
@@ -311,33 +313,48 @@ fn resizing(
|
|||||||
.occlude()
|
.occlude()
|
||||||
.debug_selector(move || format!("column-shell-border-{ix}"))
|
.debug_selector(move || format!("column-shell-border-{ix}"))
|
||||||
.on_mouse_down(MouseButton::Left, move |event: &MouseDownEvent, _, cx| {
|
.on_mouse_down(MouseButton::Left, move |event: &MouseDownEvent, _, cx| {
|
||||||
at.update(cx, |at, _| *at = Some(event.position.x))
|
at.update(cx, |at, _| {
|
||||||
|
*at = Some(Press {
|
||||||
|
at: event.position.x,
|
||||||
|
start: None,
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.on_drag(drag, |_, _, _, cx| cx.new(|_| EmptyView));
|
.on_drag(drag, |_, _, _, cx| cx.new(|_| EmptyView));
|
||||||
box_.on_drag_move(move |event: &DragMoveEvent<BorderDrag>, window, cx| {
|
box_.on_drag_move(move |event: &DragMoveEvent<BorderDrag>, window, cx| {
|
||||||
let drag = event.drag(cx);
|
let BorderDrag {
|
||||||
if drag.owner != owner {
|
owner: dragged,
|
||||||
|
side,
|
||||||
|
} = *event.drag(cx);
|
||||||
|
if dragged != owner {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let Some(anchor) = *pressed.read(cx) else {
|
let Some(press) = *pressed.read(cx) else {
|
||||||
log::error!("column shell: a drag on column {ix} with no press behind it");
|
log::error!("column shell: a drag on column {ix} with no press behind it");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
// A header's parts may hold a column wider, a short shell squeeze it narrower.
|
let moved = event.event.position.x - press.at;
|
||||||
let start = drag
|
let (start, kept) = match press.start {
|
||||||
.start
|
Some(start) => start,
|
||||||
.get()
|
// A drag that has not moved sideways yet changes nothing.
|
||||||
.unwrap_or(event.bounds.size.width.max(width));
|
None if moved == Pixels::ZERO => return,
|
||||||
drag.start.set(Some(start));
|
// A header's parts may hold a column wider, a short shell squeeze it narrower.
|
||||||
let moved = event.event.position.x - anchor;
|
None => {
|
||||||
if moved == Pixels::ZERO {
|
let start = (event.bounds.size.width.max(width), width);
|
||||||
return;
|
pressed.update(cx, |held, _| {
|
||||||
}
|
*held = Some(Press {
|
||||||
let next = match drag.side {
|
start: Some(start),
|
||||||
Side::Left => start - moved,
|
..press
|
||||||
Side::Right => start + moved,
|
})
|
||||||
}
|
});
|
||||||
.clamp(min, max);
|
start
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let next = match side {
|
||||||
|
_ if moved == Pixels::ZERO => kept,
|
||||||
|
Side::Left => (start - moved).clamp(min, max),
|
||||||
|
Side::Right => (start + moved).clamp(min, max),
|
||||||
|
};
|
||||||
if next != width {
|
if next != width {
|
||||||
on_resize(next, window, cx);
|
on_resize(next, window, cx);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,20 +62,27 @@ fn column(ix: usize, cx: &mut VisualTestContext) -> gpui::Bounds<Pixels> {
|
|||||||
cx.debug_bounds(name).expect("a column")
|
cx.debug_bounds(name).expect("a column")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Presses at `from`, moves past gpui's drag threshold, then by `by`, and lets go.
|
/// Presses at `from`, moves past gpui's drag threshold, then to each offset `path` names, and lets go.
|
||||||
fn drag(from: Point<Pixels>, by: Pixels, cx: &mut VisualTestContext) {
|
fn drag_path(from: Point<Pixels>, path: &[Pixels], cx: &mut VisualTestContext) {
|
||||||
let none = Modifiers::none();
|
let none = Modifiers::none();
|
||||||
cx.simulate_mouse_down(from, MouseButton::Left, none);
|
cx.simulate_mouse_down(from, MouseButton::Left, none);
|
||||||
let set_off = from + point(px(0.0), px(4.0));
|
let set_off = from + point(px(0.0), px(4.0));
|
||||||
cx.simulate_mouse_move(set_off, Some(MouseButton::Left), none);
|
cx.simulate_mouse_move(set_off, Some(MouseButton::Left), none);
|
||||||
cx.run_until_parked();
|
cx.run_until_parked();
|
||||||
let to = set_off + point(by, px(0.0));
|
let mut to = set_off;
|
||||||
cx.simulate_mouse_move(to, Some(MouseButton::Left), none);
|
for by in path {
|
||||||
cx.run_until_parked();
|
to = set_off + point(*by, px(0.0));
|
||||||
|
cx.simulate_mouse_move(to, Some(MouseButton::Left), none);
|
||||||
|
cx.run_until_parked();
|
||||||
|
}
|
||||||
cx.simulate_mouse_up(to, MouseButton::Left, none);
|
cx.simulate_mouse_up(to, MouseButton::Left, none);
|
||||||
cx.run_until_parked();
|
cx.run_until_parked();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn drag(from: Point<Pixels>, by: Pixels, cx: &mut VisualTestContext) {
|
||||||
|
drag_path(from, &[by], cx);
|
||||||
|
}
|
||||||
|
|
||||||
fn heard(view: &Entity<Sided>, cx: &mut VisualTestContext) -> Option<Pixels> {
|
fn heard(view: &Entity<Sided>, cx: &mut VisualTestContext) -> Option<Pixels> {
|
||||||
view.read_with(cx, |view, _| view.heard.last().copied())
|
view.read_with(cx, |view, _| view.heard.last().copied())
|
||||||
}
|
}
|
||||||
@@ -202,3 +209,56 @@ fn a_squeezed_column_drags_from_the_owner_s_width(cx: &mut TestAppContext) {
|
|||||||
"the right border moves left"
|
"the right border moves left"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A drag that comes back where it set off gives the width the owner had, a held or squeezed one too.
|
||||||
|
#[gpui::test]
|
||||||
|
fn a_drag_back_where_it_set_off_gives_the_width_it_had(cx: &mut TestAppContext) {
|
||||||
|
cx.update(Theme::init);
|
||||||
|
let cases = [
|
||||||
|
(
|
||||||
|
Platform::Linux,
|
||||||
|
px(1000.0),
|
||||||
|
[px(200.0), px(200.0)],
|
||||||
|
0,
|
||||||
|
px(50.0),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Platform::Linux,
|
||||||
|
px(1000.0),
|
||||||
|
[px(200.0), px(200.0)],
|
||||||
|
2,
|
||||||
|
px(-50.0),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Platform::Windows,
|
||||||
|
px(1000.0),
|
||||||
|
[px(200.0), px(100.0)],
|
||||||
|
2,
|
||||||
|
px(-10.0),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Platform::Linux,
|
||||||
|
px(700.0),
|
||||||
|
[px(350.0), px(350.0)],
|
||||||
|
0,
|
||||||
|
px(20.0),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
for (platform, width, widths, ix, by) in cases {
|
||||||
|
let (view, cx) = cx.add_window_view(move |_, _| Sided {
|
||||||
|
platform,
|
||||||
|
width,
|
||||||
|
widths,
|
||||||
|
heard: Vec::new(),
|
||||||
|
});
|
||||||
|
cx.run_until_parked();
|
||||||
|
let name = ["column-shell-border-0", "", "column-shell-border-2"][ix];
|
||||||
|
let border = cx.debug_bounds(name).expect("a border").center();
|
||||||
|
let drawn = column(ix, cx);
|
||||||
|
drag_path(border, &[by, px(0.0)], cx);
|
||||||
|
let heard = view.read_with(cx, |view, _| view.heard.clone());
|
||||||
|
assert_eq!(heard.len(), 2, "{platform:?} {ix}: out and back, {heard:?}");
|
||||||
|
assert_eq!(heard[1], widths[ix / 2], "{platform:?} {ix}");
|
||||||
|
assert_eq!(column(ix, cx), drawn, "{platform:?} {ix}: drawn as before");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user