diff --git a/masonry/src/widget/flex.rs b/masonry/src/widget/flex.rs index c17e2b296..1f1a2ba2a 100644 --- a/masonry/src/widget/flex.rs +++ b/masonry/src/widget/flex.rs @@ -94,6 +94,29 @@ pub enum MainAxisAlignment { SpaceAround, } +struct Spacing { + alignment: MainAxisAlignment, + extra: f64, + n_children: usize, + index: usize, + equal_space: f64, + remainder: f64, +} + +enum Child { + Fixed { + widget: WidgetPod>, + alignment: Option, + }, + Flex { + widget: WidgetPod>, + alignment: Option, + flex: f64, + }, + FixedSpacer(f64, f64), + FlexedSpacer(f64, f64), +} + // --- MARK: IMPL FLEX --- impl Flex { /// Create a new Flex oriented along the provided axis. @@ -591,108 +614,356 @@ impl<'a> WidgetMut<'a, Flex> { } } -/// The size in logical pixels of the default spacer for an axis. -fn axis_default_spacer(axis: Axis) -> f64 { - match axis { - Axis::Vertical => crate::theme::WIDGET_PADDING_VERTICAL, - Axis::Horizontal => crate::theme::WIDGET_PADDING_HORIZONTAL, +// --- MARK: OTHER IMPLS--- +impl Axis { + /// Get the axis perpendicular to this one. + pub fn cross(self) -> Axis { + match self { + Axis::Horizontal => Axis::Vertical, + Axis::Vertical => Axis::Horizontal, + } } -} -fn new_flex_child(params: FlexParams, widget: WidgetPod>) -> Child { - if let Some(flex) = params.flex { - if flex.is_normal() && flex > 0.0 { - Child::Flex { - widget, - alignment: params.alignment, - flex, - } - } else { - tracing::warn!("Flex value should be > 0.0 (was {flex}). See the docs for masonry::widget::Flex for more information"); - Child::Fixed { - widget, - alignment: params.alignment, - } - } - } else { - Child::Fixed { - widget, - alignment: params.alignment, + /// Extract from the argument the magnitude along this axis + pub fn major(self, size: Size) -> f64 { + match self { + Axis::Horizontal => size.width, + Axis::Vertical => size.height, } } -} -// --- MARK: IMPL WIDGET--- -impl Widget for Flex { - fn on_pointer_event(&mut self, _ctx: &mut EventCtx, _event: &PointerEvent) {} + /// Extract from the argument the magnitude along the perpendicular axis + pub fn minor(self, size: Size) -> f64 { + self.cross().major(size) + } - fn on_text_event(&mut self, _ctx: &mut EventCtx, _event: &TextEvent) {} + /// Extract the extent of the argument in this axis as a pair. + pub fn major_span(self, rect: Rect) -> (f64, f64) { + match self { + Axis::Horizontal => (rect.x0, rect.x1), + Axis::Vertical => (rect.y0, rect.y1), + } + } - fn on_access_event(&mut self, _ctx: &mut EventCtx, _event: &AccessEvent) {} + /// Extract the extent of the argument in the minor axis as a pair. + pub fn minor_span(self, rect: Rect) -> (f64, f64) { + self.cross().major_span(rect) + } - fn on_status_change(&mut self, _ctx: &mut LifeCycleCtx, _event: &StatusChange) {} + /// Extract the coordinate locating the argument with respect to this axis. + pub fn major_pos(self, pos: Point) -> f64 { + match self { + Axis::Horizontal => pos.x, + Axis::Vertical => pos.y, + } + } - fn register_children(&mut self, ctx: &mut crate::RegisterCtx) { - for child in self.children.iter_mut().filter_map(|x| x.widget_mut()) { - ctx.register_child(child); + /// Extract the coordinate locating the argument with respect to this axis. + pub fn major_vec(self, vec: Vec2) -> f64 { + match self { + Axis::Horizontal => vec.x, + Axis::Vertical => vec.y, } } - fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size { - bc.debug_check("Flex"); - // we loosen our constraints when passing to children. - let loosened_bc = bc.loosen(); + /// Extract the coordinate locating the argument with respect to the perpendicular axis. + pub fn minor_pos(self, pos: Point) -> f64 { + self.cross().major_pos(pos) + } - // minor-axis values for all children - let mut minor = self.direction.minor(bc.min()); - // these two are calculated but only used if we're baseline aligned - let mut max_above_baseline = 0_f64; - let mut max_below_baseline = 0_f64; - let mut any_use_baseline = false; + /// Extract the coordinate locating the argument with respect to the perpendicular axis. + pub fn minor_vec(self, vec: Vec2) -> f64 { + self.cross().major_vec(vec) + } - // indicates that the box constrains for the following children have changed. Therefore they - // have to calculate layout again. - let bc_changed = self.old_bc != *bc; - let mut any_changed = bc_changed; - self.old_bc = *bc; + // TODO - make_pos, make_size, make_rect + /// Arrange the major and minor measurements with respect to this axis such that it forms + /// an (x, y) pair. + pub fn pack(self, major: f64, minor: f64) -> (f64, f64) { + match self { + Axis::Horizontal => (major, minor), + Axis::Vertical => (minor, major), + } + } - let gap = self.gap.unwrap_or(axis_default_spacer(self.direction)); - // The gaps are only between the items, so 2 children means 1 gap. - let total_gap = self.children.len().saturating_sub(1) as f64 * gap; - // Measure non-flex children. - let mut major_non_flex = total_gap; - let mut flex_sum = 0.0; - for child in &mut self.children { - match child { - Child::Fixed { widget, alignment } => { - // The BoxConstrains of fixed-children only depends on the BoxConstrains of the - // Flex widget. - let child_size = if bc_changed || ctx.child_needs_layout(widget) { - let alignment = alignment.unwrap_or(self.cross_alignment); - any_use_baseline |= alignment == CrossAxisAlignment::Baseline; + /// Generate constraints with new values on the major axis. + pub(crate) fn constraints( + self, + bc: &BoxConstraints, + min_major: f64, + major: f64, + ) -> BoxConstraints { + match self { + Axis::Horizontal => BoxConstraints::new( + Size::new(min_major, bc.min().height), + Size::new(major, bc.max().height), + ), + Axis::Vertical => BoxConstraints::new( + Size::new(bc.min().width, min_major), + Size::new(bc.max().width, major), + ), + } + } +} - let old_size = ctx.widget_state.layout_rect().size(); - let child_size = ctx.run_layout(widget, &loosened_bc); +impl FlexParams { + /// Create custom `FlexParams` with a specific `flex_factor` and an optional + /// [`CrossAxisAlignment`]. + /// + /// You likely only need to create these manually if you need to specify + /// a custom alignment; if you only need to use a custom `flex_factor` you + /// can pass an `f64` to any of the functions that take `FlexParams`. + /// + /// By default, the widget uses the alignment of its parent [`Flex`] container. + pub fn new( + flex: impl Into>, + alignment: impl Into>, + ) -> Self { + let flex = match flex.into() { + Some(flex) if flex <= 0.0 => { + debug_panic!("Flex value should be > 0.0. Flex given was: {}", flex); + Some(0.0) + } + other => other, + }; - if child_size.width.is_infinite() { - tracing::warn!("A non-Flex child has an infinite width."); - } + FlexParams { + flex, + alignment: alignment.into(), + } + } +} - if child_size.height.is_infinite() { - tracing::warn!("A non-Flex child has an infinite height."); - } +impl CrossAxisAlignment { + /// Given the difference between the size of the container and the size + /// of the child (on their minor axis) return the necessary offset for + /// this alignment. + fn align(self, val: f64) -> f64 { + match self { + CrossAxisAlignment::Start => 0.0, + // in vertical layout, baseline is equivalent to center + CrossAxisAlignment::Center | CrossAxisAlignment::Baseline => (val / 2.0).round(), + CrossAxisAlignment::End => val, + CrossAxisAlignment::Fill => 0.0, + } + } +} - if old_size != child_size { - any_changed = true; - } +impl Spacing { + /// Given the provided extra space and children count, + /// this returns an iterator of `f64` spacing, + /// where the first element is the spacing before any children + /// and all subsequent elements are the spacing after children. + fn new(alignment: MainAxisAlignment, extra: f64, n_children: usize) -> Spacing { + let extra = if extra.is_finite() { extra } else { 0. }; + let equal_space = if n_children > 0 { + match alignment { + MainAxisAlignment::Center => extra / 2., + MainAxisAlignment::SpaceBetween => extra / (n_children - 1).max(1) as f64, + MainAxisAlignment::SpaceEvenly => extra / (n_children + 1) as f64, + MainAxisAlignment::SpaceAround => extra / (2 * n_children) as f64, + _ => 0., + } + } else { + 0. + }; + Spacing { + alignment, + extra, + n_children, + index: 0, + equal_space, + remainder: 0., + } + } - child_size - } else { - ctx.skip_layout(widget); - ctx.child_layout_rect(widget).size() - }; + fn next_space(&mut self) -> f64 { + let desired_space = self.equal_space + self.remainder; + let actual_space = desired_space.round(); + self.remainder = desired_space - actual_space; + actual_space + } +} - let baseline_offset = ctx.child_baseline_offset(widget); +impl Iterator for Spacing { + type Item = f64; + + fn next(&mut self) -> Option { + if self.index > self.n_children { + return None; + } + let result = { + if self.n_children == 0 { + self.extra + } else { + #[allow(clippy::match_bool)] + match self.alignment { + MainAxisAlignment::Start => match self.index == self.n_children { + true => self.extra, + false => 0., + }, + MainAxisAlignment::End => match self.index == 0 { + true => self.extra, + false => 0., + }, + MainAxisAlignment::Center => match self.index { + 0 => self.next_space(), + i if i == self.n_children => self.next_space(), + _ => 0., + }, + MainAxisAlignment::SpaceBetween => match self.index { + 0 => 0., + i if i != self.n_children => self.next_space(), + _ => match self.n_children { + 1 => self.next_space(), + _ => 0., + }, + }, + MainAxisAlignment::SpaceEvenly => self.next_space(), + MainAxisAlignment::SpaceAround => { + if self.index == 0 || self.index == self.n_children { + self.next_space() + } else { + self.next_space() + self.next_space() + } + } + } + } + }; + self.index += 1; + Some(result) + } +} + +impl From for FlexParams { + fn from(flex: f64) -> FlexParams { + FlexParams::new(flex, None) + } +} + +impl From for FlexParams { + fn from(alignment: CrossAxisAlignment) -> FlexParams { + FlexParams::new(None, alignment) + } +} + +impl Child { + fn widget_mut(&mut self) -> Option<&mut WidgetPod>> { + match self { + Child::Fixed { widget, .. } | Child::Flex { widget, .. } => Some(widget), + _ => None, + } + } + fn widget(&self) -> Option<&WidgetPod>> { + match self { + Child::Fixed { widget, .. } | Child::Flex { widget, .. } => Some(widget), + _ => None, + } + } +} + +/// The size in logical pixels of the default spacer for an axis. +fn axis_default_spacer(axis: Axis) -> f64 { + match axis { + Axis::Vertical => crate::theme::WIDGET_PADDING_VERTICAL, + Axis::Horizontal => crate::theme::WIDGET_PADDING_HORIZONTAL, + } +} + +fn new_flex_child(params: FlexParams, widget: WidgetPod>) -> Child { + if let Some(flex) = params.flex { + if flex.is_normal() && flex > 0.0 { + Child::Flex { + widget, + alignment: params.alignment, + flex, + } + } else { + tracing::warn!("Flex value should be > 0.0 (was {flex}). See the docs for masonry::widget::Flex for more information"); + Child::Fixed { + widget, + alignment: params.alignment, + } + } + } else { + Child::Fixed { + widget, + alignment: params.alignment, + } + } +} + +// --- MARK: IMPL WIDGET--- +impl Widget for Flex { + fn on_pointer_event(&mut self, _ctx: &mut EventCtx, _event: &PointerEvent) {} + + fn on_text_event(&mut self, _ctx: &mut EventCtx, _event: &TextEvent) {} + + fn on_access_event(&mut self, _ctx: &mut EventCtx, _event: &AccessEvent) {} + + fn on_status_change(&mut self, _ctx: &mut LifeCycleCtx, _event: &StatusChange) {} + + fn register_children(&mut self, ctx: &mut crate::RegisterCtx) { + for child in self.children.iter_mut().filter_map(|x| x.widget_mut()) { + ctx.register_child(child); + } + } + + fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size { + bc.debug_check("Flex"); + // we loosen our constraints when passing to children. + let loosened_bc = bc.loosen(); + + // minor-axis values for all children + let mut minor = self.direction.minor(bc.min()); + // these two are calculated but only used if we're baseline aligned + let mut max_above_baseline = 0_f64; + let mut max_below_baseline = 0_f64; + let mut any_use_baseline = false; + + // indicates that the box constrains for the following children have changed. Therefore they + // have to calculate layout again. + let bc_changed = self.old_bc != *bc; + let mut any_changed = bc_changed; + self.old_bc = *bc; + + let gap = self.gap.unwrap_or(axis_default_spacer(self.direction)); + // The gaps are only between the items, so 2 children means 1 gap. + let total_gap = self.children.len().saturating_sub(1) as f64 * gap; + // Measure non-flex children. + let mut major_non_flex = total_gap; + let mut flex_sum = 0.0; + for child in &mut self.children { + match child { + Child::Fixed { widget, alignment } => { + // The BoxConstrains of fixed-children only depends on the BoxConstrains of the + // Flex widget. + let child_size = if bc_changed || ctx.child_needs_layout(widget) { + let alignment = alignment.unwrap_or(self.cross_alignment); + any_use_baseline |= alignment == CrossAxisAlignment::Baseline; + + let old_size = ctx.widget_state.layout_rect().size(); + let child_size = ctx.run_layout(widget, &loosened_bc); + + if child_size.width.is_infinite() { + tracing::warn!("A non-Flex child has an infinite width."); + } + + if child_size.height.is_infinite() { + tracing::warn!("A non-Flex child has an infinite height."); + } + + if old_size != child_size { + any_changed = true; + } + + child_size + } else { + ctx.skip_layout(widget); + ctx.child_layout_rect(widget).size() + }; + + let baseline_offset = ctx.child_baseline_offset(widget); major_non_flex += self.direction.major(child_size).expand(); minor = minor.max(self.direction.minor(child_size).expand()); @@ -927,277 +1198,6 @@ impl Widget for Flex { } } -// --- MARK: OTHER IMPLS--- -impl Axis { - /// Get the axis perpendicular to this one. - pub fn cross(self) -> Axis { - match self { - Axis::Horizontal => Axis::Vertical, - Axis::Vertical => Axis::Horizontal, - } - } - - /// Extract from the argument the magnitude along this axis - pub fn major(self, size: Size) -> f64 { - match self { - Axis::Horizontal => size.width, - Axis::Vertical => size.height, - } - } - - /// Extract from the argument the magnitude along the perpendicular axis - pub fn minor(self, size: Size) -> f64 { - self.cross().major(size) - } - - /// Extract the extent of the argument in this axis as a pair. - pub fn major_span(self, rect: Rect) -> (f64, f64) { - match self { - Axis::Horizontal => (rect.x0, rect.x1), - Axis::Vertical => (rect.y0, rect.y1), - } - } - - /// Extract the extent of the argument in the minor axis as a pair. - pub fn minor_span(self, rect: Rect) -> (f64, f64) { - self.cross().major_span(rect) - } - - /// Extract the coordinate locating the argument with respect to this axis. - pub fn major_pos(self, pos: Point) -> f64 { - match self { - Axis::Horizontal => pos.x, - Axis::Vertical => pos.y, - } - } - - /// Extract the coordinate locating the argument with respect to this axis. - pub fn major_vec(self, vec: Vec2) -> f64 { - match self { - Axis::Horizontal => vec.x, - Axis::Vertical => vec.y, - } - } - - /// Extract the coordinate locating the argument with respect to the perpendicular axis. - pub fn minor_pos(self, pos: Point) -> f64 { - self.cross().major_pos(pos) - } - - /// Extract the coordinate locating the argument with respect to the perpendicular axis. - pub fn minor_vec(self, vec: Vec2) -> f64 { - self.cross().major_vec(vec) - } - - // TODO - make_pos, make_size, make_rect - /// Arrange the major and minor measurements with respect to this axis such that it forms - /// an (x, y) pair. - pub fn pack(self, major: f64, minor: f64) -> (f64, f64) { - match self { - Axis::Horizontal => (major, minor), - Axis::Vertical => (minor, major), - } - } - - /// Generate constraints with new values on the major axis. - pub(crate) fn constraints( - self, - bc: &BoxConstraints, - min_major: f64, - major: f64, - ) -> BoxConstraints { - match self { - Axis::Horizontal => BoxConstraints::new( - Size::new(min_major, bc.min().height), - Size::new(major, bc.max().height), - ), - Axis::Vertical => BoxConstraints::new( - Size::new(bc.min().width, min_major), - Size::new(bc.max().width, major), - ), - } - } -} - -impl FlexParams { - /// Create custom `FlexParams` with a specific `flex_factor` and an optional - /// [`CrossAxisAlignment`]. - /// - /// You likely only need to create these manually if you need to specify - /// a custom alignment; if you only need to use a custom `flex_factor` you - /// can pass an `f64` to any of the functions that take `FlexParams`. - /// - /// By default, the widget uses the alignment of its parent [`Flex`] container. - pub fn new( - flex: impl Into>, - alignment: impl Into>, - ) -> Self { - let flex = match flex.into() { - Some(flex) if flex <= 0.0 => { - debug_panic!("Flex value should be > 0.0. Flex given was: {}", flex); - Some(0.0) - } - other => other, - }; - - FlexParams { - flex, - alignment: alignment.into(), - } - } -} - -impl CrossAxisAlignment { - /// Given the difference between the size of the container and the size - /// of the child (on their minor axis) return the necessary offset for - /// this alignment. - fn align(self, val: f64) -> f64 { - match self { - CrossAxisAlignment::Start => 0.0, - // in vertical layout, baseline is equivalent to center - CrossAxisAlignment::Center | CrossAxisAlignment::Baseline => (val / 2.0).round(), - CrossAxisAlignment::End => val, - CrossAxisAlignment::Fill => 0.0, - } - } -} - -struct Spacing { - alignment: MainAxisAlignment, - extra: f64, - n_children: usize, - index: usize, - equal_space: f64, - remainder: f64, -} - -impl Spacing { - /// Given the provided extra space and children count, - /// this returns an iterator of `f64` spacing, - /// where the first element is the spacing before any children - /// and all subsequent elements are the spacing after children. - fn new(alignment: MainAxisAlignment, extra: f64, n_children: usize) -> Spacing { - let extra = if extra.is_finite() { extra } else { 0. }; - let equal_space = if n_children > 0 { - match alignment { - MainAxisAlignment::Center => extra / 2., - MainAxisAlignment::SpaceBetween => extra / (n_children - 1).max(1) as f64, - MainAxisAlignment::SpaceEvenly => extra / (n_children + 1) as f64, - MainAxisAlignment::SpaceAround => extra / (2 * n_children) as f64, - _ => 0., - } - } else { - 0. - }; - Spacing { - alignment, - extra, - n_children, - index: 0, - equal_space, - remainder: 0., - } - } - - fn next_space(&mut self) -> f64 { - let desired_space = self.equal_space + self.remainder; - let actual_space = desired_space.round(); - self.remainder = desired_space - actual_space; - actual_space - } -} - -impl Iterator for Spacing { - type Item = f64; - - fn next(&mut self) -> Option { - if self.index > self.n_children { - return None; - } - let result = { - if self.n_children == 0 { - self.extra - } else { - #[allow(clippy::match_bool)] - match self.alignment { - MainAxisAlignment::Start => match self.index == self.n_children { - true => self.extra, - false => 0., - }, - MainAxisAlignment::End => match self.index == 0 { - true => self.extra, - false => 0., - }, - MainAxisAlignment::Center => match self.index { - 0 => self.next_space(), - i if i == self.n_children => self.next_space(), - _ => 0., - }, - MainAxisAlignment::SpaceBetween => match self.index { - 0 => 0., - i if i != self.n_children => self.next_space(), - _ => match self.n_children { - 1 => self.next_space(), - _ => 0., - }, - }, - MainAxisAlignment::SpaceEvenly => self.next_space(), - MainAxisAlignment::SpaceAround => { - if self.index == 0 || self.index == self.n_children { - self.next_space() - } else { - self.next_space() + self.next_space() - } - } - } - } - }; - self.index += 1; - Some(result) - } -} - -impl From for FlexParams { - fn from(flex: f64) -> FlexParams { - FlexParams::new(flex, None) - } -} - -impl From for FlexParams { - fn from(alignment: CrossAxisAlignment) -> FlexParams { - FlexParams::new(None, alignment) - } -} - -enum Child { - Fixed { - widget: WidgetPod>, - alignment: Option, - }, - Flex { - widget: WidgetPod>, - alignment: Option, - flex: f64, - }, - FixedSpacer(f64, f64), - FlexedSpacer(f64, f64), -} - -impl Child { - fn widget_mut(&mut self) -> Option<&mut WidgetPod>> { - match self { - Child::Fixed { widget, .. } | Child::Flex { widget, .. } => Some(widget), - _ => None, - } - } - fn widget(&self) -> Option<&WidgetPod>> { - match self { - Child::Fixed { widget, .. } | Child::Flex { widget, .. } => Some(widget), - _ => None, - } - } -} - // --- MARK: TESTS --- #[cfg(test)] mod tests {