Skip to main content

revue/widget/traits/
mod.rs

1//! Widget traits and common types
2
3mod element;
4mod event;
5mod render_context;
6mod symbols;
7mod timeout;
8mod view;
9mod widget_state;
10
11// Re-export all public types
12pub use element::Element;
13pub use event::{EventResult, FocusStyle};
14pub use render_context::{ProgressBarConfig, RenderContext};
15pub use symbols::Symbols;
16pub use timeout::Timeout;
17pub use view::{Draggable, Interactive, StyledView, View};
18pub use widget_state::{WidgetProps, WidgetState, DISABLED_BG, DISABLED_FG};
19
20// =============================================================================
21// Builder Macros
22// =============================================================================
23
24/// Generate builder methods for widgets with `state: WidgetState` field.
25///
26/// This macro generates the following methods:
27/// - `focused(self, bool) -> Self` - Set focused state
28/// - `disabled(self, bool) -> Self` - Set disabled state
29/// - `fg(self, Color) -> Self` - Set foreground color
30/// - `bg(self, Color) -> Self` - Set background color
31/// - `is_focused(&self) -> bool` - Check if focused
32/// - `is_disabled(&self) -> bool` - Check if disabled
33/// - `set_focused(&mut self, bool)` - Mutably set focused state
34///
35/// # Example
36/// ```rust,ignore
37/// struct MyWidget {
38///     state: WidgetState,
39///     props: WidgetProps,
40/// }
41///
42/// impl_state_builders!(MyWidget);
43/// ```
44#[macro_export]
45macro_rules! impl_state_builders {
46    ($widget:ty) => {
47        impl $widget {
48            /// Set focused state
49            pub fn focused(mut self, focused: bool) -> Self {
50                self.state.focused = focused;
51                self
52            }
53
54            /// Set disabled state
55            pub fn disabled(mut self, disabled: bool) -> Self {
56                self.state.disabled = disabled;
57                self
58            }
59
60            /// Set foreground color
61            pub fn fg(mut self, color: $crate::style::Color) -> Self {
62                self.state.fg = Some(color);
63                self
64            }
65
66            /// Set background color
67            pub fn bg(mut self, color: $crate::style::Color) -> Self {
68                self.state.bg = Some(color);
69                self
70            }
71
72            /// Check if widget is focused
73            pub fn is_focused(&self) -> bool {
74                self.state.focused
75            }
76
77            /// Check if widget is disabled
78            pub fn is_disabled(&self) -> bool {
79                self.state.disabled
80            }
81
82            /// Set focused state (mutable)
83            pub fn set_focused(&mut self, focused: bool) {
84                self.state.focused = focused;
85            }
86        }
87    };
88}
89
90/// Generate builder methods for widgets with `props: WidgetProps` field.
91///
92/// This macro generates the following methods:
93/// - `element_id(self, impl Into<String>) -> Self` - Set CSS element ID
94/// - `class(self, impl Into<String>) -> Self` - Add a CSS class
95/// - `classes(self, IntoIterator<Item=S>) -> Self` - Add multiple CSS classes
96///
97/// # Example
98/// ```rust,ignore
99/// struct MyWidget {
100///     props: WidgetProps,
101/// }
102///
103/// impl_props_builders!(MyWidget);
104/// ```
105#[macro_export]
106macro_rules! impl_props_builders {
107    ($widget:ty) => {
108        impl $widget {
109            /// Set element ID for CSS selector (#id)
110            pub fn element_id(mut self, id: impl Into<String>) -> Self {
111                self.props.id = Some(id.into());
112                self
113            }
114
115            /// Add a CSS class
116            pub fn class(mut self, class: impl Into<String>) -> Self {
117                let class_str = class.into();
118                if !self.props.classes.contains(&class_str) {
119                    self.props.classes.push(class_str);
120                }
121                self
122            }
123
124            /// Add multiple CSS classes
125            pub fn classes<I, S>(mut self, classes: I) -> Self
126            where
127                I: IntoIterator<Item = S>,
128                S: Into<String>,
129            {
130                for class in classes {
131                    let class_str = class.into();
132                    if !self.props.classes.contains(&class_str) {
133                        self.props.classes.push(class_str);
134                    }
135                }
136                self
137            }
138        }
139    };
140}
141
142/// Generate all common builder methods for widgets with both `state: WidgetState`
143/// and `props: WidgetProps` fields.
144///
145/// This is a convenience macro that combines `impl_state_builders!` and
146/// `impl_props_builders!`.
147///
148/// Generated methods:
149/// - State: `focused`, `disabled`, `fg`, `bg`, `is_focused`, `is_disabled`, `set_focused`
150/// - Props: `element_id`, `class`, `classes`
151///
152/// # Example
153/// ```rust,ignore
154/// struct MyWidget {
155///     label: String,
156///     state: WidgetState,
157///     props: WidgetProps,
158/// }
159///
160/// impl MyWidget {
161///     pub fn new(label: impl Into<String>) -> Self {
162///         Self {
163///             label: label.into(),
164///             state: WidgetState::new(),
165///             props: WidgetProps::new(),
166///         }
167///     }
168/// }
169///
170/// // Generates: focused, disabled, fg, bg, is_focused, is_disabled,
171/// //            set_focused, element_id, class, classes
172/// impl_widget_builders!(MyWidget);
173/// ```
174#[macro_export]
175macro_rules! impl_widget_builders {
176    ($widget:ty) => {
177        $crate::impl_state_builders!($widget);
178        $crate::impl_props_builders!($widget);
179    };
180}
181
182/// Generate View trait id(), classes(), and meta() methods for widgets with props.
183///
184/// This macro generates the id(), classes(), and meta() methods for the View trait
185/// that delegate to WidgetProps.
186///
187/// # Example
188/// ```rust,ignore
189/// impl View for MyWidget {
190///     fn render(&self, ctx: &mut RenderContext) {
191///         // ... rendering logic
192///     }
193///
194///     crate::impl_view_meta!("MyWidget");
195/// }
196/// ```
197#[macro_export]
198macro_rules! impl_view_meta {
199    ($name:expr) => {
200        fn id(&self) -> Option<&str> {
201            self.props.id.as_deref()
202        }
203
204        fn classes(&self) -> &[String] {
205            &self.props.classes
206        }
207
208        fn meta(&self) -> $crate::dom::WidgetMeta {
209            let mut meta = $crate::dom::WidgetMeta::new($name);
210            if let Some(ref id) = self.props.id {
211                meta.id = Some(id.clone());
212            }
213            for class in &self.props.classes {
214                meta.classes.insert(class.clone());
215            }
216            meta
217        }
218    };
219}
220
221/// Generate View trait implementation for StyledView widgets.
222///
223/// This macro generates View trait methods that delegate to WidgetProps
224/// for id() and classes() methods.
225///
226/// # Example
227/// ```rust,ignore
228/// struct MyWidget {
229///     props: WidgetProps,
230/// }
231///
232/// impl View for MyWidget {
233///     fn render(&self, ctx: &mut RenderContext) {
234///         // ... rendering logic
235///     }
236/// }
237///
238/// impl_styled_view!(MyWidget);
239/// ```
240#[macro_export]
241macro_rules! impl_styled_view {
242    ($widget:ty) => {
243        impl $crate::widget::traits::StyledView for $widget {
244            fn set_id(&mut self, id: impl Into<String>) {
245                self.props.id = Some(id.into());
246            }
247
248            fn add_class(&mut self, class: impl Into<String>) {
249                let class_str = class.into();
250                if !self.props.classes.contains(&class_str) {
251                    self.props.classes.push(class_str);
252                }
253            }
254
255            fn remove_class(&mut self, class: &str) {
256                self.props.classes.retain(|c| c != class);
257            }
258
259            fn toggle_class(&mut self, class: &str) {
260                if self.props.classes.contains(&class.to_string()) {
261                    self.remove_class(class);
262                } else {
263                    self.add_class(class);
264                }
265            }
266
267            fn has_class(&self, class: &str) -> bool {
268                self.props.classes.contains(&class.to_string())
269            }
270        }
271    };
272}
273
274#[cfg(test)]
275mod tests {
276    use super::*;
277    use crate::layout::Rect;
278    use crate::render::Buffer;
279    use crate::style::Color;
280
281    #[test]
282    fn test_event_result_default() {
283        let result = EventResult::default();
284        assert!(!result.is_consumed());
285        assert!(!result.needs_render());
286    }
287
288    #[test]
289    fn test_event_result_consumed() {
290        let consumed = EventResult::Consumed;
291        assert!(consumed.is_consumed());
292        assert!(!consumed.needs_render());
293    }
294
295    #[test]
296    fn test_event_result_consumed_and_render() {
297        let result = EventResult::ConsumedAndRender;
298        assert!(result.is_consumed());
299        assert!(result.needs_render());
300    }
301
302    #[test]
303    fn test_event_result_from_bool() {
304        let handled: EventResult = true.into();
305        assert_eq!(handled, EventResult::ConsumedAndRender);
306
307        let ignored: EventResult = false.into();
308        assert_eq!(ignored, EventResult::Ignored);
309    }
310
311    #[test]
312    fn test_event_result_or() {
313        assert_eq!(
314            EventResult::Ignored.or(EventResult::ConsumedAndRender),
315            EventResult::ConsumedAndRender
316        );
317        assert_eq!(
318            EventResult::ConsumedAndRender.or(EventResult::Ignored),
319            EventResult::ConsumedAndRender
320        );
321        assert_eq!(
322            EventResult::Ignored.or(EventResult::Consumed),
323            EventResult::Consumed
324        );
325        assert_eq!(
326            EventResult::Ignored.or(EventResult::Ignored),
327            EventResult::Ignored
328        );
329    }
330
331    #[test]
332    fn test_widget_state_new() {
333        let state = WidgetState::new();
334        assert!(!state.is_focused());
335        assert!(!state.is_disabled());
336        assert!(!state.is_pressed());
337        assert!(!state.is_hovered());
338        assert!(!state.is_interactive());
339    }
340
341    #[test]
342    fn test_widget_state_builder() {
343        let state = WidgetState::new()
344            .focused(true)
345            .disabled(false)
346            .fg(Color::RED)
347            .bg(Color::BLUE);
348
349        assert!(state.is_focused());
350        assert!(!state.is_disabled());
351        assert_eq!(state.fg, Some(Color::RED));
352        assert_eq!(state.bg, Some(Color::BLUE));
353    }
354
355    #[test]
356    fn test_widget_state_effective_colors() {
357        let default_color = Color::rgb(128, 128, 128);
358
359        let normal = WidgetState::new().fg(Color::WHITE);
360        assert_eq!(normal.effective_fg(default_color), Color::WHITE);
361
362        let disabled = WidgetState::new().fg(Color::WHITE).disabled(true);
363        assert_eq!(disabled.effective_fg(default_color), DISABLED_FG);
364    }
365
366    #[test]
367    fn test_widget_state_reset_transient() {
368        let mut state = WidgetState::new()
369            .focused(true)
370            .disabled(false)
371            .pressed(true)
372            .hovered(true);
373
374        state.reset_transient();
375
376        assert!(state.focused);
377        assert!(!state.disabled);
378        assert!(!state.pressed);
379        assert!(!state.hovered);
380    }
381
382    #[test]
383    fn test_widget_classes_exposure() {
384        use crate::widget::Text;
385
386        let widget = Text::new("Test").class("btn").class("primary");
387
388        let classes = View::classes(&widget);
389        assert_eq!(classes.len(), 2);
390        assert!(classes.contains(&"btn".to_string()));
391        assert!(classes.contains(&"primary".to_string()));
392
393        let meta = widget.meta();
394        assert!(meta.classes.contains("btn"));
395        assert!(meta.classes.contains("primary"));
396    }
397
398    // Wide character tests
399    #[test]
400    fn test_draw_text_wide_chars() {
401        let mut buf = Buffer::new(20, 1);
402        let area = Rect::new(0, 0, 20, 1);
403        let mut ctx = RenderContext::new(&mut buf, area);
404
405        ctx.draw_text(0, 0, "한글", Color::WHITE);
406
407        assert_eq!(buf.get(0, 0).unwrap().symbol, '한');
408        assert!(buf.get(1, 0).unwrap().is_continuation());
409        assert_eq!(buf.get(2, 0).unwrap().symbol, '글');
410        assert!(buf.get(3, 0).unwrap().is_continuation());
411        assert_eq!(buf.get(4, 0).unwrap().symbol, ' ');
412    }
413
414    #[test]
415    fn test_draw_text_mixed_width() {
416        let mut buf = Buffer::new(20, 1);
417        let area = Rect::new(0, 0, 20, 1);
418        let mut ctx = RenderContext::new(&mut buf, area);
419
420        ctx.draw_text(0, 0, "A한B", Color::WHITE);
421
422        assert_eq!(buf.get(0, 0).unwrap().symbol, 'A');
423        assert_eq!(buf.get(1, 0).unwrap().symbol, '한');
424        assert!(buf.get(2, 0).unwrap().is_continuation());
425        assert_eq!(buf.get(3, 0).unwrap().symbol, 'B');
426    }
427
428    #[test]
429    fn test_draw_text_centered_wide_chars() {
430        let mut buf = Buffer::new(10, 1);
431        let area = Rect::new(0, 0, 10, 1);
432        let mut ctx = RenderContext::new(&mut buf, area);
433
434        ctx.draw_text_centered(0, 0, 10, "한글", Color::WHITE);
435
436        assert_eq!(buf.get(3, 0).unwrap().symbol, '한');
437        assert!(buf.get(4, 0).unwrap().is_continuation());
438        assert_eq!(buf.get(5, 0).unwrap().symbol, '글');
439        assert!(buf.get(6, 0).unwrap().is_continuation());
440    }
441
442    #[test]
443    fn test_draw_text_right_wide_chars() {
444        let mut buf = Buffer::new(10, 1);
445        let area = Rect::new(0, 0, 10, 1);
446        let mut ctx = RenderContext::new(&mut buf, area);
447
448        ctx.draw_text_right(0, 0, 10, "한글", Color::WHITE);
449
450        assert_eq!(buf.get(6, 0).unwrap().symbol, '한');
451        assert!(buf.get(7, 0).unwrap().is_continuation());
452        assert_eq!(buf.get(8, 0).unwrap().symbol, '글');
453        assert!(buf.get(9, 0).unwrap().is_continuation());
454    }
455}