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();
723        let dragged_pane = picked_pane
724            .filter(|(_, origin)| is_dragging(*origin, cursor.position().unwrap_or_default()));
725
726        let picked_split = action
727            .picked_split()
728            .and_then(|(split, axis)| {
729                let bounds = layout.bounds();
730
731                let splits = node.split_regions(self.spacing, self.min_size, bounds.size());
732
733                let (_axis, region, ratio) = splits.get(&split)?;
734
735                let region = axis.split_line_bounds(*region, *ratio, self.spacing);
736
737                Some((axis, region + Vector::new(bounds.x, bounds.y), true))
738            })
739            .or_else(|| match resize_leeway {
740                Some(leeway) => {
741                    let cursor_position = cursor.position()?;
742                    let bounds = layout.bounds();
743
744                    let relative_cursor =
745                        Point::new(cursor_position.x - bounds.x, cursor_position.y - bounds.y);
746
747                    let splits = node.split_regions(self.spacing, self.min_size, bounds.size());
748
749                    let (_split, axis, region) =
750                        hovered_split(splits.iter(), self.spacing + leeway, relative_cursor)?;
751
752                    Some((axis, region + Vector::new(bounds.x, bounds.y), false))
753                }
754                None => None,
755            });
756
757        let pane_cursor = if picked_pane.is_some() {
758            mouse::Cursor::Unavailable
759        } else {
760            cursor
761        };
762
763        let pane_in_edge = if picked_pane.is_some() {
764            cursor
765                .position()
766                .and_then(|cursor_position| in_edge(layout, cursor_position))
767        } else {
768            None
769        };
770
771        let style = Catalog::style(theme, &self.class);
772
773        for (((id, content), tree), pane_layout) in self
774            .panes
775            .iter()
776            .copied()
777            .zip(&self.contents)
778            .zip(&tree.children)
779            .zip(layout.children())
780            .filter(|(((pane, _), _), _)| {
781                self.internal
782                    .maximized()
783                    .is_none_or(|maximized| maximized == *pane)
784            })
785        {
786            match picked_pane {
787                Some((dragging, _)) if id == dragging => {}
788                Some(_) => {
789                    content.draw(
790                        tree,
791                        renderer,
792                        theme,
793                        defaults,
794                        pane_layout,
795                        pane_cursor,
796                        viewport,
797                    );
798
799                    if let Some(cursor_position) = cursor.position()
800                        && dragged_pane.is_some()
801                        && pane_in_edge.is_none()
802                        && let Some(region) = layout_region(pane_layout, cursor_position)
803                    {
804                        let bounds = layout_region_bounds(pane_layout, region);
805
806                        renderer.fill_quad(
807                            renderer::Quad {
808                                bounds,
809                                border: style.hovered_region.border,
810                                ..renderer::Quad::default()
811                            },
812                            style.hovered_region.background,
813                        );
814                    }
815                }
816                _ => {
817                    content.draw(
818                        tree,
819                        renderer,
820                        theme,
821                        defaults,
822                        pane_layout,
823                        pane_cursor,
824                        viewport,
825                    );
826                }
827            }
828        }
829
830        if dragged_pane.is_some()
831            && let Some(edge) = pane_in_edge
832        {
833            let bounds = edge_bounds(layout, edge);
834
835            renderer.fill_quad(
836                renderer::Quad {
837                    bounds,
838                    border: style.hovered_region.border,
839                    ..renderer::Quad::default()
840                },
841                style.hovered_region.background,
842            );
843        }
844
845        if dragged_pane.is_none()
846            && let Some((axis, split_region, is_picked)) = picked_split
847        {
848            let highlight = if is_picked {
849                style.picked_split
850            } else {
851                style.hovered_split
852            };
853
854            renderer.fill_quad(
855                renderer::Quad {
856                    bounds: match axis {
857                        Axis::Horizontal => Rectangle {
858                            x: split_region.x,
859                            y: (split_region.y + (split_region.height - highlight.width) / 2.0)
860                                .round(),
861                            width: split_region.width,
862                            height: highlight.width,
863                        },
864                        Axis::Vertical => Rectangle {
865                            x: (split_region.x + (split_region.width - highlight.width) / 2.0)
866                                .round(),
867                            y: split_region.y,
868                            width: highlight.width,
869                            height: split_region.height,
870                        },
871                    },
872                    ..renderer::Quad::default()
873                },
874                highlight.color,
875            );
876        }
877    }
878
879    fn overlay<'b>(
880        &'b mut self,
881        tree: &'b mut Tree,
882        layout: Layout<'b>,
883        renderer: &Renderer,
884        viewport: &Rectangle,
885        translation: Vector,
886    ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
887        let state = tree.state.downcast_ref::<Memory>();
888        let picked_pane = state.action.picked_pane();
889
890        let children = self
891            .panes
892            .iter()
893            .copied()
894            .zip(&mut self.contents)
895            .zip(&mut tree.children)
896            .zip(layout.children())
897            .filter_map(|(((pane, content), tree), layout)| {
898                if self
899                    .internal
900                    .maximized()
901                    .is_some_and(|maximized| maximized != pane)
902                {
903                    return None;
904                }
905
906                if let Some((picked_pane, origin)) = picked_pane
907                    && picked_pane == pane
908                {
909                    return Some(overlay::Element::new(Box::new(PickedPane {
910                        origin,
911                        content,
912                        tree,
913                        layout,
914                    })));
915                }
916
917                content.overlay(tree, layout, renderer, viewport, translation)
918            })
919            .collect::<Vec<_>>();
920
921        (!children.is_empty()).then(|| Group::with_children(children).overlay())
922    }
923}
924
925struct PickedPane<'a, 'b, Message, Theme, Renderer>
926where
927    Theme: container::Catalog,
928    Renderer: core::Renderer,
929{
930    content: &'a Content<'b, Message, Theme, Renderer>,
931    origin: Point,
932    tree: &'a mut Tree,
933    layout: Layout<'a>,
934}
935
936impl<'a, 'b, Message, Theme, Renderer> core::Overlay<Message, Theme, Renderer>
937    for PickedPane<'a, 'b, Message, Theme, Renderer>
938where
939    Theme: container::Catalog,
940    Renderer: core::Renderer,
941{
942    fn layout(&mut self, _renderer: &Renderer, _bounds: Size) -> layout::Node {
943        // TODO: Mouse translation
944        layout::Node::new(self.layout.bounds().size()).move_to(self.origin)
945    }
946
947    fn draw(
948        &self,
949        renderer: &mut Renderer,
950        theme: &Theme,
951        style: &renderer::Style,
952        _layout: Layout<'_>,
953        cursor: mouse::Cursor,
954    ) {
955        let cursor_position = cursor.position().unwrap_or_default();
956
957        let translation = if is_dragging(self.origin, cursor_position) {
958            cursor_position - self.origin
959        } else {
960            Vector::ZERO
961        };
962
963        renderer.with_translation(translation, |renderer| {
964            self.content.draw(
965                self.tree,
966                renderer,
967                theme,
968                style,
969                self.layout,
970                mouse::Cursor::Unavailable,
971                &Rectangle::INFINITE,
972            );
973        });
974    }
975}
976
977fn is_dragging(origin: Point, cursor: Point) -> bool {
978    cursor.distance(origin) > DRAG_DEADBAND_DISTANCE
979}
980
981impl<'a, Message, Theme, Renderer> From<PaneGrid<'a, Message, Theme, Renderer>>
982    for Element<'a, Message, Theme, Renderer>
983where
984    Message: 'a,
985    Theme: Catalog + 'a,
986    Renderer: core::Renderer + 'a,
987{
988    fn from(
989        pane_grid: PaneGrid<'a, Message, Theme, Renderer>,
990    ) -> Element<'a, Message, Theme, Renderer> {
991        Element::new(pane_grid)
992    }
993}
994
995fn layout_region(layout: Layout<'_>, cursor_position: Point) -> Option<Region> {
996    let bounds = layout.bounds();
997
998    if !bounds.contains(cursor_position) {
999        return None;
1000    }
1001
1002    let region = if cursor_position.x < (bounds.x + bounds.width / 3.0) {
1003        Region::Edge(Edge::Left)
1004    } else if cursor_position.x > (bounds.x + 2.0 * bounds.width / 3.0) {
1005        Region::Edge(Edge::Right)
1006    } else if cursor_position.y < (bounds.y + bounds.height / 3.0) {
1007        Region::Edge(Edge::Top)
1008    } else if cursor_position.y > (bounds.y + 2.0 * bounds.height / 3.0) {
1009        Region::Edge(Edge::Bottom)
1010    } else {
1011        Region::Center
1012    };
1013
1014    Some(region)
1015}
1016
1017fn click_pane<'a, Message, T>(
1018    action: &mut state::Action,
1019    layout: Layout<'_>,
1020    cursor_position: Point,
1021    shell: &mut Shell<'_, Message>,
1022    contents: impl Iterator<Item = (Pane, T)>,
1023    on_click: &Option<Box<dyn Fn(Pane) -> Message + 'a>>,
1024    on_drag: &Option<Box<dyn Fn(DragEvent) -> Message + 'a>>,
1025) where
1026    T: Draggable,
1027{
1028    let mut clicked_region = contents
1029        .zip(layout.children())
1030        .filter(|(_, layout)| layout.bounds().contains(cursor_position));
1031
1032    if let Some(((pane, content), layout)) = clicked_region.next() {
1033        if let Some(on_click) = &on_click {
1034            shell.publish(on_click(pane));
1035        }
1036
1037        if let Some(on_drag) = &on_drag
1038            && content.can_be_dragged_at(layout, cursor_position)
1039        {
1040            *action = state::Action::Dragging {
1041                pane,
1042                origin: cursor_position,
1043            };
1044
1045            shell.publish(on_drag(DragEvent::Picked { pane }));
1046        }
1047    }
1048}
1049
1050fn in_edge(layout: Layout<'_>, cursor: Point) -> Option<Edge> {
1051    let bounds = layout.bounds();
1052
1053    let height_thickness = bounds.height / THICKNESS_RATIO;
1054    let width_thickness = bounds.width / THICKNESS_RATIO;
1055    let thickness = height_thickness.min(width_thickness);
1056
1057    if cursor.x > bounds.x && cursor.x < bounds.x + thickness {
1058        Some(Edge::Left)
1059    } else if cursor.x > bounds.x + bounds.width - thickness && cursor.x < bounds.x + bounds.width {
1060        Some(Edge::Right)
1061    } else if cursor.y > bounds.y && cursor.y < bounds.y + thickness {
1062        Some(Edge::Top)
1063    } else if cursor.y > bounds.y + bounds.height - thickness && cursor.y < bounds.y + bounds.height
1064    {
1065        Some(Edge::Bottom)
1066    } else {
1067        None
1068    }
1069}
1070
1071fn edge_bounds(layout: Layout<'_>, edge: Edge) -> Rectangle {
1072    let bounds = layout.bounds();
1073
1074    let height_thickness = bounds.height / THICKNESS_RATIO;
1075    let width_thickness = bounds.width / THICKNESS_RATIO;
1076    let thickness = height_thickness.min(width_thickness);
1077
1078    match edge {
1079        Edge::Top => Rectangle {
1080            height: thickness,
1081            ..bounds
1082        },
1083        Edge::Left => Rectangle {
1084            width: thickness,
1085            ..bounds
1086        },
1087        Edge::Right => Rectangle {
1088            x: bounds.x + bounds.width - thickness,
1089            width: thickness,
1090            ..bounds
1091        },
1092        Edge::Bottom => Rectangle {
1093            y: bounds.y + bounds.height - thickness,
1094            height: thickness,
1095            ..bounds
1096        },
1097    }
1098}
1099
1100fn layout_region_bounds(layout: Layout<'_>, region: Region) -> Rectangle {
1101    let bounds = layout.bounds();
1102
1103    match region {
1104        Region::Center => bounds,
1105        Region::Edge(edge) => match edge {
1106            Edge::Top => Rectangle {
1107                height: bounds.height / 2.0,
1108                ..bounds
1109            },
1110            Edge::Left => Rectangle {
1111                width: bounds.width / 2.0,
1112                ..bounds
1113            },
1114            Edge::Right => Rectangle {
1115                x: bounds.x + bounds.width / 2.0,
1116                width: bounds.width / 2.0,
1117                ..bounds
1118            },
1119            Edge::Bottom => Rectangle {
1120                y: bounds.y + bounds.height / 2.0,
1121                height: bounds.height / 2.0,
1122                ..bounds
1123            },
1124        },
1125    }
1126}
1127
1128/// An event produced during a drag and drop interaction of a [`PaneGrid`].
1129#[derive(Debug, Clone, Copy)]
1130pub enum DragEvent {
1131    /// A [`Pane`] was picked for dragging.
1132    Picked {
1133        /// The picked [`Pane`].
1134        pane: Pane,
1135    },
1136
1137    /// A [`Pane`] was dropped on top of another [`Pane`].
1138    Dropped {
1139        /// The picked [`Pane`].
1140        pane: Pane,
1141
1142        /// The [`Target`] where the picked [`Pane`] was dropped on.
1143        target: Target,
1144    },
1145
1146    /// A [`Pane`] was picked and then dropped outside of other [`Pane`]
1147    /// boundaries.
1148    Canceled {
1149        /// The picked [`Pane`].
1150        pane: Pane,
1151    },
1152}
1153
1154/// The [`Target`] area a pane can be dropped on.
1155#[derive(Debug, Clone, Copy)]
1156pub enum Target {
1157    /// An [`Edge`] of the full [`PaneGrid`].
1158    Edge(Edge),
1159    /// A single [`Pane`] of the [`PaneGrid`].
1160    Pane(Pane, Region),
1161}
1162
1163/// The region of a [`Pane`].
1164#[derive(Debug, Clone, Copy, Default)]
1165pub enum Region {
1166    /// Center region.
1167    #[default]
1168    Center,
1169    /// Edge region.
1170    Edge(Edge),
1171}
1172
1173/// The edges of an area.
1174#[derive(Debug, Clone, Copy)]
1175pub enum Edge {
1176    /// Top edge.
1177    Top,
1178    /// Left edge.
1179    Left,
1180    /// Right edge.
1181    Right,
1182    /// Bottom edge.
1183    Bottom,
1184}
1185
1186/// An event produced during a resize interaction of a [`PaneGrid`].
1187#[derive(Debug, Clone, Copy)]
1188pub struct ResizeEvent {
1189    /// The [`Split`] that is being dragged for resizing.
1190    pub split: Split,
1191
1192    /// The new ratio of the [`Split`].
1193    ///
1194    /// The ratio is a value in [0, 1], representing the exact position of a
1195    /// [`Split`] between two panes.
1196    pub ratio: f32,
1197}
1198
1199/*
1200 * Helpers
1201 */
1202fn hovered_split<'a>(
1203    mut splits: impl Iterator<Item = (&'a Split, &'a (Axis, Rectangle, f32))>,
1204    spacing: f32,
1205    cursor_position: Point,
1206) -> Option<(Split, Axis, Rectangle)> {
1207    splits.find_map(|(split, (axis, region, ratio))| {
1208        let bounds = axis.split_line_bounds(*region, *ratio, spacing);
1209
1210        if bounds.contains(cursor_position) {
1211            Some((*split, *axis, bounds))
1212        } else {
1213            None
1214        }
1215    })
1216}
1217
1218/// The appearance of a [`PaneGrid`].
1219#[derive(Debug, Clone, Copy, PartialEq)]
1220pub struct Style {
1221    /// The appearance of a hovered region highlight.
1222    pub hovered_region: Highlight,
1223    /// The appearance of a picked split.
1224    pub picked_split: Line,
1225    /// The appearance of a hovered split.
1226    pub hovered_split: Line,
1227}
1228
1229/// The appearance of a highlight of the [`PaneGrid`].
1230#[derive(Debug, Clone, Copy, PartialEq)]
1231pub struct Highlight {
1232    /// The [`Background`] of the pane region.
1233    pub background: Background,
1234    /// The [`Border`] of the pane region.
1235    pub border: Border,
1236}
1237
1238/// A line.
1239///
1240/// It is normally used to define the highlight of something, like a split.
1241#[derive(Debug, Clone, Copy, PartialEq)]
1242pub struct Line {
1243    /// The [`Color`] of the [`Line`].
1244    pub color: Color,
1245    /// The width of the [`Line`].
1246    pub width: f32,
1247}
1248
1249/// The theme catalog of a [`PaneGrid`].
1250pub trait Catalog: container::Catalog {
1251    /// The item class of this [`Catalog`].
1252    type Class<'a>;
1253
1254    /// The default class produced by this [`Catalog`].
1255    fn default<'a>() -> <Self as Catalog>::Class<'a>;
1256
1257    /// The [`Style`] of a class with the given status.
1258    fn style(&self, class: &<Self as Catalog>::Class<'_>) -> Style;
1259}
1260
1261/// A styling function for a [`PaneGrid`].
1262///
1263/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`.
1264pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
1265
1266impl Catalog for Theme {
1267    type Class<'a> = StyleFn<'a, Self>;
1268
1269    fn default<'a>() -> StyleFn<'a, Self> {
1270        Box::new(default)
1271    }
1272
1273    fn style(&self, class: &StyleFn<'_, Self>) -> Style {
1274        class(self)
1275    }
1276}
1277
1278/// The default style of a [`PaneGrid`].
1279pub fn default(theme: &Theme) -> Style {
1280    let palette = theme.palette();
1281
1282    Style {
1283        hovered_region: Highlight {
1284            background: Background::Color(Color {
1285                a: 0.5,
1286                ..palette.primary.base.color
1287            }),
1288            border: Border {
1289                width: 2.0,
1290                color: palette.primary.strong.color,
1291                radius: 0.0.into(),
1292            },
1293        },
1294        hovered_split: Line {
1295            color: palette.primary.base.color,
1296            width: 2.0,
1297        },
1298        picked_split: Line {
1299            color: palette.primary.strong.color,
1300            width: 2.0,
1301        },
1302    }
1303}