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}