diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs index 743eec0a..357def75 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/pane_grid.rs @@ -163,6 +163,7 @@ pub struct PaneGrid< width: Length, height: Length, spacing: f32, + min_size: f32, on_click: Option Message + 'a>>, on_drag: Option Message + 'a>>, on_resize: Option<(f32, Box Message + 'a>)>, @@ -200,6 +201,7 @@ where width: Length::Fill, height: Length::Fill, spacing: 0.0, + min_size: 50.0, on_click: None, on_drag: None, on_resize: None, @@ -226,6 +228,12 @@ where self } + /// Sets the minimum size of a [`Pane`] in the [`PaneGrid`] on both axes. + pub fn min_size(mut self, min_size: impl Into) -> Self { + self.min_size = min_size.into().0; + self + } + /// Sets the message that will be produced when a [`Pane`] of the /// [`PaneGrid`] is clicked. pub fn on_click(mut self, f: F) -> Self @@ -315,8 +323,11 @@ where let cursor_position = cursor.position()?; let bounds = layout.bounds(); - let splits = - node.split_regions(self.spacing, bounds.size()); + let splits = node.split_regions( + self.spacing, + self.min_size, + bounds.size(), + ); let relative_cursor = Point::new( cursor_position.x - bounds.x, @@ -412,8 +423,12 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let size = limits.resolve(self.width, self.height, Size::ZERO); - let regions = self.internal.layout().pane_regions(self.spacing, size); + let bounds = limits.resolve(self.width, self.height, Size::ZERO); + let regions = self.internal.layout().pane_regions( + self.spacing, + self.min_size, + bounds, + ); let children = self .panes @@ -443,7 +458,7 @@ where }) .collect(); - layout::Node::with_children(size, children) + layout::Node::with_children(bounds, children) } fn operate( @@ -531,7 +546,8 @@ where let splits = node.split_regions( self.spacing, - Size::new(bounds.width, bounds.height), + self.min_size, + bounds.size(), ); let clicked_split = hovered_split( @@ -640,7 +656,8 @@ where let splits = node.split_regions( self.spacing, - Size::new(bounds.width, bounds.height), + self.min_size, + bounds.size(), ); if let Some((axis, rectangle, _)) = splits.get(&split) { @@ -652,7 +669,7 @@ where - rectangle.y; (position / rectangle.height) - .clamp(0.1, 0.9) + .clamp(0.0, 1.0) } Axis::Vertical => { let position = cursor_position.x @@ -660,7 +677,7 @@ where - rectangle.x; (position / rectangle.width) - .clamp(0.1, 0.9) + .clamp(0.0, 1.0) } }; @@ -781,7 +798,11 @@ where .and_then(|(split, axis)| { let bounds = layout.bounds(); - let splits = node.split_regions(self.spacing, bounds.size()); + let splits = node.split_regions( + self.spacing, + self.min_size, + bounds.size(), + ); let (_axis, region, ratio) = splits.get(&split)?; @@ -800,8 +821,11 @@ where cursor_position.y - bounds.y, ); - let splits = - node.split_regions(self.spacing, bounds.size()); + let splits = node.split_regions( + self.spacing, + self.min_size, + bounds.size(), + ); let (_split, axis, region) = hovered_split( splits.iter(), diff --git a/widget/src/pane_grid/axis.rs b/widget/src/pane_grid/axis.rs index a3049230..e5bb7f97 100644 --- a/widget/src/pane_grid/axis.rs +++ b/widget/src/pane_grid/axis.rs @@ -17,12 +17,20 @@ impl Axis { rectangle: &Rectangle, ratio: f32, spacing: f32, - ) -> (Rectangle, Rectangle) { + min_size_a: f32, + min_size_b: f32, + ) -> (Rectangle, Rectangle, f32) { match self { Axis::Horizontal => { - let height_top = - (rectangle.height * ratio - spacing / 2.0).round(); - let height_bottom = rectangle.height - height_top - spacing; + let height_top = (rectangle.height * ratio - spacing / 2.0) + .round() + .max(min_size_a) + .min(rectangle.height - min_size_b - spacing); + + let height_bottom = + (rectangle.height - height_top - spacing).max(min_size_b); + + let ratio = (height_top + spacing / 2.0) / rectangle.height; ( Rectangle { @@ -34,12 +42,19 @@ impl Axis { height: height_bottom, ..*rectangle }, + ratio, ) } Axis::Vertical => { - let width_left = - (rectangle.width * ratio - spacing / 2.0).round(); - let width_right = rectangle.width - width_left - spacing; + let width_left = (rectangle.width * ratio - spacing / 2.0) + .round() + .max(min_size_a) + .min(rectangle.width - min_size_b - spacing); + + let width_right = + (rectangle.width - width_left - spacing).max(min_size_b); + + let ratio = (width_left + spacing / 2.0) / rectangle.width; ( Rectangle { @@ -51,6 +66,7 @@ impl Axis { width: width_right, ..*rectangle }, + ratio, ) } } @@ -187,7 +203,8 @@ mod tests { width: 10.0, height: overall_height, }; - let (top, bottom) = a.split(&r, 0.5, spacing); + let (top, bottom, _ratio) = + a.split(&r, 0.5, spacing, 0.0, 0.0); assert_eq!( top, Rectangle { @@ -218,7 +235,8 @@ mod tests { width: overall_width, height: 10.0, }; - let (left, right) = a.split(&r, 0.5, spacing); + let (left, right, _ratio) = + a.split(&r, 0.5, spacing, 0.0, 0.0); assert_eq!( left, Rectangle { diff --git a/widget/src/pane_grid/node.rs b/widget/src/pane_grid/node.rs index 1f568f95..83a655c7 100644 --- a/widget/src/pane_grid/node.rs +++ b/widget/src/pane_grid/node.rs @@ -29,6 +29,33 @@ pub enum Node { Pane(Pane), } +#[derive(Debug)] +enum Count { + Split { + horizontal: usize, + vertical: usize, + a: Box, + b: Box, + }, + Pane, +} + +impl Count { + fn horizontal(&self) -> usize { + match self { + Count::Split { horizontal, .. } => *horizontal, + Count::Pane => 0, + } + } + + fn vertical(&self) -> usize { + match self { + Count::Split { vertical, .. } => *vertical, + Count::Pane => 0, + } + } +} + impl Node { /// Returns an iterator over each [`Split`] in this [`Node`]. pub fn splits(&self) -> impl Iterator { @@ -48,23 +75,49 @@ impl Node { }) } + fn count(&self) -> Count { + match self { + Node::Split { a, b, axis, .. } => { + let a = a.count(); + let b = b.count(); + + let (horizontal, vertical) = match axis { + Axis::Horizontal => (1, 0), + Axis::Vertical => (0, 1), + }; + + Count::Split { + horizontal: a.horizontal() + b.horizontal() + horizontal, + vertical: a.vertical() + b.vertical() + vertical, + a: Box::new(a), + b: Box::new(b), + } + } + Node::Pane(_) => Count::Pane, + } + } + /// Returns the rectangular region for each [`Pane`] in the [`Node`] given /// the spacing between panes and the total available space. pub fn pane_regions( &self, spacing: f32, - size: Size, + min_size: f32, + bounds: Size, ) -> BTreeMap { let mut regions = BTreeMap::new(); + let count = self.count(); self.compute_regions( spacing, + min_size, &Rectangle { x: 0.0, y: 0.0, - width: size.width, - height: size.height, + width: bounds.width, + height: bounds.height, }, + &count, &mut regions, ); @@ -77,18 +130,22 @@ impl Node { pub fn split_regions( &self, spacing: f32, - size: Size, + min_size: f32, + bounds: Size, ) -> BTreeMap { let mut splits = BTreeMap::new(); + let count = self.count(); self.compute_splits( spacing, + min_size, &Rectangle { x: 0.0, y: 0.0, - width: size.width, - height: size.height, + width: bounds.width, + height: bounds.height, }, + &count, &mut splits, ); @@ -192,46 +249,104 @@ impl Node { fn compute_regions( &self, spacing: f32, + min_size: f32, current: &Rectangle, + count: &Count, regions: &mut BTreeMap, ) { - match self { - Node::Split { - axis, ratio, a, b, .. - } => { - let (region_a, region_b) = axis.split(current, *ratio, spacing); + match (self, count) { + ( + Node::Split { + axis, ratio, a, b, .. + }, + Count::Split { + a: count_a, + b: count_b, + .. + }, + ) => { + let (a_factor, b_factor) = match axis { + Axis::Horizontal => { + (count_a.horizontal(), count_b.horizontal()) + } + Axis::Vertical => (count_a.vertical(), count_b.vertical()), + }; - a.compute_regions(spacing, ®ion_a, regions); - b.compute_regions(spacing, ®ion_b, regions); + let (region_a, region_b, _ratio) = axis.split( + current, + *ratio, + spacing, + min_size * (a_factor + 1) as f32 + + spacing * a_factor as f32, + min_size * (b_factor + 1) as f32 + + spacing * b_factor as f32, + ); + + a.compute_regions( + spacing, min_size, ®ion_a, count_a, regions, + ); + b.compute_regions( + spacing, min_size, ®ion_b, count_b, regions, + ); } - Node::Pane(pane) => { + (Node::Pane(pane), Count::Pane) => { let _ = regions.insert(*pane, *current); } + _ => { + unreachable!("Node configuration and count do not match") + } } } fn compute_splits( &self, spacing: f32, + min_size: f32, current: &Rectangle, + count: &Count, splits: &mut BTreeMap, ) { - match self { - Node::Split { - axis, - ratio, - a, - b, - id, - } => { - let (region_a, region_b) = axis.split(current, *ratio, spacing); + match (self, count) { + ( + Node::Split { + axis, + ratio, + a, + b, + id, + }, + Count::Split { + a: count_a, + b: count_b, + .. + }, + ) => { + let (a_factor, b_factor) = match axis { + Axis::Horizontal => { + (count_a.horizontal(), count_b.horizontal()) + } + Axis::Vertical => (count_a.vertical(), count_b.vertical()), + }; - let _ = splits.insert(*id, (*axis, *current, *ratio)); + let (region_a, region_b, ratio) = axis.split( + current, + *ratio, + spacing, + min_size * (a_factor + 1) as f32 + + spacing * a_factor as f32, + min_size * (b_factor + 1) as f32 + + spacing * b_factor as f32, + ); - a.compute_splits(spacing, ®ion_a, splits); - b.compute_splits(spacing, ®ion_b, splits); + let _ = splits.insert(*id, (*axis, *current, ratio)); + + a.compute_splits(spacing, min_size, ®ion_a, count_a, splits); + b.compute_splits(spacing, min_size, ®ion_b, count_b, splits); + } + (Node::Pane(_), Count::Pane) => {} + _ => { + unreachable!("Node configuration and split count do not match") } - Node::Pane(_) => {} } } } diff --git a/widget/src/pane_grid/state.rs b/widget/src/pane_grid/state.rs index 2f8a64ea..d999434f 100644 --- a/widget/src/pane_grid/state.rs +++ b/widget/src/pane_grid/state.rs @@ -97,10 +97,11 @@ impl State { /// Returns the adjacent [`Pane`] of another [`Pane`] in the given /// direction, if there is one. pub fn adjacent(&self, pane: Pane, direction: Direction) -> Option { - let regions = self - .internal - .layout - .pane_regions(0.0, Size::new(4096.0, 4096.0)); + let regions = self.internal.layout.pane_regions( + 0.0, + 0.0, + Size::new(4096.0, 4096.0), + ); let current_region = regions.get(&pane)?;