1#[derive(Debug, Clone, PartialEq, Default)]
41pub enum Constraint {
42 Px(f32),
44
45 Percent(f32),
47
48 #[default]
50 Auto,
51
52 Vw(f32),
54
55 Vh(f32),
57
58 Vmin(f32),
60
61 Vmax(f32),
63
64 Calc(Box<CalcExpr>),
66
67 Min(Vec<Constraint>),
69
70 Max(Vec<Constraint>),
72
73 Clamp {
75 min: Box<Constraint>,
77 val: Box<Constraint>,
79 max: Box<Constraint>,
81 },
82}
83
84impl Constraint {
85 #[inline]
87 pub fn px(value: f32) -> Self {
88 Self::Px(value)
89 }
90
91 #[inline]
93 pub fn percent(value: f32) -> Self {
94 Self::Percent(value)
95 }
96
97 #[inline]
99 pub fn vw(value: f32) -> Self {
100 Self::Vw(value)
101 }
102
103 #[inline]
105 pub fn vh(value: f32) -> Self {
106 Self::Vh(value)
107 }
108
109 #[inline]
111 pub fn vmin(value: f32) -> Self {
112 Self::Vmin(value)
113 }
114
115 #[inline]
117 pub fn vmax(value: f32) -> Self {
118 Self::Vmax(value)
119 }
120
121 pub fn calc(expr: CalcExpr) -> Self {
123 Self::Calc(Box::new(expr.simplify()))
124 }
125
126 pub fn min(values: Vec<Constraint>) -> Self {
128 debug_assert!(!values.is_empty(), "min() requires at least one value");
129 Self::Min(values)
130 }
131
132 pub fn max(values: Vec<Constraint>) -> Self {
134 debug_assert!(!values.is_empty(), "max() requires at least one value");
135 Self::Max(values)
136 }
137
138 pub fn clamp(min: Constraint, val: Constraint, max: Constraint) -> Self {
140 Self::Clamp {
141 min: Box::new(min),
142 val: Box::new(val),
143 max: Box::new(max),
144 }
145 }
146
147 pub fn is_simple(&self) -> bool {
149 matches!(
150 self,
151 Self::Px(_)
152 | Self::Percent(_)
153 | Self::Auto
154 | Self::Vw(_)
155 | Self::Vh(_)
156 | Self::Vmin(_)
157 | Self::Vmax(_)
158 )
159 }
160
161 pub fn has_viewport_units(&self) -> bool {
163 match self {
164 Self::Vw(_) | Self::Vh(_) | Self::Vmin(_) | Self::Vmax(_) => true,
165 Self::Calc(expr) => expr.has_viewport_units(),
166 Self::Min(values) | Self::Max(values) => values.iter().any(|c| c.has_viewport_units()),
167 Self::Clamp { min, val, max } => {
168 min.has_viewport_units() || val.has_viewport_units() || max.has_viewport_units()
169 }
170 _ => false,
171 }
172 }
173
174 pub fn has_percentages(&self) -> bool {
176 match self {
177 Self::Percent(_) => true,
178 Self::Calc(expr) => expr.has_percentages(),
179 Self::Min(values) | Self::Max(values) => values.iter().any(|c| c.has_percentages()),
180 Self::Clamp { min, val, max } => {
181 min.has_percentages() || val.has_percentages() || max.has_percentages()
182 }
183 _ => false,
184 }
185 }
186}
187
188impl From<f32> for Constraint {
189 fn from(value: f32) -> Self {
190 Self::Px(value)
191 }
192}
193
194impl From<crate::length::Length> for Constraint {
199 fn from(length: crate::length::Length) -> Self {
200 match length {
201 crate::length::Length::Px(v) => Self::Px(v),
202 crate::length::Length::Percent(v) => Self::Percent(v),
203 crate::length::Length::Auto => Self::Auto,
204 crate::length::Length::Vw(v) => Self::Vw(v),
205 crate::length::Length::Vh(v) => Self::Vh(v),
206 crate::length::Length::Vmin(v) => Self::Vmin(v),
207 crate::length::Length::Vmax(v) => Self::Vmax(v),
208 }
209 }
210}
211
212impl From<crate::length::LengthAuto> for Constraint {
213 fn from(length: crate::length::LengthAuto) -> Self {
214 match length {
215 crate::length::LengthAuto::Px(v) => Self::Px(v),
216 crate::length::LengthAuto::Percent(v) => Self::Percent(v),
217 crate::length::LengthAuto::Auto => Self::Auto,
218 crate::length::LengthAuto::Vw(v) => Self::Vw(v),
219 crate::length::LengthAuto::Vh(v) => Self::Vh(v),
220 crate::length::LengthAuto::Vmin(v) => Self::Vmin(v),
221 crate::length::LengthAuto::Vmax(v) => Self::Vmax(v),
222 }
223 }
224}
225
226impl From<crate::length::LengthPercentage> for Constraint {
227 fn from(length: crate::length::LengthPercentage) -> Self {
228 match length {
229 crate::length::LengthPercentage::Px(v) => Self::Px(v),
230 crate::length::LengthPercentage::Percent(v) => Self::Percent(v),
231 crate::length::LengthPercentage::Vw(v) => Self::Vw(v),
232 crate::length::LengthPercentage::Vh(v) => Self::Vh(v),
233 crate::length::LengthPercentage::Vmin(v) => Self::Vmin(v),
234 crate::length::LengthPercentage::Vmax(v) => Self::Vmax(v),
235 }
236 }
237}
238
239impl Constraint {
244 pub fn to_dimension(&self) -> taffy::Dimension {
257 match self {
258 Constraint::Px(v) => taffy::Dimension::Length(*v),
259 Constraint::Percent(v) => taffy::Dimension::Percent(*v / 100.0),
260 Constraint::Auto => taffy::Dimension::Auto,
261 Constraint::Vw(_) | Constraint::Vh(_) | Constraint::Vmin(_) | Constraint::Vmax(_) => {
262 panic!(
263 "Viewport-relative constraints must be resolved to pixels before converting to Taffy dimension. \
264 Use ConstraintResolver::resolve() first."
265 );
266 }
267 Constraint::Calc(_)
268 | Constraint::Min(_)
269 | Constraint::Max(_)
270 | Constraint::Clamp { .. } => {
271 panic!(
272 "Complex constraints (calc/min/max/clamp) must be resolved to pixels before converting to Taffy dimension. \
273 Use ConstraintResolver::resolve() first."
274 );
275 }
276 }
277 }
278
279 pub fn try_to_dimension(&self) -> Option<taffy::Dimension> {
283 match self {
284 Constraint::Px(v) => Some(taffy::Dimension::Length(*v)),
285 Constraint::Percent(v) => Some(taffy::Dimension::Percent(*v / 100.0)),
286 Constraint::Auto => Some(taffy::Dimension::Auto),
287 _ => None,
288 }
289 }
290
291 pub fn try_to_length_percentage_auto(&self) -> Option<taffy::LengthPercentageAuto> {
295 match self {
296 Constraint::Px(v) => Some(taffy::LengthPercentageAuto::Length(*v)),
297 Constraint::Percent(v) => Some(taffy::LengthPercentageAuto::Percent(*v / 100.0)),
298 Constraint::Auto => Some(taffy::LengthPercentageAuto::Auto),
299 _ => None,
300 }
301 }
302
303 pub fn try_to_length_percentage(&self) -> Option<taffy::LengthPercentage> {
307 match self {
308 Constraint::Px(v) => Some(taffy::LengthPercentage::Length(*v)),
309 Constraint::Percent(v) => Some(taffy::LengthPercentage::Percent(*v / 100.0)),
310 _ => None,
311 }
312 }
313
314 pub fn to_length_percentage_auto(&self) -> taffy::LengthPercentageAuto {
319 match self {
320 Constraint::Px(v) => taffy::LengthPercentageAuto::Length(*v),
321 Constraint::Percent(v) => taffy::LengthPercentageAuto::Percent(*v / 100.0),
322 Constraint::Auto => taffy::LengthPercentageAuto::Auto,
323 Constraint::Vw(_) | Constraint::Vh(_) | Constraint::Vmin(_) | Constraint::Vmax(_) => {
324 panic!("Viewport-relative constraints must be resolved to pixels first.");
325 }
326 Constraint::Calc(_)
327 | Constraint::Min(_)
328 | Constraint::Max(_)
329 | Constraint::Clamp { .. } => {
330 panic!("Complex constraints must be resolved to pixels first.");
331 }
332 }
333 }
334
335 pub fn to_length_percentage(&self) -> taffy::LengthPercentage {
340 match self {
341 Constraint::Px(v) => taffy::LengthPercentage::Length(*v),
342 Constraint::Percent(v) => taffy::LengthPercentage::Percent(*v / 100.0),
343 Constraint::Auto => panic!("Auto is not valid for LengthPercentage"),
344 Constraint::Vw(_) | Constraint::Vh(_) | Constraint::Vmin(_) | Constraint::Vmax(_) => {
345 panic!("Viewport-relative constraints must be resolved to pixels first.");
346 }
347 Constraint::Calc(_)
348 | Constraint::Min(_)
349 | Constraint::Max(_)
350 | Constraint::Clamp { .. } => {
351 panic!("Complex constraints must be resolved to pixels first.");
352 }
353 }
354 }
355
356 pub fn needs_resolution(&self) -> bool {
361 self.try_to_dimension().is_none()
362 }
363}
364
365impl From<Constraint> for taffy::Dimension {
366 fn from(constraint: Constraint) -> Self {
367 constraint.to_dimension()
368 }
369}
370
371#[derive(Debug, Clone, PartialEq)]
375pub enum CalcExpr {
376 Value(Constraint),
378
379 Add(Box<CalcExpr>, Box<CalcExpr>),
381
382 Sub(Box<CalcExpr>, Box<CalcExpr>),
384
385 Mul(Box<CalcExpr>, f32),
387
388 Div(Box<CalcExpr>, f32),
390}
391
392impl CalcExpr {
393 pub fn value(constraint: Constraint) -> Self {
395 Self::Value(constraint)
396 }
397
398 pub fn simplify(self) -> Self {
402 match self {
403 Self::Add(lhs, rhs) => {
405 let lhs = lhs.simplify();
406 let rhs = rhs.simplify();
407
408 match (&lhs, &rhs) {
409 (Self::Value(Constraint::Px(a)), Self::Value(Constraint::Px(b))) => {
411 Self::Value(Constraint::Px(a + b))
412 }
413 (Self::Value(Constraint::Px(0.0)), _) => rhs,
415 (_, Self::Value(Constraint::Px(0.0))) => lhs,
417 _ => Self::Add(Box::new(lhs), Box::new(rhs)),
418 }
419 }
420
421 Self::Sub(lhs, rhs) => {
423 let lhs = lhs.simplify();
424 let rhs = rhs.simplify();
425
426 match (&lhs, &rhs) {
427 (Self::Value(Constraint::Px(a)), Self::Value(Constraint::Px(b))) => {
429 Self::Value(Constraint::Px(a - b))
430 }
431 (_, Self::Value(Constraint::Px(0.0))) => lhs,
433 _ => Self::Sub(Box::new(lhs), Box::new(rhs)),
434 }
435 }
436
437 Self::Mul(expr, scalar) => {
439 let expr = expr.simplify();
440
441 match &expr {
442 Self::Value(Constraint::Px(v)) => Self::Value(Constraint::Px(v * scalar)),
444 _ if (scalar - 1.0).abs() < f32::EPSILON => expr,
446 _ if scalar.abs() < f32::EPSILON => Self::Value(Constraint::Px(0.0)),
448 _ => Self::Mul(Box::new(expr), scalar),
449 }
450 }
451
452 Self::Div(expr, scalar) => {
454 let expr = expr.simplify();
455
456 match &expr {
457 Self::Value(Constraint::Px(v)) => Self::Value(Constraint::Px(v / scalar)),
459 _ if (scalar - 1.0).abs() < f32::EPSILON => expr,
461 _ => Self::Div(Box::new(expr), scalar),
462 }
463 }
464
465 Self::Value(c) => Self::Value(c),
467 }
468 }
469
470 pub fn has_viewport_units(&self) -> bool {
472 match self {
473 Self::Value(c) => c.has_viewport_units(),
474 Self::Add(lhs, rhs) | Self::Sub(lhs, rhs) => {
475 lhs.has_viewport_units() || rhs.has_viewport_units()
476 }
477 Self::Mul(expr, _) | Self::Div(expr, _) => expr.has_viewport_units(),
478 }
479 }
480
481 pub fn has_percentages(&self) -> bool {
483 match self {
484 Self::Value(c) => c.has_percentages(),
485 Self::Add(lhs, rhs) | Self::Sub(lhs, rhs) => {
486 lhs.has_percentages() || rhs.has_percentages()
487 }
488 Self::Mul(expr, _) | Self::Div(expr, _) => expr.has_percentages(),
489 }
490 }
491}
492
493impl From<Constraint> for CalcExpr {
494 fn from(constraint: Constraint) -> Self {
495 Self::Value(constraint)
496 }
497}
498
499impl From<f32> for CalcExpr {
500 fn from(value: f32) -> Self {
501 Self::Value(Constraint::Px(value))
502 }
503}
504
505impl std::ops::Add for CalcExpr {
508 type Output = Self;
509
510 fn add(self, rhs: Self) -> Self::Output {
511 Self::Add(Box::new(self), Box::new(rhs))
512 }
513}
514
515impl std::ops::Sub for CalcExpr {
516 type Output = Self;
517
518 fn sub(self, rhs: Self) -> Self::Output {
519 Self::Sub(Box::new(self), Box::new(rhs))
520 }
521}
522
523impl std::ops::Mul<f32> for CalcExpr {
524 type Output = Self;
525
526 fn mul(self, rhs: f32) -> Self::Output {
527 Self::Mul(Box::new(self), rhs)
528 }
529}
530
531impl std::ops::Div<f32> for CalcExpr {
532 type Output = Self;
533
534 fn div(self, rhs: f32) -> Self::Output {
535 Self::Div(Box::new(self), rhs)
536 }
537}
538
539#[cfg(test)]
540mod tests {
541 use super::*;
542
543 #[test]
544 fn test_constraint_constructors() {
545 assert_eq!(Constraint::px(100.0), Constraint::Px(100.0));
546 assert_eq!(Constraint::percent(50.0), Constraint::Percent(50.0));
547 assert_eq!(Constraint::vw(80.0), Constraint::Vw(80.0));
548 assert_eq!(Constraint::vh(60.0), Constraint::Vh(60.0));
549 }
550
551 #[test]
552 fn test_constraint_is_simple() {
553 assert!(Constraint::Px(100.0).is_simple());
554 assert!(Constraint::Percent(50.0).is_simple());
555 assert!(Constraint::Auto.is_simple());
556 assert!(Constraint::Vw(50.0).is_simple());
557
558 let calc = Constraint::calc(CalcExpr::Value(Constraint::Px(100.0)));
559 assert!(!calc.is_simple());
560
561 let min = Constraint::min(vec![Constraint::Px(100.0)]);
562 assert!(!min.is_simple());
563 }
564
565 #[test]
566 fn test_constraint_has_viewport_units() {
567 assert!(!Constraint::Px(100.0).has_viewport_units());
568 assert!(!Constraint::Percent(50.0).has_viewport_units());
569 assert!(Constraint::Vw(50.0).has_viewport_units());
570 assert!(Constraint::Vh(50.0).has_viewport_units());
571 assert!(Constraint::Vmin(50.0).has_viewport_units());
572 assert!(Constraint::Vmax(50.0).has_viewport_units());
573
574 let calc = Constraint::calc(CalcExpr::Value(Constraint::Vw(50.0)));
575 assert!(calc.has_viewport_units());
576
577 let min = Constraint::min(vec![Constraint::Px(100.0), Constraint::Vw(50.0)]);
578 assert!(min.has_viewport_units());
579 }
580
581 #[test]
582 fn test_calc_expr_simplify_add() {
583 let expr = CalcExpr::Add(
585 Box::new(CalcExpr::Value(Constraint::Px(10.0))),
586 Box::new(CalcExpr::Value(Constraint::Px(20.0))),
587 );
588 assert_eq!(expr.simplify(), CalcExpr::Value(Constraint::Px(30.0)));
589
590 let expr = CalcExpr::Add(
592 Box::new(CalcExpr::Value(Constraint::Px(0.0))),
593 Box::new(CalcExpr::Value(Constraint::Percent(50.0))),
594 );
595 assert_eq!(expr.simplify(), CalcExpr::Value(Constraint::Percent(50.0)));
596 }
597
598 #[test]
599 fn test_calc_expr_simplify_sub() {
600 let expr = CalcExpr::Sub(
602 Box::new(CalcExpr::Value(Constraint::Px(30.0))),
603 Box::new(CalcExpr::Value(Constraint::Px(10.0))),
604 );
605 assert_eq!(expr.simplify(), CalcExpr::Value(Constraint::Px(20.0)));
606 }
607
608 #[test]
609 fn test_calc_expr_simplify_mul() {
610 let expr = CalcExpr::Mul(Box::new(CalcExpr::Value(Constraint::Px(10.0))), 3.0);
612 assert_eq!(expr.simplify(), CalcExpr::Value(Constraint::Px(30.0)));
613
614 let expr = CalcExpr::Mul(Box::new(CalcExpr::Value(Constraint::Percent(50.0))), 1.0);
616 assert_eq!(expr.simplify(), CalcExpr::Value(Constraint::Percent(50.0)));
617 }
618
619 #[test]
620 fn test_calc_expr_operators() {
621 let a = CalcExpr::from(Constraint::Percent(100.0));
622 let b = CalcExpr::from(Constraint::Px(40.0));
623
624 let result = (a - b).simplify();
626 match result {
627 CalcExpr::Sub(lhs, rhs) => {
628 assert_eq!(*lhs, CalcExpr::Value(Constraint::Percent(100.0)));
629 assert_eq!(*rhs, CalcExpr::Value(Constraint::Px(40.0)));
630 }
631 _ => panic!("Expected Sub expression"),
632 }
633 }
634}