1pub const DEFAULT_TEXT_PADDING: f64 = 0.0;
8
9#[derive(Debug, Clone, Copy, PartialEq, Default)]
11pub struct Point {
12 pub x: f64,
14 pub y: f64,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Default)]
20pub struct Size {
21 pub width: f64,
23 pub height: f64,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Default)]
29pub struct Rect {
30 pub x: f64,
32 pub y: f64,
34 pub width: f64,
36 pub height: f64,
38}
39
40impl Rect {
41 pub fn new(x: f64, y: f64, width: f64, height: f64) -> Self {
43 Self {
44 x,
45 y,
46 width,
47 height,
48 }
49 }
50
51 pub fn right(&self) -> f64 {
53 self.x + self.width
54 }
55
56 pub fn bottom(&self) -> f64 {
58 self.y + self.height
59 }
60
61 pub fn contains(&self, px: f64, py: f64) -> bool {
63 px >= self.x && px <= self.right() && py >= self.y && py <= self.bottom()
64 }
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Default)]
69pub struct Insets {
70 pub top: f64,
72 pub right: f64,
74 pub bottom: f64,
76 pub left: f64,
78}
79
80impl Insets {
81 pub fn uniform(value: f64) -> Self {
83 Self {
84 top: value,
85 right: value,
86 bottom: value,
87 left: value,
88 }
89 }
90
91 pub fn horizontal(&self) -> f64 {
93 self.left + self.right
94 }
95
96 pub fn vertical(&self) -> f64 {
98 self.top + self.bottom
99 }
100}
101
102#[derive(Debug, Clone, Copy, PartialEq)]
109pub struct Measurement {
110 pub value: f64,
112 pub unit: MeasurementUnit,
114}
115
116impl Measurement {
117 pub fn to_points(&self) -> f64 {
123 match self.unit {
124 MeasurementUnit::Points => self.value,
125 MeasurementUnit::Inches => self.value * 72.0,
126 MeasurementUnit::Centimeters => self.value * 72.0 / 2.54,
127 MeasurementUnit::Millimeters => self.value * 72.0 / 25.4,
128 MeasurementUnit::Em => self.value * 12.0, MeasurementUnit::Percent => self.value / 100.0 * 3.0,
132 }
133 }
134
135 pub fn parse(s: &str) -> Option<Self> {
137 let s = s.trim();
138 if s.is_empty() {
139 return None;
140 }
141 let num_end = s
143 .find(|c: char| !c.is_ascii_digit() && c != '.' && c != '-')
144 .unwrap_or(s.len());
145 let value: f64 = s[..num_end].parse().ok()?;
146 let unit_str = s[num_end..].trim();
147 let unit = match unit_str {
148 "" | "in" => MeasurementUnit::Inches,
149 "pt" => MeasurementUnit::Points,
150 "cm" => MeasurementUnit::Centimeters,
151 "mm" => MeasurementUnit::Millimeters,
152 "em" => MeasurementUnit::Em,
153 "%" => MeasurementUnit::Percent,
154 _ => return None,
155 };
156 Some(Measurement { value, unit })
157 }
158}
159
160impl Default for Measurement {
161 fn default() -> Self {
162 Self {
163 value: 0.0,
164 unit: MeasurementUnit::Points,
165 }
166 }
167}
168
169#[derive(Debug, Clone, Copy, PartialEq, Eq)]
174pub enum MeasurementUnit {
175 Inches,
177 Centimeters,
179 Millimeters,
181 Points,
183 Em,
185 Percent,
187}
188
189#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
191pub enum TextAlign {
192 #[default]
194 Left,
195 Center,
197 Right,
199 Justify,
201}
202
203#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
210pub enum LayoutStrategy {
211 #[default]
213 Positioned,
214 TopToBottom,
216 LeftToRightTB,
218 RightToLeftTB,
220 Table,
222 Row,
224}
225
226#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
228pub enum VerticalAlign {
229 #[default]
230 Top,
232 Middle,
234 Bottom,
236}
237
238#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
240pub enum CaptionPlacement {
241 #[default]
242 Left,
244 Top,
246 Right,
248 Bottom,
250 Inline,
252}
253
254#[derive(Debug, Clone, PartialEq, Default)]
272pub struct BoxModel {
273 pub width: Option<f64>,
275 pub height: Option<f64>,
277 pub x: f64,
279 pub y: f64,
281 pub margins: Insets,
283 pub border_width: f64,
285 pub min_width: f64,
287 pub max_width: f64,
289 pub min_height: f64,
291 pub max_height: f64,
293 pub caption: Option<Caption>,
295}
296
297#[derive(Debug, Clone, PartialEq)]
299pub struct Caption {
300 pub placement: CaptionPlacement,
302 pub reserve: Option<f64>,
304 pub text: String,
306}
307
308impl BoxModel {
309 pub fn content_width(&self) -> f64 {
311 let total = self.width.unwrap_or(self.max_width);
312 let mut available = total - self.margins.horizontal() - self.border_width * 2.0;
313 if let Some(ref cap) = self.caption {
314 if matches!(
315 cap.placement,
316 CaptionPlacement::Left | CaptionPlacement::Right
317 ) {
318 available -= cap.reserve.unwrap_or(0.0);
319 }
320 }
321 available.max(0.0)
322 }
323
324 pub fn content_height(&self) -> f64 {
326 let total = self.height.unwrap_or(self.max_height);
327 let mut available = total - self.margins.vertical() - self.border_width * 2.0;
328 if let Some(ref cap) = self.caption {
329 if matches!(
330 cap.placement,
331 CaptionPlacement::Top | CaptionPlacement::Bottom
332 ) {
333 available -= cap.reserve.unwrap_or(0.0);
334 }
335 }
336 available.max(0.0)
337 }
338
339 pub fn outer_size(&self, content: Size) -> Size {
341 let mut w = content.width + self.margins.horizontal() + self.border_width * 2.0;
342 let mut h = content.height + self.margins.vertical() + self.border_width * 2.0;
343 if let Some(ref cap) = self.caption {
344 match cap.placement {
345 CaptionPlacement::Left | CaptionPlacement::Right => {
346 w += cap.reserve.unwrap_or(0.0);
347 }
348 CaptionPlacement::Top | CaptionPlacement::Bottom => {
349 h += cap.reserve.unwrap_or(0.0);
350 }
351 CaptionPlacement::Inline => {}
352 }
353 }
354 if let Some(fixed_w) = self.width {
356 w = fixed_w;
357 } else {
358 w = w.clamp(self.min_width, self.max_width);
359 }
360 if let Some(fixed_h) = self.height {
361 h = fixed_h;
362 } else {
363 h = h.clamp(self.min_height, self.max_height);
364 }
365 Size {
366 width: w,
367 height: h,
368 }
369 }
370}
371
372#[cfg(test)]
373mod tests {
374 use super::*;
375
376 #[test]
377 fn measurement_parse() {
378 let m = Measurement::parse("10mm").unwrap();
379 assert_eq!(m.unit, MeasurementUnit::Millimeters);
380 assert!((m.to_points() - 28.3464).abs() < 0.01);
381
382 let m = Measurement::parse("72pt").unwrap();
383 assert_eq!(m.to_points(), 72.0);
384
385 let m = Measurement::parse("1in").unwrap();
386 assert_eq!(m.to_points(), 72.0);
387
388 let m = Measurement::parse("2.54cm").unwrap();
389 assert!((m.to_points() - 72.0).abs() < 0.01);
390 }
391
392 #[test]
393 fn box_model_content_area() {
394 let bm = BoxModel {
395 width: Some(200.0),
396 height: Some(100.0),
397 margins: Insets {
398 top: 5.0,
399 right: 10.0,
400 bottom: 5.0,
401 left: 10.0,
402 },
403 border_width: 1.0,
404 max_width: f64::MAX,
405 max_height: f64::MAX,
406 ..Default::default()
407 };
408 assert_eq!(bm.content_width(), 178.0);
410 assert_eq!(bm.content_height(), 88.0);
412 }
413
414 #[test]
415 fn box_model_with_caption() {
416 let bm = BoxModel {
417 width: Some(200.0),
418 height: Some(100.0),
419 caption: Some(Caption {
420 placement: CaptionPlacement::Left,
421 reserve: Some(50.0),
422 text: "Label".to_string(),
423 }),
424 max_width: f64::MAX,
425 max_height: f64::MAX,
426 ..Default::default()
427 };
428 assert_eq!(bm.content_width(), 150.0);
430 }
431
432 #[test]
433 fn outer_size_applies_constraints() {
434 let bm = BoxModel {
435 min_width: 100.0,
436 min_height: 50.0,
437 max_width: 500.0,
438 max_height: 300.0,
439 ..Default::default()
440 };
441 let s = bm.outer_size(Size {
442 width: 10.0,
443 height: 10.0,
444 });
445 assert_eq!(s.width, 100.0); assert_eq!(s.height, 50.0); }
448
449 #[test]
450 fn outer_size_fixed() {
451 let bm = BoxModel {
452 width: Some(200.0),
453 height: Some(100.0),
454 max_width: f64::MAX,
455 max_height: f64::MAX,
456 ..Default::default()
457 };
458 let s = bm.outer_size(Size {
459 width: 50.0,
460 height: 50.0,
461 });
462 assert_eq!(s.width, 200.0); assert_eq!(s.height, 100.0); }
465
466 #[test]
467 fn insets_helpers() {
468 let i = Insets {
469 top: 1.0,
470 right: 2.0,
471 bottom: 3.0,
472 left: 4.0,
473 };
474 assert_eq!(i.horizontal(), 6.0);
475 assert_eq!(i.vertical(), 4.0);
476 }
477}