iced_widget/
pane_grid.rs

1//! Pane grids let your users split regions of your application and organize layout dynamically.
2//!
3//! ![Pane grid - Iced](https://iced.rs/examples/pane_grid.gif)
4//!
5//! This distribution of space is common in tiling window managers (like
6//! [`awesome`](https://awesomewm.org/), [`i3`](https://i3wm.org/), or even
7//! [`tmux`](https://github.com/tmux/tmux)).
8//!
9//! A [`PaneGrid`] supports:
10//!
11//! * Vertical and horizontal splits
12//! * Tracking of the last active pane
13//! * Mouse-based resizing
14//! * Drag and drop to reorganize panes
15//! * Hotkey support
16//! * Configurable modifier keys
17//! * [`State`] API to perform actions programmatically (`split`, `swap`, `resize`, etc.)
18//!
19//! # Example
20//! ```no_run
21//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
22//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
23//! #
24//! use iced::widget::{pane_grid, text};
25//!
26//! struct State {
27//!     panes: pane_grid::State<Pane>,
28//! }
29//!
30//! enum Pane {
31//!     SomePane,
32//!     AnotherKindOfPane,
33//! }
34//!
35//! enum Message {
36//!     PaneDragged(pane_grid::DragEvent),
37//!     PaneResized(pane_grid::ResizeEvent),
38//! }
39//!
40//! fn view(state: &State) -> Element<'_, Message> {
41//!     pane_grid(&state.panes, |pane, state, is_maximized| {
42//!         pane_grid::Content::new(match state {
43//!             Pane::SomePane => text("This is some pane"),
44//!             Pane::AnotherKindOfPane => text("This is another kind of pane"),
45//!         })
46//!     })
47//!     .on_drag(Message::PaneDragged)
48//!     .on_resize(10, Message::PaneResized)
49//!     .into()
50//! }
51//! ```
52//! The [`pane_grid` example] showcases how to use a [`PaneGrid`] with resizing,
53//! drag and drop, and hotkey support.
54//!
55//! [`pane_grid` example]: https://github.com/iced-rs/iced/tree/0.13/examples/pane_grid
56mod axis;
57mod configuration;
58mod content;
59mod controls;
60mod direction;
61mod draggable;
62mod node;
63mod pane;
64mod split;
65mod title_bar;
66
67pub mod state;
68
69pub use axis::Axis;
70pub use configuration::Configuration;
71pub use content::Content;
72pub use controls::Controls;
73pub use direction::Direction;
74pub use draggable::Draggable;
75pub use node::Node;
76pub use pane::Pane;
77pub use split::Split;
78pub use state::State;
79pub use title_bar::TitleBar;
80
81use crate::container;
82use crate::core::layout;
83use crate::core::mouse;
84use crate::core::overlay::{self, Group};
85use crate::core::renderer;
86use crate::core::touch;
87use crate::core::widget;
88use crate::core::widget::tree::{self, Tree};
89use crate::core::window;
90use crate::core::{
91    self, Background, Border, Clipboard, Color, Element, Event, Layout, Length,
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/// A collection of panes distributed using either vertical or horizontal splits
99/// to completely fill the space available.
100///
101/// ![Pane grid - Iced](https://iced.rs/examples/pane_grid.gif)
102///
103/// This distribution of space is common in tiling window managers (like
104/// [`awesome`](https://awesomewm.org/), [`i3`](https://i3wm.org/), or even
105/// [`tmux`](https://github.com/tmux/tmux)).
106///
107/// A [`PaneGrid`] supports:
108///
109/// * Vertical and horizontal splits
110/// * Tracking of the last active pane
111/// * Mouse-based resizing
112/// * Drag and drop to reorganize panes
113/// * Hotkey support
114/// * Configurable modifier keys
115/// * [`State`] API to perform actions programmatically (`split`, `swap`, `resize`, etc.)
116///
117/// # Example
118/// ```no_run
119/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
120/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
121/// #
122/// use iced::widget::{pane_grid, text};
123///
124/// struct State {
125///     panes: pane_grid::State<Pane>,
126/// }
127///
128/// enum Pane {
129///     SomePane,
130///     AnotherKindOfPane,
131/// }
132///
133/// enum Message {
134///     PaneDragged(pane_grid::DragEvent),
135///     PaneResized(pane_grid::ResizeEvent),
136/// }
137///
138/// fn view(state: &State) -> Element<'_, Message> {
139///     pane_grid(&state.panes, |pane, state, is_maximized| {
140///         pane_grid::Content::new(match state {
141///             Pane::SomePane => text("This is some pane"),
142///             Pane::AnotherKindOfPane => text("This is another kind of pane"),
143///         })
144///     })
145///     .on_drag(Message::PaneDragged)
146///     .on_resize(10, Message::PaneResized)
147///     .into()
148/// }
149/// ```
150#[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    internal: &'a state::Internal,
161    panes: Vec<Pane>,
162    contents: Vec<Content<'a, Message, Theme, Renderer>>,
163    width: Length,
164    height: Length,
165    spacing: f32,
166    on_click: Option<Box<dyn Fn(Pane) -> Message + 'a>>,
167    on_drag: Option<Box<dyn Fn(DragEvent) -> Message + 'a>>,
168    on_resize: Option<(f32, Box<dyn Fn(ResizeEvent) -> Message + 'a>)>,
169    class: <Theme as Catalog>::Class<'a>,
170    last_mouse_interaction: Option<mouse::Interaction>,
171}
172
173impl<'a, Message, Theme, Renderer> PaneGrid<'a, Message, Theme, Renderer>
174where
175    Theme: Catalog,
176    Renderer: core::Renderer,
177{
178    /// Creates a [`PaneGrid`] with the given [`State`] and view function.
179    ///
180    /// The view function will be called to display each [`Pane`] present in the
181    /// [`State`]. [`bool`] is set if the pane is maximized.
182    pub fn new<T>(
183        state: &'a State<T>,
184        view: impl Fn(Pane, &'a T, bool) -> Content<'a, Message, Theme, Renderer>,
185    ) -> Self {
186        let panes = state.panes.keys().copied().collect();
187        let contents = state
188            .panes
189            .iter()
190            .map(|(pane, pane_state)| match state.maximized() {
191                Some(p) if *pane == p => view(*pane, pane_state, true),
192                _ => view(*pane, pane_state, false),
193            })
194            .collect();
195
196        Self {
197            internal: &state.internal,
198            panes,
199            contents,
200            width: Length::Fill,
201            height: Length::Fill,
202            spacing: 0.0,
203            on_click: None,
204            on_drag: None,
205            on_resize: None,
206            class: <Theme as Catalog>::default(),
207            last_mouse_interaction: None,
208        }
209    }
210
211    /// Sets the width of the [`PaneGrid`].
212    pub fn width(mut self, width: impl Into<Length>) -> Self {
213        self.width = width.into();
214        self
215    }
216
217    /// Sets the height of the [`PaneGrid`].
218    pub fn height(mut self, height: impl Into<Length>) -> Self {
219        self.height = height.into();
220        self
221    }
222
223    /// Sets the spacing _between_ the panes of the [`PaneGrid`].
224    pub fn spacing(mut self, amount: impl Into<Pixels>) -> Self {
225        self.spacing = amount.into().0;
226        self
227    }
228
229    /// Sets the message that will be produced when a [`Pane`] of the
230    /// [`PaneGrid`] is clicked.
231    pub fn on_click<F>(mut self, f: F) -> Self
232    where
233        F: 'a + Fn(Pane) -> Message,
234    {
235        self.on_click = Some(Box::new(f));
236        self
237    }
238
239    /// Enables the drag and drop interactions of the [`PaneGrid`], which will
240    /// use the provided function to produce messages.
241    pub fn on_drag<F>(mut self, f: F) -> Self
242    where
243        F: 'a + Fn(DragEvent) -> Message,
244    {
245        if self.internal.maximized().is_none() {
246            self.on_drag = Some(Box::new(f));
247        }
248        self
249    }
250
251    /// Enables the resize interactions of the [`PaneGrid`], which will
252    /// use the provided function to produce messages.
253    ///
254    /// The `leeway` describes the amount of space around a split that can be
255    /// used to grab it.
256    ///
257    /// The grabbable area of a split will have a length of `spacing + leeway`,
258    /// properly centered. In other words, a length of
259    /// `(spacing + leeway) / 2.0` on either side of the split line.
260    pub fn on_resize<F>(mut self, leeway: impl Into<Pixels>, f: F) -> Self
261    where
262        F: 'a + Fn(ResizeEvent) -> Message,
263    {
264        if self.internal.maximized().is_none() {
265            self.on_resize = Some((leeway.into().0, Box::new(f)));
266        }
267        self
268    }
269
270    /// Sets the style of the [`PaneGrid`].
271    #[must_use]
272    pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
273    where
274        <Theme as Catalog>::Class<'a>: From<StyleFn<'a, Theme>>,
275    {
276        self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
277        self
278    }
279
280    /// Sets the style class of the [`PaneGrid`].
281    #[cfg(feature = "advanced")]
282    #[must_use]
283    pub fn class(
284        mut self,
285        class: impl Into<<Theme as Catalog>::Class<'a>>,
286    ) -> Self {
287        self.class = class.into();
288        self
289    }
290
291    fn drag_enabled(&self) -> bool {
292        self.internal
293            .maximized()
294            .is_none()
295            .then(|| self.on_drag.is_some())
296            .unwrap_or_default()
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 =
313            action.picked_split().map(|(_, axis)| axis).or_else(|| {
314                resize_leeway.and_then(|leeway| {
315                    let cursor_position = cursor.position()?;
316                    let bounds = layout.bounds();
317
318                    let splits =
319                        node.split_regions(self.spacing, bounds.size());
320
321                    let relative_cursor = Point::new(
322                        cursor_position.x - bounds.x,
323                        cursor_position.y - bounds.y,
324                    );
325
326                    hovered_split(
327                        splits.iter(),
328                        self.spacing + leeway,
329                        relative_cursor,
330                    )
331                    .map(|(_, axis, _)| axis)
332                })
333            });
334
335        if let Some(resize_axis) = resize_axis {
336            return Some(match resize_axis {
337                Axis::Horizontal => mouse::Interaction::ResizingVertically,
338                Axis::Vertical => mouse::Interaction::ResizingHorizontally,
339            });
340        }
341
342        None
343    }
344}
345
346#[derive(Default)]
347struct Memory {
348    action: state::Action,
349    order: Vec<Pane>,
350}
351
352impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
353    for PaneGrid<'_, Message, Theme, Renderer>
354where
355    Theme: Catalog,
356    Renderer: core::Renderer,
357{
358    fn tag(&self) -> tree::Tag {
359        tree::Tag::of::<Memory>()
360    }
361
362    fn state(&self) -> tree::State {
363        tree::State::new(Memory::default())
364    }
365
366    fn children(&self) -> Vec<Tree> {
367        self.contents.iter().map(Content::state).collect()
368    }
369
370    fn diff(&self, tree: &mut Tree) {
371        let Memory { order, .. } = tree.state.downcast_ref();
372
373        // `Pane` always increments and is iterated by Ord so new
374        // states are always added at the end. We can simply remove
375        // states which no longer exist and `diff_children` will
376        // diff the remaining values in the correct order and
377        // add new states at the end
378
379        let mut i = 0;
380        let mut j = 0;
381        tree.children.retain(|_| {
382            let retain = self.panes.get(i) == order.get(j);
383
384            if retain {
385                i += 1;
386            }
387            j += 1;
388
389            retain
390        });
391
392        tree.diff_children_custom(
393            &self.contents,
394            |state, content| content.diff(state),
395            Content::state,
396        );
397
398        let Memory { order, .. } = tree.state.downcast_mut();
399        order.clone_from(&self.panes);
400    }
401
402    fn size(&self) -> Size<Length> {
403        Size {
404            width: self.width,
405            height: self.height,
406        }
407    }
408
409    fn layout(
410        &self,
411        tree: &mut Tree,
412        renderer: &Renderer,
413        limits: &layout::Limits,
414    ) -> layout::Node {
415        let size = limits.resolve(self.width, self.height, Size::ZERO);
416        let regions = self.internal.layout().pane_regions(self.spacing, size);
417
418        let children = self
419            .panes
420            .iter()
421            .copied()
422            .zip(&self.contents)
423            .zip(tree.children.iter_mut())
424            .filter_map(|((pane, content), tree)| {
425                if self
426                    .internal
427                    .maximized()
428                    .is_some_and(|maximized| maximized != pane)
429                {
430                    return Some(layout::Node::new(Size::ZERO));
431                }
432
433                let region = regions.get(&pane)?;
434                let size = Size::new(region.width, region.height);
435
436                let node = content.layout(
437                    tree,
438                    renderer,
439                    &layout::Limits::new(size, size),
440                );
441
442                Some(node.move_to(Point::new(region.x, region.y)))
443            })
444            .collect();
445
446        layout::Node::with_children(size, children)
447    }
448
449    fn operate(
450        &self,
451        tree: &mut Tree,
452        layout: Layout<'_>,
453        renderer: &Renderer,
454        operation: &mut dyn widget::Operation,
455    ) {
456        operation.container(None, layout.bounds(), &mut |operation| {
457            self.panes
458                .iter()
459                .copied()
460                .zip(&self.contents)
461                .zip(&mut tree.children)
462                .zip(layout.children())
463                .filter(|(((pane, _), _), _)| {
464                    self.internal
465                        .maximized()
466                        .is_none_or(|maximized| *pane == maximized)
467                })
468                .for_each(|(((_, content), state), layout)| {
469                    content.operate(state, layout, renderer, operation);
470                });
471        });
472    }
473
474    fn update(
475        &mut self,
476        tree: &mut Tree,
477        event: &Event,
478        layout: Layout<'_>,
479        cursor: mouse::Cursor,
480        renderer: &Renderer,
481        clipboard: &mut dyn Clipboard,
482        shell: &mut Shell<'_, Message>,
483        viewport: &Rectangle,
484    ) {
485        let Memory { action, .. } = tree.state.downcast_mut();
486        let node = self.internal.layout();
487
488        let on_drag = if self.drag_enabled() {
489            &self.on_drag
490        } else {
491            &None
492        };
493
494        let picked_pane = action.picked_pane().map(|(pane, _)| pane);
495
496        for (((pane, content), tree), layout) in self
497            .panes
498            .iter()
499            .copied()
500            .zip(&mut self.contents)
501            .zip(&mut tree.children)
502            .zip(layout.children())
503            .filter(|(((pane, _), _), _)| {
504                self.internal
505                    .maximized()
506                    .is_none_or(|maximized| *pane == maximized)
507            })
508        {
509            let is_picked = picked_pane == Some(pane);
510
511            content.update(
512                tree, event, layout, cursor, renderer, clipboard, shell,
513                viewport, is_picked,
514            );
515        }
516
517        match event {
518            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
519            | Event::Touch(touch::Event::FingerPressed { .. }) => {
520                let bounds = layout.bounds();
521
522                if let Some(cursor_position) = cursor.position_over(bounds) {
523                    shell.capture_event();
524
525                    match &self.on_resize {
526                        Some((leeway, _)) => {
527                            let relative_cursor = Point::new(
528                                cursor_position.x - bounds.x,
529                                cursor_position.y - bounds.y,
530                            );
531
532                            let splits = node.split_regions(
533                                self.spacing,
534                                Size::new(bounds.width, bounds.height),
535                            );
536
537                            let clicked_split = hovered_split(
538                                splits.iter(),
539                                self.spacing + leeway,
540                                relative_cursor,
541                            );
542
543                            if let Some((split, axis, _)) = clicked_split {
544                                if action.picked_pane().is_none() {
545                                    *action =
546                                        state::Action::Resizing { split, axis };
547                                }
548                            } else {
549                                click_pane(
550                                    action,
551                                    layout,
552                                    cursor_position,
553                                    shell,
554                                    self.panes
555                                        .iter()
556                                        .copied()
557                                        .zip(&self.contents),
558                                    &self.on_click,
559                                    on_drag,
560                                );
561                            }
562                        }
563                        None => {
564                            click_pane(
565                                action,
566                                layout,
567                                cursor_position,
568                                shell,
569                                self.panes.iter().copied().zip(&self.contents),
570                                &self.on_click,
571                                on_drag,
572                            );
573                        }
574                    }
575                }
576            }
577            Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
578            | Event::Touch(touch::Event::FingerLifted { .. })
579            | Event::Touch(touch::Event::FingerLost { .. }) => {
580                if let Some((pane, origin)) = action.picked_pane() {
581                    if let Some(on_drag) = on_drag {
582                        if let Some(cursor_position) = cursor.position() {
583                            if cursor_position.distance(origin)
584                                > DRAG_DEADBAND_DISTANCE
585                            {
586                                let event = if let Some(edge) =
587                                    in_edge(layout, cursor_position)
588                                {
589                                    DragEvent::Dropped {
590                                        pane,
591                                        target: Target::Edge(edge),
592                                    }
593                                } else {
594                                    let dropped_region = self
595                                        .panes
596                                        .iter()
597                                        .copied()
598                                        .zip(&self.contents)
599                                        .zip(layout.children())
600                                        .find_map(|(target, layout)| {
601                                            layout_region(
602                                                layout,
603                                                cursor_position,
604                                            )
605                                            .map(|region| (target, region))
606                                        });
607
608                                    match dropped_region {
609                                        Some(((target, _), region))
610                                            if pane != target =>
611                                        {
612                                            DragEvent::Dropped {
613                                                pane,
614                                                target: Target::Pane(
615                                                    target, region,
616                                                ),
617                                            }
618                                        }
619                                        _ => DragEvent::Canceled { pane },
620                                    }
621                                };
622
623                                shell.publish(on_drag(event));
624                            } else {
625                                shell.publish(on_drag(DragEvent::Canceled {
626                                    pane,
627                                }));
628                            }
629                        }
630                    }
631                }
632
633                *action = state::Action::Idle;
634            }
635            Event::Mouse(mouse::Event::CursorMoved { .. })
636            | Event::Touch(touch::Event::FingerMoved { .. }) => {
637                if let Some((_, on_resize)) = &self.on_resize {
638                    if let Some((split, _)) = action.picked_split() {
639                        let bounds = layout.bounds();
640
641                        let splits = node.split_regions(
642                            self.spacing,
643                            Size::new(bounds.width, bounds.height),
644                        );
645
646                        if let Some((axis, rectangle, _)) = splits.get(&split) {
647                            if let Some(cursor_position) = cursor.position() {
648                                let ratio = match axis {
649                                    Axis::Horizontal => {
650                                        let position = cursor_position.y
651                                            - bounds.y
652                                            - rectangle.y;
653
654                                        (position / rectangle.height)
655                                            .clamp(0.1, 0.9)
656                                    }
657                                    Axis::Vertical => {
658                                        let position = cursor_position.x
659                                            - bounds.x
660                                            - rectangle.x;
661
662                                        (position / rectangle.width)
663                                            .clamp(0.1, 0.9)
664                                    }
665                                };
666
667                                shell.publish(on_resize(ResizeEvent {
668                                    split,
669                                    ratio,
670                                }));
671
672                                shell.capture_event();
673                            }
674                        }
675                    } else if action.picked_pane().is_some() {
676                        shell.request_redraw();
677                    }
678                }
679            }
680            _ => {}
681        }
682
683        if shell.redraw_request() != window::RedrawRequest::NextFrame {
684            let interaction = self
685                .grid_interaction(action, layout, cursor)
686                .or_else(|| {
687                    self.panes
688                        .iter()
689                        .zip(&self.contents)
690                        .zip(layout.children())
691                        .filter(|((pane, _content), _layout)| {
692                            self.internal
693                                .maximized()
694                                .is_none_or(|maximized| **pane == maximized)
695                        })
696                        .find_map(|((_pane, content), layout)| {
697                            content.grid_interaction(
698                                layout,
699                                cursor,
700                                on_drag.is_some(),
701                            )
702                        })
703                })
704                .unwrap_or(mouse::Interaction::None);
705
706            if let Event::Window(window::Event::RedrawRequested(_now)) = event {
707                self.last_mouse_interaction = Some(interaction);
708            } else if self.last_mouse_interaction.is_some_and(
709                |last_mouse_interaction| last_mouse_interaction != interaction,
710            ) {
711                shell.request_redraw();
712            }
713        }
714    }
715
716    fn mouse_interaction(
717        &self,
718        tree: &Tree,
719        layout: Layout<'_>,
720        cursor: mouse::Cursor,
721        viewport: &Rectangle,
722        renderer: &Renderer,
723    ) -> mouse::Interaction {
724        let Memory { action, .. } = tree.state.downcast_ref();
725
726        if let Some(grid_interaction) =
727            self.grid_interaction(action, layout, cursor)
728        {
729            return grid_interaction;
730        }
731
732        self.panes
733            .iter()
734            .copied()
735            .zip(&self.contents)
736            .zip(&tree.children)
737            .zip(layout.children())
738            .filter(|(((pane, _), _), _)| {
739                self.internal
740                    .maximized()
741                    .is_none_or(|maximized| *pane == maximized)
742            })
743            .map(|(((_, content), tree), layout)| {
744                content.mouse_interaction(
745                    tree,
746                    layout,
747                    cursor,
748                    viewport,
749                    renderer,
750                    self.drag_enabled(),
751                )
752            })
753            .max()
754            .unwrap_or_default()
755    }
756
757    fn draw(
758        &self,
759        tree: &Tree,
760        renderer: &mut Renderer,
761        theme: &Theme,
762        defaults: &renderer::Style,
763        layout: Layout<'_>,
764        cursor: mouse::Cursor,
765        viewport: &Rectangle,
766    ) {
767        let Memory { action, .. } = tree.state.downcast_ref();
768        let node = self.internal.layout();
769        let resize_leeway = self.on_resize.as_ref().map(|(leeway, _)| *leeway);
770
771        let picked_pane = action.picked_pane().filter(|(_, origin)| {
772            cursor
773                .position()
774                .map(|position| position.distance(*origin))
775                .unwrap_or_default()
776                > DRAG_DEADBAND_DISTANCE
777        });
778
779        let picked_split = action
780            .picked_split()
781            .and_then(|(split, axis)| {
782                let bounds = layout.bounds();
783
784                let splits = node.split_regions(self.spacing, bounds.size());
785
786                let (_axis, region, ratio) = splits.get(&split)?;
787
788                let region =
789                    axis.split_line_bounds(*region, *ratio, self.spacing);
790
791                Some((axis, region + Vector::new(bounds.x, bounds.y), true))
792            })
793            .or_else(|| match resize_leeway {
794                Some(leeway) => {
795                    let cursor_position = cursor.position()?;
796                    let bounds = layout.bounds();
797
798                    let relative_cursor = Point::new(
799                        cursor_position.x - bounds.x,
800                        cursor_position.y - bounds.y,
801                    );
802
803                    let splits =
804                        node.split_regions(self.spacing, bounds.size());
805
806                    let (_split, axis, region) = hovered_split(
807                        splits.iter(),
808                        self.spacing + leeway,
809                        relative_cursor,
810                    )?;
811
812                    Some((
813                        axis,
814                        region + Vector::new(bounds.x, bounds.y),
815                        false,
816                    ))
817                }
818                None => None,
819            });
820
821        let pane_cursor = if picked_pane.is_some() {
822            mouse::Cursor::Unavailable
823        } else {
824            cursor
825        };
826
827        let mut render_picked_pane = None;
828
829        let pane_in_edge = if picked_pane.is_some() {
830            cursor
831                .position()
832                .and_then(|cursor_position| in_edge(layout, cursor_position))
833        } else {
834            None
835        };
836
837        let style = Catalog::style(theme, &self.class);
838
839        for (((id, content), tree), pane_layout) in self
840            .panes
841            .iter()
842            .copied()
843            .zip(&self.contents)
844            .zip(&tree.children)
845            .zip(layout.children())
846            .filter(|(((pane, _), _), _)| {
847                self.internal
848                    .maximized()
849                    .is_none_or(|maximized| maximized == *pane)
850            })
851        {
852            match picked_pane {
853                Some((dragging, origin)) if id == dragging => {
854                    render_picked_pane =
855                        Some(((content, tree), origin, pane_layout));
856                }
857                Some((dragging, _)) if id != dragging => {
858                    content.draw(
859                        tree,
860                        renderer,
861                        theme,
862                        defaults,
863                        pane_layout,
864                        pane_cursor,
865                        viewport,
866                    );
867
868                    if picked_pane.is_some() && pane_in_edge.is_none() {
869                        if let Some(region) =
870                            cursor.position().and_then(|cursor_position| {
871                                layout_region(pane_layout, cursor_position)
872                            })
873                        {
874                            let bounds =
875                                layout_region_bounds(pane_layout, region);
876
877                            renderer.fill_quad(
878                                renderer::Quad {
879                                    bounds,
880                                    border: style.hovered_region.border,
881                                    ..renderer::Quad::default()
882                                },
883                                style.hovered_region.background,
884                            );
885                        }
886                    }
887                }
888                _ => {
889                    content.draw(
890                        tree,
891                        renderer,
892                        theme,
893                        defaults,
894                        pane_layout,
895                        pane_cursor,
896                        viewport,
897                    );
898                }
899            }
900        }
901
902        if let Some(edge) = pane_in_edge {
903            let bounds = edge_bounds(layout, edge);
904
905            renderer.fill_quad(
906                renderer::Quad {
907                    bounds,
908                    border: style.hovered_region.border,
909                    ..renderer::Quad::default()
910                },
911                style.hovered_region.background,
912            );
913        }
914
915        // Render picked pane last
916        if let Some(((content, tree), origin, layout)) = render_picked_pane {
917            if let Some(cursor_position) = cursor.position() {
918                let bounds = layout.bounds();
919
920                let translation =
921                    cursor_position - Point::new(origin.x, origin.y);
922
923                renderer.with_translation(translation, |renderer| {
924                    renderer.with_layer(bounds, |renderer| {
925                        content.draw(
926                            tree,
927                            renderer,
928                            theme,
929                            defaults,
930                            layout,
931                            pane_cursor,
932                            viewport,
933                        );
934                    });
935                });
936            }
937        }
938
939        if picked_pane.is_none() {
940            if let Some((axis, split_region, is_picked)) = picked_split {
941                let highlight = if is_picked {
942                    style.picked_split
943                } else {
944                    style.hovered_split
945                };
946
947                renderer.fill_quad(
948                    renderer::Quad {
949                        bounds: match axis {
950                            Axis::Horizontal => Rectangle {
951                                x: split_region.x,
952                                y: (split_region.y
953                                    + (split_region.height - highlight.width)
954                                        / 2.0)
955                                    .round(),
956                                width: split_region.width,
957                                height: highlight.width,
958                            },
959                            Axis::Vertical => Rectangle {
960                                x: (split_region.x
961                                    + (split_region.width - highlight.width)
962                                        / 2.0)
963                                    .round(),
964                                y: split_region.y,
965                                width: highlight.width,
966                                height: split_region.height,
967                            },
968                        },
969                        ..renderer::Quad::default()
970                    },
971                    highlight.color,
972                );
973            }
974        }
975    }
976
977    fn overlay<'b>(
978        &'b mut self,
979        tree: &'b mut Tree,
980        layout: Layout<'_>,
981        renderer: &Renderer,
982        translation: Vector,
983    ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
984        let children = self
985            .panes
986            .iter()
987            .copied()
988            .zip(&mut self.contents)
989            .zip(&mut tree.children)
990            .zip(layout.children())
991            .filter_map(|(((pane, content), state), layout)| {
992                if self
993                    .internal
994                    .maximized()
995                    .is_some_and(|maximized| maximized != pane)
996                {
997                    return None;
998                }
999
1000                content.overlay(state, layout, renderer, translation)
1001            })
1002            .collect::<Vec<_>>();
1003
1004        (!children.is_empty()).then(|| Group::with_children(children).overlay())
1005    }
1006}
1007
1008impl<'a, Message, Theme, Renderer> From<PaneGrid<'a, Message, Theme, Renderer>>
1009    for Element<'a, Message, Theme, Renderer>
1010where
1011    Message: 'a,
1012    Theme: Catalog + 'a,
1013    Renderer: core::Renderer + 'a,
1014{
1015    fn from(
1016        pane_grid: PaneGrid<'a, Message, Theme, Renderer>,
1017    ) -> Element<'a, Message, Theme, Renderer> {
1018        Element::new(pane_grid)
1019    }
1020}
1021
1022fn layout_region(layout: Layout<'_>, cursor_position: Point) -> Option<Region> {
1023    let bounds = layout.bounds();
1024
1025    if !bounds.contains(cursor_position) {
1026        return None;
1027    }
1028
1029    let region = if cursor_position.x < (bounds.x + bounds.width / 3.0) {
1030        Region::Edge(Edge::Left)
1031    } else if cursor_position.x > (bounds.x + 2.0 * bounds.width / 3.0) {
1032        Region::Edge(Edge::Right)
1033    } else if cursor_position.y < (bounds.y + bounds.height / 3.0) {
1034        Region::Edge(Edge::Top)
1035    } else if cursor_position.y > (bounds.y + 2.0 * bounds.height / 3.0) {
1036        Region::Edge(Edge::Bottom)
1037    } else {
1038        Region::Center
1039    };
1040
1041    Some(region)
1042}
1043
1044fn click_pane<'a, Message, T>(
1045    action: &mut state::Action,
1046    layout: Layout<'_>,
1047    cursor_position: Point,
1048    shell: &mut Shell<'_, Message>,
1049    contents: impl Iterator<Item = (Pane, T)>,
1050    on_click: &Option<Box<dyn Fn(Pane) -> Message + 'a>>,
1051    on_drag: &Option<Box<dyn Fn(DragEvent) -> Message + 'a>>,
1052) where
1053    T: Draggable,
1054{
1055    let mut clicked_region = contents
1056        .zip(layout.children())
1057        .filter(|(_, layout)| layout.bounds().contains(cursor_position));
1058
1059    if let Some(((pane, content), layout)) = clicked_region.next() {
1060        if let Some(on_click) = &on_click {
1061            shell.publish(on_click(pane));
1062        }
1063
1064        if let Some(on_drag) = &on_drag {
1065            if content.can_be_dragged_at(layout, cursor_position) {
1066                *action = state::Action::Dragging {
1067                    pane,
1068                    origin: cursor_position,
1069                };
1070
1071                shell.publish(on_drag(DragEvent::Picked { pane }));
1072            }
1073        }
1074    }
1075}
1076
1077fn in_edge(layout: Layout<'_>, cursor: Point) -> Option<Edge> {
1078    let bounds = layout.bounds();
1079
1080    let height_thickness = bounds.height / THICKNESS_RATIO;
1081    let width_thickness = bounds.width / THICKNESS_RATIO;
1082    let thickness = height_thickness.min(width_thickness);
1083
1084    if cursor.x > bounds.x && cursor.x < bounds.x + thickness {
1085        Some(Edge::Left)
1086    } else if cursor.x > bounds.x + bounds.width - thickness
1087        && cursor.x < bounds.x + bounds.width
1088    {
1089        Some(Edge::Right)
1090    } else if cursor.y > bounds.y && cursor.y < bounds.y + thickness {
1091        Some(Edge::Top)
1092    } else if cursor.y > bounds.y + bounds.height - thickness
1093        && cursor.y < bounds.y + bounds.height
1094    {
1095        Some(Edge::Bottom)
1096    } else {
1097        None
1098    }
1099}
1100
1101fn edge_bounds(layout: Layout<'_>, edge: Edge) -> Rectangle {
1102    let bounds = layout.bounds();
1103
1104    let height_thickness = bounds.height / THICKNESS_RATIO;
1105    let width_thickness = bounds.width / THICKNESS_RATIO;
1106    let thickness = height_thickness.min(width_thickness);
1107
1108    match edge {
1109        Edge::Top => Rectangle {
1110            height: thickness,
1111            ..bounds
1112        },
1113        Edge::Left => Rectangle {
1114            width: thickness,
1115            ..bounds
1116        },
1117        Edge::Right => Rectangle {
1118            x: bounds.x + bounds.width - thickness,
1119            width: thickness,
1120            ..bounds
1121        },
1122        Edge::Bottom => Rectangle {
1123            y: bounds.y + bounds.height - thickness,
1124            height: thickness,
1125            ..bounds
1126        },
1127    }
1128}
1129
1130fn layout_region_bounds(layout: Layout<'_>, region: Region) -> Rectangle {
1131    let bounds = layout.bounds();
1132
1133    match region {
1134        Region::Center => bounds,
1135        Region::Edge(edge) => match edge {
1136            Edge::Top => Rectangle {
1137                height: bounds.height / 2.0,
1138                ..bounds
1139            },
1140            Edge::Left => Rectangle {
1141                width: bounds.width / 2.0,
1142                ..bounds
1143            },
1144            Edge::Right => Rectangle {
1145                x: bounds.x + bounds.width / 2.0,
1146                width: bounds.width / 2.0,
1147                ..bounds
1148            },
1149            Edge::Bottom => Rectangle {
1150                y: bounds.y + bounds.height / 2.0,
1151                height: bounds.height / 2.0,
1152                ..bounds
1153            },
1154        },
1155    }
1156}
1157
1158/// An event produced during a drag and drop interaction of a [`PaneGrid`].
1159#[derive(Debug, Clone, Copy)]
1160pub enum DragEvent {
1161    /// A [`Pane`] was picked for dragging.
1162    Picked {
1163        /// The picked [`Pane`].
1164        pane: Pane,
1165    },
1166
1167    /// A [`Pane`] was dropped on top of another [`Pane`].
1168    Dropped {
1169        /// The picked [`Pane`].
1170        pane: Pane,
1171
1172        /// The [`Target`] where the picked [`Pane`] was dropped on.
1173        target: Target,
1174    },
1175
1176    /// A [`Pane`] was picked and then dropped outside of other [`Pane`]
1177    /// boundaries.
1178    Canceled {
1179        /// The picked [`Pane`].
1180        pane: Pane,
1181    },
1182}
1183
1184/// The [`Target`] area a pane can be dropped on.
1185#[derive(Debug, Clone, Copy)]
1186pub enum Target {
1187    /// An [`Edge`] of the full [`PaneGrid`].
1188    Edge(Edge),
1189    /// A single [`Pane`] of the [`PaneGrid`].
1190    Pane(Pane, Region),
1191}
1192
1193/// The region of a [`Pane`].
1194#[derive(Debug, Clone, Copy, Default)]
1195pub enum Region {
1196    /// Center region.
1197    #[default]
1198    Center,
1199    /// Edge region.
1200    Edge(Edge),
1201}
1202
1203/// The edges of an area.
1204#[derive(Debug, Clone, Copy)]
1205pub enum Edge {
1206    /// Top edge.
1207    Top,
1208    /// Left edge.
1209    Left,
1210    /// Right edge.
1211    Right,
1212    /// Bottom edge.
1213    Bottom,
1214}
1215
1216/// An event produced during a resize interaction of a [`PaneGrid`].
1217#[derive(Debug, Clone, Copy)]
1218pub struct ResizeEvent {
1219    /// The [`Split`] that is being dragged for resizing.
1220    pub split: Split,
1221
1222    /// The new ratio of the [`Split`].
1223    ///
1224    /// The ratio is a value in [0, 1], representing the exact position of a
1225    /// [`Split`] between two panes.
1226    pub ratio: f32,
1227}
1228
1229/*
1230 * Helpers
1231 */
1232fn hovered_split<'a>(
1233    mut splits: impl Iterator<Item = (&'a Split, &'a (Axis, Rectangle, f32))>,
1234    spacing: f32,
1235    cursor_position: Point,
1236) -> Option<(Split, Axis, Rectangle)> {
1237    splits.find_map(|(split, (axis, region, ratio))| {
1238        let bounds = axis.split_line_bounds(*region, *ratio, spacing);
1239
1240        if bounds.contains(cursor_position) {
1241            Some((*split, *axis, bounds))
1242        } else {
1243            None
1244        }
1245    })
1246}
1247
1248/// The appearance of a [`PaneGrid`].
1249#[derive(Debug, Clone, Copy, PartialEq)]
1250pub struct Style {
1251    /// The appearance of a hovered region highlight.
1252    pub hovered_region: Highlight,
1253    /// The appearance of a picked split.
1254    pub picked_split: Line,
1255    /// The appearance of a hovered split.
1256    pub hovered_split: Line,
1257}
1258
1259/// The appearance of a highlight of the [`PaneGrid`].
1260#[derive(Debug, Clone, Copy, PartialEq)]
1261pub struct Highlight {
1262    /// The [`Background`] of the pane region.
1263    pub background: Background,
1264    /// The [`Border`] of the pane region.
1265    pub border: Border,
1266}
1267
1268/// A line.
1269///
1270/// It is normally used to define the highlight of something, like a split.
1271#[derive(Debug, Clone, Copy, PartialEq)]
1272pub struct Line {
1273    /// The [`Color`] of the [`Line`].
1274    pub color: Color,
1275    /// The width of the [`Line`].
1276    pub width: f32,
1277}
1278
1279/// The theme catalog of a [`PaneGrid`].
1280pub trait Catalog: container::Catalog {
1281    /// The item class of this [`Catalog`].
1282    type Class<'a>;
1283
1284    /// The default class produced by this [`Catalog`].
1285    fn default<'a>() -> <Self as Catalog>::Class<'a>;
1286
1287    /// The [`Style`] of a class with the given status.
1288    fn style(&self, class: &<Self as Catalog>::Class<'_>) -> Style;
1289}
1290
1291/// A styling function for a [`PaneGrid`].
1292///
1293/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`.
1294pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
1295
1296impl Catalog for Theme {
1297    type Class<'a> = StyleFn<'a, Self>;
1298
1299    fn default<'a>() -> StyleFn<'a, Self> {
1300        Box::new(default)
1301    }
1302
1303    fn style(&self, class: &StyleFn<'_, Self>) -> Style {
1304        class(self)
1305    }
1306}
1307
1308/// The default style of a [`PaneGrid`].
1309pub fn default(theme: &Theme) -> Style {
1310    let palette = theme.extended_palette();
1311
1312    Style {
1313        hovered_region: Highlight {
1314            background: Background::Color(Color {
1315                a: 0.5,
1316                ..palette.primary.base.color
1317            }),
1318            border: Border {
1319                width: 2.0,
1320                color: palette.primary.strong.color,
1321                radius: 0.0.into(),
1322            },
1323        },
1324        hovered_split: Line {
1325            color: palette.primary.base.color,
1326            width: 2.0,
1327        },
1328        picked_split: Line {
1329            color: palette.primary.strong.color,
1330            width: 2.0,
1331        },
1332    }
1333}