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 diff(&mut self, tree: &mut Tree) {
359 let Memory { order, .. } = tree.state.downcast_ref();
360
361 let mut i = 0;
368 let mut j = 0;
369 tree.children.retain(|_| {
370 let retain = self.panes.get(i) == order.get(j);
371
372 if retain {
373 i += 1;
374 }
375 j += 1;
376
377 retain
378 });
379
380 tree.diff_children_custom(
381 &mut self.contents,
382 |state, content| content.diff(state),
383 Content::state,
384 );
385
386 let Memory { order, .. } = tree.state.downcast_mut();
387 order.clone_from(&self.panes);
388 }
389
390 fn size(&self) -> Size<Length> {
391 Size {
392 width: self.width,
393 height: self.height,
394 }
395 }
396
397 fn layout(
398 &mut self,
399 tree: &mut Tree,
400 renderer: &Renderer,
401 limits: &layout::Limits,
402 ) -> layout::Node {
403 let bounds = limits.resolve(self.width, self.height, Size::ZERO);
404 let regions = self
405 .internal
406 .layout()
407 .pane_regions(self.spacing, self.min_size, bounds);
408
409 let children = self
410 .panes
411 .iter_mut()
412 .zip(&mut self.contents)
413 .zip(tree.children.iter_mut())
414 .filter_map(|((pane, content), tree)| {
415 if self
416 .internal
417 .maximized()
418 .is_some_and(|maximized| maximized != *pane)
419 {
420 return Some(layout::Node::new(Size::ZERO));
421 }
422
423 let region = regions.get(pane)?;
424 let size = Size::new(region.width, region.height);
425
426 let node = content.layout(tree, renderer, &layout::Limits::new(size, size));
427
428 Some(node.move_to(Point::new(region.x, region.y)))
429 })
430 .collect();
431
432 layout::Node::with_children(bounds, children)
433 }
434
435 fn operate(
436 &mut self,
437 tree: &mut Tree,
438 layout: Layout<'_>,
439 renderer: &Renderer,
440 operation: &mut dyn widget::Operation,
441 ) {
442 operation.container(None, layout.bounds());
443 operation.traverse(&mut |operation| {
444 self.panes
445 .iter_mut()
446 .zip(&mut self.contents)
447 .zip(&mut tree.children)
448 .zip(layout.children())
449 .filter(|(((pane, _), _), _)| {
450 self.internal
451 .maximized()
452 .is_none_or(|maximized| **pane == maximized)
453 })
454 .for_each(|(((_, content), state), layout)| {
455 content.operate(state, layout, renderer, operation);
456 });
457 });
458 }
459
460 fn update(
461 &mut self,
462 tree: &mut Tree,
463 event: &Event,
464 layout: Layout<'_>,
465 cursor: mouse::Cursor,
466 renderer: &Renderer,
467 shell: &mut Shell<'_, Message>,
468 viewport: &Rectangle,
469 ) {
470 let Memory { action, .. } = tree.state.downcast_mut();
471 let node = self.internal.layout();
472
473 let on_drag = if self.drag_enabled() {
474 &self.on_drag
475 } else {
476 &None
477 };
478
479 let picked_pane = action.picked_pane().map(|(pane, _)| pane);
480
481 for (((pane, content), tree), layout) in self
482 .panes
483 .iter()
484 .copied()
485 .zip(&mut self.contents)
486 .zip(&mut tree.children)
487 .zip(layout.children())
488 .filter(|(((pane, _), _), _)| {
489 self.internal
490 .maximized()
491 .is_none_or(|maximized| *pane == maximized)
492 })
493 {
494 let is_picked = picked_pane == Some(pane);
495
496 content.update(
497 tree, event, layout, cursor, renderer, shell, viewport, is_picked,
498 );
499 }
500
501 match event {
502 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
503 | Event::Touch(touch::Event::FingerPressed { .. }) => {
504 let bounds = layout.bounds();
505
506 if let Some(cursor_position) = cursor.position_over(bounds) {
507 shell.capture_event();
508
509 match &self.on_resize {
510 Some((leeway, _)) => {
511 let relative_cursor = Point::new(
512 cursor_position.x - bounds.x,
513 cursor_position.y - bounds.y,
514 );
515
516 let splits =
517 node.split_regions(self.spacing, self.min_size, bounds.size());
518
519 let clicked_split = hovered_split(
520 splits.iter(),
521 self.spacing + leeway,
522 relative_cursor,
523 );
524
525 if let Some((split, axis, _)) = clicked_split {
526 if action.picked_pane().is_none() {
527 *action = state::Action::Resizing { split, axis };
528 }
529 } else {
530 click_pane(
531 action,
532 layout,
533 cursor_position,
534 shell,
535 self.panes.iter().copied().zip(&self.contents),
536 &self.on_click,
537 on_drag,
538 );
539 }
540 }
541 None => {
542 click_pane(
543 action,
544 layout,
545 cursor_position,
546 shell,
547 self.panes.iter().copied().zip(&self.contents),
548 &self.on_click,
549 on_drag,
550 );
551 }
552 }
553 }
554 }
555 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
556 | Event::Touch(touch::Event::FingerLifted { .. })
557 | Event::Touch(touch::Event::FingerLost { .. }) => {
558 if let Some((pane, origin)) = action.picked_pane()
559 && let Some(on_drag) = on_drag
560 && let Some(cursor_position) = cursor.position()
561 {
562 if cursor_position.distance(origin) > DRAG_DEADBAND_DISTANCE {
563 let event = if let Some(edge) = in_edge(layout, cursor_position) {
564 DragEvent::Dropped {
565 pane,
566 target: Target::Edge(edge),
567 }
568 } else {
569 let dropped_region = self
570 .panes
571 .iter()
572 .copied()
573 .zip(&self.contents)
574 .zip(layout.children())
575 .find_map(|(target, layout)| {
576 layout_region(layout, cursor_position)
577 .map(|region| (target, region))
578 });
579
580 match dropped_region {
581 Some(((target, _), region)) if pane != target => {
582 DragEvent::Dropped {
583 pane,
584 target: Target::Pane(target, region),
585 }
586 }
587 _ => DragEvent::Canceled { pane },
588 }
589 };
590
591 shell.publish(on_drag(event));
592 } else {
593 shell.publish(on_drag(DragEvent::Canceled { pane }));
594 }
595 }
596
597 *action = state::Action::Idle;
598 }
599 Event::Mouse(mouse::Event::CursorMoved { .. })
600 | Event::Touch(touch::Event::FingerMoved { .. }) => {
601 if let Some((_, on_resize)) = &self.on_resize {
602 if let Some((split, _)) = action.picked_split() {
603 let bounds = layout.bounds();
604
605 let splits = node.split_regions(self.spacing, self.min_size, bounds.size());
606
607 if let Some((axis, rectangle, _)) = splits.get(&split)
608 && let Some(cursor_position) = cursor.position()
609 {
610 let ratio = match axis {
611 Axis::Horizontal => {
612 let position = cursor_position.y - bounds.y - rectangle.y;
613
614 (position / rectangle.height).clamp(0.0, 1.0)
615 }
616 Axis::Vertical => {
617 let position = cursor_position.x - bounds.x - rectangle.x;
618
619 (position / rectangle.width).clamp(0.0, 1.0)
620 }
621 };
622
623 shell.publish(on_resize(ResizeEvent { split, ratio }));
624
625 shell.capture_event();
626 }
627 } else if action.picked_pane().is_some() {
628 shell.request_redraw();
629 }
630 }
631 }
632 _ => {}
633 }
634
635 if shell.redraw_request() != window::RedrawRequest::NextFrame {
636 let interaction = self
637 .grid_interaction(action, layout, cursor)
638 .or_else(|| {
639 self.panes
640 .iter()
641 .zip(&self.contents)
642 .zip(layout.children())
643 .filter(|((pane, _content), _layout)| {
644 self.internal
645 .maximized()
646 .is_none_or(|maximized| **pane == maximized)
647 })
648 .find_map(|((_pane, content), layout)| {
649 content.grid_interaction(layout, cursor, on_drag.is_some())
650 })
651 })
652 .unwrap_or(mouse::Interaction::None);
653
654 if let Event::Window(window::Event::RedrawRequested(_now)) = event {
655 self.last_mouse_interaction = Some(interaction);
656 } else if self
657 .last_mouse_interaction
658 .is_some_and(|last_mouse_interaction| last_mouse_interaction != interaction)
659 {
660 shell.request_redraw();
661 }
662 }
663 }
664
665 fn mouse_interaction(
666 &self,
667 tree: &Tree,
668 layout: Layout<'_>,
669 cursor: mouse::Cursor,
670 viewport: &Rectangle,
671 renderer: &Renderer,
672 ) -> mouse::Interaction {
673 let Memory { action, .. } = tree.state.downcast_ref();
674
675 if let Some(grid_interaction) = self.grid_interaction(action, layout, cursor) {
676 return grid_interaction;
677 }
678
679 self.panes
680 .iter()
681 .copied()
682 .zip(&self.contents)
683 .zip(&tree.children)
684 .zip(layout.children())
685 .filter(|(((pane, _), _), _)| {
686 self.internal
687 .maximized()
688 .is_none_or(|maximized| *pane == maximized)
689 })
690 .map(|(((_, content), tree), layout)| {
691 content.mouse_interaction(
692 tree,
693 layout,
694 cursor,
695 viewport,
696 renderer,
697 self.drag_enabled(),
698 )
699 })
700 .max()
701 .unwrap_or_default()
702 }
703
704 fn draw(
705 &self,
706 tree: &Tree,
707 renderer: &mut Renderer,
708 theme: &Theme,
709 defaults: &renderer::Style,
710 layout: Layout<'_>,
711 cursor: mouse::Cursor,
712 viewport: &Rectangle,
713 ) {
714 let Memory { action, .. } = tree.state.downcast_ref();
715 let node = self.internal.layout();
716 let resize_leeway = self.on_resize.as_ref().map(|(leeway, _)| *leeway);
717
718 let picked_pane = action.picked_pane();
719 let dragged_pane = picked_pane
720 .filter(|(_, origin)| is_dragging(*origin, cursor.position().unwrap_or_default()));
721
722 let picked_split = action
723 .picked_split()
724 .and_then(|(split, axis)| {
725 let bounds = layout.bounds();
726
727 let splits = node.split_regions(self.spacing, self.min_size, bounds.size());
728
729 let (_axis, region, ratio) = splits.get(&split)?;
730
731 let region = axis.split_line_bounds(*region, *ratio, self.spacing);
732
733 Some((axis, region + Vector::new(bounds.x, bounds.y), true))
734 })
735 .or_else(|| match resize_leeway {
736 Some(leeway) => {
737 let cursor_position = cursor.position()?;
738 let bounds = layout.bounds();
739
740 let relative_cursor =
741 Point::new(cursor_position.x - bounds.x, cursor_position.y - bounds.y);
742
743 let splits = node.split_regions(self.spacing, self.min_size, bounds.size());
744
745 let (_split, axis, region) =
746 hovered_split(splits.iter(), self.spacing + leeway, relative_cursor)?;
747
748 Some((axis, region + Vector::new(bounds.x, bounds.y), false))
749 }
750 None => None,
751 });
752
753 let pane_cursor = if picked_pane.is_some() {
754 mouse::Cursor::Unavailable
755 } else {
756 cursor
757 };
758
759 let pane_in_edge = if picked_pane.is_some() {
760 cursor
761 .position()
762 .and_then(|cursor_position| in_edge(layout, cursor_position))
763 } else {
764 None
765 };
766
767 let style = Catalog::style(theme, &self.class);
768
769 for (((id, content), tree), pane_layout) in self
770 .panes
771 .iter()
772 .copied()
773 .zip(&self.contents)
774 .zip(&tree.children)
775 .zip(layout.children())
776 .filter(|(((pane, _), _), _)| {
777 self.internal
778 .maximized()
779 .is_none_or(|maximized| maximized == *pane)
780 })
781 {
782 match picked_pane {
783 Some((dragging, _)) if id == dragging => {}
784 Some(_) => {
785 content.draw(
786 tree,
787 renderer,
788 theme,
789 defaults,
790 pane_layout,
791 pane_cursor,
792 viewport,
793 );
794
795 if let Some(cursor_position) = cursor.position()
796 && dragged_pane.is_some()
797 && pane_in_edge.is_none()
798 && let Some(region) = layout_region(pane_layout, cursor_position)
799 {
800 let bounds = layout_region_bounds(pane_layout, region);
801
802 renderer.fill_quad(
803 renderer::Quad {
804 bounds,
805 border: style.hovered_region.border,
806 ..renderer::Quad::default()
807 },
808 style.hovered_region.background,
809 );
810 }
811 }
812 _ => {
813 content.draw(
814 tree,
815 renderer,
816 theme,
817 defaults,
818 pane_layout,
819 pane_cursor,
820 viewport,
821 );
822 }
823 }
824 }
825
826 if dragged_pane.is_some()
827 && let Some(edge) = pane_in_edge
828 {
829 let bounds = edge_bounds(layout, edge);
830
831 renderer.fill_quad(
832 renderer::Quad {
833 bounds,
834 border: style.hovered_region.border,
835 ..renderer::Quad::default()
836 },
837 style.hovered_region.background,
838 );
839 }
840
841 if dragged_pane.is_none()
842 && let Some((axis, split_region, is_picked)) = picked_split
843 {
844 let highlight = if is_picked {
845 style.picked_split
846 } else {
847 style.hovered_split
848 };
849
850 renderer.fill_quad(
851 renderer::Quad {
852 bounds: match axis {
853 Axis::Horizontal => Rectangle {
854 x: split_region.x,
855 y: (split_region.y + (split_region.height - highlight.width) / 2.0)
856 .round(),
857 width: split_region.width,
858 height: highlight.width,
859 },
860 Axis::Vertical => Rectangle {
861 x: (split_region.x + (split_region.width - highlight.width) / 2.0)
862 .round(),
863 y: split_region.y,
864 width: highlight.width,
865 height: split_region.height,
866 },
867 },
868 ..renderer::Quad::default()
869 },
870 highlight.color,
871 );
872 }
873 }
874
875 fn overlay<'b>(
876 &'b mut self,
877 tree: &'b mut Tree,
878 layout: Layout<'b>,
879 renderer: &Renderer,
880 viewport: &Rectangle,
881 translation: Vector,
882 ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
883 let state = tree.state.downcast_ref::<Memory>();
884 let picked_pane = state.action.picked_pane();
885
886 let children = self
887 .panes
888 .iter()
889 .copied()
890 .zip(&mut self.contents)
891 .zip(&mut tree.children)
892 .zip(layout.children())
893 .filter_map(|(((pane, content), tree), layout)| {
894 if self
895 .internal
896 .maximized()
897 .is_some_and(|maximized| maximized != pane)
898 {
899 return None;
900 }
901
902 if let Some((picked_pane, origin)) = picked_pane
903 && picked_pane == pane
904 {
905 return Some(overlay::Element::new(Box::new(PickedPane {
906 origin,
907 content,
908 tree,
909 layout,
910 })));
911 }
912
913 content.overlay(tree, layout, renderer, viewport, translation)
914 })
915 .collect::<Vec<_>>();
916
917 (!children.is_empty()).then(|| Group::with_children(children).overlay())
918 }
919}
920
921struct PickedPane<'a, 'b, Message, Theme, Renderer>
922where
923 Theme: container::Catalog,
924 Renderer: core::Renderer,
925{
926 content: &'a Content<'b, Message, Theme, Renderer>,
927 origin: Point,
928 tree: &'a mut Tree,
929 layout: Layout<'a>,
930}
931
932impl<'a, 'b, Message, Theme, Renderer> core::Overlay<Message, Theme, Renderer>
933 for PickedPane<'a, 'b, Message, Theme, Renderer>
934where
935 Theme: container::Catalog,
936 Renderer: core::Renderer,
937{
938 fn layout(&mut self, _renderer: &Renderer, _bounds: Size) -> layout::Node {
939 layout::Node::new(self.layout.bounds().size()).move_to(self.origin)
941 }
942
943 fn draw(
944 &self,
945 renderer: &mut Renderer,
946 theme: &Theme,
947 style: &renderer::Style,
948 _layout: Layout<'_>,
949 cursor: mouse::Cursor,
950 ) {
951 let cursor_position = cursor.position().unwrap_or_default();
952
953 let translation = if is_dragging(self.origin, cursor_position) {
954 cursor_position - self.origin
955 } else {
956 Vector::ZERO
957 };
958
959 renderer.with_translation(translation, |renderer| {
960 self.content.draw(
961 self.tree,
962 renderer,
963 theme,
964 style,
965 self.layout,
966 mouse::Cursor::Unavailable,
967 &Rectangle::INFINITE,
968 );
969 });
970 }
971}
972
973fn is_dragging(origin: Point, cursor: Point) -> bool {
974 cursor.distance(origin) > DRAG_DEADBAND_DISTANCE
975}
976
977impl<'a, Message, Theme, Renderer> From<PaneGrid<'a, Message, Theme, Renderer>>
978 for Element<'a, Message, Theme, Renderer>
979where
980 Message: 'a,
981 Theme: Catalog + 'a,
982 Renderer: core::Renderer + 'a,
983{
984 fn from(
985 pane_grid: PaneGrid<'a, Message, Theme, Renderer>,
986 ) -> Element<'a, Message, Theme, Renderer> {
987 Element::new(pane_grid)
988 }
989}
990
991fn layout_region(layout: Layout<'_>, cursor_position: Point) -> Option<Region> {
992 let bounds = layout.bounds();
993
994 if !bounds.contains(cursor_position) {
995 return None;
996 }
997
998 let region = if cursor_position.x < (bounds.x + bounds.width / 3.0) {
999 Region::Edge(Edge::Left)
1000 } else if cursor_position.x > (bounds.x + 2.0 * bounds.width / 3.0) {
1001 Region::Edge(Edge::Right)
1002 } else if cursor_position.y < (bounds.y + bounds.height / 3.0) {
1003 Region::Edge(Edge::Top)
1004 } else if cursor_position.y > (bounds.y + 2.0 * bounds.height / 3.0) {
1005 Region::Edge(Edge::Bottom)
1006 } else {
1007 Region::Center
1008 };
1009
1010 Some(region)
1011}
1012
1013fn click_pane<'a, Message, T>(
1014 action: &mut state::Action,
1015 layout: Layout<'_>,
1016 cursor_position: Point,
1017 shell: &mut Shell<'_, Message>,
1018 contents: impl Iterator<Item = (Pane, T)>,
1019 on_click: &Option<Box<dyn Fn(Pane) -> Message + 'a>>,
1020 on_drag: &Option<Box<dyn Fn(DragEvent) -> Message + 'a>>,
1021) where
1022 T: Draggable,
1023{
1024 let mut clicked_region = contents
1025 .zip(layout.children())
1026 .filter(|(_, layout)| layout.bounds().contains(cursor_position));
1027
1028 if let Some(((pane, content), layout)) = clicked_region.next() {
1029 if let Some(on_click) = &on_click {
1030 shell.publish(on_click(pane));
1031 }
1032
1033 if let Some(on_drag) = &on_drag
1034 && content.can_be_dragged_at(layout, cursor_position)
1035 {
1036 *action = state::Action::Dragging {
1037 pane,
1038 origin: cursor_position,
1039 };
1040
1041 shell.publish(on_drag(DragEvent::Picked { pane }));
1042 }
1043 }
1044}
1045
1046fn in_edge(layout: Layout<'_>, cursor: Point) -> Option<Edge> {
1047 let bounds = layout.bounds();
1048
1049 let height_thickness = bounds.height / THICKNESS_RATIO;
1050 let width_thickness = bounds.width / THICKNESS_RATIO;
1051 let thickness = height_thickness.min(width_thickness);
1052
1053 if cursor.x > bounds.x && cursor.x < bounds.x + thickness {
1054 Some(Edge::Left)
1055 } else if cursor.x > bounds.x + bounds.width - thickness && cursor.x < bounds.x + bounds.width {
1056 Some(Edge::Right)
1057 } else if cursor.y > bounds.y && cursor.y < bounds.y + thickness {
1058 Some(Edge::Top)
1059 } else if cursor.y > bounds.y + bounds.height - thickness && cursor.y < bounds.y + bounds.height
1060 {
1061 Some(Edge::Bottom)
1062 } else {
1063 None
1064 }
1065}
1066
1067fn edge_bounds(layout: Layout<'_>, edge: Edge) -> Rectangle {
1068 let bounds = layout.bounds();
1069
1070 let height_thickness = bounds.height / THICKNESS_RATIO;
1071 let width_thickness = bounds.width / THICKNESS_RATIO;
1072 let thickness = height_thickness.min(width_thickness);
1073
1074 match edge {
1075 Edge::Top => Rectangle {
1076 height: thickness,
1077 ..bounds
1078 },
1079 Edge::Left => Rectangle {
1080 width: thickness,
1081 ..bounds
1082 },
1083 Edge::Right => Rectangle {
1084 x: bounds.x + bounds.width - thickness,
1085 width: thickness,
1086 ..bounds
1087 },
1088 Edge::Bottom => Rectangle {
1089 y: bounds.y + bounds.height - thickness,
1090 height: thickness,
1091 ..bounds
1092 },
1093 }
1094}
1095
1096fn layout_region_bounds(layout: Layout<'_>, region: Region) -> Rectangle {
1097 let bounds = layout.bounds();
1098
1099 match region {
1100 Region::Center => bounds,
1101 Region::Edge(edge) => match edge {
1102 Edge::Top => Rectangle {
1103 height: bounds.height / 2.0,
1104 ..bounds
1105 },
1106 Edge::Left => Rectangle {
1107 width: bounds.width / 2.0,
1108 ..bounds
1109 },
1110 Edge::Right => Rectangle {
1111 x: bounds.x + bounds.width / 2.0,
1112 width: bounds.width / 2.0,
1113 ..bounds
1114 },
1115 Edge::Bottom => Rectangle {
1116 y: bounds.y + bounds.height / 2.0,
1117 height: bounds.height / 2.0,
1118 ..bounds
1119 },
1120 },
1121 }
1122}
1123
1124#[derive(Debug, Clone, Copy)]
1126pub enum DragEvent {
1127 Picked {
1129 pane: Pane,
1131 },
1132
1133 Dropped {
1135 pane: Pane,
1137
1138 target: Target,
1140 },
1141
1142 Canceled {
1145 pane: Pane,
1147 },
1148}
1149
1150#[derive(Debug, Clone, Copy)]
1152pub enum Target {
1153 Edge(Edge),
1155 Pane(Pane, Region),
1157}
1158
1159#[derive(Debug, Clone, Copy, Default)]
1161pub enum Region {
1162 #[default]
1164 Center,
1165 Edge(Edge),
1167}
1168
1169#[derive(Debug, Clone, Copy)]
1171pub enum Edge {
1172 Top,
1174 Left,
1176 Right,
1178 Bottom,
1180}
1181
1182#[derive(Debug, Clone, Copy)]
1184pub struct ResizeEvent {
1185 pub split: Split,
1187
1188 pub ratio: f32,
1193}
1194
1195fn hovered_split<'a>(
1199 mut splits: impl Iterator<Item = (&'a Split, &'a (Axis, Rectangle, f32))>,
1200 spacing: f32,
1201 cursor_position: Point,
1202) -> Option<(Split, Axis, Rectangle)> {
1203 splits.find_map(|(split, (axis, region, ratio))| {
1204 let bounds = axis.split_line_bounds(*region, *ratio, spacing);
1205
1206 if bounds.contains(cursor_position) {
1207 Some((*split, *axis, bounds))
1208 } else {
1209 None
1210 }
1211 })
1212}
1213
1214#[derive(Debug, Clone, Copy, PartialEq)]
1216pub struct Style {
1217 pub hovered_region: Highlight,
1219 pub picked_split: Line,
1221 pub hovered_split: Line,
1223}
1224
1225#[derive(Debug, Clone, Copy, PartialEq)]
1227pub struct Highlight {
1228 pub background: Background,
1230 pub border: Border,
1232}
1233
1234#[derive(Debug, Clone, Copy, PartialEq)]
1238pub struct Line {
1239 pub color: Color,
1241 pub width: f32,
1243}
1244
1245pub trait Catalog: container::Catalog {
1247 type Class<'a>;
1249
1250 fn default<'a>() -> <Self as Catalog>::Class<'a>;
1252
1253 fn style(&self, class: &<Self as Catalog>::Class<'_>) -> Style;
1255}
1256
1257pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
1261
1262impl Catalog for Theme {
1263 type Class<'a> = StyleFn<'a, Self>;
1264
1265 fn default<'a>() -> StyleFn<'a, Self> {
1266 Box::new(default)
1267 }
1268
1269 fn style(&self, class: &StyleFn<'_, Self>) -> Style {
1270 class(self)
1271 }
1272}
1273
1274pub fn default(theme: &Theme) -> Style {
1276 let palette = theme.palette();
1277
1278 Style {
1279 hovered_region: Highlight {
1280 background: Background::Color(Color {
1281 a: 0.5,
1282 ..palette.primary.base.color
1283 }),
1284 border: Border {
1285 width: 2.0,
1286 color: palette.primary.strong.color,
1287 radius: 0.0.into(),
1288 },
1289 },
1290 hovered_split: Line {
1291 color: palette.primary.base.color,
1292 width: 2.0,
1293 },
1294 picked_split: Line {
1295 color: palette.primary.strong.color,
1296 width: 2.0,
1297 },
1298 }
1299}