Skip to main content

presentar_core/
lib.rs

1#![allow(clippy::missing_const_for_fn)]
2#![allow(clippy::missing_fields_in_debug)]
3#![allow(clippy::struct_excessive_bools)]
4#![allow(clippy::many_single_char_names)]
5#![allow(clippy::too_many_arguments)]
6#![allow(clippy::should_implement_trait)]
7#![allow(clippy::trivially_copy_pass_by_ref)]
8#![allow(clippy::fn_params_excessive_bools)]
9#![allow(clippy::type_complexity)]
10#![allow(clippy::match_same_arms)]
11#![allow(clippy::unreadable_literal)]
12#![allow(clippy::manual_clamp)]
13#![allow(clippy::unnecessary_wraps)]
14#![allow(clippy::match_like_matches_macro)]
15#![allow(clippy::struct_field_names)]
16#![allow(clippy::needless_pass_by_value)]
17#![allow(clippy::cast_possible_wrap)]
18//! Core types and traits for Presentar UI framework.
19//!
20//! This crate provides foundational types used throughout Presentar:
21//! - Geometric primitives: [`Point`], [`Size`], [`Rect`]
22//! - Color representation: [`Color`] with WCAG contrast calculations
23//! - Layout constraints: [`Constraints`]
24//! - Events and messages: [`Event`], [`Message`]
25//! - Draw commands: [`DrawCommand`] for GPU rendering
26//!
27//! # Quick Start
28//!
29//! ```rust
30//! use presentar_core::{Color, Size, Constraints, Rect};
31//!
32//! // Create a size
33//! let size = Size::new(100.0, 50.0);
34//!
35//! // Create constraints
36//! let constraints = Constraints::new(0.0, 200.0, 0.0, 100.0);
37//! let bounded = constraints.constrain(size);
38//!
39//! // Create a color
40//! let red = Color::RED;
41//! assert_eq!(red.r, 1.0);
42//! ```
43//!
44//! # Widget Trait
45//!
46//! The core [`Widget`] trait defines the measure-layout-paint cycle:
47//!
48//! ```rust,ignore
49//! use presentar_core::{Widget, Constraints, Size, Canvas};
50//!
51//! struct MyWidget;
52//!
53//! impl Widget for MyWidget {
54//!     fn measure(&self, constraints: &Constraints) -> Size {
55//!         constraints.constrain(Size::new(100.0, 50.0))
56//!     }
57//!
58//!     fn layout(&mut self, size: Size) { }
59//!
60//!     fn paint(&self, canvas: &mut dyn Canvas) { }
61//! }
62//! ```
63
64#[macro_use]
65#[allow(unused_macros)]
66mod generated_contracts;
67
68pub mod accessibility;
69pub mod animation;
70pub mod binding;
71pub mod cache;
72mod canvas;
73pub mod chart;
74pub mod clipboard;
75mod color;
76mod constraints;
77pub mod diff;
78pub mod dnd;
79pub mod draw;
80mod event;
81mod geometry;
82pub mod gesture;
83pub mod history;
84pub mod lifecycle;
85mod runtime;
86pub mod shortcut;
87pub mod simd;
88
89// Re-export ComputeBlock primitives for convenient access
90pub use simd::{
91    batch_mean_f64, batch_min_max_f64, batch_scale_f64, batch_scale_offset_f64, batch_stddev_f64,
92    batch_sum_f64, batch_variance_f64, histogram_f64, normalize_f64, normalize_with_range_f64,
93    percentile_sorted_f64, weighted_sum_f64,
94};
95mod state;
96pub mod streaming;
97pub mod theme;
98pub mod validation;
99pub mod virtualization;
100pub mod widget;
101
102// Brick Architecture types (native - CB-081: no jugar-probar prod dep)
103pub mod brick_types;
104
105// Brick Architecture integration (PROBAR-SPEC-009: Brick is mandatory)
106pub mod brick_widget;
107
108pub use accessibility::{
109    AccessibilityTree, AccessibilityTreeBuilder, AccessibleNode, AccessibleNodeId, CheckedState,
110    HitTester, LiveRegion,
111};
112pub use animation::{
113    AnimColor, AnimatedValue, AnimationController, EasedValue, Easing, Interpolate, Keyframe,
114    KeyframeTrack, Spring, SpringConfig,
115};
116pub use cache::{
117    CacheBuilder, CacheCallback, CacheConfig, CacheEvent, CacheKey, CacheMetadata, CacheOptions,
118    CacheSize, CacheState, CacheStats, DataCache, StringCache,
119};
120pub use canvas::RecordingCanvas;
121pub use chart::{
122    ArcGeometry, CatmullRom, CubicBezier, CubicSpline, DataNormalizer, DrawBatch, HistogramBins,
123    Interpolator, LinearInterpolator, PathTessellator, Point2D,
124};
125pub use clipboard::{
126    Clipboard, ClipboardData, ClipboardEvent, ClipboardFormat, ClipboardHistory,
127    ClipboardOperation, ClipboardResult,
128};
129pub use color::{Color, ColorParseError};
130pub use constraints::Constraints;
131pub use diff::{diff_trees, DiffNode, DiffOp, DiffResult, TreeDiffer, WidgetKey};
132pub use dnd::{
133    DragData, DragDataType, DragDropManager, DragId, DragPayload, DragPhase, DragState, DropEffect,
134    DropResult, DropTarget,
135};
136pub use draw::{
137    BoxStyle, DrawCommand, FillRule, LineCap, LineJoin, PathRef, Sampling, Shadow, StrokeStyle,
138    TensorRef, Transform2D as DrawTransform,
139};
140pub use event::{Event, GestureState, Key, MouseButton, PointerId, PointerType, TouchId};
141pub use geometry::{CornerRadius, Point, Rect, Size};
142pub use gesture::{
143    GestureConfig, GestureRecognizer, PointerGestureRecognizer, PointerInfo, RecognizedGesture,
144    TouchPoint,
145};
146pub use history::{
147    Checkpoint, CheckpointId, Command as HistoryCommand, CommandHistory, CommandId, CommandResult,
148    CompositeCommand, GroupId, HistoryCallback, HistoryConfig, HistoryEvent, SetValueCommand,
149};
150pub use lifecycle::{
151    Effect, EffectManager, HookId, LifecycleEvent, LifecycleManager, LifecyclePhase,
152};
153pub use runtime::{
154    default_executor, AnimatedProperty, AnimationId, AnimationInstance, AnimationState, Animator,
155    CommandExecutor, DataRefreshManager, EasingFunction, ExecutionResult, ExecutorConfig,
156    FocusDirection, FocusManager, FocusTrap, FrameTimer, MemoryRouter, MemoryStorage, RefreshTask,
157    Router, SpringAnimation, Storage, Timer, TransitionConfig, Tween,
158};
159pub use shortcut::{
160    Modifiers, Shortcut, ShortcutBuilder, ShortcutContext, ShortcutId, ShortcutManager,
161    ShortcutPriority,
162};
163pub use state::{Command, CounterMessage, CounterState, State, Store};
164pub use streaming::{
165    ConnectionState, DataStream, MessageBuffer, RateLimiter, ReconnectConfig, StreamConfig,
166    StreamMessage, StreamSubscription,
167};
168pub use theme::{ColorPalette, ContrastCheck, Radii, Shadows, Spacing, Theme, Typography};
169pub use validation::{
170    FieldConfig, FieldState, FormValidator, MaxLength, MinLength, Pattern, PatternType, Range,
171    Required, ValidateOn, ValidationResult, Validator,
172};
173pub use virtualization::{
174    CellLayout, GridCell, ItemIndex, ItemLayout, ScrollAlign, VirtualGrid, VirtualGridConfig,
175    VirtualList, VirtualListConfig, VisibleGridRange, VisibleRange,
176};
177pub use widget::{
178    AccessibleRole, Canvas, FontStyle, FontWeight, LayoutResult, TextStyle, Transform2D, TypeId,
179    Widget, WidgetId,
180};
181
182// Re-export Brick types (PROBAR-SPEC-009: Brick is mandatory)
183pub use widget::{
184    Brick, BrickAssertion, BrickBudget, BrickError, BrickPhase, BrickResult, BrickVerification,
185    BudgetViolation,
186};
187
188// Re-export brick_widget helpers
189pub use brick_widget::{BrickWidgetExt, DefaultBrick, SimpleBrick};
190
191#[cfg(test)]
192#[allow(clippy::unwrap_used, clippy::disallowed_methods)]
193mod tests {
194    use super::*;
195
196    // ==========================================================================
197    // COLOR TESTS - Written FIRST per EXTREME TDD
198    // ==========================================================================
199
200    mod color_tests {
201        use super::*;
202        use proptest::prelude::*;
203
204        #[test]
205        fn test_color_new_clamps_values() {
206            let c = Color::new(1.5, -0.5, 0.5, 2.0);
207            assert_eq!(c.r, 1.0);
208            assert_eq!(c.g, 0.0);
209            assert_eq!(c.b, 0.5);
210            assert_eq!(c.a, 1.0);
211        }
212
213        #[test]
214        fn test_color_from_rgb() {
215            let c = Color::rgb(0.5, 0.5, 0.5);
216            assert_eq!(c.r, 0.5);
217            assert_eq!(c.g, 0.5);
218            assert_eq!(c.b, 0.5);
219            assert_eq!(c.a, 1.0);
220        }
221
222        #[test]
223        fn test_color_from_hex() {
224            let c = Color::from_hex("#ff0000").unwrap();
225            assert_eq!(c.r, 1.0);
226            assert_eq!(c.g, 0.0);
227            assert_eq!(c.b, 0.0);
228
229            let c2 = Color::from_hex("#00ff00").unwrap();
230            assert_eq!(c2.g, 1.0);
231
232            let c3 = Color::from_hex("0000ff").unwrap();
233            assert_eq!(c3.b, 1.0);
234        }
235
236        #[test]
237        fn test_color_from_hex_with_alpha() {
238            let c = Color::from_hex("#ff000080").unwrap();
239            assert_eq!(c.r, 1.0);
240            assert!((c.a - 0.502).abs() < 0.01); // 128/255 ≈ 0.502
241        }
242
243        #[test]
244        fn test_color_from_hex_invalid() {
245            assert!(Color::from_hex("invalid").is_err());
246            assert!(Color::from_hex("#gg0000").is_err());
247            assert!(Color::from_hex("#ff").is_err());
248        }
249
250        #[test]
251        fn test_color_relative_luminance_black() {
252            let black = Color::rgb(0.0, 0.0, 0.0);
253            assert_eq!(black.relative_luminance(), 0.0);
254        }
255
256        #[test]
257        fn test_color_relative_luminance_white() {
258            let white = Color::rgb(1.0, 1.0, 1.0);
259            assert!((white.relative_luminance() - 1.0).abs() < 0.001);
260        }
261
262        #[test]
263        fn test_color_contrast_ratio_black_white() {
264            let black = Color::rgb(0.0, 0.0, 0.0);
265            let white = Color::rgb(1.0, 1.0, 1.0);
266            let ratio = black.contrast_ratio(&white);
267            assert!((ratio - 21.0).abs() < 0.1); // WCAG max contrast is 21:1
268        }
269
270        #[test]
271        fn test_color_contrast_ratio_wcag_aa() {
272            // WCAG AA requires 4.5:1 for normal text
273            let dark = Color::rgb(0.0, 0.0, 0.0);
274            let light = Color::rgb(0.5, 0.5, 0.5);
275            let ratio = dark.contrast_ratio(&light);
276            assert!(ratio >= 4.5, "Contrast ratio {ratio} should be >= 4.5");
277        }
278
279        #[test]
280        fn test_color_contrast_ratio_symmetric() {
281            let c1 = Color::rgb(0.2, 0.4, 0.6);
282            let c2 = Color::rgb(0.8, 0.6, 0.4);
283            assert_eq!(c1.contrast_ratio(&c2), c2.contrast_ratio(&c1));
284        }
285
286        #[test]
287        fn test_color_to_hex() {
288            let c = Color::rgb(1.0, 0.0, 0.0);
289            assert_eq!(c.to_hex(), "#ff0000");
290
291            let c2 = Color::new(0.0, 1.0, 0.0, 0.5);
292            assert_eq!(c2.to_hex_with_alpha(), "#00ff0080");
293        }
294
295        #[test]
296        fn test_color_lerp() {
297            let black = Color::rgb(0.0, 0.0, 0.0);
298            let white = Color::rgb(1.0, 1.0, 1.0);
299
300            let mid = black.lerp(&white, 0.5);
301            assert!((mid.r - 0.5).abs() < 0.001);
302            assert!((mid.g - 0.5).abs() < 0.001);
303            assert!((mid.b - 0.5).abs() < 0.001);
304        }
305
306        proptest! {
307            #[test]
308            fn prop_color_clamps_to_valid_range(r in -1.0f32..2.0, g in -1.0f32..2.0, b in -1.0f32..2.0, a in -1.0f32..2.0) {
309                let c = Color::new(r, g, b, a);
310                prop_assert!(c.r >= 0.0 && c.r <= 1.0);
311                prop_assert!(c.g >= 0.0 && c.g <= 1.0);
312                prop_assert!(c.b >= 0.0 && c.b <= 1.0);
313                prop_assert!(c.a >= 0.0 && c.a <= 1.0);
314            }
315
316            #[test]
317            fn prop_contrast_ratio_always_positive(
318                r1 in 0.0f32..1.0, g1 in 0.0f32..1.0, b1 in 0.0f32..1.0,
319                r2 in 0.0f32..1.0, g2 in 0.0f32..1.0, b2 in 0.0f32..1.0
320            ) {
321                let c1 = Color::rgb(r1, g1, b1);
322                let c2 = Color::rgb(r2, g2, b2);
323                prop_assert!(c1.contrast_ratio(&c2) >= 1.0);
324            }
325
326            #[test]
327            fn prop_lerp_at_zero_returns_self(r in 0.0f32..1.0, g in 0.0f32..1.0, b in 0.0f32..1.0) {
328                let c1 = Color::rgb(r, g, b);
329                let c2 = Color::rgb(1.0 - r, 1.0 - g, 1.0 - b);
330                let result = c1.lerp(&c2, 0.0);
331                prop_assert!((result.r - c1.r).abs() < 0.001);
332                prop_assert!((result.g - c1.g).abs() < 0.001);
333                prop_assert!((result.b - c1.b).abs() < 0.001);
334            }
335
336            #[test]
337            fn prop_lerp_at_one_returns_other(r in 0.0f32..1.0, g in 0.0f32..1.0, b in 0.0f32..1.0) {
338                let c1 = Color::rgb(r, g, b);
339                let c2 = Color::rgb(1.0 - r, 1.0 - g, 1.0 - b);
340                let result = c1.lerp(&c2, 1.0);
341                prop_assert!((result.r - c2.r).abs() < 0.001);
342                prop_assert!((result.g - c2.g).abs() < 0.001);
343                prop_assert!((result.b - c2.b).abs() < 0.001);
344            }
345        }
346    }
347
348    // ==========================================================================
349    // GEOMETRY TESTS - Written FIRST per EXTREME TDD
350    // ==========================================================================
351
352    mod geometry_tests {
353        use super::*;
354        use proptest::prelude::*;
355
356        #[test]
357        fn test_point_new() {
358            let p = Point::new(10.0, 20.0);
359            assert_eq!(p.x, 10.0);
360            assert_eq!(p.y, 20.0);
361        }
362
363        #[test]
364        fn test_point_origin() {
365            let p = Point::ORIGIN;
366            assert_eq!(p.x, 0.0);
367            assert_eq!(p.y, 0.0);
368        }
369
370        #[test]
371        fn test_point_distance() {
372            let p1 = Point::new(0.0, 0.0);
373            let p2 = Point::new(3.0, 4.0);
374            assert!((p1.distance(&p2) - 5.0).abs() < 0.001);
375        }
376
377        #[test]
378        fn test_point_add() {
379            let p1 = Point::new(1.0, 2.0);
380            let p2 = Point::new(3.0, 4.0);
381            let sum = p1 + p2;
382            assert_eq!(sum.x, 4.0);
383            assert_eq!(sum.y, 6.0);
384        }
385
386        #[test]
387        fn test_point_sub() {
388            let p1 = Point::new(5.0, 7.0);
389            let p2 = Point::new(2.0, 3.0);
390            let diff = p1 - p2;
391            assert_eq!(diff.x, 3.0);
392            assert_eq!(diff.y, 4.0);
393        }
394
395        #[test]
396        fn test_size_new() {
397            let s = Size::new(100.0, 200.0);
398            assert_eq!(s.width, 100.0);
399            assert_eq!(s.height, 200.0);
400        }
401
402        #[test]
403        fn test_size_zero() {
404            let s = Size::ZERO;
405            assert_eq!(s.width, 0.0);
406            assert_eq!(s.height, 0.0);
407        }
408
409        #[test]
410        fn test_size_area() {
411            let s = Size::new(10.0, 20.0);
412            assert_eq!(s.area(), 200.0);
413        }
414
415        #[test]
416        fn test_size_aspect_ratio() {
417            let s = Size::new(16.0, 9.0);
418            assert!((s.aspect_ratio() - 16.0 / 9.0).abs() < 0.001);
419        }
420
421        #[test]
422        fn test_size_contains() {
423            let s = Size::new(100.0, 100.0);
424            let smaller = Size::new(50.0, 50.0);
425            let larger = Size::new(150.0, 50.0);
426            assert!(s.contains(&smaller));
427            assert!(!s.contains(&larger));
428        }
429
430        #[test]
431        fn test_rect_new() {
432            let r = Rect::new(10.0, 20.0, 100.0, 200.0);
433            assert_eq!(r.x, 10.0);
434            assert_eq!(r.y, 20.0);
435            assert_eq!(r.width, 100.0);
436            assert_eq!(r.height, 200.0);
437        }
438
439        #[test]
440        fn test_rect_from_points() {
441            let r = Rect::from_points(Point::new(10.0, 20.0), Point::new(110.0, 220.0));
442            assert_eq!(r.x, 10.0);
443            assert_eq!(r.y, 20.0);
444            assert_eq!(r.width, 100.0);
445            assert_eq!(r.height, 200.0);
446        }
447
448        #[test]
449        fn test_rect_from_size() {
450            let r = Rect::from_size(Size::new(100.0, 200.0));
451            assert_eq!(r.x, 0.0);
452            assert_eq!(r.y, 0.0);
453            assert_eq!(r.width, 100.0);
454            assert_eq!(r.height, 200.0);
455        }
456
457        #[test]
458        fn test_rect_origin_and_size() {
459            let r = Rect::new(10.0, 20.0, 100.0, 200.0);
460            assert_eq!(r.origin(), Point::new(10.0, 20.0));
461            assert_eq!(r.size(), Size::new(100.0, 200.0));
462        }
463
464        #[test]
465        fn test_rect_corners() {
466            let r = Rect::new(10.0, 20.0, 100.0, 200.0);
467            assert_eq!(r.top_left(), Point::new(10.0, 20.0));
468            assert_eq!(r.top_right(), Point::new(110.0, 20.0));
469            assert_eq!(r.bottom_left(), Point::new(10.0, 220.0));
470            assert_eq!(r.bottom_right(), Point::new(110.0, 220.0));
471        }
472
473        #[test]
474        fn test_rect_center() {
475            let r = Rect::new(0.0, 0.0, 100.0, 100.0);
476            assert_eq!(r.center(), Point::new(50.0, 50.0));
477        }
478
479        #[test]
480        fn test_rect_contains_point() {
481            let r = Rect::new(10.0, 10.0, 100.0, 100.0);
482            assert!(r.contains_point(&Point::new(50.0, 50.0)));
483            assert!(r.contains_point(&Point::new(10.0, 10.0))); // Edge inclusive
484            assert!(!r.contains_point(&Point::new(5.0, 50.0)));
485            assert!(!r.contains_point(&Point::new(111.0, 50.0)));
486        }
487
488        #[test]
489        fn test_rect_intersects() {
490            let r1 = Rect::new(0.0, 0.0, 100.0, 100.0);
491            let r2 = Rect::new(50.0, 50.0, 100.0, 100.0);
492            let r3 = Rect::new(200.0, 200.0, 100.0, 100.0);
493
494            assert!(r1.intersects(&r2));
495            assert!(!r1.intersects(&r3));
496        }
497
498        #[test]
499        fn test_rect_intersection() {
500            let r1 = Rect::new(0.0, 0.0, 100.0, 100.0);
501            let r2 = Rect::new(50.0, 50.0, 100.0, 100.0);
502
503            let inter = r1.intersection(&r2).unwrap();
504            assert_eq!(inter.x, 50.0);
505            assert_eq!(inter.y, 50.0);
506            assert_eq!(inter.width, 50.0);
507            assert_eq!(inter.height, 50.0);
508        }
509
510        #[test]
511        fn test_rect_union() {
512            let r1 = Rect::new(0.0, 0.0, 50.0, 50.0);
513            let r2 = Rect::new(25.0, 25.0, 50.0, 50.0);
514
515            let union = r1.union(&r2);
516            assert_eq!(union.x, 0.0);
517            assert_eq!(union.y, 0.0);
518            assert_eq!(union.width, 75.0);
519            assert_eq!(union.height, 75.0);
520        }
521
522        #[test]
523        fn test_rect_inset() {
524            let r = Rect::new(10.0, 10.0, 100.0, 100.0);
525            let inset = r.inset(5.0);
526            assert_eq!(inset.x, 15.0);
527            assert_eq!(inset.y, 15.0);
528            assert_eq!(inset.width, 90.0);
529            assert_eq!(inset.height, 90.0);
530        }
531
532        #[test]
533        fn test_corner_radius() {
534            let uniform = CornerRadius::uniform(10.0);
535            assert_eq!(uniform.top_left, 10.0);
536            assert_eq!(uniform.top_right, 10.0);
537            assert_eq!(uniform.bottom_left, 10.0);
538            assert_eq!(uniform.bottom_right, 10.0);
539
540            let custom = CornerRadius::new(1.0, 2.0, 3.0, 4.0);
541            assert_eq!(custom.top_left, 1.0);
542            assert_eq!(custom.top_right, 2.0);
543            assert_eq!(custom.bottom_right, 3.0);
544            assert_eq!(custom.bottom_left, 4.0);
545        }
546
547        proptest! {
548            #[test]
549            fn prop_point_distance_non_negative(x1 in -1000.0f32..1000.0, y1 in -1000.0f32..1000.0, x2 in -1000.0f32..1000.0, y2 in -1000.0f32..1000.0) {
550                let p1 = Point::new(x1, y1);
551                let p2 = Point::new(x2, y2);
552                prop_assert!(p1.distance(&p2) >= 0.0);
553            }
554
555            #[test]
556            fn prop_point_distance_symmetric(x1 in -1000.0f32..1000.0, y1 in -1000.0f32..1000.0, x2 in -1000.0f32..1000.0, y2 in -1000.0f32..1000.0) {
557                let p1 = Point::new(x1, y1);
558                let p2 = Point::new(x2, y2);
559                prop_assert!((p1.distance(&p2) - p2.distance(&p1)).abs() < 0.001);
560            }
561
562            #[test]
563            fn prop_rect_area_non_negative(x in -1000.0f32..1000.0, y in -1000.0f32..1000.0, w in 0.0f32..1000.0, h in 0.0f32..1000.0) {
564                let r = Rect::new(x, y, w, h);
565                prop_assert!(r.area() >= 0.0);
566            }
567
568            #[test]
569            fn prop_rect_contains_center(x in -1000.0f32..1000.0, y in -1000.0f32..1000.0, w in 1.0f32..1000.0, h in 1.0f32..1000.0) {
570                let r = Rect::new(x, y, w, h);
571                prop_assert!(r.contains_point(&r.center()));
572            }
573
574            #[test]
575            fn prop_rect_intersects_self(x in -1000.0f32..1000.0, y in -1000.0f32..1000.0, w in 0.1f32..1000.0, h in 0.1f32..1000.0) {
576                let r = Rect::new(x, y, w, h);
577                prop_assert!(r.intersects(&r));
578            }
579        }
580    }
581
582    // ==========================================================================
583    // CONSTRAINTS TESTS - Written FIRST per EXTREME TDD
584    // ==========================================================================
585
586    mod constraints_tests {
587        use super::*;
588
589        #[test]
590        fn test_constraints_tight() {
591            let c = Constraints::tight(Size::new(100.0, 200.0));
592            assert_eq!(c.min_width, 100.0);
593            assert_eq!(c.max_width, 100.0);
594            assert_eq!(c.min_height, 200.0);
595            assert_eq!(c.max_height, 200.0);
596        }
597
598        #[test]
599        fn test_constraints_loose() {
600            let c = Constraints::loose(Size::new(100.0, 200.0));
601            assert_eq!(c.min_width, 0.0);
602            assert_eq!(c.max_width, 100.0);
603            assert_eq!(c.min_height, 0.0);
604            assert_eq!(c.max_height, 200.0);
605        }
606
607        #[test]
608        fn test_constraints_unbounded() {
609            let c = Constraints::unbounded();
610            assert_eq!(c.min_width, 0.0);
611            assert_eq!(c.max_width, f32::INFINITY);
612            assert_eq!(c.min_height, 0.0);
613            assert_eq!(c.max_height, f32::INFINITY);
614        }
615
616        #[test]
617        fn test_constraints_constrain() {
618            let c = Constraints::new(50.0, 150.0, 50.0, 150.0);
619
620            // Within bounds
621            assert_eq!(
622                c.constrain(Size::new(100.0, 100.0)),
623                Size::new(100.0, 100.0)
624            );
625
626            // Below min
627            assert_eq!(c.constrain(Size::new(10.0, 10.0)), Size::new(50.0, 50.0));
628
629            // Above max
630            assert_eq!(
631                c.constrain(Size::new(200.0, 200.0)),
632                Size::new(150.0, 150.0)
633            );
634        }
635
636        #[test]
637        fn test_constraints_is_tight() {
638            let tight = Constraints::tight(Size::new(100.0, 100.0));
639            let loose = Constraints::loose(Size::new(100.0, 100.0));
640
641            assert!(tight.is_tight());
642            assert!(!loose.is_tight());
643        }
644
645        #[test]
646        fn test_constraints_has_bounded_width() {
647            let bounded = Constraints::new(0.0, 100.0, 0.0, f32::INFINITY);
648            let unbounded = Constraints::unbounded();
649
650            assert!(bounded.has_bounded_width());
651            assert!(!unbounded.has_bounded_width());
652        }
653    }
654
655    // ==========================================================================
656    // EVENT TESTS - Written FIRST per EXTREME TDD
657    // ==========================================================================
658
659    mod event_tests {
660        use super::*;
661
662        #[test]
663        fn test_event_mouse_move() {
664            let e = Event::MouseMove {
665                position: Point::new(100.0, 200.0),
666            };
667            if let Event::MouseMove { position } = e {
668                assert_eq!(position.x, 100.0);
669                assert_eq!(position.y, 200.0);
670            } else {
671                panic!("Expected MouseMove event");
672            }
673        }
674
675        #[test]
676        fn test_event_mouse_button() {
677            let e = Event::MouseDown {
678                position: Point::new(50.0, 50.0),
679                button: MouseButton::Left,
680            };
681            if let Event::MouseDown { button, .. } = e {
682                assert_eq!(button, MouseButton::Left);
683            } else {
684                panic!("Expected MouseDown event");
685            }
686        }
687
688        #[test]
689        fn test_event_key() {
690            let e = Event::key_down(Key::Enter);
691            if let Event::KeyDown { key, .. } = e {
692                assert_eq!(key, Key::Enter);
693            } else {
694                panic!("Expected KeyDown event");
695            }
696        }
697
698        #[test]
699        fn test_event_scroll() {
700            let e = Event::Scroll {
701                delta_x: 0.0,
702                delta_y: -10.0,
703            };
704            if let Event::Scroll { delta_y, .. } = e {
705                assert_eq!(delta_y, -10.0);
706            } else {
707                panic!("Expected Scroll event");
708            }
709        }
710
711        #[test]
712        fn test_event_text_input() {
713            let e = Event::TextInput {
714                text: "hello".to_string(),
715            };
716            if let Event::TextInput { text } = e {
717                assert_eq!(text, "hello");
718            } else {
719                panic!("Expected TextInput event");
720            }
721        }
722    }
723}