diff --git a/src/widgets/toplevels.rs b/src/widgets/toplevels/mod.rs similarity index 61% rename from src/widgets/toplevels.rs rename to src/widgets/toplevels/mod.rs index 0f3d438..e1777e0 100644 --- a/src/widgets/toplevels.rs +++ b/src/widgets/toplevels/mod.rs @@ -6,61 +6,34 @@ use cosmic::iced::{ Clipboard, Layout, Shell, Widget, }, event::{self, Event}, - Length, Point, Rectangle, Size, + Length, Rectangle, Size, Vector, }; use std::marker::PhantomData; -// Duplicate of private methods -trait AxisExt { - fn main(&self, size: Size) -> f32; - fn cross(&self, size: Size) -> f32; - fn pack(&self, main: f32, cross: f32) -> (f32, f32); -} - -impl AxisExt for Axis { - fn main(&self, size: Size) -> f32 { - match self { - Axis::Horizontal => size.width, - Axis::Vertical => size.height, - } - } - - fn cross(&self, size: Size) -> f32 { - match self { - Axis::Horizontal => size.height, - Axis::Vertical => size.width, - } - } - - fn pack(&self, main: f32, cross: f32) -> (f32, f32) { - match self { - Axis::Horizontal => (main, cross), - Axis::Vertical => (cross, main), - } - } -} +mod toplevel_layout; +use toplevel_layout::{LayoutToplevel, RowColToplevelLayout, ToplevelLayout}; pub fn toplevels(children: Vec>) -> Toplevels { Toplevels { - axis: Axis::Horizontal, + layout: RowColToplevelLayout { + // TODO configurable + spacing: 16, + axis: Axis::Horizontal, + }, children, _msg: PhantomData, } } pub struct Toplevels<'a, Msg> { - axis: Axis, + layout: RowColToplevelLayout, children: Vec>, _msg: PhantomData, } impl<'a, Msg> Widget for Toplevels<'a, Msg> { fn size(&self) -> Size { - Size { - width: Length::Fill, - // TODO Make depend on orientation or drop that option - height: Length::Shrink, - } + self.layout.size() } fn layout( @@ -69,60 +42,42 @@ impl<'a, Msg> Widget for Toplevels<'a, Msg renderer: &cosmic::Renderer, limits: &layout::Limits, ) -> layout::Node { - // TODO configurable - let spacing = 16; - - // Get total requested main axis length if widget could have all the space - let total_spacing = spacing * (self.children.len().saturating_sub(1)).max(0); - let requested_mains = self + // Call `.layout()` on each child with full limits to determine "preferred" sizes + let layout_toplevels = self .children .iter() .zip(tree.children.iter_mut()) .map(|(child, tree)| { - let child_limits = layout::Limits::new(Size::ZERO, limits.max()); - let layout = child.as_widget().layout(tree, renderer, &child_limits); - self.axis.main(layout.size()) + let preferred_size = child.as_widget().layout(tree, renderer, limits).size(); + LayoutToplevel { + preferred_size, + _phantom_data: PhantomData, + } }) .collect::>(); - let requested_main_total: f32 = requested_mains.iter().sum::() + total_spacing as f32; - let scale_factor = (self.axis.main(limits.max()) / requested_main_total).min(1.0); + // Assign rectangles for each child using `ToplevelLayout` backend + let assigned_rects = self.layout.layout(limits.max(), &layout_toplevels); - let max_cross = self.axis.cross(limits.max()); - - // XXX sill allocating maximum main axis? - // - what was it doing before? - let mut total_main = 0.0; - let mut first = true; let nodes = self .children .iter() .zip(tree.children.iter_mut()) - .zip(requested_mains.iter()) - .map(|((child, tree), requested_main)| { - if !first { - total_main += spacing as f32; - } - first = false; + .zip(assigned_rects) + .map(|((child, tree), assigned_rect)| { + let child_limits = layout::Limits::new(Size::ZERO, assigned_rect.size()); + let layout = child.as_widget().layout(tree, renderer, &child_limits); - let max_main = requested_main * scale_factor; + // Center on both axes, if child didn't consume full size allocation + let centering_offset = Vector::new( + ((assigned_rect.size().width - layout.size().width) / 2.).max(0.), + ((assigned_rect.size().height - layout.size().height) / 2.).max(0.), + ); - let (max_width, max_height) = self.axis.pack(max_main, max_cross); - let child_limits = - layout::Limits::new(Size::ZERO, Size::new(max_width, max_height)); - let mut layout = child.as_widget().layout(tree, renderer, &child_limits); - // Center on cross axis - let cross = ((max_cross - self.axis.cross(layout.size())) / 2.).max(0.); - let (x, y) = self.axis.pack(total_main, cross); - layout = layout.move_to(Point::new(x, y)); - total_main += self.axis.main(layout.size()); - layout + layout.move_to(assigned_rect.position() + centering_offset) }) .collect(); - - let (total_width, total_height) = self.axis.pack(total_main, max_cross); - let size = Size::new(total_width, total_height); - layout::Node::with_children(size, nodes) + layout::Node::with_children(limits.max(), nodes) } fn operate( diff --git a/src/widgets/toplevels/toplevel_layout/mod.rs b/src/widgets/toplevels/toplevel_layout/mod.rs new file mode 100644 index 0000000..2de767c --- /dev/null +++ b/src/widgets/toplevels/toplevel_layout/mod.rs @@ -0,0 +1,35 @@ +// TODO: More generic widget in libcosmic? Improve iced layout system? +// - preferred_size concept + +use cosmic::iced::{Length, Rectangle, Size}; +use std::marker::PhantomData; + +mod row_col_toplevel_layout; +mod utils; +pub(crate) use row_col_toplevel_layout::RowColToplevelLayout; + +pub(crate) struct LayoutToplevel<'a> { + //toplevel: &'a crate::Toplevel, + /// Preferred size of the child widget, if it fill the parent container + pub preferred_size: Size, + pub _phantom_data: PhantomData<&'a crate::Toplevel>, +} + +/// An implementor of this trait defines a layout for the [`Toplevels`] widget +/// as a pure function, without dealing with all the details of the iced layout +/// system. +pub(crate) trait ToplevelLayout { + /// [`Size`] the container widget should request + fn size(&self) -> Size; + /// Decide size and location of each widget + /// + /// - `max_limit` is the total size available for all children + /// - For each entry in `toplevels`, this should yield one `Rectangle` + /// + /// If a child doesn't use it's entire rectangle, it will be centered in that space. + fn layout( + &self, + max_limit: Size, + toplevels: &[LayoutToplevel<'_>], + ) -> impl Iterator; +} diff --git a/src/widgets/toplevels/toplevel_layout/row_col_toplevel_layout.rs b/src/widgets/toplevels/toplevel_layout/row_col_toplevel_layout.rs new file mode 100644 index 0000000..73e1235 --- /dev/null +++ b/src/widgets/toplevels/toplevel_layout/row_col_toplevel_layout.rs @@ -0,0 +1,51 @@ +use cosmic::iced::{advanced::layout::flex::Axis, Length, Point, Rectangle, Size}; + +use super::{utils::AxisExt, LayoutToplevel, ToplevelLayout}; + +pub(crate) struct RowColToplevelLayout { + pub axis: Axis, + pub spacing: u32, +} + +impl ToplevelLayout for RowColToplevelLayout { + fn size(&self) -> Size { + Size { + width: Length::Fill, + // TODO Make depend on orientation or drop that option + height: Length::Shrink, + } + } + + fn layout( + &self, + max_limit: Size, + toplevels: &[LayoutToplevel<'_>], + ) -> impl Iterator { + // Get total requested main axis length if widget could have all the space + let total_spacing = self.spacing as usize * (toplevels.len().saturating_sub(1)).max(0); + let requested_main_total: f32 = toplevels + .iter() + .map(|t| self.axis.main(t.preferred_size)) + .sum::() + + total_spacing as f32; + let scale_factor = (self.axis.main(max_limit) / requested_main_total).min(1.0); + let max_cross = self.axis.cross(max_limit); + + let mut total_main = 0.0; + let mut first = true; + toplevels.iter().map(move |child| { + let requested_main = self.axis.main(child.preferred_size); + if !first { + total_main += self.spacing as f32; + } + first = false; + + let max_main = requested_main * scale_factor; + + let (width, height) = self.axis.pack(max_main, max_cross); + let (x, y) = self.axis.pack(total_main, 0.0); + total_main += max_main; + Rectangle::new(Point::new(x, y), Size::new(width, height)) + }) + } +} diff --git a/src/widgets/toplevels/toplevel_layout/utils.rs b/src/widgets/toplevels/toplevel_layout/utils.rs new file mode 100644 index 0000000..5a4ee1d --- /dev/null +++ b/src/widgets/toplevels/toplevel_layout/utils.rs @@ -0,0 +1,31 @@ +use cosmic::iced::{advanced::layout::flex::Axis, Size}; + +// Duplicate of private methods +pub(super) trait AxisExt { + fn main(&self, size: Size) -> f32; + fn cross(&self, size: Size) -> f32; + fn pack(&self, main: f32, cross: f32) -> (f32, f32); +} + +impl AxisExt for Axis { + fn main(&self, size: Size) -> f32 { + match self { + Axis::Horizontal => size.width, + Axis::Vertical => size.height, + } + } + + fn cross(&self, size: Size) -> f32 { + match self { + Axis::Horizontal => size.height, + Axis::Vertical => size.width, + } + } + + fn pack(&self, main: f32, cross: f32) -> (f32, f32) { + match self { + Axis::Horizontal => (main, cross), + Axis::Vertical => (cross, main), + } + } +}