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 if self.internal.maximized().is_none() {
301 self.on_drag.is_some()
302 } else {
303 Default::default()
304 }
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 &mut 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_mut()
436 .zip(&mut self.contents)
437 .zip(tree.children.iter_mut())
438 .filter_map(|((pane, content), tree)| {
439 if self
440 .internal
441 .maximized()
442 .is_some_and(|maximized| maximized != *pane)
443 {
444 return Some(layout::Node::new(Size::ZERO));
445 }
446
447 let region = regions.get(pane)?;
448 let size = Size::new(region.width, region.height);
449
450 let node = content.layout(
451 tree,
452 renderer,
453 &layout::Limits::new(size, size),
454 );
455
456 Some(node.move_to(Point::new(region.x, region.y)))
457 })
458 .collect();
459
460 layout::Node::with_children(bounds, children)
461 }
462
463 fn operate(
464 &mut self,
465 tree: &mut Tree,
466 layout: Layout<'_>,
467 renderer: &Renderer,
468 operation: &mut dyn widget::Operation,
469 ) {
470 operation.container(None, layout.bounds(), &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}