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;
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 ) -> Vec<overlay::Element<'b, Message, Theme, Renderer>> {
883 let state = tree.state.downcast_ref::<Memory>();
884 let picked_pane = state.action.picked_pane();
885
886 self.panes
887 .iter()
888 .copied()
889 .zip(&mut self.contents)
890 .zip(&mut tree.children)
891 .zip(layout.children())
892 .flat_map(|(((pane, content), tree), layout)| {
893 if self
894 .internal
895 .maximized()
896 .is_some_and(|maximized| maximized != pane)
897 {
898 return Vec::new();
899 }
900
901 if let Some((picked_pane, origin)) = picked_pane
902 && picked_pane == pane
903 {
904 return vec![overlay::Element::new(Box::new(PickedPane {
905 origin,
906 content,
907 tree,
908 layout,
909 }))];
910 }
911
912 content.overlay(tree, layout, renderer, viewport, translation)
913 })
914 .collect()
915 }
916}
917
918struct PickedPane<'a, 'b, Message, Theme, Renderer>
919where
920 Theme: container::Catalog,
921 Renderer: core::Renderer,
922{
923 content: &'a Content<'b, Message, Theme, Renderer>,
924 origin: Point,
925 tree: &'a mut Tree,
926 layout: Layout<'a>,
927}
928
929impl<'a, 'b, Message, Theme, Renderer> core::Overlay<Message, Theme, Renderer>
930 for PickedPane<'a, 'b, Message, Theme, Renderer>
931where
932 Theme: container::Catalog,
933 Renderer: core::Renderer,
934{
935 fn layout(&mut self, _renderer: &Renderer, _bounds: Size) -> layout::Node {
936 layout::Node::new(self.layout.bounds().size()).move_to(self.origin)
938 }
939
940 fn draw(
941 &self,
942 renderer: &mut Renderer,
943 theme: &Theme,
944 style: &renderer::Style,
945 _layout: Layout<'_>,
946 cursor: mouse::Cursor,
947 ) {
948 let cursor_position = cursor.position().unwrap_or_default();
949
950 let translation = if is_dragging(self.origin, cursor_position) {
951 cursor_position - self.origin
952 } else {
953 Vector::ZERO
954 };
955
956 renderer.with_translation(translation, |renderer| {
957 self.content.draw(
958 self.tree,
959 renderer,
960 theme,
961 style,
962 self.layout,
963 mouse::Cursor::Unavailable,
964 &Rectangle::INFINITE,
965 );
966 });
967 }
968}
969
970fn is_dragging(origin: Point, cursor: Point) -> bool {
971 cursor.distance(origin) > DRAG_DEADBAND_DISTANCE
972}
973
974impl<'a, Message, Theme, Renderer> From<PaneGrid<'a, Message, Theme, Renderer>>
975 for Element<'a, Message, Theme, Renderer>
976where
977 Message: 'a,
978 Theme: Catalog + 'a,
979 Renderer: core::Renderer + 'a,
980{
981 fn from(
982 pane_grid: PaneGrid<'a, Message, Theme, Renderer>,
983 ) -> Element<'a, Message, Theme, Renderer> {
984 Element::new(pane_grid)
985 }
986}
987
988fn layout_region(layout: Layout<'_>, cursor_position: Point) -> Option<Region> {
989 let bounds = layout.bounds();
990
991 if !bounds.contains(cursor_position) {
992 return None;
993 }
994
995 let region = if cursor_position.x < (bounds.x + bounds.width / 3.0) {
996 Region::Edge(Edge::Left)
997 } else if cursor_position.x > (bounds.x + 2.0 * bounds.width / 3.0) {
998 Region::Edge(Edge::Right)
999 } else if cursor_position.y < (bounds.y + bounds.height / 3.0) {
1000 Region::Edge(Edge::Top)
1001 } else if cursor_position.y > (bounds.y + 2.0 * bounds.height / 3.0) {
1002 Region::Edge(Edge::Bottom)
1003 } else {
1004 Region::Center
1005 };
1006
1007 Some(region)
1008}
1009
1010fn click_pane<'a, Message, T>(
1011 action: &mut state::Action,
1012 layout: Layout<'_>,
1013 cursor_position: Point,
1014 shell: &mut Shell<'_, Message>,
1015 contents: impl Iterator<Item = (Pane, T)>,
1016 on_click: &Option<Box<dyn Fn(Pane) -> Message + 'a>>,
1017 on_drag: &Option<Box<dyn Fn(DragEvent) -> Message + 'a>>,
1018) where
1019 T: Draggable,
1020{
1021 let mut clicked_region = contents
1022 .zip(layout.children())
1023 .filter(|(_, layout)| layout.bounds().contains(cursor_position));
1024
1025 if let Some(((pane, content), layout)) = clicked_region.next() {
1026 if let Some(on_click) = &on_click {
1027 shell.publish(on_click(pane));
1028 }
1029
1030 if let Some(on_drag) = &on_drag
1031 && content.can_be_dragged_at(layout, cursor_position)
1032 {
1033 *action = state::Action::Dragging {
1034 pane,
1035 origin: cursor_position,
1036 };
1037
1038 shell.publish(on_drag(DragEvent::Picked { pane }));
1039 }
1040 }
1041}
1042
1043fn in_edge(layout: Layout<'_>, cursor: Point) -> Option<Edge> {
1044 let bounds = layout.bounds();
1045
1046 let height_thickness = bounds.height / THICKNESS_RATIO;
1047 let width_thickness = bounds.width / THICKNESS_RATIO;
1048 let thickness = height_thickness.min(width_thickness);
1049
1050 if cursor.x > bounds.x && cursor.x < bounds.x + thickness {
1051 Some(Edge::Left)
1052 } else if cursor.x > bounds.x + bounds.width - thickness && cursor.x < bounds.x + bounds.width {
1053 Some(Edge::Right)
1054 } else if cursor.y > bounds.y && cursor.y < bounds.y + thickness {
1055 Some(Edge::Top)
1056 } else if cursor.y > bounds.y + bounds.height - thickness && cursor.y < bounds.y + bounds.height
1057 {
1058 Some(Edge::Bottom)
1059 } else {
1060 None
1061 }
1062}
1063
1064fn edge_bounds(layout: Layout<'_>, edge: Edge) -> Rectangle {
1065 let bounds = layout.bounds();
1066
1067 let height_thickness = bounds.height / THICKNESS_RATIO;
1068 let width_thickness = bounds.width / THICKNESS_RATIO;
1069 let thickness = height_thickness.min(width_thickness);
1070
1071 match edge {
1072 Edge::Top => Rectangle {
1073 height: thickness,
1074 ..bounds
1075 },
1076 Edge::Left => Rectangle {
1077 width: thickness,
1078 ..bounds
1079 },
1080 Edge::Right => Rectangle {
1081 x: bounds.x + bounds.width - thickness,
1082 width: thickness,
1083 ..bounds
1084 },
1085 Edge::Bottom => Rectangle {
1086 y: bounds.y + bounds.height - thickness,
1087 height: thickness,
1088 ..bounds
1089 },
1090 }
1091}
1092
1093fn layout_region_bounds(layout: Layout<'_>, region: Region) -> Rectangle {
1094 let bounds = layout.bounds();
1095
1096 match region {
1097 Region::Center => bounds,
1098 Region::Edge(edge) => match edge {
1099 Edge::Top => Rectangle {
1100 height: bounds.height / 2.0,
1101 ..bounds
1102 },
1103 Edge::Left => Rectangle {
1104 width: bounds.width / 2.0,
1105 ..bounds
1106 },
1107 Edge::Right => Rectangle {
1108 x: bounds.x + bounds.width / 2.0,
1109 width: bounds.width / 2.0,
1110 ..bounds
1111 },
1112 Edge::Bottom => Rectangle {
1113 y: bounds.y + bounds.height / 2.0,
1114 height: bounds.height / 2.0,
1115 ..bounds
1116 },
1117 },
1118 }
1119}
1120
1121#[derive(Debug, Clone, Copy)]
1123pub enum DragEvent {
1124 Picked {
1126 pane: Pane,
1128 },
1129
1130 Dropped {
1132 pane: Pane,
1134
1135 target: Target,
1137 },
1138
1139 Canceled {
1142 pane: Pane,
1144 },
1145}
1146
1147#[derive(Debug, Clone, Copy)]
1149pub enum Target {
1150 Edge(Edge),
1152 Pane(Pane, Region),
1154}
1155
1156#[derive(Debug, Clone, Copy, Default)]
1158pub enum Region {
1159 #[default]
1161 Center,
1162 Edge(Edge),
1164}
1165
1166#[derive(Debug, Clone, Copy)]
1168pub enum Edge {
1169 Top,
1171 Left,
1173 Right,
1175 Bottom,
1177}
1178
1179#[derive(Debug, Clone, Copy)]
1181pub struct ResizeEvent {
1182 pub split: Split,
1184
1185 pub ratio: f32,
1190}
1191
1192fn hovered_split<'a>(
1196 mut splits: impl Iterator<Item = (&'a Split, &'a (Axis, Rectangle, f32))>,
1197 spacing: f32,
1198 cursor_position: Point,
1199) -> Option<(Split, Axis, Rectangle)> {
1200 splits.find_map(|(split, (axis, region, ratio))| {
1201 let bounds = axis.split_line_bounds(*region, *ratio, spacing);
1202
1203 if bounds.contains(cursor_position) {
1204 Some((*split, *axis, bounds))
1205 } else {
1206 None
1207 }
1208 })
1209}
1210
1211#[derive(Debug, Clone, Copy, PartialEq)]
1213pub struct Style {
1214 pub hovered_region: Highlight,
1216 pub picked_split: Line,
1218 pub hovered_split: Line,
1220}
1221
1222#[derive(Debug, Clone, Copy, PartialEq)]
1224pub struct Highlight {
1225 pub background: Background,
1227 pub border: Border,
1229}
1230
1231#[derive(Debug, Clone, Copy, PartialEq)]
1235pub struct Line {
1236 pub color: Color,
1238 pub width: f32,
1240}
1241
1242pub trait Catalog: container::Catalog {
1244 type Class<'a>;
1246
1247 fn default<'a>() -> <Self as Catalog>::Class<'a>;
1249
1250 fn style(&self, class: &<Self as Catalog>::Class<'_>) -> Style;
1252}
1253
1254pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
1258
1259impl Catalog for Theme {
1260 type Class<'a> = StyleFn<'a, Self>;
1261
1262 fn default<'a>() -> StyleFn<'a, Self> {
1263 Box::new(default)
1264 }
1265
1266 fn style(&self, class: &StyleFn<'_, Self>) -> Style {
1267 class(self)
1268 }
1269}
1270
1271pub fn default(theme: &Theme) -> Style {
1273 let palette = theme.palette();
1274
1275 Style {
1276 hovered_region: Highlight {
1277 background: Background::Color(Color {
1278 a: 0.5,
1279 ..palette.primary.base.color
1280 }),
1281 border: Border {
1282 width: 2.0,
1283 color: palette.primary.strong.color,
1284 radius: 0.0.into(),
1285 },
1286 },
1287 hovered_split: Line {
1288 color: palette.primary.base.color,
1289 width: 2.0,
1290 },
1291 picked_split: Line {
1292 color: palette.primary.strong.color,
1293 width: 2.0,
1294 },
1295 }
1296}