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