1use presentar_core::{
7 Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Color, Constraints, Event,
8 LayoutResult, Point, Rect, Size, TextStyle, TypeId, Widget,
9};
10use std::any::Any;
11use std::time::Duration;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
15pub enum ScrollOrientation {
16 #[default]
18 Vertical,
19 Horizontal,
21}
22
23#[derive(Debug, Clone)]
25pub struct ScrollbarChars {
26 pub track: char,
28 pub thumb: char,
30 pub arrow_start: char,
32 pub arrow_end: char,
34}
35
36impl Default for ScrollbarChars {
37 fn default() -> Self {
38 Self::unicode()
39 }
40}
41
42impl ScrollbarChars {
43 #[must_use]
45 pub fn unicode() -> Self {
46 Self {
47 track: '░',
48 thumb: '█',
49 arrow_start: '▲',
50 arrow_end: '▼',
51 }
52 }
53
54 #[must_use]
56 pub fn unicode_horizontal() -> Self {
57 Self {
58 track: '░',
59 thumb: '█',
60 arrow_start: '◀',
61 arrow_end: '▶',
62 }
63 }
64
65 #[must_use]
67 pub fn ascii() -> Self {
68 Self {
69 track: '-',
70 thumb: '#',
71 arrow_start: '^',
72 arrow_end: 'v',
73 }
74 }
75
76 #[must_use]
78 pub fn minimal() -> Self {
79 Self {
80 track: '│',
81 thumb: '┃',
82 arrow_start: '│',
83 arrow_end: '│',
84 }
85 }
86}
87
88#[derive(Debug, Clone)]
90pub struct Scrollbar {
91 orientation: ScrollOrientation,
93 content_length: usize,
95 viewport_length: usize,
97 offset: usize,
99 show_arrows: bool,
101 chars: ScrollbarChars,
103 track_color: Color,
105 thumb_color: Color,
107 arrow_color: Color,
109 bounds: Rect,
111}
112
113impl Default for Scrollbar {
114 fn default() -> Self {
115 Self::vertical(100, 10)
116 }
117}
118
119impl Scrollbar {
120 #[must_use]
122 pub fn vertical(content_length: usize, viewport_length: usize) -> Self {
123 Self {
124 orientation: ScrollOrientation::Vertical,
125 content_length,
126 viewport_length,
127 offset: 0,
128 show_arrows: true,
129 chars: ScrollbarChars::unicode(),
130 track_color: Color::new(0.3, 0.3, 0.3, 1.0),
131 thumb_color: Color::new(0.7, 0.7, 0.7, 1.0),
132 arrow_color: Color::new(0.5, 0.5, 0.5, 1.0),
133 bounds: Rect::default(),
134 }
135 }
136
137 #[must_use]
139 pub fn horizontal(content_length: usize, viewport_length: usize) -> Self {
140 Self {
141 orientation: ScrollOrientation::Horizontal,
142 content_length,
143 viewport_length,
144 offset: 0,
145 show_arrows: true,
146 chars: ScrollbarChars::unicode_horizontal(),
147 track_color: Color::new(0.3, 0.3, 0.3, 1.0),
148 thumb_color: Color::new(0.7, 0.7, 0.7, 1.0),
149 arrow_color: Color::new(0.5, 0.5, 0.5, 1.0),
150 bounds: Rect::default(),
151 }
152 }
153
154 #[must_use]
156 pub fn with_arrows(mut self, show: bool) -> Self {
157 self.show_arrows = show;
158 self
159 }
160
161 #[must_use]
163 pub fn with_chars(mut self, chars: ScrollbarChars) -> Self {
164 self.chars = chars;
165 self
166 }
167
168 #[must_use]
170 pub fn with_track_color(mut self, color: Color) -> Self {
171 self.track_color = color;
172 self
173 }
174
175 #[must_use]
177 pub fn with_thumb_color(mut self, color: Color) -> Self {
178 self.thumb_color = color;
179 self
180 }
181
182 #[must_use]
184 pub fn with_arrow_color(mut self, color: Color) -> Self {
185 self.arrow_color = color;
186 self
187 }
188
189 #[must_use]
191 pub fn offset(&self) -> usize {
192 self.offset
193 }
194
195 pub fn set_offset(&mut self, offset: usize) {
197 self.offset = offset.min(self.max_offset());
198 }
199
200 #[must_use]
202 pub fn position(&self) -> f64 {
203 let max = self.max_offset();
204 if max == 0 {
205 0.0
206 } else {
207 self.offset as f64 / max as f64
208 }
209 }
210
211 #[must_use]
213 pub fn thumb_size(&self) -> f64 {
214 if self.content_length == 0 {
215 1.0
216 } else {
217 (self.viewport_length as f64 / self.content_length as f64).min(1.0)
218 }
219 }
220
221 #[must_use]
223 pub fn max_offset(&self) -> usize {
224 self.content_length.saturating_sub(self.viewport_length)
225 }
226
227 #[must_use]
229 pub fn is_scrollable(&self) -> bool {
230 self.content_length > self.viewport_length
231 }
232
233 pub fn scroll(&mut self, delta: i32) {
235 if delta >= 0 {
236 self.offset = (self.offset + delta as usize).min(self.max_offset());
237 } else {
238 self.offset = self.offset.saturating_sub((-delta) as usize);
239 }
240 }
241
242 pub fn scroll_start(&mut self) {
244 self.scroll(-1);
245 }
246
247 pub fn scroll_end(&mut self) {
249 self.scroll(1);
250 }
251
252 pub fn page_start(&mut self) {
254 let page = self.viewport_length.max(1);
255 self.offset = self.offset.saturating_sub(page);
256 }
257
258 pub fn page_end(&mut self) {
260 let page = self.viewport_length.max(1);
261 self.offset = (self.offset + page).min(self.max_offset());
262 }
263
264 pub fn jump_to(&mut self, position: f64) {
266 let pos = position.clamp(0.0, 1.0);
267 self.offset = (pos * self.max_offset() as f64).round() as usize;
268 }
269
270 pub fn jump_start(&mut self) {
272 self.offset = 0;
273 }
274
275 pub fn jump_end(&mut self) {
277 self.offset = self.max_offset();
278 }
279
280 pub fn update_lengths(&mut self, content_length: usize, viewport_length: usize) {
282 self.content_length = content_length;
283 self.viewport_length = viewport_length;
284 self.offset = self.offset.min(self.max_offset());
285 }
286
287 #[must_use]
289 pub fn orientation(&self) -> ScrollOrientation {
290 self.orientation
291 }
292
293 #[must_use]
295 pub fn content_length(&self) -> usize {
296 self.content_length
297 }
298
299 #[must_use]
301 pub fn viewport_length(&self) -> usize {
302 self.viewport_length
303 }
304}
305
306impl Brick for Scrollbar {
307 fn brick_name(&self) -> &'static str {
308 "scrollbar"
309 }
310
311 fn assertions(&self) -> &[BrickAssertion] {
312 static ASSERTIONS: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(16)];
313 ASSERTIONS
314 }
315
316 fn budget(&self) -> BrickBudget {
317 BrickBudget::uniform(8)
318 }
319
320 fn verify(&self) -> BrickVerification {
321 BrickVerification {
322 passed: self.assertions().to_vec(),
323 failed: vec![],
324 verification_time: Duration::from_micros(5),
325 }
326 }
327
328 fn to_html(&self) -> String {
329 String::new()
330 }
331
332 fn to_css(&self) -> String {
333 String::new()
334 }
335}
336
337impl Widget for Scrollbar {
338 fn type_id(&self) -> TypeId {
339 TypeId::of::<Self>()
340 }
341
342 fn measure(&self, constraints: Constraints) -> Size {
343 match self.orientation {
344 ScrollOrientation::Vertical => Size::new(1.0, constraints.max_height.clamp(3.0, 20.0)),
345 ScrollOrientation::Horizontal => Size::new(constraints.max_width.clamp(3.0, 20.0), 1.0),
346 }
347 }
348
349 fn layout(&mut self, bounds: Rect) -> LayoutResult {
350 self.bounds = bounds;
351 LayoutResult {
352 size: Size::new(bounds.width, bounds.height),
353 }
354 }
355
356 fn paint(&self, canvas: &mut dyn Canvas) {
357 let track_style = TextStyle {
358 color: self.track_color,
359 ..Default::default()
360 };
361 let thumb_style = TextStyle {
362 color: self.thumb_color,
363 ..Default::default()
364 };
365 let arrow_style = TextStyle {
366 color: self.arrow_color,
367 ..Default::default()
368 };
369
370 match self.orientation {
371 ScrollOrientation::Vertical => {
372 self.paint_vertical(canvas, &track_style, &thumb_style, &arrow_style);
373 }
374 ScrollOrientation::Horizontal => {
375 self.paint_horizontal(canvas, &track_style, &thumb_style, &arrow_style);
376 }
377 }
378 }
379
380 fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
381 None
382 }
383
384 fn children(&self) -> &[Box<dyn Widget>] {
385 &[]
386 }
387
388 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
389 &mut []
390 }
391}
392
393impl Scrollbar {
394 fn paint_vertical(
395 &self,
396 canvas: &mut dyn Canvas,
397 track_style: &TextStyle,
398 thumb_style: &TextStyle,
399 arrow_style: &TextStyle,
400 ) {
401 let height = self.bounds.height as usize;
402 if height < 3 {
403 return;
404 }
405
406 let arrow_offset = usize::from(self.show_arrows);
407 let track_start = arrow_offset;
408 let track_end = height.saturating_sub(arrow_offset);
409 let track_len = track_end.saturating_sub(track_start);
410
411 if track_len == 0 {
412 return;
413 }
414
415 if self.show_arrows {
417 canvas.draw_text(
418 &self.chars.arrow_start.to_string(),
419 Point::new(self.bounds.x, self.bounds.y),
420 arrow_style,
421 );
422 canvas.draw_text(
423 &self.chars.arrow_end.to_string(),
424 Point::new(self.bounds.x, self.bounds.y + (height - 1) as f32),
425 arrow_style,
426 );
427 }
428
429 let thumb_size = ((self.thumb_size() * track_len as f64).round() as usize).max(1);
431 let thumb_pos = if self.is_scrollable() {
432 (self.position() * (track_len.saturating_sub(thumb_size)) as f64).round() as usize
433 } else {
434 0
435 };
436
437 for i in 0..track_len {
439 let y = track_start + i;
440 let in_thumb = i >= thumb_pos && i < thumb_pos + thumb_size;
441 let ch = if in_thumb {
442 self.chars.thumb
443 } else {
444 self.chars.track
445 };
446 let style = if in_thumb { thumb_style } else { track_style };
447 canvas.draw_text(
448 &ch.to_string(),
449 Point::new(self.bounds.x, self.bounds.y + y as f32),
450 style,
451 );
452 }
453 }
454
455 fn paint_horizontal(
456 &self,
457 canvas: &mut dyn Canvas,
458 track_style: &TextStyle,
459 thumb_style: &TextStyle,
460 arrow_style: &TextStyle,
461 ) {
462 let width = self.bounds.width as usize;
463 if width < 3 {
464 return;
465 }
466
467 let arrow_offset = usize::from(self.show_arrows);
468 let track_start = arrow_offset;
469 let track_end = width.saturating_sub(arrow_offset);
470 let track_len = track_end.saturating_sub(track_start);
471
472 if track_len == 0 {
473 return;
474 }
475
476 if self.show_arrows {
478 canvas.draw_text(
479 &self.chars.arrow_start.to_string(),
480 Point::new(self.bounds.x, self.bounds.y),
481 arrow_style,
482 );
483 canvas.draw_text(
484 &self.chars.arrow_end.to_string(),
485 Point::new(self.bounds.x + (width - 1) as f32, self.bounds.y),
486 arrow_style,
487 );
488 }
489
490 let thumb_size = ((self.thumb_size() * track_len as f64).round() as usize).max(1);
492 let thumb_pos = if self.is_scrollable() {
493 (self.position() * (track_len.saturating_sub(thumb_size)) as f64).round() as usize
494 } else {
495 0
496 };
497
498 for i in 0..track_len {
500 let x = track_start + i;
501 let in_thumb = i >= thumb_pos && i < thumb_pos + thumb_size;
502 let ch = if in_thumb {
503 self.chars.thumb
504 } else {
505 self.chars.track
506 };
507 let style = if in_thumb { thumb_style } else { track_style };
508 canvas.draw_text(
509 &ch.to_string(),
510 Point::new(self.bounds.x + x as f32, self.bounds.y),
511 style,
512 );
513 }
514 }
515}
516
517impl PartialEq for ScrollbarChars {
518 fn eq(&self, other: &Self) -> bool {
519 self.track == other.track
520 && self.thumb == other.thumb
521 && self.arrow_start == other.arrow_start
522 && self.arrow_end == other.arrow_end
523 }
524}
525
526#[cfg(test)]
527mod tests {
528 use super::*;
529
530 struct MockCanvas {
531 texts: Vec<(String, Point)>,
532 }
533
534 impl MockCanvas {
535 fn new() -> Self {
536 Self { texts: vec![] }
537 }
538 }
539
540 impl Canvas for MockCanvas {
541 fn fill_rect(&mut self, _rect: Rect, _color: Color) {}
542 fn stroke_rect(&mut self, _rect: Rect, _color: Color, _width: f32) {}
543 fn draw_text(&mut self, text: &str, position: Point, _style: &TextStyle) {
544 self.texts.push((text.to_string(), position));
545 }
546 fn draw_line(&mut self, _from: Point, _to: Point, _color: Color, _width: f32) {}
547 fn fill_circle(&mut self, _center: Point, _radius: f32, _color: Color) {}
548 fn stroke_circle(&mut self, _center: Point, _radius: f32, _color: Color, _width: f32) {}
549 fn fill_arc(&mut self, _c: Point, _r: f32, _s: f32, _e: f32, _color: Color) {}
550 fn draw_path(&mut self, _points: &[Point], _color: Color, _width: f32) {}
551 fn fill_polygon(&mut self, _points: &[Point], _color: Color) {}
552 fn push_clip(&mut self, _rect: Rect) {}
553 fn pop_clip(&mut self) {}
554 fn push_transform(&mut self, _transform: presentar_core::Transform2D) {}
555 fn pop_transform(&mut self) {}
556 }
557
558 #[test]
563 fn test_vertical_scrollbar_creation() {
564 let sb = Scrollbar::vertical(100, 10);
565 assert_eq!(sb.orientation(), ScrollOrientation::Vertical);
566 assert_eq!(sb.content_length(), 100);
567 assert_eq!(sb.viewport_length(), 10);
568 }
569
570 #[test]
571 fn test_horizontal_scrollbar_creation() {
572 let sb = Scrollbar::horizontal(100, 20);
573 assert_eq!(sb.orientation(), ScrollOrientation::Horizontal);
574 assert_eq!(sb.content_length(), 100);
575 assert_eq!(sb.viewport_length(), 20);
576 }
577
578 #[test]
579 fn test_scrollbar_default() {
580 let sb = Scrollbar::default();
581 assert_eq!(sb.orientation(), ScrollOrientation::Vertical);
582 assert_eq!(sb.offset(), 0);
583 }
584
585 #[test]
590 fn test_with_arrows() {
591 let sb = Scrollbar::vertical(100, 10).with_arrows(false);
592 assert!(!sb.show_arrows);
593 }
594
595 #[test]
596 fn test_with_chars() {
597 let chars = ScrollbarChars::ascii();
598 let sb = Scrollbar::vertical(100, 10).with_chars(chars);
599 assert_eq!(sb.chars.track, '-');
600 }
601
602 #[test]
603 fn test_with_track_color() {
604 let sb = Scrollbar::vertical(100, 10).with_track_color(Color::RED);
605 assert_eq!(sb.track_color, Color::RED);
606 }
607
608 #[test]
609 fn test_with_thumb_color() {
610 let sb = Scrollbar::vertical(100, 10).with_thumb_color(Color::GREEN);
611 assert_eq!(sb.thumb_color, Color::GREEN);
612 }
613
614 #[test]
615 fn test_with_arrow_color() {
616 let sb = Scrollbar::vertical(100, 10).with_arrow_color(Color::BLUE);
617 assert_eq!(sb.arrow_color, Color::BLUE);
618 }
619
620 #[test]
625 fn test_chars_unicode() {
626 let chars = ScrollbarChars::unicode();
627 assert_eq!(chars.track, '░');
628 assert_eq!(chars.thumb, '█');
629 assert_eq!(chars.arrow_start, '▲');
630 assert_eq!(chars.arrow_end, '▼');
631 }
632
633 #[test]
634 fn test_chars_unicode_horizontal() {
635 let chars = ScrollbarChars::unicode_horizontal();
636 assert_eq!(chars.arrow_start, '◀');
637 assert_eq!(chars.arrow_end, '▶');
638 }
639
640 #[test]
641 fn test_chars_ascii() {
642 let chars = ScrollbarChars::ascii();
643 assert_eq!(chars.track, '-');
644 assert_eq!(chars.thumb, '#');
645 }
646
647 #[test]
648 fn test_chars_minimal() {
649 let chars = ScrollbarChars::minimal();
650 assert_eq!(chars.track, '│');
651 assert_eq!(chars.thumb, '┃');
652 }
653
654 #[test]
655 fn test_chars_default() {
656 let chars = ScrollbarChars::default();
657 assert_eq!(chars, ScrollbarChars::unicode());
658 }
659
660 #[test]
665 fn test_offset_initial() {
666 let sb = Scrollbar::vertical(100, 10);
667 assert_eq!(sb.offset(), 0);
668 }
669
670 #[test]
671 fn test_set_offset() {
672 let mut sb = Scrollbar::vertical(100, 10);
673 sb.set_offset(50);
674 assert_eq!(sb.offset(), 50);
675 }
676
677 #[test]
678 fn test_set_offset_clamps() {
679 let mut sb = Scrollbar::vertical(100, 10);
680 sb.set_offset(1000);
681 assert_eq!(sb.offset(), 90); }
683
684 #[test]
685 fn test_position_zero() {
686 let sb = Scrollbar::vertical(100, 10);
687 assert!((sb.position() - 0.0).abs() < f64::EPSILON);
688 }
689
690 #[test]
691 fn test_position_mid() {
692 let mut sb = Scrollbar::vertical(100, 10);
693 sb.set_offset(45);
694 assert!((sb.position() - 0.5).abs() < 0.01);
695 }
696
697 #[test]
698 fn test_position_end() {
699 let mut sb = Scrollbar::vertical(100, 10);
700 sb.set_offset(90);
701 assert!((sb.position() - 1.0).abs() < f64::EPSILON);
702 }
703
704 #[test]
705 fn test_position_no_scroll() {
706 let sb = Scrollbar::vertical(10, 20);
707 assert!((sb.position() - 0.0).abs() < f64::EPSILON);
708 }
709
710 #[test]
715 fn test_thumb_size_small_viewport() {
716 let sb = Scrollbar::vertical(100, 10);
717 assert!((sb.thumb_size() - 0.1).abs() < f64::EPSILON);
718 }
719
720 #[test]
721 fn test_thumb_size_large_viewport() {
722 let sb = Scrollbar::vertical(100, 50);
723 assert!((sb.thumb_size() - 0.5).abs() < f64::EPSILON);
724 }
725
726 #[test]
727 fn test_thumb_size_viewport_exceeds() {
728 let sb = Scrollbar::vertical(50, 100);
729 assert!((sb.thumb_size() - 1.0).abs() < f64::EPSILON);
730 }
731
732 #[test]
733 fn test_thumb_size_empty_content() {
734 let sb = Scrollbar::vertical(0, 10);
735 assert!((sb.thumb_size() - 1.0).abs() < f64::EPSILON);
736 }
737
738 #[test]
743 fn test_max_offset() {
744 let sb = Scrollbar::vertical(100, 10);
745 assert_eq!(sb.max_offset(), 90);
746 }
747
748 #[test]
749 fn test_max_offset_no_scroll() {
750 let sb = Scrollbar::vertical(10, 20);
751 assert_eq!(sb.max_offset(), 0);
752 }
753
754 #[test]
755 fn test_is_scrollable_true() {
756 let sb = Scrollbar::vertical(100, 10);
757 assert!(sb.is_scrollable());
758 }
759
760 #[test]
761 fn test_is_scrollable_false() {
762 let sb = Scrollbar::vertical(10, 20);
763 assert!(!sb.is_scrollable());
764 }
765
766 #[test]
771 fn test_scroll_positive() {
772 let mut sb = Scrollbar::vertical(100, 10);
773 sb.scroll(5);
774 assert_eq!(sb.offset(), 5);
775 }
776
777 #[test]
778 fn test_scroll_negative() {
779 let mut sb = Scrollbar::vertical(100, 10);
780 sb.set_offset(10);
781 sb.scroll(-3);
782 assert_eq!(sb.offset(), 7);
783 }
784
785 #[test]
786 fn test_scroll_clamps_max() {
787 let mut sb = Scrollbar::vertical(100, 10);
788 sb.scroll(1000);
789 assert_eq!(sb.offset(), 90);
790 }
791
792 #[test]
793 fn test_scroll_clamps_min() {
794 let mut sb = Scrollbar::vertical(100, 10);
795 sb.scroll(-100);
796 assert_eq!(sb.offset(), 0);
797 }
798
799 #[test]
800 fn test_scroll_start() {
801 let mut sb = Scrollbar::vertical(100, 10);
802 sb.set_offset(10);
803 sb.scroll_start();
804 assert_eq!(sb.offset(), 9);
805 }
806
807 #[test]
808 fn test_scroll_end() {
809 let mut sb = Scrollbar::vertical(100, 10);
810 sb.scroll_end();
811 assert_eq!(sb.offset(), 1);
812 }
813
814 #[test]
815 fn test_page_start() {
816 let mut sb = Scrollbar::vertical(100, 10);
817 sb.set_offset(50);
818 sb.page_start();
819 assert_eq!(sb.offset(), 40);
820 }
821
822 #[test]
823 fn test_page_end() {
824 let mut sb = Scrollbar::vertical(100, 10);
825 sb.page_end();
826 assert_eq!(sb.offset(), 10);
827 }
828
829 #[test]
834 fn test_jump_to_mid() {
835 let mut sb = Scrollbar::vertical(100, 10);
836 sb.jump_to(0.5);
837 assert_eq!(sb.offset(), 45);
838 }
839
840 #[test]
841 fn test_jump_to_start() {
842 let mut sb = Scrollbar::vertical(100, 10);
843 sb.set_offset(50);
844 sb.jump_to(0.0);
845 assert_eq!(sb.offset(), 0);
846 }
847
848 #[test]
849 fn test_jump_to_end() {
850 let mut sb = Scrollbar::vertical(100, 10);
851 sb.jump_to(1.0);
852 assert_eq!(sb.offset(), 90);
853 }
854
855 #[test]
856 fn test_jump_to_clamps() {
857 let mut sb = Scrollbar::vertical(100, 10);
858 sb.jump_to(2.0);
859 assert_eq!(sb.offset(), 90);
860 sb.jump_to(-1.0);
861 assert_eq!(sb.offset(), 0);
862 }
863
864 #[test]
865 fn test_jump_start() {
866 let mut sb = Scrollbar::vertical(100, 10);
867 sb.set_offset(50);
868 sb.jump_start();
869 assert_eq!(sb.offset(), 0);
870 }
871
872 #[test]
873 fn test_jump_end() {
874 let mut sb = Scrollbar::vertical(100, 10);
875 sb.jump_end();
876 assert_eq!(sb.offset(), 90);
877 }
878
879 #[test]
884 fn test_update_lengths() {
885 let mut sb = Scrollbar::vertical(100, 10);
886 sb.update_lengths(200, 20);
887 assert_eq!(sb.content_length(), 200);
888 assert_eq!(sb.viewport_length(), 20);
889 }
890
891 #[test]
892 fn test_update_lengths_clamps_offset() {
893 let mut sb = Scrollbar::vertical(100, 10);
894 sb.set_offset(80);
895 sb.update_lengths(50, 10);
896 assert_eq!(sb.offset(), 40); }
898
899 #[test]
904 fn test_brick_name() {
905 let sb = Scrollbar::vertical(100, 10);
906 assert_eq!(sb.brick_name(), "scrollbar");
907 }
908
909 #[test]
910 fn test_assertions_not_empty() {
911 let sb = Scrollbar::vertical(100, 10);
912 assert!(!sb.assertions().is_empty());
913 }
914
915 #[test]
916 fn test_budget() {
917 let sb = Scrollbar::vertical(100, 10);
918 let budget = sb.budget();
919 assert!(budget.paint_ms > 0);
920 }
921
922 #[test]
923 fn test_verify() {
924 let sb = Scrollbar::vertical(100, 10);
925 assert!(sb.verify().is_valid());
926 }
927
928 #[test]
929 fn test_to_html() {
930 let sb = Scrollbar::vertical(100, 10);
931 assert!(sb.to_html().is_empty());
932 }
933
934 #[test]
935 fn test_to_css() {
936 let sb = Scrollbar::vertical(100, 10);
937 assert!(sb.to_css().is_empty());
938 }
939
940 #[test]
945 fn test_type_id() {
946 let sb = Scrollbar::vertical(100, 10);
947 assert_eq!(Widget::type_id(&sb), TypeId::of::<Scrollbar>());
948 }
949
950 #[test]
951 fn test_measure_vertical() {
952 let sb = Scrollbar::vertical(100, 10);
953 let size = sb.measure(Constraints::loose(Size::new(100.0, 50.0)));
954 assert_eq!(size.width, 1.0);
955 assert!(size.height >= 3.0);
956 }
957
958 #[test]
959 fn test_measure_horizontal() {
960 let sb = Scrollbar::horizontal(100, 10);
961 let size = sb.measure(Constraints::loose(Size::new(50.0, 100.0)));
962 assert_eq!(size.height, 1.0);
963 assert!(size.width >= 3.0);
964 }
965
966 #[test]
967 fn test_layout() {
968 let mut sb = Scrollbar::vertical(100, 10);
969 let bounds = Rect::new(5.0, 10.0, 1.0, 20.0);
970 let result = sb.layout(bounds);
971 assert_eq!(result.size.height, 20.0);
972 assert_eq!(sb.bounds, bounds);
973 }
974
975 #[test]
976 fn test_children() {
977 let sb = Scrollbar::vertical(100, 10);
978 assert!(sb.children().is_empty());
979 }
980
981 #[test]
982 fn test_children_mut() {
983 let mut sb = Scrollbar::vertical(100, 10);
984 assert!(sb.children_mut().is_empty());
985 }
986
987 #[test]
988 fn test_event() {
989 let mut sb = Scrollbar::vertical(100, 10);
990 let event = Event::key_down(presentar_core::Key::Enter);
991 assert!(sb.event(&event).is_none());
992 }
993
994 #[test]
999 fn test_paint_vertical() {
1000 let mut sb = Scrollbar::vertical(100, 10);
1001 sb.bounds = Rect::new(0.0, 0.0, 1.0, 10.0);
1002 let mut canvas = MockCanvas::new();
1003 sb.paint(&mut canvas);
1004 assert!(!canvas.texts.is_empty());
1005 }
1006
1007 #[test]
1008 fn test_paint_vertical_no_arrows() {
1009 let mut sb = Scrollbar::vertical(100, 10).with_arrows(false);
1010 sb.bounds = Rect::new(0.0, 0.0, 1.0, 10.0);
1011 let mut canvas = MockCanvas::new();
1012 sb.paint(&mut canvas);
1013 let has_arrow = canvas.texts.iter().any(|(t, _)| t == "▲" || t == "▼");
1015 assert!(!has_arrow);
1016 }
1017
1018 #[test]
1019 fn test_paint_horizontal() {
1020 let mut sb = Scrollbar::horizontal(100, 10);
1021 sb.bounds = Rect::new(0.0, 0.0, 10.0, 1.0);
1022 let mut canvas = MockCanvas::new();
1023 sb.paint(&mut canvas);
1024 assert!(!canvas.texts.is_empty());
1025 }
1026
1027 #[test]
1028 fn test_paint_small_bounds() {
1029 let mut sb = Scrollbar::vertical(100, 10);
1030 sb.bounds = Rect::new(0.0, 0.0, 1.0, 2.0);
1031 let mut canvas = MockCanvas::new();
1032 sb.paint(&mut canvas);
1033 }
1035
1036 #[test]
1037 fn test_paint_with_offset() {
1038 let mut sb = Scrollbar::vertical(100, 10);
1039 sb.set_offset(50);
1040 sb.bounds = Rect::new(0.0, 0.0, 1.0, 10.0);
1041 let mut canvas = MockCanvas::new();
1042 sb.paint(&mut canvas);
1043 assert!(!canvas.texts.is_empty());
1044 }
1045
1046 #[test]
1051 fn test_orientation_default() {
1052 assert_eq!(ScrollOrientation::default(), ScrollOrientation::Vertical);
1053 }
1054
1055 #[test]
1056 fn test_orientation_eq() {
1057 assert_eq!(ScrollOrientation::Vertical, ScrollOrientation::Vertical);
1058 assert_ne!(ScrollOrientation::Vertical, ScrollOrientation::Horizontal);
1059 }
1060}