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().filter(|(_, origin)| {
723 cursor
724 .position()
725 .map(|position| position.distance(*origin))
726 .unwrap_or_default()
727 > DRAG_DEADBAND_DISTANCE
728 });
729
730 let picked_split = action
731 .picked_split()
732 .and_then(|(split, axis)| {
733 let bounds = layout.bounds();
734
735 let splits = node.split_regions(self.spacing, self.min_size, bounds.size());
736
737 let (_axis, region, ratio) = splits.get(&split)?;
738
739 let region = axis.split_line_bounds(*region, *ratio, self.spacing);
740
741 Some((axis, region + Vector::new(bounds.x, bounds.y), true))
742 })
743 .or_else(|| match resize_leeway {
744 Some(leeway) => {
745 let cursor_position = cursor.position()?;
746 let bounds = layout.bounds();
747
748 let relative_cursor =
749 Point::new(cursor_position.x - bounds.x, cursor_position.y - bounds.y);
750
751 let splits = node.split_regions(self.spacing, self.min_size, bounds.size());
752
753 let (_split, axis, region) =
754 hovered_split(splits.iter(), self.spacing + leeway, relative_cursor)?;
755
756 Some((axis, region + Vector::new(bounds.x, bounds.y), false))
757 }
758 None => None,
759 });
760
761 let pane_cursor = if picked_pane.is_some() {
762 mouse::Cursor::Unavailable
763 } else {
764 cursor
765 };
766
767 let mut render_picked_pane = None;
768
769 let pane_in_edge = if picked_pane.is_some() {
770 cursor
771 .position()
772 .and_then(|cursor_position| in_edge(layout, cursor_position))
773 } else {
774 None
775 };
776
777 let style = Catalog::style(theme, &self.class);
778
779 for (((id, content), tree), pane_layout) in self
780 .panes
781 .iter()
782 .copied()
783 .zip(&self.contents)
784 .zip(&tree.children)
785 .zip(layout.children())
786 .filter(|(((pane, _), _), _)| {
787 self.internal
788 .maximized()
789 .is_none_or(|maximized| maximized == *pane)
790 })
791 {
792 match picked_pane {
793 Some((dragging, origin)) if id == dragging => {
794 render_picked_pane = Some(((content, tree), origin, pane_layout));
795 }
796 Some((dragging, _)) if id != dragging => {
797 content.draw(
798 tree,
799 renderer,
800 theme,
801 defaults,
802 pane_layout,
803 pane_cursor,
804 viewport,
805 );
806
807 if picked_pane.is_some()
808 && pane_in_edge.is_none()
809 && let Some(region) = cursor
810 .position()
811 .and_then(|cursor_position| layout_region(pane_layout, cursor_position))
812 {
813 let bounds = layout_region_bounds(pane_layout, region);
814
815 renderer.fill_quad(
816 renderer::Quad {
817 bounds,
818 border: style.hovered_region.border,
819 ..renderer::Quad::default()
820 },
821 style.hovered_region.background,
822 );
823 }
824 }
825 _ => {
826 content.draw(
827 tree,
828 renderer,
829 theme,
830 defaults,
831 pane_layout,
832 pane_cursor,
833 viewport,
834 );
835 }
836 }
837 }
838
839 if let Some(edge) = pane_in_edge {
840 let bounds = edge_bounds(layout, edge);
841
842 renderer.fill_quad(
843 renderer::Quad {
844 bounds,
845 border: style.hovered_region.border,
846 ..renderer::Quad::default()
847 },
848 style.hovered_region.background,
849 );
850 }
851
852 if let Some(((content, tree), origin, layout)) = render_picked_pane
854 && let Some(cursor_position) = cursor.position()
855 {
856 let bounds = layout.bounds();
857
858 let translation = cursor_position - Point::new(origin.x, origin.y);
859
860 renderer.with_translation(translation, |renderer| {
861 renderer.with_layer(bounds, |renderer| {
862 content.draw(
863 tree,
864 renderer,
865 theme,
866 defaults,
867 layout,
868 pane_cursor,
869 viewport,
870 );
871 });
872 });
873 }
874
875 if picked_pane.is_none()
876 && let Some((axis, split_region, is_picked)) = picked_split
877 {
878 let highlight = if is_picked {
879 style.picked_split
880 } else {
881 style.hovered_split
882 };
883
884 renderer.fill_quad(
885 renderer::Quad {
886 bounds: match axis {
887 Axis::Horizontal => Rectangle {
888 x: split_region.x,
889 y: (split_region.y + (split_region.height - highlight.width) / 2.0)
890 .round(),
891 width: split_region.width,
892 height: highlight.width,
893 },
894 Axis::Vertical => Rectangle {
895 x: (split_region.x + (split_region.width - highlight.width) / 2.0)
896 .round(),
897 y: split_region.y,
898 width: highlight.width,
899 height: split_region.height,
900 },
901 },
902 ..renderer::Quad::default()
903 },
904 highlight.color,
905 );
906 }
907 }
908
909 fn overlay<'b>(
910 &'b mut self,
911 tree: &'b mut Tree,
912 layout: Layout<'b>,
913 renderer: &Renderer,
914 viewport: &Rectangle,
915 translation: Vector,
916 ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
917 let children = self
918 .panes
919 .iter()
920 .copied()
921 .zip(&mut self.contents)
922 .zip(&mut tree.children)
923 .zip(layout.children())
924 .filter_map(|(((pane, content), state), layout)| {
925 if self
926 .internal
927 .maximized()
928 .is_some_and(|maximized| maximized != pane)
929 {
930 return None;
931 }
932
933 content.overlay(state, layout, renderer, viewport, translation)
934 })
935 .collect::<Vec<_>>();
936
937 (!children.is_empty()).then(|| Group::with_children(children).overlay())
938 }
939}
940
941impl<'a, Message, Theme, Renderer> From<PaneGrid<'a, Message, Theme, Renderer>>
942 for Element<'a, Message, Theme, Renderer>
943where
944 Message: 'a,
945 Theme: Catalog + 'a,
946 Renderer: core::Renderer + 'a,
947{
948 fn from(
949 pane_grid: PaneGrid<'a, Message, Theme, Renderer>,
950 ) -> Element<'a, Message, Theme, Renderer> {
951 Element::new(pane_grid)
952 }
953}
954
955fn layout_region(layout: Layout<'_>, cursor_position: Point) -> Option<Region> {
956 let bounds = layout.bounds();
957
958 if !bounds.contains(cursor_position) {
959 return None;
960 }
961
962 let region = if cursor_position.x < (bounds.x + bounds.width / 3.0) {
963 Region::Edge(Edge::Left)
964 } else if cursor_position.x > (bounds.x + 2.0 * bounds.width / 3.0) {
965 Region::Edge(Edge::Right)
966 } else if cursor_position.y < (bounds.y + bounds.height / 3.0) {
967 Region::Edge(Edge::Top)
968 } else if cursor_position.y > (bounds.y + 2.0 * bounds.height / 3.0) {
969 Region::Edge(Edge::Bottom)
970 } else {
971 Region::Center
972 };
973
974 Some(region)
975}
976
977fn click_pane<'a, Message, T>(
978 action: &mut state::Action,
979 layout: Layout<'_>,
980 cursor_position: Point,
981 shell: &mut Shell<'_, Message>,
982 contents: impl Iterator<Item = (Pane, T)>,
983 on_click: &Option<Box<dyn Fn(Pane) -> Message + 'a>>,
984 on_drag: &Option<Box<dyn Fn(DragEvent) -> Message + 'a>>,
985) where
986 T: Draggable,
987{
988 let mut clicked_region = contents
989 .zip(layout.children())
990 .filter(|(_, layout)| layout.bounds().contains(cursor_position));
991
992 if let Some(((pane, content), layout)) = clicked_region.next() {
993 if let Some(on_click) = &on_click {
994 shell.publish(on_click(pane));
995 }
996
997 if let Some(on_drag) = &on_drag
998 && content.can_be_dragged_at(layout, cursor_position)
999 {
1000 *action = state::Action::Dragging {
1001 pane,
1002 origin: cursor_position,
1003 };
1004
1005 shell.publish(on_drag(DragEvent::Picked { pane }));
1006 }
1007 }
1008}
1009
1010fn in_edge(layout: Layout<'_>, cursor: Point) -> Option<Edge> {
1011 let bounds = layout.bounds();
1012
1013 let height_thickness = bounds.height / THICKNESS_RATIO;
1014 let width_thickness = bounds.width / THICKNESS_RATIO;
1015 let thickness = height_thickness.min(width_thickness);
1016
1017 if cursor.x > bounds.x && cursor.x < bounds.x + thickness {
1018 Some(Edge::Left)
1019 } else if cursor.x > bounds.x + bounds.width - thickness && cursor.x < bounds.x + bounds.width {
1020 Some(Edge::Right)
1021 } else if cursor.y > bounds.y && cursor.y < bounds.y + thickness {
1022 Some(Edge::Top)
1023 } else if cursor.y > bounds.y + bounds.height - thickness && cursor.y < bounds.y + bounds.height
1024 {
1025 Some(Edge::Bottom)
1026 } else {
1027 None
1028 }
1029}
1030
1031fn edge_bounds(layout: Layout<'_>, edge: Edge) -> Rectangle {
1032 let bounds = layout.bounds();
1033
1034 let height_thickness = bounds.height / THICKNESS_RATIO;
1035 let width_thickness = bounds.width / THICKNESS_RATIO;
1036 let thickness = height_thickness.min(width_thickness);
1037
1038 match edge {
1039 Edge::Top => Rectangle {
1040 height: thickness,
1041 ..bounds
1042 },
1043 Edge::Left => Rectangle {
1044 width: thickness,
1045 ..bounds
1046 },
1047 Edge::Right => Rectangle {
1048 x: bounds.x + bounds.width - thickness,
1049 width: thickness,
1050 ..bounds
1051 },
1052 Edge::Bottom => Rectangle {
1053 y: bounds.y + bounds.height - thickness,
1054 height: thickness,
1055 ..bounds
1056 },
1057 }
1058}
1059
1060fn layout_region_bounds(layout: Layout<'_>, region: Region) -> Rectangle {
1061 let bounds = layout.bounds();
1062
1063 match region {
1064 Region::Center => bounds,
1065 Region::Edge(edge) => match edge {
1066 Edge::Top => Rectangle {
1067 height: bounds.height / 2.0,
1068 ..bounds
1069 },
1070 Edge::Left => Rectangle {
1071 width: bounds.width / 2.0,
1072 ..bounds
1073 },
1074 Edge::Right => Rectangle {
1075 x: bounds.x + bounds.width / 2.0,
1076 width: bounds.width / 2.0,
1077 ..bounds
1078 },
1079 Edge::Bottom => Rectangle {
1080 y: bounds.y + bounds.height / 2.0,
1081 height: bounds.height / 2.0,
1082 ..bounds
1083 },
1084 },
1085 }
1086}
1087
1088#[derive(Debug, Clone, Copy)]
1090pub enum DragEvent {
1091 Picked {
1093 pane: Pane,
1095 },
1096
1097 Dropped {
1099 pane: Pane,
1101
1102 target: Target,
1104 },
1105
1106 Canceled {
1109 pane: Pane,
1111 },
1112}
1113
1114#[derive(Debug, Clone, Copy)]
1116pub enum Target {
1117 Edge(Edge),
1119 Pane(Pane, Region),
1121}
1122
1123#[derive(Debug, Clone, Copy, Default)]
1125pub enum Region {
1126 #[default]
1128 Center,
1129 Edge(Edge),
1131}
1132
1133#[derive(Debug, Clone, Copy)]
1135pub enum Edge {
1136 Top,
1138 Left,
1140 Right,
1142 Bottom,
1144}
1145
1146#[derive(Debug, Clone, Copy)]
1148pub struct ResizeEvent {
1149 pub split: Split,
1151
1152 pub ratio: f32,
1157}
1158
1159fn hovered_split<'a>(
1163 mut splits: impl Iterator<Item = (&'a Split, &'a (Axis, Rectangle, f32))>,
1164 spacing: f32,
1165 cursor_position: Point,
1166) -> Option<(Split, Axis, Rectangle)> {
1167 splits.find_map(|(split, (axis, region, ratio))| {
1168 let bounds = axis.split_line_bounds(*region, *ratio, spacing);
1169
1170 if bounds.contains(cursor_position) {
1171 Some((*split, *axis, bounds))
1172 } else {
1173 None
1174 }
1175 })
1176}
1177
1178#[derive(Debug, Clone, Copy, PartialEq)]
1180pub struct Style {
1181 pub hovered_region: Highlight,
1183 pub picked_split: Line,
1185 pub hovered_split: Line,
1187}
1188
1189#[derive(Debug, Clone, Copy, PartialEq)]
1191pub struct Highlight {
1192 pub background: Background,
1194 pub border: Border,
1196}
1197
1198#[derive(Debug, Clone, Copy, PartialEq)]
1202pub struct Line {
1203 pub color: Color,
1205 pub width: f32,
1207}
1208
1209pub trait Catalog: container::Catalog {
1211 type Class<'a>;
1213
1214 fn default<'a>() -> <Self as Catalog>::Class<'a>;
1216
1217 fn style(&self, class: &<Self as Catalog>::Class<'_>) -> Style;
1219}
1220
1221pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
1225
1226impl Catalog for Theme {
1227 type Class<'a> = StyleFn<'a, Self>;
1228
1229 fn default<'a>() -> StyleFn<'a, Self> {
1230 Box::new(default)
1231 }
1232
1233 fn style(&self, class: &StyleFn<'_, Self>) -> Style {
1234 class(self)
1235 }
1236}
1237
1238pub fn default(theme: &Theme) -> Style {
1240 let palette = theme.extended_palette();
1241
1242 Style {
1243 hovered_region: Highlight {
1244 background: Background::Color(Color {
1245 a: 0.5,
1246 ..palette.primary.base.color
1247 }),
1248 border: Border {
1249 width: 2.0,
1250 color: palette.primary.strong.color,
1251 radius: 0.0.into(),
1252 },
1253 },
1254 hovered_split: Line {
1255 color: palette.primary.base.color,
1256 width: 2.0,
1257 },
1258 picked_split: Line {
1259 color: palette.primary.strong.color,
1260 width: 2.0,
1261 },
1262 }
1263}