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