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