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