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}
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        child.set_clip_rect(rect.intersect(self.ui.clip_rect()));
137        add_contents(&mut child)
138    }
139
140    /// Allocates the container rect response after placing all subviews.
141    pub fn finish(self) -> Response {
142        self.ui.allocate_rect(self.container_rect, Sense::hover())
143    }
144}
145
146/// Deterministic horizontal row container with fixed height and spacing budget.
147#[derive(Debug, Clone, Copy, PartialEq)]
148pub struct MacRow {
149    /// Vertical offset of the row in container coordinates.
150    pub y_offset: f32,
151    /// Fixed row height in points.
152    pub height: f32,
153    /// Horizontal cursor advancing as cells are allocated.
154    pub cursor_x: f32,
155    /// Spacing between adjacent cells in points.
156    pub item_spacing: f32,
157    /// Total available container width in points.
158    pub container_width: f32,
159}
160
161impl MacRow {
162    /// Creates a new `MacRow` container.
163    pub fn new(y_offset: f32, height: f32, container_width: f32, item_spacing: f32) -> Self {
164        Self {
165            y_offset,
166            height,
167            cursor_x: 0.0,
168            item_spacing,
169            container_width,
170        }
171    }
172
173    /// Allocates next cell of width `width`, advancing cursor with spacing.
174    /// Returns relative bounds `(x, y, width, height)`.
175    pub fn allocate_cell(&mut self, width: f32) -> (f32, f32, f32, f32) {
176        let x = self.cursor_x;
177        let y = self.y_offset;
178        let h = self.height;
179        self.cursor_x += width + self.item_spacing;
180        (x, y, width, h)
181    }
182
183    /// Returns remaining horizontal budget in points.
184    pub fn remaining_width(&self) -> f32 {
185        (self.container_width - self.cursor_x).max(0.0)
186    }
187}