Skip to main content

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}