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, Clipboard, Color, Element, Event, Layout, Length, Pixels, Point,
92    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/// ```
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        clipboard: &mut dyn Clipboard,
472        shell: &mut Shell<'_, Message>,
473        viewport: &Rectangle,
474    ) {
475        let Memory { action, .. } = tree.state.downcast_mut();
476        let node = self.internal.layout();
477
478        let on_drag = if self.drag_enabled() {
479            &self.on_drag
480        } else {
481            &None
482        };
483
484        let picked_pane = action.picked_pane().map(|(pane, _)| pane);
485
486        for (((pane, content), tree), layout) in self
487            .panes
488            .iter()
489            .copied()
490            .zip(&mut self.contents)
491            .zip(&mut tree.children)
492            .zip(layout.children())
493            .filter(|(((pane, _), _), _)| {
494                self.internal
495                    .maximized()
496                    .is_none_or(|maximized| *pane == maximized)
497            })
498        {
499            let is_picked = picked_pane == Some(pane);
500
501            content.update(
502                tree, event, layout, cursor, renderer, clipboard, shell, viewport, is_picked,
503            );
504        }
505
506        match event {
507            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
508            | Event::Touch(touch::Event::FingerPressed { .. }) => {
509                let bounds = layout.bounds();
510
511                if let Some(cursor_position) = cursor.position_over(bounds) {
512                    shell.capture_event();
513
514                    match &self.on_resize {
515                        Some((leeway, _)) => {
516                            let relative_cursor = Point::new(
517                                cursor_position.x - bounds.x,
518                                cursor_position.y - bounds.y,
519                            );
520
521                            let splits =
522                                node.split_regions(self.spacing, self.min_size, bounds.size());
523
524                            let clicked_split = hovered_split(
525                                splits.iter(),
526                                self.spacing + leeway,
527                                relative_cursor,
528                            );
529
530                            if let Some((split, axis, _)) = clicked_split {
531                                if action.picked_pane().is_none() {
532                                    *action = state::Action::Resizing { split, axis };
533                                }
534                            } else {
535                                click_pane(
536                                    action,
537                                    layout,
538                                    cursor_position,
539                                    shell,
540                                    self.panes.iter().copied().zip(&self.contents),
541                                    &self.on_click,
542                                    on_drag,
543                                );
544                            }
545                        }
546                        None => {
547                            click_pane(
548                                action,
549                                layout,
550                                cursor_position,
551                                shell,
552                                self.panes.iter().copied().zip(&self.contents),
553                                &self.on_click,
554                                on_drag,
555                            );
556                        }
557                    }
558                }
559            }
560            Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
561            | Event::Touch(touch::Event::FingerLifted { .. })
562            | Event::Touch(touch::Event::FingerLost { .. }) => {
563                if let Some((pane, origin)) = action.picked_pane()
564                    && let Some(on_drag) = on_drag
565                    && let Some(cursor_position) = cursor.position()
566                {
567                    if cursor_position.distance(origin) > DRAG_DEADBAND_DISTANCE {
568                        let event = if let Some(edge) = in_edge(layout, cursor_position) {
569                            DragEvent::Dropped {
570                                pane,
571                                target: Target::Edge(edge),
572                            }
573                        } else {
574                            let dropped_region = self
575                                .panes
576                                .iter()
577                                .copied()
578                                .zip(&self.contents)
579                                .zip(layout.children())
580                                .find_map(|(target, layout)| {
581                                    layout_region(layout, cursor_position)
582                                        .map(|region| (target, region))
583                                });
584
585                            match dropped_region {
586                                Some(((target, _), region)) if pane != target => {
587                                    DragEvent::Dropped {
588                                        pane,
589                                        target: Target::Pane(target, region),
590                                    }
591                                }
592                                _ => DragEvent::Canceled { pane },
593                            }
594                        };
595
596                        shell.publish(on_drag(event));
597                    } else {
598                        shell.publish(on_drag(DragEvent::Canceled { pane }));
599                    }
600                }
601
602                *action = state::Action::Idle;
603            }
604            Event::Mouse(mouse::Event::CursorMoved { .. })
605            | Event::Touch(touch::Event::FingerMoved { .. }) => {
606                if let Some((_, on_resize)) = &self.on_resize {
607                    if let Some((split, _)) = action.picked_split() {
608                        let bounds = layout.bounds();
609
610                        let splits = node.split_regions(self.spacing, self.min_size, bounds.size());
611
612                        if let Some((axis, rectangle, _)) = splits.get(&split)
613                            && let Some(cursor_position) = cursor.position()
614                        {
615                            let ratio = match axis {
616                                Axis::Horizontal => {
617                                    let position = cursor_position.y - bounds.y - rectangle.y;
618
619                                    (position / rectangle.height).clamp(0.0, 1.0)
620                                }
621                                Axis::Vertical => {
622                                    let position = cursor_position.x - bounds.x - rectangle.x;
623
624                                    (position / rectangle.width).clamp(0.0, 1.0)
625                                }
626                            };
627
628                            shell.publish(on_resize(ResizeEvent { split, ratio }));
629
630                            shell.capture_event();
631                        }
632                    } else if action.picked_pane().is_some() {
633                        shell.request_redraw();
634                    }
635                }
636            }
637            _ => {}
638        }
639
640        if shell.redraw_request() != window::RedrawRequest::NextFrame {
641            let interaction = self
642                .grid_interaction(action, layout, cursor)
643                .or_else(|| {
644                    self.panes
645                        .iter()
646                        .zip(&self.contents)
647                        .zip(layout.children())
648                        .filter(|((pane, _content), _layout)| {
649                            self.internal
650                                .maximized()
651                                .is_none_or(|maximized| **pane == maximized)
652                        })
653                        .find_map(|((_pane, content), layout)| {
654                            content.grid_interaction(layout, cursor, on_drag.is_some())
655                        })
656                })
657                .unwrap_or(mouse::Interaction::None);
658
659            if let Event::Window(window::Event::RedrawRequested(_now)) = event {
660                self.last_mouse_interaction = Some(interaction);
661            } else if self
662                .last_mouse_interaction
663                .is_some_and(|last_mouse_interaction| last_mouse_interaction != interaction)
664            {
665                shell.request_redraw();
666            }
667        }
668    }
669
670    fn mouse_interaction(
671        &self,
672        tree: &Tree,
673        layout: Layout<'_>,
674        cursor: mouse::Cursor,
675        viewport: &Rectangle,
676        renderer: &Renderer,
677    ) -> mouse::Interaction {
678        let Memory { action, .. } = tree.state.downcast_ref();
679
680        if let Some(grid_interaction) = self.grid_interaction(action, layout, cursor) {
681            return grid_interaction;
682        }
683
684        self.panes
685            .iter()
686            .copied()
687            .zip(&self.contents)
688            .zip(&tree.children)
689            .zip(layout.children())
690            .filter(|(((pane, _), _), _)| {
691                self.internal
692                    .maximized()
693                    .is_none_or(|maximized| *pane == maximized)
694            })
695            .map(|(((_, content), tree), layout)| {
696                content.mouse_interaction(
697                    tree,
698                    layout,
699                    cursor,
700                    viewport,
701                    renderer,
702                    self.drag_enabled(),
703                )
704            })
705            .max()
706            .unwrap_or_default()
707    }
708
709    fn draw(
710        &self,
711        tree: &Tree,
712        renderer: &mut Renderer,
713        theme: &Theme,
714        defaults: &renderer::Style,
715        layout: Layout<'_>,
716        cursor: mouse::Cursor,
717        viewport: &Rectangle,
718    ) {
719        let Memory { action, .. } = tree.state.downcast_ref();
720        let node = self.internal.layout();
721        let resize_leeway = self.on_resize.as_ref().map(|(leeway, _)| *leeway);
722
723        let picked_pane = action.picked_pane().filter(|(_, origin)| {
724            cursor
725                .position()
726                .map(|position| position.distance(*origin))
727                .unwrap_or_default()
728                > DRAG_DEADBAND_DISTANCE
729        });
730
731        let picked_split = action
732            .picked_split()
733            .and_then(|(split, axis)| {
734                let bounds = layout.bounds();
735
736                let splits = node.split_regions(self.spacing, self.min_size, bounds.size());
737
738                let (_axis, region, ratio) = splits.get(&split)?;
739
740                let region = axis.split_line_bounds(*region, *ratio, self.spacing);
741
742                Some((axis, region + Vector::new(bounds.x, bounds.y), true))
743            })
744            .or_else(|| match resize_leeway {
745                Some(leeway) => {
746                    let cursor_position = cursor.position()?;
747                    let bounds = layout.bounds();
748
749                    let relative_cursor =
750                        Point::new(cursor_position.x - bounds.x, cursor_position.y - bounds.y);
751
752                    let splits = node.split_regions(self.spacing, self.min_size, bounds.size());
753
754                    let (_split, axis, region) =
755                        hovered_split(splits.iter(), self.spacing + leeway, relative_cursor)?;
756
757                    Some((axis, region + Vector::new(bounds.x, bounds.y), false))
758                }
759                None => None,
760            });
761
762        let pane_cursor = if picked_pane.is_some() {
763            mouse::Cursor::Unavailable
764        } else {
765            cursor
766        };
767
768        let mut render_picked_pane = None;
769
770        let pane_in_edge = if picked_pane.is_some() {
771            cursor
772                .position()
773                .and_then(|cursor_position| in_edge(layout, cursor_position))
774        } else {
775            None
776        };
777
778        let style = Catalog::style(theme, &self.class);
779
780        for (((id, content), tree), pane_layout) in self
781            .panes
782            .iter()
783            .copied()
784            .zip(&self.contents)
785            .zip(&tree.children)
786            .zip(layout.children())
787            .filter(|(((pane, _), _), _)| {
788                self.internal
789                    .maximized()
790                    .is_none_or(|maximized| maximized == *pane)
791            })
792        {
793            match picked_pane {
794                Some((dragging, origin)) if id == dragging => {
795                    render_picked_pane = Some(((content, tree), origin, pane_layout));
796                }
797                Some((dragging, _)) if id != dragging => {
798                    content.draw(
799                        tree,
800                        renderer,
801                        theme,
802                        defaults,
803                        pane_layout,
804                        pane_cursor,
805                        viewport,
806                    );
807
808                    if picked_pane.is_some()
809                        && pane_in_edge.is_none()
810                        && let Some(region) = cursor
811                            .position()
812                            .and_then(|cursor_position| layout_region(pane_layout, cursor_position))
813                    {
814                        let bounds = layout_region_bounds(pane_layout, region);
815
816                        renderer.fill_quad(
817                            renderer::Quad {
818                                bounds,
819                                border: style.hovered_region.border,
820                                ..renderer::Quad::default()
821                            },
822                            style.hovered_region.background,
823                        );
824                    }
825                }
826                _ => {
827                    content.draw(
828                        tree,
829                        renderer,
830                        theme,
831                        defaults,
832                        pane_layout,
833                        pane_cursor,
834                        viewport,
835                    );
836                }
837            }
838        }
839
840        if let Some(edge) = pane_in_edge {
841            let bounds = edge_bounds(layout, edge);
842
843            renderer.fill_quad(
844                renderer::Quad {
845                    bounds,
846                    border: style.hovered_region.border,
847                    ..renderer::Quad::default()
848                },
849                style.hovered_region.background,
850            );
851        }
852
853        // Render picked pane last
854        if let Some(((content, tree), origin, layout)) = render_picked_pane
855            && let Some(cursor_position) = cursor.position()
856        {
857            let bounds = layout.bounds();
858
859            let translation = cursor_position - Point::new(origin.x, origin.y);
860
861            renderer.with_translation(translation, |renderer| {
862                renderer.with_layer(bounds, |renderer| {
863                    content.draw(
864                        tree,
865                        renderer,
866                        theme,
867                        defaults,
868                        layout,
869                        pane_cursor,
870                        viewport,
871                    );
872                });
873            });
874        }
875
876        if picked_pane.is_none()
877            && let Some((axis, split_region, is_picked)) = picked_split
878        {
879            let highlight = if is_picked {
880                style.picked_split
881            } else {
882                style.hovered_split
883            };
884
885            renderer.fill_quad(
886                renderer::Quad {
887                    bounds: match axis {
888                        Axis::Horizontal => Rectangle {
889                            x: split_region.x,
890                            y: (split_region.y + (split_region.height - highlight.width) / 2.0)
891                                .round(),
892                            width: split_region.width,
893                            height: highlight.width,
894                        },
895                        Axis::Vertical => Rectangle {
896                            x: (split_region.x + (split_region.width - highlight.width) / 2.0)
897                                .round(),
898                            y: split_region.y,
899                            width: highlight.width,
900                            height: split_region.height,
901                        },
902                    },
903                    ..renderer::Quad::default()
904                },
905                highlight.color,
906            );
907        }
908    }
909
910    fn overlay<'b>(
911        &'b mut self,
912        tree: &'b mut Tree,
913        layout: Layout<'b>,
914        renderer: &Renderer,
915        viewport: &Rectangle,
916        translation: Vector,
917    ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
918        let children = self
919            .panes
920            .iter()
921            .copied()
922            .zip(&mut self.contents)
923            .zip(&mut tree.children)
924            .zip(layout.children())
925            .filter_map(|(((pane, content), state), layout)| {
926                if self
927                    .internal
928                    .maximized()
929                    .is_some_and(|maximized| maximized != pane)
930                {
931                    return None;
932                }
933
934                content.overlay(state, layout, renderer, viewport, translation)
935            })
936            .collect::<Vec<_>>();
937
938        (!children.is_empty()).then(|| Group::with_children(children).overlay())
939    }
940}
941
942impl<'a, Message, Theme, Renderer> From<PaneGrid<'a, Message, Theme, Renderer>>
943    for Element<'a, Message, Theme, Renderer>
944where
945    Message: 'a,
946    Theme: Catalog + 'a,
947    Renderer: core::Renderer + 'a,
948{
949    fn from(
950        pane_grid: PaneGrid<'a, Message, Theme, Renderer>,
951    ) -> Element<'a, Message, Theme, Renderer> {
952        Element::new(pane_grid)
953    }
954}
955
956fn layout_region(layout: Layout<'_>, cursor_position: Point) -> Option<Region> {
957    let bounds = layout.bounds();
958
959    if !bounds.contains(cursor_position) {
960        return None;
961    }
962
963    let region = if cursor_position.x < (bounds.x + bounds.width / 3.0) {
964        Region::Edge(Edge::Left)
965    } else if cursor_position.x > (bounds.x + 2.0 * bounds.width / 3.0) {
966        Region::Edge(Edge::Right)
967    } else if cursor_position.y < (bounds.y + bounds.height / 3.0) {
968        Region::Edge(Edge::Top)
969    } else if cursor_position.y > (bounds.y + 2.0 * bounds.height / 3.0) {
970        Region::Edge(Edge::Bottom)
971    } else {
972        Region::Center
973    };
974
975    Some(region)
976}
977
978fn click_pane<'a, Message, T>(
979    action: &mut state::Action,
980    layout: Layout<'_>,
981    cursor_position: Point,
982    shell: &mut Shell<'_, Message>,
983    contents: impl Iterator<Item = (Pane, T)>,
984    on_click: &Option<Box<dyn Fn(Pane) -> Message + 'a>>,
985    on_drag: &Option<Box<dyn Fn(DragEvent) -> Message + 'a>>,
986) where
987    T: Draggable,
988{
989    let mut clicked_region = contents
990        .zip(layout.children())
991        .filter(|(_, layout)| layout.bounds().contains(cursor_position));
992
993    if let Some(((pane, content), layout)) = clicked_region.next() {
994        if let Some(on_click) = &on_click {
995            shell.publish(on_click(pane));
996        }
997
998        if let Some(on_drag) = &on_drag
999            && content.can_be_dragged_at(layout, cursor_position)
1000        {
1001            *action = state::Action::Dragging {
1002                pane,
1003                origin: cursor_position,
1004            };
1005
1006            shell.publish(on_drag(DragEvent::Picked { pane }));
1007        }
1008    }
1009}
1010
1011fn in_edge(layout: Layout<'_>, cursor: Point) -> Option<Edge> {
1012    let bounds = layout.bounds();
1013
1014    let height_thickness = bounds.height / THICKNESS_RATIO;
1015    let width_thickness = bounds.width / THICKNESS_RATIO;
1016    let thickness = height_thickness.min(width_thickness);
1017
1018    if cursor.x > bounds.x && cursor.x < bounds.x + thickness {
1019        Some(Edge::Left)
1020    } else if cursor.x > bounds.x + bounds.width - thickness && cursor.x < bounds.x + bounds.width {
1021        Some(Edge::Right)
1022    } else if cursor.y > bounds.y && cursor.y < bounds.y + thickness {
1023        Some(Edge::Top)
1024    } else if cursor.y > bounds.y + bounds.height - thickness && cursor.y < bounds.y + bounds.height
1025    {
1026        Some(Edge::Bottom)
1027    } else {
1028        None
1029    }
1030}
1031
1032fn edge_bounds(layout: Layout<'_>, edge: Edge) -> Rectangle {
1033    let bounds = layout.bounds();
1034
1035    let height_thickness = bounds.height / THICKNESS_RATIO;
1036    let width_thickness = bounds.width / THICKNESS_RATIO;
1037    let thickness = height_thickness.min(width_thickness);
1038
1039    match edge {
1040        Edge::Top => Rectangle {
1041            height: thickness,
1042            ..bounds
1043        },
1044        Edge::Left => Rectangle {
1045            width: thickness,
1046            ..bounds
1047        },
1048        Edge::Right => Rectangle {
1049            x: bounds.x + bounds.width - thickness,
1050            width: thickness,
1051            ..bounds
1052        },
1053        Edge::Bottom => Rectangle {
1054            y: bounds.y + bounds.height - thickness,
1055            height: thickness,
1056            ..bounds
1057        },
1058    }
1059}
1060
1061fn layout_region_bounds(layout: Layout<'_>, region: Region) -> Rectangle {
1062    let bounds = layout.bounds();
1063
1064    match region {
1065        Region::Center => bounds,
1066        Region::Edge(edge) => match edge {
1067            Edge::Top => Rectangle {
1068                height: bounds.height / 2.0,
1069                ..bounds
1070            },
1071            Edge::Left => Rectangle {
1072                width: bounds.width / 2.0,
1073                ..bounds
1074            },
1075            Edge::Right => Rectangle {
1076                x: bounds.x + bounds.width / 2.0,
1077                width: bounds.width / 2.0,
1078                ..bounds
1079            },
1080            Edge::Bottom => Rectangle {
1081                y: bounds.y + bounds.height / 2.0,
1082                height: bounds.height / 2.0,
1083                ..bounds
1084            },
1085        },
1086    }
1087}
1088
1089/// An event produced during a drag and drop interaction of a [`PaneGrid`].
1090#[derive(Debug, Clone, Copy)]
1091pub enum DragEvent {
1092    /// A [`Pane`] was picked for dragging.
1093    Picked {
1094        /// The picked [`Pane`].
1095        pane: Pane,
1096    },
1097
1098    /// A [`Pane`] was dropped on top of another [`Pane`].
1099    Dropped {
1100        /// The picked [`Pane`].
1101        pane: Pane,
1102
1103        /// The [`Target`] where the picked [`Pane`] was dropped on.
1104        target: Target,
1105    },
1106
1107    /// A [`Pane`] was picked and then dropped outside of other [`Pane`]
1108    /// boundaries.
1109    Canceled {
1110        /// The picked [`Pane`].
1111        pane: Pane,
1112    },
1113}
1114
1115/// The [`Target`] area a pane can be dropped on.
1116#[derive(Debug, Clone, Copy)]
1117pub enum Target {
1118    /// An [`Edge`] of the full [`PaneGrid`].
1119    Edge(Edge),
1120    /// A single [`Pane`] of the [`PaneGrid`].
1121    Pane(Pane, Region),
1122}
1123
1124/// The region of a [`Pane`].
1125#[derive(Debug, Clone, Copy, Default)]
1126pub enum Region {
1127    /// Center region.
1128    #[default]
1129    Center,
1130    /// Edge region.
1131    Edge(Edge),
1132}
1133
1134/// The edges of an area.
1135#[derive(Debug, Clone, Copy)]
1136pub enum Edge {
1137    /// Top edge.
1138    Top,
1139    /// Left edge.
1140    Left,
1141    /// Right edge.
1142    Right,
1143    /// Bottom edge.
1144    Bottom,
1145}
1146
1147/// An event produced during a resize interaction of a [`PaneGrid`].
1148#[derive(Debug, Clone, Copy)]
1149pub struct ResizeEvent {
1150    /// The [`Split`] that is being dragged for resizing.
1151    pub split: Split,
1152
1153    /// The new ratio of the [`Split`].
1154    ///
1155    /// The ratio is a value in [0, 1], representing the exact position of a
1156    /// [`Split`] between two panes.
1157    pub ratio: f32,
1158}
1159
1160/*
1161 * Helpers
1162 */
1163fn hovered_split<'a>(
1164    mut splits: impl Iterator<Item = (&'a Split, &'a (Axis, Rectangle, f32))>,
1165    spacing: f32,
1166    cursor_position: Point,
1167) -> Option<(Split, Axis, Rectangle)> {
1168    splits.find_map(|(split, (axis, region, ratio))| {
1169        let bounds = axis.split_line_bounds(*region, *ratio, spacing);
1170
1171        if bounds.contains(cursor_position) {
1172            Some((*split, *axis, bounds))
1173        } else {
1174            None
1175        }
1176    })
1177}
1178
1179/// The appearance of a [`PaneGrid`].
1180#[derive(Debug, Clone, Copy, PartialEq)]
1181pub struct Style {
1182    /// The appearance of a hovered region highlight.
1183    pub hovered_region: Highlight,
1184    /// The appearance of a picked split.
1185    pub picked_split: Line,
1186    /// The appearance of a hovered split.
1187    pub hovered_split: Line,
1188}
1189
1190/// The appearance of a highlight of the [`PaneGrid`].
1191#[derive(Debug, Clone, Copy, PartialEq)]
1192pub struct Highlight {
1193    /// The [`Background`] of the pane region.
1194    pub background: Background,
1195    /// The [`Border`] of the pane region.
1196    pub border: Border,
1197}
1198
1199/// A line.
1200///
1201/// It is normally used to define the highlight of something, like a split.
1202#[derive(Debug, Clone, Copy, PartialEq)]
1203pub struct Line {
1204    /// The [`Color`] of the [`Line`].
1205    pub color: Color,
1206    /// The width of the [`Line`].
1207    pub width: f32,
1208}
1209
1210/// The theme catalog of a [`PaneGrid`].
1211pub trait Catalog: container::Catalog {
1212    /// The item class of this [`Catalog`].
1213    type Class<'a>;
1214
1215    /// The default class produced by this [`Catalog`].
1216    fn default<'a>() -> <Self as Catalog>::Class<'a>;
1217
1218    /// The [`Style`] of a class with the given status.
1219    fn style(&self, class: &<Self as Catalog>::Class<'_>) -> Style;
1220}
1221
1222/// A styling function for a [`PaneGrid`].
1223///
1224/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`.
1225pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
1226
1227impl Catalog for Theme {
1228    type Class<'a> = StyleFn<'a, Self>;
1229
1230    fn default<'a>() -> StyleFn<'a, Self> {
1231        Box::new(default)
1232    }
1233
1234    fn style(&self, class: &StyleFn<'_, Self>) -> Style {
1235        class(self)
1236    }
1237}
1238
1239/// The default style of a [`PaneGrid`].
1240pub fn default(theme: &Theme) -> Style {
1241    let palette = theme.extended_palette();
1242
1243    Style {
1244        hovered_region: Highlight {
1245            background: Background::Color(Color {
1246                a: 0.5,
1247                ..palette.primary.base.color
1248            }),
1249            border: Border {
1250                width: 2.0,
1251                color: palette.primary.strong.color,
1252                radius: 0.0.into(),
1253            },
1254        },
1255        hovered_split: Line {
1256            color: palette.primary.base.color,
1257            width: 2.0,
1258        },
1259        picked_split: Line {
1260            color: palette.primary.strong.color,
1261            width: 2.0,
1262        },
1263    }
1264}