use crate::geometry::{AbsoluteAxis, Line, Point, Rect, Size};
use crate::style::{AlignItemsKeyword, AlignSelf, AlignmentSafety, AvailableSpace, CheapCloneStr, Position};
use crate::style_helpers::TakiaMaxContent;
use crate::sys::DefaultCheapStr;
use crate::tree::NodeId;
use crate::util::sys::{abs, copysign, f32_max, f32_min};
#[derive(Copy, Clone, Debug)]
pub struct ScrollableOverflow(Rect<f32>);
impl ScrollableOverflow {
pub const ZERO: Self = Self(Rect::ZERO);
#[inline]
pub fn new(rect: Rect<f32>, reversed: Point<bool>) -> Self {
debug_assert!(rect.left <= 0.0 && rect.top <= 0.0 && rect.right >= 0.0 && rect.bottom >= 0.0);
Self(Rect {
right: copysign(rect.right, if reversed.x { -1.0 } else { 1.0 }),
bottom: copysign(rect.bottom, if reversed.y { -1.0 } else { 1.0 }),
..rect
})
}
#[inline(always)]
pub fn rect(self) -> Rect<f32> {
Rect { right: abs(self.0.right), bottom: abs(self.0.bottom), ..self.0 }
}
#[inline(always)]
pub fn left(self) -> f32 {
self.0.left
}
#[inline(always)]
pub fn right(self) -> f32 {
abs(self.0.right)
}
#[inline(always)]
pub fn top(self) -> f32 {
self.0.top
}
#[inline(always)]
pub fn bottom(self) -> f32 {
abs(self.0.bottom)
}
#[inline(always)]
pub fn is_reversed(self) -> Point<bool> {
Point { x: self.0.right.is_sign_negative(), y: self.0.bottom.is_sign_negative() }
}
#[inline]
pub fn transpose(self) -> Self {
Self(self.0.transpose())
}
}
impl From<(Rect<f32>, Point<bool>)> for ScrollableOverflow {
#[inline]
fn from((rect, reversed): (Rect<f32>, Point<bool>)) -> Self {
Self::new(rect, reversed)
}
}
impl Default for ScrollableOverflow {
#[inline]
fn default() -> Self {
Self::ZERO
}
}
impl PartialEq for ScrollableOverflow {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.0.left.to_bits() == other.0.left.to_bits()
&& self.0.right.to_bits() == other.0.right.to_bits()
&& self.0.top.to_bits() == other.0.top.to_bits()
&& self.0.bottom.to_bits() == other.0.bottom.to_bits()
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for ScrollableOverflow {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeStruct;
let mut state = serializer.serialize_struct("ScrollableOverflow", 2)?;
state.serialize_field("rect", &self.rect())?;
state.serialize_field("reversed", &self.is_reversed())?;
state.end()
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub enum RunMode {
PerformLayout,
ComputeSize,
PerformHiddenLayout,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub enum SizingMode {
ContentSize,
InherentSize,
}
#[derive(Copy, Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct CollapsibleMarginSet {
positive: f32,
negative: f32,
}
impl CollapsibleMarginSet {
pub const ZERO: Self = Self { positive: 0.0, negative: 0.0 };
pub fn from_margin(margin: f32) -> Self {
if margin >= 0.0 {
Self { positive: margin, negative: 0.0 }
} else {
Self { positive: 0.0, negative: margin }
}
}
pub fn collapse_with_margin(mut self, margin: f32) -> Self {
if margin >= 0.0 {
self.positive = f32_max(self.positive, margin);
} else {
self.negative = f32_min(self.negative, margin);
}
self
}
pub fn collapse_with_set(mut self, other: CollapsibleMarginSet) -> Self {
self.positive = f32_max(self.positive, other.positive);
self.negative = f32_min(self.negative, other.negative);
self
}
pub fn resolve(&self) -> f32 {
self.positive + self.negative
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub enum RequestedAxis {
Horizontal,
Vertical,
Both,
}
impl RequestedAxis {
#[inline(always)]
pub fn transpose(self) -> RequestedAxis {
match self {
RequestedAxis::Horizontal => RequestedAxis::Vertical,
RequestedAxis::Vertical => RequestedAxis::Horizontal,
RequestedAxis::Both => RequestedAxis::Both,
}
}
}
impl From<AbsoluteAxis> for RequestedAxis {
fn from(value: AbsoluteAxis) -> Self {
match value {
AbsoluteAxis::Horizontal => RequestedAxis::Horizontal,
AbsoluteAxis::Vertical => RequestedAxis::Vertical,
}
}
}
impl TryFrom<RequestedAxis> for AbsoluteAxis {
type Error = ();
fn try_from(value: RequestedAxis) -> Result<Self, Self::Error> {
match value {
RequestedAxis::Horizontal => Ok(AbsoluteAxis::Horizontal),
RequestedAxis::Vertical => Ok(AbsoluteAxis::Vertical),
RequestedAxis::Both => Err(()),
}
}
}
#[derive(Debug, Copy, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct LayoutInput {
pub run_mode: RunMode,
pub sizing_mode: SizingMode,
pub axis: RequestedAxis,
pub known_dimensions: Size<Option<f32>>,
pub known_dimensions_are_definite: Size<bool>,
pub parent_size: Size<Option<f32>>,
pub available_space: Size<AvailableSpace>,
pub vertical_margins_are_collapsible: Line<bool>,
}
impl LayoutInput {
pub const HIDDEN: LayoutInput = LayoutInput {
run_mode: RunMode::PerformHiddenLayout,
known_dimensions: Size::NONE,
known_dimensions_are_definite: Size { width: true, height: true },
parent_size: Size::NONE,
available_space: Size::MAX_CONTENT,
sizing_mode: SizingMode::InherentSize,
axis: RequestedAxis::Both,
vertical_margins_are_collapsible: Line::FALSE,
};
pub fn transpose(self) -> LayoutInput {
LayoutInput {
run_mode: self.run_mode,
sizing_mode: self.sizing_mode,
axis: self.axis.transpose(),
known_dimensions: self.known_dimensions.transpose(),
known_dimensions_are_definite: self.known_dimensions_are_definite.transpose(),
parent_size: self.parent_size.transpose(),
available_space: self.available_space.transpose(),
vertical_margins_are_collapsible: Line::FALSE,
}
}
}
#[derive(Debug, Copy, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct Baselines {
pub first: Option<f32>,
pub last: Option<f32>,
}
impl Baselines {
pub const NONE: Self = Self { first: None, last: None };
pub const fn from_first(first: Option<f32>) -> Self {
Self { first, last: None }
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum AxisStaticEdge {
Start,
Center,
End,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct AxisStaticAlign {
pub keyword: AxisStaticEdge,
pub safety: AlignmentSafety,
pub fallback: AxisStaticEdge,
}
impl AxisStaticAlign {
pub const fn from_keyword(keyword: AxisStaticEdge) -> Self {
Self { keyword, safety: AlignmentSafety::Default, fallback: keyword }
}
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct AxisStaticPosition {
pub area: Line<f32>,
pub align: AxisStaticAlign,
}
impl AxisStaticPosition {
pub const fn from_edge(anchor: f32, edge: AxisStaticEdge) -> Self {
Self { area: Line { start: anchor, end: anchor }, align: AxisStaticAlign::from_keyword(edge) }
}
pub fn from_alignment(alignment: AlignSelf, area: Line<f32>, axis_is_rtl: bool) -> Self {
debug_assert!(
!matches!(alignment.keyword, AlignItemsKeyword::SelfStart | AlignItemsKeyword::SelfEnd),
"self-start/self-end must be resolved before computing the static position"
);
fn edge_for(keyword: AlignItemsKeyword, axis_is_rtl: bool) -> AxisStaticEdge {
let start_position = !matches!(keyword, AlignItemsKeyword::End | AlignItemsKeyword::FlexEnd) ^ axis_is_rtl;
match keyword {
AlignItemsKeyword::Center => AxisStaticEdge::Center,
_ if start_position => AxisStaticEdge::Start,
_ => AxisStaticEdge::End,
}
}
let fallback = if matches!(alignment.safety, AlignmentSafety::Safe) {
AlignItemsKeyword::Start
} else {
alignment.keyword
};
Self {
area,
align: AxisStaticAlign {
keyword: edge_for(alignment.keyword, axis_is_rtl),
safety: alignment.safety,
fallback: edge_for(fallback, axis_is_rtl),
},
}
}
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct OofCandidate {
pub node: NodeId,
pub order: u32,
pub position: Position,
pub static_position: Point<AxisStaticPosition>,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct OofCandidates {
inner: Option<crate::util::sys::Box<crate::util::sys::Vec<OofCandidate>>>,
}
impl OofCandidates {
pub const NONE: Self = Self { inner: None };
pub const fn new() -> Self {
Self::NONE
}
#[inline]
pub fn is_empty(&self) -> bool {
self.as_slice().is_empty()
}
#[inline]
pub fn as_slice(&self) -> &[OofCandidate] {
self.inner.as_ref().map(|boxed| boxed.as_slice()).unwrap_or(&[])
}
#[inline]
pub fn as_mut_slice(&mut self) -> &mut [OofCandidate] {
self.inner.as_mut().map(|boxed| boxed.as_mut_slice()).unwrap_or(&mut [])
}
#[inline]
pub fn push(&mut self, candidate: OofCandidate) {
self.inner.get_or_insert_with(Default::default).push(candidate);
}
#[inline]
pub fn append(&mut self, other: &mut OofCandidates) {
if other.is_empty() {
return;
}
match &mut self.inner {
None => self.inner = other.inner.take(),
Some(vec) => vec.append(other.inner.as_mut().unwrap()),
}
}
#[inline]
pub fn take(&mut self) -> Self {
core::mem::take(self)
}
#[inline]
pub fn clear(&mut self) {
self.inner = None;
}
#[inline]
pub fn iter(&self) -> core::slice::Iter<'_, OofCandidate> {
self.as_slice().iter()
}
#[inline]
pub fn translate(&mut self, offset: Point<f32>) {
for candidate in self.as_mut_slice() {
candidate.static_position.x.area.start += offset.x;
candidate.static_position.x.area.end += offset.x;
candidate.static_position.y.area.start += offset.y;
candidate.static_position.y.area.end += offset.y;
}
}
}
impl<'a> IntoIterator for &'a OofCandidates {
type Item = &'a OofCandidate;
type IntoIter = core::slice::Iter<'a, OofCandidate>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
#[derive(Debug, Copy, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct OofPositioningArea {
pub size: Size<f32>,
pub offset: Point<f32>,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct LayoutOutput {
pub size: Size<f32>,
#[cfg(feature = "content_size")]
pub scrollable_overflow_rect: ScrollableOverflow,
pub baselines: Baselines,
pub top_margin: CollapsibleMarginSet,
pub bottom_margin: CollapsibleMarginSet,
pub margins_can_collapse_through: bool,
#[cfg_attr(feature = "serde", serde(skip_serializing))]
pub oof_candidates: OofCandidates,
pub oof_positioning_area: Option<OofPositioningArea>,
}
impl LayoutOutput {
pub const HIDDEN: Self = Self {
size: Size::ZERO,
#[cfg(feature = "content_size")]
scrollable_overflow_rect: ScrollableOverflow::ZERO,
baselines: Baselines::NONE,
top_margin: CollapsibleMarginSet::ZERO,
bottom_margin: CollapsibleMarginSet::ZERO,
margins_can_collapse_through: false,
oof_candidates: OofCandidates::NONE,
oof_positioning_area: None,
};
pub const DEFAULT: Self = Self::HIDDEN;
pub fn from_sizes_and_baselines(
size: Size<f32>,
#[cfg_attr(not(feature = "content_size"), allow(unused_variables))]
scrollable_overflow_rect: ScrollableOverflow,
baselines: Baselines,
) -> Self {
Self {
size,
#[cfg(feature = "content_size")]
scrollable_overflow_rect,
baselines,
top_margin: CollapsibleMarginSet::ZERO,
bottom_margin: CollapsibleMarginSet::ZERO,
margins_can_collapse_through: false,
oof_candidates: OofCandidates::NONE,
oof_positioning_area: None,
}
}
pub fn from_sizes(size: Size<f32>, scrollable_overflow_rect: ScrollableOverflow) -> Self {
Self::from_sizes_and_baselines(size, scrollable_overflow_rect, Baselines::NONE)
}
pub fn transpose(self) -> Self {
Self {
size: self.size.transpose(),
#[cfg(feature = "content_size")]
scrollable_overflow_rect: self.scrollable_overflow_rect.transpose(),
baselines: Baselines::NONE,
top_margin: CollapsibleMarginSet::ZERO,
bottom_margin: CollapsibleMarginSet::ZERO,
margins_can_collapse_through: false,
oof_candidates: self.oof_candidates,
oof_positioning_area: self
.oof_positioning_area
.map(|area| OofPositioningArea { size: area.size.transpose(), offset: area.offset.transpose() }),
}
}
pub fn from_outer_size(size: Size<f32>) -> Self {
Self::from_sizes(size, ScrollableOverflow::ZERO)
}
}
#[derive(Debug, Copy, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct Layout {
pub order: u32,
pub location: Point<f32>,
pub size: Size<f32>,
#[cfg(feature = "content_size")]
pub scrollable_overflow_rect: ScrollableOverflow,
pub scrollbar_size: Size<f32>,
pub border: Rect<f32>,
pub padding: Rect<f32>,
pub margin: Rect<f32>,
}
impl Default for Layout {
fn default() -> Self {
Self::new()
}
}
impl Layout {
#[must_use]
pub const fn new() -> Self {
Self {
order: 0,
location: Point::ZERO,
size: Size::zero(),
#[cfg(feature = "content_size")]
scrollable_overflow_rect: ScrollableOverflow::ZERO,
scrollbar_size: Size::zero(),
border: Rect::zero(),
padding: Rect::zero(),
margin: Rect::zero(),
}
}
#[must_use]
pub const fn with_order(order: u32) -> Self {
Self {
order,
size: Size::zero(),
location: Point::ZERO,
#[cfg(feature = "content_size")]
scrollable_overflow_rect: ScrollableOverflow::ZERO,
scrollbar_size: Size::zero(),
border: Rect::zero(),
padding: Rect::zero(),
margin: Rect::zero(),
}
}
#[inline]
pub fn content_box_width(&self) -> f32 {
self.size.width - self.padding.left - self.padding.right - self.border.left - self.border.right
}
#[inline]
pub fn content_box_height(&self) -> f32 {
self.size.height - self.padding.top - self.padding.bottom - self.border.top - self.border.bottom
}
#[inline]
pub fn content_box_size(&self) -> Size<f32> {
Size { width: self.content_box_width(), height: self.content_box_height() }
}
pub fn content_box_x(&self) -> f32 {
self.location.x + self.border.left + self.padding.left
}
pub fn content_box_y(&self) -> f32 {
self.location.y + self.border.top + self.padding.top
}
}
#[cfg(feature = "content_size")]
impl Layout {
pub fn scroll_width(&self) -> f32 {
if self.scrollable_overflow_rect.is_reversed().x {
return -self.scrollable_overflow_rect.left();
}
f32_max(
0.0,
self.scrollable_overflow_rect.right() + f32_min(self.scrollbar_size.width, self.size.width)
- self.size.width
+ self.border.left
+ self.border.right,
)
}
pub fn scroll_height(&self) -> f32 {
if self.scrollable_overflow_rect.is_reversed().y {
return -self.scrollable_overflow_rect.top();
}
f32_max(
0.0,
self.scrollable_overflow_rect.bottom() + f32_min(self.scrollbar_size.height, self.size.height)
- self.size.height
+ self.border.top
+ self.border.bottom,
)
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum DetailedLayoutInfo<S: CheapCloneStr = DefaultCheapStr> {
#[cfg(feature = "grid")]
Grid(crate::util::sys::Box<crate::compute::grid::DetailedGridInfo<S>>),
None,
#[cfg(not(feature = "grid"))]
#[doc(hidden)]
Phantom(core::convert::Infallible, core::marker::PhantomData<S>),
}
#[cfg(test)]
mod scrollable_overflow_tests {
use super::ScrollableOverflow;
use crate::geometry::{Point, Rect};
#[test]
fn side_accessors_match_restored_rectangle() {
for rect in [
Rect::ZERO,
Rect { left: -0.0, top: -0.0, right: 0.0, bottom: 0.0 },
Rect { left: -10.0, top: -20.0, right: 30.0, bottom: 40.0 },
] {
for x in [false, true] {
for y in [false, true] {
let overflow = ScrollableOverflow::new(rect, Point { x, y });
let restored = overflow.rect();
assert_eq!(overflow.left().to_bits(), restored.left.to_bits());
assert_eq!(overflow.right().to_bits(), restored.right.to_bits());
assert_eq!(overflow.top().to_bits(), restored.top.to_bits());
assert_eq!(overflow.bottom().to_bits(), restored.bottom.to_bits());
}
}
}
}
#[test]
fn packing_preserves_flags_rectangles_and_signed_zeroes() {
for right in [0.0, 10.0] {
for bottom in [0.0, 20.0] {
for x in [false, true] {
for y in [false, true] {
let rect = Rect { left: -0.0, right, top: -0.0, bottom };
let reversed = Point { x, y };
let overflow = ScrollableOverflow::new(rect, reversed);
assert_eq!(overflow.left().to_bits(), (-0.0_f32).to_bits());
assert_eq!(overflow.top().to_bits(), (-0.0_f32).to_bits());
assert_eq!(overflow.right().to_bits(), right.to_bits());
assert_eq!(overflow.bottom().to_bits(), bottom.to_bits());
assert_eq!(overflow.is_reversed(), reversed);
assert_eq!(overflow.0.right.is_sign_negative(), x);
assert_eq!(overflow.0.bottom.is_sign_negative(), y);
assert_eq!(overflow.transpose().is_reversed(), Point { x: y, y: x });
assert_eq!(overflow.transpose().transpose(), overflow);
}
}
}
}
assert_eq!(ScrollableOverflow::default(), ScrollableOverflow::ZERO);
assert_ne!(ScrollableOverflow::ZERO, ScrollableOverflow::new(Rect::ZERO, Point { x: true, y: false }));
assert_ne!(
ScrollableOverflow::ZERO,
ScrollableOverflow::new(Rect { left: -0.0, ..Rect::ZERO }, Point { x: false, y: false })
);
assert_ne!(
ScrollableOverflow::ZERO,
ScrollableOverflow::new(Rect { top: -0.0, ..Rect::ZERO }, Point { x: false, y: false })
);
}
#[test]
#[cfg(debug_assertions)]
#[should_panic]
fn packing_rejects_invalid_rectangles() {
ScrollableOverflow::new(Rect { left: 1.0, ..Rect::ZERO }, Point { x: false, y: false });
}
#[test]
#[cfg(feature = "serde")]
fn serialization_exposes_rect_and_flags_not_packed_signs() {
let overflow = ScrollableOverflow::new(
Rect { left: -10.0, right: 20.0, top: 0.0, bottom: 0.0 },
Point { x: true, y: true },
);
assert_eq!(
serde_json::to_value(overflow).unwrap(),
serde_json::json!({
"rect": { "left": -10.0, "right": 20.0, "top": 0.0, "bottom": 0.0 },
"reversed": { "x": true, "y": true }
})
);
}
}