1use std::sync::{Arc, Mutex};
15
16use crate::draw_ctx::DrawCtx;
17use crate::event::{Event, EventResult, MouseButton};
18use crate::geometry::{Point, Rect, Size};
19use crate::layout_props::{HAnchor, Insets, VAnchor, WidgetBase};
20use crate::widget::Widget;
21
22#[derive(Clone, Debug)]
34pub struct SplitterRatio(Arc<Mutex<f64>>);
35
36impl SplitterRatio {
37 pub fn new(ratio: f64) -> Self {
41 Self(Arc::new(Mutex::new(clamp_ratio(ratio, 0.5))))
42 }
43
44 pub fn get(&self) -> f64 {
47 *self.0.lock().unwrap_or_else(|e| e.into_inner())
48 }
49
50 pub fn set(&self, ratio: f64) -> bool {
59 if !ratio.is_finite() {
60 return false;
61 }
62 *self.0.lock().unwrap_or_else(|e| e.into_inner()) = ratio.clamp(RATIO_MIN, RATIO_MAX);
63 crate::animation::request_draw();
64 true
65 }
66}
67
68pub const RATIO_MIN: f64 = 0.05;
76pub const RATIO_MAX: f64 = 0.95;
77
78fn clamp_ratio(ratio: f64, fallback: f64) -> f64 {
81 if ratio.is_finite() {
82 ratio.clamp(RATIO_MIN, RATIO_MAX)
83 } else {
84 fallback
85 }
86}
87
88pub struct Splitter {
96 bounds: Rect,
97 children: Vec<Box<dyn Widget>>, base: WidgetBase,
99 pub ratio: f64,
102 pub divider_width: f64,
104 pub vertical: bool,
107
108 hovered: bool,
109 dragging: bool,
110 ratio_handle: Option<SplitterRatio>,
112}
113
114impl Splitter {
115 pub fn new(left: Box<dyn Widget>, right: Box<dyn Widget>) -> Self {
117 Self {
118 bounds: Rect::default(),
119 children: vec![left, right],
120 base: WidgetBase::new(),
121 ratio: 0.5,
122 divider_width: 6.0,
123 vertical: false,
124 hovered: false,
125 dragging: false,
126 ratio_handle: None,
127 }
128 }
129
130 pub fn vertical(top: Box<dyn Widget>, bottom: Box<dyn Widget>) -> Self {
134 Self {
135 bounds: Rect::default(),
136 children: vec![top, bottom],
137 base: WidgetBase::new(),
138 ratio: 0.5,
139 divider_width: 6.0,
140 vertical: true,
141 hovered: false,
142 dragging: false,
143 ratio_handle: None,
144 }
145 }
146
147 pub fn with_ratio(mut self, ratio: f64) -> Self {
148 self.set_ratio(ratio);
149 self
150 }
151
152 pub fn with_ratio_handle(mut self, handle: SplitterRatio) -> Self {
159 self.ratio = handle.get();
160 self.ratio_handle = Some(handle);
161 self
162 }
163
164 fn set_ratio(&mut self, ratio: f64) {
168 self.ratio = clamp_ratio(ratio, self.ratio);
169 if let Some(handle) = &self.ratio_handle {
170 handle.set(self.ratio);
171 }
172 }
173
174 fn adopt_handle_ratio(&mut self) {
177 if let Some(handle) = &self.ratio_handle {
178 let wanted = handle.get();
179 if wanted != self.ratio {
180 self.ratio = wanted;
181 }
182 }
183 }
184
185 pub fn with_divider_width(mut self, w: f64) -> Self {
186 self.divider_width = w;
187 self
188 }
189
190 pub fn with_margin(mut self, m: Insets) -> Self {
191 self.base.margin = m;
192 self
193 }
194 pub fn with_h_anchor(mut self, h: HAnchor) -> Self {
195 self.base.h_anchor = h;
196 self
197 }
198 pub fn with_v_anchor(mut self, v: VAnchor) -> Self {
199 self.base.v_anchor = v;
200 self
201 }
202 pub fn with_min_size(mut self, s: Size) -> Self {
203 self.base.min_size = s;
204 self
205 }
206 pub fn with_max_size(mut self, s: Size) -> Self {
207 self.base.max_size = s;
208 self
209 }
210
211 fn axis_length(&self) -> f64 {
213 if self.vertical {
214 self.bounds.height
215 } else {
216 self.bounds.width
217 }
218 }
219
220 fn divider_pos(&self) -> f64 {
227 (self.axis_length() - self.divider_width) * self.ratio
228 }
229}
230
231impl Widget for Splitter {
232 fn type_name(&self) -> &'static str {
233 "Splitter"
234 }
235 fn bounds(&self) -> Rect {
236 self.bounds
237 }
238 fn set_bounds(&mut self, b: Rect) {
239 self.bounds = b;
240 }
241 fn children(&self) -> &[Box<dyn Widget>] {
242 &self.children
243 }
244 fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
245 &mut self.children
246 }
247
248 fn margin(&self) -> Insets {
249 self.base.margin
250 }
251 fn widget_base(&self) -> Option<&WidgetBase> {
252 Some(&self.base)
253 }
254 fn widget_base_mut(&mut self) -> Option<&mut WidgetBase> {
255 Some(&mut self.base)
256 }
257 fn h_anchor(&self) -> HAnchor {
258 self.base.h_anchor
259 }
260 fn v_anchor(&self) -> VAnchor {
261 self.base.v_anchor
262 }
263 fn min_size(&self) -> Size {
264 self.base.min_size
265 }
266 fn max_size(&self) -> Size {
267 self.base.max_size
268 }
269
270 fn hit_test(&self, local_pos: Point) -> bool {
271 if self.dragging {
273 return true;
274 }
275 let b = self.bounds();
276 local_pos.x >= 0.0
277 && local_pos.x <= b.width
278 && local_pos.y >= 0.0
279 && local_pos.y <= b.height
280 }
281
282 fn layout(&mut self, available: Size) -> Size {
283 self.adopt_handle_ratio();
284 let div = self.divider_width;
285
286 if self.children.len() < 2 {
287 return available;
288 }
289
290 if self.vertical {
291 let top_h = ((available.height - div) * self.ratio).max(0.0);
294 let bot_h = (available.height - div - top_h).max(0.0);
295 let w = available.width;
296
297 self.children[0].layout(Size::new(w, top_h));
300 self.children[0].set_bounds(Rect::new(0.0, bot_h + div, w, top_h));
301
302 self.children[1].layout(Size::new(w, bot_h));
304 self.children[1].set_bounds(Rect::new(0.0, 0.0, w, bot_h));
305 } else {
306 let left_w = ((available.width - div) * self.ratio).max(0.0);
307 let right_w = (available.width - div - left_w).max(0.0);
308 let h = available.height;
309
310 self.children[0].layout(Size::new(left_w, h));
311 self.children[0].set_bounds(Rect::new(0.0, 0.0, left_w, h));
312
313 let right_x = left_w + div;
314 self.children[1].layout(Size::new(right_w, h));
315 self.children[1].set_bounds(Rect::new(right_x, 0.0, right_w, h));
316 }
317
318 available
319 }
320
321 fn paint(&mut self, ctx: &mut dyn DrawCtx) {
322 let v = ctx.visuals();
323 let color = if self.dragging {
324 v.accent.with_alpha(0.6)
325 } else if self.hovered {
326 v.text_color.with_alpha(0.15)
327 } else {
328 v.text_color.with_alpha(0.08)
329 };
330
331 let grip_color = if self.hovered || self.dragging {
332 v.accent.with_alpha(0.7)
333 } else {
334 v.text_color.with_alpha(0.25)
335 };
336
337 ctx.set_fill_color(color);
338
339 if self.vertical {
340 let bot_h = ((self.bounds.height - self.divider_width) * (1.0 - self.ratio)).max(0.0);
342 let div_y = bot_h;
343 let w = self.bounds.width;
344 ctx.begin_path();
345 ctx.rect(0.0, div_y, w, self.divider_width);
346 ctx.fill();
347
348 if w > 30.0 {
350 ctx.set_fill_color(grip_color);
351 let cy = div_y + self.divider_width * 0.5;
352 let cx = w * 0.5;
353 for i in -1i32..=1 {
354 ctx.begin_path();
355 ctx.circle(cx + i as f64 * 5.0, cy, 1.5);
356 ctx.fill();
357 }
358 }
359 } else {
360 let div_x = self.divider_pos();
361 let h = self.bounds.height;
362 ctx.begin_path();
363 ctx.rect(div_x, 0.0, self.divider_width, h);
364 ctx.fill();
365
366 if h > 30.0 {
367 ctx.set_fill_color(grip_color);
368 let cx = div_x + self.divider_width * 0.5;
369 let cy = h * 0.5;
370 for i in -1i32..=1 {
371 ctx.begin_path();
372 ctx.circle(cx, cy + i as f64 * 5.0, 1.5);
373 ctx.fill();
374 }
375 }
376 }
377 }
378
379 fn on_event(&mut self, event: &Event) -> EventResult {
380 if self.vertical {
381 let div = self.divider_width;
382 let total = self.bounds.height;
383 let bot_h = ((total - div) * (1.0 - self.ratio)).max(0.0);
385 let div_y = bot_h;
386 let div_end = div_y + div;
387
388 match event {
389 Event::MouseMove { pos } => {
390 let over_div = pos.y >= div_y - 2.0 && pos.y <= div_end + 2.0;
391 let was = self.hovered;
392 self.hovered = over_div;
393 if self.dragging {
394 if total > div {
395 let div_mid = pos.y;
399 let top_h = (total - div_mid).max(0.0);
400 self.set_ratio(top_h / total);
401 }
402 crate::animation::request_draw();
403 EventResult::Consumed
404 } else {
405 if was != self.hovered {
406 crate::animation::request_draw();
407 return EventResult::Consumed;
408 }
409 EventResult::Ignored
410 }
411 }
412 Event::MouseDown {
413 pos,
414 button: MouseButton::Left,
415 ..
416 } => {
417 if pos.y >= div_y - 2.0 && pos.y <= div_end + 2.0 {
418 self.dragging = true;
419 EventResult::Consumed
420 } else {
421 EventResult::Ignored
422 }
423 }
424 Event::MouseUp {
425 button: MouseButton::Left,
426 ..
427 } => {
428 let was_dragging = self.dragging;
429 self.dragging = false;
430 if was_dragging {
431 crate::animation::request_draw();
432 EventResult::Consumed
433 } else {
434 EventResult::Ignored
435 }
436 }
437 _ => EventResult::Ignored,
438 }
439 } else {
440 let div_x = self.divider_pos();
441 let div_end = div_x + self.divider_width;
442
443 match event {
444 Event::MouseMove { pos } => {
445 let over_div = pos.x >= div_x - 2.0 && pos.x <= div_end + 2.0;
446 let was = self.hovered;
447 self.hovered = over_div;
448 if self.dragging {
449 let total = self.bounds.width;
450 if total > self.divider_width {
451 self.set_ratio(pos.x / total);
452 }
453 crate::animation::request_draw();
454 EventResult::Consumed
455 } else {
456 if was != self.hovered {
457 crate::animation::request_draw();
458 return EventResult::Consumed;
459 }
460 EventResult::Ignored
461 }
462 }
463 Event::MouseDown {
464 pos,
465 button: MouseButton::Left,
466 ..
467 } => {
468 if pos.x >= div_x - 2.0 && pos.x <= div_end + 2.0 {
469 self.dragging = true;
470 EventResult::Consumed
471 } else {
472 EventResult::Ignored
473 }
474 }
475 Event::MouseUp {
476 button: MouseButton::Left,
477 ..
478 } => {
479 let was_dragging = self.dragging;
480 self.dragging = false;
481 if was_dragging {
482 crate::animation::request_draw();
483 EventResult::Consumed
484 } else {
485 EventResult::Ignored
486 }
487 }
488 _ => EventResult::Ignored,
489 }
490 }
491 }
492}