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