1mod axis;
57mod configuration;
58mod content;
59mod controls;
60mod direction;
61mod draggable;
62mod node;
63mod pane;
64mod split;
65mod title_bar;
66
67pub mod state;
68
69pub use axis::Axis;
70pub use configuration::Configuration;
71pub use content::Content;
72pub use controls::Controls;
73pub use direction::Direction;
74pub use draggable::Draggable;
75pub use node::Node;
76pub use pane::Pane;
77pub use split::Split;
78pub use state::State;
79pub use title_bar::TitleBar;
80
81use crate::container;
82use crate::core::layout;
83use crate::core::mouse;
84use crate::core::overlay::{self, Group};
85use crate::core::renderer;
86use crate::core::touch;
87use crate::core::widget;
88use crate::core::widget::tree::{self, Tree};
89use crate::core::window;
90use crate::core::{
91 self, Background, Border, Clipboard, Color, Element, Event, Layout, Length, Pixels, Point,
92 Rectangle, 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 clipboard: &mut dyn Clipboard,
472 shell: &mut Shell<'_, Message>,
473 viewport: &Rectangle,
474 ) {
475 let Memory { action, .. } = tree.state.downcast_mut();
476 let node = self.internal.layout();
477
478 let on_drag = if self.drag_enabled() {
479 &self.on_drag
480 } else {
481 &None
482 };
483
484 let picked_pane = action.picked_pane().map(|(pane, _)| pane);
485
486 for (((pane, content), tree), layout) in self
487 .panes
488 .iter()
489 .copied()
490 .zip(&mut self.contents)
491 .zip(&mut tree.children)
492 .zip(layout.children())
493 .filter(|(((pane, _), _), _)| {
494 self.internal
495 .maximized()
496 .is_none_or(|maximized| *pane == maximized)
497 })
498 {
499 let is_picked = picked_pane == Some(pane);
500
501 content.update(
502 tree, event, layout, cursor, renderer, clipboard, shell, viewport, is_picked,
503 );
504 }
505
506 match event {
507 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
508 | Event::Touch(touch::Event::FingerPressed { .. }) => {
509 let bounds = layout.bounds();
510
511 if let Some(cursor_position) = cursor.position_over(bounds) {
512 shell.capture_event();
513
514 match &self.on_resize {
515 Some((leeway, _)) => {
516 let relative_cursor = Point::new(
517 cursor_position.x - bounds.x,
518 cursor_position.y - bounds.y,
519 );
520
521 let splits =
522 node.split_regions(self.spacing, self.min_size, bounds.size());
523
524 let clicked_split = hovered_split(
525 splits.iter(),
526 self.spacing + leeway,
527 relative_cursor,
528 );
529
530 if let Some((split, axis, _)) = clicked_split {
531 if action.picked_pane().is_none() {
532 *action = state::Action::Resizing { split, axis };
533 }
534 } else {
535 click_pane(
536 action,
537 layout,
538 cursor_position,
539 shell,
540 self.panes.iter().copied().zip(&self.contents),
541 &self.on_click,
542 on_drag,
543 );
544 }
545 }
546 None => {
547 click_pane(
548 action,
549 layout,
550 cursor_position,
551 shell,
552 self.panes.iter().copied().zip(&self.contents),
553 &self.on_click,
554 on_drag,
555 );
556 }
557 }
558 }
559 }
560 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
561 | Event::Touch(touch::Event::FingerLifted { .. })
562 | Event::Touch(touch::Event::FingerLost { .. }) => {
563 if let Some((pane, origin)) = action.picked_pane()
564 && let Some(on_drag) = on_drag
565 && let Some(cursor_position) = cursor.position()
566 {
567 if cursor_position.distance(origin) > DRAG_DEADBAND_DISTANCE {
568 let event = if let Some(edge) = in_edge(layout, cursor_position) {
569 DragEvent::Dropped {
570 pane,
571 target: Target::Edge(edge),
572 }
573 } else {
574 let dropped_region = self
575 .panes
576 .iter()
577 .copied()
578 .zip(&self.contents)
579 .zip(layout.children())
580 .find_map(|(target, layout)| {
581 layout_region(layout, cursor_position)
582 .map(|region| (target, region))
583 });
584
585 match dropped_region {
586 Some(((target, _), region)) if pane != target => {
587 DragEvent::Dropped {
588 pane,
589 target: Target::Pane(target, region),
590 }
591 }
592 _ => DragEvent::Canceled { pane },
593 }
594 };
595
596 shell.publish(on_drag(event));
597 } else {
598 shell.publish(on_drag(DragEvent::Canceled { pane }));
599 }
600 }
601
602 *action = state::Action::Idle;
603 }
604 Event::Mouse(mouse::Event::CursorMoved { .. })
605 | Event::Touch(touch::Event::FingerMoved { .. }) => {
606 if let Some((_, on_resize)) = &self.on_resize {
607 if let Some((split, _)) = action.picked_split() {
608 let bounds = layout.bounds();
609
610 let splits = node.split_regions(self.spacing, self.min_size, bounds.size());
611
612 if let Some((axis, rectangle, _)) = splits.get(&split)
613 && let Some(cursor_position) = cursor.position()
614 {
615 let ratio = match axis {
616 Axis::Horizontal => {
617 let position = cursor_position.y - bounds.y - rectangle.y;
618
619 (position / rectangle.height).clamp(0.0, 1.0)
620 }
621 Axis::Vertical => {
622 let position = cursor_position.x - bounds.x - rectangle.x;
623
624 (position / rectangle.width).clamp(0.0, 1.0)
625 }
626 };
627
628 shell.publish(on_resize(ResizeEvent { split, ratio }));
629
630 shell.capture_event();
631 }
632 } else if action.picked_pane().is_some() {
633 shell.request_redraw();
634 }
635 }
636 }
637 _ => {}
638 }
639
640 if shell.redraw_request() != window::RedrawRequest::NextFrame {
641 let interaction = self
642 .grid_interaction(action, layout, cursor)
643 .or_else(|| {
644 self.panes
645 .iter()
646 .zip(&self.contents)
647 .zip(layout.children())
648 .filter(|((pane, _content), _layout)| {
649 self.internal
650 .maximized()
651 .is_none_or(|maximized| **pane == maximized)
652 })
653 .find_map(|((_pane, content), layout)| {
654 content.grid_interaction(layout, cursor, on_drag.is_some())
655 })
656 })
657 .unwrap_or(mouse::Interaction::None);
658
659 if let Event::Window(window::Event::RedrawRequested(_now)) = event {
660 self.last_mouse_interaction = Some(interaction);
661 } else if self
662 .last_mouse_interaction
663 .is_some_and(|last_mouse_interaction| last_mouse_interaction != interaction)
664 {
665 shell.request_redraw();
666 }
667 }
668 }
669
670 fn mouse_interaction(
671 &self,
672 tree: &Tree,
673 layout: Layout<'_>,
674 cursor: mouse::Cursor,
675 viewport: &Rectangle,
676 renderer: &Renderer,
677 ) -> mouse::Interaction {
678 let Memory { action, .. } = tree.state.downcast_ref();
679
680 if let Some(grid_interaction) = self.grid_interaction(action, layout, cursor) {
681 return grid_interaction;
682 }
683
684 self.panes
685 .iter()
686 .copied()
687 .zip(&self.contents)
688 .zip(&tree.children)
689 .zip(layout.children())
690 .filter(|(((pane, _), _), _)| {
691 self.internal
692 .maximized()
693 .is_none_or(|maximized| *pane == maximized)
694 })
695 .map(|(((_, content), tree), layout)| {
696 content.mouse_interaction(
697 tree,
698 layout,
699 cursor,
700 viewport,
701 renderer,
702 self.drag_enabled(),
703 )
704 })
705 .max()
706 .unwrap_or_default()
707 }
708
709 fn draw(
710 &self,
711 tree: &Tree,
712 renderer: &mut Renderer,
713 theme: &Theme,
714 defaults: &renderer::Style,
715 layout: Layout<'_>,
716 cursor: mouse::Cursor,
717 viewport: &Rectangle,
718 ) {
719 let Memory { action, .. } = tree.state.downcast_ref();
720 let node = self.internal.layout();
721 let resize_leeway = self.on_resize.as_ref().map(|(leeway, _)| *leeway);
722
723 let picked_pane = action.picked_pane().filter(|(_, origin)| {
724 cursor
725 .position()
726 .map(|position| position.distance(*origin))
727 .unwrap_or_default()
728 > DRAG_DEADBAND_DISTANCE
729 });
730
731 let picked_split = action
732 .picked_split()
733 .and_then(|(split, axis)| {
734 let bounds = layout.bounds();
735
736 let splits = node.split_regions(self.spacing, self.min_size, bounds.size());
737
738 let (_axis, region, ratio) = splits.get(&split)?;
739
740 let region = axis.split_line_bounds(*region, *ratio, self.spacing);
741
742 Some((axis, region + Vector::new(bounds.x, bounds.y), true))
743 })
744 .or_else(|| match resize_leeway {
745 Some(leeway) => {
746 let cursor_position = cursor.position()?;
747 let bounds = layout.bounds();
748
749 let relative_cursor =
750 Point::new(cursor_position.x - bounds.x, cursor_position.y - bounds.y);
751
752 let splits = node.split_regions(self.spacing, self.min_size, bounds.size());
753
754 let (_split, axis, region) =
755 hovered_split(splits.iter(), self.spacing + leeway, relative_cursor)?;
756
757 Some((axis, region + Vector::new(bounds.x, bounds.y), false))
758 }
759 None => None,
760 });
761
762 let pane_cursor = if picked_pane.is_some() {
763 mouse::Cursor::Unavailable
764 } else {
765 cursor
766 };
767
768 let mut render_picked_pane = None;
769
770 let pane_in_edge = if picked_pane.is_some() {
771 cursor
772 .position()
773 .and_then(|cursor_position| in_edge(layout, cursor_position))
774 } else {
775 None
776 };
777
778 let style = Catalog::style(theme, &self.class);
779
780 for (((id, content), tree), pane_layout) in self
781 .panes
782 .iter()
783 .copied()
784 .zip(&self.contents)
785 .zip(&tree.children)
786 .zip(layout.children())
787 .filter(|(((pane, _), _), _)| {
788 self.internal
789 .maximized()
790 .is_none_or(|maximized| maximized == *pane)
791 })
792 {
793 match picked_pane {
794 Some((dragging, origin)) if id == dragging => {
795 render_picked_pane = Some(((content, tree), origin, pane_layout));
796 }
797 Some((dragging, _)) if id != dragging => {
798 content.draw(
799 tree,
800 renderer,
801 theme,
802 defaults,
803 pane_layout,
804 pane_cursor,
805 viewport,
806 );
807
808 if picked_pane.is_some()
809 && pane_in_edge.is_none()
810 && let Some(region) = cursor
811 .position()
812 .and_then(|cursor_position| layout_region(pane_layout, cursor_position))
813 {
814 let bounds = layout_region_bounds(pane_layout, region);
815
816 renderer.fill_quad(
817 renderer::Quad {
818 bounds,
819 border: style.hovered_region.border,
820 ..renderer::Quad::default()
821 },
822 style.hovered_region.background,
823 );
824 }
825 }
826 _ => {
827 content.draw(
828 tree,
829 renderer,
830 theme,
831 defaults,
832 pane_layout,
833 pane_cursor,
834 viewport,
835 );
836 }
837 }
838 }
839
840 if let Some(edge) = pane_in_edge {
841 let bounds = edge_bounds(layout, edge);
842
843 renderer.fill_quad(
844 renderer::Quad {
845 bounds,
846 border: style.hovered_region.border,
847 ..renderer::Quad::default()
848 },
849 style.hovered_region.background,
850 );
851 }
852
853 if let Some(((content, tree), origin, layout)) = render_picked_pane
855 && let Some(cursor_position) = cursor.position()
856 {
857 let bounds = layout.bounds();
858
859 let translation = cursor_position - Point::new(origin.x, origin.y);
860
861 renderer.with_translation(translation, |renderer| {
862 renderer.with_layer(bounds, |renderer| {
863 content.draw(
864 tree,
865 renderer,
866 theme,
867 defaults,
868 layout,
869 pane_cursor,
870 viewport,
871 );
872 });
873 });
874 }
875
876 if picked_pane.is_none()
877 && let Some((axis, split_region, is_picked)) = picked_split
878 {
879 let highlight = if is_picked {
880 style.picked_split
881 } else {
882 style.hovered_split
883 };
884
885 renderer.fill_quad(
886 renderer::Quad {
887 bounds: match axis {
888 Axis::Horizontal => Rectangle {
889 x: split_region.x,
890 y: (split_region.y + (split_region.height - highlight.width) / 2.0)
891 .round(),
892 width: split_region.width,
893 height: highlight.width,
894 },
895 Axis::Vertical => Rectangle {
896 x: (split_region.x + (split_region.width - highlight.width) / 2.0)
897 .round(),
898 y: split_region.y,
899 width: highlight.width,
900 height: split_region.height,
901 },
902 },
903 ..renderer::Quad::default()
904 },
905 highlight.color,
906 );
907 }
908 }
909
910 fn overlay<'b>(
911 &'b mut self,
912 tree: &'b mut Tree,
913 layout: Layout<'b>,
914 renderer: &Renderer,
915 viewport: &Rectangle,
916 translation: Vector,
917 ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
918 let children = self
919 .panes
920 .iter()
921 .copied()
922 .zip(&mut self.contents)
923 .zip(&mut tree.children)
924 .zip(layout.children())
925 .filter_map(|(((pane, content), state), layout)| {
926 if self
927 .internal
928 .maximized()
929 .is_some_and(|maximized| maximized != pane)
930 {
931 return None;
932 }
933
934 content.overlay(state, layout, renderer, viewport, translation)
935 })
936 .collect::<Vec<_>>();
937
938 (!children.is_empty()).then(|| Group::with_children(children).overlay())
939 }
940}
941
942impl<'a, Message, Theme, Renderer> From<PaneGrid<'a, Message, Theme, Renderer>>
943 for Element<'a, Message, Theme, Renderer>
944where
945 Message: 'a,
946 Theme: Catalog + 'a,
947 Renderer: core::Renderer + 'a,
948{
949 fn from(
950 pane_grid: PaneGrid<'a, Message, Theme, Renderer>,
951 ) -> Element<'a, Message, Theme, Renderer> {
952 Element::new(pane_grid)
953 }
954}
955
956fn layout_region(layout: Layout<'_>, cursor_position: Point) -> Option<Region> {
957 let bounds = layout.bounds();
958
959 if !bounds.contains(cursor_position) {
960 return None;
961 }
962
963 let region = if cursor_position.x < (bounds.x + bounds.width / 3.0) {
964 Region::Edge(Edge::Left)
965 } else if cursor_position.x > (bounds.x + 2.0 * bounds.width / 3.0) {
966 Region::Edge(Edge::Right)
967 } else if cursor_position.y < (bounds.y + bounds.height / 3.0) {
968 Region::Edge(Edge::Top)
969 } else if cursor_position.y > (bounds.y + 2.0 * bounds.height / 3.0) {
970 Region::Edge(Edge::Bottom)
971 } else {
972 Region::Center
973 };
974
975 Some(region)
976}
977
978fn click_pane<'a, Message, T>(
979 action: &mut state::Action,
980 layout: Layout<'_>,
981 cursor_position: Point,
982 shell: &mut Shell<'_, Message>,
983 contents: impl Iterator<Item = (Pane, T)>,
984 on_click: &Option<Box<dyn Fn(Pane) -> Message + 'a>>,
985 on_drag: &Option<Box<dyn Fn(DragEvent) -> Message + 'a>>,
986) where
987 T: Draggable,
988{
989 let mut clicked_region = contents
990 .zip(layout.children())
991 .filter(|(_, layout)| layout.bounds().contains(cursor_position));
992
993 if let Some(((pane, content), layout)) = clicked_region.next() {
994 if let Some(on_click) = &on_click {
995 shell.publish(on_click(pane));
996 }
997
998 if let Some(on_drag) = &on_drag
999 && content.can_be_dragged_at(layout, cursor_position)
1000 {
1001 *action = state::Action::Dragging {
1002 pane,
1003 origin: cursor_position,
1004 };
1005
1006 shell.publish(on_drag(DragEvent::Picked { pane }));
1007 }
1008 }
1009}
1010
1011fn in_edge(layout: Layout<'_>, cursor: Point) -> Option<Edge> {
1012 let bounds = layout.bounds();
1013
1014 let height_thickness = bounds.height / THICKNESS_RATIO;
1015 let width_thickness = bounds.width / THICKNESS_RATIO;
1016 let thickness = height_thickness.min(width_thickness);
1017
1018 if cursor.x > bounds.x && cursor.x < bounds.x + thickness {
1019 Some(Edge::Left)
1020 } else if cursor.x > bounds.x + bounds.width - thickness && cursor.x < bounds.x + bounds.width {
1021 Some(Edge::Right)
1022 } else if cursor.y > bounds.y && cursor.y < bounds.y + thickness {
1023 Some(Edge::Top)
1024 } else if cursor.y > bounds.y + bounds.height - thickness && cursor.y < bounds.y + bounds.height
1025 {
1026 Some(Edge::Bottom)
1027 } else {
1028 None
1029 }
1030}
1031
1032fn edge_bounds(layout: Layout<'_>, edge: Edge) -> Rectangle {
1033 let bounds = layout.bounds();
1034
1035 let height_thickness = bounds.height / THICKNESS_RATIO;
1036 let width_thickness = bounds.width / THICKNESS_RATIO;
1037 let thickness = height_thickness.min(width_thickness);
1038
1039 match edge {
1040 Edge::Top => Rectangle {
1041 height: thickness,
1042 ..bounds
1043 },
1044 Edge::Left => Rectangle {
1045 width: thickness,
1046 ..bounds
1047 },
1048 Edge::Right => Rectangle {
1049 x: bounds.x + bounds.width - thickness,
1050 width: thickness,
1051 ..bounds
1052 },
1053 Edge::Bottom => Rectangle {
1054 y: bounds.y + bounds.height - thickness,
1055 height: thickness,
1056 ..bounds
1057 },
1058 }
1059}
1060
1061fn layout_region_bounds(layout: Layout<'_>, region: Region) -> Rectangle {
1062 let bounds = layout.bounds();
1063
1064 match region {
1065 Region::Center => bounds,
1066 Region::Edge(edge) => match edge {
1067 Edge::Top => Rectangle {
1068 height: bounds.height / 2.0,
1069 ..bounds
1070 },
1071 Edge::Left => Rectangle {
1072 width: bounds.width / 2.0,
1073 ..bounds
1074 },
1075 Edge::Right => Rectangle {
1076 x: bounds.x + bounds.width / 2.0,
1077 width: bounds.width / 2.0,
1078 ..bounds
1079 },
1080 Edge::Bottom => Rectangle {
1081 y: bounds.y + bounds.height / 2.0,
1082 height: bounds.height / 2.0,
1083 ..bounds
1084 },
1085 },
1086 }
1087}
1088
1089#[derive(Debug, Clone, Copy)]
1091pub enum DragEvent {
1092 Picked {
1094 pane: Pane,
1096 },
1097
1098 Dropped {
1100 pane: Pane,
1102
1103 target: Target,
1105 },
1106
1107 Canceled {
1110 pane: Pane,
1112 },
1113}
1114
1115#[derive(Debug, Clone, Copy)]
1117pub enum Target {
1118 Edge(Edge),
1120 Pane(Pane, Region),
1122}
1123
1124#[derive(Debug, Clone, Copy, Default)]
1126pub enum Region {
1127 #[default]
1129 Center,
1130 Edge(Edge),
1132}
1133
1134#[derive(Debug, Clone, Copy)]
1136pub enum Edge {
1137 Top,
1139 Left,
1141 Right,
1143 Bottom,
1145}
1146
1147#[derive(Debug, Clone, Copy)]
1149pub struct ResizeEvent {
1150 pub split: Split,
1152
1153 pub ratio: f32,
1158}
1159
1160fn hovered_split<'a>(
1164 mut splits: impl Iterator<Item = (&'a Split, &'a (Axis, Rectangle, f32))>,
1165 spacing: f32,
1166 cursor_position: Point,
1167) -> Option<(Split, Axis, Rectangle)> {
1168 splits.find_map(|(split, (axis, region, ratio))| {
1169 let bounds = axis.split_line_bounds(*region, *ratio, spacing);
1170
1171 if bounds.contains(cursor_position) {
1172 Some((*split, *axis, bounds))
1173 } else {
1174 None
1175 }
1176 })
1177}
1178
1179#[derive(Debug, Clone, Copy, PartialEq)]
1181pub struct Style {
1182 pub hovered_region: Highlight,
1184 pub picked_split: Line,
1186 pub hovered_split: Line,
1188}
1189
1190#[derive(Debug, Clone, Copy, PartialEq)]
1192pub struct Highlight {
1193 pub background: Background,
1195 pub border: Border,
1197}
1198
1199#[derive(Debug, Clone, Copy, PartialEq)]
1203pub struct Line {
1204 pub color: Color,
1206 pub width: f32,
1208}
1209
1210pub trait Catalog: container::Catalog {
1212 type Class<'a>;
1214
1215 fn default<'a>() -> <Self as Catalog>::Class<'a>;
1217
1218 fn style(&self, class: &<Self as Catalog>::Class<'_>) -> Style;
1220}
1221
1222pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
1226
1227impl Catalog for Theme {
1228 type Class<'a> = StyleFn<'a, Self>;
1229
1230 fn default<'a>() -> StyleFn<'a, Self> {
1231 Box::new(default)
1232 }
1233
1234 fn style(&self, class: &StyleFn<'_, Self>) -> Style {
1235 class(self)
1236 }
1237}
1238
1239pub fn default(theme: &Theme) -> Style {
1241 let palette = theme.extended_palette();
1242
1243 Style {
1244 hovered_region: Highlight {
1245 background: Background::Color(Color {
1246 a: 0.5,
1247 ..palette.primary.base.color
1248 }),
1249 border: Border {
1250 width: 2.0,
1251 color: palette.primary.strong.color,
1252 radius: 0.0.into(),
1253 },
1254 },
1255 hovered_split: Line {
1256 color: palette.primary.base.color,
1257 width: 2.0,
1258 },
1259 picked_split: Line {
1260 color: palette.primary.strong.color,
1261 width: 2.0,
1262 },
1263 }
1264}