Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T06:50:37.315Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

window_controls.rs

504 lines · 16.4 KB · rust
1//! This holds the resize logic for picker windows.
2//!
3//! # Resizing basics:
4//! We render a resize handle (`render_resize`) for each side and corner that
5//! can be dragged. The `Side` trait implementations (`Left`, `Right`,
6//! `Bottom`, `LeftCorner`, `RightCorner`, ...) determine where each handle is
7//! placed and how a drag changes the picker's shape.
8//!
9//! If there is a preview to the right or below there is an additional resize
10//! handle on the divider between the results and the preview.
11//!
12//! The resize's are div's aka rectangles that are "placed" by specifying the
13//! position of their sides. When hovering over these the cursor is changed
14//! so it is clear you can resize.
15//!
16//! We set up two callbacks in each
17//!  - on_drag: fires when the user starts dragging
18//!  - on_drag_move: runs every frame while the user is dragging
19//!
20//! The actual shape of the resize is updated in `on_drag_move`. When a preview
21//! is active dragging the outside edge modifies the
22//!
23//! # Resizing persistence
24//! Each picker has a 'fixed' size and tracks it's last resize. When manually
25//! resized the window size is stored as a percentage of the viewport
26//! width/height. The size is serialized as soon as the use lets go of the drag.
27//!
28//! # Diagrams & Details
29//! ```txt
30//! ================ CHANGING WIDTH ======================================
31//! The picker position stays constant during the drag but it is centered
32//! directly after. (when the user lets go)
33//! ================ DRAGGING RIGHT ======================================
34//!
35//! ------------
36//! |    |     |  <- dragging this edge right
37//! |    |     |
38//! ------------
39//! leads to:
40//! --------------------
41//! |    |             |  <- dragged on this edge
42//! |    |  preview    |
43//! --------------------
44//!
45//! self.w_preview = w_preview + drag
46//! ================ DRAGGING LEFT =======================================
47//!
48//!                       ------------
49//! dragging this left -> |   |      |
50//!                       |   |      |
51//!                       ------------
52//! leads to:
53//! -------------------
54//! | list     |      |
55//! |          |      |
56//! -------------------
57//! ```
58
59use std::{any::type_name, marker::PhantomData};
60
61use gpui::{ClickEvent, Context, CursorStyle, DragMoveEvent, MouseButton, Point, Styled, Window};
62use ui::prelude::*;
63
64use crate::shape::{Centered, PositionAndShape, Shape, SizeBounds};
65use crate::{Picker, PickerDelegate, preview::Layout};
66
67pub struct DragPreview;
68
69impl Render for DragPreview {
70    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
71        div()
72    }
73}
74
75#[derive(Clone, Copy)]
76struct ResizeDrag<S> {
77    shape_before: PositionAndShape,
78    phantom_data: PhantomData<S>,
79    mouse_pos_before: Point<Pixels>,
80}
81
82pub(crate) trait Side: Copy + 'static {
83    fn id() -> &'static str {
84        type_name::<Self>()
85    }
86    /// The thickness of the grab strip along the picker's edge.
87    ///
88    /// Expressed in rems so it scales with the user's UI font size.
89    fn handle_width(window: &Window) -> Pixels {
90        rems(0.375).to_pixels(window.rem_size())
91    }
92    fn handle_offset(window: &Window) -> Pixels {
93        Self::handle_width(window) / 2.0
94    }
95    /// How far the grab strip is inset from the top and bottom corners so it doesn't overlap the
96    /// corner resize handles.
97    fn corner_clearance(window: &Window) -> Pixels {
98        rems(1.125).to_pixels(window.rem_size())
99    }
100    /// The resize cursor for this side's handle.
101    fn cursor(&self) -> CursorStyle;
102    /// Places and sizes the grab strip along this side's edge.
103    fn position(
104        &self,
105        div: gpui::Stateful<Div>,
106        shape: PositionAndShape,
107        window: &Window,
108    ) -> gpui::Stateful<Div>;
109    fn current_position_and_shape(
110        &self,
111        shape_before: PositionAndShape,
112        mouse_movement: Point<Pixels>,
113    ) -> PositionAndShape;
114    fn clamp(
115        &self,
116        working: &mut PositionAndShape,
117        bounds: &SizeBounds,
118        layout: Option<Layout>,
119        window: &Window,
120    );
121    fn revert_to_default_size(&self, shape: &mut Shape, default: &Centered, window: &Window);
122}
123
124#[derive(Clone, Copy)]
125pub(crate) struct Left;
126impl Side for Left {
127    fn cursor(&self) -> CursorStyle {
128        CursorStyle::ResizeColumn
129    }
130    fn position(
131        &self,
132        div: gpui::Stateful<Div>,
133        _: PositionAndShape,
134        window: &Window,
135    ) -> gpui::Stateful<Div> {
136        div.top_0()
137            .bottom(Self::corner_clearance(window))
138            .w(Self::handle_width(window))
139            .left(-Self::handle_offset(window))
140    }
141    fn current_position_and_shape(
142        &self,
143        mut shape_before: PositionAndShape,
144        mouse_movement: Point<Pixels>,
145    ) -> PositionAndShape {
146        shape_before.left += mouse_movement.x;
147        shape_before
148    }
149    fn clamp(
150        &self,
151        working: &mut PositionAndShape,
152        bounds: &SizeBounds,
153        layout: Option<Layout>,
154        window: &Window,
155    ) {
156        bounds.clamp_left_edge(working, layout, window);
157        bounds.clamp_divider(working, layout, window);
158    }
159    fn revert_to_default_size(&self, shape: &mut Shape, default: &Centered, _window: &Window) {
160        shape.reset_width(default);
161    }
162}
163#[derive(Clone, Copy)]
164pub(crate) struct Right(pub(crate) Layout);
165impl Side for Right {
166    fn cursor(&self) -> CursorStyle {
167        CursorStyle::ResizeColumn
168    }
169    fn position(
170        &self,
171        div: gpui::Stateful<Div>,
172        _: PositionAndShape,
173        window: &Window,
174    ) -> gpui::Stateful<Div> {
175        div.top_0()
176            .bottom(Self::corner_clearance(window))
177            .w(Self::handle_width(window))
178            .right(-Self::handle_offset(window))
179    }
180    fn current_position_and_shape(
181        &self,
182        mut shape_before: PositionAndShape,
183        mouse_movement: Point<Pixels>,
184    ) -> PositionAndShape {
185        if let Layout::Right = self.0 {
186            shape_before.preview += mouse_movement.x;
187        }
188        shape_before.right += mouse_movement.x;
189        shape_before
190    }
191    fn clamp(
192        &self,
193        working: &mut PositionAndShape,
194        bounds: &SizeBounds,
195        layout: Option<Layout>,
196        window: &Window,
197    ) {
198        bounds.clamp_right_edge(working, layout, window);
199        bounds.clamp_divider(working, layout, window);
200    }
201    fn revert_to_default_size(&self, shape: &mut Shape, default: &Centered, _window: &Window) {
202        shape.reset_width(default);
203    }
204}
205
206#[derive(Clone, Copy)]
207pub(crate) struct Middle(pub(crate) Layout);
208impl Side for Middle {
209    fn cursor(&self) -> CursorStyle {
210        match self.0 {
211            Layout::Hidden => {
212                unreachable!("This resize handle is not drawn when the preview is hidden")
213            }
214            Layout::Below => CursorStyle::ResizeRow,
215            Layout::Right => CursorStyle::ResizeColumn,
216        }
217    }
218
219    fn position(
220        &self,
221        div: gpui::Stateful<Div>,
222        shape: PositionAndShape,
223        window: &Window,
224    ) -> gpui::Stateful<Div> {
225        match self.0 {
226            Layout::Hidden => {
227                unreachable!("This resize handle is not drawn when the preview is hidden")
228            }
229            Layout::Below => div
230                .left(Self::corner_clearance(window))
231                .right(Self::corner_clearance(window))
232                .h(Self::handle_width(window))
233                .bottom(shape.preview - Self::handle_offset(window)),
234            Layout::Right => div
235                .top_0()
236                .bottom(Self::corner_clearance(window))
237                .w(Self::handle_width(window))
238                .right(shape.preview - Self::handle_offset(window)),
239        }
240    }
241
242    fn current_position_and_shape(
243        &self,
244        mut shape_before: PositionAndShape,
245        mouse_movement: Point<Pixels>,
246    ) -> PositionAndShape {
247        match self.0 {
248            Layout::Hidden => {
249                unreachable!("This resize handle is not drawn when the preview is hidden")
250            }
251            Layout::Below => shape_before.preview -= mouse_movement.y,
252            Layout::Right => shape_before.preview -= mouse_movement.x,
253        }
254        shape_before
255    }
256    fn clamp(
257        &self,
258        working: &mut PositionAndShape,
259        bounds: &SizeBounds,
260        layout: Option<Layout>,
261        window: &Window,
262    ) {
263        // The divider only moves the preview; the outer edges are unchanged.
264        bounds.clamp_divider(working, layout, window);
265    }
266    fn revert_to_default_size(&self, shape: &mut Shape, _default: &Centered, window: &Window) {
267        shape.center_divider(self.0, window);
268    }
269}
270
271#[derive(Clone, Copy)]
272pub(crate) struct Bottom(pub(crate) Layout);
273impl Side for Bottom {
274    fn cursor(&self) -> CursorStyle {
275        CursorStyle::ResizeRow
276    }
277    fn position(
278        &self,
279        div: gpui::Stateful<Div>,
280        _: PositionAndShape,
281        window: &Window,
282    ) -> gpui::Stateful<Div> {
283        div.left(Self::corner_clearance(window))
284            .right(Self::corner_clearance(window))
285            .h(Self::handle_width(window))
286            .bottom(-Self::handle_offset(window))
287    }
288    fn current_position_and_shape(
289        &self,
290        mut shape_before: PositionAndShape,
291        mouse_movement: Point<Pixels>,
292    ) -> PositionAndShape {
293        if let Layout::Below = self.0 {
294            shape_before.preview += mouse_movement.y;
295        }
296        shape_before.bottom += mouse_movement.y;
297        shape_before
298    }
299    fn clamp(
300        &self,
301        working: &mut PositionAndShape,
302        bounds: &SizeBounds,
303        layout: Option<Layout>,
304        window: &Window,
305    ) {
306        bounds.clamp_bottom_edge(working, layout, window);
307        bounds.clamp_divider(working, layout, window);
308    }
309    fn revert_to_default_size(&self, shape: &mut Shape, default: &Centered, _window: &Window) {
310        shape.reset_height(default);
311    }
312}
313#[derive(Clone, Copy)]
314pub(crate) struct LeftCorner(pub(crate) Layout);
315impl Side for LeftCorner {
316    fn cursor(&self) -> CursorStyle {
317        CursorStyle::ResizeUpRightDownLeft
318    }
319    fn position(
320        &self,
321        div: gpui::Stateful<Div>,
322        _: PositionAndShape,
323        window: &Window,
324    ) -> gpui::Stateful<Div> {
325        div.w(Self::handle_width(window))
326            .h(Self::handle_width(window))
327            .left(-Self::handle_offset(window))
328            .bottom(-Self::handle_offset(window))
329    }
330    fn current_position_and_shape(
331        &self,
332        mut shape_before: PositionAndShape,
333        mouse_movement: Point<Pixels>,
334    ) -> PositionAndShape {
335        match self.0 {
336            Layout::Hidden => (),
337            Layout::Below => shape_before.preview += mouse_movement.y,
338            Layout::Right => shape_before.preview += mouse_movement.x,
339        }
340        shape_before.left += mouse_movement.x;
341        shape_before.bottom += mouse_movement.y;
342        shape_before
343    }
344    fn clamp(
345        &self,
346        working: &mut PositionAndShape,
347        bounds: &SizeBounds,
348        layout: Option<Layout>,
349        window: &Window,
350    ) {
351        bounds.clamp_left_edge(working, layout, window);
352        bounds.clamp_bottom_edge(working, layout, window);
353        bounds.clamp_divider(working, layout, window);
354    }
355    fn revert_to_default_size(&self, shape: &mut Shape, default: &Centered, _window: &Window) {
356        shape.reset_width(default);
357        shape.reset_height(default);
358    }
359}
360#[derive(Clone, Copy)]
361pub(crate) struct RightCorner(pub(crate) Layout);
362impl Side for RightCorner {
363    fn cursor(&self) -> CursorStyle {
364        CursorStyle::ResizeUpLeftDownRight
365    }
366    fn position(
367        &self,
368        div: gpui::Stateful<Div>,
369        _: PositionAndShape,
370        window: &Window,
371    ) -> gpui::Stateful<Div> {
372        div.w(Self::handle_width(window))
373            .h(Self::handle_width(window))
374            .right(-Self::handle_offset(window))
375            .bottom(-Self::handle_offset(window))
376    }
377    fn current_position_and_shape(
378        &self,
379        mut shape_before: PositionAndShape,
380        mouse_movement: Point<Pixels>,
381    ) -> PositionAndShape {
382        match self.0 {
383            Layout::Hidden => (),
384            Layout::Below => shape_before.preview += mouse_movement.y,
385            Layout::Right => shape_before.preview += mouse_movement.x,
386        }
387        shape_before.right += mouse_movement.x;
388        shape_before.bottom += mouse_movement.y;
389        shape_before
390    }
391    fn clamp(
392        &self,
393        working: &mut PositionAndShape,
394        bounds: &SizeBounds,
395        layout: Option<Layout>,
396        window: &Window,
397    ) {
398        bounds.clamp_right_edge(working, layout, window);
399        bounds.clamp_bottom_edge(working, layout, window);
400        bounds.clamp_divider(working, layout, window);
401    }
402    fn revert_to_default_size(&self, shape: &mut Shape, default: &Centered, _window: &Window) {
403        shape.reset_width(default);
404        shape.reset_height(default);
405    }
406}
407
408impl<S: Side> ResizeDrag<S> {
409    fn start_new(
410        shape: Shape,
411        bounds: &SizeBounds,
412        layout: Option<Layout>,
413        window: &mut Window,
414    ) -> Self {
415        Self {
416            mouse_pos_before: window.mouse_position(),
417            // Before rendering we always clamp so the current shape may not be
418            // within SizeBounds so use a clamped one
419            shape_before: shape.clamped_position_and_size(layout, bounds, window),
420            phantom_data: PhantomData,
421        }
422    }
423}
424
425impl<D: PickerDelegate> Picker<D> {
426    /// Resizes the picker modal by dragging the handle on the given side or corner
427    pub(crate) fn render_resize<S: Side>(
428        &self,
429        side: S,
430        window: &mut Window,
431        cx: &mut Context<Self>,
432    ) -> impl IntoElement {
433        div()
434            .id(S::id())
435            .absolute()
436            .cursor(side.cursor())
437            .map(|this| {
438                side.position(
439                    this,
440                    self.shape.clamped_position_and_size(
441                        self.preview_layout_rendered(window),
442                        &self.size_bounds,
443                        window,
444                    ),
445                    window,
446                )
447            })
448            .block_mouse_except_scroll()
449            .on_mouse_down(MouseButton::Left, do_nothing)
450            .on_drag(
451                ResizeDrag::<S>::start_new(
452                    self.shape,
453                    &self.size_bounds,
454                    self.preview_layout_rendered(window),
455                    window,
456                ),
457                |_, _, _, cx| cx.new(|_| DragPreview),
458            )
459            .on_drag_move::<ResizeDrag<S>>(cx.listener(
460                move |this, event: &DragMoveEvent<ResizeDrag<S>>, window, cx| {
461                    let drag = event.drag(cx);
462                    let delta = event.event.position - drag.mouse_pos_before;
463                    let mut working = side.current_position_and_shape(drag.shape_before, delta);
464                    side.clamp(
465                        &mut working,
466                        &this.size_bounds,
467                        this.preview_layout_rendered(window),
468                        window,
469                    );
470                    this.shape = Shape::Resizing(working);
471                    cx.notify();
472                },
473            ))
474            .on_click(cx.listener(move |this, event: &ClickEvent, window, cx| {
475                this.reset_size_to_default_on_double_click(side, event, window, cx)
476            }))
477    }
478
479    fn reset_size_to_default_on_double_click<S: Side>(
480        &mut self,
481        side: S,
482        event: &ClickEvent,
483        window: &mut Window,
484        cx: &mut Context<Picker<D>>,
485    ) {
486        if event.click_count() < 2 {
487            return;
488        }
489        side.revert_to_default_size(&mut self.shape, &self.default_shape, window);
490        let pos = self.shape.clamped_position_and_size(
491            self.preview_layout_rendered(window),
492            &self.size_bounds,
493            window,
494        );
495        self.shape = Shape::Resizing(pos);
496        cx.notify();
497    }
498}
499
500fn do_nothing(_: &gpui::MouseDownEvent, window: &mut Window, cx: &mut App) {
501    window.prevent_default();
502    cx.stop_propagation();
503}
504
Served at tenant.openagents/omega Member data and write actions are omitted.