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