///|
/// Parse a CSS easing keyword or basic timing function.
pub fn parse_easing(value : String) -> Easing? {
let normalized = value.trim().to_lower().to_owned()
match parse_named_easing(normalized) {
Some(easing) => Some(easing)
None =>
match parse_linear_function(normalized) {
Some(easing) => Some(easing)
None =>
match parse_cubic_bezier(normalized) {
Some(easing) => Some(easing)
None => parse_steps(normalized)
}
}
}
}
///|
/// CSS comparison/math function selector for min() / max() / clamp().
pub(all) enum MathOp {
Min
Max
Clamp
} derive(Eq, Debug)
///|
/// Apply a min()/max()/clamp() reduction over already-resolved scalar values.
/// clamp expects exactly 3 values (lo, val, hi); other lengths fall back to the
/// first value (or 0.0 when empty) so callers never panic on malformed input.
pub fn apply_math_op(op : MathOp, vals : Array[Double]) -> Double {
match op {
Clamp =>
if vals.length() == 3 {
// CSS: clamp(MIN, VAL, MAX) == max(MIN, min(VAL, MAX)). This stays
// correct even when MIN > MAX (it returns MIN), unlike ordered ifs.
let lo = vals[0]
let mid = vals[1]
let hi = vals[2]
let inner = if mid < hi { mid } else { hi }
if lo > inner {
lo
} else {
inner
}
} else if vals.length() > 0 {
vals[0]
} else {
0.0
}
_ => {
if vals.is_empty() {
return 0.0
}
let mut acc = vals[0]
for k = 1; k < vals.length(); k = k + 1 {
match op {
Min => if vals[k] < acc { acc = vals[k] }
_ => if vals[k] > acc { acc = vals[k] }
}
}
acc
}
}
}
///|
/// CSS dimension value: length, percentage, auto, or intrinsic sizing keywords
pub(all) enum Dimension {
Length(Double)
Percent(Double) // 0.0 to 1.0
Auto
MinContent // Intrinsic minimum content size
MaxContent // Intrinsic maximum content size
FitContent(Double) // fit-content(length) - clamp between min-content and max-content
// Mixed calc() carrying both a fixed length (px) and a percentage ratio,
// e.g. calc(100% - 20px) => Calc(px=-20.0, percent=1.0). Resolves to
// `px + percent * basis` once a containing-block basis is known at layout
// time. Pure-length / pure-percent calc() stay Length / Percent.
Calc(Double, Double) // Calc(px, percent_ratio)
// min()/max()/clamp() whose arguments mix length and percentage and so cannot
// be ordered until a containing-block basis is known. Each argument is a
// linear (px, percent_ratio) form. Resolved at layout time like Calc.
MathFn(MathOp, Array[(Double, Double)])
} derive(Debug, Eq)
///|
/// Check if dimension equals a specific length value
pub fn Dimension::eq_length(self : Dimension, value : Double) -> Bool {
match self {
Length(v) => (v - value).abs() < 0.0001
_ => false
}
}
///|
/// Check if dimension equals a specific percent value
pub fn Dimension::eq_percent(self : Dimension, value : Double) -> Bool {
match self {
Percent(v) => (v - value).abs() < 0.0001
_ => false
}
}
///|
pub impl Show for Dimension with fn output(self, logger) {
match self {
Length(v) => {
logger.write_string("Length(")
v.output(logger)
logger.write_string(")")
}
Percent(v) => {
logger.write_string("Percent(")
v.output(logger)
logger.write_string(")")
}
Auto => logger.write_string("Auto")
MinContent => logger.write_string("MinContent")
MaxContent => logger.write_string("MaxContent")
FitContent(v) => {
logger.write_string("FitContent(")
v.output(logger)
logger.write_string(")")
}
Calc(px, pct) => {
logger.write_string("Calc(")
px.output(logger)
logger.write_string(", ")
pct.output(logger)
logger.write_string(")")
}
MathFn(op, args) => {
logger.write_string("MathFn(")
logger.write_string(
match op {
Min => "Min"
Max => "Max"
Clamp => "Clamp"
},
)
logger.write_string(", [")
for i = 0; i < args.length(); i = i + 1 {
if i > 0 {
logger.write_string(", ")
}
let (px, pct) = args[i]
logger.write_string("(")
px.output(logger)
logger.write_string(", ")
pct.output(logger)
logger.write_string(")")
}
logger.write_string("])")
}
}
}
///|
/// Resolve dimension to concrete value
/// context: parent size for percentage calculation
/// Note: MinContent, MaxContent, FitContent return None - they need intrinsic size calculation
pub fn Dimension::resolve(self : Dimension, context : Double) -> Double? {
match self {
Length(v) => Some(v)
Percent(v) => Some(context * v)
Calc(px, pct) => Some(px + pct * context)
MathFn(op, args) =>
if args.is_empty() {
None
} else {
Some(apply_math_op(op, args.map(fn(a) { a.0 + a.1 * context })))
}
Auto => None
MinContent => None // Requires intrinsic size calculation
MaxContent => None // Requires intrinsic size calculation
FitContent(_) => None // Requires intrinsic size calculation
}
}
///|
/// Resolve with fallback value for Auto
///
/// Matches `self` directly rather than going through `resolve`, which returns a
/// `Double?` and so boxes a `Some(..)` on every call. This is on the hot layout
/// path (every width/height/padding/margin per element, plus all four sides via
/// `resolve_rect`), where the intermediate `Option` allocation dominated.
pub fn Dimension::resolve_or(
self : Dimension,
context : Double,
fallback : Double,
) -> Double {
match self {
Length(v) => v
Percent(v) => context * v
Calc(px, pct) => px + pct * context
MathFn(op, args) =>
if args.is_empty() {
fallback
} else {
apply_math_op(op, args.map(fn(a) { a.0 + a.1 * context }))
}
Auto => fallback
MinContent => fallback // Requires intrinsic size calculation
MaxContent => fallback
FitContent(_) => fallback
}
}
///|
/// Check if dimension is a definite value (Length or Percent)
pub fn Dimension::is_definite(self : Dimension) -> Bool {
match self {
Length(_) | Percent(_) | Calc(_, _) | MathFn(_, _) => true
_ => false
}
}
///|
/// Check if dimension requires intrinsic sizing
pub fn Dimension::is_intrinsic(self : Dimension) -> Bool {
match self {
MinContent | MaxContent | FitContent(_) => true
_ => false
}
}
///|
/// Resolve dimension to length, treating intrinsic keywords as fallback
pub fn Dimension::resolve_or_intrinsic(
self : Dimension,
context : Double,
fallback : Double,
) -> Double {
match self {
Length(v) => v
Percent(p) => context * p
Calc(px, p) => px + context * p
MathFn(op, args) =>
if args.is_empty() {
fallback
} else {
apply_math_op(op, args.map(fn(a) { a.0 + a.1 * context }))
}
Auto => fallback
MinContent => fallback // Intrinsic sizing handled by layout engine
MaxContent => fallback
FitContent(_) => fallback
}
}
///|
/// Resolve a Rect[Dimension] (margin/padding/border) to Rect[Double]
/// Uses parent_width for percentage resolution (CSS spec: percentages resolve against width)
pub fn resolve_rect(
rect : Rect[Dimension],
parent_width : Double,
) -> Rect[Double] {
{
left: rect.left.resolve_or(parent_width, 0.0),
right: rect.right.resolve_or(parent_width, 0.0),
top: rect.top.resolve_or(parent_width, 0.0),
bottom: rect.bottom.resolve_or(parent_width, 0.0),
}
}
///|
/// Resolve a dimension rect for intrinsic sizing
/// Percentages resolve to 0 in intrinsic sizing contexts
pub fn resolve_rect_intrinsic(rect : Rect[Dimension]) -> Rect[Double] {
fn resolve_intrinsic(dim : Dimension) -> Double {
match dim {
Length(v) => v
Percent(_) => 0.0
Calc(px, _) => px // percent resolves to 0 in intrinsic context; keep px
MathFn(op, args) =>
// intrinsic context: percentages resolve to 0, so keep px parts only
if args.is_empty() {
0.0
} else {
apply_math_op(op, args.map(fn(a) { a.0 }))
}
Auto => 0.0
MinContent => 0.0 // Not applicable for margin/padding/border
MaxContent => 0.0
FitContent(_) => 0.0
}
}
{
left: resolve_intrinsic(rect.left),
right: resolve_intrinsic(rect.right),
top: resolve_intrinsic(rect.top),
bottom: resolve_intrinsic(rect.bottom),
}
}
///|
/// BoundingRect - the output of layout computation
/// Matches browser's getBoundingClientRect()
pub(all) struct BoundingRect {
x : Double
y : Double
width : Double
height : Double
}
///|
pub fn BoundingRect::new(
x : Double,
y : Double,
width : Double,
height : Double,
) -> BoundingRect {
{ x, y, width, height }
}
///|
pub impl Show for BoundingRect with fn output(self, logger) {
logger.write_string("BoundingRect { x: ")
self.x.output(logger)
logger.write_string(", y: ")
self.y.output(logger)
logger.write_string(", width: ")
self.width.output(logger)
logger.write_string(", height: ")
self.height.output(logger)
logger.write_string(" }")
}
///|
pub fn BoundingRect::zero() -> BoundingRect {
{ x: 0.0, y: 0.0, width: 0.0, height: 0.0 }
}
///|
/// Get the right edge (x + width)
pub fn BoundingRect::right(self : BoundingRect) -> Double {
self.x + self.width
}
///|
/// Get the bottom edge (y + height)
pub fn BoundingRect::bottom(self : BoundingRect) -> Double {
self.y + self.height
}
///|
/// Calculate the area of the bounding rect
pub fn BoundingRect::area(self : BoundingRect) -> Double {
self.width * self.height
}
///|
/// Calculate union of two bounding rects
pub fn BoundingRect::union(
self : BoundingRect,
other : BoundingRect,
) -> BoundingRect {
let x = min(self.x, other.x)
let y = min(self.y, other.y)
let right = max(self.right(), other.right())
let bottom = max(self.bottom(), other.bottom())
{ x, y, width: right - x, height: bottom - y }
}