1#![forbid(unsafe_code)]
2
3use crate::arena::FrameArena;
33use crate::budget::DegradationLevel;
34use crate::buffer::Buffer;
35use crate::cell::{Cell, CellContent, GraphemeId};
36use crate::drawing::{BorderChars, Draw};
37use crate::grapheme_pool::GraphemePool;
38use crate::{display_width, grapheme_width};
39use ftui_core::geometry::Rect;
40use unicode_segmentation::UnicodeSegmentation;
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
46pub struct HitId(pub u32);
47
48impl HitId {
49 #[inline]
51 pub const fn new(id: u32) -> Self {
52 Self(id)
53 }
54
55 #[inline]
57 pub const fn id(self) -> u32 {
58 self.0
59 }
60}
61
62pub type HitData = u64;
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
67pub enum HitRegion {
68 #[default]
70 None,
71 Content,
73 Border,
75 Scrollbar,
77 Handle,
79 Button,
81 Link,
83 Custom(u8),
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
89pub struct HitCell {
90 pub widget_id: Option<HitId>,
92 pub region: HitRegion,
94 pub data: HitData,
96}
97
98impl HitCell {
99 #[inline]
101 pub const fn new(widget_id: HitId, region: HitRegion, data: HitData) -> Self {
102 Self {
103 widget_id: Some(widget_id),
104 region,
105 data,
106 }
107 }
108
109 #[inline]
111 pub const fn is_empty(&self) -> bool {
112 self.widget_id.is_none()
113 }
114}
115
116#[derive(Debug, Clone)]
121pub struct HitGrid {
122 width: u16,
123 height: u16,
124 cells: Vec<HitCell>,
125}
126
127impl HitGrid {
128 pub fn new(width: u16, height: u16) -> Self {
130 let size = width as usize * height as usize;
131 Self {
132 width,
133 height,
134 cells: vec![HitCell::default(); size],
135 }
136 }
137
138 #[inline]
140 pub const fn width(&self) -> u16 {
141 self.width
142 }
143
144 #[inline]
146 pub const fn height(&self) -> u16 {
147 self.height
148 }
149
150 #[inline]
152 fn index(&self, x: u16, y: u16) -> Option<usize> {
153 if x < self.width && y < self.height {
154 Some(y as usize * self.width as usize + x as usize)
155 } else {
156 None
157 }
158 }
159
160 #[inline]
162 #[must_use]
163 pub fn get(&self, x: u16, y: u16) -> Option<&HitCell> {
164 self.index(x, y).map(|i| &self.cells[i])
165 }
166
167 #[inline]
169 #[must_use]
170 pub fn get_mut(&mut self, x: u16, y: u16) -> Option<&mut HitCell> {
171 self.index(x, y).map(|i| &mut self.cells[i])
172 }
173
174 pub fn register(&mut self, rect: Rect, widget_id: HitId, region: HitRegion, data: HitData) {
178 let x_end = (rect.x as usize + rect.width as usize).min(self.width as usize);
180 let y_end = (rect.y as usize + rect.height as usize).min(self.height as usize);
181
182 if rect.x as usize >= x_end || rect.y as usize >= y_end {
184 return;
185 }
186
187 let hit_cell = HitCell::new(widget_id, region, data);
188
189 for y in rect.y as usize..y_end {
190 let row_start = y * self.width as usize;
191 let start = row_start + rect.x as usize;
192 let end = row_start + x_end;
193
194 self.cells[start..end].fill(hit_cell);
196 }
197 }
198
199 #[must_use]
203 pub fn hit_test(&self, x: u16, y: u16) -> Option<(HitId, HitRegion, HitData)> {
204 self.get(x, y)
205 .and_then(|cell| cell.widget_id.map(|id| (id, cell.region, cell.data)))
206 }
207
208 pub fn hits_in(&self, rect: Rect) -> Vec<(HitId, HitRegion, HitData)> {
210 let x_end = (rect.x as usize + rect.width as usize).min(self.width as usize) as u16;
211 let y_end = (rect.y as usize + rect.height as usize).min(self.height as usize) as u16;
212 let mut hits = Vec::new();
213
214 for y in rect.y..y_end {
215 for x in rect.x..x_end {
216 if let Some((id, region, data)) = self.hit_test(x, y) {
217 hits.push((id, region, data));
218 }
219 }
220 }
221
222 hits
223 }
224
225 pub fn clear(&mut self) {
227 self.cells.fill(HitCell::default());
228 }
229}
230
231use crate::link_registry::LinkRegistry;
232
233#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
235pub enum CostEstimateSource {
236 Measured,
238 AreaFallback,
240 #[default]
242 FixedDefault,
243}
244
245#[derive(Debug, Clone)]
250pub struct WidgetSignal {
251 pub widget_id: u64,
253 pub essential: bool,
255 pub priority: f32,
257 pub staleness_ms: u64,
259 pub focus_boost: f32,
261 pub interaction_boost: f32,
263 pub area_cells: u32,
265 pub cost_estimate_us: f32,
267 pub recent_cost_us: f32,
269 pub estimate_source: CostEstimateSource,
271}
272
273impl Default for WidgetSignal {
274 fn default() -> Self {
275 Self {
276 widget_id: 0,
277 essential: false,
278 priority: 0.5,
279 staleness_ms: 0,
280 focus_boost: 0.0,
281 interaction_boost: 0.0,
282 area_cells: 1,
283 cost_estimate_us: 5.0,
284 recent_cost_us: 5.0,
285 estimate_source: CostEstimateSource::FixedDefault,
286 }
287 }
288}
289
290impl WidgetSignal {
291 #[must_use]
293 pub fn new(widget_id: u64) -> Self {
294 Self {
295 widget_id,
296 ..Self::default()
297 }
298 }
299}
300
301#[derive(Debug, Clone)]
303pub struct WidgetBudget {
304 allow_list: Option<Vec<u64>>,
305}
306
307impl Default for WidgetBudget {
308 fn default() -> Self {
309 Self::allow_all()
310 }
311}
312
313impl WidgetBudget {
314 #[must_use]
316 pub fn allow_all() -> Self {
317 Self { allow_list: None }
318 }
319
320 #[must_use]
322 pub fn allow_only(mut ids: Vec<u64>) -> Self {
323 ids.sort_unstable();
324 ids.dedup();
325 Self {
326 allow_list: Some(ids),
327 }
328 }
329
330 #[inline]
332 pub fn allows(&self, widget_id: u64, essential: bool) -> bool {
333 if essential {
334 return true;
335 }
336 match &self.allow_list {
337 None => true,
338 Some(ids) => ids.binary_search(&widget_id).is_ok(),
339 }
340 }
341}
342
343#[derive(Debug)]
354pub struct Frame<'a> {
355 pub buffer: Buffer,
357
358 pub pool: &'a mut GraphemePool,
360
361 pub links: Option<&'a mut LinkRegistry>,
363
364 pub hit_grid: Option<HitGrid>,
368
369 pub widget_budget: WidgetBudget,
371
372 pub widget_signals: Vec<WidgetSignal>,
374
375 pub cursor_position: Option<(u16, u16)>,
379
380 pub cursor_visible: bool,
382
383 pub degradation: DegradationLevel,
389
390 pub arena: Option<&'a FrameArena>,
397}
398
399impl<'a> Frame<'a> {
400 pub fn new(width: u16, height: u16, pool: &'a mut GraphemePool) -> Self {
404 Self {
405 buffer: Buffer::new(width, height),
406 pool,
407 links: None,
408 hit_grid: None,
409 widget_budget: WidgetBudget::default(),
410 widget_signals: Vec::new(),
411 cursor_position: None,
412 cursor_visible: true,
413 degradation: DegradationLevel::Full,
414 arena: None,
415 }
416 }
417
418 pub fn from_buffer(buffer: Buffer, pool: &'a mut GraphemePool) -> Self {
422 Self {
423 buffer,
424 pool,
425 links: None,
426 hit_grid: None,
427 widget_budget: WidgetBudget::default(),
428 widget_signals: Vec::new(),
429 cursor_position: None,
430 cursor_visible: true,
431 degradation: DegradationLevel::Full,
432 arena: None,
433 }
434 }
435
436 pub fn with_links(
441 width: u16,
442 height: u16,
443 pool: &'a mut GraphemePool,
444 links: &'a mut LinkRegistry,
445 ) -> Self {
446 Self {
447 buffer: Buffer::new(width, height),
448 pool,
449 links: Some(links),
450 hit_grid: None,
451 widget_budget: WidgetBudget::default(),
452 widget_signals: Vec::new(),
453 cursor_position: None,
454 cursor_visible: true,
455 degradation: DegradationLevel::Full,
456 arena: None,
457 }
458 }
459
460 pub fn with_hit_grid(width: u16, height: u16, pool: &'a mut GraphemePool) -> Self {
464 Self {
465 buffer: Buffer::new(width, height),
466 pool,
467 links: None,
468 hit_grid: Some(HitGrid::new(width, height)),
469 widget_budget: WidgetBudget::default(),
470 widget_signals: Vec::new(),
471 cursor_position: None,
472 cursor_visible: true,
473 degradation: DegradationLevel::Full,
474 arena: None,
475 }
476 }
477
478 pub fn set_links(&mut self, links: &'a mut LinkRegistry) {
480 self.links = Some(links);
481 }
482
483 pub fn set_arena(&mut self, arena: &'a FrameArena) {
488 self.arena = Some(arena);
489 }
490
491 pub fn arena(&self) -> Option<&FrameArena> {
496 self.arena
497 }
498
499 pub fn register_link(&mut self, url: &str) -> u32 {
503 if let Some(ref mut links) = self.links {
504 links.register(url)
505 } else {
506 0
507 }
508 }
509
510 pub fn set_widget_budget(&mut self, budget: WidgetBudget) {
512 self.widget_budget = budget;
513 }
514
515 #[inline]
517 pub fn should_render_widget(&self, widget_id: u64, essential: bool) -> bool {
518 self.widget_budget.allows(widget_id, essential)
519 }
520
521 pub fn register_widget_signal(&mut self, signal: WidgetSignal) {
523 self.widget_signals.push(signal);
524 }
525
526 #[inline]
528 pub fn widget_signals(&self) -> &[WidgetSignal] {
529 &self.widget_signals
530 }
531
532 #[inline]
534 pub fn take_widget_signals(&mut self) -> Vec<WidgetSignal> {
535 std::mem::take(&mut self.widget_signals)
536 }
537
538 pub fn intern(&mut self, text: &str) -> GraphemeId {
547 let width = display_width(text).min(127) as u8;
548 self.pool.intern(text, width)
549 }
550
551 pub fn intern_with_width(&mut self, text: &str, width: u8) -> GraphemeId {
553 self.pool.intern(text, width)
554 }
555
556 pub fn enable_hit_testing(&mut self) {
558 if self.hit_grid.is_none() {
559 self.hit_grid = Some(HitGrid::new(self.width(), self.height()));
560 }
561 }
562
563 #[inline]
565 pub fn width(&self) -> u16 {
566 self.buffer.width()
567 }
568
569 #[inline]
571 pub fn height(&self) -> u16 {
572 self.buffer.height()
573 }
574
575 pub fn clear(&mut self) {
579 self.buffer.clear();
580 if let Some(ref mut grid) = self.hit_grid {
581 grid.clear();
582 }
583 self.cursor_position = None;
584 self.widget_signals.clear();
585 }
586
587 #[inline]
591 pub fn set_cursor(&mut self, position: Option<(u16, u16)>) {
592 self.cursor_position = position;
593 }
594
595 #[inline]
597 pub fn set_cursor_visible(&mut self, visible: bool) {
598 self.cursor_visible = visible;
599 }
600
601 #[inline]
606 pub fn set_degradation(&mut self, level: DegradationLevel) {
607 self.degradation = level;
608 self.buffer.degradation = level;
609 }
610
611 #[inline]
613 pub fn bounds(&self) -> Rect {
614 self.buffer.bounds()
615 }
616
617 pub fn register_hit(
627 &mut self,
628 rect: Rect,
629 id: HitId,
630 region: HitRegion,
631 data: HitData,
632 ) -> bool {
633 if let Some(ref mut grid) = self.hit_grid {
634 let clipped = rect.intersection(&self.buffer.current_scissor());
636 if !clipped.is_empty() {
637 grid.register(clipped, id, region, data);
638 }
639 true
640 } else {
641 false
642 }
643 }
644
645 #[must_use]
647 pub fn hit_test(&self, x: u16, y: u16) -> Option<(HitId, HitRegion, HitData)> {
648 self.hit_grid.as_ref().and_then(|grid| grid.hit_test(x, y))
649 }
650
651 pub fn register_hit_region(&mut self, rect: Rect, id: HitId) -> bool {
653 self.register_hit(rect, id, HitRegion::Content, 0)
654 }
655}
656
657impl<'a> Draw for Frame<'a> {
658 fn draw_horizontal_line(&mut self, x: u16, y: u16, width: u16, cell: Cell) {
659 self.buffer.draw_horizontal_line(x, y, width, cell);
660 }
661
662 fn draw_vertical_line(&mut self, x: u16, y: u16, height: u16, cell: Cell) {
663 self.buffer.draw_vertical_line(x, y, height, cell);
664 }
665
666 fn draw_rect_filled(&mut self, rect: Rect, cell: Cell) {
667 self.buffer.draw_rect_filled(rect, cell);
668 }
669
670 fn draw_rect_outline(&mut self, rect: Rect, cell: Cell) {
671 self.buffer.draw_rect_outline(rect, cell);
672 }
673
674 fn print_text(&mut self, x: u16, y: u16, text: &str, base_cell: Cell) -> u16 {
675 self.print_text_clipped(x, y, text, base_cell, self.width())
676 }
677
678 fn print_text_clipped(
679 &mut self,
680 x: u16,
681 y: u16,
682 text: &str,
683 base_cell: Cell,
684 max_x: u16,
685 ) -> u16 {
686 let mut cx = x;
687 for grapheme in text.graphemes(true) {
688 let width = grapheme_width(grapheme);
689 if width == 0 {
690 continue;
691 }
692
693 if cx >= max_x {
694 break;
695 }
696
697 if cx as u32 + width as u32 > max_x as u32 {
699 break;
700 }
701
702 let content = if width > 1 || grapheme.chars().count() > 1 {
704 let id = self.intern_with_width(grapheme, width as u8);
705 CellContent::from_grapheme(id)
706 } else if let Some(c) = grapheme.chars().next() {
707 CellContent::from_char(c)
708 } else {
709 continue;
710 };
711
712 let cell = Cell {
713 content,
714 fg: base_cell.fg,
715 bg: base_cell.bg,
716 attrs: base_cell.attrs,
717 };
718 self.buffer.set_fast(cx, y, cell);
719
720 cx = cx.saturating_add(width as u16);
721 }
722 cx
723 }
724
725 fn draw_border(&mut self, rect: Rect, chars: BorderChars, base_cell: Cell) {
726 self.buffer.draw_border(rect, chars, base_cell);
727 }
728
729 fn draw_box(&mut self, rect: Rect, chars: BorderChars, border_cell: Cell, fill_cell: Cell) {
730 self.buffer.draw_box(rect, chars, border_cell, fill_cell);
731 }
732
733 fn paint_area(
734 &mut self,
735 rect: Rect,
736 fg: Option<crate::cell::PackedRgba>,
737 bg: Option<crate::cell::PackedRgba>,
738 ) {
739 self.buffer.paint_area(rect, fg, bg);
740 }
741}
742
743#[cfg(test)]
744mod tests {
745 use super::*;
746 use crate::cell::Cell;
747
748 #[test]
749 fn frame_creation() {
750 let mut pool = GraphemePool::new();
751 let frame = Frame::new(80, 24, &mut pool);
752 assert_eq!(frame.width(), 80);
753 assert_eq!(frame.height(), 24);
754 assert!(frame.hit_grid.is_none());
755 assert!(frame.cursor_position.is_none());
756 assert!(frame.cursor_visible);
757 }
758
759 #[test]
760 fn frame_with_hit_grid() {
761 let mut pool = GraphemePool::new();
762 let frame = Frame::with_hit_grid(80, 24, &mut pool);
763 assert!(frame.hit_grid.is_some());
764 assert_eq!(frame.width(), 80);
765 assert_eq!(frame.height(), 24);
766 }
767
768 #[test]
769 fn frame_cursor() {
770 let mut pool = GraphemePool::new();
771 let mut frame = Frame::new(80, 24, &mut pool);
772 assert!(frame.cursor_position.is_none());
773 assert!(frame.cursor_visible);
774
775 frame.set_cursor(Some((10, 5)));
776 assert_eq!(frame.cursor_position, Some((10, 5)));
777
778 frame.set_cursor_visible(false);
779 assert!(!frame.cursor_visible);
780
781 frame.set_cursor(None);
782 assert!(frame.cursor_position.is_none());
783 }
784
785 #[test]
786 fn frame_clear() {
787 let mut pool = GraphemePool::new();
788 let mut frame = Frame::with_hit_grid(10, 10, &mut pool);
789
790 frame.buffer.set_raw(5, 5, Cell::from_char('X'));
792 frame.register_hit_region(Rect::new(0, 0, 5, 5), HitId::new(1));
793
794 assert_eq!(frame.buffer.get(5, 5).unwrap().content.as_char(), Some('X'));
796 assert_eq!(
797 frame.hit_test(2, 2),
798 Some((HitId::new(1), HitRegion::Content, 0))
799 );
800
801 frame.clear();
803
804 assert!(frame.buffer.get(5, 5).unwrap().is_empty());
806 assert!(frame.hit_test(2, 2).is_none());
807 }
808
809 #[test]
810 fn frame_bounds() {
811 let mut pool = GraphemePool::new();
812 let frame = Frame::new(80, 24, &mut pool);
813 let bounds = frame.bounds();
814 assert_eq!(bounds.x, 0);
815 assert_eq!(bounds.y, 0);
816 assert_eq!(bounds.width, 80);
817 assert_eq!(bounds.height, 24);
818 }
819
820 #[test]
821 fn hit_grid_creation() {
822 let grid = HitGrid::new(80, 24);
823 assert_eq!(grid.width(), 80);
824 assert_eq!(grid.height(), 24);
825 }
826
827 #[test]
828 fn hit_grid_registration() {
829 let mut pool = GraphemePool::new();
830 let mut frame = Frame::with_hit_grid(80, 24, &mut pool);
831 let hit_id = HitId::new(42);
832 let rect = Rect::new(10, 5, 20, 3);
833
834 frame.register_hit(rect, hit_id, HitRegion::Button, 99);
835
836 assert_eq!(frame.hit_test(15, 6), Some((hit_id, HitRegion::Button, 99)));
838 assert_eq!(frame.hit_test(10, 5), Some((hit_id, HitRegion::Button, 99))); assert_eq!(frame.hit_test(29, 7), Some((hit_id, HitRegion::Button, 99))); assert!(frame.hit_test(5, 5).is_none()); assert!(frame.hit_test(30, 6).is_none()); assert!(frame.hit_test(15, 8).is_none()); assert!(frame.hit_test(15, 4).is_none()); }
847
848 #[test]
849 fn hit_grid_overlapping_regions() {
850 let mut pool = GraphemePool::new();
851 let mut frame = Frame::with_hit_grid(20, 20, &mut pool);
852
853 frame.register_hit(
855 Rect::new(0, 0, 10, 10),
856 HitId::new(1),
857 HitRegion::Content,
858 1,
859 );
860 frame.register_hit(Rect::new(5, 5, 10, 10), HitId::new(2), HitRegion::Border, 2);
861
862 assert_eq!(
864 frame.hit_test(2, 2),
865 Some((HitId::new(1), HitRegion::Content, 1))
866 );
867
868 assert_eq!(
870 frame.hit_test(7, 7),
871 Some((HitId::new(2), HitRegion::Border, 2))
872 );
873
874 assert_eq!(
876 frame.hit_test(12, 12),
877 Some((HitId::new(2), HitRegion::Border, 2))
878 );
879 }
880
881 #[test]
882 fn hit_grid_out_of_bounds() {
883 let mut pool = GraphemePool::new();
884 let frame = Frame::with_hit_grid(10, 10, &mut pool);
885
886 assert!(frame.hit_test(100, 100).is_none());
888 assert!(frame.hit_test(10, 0).is_none()); assert!(frame.hit_test(0, 10).is_none()); }
891
892 #[test]
893 fn hit_id_properties() {
894 let id = HitId::new(42);
895 assert_eq!(id.id(), 42);
896 assert_eq!(id, HitId(42));
897 }
898
899 #[test]
900 fn register_hit_region_no_grid() {
901 let mut pool = GraphemePool::new();
902 let mut frame = Frame::new(10, 10, &mut pool);
903 let result = frame.register_hit_region(Rect::new(0, 0, 5, 5), HitId::new(1));
904 assert!(!result); }
906
907 #[test]
908 fn register_hit_region_with_grid() {
909 let mut pool = GraphemePool::new();
910 let mut frame = Frame::with_hit_grid(10, 10, &mut pool);
911 let result = frame.register_hit_region(Rect::new(0, 0, 5, 5), HitId::new(1));
912 assert!(result); }
914
915 #[test]
916 fn hit_grid_clear() {
917 let mut grid = HitGrid::new(10, 10);
918 grid.register(Rect::new(0, 0, 5, 5), HitId::new(1), HitRegion::Content, 0);
919
920 assert_eq!(
921 grid.hit_test(2, 2),
922 Some((HitId::new(1), HitRegion::Content, 0))
923 );
924
925 grid.clear();
926
927 assert!(grid.hit_test(2, 2).is_none());
928 }
929
930 #[test]
931 fn hit_grid_boundary_clipping() {
932 let mut grid = HitGrid::new(10, 10);
933
934 grid.register(
936 Rect::new(8, 8, 10, 10),
937 HitId::new(1),
938 HitRegion::Content,
939 0,
940 );
941
942 assert_eq!(
944 grid.hit_test(9, 9),
945 Some((HitId::new(1), HitRegion::Content, 0))
946 );
947
948 assert!(grid.hit_test(10, 10).is_none());
950 }
951
952 #[test]
953 fn hit_grid_edge_and_corner_cells() {
954 let mut grid = HitGrid::new(4, 4);
955 grid.register(Rect::new(3, 0, 1, 4), HitId::new(7), HitRegion::Border, 11);
956
957 assert_eq!(
959 grid.hit_test(3, 0),
960 Some((HitId::new(7), HitRegion::Border, 11))
961 );
962 assert_eq!(
963 grid.hit_test(3, 3),
964 Some((HitId::new(7), HitRegion::Border, 11))
965 );
966
967 assert!(grid.hit_test(2, 0).is_none());
969 assert!(grid.hit_test(4, 0).is_none());
970 assert!(grid.hit_test(3, 4).is_none());
971
972 let mut grid = HitGrid::new(4, 4);
973 grid.register(Rect::new(0, 3, 4, 1), HitId::new(9), HitRegion::Content, 21);
974
975 assert_eq!(
977 grid.hit_test(0, 3),
978 Some((HitId::new(9), HitRegion::Content, 21))
979 );
980 assert_eq!(
981 grid.hit_test(3, 3),
982 Some((HitId::new(9), HitRegion::Content, 21))
983 );
984
985 assert!(grid.hit_test(0, 2).is_none());
987 assert!(grid.hit_test(0, 4).is_none());
988 }
989
990 #[test]
991 fn frame_register_hit_respects_nested_scissor() {
992 let mut pool = GraphemePool::new();
993 let mut frame = Frame::with_hit_grid(10, 10, &mut pool);
994
995 let outer = Rect::new(1, 1, 8, 8);
996 frame.buffer.push_scissor(outer);
997 assert_eq!(frame.buffer.current_scissor(), outer);
998
999 let inner = Rect::new(4, 4, 10, 10);
1000 frame.buffer.push_scissor(inner);
1001 let clipped = outer.intersection(&inner);
1002 let current = frame.buffer.current_scissor();
1003 assert_eq!(current, clipped);
1004
1005 assert!(outer.contains(current.x, current.y));
1007 assert!(outer.contains(
1008 current.right().saturating_sub(1),
1009 current.bottom().saturating_sub(1)
1010 ));
1011
1012 frame.register_hit(
1013 Rect::new(0, 0, 10, 10),
1014 HitId::new(3),
1015 HitRegion::Button,
1016 99,
1017 );
1018
1019 assert_eq!(
1020 frame.hit_test(4, 4),
1021 Some((HitId::new(3), HitRegion::Button, 99))
1022 );
1023 assert_eq!(
1024 frame.hit_test(8, 8),
1025 Some((HitId::new(3), HitRegion::Button, 99))
1026 );
1027 assert!(frame.hit_test(3, 3).is_none()); assert!(frame.hit_test(0, 0).is_none()); frame.buffer.pop_scissor();
1031 assert_eq!(frame.buffer.current_scissor(), outer);
1032 }
1033
1034 #[test]
1035 fn hit_grid_hits_in_area() {
1036 let mut grid = HitGrid::new(5, 5);
1037 grid.register(Rect::new(0, 0, 2, 2), HitId::new(1), HitRegion::Content, 10);
1038 grid.register(Rect::new(1, 1, 2, 2), HitId::new(2), HitRegion::Button, 20);
1039
1040 let hits = grid.hits_in(Rect::new(0, 0, 3, 3));
1041 assert!(hits.contains(&(HitId::new(1), HitRegion::Content, 10)));
1042 assert!(hits.contains(&(HitId::new(2), HitRegion::Button, 20)));
1043 }
1044
1045 #[test]
1046 fn frame_intern() {
1047 let mut pool = GraphemePool::new();
1048 let mut frame = Frame::new(10, 10, &mut pool);
1049
1050 let id = frame.intern("👋");
1051 assert_eq!(frame.pool.get(id), Some("👋"));
1052 }
1053
1054 #[test]
1055 fn frame_intern_with_width() {
1056 let mut pool = GraphemePool::new();
1057 let mut frame = Frame::new(10, 10, &mut pool);
1058
1059 let id = frame.intern_with_width("🧪", 2);
1060 assert_eq!(id.width(), 2);
1061 assert_eq!(frame.pool.get(id), Some("🧪"));
1062 }
1063
1064 #[test]
1065 fn frame_print_text_emoji_presentation_sets_continuation() {
1066 let mut pool = GraphemePool::new();
1067 let mut frame = Frame::new(5, 1, &mut pool);
1068
1069 frame.print_text(0, 0, "👍🏽", Cell::from_char(' '));
1072
1073 let head = frame.buffer.get(0, 0).unwrap();
1074 let tail = frame.buffer.get(1, 0).unwrap();
1075
1076 assert_eq!(head.content.width(), 2);
1077 assert!(tail.content.is_continuation());
1078 }
1079
1080 #[test]
1081 fn frame_enable_hit_testing() {
1082 let mut pool = GraphemePool::new();
1083 let mut frame = Frame::new(10, 10, &mut pool);
1084 assert!(frame.hit_grid.is_none());
1085
1086 frame.enable_hit_testing();
1087 assert!(frame.hit_grid.is_some());
1088
1089 frame.enable_hit_testing();
1091 assert!(frame.hit_grid.is_some());
1092 }
1093
1094 #[test]
1095 fn frame_enable_hit_testing_then_register() {
1096 let mut pool = GraphemePool::new();
1097 let mut frame = Frame::new(10, 10, &mut pool);
1098 frame.enable_hit_testing();
1099
1100 let registered = frame.register_hit_region(Rect::new(0, 0, 5, 5), HitId::new(1));
1101 assert!(registered);
1102 assert_eq!(
1103 frame.hit_test(2, 2),
1104 Some((HitId::new(1), HitRegion::Content, 0))
1105 );
1106 }
1107
1108 #[test]
1109 fn hit_cell_default_is_empty() {
1110 let cell = HitCell::default();
1111 assert!(cell.is_empty());
1112 assert_eq!(cell.widget_id, None);
1113 assert_eq!(cell.region, HitRegion::None);
1114 assert_eq!(cell.data, 0);
1115 }
1116
1117 #[test]
1118 fn hit_cell_new_is_not_empty() {
1119 let cell = HitCell::new(HitId::new(1), HitRegion::Button, 42);
1120 assert!(!cell.is_empty());
1121 assert_eq!(cell.widget_id, Some(HitId::new(1)));
1122 assert_eq!(cell.region, HitRegion::Button);
1123 assert_eq!(cell.data, 42);
1124 }
1125
1126 #[test]
1127 fn hit_region_variants() {
1128 assert_eq!(HitRegion::default(), HitRegion::None);
1129
1130 let variants = [
1132 HitRegion::None,
1133 HitRegion::Content,
1134 HitRegion::Border,
1135 HitRegion::Scrollbar,
1136 HitRegion::Handle,
1137 HitRegion::Button,
1138 HitRegion::Link,
1139 HitRegion::Custom(0),
1140 HitRegion::Custom(1),
1141 HitRegion::Custom(255),
1142 ];
1143 for i in 0..variants.len() {
1144 for j in (i + 1)..variants.len() {
1145 assert_ne!(
1146 variants[i], variants[j],
1147 "variants {i} and {j} should differ"
1148 );
1149 }
1150 }
1151 }
1152
1153 #[test]
1154 fn hit_id_default() {
1155 let id = HitId::default();
1156 assert_eq!(id.id(), 0);
1157 }
1158
1159 #[test]
1160 fn hit_grid_initial_cells_empty() {
1161 let grid = HitGrid::new(5, 5);
1162 for y in 0..5 {
1163 for x in 0..5 {
1164 let cell = grid.get(x, y).unwrap();
1165 assert!(cell.is_empty());
1166 }
1167 }
1168 }
1169
1170 #[test]
1171 fn hit_grid_zero_dimensions() {
1172 let grid = HitGrid::new(0, 0);
1173 assert_eq!(grid.width(), 0);
1174 assert_eq!(grid.height(), 0);
1175 assert!(grid.get(0, 0).is_none());
1176 assert!(grid.hit_test(0, 0).is_none());
1177 }
1178
1179 #[test]
1180 fn hit_grid_hits_in_empty_area() {
1181 let grid = HitGrid::new(10, 10);
1182 let hits = grid.hits_in(Rect::new(0, 0, 5, 5));
1183 assert!(hits.is_empty());
1185 }
1186
1187 #[test]
1188 fn hit_grid_hits_in_clipped_area() {
1189 let mut grid = HitGrid::new(5, 5);
1190 grid.register(Rect::new(0, 0, 5, 5), HitId::new(1), HitRegion::Content, 0);
1191
1192 let hits = grid.hits_in(Rect::new(3, 3, 10, 10));
1194 assert_eq!(hits.len(), 4); }
1196
1197 #[test]
1198 fn hit_test_no_grid_returns_none() {
1199 let mut pool = GraphemePool::new();
1200 let frame = Frame::new(10, 10, &mut pool);
1201 assert!(frame.hit_test(0, 0).is_none());
1202 }
1203
1204 #[test]
1205 fn frame_cursor_operations() {
1206 let mut pool = GraphemePool::new();
1207 let mut frame = Frame::new(80, 24, &mut pool);
1208
1209 frame.set_cursor(Some((79, 23)));
1211 assert_eq!(frame.cursor_position, Some((79, 23)));
1212
1213 frame.set_cursor(Some((0, 0)));
1215 assert_eq!(frame.cursor_position, Some((0, 0)));
1216
1217 frame.set_cursor_visible(false);
1219 assert!(!frame.cursor_visible);
1220 frame.set_cursor_visible(true);
1221 assert!(frame.cursor_visible);
1222 }
1223
1224 #[test]
1225 fn hit_data_large_values() {
1226 let mut grid = HitGrid::new(5, 5);
1227 grid.register(
1229 Rect::new(0, 0, 1, 1),
1230 HitId::new(1),
1231 HitRegion::Content,
1232 u64::MAX,
1233 );
1234 let result = grid.hit_test(0, 0);
1235 assert_eq!(result, Some((HitId::new(1), HitRegion::Content, u64::MAX)));
1236 }
1237
1238 #[test]
1239 fn hit_id_large_value() {
1240 let id = HitId::new(u32::MAX);
1241 assert_eq!(id.id(), u32::MAX);
1242 }
1243
1244 #[test]
1245 fn frame_print_text_interns_complex_graphemes() {
1246 let mut pool = GraphemePool::new();
1247 let mut frame = Frame::new(10, 1, &mut pool);
1248
1249 let flag = "🇺🇸";
1251 assert!(flag.chars().count() > 1);
1252
1253 frame.print_text(0, 0, flag, Cell::default());
1254
1255 let cell = frame.buffer.get(0, 0).unwrap();
1256 assert!(cell.content.is_grapheme());
1257
1258 let id = cell.content.grapheme_id().unwrap();
1259 assert_eq!(frame.pool.get(id), Some(flag));
1260 }
1261
1262 #[test]
1265 fn hit_id_debug_clone_copy_hash() {
1266 let id = HitId::new(99);
1267 let dbg = format!("{:?}", id);
1268 assert!(dbg.contains("99"), "Debug: {dbg}");
1269 let copied: HitId = id; assert_eq!(id, copied);
1271 use std::collections::HashSet;
1273 let mut set = HashSet::new();
1274 set.insert(id);
1275 set.insert(HitId::new(99));
1276 assert_eq!(set.len(), 1);
1277 set.insert(HitId::new(100));
1278 assert_eq!(set.len(), 2);
1279 }
1280
1281 #[test]
1282 fn hit_id_eq_and_ne() {
1283 assert_eq!(HitId::new(0), HitId::new(0));
1284 assert_ne!(HitId::new(0), HitId::new(1));
1285 assert_ne!(HitId::new(u32::MAX), HitId::default());
1286 }
1287
1288 #[test]
1291 fn hit_region_debug_clone_copy_hash() {
1292 let r = HitRegion::Custom(42);
1293 let dbg = format!("{:?}", r);
1294 assert!(dbg.contains("Custom"), "Debug: {dbg}");
1295 let copied: HitRegion = r; assert_eq!(r, copied);
1297 use std::collections::HashSet;
1298 let mut set = HashSet::new();
1299 set.insert(r);
1300 set.insert(HitRegion::Custom(42));
1301 assert_eq!(set.len(), 1);
1302 }
1303
1304 #[test]
1307 fn hit_cell_debug_clone_copy_eq() {
1308 let cell = HitCell::new(HitId::new(5), HitRegion::Link, 123);
1309 let dbg = format!("{:?}", cell);
1310 assert!(dbg.contains("Link"), "Debug: {dbg}");
1311 let copied: HitCell = cell; assert_eq!(cell, copied);
1313 assert_ne!(cell, HitCell::default());
1315 }
1316
1317 #[test]
1320 fn hit_grid_clone() {
1321 let mut grid = HitGrid::new(5, 5);
1322 grid.register(Rect::new(0, 0, 2, 2), HitId::new(1), HitRegion::Content, 7);
1323 let clone = grid.clone();
1324 assert_eq!(clone.width(), 5);
1325 assert_eq!(
1326 clone.hit_test(0, 0),
1327 Some((HitId::new(1), HitRegion::Content, 7))
1328 );
1329 }
1330
1331 #[test]
1332 fn hit_grid_get_mut() {
1333 let mut grid = HitGrid::new(5, 5);
1334 if let Some(cell) = grid.get_mut(2, 3) {
1336 *cell = HitCell::new(HitId::new(77), HitRegion::Handle, 55);
1337 }
1338 assert_eq!(
1339 grid.hit_test(2, 3),
1340 Some((HitId::new(77), HitRegion::Handle, 55))
1341 );
1342 assert!(grid.get_mut(5, 5).is_none());
1344 }
1345
1346 #[test]
1347 fn hit_grid_zero_width_nonzero_height() {
1348 let grid = HitGrid::new(0, 10);
1349 assert_eq!(grid.width(), 0);
1350 assert_eq!(grid.height(), 10);
1351 assert!(grid.get(0, 0).is_none());
1352 assert!(grid.hit_test(0, 5).is_none());
1353 }
1354
1355 #[test]
1356 fn hit_grid_nonzero_width_zero_height() {
1357 let grid = HitGrid::new(10, 0);
1358 assert_eq!(grid.width(), 10);
1359 assert_eq!(grid.height(), 0);
1360 assert!(grid.get(0, 0).is_none());
1361 }
1362
1363 #[test]
1364 fn hit_grid_register_zero_width_rect() {
1365 let mut grid = HitGrid::new(10, 10);
1366 grid.register(Rect::new(2, 2, 0, 5), HitId::new(1), HitRegion::Content, 0);
1367 assert!(grid.hit_test(2, 2).is_none());
1369 }
1370
1371 #[test]
1372 fn hit_grid_register_zero_height_rect() {
1373 let mut grid = HitGrid::new(10, 10);
1374 grid.register(Rect::new(2, 2, 5, 0), HitId::new(1), HitRegion::Content, 0);
1375 assert!(grid.hit_test(2, 2).is_none());
1376 }
1377
1378 #[test]
1379 fn hit_grid_register_past_bounds() {
1380 let mut grid = HitGrid::new(10, 10);
1381 grid.register(
1383 Rect::new(10, 10, 5, 5),
1384 HitId::new(1),
1385 HitRegion::Content,
1386 0,
1387 );
1388 assert!(grid.hit_test(9, 9).is_none());
1389 }
1390
1391 #[test]
1392 fn hit_grid_full_coverage() {
1393 let mut grid = HitGrid::new(3, 3);
1394 grid.register(Rect::new(0, 0, 3, 3), HitId::new(1), HitRegion::Content, 0);
1395 for y in 0..3 {
1397 for x in 0..3 {
1398 assert_eq!(
1399 grid.hit_test(x, y),
1400 Some((HitId::new(1), HitRegion::Content, 0))
1401 );
1402 }
1403 }
1404 }
1405
1406 #[test]
1407 fn hit_grid_single_cell() {
1408 let mut grid = HitGrid::new(1, 1);
1409 grid.register(Rect::new(0, 0, 1, 1), HitId::new(1), HitRegion::Button, 42);
1410 assert_eq!(
1411 grid.hit_test(0, 0),
1412 Some((HitId::new(1), HitRegion::Button, 42))
1413 );
1414 assert!(grid.hit_test(1, 0).is_none());
1415 assert!(grid.hit_test(0, 1).is_none());
1416 }
1417
1418 #[test]
1419 fn hit_grid_hits_in_outside_rect() {
1420 let mut grid = HitGrid::new(5, 5);
1421 grid.register(Rect::new(0, 0, 2, 2), HitId::new(1), HitRegion::Content, 0);
1422 let hits = grid.hits_in(Rect::new(3, 3, 2, 2));
1424 assert!(hits.is_empty());
1425 }
1426
1427 #[test]
1428 fn hit_grid_hits_in_zero_rect() {
1429 let mut grid = HitGrid::new(5, 5);
1430 grid.register(Rect::new(0, 0, 5, 5), HitId::new(1), HitRegion::Content, 0);
1431 let hits = grid.hits_in(Rect::new(2, 2, 0, 0));
1432 assert!(hits.is_empty());
1433 }
1434
1435 #[test]
1438 fn cost_estimate_source_traits() {
1439 let a = CostEstimateSource::Measured;
1440 let b = CostEstimateSource::AreaFallback;
1441 let c = CostEstimateSource::FixedDefault;
1442 let dbg = format!("{:?}", a);
1443 assert!(dbg.contains("Measured"), "Debug: {dbg}");
1444
1445 assert_eq!(
1447 CostEstimateSource::default(),
1448 CostEstimateSource::FixedDefault
1449 );
1450
1451 let copied: CostEstimateSource = a;
1453 assert_eq!(a, copied);
1454
1455 assert_ne!(a, b);
1457 assert_ne!(b, c);
1458 assert_ne!(a, c);
1459 }
1460
1461 #[test]
1464 fn widget_signal_default() {
1465 let sig = WidgetSignal::default();
1466 assert_eq!(sig.widget_id, 0);
1467 assert!(!sig.essential);
1468 assert!((sig.priority - 0.5).abs() < f32::EPSILON);
1469 assert_eq!(sig.staleness_ms, 0);
1470 assert!((sig.focus_boost - 0.0).abs() < f32::EPSILON);
1471 assert!((sig.interaction_boost - 0.0).abs() < f32::EPSILON);
1472 assert_eq!(sig.area_cells, 1);
1473 assert!((sig.cost_estimate_us - 5.0).abs() < f32::EPSILON);
1474 assert!((sig.recent_cost_us - 5.0).abs() < f32::EPSILON);
1475 assert_eq!(sig.estimate_source, CostEstimateSource::FixedDefault);
1476 }
1477
1478 #[test]
1479 fn widget_signal_new() {
1480 let sig = WidgetSignal::new(42);
1481 assert_eq!(sig.widget_id, 42);
1482 assert!(!sig.essential);
1484 assert!((sig.priority - 0.5).abs() < f32::EPSILON);
1485 }
1486
1487 #[test]
1488 fn widget_signal_debug_clone() {
1489 let sig = WidgetSignal::new(7);
1490 let dbg = format!("{:?}", sig);
1491 assert!(dbg.contains("widget_id"), "Debug: {dbg}");
1492 let cloned = sig.clone();
1493 assert_eq!(cloned.widget_id, 7);
1494 }
1495
1496 #[test]
1499 fn widget_budget_default_is_allow_all() {
1500 let budget = WidgetBudget::default();
1501 assert!(budget.allows(0, false));
1502 assert!(budget.allows(u64::MAX, false));
1503 assert!(budget.allows(42, true));
1504 }
1505
1506 #[test]
1507 fn widget_budget_allow_only() {
1508 let budget = WidgetBudget::allow_only(vec![10, 20, 30]);
1509 assert!(budget.allows(10, false));
1510 assert!(budget.allows(20, false));
1511 assert!(budget.allows(30, false));
1512 assert!(!budget.allows(15, false));
1513 assert!(!budget.allows(0, false));
1514 }
1515
1516 #[test]
1517 fn widget_budget_essential_always_allowed() {
1518 let budget = WidgetBudget::allow_only(vec![10]);
1519 assert!(budget.allows(999, true));
1521 assert!(budget.allows(0, true));
1522 }
1523
1524 #[test]
1525 fn widget_budget_allow_only_dedup() {
1526 let budget = WidgetBudget::allow_only(vec![5, 5, 5, 10, 10]);
1527 assert!(budget.allows(5, false));
1528 assert!(budget.allows(10, false));
1529 assert!(!budget.allows(7, false));
1530 }
1531
1532 #[test]
1533 fn widget_budget_allow_only_empty() {
1534 let budget = WidgetBudget::allow_only(vec![]);
1535 assert!(!budget.allows(0, false));
1537 assert!(!budget.allows(1, false));
1538 assert!(budget.allows(1, true)); }
1540
1541 #[test]
1542 fn widget_budget_debug_clone() {
1543 let budget = WidgetBudget::allow_only(vec![1, 2, 3]);
1544 let dbg = format!("{:?}", budget);
1545 assert!(dbg.contains("allow_list"), "Debug: {dbg}");
1546 let cloned = budget.clone();
1547 assert!(cloned.allows(2, false));
1548 }
1549
1550 #[test]
1553 #[should_panic(expected = "buffer width must be > 0")]
1554 fn frame_zero_dimensions_panics() {
1555 let mut pool = GraphemePool::new();
1556 let _frame = Frame::new(0, 0, &mut pool);
1557 }
1558
1559 #[test]
1560 fn frame_from_buffer() {
1561 let mut pool = GraphemePool::new();
1562 let mut buf = Buffer::new(20, 10);
1563 buf.set_raw(5, 5, Cell::from_char('Z'));
1564 let frame = Frame::from_buffer(buf, &mut pool);
1565 assert_eq!(frame.width(), 20);
1566 assert_eq!(frame.height(), 10);
1567 assert_eq!(frame.buffer.get(5, 5).unwrap().content.as_char(), Some('Z'));
1568 assert!(frame.hit_grid.is_none());
1569 assert!(frame.cursor_visible);
1570 }
1571
1572 #[test]
1573 fn frame_with_links() {
1574 let mut pool = GraphemePool::new();
1575 let mut links = LinkRegistry::new();
1576 let frame = Frame::with_links(10, 5, &mut pool, &mut links);
1577 assert!(frame.links.is_some());
1578 assert_eq!(frame.width(), 10);
1579 assert_eq!(frame.height(), 5);
1580 }
1581
1582 #[test]
1583 fn frame_set_links() {
1584 let mut pool = GraphemePool::new();
1585 let mut links = LinkRegistry::new();
1586 let mut frame = Frame::new(10, 5, &mut pool);
1587 assert!(frame.links.is_none());
1588 frame.set_links(&mut links);
1589 assert!(frame.links.is_some());
1590 }
1591
1592 #[test]
1593 fn frame_register_link_no_registry() {
1594 let mut pool = GraphemePool::new();
1595 let mut frame = Frame::new(10, 5, &mut pool);
1596 let id = frame.register_link("https://example.com");
1598 assert_eq!(id, 0);
1599 }
1600
1601 #[test]
1602 fn frame_register_link_with_registry() {
1603 let mut pool = GraphemePool::new();
1604 let mut links = LinkRegistry::new();
1605 let mut frame = Frame::with_links(10, 5, &mut pool, &mut links);
1606 let id = frame.register_link("https://example.com");
1607 assert!(id > 0);
1608 let id2 = frame.register_link("https://example.com");
1610 assert_eq!(id, id2);
1611 let id3 = frame.register_link("https://other.com");
1613 assert_ne!(id, id3);
1614 }
1615
1616 #[test]
1619 fn frame_set_widget_budget() {
1620 let mut pool = GraphemePool::new();
1621 let mut frame = Frame::new(10, 10, &mut pool);
1622
1623 assert!(frame.should_render_widget(42, false));
1625
1626 frame.set_widget_budget(WidgetBudget::allow_only(vec![1, 2]));
1628 assert!(frame.should_render_widget(1, false));
1629 assert!(!frame.should_render_widget(42, false));
1630 assert!(frame.should_render_widget(42, true)); }
1632
1633 #[test]
1636 fn frame_widget_signals_lifecycle() {
1637 let mut pool = GraphemePool::new();
1638 let mut frame = Frame::new(10, 10, &mut pool);
1639 assert!(frame.widget_signals().is_empty());
1640
1641 frame.register_widget_signal(WidgetSignal::new(1));
1642 frame.register_widget_signal(WidgetSignal::new(2));
1643 assert_eq!(frame.widget_signals().len(), 2);
1644 assert_eq!(frame.widget_signals()[0].widget_id, 1);
1645 assert_eq!(frame.widget_signals()[1].widget_id, 2);
1646
1647 let taken = frame.take_widget_signals();
1648 assert_eq!(taken.len(), 2);
1649 assert!(frame.widget_signals().is_empty());
1650 }
1651
1652 #[test]
1653 fn frame_clear_resets_signals_and_cursor() {
1654 let mut pool = GraphemePool::new();
1655 let mut frame = Frame::new(10, 10, &mut pool);
1656 frame.set_cursor(Some((5, 5)));
1657 frame.register_widget_signal(WidgetSignal::new(1));
1658 assert!(frame.cursor_position.is_some());
1659 assert!(!frame.widget_signals().is_empty());
1660
1661 frame.clear();
1662 assert!(frame.cursor_position.is_none());
1663 assert!(frame.widget_signals().is_empty());
1664 }
1665
1666 #[test]
1669 fn frame_set_degradation_propagates_to_buffer() {
1670 let mut pool = GraphemePool::new();
1671 let mut frame = Frame::new(10, 10, &mut pool);
1672 assert_eq!(frame.degradation, DegradationLevel::Full);
1673 assert_eq!(frame.buffer.degradation, DegradationLevel::Full);
1674
1675 frame.set_degradation(DegradationLevel::SimpleBorders);
1676 assert_eq!(frame.degradation, DegradationLevel::SimpleBorders);
1677 assert_eq!(frame.buffer.degradation, DegradationLevel::SimpleBorders);
1678
1679 frame.set_degradation(DegradationLevel::EssentialOnly);
1680 assert_eq!(frame.degradation, DegradationLevel::EssentialOnly);
1681 assert_eq!(frame.buffer.degradation, DegradationLevel::EssentialOnly);
1682 }
1683
1684 #[test]
1687 #[should_panic(expected = "buffer width must be > 0")]
1688 fn frame_with_hit_grid_zero_size_panics() {
1689 let mut pool = GraphemePool::new();
1690 let _frame = Frame::with_hit_grid(0, 0, &mut pool);
1691 }
1692
1693 #[test]
1696 fn frame_register_hit_with_all_regions() {
1697 let mut pool = GraphemePool::new();
1698 let mut frame = Frame::with_hit_grid(20, 20, &mut pool);
1699 let regions = [
1700 HitRegion::Content,
1701 HitRegion::Border,
1702 HitRegion::Scrollbar,
1703 HitRegion::Handle,
1704 HitRegion::Button,
1705 HitRegion::Link,
1706 HitRegion::Custom(0),
1707 HitRegion::Custom(255),
1708 ];
1709 for (i, ®ion) in regions.iter().enumerate() {
1710 let y = i as u16;
1711 frame.register_hit(Rect::new(0, y, 1, 1), HitId::new(i as u32), region, 0);
1712 }
1713 for (i, ®ion) in regions.iter().enumerate() {
1714 let y = i as u16;
1715 assert_eq!(
1716 frame.hit_test(0, y),
1717 Some((HitId::new(i as u32), region, 0))
1718 );
1719 }
1720 }
1721
1722 #[test]
1725 fn frame_draw_horizontal_line() {
1726 let mut pool = GraphemePool::new();
1727 let mut frame = Frame::new(10, 5, &mut pool);
1728 let cell = Cell::from_char('-');
1729 frame.draw_horizontal_line(2, 1, 5, cell);
1730 for x in 2..7 {
1731 assert_eq!(frame.buffer.get(x, 1).unwrap().content.as_char(), Some('-'));
1732 }
1733 assert!(frame.buffer.get(1, 1).unwrap().is_empty());
1735 assert!(frame.buffer.get(7, 1).unwrap().is_empty());
1736 }
1737
1738 #[test]
1739 fn frame_draw_vertical_line() {
1740 let mut pool = GraphemePool::new();
1741 let mut frame = Frame::new(10, 10, &mut pool);
1742 let cell = Cell::from_char('|');
1743 frame.draw_vertical_line(3, 2, 4, cell);
1744 for y in 2..6 {
1745 assert_eq!(frame.buffer.get(3, y).unwrap().content.as_char(), Some('|'));
1746 }
1747 assert!(frame.buffer.get(3, 1).unwrap().is_empty());
1748 assert!(frame.buffer.get(3, 6).unwrap().is_empty());
1749 }
1750
1751 #[test]
1752 fn frame_draw_rect_filled() {
1753 let mut pool = GraphemePool::new();
1754 let mut frame = Frame::new(10, 10, &mut pool);
1755 let cell = Cell::from_char('#');
1756 frame.draw_rect_filled(Rect::new(1, 1, 3, 3), cell);
1757 for y in 1..4 {
1758 for x in 1..4 {
1759 assert_eq!(frame.buffer.get(x, y).unwrap().content.as_char(), Some('#'));
1760 }
1761 }
1762 assert!(frame.buffer.get(0, 0).unwrap().is_empty());
1764 assert!(frame.buffer.get(4, 4).unwrap().is_empty());
1765 }
1766
1767 #[test]
1768 fn frame_paint_area() {
1769 use crate::cell::PackedRgba;
1770 let mut pool = GraphemePool::new();
1771 let mut frame = Frame::new(5, 5, &mut pool);
1772 let red = PackedRgba::rgb(255, 0, 0);
1773 frame.paint_area(Rect::new(0, 0, 2, 2), Some(red), None);
1774 let cell = frame.buffer.get(0, 0).unwrap();
1775 assert_eq!(cell.fg, red);
1776 }
1777
1778 #[test]
1781 fn frame_print_text_clipped_at_boundary() {
1782 let mut pool = GraphemePool::new();
1783 let mut frame = Frame::new(5, 1, &mut pool);
1784 let end = frame.print_text(0, 0, "Hello World", Cell::from_char(' '));
1786 assert_eq!(end, 5);
1787 for x in 0..5 {
1788 assert!(!frame.buffer.get(x, 0).unwrap().is_empty());
1789 }
1790 }
1791
1792 #[test]
1793 fn frame_print_text_empty_string() {
1794 let mut pool = GraphemePool::new();
1795 let mut frame = Frame::new(10, 1, &mut pool);
1796 let end = frame.print_text(0, 0, "", Cell::from_char(' '));
1797 assert_eq!(end, 0);
1798 }
1799
1800 #[test]
1801 fn frame_print_text_at_right_edge() {
1802 let mut pool = GraphemePool::new();
1803 let mut frame = Frame::new(5, 1, &mut pool);
1804 let end = frame.print_text(4, 0, "AB", Cell::from_char(' '));
1806 assert_eq!(end, 5);
1807 assert_eq!(frame.buffer.get(4, 0).unwrap().content.as_char(), Some('A'));
1808 }
1809
1810 #[test]
1813 fn frame_debug() {
1814 let mut pool = GraphemePool::new();
1815 let frame = Frame::new(5, 3, &mut pool);
1816 let dbg = format!("{:?}", frame);
1817 assert!(dbg.contains("Frame"), "Debug: {dbg}");
1818 }
1819
1820 #[test]
1823 fn hit_grid_debug() {
1824 let grid = HitGrid::new(3, 3);
1825 let dbg = format!("{:?}", grid);
1826 assert!(dbg.contains("HitGrid"), "Debug: {dbg}");
1827 }
1828
1829 #[test]
1832 fn frame_cursor_beyond_bounds() {
1833 let mut pool = GraphemePool::new();
1834 let mut frame = Frame::new(10, 10, &mut pool);
1835 frame.set_cursor(Some((100, 200)));
1837 assert_eq!(frame.cursor_position, Some((100, 200)));
1838 }
1839
1840 #[test]
1843 fn hit_grid_register_overwrite() {
1844 let mut grid = HitGrid::new(5, 5);
1845 grid.register(Rect::new(0, 0, 3, 3), HitId::new(1), HitRegion::Content, 10);
1846 grid.register(Rect::new(0, 0, 3, 3), HitId::new(2), HitRegion::Button, 20);
1847 assert_eq!(
1849 grid.hit_test(1, 1),
1850 Some((HitId::new(2), HitRegion::Button, 20))
1851 );
1852 }
1853}