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