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 pub font_family: Option<String>,
310 pub font_size: Option<f64>,
312}
313
314impl BoxModel {
315 pub fn content_width(&self) -> f64 {
317 let total = self.width.unwrap_or(self.max_width);
318 let mut available = total - self.margins.horizontal() - self.border_width * 2.0;
319 if let Some(ref cap) = self.caption {
320 if matches!(
321 cap.placement,
322 CaptionPlacement::Left | CaptionPlacement::Right
323 ) {
324 available -= cap.reserve.unwrap_or(0.0);
325 }
326 }
327 available.max(0.0)
328 }
329
330 pub fn content_height(&self) -> f64 {
332 let total = self.height.unwrap_or(self.max_height);
333 let mut available = total - self.margins.vertical() - self.border_width * 2.0;
334 if let Some(ref cap) = self.caption {
335 if matches!(
336 cap.placement,
337 CaptionPlacement::Top | CaptionPlacement::Bottom
338 ) {
339 available -= cap.reserve.unwrap_or(0.0);
340 }
341 }
342 available.max(0.0)
343 }
344
345 pub fn outer_size(&self, content: Size) -> Size {
347 let mut w = content.width + self.margins.horizontal() + self.border_width * 2.0;
348 let mut h = content.height + self.margins.vertical() + self.border_width * 2.0;
349 if let Some(ref cap) = self.caption {
350 match cap.placement {
351 CaptionPlacement::Left | CaptionPlacement::Right => {
352 w += cap.reserve.unwrap_or(0.0);
353 }
354 CaptionPlacement::Top | CaptionPlacement::Bottom => {
355 h += cap.reserve.unwrap_or(0.0);
356 }
357 CaptionPlacement::Inline => {}
358 }
359 }
360 if let Some(fixed_w) = self.width {
362 w = fixed_w;
363 } else {
364 w = w.clamp(self.min_width, self.max_width);
365 }
366 if let Some(fixed_h) = self.height {
367 h = fixed_h;
368 } else {
369 h = h.clamp(self.min_height, self.max_height);
370 }
371 Size {
372 width: w,
373 height: h,
374 }
375 }
376}
377
378#[cfg(test)]
379mod tests {
380 use super::*;
381
382 #[test]
383 fn measurement_parse() {
384 let m = Measurement::parse("10mm").unwrap();
385 assert_eq!(m.unit, MeasurementUnit::Millimeters);
386 assert!((m.to_points() - 28.3464).abs() < 0.01);
387
388 let m = Measurement::parse("72pt").unwrap();
389 assert_eq!(m.to_points(), 72.0);
390
391 let m = Measurement::parse("1in").unwrap();
392 assert_eq!(m.to_points(), 72.0);
393
394 let m = Measurement::parse("2.54cm").unwrap();
395 assert!((m.to_points() - 72.0).abs() < 0.01);
396 }
397
398 #[test]
399 fn box_model_content_area() {
400 let bm = BoxModel {
401 width: Some(200.0),
402 height: Some(100.0),
403 margins: Insets {
404 top: 5.0,
405 right: 10.0,
406 bottom: 5.0,
407 left: 10.0,
408 },
409 border_width: 1.0,
410 max_width: f64::MAX,
411 max_height: f64::MAX,
412 ..Default::default()
413 };
414 assert_eq!(bm.content_width(), 178.0);
416 assert_eq!(bm.content_height(), 88.0);
418 }
419
420 #[test]
421 fn box_model_with_caption() {
422 let bm = BoxModel {
423 width: Some(200.0),
424 height: Some(100.0),
425 caption: Some(Caption {
426 placement: CaptionPlacement::Left,
427 reserve: Some(50.0),
428 text: "Label".to_string(),
429 font_family: None,
430 font_size: None,
431 }),
432 max_width: f64::MAX,
433 max_height: f64::MAX,
434 ..Default::default()
435 };
436 assert_eq!(bm.content_width(), 150.0);
438 }
439
440 #[test]
441 fn outer_size_applies_constraints() {
442 let bm = BoxModel {
443 min_width: 100.0,
444 min_height: 50.0,
445 max_width: 500.0,
446 max_height: 300.0,
447 ..Default::default()
448 };
449 let s = bm.outer_size(Size {
450 width: 10.0,
451 height: 10.0,
452 });
453 assert_eq!(s.width, 100.0); assert_eq!(s.height, 50.0); }
456
457 #[test]
458 fn outer_size_fixed() {
459 let bm = BoxModel {
460 width: Some(200.0),
461 height: Some(100.0),
462 max_width: f64::MAX,
463 max_height: f64::MAX,
464 ..Default::default()
465 };
466 let s = bm.outer_size(Size {
467 width: 50.0,
468 height: 50.0,
469 });
470 assert_eq!(s.width, 200.0); assert_eq!(s.height, 100.0); }
473
474 #[test]
475 fn insets_helpers() {
476 let i = Insets {
477 top: 1.0,
478 right: 2.0,
479 bottom: 3.0,
480 left: 4.0,
481 };
482 assert_eq!(i.horizontal(), 6.0);
483 assert_eq!(i.vertical(), 4.0);
484 }
485}