1use 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#[repr(u8)]
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
11pub enum MacOriginAnchor {
12 TopLeft = 0,
14 TopCenter = 1,
16 TopRight = 2,
18 MiddleLeft = 3,
20 #[default]
22 Center = 4,
23 MiddleRight = 5,
25 BottomLeft = 6,
27 BottomCenter = 7,
29 BottomRight = 8,
31}
32
33impl MacOriginAnchor {
34 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 pub const fn index(self) -> usize { self as usize }
43
44 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 pub const fn row(self) -> usize { (self as usize) / 3 }
62
63 pub const fn col(self) -> usize { (self as usize) % 3 }
65
66 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
95pub 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 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 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 pub fn control_size(mut self, size: MacControlSize) -> Self {
141 self.control_size = size;
142 self
143 }
144
145 pub fn fixed_rect(mut self, rect: Rect) -> Self {
147 self.fixed_rect = Some(rect);
148 self
149 }
150
151 pub fn enabled(mut self, enabled: bool) -> Self {
153 self.enabled = enabled;
154 self
155 }
156
157 pub fn theme(mut self, theme: &'a MacTheme) -> Self {
159 self.theme = Some(theme);
160 self
161 }
162
163 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 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}