1use serde::{Deserialize, Serialize};
8
9#[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 #[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 #[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 #[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 #[must_use]
80 pub fn with_alpha(self, alpha: f32) -> Self {
81 Self { a: alpha, ..self }
82 }
83}
84
85#[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#[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#[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#[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#[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#[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 #[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 #[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))); assert!(!r.contains(Position::new(110.0, 110.0))); assert!(!r.contains(Position::new(5.0, 50.0))); }
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 #[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 #[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 #[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 #[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}