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