1use serde::{Deserialize, Serialize};
14
15use crate::{ErrorCode, FileMakerError, Length, Result, Size, Unit};
16
17#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
19#[serde(rename_all = "snake_case")]
20pub enum Alignment {
21 Start,
23 Center,
25 End,
27}
28
29#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
31#[serde(rename_all = "snake_case")]
32pub enum Distribution {
33 #[default]
35 Start,
36 Center,
38 End,
40 SpaceBetween,
42 SpaceAround,
44 SpaceEvenly,
46}
47
48#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
50#[serde(default, deny_unknown_fields)]
51pub struct LayoutConstraints {
52 pub min_width: Option<Length>,
54 pub preferred_width: Option<Length>,
56 pub max_width: Option<Length>,
58 pub min_height: Option<Length>,
60 pub preferred_height: Option<Length>,
62 pub max_height: Option<Length>,
64 pub aspect_ratio: Option<u32>,
66}
67
68impl LayoutConstraints {
69 pub(crate) fn validate(&self) -> Result<()> {
70 if self.aspect_ratio == Some(0)
71 || [
72 self.min_width,
73 self.preferred_width,
74 self.max_width,
75 self.min_height,
76 self.preferred_height,
77 self.max_height,
78 ]
79 .into_iter()
80 .flatten()
81 .any(|length| matches!(length, Length::Auto))
82 {
83 return Err(constraint_error(
84 "layout constraints require concrete lengths and a positive aspect ratio",
85 ));
86 }
87 Ok(())
88 }
89}
90
91pub(crate) fn resolve_constrained_size(
92 explicit_width: Option<Length>,
93 explicit_height: Option<Length>,
94 constraints: LayoutConstraints,
95 container: Size,
96 default_size: Size,
97 logical_unit: Unit,
98) -> Result<Size> {
99 constraints.validate()?;
100 let width_intent = resolve_optional(explicit_width, container.width, logical_unit)?.or(
101 resolve_optional(constraints.preferred_width, container.width, logical_unit)?,
102 );
103 let height_intent = resolve_optional(explicit_height, container.height, logical_unit)?.or(
104 resolve_optional(constraints.preferred_height, container.height, logical_unit)?,
105 );
106 let width_range = resolve_range(
107 constraints.min_width,
108 constraints.max_width,
109 container.width,
110 logical_unit,
111 )?;
112 let height_range = resolve_range(
113 constraints.min_height,
114 constraints.max_height,
115 container.height,
116 logical_unit,
117 )?;
118 let mut width = clamp(width_intent.unwrap_or(default_size.width), width_range)?;
119 let mut height = clamp(height_intent.unwrap_or(default_size.height), height_range)?;
120 if let Some(ratio) = constraints.aspect_ratio {
121 match (width_intent, height_intent) {
122 (Some(_), Some(_)) => validate_ratio(width, height, ratio)?,
123 (None, Some(_)) => {
124 width = clamp(height.checked_scale(i64::from(ratio))?, width_range)?;
125 validate_ratio(width, height, ratio)?;
126 }
127 (Some(_), None) | (None, None) => {
128 height = clamp(
129 Unit::from_ratio(i128::from(width.raw()), i128::from(ratio))?,
130 height_range,
131 )?;
132 validate_ratio(width, height, ratio)?;
133 }
134 }
135 }
136 Size::new(width, height)
137}
138
139fn resolve_optional(
140 value: Option<Length>,
141 percent_base: Unit,
142 logical_unit: Unit,
143) -> Result<Option<Unit>> {
144 value.map_or(Ok(None), |length| {
145 length.resolve(percent_base, logical_unit)
146 })
147}
148
149fn resolve_range(
150 minimum: Option<Length>,
151 maximum: Option<Length>,
152 percent_base: Unit,
153 logical_unit: Unit,
154) -> Result<(Option<Unit>, Option<Unit>)> {
155 let minimum = resolve_optional(minimum, percent_base, logical_unit)?;
156 let maximum = resolve_optional(maximum, percent_base, logical_unit)?;
157 if minimum.zip(maximum).is_some_and(|(min, max)| min > max) {
158 return Err(constraint_error(
159 "minimum layout constraint exceeds maximum",
160 ));
161 }
162 Ok((minimum, maximum))
163}
164
165fn clamp(value: Unit, range: (Option<Unit>, Option<Unit>)) -> Result<Unit> {
166 let value = range.0.map_or(value, |minimum| value.max(minimum));
167 let value = range.1.map_or(value, |maximum| value.min(maximum));
168 if value < Unit::ZERO {
169 return Err(constraint_error(
170 "resolved constraint size cannot be negative",
171 ));
172 }
173 Ok(value)
174}
175
176fn validate_ratio(width: Unit, height: Unit, ratio: u32) -> Result<()> {
177 if height <= Unit::ZERO {
178 return Err(constraint_error("aspect ratio requires positive height"));
179 }
180 let expected = height.checked_scale(i64::from(ratio))?;
181 if width.raw().abs_diff(expected.raw()) > 1 {
182 return Err(constraint_error(
183 "resolved min/max constraints conflict with aspect ratio",
184 ));
185 }
186 Ok(())
187}
188
189fn constraint_error(message: impl Into<String>) -> FileMakerError {
190 FileMakerError::new(ErrorCode::LayoutInvalid, message)
191}