1pub const DEFAULT_TEXT_PADDING: f64 = 0.0;
8
9#[derive(Debug, Clone, Copy, PartialEq, Default)]
11pub struct Point {
12 pub x: f64,
13 pub y: f64,
14}
15
16#[derive(Debug, Clone, Copy, PartialEq, Default)]
18pub struct Size {
19 pub width: f64,
20 pub height: f64,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Default)]
25pub struct Rect {
26 pub x: f64,
27 pub y: f64,
28 pub width: f64,
29 pub height: f64,
30}
31
32impl Rect {
33 pub fn new(x: f64, y: f64, width: f64, height: f64) -> Self {
34 Self {
35 x,
36 y,
37 width,
38 height,
39 }
40 }
41
42 pub fn right(&self) -> f64 {
43 self.x + self.width
44 }
45
46 pub fn bottom(&self) -> f64 {
47 self.y + self.height
48 }
49
50 pub fn contains(&self, px: f64, py: f64) -> bool {
52 px >= self.x && px <= self.right() && py >= self.y && py <= self.bottom()
53 }
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Default)]
58pub struct Insets {
59 pub top: f64,
60 pub right: f64,
61 pub bottom: f64,
62 pub left: f64,
63}
64
65impl Insets {
66 pub fn uniform(value: f64) -> Self {
67 Self {
68 top: value,
69 right: value,
70 bottom: value,
71 left: value,
72 }
73 }
74
75 pub fn horizontal(&self) -> f64 {
76 self.left + self.right
77 }
78
79 pub fn vertical(&self) -> f64 {
80 self.top + self.bottom
81 }
82}
83
84#[derive(Debug, Clone, Copy, PartialEq)]
91pub struct Measurement {
92 pub value: f64,
93 pub unit: MeasurementUnit,
94}
95
96impl Measurement {
97 pub fn to_points(&self) -> f64 {
103 match self.unit {
104 MeasurementUnit::Points => self.value,
105 MeasurementUnit::Inches => self.value * 72.0,
106 MeasurementUnit::Centimeters => self.value * 72.0 / 2.54,
107 MeasurementUnit::Millimeters => self.value * 72.0 / 25.4,
108 MeasurementUnit::Em => self.value * 12.0, MeasurementUnit::Percent => self.value / 100.0 * 3.0,
112 }
113 }
114
115 pub fn parse(s: &str) -> Option<Self> {
117 let s = s.trim();
118 if s.is_empty() {
119 return None;
120 }
121 let num_end = s
123 .find(|c: char| !c.is_ascii_digit() && c != '.' && c != '-')
124 .unwrap_or(s.len());
125 let value: f64 = s[..num_end].parse().ok()?;
126 let unit_str = s[num_end..].trim();
127 let unit = match unit_str {
128 "" | "in" => MeasurementUnit::Inches,
129 "pt" => MeasurementUnit::Points,
130 "cm" => MeasurementUnit::Centimeters,
131 "mm" => MeasurementUnit::Millimeters,
132 "em" => MeasurementUnit::Em,
133 "%" => MeasurementUnit::Percent,
134 _ => return None,
135 };
136 Some(Measurement { value, unit })
137 }
138}
139
140impl Default for Measurement {
141 fn default() -> Self {
142 Self {
143 value: 0.0,
144 unit: MeasurementUnit::Points,
145 }
146 }
147}
148
149#[derive(Debug, Clone, Copy, PartialEq, Eq)]
154pub enum MeasurementUnit {
155 Inches,
156 Centimeters,
157 Millimeters,
158 Points,
159 Em,
160 Percent,
162}
163
164#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
166pub enum TextAlign {
167 #[default]
169 Left,
170 Center,
172 Right,
174 Justify,
176}
177
178#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
185pub enum LayoutStrategy {
186 #[default]
188 Positioned,
189 TopToBottom,
191 LeftToRightTB,
193 RightToLeftTB,
195 Table,
197 Row,
199}
200
201#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
203pub enum VerticalAlign {
204 #[default]
205 Top,
206 Middle,
207 Bottom,
208}
209
210#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
212pub enum CaptionPlacement {
213 #[default]
214 Left,
215 Top,
216 Right,
217 Bottom,
218 Inline,
219}
220
221#[derive(Debug, Clone, PartialEq, Default)]
239pub struct BoxModel {
240 pub width: Option<f64>,
242 pub height: Option<f64>,
244 pub x: f64,
246 pub y: f64,
248 pub margins: Insets,
250 pub border_width: f64,
252 pub min_width: f64,
254 pub max_width: f64,
256 pub min_height: f64,
258 pub max_height: f64,
260 pub caption: Option<Caption>,
262}
263
264#[derive(Debug, Clone, PartialEq)]
266pub struct Caption {
267 pub placement: CaptionPlacement,
268 pub reserve: Option<f64>,
270 pub text: String,
271}
272
273impl BoxModel {
274 pub fn content_width(&self) -> f64 {
276 let total = self.width.unwrap_or(self.max_width);
277 let mut available = total - self.margins.horizontal() - self.border_width * 2.0;
278 if let Some(ref cap) = self.caption {
279 if matches!(
280 cap.placement,
281 CaptionPlacement::Left | CaptionPlacement::Right
282 ) {
283 available -= cap.reserve.unwrap_or(0.0);
284 }
285 }
286 available.max(0.0)
287 }
288
289 pub fn content_height(&self) -> f64 {
291 let total = self.height.unwrap_or(self.max_height);
292 let mut available = total - self.margins.vertical() - self.border_width * 2.0;
293 if let Some(ref cap) = self.caption {
294 if matches!(
295 cap.placement,
296 CaptionPlacement::Top | CaptionPlacement::Bottom
297 ) {
298 available -= cap.reserve.unwrap_or(0.0);
299 }
300 }
301 available.max(0.0)
302 }
303
304 pub fn outer_size(&self, content: Size) -> Size {
306 let mut w = content.width + self.margins.horizontal() + self.border_width * 2.0;
307 let mut h = content.height + self.margins.vertical() + self.border_width * 2.0;
308 if let Some(ref cap) = self.caption {
309 match cap.placement {
310 CaptionPlacement::Left | CaptionPlacement::Right => {
311 w += cap.reserve.unwrap_or(0.0);
312 }
313 CaptionPlacement::Top | CaptionPlacement::Bottom => {
314 h += cap.reserve.unwrap_or(0.0);
315 }
316 CaptionPlacement::Inline => {}
317 }
318 }
319 if let Some(fixed_w) = self.width {
321 w = fixed_w;
322 } else {
323 w = w.clamp(self.min_width, self.max_width);
324 }
325 if let Some(fixed_h) = self.height {
326 h = fixed_h;
327 } else {
328 h = h.clamp(self.min_height, self.max_height);
329 }
330 Size {
331 width: w,
332 height: h,
333 }
334 }
335}
336
337#[cfg(test)]
338mod tests {
339 use super::*;
340
341 #[test]
342 fn measurement_parse() {
343 let m = Measurement::parse("10mm").unwrap();
344 assert_eq!(m.unit, MeasurementUnit::Millimeters);
345 assert!((m.to_points() - 28.3464).abs() < 0.01);
346
347 let m = Measurement::parse("72pt").unwrap();
348 assert_eq!(m.to_points(), 72.0);
349
350 let m = Measurement::parse("1in").unwrap();
351 assert_eq!(m.to_points(), 72.0);
352
353 let m = Measurement::parse("2.54cm").unwrap();
354 assert!((m.to_points() - 72.0).abs() < 0.01);
355 }
356
357 #[test]
358 fn box_model_content_area() {
359 let bm = BoxModel {
360 width: Some(200.0),
361 height: Some(100.0),
362 margins: Insets {
363 top: 5.0,
364 right: 10.0,
365 bottom: 5.0,
366 left: 10.0,
367 },
368 border_width: 1.0,
369 max_width: f64::MAX,
370 max_height: f64::MAX,
371 ..Default::default()
372 };
373 assert_eq!(bm.content_width(), 178.0);
375 assert_eq!(bm.content_height(), 88.0);
377 }
378
379 #[test]
380 fn box_model_with_caption() {
381 let bm = BoxModel {
382 width: Some(200.0),
383 height: Some(100.0),
384 caption: Some(Caption {
385 placement: CaptionPlacement::Left,
386 reserve: Some(50.0),
387 text: "Label".to_string(),
388 }),
389 max_width: f64::MAX,
390 max_height: f64::MAX,
391 ..Default::default()
392 };
393 assert_eq!(bm.content_width(), 150.0);
395 }
396
397 #[test]
398 fn outer_size_applies_constraints() {
399 let bm = BoxModel {
400 min_width: 100.0,
401 min_height: 50.0,
402 max_width: 500.0,
403 max_height: 300.0,
404 ..Default::default()
405 };
406 let s = bm.outer_size(Size {
407 width: 10.0,
408 height: 10.0,
409 });
410 assert_eq!(s.width, 100.0); assert_eq!(s.height, 50.0); }
413
414 #[test]
415 fn outer_size_fixed() {
416 let bm = BoxModel {
417 width: Some(200.0),
418 height: Some(100.0),
419 max_width: f64::MAX,
420 max_height: f64::MAX,
421 ..Default::default()
422 };
423 let s = bm.outer_size(Size {
424 width: 50.0,
425 height: 50.0,
426 });
427 assert_eq!(s.width, 200.0); assert_eq!(s.height, 100.0); }
430
431 #[test]
432 fn insets_helpers() {
433 let i = Insets {
434 top: 1.0,
435 right: 2.0,
436 bottom: 3.0,
437 left: 4.0,
438 };
439 assert_eq!(i.horizontal(), 6.0);
440 assert_eq!(i.vertical(), 4.0);
441 }
442}