Skip to main content

egui_macview/layout/
mod.rs

1//! Layout engines: `MacFixedView` (absolute pt), `MacFlexView` (springs/struts), `MacSplitView` (NSSplitView),
2//! `MacScrollView` (NSScrollView), and `MacStackView` (NSStackView).
3
4pub mod form;
5pub mod scroll_view;
6pub mod scrollbar;
7pub mod split_view;
8pub mod stack_view;
9
10pub use form::{MacForm, MacFormUi};
11pub use scroll_view::{MacScrollView, MacScrollViewOutput};
12pub use scrollbar::render_overlay_thumb;
13pub use split_view::{MacSplitOrientation, MacSplitView};
14pub use stack_view::{
15    MacStackAlignment, MacStackOrientation, MacStackUi, MacStackView, MacStackViewOutput,
16};
17
18use egui::{Pos2, Rect, Response, Sense, Ui, Vec2};
19
20/// Snaps a logical coordinate to the nearest physical hardware pixel boundary
21/// according to the Subpixel Snap Criterion (SPEC-001 ยง2.1 A):
22///
23/// $$x_{\text{pixel}} = \frac{\lfloor x_{\text{logical}} \cdot s + 0.5 \rfloor}{s}$$
24///
25/// # Example
26///
27/// ```rust
28/// use egui_macview::layout::subpixel_snap;
29///
30/// let snapped = subpixel_snap(10.333, 2.0); // 200% scaling
31/// assert_eq!(snapped, 10.5);
32/// ```
33#[inline]
34pub fn subpixel_snap(logical: f32, scale_factor: f32) -> f32 {
35    let s = scale_factor.max(f32::EPSILON);
36    (logical * s + 0.5).floor() / s
37}
38
39/// Snaps a 2D logical point `Pos2` to hardware pixel boundaries.
40///
41/// # Example
42///
43/// ```rust
44/// use egui_macview::layout::subpixel_snap_pos;
45/// use egui::Pos2;
46///
47/// let snapped = subpixel_snap_pos(Pos2::new(10.25, 20.75), 1.0);
48/// assert_eq!(snapped, Pos2::new(10.0, 21.0));
49/// ```
50#[inline]
51pub fn subpixel_snap_pos(pos: Pos2, scale_factor: f32) -> Pos2 {
52    Pos2::new(
53        subpixel_snap(pos.x, scale_factor),
54        subpixel_snap(pos.y, scale_factor),
55    )
56}
57
58/// Snaps a logical `Rect` to hardware pixel boundaries.
59///
60/// # Example
61///
62/// ```rust
63/// use egui_macview::layout::subpixel_snap_rect;
64/// use egui::{Pos2, Rect};
65///
66/// let rect = Rect::from_min_max(Pos2::new(0.4, 0.4), Pos2::new(10.6, 20.6));
67/// let snapped = subpixel_snap_rect(rect, 1.0);
68/// assert_eq!(snapped.min, Pos2::new(0.0, 0.0));
69/// assert_eq!(snapped.max, Pos2::new(11.0, 21.0));
70/// ```
71#[inline]
72pub fn subpixel_snap_rect(rect: Rect, scale_factor: f32) -> Rect {
73    let min = subpixel_snap_pos(rect.min, scale_factor);
74    let max = subpixel_snap_pos(rect.max, scale_factor);
75    Rect::from_min_max(min, max)
76}
77
78/// A logical geometry frame in AppKit points.
79#[derive(Debug, Clone, Copy, PartialEq)]
80pub struct MacFrame {
81    /// Top-left origin point in logical points.
82    pub origin: Pos2,
83    /// Width and height dimensions in logical points.
84    pub size: Vec2,
85}
86
87impl MacFrame {
88    /// Constructs a `MacFrame` with the given origin `(x, y)` and size `(w, h)`.
89    pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
90        Self {
91            origin: Pos2::new(x, y),
92            size: Vec2::new(w, h),
93        }
94    }
95
96    /// Converts frame into an `egui::Rect`.
97    pub fn to_rect(&self) -> Rect {
98        Rect::from_min_size(self.origin, self.size)
99    }
100}
101
102/// Absolute pt coordinate container based on macOS NIB geometries.
103pub struct MacFixedView<'a> {
104    /// Underlying egui `Ui` context.
105    pub ui: &'a mut Ui,
106    /// Absolute bounding rectangle of the fixed container.
107    pub container_rect: Rect,
108}
109
110impl<'a> MacFixedView<'a> {
111    /// Creates a new `MacFixedView` wrapping `ui` and bounded by `container_rect`.
112    pub fn new(ui: &'a mut Ui, container_rect: Rect) -> Self {
113        Self { ui, container_rect }
114    }
115
116    /// Allocates an exact subview at relative NIB coordinates (dx, dy, w, h).
117    pub fn subview_rect(&self, rel_origin: Pos2, size: Vec2) -> Rect {
118        Rect::from_min_size(
119            Pos2::new(
120                self.container_rect.left() + rel_origin.x,
121                self.container_rect.top() + rel_origin.y,
122            ),
123            size,
124        )
125    }
126
127    /// Executes a closure inside a subview allocated at relative NIB coordinates.
128    pub fn subview<R>(
129        &mut self,
130        rel_origin: Pos2,
131        size: Vec2,
132        add_contents: impl FnOnce(&mut Ui) -> R,
133    ) -> R {
134        let rect = self.subview_rect(rel_origin, size);
135        let mut child = self.ui.new_child(egui::UiBuilder::new().max_rect(rect));
136        add_contents(&mut child)
137    }
138
139    /// Allocates the container rect response after placing all subviews.
140    pub fn finish(self) -> Response {
141        self.ui.allocate_rect(self.container_rect, Sense::hover())
142    }
143}
144
145/// Flexible container supporting auto-resizing masks and fill splits.
146pub struct MacFlexView<'a> {
147    /// Underlying egui `Ui` context.
148    pub ui: &'a mut Ui,
149    /// Available bounding rectangle for flexible layout.
150    pub available_rect: Rect,
151}
152
153impl<'a> MacFlexView<'a> {
154    /// Creates a new `MacFlexView` wrapping `ui` within `available_rect`.
155    pub fn new(ui: &'a mut Ui, available_rect: Rect) -> Self {
156        Self { ui, available_rect }
157    }
158}
159
160/// Deterministic horizontal row container with fixed height and spacing budget.
161#[derive(Debug, Clone, Copy, PartialEq)]
162pub struct MacRow {
163    /// Vertical offset of the row in container coordinates.
164    pub y_offset: f32,
165    /// Fixed row height in points.
166    pub height: f32,
167    /// Horizontal cursor advancing as cells are allocated.
168    pub cursor_x: f32,
169    /// Spacing between adjacent cells in points.
170    pub item_spacing: f32,
171    /// Total available container width in points.
172    pub container_width: f32,
173}
174
175impl MacRow {
176    /// Creates a new `MacRow` container.
177    pub fn new(y_offset: f32, height: f32, container_width: f32, item_spacing: f32) -> Self {
178        Self {
179            y_offset,
180            height,
181            cursor_x: 0.0,
182            item_spacing,
183            container_width,
184        }
185    }
186
187    /// Allocates next cell of width `width`, advancing cursor with spacing.
188    /// Returns relative bounds `(x, y, width, height)`.
189    pub fn allocate_cell(&mut self, width: f32) -> (f32, f32, f32, f32) {
190        let x = self.cursor_x;
191        let y = self.y_offset;
192        let h = self.height;
193        self.cursor_x += width + self.item_spacing;
194        (x, y, width, h)
195    }
196
197    /// Returns remaining horizontal budget in points.
198    pub fn remaining_width(&self) -> f32 {
199        (self.container_width - self.cursor_x).max(0.0)
200    }
201}