Skip to main content

iced_widget/
pane_grid.rs

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