1mod axis;
56mod configuration;
57mod content;
58mod controls;
59mod direction;
60mod draggable;
61mod node;
62mod pane;
63mod split;
64mod title_bar;
65
66pub mod state;
67
68pub use axis::Axis;
69pub use configuration::Configuration;
70pub use content::Content;
71pub use controls::Controls;
72pub use direction::Direction;
73pub use draggable::Draggable;
74pub use node::Node;
75pub use pane::Pane;
76pub use split::Split;
77pub use state::State;
78pub use title_bar::TitleBar;
79
80use crate::container;
81use crate::core::layout;
82use crate::core::mouse;
83use crate::core::overlay;
84use crate::core::renderer;
85use crate::core::touch;
86use crate::core::widget;
87use crate::core::widget::tree::{self, Tree};
88use crate::core::window;
89use crate::core::{
90 self, Background, Border, Color, Event, Layout, Length, Pixels, Point, Rectangle, Shell, Size,
91 Theme, Vector, Widget,
92};
93
94const DRAG_DEADBAND_DISTANCE: f32 = 10.0;
95const THICKNESS_RATIO: f32 = 25.0;
96
97pub struct PaneGrid<'a, Message, T, W, Theme = crate::Theme, Renderer = crate::Renderer>
149where
150 Theme: Catalog,
151 Renderer: core::Renderer,
152{
153 internal: &'a state::Internal,
154 panes: Vec<Pane>,
155 contents: Vec<Content<'a, Message, T, W, Theme, Renderer>>,
156 width: Length,
157 height: Length,
158 spacing: f32,
159 min_size: f32,
160 on_click: Option<Box<dyn Fn(Pane) -> Message + 'a>>,
161 on_drag: Option<Box<dyn Fn(DragEvent) -> Message + 'a>>,
162 on_resize: Option<(f32, Box<dyn Fn(ResizeEvent) -> Message + 'a>)>,
163 class: <Theme as Catalog>::Class<'a>,
164 last_mouse_interaction: Option<mouse::Interaction>,
165}
166
167impl<'a, Message, T, W, Theme, Renderer> PaneGrid<'a, Message, T, W, Theme, Renderer>
168where
169 Theme: Catalog,
170 Renderer: core::Renderer,
171{
172 pub fn new<S>(
177 state: &'a State<S>,
178 view: impl Fn(Pane, &'a S, bool) -> Content<'a, Message, T, W, Theme, Renderer>,
179 ) -> Self {
180 let panes = state.panes.keys().copied().collect();
181 let contents = state
182 .panes
183 .iter()
184 .map(|(pane, pane_state)| match state.maximized() {
185 Some(p) if *pane == p => view(*pane, pane_state, true),
186 _ => view(*pane, pane_state, false),
187 })
188 .collect();
189
190 Self {
191 internal: &state.internal,
192 panes,
193 contents,
194 width: Length::Fill,
195 height: Length::Fill,
196 spacing: 0.0,
197 min_size: 50.0,
198 on_click: None,
199 on_drag: None,
200 on_resize: None,
201 class: <Theme as Catalog>::default(),
202 last_mouse_interaction: None,
203 }
204 }
205
206 pub fn width(mut self, width: impl Into<Length>) -> Self {
208 self.width = width.into();
209 self
210 }
211
212 pub fn height(mut self, height: impl Into<Length>) -> Self {
214 self.height = height.into();
215 self
216 }
217
218 pub fn spacing(mut self, amount: impl Into<Pixels>) -> Self {
220 self.spacing = amount.into().0;
221 self
222 }
223
224 pub fn min_size(mut self, min_size: impl Into<Pixels>) -> Self {
226 self.min_size = min_size.into().0;
227 self
228 }
229
230 pub fn on_click<F>(mut self, f: F) -> Self
233 where
234 F: 'a + Fn(Pane) -> Message,
235 {
236 self.on_click = Some(Box::new(f));
237 self
238 }
239
240 pub fn on_drag<F>(mut self, f: F) -> Self
243 where
244 F: 'a + Fn(DragEvent) -> Message,
245 {
246 if self.internal.maximized().is_none() {
247 self.on_drag = Some(Box::new(f));
248 }
249 self
250 }
251
252 pub fn on_resize<F>(mut self, leeway: impl Into<Pixels>, f: F) -> Self
262 where
263 F: 'a + Fn(ResizeEvent) -> Message,
264 {
265 if self.internal.maximized().is_none() {
266 self.on_resize = Some((leeway.into().0, Box::new(f)));
267 }
268 self
269 }
270
271 #[must_use]
273 pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
274 where
275 <Theme as Catalog>::Class<'a>: From<StyleFn<'a, Theme>>,
276 {
277 self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
278 self
279 }
280
281 #[cfg(feature = "advanced")]
283 #[must_use]
284 pub fn class(mut self, class: impl Into<<Theme as Catalog>::Class<'a>>) -> Self {
285 self.class = class.into();
286 self
287 }
288
289 fn drag_enabled(&self) -> bool {
290 if self.internal.maximized().is_none() {
291 self.on_drag.is_some()
292 } else {
293 Default::default()
294 }
295 }
296
297 fn grid_interaction(
298 &self,
299 action: &state::Action,
300 layout: Layout,
301 cursor: mouse::Cursor,
302 ) -> Option<mouse::Interaction> {
303 if action.picked_pane().is_some() {
304 return Some(mouse::Interaction::Grabbing);
305 }
306
307 let resize_leeway = self.on_resize.as_ref().map(|(leeway, _)| *leeway);
308 let node = self.internal.layout();
309
310 let resize_axis = action.picked_split().map(|(_, axis)| axis).or_else(|| {
311 resize_leeway.and_then(|leeway| {
312 let cursor_position = cursor.position()?;
313 let bounds = layout.bounds();
314
315 let splits = node.split_regions(self.spacing, self.min_size, bounds.size());
316
317 let relative_cursor =
318 Point::new(cursor_position.x - bounds.x, cursor_position.y - bounds.y);
319
320 hovered_split(splits.iter(), self.spacing + leeway, relative_cursor)
321 .map(|(_, axis, _)| axis)
322 })
323 });
324
325 if let Some(resize_axis) = resize_axis {
326 return Some(match resize_axis {
327 Axis::Horizontal => mouse::Interaction::ResizingVertically,
328 Axis::Vertical => mouse::Interaction::ResizingHorizontally,
329 });
330 }
331
332 None
333 }
334}
335
336#[derive(Default)]
337struct Memory {
338 action: state::Action,
339 order: Vec<Pane>,
340}
341
342impl<Message, T, W, Theme, Renderer> widget::Meta for PaneGrid<'_, Message, T, W, Theme, Renderer>
343where
344 Theme: Catalog,
345 Renderer: core::Renderer,
346{
347}
348
349impl<Message, T, W, Theme, Renderer> Widget<Message, Theme, Renderer>
350 for PaneGrid<'_, Message, T, W, Theme, Renderer>
351where
352 Theme: Catalog,
353 Renderer: core::Renderer,
354 T: Widget<Message, Theme, Renderer>,
355 W: Widget<Message, Theme, Renderer>,
356{
357 fn tag(&self) -> tree::Tag {
358 tree::Tag::of::<Memory>()
359 }
360
361 fn state(&self) -> tree::State {
362 tree::State::new(Memory::default())
363 }
364
365 fn diff(&mut self, tree: &mut Tree) {
366 let Memory { order, .. } = tree.state.downcast_ref();
367
368 let mut i = 0;
375 let mut j = 0;
376 tree.children.retain(|_| {
377 let retain = self.panes.get(i) == order.get(j);
378
379 if retain {
380 i += 1;
381 }
382 j += 1;
383
384 retain
385 });
386
387 tree.diff_children_custom(
388 &mut self.contents,
389 |state, content| content.diff(state),
390 Content::state,
391 );
392
393 let Memory { order, .. } = tree.state.downcast_mut();
394 order.clone_from(&self.panes);
395 }
396
397 fn size(&self) -> Size<Length> {
398 Size {
399 width: self.width,
400 height: self.height,
401 }
402 }
403
404 fn layout(&mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits) {
405 let bounds = limits.resolve(self.width, self.height, Size::ZERO);
406 let regions = self
407 .internal
408 .layout()
409 .pane_regions(self.spacing, self.min_size, bounds);
410
411 for ((pane, content), child) in self
412 .panes
413 .iter()
414 .zip(&mut self.contents)
415 .zip(tree.children.iter_mut())
416 {
417 if self
418 .internal
419 .maximized()
420 .is_some_and(|maximized| maximized != *pane)
421 {
422 child.size = Size::ZERO;
423 continue;
424 }
425
426 let Some(region) = regions.get(pane) else {
427 child.size = Size::ZERO;
428 continue;
429 };
430
431 let size = Size::new(region.width, region.height);
432
433 content.layout(child, renderer, &layout::Limits::new(size, size));
434
435 child.translation = Vector::new(region.x, region.y);
436 }
437
438 tree.size = bounds;
439 }
440
441 fn operate(
442 &mut self,
443 tree: &mut Tree,
444 layout: Layout,
445 viewport: &Rectangle,
446 renderer: &Renderer,
447 operation: &mut dyn widget::Operation,
448 ) {
449 operation.container(None, layout.bounds(), viewport);
450 operation.traverse(&mut |operation| {
451 self.panes
452 .iter()
453 .zip(&mut self.contents)
454 .zip(layout.iter_mut(&mut tree.children))
455 .filter(|((pane, _), _)| {
456 self.internal
457 .maximized()
458 .is_none_or(|maximized| **pane == maximized)
459 })
460 .for_each(|((_pane, content), (layout, state))| {
461 content.operate(state, layout, viewport, renderer, operation);
462 });
463 });
464 }
465
466 fn update(
467 &mut self,
468 tree: &mut Tree,
469 event: &Event,
470 layout: Layout,
471 cursor: mouse::Cursor,
472 renderer: &Renderer,
473 shell: &mut Shell<'_, Message>,
474 viewport: &Rectangle,
475 ) {
476 let node = self.internal.layout();
477
478 let on_drag = if self.drag_enabled() {
479 &self.on_drag
480 } else {
481 &None
482 };
483
484 let Memory { action, .. } = tree.state.downcast_mut();
485 let picked_pane = action.picked_pane().map(|(pane, _)| pane);
486
487 for ((pane, content), (layout, child)) in self
488 .panes
489 .iter()
490 .copied()
491 .zip(&mut self.contents)
492 .zip(layout.iter_mut(&mut tree.children))
493 .filter(|((pane, _), _)| {
494 self.internal
495 .maximized()
496 .is_none_or(|maximized| *pane == maximized)
497 })
498 {
499 let is_picked = picked_pane == Some(pane);
500
501 content.update(
502 child, event, layout, cursor, renderer, shell, viewport, is_picked,
503 );
504 }
505
506 match event {
507 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
508 | Event::Touch(touch::Event::FingerPressed { .. }) => {
509 let bounds = layout.bounds();
510
511 if let Some(cursor_position) = cursor.position_over(bounds) {
512 shell.capture_event();
513
514 match &self.on_resize {
515 Some((leeway, _)) => {
516 let relative_cursor = Point::new(
517 cursor_position.x - bounds.x,
518 cursor_position.y - bounds.y,
519 );
520
521 let splits =
522 node.split_regions(self.spacing, self.min_size, bounds.size());
523
524 let clicked_split = hovered_split(
525 splits.iter(),
526 self.spacing + leeway,
527 relative_cursor,
528 );
529
530 if let Some((split, axis, _)) = clicked_split {
531 if action.picked_pane().is_none() {
532 *action = state::Action::Resizing { split, axis };
533 }
534 } else {
535 click_pane(
536 action,
537 cursor_position,
538 shell,
539 self.panes
540 .iter()
541 .copied()
542 .zip(&self.contents)
543 .zip(layout.iter(&tree.children))
544 .map(|((pane, content), (layout, tree))| {
545 (pane, content, layout, tree)
546 }),
547 &self.on_click,
548 on_drag,
549 );
550 }
551 }
552 None => {
553 click_pane(
554 action,
555 cursor_position,
556 shell,
557 self.panes
558 .iter()
559 .copied()
560 .zip(&self.contents)
561 .zip(layout.iter(&tree.children))
562 .map(|((pane, content), (layout, tree))| {
563 (pane, content, layout, tree)
564 }),
565 &self.on_click,
566 on_drag,
567 );
568 }
569 }
570 }
571 }
572 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
573 | Event::Touch(touch::Event::FingerLifted { .. })
574 | Event::Touch(touch::Event::FingerLost { .. }) => {
575 if let Some((pane, origin)) = action.picked_pane()
576 && let Some(on_drag) = on_drag
577 && let Some(cursor_position) = cursor.position()
578 {
579 if cursor_position.distance(origin) > DRAG_DEADBAND_DISTANCE {
580 let event = if let Some(edge) = in_edge(layout, cursor_position) {
581 DragEvent::Dropped {
582 pane,
583 target: Target::Edge(edge),
584 }
585 } else {
586 let dropped_region = self
587 .panes
588 .iter()
589 .copied()
590 .zip(&self.contents)
591 .zip(layout.iter(&tree.children))
592 .find_map(|(target, (layout, _))| {
593 layout_region(layout, cursor_position)
594 .map(|region| (target, region))
595 });
596
597 match dropped_region {
598 Some(((target, _), region)) if pane != target => {
599 DragEvent::Dropped {
600 pane,
601 target: Target::Pane(target, region),
602 }
603 }
604 _ => DragEvent::Canceled { pane },
605 }
606 };
607
608 shell.publish(on_drag(event));
609 } else {
610 shell.publish(on_drag(DragEvent::Canceled { pane }));
611 }
612 }
613
614 *action = state::Action::Idle;
615 }
616 Event::Mouse(mouse::Event::CursorMoved { .. })
617 | Event::Touch(touch::Event::FingerMoved { .. }) => {
618 if let Some((_, on_resize)) = &self.on_resize {
619 if let Some((split, _)) = action.picked_split() {
620 let bounds = layout.bounds();
621
622 let splits = node.split_regions(self.spacing, self.min_size, bounds.size());
623
624 if let Some((axis, rectangle, _)) = splits.get(&split)
625 && let Some(cursor_position) = cursor.position()
626 {
627 let ratio = match axis {
628 Axis::Horizontal => {
629 let position = cursor_position.y - bounds.y - rectangle.y;
630
631 (position / rectangle.height).clamp(0.0, 1.0)
632 }
633 Axis::Vertical => {
634 let position = cursor_position.x - bounds.x - rectangle.x;
635
636 (position / rectangle.width).clamp(0.0, 1.0)
637 }
638 };
639
640 shell.publish(on_resize(ResizeEvent { split, ratio }));
641
642 shell.capture_event();
643 }
644 } else if action.picked_pane().is_some() {
645 shell.request_redraw();
646 }
647 }
648 }
649 _ => {}
650 }
651
652 if shell.redraw_request() != window::RedrawRequest::NextFrame {
653 let interaction = self
654 .grid_interaction(action, layout, cursor)
655 .or_else(|| {
656 self.panes
657 .iter()
658 .zip(&self.contents)
659 .zip(layout.iter(&tree.children))
660 .filter(|((pane, _content), (_layout, _tree))| {
661 self.internal
662 .maximized()
663 .is_none_or(|maximized| **pane == maximized)
664 })
665 .find_map(|((_pane, content), (layout, tree))| {
666 content.grid_interaction(tree, layout, cursor, on_drag.is_some())
667 })
668 })
669 .unwrap_or(mouse::Interaction::None);
670
671 if let Event::Window(window::Event::RedrawRequested(_now)) = event {
672 self.last_mouse_interaction = Some(interaction);
673 } else if self
674 .last_mouse_interaction
675 .is_some_and(|last_mouse_interaction| last_mouse_interaction != interaction)
676 {
677 shell.request_redraw();
678 }
679 }
680 }
681
682 fn mouse_interaction(
683 &self,
684 tree: &Tree,
685 layout: Layout,
686 cursor: mouse::Cursor,
687 viewport: &Rectangle,
688 renderer: &Renderer,
689 ) -> mouse::Interaction {
690 let Memory { action, .. } = tree.state.downcast_ref();
691
692 if let Some(grid_interaction) = self.grid_interaction(action, layout, cursor) {
693 return grid_interaction;
694 }
695
696 self.panes
697 .iter()
698 .copied()
699 .zip(&self.contents)
700 .zip(layout.iter(&tree.children))
701 .filter(|((pane, _), _)| {
702 self.internal
703 .maximized()
704 .is_none_or(|maximized| *pane == maximized)
705 })
706 .map(|((_pane, content), (layout, tree))| {
707 content.mouse_interaction(
708 tree,
709 layout,
710 cursor,
711 viewport,
712 renderer,
713 self.drag_enabled(),
714 )
715 })
716 .max()
717 .unwrap_or_default()
718 }
719
720 fn draw(
721 &self,
722 tree: &Tree,
723 renderer: &mut Renderer,
724 theme: &Theme,
725 defaults: &renderer::Style,
726 layout: Layout,
727 cursor: mouse::Cursor,
728 viewport: &Rectangle,
729 ) {
730 let Memory { action, .. } = tree.state.downcast_ref();
731 let node = self.internal.layout();
732 let resize_leeway = self.on_resize.as_ref().map(|(leeway, _)| *leeway);
733
734 let picked_pane = action.picked_pane();
735 let dragged_pane = picked_pane
736 .filter(|(_, origin)| is_dragging(*origin, cursor.position().unwrap_or_default()));
737
738 let picked_split = action
739 .picked_split()
740 .and_then(|(split, axis)| {
741 let bounds = layout.bounds();
742
743 let splits = node.split_regions(self.spacing, self.min_size, bounds.size());
744
745 let (_axis, region, ratio) = splits.get(&split)?;
746
747 let region = axis.split_line_bounds(*region, *ratio, self.spacing);
748
749 Some((axis, region + Vector::new(bounds.x, bounds.y), true))
750 })
751 .or_else(|| match resize_leeway {
752 Some(leeway) => {
753 let cursor_position = cursor.position()?;
754 let bounds = layout.bounds();
755
756 let relative_cursor =
757 Point::new(cursor_position.x - bounds.x, cursor_position.y - bounds.y);
758
759 let splits = node.split_regions(self.spacing, self.min_size, bounds.size());
760
761 let (_split, axis, region) =
762 hovered_split(splits.iter(), self.spacing + leeway, relative_cursor)?;
763
764 Some((axis, region + Vector::new(bounds.x, bounds.y), false))
765 }
766 None => None,
767 });
768
769 let pane_cursor = if picked_pane.is_some() {
770 mouse::Cursor::Unavailable
771 } else {
772 cursor
773 };
774
775 let pane_in_edge = if picked_pane.is_some() {
776 cursor
777 .position()
778 .and_then(|cursor_position| in_edge(layout, cursor_position))
779 } else {
780 None
781 };
782
783 let style = Catalog::style(theme, &self.class);
784
785 for ((id, content), (pane_layout, tree)) in self
786 .panes
787 .iter()
788 .copied()
789 .zip(&self.contents)
790 .zip(layout.iter(&tree.children))
791 .filter(|((pane, _), _)| {
792 self.internal
793 .maximized()
794 .is_none_or(|maximized| maximized == *pane)
795 })
796 {
797 match picked_pane {
798 Some((dragging, _)) if id == dragging => {}
799 Some(_) => {
800 content.draw(
801 tree,
802 renderer,
803 theme,
804 defaults,
805 pane_layout,
806 pane_cursor,
807 viewport,
808 );
809
810 if let Some(cursor_position) = cursor.position()
811 && dragged_pane.is_some()
812 && pane_in_edge.is_none()
813 && let Some(region) = layout_region(pane_layout, cursor_position)
814 {
815 let bounds = layout_region_bounds(pane_layout, region);
816
817 renderer.fill_quad(
818 renderer::Quad {
819 bounds,
820 border: style.hovered_region.border,
821 ..renderer::Quad::default()
822 },
823 style.hovered_region.background,
824 );
825 }
826 }
827 _ => {
828 content.draw(
829 tree,
830 renderer,
831 theme,
832 defaults,
833 pane_layout,
834 pane_cursor,
835 viewport,
836 );
837 }
838 }
839 }
840
841 if dragged_pane.is_some()
842 && let Some(edge) = pane_in_edge
843 {
844 let bounds = edge_bounds(layout, edge);
845
846 renderer.fill_quad(
847 renderer::Quad {
848 bounds,
849 border: style.hovered_region.border,
850 ..renderer::Quad::default()
851 },
852 style.hovered_region.background,
853 );
854 }
855
856 if dragged_pane.is_none()
857 && let Some((axis, split_region, is_picked)) = picked_split
858 {
859 let highlight = if is_picked {
860 style.picked_split
861 } else {
862 style.hovered_split
863 };
864
865 renderer.fill_quad(
866 renderer::Quad {
867 bounds: match axis {
868 Axis::Horizontal => Rectangle {
869 x: split_region.x,
870 y: (split_region.y + (split_region.height - highlight.width) / 2.0)
871 .round(),
872 width: split_region.width,
873 height: highlight.width,
874 },
875 Axis::Vertical => Rectangle {
876 x: (split_region.x + (split_region.width - highlight.width) / 2.0)
877 .round(),
878 y: split_region.y,
879 width: highlight.width,
880 height: split_region.height,
881 },
882 },
883 ..renderer::Quad::default()
884 },
885 highlight.color,
886 );
887 }
888 }
889
890 fn overlay<'b>(
891 &'b mut self,
892 tree: &'b mut Tree,
893 layout: Layout,
894 renderer: &Renderer,
895 viewport: &Rectangle,
896 translation: Vector,
897 window: Size,
898 ) -> Vec<overlay::Element<'b, Message, Theme, Renderer>> {
899 let state = tree.state.downcast_ref::<Memory>();
900 let picked_pane = state.action.picked_pane();
901
902 self.panes
903 .iter()
904 .copied()
905 .zip(&mut self.contents)
906 .zip(layout.iter_mut(&mut tree.children))
907 .flat_map(|((pane, content), (layout, tree))| {
908 if self
909 .internal
910 .maximized()
911 .is_some_and(|maximized| maximized != pane)
912 {
913 return Vec::new();
914 }
915
916 if let Some((picked_pane, origin)) = picked_pane
917 && picked_pane == pane
918 {
919 return vec![overlay::Element::new(Box::new(PickedPane {
920 origin,
921 content,
922 tree,
923 layout,
924 }))];
925 }
926
927 content.overlay(tree, layout, renderer, viewport, translation, window)
928 })
929 .collect()
930 }
931}
932
933struct PickedPane<'a, 'b, Message, T, W, Theme, Renderer>
934where
935 Theme: container::Catalog,
936 Renderer: core::Renderer,
937{
938 content: &'a Content<'b, Message, T, W, Theme, Renderer>,
939 origin: Point,
940 tree: &'a mut Tree,
941 layout: Layout,
942}
943
944impl<'a, 'b, Message, T, W, Theme, Renderer> PickedPane<'a, 'b, Message, T, W, Theme, Renderer>
945where
946 Theme: container::Catalog,
947 Renderer: core::Renderer,
948{
949 fn bounds(&self) -> Rectangle {
950 Rectangle::new(self.origin, self.layout.bounds().size())
952 }
953}
954
955impl<'a, 'b, Message, T, W, Theme, Renderer> core::Overlay<Message, Theme, Renderer>
956 for PickedPane<'a, 'b, Message, T, W, Theme, Renderer>
957where
958 Theme: container::Catalog,
959 Renderer: core::Renderer,
960 T: Widget<Message, Theme, Renderer>,
961 W: Widget<Message, Theme, Renderer>,
962{
963 fn draw(
964 &self,
965 renderer: &mut Renderer,
966 theme: &Theme,
967 style: &renderer::Style,
968 cursor: mouse::Cursor,
969 ) {
970 let cursor_position = cursor.position().unwrap_or_default();
971
972 let translation = if is_dragging(self.origin, cursor_position) {
973 cursor_position - self.origin
974 } else {
975 Vector::ZERO
976 };
977
978 renderer.with_layer(self.bounds(), |renderer| {
979 renderer.with_translation(translation, |renderer| {
980 self.content.draw(
981 self.tree,
982 renderer,
983 theme,
984 style,
985 self.layout,
986 mouse::Cursor::Unavailable,
987 &Rectangle::INFINITE,
988 );
989 });
990 });
991 }
992}
993
994fn is_dragging(origin: Point, cursor: Point) -> bool {
995 cursor.distance(origin) > DRAG_DEADBAND_DISTANCE
996}
997
998fn layout_region(layout: Layout, cursor_position: Point) -> Option<Region> {
999 let bounds = layout.bounds();
1000
1001 if !bounds.contains(cursor_position) {
1002 return None;
1003 }
1004
1005 let region = if cursor_position.x < (bounds.x + bounds.width / 3.0) {
1006 Region::Edge(Edge::Left)
1007 } else if cursor_position.x > (bounds.x + 2.0 * bounds.width / 3.0) {
1008 Region::Edge(Edge::Right)
1009 } else if cursor_position.y < (bounds.y + bounds.height / 3.0) {
1010 Region::Edge(Edge::Top)
1011 } else if cursor_position.y > (bounds.y + 2.0 * bounds.height / 3.0) {
1012 Region::Edge(Edge::Bottom)
1013 } else {
1014 Region::Center
1015 };
1016
1017 Some(region)
1018}
1019
1020fn click_pane<'a, 'b, Message, T>(
1021 action: &mut state::Action,
1022 cursor_position: Point,
1023 shell: &mut Shell<'_, Message>,
1024 contents: impl Iterator<Item = (Pane, T, Layout, &'b Tree)>,
1025 on_click: &Option<Box<dyn Fn(Pane) -> Message + 'a>>,
1026 on_drag: &Option<Box<dyn Fn(DragEvent) -> Message + 'a>>,
1027) where
1028 T: Draggable,
1029{
1030 let mut clicked_region =
1031 contents.filter(|(_, _, layout, _)| layout.bounds().contains(cursor_position));
1032
1033 if let Some((pane, content, layout, tree)) = clicked_region.next() {
1034 if let Some(on_click) = &on_click {
1035 shell.publish(on_click(pane));
1036 }
1037
1038 if let Some(on_drag) = &on_drag
1039 && content.can_be_dragged_at(tree, layout, cursor_position)
1040 {
1041 *action = state::Action::Dragging {
1042 pane,
1043 origin: cursor_position,
1044 };
1045
1046 shell.publish(on_drag(DragEvent::Picked { pane }));
1047 }
1048 }
1049}
1050
1051fn in_edge(layout: Layout, cursor: Point) -> Option<Edge> {
1052 let bounds = layout.bounds();
1053
1054 let height_thickness = bounds.height / THICKNESS_RATIO;
1055 let width_thickness = bounds.width / THICKNESS_RATIO;
1056 let thickness = height_thickness.min(width_thickness);
1057
1058 if cursor.x > bounds.x && cursor.x < bounds.x + thickness {
1059 Some(Edge::Left)
1060 } else if cursor.x > bounds.x + bounds.width - thickness && cursor.x < bounds.x + bounds.width {
1061 Some(Edge::Right)
1062 } else if cursor.y > bounds.y && cursor.y < bounds.y + thickness {
1063 Some(Edge::Top)
1064 } else if cursor.y > bounds.y + bounds.height - thickness && cursor.y < bounds.y + bounds.height
1065 {
1066 Some(Edge::Bottom)
1067 } else {
1068 None
1069 }
1070}
1071
1072fn edge_bounds(layout: Layout, edge: Edge) -> Rectangle {
1073 let bounds = layout.bounds();
1074
1075 let height_thickness = bounds.height / THICKNESS_RATIO;
1076 let width_thickness = bounds.width / THICKNESS_RATIO;
1077 let thickness = height_thickness.min(width_thickness);
1078
1079 match edge {
1080 Edge::Top => Rectangle {
1081 height: thickness,
1082 ..bounds
1083 },
1084 Edge::Left => Rectangle {
1085 width: thickness,
1086 ..bounds
1087 },
1088 Edge::Right => Rectangle {
1089 x: bounds.x + bounds.width - thickness,
1090 width: thickness,
1091 ..bounds
1092 },
1093 Edge::Bottom => Rectangle {
1094 y: bounds.y + bounds.height - thickness,
1095 height: thickness,
1096 ..bounds
1097 },
1098 }
1099}
1100
1101fn layout_region_bounds(layout: Layout, region: Region) -> Rectangle {
1102 let bounds = layout.bounds();
1103
1104 match region {
1105 Region::Center => bounds,
1106 Region::Edge(edge) => match edge {
1107 Edge::Top => Rectangle {
1108 height: bounds.height / 2.0,
1109 ..bounds
1110 },
1111 Edge::Left => Rectangle {
1112 width: bounds.width / 2.0,
1113 ..bounds
1114 },
1115 Edge::Right => Rectangle {
1116 x: bounds.x + bounds.width / 2.0,
1117 width: bounds.width / 2.0,
1118 ..bounds
1119 },
1120 Edge::Bottom => Rectangle {
1121 y: bounds.y + bounds.height / 2.0,
1122 height: bounds.height / 2.0,
1123 ..bounds
1124 },
1125 },
1126 }
1127}
1128
1129#[derive(Debug, Clone, Copy)]
1131pub enum DragEvent {
1132 Picked {
1134 pane: Pane,
1136 },
1137
1138 Dropped {
1140 pane: Pane,
1142
1143 target: Target,
1145 },
1146
1147 Canceled {
1150 pane: Pane,
1152 },
1153}
1154
1155#[derive(Debug, Clone, Copy)]
1157pub enum Target {
1158 Edge(Edge),
1160 Pane(Pane, Region),
1162}
1163
1164#[derive(Debug, Clone, Copy, Default)]
1166pub enum Region {
1167 #[default]
1169 Center,
1170 Edge(Edge),
1172}
1173
1174#[derive(Debug, Clone, Copy)]
1176pub enum Edge {
1177 Top,
1179 Left,
1181 Right,
1183 Bottom,
1185}
1186
1187#[derive(Debug, Clone, Copy)]
1189pub struct ResizeEvent {
1190 pub split: Split,
1192
1193 pub ratio: f32,
1198}
1199
1200fn hovered_split<'a>(
1204 mut splits: impl Iterator<Item = (&'a Split, &'a (Axis, Rectangle, f32))>,
1205 spacing: f32,
1206 cursor_position: Point,
1207) -> Option<(Split, Axis, Rectangle)> {
1208 splits.find_map(|(split, (axis, region, ratio))| {
1209 let bounds = axis.split_line_bounds(*region, *ratio, spacing);
1210
1211 if bounds.contains(cursor_position) {
1212 Some((*split, *axis, bounds))
1213 } else {
1214 None
1215 }
1216 })
1217}
1218
1219#[derive(Debug, Clone, Copy, PartialEq)]
1221pub struct Style {
1222 pub hovered_region: Highlight,
1224 pub picked_split: Line,
1226 pub hovered_split: Line,
1228}
1229
1230#[derive(Debug, Clone, Copy, PartialEq)]
1232pub struct Highlight {
1233 pub background: Background,
1235 pub border: Border,
1237}
1238
1239#[derive(Debug, Clone, Copy, PartialEq)]
1243pub struct Line {
1244 pub color: Color,
1246 pub width: f32,
1248}
1249
1250pub trait Catalog: container::Catalog {
1252 type Class<'a>;
1254
1255 fn default<'a>() -> <Self as Catalog>::Class<'a>;
1257
1258 fn style(&self, class: &<Self as Catalog>::Class<'_>) -> Style;
1260}
1261
1262pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
1266
1267impl Catalog for Theme {
1268 type Class<'a> = StyleFn<'a, Self>;
1269
1270 fn default<'a>() -> StyleFn<'a, Self> {
1271 Box::new(default)
1272 }
1273
1274 fn style(&self, class: &StyleFn<'_, Self>) -> Style {
1275 class(self)
1276 }
1277}
1278
1279pub fn default(theme: &Theme) -> Style {
1281 let palette = theme.palette();
1282
1283 Style {
1284 hovered_region: Highlight {
1285 background: Background::Color(Color {
1286 a: 0.5,
1287 ..palette.primary.base.color
1288 }),
1289 border: Border {
1290 width: 2.0,
1291 color: palette.primary.strong.color,
1292 radius: 0.0.into(),
1293 },
1294 },
1295 hovered_split: Line {
1296 color: palette.primary.base.color,
1297 width: 2.0,
1298 },
1299 picked_split: Line {
1300 color: palette.primary.strong.color,
1301 width: 2.0,
1302 },
1303 }
1304}