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