1use crate::debug_state::DebugState;
18use crate::kurbo::Line;
19use crate::widget::flex::Axis;
20use crate::widget::prelude::*;
21use crate::{theme, Color, Cursor, Data, Point, Rect, WidgetPod};
22use tracing::{instrument, trace, warn};
23
24pub struct Split<T> {
26 split_axis: Axis,
27 split_point_chosen: f64,
28 split_point_effective: f64,
29 min_size: (f64, f64), bar_size: f64, min_bar_area: f64, solid: bool,
33 draggable: bool,
34 is_bar_hover: bool,
39 click_offset: f64,
43 child1: WidgetPod<T, Box<dyn Widget<T>>>,
44 old_bc_1: BoxConstraints,
45 child2: WidgetPod<T, Box<dyn Widget<T>>>,
46 old_bc_2: BoxConstraints,
47}
48
49impl<T> Split<T> {
50 fn new(
55 split_axis: Axis,
56 child1: impl Widget<T> + 'static,
57 child2: impl Widget<T> + 'static,
58 ) -> Self {
59 Split {
60 split_axis,
61 split_point_chosen: 0.5,
62 split_point_effective: 0.5,
63 min_size: (0.0, 0.0),
64 bar_size: 6.0,
65 min_bar_area: 6.0,
66 solid: false,
67 draggable: false,
68 is_bar_hover: false,
69 click_offset: 0.0,
70 child1: WidgetPod::new(child1).boxed(),
71 old_bc_1: BoxConstraints::tight(Size::ZERO),
72 child2: WidgetPod::new(child2).boxed(),
73 old_bc_2: BoxConstraints::tight(Size::ZERO),
74 }
75 }
76
77 pub fn columns(
79 left_child: impl Widget<T> + 'static,
80 right_child: impl Widget<T> + 'static,
81 ) -> Self {
82 Self::new(Axis::Horizontal, left_child, right_child)
83 }
84
85 pub fn rows(
87 upper_child: impl Widget<T> + 'static,
88 lower_child: impl Widget<T> + 'static,
89 ) -> Self {
90 Self::new(Axis::Vertical, upper_child, lower_child)
91 }
92
93 pub fn split_point(mut self, split_point: f64) -> Self {
98 assert!(
99 (0.0..=1.0).contains(&split_point),
100 "split_point must be in the range [0.0-1.0]!"
101 );
102 self.split_point_chosen = split_point;
103 self
104 }
105
106 pub fn min_size(mut self, first: f64, second: f64) -> Self {
111 assert!(first >= 0.0);
112 assert!(second >= 0.0);
113 self.min_size = (first.ceil(), second.ceil());
114 self
115 }
116
117 pub fn bar_size(mut self, bar_size: f64) -> Self {
123 assert!(bar_size >= 0.0, "bar_size must be 0.0 or greater!");
124 self.bar_size = bar_size.ceil();
125 self
126 }
127
128 pub fn min_bar_area(mut self, min_bar_area: f64) -> Self {
141 assert!(min_bar_area >= 0.0, "min_bar_area must be 0.0 or greater!");
142 self.min_bar_area = min_bar_area.ceil();
143 self
144 }
145
146 pub fn draggable(mut self, draggable: bool) -> Self {
148 self.draggable = draggable;
149 self
150 }
151
152 pub fn solid_bar(mut self, solid: bool) -> Self {
156 self.solid = solid;
157 self
158 }
159
160 #[inline]
162 fn bar_area(&self) -> f64 {
163 self.bar_size.max(self.min_bar_area)
164 }
165
166 #[inline]
168 fn bar_padding(&self) -> f64 {
169 (self.bar_area() - self.bar_size) / 2.0
170 }
171
172 fn bar_position(&self, size: Size) -> f64 {
174 let bar_area = self.bar_area();
175 match self.split_axis {
176 Axis::Horizontal => {
177 let reduced_width = size.width - bar_area;
178 let edge1 = (reduced_width * self.split_point_effective).floor();
179 edge1 + bar_area / 2.0
180 }
181 Axis::Vertical => {
182 let reduced_height = size.height - bar_area;
183 let edge1 = (reduced_height * self.split_point_effective).floor();
184 edge1 + bar_area / 2.0
185 }
186 }
187 }
188
189 fn bar_edges(&self, size: Size) -> (f64, f64) {
192 let bar_area = self.bar_area();
193 match self.split_axis {
194 Axis::Horizontal => {
195 let reduced_width = size.width - bar_area;
196 let edge1 = (reduced_width * self.split_point_effective).floor();
197 let edge2 = edge1 + bar_area;
198 (edge1, edge2)
199 }
200 Axis::Vertical => {
201 let reduced_height = size.height - bar_area;
202 let edge1 = (reduced_height * self.split_point_effective).floor();
203 let edge2 = edge1 + bar_area;
204 (edge1, edge2)
205 }
206 }
207 }
208
209 fn bar_hit_test(&self, size: Size, mouse_pos: Point) -> bool {
211 let (edge1, edge2) = self.bar_edges(size);
212 match self.split_axis {
213 Axis::Horizontal => mouse_pos.x >= edge1 && mouse_pos.x <= edge2,
214 Axis::Vertical => mouse_pos.y >= edge1 && mouse_pos.y <= edge2,
215 }
216 }
217
218 fn split_side_limits(&self, size: Size) -> (f64, f64) {
220 let split_axis_size = self.split_axis.major(size);
221
222 let (mut min_limit, min_second) = self.min_size;
223 let mut max_limit = (split_axis_size - min_second).max(0.0);
224
225 if min_limit > max_limit {
226 min_limit = 0.5 * (min_limit + max_limit);
227 max_limit = min_limit;
228 }
229
230 (min_limit, max_limit)
231 }
232
233 fn update_split_point(&mut self, size: Size, mouse_pos: Point) {
235 let (min_limit, max_limit) = self.split_side_limits(size);
236 self.split_point_chosen = match self.split_axis {
237 Axis::Horizontal => mouse_pos.x.clamp(min_limit, max_limit) / size.width,
238 Axis::Vertical => mouse_pos.y.clamp(min_limit, max_limit) / size.height,
239 }
240 }
241
242 fn bar_color(&self, env: &Env) -> Color {
244 if self.draggable {
245 env.get(theme::BORDER_LIGHT)
246 } else {
247 env.get(theme::BORDER_DARK)
248 }
249 }
250
251 fn paint_solid_bar(&mut self, ctx: &mut PaintCtx, env: &Env) {
252 let size = ctx.size();
253 let (edge1, edge2) = self.bar_edges(size);
254 let padding = self.bar_padding();
255 let rect = match self.split_axis {
256 Axis::Horizontal => Rect::from_points(
257 Point::new(edge1 + padding.ceil(), 0.0),
258 Point::new(edge2 - padding.floor(), size.height),
259 ),
260 Axis::Vertical => Rect::from_points(
261 Point::new(0.0, edge1 + padding.ceil()),
262 Point::new(size.width, edge2 - padding.floor()),
263 ),
264 };
265 let splitter_color = self.bar_color(env);
266 ctx.fill(rect, &splitter_color);
267 }
268
269 fn paint_stroked_bar(&mut self, ctx: &mut PaintCtx, env: &Env) {
270 let size = ctx.size();
271 let line_width = (self.bar_size / 3.0).floor();
274 let line_midpoint = line_width / 2.0;
275 let (edge1, edge2) = self.bar_edges(size);
276 let padding = self.bar_padding();
277 let (line1, line2) = match self.split_axis {
278 Axis::Horizontal => (
279 Line::new(
280 Point::new(edge1 + line_midpoint + padding.ceil(), 0.0),
281 Point::new(edge1 + line_midpoint + padding.ceil(), size.height),
282 ),
283 Line::new(
284 Point::new(edge2 - line_midpoint - padding.floor(), 0.0),
285 Point::new(edge2 - line_midpoint - padding.floor(), size.height),
286 ),
287 ),
288 Axis::Vertical => (
289 Line::new(
290 Point::new(0.0, edge1 + line_midpoint + padding.ceil()),
291 Point::new(size.width, edge1 + line_midpoint + padding.ceil()),
292 ),
293 Line::new(
294 Point::new(0.0, edge2 - line_midpoint - padding.floor()),
295 Point::new(size.width, edge2 - line_midpoint - padding.floor()),
296 ),
297 ),
298 };
299 let splitter_color = self.bar_color(env);
300 ctx.stroke(line1, &splitter_color, line_width);
301 ctx.stroke(line2, &splitter_color, line_width);
302 }
303}
304
305impl<T: Data> Widget<T> for Split<T> {
306 #[instrument(name = "Split", level = "trace", skip(self, ctx, event, data, env))]
307 fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) {
308 if self.child1.is_active() {
309 self.child1.event(ctx, event, data, env);
310 if ctx.is_handled() {
311 return;
312 }
313 }
314 if self.child2.is_active() {
315 self.child2.event(ctx, event, data, env);
316 if ctx.is_handled() {
317 return;
318 }
319 }
320 if self.draggable {
321 match event {
322 Event::MouseDown(mouse) => {
323 if mouse.button.is_left() && self.bar_hit_test(ctx.size(), mouse.pos) {
324 ctx.set_handled();
325 ctx.set_active(true);
326 self.click_offset = match self.split_axis {
328 Axis::Horizontal => mouse.pos.x,
329 Axis::Vertical => mouse.pos.y,
330 } - self.bar_position(ctx.size());
331 if !self.is_bar_hover {
333 self.is_bar_hover = true;
334 match self.split_axis {
335 Axis::Horizontal => ctx.set_cursor(&Cursor::ResizeLeftRight),
336 Axis::Vertical => ctx.set_cursor(&Cursor::ResizeUpDown),
337 };
338 }
339 }
340 }
341 Event::MouseUp(mouse) => {
342 if mouse.button.is_left() && ctx.is_active() {
343 ctx.set_handled();
344 ctx.set_active(false);
345 self.is_bar_hover =
348 ctx.is_hot() && self.bar_hit_test(ctx.size(), mouse.pos);
349 if !self.is_bar_hover {
350 ctx.clear_cursor()
351 }
352 }
353 }
354 Event::MouseMove(mouse) => {
355 if ctx.is_active() {
356 let effective_pos = match self.split_axis {
358 Axis::Horizontal => {
359 Point::new(mouse.pos.x - self.click_offset, mouse.pos.y)
360 }
361 Axis::Vertical => {
362 Point::new(mouse.pos.x, mouse.pos.y - self.click_offset)
363 }
364 };
365 self.update_split_point(ctx.size(), effective_pos);
366 ctx.request_layout();
367 } else {
368 let hover = ctx.is_hot() && self.bar_hit_test(ctx.size(), mouse.pos);
370 if hover != self.is_bar_hover {
371 self.is_bar_hover = hover;
372 if hover {
373 match self.split_axis {
374 Axis::Horizontal => ctx.set_cursor(&Cursor::ResizeLeftRight),
375 Axis::Vertical => ctx.set_cursor(&Cursor::ResizeUpDown),
376 };
377 } else {
378 ctx.clear_cursor();
379 }
380 }
381 }
382 }
383 _ => {}
384 }
385 }
386 if !self.child1.is_active() {
387 self.child1.event(ctx, event, data, env);
388 }
389 if !self.child2.is_active() {
390 self.child2.event(ctx, event, data, env);
391 }
392 }
393
394 #[instrument(name = "Split", level = "trace", skip(self, ctx, event, data, env))]
395 fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, env: &Env) {
396 self.child1.lifecycle(ctx, event, data, env);
397 self.child2.lifecycle(ctx, event, data, env);
398 }
399
400 #[instrument(name = "Split", level = "trace", skip(self, ctx, _old_data, data, env))]
401 fn update(&mut self, ctx: &mut UpdateCtx, _old_data: &T, data: &T, env: &Env) {
402 self.child1.update(ctx, data, env);
403 self.child2.update(ctx, data, env);
404 }
405
406 #[instrument(name = "Split", level = "trace", skip(self, ctx, bc, data, env))]
407 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T, env: &Env) -> Size {
408 bc.debug_check("Split");
409
410 match self.split_axis {
411 Axis::Horizontal => {
412 if !bc.is_width_bounded() {
413 warn!("A Split widget was given an unbounded width to split.")
414 }
415 }
416 Axis::Vertical => {
417 if !bc.is_height_bounded() {
418 warn!("A Split widget was given an unbounded height to split.")
419 }
420 }
421 }
422
423 let mut my_size = bc.max();
424 let bar_area = self.bar_area();
425 let reduced_size = Size::new(
426 (my_size.width - bar_area).max(0.),
427 (my_size.height - bar_area).max(0.),
428 );
429
430 self.split_point_effective = {
432 let (min_limit, max_limit) = self.split_side_limits(reduced_size);
433 let reduced_axis_size = self.split_axis.major(reduced_size);
434 if reduced_axis_size.is_infinite() || reduced_axis_size <= std::f64::EPSILON {
435 0.5
436 } else {
437 self.split_point_chosen
438 .clamp(min_limit / reduced_axis_size, max_limit / reduced_axis_size)
439 }
440 };
441
442 let (child1_bc, child2_bc) = match self.split_axis {
443 Axis::Horizontal => {
444 let child1_width = (reduced_size.width * self.split_point_effective)
445 .floor()
446 .max(0.0);
447 let child2_width = (reduced_size.width - child1_width).max(0.0);
448 (
449 BoxConstraints::new(
450 Size::new(child1_width, bc.min().height),
451 Size::new(child1_width, bc.max().height),
452 ),
453 BoxConstraints::new(
454 Size::new(child2_width, bc.min().height),
455 Size::new(child2_width, bc.max().height),
456 ),
457 )
458 }
459 Axis::Vertical => {
460 let child1_height = (reduced_size.height * self.split_point_effective)
461 .floor()
462 .max(0.0);
463 let child2_height = (reduced_size.height - child1_height).max(0.0);
464 (
465 BoxConstraints::new(
466 Size::new(bc.min().width, child1_height),
467 Size::new(bc.max().width, child1_height),
468 ),
469 BoxConstraints::new(
470 Size::new(bc.min().width, child2_height),
471 Size::new(bc.max().width, child2_height),
472 ),
473 )
474 }
475 };
476
477 let child1_size = if self.old_bc_1 != child1_bc || self.child1.layout_requested() {
478 self.child1.layout(ctx, &child1_bc, data, env)
479 } else {
480 self.child1.layout_rect().size()
481 };
482 self.old_bc_1 = child1_bc;
483 let child2_size = if self.old_bc_2 != child2_bc || self.child2.layout_requested() {
484 self.child2.layout(ctx, &child2_bc, data, env)
485 } else {
486 self.child2.layout_rect().size()
487 };
488 self.old_bc_2 = child2_bc;
489
490 let child1_pos = Point::ORIGIN;
493 let child2_pos = match self.split_axis {
494 Axis::Horizontal => {
495 my_size.height = child1_size.height.max(child2_size.height);
496 Point::new(child1_size.width + bar_area, 0.0)
497 }
498 Axis::Vertical => {
499 my_size.width = child1_size.width.max(child2_size.width);
500 Point::new(0.0, child1_size.height + bar_area)
501 }
502 };
503 self.child1.set_origin(ctx, child1_pos);
504 self.child2.set_origin(ctx, child2_pos);
505
506 let paint_rect = self.child1.paint_rect().union(self.child2.paint_rect());
507 let insets = paint_rect - my_size.to_rect();
508 ctx.set_paint_insets(insets);
509
510 trace!("Computed layout: size={}, insets={:?}", my_size, insets);
511 my_size
512 }
513
514 #[instrument(name = "Split", level = "trace", skip(self, ctx, data, env))]
515 fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) {
516 if self.solid {
517 self.paint_solid_bar(ctx, env);
518 } else {
519 self.paint_stroked_bar(ctx, env);
520 }
521 self.child1.paint(ctx, data, env);
522 self.child2.paint(ctx, data, env);
523 }
524
525 fn debug_state(&self, data: &T) -> DebugState {
526 DebugState {
527 display_name: self.short_type_name().to_string(),
528 children: vec![
529 self.child1.widget().debug_state(data),
530 self.child2.widget().debug_state(data),
531 ],
532 ..Default::default()
533 }
534 }
535}