1use crate::theme::MacTheme;
7use egui::{
8 Align, CornerRadius, CursorIcon, Id, Layout, Pos2, Rect, Stroke, StrokeKind, Ui, UiBuilder,
9 Vec2,
10};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
14pub enum MacSplitOrientation {
15 #[default]
17 Horizontal,
18 Vertical,
20}
21
22pub struct MacSplitView<'a> {
47 id: Id,
48 orientation: MacSplitOrientation,
49 pos_override: Option<&'a mut f32>,
50 theme: Option<&'a MacTheme>,
51 min_first: Option<f32>,
52 max_first: Option<f32>,
53 min_second: Option<f32>,
54 collapsed_first: bool,
55 collapsed_second: bool,
56 show_handle: bool,
57 default_ratio: Option<f32>,
58}
59
60impl<'a> MacSplitView<'a> {
61 pub fn new(
63 id_source: impl std::hash::Hash + std::fmt::Debug,
64 orientation: MacSplitOrientation,
65 ) -> Self {
66 let id = Id::NULL.with(id_source);
67 Self {
68 id,
69 orientation,
70 pos_override: None,
71 theme: None,
72 min_first: None,
73 max_first: None,
74 min_second: None,
75 collapsed_first: false,
76 collapsed_second: false,
77 show_handle: true,
78 default_ratio: None,
79 }
80 }
81
82 pub fn with_pos(
84 id_source: impl std::hash::Hash + std::fmt::Debug,
85 orientation: MacSplitOrientation,
86 pos: &'a mut f32,
87 ) -> Self {
88 let mut view = Self::new(id_source, orientation);
89 view.pos_override = Some(pos);
90 view
91 }
92
93 pub fn theme(mut self, theme: &'a MacTheme) -> Self {
95 self.theme = Some(theme);
96 self
97 }
98
99 pub fn min_first(mut self, min: f32) -> Self {
101 self.min_first = Some(min);
102 self
103 }
104
105 pub fn max_first(mut self, max: f32) -> Self {
107 self.max_first = Some(max);
108 self
109 }
110
111 pub fn min_second(mut self, min: f32) -> Self {
113 self.min_second = Some(min);
114 self
115 }
116
117 pub fn collapsed_first(mut self, collapsed: bool) -> Self {
119 self.collapsed_first = collapsed;
120 self
121 }
122
123 pub fn collapsed_second(mut self, collapsed: bool) -> Self {
125 self.collapsed_second = collapsed;
126 self
127 }
128
129 pub fn show_handle(mut self, show: bool) -> Self {
131 self.show_handle = show;
132 self
133 }
134
135 pub fn default_ratio(mut self, ratio: f32) -> Self {
137 self.default_ratio = Some(ratio.clamp(0.0, 1.0));
138 self
139 }
140
141 pub fn split_panes(&mut self, ui: &mut Ui) -> (Rect, Rect) {
144 let theme_cow = MacTheme::resolve(self.theme, ui.ctx());
145 let theme: &MacTheme = &theme_cow;
146
147 let metrics = theme.split_view();
148 let divider_thickness = metrics.divider_thickness.points();
149 let hit_thickness = metrics.hit_thickness.points();
150 let min_pane_size = metrics.min_pane_size.points();
151 let handle_len = metrics.handle_length.points();
152 let handle_th = metrics.handle_thickness.points();
153
154 let container_rect = ui.available_rect_before_wrap();
155 let total_span = match self.orientation {
156 MacSplitOrientation::Horizontal => container_rect.width(),
157 MacSplitOrientation::Vertical => container_rect.height(),
158 };
159
160 let default_ratio = self
161 .default_ratio
162 .unwrap_or_else(|| metrics.default_ratio.points());
163 let mut pos = if let Some(ref p) = self.pos_override {
165 **p
166 } else {
167 ui.data_mut(|d| *d.get_temp_mut_or_insert_with(self.id, || total_span * default_ratio))
168 };
169
170 let min_1 = self.min_first.unwrap_or(min_pane_size);
171 let min_2 = self.min_second.unwrap_or(min_pane_size);
172 let max_1 = self
173 .max_first
174 .unwrap_or(total_span - min_2)
175 .min(total_span - min_2);
176 pos = pos.clamp(min_1, max_1.max(min_1));
177
178 let (first_rect, second_rect, divider_line, handle_rect) = if self.collapsed_first {
179 let r2 = container_rect;
180 let r1 = Rect::from_min_size(container_rect.min, Vec2::ZERO);
181 (r1, r2, None, None)
182 } else if self.collapsed_second {
183 let r1 = container_rect;
184 let r2 = Rect::from_min_size(container_rect.max, Vec2::ZERO);
185 (r1, r2, None, None)
186 } else {
187 match self.orientation {
188 MacSplitOrientation::Horizontal => {
189 let hit_rect = Rect::from_min_max(
190 Pos2::new(
191 container_rect.left() + pos - hit_thickness * 0.5,
192 container_rect.top(),
193 ),
194 Pos2::new(
195 container_rect.left() + pos + hit_thickness * 0.5,
196 container_rect.bottom(),
197 ),
198 );
199 let hit_resp = ui.interact(
200 hit_rect,
201 self.id.with("split_drag"),
202 egui::Sense::click_and_drag(),
203 );
204 if hit_resp.hovered() || hit_resp.dragged() {
205 ui.output_mut(|o| o.cursor_icon = CursorIcon::ResizeHorizontal);
206 }
207 if hit_resp.dragged() {
208 pos += hit_resp.drag_delta().x;
209 pos = pos.clamp(min_1, max_1.max(min_1));
210 }
211
212 let split_x = container_rect.left() + pos;
213 let divider_center_x = split_x + divider_thickness * 0.5;
214 let r1 = Rect::from_min_max(
215 container_rect.min,
216 Pos2::new(split_x.min(container_rect.right()), container_rect.bottom()),
217 );
218 let r2_left = (split_x + divider_thickness).min(container_rect.right());
219 let r2 = Rect::from_min_max(
220 Pos2::new(r2_left, container_rect.top()),
221 container_rect.max,
222 );
223
224 let line = (
225 Pos2::new(divider_center_x, container_rect.top()),
226 Pos2::new(divider_center_x, container_rect.bottom()),
227 );
228 let h_rect = if self.show_handle {
229 let center_y = container_rect.center().y;
230 Some(Rect::from_center_size(
231 Pos2::new(divider_center_x, center_y),
232 Vec2::new(handle_th, handle_len),
233 ))
234 } else {
235 None
236 };
237
238 (r1, r2, Some(line), h_rect)
239 }
240 MacSplitOrientation::Vertical => {
241 let hit_rect = Rect::from_min_max(
242 Pos2::new(
243 container_rect.left(),
244 container_rect.top() + pos - hit_thickness * 0.5,
245 ),
246 Pos2::new(
247 container_rect.right(),
248 container_rect.top() + pos + hit_thickness * 0.5,
249 ),
250 );
251 let hit_resp = ui.interact(
252 hit_rect,
253 self.id.with("split_drag"),
254 egui::Sense::click_and_drag(),
255 );
256 if hit_resp.hovered() || hit_resp.dragged() {
257 ui.output_mut(|o| o.cursor_icon = CursorIcon::ResizeVertical);
258 }
259 if hit_resp.dragged() {
260 pos += hit_resp.drag_delta().y;
261 pos = pos.clamp(min_1, max_1.max(min_1));
262 }
263
264 let split_y = container_rect.top() + pos;
265 let divider_center_y = split_y + divider_thickness * 0.5;
266 let r1 = Rect::from_min_max(
267 container_rect.min,
268 Pos2::new(container_rect.right(), split_y.min(container_rect.bottom())),
269 );
270 let r2_top = (split_y + divider_thickness).min(container_rect.bottom());
271 let r2 = Rect::from_min_max(
272 Pos2::new(container_rect.left(), r2_top),
273 container_rect.max,
274 );
275
276 let line = (
277 Pos2::new(container_rect.left(), divider_center_y),
278 Pos2::new(container_rect.right(), divider_center_y),
279 );
280 let h_rect = if self.show_handle {
281 let center_x = container_rect.center().x;
282 Some(Rect::from_center_size(
283 Pos2::new(center_x, divider_center_y),
284 Vec2::new(handle_len, handle_th),
285 ))
286 } else {
287 None
288 };
289
290 (r1, r2, Some(line), h_rect)
291 }
292 }
293 };
294
295 if let Some(ref mut p) = self.pos_override {
297 **p = pos;
298 } else {
299 ui.data_mut(|d| d.insert_temp(self.id, pos));
300 }
301
302 let painter = ui.painter_at(container_rect);
304 if let Some((p1, p2)) = divider_line {
305 painter.line_segment([p1, p2], Stroke::new(divider_thickness, theme.separator()));
306 }
307 if let Some(hr) = handle_rect {
308 painter.rect(
309 hr,
310 CornerRadius::same((handle_th * 0.5) as u8),
311 theme.control(),
312 Stroke::NONE,
313 StrokeKind::Inside,
314 );
315 }
316
317 (first_rect, second_rect)
318 }
319
320 pub fn show<R1, R2>(
322 mut self,
323 ui: &mut Ui,
324 first: impl FnOnce(&mut Ui) -> R1,
325 second: impl FnOnce(&mut Ui) -> R2,
326 ) -> (R1, R2) {
327 let container_rect = ui.available_rect_before_wrap();
328 let (first_rect, second_rect) = self.split_panes(ui);
329
330 let mut ui1 = ui.new_child(
332 UiBuilder::new()
333 .id_salt((self.id, "split_first"))
334 .max_rect(first_rect)
335 .layout(Layout::top_down(Align::Min)),
336 );
337 let r1 = first(&mut ui1);
338
339 let mut ui2 = ui.new_child(
340 UiBuilder::new()
341 .id_salt((self.id, "split_second"))
342 .max_rect(second_rect)
343 .layout(Layout::top_down(Align::Min)),
344 );
345 let r2 = second(&mut ui2);
346
347 ui.advance_cursor_after_rect(container_rect);
348
349 (r1, r2)
350 }
351}