Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T02:33:57.650Z 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

shape.rs

679 lines · 22.6 KB · rust
1use gpui::Window;
2use gpui::{Pixels, Rems, Size};
3use ui::{Div, Styled, rems_from_px};
4
5use crate::preview::Layout;
6
7#[derive(Debug, Clone, Copy)]
8pub(crate) struct PositionAndShape {
9    /// Absolute position of left most side of the picker
10    pub(crate) left: Pixels,
11    /// Absolute position of right most side of the picker
12    pub(crate) right: Pixels,
13    /// Absolute position of top most side of the picker
14    pub(crate) top: Pixels,
15    /// Absolute position of bottom most side of the picker
16    pub(crate) bottom: Pixels,
17    /// Relative position of divide between results and preview,
18    /// either a height or a width depends on previews layoutmode.
19    /// Should be zero when preview is disabled or hidden
20    pub(crate) preview: Pixels,
21}
22
23impl PositionAndShape {
24    pub(crate) fn width(&self) -> Pixels {
25        self.right - self.left
26    }
27}
28
29macro_rules! relative_size {
30    ($name:ident, $accessor:ident) => {
31        /// Size type that is the sum of a relative size to the viewport and a
32        /// size relative to the font size (Rems). You can
33        /// add/subtract/multiple/divide to your harts content but once you
34        /// need a single unit you must provide a window to get it.
35        #[derive(Debug, Clone, Copy, PartialEq)]
36        pub struct $name {
37            viewport_fraction: f32,
38            rems: Rems,
39        }
40
41        impl From<Rems> for $name {
42            fn from(v: Rems) -> Self {
43                Self::rems(v)
44            }
45        }
46
47        impl $name {
48            pub const FULL: Self = Self {
49                viewport_fraction: 1.0,
50                rems: Rems::ZERO,
51            };
52
53            pub const fn viewport(fraction: f32) -> Self {
54                debug_assert!(fraction <= 1.0);
55                debug_assert!(fraction >= 0.0);
56                Self {
57                    viewport_fraction: fraction.clamp(0.0, 1.0),
58                    rems: Rems::ZERO,
59                }
60            }
61
62            pub const fn rems(val: Rems) -> Self {
63                Self {
64                    viewport_fraction: 0.0,
65                    rems: val,
66                }
67            }
68
69            pub fn as_pixels(&self, window: &Window) -> Pixels {
70                self.viewport_fraction * window.viewport_size().$accessor
71                    + self.rems * window.rem_size()
72            }
73
74            pub fn from_pixels(width: Pixels, window: &Window) -> Self {
75                Self {
76                    viewport_fraction: width / window.viewport_size().$accessor,
77                    rems: Rems::ZERO,
78                }
79            }
80
81            /// Returns this size as [`Rems`] when it has no viewport-relative
82            /// component. Used to derive a rems-based minimum from an initial
83            /// size without needing a [`Window`].
84            pub fn as_rems(&self) -> Option<Rems> {
85                (self.viewport_fraction == 0.0).then_some(self.rems)
86            }
87
88            pub fn as_viewport_fraction(&self, window: &Window) -> ViewportFraction {
89                ViewportFraction(
90                    self.viewport_fraction
91                        + self.rems * window.rem_size() / window.viewport_size().$accessor,
92                )
93            }
94        }
95
96        impl std::ops::Add for $name {
97            type Output = Self;
98
99            fn add(self, rhs: Self) -> Self::Output {
100                Self {
101                    viewport_fraction: self.viewport_fraction + rhs.viewport_fraction,
102                    rems: self.rems + rhs.rems,
103                }
104            }
105        }
106
107        impl std::ops::Sub for $name {
108            type Output = Self;
109
110            fn sub(self, rhs: Self) -> Self::Output {
111                Self {
112                    viewport_fraction: self.viewport_fraction - rhs.viewport_fraction,
113                    rems: self.rems - rhs.rems,
114                }
115            }
116        }
117
118        impl std::ops::Sub<Rems> for $name {
119            type Output = Self;
120
121            fn sub(self, rhs: Rems) -> Self::Output {
122                Self {
123                    viewport_fraction: self.viewport_fraction,
124                    rems: self.rems - rhs,
125                }
126            }
127        }
128
129        impl std::ops::Div<f32> for $name {
130            type Output = Self;
131
132            fn div(mut self, rhs: f32) -> Self::Output {
133                self.viewport_fraction /= rhs;
134                self.rems = Rems(self.rems.0 / rhs);
135                self
136            }
137        }
138
139        impl std::ops::Mul<f32> for $name {
140            type Output = Self;
141
142            fn mul(mut self, rhs: f32) -> Self::Output {
143                self.viewport_fraction *= rhs;
144                self.rems = Rems(self.rems.0 * rhs);
145                self
146            }
147        }
148    };
149}
150
151relative_size!(RelativeHeight, height);
152relative_size!(RelativeWidth, width);
153
154#[derive(Debug, Clone, Copy)]
155pub struct ViewportFraction(f32);
156
157impl ViewportFraction {
158    pub(crate) const ZERO: Self = Self(0.0);
159    pub(crate) fn fraction(v: f32) -> Self {
160        debug_assert!(
161            (0.0..=1.0).contains(&v),
162            "ViewportFraction must be between zero and one"
163        );
164        Self(v)
165    }
166
167    pub(crate) fn width_as_pixels(&self, window: &Window) -> Pixels {
168        window.viewport_size().width * self.0
169    }
170    pub(crate) fn height_as_pixels(&self, window: &Window) -> Pixels {
171        window.viewport_size().height * self.0
172    }
173
174    pub(crate) fn from_height_pixels(preview: Pixels, window: &Window) -> Self {
175        Self(preview / window.viewport_size().height)
176    }
177
178    pub(crate) fn from_width_pixels(preview: Pixels, window: &Window) -> Self {
179        Self(preview / window.viewport_size().width)
180    }
181
182    /// Returns the fraction of the viewport that this describes.
183    /// Guaranteed to be between zero and one
184    pub(crate) fn raw(&self) -> f32 {
185        self.0
186    }
187}
188
189impl std::ops::Mul<f32> for ViewportFraction {
190    type Output = Self;
191
192    fn mul(self, rhs: f32) -> Self::Output {
193        Self(self.0 * rhs)
194    }
195}
196
197#[derive(Debug, Clone, Copy)]
198pub(crate) struct Centered {
199    pub(crate) width: RelativeWidth,
200    pub(crate) height: RelativeHeight,
201    pub(crate) preview_size: ViewportFraction,
202}
203
204impl Centered {
205    /// The default size for a plain picker (no preview): a fixed standard width
206    /// and a standard *max* height that the picker shrinks below when it has
207    /// little content.
208    pub(crate) fn simple() -> Self {
209        Centered {
210            width: RelativeWidth::rems(crate::DEFAULT_MODAL_WIDTH),
211            height: RelativeHeight::rems(crate::DEFAULT_MODAL_MAX_HEIGHT),
212            preview_size: ViewportFraction::ZERO,
213        }
214    }
215}
216
217#[derive(Debug, Clone, Copy)]
218pub(crate) enum Shape {
219    Resizing(PositionAndShape),
220    /// This may be persisted between zero and three times:
221    /// - for a user resized picker without preview
222    /// - for a user resized picker with preview below
223    /// - for a user resized picker with preview right
224    HorizontallyCentered(Centered),
225}
226
227#[derive(Debug)]
228pub struct SizeBounds {
229    pub(crate) max_width: RelativeWidth,
230    pub(crate) max_height: RelativeHeight,
231    pub(crate) min_results: Size<Rems>,
232    /// Minimum size of the preview pane. Along the split axis this only needs to
233    /// be large enough to grab the divider and shrink it back.
234    pub(crate) min_preview: Size<Rems>,
235}
236
237impl Default for SizeBounds {
238    fn default() -> Self {
239        Self {
240            max_width: RelativeWidth::viewport(0.95),
241            // Modals are placed at 5 rems from the top we also do not want them to go
242            // over the lower bar so clear another 5 rems there.
243            max_height: (RelativeHeight::FULL - Rems(10.0)) * 0.95,
244            min_results: Size {
245                width: rems_from_px(280.),
246                height: rems_from_px(320.),
247            },
248            min_preview: Size {
249                width: rems_from_px(128.),
250                height: rems_from_px(96.),
251            },
252        }
253    }
254}
255
256impl SizeBounds {
257    /// Minimum total picker width for the given layout, composed from the
258    /// results and preview minimums (they stack along the split axis and share
259    /// the cross axis).
260    fn min_width(&self, layout: Option<Layout>, window: &Window) -> Pixels {
261        let rem = window.rem_size();
262        let results = self.min_results.width * rem;
263        let preview = self.min_preview.width * rem;
264        match layout {
265            Some(Layout::Right) => results + preview,
266            Some(Layout::Below) => results.max(preview),
267            Some(Layout::Hidden) | None => results,
268        }
269    }
270
271    /// Minimum total picker height for the given layout. See [`Self::min_width`].
272    fn min_height(&self, layout: Option<Layout>, window: &Window) -> Pixels {
273        let rem = window.rem_size();
274        let results = self.min_results.height * rem;
275        let preview = self.min_preview.height * rem;
276        match layout {
277            Some(Layout::Below) => results + preview,
278            Some(Layout::Right) => results.max(preview),
279            Some(Layout::Hidden) | None => results,
280        }
281    }
282
283    /// Clamps a width in pixels to the configured min/max width.
284    pub(crate) fn clamp_width(
285        &self,
286        width: Pixels,
287        layout: Option<Layout>,
288        window: &Window,
289    ) -> Pixels {
290        width
291            .min(self.max_width.as_pixels(window))
292            .max(self.min_width(layout, window))
293    }
294
295    /// Clamps a height in pixels to the configured min/max height.
296    pub(crate) fn clamp_height(
297        &self,
298        height: Pixels,
299        layout: Option<Layout>,
300        window: &Window,
301    ) -> Pixels {
302        height
303            .min(self.max_height.as_pixels(window))
304            .max(self.min_height(layout, window))
305    }
306
307    /// Clamps the picker's width by moving its left edge back into bounds.
308    ///
309    /// For a preview-to-the-right drag the preview is corrected by the same
310    /// amount any snap moves the edge, so the results pane keeps its size at the
311    /// boundary.
312    pub(crate) fn clamp_left_edge(
313        &self,
314        working: &mut PositionAndShape,
315        layout: Option<Layout>,
316        window: &Window,
317    ) {
318        let target_width = self.clamp_width(working.right - working.left, layout, window);
319        let new_left = working.right - target_width;
320        let correction = new_left - working.left;
321        working.left = new_left;
322        if layout == Some(Layout::Right) {
323            working.preview += correction;
324        }
325    }
326
327    /// Clamps the picker's width by moving its right edge back into bounds.
328    /// See [`Self::clamp_left_edge`].
329    pub(crate) fn clamp_right_edge(
330        &self,
331        working: &mut PositionAndShape,
332        layout: Option<Layout>,
333        window: &Window,
334    ) {
335        let target_width = self.clamp_width(working.right - working.left, layout, window);
336        let new_right = working.left + target_width;
337        let correction = new_right - working.right;
338        working.right = new_right;
339        if layout == Some(Layout::Right) {
340            working.preview += correction;
341        }
342    }
343
344    /// Clamps the picker's height by moving its bottom edge back into bounds.
345    /// See [`Self::clamp_left_edge`].
346    pub(crate) fn clamp_bottom_edge(
347        &self,
348        working: &mut PositionAndShape,
349        layout: Option<Layout>,
350        window: &Window,
351    ) {
352        let target_height = self.clamp_height(working.bottom - working.top, layout, window);
353        let new_bottom = working.top + target_height;
354        let correction = new_bottom - working.bottom;
355        working.bottom = new_bottom;
356        if layout == Some(Layout::Below) {
357            working.preview += correction;
358        }
359    }
360
361    /// Clamps the divider between the results and the preview so both panes stay
362    /// above their minimums. Runs last in each side's clamp so the divider gets
363    /// the final say after the outer edges have been bounded.
364    pub(crate) fn clamp_divider(
365        &self,
366        working: &mut PositionAndShape,
367        layout: Option<Layout>,
368        window: &Window,
369    ) {
370        let rem = window.rem_size();
371        let (total, min_preview, min_results) = match layout {
372            Some(Layout::Right) => (
373                working.right - working.left,
374                self.min_preview.width * rem,
375                self.min_results.width * rem,
376            ),
377            Some(Layout::Below) => (
378                working.bottom - working.top,
379                self.min_preview.height * rem,
380                self.min_results.height * rem,
381            ),
382            Some(Layout::Hidden) | None => return,
383        };
384        let max_preview = (total - min_results).max(min_preview);
385        working.preview = working.preview.clamp(min_preview, max_preview);
386    }
387
388    pub(crate) fn would_clamp_width_if_horizontal(&self, shape: &Shape, window: &Window) -> bool {
389        let min_width = self.min_width(Some(Layout::Right), window);
390
391        let unbounded_width = shape
392            .picker_position_and_size(Some(Layout::Right), window)
393            .width();
394
395        unbounded_width <= min_width
396    }
397
398    /// Clamps a whole picker rect (results + preview) into bounds: the total size
399    /// against the per-layout min/max, then the divider so both panes keep their
400    /// minimums. Width is clamped about its center, height anchored at the top.
401    ///
402    /// This is idempotent on an already-bounded rect, so it can run on every
403    /// render and drag-start read without shifting an in-bounds shape.
404    pub(crate) fn clamp_position_and_size(
405        &self,
406        working: &mut PositionAndShape,
407        layout: Option<Layout>,
408        window: &Window,
409    ) {
410        let target_width = self.clamp_width(working.right - working.left, layout, window);
411        let center = (working.left + working.right) / 2.0;
412        working.left = center - target_width / 2.0;
413        working.right = center + target_width / 2.0;
414
415        let target_height = self.clamp_height(working.bottom - working.top, layout, window);
416        working.bottom = working.top + target_height;
417
418        self.clamp_divider(working, layout, window);
419    }
420}
421
422impl Shape {
423    pub(crate) fn picker_position_and_size(
424        &self,
425        layout: impl Into<Option<Layout>>,
426        window: &Window,
427    ) -> PositionAndShape {
428        match self {
429            Shape::Resizing(pos) => *pos,
430            Shape::HorizontallyCentered(Centered {
431                width,
432                height,
433                preview_size,
434            }) => PositionAndShape {
435                //        W              V: full width     xxxxx: picker modal
436                // -----xxxxx------      left = (V - W) / 2
437                //     L     R           right = left + W = (V/2 - W/2) + W =  V/2 + W/2
438                left: ((RelativeWidth::FULL - *width) / 2.0).as_pixels(window),
439                right: (RelativeWidth::FULL / 2.0 + *width / 2.0).as_pixels(window),
440                top: Pixels::ZERO,
441                bottom: height.as_pixels(window),
442                preview: match layout.into() {
443                    Some(Layout::Below) => preview_size.height_as_pixels(window),
444                    Some(Layout::Right) => preview_size.width_as_pixels(window),
445                    Some(Layout::Hidden) | None => Pixels::ZERO,
446                },
447            },
448        }
449    }
450
451    /// The picker rect with all bounds applied (total size and divider). This is
452    /// the single source of truth for sizing during render and for seeding a
453    /// drag, so a shape left dirty (e.g. after a layout toggle changes the
454    /// minimums) is sanitized identically everywhere and never jumps on the
455    /// first drag.
456    pub(crate) fn clamped_position_and_size(
457        &self,
458        layout: Option<Layout>,
459        bounds: &SizeBounds,
460        window: &Window,
461    ) -> PositionAndShape {
462        let mut pos = self.picker_position_and_size(layout, window);
463        bounds.clamp_position_and_size(&mut pos, layout, window);
464        pos
465    }
466
467    pub(crate) fn results_position_and_size(
468        &self,
469        layout: Layout,
470        bounds: &SizeBounds,
471        window: &Window,
472    ) -> PositionAndShape {
473        let mut pos = self.clamped_position_and_size(Some(layout), bounds, window);
474
475        match layout {
476            Layout::Below => pos.bottom -= pos.preview,
477            Layout::Right => pos.right -= pos.preview,
478            Layout::Hidden => (),
479        }
480        pos
481    }
482
483    pub(crate) fn preview_position_and_size(
484        &self,
485        layout: Layout,
486        bounds: &SizeBounds,
487        window: &Window,
488    ) -> PositionAndShape {
489        let mut pos = self.clamped_position_and_size(Some(layout), bounds, window);
490
491        match layout {
492            Layout::Below => pos.top = pos.bottom - pos.preview,
493            Layout::Right => pos.left = pos.right - pos.preview,
494            Layout::Hidden => (),
495        }
496        pos
497    }
498
499    /// How far the center of the picker has been moved during dragging
500    /// this allows extending it on one side without the picker centering during
501    /// the resize. The drag is clamped to the size bounds (see
502    /// [`Side::current_position_and_shape`]), so the center stays in bounds too.
503    pub(crate) fn horizontal_offset(&self, window: &Window) -> Pixels {
504        let Shape::Resizing(PositionAndShape { left, right, .. }) = self else {
505            return Pixels::ZERO; // picker should be centered
506        };
507        let center = (*left + *right) / 2.0;
508        let viewport_center = window.viewport_size().width / 2.0;
509        center - viewport_center // shifting the picker by this uncenters it again
510    }
511
512    pub(crate) fn apply_results_size(
513        &self,
514        layout: impl Into<Option<Layout>>,
515        bounds: &SizeBounds,
516        fill_height: bool,
517        div: Div,
518        window: &Window,
519    ) -> Div {
520        let layout = layout.into();
521        // Work from the total picker size (full) to keep the divider from
522        // growing the picker when dragged.
523        let full = self.clamped_position_and_size(layout, bounds, window);
524        let width = match layout {
525            Some(Layout::Right) => (full.right - full.left) - full.preview,
526            _ => full.right - full.left,
527        };
528        let div = div.w(width);
529        if fill_height {
530            let height = match layout {
531                Some(Layout::Below) => (full.bottom - full.top) - full.preview,
532                _ => full.bottom - full.top,
533            };
534            div.h(height)
535        } else {
536            div
537        }
538    }
539
540    pub(crate) fn results_max_height(
541        &self,
542        bounds: &SizeBounds,
543        fill_height: bool,
544        window: &Window,
545    ) -> Option<Pixels> {
546        if fill_height {
547            None
548        } else {
549            Some(self.height(None, bounds, window))
550        }
551    }
552
553    /// The clamped total height of the picker for the given layout.
554    pub(crate) fn height(
555        &self,
556        layout: Option<Layout>,
557        bounds: &SizeBounds,
558        window: &Window,
559    ) -> Pixels {
560        let pos = self.clamped_position_and_size(layout, bounds, window);
561        pos.bottom - pos.top
562    }
563
564    pub(crate) fn apply_height(
565        &self,
566        layout: Option<Layout>,
567        bounds: &SizeBounds,
568        div: Div,
569        window: &Window,
570    ) -> Div {
571        div.h(self.height(layout, bounds, window))
572    }
573
574    pub(crate) fn results_height(
575        &self,
576        layout: Layout,
577        bounds: &SizeBounds,
578        window: &Window,
579    ) -> Pixels {
580        let pos = self.results_position_and_size(layout, bounds, window);
581        pos.bottom - pos.top
582    }
583
584    pub(crate) fn preview_width(
585        &self,
586        layout: Layout,
587        bounds: &SizeBounds,
588        window: &Window,
589    ) -> Pixels {
590        let pos = self.preview_position_and_size(layout, bounds, window);
591        pos.right - pos.left
592    }
593
594    pub(crate) fn preview_height(
595        &self,
596        layout: Layout,
597        bounds: &SizeBounds,
598        window: &Window,
599    ) -> Pixels {
600        let pos = self.preview_position_and_size(layout, bounds, window);
601        pos.bottom - pos.top
602    }
603
604    pub(crate) fn centered_and_relative(
605        pos: PositionAndShape,
606        layout: impl Into<Option<Layout>>,
607        window: &Window,
608    ) -> Centered {
609        use Layout as Pl;
610        let preview_size = match layout.into() {
611            Some(Pl::Below) => ViewportFraction::from_height_pixels(pos.preview, window),
612            Some(Pl::Right) => ViewportFraction::from_width_pixels(pos.preview, window),
613            Some(Pl::Hidden) | None => ViewportFraction::ZERO,
614        };
615
616        Centered {
617            width: RelativeWidth::from_pixels(pos.right - pos.left, window),
618            height: RelativeHeight::from_pixels(pos.bottom - pos.top, window),
619            preview_size,
620        }
621    }
622
623    pub(crate) fn set_initial_width(&mut self, w: impl Into<RelativeWidth>) {
624        if let Shape::HorizontallyCentered(Centered { width, .. }) = self {
625            *width = w.into();
626        }
627    }
628
629    pub(crate) fn set_initial_height(&mut self, h: impl Into<RelativeHeight>) {
630        if let Shape::HorizontallyCentered(Centered { height, .. }) = self {
631            *height = h.into();
632        }
633    }
634
635    pub(crate) fn reset_width(&mut self, default: &Centered) {
636        if let Shape::HorizontallyCentered(Centered { width, .. }) = self {
637            *width = default.width;
638        }
639    }
640
641    pub(crate) fn reset_height(&mut self, default: &Centered) {
642        if let Shape::HorizontallyCentered(Centered { height, .. }) = self {
643            *height = default.height;
644        }
645    }
646
647    pub(crate) fn center_divider(&mut self, layout: Layout, window: &Window) {
648        if let Shape::HorizontallyCentered(Centered {
649            width,
650            height,
651            preview_size,
652        }) = self
653        {
654            let total = match layout {
655                Layout::Right => width.as_viewport_fraction(window),
656                Layout::Below => height.as_viewport_fraction(window),
657                Layout::Hidden => return,
658            };
659            *preview_size = total * 0.5;
660        }
661    }
662}
663
664impl Default for Centered {
665    fn default() -> Self {
666        Centered {
667            width: RelativeWidth::viewport(0.6),
668            height: RelativeHeight::viewport(0.6),
669            preview_size: ViewportFraction::fraction(0.3),
670        }
671    }
672}
673
674impl Default for Shape {
675    fn default() -> Self {
676        Self::HorizontallyCentered(Centered::default())
677    }
678}
679
Served at tenant.openagents/omega Member data and write actions are omitted.