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}