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