Skip to main content

khora_core/ui/
geometry.rs

1// Copyright 2025 eraflo
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8
9//! Backend-agnostic UI geometry types — `Margin`, `Stroke`,
10//! `CornerRadius`, `Align`, `Align2`.
11//!
12//! These mirror the same concepts every immediate-mode UI library
13//! exposes (egui, iced, druid, …). Apps and widget code reference
14//! only these neutral types; the concrete backend in `khora-infra`
15//! converts them to its native equivalents at the trait-impl boundary.
16
17use crate::math::LinearRgba;
18
19/// Padding (or outer spacing) on the four sides of a region.
20///
21/// Values are in logical pixels. Negative values are allowed (useful
22/// for overlap effects); backends that don't support them clamp.
23#[derive(Debug, Clone, Copy, PartialEq, Default)]
24pub struct Margin {
25    /// Top edge.
26    pub top: f32,
27    /// Bottom edge.
28    pub bottom: f32,
29    /// Left edge.
30    pub left: f32,
31    /// Right edge.
32    pub right: f32,
33}
34
35impl Margin {
36    /// Zero margin on every side.
37    pub const ZERO: Self = Self {
38        top: 0.0,
39        bottom: 0.0,
40        left: 0.0,
41        right: 0.0,
42    };
43
44    /// Same value on every side.
45    #[inline]
46    pub const fn same(value: f32) -> Self {
47        Self {
48            top: value,
49            bottom: value,
50            left: value,
51            right: value,
52        }
53    }
54
55    /// Symmetric padding (different `vertical` and `horizontal` values).
56    #[inline]
57    pub const fn symmetric(horizontal: f32, vertical: f32) -> Self {
58        Self {
59            top: vertical,
60            bottom: vertical,
61            left: horizontal,
62            right: horizontal,
63        }
64    }
65}
66
67/// A border or line stroke — colour + thickness.
68///
69/// Thickness is in logical pixels. A thickness of `0.0` paints
70/// nothing; backends should fast-path that case.
71#[derive(Debug, Clone, Copy, PartialEq)]
72pub struct Stroke {
73    /// Stroke colour.
74    pub color: LinearRgba,
75    /// Stroke thickness in logical pixels.
76    pub width: f32,
77}
78
79impl Stroke {
80    /// A `None`-equivalent — zero-width transparent stroke. Backends
81    /// recognise it and skip painting.
82    pub const NONE: Self = Self {
83        color: LinearRgba::TRANSPARENT,
84        width: 0.0,
85    };
86
87    /// Convenience constructor.
88    #[inline]
89    pub const fn new(color: LinearRgba, width: f32) -> Self {
90        Self { color, width }
91    }
92}
93
94impl Default for Stroke {
95    fn default() -> Self {
96        Self::NONE
97    }
98}
99
100/// Per-corner radius of a rounded rectangle, in logical pixels.
101///
102/// Backends that only support a single radius use the maximum of the
103/// four corners (or `nw` if they're all expected equal).
104#[derive(Debug, Clone, Copy, PartialEq, Default)]
105pub struct CornerRadius {
106    /// North-west (top-left).
107    pub nw: f32,
108    /// North-east (top-right).
109    pub ne: f32,
110    /// South-west (bottom-left).
111    pub sw: f32,
112    /// South-east (bottom-right).
113    pub se: f32,
114}
115
116impl CornerRadius {
117    /// All corners share `value`.
118    #[inline]
119    pub const fn same(value: f32) -> Self {
120        Self {
121            nw: value,
122            ne: value,
123            sw: value,
124            se: value,
125        }
126    }
127
128    /// Square (no rounding).
129    pub const ZERO: Self = Self::same(0.0);
130
131    /// Returns the largest of the four corner radii. Useful when a
132    /// backend can only express a single uniform corner radius.
133    #[inline]
134    pub fn max(&self) -> f32 {
135        self.nw.max(self.ne).max(self.sw).max(self.se)
136    }
137}
138
139/// One-axis alignment — `Min` (top/left), `Center`, or `Max` (bottom/right).
140#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
141pub enum Align {
142    /// Anchor at the start of the axis (top for vertical, left for
143    /// horizontal).
144    #[default]
145    Min,
146    /// Anchor in the middle of the axis.
147    Center,
148    /// Anchor at the end (bottom for vertical, right for horizontal).
149    Max,
150}
151
152/// Two-axis alignment — combination of horizontal + vertical [`Align`].
153///
154/// Common variants are exposed as constants
155/// (`LEFT_TOP`, `CENTER_CENTER`, `RIGHT_BOTTOM`, …) so call sites read
156/// like CSS `text-align` / `vertical-align`.
157#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
158pub struct Align2 {
159    /// Horizontal alignment.
160    pub x: Align,
161    /// Vertical alignment.
162    pub y: Align,
163}
164
165impl Align2 {
166    /// Top-left.
167    pub const LEFT_TOP: Self = Self {
168        x: Align::Min,
169        y: Align::Min,
170    };
171    /// Top, horizontally centered.
172    pub const CENTER_TOP: Self = Self {
173        x: Align::Center,
174        y: Align::Min,
175    };
176    /// Top-right.
177    pub const RIGHT_TOP: Self = Self {
178        x: Align::Max,
179        y: Align::Min,
180    };
181    /// Vertically centered, left-aligned.
182    pub const LEFT_CENTER: Self = Self {
183        x: Align::Min,
184        y: Align::Center,
185    };
186    /// Centered on both axes.
187    pub const CENTER_CENTER: Self = Self {
188        x: Align::Center,
189        y: Align::Center,
190    };
191    /// Vertically centered, right-aligned.
192    pub const RIGHT_CENTER: Self = Self {
193        x: Align::Max,
194        y: Align::Center,
195    };
196    /// Bottom-left.
197    pub const LEFT_BOTTOM: Self = Self {
198        x: Align::Min,
199        y: Align::Max,
200    };
201    /// Bottom, horizontally centered.
202    pub const CENTER_BOTTOM: Self = Self {
203        x: Align::Center,
204        y: Align::Max,
205    };
206    /// Bottom-right.
207    pub const RIGHT_BOTTOM: Self = Self {
208        x: Align::Max,
209        y: Align::Max,
210    };
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216
217    #[test]
218    fn margin_helpers() {
219        let m = Margin::same(4.0);
220        assert_eq!(m.top, 4.0);
221        assert_eq!(m.left, 4.0);
222
223        let s = Margin::symmetric(8.0, 6.0);
224        assert_eq!(s.top, 6.0);
225        assert_eq!(s.bottom, 6.0);
226        assert_eq!(s.left, 8.0);
227        assert_eq!(s.right, 8.0);
228    }
229
230    #[test]
231    fn stroke_none_is_invisible() {
232        assert_eq!(Stroke::NONE.width, 0.0);
233        assert_eq!(Stroke::NONE.color.a, 0.0);
234    }
235
236    #[test]
237    fn corner_radius_max() {
238        let cr = CornerRadius {
239            nw: 4.0,
240            ne: 8.0,
241            sw: 2.0,
242            se: 6.0,
243        };
244        assert_eq!(cr.max(), 8.0);
245    }
246
247    #[test]
248    fn align2_constants() {
249        assert_eq!(Align2::LEFT_TOP.x, Align::Min);
250        assert_eq!(Align2::LEFT_TOP.y, Align::Min);
251        assert_eq!(Align2::RIGHT_BOTTOM.x, Align::Max);
252        assert_eq!(Align2::CENTER_CENTER.y, Align::Center);
253    }
254}