Skip to main content

agpu/
core.rs

1//! Core geometry and style types for agpu.
2//!
3//! Provides [`Color`], [`Rect`], [`Position`], [`Size`], [`Margin`],
4//! [`TextStyle`], and [`FontWeight`] — the spatial and visual primitives
5//! used throughout the rendering pipeline.
6
7use serde::{Deserialize, Serialize};
8
9// ── Color ───────────────────────────────────────────────────────────
10
11/// RGBA color with f32 components in [0.0, 1.0].
12#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
13pub struct Color {
14    pub r: f32,
15    pub g: f32,
16    pub b: f32,
17    pub a: f32,
18}
19
20impl Default for Color {
21    fn default() -> Self {
22        Self::WHITE
23    }
24}
25
26impl Color {
27    pub const TRANSPARENT: Self = Self::rgba(0.0, 0.0, 0.0, 0.0);
28    pub const BLACK: Self = Self::rgb(0.0, 0.0, 0.0);
29    pub const WHITE: Self = Self::rgb(1.0, 1.0, 1.0);
30    pub const RED: Self = Self::rgb(1.0, 0.0, 0.0);
31    pub const GREEN: Self = Self::rgb(0.0, 1.0, 0.0);
32    pub const BLUE: Self = Self::rgb(0.0, 0.0, 1.0);
33    pub const YELLOW: Self = Self::rgb(1.0, 1.0, 0.0);
34    pub const CYAN: Self = Self::rgb(0.0, 1.0, 1.0);
35    pub const MAGENTA: Self = Self::rgb(1.0, 0.0, 1.0);
36    pub const GRAY: Self = Self::rgb(0.5, 0.5, 0.5);
37    pub const DARK_GRAY: Self = Self::rgb(0.25, 0.25, 0.25);
38    pub const LIGHT_GRAY: Self = Self::rgb(0.75, 0.75, 0.75);
39
40    #[must_use]
41    pub const fn rgb(r: f32, g: f32, b: f32) -> Self {
42        Self { r, g, b, a: 1.0 }
43    }
44
45    #[must_use]
46    pub const fn rgba(r: f32, g: f32, b: f32, a: f32) -> Self {
47        Self { r, g, b, a }
48    }
49
50    /// Create from 0-255 integer components.
51    #[must_use]
52    pub fn from_rgb8(r: u8, g: u8, b: u8) -> Self {
53        Self::rgb(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0)
54    }
55
56    /// Create from 0-255 integer components with alpha.
57    #[must_use]
58    pub fn from_rgba8(r: u8, g: u8, b: u8, a: u8) -> Self {
59        Self::rgba(
60            r as f32 / 255.0,
61            g as f32 / 255.0,
62            b as f32 / 255.0,
63            a as f32 / 255.0,
64        )
65    }
66
67    /// Linear interpolation between two colors.
68    #[must_use]
69    pub fn lerp(self, other: Self, t: f32) -> Self {
70        Self {
71            r: self.r + (other.r - self.r) * t,
72            g: self.g + (other.g - self.g) * t,
73            b: self.b + (other.b - self.b) * t,
74            a: self.a + (other.a - self.a) * t,
75        }
76    }
77
78    /// Apply alpha multiplier.
79    #[must_use]
80    pub fn with_alpha(self, alpha: f32) -> Self {
81        Self { a: alpha, ..self }
82    }
83}
84
85// ── Rect ────────────────────────────────────────────────────────────
86
87/// A 2D rectangle defined by position and size in logical pixels.
88#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
89pub struct Rect {
90    pub x: f32,
91    pub y: f32,
92    pub width: f32,
93    pub height: f32,
94}
95
96impl Default for Rect {
97    fn default() -> Self {
98        Self::ZERO
99    }
100}
101
102impl Rect {
103    pub const ZERO: Self = Self {
104        x: 0.0,
105        y: 0.0,
106        width: 0.0,
107        height: 0.0,
108    };
109
110    #[must_use]
111    pub const fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
112        Self {
113            x,
114            y,
115            width,
116            height,
117        }
118    }
119
120    #[must_use]
121    pub fn from_size(width: f32, height: f32) -> Self {
122        Self::new(0.0, 0.0, width, height)
123    }
124
125    #[must_use]
126    pub fn right(&self) -> f32 {
127        self.x + self.width
128    }
129
130    #[must_use]
131    pub fn bottom(&self) -> f32 {
132        self.y + self.height
133    }
134
135    #[must_use]
136    pub fn center(&self) -> Position {
137        Position {
138            x: self.x + self.width / 2.0,
139            y: self.y + self.height / 2.0,
140        }
141    }
142
143    #[must_use]
144    pub fn contains(&self, pos: Position) -> bool {
145        pos.x >= self.x && pos.x < self.right() && pos.y >= self.y && pos.y < self.bottom()
146    }
147
148    #[must_use]
149    pub fn intersection(&self, other: &Rect) -> Option<Rect> {
150        let x = self.x.max(other.x);
151        let y = self.y.max(other.y);
152        let right = self.right().min(other.right());
153        let bottom = self.bottom().min(other.bottom());
154        if right > x && bottom > y {
155            Some(Rect::new(x, y, right - x, bottom - y))
156        } else {
157            None
158        }
159    }
160
161    #[must_use]
162    pub fn union(&self, other: &Rect) -> Rect {
163        let x = self.x.min(other.x);
164        let y = self.y.min(other.y);
165        let right = self.right().max(other.right());
166        let bottom = self.bottom().max(other.bottom());
167        Rect::new(x, y, right - x, bottom - y)
168    }
169
170    #[must_use]
171    pub fn inner(&self, margin: &Margin) -> Rect {
172        Rect::new(
173            self.x + margin.left,
174            self.y + margin.top,
175            (self.width - margin.left - margin.right).max(0.0),
176            (self.height - margin.top - margin.bottom).max(0.0),
177        )
178    }
179
180    #[must_use]
181    pub fn is_empty(&self) -> bool {
182        self.width <= 0.0 || self.height <= 0.0
183    }
184
185    #[must_use]
186    pub fn size(&self) -> Size {
187        Size {
188            width: self.width,
189            height: self.height,
190        }
191    }
192
193    #[must_use]
194    pub fn position(&self) -> Position {
195        Position {
196            x: self.x,
197            y: self.y,
198        }
199    }
200
201    #[must_use]
202    pub fn translate(&self, dx: f32, dy: f32) -> Self {
203        Self::new(self.x + dx, self.y + dy, self.width, self.height)
204    }
205
206    #[must_use]
207    pub fn inflate(&self, dx: f32, dy: f32) -> Self {
208        Self::new(
209            self.x - dx,
210            self.y - dy,
211            self.width + 2.0 * dx,
212            self.height + 2.0 * dy,
213        )
214    }
215}
216
217// ── Position ────────────────────────────────────────────────────────
218
219/// A 2D point in logical pixel coordinates.
220#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
221pub struct Position {
222    pub x: f32,
223    pub y: f32,
224}
225
226impl Default for Position {
227    fn default() -> Self {
228        Self::ZERO
229    }
230}
231
232impl Position {
233    pub const ZERO: Self = Self { x: 0.0, y: 0.0 };
234
235    #[must_use]
236    pub const fn new(x: f32, y: f32) -> Self {
237        Self { x, y }
238    }
239
240    #[must_use]
241    pub fn distance_to(&self, other: &Position) -> f32 {
242        let dx = self.x - other.x;
243        let dy = self.y - other.y;
244        (dx * dx + dy * dy).sqrt()
245    }
246}
247
248// ── Size ────────────────────────────────────────────────────────────
249
250/// A 2D size in logical pixels.
251#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
252pub struct Size {
253    pub width: f32,
254    pub height: f32,
255}
256
257impl Default for Size {
258    fn default() -> Self {
259        Self::ZERO
260    }
261}
262
263impl Size {
264    pub const ZERO: Self = Self {
265        width: 0.0,
266        height: 0.0,
267    };
268
269    #[must_use]
270    pub const fn new(width: f32, height: f32) -> Self {
271        Self { width, height }
272    }
273}
274
275// ── Margin ──────────────────────────────────────────────────────────
276
277/// Margins around a rectangle.
278#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
279pub struct Margin {
280    pub top: f32,
281    pub right: f32,
282    pub bottom: f32,
283    pub left: f32,
284}
285
286impl Default for Margin {
287    fn default() -> Self {
288        Self::ZERO
289    }
290}
291
292impl Margin {
293    pub const ZERO: Self = Self {
294        top: 0.0,
295        right: 0.0,
296        bottom: 0.0,
297        left: 0.0,
298    };
299
300    #[must_use]
301    pub const fn new(top: f32, right: f32, bottom: f32, left: f32) -> Self {
302        Self {
303            top,
304            right,
305            bottom,
306            left,
307        }
308    }
309
310    #[must_use]
311    pub const fn uniform(val: f32) -> Self {
312        Self::new(val, val, val, val)
313    }
314}
315
316// ── TextStyle ───────────────────────────────────────────────────────
317
318/// Font weight categories.
319#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
320pub enum FontWeight {
321    Thin,
322    Light,
323    #[default]
324    Regular,
325    Medium,
326    SemiBold,
327    Bold,
328    ExtraBold,
329}
330
331/// Text style properties.
332#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
333pub struct TextStyle {
334    pub font_size: f32,
335    pub color: Color,
336    pub weight: FontWeight,
337    pub italic: bool,
338    pub underline: bool,
339    pub strikethrough: bool,
340    pub line_height: Option<f32>,
341    pub letter_spacing: f32,
342}
343
344impl Default for TextStyle {
345    fn default() -> Self {
346        Self {
347            font_size: 14.0,
348            color: Color::WHITE,
349            weight: FontWeight::Regular,
350            italic: false,
351            underline: false,
352            strikethrough: false,
353            line_height: None,
354            letter_spacing: 0.0,
355        }
356    }
357}
358
359#[cfg(test)]
360mod tests {
361    use super::*;
362
363    // ── Color ────────────────────────────────────────────────────
364
365    #[test]
366    fn color_rgb_constructor() {
367        let c = Color::rgb(0.5, 0.6, 0.7);
368        assert_eq!(c.a, 1.0);
369        assert_eq!(c.r, 0.5);
370    }
371
372    #[test]
373    fn color_rgba_constructor() {
374        let c = Color::rgba(0.1, 0.2, 0.3, 0.4);
375        assert_eq!(c.a, 0.4);
376    }
377
378    #[test]
379    fn color_from_rgb8() {
380        let c = Color::from_rgb8(255, 0, 128);
381        assert!((c.r - 1.0).abs() < 1e-5);
382        assert_eq!(c.g, 0.0);
383        assert!((c.b - 128.0 / 255.0).abs() < 1e-5);
384        assert_eq!(c.a, 1.0);
385    }
386
387    #[test]
388    fn color_from_rgba8() {
389        let c = Color::from_rgba8(0, 0, 0, 127);
390        assert!((c.a - 127.0 / 255.0).abs() < 1e-5);
391    }
392
393    #[test]
394    fn color_lerp() {
395        let a = Color::BLACK;
396        let b = Color::WHITE;
397        let mid = a.lerp(b, 0.5);
398        assert!((mid.r - 0.5).abs() < 1e-5);
399        assert!((mid.g - 0.5).abs() < 1e-5);
400        assert!((mid.b - 0.5).abs() < 1e-5);
401    }
402
403    #[test]
404    fn color_lerp_endpoints() {
405        let a = Color::RED;
406        let b = Color::BLUE;
407        let start = a.lerp(b, 0.0);
408        let end = a.lerp(b, 1.0);
409        assert_eq!(start, a);
410        assert_eq!(end, b);
411    }
412
413    #[test]
414    fn color_with_alpha() {
415        let c = Color::RED.with_alpha(0.5);
416        assert_eq!(c.r, 1.0);
417        assert_eq!(c.a, 0.5);
418    }
419
420    #[test]
421    fn color_default_is_white() {
422        assert_eq!(Color::default(), Color::WHITE);
423    }
424
425    #[test]
426    fn color_constants_correct() {
427        assert_eq!(Color::TRANSPARENT.a, 0.0);
428        assert_eq!(Color::BLACK, Color::rgb(0.0, 0.0, 0.0));
429        assert_eq!(Color::RED.r, 1.0);
430        assert_eq!(Color::GREEN.g, 1.0);
431        assert_eq!(Color::BLUE.b, 1.0);
432    }
433
434    #[test]
435    fn color_serialize_roundtrip() {
436        let c = Color::rgba(0.1, 0.2, 0.3, 0.4);
437        let json = serde_json::to_string(&c).unwrap();
438        let c2: Color = serde_json::from_str(&json).unwrap();
439        assert_eq!(c, c2);
440    }
441
442    // ── Rect ─────────────────────────────────────────────────────
443
444    #[test]
445    fn rect_new_and_accessors() {
446        let r = Rect::new(10.0, 20.0, 100.0, 50.0);
447        assert_eq!(r.right(), 110.0);
448        assert_eq!(r.bottom(), 70.0);
449    }
450
451    #[test]
452    fn rect_from_size() {
453        let r = Rect::from_size(100.0, 50.0);
454        assert_eq!(r.x, 0.0);
455        assert_eq!(r.y, 0.0);
456        assert_eq!(r.width, 100.0);
457    }
458
459    #[test]
460    fn rect_center() {
461        let r = Rect::new(0.0, 0.0, 100.0, 200.0);
462        let c = r.center();
463        assert_eq!(c.x, 50.0);
464        assert_eq!(c.y, 100.0);
465    }
466
467    #[test]
468    fn rect_contains() {
469        let r = Rect::new(10.0, 10.0, 100.0, 100.0);
470        assert!(r.contains(Position::new(50.0, 50.0)));
471        assert!(r.contains(Position::new(10.0, 10.0))); // top-left inclusive
472        assert!(!r.contains(Position::new(110.0, 110.0))); // right edge exclusive
473        assert!(!r.contains(Position::new(5.0, 50.0))); // outside left
474    }
475
476    #[test]
477    fn rect_intersection() {
478        let a = Rect::new(0.0, 0.0, 100.0, 100.0);
479        let b = Rect::new(50.0, 50.0, 100.0, 100.0);
480        let i = a.intersection(&b).unwrap();
481        assert_eq!(i, Rect::new(50.0, 50.0, 50.0, 50.0));
482    }
483
484    #[test]
485    fn rect_intersection_none() {
486        let a = Rect::new(0.0, 0.0, 50.0, 50.0);
487        let b = Rect::new(100.0, 100.0, 50.0, 50.0);
488        assert!(a.intersection(&b).is_none());
489    }
490
491    #[test]
492    fn rect_union() {
493        let a = Rect::new(10.0, 10.0, 50.0, 50.0);
494        let b = Rect::new(40.0, 40.0, 80.0, 80.0);
495        let u = a.union(&b);
496        assert_eq!(u.x, 10.0);
497        assert_eq!(u.y, 10.0);
498        assert_eq!(u.right(), 120.0);
499        assert_eq!(u.bottom(), 120.0);
500    }
501
502    #[test]
503    fn rect_inner_with_margin() {
504        let r = Rect::new(0.0, 0.0, 100.0, 100.0);
505        let m = Margin::uniform(10.0);
506        let inner = r.inner(&m);
507        assert_eq!(inner, Rect::new(10.0, 10.0, 80.0, 80.0));
508    }
509
510    #[test]
511    fn rect_inner_clamps_to_zero() {
512        let r = Rect::new(0.0, 0.0, 10.0, 10.0);
513        let m = Margin::uniform(20.0);
514        let inner = r.inner(&m);
515        assert_eq!(inner.width, 0.0);
516        assert_eq!(inner.height, 0.0);
517    }
518
519    #[test]
520    fn rect_is_empty() {
521        assert!(Rect::ZERO.is_empty());
522        assert!(Rect::new(0.0, 0.0, 0.0, 10.0).is_empty());
523        assert!(!Rect::new(0.0, 0.0, 1.0, 1.0).is_empty());
524    }
525
526    #[test]
527    fn rect_size_and_position() {
528        let r = Rect::new(5.0, 10.0, 20.0, 30.0);
529        assert_eq!(r.size(), Size::new(20.0, 30.0));
530        assert_eq!(r.position(), Position::new(5.0, 10.0));
531    }
532
533    #[test]
534    fn rect_translate() {
535        let r = Rect::new(10.0, 20.0, 50.0, 50.0);
536        let t = r.translate(5.0, -10.0);
537        assert_eq!(t, Rect::new(15.0, 10.0, 50.0, 50.0));
538    }
539
540    #[test]
541    fn rect_inflate() {
542        let r = Rect::new(10.0, 10.0, 20.0, 20.0);
543        let i = r.inflate(5.0, 5.0);
544        assert_eq!(i, Rect::new(5.0, 5.0, 30.0, 30.0));
545    }
546
547    #[test]
548    fn rect_default_is_zero() {
549        assert_eq!(Rect::default(), Rect::ZERO);
550    }
551
552    #[test]
553    fn rect_serialize_roundtrip() {
554        let r = Rect::new(1.0, 2.0, 3.0, 4.0);
555        let json = serde_json::to_string(&r).unwrap();
556        let r2: Rect = serde_json::from_str(&json).unwrap();
557        assert_eq!(r, r2);
558    }
559
560    // ── Position ─────────────────────────────────────────────────
561
562    #[test]
563    fn position_distance() {
564        let a = Position::new(0.0, 0.0);
565        let b = Position::new(3.0, 4.0);
566        assert!((a.distance_to(&b) - 5.0).abs() < 1e-5);
567    }
568
569    #[test]
570    fn position_distance_to_self_is_zero() {
571        let p = Position::new(42.0, 7.0);
572        assert_eq!(p.distance_to(&p), 0.0);
573    }
574
575    #[test]
576    fn position_default_is_zero() {
577        assert_eq!(Position::default(), Position::ZERO);
578    }
579
580    // ── Size ─────────────────────────────────────────────────────
581
582    #[test]
583    fn size_new() {
584        let s = Size::new(100.0, 200.0);
585        assert_eq!(s.width, 100.0);
586        assert_eq!(s.height, 200.0);
587    }
588
589    #[test]
590    fn size_default_is_zero() {
591        assert_eq!(Size::default(), Size::ZERO);
592    }
593
594    // ── Margin ───────────────────────────────────────────────────
595
596    #[test]
597    fn margin_uniform() {
598        let m = Margin::uniform(5.0);
599        assert_eq!(m.top, 5.0);
600        assert_eq!(m.right, 5.0);
601        assert_eq!(m.bottom, 5.0);
602        assert_eq!(m.left, 5.0);
603    }
604
605    #[test]
606    fn margin_default_is_zero() {
607        assert_eq!(Margin::default(), Margin::ZERO);
608    }
609
610    // ── TextStyle ────────────────────────────────────────────────
611
612    #[test]
613    fn text_style_defaults() {
614        let ts = TextStyle::default();
615        assert_eq!(ts.font_size, 14.0);
616        assert_eq!(ts.color, Color::WHITE);
617        assert_eq!(ts.weight, FontWeight::Regular);
618        assert!(!ts.italic);
619        assert!(!ts.underline);
620        assert!(!ts.strikethrough);
621        assert!(ts.line_height.is_none());
622        assert_eq!(ts.letter_spacing, 0.0);
623    }
624
625    #[test]
626    fn font_weight_default() {
627        assert_eq!(FontWeight::default(), FontWeight::Regular);
628    }
629}