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