1mod element;
4mod event;
5mod render_context;
6mod symbols;
7mod timeout;
8mod view;
9mod widget_state;
10
11pub 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#[macro_export]
45macro_rules! impl_state_builders {
46 ($widget:ty) => {
47 impl $widget {
48 pub fn focused(mut self, focused: bool) -> Self {
50 self.state.focused = focused;
51 self
52 }
53
54 pub fn disabled(mut self, disabled: bool) -> Self {
56 self.state.disabled = disabled;
57 self
58 }
59
60 pub fn fg(mut self, color: $crate::style::Color) -> Self {
62 self.state.fg = Some(color);
63 self
64 }
65
66 pub fn bg(mut self, color: $crate::style::Color) -> Self {
68 self.state.bg = Some(color);
69 self
70 }
71
72 pub fn is_focused(&self) -> bool {
74 self.state.focused
75 }
76
77 pub fn is_disabled(&self) -> bool {
79 self.state.disabled
80 }
81
82 pub fn set_focused(&mut self, focused: bool) {
84 self.state.focused = focused;
85 }
86 }
87 };
88}
89
90#[macro_export]
106macro_rules! impl_props_builders {
107 ($widget:ty) => {
108 impl $widget {
109 pub fn element_id(mut self, id: impl Into<String>) -> Self {
111 self.props.id = Some(id.into());
112 self
113 }
114
115 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 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#[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#[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#[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 #[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}