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