Skip to main content

iced_widget/
pane_grid.rs

1//! Pane grids let your users split regions of your application and organize layout dynamically.
2//!
3//! ![Pane grid - Iced](https://iced.rs/examples/pane_grid.gif)
4//!
5//! This distribution of space is common in tiling window managers (like
6//! [`awesome`](https://awesomewm.org/), [`i3`](https://i3wm.org/), or even
7//! [`tmux`](https://github.com/tmux/tmux)).
8//!
9//! A [`PaneGrid`] supports:
10//!
11//! * Vertical and horizontal splits
12//! * Tracking of the last active pane
13//! * Mouse-based resizing
14//! * Drag and drop to reorganize panes
15//! * Hotkey support
16//! * Configurable modifier keys
17//! * [`State`] API to perform actions programmatically (`split`, `swap`, `resize`, etc.)
18//!
19//! # Example
20//! ```no_run
21//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
22//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
23//! #
24//! use iced::widget::{pane_grid, text};
25//!
26//! struct State {
27//!     panes: pane_grid::State<Pane>,
28//! }
29//!
30//! enum Pane {
31//!     SomePane,
32//!     AnotherKindOfPane,
33//! }
34//!
35//! enum Message {
36//!     PaneDragged(pane_grid::DragEvent),
37//!     PaneResized(pane_grid::ResizeEvent),
38//! }
39//!
40//! fn view(state: &State) -> Element<'_, Message> {
41//!     pane_grid(&state.panes, |pane, state, is_maximized| {
42//!         pane_grid::Content::new(match state {
43//!             Pane::SomePane => text("This is some pane"),
44//!             Pane::AnotherKindOfPane => text("This is another kind of pane"),
45//!         })
46//!     })
47//!     .on_drag(Message::PaneDragged)
48//!     .on_resize(10, Message::PaneResized)
49//!     .into()
50//! }
51//! ```
52//! The [`pane_grid` example] showcases how to use a [`PaneGrid`] with resizing,
53//! drag and drop, and hotkey support.
54//!
55//! [`pane_grid` example]: https://github.com/iced-rs/iced/tree/master/examples/pane_grid
56mod axis;
57mod configuration;
58mod content;
59mod controls;
60mod direction;
61mod draggable;
62mod node;
63mod pane;
64mod split;
65mod title_bar;
66
67pub mod state;
68
69pub use axis::Axis;
70pub use configuration::Configuration;
71pub use content::Content;
72pub use controls::Controls;
73pub use direction::Direction;
74pub use draggable::Draggable;
75pub use node::Node;
76pub use pane::Pane;
77pub use split::Split;
78pub use state::State;
79pub use title_bar::TitleBar;
80
81use crate::container;
82use crate::core::layout;
83use crate::core::mouse;
84use crate::core::overlay::{self, Group};
85use crate::core::renderer;
86use crate::core::touch;
87use crate::core::widget;
88use crate::core::widget::tree::{self, Tree};
89use crate::core::window;
90use crate::core::{
91    self, Background, Border, Color, Element, Event, Layout, Length, Pixels, Point, Rectangle,
92    Shell, Size, Theme, Vector, Widget,
93};
94
95const DRAG_DEADBAND_DISTANCE: f32 = 10.0;
96const THICKNESS_RATIO: f32 = 25.0;
97
98/// A collection of panes distributed using either vertical or horizontal splits
99/// to completely fill the space available.
100///
101/// ![Pane grid - Iced](https://iced.rs/examples/pane_grid.gif)
102///
103/// This distribution of space is common in tiling window managers (like
104/// [`awesome`](https://awesomewm.org/), [`i3`](https://i3wm.org/), or even
105/// [`tmux`](https://github.com/tmux/tmux)).
106///
107/// A [`PaneGrid`] supports:
108///
109/// * Vertical and horizontal splits
110/// * Tracking of the last active pane
111/// * Mouse-based resizing
112/// * Drag and drop to reorganize panes
113/// * Hotkey support
114/// * Configurable modifier keys
115/// * [`State`] API to perform actions programmatically (`split`, `swap`, `resize`, etc.)
116///
117/// # Example
118/// ```no_run
119/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
120/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
121/// #
122/// use iced::widget::{pane_grid, text};
123///
124/// struct State {
125///     panes: pane_grid::State<Pane>,
126/// }
127///
128/// enum Pane {
129///     SomePane,
130///     AnotherKindOfPane,
131/// }
132///
133/// enum Message {
134///     PaneDragged(pane_grid::DragEvent),
135///     PaneResized(pane_grid::ResizeEvent),
136/// }
137///
138/// fn view(state: &State) -> Element<'_, Message> {
139///     pane_grid(&state.panes, |pane, state, is_maximized| {
140///         pane_grid::Content::new(match state {
141///             Pane::SomePane => text("This is some pane"),
142///             Pane::AnotherKindOfPane => text("This is another kind of pane"),
143///         })
144///     })
145///     .on_drag(Message::PaneDragged)
146///     .on_resize(10, Message::PaneResized)
147///     .into()
148/// }
149/// ```
150pub struct PaneGrid<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
151where
152    Theme: Catalog,
153    Renderer: core::Renderer,
154{
155    internal: &'a state::Internal,
156    panes: Vec<Pane>,
157    contents: Vec<Content<'a, Message, Theme, Renderer>>,
158    width: Length,
159    height: Length,
160    spacing: f32,
161    min_size: f32,
162    on_click: Option<Box<dyn Fn(Pane) -> Message + 'a>>,
163    on_drag: Option<Box<dyn Fn(DragEvent) -> Message + 'a>>,
164    on_resize: Option<(f32, Box<dyn Fn(ResizeEvent) -> Message + 'a>)>,
165    class: <Theme as Catalog>::Class<'a>,
166    last_mouse_interaction: Option<mouse::Interaction>,
167}
168
169impl<'a, Message, Theme, Renderer> PaneGrid<'a, Message, Theme, Renderer>
170where
171    Theme: Catalog,
172    Renderer: core::Renderer,
173{
174    /// Creates a [`PaneGrid`] with the given [`State`] and view function.
175    ///
176    /// The view function will be called to display each [`Pane`] present in the
177    /// [`State`]. [`bool`] is set if the pane is maximized.
178    pub fn new<T>(
179        state: &'a State<T>,
180        view: impl Fn(Pane, &'a T, bool) -> Content<'a, Message, Theme, Renderer>,
181    ) -> Self {
182        let panes = state.panes.keys().copied().collect();
183        let contents = state
184            .panes
185            .iter()
186            .map(|(pane, pane_state)| match state.maximized() {
187                Some(p) if *pane == p => view(*pane, pane_state, true),
188                _ => view(*pane, pane_state, false),
189            })
190            .collect();
191
192        Self {
193            internal: &state.internal,
194            panes,
195            contents,
196            width: Length::Fill,
197            height: Length::Fill,
198            spacing: 0.0,
199            min_size: 50.0,
200            on_click: None,
201            on_drag: None,
202            on_resize: None,
203            class: <Theme as Catalog>::default(),
204            last_mouse_interaction: None,
205        }
206    }
207
208    /// Sets the width of the [`PaneGrid`].
209    pub fn width(mut self, width: impl Into<Length>) -> Self {
210        self.width = width.into();
211        self
212    }
213
214    /// Sets the height of the [`PaneGrid`].
215    pub fn height(mut self, height: impl Into<Length>) -> Self {
216        self.height = height.into();
217        self
218    }
219
220    /// Sets the spacing _between_ the panes of the [`PaneGrid`].
221    pub fn spacing(mut self, amount: impl Into<Pixels>) -> Self {
222        self.spacing = amount.into().0;
223        self
224    }
225
226    /// Sets the minimum size of a [`Pane`] in the [`PaneGrid`] on both axes.
227    pub fn min_size(mut self, min_size: impl Into<Pixels>) -> Self {
228        self.min_size = min_size.into().0;
229        self
230    }
231
232    /// Sets the message that will be produced when a [`Pane`] of the
233    /// [`PaneGrid`] is clicked.
234    pub fn on_click<F>(mut self, f: F) -> Self
235    where
236        F: 'a + Fn(Pane) -> Message,
237    {
238        self.on_click = Some(Box::new(f));
239        self
240    }
241
242    /// Enables the drag and drop interactions of the [`PaneGrid`], which will
243    /// use the provided function to produce messages.
244    pub fn on_drag<F>(mut self, f: F) -> Self
245    where
246        F: 'a + Fn(DragEvent) -> Message,
247    {
248        if self.internal.maximized().is_none() {
249            self.on_drag = Some(Box::new(f));
250        }
251        self
252    }
253
254    /// Enables the resize interactions of the [`PaneGrid`], which will
255    /// use the provided function to produce messages.
256    ///
257    /// The `leeway` describes the amount of space around a split that can be
258    /// used to grab it.
259    ///
260    /// The grabbable area of a split will have a length of `spacing + leeway`,
261    /// properly centered. In other words, a length of
262    /// `(spacing + leeway) / 2.0` on either side of the split line.
263    pub fn on_resize<F>(mut self, leeway: impl Into<Pixels>, f: F) -> Self
264    where
265        F: 'a + Fn(ResizeEvent) -> Message,
266    {
267        if self.internal.maximized().is_none() {
268            self.on_resize = Some((leeway.into().0, Box::new(f)));
269        }
270        self
271    }
272
273    /// Sets the style of the [`PaneGrid`].
274    #[must_use]
275    pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
276    where
277        <Theme as Catalog>::Class<'a>: From<StyleFn<'a, Theme>>,
278    {
279        self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
280        self
281    }
282
283    /// Sets the style class of the [`PaneGrid`].
284    #[cfg(feature = "advanced")]
285    #[must_use]
286    pub fn class(mut self, class: impl Into<<Theme as Catalog>::Class<'a>>) -> Self {
287        self.class = class.into();
288        self
289    }
290
291    fn drag_enabled(&self) -> bool {
292        if self.internal.maximized().is_none() {
293            self.on_drag.is_some()
294        } else {
295            Default::default()
296        }
297    }
298
299    fn grid_interaction(
300        &self,
301        action: &state::Action,
302        layout: Layout<'_>,
303        cursor: mouse::Cursor,
304    ) -> Option<mouse::Interaction> {
305        if action.picked_pane().is_some() {
306            return Some(mouse::Interaction::Grabbing);
307        }
308
309        let resize_leeway = self.on_resize.as_ref().map(|(leeway, _)| *leeway);
310        let node = self.internal.layout();
311
312        let resize_axis = action.picked_split().map(|(_, axis)| axis).or_else(|| {
313            resize_leeway.and_then(|leeway| {
314                let cursor_position = cursor.position()?;
315                let bounds = layout.bounds();
316
317                let splits = node.split_regions(self.spacing, self.min_size, bounds.size());
318
319                let relative_cursor =
320                    Point::new(cursor_position.x - bounds.x, cursor_position.y - bounds.y);
321
322                hovered_split(splits.iter(), self.spacing + leeway, relative_cursor)
323                    .map(|(_, axis, _)| axis)
324            })
325        });
326
327        if let Some(resize_axis) = resize_axis {
328            return Some(match resize_axis {
329                Axis::Horizontal => mouse::Interaction::ResizingVertically,
330                Axis::Vertical => mouse::Interaction::ResizingHorizontally,
331            });
332        }
333
334        None
335    }
336}
337
338#[derive(Default)]
339struct Memory {
340    action: state::Action,
341    order: Vec<Pane>,
342}
343
344impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
345    for PaneGrid<'_, Message, Theme, Renderer>
346where
347    Theme: Catalog,
348    Renderer: core::Renderer,
349{
350    fn tag(&self) -> tree::Tag {
351        tree::Tag::of::<Memory>()
352    }
353
354    fn state(&self) -> tree::State {
355        tree::State::new(Memory::default())
356    }
357
358    fn 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    ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
883        let state = tree.state.downcast_ref::<Memory>();
884        let picked_pane = state.action.picked_pane();
885
886        let children = self
887            .panes
888            .iter()
889            .copied()
890            .zip(&mut self.contents)
891            .zip(&mut tree.children)
892            .zip(layout.children())
893            .filter_map(|(((pane, content), tree), layout)| {
894                if self
895                    .internal
896                    .maximized()
897                    .is_some_and(|maximized| maximized != pane)
898                {
899                    return None;
900                }
901
902                if let Some((picked_pane, origin)) = picked_pane
903                    && picked_pane == pane
904                {
905                    return Some(overlay::Element::new(Box::new(PickedPane {
906                        origin,
907                        content,
908                        tree,
909                        layout,
910                    })));
911                }
912
913                content.overlay(tree, layout, renderer, viewport, translation)
914            })
915            .collect::<Vec<_>>();
916
917        (!children.is_empty()).then(|| Group::with_children(children).overlay())
918    }
919}
920
921struct PickedPane<'a, 'b, Message, Theme, Renderer>
922where
923    Theme: container::Catalog,
924    Renderer: core::Renderer,
925{
926    content: &'a Content<'b, Message, Theme, Renderer>,
927    origin: Point,
928    tree: &'a mut Tree,
929    layout: Layout<'a>,
930}
931
932impl<'a, 'b, Message, Theme, Renderer> core::Overlay<Message, Theme, Renderer>
933    for PickedPane<'a, 'b, Message, Theme, Renderer>
934where
935    Theme: container::Catalog,
936    Renderer: core::Renderer,
937{
938    fn layout(&mut self, _renderer: &Renderer, _bounds: Size) -> layout::Node {
939        // TODO: Mouse translation
940        layout::Node::new(self.layout.bounds().size()).move_to(self.origin)
941    }
942
943    fn draw(
944        &self,
945        renderer: &mut Renderer,
946        theme: &Theme,
947        style: &renderer::Style,
948        _layout: Layout<'_>,
949        cursor: mouse::Cursor,
950    ) {
951        let cursor_position = cursor.position().unwrap_or_default();
952
953        let translation = if is_dragging(self.origin, cursor_position) {
954            cursor_position - self.origin
955        } else {
956            Vector::ZERO
957        };
958
959        renderer.with_translation(translation, |renderer| {
960            self.content.draw(
961                self.tree,
962                renderer,
963                theme,
964                style,
965                self.layout,
966                mouse::Cursor::Unavailable,
967                &Rectangle::INFINITE,
968            );
969        });
970    }
971}
972
973fn is_dragging(origin: Point, cursor: Point) -> bool {
974    cursor.distance(origin) > DRAG_DEADBAND_DISTANCE
975}
976
977impl<'a, Message, Theme, Renderer> From<PaneGrid<'a, Message, Theme, Renderer>>
978    for Element<'a, Message, Theme, Renderer>
979where
980    Message: 'a,
981    Theme: Catalog + 'a,
982    Renderer: core::Renderer + 'a,
983{
984    fn from(
985        pane_grid: PaneGrid<'a, Message, Theme, Renderer>,
986    ) -> Element<'a, Message, Theme, Renderer> {
987        Element::new(pane_grid)
988    }
989}
990
991fn layout_region(layout: Layout<'_>, cursor_position: Point) -> Option<Region> {
992    let bounds = layout.bounds();
993
994    if !bounds.contains(cursor_position) {
995        return None;
996    }
997
998    let region = if cursor_position.x < (bounds.x + bounds.width / 3.0) {
999        Region::Edge(Edge::Left)
1000    } else if cursor_position.x > (bounds.x + 2.0 * bounds.width / 3.0) {
1001        Region::Edge(Edge::Right)
1002    } else if cursor_position.y < (bounds.y + bounds.height / 3.0) {
1003        Region::Edge(Edge::Top)
1004    } else if cursor_position.y > (bounds.y + 2.0 * bounds.height / 3.0) {
1005        Region::Edge(Edge::Bottom)
1006    } else {
1007        Region::Center
1008    };
1009
1010    Some(region)
1011}
1012
1013fn click_pane<'a, Message, T>(
1014    action: &mut state::Action,
1015    layout: Layout<'_>,
1016    cursor_position: Point,
1017    shell: &mut Shell<'_, Message>,
1018    contents: impl Iterator<Item = (Pane, T)>,
1019    on_click: &Option<Box<dyn Fn(Pane) -> Message + 'a>>,
1020    on_drag: &Option<Box<dyn Fn(DragEvent) -> Message + 'a>>,
1021) where
1022    T: Draggable,
1023{
1024    let mut clicked_region = contents
1025        .zip(layout.children())
1026        .filter(|(_, layout)| layout.bounds().contains(cursor_position));
1027
1028    if let Some(((pane, content), layout)) = clicked_region.next() {
1029        if let Some(on_click) = &on_click {
1030            shell.publish(on_click(pane));
1031        }
1032
1033        if let Some(on_drag) = &on_drag
1034            && content.can_be_dragged_at(layout, cursor_position)
1035        {
1036            *action = state::Action::Dragging {
1037                pane,
1038                origin: cursor_position,
1039            };
1040
1041            shell.publish(on_drag(DragEvent::Picked { pane }));
1042        }
1043    }
1044}
1045
1046fn in_edge(layout: Layout<'_>, cursor: Point) -> Option<Edge> {
1047    let bounds = layout.bounds();
1048
1049    let height_thickness = bounds.height / THICKNESS_RATIO;
1050    let width_thickness = bounds.width / THICKNESS_RATIO;
1051    let thickness = height_thickness.min(width_thickness);
1052
1053    if cursor.x > bounds.x && cursor.x < bounds.x + thickness {
1054        Some(Edge::Left)
1055    } else if cursor.x > bounds.x + bounds.width - thickness && cursor.x < bounds.x + bounds.width {
1056        Some(Edge::Right)
1057    } else if cursor.y > bounds.y && cursor.y < bounds.y + thickness {
1058        Some(Edge::Top)
1059    } else if cursor.y > bounds.y + bounds.height - thickness && cursor.y < bounds.y + bounds.height
1060    {
1061        Some(Edge::Bottom)
1062    } else {
1063        None
1064    }
1065}
1066
1067fn edge_bounds(layout: Layout<'_>, edge: Edge) -> Rectangle {
1068    let bounds = layout.bounds();
1069
1070    let height_thickness = bounds.height / THICKNESS_RATIO;
1071    let width_thickness = bounds.width / THICKNESS_RATIO;
1072    let thickness = height_thickness.min(width_thickness);
1073
1074    match edge {
1075        Edge::Top => Rectangle {
1076            height: thickness,
1077            ..bounds
1078        },
1079        Edge::Left => Rectangle {
1080            width: thickness,
1081            ..bounds
1082        },
1083        Edge::Right => Rectangle {
1084            x: bounds.x + bounds.width - thickness,
1085            width: thickness,
1086            ..bounds
1087        },
1088        Edge::Bottom => Rectangle {
1089            y: bounds.y + bounds.height - thickness,
1090            height: thickness,
1091            ..bounds
1092        },
1093    }
1094}
1095
1096fn layout_region_bounds(layout: Layout<'_>, region: Region) -> Rectangle {
1097    let bounds = layout.bounds();
1098
1099    match region {
1100        Region::Center => bounds,
1101        Region::Edge(edge) => match edge {
1102            Edge::Top => Rectangle {
1103                height: bounds.height / 2.0,
1104                ..bounds
1105            },
1106            Edge::Left => Rectangle {
1107                width: bounds.width / 2.0,
1108                ..bounds
1109            },
1110            Edge::Right => Rectangle {
1111                x: bounds.x + bounds.width / 2.0,
1112                width: bounds.width / 2.0,
1113                ..bounds
1114            },
1115            Edge::Bottom => Rectangle {
1116                y: bounds.y + bounds.height / 2.0,
1117                height: bounds.height / 2.0,
1118                ..bounds
1119            },
1120        },
1121    }
1122}
1123
1124/// An event produced during a drag and drop interaction of a [`PaneGrid`].
1125#[derive(Debug, Clone, Copy)]
1126pub enum DragEvent {
1127    /// A [`Pane`] was picked for dragging.
1128    Picked {
1129        /// The picked [`Pane`].
1130        pane: Pane,
1131    },
1132
1133    /// A [`Pane`] was dropped on top of another [`Pane`].
1134    Dropped {
1135        /// The picked [`Pane`].
1136        pane: Pane,
1137
1138        /// The [`Target`] where the picked [`Pane`] was dropped on.
1139        target: Target,
1140    },
1141
1142    /// A [`Pane`] was picked and then dropped outside of other [`Pane`]
1143    /// boundaries.
1144    Canceled {
1145        /// The picked [`Pane`].
1146        pane: Pane,
1147    },
1148}
1149
1150/// The [`Target`] area a pane can be dropped on.
1151#[derive(Debug, Clone, Copy)]
1152pub enum Target {
1153    /// An [`Edge`] of the full [`PaneGrid`].
1154    Edge(Edge),
1155    /// A single [`Pane`] of the [`PaneGrid`].
1156    Pane(Pane, Region),
1157}
1158
1159/// The region of a [`Pane`].
1160#[derive(Debug, Clone, Copy, Default)]
1161pub enum Region {
1162    /// Center region.
1163    #[default]
1164    Center,
1165    /// Edge region.
1166    Edge(Edge),
1167}
1168
1169/// The edges of an area.
1170#[derive(Debug, Clone, Copy)]
1171pub enum Edge {
1172    /// Top edge.
1173    Top,
1174    /// Left edge.
1175    Left,
1176    /// Right edge.
1177    Right,
1178    /// Bottom edge.
1179    Bottom,
1180}
1181
1182/// An event produced during a resize interaction of a [`PaneGrid`].
1183#[derive(Debug, Clone, Copy)]
1184pub struct ResizeEvent {
1185    /// The [`Split`] that is being dragged for resizing.
1186    pub split: Split,
1187
1188    /// The new ratio of the [`Split`].
1189    ///
1190    /// The ratio is a value in [0, 1], representing the exact position of a
1191    /// [`Split`] between two panes.
1192    pub ratio: f32,
1193}
1194
1195/*
1196 * Helpers
1197 */
1198fn hovered_split<'a>(
1199    mut splits: impl Iterator<Item = (&'a Split, &'a (Axis, Rectangle, f32))>,
1200    spacing: f32,
1201    cursor_position: Point,
1202) -> Option<(Split, Axis, Rectangle)> {
1203    splits.find_map(|(split, (axis, region, ratio))| {
1204        let bounds = axis.split_line_bounds(*region, *ratio, spacing);
1205
1206        if bounds.contains(cursor_position) {
1207            Some((*split, *axis, bounds))
1208        } else {
1209            None
1210        }
1211    })
1212}
1213
1214/// The appearance of a [`PaneGrid`].
1215#[derive(Debug, Clone, Copy, PartialEq)]
1216pub struct Style {
1217    /// The appearance of a hovered region highlight.
1218    pub hovered_region: Highlight,
1219    /// The appearance of a picked split.
1220    pub picked_split: Line,
1221    /// The appearance of a hovered split.
1222    pub hovered_split: Line,
1223}
1224
1225/// The appearance of a highlight of the [`PaneGrid`].
1226#[derive(Debug, Clone, Copy, PartialEq)]
1227pub struct Highlight {
1228    /// The [`Background`] of the pane region.
1229    pub background: Background,
1230    /// The [`Border`] of the pane region.
1231    pub border: Border,
1232}
1233
1234/// A line.
1235///
1236/// It is normally used to define the highlight of something, like a split.
1237#[derive(Debug, Clone, Copy, PartialEq)]
1238pub struct Line {
1239    /// The [`Color`] of the [`Line`].
1240    pub color: Color,
1241    /// The width of the [`Line`].
1242    pub width: f32,
1243}
1244
1245/// The theme catalog of a [`PaneGrid`].
1246pub trait Catalog: container::Catalog {
1247    /// The item class of this [`Catalog`].
1248    type Class<'a>;
1249
1250    /// The default class produced by this [`Catalog`].
1251    fn default<'a>() -> <Self as Catalog>::Class<'a>;
1252
1253    /// The [`Style`] of a class with the given status.
1254    fn style(&self, class: &<Self as Catalog>::Class<'_>) -> Style;
1255}
1256
1257/// A styling function for a [`PaneGrid`].
1258///
1259/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`.
1260pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
1261
1262impl Catalog for Theme {
1263    type Class<'a> = StyleFn<'a, Self>;
1264
1265    fn default<'a>() -> StyleFn<'a, Self> {
1266        Box::new(default)
1267    }
1268
1269    fn style(&self, class: &StyleFn<'_, Self>) -> Style {
1270        class(self)
1271    }
1272}
1273
1274/// The default style of a [`PaneGrid`].
1275pub fn default(theme: &Theme) -> Style {
1276    let palette = theme.palette();
1277
1278    Style {
1279        hovered_region: Highlight {
1280            background: Background::Color(Color {
1281                a: 0.5,
1282                ..palette.primary.base.color
1283            }),
1284            border: Border {
1285                width: 2.0,
1286                color: palette.primary.strong.color,
1287                radius: 0.0.into(),
1288            },
1289        },
1290        hovered_split: Line {
1291            color: palette.primary.base.color,
1292            width: 2.0,
1293        },
1294        picked_split: Line {
1295            color: palette.primary.strong.color,
1296            width: 2.0,
1297        },
1298    }
1299}