mirror of https://github.com/linebender/xilem
1704 lines
60 KiB
Rust
1704 lines
60 KiB
Rust
// Copyright 2018 the Xilem Authors and the Druid Authors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
//! A widget that arranges its children in a one-dimensional array.
|
|
|
|
use accesskit::{Node, Role};
|
|
use smallvec::SmallVec;
|
|
use tracing::{Span, trace_span};
|
|
use vello::Scene;
|
|
use vello::kurbo::common::FloatExt;
|
|
use vello::kurbo::{Affine, Line, Stroke, Vec2};
|
|
|
|
use crate::core::{
|
|
AccessCtx, AccessEvent, BoxConstraints, EventCtx, LayoutCtx, PaintCtx, PointerEvent,
|
|
PropertiesMut, PropertiesRef, QueryCtx, TextEvent, Widget, WidgetId, WidgetMut, WidgetPod,
|
|
};
|
|
use crate::kurbo::{Point, Rect, Size};
|
|
|
|
/// A container with either horizontal or vertical layout.
|
|
///
|
|
/// This widget is the foundation of most layouts, and is highly configurable.
|
|
///
|
|
#[doc = crate::include_screenshot!("widget/screenshots/masonry__widget__flex__tests__col_main_axis_spaceAround.png", "Flex column with multiple labels.")]
|
|
pub struct Flex {
|
|
direction: Axis,
|
|
cross_alignment: CrossAxisAlignment,
|
|
main_alignment: MainAxisAlignment,
|
|
fill_major_axis: bool,
|
|
children: Vec<Child>,
|
|
old_bc: BoxConstraints,
|
|
gap: Option<f64>,
|
|
}
|
|
|
|
/// Optional parameters for an item in a [`Flex`] container (row or column).
|
|
///
|
|
/// Generally, when you would like to add a flexible child to a container,
|
|
/// you can simply call [`with_flex_child`](Flex::with_flex_child) or [`add_flex_child`](Flex::add_flex_child),
|
|
/// passing the child and the desired flex factor as a `f64`, which has an impl of
|
|
/// `Into<FlexParams>`.
|
|
#[derive(Default, Debug, Copy, Clone, PartialEq)]
|
|
pub struct FlexParams {
|
|
flex: Option<f64>,
|
|
alignment: Option<CrossAxisAlignment>,
|
|
}
|
|
|
|
/// An axis in visual space.
|
|
///
|
|
/// Most often used by widgets to describe
|
|
/// the direction in which they grow as their number of children increases.
|
|
/// Has some methods for manipulating geometry with respect to the axis.
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
pub enum Axis {
|
|
/// The x axis
|
|
Horizontal,
|
|
/// The y axis
|
|
Vertical,
|
|
}
|
|
|
|
/// The alignment of the widgets on the container's cross (or minor) axis.
|
|
///
|
|
/// If a widget is smaller than the container on the minor axis, this determines
|
|
/// where it is positioned.
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
pub enum CrossAxisAlignment {
|
|
/// Top or leading.
|
|
Start,
|
|
/// Widgets are centered in the container.
|
|
Center,
|
|
/// Bottom or trailing.
|
|
End,
|
|
/// Align on the baseline.
|
|
Baseline,
|
|
/// Fill the available space.
|
|
Fill,
|
|
}
|
|
|
|
/// Arrangement of children on the main axis.
|
|
///
|
|
/// If there is surplus space on the main axis after laying out children, this
|
|
/// enum represents how children are laid out in this space.
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
pub enum MainAxisAlignment {
|
|
/// Top or leading.
|
|
Start,
|
|
/// Children are centered, without padding.
|
|
Center,
|
|
/// Bottom or trailing.
|
|
End,
|
|
/// Extra space is divided evenly between each child.
|
|
SpaceBetween,
|
|
/// Extra space is divided evenly between each child, as well as at the ends.
|
|
SpaceEvenly,
|
|
/// Space between each child, with less at the start and end.
|
|
SpaceAround,
|
|
}
|
|
|
|
struct Spacing {
|
|
alignment: MainAxisAlignment,
|
|
extra: f64,
|
|
n_children: usize,
|
|
index: usize,
|
|
equal_space: f64,
|
|
remainder: f64,
|
|
}
|
|
|
|
enum Child {
|
|
Fixed {
|
|
widget: WidgetPod<dyn Widget>,
|
|
alignment: Option<CrossAxisAlignment>,
|
|
},
|
|
Flex {
|
|
widget: WidgetPod<dyn Widget>,
|
|
alignment: Option<CrossAxisAlignment>,
|
|
flex: f64,
|
|
},
|
|
FixedSpacer(f64, f64),
|
|
FlexedSpacer(f64, f64),
|
|
}
|
|
|
|
// --- MARK: IMPL FLEX ---
|
|
impl Flex {
|
|
/// Create a new Flex oriented along the provided axis.
|
|
pub fn for_axis(axis: Axis) -> Self {
|
|
Self {
|
|
direction: axis,
|
|
children: Vec::new(),
|
|
cross_alignment: CrossAxisAlignment::Center,
|
|
main_alignment: MainAxisAlignment::Start,
|
|
fill_major_axis: false,
|
|
old_bc: BoxConstraints::tight(Size::ZERO),
|
|
gap: None,
|
|
}
|
|
}
|
|
|
|
/// Create a new horizontal stack.
|
|
///
|
|
/// The child widgets are laid out horizontally, from left to right.
|
|
///
|
|
pub fn row() -> Self {
|
|
Self::for_axis(Axis::Horizontal)
|
|
}
|
|
|
|
/// Create a new vertical stack.
|
|
///
|
|
/// The child widgets are laid out vertically, from top to bottom.
|
|
pub fn column() -> Self {
|
|
Self::for_axis(Axis::Vertical)
|
|
}
|
|
|
|
/// Builder-style method for specifying the childrens' [`CrossAxisAlignment`].
|
|
pub fn cross_axis_alignment(mut self, alignment: CrossAxisAlignment) -> Self {
|
|
self.cross_alignment = alignment;
|
|
self
|
|
}
|
|
|
|
/// Builder-style method for specifying the childrens' [`MainAxisAlignment`].
|
|
pub fn main_axis_alignment(mut self, alignment: MainAxisAlignment) -> Self {
|
|
self.main_alignment = alignment;
|
|
self
|
|
}
|
|
|
|
/// Builder-style method for setting whether the container must expand
|
|
/// to fill the available space on its main axis.
|
|
pub fn must_fill_main_axis(mut self, fill: bool) -> Self {
|
|
self.fill_major_axis = fill;
|
|
self
|
|
}
|
|
|
|
/// Builder-style method for setting the spacing along the
|
|
/// major axis between any two elements in logical pixels.
|
|
///
|
|
/// Equivalent to the css [gap] property.
|
|
/// This gap is also present between spacers.
|
|
///
|
|
/// See also [`default_gap`](Self::default_gap).
|
|
///
|
|
/// ## Panics
|
|
///
|
|
/// If `gap` is not a non-negative finite value.
|
|
///
|
|
/// [gap]: https://developer.mozilla.org/en-US/docs/Web/CSS/gap
|
|
// TODO: Semantics - should this include fixed spacers?
|
|
pub fn gap(mut self, gap: f64) -> Self {
|
|
if gap.is_finite() && gap >= 0.0 {
|
|
self.gap = Some(gap);
|
|
} else {
|
|
panic!("Invalid `gap` {gap}, expected a non-negative finite value.")
|
|
}
|
|
self
|
|
}
|
|
|
|
/// Builder-style method to use the default gap value.
|
|
///
|
|
/// This is [`WIDGET_PADDING_VERTICAL`] for a flex column and
|
|
/// [`WIDGET_PADDING_HORIZONTAL`] for flex row.
|
|
///
|
|
/// See also [`gap`](Self::gap)
|
|
///
|
|
/// [`WIDGET_PADDING_VERTICAL`]: crate::theme::WIDGET_PADDING_VERTICAL
|
|
/// [`WIDGET_PADDING_HORIZONTAL`]: crate::theme::WIDGET_PADDING_VERTICAL
|
|
pub fn default_gap(mut self) -> Self {
|
|
self.gap = None;
|
|
self
|
|
}
|
|
|
|
/// Equivalent to [`gap`](Self::gap) if `gap` is `Some`, or
|
|
/// [`default_gap`](Self::default_gap) otherwise.
|
|
///
|
|
/// Does not perform validation of the provided value.
|
|
pub fn raw_gap(mut self, gap: Option<f64>) -> Self {
|
|
self.gap = gap;
|
|
self
|
|
}
|
|
|
|
/// Builder-style variant of [`Flex::add_child`].
|
|
///
|
|
/// Convenient for assembling a group of widgets in a single expression.
|
|
pub fn with_child(self, child: impl Widget) -> Self {
|
|
self.with_child_pod(WidgetPod::new(child).erased())
|
|
}
|
|
|
|
/// Builder-style variant of [`Flex::add_child`], that takes the id that the child will have.
|
|
///
|
|
/// Useful for unit tests.
|
|
pub fn with_child_id(self, child: impl Widget, id: WidgetId) -> Self {
|
|
self.with_child_pod(WidgetPod::new_with_id(child, id).erased())
|
|
}
|
|
|
|
/// Builder-style method for [adding](Flex::add_child) a type-erased child to this.
|
|
pub fn with_child_pod(mut self, widget: WidgetPod<dyn Widget>) -> Self {
|
|
let child = Child::Fixed {
|
|
widget,
|
|
alignment: None,
|
|
};
|
|
self.children.push(child);
|
|
self
|
|
}
|
|
|
|
/// Builder-style method to add a flexible child to the container.
|
|
pub fn with_flex_child(self, child: impl Widget, params: impl Into<FlexParams>) -> Self {
|
|
self.with_flex_child_pod(WidgetPod::new(child).erased(), params)
|
|
}
|
|
|
|
/// Builder-style method to add a flexible child to the container.
|
|
pub fn with_flex_child_pod(
|
|
mut self,
|
|
widget: WidgetPod<dyn Widget>,
|
|
params: impl Into<FlexParams>,
|
|
) -> Self {
|
|
// TODO - dedup?
|
|
let params: FlexParams = params.into();
|
|
|
|
let child = new_flex_child(params, widget);
|
|
self.children.push(child);
|
|
self
|
|
}
|
|
|
|
/// Builder-style method to add a spacer widget with a standard size.
|
|
///
|
|
/// The actual value of this spacer depends on whether this container is
|
|
/// a row or column, as well as theme settings.
|
|
pub fn with_default_spacer(self) -> Self {
|
|
let key = axis_default_spacer(self.direction);
|
|
self.with_spacer(key)
|
|
}
|
|
|
|
/// Builder-style method for adding a fixed-size spacer to the container.
|
|
///
|
|
/// If you are laying out standard controls in this container, you should
|
|
/// generally prefer to use [`add_default_spacer`].
|
|
///
|
|
/// [`add_default_spacer`]: Flex::add_default_spacer
|
|
pub fn with_spacer(mut self, mut len: f64) -> Self {
|
|
if len < 0.0 {
|
|
tracing::warn!("add_spacer called with negative length: {}", len);
|
|
}
|
|
len = len.clamp(0.0, f64::MAX);
|
|
|
|
let new_child = Child::FixedSpacer(len, 0.0);
|
|
self.children.push(new_child);
|
|
self
|
|
}
|
|
|
|
/// Builder-style method for adding a `flex` spacer to the container.
|
|
pub fn with_flex_spacer(mut self, flex: f64) -> Self {
|
|
let flex = if flex >= 0.0 {
|
|
flex
|
|
} else {
|
|
debug_panic!("add_spacer called with negative length: {}", flex);
|
|
0.0
|
|
};
|
|
let new_child = Child::FlexedSpacer(flex, 0.0);
|
|
self.children.push(new_child);
|
|
self
|
|
}
|
|
|
|
/// Returns the number of children (widgets and spacers) this flex container has.
|
|
pub fn len(&self) -> usize {
|
|
self.children.len()
|
|
}
|
|
|
|
/// Returns `true` if this flex container has no children (widgets or spacers).
|
|
pub fn is_empty(&self) -> bool {
|
|
self.len() == 0
|
|
}
|
|
}
|
|
|
|
// --- MARK: WIDGETMUT---
|
|
impl Flex {
|
|
/// Set the flex direction (see [`Axis`]).
|
|
pub fn set_direction(this: &mut WidgetMut<'_, Self>, direction: Axis) {
|
|
this.widget.direction = direction;
|
|
this.ctx.request_layout();
|
|
}
|
|
|
|
/// Set the childrens' [`CrossAxisAlignment`].
|
|
pub fn set_cross_axis_alignment(this: &mut WidgetMut<'_, Self>, alignment: CrossAxisAlignment) {
|
|
this.widget.cross_alignment = alignment;
|
|
this.ctx.request_layout();
|
|
}
|
|
|
|
/// Set the childrens' [`MainAxisAlignment`].
|
|
pub fn set_main_axis_alignment(this: &mut WidgetMut<'_, Self>, alignment: MainAxisAlignment) {
|
|
this.widget.main_alignment = alignment;
|
|
this.ctx.request_layout();
|
|
}
|
|
|
|
/// Set whether the container must expand to fill the available space on
|
|
/// its main axis.
|
|
pub fn set_must_fill_main_axis(this: &mut WidgetMut<'_, Self>, fill: bool) {
|
|
this.widget.fill_major_axis = fill;
|
|
this.ctx.request_layout();
|
|
}
|
|
|
|
/// Set the spacing along the major axis between any two elements in logical pixels.
|
|
///
|
|
/// Equivalent to the css [gap] property.
|
|
/// This gap is also present between spacers.
|
|
///
|
|
/// [gap]: https://developer.mozilla.org/en-US/docs/Web/CSS/gap
|
|
///
|
|
/// ## Panics
|
|
///
|
|
/// If `gap` is not a non-negative finite value.
|
|
///
|
|
/// See also [`use_default_gap`](Self::use_default_gap).
|
|
pub fn set_gap(this: &mut WidgetMut<'_, Self>, gap: f64) {
|
|
if gap.is_finite() && gap >= 0.0 {
|
|
this.widget.gap = Some(gap);
|
|
} else {
|
|
panic!("Invalid `gap` {gap}, expected a non-negative finite value.")
|
|
}
|
|
this.ctx.request_layout();
|
|
}
|
|
|
|
/// Use the default gap value.
|
|
///
|
|
/// This is [`WIDGET_PADDING_VERTICAL`] for a flex column and
|
|
/// [`WIDGET_PADDING_HORIZONTAL`] for flex row.
|
|
///
|
|
/// See also [`set_gap`](Self::set_gap)
|
|
///
|
|
/// [`WIDGET_PADDING_VERTICAL`]: crate::theme::WIDGET_PADDING_VERTICAL
|
|
/// [`WIDGET_PADDING_HORIZONTAL`]: crate::theme::WIDGET_PADDING_VERTICAL
|
|
pub fn use_default_gap(this: &mut WidgetMut<'_, Self>) {
|
|
this.widget.gap = None;
|
|
this.ctx.request_layout();
|
|
}
|
|
|
|
/// Equivalent to [`set_gap`](Self::set_gap) if `gap` is `Some`, or
|
|
/// [`use_default_gap`](Self::use_default_gap) otherwise.
|
|
///
|
|
/// Does not perform validation of the provided value.
|
|
pub fn set_raw_gap(this: &mut WidgetMut<'_, Self>, gap: Option<f64>) {
|
|
this.widget.gap = gap;
|
|
this.ctx.request_layout();
|
|
}
|
|
|
|
/// Add a non-flex child widget.
|
|
///
|
|
/// See also [`with_child`].
|
|
///
|
|
/// [`with_child`]: Flex::with_child
|
|
pub fn add_child(this: &mut WidgetMut<'_, Self>, child: impl Widget) {
|
|
let child = Child::Fixed {
|
|
widget: WidgetPod::new(child).erased(),
|
|
alignment: None,
|
|
};
|
|
this.widget.children.push(child);
|
|
this.ctx.children_changed();
|
|
}
|
|
|
|
/// Add a non-flex child widget with a pre-assigned id.
|
|
///
|
|
/// See also [`with_child_id`].
|
|
///
|
|
/// [`with_child_id`]: Flex::with_child_id
|
|
pub fn add_child_id(this: &mut WidgetMut<'_, Self>, child: impl Widget, id: WidgetId) {
|
|
let child = Child::Fixed {
|
|
widget: WidgetPod::new_with_id(child, id).erased(),
|
|
alignment: None,
|
|
};
|
|
this.widget.children.push(child);
|
|
this.ctx.children_changed();
|
|
}
|
|
|
|
/// Add a flexible child widget.
|
|
pub fn add_flex_child(
|
|
this: &mut WidgetMut<'_, Self>,
|
|
child: impl Widget,
|
|
params: impl Into<FlexParams>,
|
|
) {
|
|
let params = params.into();
|
|
let child = new_flex_child(params, WidgetPod::new(child).erased());
|
|
|
|
this.widget.children.push(child);
|
|
this.ctx.children_changed();
|
|
}
|
|
|
|
/// Add a spacer widget with a standard size.
|
|
///
|
|
/// The actual value of this spacer depends on whether this container is
|
|
/// a row or column, as well as theme settings.
|
|
pub fn add_default_spacer(this: &mut WidgetMut<'_, Self>) {
|
|
let key = axis_default_spacer(this.widget.direction);
|
|
Self::add_spacer(this, key);
|
|
this.ctx.request_layout();
|
|
}
|
|
|
|
/// Add an empty spacer widget with the given size.
|
|
///
|
|
/// If you are laying out standard controls in this container, you should
|
|
/// generally prefer to use [`add_default_spacer`].
|
|
///
|
|
/// [`add_default_spacer`]: Flex::add_default_spacer
|
|
pub fn add_spacer(this: &mut WidgetMut<'_, Self>, mut len: f64) {
|
|
if len < 0.0 {
|
|
tracing::warn!("add_spacer called with negative length: {}", len);
|
|
}
|
|
len = len.clamp(0.0, f64::MAX);
|
|
|
|
let new_child = Child::FixedSpacer(len, 0.0);
|
|
this.widget.children.push(new_child);
|
|
this.ctx.request_layout();
|
|
}
|
|
|
|
/// Add an empty spacer widget with a specific `flex` factor.
|
|
pub fn add_flex_spacer(this: &mut WidgetMut<'_, Self>, flex: f64) {
|
|
let flex = if flex >= 0.0 {
|
|
flex
|
|
} else {
|
|
debug_panic!("add_spacer called with negative length: {}", flex);
|
|
0.0
|
|
};
|
|
let new_child = Child::FlexedSpacer(flex, 0.0);
|
|
this.widget.children.push(new_child);
|
|
this.ctx.request_layout();
|
|
}
|
|
|
|
/// Insert a non-flex child widget at the given index.
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// Panics if the index is larger than the number of children.
|
|
pub fn insert_child(this: &mut WidgetMut<'_, Self>, idx: usize, child: impl Widget) {
|
|
Self::insert_child_pod(this, idx, WidgetPod::new(child).erased());
|
|
}
|
|
|
|
/// Insert a non-flex child widget wrapped in a [`WidgetPod`] at the given index.
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// Panics if the index is larger than the number of children.
|
|
pub fn insert_child_pod(
|
|
this: &mut WidgetMut<'_, Self>,
|
|
idx: usize,
|
|
widget: WidgetPod<dyn Widget>,
|
|
) {
|
|
let child = Child::Fixed {
|
|
widget,
|
|
alignment: None,
|
|
};
|
|
this.widget.children.insert(idx, child);
|
|
this.ctx.children_changed();
|
|
}
|
|
|
|
/// Insert a flex child widget at the given index.
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// Panics if the index is larger than the number of children.
|
|
pub fn insert_flex_child(
|
|
this: &mut WidgetMut<'_, Self>,
|
|
idx: usize,
|
|
child: impl Widget,
|
|
params: impl Into<FlexParams>,
|
|
) {
|
|
Self::insert_flex_child_pod(this, idx, WidgetPod::new(child).erased(), params);
|
|
}
|
|
|
|
/// Insert a flex child widget wrapped in a [`WidgetPod`] at the given index.
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// Panics if the index is larger than the number of children.
|
|
pub fn insert_flex_child_pod(
|
|
this: &mut WidgetMut<'_, Self>,
|
|
idx: usize,
|
|
child: WidgetPod<dyn Widget>,
|
|
params: impl Into<FlexParams>,
|
|
) {
|
|
let child = new_flex_child(params.into(), child);
|
|
this.widget.children.insert(idx, child);
|
|
this.ctx.children_changed();
|
|
}
|
|
|
|
// TODO - remove
|
|
/// Insert a spacer widget with a standard size at the given index.
|
|
///
|
|
/// The actual value of this spacer depends on whether this container is
|
|
/// a row or column, as well as theme settings.
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// Panics if the index is larger than the number of children.
|
|
pub fn insert_default_spacer(this: &mut WidgetMut<'_, Self>, idx: usize) {
|
|
let key = axis_default_spacer(this.widget.direction);
|
|
Self::insert_spacer(this, idx, key);
|
|
this.ctx.request_layout();
|
|
}
|
|
|
|
/// Insert an empty spacer widget with the given size at the given index.
|
|
///
|
|
/// If you are laying out standard controls in this container, you should
|
|
/// generally prefer to use [`add_default_spacer`].
|
|
///
|
|
/// [`add_default_spacer`]: Flex::add_default_spacer
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// Panics if the index is larger than the number of children.
|
|
pub fn insert_spacer(this: &mut WidgetMut<'_, Self>, idx: usize, mut len: f64) {
|
|
if len < 0.0 {
|
|
tracing::warn!("add_spacer called with negative length: {}", len);
|
|
}
|
|
len = len.clamp(0.0, f64::MAX);
|
|
|
|
let new_child = Child::FixedSpacer(len, 0.0);
|
|
this.widget.children.insert(idx, new_child);
|
|
this.ctx.request_layout();
|
|
}
|
|
|
|
/// Add an empty spacer widget with a specific `flex` factor.
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// Panics if the index is larger than the number of children.
|
|
pub fn insert_flex_spacer(this: &mut WidgetMut<'_, Self>, idx: usize, flex: f64) {
|
|
let flex = if flex >= 0.0 {
|
|
flex
|
|
} else {
|
|
debug_panic!("add_spacer called with negative length: {}", flex);
|
|
0.0
|
|
};
|
|
let new_child = Child::FlexedSpacer(flex, 0.0);
|
|
this.widget.children.insert(idx, new_child);
|
|
this.ctx.request_layout();
|
|
}
|
|
|
|
/// Remove the child at `idx`.
|
|
///
|
|
/// This child can be a widget or a spacer.
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// Panics if the index is larger than the number of children.
|
|
pub fn remove_child(this: &mut WidgetMut<'_, Self>, idx: usize) {
|
|
let child = this.widget.children.remove(idx);
|
|
if let Child::Fixed { widget, .. } | Child::Flex { widget, .. } = child {
|
|
this.ctx.remove_child(widget);
|
|
}
|
|
this.ctx.request_layout();
|
|
}
|
|
|
|
/// Returns a mutable reference to the child widget at `idx`.
|
|
///
|
|
/// Returns `None` if the child at `idx` is a spacer.
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// Panics if the index is larger than the number of children.
|
|
pub fn child_mut<'t>(
|
|
this: &'t mut WidgetMut<'_, Self>,
|
|
idx: usize,
|
|
) -> Option<WidgetMut<'t, dyn Widget>> {
|
|
let child = match &mut this.widget.children[idx] {
|
|
Child::Fixed { widget, .. } | Child::Flex { widget, .. } => widget,
|
|
Child::FixedSpacer(..) => return None,
|
|
Child::FlexedSpacer(..) => return None,
|
|
};
|
|
|
|
Some(this.ctx.get_mut(child))
|
|
}
|
|
|
|
/// Updates the flex parameters for the child at `idx`,
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// Panics if the element at `idx` is not a widget.
|
|
pub fn update_child_flex_params(
|
|
this: &mut WidgetMut<'_, Self>,
|
|
idx: usize,
|
|
params: impl Into<FlexParams>,
|
|
) {
|
|
let child = &mut this.widget.children[idx];
|
|
let child_val = std::mem::replace(child, Child::FixedSpacer(0.0, 0.0));
|
|
let widget = match child_val {
|
|
Child::Fixed { widget, .. } | Child::Flex { widget, .. } => widget,
|
|
_ => {
|
|
panic!("Can't update flex parameters of a spacer element");
|
|
}
|
|
};
|
|
let new_child = new_flex_child(params.into(), widget);
|
|
*child = new_child;
|
|
this.ctx.children_changed();
|
|
}
|
|
|
|
/// Updates the spacer at `idx`, if the spacer was a fixed spacer, it will be overwritten with a flex spacer
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// Panics if the element at `idx` is not a spacer.
|
|
pub fn update_spacer_flex(this: &mut WidgetMut<'_, Self>, idx: usize, flex: f64) {
|
|
let child = &mut this.widget.children[idx];
|
|
|
|
match *child {
|
|
Child::FixedSpacer(_, _) | Child::FlexedSpacer(_, _) => {
|
|
*child = Child::FlexedSpacer(flex, 0.0);
|
|
}
|
|
_ => {
|
|
panic!("Can't update spacer parameters of a non-spacer element");
|
|
}
|
|
};
|
|
this.ctx.children_changed();
|
|
}
|
|
|
|
/// Updates the spacer at `idx`, if the spacer was a flex spacer, it will be overwritten with a fixed spacer
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// Panics if the element at `idx` is not a spacer.
|
|
pub fn update_spacer_fixed(this: &mut WidgetMut<'_, Self>, idx: usize, len: f64) {
|
|
let child = &mut this.widget.children[idx];
|
|
|
|
match *child {
|
|
Child::FixedSpacer(_, _) | Child::FlexedSpacer(_, _) => {
|
|
*child = Child::FixedSpacer(len, 0.0);
|
|
}
|
|
_ => {
|
|
panic!("Can't update spacer parameters of a non-spacer element");
|
|
}
|
|
};
|
|
this.ctx.children_changed();
|
|
}
|
|
|
|
/// Remove all children from the container.
|
|
pub fn clear(this: &mut WidgetMut<'_, Self>) {
|
|
if !this.widget.children.is_empty() {
|
|
this.ctx.request_layout();
|
|
|
|
for child in this.widget.children.drain(..) {
|
|
if let Child::Fixed { widget, .. } | Child::Flex { widget, .. } = child {
|
|
this.ctx.remove_child(widget);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- MARK: OTHER IMPLS---
|
|
impl Axis {
|
|
/// Get the axis perpendicular to this one.
|
|
pub fn cross(self) -> Self {
|
|
match self {
|
|
Self::Horizontal => Self::Vertical,
|
|
Self::Vertical => Self::Horizontal,
|
|
}
|
|
}
|
|
|
|
/// Extract from the argument the magnitude along this axis
|
|
pub fn major(self, size: Size) -> f64 {
|
|
match self {
|
|
Self::Horizontal => size.width,
|
|
Self::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 {
|
|
Self::Horizontal => (rect.x0, rect.x1),
|
|
Self::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 {
|
|
Self::Horizontal => pos.x,
|
|
Self::Vertical => pos.y,
|
|
}
|
|
}
|
|
|
|
/// Extract the coordinate locating the argument with respect to this axis.
|
|
pub fn major_vec(self, vec: Vec2) -> f64 {
|
|
match self {
|
|
Self::Horizontal => vec.x,
|
|
Self::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 {
|
|
Self::Horizontal => (major, minor),
|
|
Self::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 {
|
|
Self::Horizontal => BoxConstraints::new(
|
|
Size::new(min_major, bc.min().height),
|
|
Size::new(major, bc.max().height),
|
|
),
|
|
Self::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<Option<f64>>,
|
|
alignment: impl Into<Option<CrossAxisAlignment>>,
|
|
) -> 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,
|
|
};
|
|
|
|
Self {
|
|
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 {
|
|
Self::Start => 0.0,
|
|
// in vertical layout, baseline is equivalent to center
|
|
Self::Center | Self::Baseline => (val / 2.0).round(),
|
|
Self::End => val,
|
|
Self::Fill => 0.0,
|
|
}
|
|
}
|
|
}
|
|
|
|
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) -> Self {
|
|
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.
|
|
};
|
|
Self {
|
|
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<f64> {
|
|
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<f64> for FlexParams {
|
|
fn from(flex: f64) -> Self {
|
|
Self::new(flex, None)
|
|
}
|
|
}
|
|
|
|
impl From<CrossAxisAlignment> for FlexParams {
|
|
fn from(alignment: CrossAxisAlignment) -> Self {
|
|
Self::new(None, alignment)
|
|
}
|
|
}
|
|
|
|
impl Child {
|
|
fn widget_mut(&mut self) -> Option<&mut WidgetPod<dyn Widget>> {
|
|
match self {
|
|
Self::Fixed { widget, .. } | Self::Flex { widget, .. } => Some(widget),
|
|
_ => None,
|
|
}
|
|
}
|
|
fn widget(&self) -> Option<&WidgetPod<dyn Widget>> {
|
|
match self {
|
|
Self::Fixed { widget, .. } | Self::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<dyn Widget>) -> 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::widgets::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,
|
|
_props: &mut PropertiesMut<'_>,
|
|
_event: &PointerEvent,
|
|
) {
|
|
}
|
|
|
|
fn on_text_event(
|
|
&mut self,
|
|
_ctx: &mut EventCtx,
|
|
_props: &mut PropertiesMut<'_>,
|
|
_event: &TextEvent,
|
|
) {
|
|
}
|
|
|
|
fn on_access_event(
|
|
&mut self,
|
|
_ctx: &mut EventCtx,
|
|
_props: &mut PropertiesMut<'_>,
|
|
_event: &AccessEvent,
|
|
) {
|
|
}
|
|
|
|
fn register_children(&mut self, ctx: &mut crate::core::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,
|
|
_props: &mut PropertiesMut<'_>,
|
|
bc: &BoxConstraints,
|
|
) -> Size {
|
|
// 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;
|
|
// We start with a small value to avoid divide-by-zero errors.
|
|
const MIN_FLEX_SUM: f64 = 0.0001;
|
|
let mut flex_sum = MIN_FLEX_SUM;
|
|
for child in &mut self.children {
|
|
match child {
|
|
Child::Fixed { widget, alignment } => {
|
|
// The BoxConstraints of fixed-children only depends on the BoxConstraints 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());
|
|
max_above_baseline =
|
|
max_above_baseline.max(child_size.height - baseline_offset);
|
|
max_below_baseline = max_below_baseline.max(baseline_offset);
|
|
}
|
|
Child::FixedSpacer(kv, calculated_size) => {
|
|
*calculated_size = *kv;
|
|
if *calculated_size < 0.0 {
|
|
tracing::warn!("Length provided to fixed spacer was less than 0");
|
|
}
|
|
*calculated_size = calculated_size.max(0.0);
|
|
major_non_flex += *calculated_size;
|
|
}
|
|
Child::Flex { flex, .. } | Child::FlexedSpacer(flex, _) => flex_sum += *flex,
|
|
}
|
|
}
|
|
|
|
let total_major = self.direction.major(bc.max());
|
|
let remaining = (total_major - major_non_flex).max(0.0);
|
|
let mut remainder: f64 = 0.0;
|
|
|
|
let mut major_flex: f64 = 0.0;
|
|
let px_per_flex = remaining / flex_sum;
|
|
// Measure flex children.
|
|
for child in &mut self.children {
|
|
match child {
|
|
Child::Flex {
|
|
widget,
|
|
flex,
|
|
alignment,
|
|
} => {
|
|
// The BoxConstraints of flex-children depends on the size of every sibling, which
|
|
// received layout earlier. Therefore we use any_changed.
|
|
let child_size = if any_changed || ctx.child_needs_layout(widget) {
|
|
let alignment = alignment.unwrap_or(self.cross_alignment);
|
|
any_use_baseline |= alignment == CrossAxisAlignment::Baseline;
|
|
|
|
let desired_major = (*flex) * px_per_flex + remainder;
|
|
let actual_major = desired_major.round();
|
|
remainder = desired_major - actual_major;
|
|
|
|
let old_size = ctx.widget_state.layout_rect().size();
|
|
let child_bc = self.direction.constraints(&loosened_bc, 0.0, actual_major);
|
|
let child_size = ctx.run_layout(widget, &child_bc);
|
|
|
|
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_flex += self.direction.major(child_size).expand();
|
|
minor = minor.max(self.direction.minor(child_size).expand());
|
|
max_above_baseline =
|
|
max_above_baseline.max(child_size.height - baseline_offset);
|
|
max_below_baseline = max_below_baseline.max(baseline_offset);
|
|
}
|
|
Child::FlexedSpacer(flex, calculated_size) => {
|
|
let desired_major = (*flex) * px_per_flex + remainder;
|
|
*calculated_size = desired_major.round();
|
|
remainder = desired_major - *calculated_size;
|
|
major_flex += *calculated_size;
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
// figure out if we have extra space on major axis, and if so how to use it
|
|
let extra = if self.fill_major_axis {
|
|
(remaining - major_flex).max(0.0)
|
|
} else {
|
|
// if we are *not* expected to fill our available space this usually
|
|
// means we don't have any extra, unless dictated by our constraints.
|
|
(self.direction.major(bc.min()) - (major_non_flex + major_flex)).max(0.0)
|
|
};
|
|
|
|
let mut spacing = Spacing::new(self.main_alignment, extra, self.children.len());
|
|
|
|
// the actual size needed to tightly fit the children on the minor axis.
|
|
// Unlike the 'minor' var, this ignores the incoming constraints.
|
|
let minor_dim = match self.direction {
|
|
Axis::Horizontal if any_use_baseline => max_below_baseline + max_above_baseline,
|
|
_ => minor,
|
|
};
|
|
|
|
let extra_height = minor - minor_dim.min(minor);
|
|
|
|
let mut major = spacing.next().unwrap_or(0.);
|
|
let mut child_paint_rect = Rect::ZERO;
|
|
|
|
for child in &mut self.children {
|
|
match child {
|
|
Child::Fixed { widget, alignment }
|
|
| Child::Flex {
|
|
widget, alignment, ..
|
|
} => {
|
|
let child_size = ctx.child_size(widget);
|
|
let alignment = alignment.unwrap_or(self.cross_alignment);
|
|
let child_minor_offset = match alignment {
|
|
// This will ignore baseline alignment if it is overridden on children,
|
|
// but is not the default for the container. Is this okay?
|
|
CrossAxisAlignment::Baseline
|
|
if matches!(self.direction, Axis::Horizontal) =>
|
|
{
|
|
let child_baseline = ctx.child_baseline_offset(widget);
|
|
let child_above_baseline = child_size.height - child_baseline;
|
|
extra_height + (max_above_baseline - child_above_baseline)
|
|
}
|
|
CrossAxisAlignment::Fill => {
|
|
let fill_size: Size = self
|
|
.direction
|
|
.pack(self.direction.major(child_size), minor_dim)
|
|
.into();
|
|
if ctx.widget_state.layout_rect().size() != fill_size {
|
|
let child_bc = BoxConstraints::tight(fill_size);
|
|
//TODO: this is the second call of layout on the same child, which
|
|
// is bad, because it can lead to exponential increase in layout calls
|
|
// when used multiple times in the widget hierarchy.
|
|
ctx.run_layout(widget, &child_bc);
|
|
}
|
|
0.0
|
|
}
|
|
_ => {
|
|
let extra_minor = minor_dim - self.direction.minor(child_size);
|
|
alignment.align(extra_minor)
|
|
}
|
|
};
|
|
|
|
let child_pos: Point = self.direction.pack(major, child_minor_offset).into();
|
|
ctx.place_child(widget, child_pos);
|
|
child_paint_rect = child_paint_rect.union(ctx.widget_state.paint_rect());
|
|
major += self.direction.major(child_size).expand();
|
|
major += spacing.next().unwrap_or(0.);
|
|
major += gap;
|
|
}
|
|
Child::FlexedSpacer(_, calculated_size)
|
|
| Child::FixedSpacer(_, calculated_size) => {
|
|
major += *calculated_size;
|
|
major += gap;
|
|
}
|
|
}
|
|
}
|
|
|
|
if flex_sum > 0.0 && total_major.is_infinite() {
|
|
tracing::warn!("A child of Flex is flex, but Flex is unbounded.");
|
|
}
|
|
|
|
if !self.children.is_empty() {
|
|
// If we have at least one child, the last child added `gap` to `major`, which means that `major` is
|
|
// not the total size of the flex in the major axis, it's instead where the "next widget" will be placed.
|
|
// However, for the rest of this value, we need the total size of the widget in the major axis.
|
|
major -= gap;
|
|
}
|
|
|
|
if flex_sum > MIN_FLEX_SUM {
|
|
major = total_major;
|
|
}
|
|
|
|
// my_size may be larger than the given constraints.
|
|
// In which case, the Flex widget will either overflow its parent
|
|
// or be clipped (e.g. if its parent is a Portal).
|
|
let my_size: Size = self.direction.pack(major, minor_dim).into();
|
|
|
|
let my_bounds = my_size.to_rect();
|
|
let insets = child_paint_rect - my_bounds;
|
|
ctx.set_paint_insets(insets);
|
|
|
|
let baseline_offset = match self.direction {
|
|
Axis::Horizontal => max_below_baseline,
|
|
Axis::Vertical => self
|
|
.children
|
|
.last()
|
|
.map(|last| {
|
|
let child = last.widget();
|
|
if let Some(widget) = child {
|
|
let child_bl = ctx.child_baseline_offset(widget);
|
|
let child_max_y = ctx.child_layout_rect(widget).max_y();
|
|
let extra_bottom_padding = my_size.height - child_max_y;
|
|
child_bl + extra_bottom_padding
|
|
} else {
|
|
0.0
|
|
}
|
|
})
|
|
.unwrap_or(0.0),
|
|
};
|
|
|
|
ctx.set_baseline_offset(baseline_offset);
|
|
my_size
|
|
}
|
|
|
|
fn paint(&mut self, ctx: &mut PaintCtx, _props: &PropertiesRef<'_>, scene: &mut Scene) {
|
|
// paint the baseline if we're debugging layout
|
|
if ctx.debug_paint_enabled() && ctx.baseline_offset() != 0.0 {
|
|
let color = ctx.debug_color();
|
|
let my_baseline = ctx.size().height - ctx.baseline_offset();
|
|
let line = Line::new((0.0, my_baseline), (ctx.size().width, my_baseline));
|
|
|
|
let stroke_style = Stroke::new(1.0).with_dashes(0., [4.0, 4.0]);
|
|
scene.stroke(&stroke_style, Affine::IDENTITY, color, None, &line);
|
|
}
|
|
}
|
|
|
|
fn accessibility_role(&self) -> Role {
|
|
Role::GenericContainer
|
|
}
|
|
|
|
fn accessibility(
|
|
&mut self,
|
|
_ctx: &mut AccessCtx,
|
|
_props: &PropertiesRef<'_>,
|
|
_node: &mut Node,
|
|
) {
|
|
}
|
|
|
|
fn children_ids(&self) -> SmallVec<[WidgetId; 16]> {
|
|
self.children
|
|
.iter()
|
|
.filter_map(|child| child.widget())
|
|
.map(|widget_pod| widget_pod.id())
|
|
.collect()
|
|
}
|
|
|
|
fn make_trace_span(&self, ctx: &QueryCtx<'_>) -> Span {
|
|
trace_span!("Flex", id = ctx.widget_id().trace())
|
|
}
|
|
}
|
|
|
|
// --- MARK: TESTS ---
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::assert_render_snapshot;
|
|
use crate::testing::TestHarness;
|
|
use crate::widgets::Label;
|
|
|
|
#[test]
|
|
#[allow(clippy::cognitive_complexity)]
|
|
fn test_main_axis_alignment_spacing() {
|
|
// The following alignment strategy is based on how
|
|
// Chrome 80 handles it with CSS flex.
|
|
|
|
let vec = |a, e, n| -> Vec<f64> { Spacing::new(a, e, n).collect() };
|
|
|
|
let a = MainAxisAlignment::Start;
|
|
assert_eq!(vec(a, 10., 0), vec![10.]);
|
|
assert_eq!(vec(a, 10., 1), vec![0., 10.]);
|
|
assert_eq!(vec(a, 10., 2), vec![0., 0., 10.]);
|
|
assert_eq!(vec(a, 10., 3), vec![0., 0., 0., 10.]);
|
|
|
|
let a = MainAxisAlignment::End;
|
|
assert_eq!(vec(a, 10., 0), vec![10.]);
|
|
assert_eq!(vec(a, 10., 1), vec![10., 0.]);
|
|
assert_eq!(vec(a, 10., 2), vec![10., 0., 0.]);
|
|
assert_eq!(vec(a, 10., 3), vec![10., 0., 0., 0.]);
|
|
|
|
let a = MainAxisAlignment::Center;
|
|
assert_eq!(vec(a, 10., 0), vec![10.]);
|
|
assert_eq!(vec(a, 10., 1), vec![5., 5.]);
|
|
assert_eq!(vec(a, 10., 2), vec![5., 0., 5.]);
|
|
assert_eq!(vec(a, 10., 3), vec![5., 0., 0., 5.]);
|
|
assert_eq!(vec(a, 1., 0), vec![1.]);
|
|
assert_eq!(vec(a, 3., 1), vec![2., 1.]);
|
|
assert_eq!(vec(a, 5., 2), vec![3., 0., 2.]);
|
|
assert_eq!(vec(a, 17., 3), vec![9., 0., 0., 8.]);
|
|
|
|
let a = MainAxisAlignment::SpaceBetween;
|
|
assert_eq!(vec(a, 10., 0), vec![10.]);
|
|
assert_eq!(vec(a, 10., 1), vec![0., 10.]);
|
|
assert_eq!(vec(a, 10., 2), vec![0., 10., 0.]);
|
|
assert_eq!(vec(a, 10., 3), vec![0., 5., 5., 0.]);
|
|
assert_eq!(vec(a, 33., 5), vec![0., 8., 9., 8., 8., 0.]);
|
|
assert_eq!(vec(a, 34., 5), vec![0., 9., 8., 9., 8., 0.]);
|
|
assert_eq!(vec(a, 35., 5), vec![0., 9., 9., 8., 9., 0.]);
|
|
assert_eq!(vec(a, 36., 5), vec![0., 9., 9., 9., 9., 0.]);
|
|
assert_eq!(vec(a, 37., 5), vec![0., 9., 10., 9., 9., 0.]);
|
|
assert_eq!(vec(a, 38., 5), vec![0., 10., 9., 10., 9., 0.]);
|
|
assert_eq!(vec(a, 39., 5), vec![0., 10., 10., 9., 10., 0.]);
|
|
|
|
let a = MainAxisAlignment::SpaceEvenly;
|
|
assert_eq!(vec(a, 10., 0), vec![10.]);
|
|
assert_eq!(vec(a, 10., 1), vec![5., 5.]);
|
|
assert_eq!(vec(a, 10., 2), vec![3., 4., 3.]);
|
|
assert_eq!(vec(a, 10., 3), vec![3., 2., 3., 2.]);
|
|
assert_eq!(vec(a, 33., 5), vec![6., 5., 6., 5., 6., 5.]);
|
|
assert_eq!(vec(a, 34., 5), vec![6., 5., 6., 6., 5., 6.]);
|
|
assert_eq!(vec(a, 35., 5), vec![6., 6., 5., 6., 6., 6.]);
|
|
assert_eq!(vec(a, 36., 5), vec![6., 6., 6., 6., 6., 6.]);
|
|
assert_eq!(vec(a, 37., 5), vec![6., 6., 7., 6., 6., 6.]);
|
|
assert_eq!(vec(a, 38., 5), vec![6., 7., 6., 6., 7., 6.]);
|
|
assert_eq!(vec(a, 39., 5), vec![7., 6., 7., 6., 7., 6.]);
|
|
|
|
let a = MainAxisAlignment::SpaceAround;
|
|
assert_eq!(vec(a, 10., 0), vec![10.]);
|
|
assert_eq!(vec(a, 10., 1), vec![5., 5.]);
|
|
assert_eq!(vec(a, 10., 2), vec![3., 5., 2.]);
|
|
assert_eq!(vec(a, 10., 3), vec![2., 3., 3., 2.]);
|
|
assert_eq!(vec(a, 33., 5), vec![3., 7., 6., 7., 7., 3.]);
|
|
assert_eq!(vec(a, 34., 5), vec![3., 7., 7., 7., 7., 3.]);
|
|
assert_eq!(vec(a, 35., 5), vec![4., 7., 7., 7., 7., 3.]);
|
|
assert_eq!(vec(a, 36., 5), vec![4., 7., 7., 7., 7., 4.]);
|
|
assert_eq!(vec(a, 37., 5), vec![4., 7., 8., 7., 7., 4.]);
|
|
assert_eq!(vec(a, 38., 5), vec![4., 7., 8., 8., 7., 4.]);
|
|
assert_eq!(vec(a, 39., 5), vec![4., 8., 7., 8., 8., 4.]);
|
|
}
|
|
|
|
// TODO - fix this test
|
|
#[test]
|
|
#[ignore = "Unclear what test is trying to validate"]
|
|
fn test_invalid_flex_params() {
|
|
use float_cmp::approx_eq;
|
|
let params = FlexParams::new(0.0, None);
|
|
approx_eq!(f64, params.flex.unwrap(), 1.0, ulps = 2);
|
|
|
|
let params = FlexParams::new(-0.0, None);
|
|
approx_eq!(f64, params.flex.unwrap(), 1.0, ulps = 2);
|
|
|
|
let params = FlexParams::new(-1.0, None);
|
|
approx_eq!(f64, params.flex.unwrap(), 1.0, ulps = 2);
|
|
}
|
|
|
|
// TODO - Reduce copy-pasting?
|
|
#[test]
|
|
fn flex_row_cross_axis_snapshots() {
|
|
let widget = Flex::row()
|
|
.with_child(Label::new("hello"))
|
|
.with_flex_child(Label::new("world"), 1.0)
|
|
.with_child(Label::new("foo"))
|
|
.with_flex_child(
|
|
Label::new("bar"),
|
|
FlexParams::new(2.0, CrossAxisAlignment::Start),
|
|
);
|
|
|
|
let window_size = Size::new(200.0, 150.0);
|
|
let mut harness = TestHarness::create_with_size(widget, window_size);
|
|
|
|
harness.edit_root_widget(|mut flex| {
|
|
let mut flex = flex.downcast::<Flex>();
|
|
Flex::set_cross_axis_alignment(&mut flex, CrossAxisAlignment::Start);
|
|
});
|
|
assert_render_snapshot!(harness, "row_cross_axis_start");
|
|
|
|
harness.edit_root_widget(|mut flex| {
|
|
let mut flex = flex.downcast::<Flex>();
|
|
Flex::set_cross_axis_alignment(&mut flex, CrossAxisAlignment::Center);
|
|
});
|
|
assert_render_snapshot!(harness, "row_cross_axis_center");
|
|
|
|
harness.edit_root_widget(|mut flex| {
|
|
let mut flex = flex.downcast::<Flex>();
|
|
Flex::set_cross_axis_alignment(&mut flex, CrossAxisAlignment::End);
|
|
});
|
|
assert_render_snapshot!(harness, "row_cross_axis_end");
|
|
|
|
harness.edit_root_widget(|mut flex| {
|
|
let mut flex = flex.downcast::<Flex>();
|
|
Flex::set_cross_axis_alignment(&mut flex, CrossAxisAlignment::Baseline);
|
|
});
|
|
assert_render_snapshot!(harness, "row_cross_axis_baseline");
|
|
|
|
harness.edit_root_widget(|mut flex| {
|
|
let mut flex = flex.downcast::<Flex>();
|
|
Flex::set_cross_axis_alignment(&mut flex, CrossAxisAlignment::Fill);
|
|
});
|
|
assert_render_snapshot!(harness, "row_cross_axis_fill");
|
|
}
|
|
|
|
#[test]
|
|
fn flex_row_main_axis_snapshots() {
|
|
let widget = Flex::row()
|
|
.with_child(Label::new("hello"))
|
|
.with_flex_child(Label::new("world"), 1.0)
|
|
.with_child(Label::new("foo"))
|
|
.with_flex_child(
|
|
Label::new("bar"),
|
|
FlexParams::new(2.0, CrossAxisAlignment::Start),
|
|
);
|
|
|
|
let window_size = Size::new(200.0, 150.0);
|
|
let mut harness = TestHarness::create_with_size(widget, window_size);
|
|
|
|
// MAIN AXIS ALIGNMENT
|
|
|
|
harness.edit_root_widget(|mut flex| {
|
|
let mut flex = flex.downcast::<Flex>();
|
|
Flex::set_main_axis_alignment(&mut flex, MainAxisAlignment::Start);
|
|
});
|
|
assert_render_snapshot!(harness, "row_main_axis_start");
|
|
|
|
harness.edit_root_widget(|mut flex| {
|
|
let mut flex = flex.downcast::<Flex>();
|
|
Flex::set_main_axis_alignment(&mut flex, MainAxisAlignment::Center);
|
|
});
|
|
assert_render_snapshot!(harness, "row_main_axis_center");
|
|
|
|
harness.edit_root_widget(|mut flex| {
|
|
let mut flex = flex.downcast::<Flex>();
|
|
Flex::set_main_axis_alignment(&mut flex, MainAxisAlignment::End);
|
|
});
|
|
assert_render_snapshot!(harness, "row_main_axis_end");
|
|
|
|
harness.edit_root_widget(|mut flex| {
|
|
let mut flex = flex.downcast::<Flex>();
|
|
Flex::set_main_axis_alignment(&mut flex, MainAxisAlignment::SpaceBetween);
|
|
});
|
|
assert_render_snapshot!(harness, "row_main_axis_spaceBetween");
|
|
|
|
harness.edit_root_widget(|mut flex| {
|
|
let mut flex = flex.downcast::<Flex>();
|
|
Flex::set_main_axis_alignment(&mut flex, MainAxisAlignment::SpaceEvenly);
|
|
});
|
|
assert_render_snapshot!(harness, "row_main_axis_spaceEvenly");
|
|
|
|
harness.edit_root_widget(|mut flex| {
|
|
let mut flex = flex.downcast::<Flex>();
|
|
Flex::set_main_axis_alignment(&mut flex, MainAxisAlignment::SpaceAround);
|
|
});
|
|
assert_render_snapshot!(harness, "row_main_axis_spaceAround");
|
|
|
|
// FILL MAIN AXIS
|
|
// TODO - This doesn't seem to do anything?
|
|
|
|
harness.edit_root_widget(|mut flex| {
|
|
let mut flex = flex.downcast::<Flex>();
|
|
Flex::set_must_fill_main_axis(&mut flex, true);
|
|
});
|
|
assert_render_snapshot!(harness, "row_fill_main_axis");
|
|
}
|
|
|
|
#[test]
|
|
fn flex_col_cross_axis_snapshots() {
|
|
let widget = Flex::column()
|
|
.with_child(Label::new("hello"))
|
|
.with_flex_child(Label::new("world"), 1.0)
|
|
.with_child(Label::new("foo"))
|
|
.with_flex_child(
|
|
Label::new("bar"),
|
|
FlexParams::new(2.0, CrossAxisAlignment::Start),
|
|
);
|
|
|
|
let window_size = Size::new(200.0, 150.0);
|
|
let mut harness = TestHarness::create_with_size(widget, window_size);
|
|
|
|
harness.edit_root_widget(|mut flex| {
|
|
let mut flex = flex.downcast::<Flex>();
|
|
Flex::set_cross_axis_alignment(&mut flex, CrossAxisAlignment::Start);
|
|
});
|
|
assert_render_snapshot!(harness, "col_cross_axis_start");
|
|
|
|
harness.edit_root_widget(|mut flex| {
|
|
let mut flex = flex.downcast::<Flex>();
|
|
Flex::set_cross_axis_alignment(&mut flex, CrossAxisAlignment::Center);
|
|
});
|
|
assert_render_snapshot!(harness, "col_cross_axis_center");
|
|
|
|
harness.edit_root_widget(|mut flex| {
|
|
let mut flex = flex.downcast::<Flex>();
|
|
Flex::set_cross_axis_alignment(&mut flex, CrossAxisAlignment::End);
|
|
});
|
|
assert_render_snapshot!(harness, "col_cross_axis_end");
|
|
|
|
harness.edit_root_widget(|mut flex| {
|
|
let mut flex = flex.downcast::<Flex>();
|
|
Flex::set_cross_axis_alignment(&mut flex, CrossAxisAlignment::Baseline);
|
|
});
|
|
assert_render_snapshot!(harness, "col_cross_axis_baseline");
|
|
|
|
harness.edit_root_widget(|mut flex| {
|
|
let mut flex = flex.downcast::<Flex>();
|
|
Flex::set_cross_axis_alignment(&mut flex, CrossAxisAlignment::Fill);
|
|
});
|
|
assert_render_snapshot!(harness, "col_cross_axis_fill");
|
|
}
|
|
|
|
#[test]
|
|
fn flex_col_main_axis_snapshots() {
|
|
let widget = Flex::column()
|
|
.with_child(Label::new("hello"))
|
|
.with_flex_child(Label::new("world"), 1.0)
|
|
.with_child(Label::new("foo"))
|
|
.with_flex_child(
|
|
Label::new("bar"),
|
|
FlexParams::new(2.0, CrossAxisAlignment::Start),
|
|
);
|
|
|
|
let window_size = Size::new(200.0, 150.0);
|
|
let mut harness = TestHarness::create_with_size(widget, window_size);
|
|
|
|
// MAIN AXIS ALIGNMENT
|
|
|
|
harness.edit_root_widget(|mut flex| {
|
|
let mut flex = flex.downcast::<Flex>();
|
|
Flex::set_main_axis_alignment(&mut flex, MainAxisAlignment::Start);
|
|
});
|
|
assert_render_snapshot!(harness, "col_main_axis_start");
|
|
|
|
harness.edit_root_widget(|mut flex| {
|
|
let mut flex = flex.downcast::<Flex>();
|
|
Flex::set_main_axis_alignment(&mut flex, MainAxisAlignment::Center);
|
|
});
|
|
assert_render_snapshot!(harness, "col_main_axis_center");
|
|
|
|
harness.edit_root_widget(|mut flex| {
|
|
let mut flex = flex.downcast::<Flex>();
|
|
Flex::set_main_axis_alignment(&mut flex, MainAxisAlignment::End);
|
|
});
|
|
assert_render_snapshot!(harness, "col_main_axis_end");
|
|
|
|
harness.edit_root_widget(|mut flex| {
|
|
let mut flex = flex.downcast::<Flex>();
|
|
Flex::set_main_axis_alignment(&mut flex, MainAxisAlignment::SpaceBetween);
|
|
});
|
|
assert_render_snapshot!(harness, "col_main_axis_spaceBetween");
|
|
|
|
harness.edit_root_widget(|mut flex| {
|
|
let mut flex = flex.downcast::<Flex>();
|
|
Flex::set_main_axis_alignment(&mut flex, MainAxisAlignment::SpaceEvenly);
|
|
});
|
|
assert_render_snapshot!(harness, "col_main_axis_spaceEvenly");
|
|
|
|
harness.edit_root_widget(|mut flex| {
|
|
let mut flex = flex.downcast::<Flex>();
|
|
Flex::set_main_axis_alignment(&mut flex, MainAxisAlignment::SpaceAround);
|
|
});
|
|
assert_render_snapshot!(harness, "col_main_axis_spaceAround");
|
|
|
|
// FILL MAIN AXIS
|
|
// TODO - This doesn't seem to do anything?
|
|
|
|
harness.edit_root_widget(|mut flex| {
|
|
let mut flex = flex.downcast::<Flex>();
|
|
Flex::set_must_fill_main_axis(&mut flex, true);
|
|
});
|
|
assert_render_snapshot!(harness, "col_fill_main_axis");
|
|
}
|
|
|
|
#[test]
|
|
fn edit_flex_container() {
|
|
let image_1 = {
|
|
let widget = Flex::column()
|
|
.with_child(Label::new("a"))
|
|
.with_child(Label::new("b"))
|
|
.with_child(Label::new("c"))
|
|
.with_child(Label::new("d"));
|
|
// -> abcd
|
|
|
|
let window_size = Size::new(200.0, 150.0);
|
|
let mut harness = TestHarness::create_with_size(widget, window_size);
|
|
|
|
harness.edit_root_widget(|mut flex| {
|
|
let mut flex = flex.downcast::<Flex>();
|
|
|
|
Flex::remove_child(&mut flex, 1);
|
|
// -> acd
|
|
Flex::add_child(&mut flex, Label::new("x"));
|
|
// -> acdx
|
|
Flex::add_flex_child(&mut flex, Label::new("y"), 2.0);
|
|
// -> acdxy
|
|
Flex::add_default_spacer(&mut flex);
|
|
// -> acdxy_
|
|
Flex::add_spacer(&mut flex, 5.0);
|
|
// -> acdxy__
|
|
Flex::add_flex_spacer(&mut flex, 1.0);
|
|
// -> acdxy___
|
|
Flex::insert_child(&mut flex, 2, Label::new("i"));
|
|
// -> acidxy___
|
|
Flex::insert_flex_child(&mut flex, 2, Label::new("j"), 2.0);
|
|
// -> acjidxy___
|
|
Flex::insert_default_spacer(&mut flex, 2);
|
|
// -> ac_jidxy___
|
|
Flex::insert_spacer(&mut flex, 2, 5.0);
|
|
// -> ac__jidxy___
|
|
Flex::insert_flex_spacer(&mut flex, 2, 1.0);
|
|
});
|
|
|
|
harness.render()
|
|
};
|
|
|
|
let image_2 = {
|
|
let widget = Flex::column()
|
|
.with_child(Label::new("a"))
|
|
.with_child(Label::new("c"))
|
|
.with_flex_spacer(1.0)
|
|
.with_spacer(5.0)
|
|
.with_default_spacer()
|
|
.with_flex_child(Label::new("j"), 2.0)
|
|
.with_child(Label::new("i"))
|
|
.with_child(Label::new("d"))
|
|
.with_child(Label::new("x"))
|
|
.with_flex_child(Label::new("y"), 2.0)
|
|
.with_default_spacer()
|
|
.with_spacer(5.0)
|
|
.with_flex_spacer(1.0);
|
|
|
|
let window_size = Size::new(200.0, 150.0);
|
|
let mut harness = TestHarness::create_with_size(widget, window_size);
|
|
harness.render()
|
|
};
|
|
|
|
// We don't use assert_eq because we don't want rich assert
|
|
assert!(image_1 == image_2);
|
|
}
|
|
|
|
#[test]
|
|
fn get_flex_child() {
|
|
let widget = Flex::column()
|
|
.with_child(Label::new("hello"))
|
|
.with_child(Label::new("world"))
|
|
.with_spacer(1.0);
|
|
|
|
let window_size = Size::new(200.0, 150.0);
|
|
let mut harness = TestHarness::create_with_size(widget, window_size);
|
|
harness.edit_root_widget(|mut flex| {
|
|
let mut flex = flex.downcast::<Flex>();
|
|
|
|
let mut child = Flex::child_mut(&mut flex, 1).unwrap();
|
|
assert_eq!(
|
|
child
|
|
.try_downcast::<Label>()
|
|
.unwrap()
|
|
.widget
|
|
.text()
|
|
.to_string(),
|
|
"world"
|
|
);
|
|
std::mem::drop(child);
|
|
|
|
assert!(Flex::child_mut(&mut flex, 2).is_none());
|
|
});
|
|
|
|
// TODO - test out-of-bounds access?
|
|
}
|
|
|
|
#[test]
|
|
fn divide_by_zero() {
|
|
let widget = Flex::column().with_flex_spacer(0.0);
|
|
|
|
// Running layout should not panic when the flex sum is zero.
|
|
let window_size = Size::new(200.0, 150.0);
|
|
let mut harness = TestHarness::create_with_size(widget, window_size);
|
|
harness.render();
|
|
}
|
|
}
|