iced_widget/
pane_grid.rs

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