presentar_core/
widget.rs

1//! Widget trait and related types.
2//!
3//! This module defines the core `Widget` trait and supporting types for building
4//! UI components in Presentar.
5//!
6//! # Widget Lifecycle (Brick Architecture - PROBAR-SPEC-009)
7//!
8//! Widgets follow a verify-measure-layout-paint cycle:
9//!
10//! 1. **Verify**: Check all Brick assertions pass (Popperian falsification)
11//! 2. **Measure**: Compute intrinsic size given constraints
12//! 3. **Layout**: Position self and children within allocated bounds
13//! 4. **Paint**: Generate draw commands for rendering (only if verified)
14//!
15//! # Brick Integration
16//!
17//! When the `brick` feature is enabled, all Widgets must implement the `Brick`
18//! trait from `jugar_probar`. This enforces the "tests define interface" philosophy:
19//!
20//! - Assertions are verified before every paint
21//! - Budget violations trigger Jidoka (stop-the-line)
22//! - Rendering is blocked if any assertion fails
23//!
24//! # Examples
25//!
26//! ```
27//! use presentar_core::{WidgetId, TypeId, Transform2D};
28//!
29//! // Create widget identifiers
30//! let id = WidgetId::new(42);
31//! assert_eq!(id.0, 42);
32//!
33//! // Get type IDs for widget type comparison
34//! let string_type = TypeId::of::<String>();
35//! let i32_type = TypeId::of::<i32>();
36//! assert_ne!(string_type, i32_type);
37//!
38//! // Create transforms for rendering
39//! let translate = Transform2D::translate(10.0, 20.0);
40//! let scale = Transform2D::scale(2.0, 2.0);
41//! ```
42
43use crate::constraints::Constraints;
44use crate::event::Event;
45use crate::geometry::{Rect, Size};
46use serde::{Deserialize, Serialize};
47use std::any::Any;
48
49// Re-export Brick types (PROBAR-SPEC-009: Brick is mandatory)
50pub use jugar_probar::brick::{
51    Brick, BrickAssertion, BrickBudget, BrickError, BrickPhase, BrickResult, BrickVerification,
52    BudgetViolation,
53};
54
55/// Unique identifier for a widget instance.
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
57pub struct WidgetId(pub u64);
58
59impl WidgetId {
60    /// Create a new widget ID.
61    #[must_use]
62    pub const fn new(id: u64) -> Self {
63        Self(id)
64    }
65}
66
67/// Type identifier for widget types (used for diffing).
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
69pub struct TypeId(std::any::TypeId);
70
71impl TypeId {
72    /// Get the type ID for a type.
73    #[must_use]
74    pub fn of<T: 'static>() -> Self {
75        Self(std::any::TypeId::of::<T>())
76    }
77}
78
79/// Result of laying out a widget.
80#[derive(Debug, Clone, Copy, Default)]
81pub struct LayoutResult {
82    /// Computed size after layout
83    pub size: Size,
84}
85
86/// Core widget trait that all UI elements implement.
87///
88/// # Brick Architecture (PROBAR-SPEC-009)
89///
90/// Widget REQUIRES the `Brick` trait, enforcing the "tests define interface" philosophy:
91///
92/// - Every Widget has assertions that define its contract
93/// - Every Widget has a performance budget
94/// - Rendering is blocked if assertions fail (Popperian falsification)
95///
96/// # Lifecycle
97///
98/// 1. `verify`: Check Brick assertions (mandatory)
99/// 2. `measure`: Compute intrinsic size given constraints
100/// 3. `layout`: Position self and children within allocated bounds
101/// 4. `paint`: Generate draw commands (only if `can_render()` returns true)
102pub trait Widget: Brick + Send + Sync {
103    /// Get the type identifier for this widget type.
104    fn type_id(&self) -> TypeId;
105
106    /// Compute intrinsic size constraints.
107    fn measure(&self, constraints: Constraints) -> Size;
108
109    /// Position children within allocated bounds.
110    fn layout(&mut self, bounds: Rect) -> LayoutResult;
111
112    /// Generate draw commands for rendering.
113    ///
114    /// # Panics
115    ///
116    /// Panics if called when `can_render()` returns false (Brick verification failed).
117    fn paint(&self, canvas: &mut dyn Canvas);
118
119    /// Handle input events.
120    fn event(&mut self, event: &Event) -> Option<Box<dyn Any + Send>>;
121
122    /// Get child widgets for tree traversal.
123    fn children(&self) -> &[Box<dyn Widget>];
124
125    /// Get mutable child widgets.
126    fn children_mut(&mut self) -> &mut [Box<dyn Widget>];
127
128    /// Check if this widget is interactive (can receive focus/events).
129    fn is_interactive(&self) -> bool {
130        false
131    }
132
133    /// Check if this widget can receive keyboard focus.
134    fn is_focusable(&self) -> bool {
135        false
136    }
137
138    /// Get the accessible name for screen readers.
139    fn accessible_name(&self) -> Option<&str> {
140        None
141    }
142
143    /// Get the accessible role.
144    fn accessible_role(&self) -> AccessibleRole {
145        AccessibleRole::Generic
146    }
147
148    /// Get the test ID for this widget (if any).
149    fn test_id(&self) -> Option<&str> {
150        None
151    }
152
153    /// Get the current bounds of this widget.
154    fn bounds(&self) -> Rect {
155        Rect::new(0.0, 0.0, 0.0, 0.0)
156    }
157}
158
159// NOTE: Non-Brick Widget trait has been REMOVED (PROBAR-SPEC-009 Phase 6)
160// All widgets MUST implement Brick trait. There is no backwards compatibility path.
161// This is intentional - "tests define interface" is mandatory, not optional.
162
163/// Canvas trait for paint operations.
164///
165/// This is a minimal abstraction over the rendering backend.
166pub trait Canvas {
167    /// Draw a filled rectangle.
168    fn fill_rect(&mut self, rect: Rect, color: crate::Color);
169
170    /// Draw a stroked rectangle.
171    fn stroke_rect(&mut self, rect: Rect, color: crate::Color, width: f32);
172
173    /// Draw text.
174    fn draw_text(&mut self, text: &str, position: crate::Point, style: &TextStyle);
175
176    /// Draw a line between two points.
177    fn draw_line(&mut self, from: crate::Point, to: crate::Point, color: crate::Color, width: f32);
178
179    /// Draw a filled circle.
180    fn fill_circle(&mut self, center: crate::Point, radius: f32, color: crate::Color);
181
182    /// Draw a stroked circle.
183    fn stroke_circle(&mut self, center: crate::Point, radius: f32, color: crate::Color, width: f32);
184
185    /// Draw a filled arc (pie slice).
186    fn fill_arc(
187        &mut self,
188        center: crate::Point,
189        radius: f32,
190        start_angle: f32,
191        end_angle: f32,
192        color: crate::Color,
193    );
194
195    /// Draw a path (polyline).
196    fn draw_path(&mut self, points: &[crate::Point], color: crate::Color, width: f32);
197
198    /// Fill a polygon.
199    fn fill_polygon(&mut self, points: &[crate::Point], color: crate::Color);
200
201    /// Push a clip region.
202    fn push_clip(&mut self, rect: Rect);
203
204    /// Pop the clip region.
205    fn pop_clip(&mut self);
206
207    /// Push a transform.
208    fn push_transform(&mut self, transform: Transform2D);
209
210    /// Pop the transform.
211    fn pop_transform(&mut self);
212}
213
214/// Text style for rendering.
215///
216/// # Examples
217///
218/// ```
219/// use presentar_core::{TextStyle, FontWeight, FontStyle, Color};
220///
221/// // Use default style
222/// let default_style = TextStyle::default();
223/// assert_eq!(default_style.size, 16.0);
224/// assert_eq!(default_style.weight, FontWeight::Normal);
225///
226/// // Create custom style
227/// let heading_style = TextStyle {
228///     size: 24.0,
229///     color: Color::from_hex("#1a1a1a").expect("valid hex"),
230///     weight: FontWeight::Bold,
231///     style: FontStyle::Normal,
232/// };
233/// ```
234#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
235pub struct TextStyle {
236    /// Font size in pixels
237    pub size: f32,
238    /// Text color
239    pub color: crate::Color,
240    /// Font weight
241    pub weight: FontWeight,
242    /// Font style
243    pub style: FontStyle,
244}
245
246impl Default for TextStyle {
247    fn default() -> Self {
248        Self {
249            size: 16.0,
250            color: crate::Color::BLACK,
251            weight: FontWeight::Normal,
252            style: FontStyle::Normal,
253        }
254    }
255}
256
257/// Font weight.
258#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
259pub enum FontWeight {
260    /// Thin (100)
261    Thin,
262    /// Light (300)
263    Light,
264    /// Normal (400)
265    Normal,
266    /// Medium (500)
267    Medium,
268    /// Semibold (600)
269    Semibold,
270    /// Bold (700)
271    Bold,
272    /// Black (900)
273    Black,
274}
275
276/// Font style.
277#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
278pub enum FontStyle {
279    /// Normal style
280    Normal,
281    /// Italic style
282    Italic,
283}
284
285/// 2D affine transform.
286#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
287pub struct Transform2D {
288    /// Matrix elements [a, b, c, d, e, f] for:
289    /// | a c e |
290    /// | b d f |
291    /// | 0 0 1 |
292    pub matrix: [f32; 6],
293}
294
295impl Transform2D {
296    /// Identity transform.
297    pub const IDENTITY: Self = Self {
298        matrix: [1.0, 0.0, 0.0, 1.0, 0.0, 0.0],
299    };
300
301    /// Create a translation transform.
302    #[must_use]
303    pub const fn translate(x: f32, y: f32) -> Self {
304        Self {
305            matrix: [1.0, 0.0, 0.0, 1.0, x, y],
306        }
307    }
308
309    /// Create a scale transform.
310    #[must_use]
311    pub const fn scale(sx: f32, sy: f32) -> Self {
312        Self {
313            matrix: [sx, 0.0, 0.0, sy, 0.0, 0.0],
314        }
315    }
316
317    /// Create a rotation transform (angle in radians).
318    #[must_use]
319    pub fn rotate(angle: f32) -> Self {
320        let (sin, cos) = angle.sin_cos();
321        Self {
322            matrix: [cos, sin, -sin, cos, 0.0, 0.0],
323        }
324    }
325}
326
327impl Default for Transform2D {
328    fn default() -> Self {
329        Self::IDENTITY
330    }
331}
332
333/// Accessible role for screen readers.
334#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
335pub enum AccessibleRole {
336    /// Generic element
337    #[default]
338    Generic,
339    /// Button
340    Button,
341    /// Checkbox
342    Checkbox,
343    /// Text input
344    TextInput,
345    /// Link
346    Link,
347    /// Heading
348    Heading,
349    /// Image
350    Image,
351    /// List
352    List,
353    /// List item
354    ListItem,
355    /// Table
356    Table,
357    /// Table row
358    TableRow,
359    /// Table cell
360    TableCell,
361    /// Menu
362    Menu,
363    /// Menu item
364    MenuItem,
365    /// Combo box / dropdown select
366    ComboBox,
367    /// Slider
368    Slider,
369    /// Progress bar
370    ProgressBar,
371    /// Tab
372    Tab,
373    /// Tab panel
374    TabPanel,
375    /// Radio group
376    RadioGroup,
377    /// Radio button
378    Radio,
379}
380
381#[cfg(test)]
382mod tests {
383    use super::*;
384
385    #[test]
386    fn test_widget_id() {
387        let id = WidgetId::new(42);
388        assert_eq!(id.0, 42);
389    }
390
391    #[test]
392    fn test_widget_id_eq() {
393        let id1 = WidgetId::new(1);
394        let id2 = WidgetId::new(1);
395        let id3 = WidgetId::new(2);
396        assert_eq!(id1, id2);
397        assert_ne!(id1, id3);
398    }
399
400    #[test]
401    fn test_widget_id_hash() {
402        use std::collections::HashSet;
403        let mut set = HashSet::new();
404        set.insert(WidgetId::new(1));
405        set.insert(WidgetId::new(2));
406        assert_eq!(set.len(), 2);
407        assert!(set.contains(&WidgetId::new(1)));
408    }
409
410    #[test]
411    fn test_type_id() {
412        let id1 = TypeId::of::<u32>();
413        let id2 = TypeId::of::<u32>();
414        let id3 = TypeId::of::<String>();
415
416        assert_eq!(id1, id2);
417        assert_ne!(id1, id3);
418    }
419
420    #[test]
421    fn test_type_id_hash() {
422        use std::collections::HashSet;
423        let mut set = HashSet::new();
424        set.insert(TypeId::of::<u32>());
425        set.insert(TypeId::of::<String>());
426        assert_eq!(set.len(), 2);
427    }
428
429    #[test]
430    fn test_transform2d_identity() {
431        let t = Transform2D::IDENTITY;
432        assert_eq!(t.matrix, [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]);
433    }
434
435    #[test]
436    fn test_transform2d_default() {
437        let t = Transform2D::default();
438        assert_eq!(t.matrix, Transform2D::IDENTITY.matrix);
439    }
440
441    #[test]
442    fn test_transform2d_translate() {
443        let t = Transform2D::translate(10.0, 20.0);
444        assert_eq!(t.matrix[4], 10.0);
445        assert_eq!(t.matrix[5], 20.0);
446    }
447
448    #[test]
449    fn test_transform2d_scale() {
450        let t = Transform2D::scale(2.0, 3.0);
451        assert_eq!(t.matrix[0], 2.0);
452        assert_eq!(t.matrix[3], 3.0);
453    }
454
455    #[test]
456    fn test_transform2d_rotate() {
457        let t = Transform2D::rotate(std::f32::consts::PI / 2.0);
458        // 90 degrees rotation: cos = 0, sin = 1
459        assert!((t.matrix[0] - 0.0).abs() < 1e-6);
460        assert!((t.matrix[1] - 1.0).abs() < 1e-6);
461        assert!((t.matrix[2] - (-1.0)).abs() < 1e-6);
462        assert!((t.matrix[3] - 0.0).abs() < 1e-6);
463    }
464
465    #[test]
466    fn test_text_style_default() {
467        let style = TextStyle::default();
468        assert_eq!(style.size, 16.0);
469        assert_eq!(style.weight, FontWeight::Normal);
470        assert_eq!(style.style, FontStyle::Normal);
471        assert_eq!(style.color, crate::Color::BLACK);
472    }
473
474    #[test]
475    fn test_text_style_eq() {
476        let s1 = TextStyle::default();
477        let s2 = TextStyle::default();
478        assert_eq!(s1, s2);
479    }
480
481    #[test]
482    fn test_text_style_custom() {
483        let style = TextStyle {
484            size: 24.0,
485            color: crate::Color::RED,
486            weight: FontWeight::Bold,
487            style: FontStyle::Italic,
488        };
489        assert_eq!(style.size, 24.0);
490        assert_eq!(style.weight, FontWeight::Bold);
491        assert_eq!(style.style, FontStyle::Italic);
492    }
493
494    #[test]
495    fn test_font_weight_variants() {
496        let weights = [
497            FontWeight::Thin,
498            FontWeight::Light,
499            FontWeight::Normal,
500            FontWeight::Medium,
501            FontWeight::Semibold,
502            FontWeight::Bold,
503            FontWeight::Black,
504        ];
505        assert_eq!(weights.len(), 7);
506    }
507
508    #[test]
509    fn test_font_style_variants() {
510        assert_ne!(FontStyle::Normal, FontStyle::Italic);
511    }
512
513    #[test]
514    fn test_accessible_role_default() {
515        assert_eq!(AccessibleRole::default(), AccessibleRole::Generic);
516    }
517
518    #[test]
519    fn test_accessible_role_variants() {
520        let roles = [
521            AccessibleRole::Generic,
522            AccessibleRole::Button,
523            AccessibleRole::Checkbox,
524            AccessibleRole::TextInput,
525            AccessibleRole::Link,
526            AccessibleRole::Heading,
527            AccessibleRole::Image,
528            AccessibleRole::List,
529            AccessibleRole::ListItem,
530            AccessibleRole::Table,
531            AccessibleRole::TableRow,
532            AccessibleRole::TableCell,
533            AccessibleRole::Menu,
534            AccessibleRole::MenuItem,
535            AccessibleRole::ComboBox,
536            AccessibleRole::Slider,
537            AccessibleRole::ProgressBar,
538            AccessibleRole::Tab,
539            AccessibleRole::TabPanel,
540            AccessibleRole::RadioGroup,
541            AccessibleRole::Radio,
542        ];
543        assert_eq!(roles.len(), 21);
544    }
545
546    #[test]
547    fn test_layout_result_default() {
548        let result = LayoutResult::default();
549        assert_eq!(result.size, Size::new(0.0, 0.0));
550    }
551
552    #[test]
553    fn test_layout_result_with_size() {
554        let result = LayoutResult {
555            size: Size::new(100.0, 50.0),
556        };
557        assert_eq!(result.size.width, 100.0);
558        assert_eq!(result.size.height, 50.0);
559    }
560}