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}