summaryrefslogtreecommitdiffstats
path: root/native/src/widget/pane_grid
diff options
context:
space:
mode:
Diffstat (limited to 'native/src/widget/pane_grid')
-rw-r--r--native/src/widget/pane_grid/axis.rs54
-rw-r--r--native/src/widget/pane_grid/direction.rs12
-rw-r--r--native/src/widget/pane_grid/node.rs199
-rw-r--r--native/src/widget/pane_grid/pane.rs5
-rw-r--r--native/src/widget/pane_grid/split.rs5
-rw-r--r--native/src/widget/pane_grid/state.rs368
6 files changed, 643 insertions, 0 deletions
diff --git a/native/src/widget/pane_grid/axis.rs b/native/src/widget/pane_grid/axis.rs
new file mode 100644
index 00000000..f0e3f362
--- /dev/null
+++ b/native/src/widget/pane_grid/axis.rs
@@ -0,0 +1,54 @@
+use crate::Rectangle;
+
+/// A fixed reference line for the measurement of coordinates.
+#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
+pub enum Axis {
+ /// The horizontal axis: —
+ Horizontal,
+ /// The vertical axis: |
+ Vertical,
+}
+
+impl Axis {
+ pub(super) fn split(
+ &self,
+ rectangle: &Rectangle,
+ ratio: f32,
+ halved_spacing: f32,
+ ) -> (Rectangle, Rectangle) {
+ match self {
+ Axis::Horizontal => {
+ let height_top = (rectangle.height * ratio).round();
+ let height_bottom = rectangle.height - height_top;
+
+ (
+ Rectangle {
+ height: height_top - halved_spacing,
+ ..*rectangle
+ },
+ Rectangle {
+ y: rectangle.y + height_top + halved_spacing,
+ height: height_bottom - halved_spacing,
+ ..*rectangle
+ },
+ )
+ }
+ Axis::Vertical => {
+ let width_left = (rectangle.width * ratio).round();
+ let width_right = rectangle.width - width_left;
+
+ (
+ Rectangle {
+ width: width_left - halved_spacing,
+ ..*rectangle
+ },
+ Rectangle {
+ x: rectangle.x + width_left + halved_spacing,
+ width: width_right - halved_spacing,
+ ..*rectangle
+ },
+ )
+ }
+ }
+ }
+}
diff --git a/native/src/widget/pane_grid/direction.rs b/native/src/widget/pane_grid/direction.rs
new file mode 100644
index 00000000..b31a8737
--- /dev/null
+++ b/native/src/widget/pane_grid/direction.rs
@@ -0,0 +1,12 @@
+/// A four cardinal direction.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum Direction {
+ /// ↑
+ Up,
+ /// ↓
+ Down,
+ /// ←
+ Left,
+ /// →
+ Right,
+}
diff --git a/native/src/widget/pane_grid/node.rs b/native/src/widget/pane_grid/node.rs
new file mode 100644
index 00000000..4d5970b8
--- /dev/null
+++ b/native/src/widget/pane_grid/node.rs
@@ -0,0 +1,199 @@
+use crate::{
+ pane_grid::{Axis, Pane, Split},
+ Rectangle, Size,
+};
+
+use std::collections::HashMap;
+
+#[derive(Debug, Clone, Hash)]
+pub enum Node {
+ Split {
+ id: Split,
+ axis: Axis,
+ ratio: u32,
+ a: Box<Node>,
+ b: Box<Node>,
+ },
+ Pane(Pane),
+}
+
+impl Node {
+ pub fn find(&mut self, pane: &Pane) -> Option<&mut Node> {
+ match self {
+ Node::Split { a, b, .. } => {
+ a.find(pane).or_else(move || b.find(pane))
+ }
+ Node::Pane(p) => {
+ if p == pane {
+ Some(self)
+ } else {
+ None
+ }
+ }
+ }
+ }
+
+ pub fn split(&mut self, id: Split, axis: Axis, new_pane: Pane) {
+ *self = Node::Split {
+ id,
+ axis,
+ ratio: 500_000,
+ a: Box::new(self.clone()),
+ b: Box::new(Node::Pane(new_pane)),
+ };
+ }
+
+ pub fn update(&mut self, f: &impl Fn(&mut Node)) {
+ match self {
+ Node::Split { a, b, .. } => {
+ a.update(f);
+ b.update(f);
+ }
+ _ => {}
+ }
+
+ f(self);
+ }
+
+ pub fn resize(&mut self, split: &Split, percentage: f32) -> bool {
+ match self {
+ Node::Split {
+ id, ratio, a, b, ..
+ } => {
+ if id == split {
+ *ratio = (percentage * 1_000_000.0).round() as u32;
+
+ true
+ } else if a.resize(split, percentage) {
+ true
+ } else {
+ b.resize(split, percentage)
+ }
+ }
+ Node::Pane(_) => false,
+ }
+ }
+
+ pub fn remove(&mut self, pane: &Pane) -> Option<Pane> {
+ match self {
+ Node::Split { a, b, .. } => {
+ if a.pane() == Some(*pane) {
+ *self = *b.clone();
+ Some(self.first_pane())
+ } else if b.pane() == Some(*pane) {
+ *self = *a.clone();
+ Some(self.first_pane())
+ } else {
+ a.remove(pane).or_else(|| b.remove(pane))
+ }
+ }
+ Node::Pane(_) => None,
+ }
+ }
+
+ pub fn regions(
+ &self,
+ spacing: f32,
+ size: Size,
+ ) -> HashMap<Pane, Rectangle> {
+ let mut regions = HashMap::new();
+
+ self.compute_regions(
+ spacing / 2.0,
+ &Rectangle {
+ x: 0.0,
+ y: 0.0,
+ width: size.width,
+ height: size.height,
+ },
+ &mut regions,
+ );
+
+ regions
+ }
+
+ pub fn splits(
+ &self,
+ spacing: f32,
+ size: Size,
+ ) -> HashMap<Split, (Axis, Rectangle, f32)> {
+ let mut splits = HashMap::new();
+
+ self.compute_splits(
+ spacing / 2.0,
+ &Rectangle {
+ x: 0.0,
+ y: 0.0,
+ width: size.width,
+ height: size.height,
+ },
+ &mut splits,
+ );
+
+ splits
+ }
+
+ pub fn pane(&self) -> Option<Pane> {
+ match self {
+ Node::Split { .. } => None,
+ Node::Pane(pane) => Some(*pane),
+ }
+ }
+
+ pub fn first_pane(&self) -> Pane {
+ match self {
+ Node::Split { a, .. } => a.first_pane(),
+ Node::Pane(pane) => *pane,
+ }
+ }
+
+ fn compute_regions(
+ &self,
+ halved_spacing: f32,
+ current: &Rectangle,
+ regions: &mut HashMap<Pane, Rectangle>,
+ ) {
+ match self {
+ Node::Split {
+ axis, ratio, a, b, ..
+ } => {
+ let ratio = *ratio as f32 / 1_000_000.0;
+ let (region_a, region_b) =
+ axis.split(current, ratio, halved_spacing);
+
+ a.compute_regions(halved_spacing, &region_a, regions);
+ b.compute_regions(halved_spacing, &region_b, regions);
+ }
+ Node::Pane(pane) => {
+ let _ = regions.insert(*pane, *current);
+ }
+ }
+ }
+
+ fn compute_splits(
+ &self,
+ halved_spacing: f32,
+ current: &Rectangle,
+ splits: &mut HashMap<Split, (Axis, Rectangle, f32)>,
+ ) {
+ match self {
+ Node::Split {
+ axis,
+ ratio,
+ a,
+ b,
+ id,
+ } => {
+ let ratio = *ratio as f32 / 1_000_000.0;
+ let (region_a, region_b) =
+ axis.split(current, ratio, halved_spacing);
+
+ let _ = splits.insert(*id, (*axis, *current, ratio));
+
+ a.compute_splits(halved_spacing, &region_a, splits);
+ b.compute_splits(halved_spacing, &region_b, splits);
+ }
+ Node::Pane(_) => {}
+ }
+ }
+}
diff --git a/native/src/widget/pane_grid/pane.rs b/native/src/widget/pane_grid/pane.rs
new file mode 100644
index 00000000..f9866407
--- /dev/null
+++ b/native/src/widget/pane_grid/pane.rs
@@ -0,0 +1,5 @@
+/// A rectangular region in a [`PaneGrid`] used to display widgets.
+///
+/// [`PaneGrid`]: struct.PaneGrid.html
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub struct Pane(pub(super) usize);
diff --git a/native/src/widget/pane_grid/split.rs b/native/src/widget/pane_grid/split.rs
new file mode 100644
index 00000000..d020c510
--- /dev/null
+++ b/native/src/widget/pane_grid/split.rs
@@ -0,0 +1,5 @@
+/// A divider that splits a region in a [`PaneGrid`] into two different panes.
+///
+/// [`PaneGrid`]: struct.PaneGrid.html
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub struct Split(pub(super) usize);
diff --git a/native/src/widget/pane_grid/state.rs b/native/src/widget/pane_grid/state.rs
new file mode 100644
index 00000000..0a8b8419
--- /dev/null
+++ b/native/src/widget/pane_grid/state.rs
@@ -0,0 +1,368 @@
+use crate::{
+ input::keyboard,
+ pane_grid::{node::Node, Axis, Direction, Pane, Split},
+ Hasher, Point, Rectangle, Size,
+};
+
+use std::collections::HashMap;
+
+/// The state of a [`PaneGrid`].
+///
+/// It keeps track of the state of each [`Pane`] and the position of each
+/// [`Split`].
+///
+/// The [`State`] needs to own any mutable contents a [`Pane`] may need. This is
+/// why this struct is generic over the type `T`. Values of this type are
+/// provided to the view function of [`PaneGrid::new`] for displaying each
+/// [`Pane`].
+///
+/// [`PaneGrid`]: struct.PaneGrid.html
+/// [`PaneGrid::new`]: struct.PaneGrid.html#method.new
+/// [`Pane`]: struct.Pane.html
+/// [`Split`]: struct.Split.html
+/// [`State`]: struct.State.html
+#[derive(Debug)]
+pub struct State<T> {
+ pub(super) panes: HashMap<Pane, T>,
+ pub(super) internal: Internal,
+ pub(super) modifiers: keyboard::ModifiersState,
+}
+
+/// The current focus of a [`Pane`].
+///
+/// [`Pane`]: struct.Pane.html
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum Focus {
+ /// The [`Pane`] is just focused.
+ ///
+ /// [`Pane`]: struct.Pane.html
+ Idle,
+
+ /// The [`Pane`] is being dragged.
+ ///
+ /// [`Pane`]: struct.Pane.html
+ Dragging,
+}
+
+impl<T> State<T> {
+ /// Creates a new [`State`], initializing the first pane with the provided
+ /// state.
+ ///
+ /// Alongside the [`State`], it returns the first [`Pane`] identifier.
+ ///
+ /// [`State`]: struct.State.html
+ /// [`Pane`]: struct.Pane.html
+ pub fn new(first_pane_state: T) -> (Self, Pane) {
+ let first_pane = Pane(0);
+
+ let mut panes = HashMap::new();
+ let _ = panes.insert(first_pane, first_pane_state);
+
+ (
+ State {
+ panes,
+ internal: Internal {
+ layout: Node::Pane(first_pane),
+ last_id: 0,
+ action: Action::Idle { focus: None },
+ },
+ modifiers: keyboard::ModifiersState::default(),
+ },
+ first_pane,
+ )
+ }
+
+ /// Returns the total amount of panes in the [`State`].
+ ///
+ /// [`State`]: struct.State.html
+ pub fn len(&self) -> usize {
+ self.panes.len()
+ }
+
+ /// Returns the internal state of the given [`Pane`], if it exists.
+ ///
+ /// [`Pane`]: struct.Pane.html
+ pub fn get_mut(&mut self, pane: &Pane) -> Option<&mut T> {
+ self.panes.get_mut(pane)
+ }
+
+ /// Returns an iterator over all the panes of the [`State`], alongside its
+ /// internal state.
+ ///
+ /// [`State`]: struct.State.html
+ pub fn iter(&self) -> impl Iterator<Item = (&Pane, &T)> {
+ self.panes.iter()
+ }
+
+ /// Returns a mutable iterator over all the panes of the [`State`],
+ /// alongside its internal state.
+ ///
+ /// [`State`]: struct.State.html
+ pub fn iter_mut(&mut self) -> impl Iterator<Item = (&Pane, &mut T)> {
+ self.panes.iter_mut()
+ }
+
+ /// Returns the active [`Pane`] of the [`State`], if there is one.
+ ///
+ /// A [`Pane`] is active if it is focused and is __not__ being dragged.
+ ///
+ /// [`Pane`]: struct.Pane.html
+ /// [`State`]: struct.State.html
+ pub fn active(&self) -> Option<Pane> {
+ self.internal.active_pane()
+ }
+
+ /// Returns the adjacent [`Pane`] of another [`Pane`] in the given
+ /// direction, if there is one.
+ ///
+ /// ## Example
+ /// You can combine this with [`State::active`] to find the pane that is
+ /// adjacent to the current active one, and then swap them. For instance:
+ ///
+ /// ```
+ /// # use iced_native::pane_grid;
+ /// #
+ /// # let (mut state, _) = pane_grid::State::new(());
+ /// #
+ /// if let Some(active) = state.active() {
+ /// if let Some(adjacent) = state.adjacent(&active, pane_grid::Direction::Right) {
+ /// state.swap(&active, &adjacent);
+ /// }
+ /// }
+ /// ```
+ ///
+ /// [`Pane`]: struct.Pane.html
+ /// [`State::active`]: struct.State.html#method.active
+ pub fn adjacent(&self, pane: &Pane, direction: Direction) -> Option<Pane> {
+ let regions =
+ self.internal.layout.regions(0.0, Size::new(4096.0, 4096.0));
+
+ let current_region = regions.get(pane)?;
+
+ let target = match direction {
+ Direction::Left => {
+ Point::new(current_region.x - 1.0, current_region.y + 1.0)
+ }
+ Direction::Right => Point::new(
+ current_region.x + current_region.width + 1.0,
+ current_region.y + 1.0,
+ ),
+ Direction::Up => {
+ Point::new(current_region.x + 1.0, current_region.y - 1.0)
+ }
+ Direction::Down => Point::new(
+ current_region.x + 1.0,
+ current_region.y + current_region.height + 1.0,
+ ),
+ };
+
+ let mut colliding_regions =
+ regions.iter().filter(|(_, region)| region.contains(target));
+
+ let (pane, _) = colliding_regions.next()?;
+
+ Some(*pane)
+ }
+
+ /// Focuses the given [`Pane`].
+ ///
+ /// [`Pane`]: struct.Pane.html
+ pub fn focus(&mut self, pane: &Pane) {
+ self.internal.focus(pane);
+ }
+
+ /// Splits the given [`Pane`] into two in the given [`Axis`] and
+ /// initializing the new [`Pane`] with the provided internal state.
+ ///
+ /// [`Pane`]: struct.Pane.html
+ /// [`Axis`]: enum.Axis.html
+ pub fn split(&mut self, axis: Axis, pane: &Pane, state: T) -> Option<Pane> {
+ let node = self.internal.layout.find(pane)?;
+
+ let new_pane = {
+ self.internal.last_id = self.internal.last_id.checked_add(1)?;
+
+ Pane(self.internal.last_id)
+ };
+
+ let new_split = {
+ self.internal.last_id = self.internal.last_id.checked_add(1)?;
+
+ Split(self.internal.last_id)
+ };
+
+ node.split(new_split, axis, new_pane);
+
+ let _ = self.panes.insert(new_pane, state);
+ self.focus(&new_pane);
+
+ Some(new_pane)
+ }
+
+ /// Swaps the position of the provided panes in the [`State`].
+ ///
+ /// If you want to swap panes on drag and drop in your [`PaneGrid`], you
+ /// will need to call this method when handling a [`DragEvent`].
+ ///
+ /// [`State`]: struct.State.html
+ /// [`PaneGrid`]: struct.PaneGrid.html
+ /// [`DragEvent`]: struct.DragEvent.html
+ pub fn swap(&mut self, a: &Pane, b: &Pane) {
+ self.internal.layout.update(&|node| match node {
+ Node::Split { .. } => {}
+ Node::Pane(pane) => {
+ if pane == a {
+ *node = Node::Pane(*b);
+ } else if pane == b {
+ *node = Node::Pane(*a);
+ }
+ }
+ });
+ }
+
+ /// Resizes two panes by setting the position of the provided [`Split`].
+ ///
+ /// The ratio is a value in [0, 1], representing the exact position of a
+ /// [`Split`] between two panes.
+ ///
+ /// If you want to enable resize interactions in your [`PaneGrid`], you will
+ /// need to call this method when handling a [`ResizeEvent`].
+ ///
+ /// [`Split`]: struct.Split.html
+ /// [`PaneGrid`]: struct.PaneGrid.html
+ /// [`ResizeEvent`]: struct.ResizeEvent.html
+ pub fn resize(&mut self, split: &Split, ratio: f32) {
+ let _ = self.internal.layout.resize(split, ratio);
+ }
+
+ /// Closes the given [`Pane`] and returns its internal state, if it exists.
+ ///
+ /// [`Pane`]: struct.Pane.html
+ pub fn close(&mut self, pane: &Pane) -> Option<T> {
+ if let Some(sibling) = self.internal.layout.remove(pane) {
+ self.focus(&sibling);
+ self.panes.remove(pane)
+ } else {
+ None
+ }
+ }
+}
+
+#[derive(Debug)]
+pub struct Internal {
+ layout: Node,
+ last_id: usize,
+ action: Action,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum Action {
+ Idle {
+ focus: Option<Pane>,
+ },
+ Dragging {
+ pane: Pane,
+ },
+ Resizing {
+ split: Split,
+ axis: Axis,
+ focus: Option<Pane>,
+ },
+}
+
+impl Action {
+ pub fn focus(&self) -> Option<(Pane, Focus)> {
+ match self {
+ Action::Idle { focus } | Action::Resizing { focus, .. } => {
+ focus.map(|pane| (pane, Focus::Idle))
+ }
+ Action::Dragging { pane } => Some((*pane, Focus::Dragging)),
+ }
+ }
+}
+
+impl Internal {
+ pub fn action(&self) -> Action {
+ self.action
+ }
+
+ pub fn active_pane(&self) -> Option<Pane> {
+ match self.action {
+ Action::Idle { focus } => focus,
+ _ => None,
+ }
+ }
+
+ pub fn picked_pane(&self) -> Option<Pane> {
+ match self.action {
+ Action::Dragging { pane } => Some(pane),
+ _ => None,
+ }
+ }
+
+ pub fn picked_split(&self) -> Option<(Split, Axis)> {
+ match self.action {
+ Action::Resizing { split, axis, .. } => Some((split, axis)),
+ _ => None,
+ }
+ }
+
+ pub fn regions(
+ &self,
+ spacing: f32,
+ size: Size,
+ ) -> HashMap<Pane, Rectangle> {
+ self.layout.regions(spacing, size)
+ }
+
+ pub fn splits(
+ &self,
+ spacing: f32,
+ size: Size,
+ ) -> HashMap<Split, (Axis, Rectangle, f32)> {
+ self.layout.splits(spacing, size)
+ }
+
+ pub fn focus(&mut self, pane: &Pane) {
+ self.action = Action::Idle { focus: Some(*pane) };
+ }
+
+ pub fn pick_pane(&mut self, pane: &Pane) {
+ self.action = Action::Dragging { pane: *pane };
+ }
+
+ pub fn pick_split(&mut self, split: &Split, axis: Axis) {
+ // TODO: Obtain `axis` from layout itself. Maybe we should implement
+ // `Node::find_split`
+ if self.picked_pane().is_some() {
+ return;
+ }
+
+ let focus = self.action.focus().map(|(pane, _)| pane);
+
+ self.action = Action::Resizing {
+ split: *split,
+ axis,
+ focus,
+ };
+ }
+
+ pub fn drop_split(&mut self) {
+ match self.action {
+ Action::Resizing { focus, .. } => {
+ self.action = Action::Idle { focus };
+ }
+ _ => {}
+ }
+ }
+
+ pub fn unfocus(&mut self) {
+ self.action = Action::Idle { focus: None };
+ }
+
+ pub fn hash_layout(&self, hasher: &mut Hasher) {
+ use std::hash::Hash;
+
+ self.layout.hash(hasher);
+ }
+}