Skip to main content

oxiui_core/
style.rs

1//! Spacing and decoration styling primitives.
2//!
3//! [`Padding`] and [`Margin`] are newtype wrappers over [`Insets`] that keep the
4//! two concepts distinct at the type level (a value meant for inner padding can
5//! never be silently used as an outer margin). [`Border`] couples a set of
6//! [`Insets`] (the per-side border widths) with a [`Color`] and a
7//! [`BorderStyle`].
8//!
9//! These are additive styling types consumed by [`WidgetExt`](crate::WidgetExt)
10//! combinators and by adapters that honour design tokens.
11
12use crate::geometry::{Insets, Rect};
13use crate::Color;
14
15/// Inner spacing between a widget's border box and its content.
16///
17/// A transparent newtype over [`Insets`]; the wrapper exists purely to prevent
18/// confusing padding with [`Margin`] at call sites.
19#[derive(Clone, Copy, Debug, Default, PartialEq)]
20pub struct Padding(pub Insets);
21
22impl Padding {
23    /// Zero padding on all sides.
24    pub const ZERO: Padding = Padding(Insets::ZERO);
25
26    /// Per-side padding.
27    pub const fn new(top: f32, right: f32, bottom: f32, left: f32) -> Self {
28        Self(Insets::new(top, right, bottom, left))
29    }
30
31    /// The same padding on all four sides.
32    pub const fn all(v: f32) -> Self {
33        Self(Insets::all(v))
34    }
35
36    /// Symmetric padding: `vertical` on top/bottom, `horizontal` on left/right.
37    pub const fn symmetric(vertical: f32, horizontal: f32) -> Self {
38        Self(Insets::symmetric(vertical, horizontal))
39    }
40
41    /// Borrow the underlying [`Insets`].
42    pub const fn insets(self) -> Insets {
43        self.0
44    }
45
46    /// Apply this padding inward to `rect`, yielding the content rectangle.
47    pub fn shrink(self, rect: Rect) -> Rect {
48        rect.deflate(self.0)
49    }
50}
51
52impl From<Insets> for Padding {
53    fn from(insets: Insets) -> Self {
54        Self(insets)
55    }
56}
57
58impl From<Padding> for Insets {
59    fn from(padding: Padding) -> Self {
60        padding.0
61    }
62}
63
64/// Outer spacing between a widget's margin box and its siblings.
65///
66/// A transparent newtype over [`Insets`]; distinct from [`Padding`] by type.
67#[derive(Clone, Copy, Debug, Default, PartialEq)]
68pub struct Margin(pub Insets);
69
70impl Margin {
71    /// Zero margin on all sides.
72    pub const ZERO: Margin = Margin(Insets::ZERO);
73
74    /// Per-side margin.
75    pub const fn new(top: f32, right: f32, bottom: f32, left: f32) -> Self {
76        Self(Insets::new(top, right, bottom, left))
77    }
78
79    /// The same margin on all four sides.
80    pub const fn all(v: f32) -> Self {
81        Self(Insets::all(v))
82    }
83
84    /// Symmetric margin: `vertical` on top/bottom, `horizontal` on left/right.
85    pub const fn symmetric(vertical: f32, horizontal: f32) -> Self {
86        Self(Insets::symmetric(vertical, horizontal))
87    }
88
89    /// Borrow the underlying [`Insets`].
90    pub const fn insets(self) -> Insets {
91        self.0
92    }
93
94    /// Apply this margin outward to `rect`, yielding the margin box.
95    pub fn grow(self, rect: Rect) -> Rect {
96        rect.inflate(self.0)
97    }
98}
99
100impl From<Insets> for Margin {
101    fn from(insets: Insets) -> Self {
102        Self(insets)
103    }
104}
105
106impl From<Margin> for Insets {
107    fn from(margin: Margin) -> Self {
108        margin.0
109    }
110}
111
112/// How a [`Border`] is rendered along its edges.
113#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
114pub enum BorderStyle {
115    /// A continuous solid line.
116    #[default]
117    Solid,
118    /// A series of dashes.
119    Dashed,
120    /// A series of dots.
121    Dotted,
122    /// Two parallel solid lines.
123    Double,
124    /// No visible border (still occupies its width for layout).
125    None,
126}
127
128/// A widget border: per-side widths, a colour, and a line style.
129#[derive(Clone, Copy, Debug, PartialEq)]
130pub struct Border {
131    /// Per-side border widths (logical pixels).
132    pub insets: Insets,
133    /// Border colour.
134    pub color: Color,
135    /// Line style.
136    pub style: BorderStyle,
137}
138
139impl Border {
140    /// A uniform-width solid border in `color`.
141    pub fn solid(width: f32, color: Color) -> Self {
142        Self {
143            insets: Insets::all(width),
144            color,
145            style: BorderStyle::Solid,
146        }
147    }
148
149    /// A border with explicit per-side widths.
150    pub fn new(insets: Insets, color: Color, style: BorderStyle) -> Self {
151        Self {
152            insets,
153            color,
154            style,
155        }
156    }
157
158    /// Builder: replace the line style.
159    pub fn with_style(mut self, style: BorderStyle) -> Self {
160        self.style = style;
161        self
162    }
163
164    /// Builder: replace the colour.
165    pub fn with_color(mut self, color: Color) -> Self {
166        self.color = color;
167        self
168    }
169
170    /// Returns `true` if the border occupies no layout space or is invisible.
171    pub fn is_none(&self) -> bool {
172        self.style == BorderStyle::None
173            || (self.insets.top <= 0.0
174                && self.insets.right <= 0.0
175                && self.insets.bottom <= 0.0
176                && self.insets.left <= 0.0)
177    }
178
179    /// The content rectangle after subtracting the border widths from `rect`.
180    pub fn content_rect(&self, rect: Rect) -> Rect {
181        rect.deflate(self.insets)
182    }
183}
184
185impl Default for Border {
186    /// A zero-width transparent solid border (effectively no border).
187    fn default() -> Self {
188        Self {
189            insets: Insets::ZERO,
190            color: Color(0, 0, 0, 0),
191            style: BorderStyle::None,
192        }
193    }
194}
195
196/// The shape the OS cursor should take while over a widget.
197///
198/// Mirrors the common CSS/`winit` cursor set. Adapters map these onto their
199/// platform cursor enums; unknown shapes fall back to [`CursorShape::Default`].
200#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
201#[non_exhaustive]
202pub enum CursorShape {
203    /// The platform default arrow.
204    #[default]
205    Default,
206    /// A pointing hand (links / clickable affordances).
207    Pointer,
208    /// An I-beam for editable text.
209    Text,
210    /// A crosshair for precision selection.
211    Crosshair,
212    /// A "move" four-way arrow.
213    Move,
214    /// A "not allowed" indicator.
215    NotAllowed,
216    /// A spinning / busy "wait" cursor.
217    Wait,
218    /// A progress cursor (busy but still interactive).
219    Progress,
220    /// An open grab hand.
221    Grab,
222    /// A closed grabbing hand.
223    Grabbing,
224    /// Horizontal resize (east-west).
225    ResizeEw,
226    /// Vertical resize (north-south).
227    ResizeNs,
228    /// Diagonal resize (north-east / south-west).
229    ResizeNesw,
230    /// Diagonal resize (north-west / south-east).
231    ResizeNwse,
232    /// The cursor is hidden.
233    None,
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239
240    #[test]
241    fn padding_shrink_matches_deflate() {
242        let r = Rect::new(0.0, 0.0, 100.0, 100.0);
243        let p = Padding::all(10.0);
244        assert_eq!(p.shrink(r), r.deflate(Insets::all(10.0)));
245        assert_eq!(p.insets(), Insets::all(10.0));
246    }
247
248    #[test]
249    fn margin_grow_matches_inflate() {
250        let r = Rect::new(10.0, 10.0, 50.0, 50.0);
251        let m = Margin::symmetric(4.0, 8.0);
252        assert_eq!(m.grow(r), r.inflate(Insets::symmetric(4.0, 8.0)));
253    }
254
255    #[test]
256    fn padding_margin_conversions() {
257        let i = Insets::new(1.0, 2.0, 3.0, 4.0);
258        let p: Padding = i.into();
259        let back: Insets = p.into();
260        assert_eq!(back, i);
261        let m: Margin = i.into();
262        assert_eq!(Insets::from(m), i);
263    }
264
265    #[test]
266    fn border_solid_and_content_rect() {
267        let b = Border::solid(2.0, Color(255, 0, 0, 255));
268        assert_eq!(b.style, BorderStyle::Solid);
269        assert_eq!(b.insets, Insets::all(2.0));
270        let content = b.content_rect(Rect::new(0.0, 0.0, 20.0, 20.0));
271        assert_eq!(content, Rect::new(2.0, 2.0, 16.0, 16.0));
272        assert!(!b.is_none());
273    }
274
275    #[test]
276    fn border_default_is_none() {
277        let b = Border::default();
278        assert!(b.is_none());
279        assert_eq!(b.style, BorderStyle::None);
280        // An explicit None style is also "none" even with width.
281        let styled = Border::solid(3.0, Color(0, 0, 0, 255)).with_style(BorderStyle::None);
282        assert!(styled.is_none());
283    }
284
285    #[test]
286    fn cursor_shape_default() {
287        assert_eq!(CursorShape::default(), CursorShape::Default);
288    }
289}