1#![forbid(unsafe_code)]
2
3use crate::budget::DegradationLevel;
33use crate::buffer::Buffer;
34use crate::cell::{Cell, CellContent, GraphemeId};
35use crate::drawing::{BorderChars, Draw};
36use crate::grapheme_pool::GraphemePool;
37use crate::{display_width, grapheme_width};
38use ftui_core::geometry::Rect;
39use unicode_segmentation::UnicodeSegmentation;
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
45pub struct HitId(pub u32);
46
47impl HitId {
48 #[inline]
50 pub const fn new(id: u32) -> Self {
51 Self(id)
52 }
53
54 #[inline]
56 pub const fn id(self) -> u32 {
57 self.0
58 }
59}
60
61pub type HitData = u64;
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
66pub enum HitRegion {
67 #[default]
69 None,
70 Content,
72 Border,
74 Scrollbar,
76 Handle,
78 Button,
80 Link,
82 Custom(u8),
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
88pub struct HitCell {
89 pub widget_id: Option<HitId>,
91 pub region: HitRegion,
93 pub data: HitData,
95}
96
97impl HitCell {
98 #[inline]
100 pub const fn new(widget_id: HitId, region: HitRegion, data: HitData) -> Self {
101 Self {
102 widget_id: Some(widget_id),
103 region,
104 data,
105 }
106 }
107
108 #[inline]
110 pub const fn is_empty(&self) -> bool {
111 self.widget_id.is_none()
112 }
113}
114
115#[derive(Debug, Clone)]
120pub struct HitGrid {
121 width: u16,
122 height: u16,
123 cells: Vec<HitCell>,
124}
125
126impl HitGrid {
127 pub fn new(width: u16, height: u16) -> Self {
129 let size = width as usize * height as usize;
130 Self {
131 width,
132 height,
133 cells: vec![HitCell::default(); size],
134 }
135 }
136
137 #[inline]
139 pub const fn width(&self) -> u16 {
140 self.width
141 }
142
143 #[inline]
145 pub const fn height(&self) -> u16 {
146 self.height
147 }
148
149 #[inline]
151 fn index(&self, x: u16, y: u16) -> Option<usize> {
152 if x < self.width && y < self.height {
153 Some(y as usize * self.width as usize + x as usize)
154 } else {
155 None
156 }
157 }
158
159 #[inline]
161 pub fn get(&self, x: u16, y: u16) -> Option<&HitCell> {
162 self.index(x, y).map(|i| &self.cells[i])
163 }
164
165 #[inline]
167 pub fn get_mut(&mut self, x: u16, y: u16) -> Option<&mut HitCell> {
168 self.index(x, y).map(|i| &mut self.cells[i])
169 }
170
171 pub fn register(&mut self, rect: Rect, widget_id: HitId, region: HitRegion, data: HitData) {
175 let x_end = (rect.x as usize + rect.width as usize).min(self.width as usize);
177 let y_end = (rect.y as usize + rect.height as usize).min(self.height as usize);
178
179 if rect.x as usize >= x_end || rect.y as usize >= y_end {
181 return;
182 }
183
184 let hit_cell = HitCell::new(widget_id, region, data);
185
186 for y in rect.y as usize..y_end {
187 let row_start = y * self.width as usize;
188 let start = row_start + rect.x as usize;
189 let end = row_start + x_end;
190
191 self.cells[start..end].fill(hit_cell);
193 }
194 }
195
196 pub fn hit_test(&self, x: u16, y: u16) -> Option<(HitId, HitRegion, HitData)> {
200 self.get(x, y)
201 .and_then(|cell| cell.widget_id.map(|id| (id, cell.region, cell.data)))
202 }
203
204 pub fn hits_in(&self, rect: Rect) -> Vec<(HitId, HitRegion, HitData)> {
206 let x_end = (rect.x as usize + rect.width as usize).min(self.width as usize) as u16;
207 let y_end = (rect.y as usize + rect.height as usize).min(self.height as usize) as u16;
208 let mut hits = Vec::new();
209
210 for y in rect.y..y_end {
211 for x in rect.x..x_end {
212 if let Some((id, region, data)) = self.hit_test(x, y) {
213 hits.push((id, region, data));
214 }
215 }
216 }
217
218 hits
219 }
220
221 pub fn clear(&mut self) {
223 self.cells.fill(HitCell::default());
224 }
225}
226
227use crate::link_registry::LinkRegistry;
228
229#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
231pub enum CostEstimateSource {
232 Measured,
234 AreaFallback,
236 #[default]
238 FixedDefault,
239}
240
241#[derive(Debug, Clone)]
246pub struct WidgetSignal {
247 pub widget_id: u64,
249 pub essential: bool,
251 pub priority: f32,
253 pub staleness_ms: u64,
255 pub focus_boost: f32,
257 pub interaction_boost: f32,
259 pub area_cells: u32,
261 pub cost_estimate_us: f32,
263 pub recent_cost_us: f32,
265 pub estimate_source: CostEstimateSource,
267}
268
269impl Default for WidgetSignal {
270 fn default() -> Self {
271 Self {
272 widget_id: 0,
273 essential: false,
274 priority: 0.5,
275 staleness_ms: 0,
276 focus_boost: 0.0,
277 interaction_boost: 0.0,
278 area_cells: 1,
279 cost_estimate_us: 5.0,
280 recent_cost_us: 5.0,
281 estimate_source: CostEstimateSource::FixedDefault,
282 }
283 }
284}
285
286impl WidgetSignal {
287 #[must_use]
289 pub fn new(widget_id: u64) -> Self {
290 Self {
291 widget_id,
292 ..Self::default()
293 }
294 }
295}
296
297#[derive(Debug, Clone)]
299pub struct WidgetBudget {
300 allow_list: Option<Vec<u64>>,
301}
302
303impl Default for WidgetBudget {
304 fn default() -> Self {
305 Self::allow_all()
306 }
307}
308
309impl WidgetBudget {
310 #[must_use]
312 pub fn allow_all() -> Self {
313 Self { allow_list: None }
314 }
315
316 #[must_use]
318 pub fn allow_only(mut ids: Vec<u64>) -> Self {
319 ids.sort_unstable();
320 ids.dedup();
321 Self {
322 allow_list: Some(ids),
323 }
324 }
325
326 #[inline]
328 pub fn allows(&self, widget_id: u64, essential: bool) -> bool {
329 if essential {
330 return true;
331 }
332 match &self.allow_list {
333 None => true,
334 Some(ids) => ids.binary_search(&widget_id).is_ok(),
335 }
336 }
337}
338
339#[derive(Debug)]
350pub struct Frame<'a> {
351 pub buffer: Buffer,
353
354 pub pool: &'a mut GraphemePool,
356
357 pub links: Option<&'a mut LinkRegistry>,
359
360 pub hit_grid: Option<HitGrid>,
364
365 pub widget_budget: WidgetBudget,
367
368 pub widget_signals: Vec<WidgetSignal>,
370
371 pub cursor_position: Option<(u16, u16)>,
375
376 pub cursor_visible: bool,
378
379 pub degradation: DegradationLevel,
385}
386
387impl<'a> Frame<'a> {
388 pub fn new(width: u16, height: u16, pool: &'a mut GraphemePool) -> Self {
392 Self {
393 buffer: Buffer::new(width, height),
394 pool,
395 links: None,
396 hit_grid: None,
397 widget_budget: WidgetBudget::default(),
398 widget_signals: Vec::new(),
399 cursor_position: None,
400 cursor_visible: true,
401 degradation: DegradationLevel::Full,
402 }
403 }
404
405 pub fn from_buffer(buffer: Buffer, pool: &'a mut GraphemePool) -> Self {
409 Self {
410 buffer,
411 pool,
412 links: None,
413 hit_grid: None,
414 widget_budget: WidgetBudget::default(),
415 widget_signals: Vec::new(),
416 cursor_position: None,
417 cursor_visible: true,
418 degradation: DegradationLevel::Full,
419 }
420 }
421
422 pub fn with_links(
427 width: u16,
428 height: u16,
429 pool: &'a mut GraphemePool,
430 links: &'a mut LinkRegistry,
431 ) -> Self {
432 Self {
433 buffer: Buffer::new(width, height),
434 pool,
435 links: Some(links),
436 hit_grid: None,
437 widget_budget: WidgetBudget::default(),
438 widget_signals: Vec::new(),
439 cursor_position: None,
440 cursor_visible: true,
441 degradation: DegradationLevel::Full,
442 }
443 }
444
445 pub fn with_hit_grid(width: u16, height: u16, pool: &'a mut GraphemePool) -> Self {
449 Self {
450 buffer: Buffer::new(width, height),
451 pool,
452 links: None,
453 hit_grid: Some(HitGrid::new(width, height)),
454 widget_budget: WidgetBudget::default(),
455 widget_signals: Vec::new(),
456 cursor_position: None,
457 cursor_visible: true,
458 degradation: DegradationLevel::Full,
459 }
460 }
461
462 pub fn set_links(&mut self, links: &'a mut LinkRegistry) {
464 self.links = Some(links);
465 }
466
467 pub fn register_link(&mut self, url: &str) -> u32 {
471 if let Some(ref mut links) = self.links {
472 links.register(url)
473 } else {
474 0
475 }
476 }
477
478 pub fn set_widget_budget(&mut self, budget: WidgetBudget) {
480 self.widget_budget = budget;
481 }
482
483 #[inline]
485 pub fn should_render_widget(&self, widget_id: u64, essential: bool) -> bool {
486 self.widget_budget.allows(widget_id, essential)
487 }
488
489 pub fn register_widget_signal(&mut self, signal: WidgetSignal) {
491 self.widget_signals.push(signal);
492 }
493
494 #[inline]
496 pub fn widget_signals(&self) -> &[WidgetSignal] {
497 &self.widget_signals
498 }
499
500 #[inline]
502 pub fn take_widget_signals(&mut self) -> Vec<WidgetSignal> {
503 std::mem::take(&mut self.widget_signals)
504 }
505
506 pub fn intern(&mut self, text: &str) -> GraphemeId {
515 let width = display_width(text).min(127) as u8;
516 self.pool.intern(text, width)
517 }
518
519 pub fn intern_with_width(&mut self, text: &str, width: u8) -> GraphemeId {
521 self.pool.intern(text, width)
522 }
523
524 pub fn enable_hit_testing(&mut self) {
526 if self.hit_grid.is_none() {
527 self.hit_grid = Some(HitGrid::new(self.width(), self.height()));
528 }
529 }
530
531 #[inline]
533 pub fn width(&self) -> u16 {
534 self.buffer.width()
535 }
536
537 #[inline]
539 pub fn height(&self) -> u16 {
540 self.buffer.height()
541 }
542
543 pub fn clear(&mut self) {
547 self.buffer.clear();
548 if let Some(ref mut grid) = self.hit_grid {
549 grid.clear();
550 }
551 self.cursor_position = None;
552 self.widget_signals.clear();
553 }
554
555 #[inline]
559 pub fn set_cursor(&mut self, position: Option<(u16, u16)>) {
560 self.cursor_position = position;
561 }
562
563 #[inline]
565 pub fn set_cursor_visible(&mut self, visible: bool) {
566 self.cursor_visible = visible;
567 }
568
569 #[inline]
574 pub fn set_degradation(&mut self, level: DegradationLevel) {
575 self.degradation = level;
576 self.buffer.degradation = level;
577 }
578
579 #[inline]
581 pub fn bounds(&self) -> Rect {
582 self.buffer.bounds()
583 }
584
585 pub fn register_hit(
595 &mut self,
596 rect: Rect,
597 id: HitId,
598 region: HitRegion,
599 data: HitData,
600 ) -> bool {
601 if let Some(ref mut grid) = self.hit_grid {
602 let clipped = rect.intersection(&self.buffer.current_scissor());
604 if !clipped.is_empty() {
605 grid.register(clipped, id, region, data);
606 }
607 true
608 } else {
609 false
610 }
611 }
612
613 pub fn hit_test(&self, x: u16, y: u16) -> Option<(HitId, HitRegion, HitData)> {
615 self.hit_grid.as_ref().and_then(|grid| grid.hit_test(x, y))
616 }
617
618 pub fn register_hit_region(&mut self, rect: Rect, id: HitId) -> bool {
620 self.register_hit(rect, id, HitRegion::Content, 0)
621 }
622}
623
624impl<'a> Draw for Frame<'a> {
625 fn draw_horizontal_line(&mut self, x: u16, y: u16, width: u16, cell: Cell) {
626 self.buffer.draw_horizontal_line(x, y, width, cell);
627 }
628
629 fn draw_vertical_line(&mut self, x: u16, y: u16, height: u16, cell: Cell) {
630 self.buffer.draw_vertical_line(x, y, height, cell);
631 }
632
633 fn draw_rect_filled(&mut self, rect: Rect, cell: Cell) {
634 self.buffer.draw_rect_filled(rect, cell);
635 }
636
637 fn draw_rect_outline(&mut self, rect: Rect, cell: Cell) {
638 self.buffer.draw_rect_outline(rect, cell);
639 }
640
641 fn print_text(&mut self, x: u16, y: u16, text: &str, base_cell: Cell) -> u16 {
642 self.print_text_clipped(x, y, text, base_cell, self.width())
643 }
644
645 fn print_text_clipped(
646 &mut self,
647 x: u16,
648 y: u16,
649 text: &str,
650 base_cell: Cell,
651 max_x: u16,
652 ) -> u16 {
653 let mut cx = x;
654 for grapheme in text.graphemes(true) {
655 let width = grapheme_width(grapheme);
656 if width == 0 {
657 continue;
658 }
659
660 if cx >= max_x {
661 break;
662 }
663
664 if cx as u32 + width as u32 > max_x as u32 {
666 break;
667 }
668
669 let content = if width > 1 || grapheme.chars().count() > 1 {
671 let id = self.intern_with_width(grapheme, width as u8);
672 CellContent::from_grapheme(id)
673 } else if let Some(c) = grapheme.chars().next() {
674 CellContent::from_char(c)
675 } else {
676 continue;
677 };
678
679 let cell = Cell {
680 content,
681 fg: base_cell.fg,
682 bg: base_cell.bg,
683 attrs: base_cell.attrs,
684 };
685 self.buffer.set(cx, y, cell);
686
687 cx = cx.saturating_add(width as u16);
688 }
689 cx
690 }
691
692 fn draw_border(&mut self, rect: Rect, chars: BorderChars, base_cell: Cell) {
693 self.buffer.draw_border(rect, chars, base_cell);
694 }
695
696 fn draw_box(&mut self, rect: Rect, chars: BorderChars, border_cell: Cell, fill_cell: Cell) {
697 self.buffer.draw_box(rect, chars, border_cell, fill_cell);
698 }
699
700 fn paint_area(
701 &mut self,
702 rect: Rect,
703 fg: Option<crate::cell::PackedRgba>,
704 bg: Option<crate::cell::PackedRgba>,
705 ) {
706 self.buffer.paint_area(rect, fg, bg);
707 }
708}
709
710#[cfg(test)]
711mod tests {
712 use super::*;
713 use crate::cell::Cell;
714
715 #[test]
716 fn frame_creation() {
717 let mut pool = GraphemePool::new();
718 let frame = Frame::new(80, 24, &mut pool);
719 assert_eq!(frame.width(), 80);
720 assert_eq!(frame.height(), 24);
721 assert!(frame.hit_grid.is_none());
722 assert!(frame.cursor_position.is_none());
723 assert!(frame.cursor_visible);
724 }
725
726 #[test]
727 fn frame_with_hit_grid() {
728 let mut pool = GraphemePool::new();
729 let frame = Frame::with_hit_grid(80, 24, &mut pool);
730 assert!(frame.hit_grid.is_some());
731 assert_eq!(frame.width(), 80);
732 assert_eq!(frame.height(), 24);
733 }
734
735 #[test]
736 fn frame_cursor() {
737 let mut pool = GraphemePool::new();
738 let mut frame = Frame::new(80, 24, &mut pool);
739 assert!(frame.cursor_position.is_none());
740 assert!(frame.cursor_visible);
741
742 frame.set_cursor(Some((10, 5)));
743 assert_eq!(frame.cursor_position, Some((10, 5)));
744
745 frame.set_cursor_visible(false);
746 assert!(!frame.cursor_visible);
747
748 frame.set_cursor(None);
749 assert!(frame.cursor_position.is_none());
750 }
751
752 #[test]
753 fn frame_clear() {
754 let mut pool = GraphemePool::new();
755 let mut frame = Frame::with_hit_grid(10, 10, &mut pool);
756
757 frame.buffer.set_raw(5, 5, Cell::from_char('X'));
759 frame.register_hit_region(Rect::new(0, 0, 5, 5), HitId::new(1));
760
761 assert_eq!(frame.buffer.get(5, 5).unwrap().content.as_char(), Some('X'));
763 assert_eq!(
764 frame.hit_test(2, 2),
765 Some((HitId::new(1), HitRegion::Content, 0))
766 );
767
768 frame.clear();
770
771 assert!(frame.buffer.get(5, 5).unwrap().is_empty());
773 assert!(frame.hit_test(2, 2).is_none());
774 }
775
776 #[test]
777 fn frame_bounds() {
778 let mut pool = GraphemePool::new();
779 let frame = Frame::new(80, 24, &mut pool);
780 let bounds = frame.bounds();
781 assert_eq!(bounds.x, 0);
782 assert_eq!(bounds.y, 0);
783 assert_eq!(bounds.width, 80);
784 assert_eq!(bounds.height, 24);
785 }
786
787 #[test]
788 fn hit_grid_creation() {
789 let grid = HitGrid::new(80, 24);
790 assert_eq!(grid.width(), 80);
791 assert_eq!(grid.height(), 24);
792 }
793
794 #[test]
795 fn hit_grid_registration() {
796 let mut pool = GraphemePool::new();
797 let mut frame = Frame::with_hit_grid(80, 24, &mut pool);
798 let hit_id = HitId::new(42);
799 let rect = Rect::new(10, 5, 20, 3);
800
801 frame.register_hit(rect, hit_id, HitRegion::Button, 99);
802
803 assert_eq!(frame.hit_test(15, 6), Some((hit_id, HitRegion::Button, 99)));
805 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()); }
814
815 #[test]
816 fn hit_grid_overlapping_regions() {
817 let mut pool = GraphemePool::new();
818 let mut frame = Frame::with_hit_grid(20, 20, &mut pool);
819
820 frame.register_hit(
822 Rect::new(0, 0, 10, 10),
823 HitId::new(1),
824 HitRegion::Content,
825 1,
826 );
827 frame.register_hit(Rect::new(5, 5, 10, 10), HitId::new(2), HitRegion::Border, 2);
828
829 assert_eq!(
831 frame.hit_test(2, 2),
832 Some((HitId::new(1), HitRegion::Content, 1))
833 );
834
835 assert_eq!(
837 frame.hit_test(7, 7),
838 Some((HitId::new(2), HitRegion::Border, 2))
839 );
840
841 assert_eq!(
843 frame.hit_test(12, 12),
844 Some((HitId::new(2), HitRegion::Border, 2))
845 );
846 }
847
848 #[test]
849 fn hit_grid_out_of_bounds() {
850 let mut pool = GraphemePool::new();
851 let frame = Frame::with_hit_grid(10, 10, &mut pool);
852
853 assert!(frame.hit_test(100, 100).is_none());
855 assert!(frame.hit_test(10, 0).is_none()); assert!(frame.hit_test(0, 10).is_none()); }
858
859 #[test]
860 fn hit_id_properties() {
861 let id = HitId::new(42);
862 assert_eq!(id.id(), 42);
863 assert_eq!(id, HitId(42));
864 }
865
866 #[test]
867 fn register_hit_region_no_grid() {
868 let mut pool = GraphemePool::new();
869 let mut frame = Frame::new(10, 10, &mut pool);
870 let result = frame.register_hit_region(Rect::new(0, 0, 5, 5), HitId::new(1));
871 assert!(!result); }
873
874 #[test]
875 fn register_hit_region_with_grid() {
876 let mut pool = GraphemePool::new();
877 let mut frame = Frame::with_hit_grid(10, 10, &mut pool);
878 let result = frame.register_hit_region(Rect::new(0, 0, 5, 5), HitId::new(1));
879 assert!(result); }
881
882 #[test]
883 fn hit_grid_clear() {
884 let mut grid = HitGrid::new(10, 10);
885 grid.register(Rect::new(0, 0, 5, 5), HitId::new(1), HitRegion::Content, 0);
886
887 assert_eq!(
888 grid.hit_test(2, 2),
889 Some((HitId::new(1), HitRegion::Content, 0))
890 );
891
892 grid.clear();
893
894 assert!(grid.hit_test(2, 2).is_none());
895 }
896
897 #[test]
898 fn hit_grid_boundary_clipping() {
899 let mut grid = HitGrid::new(10, 10);
900
901 grid.register(
903 Rect::new(8, 8, 10, 10),
904 HitId::new(1),
905 HitRegion::Content,
906 0,
907 );
908
909 assert_eq!(
911 grid.hit_test(9, 9),
912 Some((HitId::new(1), HitRegion::Content, 0))
913 );
914
915 assert!(grid.hit_test(10, 10).is_none());
917 }
918
919 #[test]
920 fn hit_grid_edge_and_corner_cells() {
921 let mut grid = HitGrid::new(4, 4);
922 grid.register(Rect::new(3, 0, 1, 4), HitId::new(7), HitRegion::Border, 11);
923
924 assert_eq!(
926 grid.hit_test(3, 0),
927 Some((HitId::new(7), HitRegion::Border, 11))
928 );
929 assert_eq!(
930 grid.hit_test(3, 3),
931 Some((HitId::new(7), HitRegion::Border, 11))
932 );
933
934 assert!(grid.hit_test(2, 0).is_none());
936 assert!(grid.hit_test(4, 0).is_none());
937 assert!(grid.hit_test(3, 4).is_none());
938
939 let mut grid = HitGrid::new(4, 4);
940 grid.register(Rect::new(0, 3, 4, 1), HitId::new(9), HitRegion::Content, 21);
941
942 assert_eq!(
944 grid.hit_test(0, 3),
945 Some((HitId::new(9), HitRegion::Content, 21))
946 );
947 assert_eq!(
948 grid.hit_test(3, 3),
949 Some((HitId::new(9), HitRegion::Content, 21))
950 );
951
952 assert!(grid.hit_test(0, 2).is_none());
954 assert!(grid.hit_test(0, 4).is_none());
955 }
956
957 #[test]
958 fn frame_register_hit_respects_nested_scissor() {
959 let mut pool = GraphemePool::new();
960 let mut frame = Frame::with_hit_grid(10, 10, &mut pool);
961
962 let outer = Rect::new(1, 1, 8, 8);
963 frame.buffer.push_scissor(outer);
964 assert_eq!(frame.buffer.current_scissor(), outer);
965
966 let inner = Rect::new(4, 4, 10, 10);
967 frame.buffer.push_scissor(inner);
968 let clipped = outer.intersection(&inner);
969 let current = frame.buffer.current_scissor();
970 assert_eq!(current, clipped);
971
972 assert!(outer.contains(current.x, current.y));
974 assert!(outer.contains(
975 current.right().saturating_sub(1),
976 current.bottom().saturating_sub(1)
977 ));
978
979 frame.register_hit(
980 Rect::new(0, 0, 10, 10),
981 HitId::new(3),
982 HitRegion::Button,
983 99,
984 );
985
986 assert_eq!(
987 frame.hit_test(4, 4),
988 Some((HitId::new(3), HitRegion::Button, 99))
989 );
990 assert_eq!(
991 frame.hit_test(8, 8),
992 Some((HitId::new(3), HitRegion::Button, 99))
993 );
994 assert!(frame.hit_test(3, 3).is_none()); assert!(frame.hit_test(0, 0).is_none()); frame.buffer.pop_scissor();
998 assert_eq!(frame.buffer.current_scissor(), outer);
999 }
1000
1001 #[test]
1002 fn hit_grid_hits_in_area() {
1003 let mut grid = HitGrid::new(5, 5);
1004 grid.register(Rect::new(0, 0, 2, 2), HitId::new(1), HitRegion::Content, 10);
1005 grid.register(Rect::new(1, 1, 2, 2), HitId::new(2), HitRegion::Button, 20);
1006
1007 let hits = grid.hits_in(Rect::new(0, 0, 3, 3));
1008 assert!(hits.contains(&(HitId::new(1), HitRegion::Content, 10)));
1009 assert!(hits.contains(&(HitId::new(2), HitRegion::Button, 20)));
1010 }
1011
1012 #[test]
1013 fn frame_intern() {
1014 let mut pool = GraphemePool::new();
1015 let mut frame = Frame::new(10, 10, &mut pool);
1016
1017 let id = frame.intern("π");
1018 assert_eq!(frame.pool.get(id), Some("π"));
1019 }
1020
1021 #[test]
1022 fn frame_intern_with_width() {
1023 let mut pool = GraphemePool::new();
1024 let mut frame = Frame::new(10, 10, &mut pool);
1025
1026 let id = frame.intern_with_width("π§ͺ", 2);
1027 assert_eq!(id.width(), 2);
1028 assert_eq!(frame.pool.get(id), Some("π§ͺ"));
1029 }
1030
1031 #[test]
1032 fn frame_print_text_emoji_presentation_sets_continuation() {
1033 let mut pool = GraphemePool::new();
1034 let mut frame = Frame::new(5, 1, &mut pool);
1035
1036 frame.print_text(0, 0, "βοΈ", Cell::from_char(' '));
1037
1038 let head = frame.buffer.get(0, 0).unwrap();
1039 let tail = frame.buffer.get(1, 0).unwrap();
1040
1041 assert_eq!(head.content.width(), 2);
1042 assert!(tail.content.is_continuation());
1043 }
1044
1045 #[test]
1046 fn frame_enable_hit_testing() {
1047 let mut pool = GraphemePool::new();
1048 let mut frame = Frame::new(10, 10, &mut pool);
1049 assert!(frame.hit_grid.is_none());
1050
1051 frame.enable_hit_testing();
1052 assert!(frame.hit_grid.is_some());
1053
1054 frame.enable_hit_testing();
1056 assert!(frame.hit_grid.is_some());
1057 }
1058
1059 #[test]
1060 fn frame_enable_hit_testing_then_register() {
1061 let mut pool = GraphemePool::new();
1062 let mut frame = Frame::new(10, 10, &mut pool);
1063 frame.enable_hit_testing();
1064
1065 let registered = frame.register_hit_region(Rect::new(0, 0, 5, 5), HitId::new(1));
1066 assert!(registered);
1067 assert_eq!(
1068 frame.hit_test(2, 2),
1069 Some((HitId::new(1), HitRegion::Content, 0))
1070 );
1071 }
1072
1073 #[test]
1074 fn hit_cell_default_is_empty() {
1075 let cell = HitCell::default();
1076 assert!(cell.is_empty());
1077 assert_eq!(cell.widget_id, None);
1078 assert_eq!(cell.region, HitRegion::None);
1079 assert_eq!(cell.data, 0);
1080 }
1081
1082 #[test]
1083 fn hit_cell_new_is_not_empty() {
1084 let cell = HitCell::new(HitId::new(1), HitRegion::Button, 42);
1085 assert!(!cell.is_empty());
1086 assert_eq!(cell.widget_id, Some(HitId::new(1)));
1087 assert_eq!(cell.region, HitRegion::Button);
1088 assert_eq!(cell.data, 42);
1089 }
1090
1091 #[test]
1092 fn hit_region_variants() {
1093 assert_eq!(HitRegion::default(), HitRegion::None);
1094
1095 let variants = [
1097 HitRegion::None,
1098 HitRegion::Content,
1099 HitRegion::Border,
1100 HitRegion::Scrollbar,
1101 HitRegion::Handle,
1102 HitRegion::Button,
1103 HitRegion::Link,
1104 HitRegion::Custom(0),
1105 HitRegion::Custom(1),
1106 HitRegion::Custom(255),
1107 ];
1108 for i in 0..variants.len() {
1109 for j in (i + 1)..variants.len() {
1110 assert_ne!(
1111 variants[i], variants[j],
1112 "variants {i} and {j} should differ"
1113 );
1114 }
1115 }
1116 }
1117
1118 #[test]
1119 fn hit_id_default() {
1120 let id = HitId::default();
1121 assert_eq!(id.id(), 0);
1122 }
1123
1124 #[test]
1125 fn hit_grid_initial_cells_empty() {
1126 let grid = HitGrid::new(5, 5);
1127 for y in 0..5 {
1128 for x in 0..5 {
1129 let cell = grid.get(x, y).unwrap();
1130 assert!(cell.is_empty());
1131 }
1132 }
1133 }
1134
1135 #[test]
1136 fn hit_grid_zero_dimensions() {
1137 let grid = HitGrid::new(0, 0);
1138 assert_eq!(grid.width(), 0);
1139 assert_eq!(grid.height(), 0);
1140 assert!(grid.get(0, 0).is_none());
1141 assert!(grid.hit_test(0, 0).is_none());
1142 }
1143
1144 #[test]
1145 fn hit_grid_hits_in_empty_area() {
1146 let grid = HitGrid::new(10, 10);
1147 let hits = grid.hits_in(Rect::new(0, 0, 5, 5));
1148 assert!(hits.is_empty());
1150 }
1151
1152 #[test]
1153 fn hit_grid_hits_in_clipped_area() {
1154 let mut grid = HitGrid::new(5, 5);
1155 grid.register(Rect::new(0, 0, 5, 5), HitId::new(1), HitRegion::Content, 0);
1156
1157 let hits = grid.hits_in(Rect::new(3, 3, 10, 10));
1159 assert_eq!(hits.len(), 4); }
1161
1162 #[test]
1163 fn hit_test_no_grid_returns_none() {
1164 let mut pool = GraphemePool::new();
1165 let frame = Frame::new(10, 10, &mut pool);
1166 assert!(frame.hit_test(0, 0).is_none());
1167 }
1168
1169 #[test]
1170 fn frame_cursor_operations() {
1171 let mut pool = GraphemePool::new();
1172 let mut frame = Frame::new(80, 24, &mut pool);
1173
1174 frame.set_cursor(Some((79, 23)));
1176 assert_eq!(frame.cursor_position, Some((79, 23)));
1177
1178 frame.set_cursor(Some((0, 0)));
1180 assert_eq!(frame.cursor_position, Some((0, 0)));
1181
1182 frame.set_cursor_visible(false);
1184 assert!(!frame.cursor_visible);
1185 frame.set_cursor_visible(true);
1186 assert!(frame.cursor_visible);
1187 }
1188
1189 #[test]
1190 fn hit_data_large_values() {
1191 let mut grid = HitGrid::new(5, 5);
1192 grid.register(
1194 Rect::new(0, 0, 1, 1),
1195 HitId::new(1),
1196 HitRegion::Content,
1197 u64::MAX,
1198 );
1199 let result = grid.hit_test(0, 0);
1200 assert_eq!(result, Some((HitId::new(1), HitRegion::Content, u64::MAX)));
1201 }
1202
1203 #[test]
1204 fn hit_id_large_value() {
1205 let id = HitId::new(u32::MAX);
1206 assert_eq!(id.id(), u32::MAX);
1207 }
1208
1209 #[test]
1210 fn frame_print_text_interns_complex_graphemes() {
1211 let mut pool = GraphemePool::new();
1212 let mut frame = Frame::new(10, 1, &mut pool);
1213
1214 let flag = "πΊπΈ";
1216 assert!(flag.chars().count() > 1);
1217
1218 frame.print_text(0, 0, flag, Cell::default());
1219
1220 let cell = frame.buffer.get(0, 0).unwrap();
1221 assert!(cell.content.is_grapheme());
1222
1223 let id = cell.content.grapheme_id().unwrap();
1224 assert_eq!(frame.pool.get(id), Some(flag));
1225 }
1226}