diff --git a/AGENTS.md b/AGENTS.md index d19e998..5430892 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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). - 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. -- 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). - 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. diff --git a/src/shell/columns.rs b/src/shell/columns.rs index 1a02d9d..2d1b4eb 100644 --- a/src/shell/columns.rs +++ b/src/shell/columns.rs @@ -1,4 +1,4 @@ -use std::{cell::Cell, rc::Rc}; +use std::rc::Rc; use gpui::{ AnyElement, App, Axis, DefiniteLength, Div, DragMoveEvent, ElementId, EmptyView, EntityId, @@ -31,11 +31,17 @@ enum Side { Right, } +#[derive(Clone, Copy)] struct BorderDrag { owner: EntityId, side: Side, - /// The width drawn when the drag set off. - start: Rc>>, +} + +/// 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. @@ -292,13 +298,9 @@ fn resizing( max, on_resize, } = resize; - let pressed = window.use_keyed_state((id.clone(), "press"), cx, |_, _| None::); + let pressed = window.use_keyed_state((id.clone(), "press"), cx, |_, _| None::); let owner = pressed.entity_id(); - let drag = BorderDrag { - owner, - side, - start: Rc::default(), - }; + let drag = BorderDrag { owner, side }; let at = pressed.clone(); let handle = resize_handle((id, "border"), Axis::Horizontal, cx) .absolute() @@ -311,33 +313,48 @@ fn resizing( .occlude() .debug_selector(move || format!("column-shell-border-{ix}")) .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)); box_.on_drag_move(move |event: &DragMoveEvent, window, cx| { - let drag = event.drag(cx); - if drag.owner != owner { + let BorderDrag { + owner: dragged, + side, + } = *event.drag(cx); + if dragged != owner { 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"); return; }; - // A header's parts may hold a column wider, a short shell squeeze it narrower. - let start = drag - .start - .get() - .unwrap_or(event.bounds.size.width.max(width)); - drag.start.set(Some(start)); - let moved = event.event.position.x - anchor; - if moved == Pixels::ZERO { - return; - } - let next = match drag.side { - Side::Left => start - moved, - Side::Right => start + moved, - } - .clamp(min, max); + let moved = event.event.position.x - press.at; + let (start, kept) = match press.start { + Some(start) => start, + // A drag that has not moved sideways yet changes nothing. + None if moved == Pixels::ZERO => return, + // A header's parts may hold a column wider, a short shell squeeze it narrower. + None => { + let start = (event.bounds.size.width.max(width), width); + pressed.update(cx, |held, _| { + *held = Some(Press { + start: Some(start), + ..press + }) + }); + 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 { on_resize(next, window, cx); } diff --git a/src/shell/tests/border.rs b/src/shell/tests/border.rs index b5f50a7..f674155 100644 --- a/src/shell/tests/border.rs +++ b/src/shell/tests/border.rs @@ -62,20 +62,27 @@ fn column(ix: usize, cx: &mut VisualTestContext) -> gpui::Bounds { cx.debug_bounds(name).expect("a column") } -/// Presses at `from`, moves past gpui's drag threshold, then by `by`, and lets go. -fn drag(from: Point, by: Pixels, cx: &mut VisualTestContext) { +/// Presses at `from`, moves past gpui's drag threshold, then to each offset `path` names, and lets go. +fn drag_path(from: Point, path: &[Pixels], cx: &mut VisualTestContext) { let none = Modifiers::none(); cx.simulate_mouse_down(from, MouseButton::Left, none); let set_off = from + point(px(0.0), px(4.0)); cx.simulate_mouse_move(set_off, Some(MouseButton::Left), none); cx.run_until_parked(); - let to = set_off + point(by, px(0.0)); - cx.simulate_mouse_move(to, Some(MouseButton::Left), none); - cx.run_until_parked(); + let mut to = set_off; + for by in path { + 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.run_until_parked(); } +fn drag(from: Point, by: Pixels, cx: &mut VisualTestContext) { + drag_path(from, &[by], cx); +} + fn heard(view: &Entity, cx: &mut VisualTestContext) -> Option { 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" ); } + +/// 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"); + } +}