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