Skip to repository content679 lines · 22.6 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T02:33:57.650Z 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
shape.rs
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