egui_macview/layout/mod.rs
1//! Layout engines: `MacFixedView` (absolute pt), `MacSplitView` (NSSplitView),
2//! `MacScrollView` (NSScrollView), `MacStackView` (NSStackView), and `MacForm` (NSForm).
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 subview_count: usize,
109}
110
111impl<'a> MacFixedView<'a> {
112 /// Creates a new `MacFixedView` wrapping `ui` and bounded by `container_rect`.
113 pub fn new(ui: &'a mut Ui, container_rect: Rect) -> Self {
114 Self {
115 ui,
116 container_rect,
117 subview_count: 0,
118 }
119 }
120
121 /// Allocates an exact subview at relative NIB coordinates (dx, dy, w, h).
122 pub fn subview_rect(&self, rel_origin: Pos2, size: Vec2) -> Rect {
123 Rect::from_min_size(
124 Pos2::new(
125 self.container_rect.left() + rel_origin.x,
126 self.container_rect.top() + rel_origin.y,
127 ),
128 size,
129 )
130 }
131
132 /// Executes a closure inside a subview allocated at relative NIB coordinates.
133 pub fn subview<R>(
134 &mut self,
135 rel_origin: Pos2,
136 size: Vec2,
137 add_contents: impl FnOnce(&mut Ui) -> R,
138 ) -> R {
139 let idx = self.subview_count;
140 self.subview_count += 1;
141 let rect = self.subview_rect(rel_origin, size);
142 let mut child = self.ui.new_child(
143 egui::UiBuilder::new()
144 .id_salt(("fixed_subview", idx))
145 .max_rect(rect),
146 );
147 child.set_clip_rect(rect.intersect(self.ui.clip_rect()));
148 add_contents(&mut child)
149 }
150
151 /// Allocates the container rect response after placing all subviews.
152 pub fn finish(self) -> Response {
153 self.ui.allocate_rect(self.container_rect, Sense::hover())
154 }
155}
156
157/// Deterministic horizontal row container with fixed height and spacing budget.
158#[derive(Debug, Clone, Copy, PartialEq)]
159pub struct MacRow {
160 /// Vertical offset of the row in container coordinates.
161 pub y_offset: f32,
162 /// Fixed row height in points.
163 pub height: f32,
164 /// Horizontal cursor advancing as cells are allocated.
165 pub cursor_x: f32,
166 /// Spacing between adjacent cells in points.
167 pub item_spacing: f32,
168 /// Total available container width in points.
169 pub container_width: f32,
170}
171
172impl MacRow {
173 /// Creates a new `MacRow` container.
174 pub fn new(y_offset: f32, height: f32, container_width: f32, item_spacing: f32) -> Self {
175 Self {
176 y_offset,
177 height,
178 cursor_x: 0.0,
179 item_spacing,
180 container_width,
181 }
182 }
183
184 /// Allocates next cell of width `width`, advancing cursor with spacing.
185 /// Returns relative bounds `(x, y, width, height)`.
186 pub fn allocate_cell(&mut self, width: f32) -> (f32, f32, f32, f32) {
187 let x = self.cursor_x;
188 let y = self.y_offset;
189 let h = self.height;
190 self.cursor_x += width + self.item_spacing;
191 (x, y, width, h)
192 }
193
194 /// Returns remaining horizontal budget in points.
195 pub fn remaining_width(&self) -> f32 {
196 (self.container_width - self.cursor_x).max(0.0)
197 }
198}
199
200/// Returns whether the host window and viewport are currently focused, active,
201/// and not minimized. Used across timers, animations, and audio synchronization
202/// to eliminate wasteful background repaints and battery drain.
203pub fn is_window_active(ctx: &egui::Context) -> bool {
204 ctx.input(|i| {
205 i.focused
206 && i.viewport().focused.unwrap_or(true)
207 && !i.viewport().minimized.unwrap_or(false)
208 })
209}