Skip to main content

egui_macview/controls/
origin_matrix.rs

1//! Authentic macOS AppKit / Pro-Inspector `3x3` Transform Origin / Anchor Pin Grid (`MacOriginMatrix`).
2
3use crate::controls::MacControlSize;
4use crate::layout::subpixel_snap_rect;
5use crate::theme::MacTheme;
6use egui::{Key, Pos2, Rect, Response, Sense, Stroke, Ui, Vec2, Widget};
7
8/// Nine-point `3x3` transform origin / anchor pin position.
9#[repr(u8)]
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
11pub enum MacOriginAnchor {
12    /// Top-left pin (`row = 0, col = 0`).
13    TopLeft = 0,
14    /// Top-center pin (`row = 0, col = 1`).
15    TopCenter = 1,
16    /// Top-right pin (`row = 0, col = 2`).
17    TopRight = 2,
18    /// Middle-left pin (`row = 1, col = 0`).
19    MiddleLeft = 3,
20    /// Center origin pin (`row = 1, col = 1`).
21    #[default]
22    Center = 4,
23    /// Middle-right pin (`row = 1, col = 2`).
24    MiddleRight = 5,
25    /// Bottom-left pin (`row = 2, col = 0`).
26    BottomLeft = 6,
27    /// Bottom-center pin (`row = 2, col = 1`).
28    BottomCenter = 7,
29    /// Bottom-right pin (`row = 2, col = 2`).
30    BottomRight = 8,
31}
32
33impl MacOriginAnchor {
34    /// All nine anchor positions in row-major order (`0..9`).
35    pub const ALL: [Self; 9] = [
36        Self::TopLeft, Self::TopCenter, Self::TopRight,
37        Self::MiddleLeft, Self::Center, Self::MiddleRight,
38        Self::BottomLeft, Self::BottomCenter, Self::BottomRight,
39    ];
40
41    /// Returns the row-major index in `0..9`.
42    pub const fn index(self) -> usize { self as usize }
43
44    /// Converts a row-major index `0..9` into a [`MacOriginAnchor`], falling back to [`Self::Center`].
45    pub const fn from_index(index: usize) -> Self {
46        match index {
47            0 => Self::TopLeft,
48            1 => Self::TopCenter,
49            2 => Self::TopRight,
50            3 => Self::MiddleLeft,
51            4 => Self::Center,
52            5 => Self::MiddleRight,
53            6 => Self::BottomLeft,
54            7 => Self::BottomCenter,
55            8 => Self::BottomRight,
56            _ => Self::Center,
57        }
58    }
59
60    /// Returns the grid row index in `0..=2`.
61    pub const fn row(self) -> usize { (self as usize) / 3 }
62
63    /// Returns the grid column index in `0..=2`.
64    pub const fn col(self) -> usize { (self as usize) % 3 }
65
66    /// Constructs a [`MacOriginAnchor`] from `(row, col)`, clamping each axis to `0..=2`.
67    pub const fn from_row_col(row: usize, col: usize) -> Self {
68        let r = if row > 2 { 2 } else { row };
69        let c = if col > 2 { 2 } else { col };
70        Self::from_index(r * 3 + c)
71    }
72}
73
74enum OriginBinding<'a> {
75    Anchor(&'a mut MacOriginAnchor),
76    Index(&'a mut usize),
77}
78
79impl OriginBinding<'_> {
80    fn get(&self) -> MacOriginAnchor {
81        match self {
82            Self::Anchor(a) => **a,
83            Self::Index(idx) => MacOriginAnchor::from_index(**idx),
84        }
85    }
86
87    fn set(&mut self, anchor: MacOriginAnchor) {
88        match self {
89            Self::Anchor(a) => **a = anchor,
90            Self::Index(idx) => **idx = anchor.index(),
91        }
92    }
93}
94
95/// Authentic macOS Pro-Inspector `3x3` Transform Origin / Anchor Pin Grid (`MacOriginMatrix`).
96///
97/// # Example
98/// ```rust
99/// use egui_macview::{MacAppearance, MacOriginAnchor, MacOriginMatrix, MacTheme};
100/// # let ctx = egui::Context::default();
101/// # let mut out = ctx.run_ui(egui::RawInput::default(), |ui| {
102/// let theme = MacTheme::new(MacAppearance::Aqua);
103/// let mut anchor = MacOriginAnchor::Center;
104/// MacOriginMatrix::new(&mut anchor).theme(&theme).show(ui);
105/// # });
106/// # out.textures_delta.clear();
107/// ```
108pub struct MacOriginMatrix<'a> {
109    binding: OriginBinding<'a>,
110    control_size: MacControlSize,
111    fixed_rect: Option<Rect>,
112    enabled: bool,
113    theme: Option<&'a MacTheme>,
114}
115
116impl<'a> MacOriginMatrix<'a> {
117    /// Creates a new `3x3` origin matrix bound to a [`MacOriginAnchor`].
118    pub fn new(selected: &'a mut MacOriginAnchor) -> Self {
119        Self {
120            binding: OriginBinding::Anchor(selected),
121            control_size: MacControlSize::Regular,
122            fixed_rect: None,
123            enabled: true,
124            theme: None,
125        }
126    }
127
128    /// Creates a new `3x3` origin matrix bound to a row-major `usize` index (`0..9`).
129    pub fn from_index(selected_index: &'a mut usize) -> Self {
130        Self {
131            binding: OriginBinding::Index(selected_index),
132            control_size: MacControlSize::Regular,
133            fixed_rect: None,
134            enabled: true,
135            theme: None,
136        }
137    }
138
139    /// Sets the AppKit control size (`Regular` `34pt`, `Small` `28pt`, `Mini` `22pt`).
140    pub fn control_size(mut self, size: MacControlSize) -> Self {
141        self.control_size = size;
142        self
143    }
144
145    /// Pins the origin matrix to an exact NIB rectangle.
146    pub fn fixed_rect(mut self, rect: Rect) -> Self {
147        self.fixed_rect = Some(rect);
148        self
149    }
150
151    /// Enables or disables interaction.
152    pub fn enabled(mut self, enabled: bool) -> Self {
153        self.enabled = enabled;
154        self
155    }
156
157    /// Injects dynamic [`MacTheme`].
158    pub fn theme(mut self, theme: &'a MacTheme) -> Self {
159        self.theme = Some(theme);
160        self
161    }
162
163    /// Computes the exact center position of `anchor` inside `rect` given `cell_step`.
164    pub fn node_center(rect: Rect, anchor: MacOriginAnchor, cell_step: f32) -> Pos2 {
165        let c = rect.center();
166        let dx = (anchor.col() as f32 - 1.0) * cell_step;
167        let dy = (anchor.row() as f32 - 1.0) * cell_step;
168        Pos2::new(c.x + dx, c.y + dy)
169    }
170
171    /// Renders the origin matrix into `ui`.
172    pub fn show(self, ui: &mut Ui) -> Response {
173        ui.add(self)
174    }
175}
176
177impl Widget for MacOriginMatrix<'_> {
178    fn ui(mut self, ui: &mut Ui) -> Response {
179        let theme_cow = MacTheme::resolve(self.theme, ui.ctx());
180        let theme: &MacTheme = &theme_cow;
181        let om = theme.origin_matrix();
182
183        let (grid_sz, cell_step) = match self.control_size {
184            MacControlSize::Regular | MacControlSize::Large => (om.grid_size.points(), om.cell_step.points()),
185            MacControlSize::Small | MacControlSize::CompactInfobox => (om.small_grid_size.points(), om.small_cell_step.points()),
186            MacControlSize::Mini => (om.mini_grid_size.points(), om.mini_cell_step.points()),
187        };
188
189        let enabled = self.enabled && ui.is_enabled();
190        let sense = if enabled { Sense::click() } else { Sense::hover() };
191        let ppp = ui.ctx().pixels_per_point();
192        let (rect, mut response) = if let Some(fixed) = self.fixed_rect {
193            let snapped = subpixel_snap_rect(fixed, ppp);
194            let id = ui.next_auto_id().with("mac_origin_matrix_fixed");
195            ui.skip_ahead_auto_ids(1);
196            (snapped, ui.interact(snapped, id, sense))
197        } else {
198            let (r, resp) = ui.allocate_exact_size(Vec2::splat(grid_sz), sense);
199            (subpixel_snap_rect(r, ppp), resp)
200        };
201
202        let mut active = self.binding.get();
203        self.binding.set(active);
204        if enabled && response.clicked() {
205            let click_pos = response.interact_pointer_pos().unwrap_or(rect.center());
206            let (mut best, mut best_dist) = (active, f32::INFINITY);
207            for candidate in MacOriginAnchor::ALL {
208                let d = Self::node_center(rect, candidate, cell_step).distance(click_pos);
209                if d < best_dist {
210                    best_dist = d;
211                    best = candidate;
212                }
213            }
214            if best_dist <= cell_step {
215                let changed = best != active;
216                active = best;
217                self.binding.set(active);
218                response.request_focus();
219                if changed {
220                    response.mark_changed();
221                }
222            }
223        }
224
225        if enabled && response.has_focus() {
226            let (mut r, mut c) = (active.row(), active.col());
227            ui.input(|i| {
228                if i.key_pressed(Key::ArrowLeft) { c = c.saturating_sub(1); }
229                if i.key_pressed(Key::ArrowRight) { c = (c + 1).min(2); }
230                if i.key_pressed(Key::ArrowUp) { r = r.saturating_sub(1); }
231                if i.key_pressed(Key::ArrowDown) { r = (r + 1).min(2); }
232            });
233            let next = MacOriginAnchor::from_row_col(r, c);
234            if next != active {
235                active = next;
236                self.binding.set(active);
237                response.mark_changed();
238            }
239        }
240
241        let alpha = if enabled { 1.0 } else { theme.disabled_opacity() };
242        let painter = ui.painter_at(rect);
243        let visual_rect = theme.clamp_nib_visual_rect(rect, grid_sz, ppp);
244        if enabled && response.has_focus() {
245            theme.paint_focus_ring(&painter, visual_rect, om.node_radius.points());
246        }
247
248        let surface = theme.active_surface_at(ui, rect.center());
249        let line_col = theme.tertiary_label_on(surface).gamma_multiply(alpha);
250        let line_stroke = Stroke::new(om.line_stroke_width.points(), line_col);
251        for edge in [0usize, 2usize] {
252            let r_start = Self::node_center(rect, MacOriginAnchor::from_row_col(edge, 0), cell_step);
253            let r_end = Self::node_center(rect, MacOriginAnchor::from_row_col(edge, 2), cell_step);
254            painter.line_segment([r_start, r_end], line_stroke);
255            let c_start = Self::node_center(rect, MacOriginAnchor::from_row_col(0, edge), cell_step);
256            let c_end = Self::node_center(rect, MacOriginAnchor::from_row_col(2, edge), cell_step);
257            painter.line_segment([c_start, c_end], line_stroke);
258        }
259
260        let hover_pos = ui.input(|i| i.pointer.hover_pos());
261        let widget_hovered = enabled && (response.hovered() || hover_pos.is_some_and(|hp| rect.contains(hp)));
262        for anchor in MacOriginAnchor::ALL {
263            let pos = Self::node_center(rect, anchor, cell_step);
264            if anchor == active {
265                painter.circle_filled(
266                    pos,
267                    om.active_node_radius.points(),
268                    theme.control_accent().gamma_multiply(alpha),
269                );
270            } else {
271                let hovered = widget_hovered && hover_pos.is_some_and(|hp| hp.distance(pos) <= cell_step * 0.5);
272                let rad = if hovered { om.hover_node_radius.points() } else { om.node_radius.points() };
273                let fill_col = if hovered { theme.label_on(surface) } else { theme.secondary_label_on(surface) };
274                painter.circle_filled(pos, rad, fill_col.gamma_multiply(alpha));
275            }
276        }
277        response
278    }
279}