Skip to main content

revue/widget/traits/
view.rs

1//! View and related traits for widgets
2
3use crate::dom::WidgetMeta;
4use crate::event::drag::{DragData, DropResult};
5use crate::event::{KeyEvent, MouseEvent};
6use crate::layout::Rect;
7
8use super::event::EventResult;
9use super::render_context::RenderContext;
10
11/// The core trait for all renderable components
12///
13/// Every widget in Revue implements the `View` trait, which provides:
14/// - Rendering via [`render()`][Self::render]
15/// - CSS selector support via [`id()`][Self::id], [`classes()`][Self::classes], and [`widget_type()`][Self::widget_type]
16/// - Child exposure for container widgets via [`children()`][Self::children]
17/// - Metadata generation via [`meta()`][Self::meta]
18///
19/// # Implementing View
20///
21/// At minimum, you only need to implement `render`:
22///
23/// ```ignore
24/// use revue::prelude::*;
25///
26/// struct MyWidget {
27///     text: String,
28/// }
29///
30/// impl View for MyWidget {
31///     fn render(&self, ctx: &mut RenderContext) {
32///         ctx.draw_text(0, 0, &self.text, Color::WHITE);
33///     }
34/// }
35/// ```
36///
37/// # CSS Selector Support
38///
39/// Widgets can be styled via CSS using three selector types:
40///
41/// 1. **Type selector** - All widgets of a given type
42///    ```css
43///    MyWidget { color: red; }
44///    ```
45///
46/// 2. **ID selector** - A specific widget (unique identifier)
47///    ```ignore
48///    widget.id("my-special-widget");
49///    ```
50///    ```css
51///    #my-special-widget { color: blue; }
52///    ```
53///
54/// 3. **Class selector** - Widgets with a specific class
55///    ```ignore
56///    widget.class("primary").class("active");
57///    ```
58///    ```css
59///    .primary { color: green; }
60///    .active { font-weight: bold; }
61///    ```
62///
63/// # Container Widgets
64///
65/// Container widgets (like `Stack`, `Grid`) should override [`children()`][Self::children]
66/// to expose their children for DOM traversal:
67///
68/// ```ignore
69/// impl View for MyContainer {
70///     fn children(&self) -> &[Box<dyn View>] {
71///         &self.children
72///     }
73///     // ...
74/// }
75/// ```
76///
77/// # View Trait Object
78///
79/// `View` can be used as a trait object (`Box<dyn View>`) for dynamic polymorphism:
80///
81/// ```ignore
82/// fn render_any_widget(widget: Box<dyn View>, ctx: &mut RenderContext) {
83///     widget.render(ctx);
84/// }
85/// ```
86pub trait View {
87    /// Render the view to the given context
88    ///
89    /// The `RenderContext` provides drawing primitives like:
90    /// - `draw_text()` - Render text at a position
91    /// - `fill()` - Fill a region with a character/color
92    /// - `draw_border()` - Draw borders
93    /// - `clear()` - Clear a region
94    ///
95    /// # Example
96    ///
97    /// ```ignore
98    /// fn render(&self, ctx: &mut RenderContext) {
99    ///     ctx.clear(ctx.area());
100    ///     ctx.draw_text(0, 0, &self.label, Color::WHITE);
101    /// }
102    /// ```
103    fn render(&self, ctx: &mut RenderContext);
104
105    /// Get widget type name (for CSS type selectors)
106    ///
107    /// The default implementation extracts the type name from the Rust type.
108    /// For example, `MyApp::MyWidget` becomes `"MyWidget"`.
109    ///
110    /// Override this if you want a custom type name for CSS matching:
111    ///
112    /// ```ignore
113    /// fn widget_type(&self) -> &'static str {
114    ///     "button"  // Always match as "button" regardless of Rust type
115    /// }
116    /// ```
117    fn widget_type(&self) -> &'static str {
118        std::any::type_name::<Self>()
119            .rsplit("::")
120            .next()
121            .unwrap_or("Unknown")
122    }
123
124    /// Get element ID (for CSS #id selectors)
125    ///
126    /// Returns `None` by default. Override to provide a unique ID:
127    ///
128    /// ```ignore
129    /// struct MyWidget {
130    ///     id: Option<String>,
131    /// }
132    ///
133    /// impl View for MyWidget {
134    ///     fn id(&self) -> Option<&str> {
135    ///         self.id.as_deref()
136    ///     }
137    /// }
138    /// ```
139    fn id(&self) -> Option<&str> {
140        None
141    }
142
143    /// Get CSS classes (for CSS .class selectors)
144    ///
145    /// Returns an empty slice by default. Override to provide classes:
146    ///
147    /// ```ignore
148    /// struct MyWidget {
149    ///     classes: Vec<String>,
150    /// }
151    ///
152    /// impl View for MyWidget {
153    ///     fn classes(&self) -> &[String] {
154    ///         &self.classes
155    ///     }
156    /// }
157    /// ```
158    fn classes(&self) -> &[String] {
159        &[]
160    }
161
162    /// Get child views (for container widgets)
163    ///
164    /// Container widgets (Stack, Grid, etc.) should override this to expose
165    /// their children, enabling the DOM builder to traverse the full widget tree.
166    ///
167    /// The returned slice should contain **boxed trait objects** to enable
168    /// heterogeneous child collections.
169    ///
170    /// # Example
171    ///
172    /// ```ignore
173    /// struct MyContainer {
174    ///     children: Vec<Box<dyn View>>,
175    /// }
176    ///
177    /// impl View for MyContainer {
178    ///     fn children(&self) -> &[Box<dyn View>] {
179    ///         &self.children
180    ///     }
181    /// }
182    /// ```
183    fn children(&self) -> &[Box<dyn View>] {
184        &[]
185    }
186
187    /// Get widget metadata for DOM
188    ///
189    /// This method combines `widget_type()`, `id()`, and `classes()` into
190    /// a `WidgetMeta` struct used by the DOM builder. You typically don't need
191    /// to override this.
192    fn meta(&self) -> WidgetMeta {
193        let mut meta = WidgetMeta::new(self.widget_type());
194        if let Some(id) = self.id() {
195            meta.id = Some(id.to_string());
196        }
197        for class in self.classes() {
198            meta.classes.insert(class.clone());
199        }
200        meta
201    }
202}
203
204/// Implement View for `Box<dyn View>` to allow boxed views to be used as children
205impl View for Box<dyn View> {
206    fn render(&self, ctx: &mut RenderContext) {
207        (**self).render(ctx);
208    }
209
210    fn widget_type(&self) -> &'static str {
211        (**self).widget_type()
212    }
213
214    fn id(&self) -> Option<&str> {
215        (**self).id()
216    }
217
218    fn classes(&self) -> &[String] {
219        (**self).classes()
220    }
221
222    fn children(&self) -> &[Box<dyn View>] {
223        (**self).children()
224    }
225
226    fn meta(&self) -> WidgetMeta {
227        (**self).meta()
228    }
229}
230
231/// Trait for interactive widgets that handle events
232///
233/// This trait extends [`View`] with keyboard and mouse handling capabilities.
234/// Widgets that need to respond to user input should implement this trait.
235///
236/// # Implementing Interactive
237///
238/// ```ignore
239/// use revue::prelude::*;
240///
241/// struct MyButton {
242///     label: String,
243///     focused: bool,
244/// }
245///
246/// impl View for MyButton {
247///     fn render(&self, ctx: &mut RenderContext) {
248///         // Render button with focus indication
249///     }
250/// }
251///
252/// impl Interactive for MyButton {
253///     fn handle_key(&mut self, event: &KeyEvent) -> EventResult {
254///         match event.key {
255///             Key::Enter => {
256///                 // Handle button click
257///                 EventResult::ConsumedAndRender
258///             }
259///             _ => EventResult::Ignored,
260///         }
261///     }
262///
263///     fn focusable(&self) -> bool {
264///         true
265///     }
266///
267///     fn on_focus(&mut self) {
268///         self.focused = true;
269///     }
270///
271///     fn on_blur(&mut self) {
272///         self.focused = false;
273///     }
274/// }
275/// ```
276///
277/// # Event Result Types
278///
279/// - `EventResult::Ignored` - Event not handled, propagate to parent
280/// - `EventResult::Consumed` - Event handled, no redraw needed
281/// - `EventResult::ConsumedAndRender` - Event handled, redraw needed
282pub trait Interactive: View {
283    /// Handle keyboard event
284    ///
285    /// Returns `EventResult::Consumed` or `EventResult::ConsumedAndRender` if
286    /// the event was handled, `EventResult::Ignored` to let it propagate.
287    ///
288    /// # Example
289    ///
290    /// ```ignore
291    /// fn handle_key(&mut self, event: &KeyEvent) -> EventResult {
292    ///     match event.key {
293    ///         Key::Char('q') => EventResult::Consumed,
294    ///         Key::Enter => EventResult::ConsumedAndRender,
295    ///         _ => EventResult::Ignored,
296    ///     }
297    /// }
298    /// ```
299    fn handle_key(&mut self, event: &KeyEvent) -> EventResult {
300        let _ = event;
301        EventResult::Ignored
302    }
303
304    /// Handle mouse event
305    ///
306    /// Returns `EventResult` indicating if event was handled.
307    /// The `area` parameter is the widget's layout bounds.
308    ///
309    /// # Example
310    ///
311    /// ```ignore
312    /// fn handle_mouse(&mut self, event: &MouseEvent, area: Rect) -> EventResult {
313    ///     if !area.contains(event.x, event.y) {
314    ///         return EventResult::Ignored;
315    ///     }
316    ///     match event.kind {
317    ///         MouseEventKind::Down(_) => EventResult::ConsumedAndRender,
318    ///         _ => EventResult::Ignored,
319    ///     }
320    /// }
321    /// ```
322    fn handle_mouse(&mut self, event: &MouseEvent, area: Rect) -> EventResult {
323        let _ = (event, area);
324        EventResult::Ignored
325    }
326
327    /// Check if the widget can receive focus
328    ///
329    /// Return `false` for widgets that shouldn't be focusable (e.g., labels).
330    fn focusable(&self) -> bool {
331        true
332    }
333
334    /// Called when the widget receives focus
335    ///
336    /// Use this to update visual state or perform setup.
337    fn on_focus(&mut self) {}
338
339    /// Called when the widget loses focus
340    ///
341    /// Use this to clean up or update visual state.
342    fn on_blur(&mut self) {}
343}
344
345/// Trait for widgets that support drag-and-drop
346///
347/// This trait enables widgets to participate in drag-and-drop operations.
348/// Widgets can be drag sources, drop targets, or both.
349///
350/// # Implementing Draggable
351///
352/// ## As a Drag Source
353///
354/// ```ignore
355/// impl Draggable for MyWidget {
356///     fn can_drag(&self) -> bool {
357///         !self.is_empty
358///     }
359///
360///     fn drag_data(&self) -> Option<DragData> {
361///         Some(DragData::text(self.content.clone()))
362///     }
363///
364///     fn drag_preview(&self) -> Option<String> {
365///         Some(format!("Move: {}", self.label))
366///     }
367/// }
368/// ```
369///
370/// ## As a Drop Target
371///
372/// ```ignore
373/// impl Draggable for MyDropZone {
374///     fn can_drop(&self) -> bool {
375///         true
376///     }
377///
378///     fn accepted_types(&self) -> &[&str] {
379///         &["text", "file"]
380///     }
381///
382///     fn on_drop(&mut self, data: DragData) -> bool {
383///         match data.type_id {
384///             "text" => {
385///                 self.handle_text_drop(data.content);
386///                 true
387///             }
388///             _ => false,
389///         }
390///     }
391/// }
392/// ```
393pub trait Draggable: View {
394    /// Check if this widget can be dragged
395    ///
396    /// Return `true` to allow drag operations on this widget.
397    fn can_drag(&self) -> bool {
398        false
399    }
400
401    /// Get the drag data when a drag starts
402    ///
403    /// Return `None` to cancel the drag.
404    fn drag_data(&self) -> Option<DragData> {
405        None
406    }
407
408    /// Get a text preview for the drag operation
409    ///
410    /// This is shown near the cursor during drag.
411    fn drag_preview(&self) -> Option<String> {
412        None
413    }
414
415    /// Called when drag starts
416    fn on_drag_start(&mut self) {}
417
418    /// Called when drag ends (regardless of outcome)
419    fn on_drag_end(&mut self, _result: DropResult) {}
420
421    /// Check if this widget accepts drops
422    fn can_drop(&self) -> bool {
423        false
424    }
425
426    /// Get the types this widget accepts for drops
427    ///
428    /// Return an empty slice to accept all types.
429    fn accepted_types(&self) -> &[&'static str] {
430        &[]
431    }
432
433    /// Check if this widget can accept specific drag data
434    fn can_accept(&self, data: &DragData) -> bool {
435        let types = self.accepted_types();
436        types.is_empty() || types.contains(&data.type_id)
437    }
438
439    /// Called when a drag enters this widget's bounds
440    fn on_drag_enter(&mut self, _data: &DragData) {}
441
442    /// Called when a drag leaves this widget's bounds
443    fn on_drag_leave(&mut self) {}
444
445    /// Called when a drop occurs on this widget
446    ///
447    /// Return `true` if the drop was accepted, `false` to reject.
448    fn on_drop(&mut self, _data: DragData) -> bool {
449        false
450    }
451
452    /// Get the drop zone bounds for this widget
453    ///
454    /// Override this if the drop zone differs from the render area.
455    fn drop_bounds(&self, area: Rect) -> Rect {
456        area
457    }
458}
459
460/// Extended View trait with styling support
461///
462/// This trait provides runtime mutable access to CSS styling properties.
463/// Unlike the base `View` trait which returns immutable references,
464/// `StyledView` allows modifying IDs and classes after widget creation.
465///
466/// # Example
467///
468/// ```ignore
469/// struct MyWidget {
470///     id: String,
471///     classes: Vec<String>,
472/// }
473///
474/// impl StyledView for MyWidget {
475///     fn set_id(&mut self, id: impl Into<String>) {
476///         self.id = id.into();
477///     }
478///
479///     fn add_class(&mut self, class: impl Into<String>) {
480///         self.classes.push(class.into());
481///     }
482///
483///     fn remove_class(&mut self, class: &str) {
484///         self.classes.retain(|c| c != class);
485///     }
486///
487///     fn toggle_class(&mut self, class: &str) {
488///         if self.has_class(class) {
489///             self.remove_class(class);
490///         } else {
491///             self.add_class(class);
492///         }
493///     }
494///
495///     fn has_class(&self, class: &str) -> bool {
496///         self.classes.iter().any(|c| c == class)
497///     }
498/// }
499/// ```
500pub trait StyledView: View {
501    /// Set element ID
502    fn set_id(&mut self, id: impl Into<String>);
503
504    /// Add a CSS class
505    fn add_class(&mut self, class: impl Into<String>);
506
507    /// Remove a CSS class
508    fn remove_class(&mut self, class: &str);
509
510    /// Toggle a CSS class
511    fn toggle_class(&mut self, class: &str);
512
513    /// Check if has class
514    fn has_class(&self, class: &str) -> bool;
515}