Skip to main content

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