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