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