Skip to main content

appcore_filemaker/
text_padding.rs

1// =============================================================================
2//        #######
3//     ###       ###     F: text_padding.rs
4//    ##   ## ##   ##    P: AppCore-Runtime
5//         ## ##
6//                       C: 2026/09/30 by dnettoRaw
7//    ##   ## ##   ##    U: 2026/09/30 working-tree by dnettoRaw
8//      ###########      S: 0.1.0-beta.5
9// =============================================================================
10
11//! Resolves standalone text-block padding and exporter content geometry.
12
13use serde::{Deserialize, Serialize};
14
15use crate::{ErrorCode, FileMakerError, Insets, Length, Rect, Result, Size, TextLayout, Unit};
16
17/// Declarative per-side padding around a text element's measured content.
18#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
19#[serde(default, deny_unknown_fields)]
20pub struct TextBlockPadding {
21    /// Top inset, relative percentages use the element height.
22    pub top: Length,
23    /// Right inset, relative percentages use the element width.
24    pub right: Length,
25    /// Bottom inset, relative percentages use the element height.
26    pub bottom: Length,
27    /// Left inset, relative percentages use the element width.
28    pub left: Length,
29}
30
31impl Default for TextBlockPadding {
32    fn default() -> Self {
33        let zero = Length::Absolute(Unit::ZERO);
34        Self {
35            top: zero,
36            right: zero,
37            bottom: zero,
38            left: zero,
39        }
40    }
41}
42
43impl TextBlockPadding {
44    pub(crate) fn resolve(self, width: Unit, height: Unit, logical_unit: Unit) -> Result<Insets> {
45        let resolve = |value: Length, percent_base: Unit| {
46            value
47                .resolve(percent_base, logical_unit)?
48                .ok_or_else(|| padding_error("text block padding cannot be auto"))
49        };
50        Ok(Insets {
51            top: resolve(self.top, height)?,
52            right: resolve(self.right, width)?,
53            bottom: resolve(self.bottom, height)?,
54            left: resolve(self.left, width)?,
55        })
56    }
57}
58
59impl TextLayout {
60    /// Returns the measured outer size including block padding.
61    pub fn outer_measured(&self) -> Result<Size> {
62        Size::new(
63            self.measured
64                .width
65                .checked_add(self.padding.left.checked_add(self.padding.right)?)?,
66            self.measured
67                .height
68                .checked_add(self.padding.top.checked_add(self.padding.bottom)?)?,
69        )
70    }
71
72    /// Returns the inner text rectangle after applying block padding.
73    pub fn content_bounds(&self, bounds: Rect) -> Result<Rect> {
74        let x = bounds.origin.x.checked_add(self.padding.left)?;
75        let y = bounds.origin.y.checked_add(self.padding.top)?;
76        let width = bounds
77            .size
78            .width
79            .checked_sub(self.padding.left.checked_add(self.padding.right)?)?;
80        let height = bounds
81            .size
82            .height
83            .checked_sub(self.padding.top.checked_add(self.padding.bottom)?)?;
84        if width <= Unit::ZERO || height <= Unit::ZERO {
85            return Err(padding_error("text block padding leaves no content area"));
86        }
87        Rect::new(x, y, width, height)
88    }
89}
90
91fn padding_error(message: &'static str) -> FileMakerError {
92    FileMakerError::new(ErrorCode::LayoutInvalid, message)
93}