Skip to repository content504 lines · 16.4 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T07:55:45.507Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
window_controls.rs
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