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
65pub type HitOwner = u64;
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
73pub enum HitRegion {
74 #[default]
76 None,
77 Content,
79 Border,
81 Scrollbar,
83 Handle,
85 Button,
87 Link,
89 Custom(u8),
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub struct HitTestResult {
96 pub id: HitId,
97 pub region: HitRegion,
98 pub data: HitData,
99 pub owner: Option<HitOwner>,
100}
101
102impl HitTestResult {
103 #[inline]
104 pub const fn new(id: HitId, region: HitRegion, data: HitData, owner: Option<HitOwner>) -> Self {
105 Self {
106 id,
107 region,
108 data,
109 owner,
110 }
111 }
112
113 #[inline]
114 pub const fn into_tuple(self) -> (HitId, HitRegion, HitData) {
115 (self.id, self.region, self.data)
116 }
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
121pub struct HitCell {
122 pub widget_id: Option<HitId>,
124 pub region: HitRegion,
126 pub data: HitData,
128 pub owner: Option<HitOwner>,
130}
131
132impl HitCell {
133 #[inline]
135 pub const fn new(widget_id: HitId, region: HitRegion, data: HitData) -> Self {
136 Self {
137 widget_id: Some(widget_id),
138 region,
139 data,
140 owner: None,
141 }
142 }
143
144 #[inline]
146 pub const fn new_with_owner(
147 widget_id: HitId,
148 region: HitRegion,
149 data: HitData,
150 owner: Option<HitOwner>,
151 ) -> Self {
152 Self {
153 widget_id: Some(widget_id),
154 region,
155 data,
156 owner,
157 }
158 }
159
160 #[inline]
162 pub const fn is_empty(&self) -> bool {
163 self.widget_id.is_none()
164 }
165}
166
167#[derive(Debug, Clone)]
172pub struct HitGrid {
173 width: u16,
174 height: u16,
175 cells: Vec<HitCell>,
176}
177
178impl HitGrid {
179 pub fn new(width: u16, height: u16) -> Self {
181 let size = width as usize * height as usize;
182 Self {
183 width,
184 height,
185 cells: vec![HitCell::default(); size],
186 }
187 }
188
189 #[inline]
191 pub const fn width(&self) -> u16 {
192 self.width
193 }
194
195 #[inline]
197 pub const fn height(&self) -> u16 {
198 self.height
199 }
200
201 #[inline]
203 fn index(&self, x: u16, y: u16) -> Option<usize> {
204 if x < self.width && y < self.height {
205 Some(y as usize * self.width as usize + x as usize)
206 } else {
207 None
208 }
209 }
210
211 #[inline]
213 #[must_use]
214 pub fn get(&self, x: u16, y: u16) -> Option<&HitCell> {
215 self.index(x, y).map(|i| &self.cells[i])
216 }
217
218 #[inline]
220 #[must_use]
221 pub fn get_mut(&mut self, x: u16, y: u16) -> Option<&mut HitCell> {
222 self.index(x, y).map(|i| &mut self.cells[i])
223 }
224
225 pub fn register(&mut self, rect: Rect, widget_id: HitId, region: HitRegion, data: HitData) {
229 self.register_with_owner(rect, widget_id, region, data, None);
230 }
231
232 pub fn register_with_owner(
234 &mut self,
235 rect: Rect,
236 widget_id: HitId,
237 region: HitRegion,
238 data: HitData,
239 owner: Option<HitOwner>,
240 ) {
241 let x_end = (rect.x as usize + rect.width as usize).min(self.width as usize);
243 let y_end = (rect.y as usize + rect.height as usize).min(self.height as usize);
244
245 if rect.x as usize >= x_end || rect.y as usize >= y_end {
247 return;
248 }
249
250 let hit_cell = HitCell::new_with_owner(widget_id, region, data, owner);
251
252 for y in rect.y as usize..y_end {
253 let row_start = y * self.width as usize;
254 let start = row_start + rect.x as usize;
255 let end = row_start + x_end;
256
257 self.cells[start..end].fill(hit_cell);
259 }
260 }
261
262 #[must_use]
266 pub fn hit_test(&self, x: u16, y: u16) -> Option<(HitId, HitRegion, HitData)> {
267 self.hit_test_detailed(x, y).map(HitTestResult::into_tuple)
268 }
269
270 #[must_use]
272 pub fn hit_test_detailed(&self, x: u16, y: u16) -> Option<HitTestResult> {
273 self.get(x, y).and_then(|cell| {
274 cell.widget_id
275 .map(|id| HitTestResult::new(id, cell.region, cell.data, cell.owner))
276 })
277 }
278
279 pub fn hits_in(&self, rect: Rect) -> Vec<(HitId, HitRegion, HitData)> {
281 let x_end = (rect.x as usize + rect.width as usize).min(self.width as usize) as u16;
282 let y_end = (rect.y as usize + rect.height as usize).min(self.height as usize) as u16;
283 let mut hits = Vec::new();
284
285 for y in rect.y..y_end {
286 for x in rect.x..x_end {
287 if let Some((id, region, data)) = self.hit_test(x, y) {
288 hits.push((id, region, data));
289 }
290 }
291 }
292
293 hits
294 }
295
296 pub fn clear(&mut self) {
298 self.cells.fill(HitCell::default());
299 }
300}
301
302use crate::link_registry::LinkRegistry;
303
304#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
306pub enum CostEstimateSource {
307 Measured,
309 AreaFallback,
311 #[default]
313 FixedDefault,
314}
315
316#[derive(Debug, Clone)]
321pub struct WidgetSignal {
322 pub widget_id: u64,
324 pub essential: bool,
326 pub priority: f32,
328 pub staleness_ms: u64,
330 pub focus_boost: f32,
332 pub interaction_boost: f32,
334 pub area_cells: u32,
336 pub cost_estimate_us: f32,
338 pub recent_cost_us: f32,
340 pub estimate_source: CostEstimateSource,
342}
343
344impl Default for WidgetSignal {
345 fn default() -> Self {
346 Self {
347 widget_id: 0,
348 essential: false,
349 priority: 0.5,
350 staleness_ms: 0,
351 focus_boost: 0.0,
352 interaction_boost: 0.0,
353 area_cells: 1,
354 cost_estimate_us: 5.0,
355 recent_cost_us: 5.0,
356 estimate_source: CostEstimateSource::FixedDefault,
357 }
358 }
359}
360
361impl WidgetSignal {
362 #[must_use]
364 pub fn new(widget_id: u64) -> Self {
365 Self {
366 widget_id,
367 ..Self::default()
368 }
369 }
370}
371
372#[derive(Debug, Clone)]
374pub struct WidgetBudget {
375 allow_list: Option<Vec<u64>>,
376}
377
378impl Default for WidgetBudget {
379 fn default() -> Self {
380 Self::allow_all()
381 }
382}
383
384impl WidgetBudget {
385 #[must_use]
387 pub fn allow_all() -> Self {
388 Self { allow_list: None }
389 }
390
391 #[must_use]
393 pub fn allow_only(mut ids: Vec<u64>) -> Self {
394 ids.sort_unstable();
395 ids.dedup();
396 Self {
397 allow_list: Some(ids),
398 }
399 }
400
401 #[inline]
403 pub fn allows(&self, widget_id: u64, essential: bool) -> bool {
404 if essential {
405 return true;
406 }
407 match &self.allow_list {
408 None => true,
409 Some(ids) => ids.binary_search(&widget_id).is_ok(),
410 }
411 }
412}
413
414#[derive(Debug)]
425pub struct Frame<'a> {
426 pub buffer: Buffer,
428
429 pub pool: &'a mut GraphemePool,
431
432 pub links: Option<&'a mut LinkRegistry>,
434
435 pub hit_grid: Option<HitGrid>,
439
440 hit_owner_stack: Vec<HitOwner>,
442
443 pub widget_budget: WidgetBudget,
445
446 pub widget_signals: Vec<WidgetSignal>,
448
449 pub cursor_position: Option<(u16, u16)>,
453
454 pub cursor_visible: bool,
456
457 pub degradation: DegradationLevel,
463
464 pub arena: Option<&'a FrameArena>,
471}
472
473impl<'a> Frame<'a> {
474 pub fn new(width: u16, height: u16, pool: &'a mut GraphemePool) -> Self {
478 Self {
479 buffer: Buffer::new(width, height),
480 pool,
481 links: None,
482 hit_grid: None,
483 hit_owner_stack: Vec::new(),
484 widget_budget: WidgetBudget::default(),
485 widget_signals: Vec::new(),
486 cursor_position: None,
487 cursor_visible: true,
488 degradation: DegradationLevel::Full,
489 arena: None,
490 }
491 }
492
493 pub fn from_buffer(buffer: Buffer, pool: &'a mut GraphemePool) -> Self {
497 Self {
498 buffer,
499 pool,
500 links: None,
501 hit_grid: None,
502 hit_owner_stack: Vec::new(),
503 widget_budget: WidgetBudget::default(),
504 widget_signals: Vec::new(),
505 cursor_position: None,
506 cursor_visible: true,
507 degradation: DegradationLevel::Full,
508 arena: None,
509 }
510 }
511
512 pub fn with_links(
517 width: u16,
518 height: u16,
519 pool: &'a mut GraphemePool,
520 links: &'a mut LinkRegistry,
521 ) -> Self {
522 Self {
523 buffer: Buffer::new(width, height),
524 pool,
525 links: Some(links),
526 hit_grid: None,
527 hit_owner_stack: Vec::new(),
528 widget_budget: WidgetBudget::default(),
529 widget_signals: Vec::new(),
530 cursor_position: None,
531 cursor_visible: true,
532 degradation: DegradationLevel::Full,
533 arena: None,
534 }
535 }
536
537 pub fn with_hit_grid(width: u16, height: u16, pool: &'a mut GraphemePool) -> Self {
541 Self {
542 buffer: Buffer::new(width, height),
543 pool,
544 links: None,
545 hit_grid: Some(HitGrid::new(width, height)),
546 hit_owner_stack: Vec::new(),
547 widget_budget: WidgetBudget::default(),
548 widget_signals: Vec::new(),
549 cursor_position: None,
550 cursor_visible: true,
551 degradation: DegradationLevel::Full,
552 arena: None,
553 }
554 }
555
556 pub fn set_links(&mut self, links: &'a mut LinkRegistry) {
558 self.links = Some(links);
559 }
560
561 pub fn set_arena(&mut self, arena: &'a FrameArena) {
566 self.arena = Some(arena);
567 }
568
569 pub fn arena(&self) -> Option<&FrameArena> {
574 self.arena
575 }
576
577 pub fn register_link(&mut self, url: &str) -> u32 {
581 if let Some(ref mut links) = self.links {
582 links.register(url)
583 } else {
584 0
585 }
586 }
587
588 pub fn set_widget_budget(&mut self, budget: WidgetBudget) {
590 self.widget_budget = budget;
591 }
592
593 #[inline]
595 pub fn should_render_widget(&self, widget_id: u64, essential: bool) -> bool {
596 self.widget_budget.allows(widget_id, essential)
597 }
598
599 pub fn register_widget_signal(&mut self, signal: WidgetSignal) {
601 self.widget_signals.push(signal);
602 }
603
604 #[inline]
606 pub fn widget_signals(&self) -> &[WidgetSignal] {
607 &self.widget_signals
608 }
609
610 #[inline]
612 pub fn take_widget_signals(&mut self) -> Vec<WidgetSignal> {
613 std::mem::take(&mut self.widget_signals)
614 }
615
616 pub fn intern(&mut self, text: &str) -> GraphemeId {
625 let width = display_width(text).min(GraphemeId::MAX_WIDTH as usize) as u8;
626 self.pool.intern(text, width)
627 }
628
629 pub fn intern_with_width(&mut self, text: &str, width: u8) -> GraphemeId {
631 self.pool.intern(text, width)
632 }
633
634 pub fn enable_hit_testing(&mut self) {
636 if self.hit_grid.is_none() {
637 self.hit_grid = Some(HitGrid::new(self.width(), self.height()));
638 }
639 }
640
641 #[inline]
643 pub fn width(&self) -> u16 {
644 self.buffer.width()
645 }
646
647 #[inline]
649 pub fn height(&self) -> u16 {
650 self.buffer.height()
651 }
652
653 pub fn clear(&mut self) {
657 self.buffer.clear();
658 if let Some(ref mut grid) = self.hit_grid {
659 grid.clear();
660 }
661 self.cursor_position = None;
662 self.widget_signals.clear();
663 }
664
665 #[inline]
669 pub fn set_cursor(&mut self, position: Option<(u16, u16)>) {
670 self.cursor_position = position;
671 }
672
673 #[inline]
675 pub fn set_cursor_visible(&mut self, visible: bool) {
676 self.cursor_visible = visible;
677 }
678
679 #[inline]
684 pub fn set_degradation(&mut self, level: DegradationLevel) {
685 self.degradation = level;
686 self.buffer.degradation = level;
687 }
688
689 #[inline]
691 pub fn bounds(&self) -> Rect {
692 self.buffer.bounds()
693 }
694
695 pub fn register_hit(
705 &mut self,
706 rect: Rect,
707 id: HitId,
708 region: HitRegion,
709 data: HitData,
710 ) -> bool {
711 let owner = self.current_hit_owner();
712 if let Some(ref mut grid) = self.hit_grid {
713 let clipped = rect.intersection(&self.buffer.current_scissor());
715 if !clipped.is_empty() {
716 grid.register_with_owner(clipped, id, region, data, owner);
717 }
718 true
719 } else {
720 false
721 }
722 }
723
724 pub fn with_hit_owner<R>(&mut self, owner: HitOwner, f: impl FnOnce(&mut Self) -> R) -> R {
726 self.hit_owner_stack.push(owner);
727 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| f(self)));
728 self.hit_owner_stack.pop();
729 match result {
730 Ok(result) => result,
731 Err(payload) => std::panic::resume_unwind(payload),
732 }
733 }
734
735 #[must_use]
737 pub fn hit_test(&self, x: u16, y: u16) -> Option<(HitId, HitRegion, HitData)> {
738 self.hit_grid.as_ref().and_then(|grid| grid.hit_test(x, y))
739 }
740
741 #[must_use]
743 pub fn hit_test_detailed(&self, x: u16, y: u16) -> Option<HitTestResult> {
744 self.hit_grid
745 .as_ref()
746 .and_then(|grid| grid.hit_test_detailed(x, y))
747 }
748
749 pub fn register_hit_region(&mut self, rect: Rect, id: HitId) -> bool {
751 self.register_hit(rect, id, HitRegion::Content, 0)
752 }
753
754 #[inline]
755 fn current_hit_owner(&self) -> Option<HitOwner> {
756 self.hit_owner_stack.last().copied()
757 }
758}
759
760impl<'a> Draw for Frame<'a> {
761 fn draw_horizontal_line(&mut self, x: u16, y: u16, width: u16, cell: Cell) {
762 self.buffer.draw_horizontal_line(x, y, width, cell);
763 }
764
765 fn draw_vertical_line(&mut self, x: u16, y: u16, height: u16, cell: Cell) {
766 self.buffer.draw_vertical_line(x, y, height, cell);
767 }
768
769 fn draw_rect_filled(&mut self, rect: Rect, cell: Cell) {
770 self.buffer.draw_rect_filled(rect, cell);
771 }
772
773 fn draw_rect_outline(&mut self, rect: Rect, cell: Cell) {
774 self.buffer.draw_rect_outline(rect, cell);
775 }
776
777 fn print_text(&mut self, x: u16, y: u16, text: &str, base_cell: Cell) -> u16 {
778 self.print_text_clipped(x, y, text, base_cell, self.width())
779 }
780
781 fn print_text_clipped(
782 &mut self,
783 x: u16,
784 y: u16,
785 text: &str,
786 base_cell: Cell,
787 max_x: u16,
788 ) -> u16 {
789 let mut cx = x;
790 for grapheme in text.graphemes(true) {
791 let width = grapheme_width(grapheme);
792 if width == 0 {
793 continue;
794 }
795
796 if cx >= max_x {
797 break;
798 }
799
800 if cx as u32 + width as u32 > max_x as u32 {
802 break;
803 }
804
805 let content = if width > 1 || grapheme.chars().count() > 1 {
807 let id = self.intern_with_width(grapheme, width as u8);
808 CellContent::from_grapheme(id)
809 } else if let Some(c) = grapheme.chars().next() {
810 CellContent::from_char(c)
811 } else {
812 continue;
813 };
814
815 let cell = Cell {
816 content,
817 fg: base_cell.fg,
818 bg: base_cell.bg,
819 attrs: base_cell.attrs,
820 };
821 self.buffer.set_fast(cx, y, cell);
822
823 cx = cx.saturating_add(width as u16);
824 }
825 cx
826 }
827
828 fn draw_border(&mut self, rect: Rect, chars: BorderChars, base_cell: Cell) {
829 self.buffer.draw_border(rect, chars, base_cell);
830 }
831
832 fn draw_box(&mut self, rect: Rect, chars: BorderChars, border_cell: Cell, fill_cell: Cell) {
833 self.buffer.draw_box(rect, chars, border_cell, fill_cell);
834 }
835
836 fn paint_area(
837 &mut self,
838 rect: Rect,
839 fg: Option<crate::cell::PackedRgba>,
840 bg: Option<crate::cell::PackedRgba>,
841 ) {
842 self.buffer.paint_area(rect, fg, bg);
843 }
844}
845
846#[cfg(test)]
847mod tests {
848 use super::*;
849 use crate::cell::Cell;
850
851 #[test]
852 fn frame_creation() {
853 let mut pool = GraphemePool::new();
854 let frame = Frame::new(80, 24, &mut pool);
855 assert_eq!(frame.width(), 80);
856 assert_eq!(frame.height(), 24);
857 assert!(frame.hit_grid.is_none());
858 assert!(frame.cursor_position.is_none());
859 assert!(frame.cursor_visible);
860 }
861
862 #[test]
863 fn frame_with_hit_grid() {
864 let mut pool = GraphemePool::new();
865 let frame = Frame::with_hit_grid(80, 24, &mut pool);
866 assert!(frame.hit_grid.is_some());
867 assert_eq!(frame.width(), 80);
868 assert_eq!(frame.height(), 24);
869 }
870
871 #[test]
872 fn frame_cursor() {
873 let mut pool = GraphemePool::new();
874 let mut frame = Frame::new(80, 24, &mut pool);
875 assert!(frame.cursor_position.is_none());
876 assert!(frame.cursor_visible);
877
878 frame.set_cursor(Some((10, 5)));
879 assert_eq!(frame.cursor_position, Some((10, 5)));
880
881 frame.set_cursor_visible(false);
882 assert!(!frame.cursor_visible);
883
884 frame.set_cursor(None);
885 assert!(frame.cursor_position.is_none());
886 }
887
888 #[test]
889 fn frame_clear() {
890 let mut pool = GraphemePool::new();
891 let mut frame = Frame::with_hit_grid(10, 10, &mut pool);
892
893 frame.buffer.set_raw(5, 5, Cell::from_char('X'));
895 frame.register_hit_region(Rect::new(0, 0, 5, 5), HitId::new(1));
896
897 assert_eq!(frame.buffer.get(5, 5).unwrap().content.as_char(), Some('X'));
899 assert_eq!(
900 frame.hit_test(2, 2),
901 Some((HitId::new(1), HitRegion::Content, 0))
902 );
903
904 frame.clear();
906
907 assert!(frame.buffer.get(5, 5).unwrap().is_empty());
909 assert!(frame.hit_test(2, 2).is_none());
910 }
911
912 #[test]
913 fn frame_bounds() {
914 let mut pool = GraphemePool::new();
915 let frame = Frame::new(80, 24, &mut pool);
916 let bounds = frame.bounds();
917 assert_eq!(bounds.x, 0);
918 assert_eq!(bounds.y, 0);
919 assert_eq!(bounds.width, 80);
920 assert_eq!(bounds.height, 24);
921 }
922
923 #[test]
924 fn hit_grid_creation() {
925 let grid = HitGrid::new(80, 24);
926 assert_eq!(grid.width(), 80);
927 assert_eq!(grid.height(), 24);
928 }
929
930 #[test]
931 fn hit_grid_registration() {
932 let mut pool = GraphemePool::new();
933 let mut frame = Frame::with_hit_grid(80, 24, &mut pool);
934 let hit_id = HitId::new(42);
935 let rect = Rect::new(10, 5, 20, 3);
936
937 frame.register_hit(rect, hit_id, HitRegion::Button, 99);
938
939 assert_eq!(frame.hit_test(15, 6), Some((hit_id, HitRegion::Button, 99)));
941 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()); }
950
951 #[test]
952 fn hit_grid_overlapping_regions() {
953 let mut pool = GraphemePool::new();
954 let mut frame = Frame::with_hit_grid(20, 20, &mut pool);
955
956 frame.register_hit(
958 Rect::new(0, 0, 10, 10),
959 HitId::new(1),
960 HitRegion::Content,
961 1,
962 );
963 frame.register_hit(Rect::new(5, 5, 10, 10), HitId::new(2), HitRegion::Border, 2);
964
965 assert_eq!(
967 frame.hit_test(2, 2),
968 Some((HitId::new(1), HitRegion::Content, 1))
969 );
970
971 assert_eq!(
973 frame.hit_test(7, 7),
974 Some((HitId::new(2), HitRegion::Border, 2))
975 );
976
977 assert_eq!(
979 frame.hit_test(12, 12),
980 Some((HitId::new(2), HitRegion::Border, 2))
981 );
982 }
983
984 #[test]
985 fn hit_grid_out_of_bounds() {
986 let mut pool = GraphemePool::new();
987 let frame = Frame::with_hit_grid(10, 10, &mut pool);
988
989 assert!(frame.hit_test(100, 100).is_none());
991 assert!(frame.hit_test(10, 0).is_none()); assert!(frame.hit_test(0, 10).is_none()); }
994
995 #[test]
996 fn hit_id_properties() {
997 let id = HitId::new(42);
998 assert_eq!(id.id(), 42);
999 assert_eq!(id, HitId(42));
1000 }
1001
1002 #[test]
1003 fn register_hit_region_no_grid() {
1004 let mut pool = GraphemePool::new();
1005 let mut frame = Frame::new(10, 10, &mut pool);
1006 let result = frame.register_hit_region(Rect::new(0, 0, 5, 5), HitId::new(1));
1007 assert!(!result); }
1009
1010 #[test]
1011 fn register_hit_region_with_grid() {
1012 let mut pool = GraphemePool::new();
1013 let mut frame = Frame::with_hit_grid(10, 10, &mut pool);
1014 let result = frame.register_hit_region(Rect::new(0, 0, 5, 5), HitId::new(1));
1015 assert!(result); }
1017
1018 #[test]
1019 fn hit_grid_clear() {
1020 let mut grid = HitGrid::new(10, 10);
1021 grid.register(Rect::new(0, 0, 5, 5), HitId::new(1), HitRegion::Content, 0);
1022
1023 assert_eq!(
1024 grid.hit_test(2, 2),
1025 Some((HitId::new(1), HitRegion::Content, 0))
1026 );
1027
1028 grid.clear();
1029
1030 assert!(grid.hit_test(2, 2).is_none());
1031 }
1032
1033 #[test]
1034 fn hit_grid_boundary_clipping() {
1035 let mut grid = HitGrid::new(10, 10);
1036
1037 grid.register(
1039 Rect::new(8, 8, 10, 10),
1040 HitId::new(1),
1041 HitRegion::Content,
1042 0,
1043 );
1044
1045 assert_eq!(
1047 grid.hit_test(9, 9),
1048 Some((HitId::new(1), HitRegion::Content, 0))
1049 );
1050
1051 assert!(grid.hit_test(10, 10).is_none());
1053 }
1054
1055 #[test]
1056 fn hit_grid_edge_and_corner_cells() {
1057 let mut grid = HitGrid::new(4, 4);
1058 grid.register(Rect::new(3, 0, 1, 4), HitId::new(7), HitRegion::Border, 11);
1059
1060 assert_eq!(
1062 grid.hit_test(3, 0),
1063 Some((HitId::new(7), HitRegion::Border, 11))
1064 );
1065 assert_eq!(
1066 grid.hit_test(3, 3),
1067 Some((HitId::new(7), HitRegion::Border, 11))
1068 );
1069
1070 assert!(grid.hit_test(2, 0).is_none());
1072 assert!(grid.hit_test(4, 0).is_none());
1073 assert!(grid.hit_test(3, 4).is_none());
1074
1075 let mut grid = HitGrid::new(4, 4);
1076 grid.register(Rect::new(0, 3, 4, 1), HitId::new(9), HitRegion::Content, 21);
1077
1078 assert_eq!(
1080 grid.hit_test(0, 3),
1081 Some((HitId::new(9), HitRegion::Content, 21))
1082 );
1083 assert_eq!(
1084 grid.hit_test(3, 3),
1085 Some((HitId::new(9), HitRegion::Content, 21))
1086 );
1087
1088 assert!(grid.hit_test(0, 2).is_none());
1090 assert!(grid.hit_test(0, 4).is_none());
1091 }
1092
1093 #[test]
1094 fn frame_register_hit_respects_nested_scissor() {
1095 let mut pool = GraphemePool::new();
1096 let mut frame = Frame::with_hit_grid(10, 10, &mut pool);
1097
1098 let outer = Rect::new(1, 1, 8, 8);
1099 frame.buffer.push_scissor(outer);
1100 assert_eq!(frame.buffer.current_scissor(), outer);
1101
1102 let inner = Rect::new(4, 4, 10, 10);
1103 frame.buffer.push_scissor(inner);
1104 let clipped = outer.intersection(&inner);
1105 let current = frame.buffer.current_scissor();
1106 assert_eq!(current, clipped);
1107
1108 assert!(outer.contains(current.x, current.y));
1110 assert!(outer.contains(
1111 current.right().saturating_sub(1),
1112 current.bottom().saturating_sub(1)
1113 ));
1114
1115 frame.register_hit(
1116 Rect::new(0, 0, 10, 10),
1117 HitId::new(3),
1118 HitRegion::Button,
1119 99,
1120 );
1121
1122 assert_eq!(
1123 frame.hit_test(4, 4),
1124 Some((HitId::new(3), HitRegion::Button, 99))
1125 );
1126 assert_eq!(
1127 frame.hit_test(8, 8),
1128 Some((HitId::new(3), HitRegion::Button, 99))
1129 );
1130 assert!(frame.hit_test(3, 3).is_none()); assert!(frame.hit_test(0, 0).is_none()); frame.buffer.pop_scissor();
1134 assert_eq!(frame.buffer.current_scissor(), outer);
1135 }
1136
1137 #[test]
1138 fn hit_grid_hits_in_area() {
1139 let mut grid = HitGrid::new(5, 5);
1140 grid.register(Rect::new(0, 0, 2, 2), HitId::new(1), HitRegion::Content, 10);
1141 grid.register(Rect::new(1, 1, 2, 2), HitId::new(2), HitRegion::Button, 20);
1142
1143 let hits = grid.hits_in(Rect::new(0, 0, 3, 3));
1144 assert!(hits.contains(&(HitId::new(1), HitRegion::Content, 10)));
1145 assert!(hits.contains(&(HitId::new(2), HitRegion::Button, 20)));
1146 }
1147
1148 #[test]
1149 fn frame_intern() {
1150 let mut pool = GraphemePool::new();
1151 let mut frame = Frame::new(10, 10, &mut pool);
1152
1153 let id = frame.intern("👋");
1154 assert_eq!(frame.pool.get(id), Some("👋"));
1155 }
1156
1157 #[test]
1158 fn frame_intern_with_width() {
1159 let mut pool = GraphemePool::new();
1160 let mut frame = Frame::new(10, 10, &mut pool);
1161
1162 let id = frame.intern_with_width("🧪", 2);
1163 assert_eq!(id.width(), 2);
1164 assert_eq!(frame.pool.get(id), Some("🧪"));
1165 }
1166
1167 #[test]
1168 fn frame_print_text_emoji_presentation_sets_continuation() {
1169 let mut pool = GraphemePool::new();
1170 let mut frame = Frame::new(5, 1, &mut pool);
1171
1172 frame.print_text(0, 0, "👍🏽", Cell::from_char(' '));
1175
1176 let head = frame.buffer.get(0, 0).unwrap();
1177 let tail = frame.buffer.get(1, 0).unwrap();
1178
1179 assert_eq!(head.content.width(), 2);
1180 assert!(tail.content.is_continuation());
1181 }
1182
1183 #[test]
1184 fn frame_enable_hit_testing() {
1185 let mut pool = GraphemePool::new();
1186 let mut frame = Frame::new(10, 10, &mut pool);
1187 assert!(frame.hit_grid.is_none());
1188
1189 frame.enable_hit_testing();
1190 assert!(frame.hit_grid.is_some());
1191
1192 frame.enable_hit_testing();
1194 assert!(frame.hit_grid.is_some());
1195 }
1196
1197 #[test]
1198 fn frame_enable_hit_testing_then_register() {
1199 let mut pool = GraphemePool::new();
1200 let mut frame = Frame::new(10, 10, &mut pool);
1201 frame.enable_hit_testing();
1202
1203 let registered = frame.register_hit_region(Rect::new(0, 0, 5, 5), HitId::new(1));
1204 assert!(registered);
1205 assert_eq!(
1206 frame.hit_test(2, 2),
1207 Some((HitId::new(1), HitRegion::Content, 0))
1208 );
1209 }
1210
1211 #[test]
1212 fn hit_cell_default_is_empty() {
1213 let cell = HitCell::default();
1214 assert!(cell.is_empty());
1215 assert_eq!(cell.widget_id, None);
1216 assert_eq!(cell.region, HitRegion::None);
1217 assert_eq!(cell.data, 0);
1218 }
1219
1220 #[test]
1221 fn hit_cell_new_is_not_empty() {
1222 let cell = HitCell::new(HitId::new(1), HitRegion::Button, 42);
1223 assert!(!cell.is_empty());
1224 assert_eq!(cell.widget_id, Some(HitId::new(1)));
1225 assert_eq!(cell.region, HitRegion::Button);
1226 assert_eq!(cell.data, 42);
1227 }
1228
1229 #[test]
1230 fn hit_region_variants() {
1231 assert_eq!(HitRegion::default(), HitRegion::None);
1232
1233 let variants = [
1235 HitRegion::None,
1236 HitRegion::Content,
1237 HitRegion::Border,
1238 HitRegion::Scrollbar,
1239 HitRegion::Handle,
1240 HitRegion::Button,
1241 HitRegion::Link,
1242 HitRegion::Custom(0),
1243 HitRegion::Custom(1),
1244 HitRegion::Custom(255),
1245 ];
1246 for i in 0..variants.len() {
1247 for j in (i + 1)..variants.len() {
1248 assert_ne!(
1249 variants[i], variants[j],
1250 "variants {i} and {j} should differ"
1251 );
1252 }
1253 }
1254 }
1255
1256 #[test]
1257 fn hit_id_default() {
1258 let id = HitId::default();
1259 assert_eq!(id.id(), 0);
1260 }
1261
1262 #[test]
1263 fn hit_grid_initial_cells_empty() {
1264 let grid = HitGrid::new(5, 5);
1265 for y in 0..5 {
1266 for x in 0..5 {
1267 let cell = grid.get(x, y).unwrap();
1268 assert!(cell.is_empty());
1269 }
1270 }
1271 }
1272
1273 #[test]
1274 fn hit_grid_zero_dimensions() {
1275 let grid = HitGrid::new(0, 0);
1276 assert_eq!(grid.width(), 0);
1277 assert_eq!(grid.height(), 0);
1278 assert!(grid.get(0, 0).is_none());
1279 assert!(grid.hit_test(0, 0).is_none());
1280 }
1281
1282 #[test]
1283 fn hit_grid_hits_in_empty_area() {
1284 let grid = HitGrid::new(10, 10);
1285 let hits = grid.hits_in(Rect::new(0, 0, 5, 5));
1286 assert!(hits.is_empty());
1288 }
1289
1290 #[test]
1291 fn hit_grid_hits_in_clipped_area() {
1292 let mut grid = HitGrid::new(5, 5);
1293 grid.register(Rect::new(0, 0, 5, 5), HitId::new(1), HitRegion::Content, 0);
1294
1295 let hits = grid.hits_in(Rect::new(3, 3, 10, 10));
1297 assert_eq!(hits.len(), 4); }
1299
1300 #[test]
1301 fn hit_test_no_grid_returns_none() {
1302 let mut pool = GraphemePool::new();
1303 let frame = Frame::new(10, 10, &mut pool);
1304 assert!(frame.hit_test(0, 0).is_none());
1305 }
1306
1307 #[test]
1308 fn frame_cursor_operations() {
1309 let mut pool = GraphemePool::new();
1310 let mut frame = Frame::new(80, 24, &mut pool);
1311
1312 frame.set_cursor(Some((79, 23)));
1314 assert_eq!(frame.cursor_position, Some((79, 23)));
1315
1316 frame.set_cursor(Some((0, 0)));
1318 assert_eq!(frame.cursor_position, Some((0, 0)));
1319
1320 frame.set_cursor_visible(false);
1322 assert!(!frame.cursor_visible);
1323 frame.set_cursor_visible(true);
1324 assert!(frame.cursor_visible);
1325 }
1326
1327 #[test]
1328 fn hit_data_large_values() {
1329 let mut grid = HitGrid::new(5, 5);
1330 grid.register(
1332 Rect::new(0, 0, 1, 1),
1333 HitId::new(1),
1334 HitRegion::Content,
1335 u64::MAX,
1336 );
1337 let result = grid.hit_test(0, 0);
1338 assert_eq!(result, Some((HitId::new(1), HitRegion::Content, u64::MAX)));
1339 }
1340
1341 #[test]
1342 fn hit_id_large_value() {
1343 let id = HitId::new(u32::MAX);
1344 assert_eq!(id.id(), u32::MAX);
1345 }
1346
1347 #[test]
1348 fn frame_print_text_interns_complex_graphemes() {
1349 let mut pool = GraphemePool::new();
1350 let mut frame = Frame::new(10, 1, &mut pool);
1351
1352 let flag = "🇺🇸";
1354 assert!(flag.chars().count() > 1);
1355
1356 frame.print_text(0, 0, flag, Cell::default());
1357
1358 let cell = frame.buffer.get(0, 0).unwrap();
1359 assert!(cell.content.is_grapheme());
1360
1361 let id = cell.content.grapheme_id().unwrap();
1362 assert_eq!(frame.pool.get(id), Some(flag));
1363 }
1364
1365 #[test]
1368 fn hit_id_debug_clone_copy_hash() {
1369 let id = HitId::new(99);
1370 let dbg = format!("{:?}", id);
1371 assert!(dbg.contains("99"), "Debug: {dbg}");
1372 let copied: HitId = id; assert_eq!(id, copied);
1374 use std::collections::HashSet;
1376 let mut set = HashSet::new();
1377 set.insert(id);
1378 set.insert(HitId::new(99));
1379 assert_eq!(set.len(), 1);
1380 set.insert(HitId::new(100));
1381 assert_eq!(set.len(), 2);
1382 }
1383
1384 #[test]
1385 fn hit_id_eq_and_ne() {
1386 assert_eq!(HitId::new(0), HitId::new(0));
1387 assert_ne!(HitId::new(0), HitId::new(1));
1388 assert_ne!(HitId::new(u32::MAX), HitId::default());
1389 }
1390
1391 #[test]
1394 fn hit_region_debug_clone_copy_hash() {
1395 let r = HitRegion::Custom(42);
1396 let dbg = format!("{:?}", r);
1397 assert!(dbg.contains("Custom"), "Debug: {dbg}");
1398 let copied: HitRegion = r; assert_eq!(r, copied);
1400 use std::collections::HashSet;
1401 let mut set = HashSet::new();
1402 set.insert(r);
1403 set.insert(HitRegion::Custom(42));
1404 assert_eq!(set.len(), 1);
1405 }
1406
1407 #[test]
1410 fn hit_cell_debug_clone_copy_eq() {
1411 let cell = HitCell::new(HitId::new(5), HitRegion::Link, 123);
1412 let dbg = format!("{:?}", cell);
1413 assert!(dbg.contains("Link"), "Debug: {dbg}");
1414 let copied: HitCell = cell; assert_eq!(cell, copied);
1416 assert_ne!(cell, HitCell::default());
1418 }
1419
1420 #[test]
1423 fn hit_grid_clone() {
1424 let mut grid = HitGrid::new(5, 5);
1425 grid.register(Rect::new(0, 0, 2, 2), HitId::new(1), HitRegion::Content, 7);
1426 let clone = grid.clone();
1427 assert_eq!(clone.width(), 5);
1428 assert_eq!(
1429 clone.hit_test(0, 0),
1430 Some((HitId::new(1), HitRegion::Content, 7))
1431 );
1432 }
1433
1434 #[test]
1435 fn hit_grid_get_mut() {
1436 let mut grid = HitGrid::new(5, 5);
1437 if let Some(cell) = grid.get_mut(2, 3) {
1439 *cell = HitCell::new(HitId::new(77), HitRegion::Handle, 55);
1440 }
1441 assert_eq!(
1442 grid.hit_test(2, 3),
1443 Some((HitId::new(77), HitRegion::Handle, 55))
1444 );
1445 assert!(grid.get_mut(5, 5).is_none());
1447 }
1448
1449 #[test]
1450 fn hit_grid_zero_width_nonzero_height() {
1451 let grid = HitGrid::new(0, 10);
1452 assert_eq!(grid.width(), 0);
1453 assert_eq!(grid.height(), 10);
1454 assert!(grid.get(0, 0).is_none());
1455 assert!(grid.hit_test(0, 5).is_none());
1456 }
1457
1458 #[test]
1459 fn hit_grid_nonzero_width_zero_height() {
1460 let grid = HitGrid::new(10, 0);
1461 assert_eq!(grid.width(), 10);
1462 assert_eq!(grid.height(), 0);
1463 assert!(grid.get(0, 0).is_none());
1464 }
1465
1466 #[test]
1467 fn hit_grid_register_zero_width_rect() {
1468 let mut grid = HitGrid::new(10, 10);
1469 grid.register(Rect::new(2, 2, 0, 5), HitId::new(1), HitRegion::Content, 0);
1470 assert!(grid.hit_test(2, 2).is_none());
1472 }
1473
1474 #[test]
1475 fn hit_grid_register_zero_height_rect() {
1476 let mut grid = HitGrid::new(10, 10);
1477 grid.register(Rect::new(2, 2, 5, 0), HitId::new(1), HitRegion::Content, 0);
1478 assert!(grid.hit_test(2, 2).is_none());
1479 }
1480
1481 #[test]
1482 fn hit_grid_register_past_bounds() {
1483 let mut grid = HitGrid::new(10, 10);
1484 grid.register(
1486 Rect::new(10, 10, 5, 5),
1487 HitId::new(1),
1488 HitRegion::Content,
1489 0,
1490 );
1491 assert!(grid.hit_test(9, 9).is_none());
1492 }
1493
1494 #[test]
1495 fn hit_grid_full_coverage() {
1496 let mut grid = HitGrid::new(3, 3);
1497 grid.register(Rect::new(0, 0, 3, 3), HitId::new(1), HitRegion::Content, 0);
1498 for y in 0..3 {
1500 for x in 0..3 {
1501 assert_eq!(
1502 grid.hit_test(x, y),
1503 Some((HitId::new(1), HitRegion::Content, 0))
1504 );
1505 }
1506 }
1507 }
1508
1509 #[test]
1510 fn hit_grid_single_cell() {
1511 let mut grid = HitGrid::new(1, 1);
1512 grid.register(Rect::new(0, 0, 1, 1), HitId::new(1), HitRegion::Button, 42);
1513 assert_eq!(
1514 grid.hit_test(0, 0),
1515 Some((HitId::new(1), HitRegion::Button, 42))
1516 );
1517 assert!(grid.hit_test(1, 0).is_none());
1518 assert!(grid.hit_test(0, 1).is_none());
1519 }
1520
1521 #[test]
1522 fn hit_grid_hits_in_outside_rect() {
1523 let mut grid = HitGrid::new(5, 5);
1524 grid.register(Rect::new(0, 0, 2, 2), HitId::new(1), HitRegion::Content, 0);
1525 let hits = grid.hits_in(Rect::new(3, 3, 2, 2));
1527 assert!(hits.is_empty());
1528 }
1529
1530 #[test]
1531 fn hit_grid_hits_in_zero_rect() {
1532 let mut grid = HitGrid::new(5, 5);
1533 grid.register(Rect::new(0, 0, 5, 5), HitId::new(1), HitRegion::Content, 0);
1534 let hits = grid.hits_in(Rect::new(2, 2, 0, 0));
1535 assert!(hits.is_empty());
1536 }
1537
1538 #[test]
1541 fn cost_estimate_source_traits() {
1542 let a = CostEstimateSource::Measured;
1543 let b = CostEstimateSource::AreaFallback;
1544 let c = CostEstimateSource::FixedDefault;
1545 let dbg = format!("{:?}", a);
1546 assert!(dbg.contains("Measured"), "Debug: {dbg}");
1547
1548 assert_eq!(
1550 CostEstimateSource::default(),
1551 CostEstimateSource::FixedDefault
1552 );
1553
1554 let copied: CostEstimateSource = a;
1556 assert_eq!(a, copied);
1557
1558 assert_ne!(a, b);
1560 assert_ne!(b, c);
1561 assert_ne!(a, c);
1562 }
1563
1564 #[test]
1567 fn widget_signal_default() {
1568 let sig = WidgetSignal::default();
1569 assert_eq!(sig.widget_id, 0);
1570 assert!(!sig.essential);
1571 assert!((sig.priority - 0.5).abs() < f32::EPSILON);
1572 assert_eq!(sig.staleness_ms, 0);
1573 assert!((sig.focus_boost - 0.0).abs() < f32::EPSILON);
1574 assert!((sig.interaction_boost - 0.0).abs() < f32::EPSILON);
1575 assert_eq!(sig.area_cells, 1);
1576 assert!((sig.cost_estimate_us - 5.0).abs() < f32::EPSILON);
1577 assert!((sig.recent_cost_us - 5.0).abs() < f32::EPSILON);
1578 assert_eq!(sig.estimate_source, CostEstimateSource::FixedDefault);
1579 }
1580
1581 #[test]
1582 fn widget_signal_new() {
1583 let sig = WidgetSignal::new(42);
1584 assert_eq!(sig.widget_id, 42);
1585 assert!(!sig.essential);
1587 assert!((sig.priority - 0.5).abs() < f32::EPSILON);
1588 }
1589
1590 #[test]
1591 fn widget_signal_debug_clone() {
1592 let sig = WidgetSignal::new(7);
1593 let dbg = format!("{:?}", sig);
1594 assert!(dbg.contains("widget_id"), "Debug: {dbg}");
1595 let cloned = sig.clone();
1596 assert_eq!(cloned.widget_id, 7);
1597 }
1598
1599 #[test]
1602 fn widget_budget_default_is_allow_all() {
1603 let budget = WidgetBudget::default();
1604 assert!(budget.allows(0, false));
1605 assert!(budget.allows(u64::MAX, false));
1606 assert!(budget.allows(42, true));
1607 }
1608
1609 #[test]
1610 fn widget_budget_allow_only() {
1611 let budget = WidgetBudget::allow_only(vec![10, 20, 30]);
1612 assert!(budget.allows(10, false));
1613 assert!(budget.allows(20, false));
1614 assert!(budget.allows(30, false));
1615 assert!(!budget.allows(15, false));
1616 assert!(!budget.allows(0, false));
1617 }
1618
1619 #[test]
1620 fn widget_budget_essential_always_allowed() {
1621 let budget = WidgetBudget::allow_only(vec![10]);
1622 assert!(budget.allows(999, true));
1624 assert!(budget.allows(0, true));
1625 }
1626
1627 #[test]
1628 fn widget_budget_allow_only_dedup() {
1629 let budget = WidgetBudget::allow_only(vec![5, 5, 5, 10, 10]);
1630 assert!(budget.allows(5, false));
1631 assert!(budget.allows(10, false));
1632 assert!(!budget.allows(7, false));
1633 }
1634
1635 #[test]
1636 fn widget_budget_allow_only_empty() {
1637 let budget = WidgetBudget::allow_only(vec![]);
1638 assert!(!budget.allows(0, false));
1640 assert!(!budget.allows(1, false));
1641 assert!(budget.allows(1, true)); }
1643
1644 #[test]
1645 fn widget_budget_debug_clone() {
1646 let budget = WidgetBudget::allow_only(vec![1, 2, 3]);
1647 let dbg = format!("{:?}", budget);
1648 assert!(dbg.contains("allow_list"), "Debug: {dbg}");
1649 let cloned = budget.clone();
1650 assert!(cloned.allows(2, false));
1651 }
1652
1653 #[test]
1656 fn frame_zero_dimensions_clamped_to_one() {
1657 let mut pool = GraphemePool::new();
1658 let frame = Frame::new(0, 0, &mut pool);
1659 assert_eq!(frame.buffer.width(), 1);
1660 assert_eq!(frame.buffer.height(), 1);
1661 }
1662
1663 #[test]
1664 fn frame_from_buffer() {
1665 let mut pool = GraphemePool::new();
1666 let mut buf = Buffer::new(20, 10);
1667 buf.set_raw(5, 5, Cell::from_char('Z'));
1668 let frame = Frame::from_buffer(buf, &mut pool);
1669 assert_eq!(frame.width(), 20);
1670 assert_eq!(frame.height(), 10);
1671 assert_eq!(frame.buffer.get(5, 5).unwrap().content.as_char(), Some('Z'));
1672 assert!(frame.hit_grid.is_none());
1673 assert!(frame.cursor_visible);
1674 }
1675
1676 #[test]
1677 fn frame_with_links() {
1678 let mut pool = GraphemePool::new();
1679 let mut links = LinkRegistry::new();
1680 let frame = Frame::with_links(10, 5, &mut pool, &mut links);
1681 assert!(frame.links.is_some());
1682 assert_eq!(frame.width(), 10);
1683 assert_eq!(frame.height(), 5);
1684 }
1685
1686 #[test]
1687 fn frame_set_links() {
1688 let mut pool = GraphemePool::new();
1689 let mut links = LinkRegistry::new();
1690 let mut frame = Frame::new(10, 5, &mut pool);
1691 assert!(frame.links.is_none());
1692 frame.set_links(&mut links);
1693 assert!(frame.links.is_some());
1694 }
1695
1696 #[test]
1697 fn frame_register_link_no_registry() {
1698 let mut pool = GraphemePool::new();
1699 let mut frame = Frame::new(10, 5, &mut pool);
1700 let id = frame.register_link("https://example.com");
1702 assert_eq!(id, 0);
1703 }
1704
1705 #[test]
1706 fn frame_register_link_with_registry() {
1707 let mut pool = GraphemePool::new();
1708 let mut links = LinkRegistry::new();
1709 let mut frame = Frame::with_links(10, 5, &mut pool, &mut links);
1710 let id = frame.register_link("https://example.com");
1711 assert!(id > 0);
1712 let id2 = frame.register_link("https://example.com");
1714 assert_eq!(id, id2);
1715 let id3 = frame.register_link("https://other.com");
1717 assert_ne!(id, id3);
1718 }
1719
1720 #[test]
1723 fn frame_set_widget_budget() {
1724 let mut pool = GraphemePool::new();
1725 let mut frame = Frame::new(10, 10, &mut pool);
1726
1727 assert!(frame.should_render_widget(42, false));
1729
1730 frame.set_widget_budget(WidgetBudget::allow_only(vec![1, 2]));
1732 assert!(frame.should_render_widget(1, false));
1733 assert!(!frame.should_render_widget(42, false));
1734 assert!(frame.should_render_widget(42, true)); }
1736
1737 #[test]
1740 fn frame_widget_signals_lifecycle() {
1741 let mut pool = GraphemePool::new();
1742 let mut frame = Frame::new(10, 10, &mut pool);
1743 assert!(frame.widget_signals().is_empty());
1744
1745 frame.register_widget_signal(WidgetSignal::new(1));
1746 frame.register_widget_signal(WidgetSignal::new(2));
1747 assert_eq!(frame.widget_signals().len(), 2);
1748 assert_eq!(frame.widget_signals()[0].widget_id, 1);
1749 assert_eq!(frame.widget_signals()[1].widget_id, 2);
1750
1751 let taken = frame.take_widget_signals();
1752 assert_eq!(taken.len(), 2);
1753 assert!(frame.widget_signals().is_empty());
1754 }
1755
1756 #[test]
1757 fn frame_clear_resets_signals_and_cursor() {
1758 let mut pool = GraphemePool::new();
1759 let mut frame = Frame::new(10, 10, &mut pool);
1760 frame.set_cursor(Some((5, 5)));
1761 frame.register_widget_signal(WidgetSignal::new(1));
1762 assert!(frame.cursor_position.is_some());
1763 assert!(!frame.widget_signals().is_empty());
1764
1765 frame.clear();
1766 assert!(frame.cursor_position.is_none());
1767 assert!(frame.widget_signals().is_empty());
1768 }
1769
1770 #[test]
1773 fn frame_set_degradation_propagates_to_buffer() {
1774 let mut pool = GraphemePool::new();
1775 let mut frame = Frame::new(10, 10, &mut pool);
1776 assert_eq!(frame.degradation, DegradationLevel::Full);
1777 assert_eq!(frame.buffer.degradation, DegradationLevel::Full);
1778
1779 frame.set_degradation(DegradationLevel::SimpleBorders);
1780 assert_eq!(frame.degradation, DegradationLevel::SimpleBorders);
1781 assert_eq!(frame.buffer.degradation, DegradationLevel::SimpleBorders);
1782
1783 frame.set_degradation(DegradationLevel::EssentialOnly);
1784 assert_eq!(frame.degradation, DegradationLevel::EssentialOnly);
1785 assert_eq!(frame.buffer.degradation, DegradationLevel::EssentialOnly);
1786 }
1787
1788 #[test]
1791 fn frame_with_hit_grid_zero_size_clamped_to_one() {
1792 let mut pool = GraphemePool::new();
1793 let frame = Frame::with_hit_grid(0, 0, &mut pool);
1794 assert_eq!(frame.buffer.width(), 1);
1795 assert_eq!(frame.buffer.height(), 1);
1796 }
1797
1798 #[test]
1801 fn frame_register_hit_with_all_regions() {
1802 let mut pool = GraphemePool::new();
1803 let mut frame = Frame::with_hit_grid(20, 20, &mut pool);
1804 let regions = [
1805 HitRegion::Content,
1806 HitRegion::Border,
1807 HitRegion::Scrollbar,
1808 HitRegion::Handle,
1809 HitRegion::Button,
1810 HitRegion::Link,
1811 HitRegion::Custom(0),
1812 HitRegion::Custom(255),
1813 ];
1814 for (i, ®ion) in regions.iter().enumerate() {
1815 let y = i as u16;
1816 frame.register_hit(Rect::new(0, y, 1, 1), HitId::new(i as u32), region, 0);
1817 }
1818 for (i, ®ion) in regions.iter().enumerate() {
1819 let y = i as u16;
1820 assert_eq!(
1821 frame.hit_test(0, y),
1822 Some((HitId::new(i as u32), region, 0))
1823 );
1824 }
1825 }
1826
1827 #[test]
1830 fn frame_draw_horizontal_line() {
1831 let mut pool = GraphemePool::new();
1832 let mut frame = Frame::new(10, 5, &mut pool);
1833 let cell = Cell::from_char('-');
1834 frame.draw_horizontal_line(2, 1, 5, cell);
1835 for x in 2..7 {
1836 assert_eq!(frame.buffer.get(x, 1).unwrap().content.as_char(), Some('-'));
1837 }
1838 assert!(frame.buffer.get(1, 1).unwrap().is_empty());
1840 assert!(frame.buffer.get(7, 1).unwrap().is_empty());
1841 }
1842
1843 #[test]
1844 fn frame_draw_vertical_line() {
1845 let mut pool = GraphemePool::new();
1846 let mut frame = Frame::new(10, 10, &mut pool);
1847 let cell = Cell::from_char('|');
1848 frame.draw_vertical_line(3, 2, 4, cell);
1849 for y in 2..6 {
1850 assert_eq!(frame.buffer.get(3, y).unwrap().content.as_char(), Some('|'));
1851 }
1852 assert!(frame.buffer.get(3, 1).unwrap().is_empty());
1853 assert!(frame.buffer.get(3, 6).unwrap().is_empty());
1854 }
1855
1856 #[test]
1857 fn frame_draw_rect_filled() {
1858 let mut pool = GraphemePool::new();
1859 let mut frame = Frame::new(10, 10, &mut pool);
1860 let cell = Cell::from_char('#');
1861 frame.draw_rect_filled(Rect::new(1, 1, 3, 3), cell);
1862 for y in 1..4 {
1863 for x in 1..4 {
1864 assert_eq!(frame.buffer.get(x, y).unwrap().content.as_char(), Some('#'));
1865 }
1866 }
1867 assert!(frame.buffer.get(0, 0).unwrap().is_empty());
1869 assert!(frame.buffer.get(4, 4).unwrap().is_empty());
1870 }
1871
1872 #[test]
1873 fn frame_paint_area() {
1874 use crate::cell::PackedRgba;
1875 let mut pool = GraphemePool::new();
1876 let mut frame = Frame::new(5, 5, &mut pool);
1877 let red = PackedRgba::rgb(255, 0, 0);
1878 frame.paint_area(Rect::new(0, 0, 2, 2), Some(red), None);
1879 let cell = frame.buffer.get(0, 0).unwrap();
1880 assert_eq!(cell.fg, red);
1881 }
1882
1883 #[test]
1886 fn frame_print_text_clipped_at_boundary() {
1887 let mut pool = GraphemePool::new();
1888 let mut frame = Frame::new(5, 1, &mut pool);
1889 let end = frame.print_text(0, 0, "Hello World", Cell::from_char(' '));
1891 assert_eq!(end, 5);
1892 for x in 0..5 {
1893 assert!(!frame.buffer.get(x, 0).unwrap().is_empty());
1894 }
1895 }
1896
1897 #[test]
1898 fn frame_print_text_empty_string() {
1899 let mut pool = GraphemePool::new();
1900 let mut frame = Frame::new(10, 1, &mut pool);
1901 let end = frame.print_text(0, 0, "", Cell::from_char(' '));
1902 assert_eq!(end, 0);
1903 }
1904
1905 #[test]
1906 fn frame_print_text_at_right_edge() {
1907 let mut pool = GraphemePool::new();
1908 let mut frame = Frame::new(5, 1, &mut pool);
1909 let end = frame.print_text(4, 0, "AB", Cell::from_char(' '));
1911 assert_eq!(end, 5);
1912 assert_eq!(frame.buffer.get(4, 0).unwrap().content.as_char(), Some('A'));
1913 }
1914
1915 #[test]
1918 fn frame_debug() {
1919 let mut pool = GraphemePool::new();
1920 let frame = Frame::new(5, 3, &mut pool);
1921 let dbg = format!("{:?}", frame);
1922 assert!(dbg.contains("Frame"), "Debug: {dbg}");
1923 }
1924
1925 #[test]
1928 fn hit_grid_debug() {
1929 let grid = HitGrid::new(3, 3);
1930 let dbg = format!("{:?}", grid);
1931 assert!(dbg.contains("HitGrid"), "Debug: {dbg}");
1932 }
1933
1934 #[test]
1937 fn frame_cursor_beyond_bounds() {
1938 let mut pool = GraphemePool::new();
1939 let mut frame = Frame::new(10, 10, &mut pool);
1940 frame.set_cursor(Some((100, 200)));
1942 assert_eq!(frame.cursor_position, Some((100, 200)));
1943 }
1944
1945 #[test]
1948 fn hit_grid_register_overwrite() {
1949 let mut grid = HitGrid::new(5, 5);
1950 grid.register(Rect::new(0, 0, 3, 3), HitId::new(1), HitRegion::Content, 10);
1951 grid.register(Rect::new(0, 0, 3, 3), HitId::new(2), HitRegion::Button, 20);
1952 assert_eq!(
1954 grid.hit_test(1, 1),
1955 Some((HitId::new(2), HitRegion::Button, 20))
1956 );
1957 }
1958
1959 #[test]
1960 fn frame_hit_test_detailed_preserves_owner() {
1961 let mut pool = GraphemePool::new();
1962 let mut frame = Frame::with_hit_grid(4, 4, &mut pool);
1963
1964 frame.with_hit_owner(77, |frame| {
1965 frame.register_hit(Rect::new(1, 1, 2, 2), HitId::new(5), HitRegion::Button, 9);
1966 });
1967
1968 assert_eq!(
1969 frame.hit_test_detailed(1, 1),
1970 Some(HitTestResult::new(
1971 HitId::new(5),
1972 HitRegion::Button,
1973 9,
1974 Some(77),
1975 ))
1976 );
1977 assert_eq!(
1978 frame.hit_test(1, 1),
1979 Some((HitId::new(5), HitRegion::Button, 9))
1980 );
1981 }
1982
1983 #[test]
1984 fn frame_hit_owner_scope_restores_previous_owner() {
1985 let mut pool = GraphemePool::new();
1986 let mut frame = Frame::with_hit_grid(4, 4, &mut pool);
1987
1988 frame.with_hit_owner(10, |frame| {
1989 frame.register_hit(Rect::new(0, 0, 1, 1), HitId::new(1), HitRegion::Content, 1);
1990 frame.with_hit_owner(20, |frame| {
1991 frame.register_hit(Rect::new(1, 0, 1, 1), HitId::new(2), HitRegion::Content, 2);
1992 });
1993 frame.register_hit(Rect::new(2, 0, 1, 1), HitId::new(3), HitRegion::Content, 3);
1994 });
1995
1996 assert_eq!(frame.hit_test_detailed(0, 0).unwrap().owner, Some(10));
1997 assert_eq!(frame.hit_test_detailed(1, 0).unwrap().owner, Some(20));
1998 assert_eq!(frame.hit_test_detailed(2, 0).unwrap().owner, Some(10));
1999 }
2000
2001 #[test]
2002 fn frame_hit_owner_scope_restores_after_panic() {
2003 let mut pool = GraphemePool::new();
2004 let mut frame = Frame::with_hit_grid(4, 4, &mut pool);
2005
2006 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2007 frame.with_hit_owner(55, |_frame| panic!("boom"));
2008 }));
2009 assert!(result.is_err());
2010
2011 frame.register_hit(Rect::new(0, 0, 1, 1), HitId::new(9), HitRegion::Content, 0);
2012 assert_eq!(frame.hit_test_detailed(0, 0).unwrap().owner, None);
2013 }
2014}