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 let buffer = Buffer::new(width, height);
546 let hit_grid = HitGrid::new(buffer.width(), buffer.height());
547 Self {
548 buffer,
549 pool,
550 links: None,
551 hit_grid: Some(hit_grid),
552 hit_owner_stack: Vec::new(),
553 widget_budget: WidgetBudget::default(),
554 widget_signals: Vec::new(),
555 cursor_position: None,
556 cursor_visible: true,
557 degradation: DegradationLevel::Full,
558 arena: None,
559 }
560 }
561
562 pub fn set_links(&mut self, links: &'a mut LinkRegistry) {
564 self.links = Some(links);
565 }
566
567 pub fn set_arena(&mut self, arena: &'a FrameArena) {
572 self.arena = Some(arena);
573 }
574
575 pub fn arena(&self) -> Option<&FrameArena> {
580 self.arena
581 }
582
583 pub fn register_link(&mut self, url: &str) -> u32 {
587 if let Some(ref mut links) = self.links {
588 links.register(url)
589 } else {
590 0
591 }
592 }
593
594 pub fn set_widget_budget(&mut self, budget: WidgetBudget) {
596 self.widget_budget = budget;
597 }
598
599 #[inline]
601 pub fn should_render_widget(&self, widget_id: u64, essential: bool) -> bool {
602 self.widget_budget.allows(widget_id, essential)
603 }
604
605 pub fn register_widget_signal(&mut self, signal: WidgetSignal) {
607 self.widget_signals.push(signal);
608 }
609
610 #[inline]
612 pub fn widget_signals(&self) -> &[WidgetSignal] {
613 &self.widget_signals
614 }
615
616 #[inline]
618 pub fn take_widget_signals(&mut self) -> Vec<WidgetSignal> {
619 std::mem::take(&mut self.widget_signals)
620 }
621
622 pub fn intern(&mut self, text: &str) -> GraphemeId {
631 let width = display_width(text).min(GraphemeId::MAX_WIDTH as usize) as u8;
632 self.pool.intern(text, width)
633 }
634
635 pub fn intern_with_width(&mut self, text: &str, width: u8) -> GraphemeId {
637 self.pool.intern(text, width)
638 }
639
640 pub fn enable_hit_testing(&mut self) {
642 if self.hit_grid.is_none() {
643 self.hit_grid = Some(HitGrid::new(self.width(), self.height()));
644 }
645 }
646
647 #[inline]
649 pub fn width(&self) -> u16 {
650 self.buffer.width()
651 }
652
653 #[inline]
655 pub fn height(&self) -> u16 {
656 self.buffer.height()
657 }
658
659 pub fn clear(&mut self) {
663 self.buffer.clear();
664 if let Some(ref mut grid) = self.hit_grid {
665 grid.clear();
666 }
667 self.cursor_position = None;
668 self.widget_signals.clear();
669 }
670
671 #[inline]
675 pub fn set_cursor(&mut self, position: Option<(u16, u16)>) {
676 self.cursor_position = position;
677 }
678
679 #[inline]
681 pub fn set_cursor_visible(&mut self, visible: bool) {
682 self.cursor_visible = visible;
683 }
684
685 #[inline]
690 pub fn set_degradation(&mut self, level: DegradationLevel) {
691 self.degradation = level;
692 self.buffer.degradation = level;
693 }
694
695 #[inline]
697 pub fn bounds(&self) -> Rect {
698 self.buffer.bounds()
699 }
700
701 pub fn register_hit(
711 &mut self,
712 rect: Rect,
713 id: HitId,
714 region: HitRegion,
715 data: HitData,
716 ) -> bool {
717 let owner = self.current_hit_owner();
718 if let Some(ref mut grid) = self.hit_grid {
719 let clipped = rect.intersection(&self.buffer.current_scissor());
721 if !clipped.is_empty() {
722 grid.register_with_owner(clipped, id, region, data, owner);
723 }
724 true
725 } else {
726 false
727 }
728 }
729
730 pub fn with_hit_owner<R>(&mut self, owner: HitOwner, f: impl FnOnce(&mut Self) -> R) -> R {
732 self.hit_owner_stack.push(owner);
733 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| f(self)));
734 self.hit_owner_stack.pop();
735 match result {
736 Ok(result) => result,
737 Err(payload) => std::panic::resume_unwind(payload),
738 }
739 }
740
741 #[must_use]
743 pub fn hit_test(&self, x: u16, y: u16) -> Option<(HitId, HitRegion, HitData)> {
744 self.hit_grid.as_ref().and_then(|grid| grid.hit_test(x, y))
745 }
746
747 #[must_use]
749 pub fn hit_test_detailed(&self, x: u16, y: u16) -> Option<HitTestResult> {
750 self.hit_grid
751 .as_ref()
752 .and_then(|grid| grid.hit_test_detailed(x, y))
753 }
754
755 pub fn register_hit_region(&mut self, rect: Rect, id: HitId) -> bool {
757 self.register_hit(rect, id, HitRegion::Content, 0)
758 }
759
760 #[inline]
761 fn current_hit_owner(&self) -> Option<HitOwner> {
762 self.hit_owner_stack.last().copied()
763 }
764}
765
766impl<'a> Draw for Frame<'a> {
767 fn draw_horizontal_line(&mut self, x: u16, y: u16, width: u16, cell: Cell) {
768 self.buffer.draw_horizontal_line(x, y, width, cell);
769 }
770
771 fn draw_vertical_line(&mut self, x: u16, y: u16, height: u16, cell: Cell) {
772 self.buffer.draw_vertical_line(x, y, height, cell);
773 }
774
775 fn draw_rect_filled(&mut self, rect: Rect, cell: Cell) {
776 self.buffer.draw_rect_filled(rect, cell);
777 }
778
779 fn draw_rect_outline(&mut self, rect: Rect, cell: Cell) {
780 self.buffer.draw_rect_outline(rect, cell);
781 }
782
783 fn print_text(&mut self, x: u16, y: u16, text: &str, base_cell: Cell) -> u16 {
784 self.print_text_clipped(x, y, text, base_cell, self.width())
785 }
786
787 fn print_text_clipped(
788 &mut self,
789 x: u16,
790 y: u16,
791 text: &str,
792 base_cell: Cell,
793 max_x: u16,
794 ) -> u16 {
795 let mut cx = x;
796 for grapheme in text.graphemes(true) {
797 let width = grapheme_width(grapheme);
798 if width == 0 {
799 continue;
800 }
801
802 if cx >= max_x {
803 break;
804 }
805
806 if cx as u32 + width as u32 > max_x as u32 {
808 break;
809 }
810
811 let content = if width > 1 || grapheme.chars().count() > 1 {
813 let id = self.intern_with_width(grapheme, width as u8);
814 CellContent::from_grapheme(id)
815 } else if let Some(c) = grapheme.chars().next() {
816 CellContent::from_char(c)
817 } else {
818 continue;
819 };
820
821 let cell = Cell {
822 content,
823 fg: base_cell.fg,
824 bg: base_cell.bg,
825 attrs: base_cell.attrs,
826 };
827 self.buffer.set_fast(cx, y, cell);
828
829 cx = cx.saturating_add(width as u16);
830 }
831 cx
832 }
833
834 fn draw_border(&mut self, rect: Rect, chars: BorderChars, base_cell: Cell) {
835 self.buffer.draw_border(rect, chars, base_cell);
836 }
837
838 fn draw_box(&mut self, rect: Rect, chars: BorderChars, border_cell: Cell, fill_cell: Cell) {
839 self.buffer.draw_box(rect, chars, border_cell, fill_cell);
840 }
841
842 fn paint_area(
843 &mut self,
844 rect: Rect,
845 fg: Option<crate::cell::PackedRgba>,
846 bg: Option<crate::cell::PackedRgba>,
847 ) {
848 self.buffer.paint_area(rect, fg, bg);
849 }
850}
851
852#[cfg(test)]
853mod tests {
854 use super::*;
855 use crate::cell::Cell;
856
857 #[test]
858 fn frame_creation() {
859 let mut pool = GraphemePool::new();
860 let frame = Frame::new(80, 24, &mut pool);
861 assert_eq!(frame.width(), 80);
862 assert_eq!(frame.height(), 24);
863 assert!(frame.hit_grid.is_none());
864 assert!(frame.cursor_position.is_none());
865 assert!(frame.cursor_visible);
866 }
867
868 #[test]
869 fn frame_with_hit_grid() {
870 let mut pool = GraphemePool::new();
871 let frame = Frame::with_hit_grid(80, 24, &mut pool);
872 assert!(frame.hit_grid.is_some());
873 assert_eq!(frame.width(), 80);
874 assert_eq!(frame.height(), 24);
875 }
876
877 #[test]
878 fn frame_cursor() {
879 let mut pool = GraphemePool::new();
880 let mut frame = Frame::new(80, 24, &mut pool);
881 assert!(frame.cursor_position.is_none());
882 assert!(frame.cursor_visible);
883
884 frame.set_cursor(Some((10, 5)));
885 assert_eq!(frame.cursor_position, Some((10, 5)));
886
887 frame.set_cursor_visible(false);
888 assert!(!frame.cursor_visible);
889
890 frame.set_cursor(None);
891 assert!(frame.cursor_position.is_none());
892 }
893
894 #[test]
895 fn frame_clear() {
896 let mut pool = GraphemePool::new();
897 let mut frame = Frame::with_hit_grid(10, 10, &mut pool);
898
899 frame.buffer.set_raw(5, 5, Cell::from_char('X'));
901 frame.register_hit_region(Rect::new(0, 0, 5, 5), HitId::new(1));
902
903 assert_eq!(frame.buffer.get(5, 5).unwrap().content.as_char(), Some('X'));
905 assert_eq!(
906 frame.hit_test(2, 2),
907 Some((HitId::new(1), HitRegion::Content, 0))
908 );
909
910 frame.clear();
912
913 assert!(frame.buffer.get(5, 5).unwrap().is_empty());
915 assert!(frame.hit_test(2, 2).is_none());
916 }
917
918 #[test]
919 fn frame_bounds() {
920 let mut pool = GraphemePool::new();
921 let frame = Frame::new(80, 24, &mut pool);
922 let bounds = frame.bounds();
923 assert_eq!(bounds.x, 0);
924 assert_eq!(bounds.y, 0);
925 assert_eq!(bounds.width, 80);
926 assert_eq!(bounds.height, 24);
927 }
928
929 #[test]
930 fn hit_grid_creation() {
931 let grid = HitGrid::new(80, 24);
932 assert_eq!(grid.width(), 80);
933 assert_eq!(grid.height(), 24);
934 }
935
936 #[test]
937 fn hit_grid_registration() {
938 let mut pool = GraphemePool::new();
939 let mut frame = Frame::with_hit_grid(80, 24, &mut pool);
940 let hit_id = HitId::new(42);
941 let rect = Rect::new(10, 5, 20, 3);
942
943 frame.register_hit(rect, hit_id, HitRegion::Button, 99);
944
945 assert_eq!(frame.hit_test(15, 6), Some((hit_id, HitRegion::Button, 99)));
947 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()); }
956
957 #[test]
958 fn hit_grid_overlapping_regions() {
959 let mut pool = GraphemePool::new();
960 let mut frame = Frame::with_hit_grid(20, 20, &mut pool);
961
962 frame.register_hit(
964 Rect::new(0, 0, 10, 10),
965 HitId::new(1),
966 HitRegion::Content,
967 1,
968 );
969 frame.register_hit(Rect::new(5, 5, 10, 10), HitId::new(2), HitRegion::Border, 2);
970
971 assert_eq!(
973 frame.hit_test(2, 2),
974 Some((HitId::new(1), HitRegion::Content, 1))
975 );
976
977 assert_eq!(
979 frame.hit_test(7, 7),
980 Some((HitId::new(2), HitRegion::Border, 2))
981 );
982
983 assert_eq!(
985 frame.hit_test(12, 12),
986 Some((HitId::new(2), HitRegion::Border, 2))
987 );
988 }
989
990 #[test]
991 fn hit_grid_out_of_bounds() {
992 let mut pool = GraphemePool::new();
993 let frame = Frame::with_hit_grid(10, 10, &mut pool);
994
995 assert!(frame.hit_test(100, 100).is_none());
997 assert!(frame.hit_test(10, 0).is_none()); assert!(frame.hit_test(0, 10).is_none()); }
1000
1001 #[test]
1002 fn hit_id_properties() {
1003 let id = HitId::new(42);
1004 assert_eq!(id.id(), 42);
1005 assert_eq!(id, HitId(42));
1006 }
1007
1008 #[test]
1009 fn register_hit_region_no_grid() {
1010 let mut pool = GraphemePool::new();
1011 let mut frame = Frame::new(10, 10, &mut pool);
1012 let result = frame.register_hit_region(Rect::new(0, 0, 5, 5), HitId::new(1));
1013 assert!(!result); }
1015
1016 #[test]
1017 fn register_hit_region_with_grid() {
1018 let mut pool = GraphemePool::new();
1019 let mut frame = Frame::with_hit_grid(10, 10, &mut pool);
1020 let result = frame.register_hit_region(Rect::new(0, 0, 5, 5), HitId::new(1));
1021 assert!(result); }
1023
1024 #[test]
1025 fn hit_grid_clear() {
1026 let mut grid = HitGrid::new(10, 10);
1027 grid.register(Rect::new(0, 0, 5, 5), HitId::new(1), HitRegion::Content, 0);
1028
1029 assert_eq!(
1030 grid.hit_test(2, 2),
1031 Some((HitId::new(1), HitRegion::Content, 0))
1032 );
1033
1034 grid.clear();
1035
1036 assert!(grid.hit_test(2, 2).is_none());
1037 }
1038
1039 #[test]
1040 fn hit_grid_boundary_clipping() {
1041 let mut grid = HitGrid::new(10, 10);
1042
1043 grid.register(
1045 Rect::new(8, 8, 10, 10),
1046 HitId::new(1),
1047 HitRegion::Content,
1048 0,
1049 );
1050
1051 assert_eq!(
1053 grid.hit_test(9, 9),
1054 Some((HitId::new(1), HitRegion::Content, 0))
1055 );
1056
1057 assert!(grid.hit_test(10, 10).is_none());
1059 }
1060
1061 #[test]
1062 fn hit_grid_edge_and_corner_cells() {
1063 let mut grid = HitGrid::new(4, 4);
1064 grid.register(Rect::new(3, 0, 1, 4), HitId::new(7), HitRegion::Border, 11);
1065
1066 assert_eq!(
1068 grid.hit_test(3, 0),
1069 Some((HitId::new(7), HitRegion::Border, 11))
1070 );
1071 assert_eq!(
1072 grid.hit_test(3, 3),
1073 Some((HitId::new(7), HitRegion::Border, 11))
1074 );
1075
1076 assert!(grid.hit_test(2, 0).is_none());
1078 assert!(grid.hit_test(4, 0).is_none());
1079 assert!(grid.hit_test(3, 4).is_none());
1080
1081 let mut grid = HitGrid::new(4, 4);
1082 grid.register(Rect::new(0, 3, 4, 1), HitId::new(9), HitRegion::Content, 21);
1083
1084 assert_eq!(
1086 grid.hit_test(0, 3),
1087 Some((HitId::new(9), HitRegion::Content, 21))
1088 );
1089 assert_eq!(
1090 grid.hit_test(3, 3),
1091 Some((HitId::new(9), HitRegion::Content, 21))
1092 );
1093
1094 assert!(grid.hit_test(0, 2).is_none());
1096 assert!(grid.hit_test(0, 4).is_none());
1097 }
1098
1099 #[test]
1100 fn frame_register_hit_respects_nested_scissor() {
1101 let mut pool = GraphemePool::new();
1102 let mut frame = Frame::with_hit_grid(10, 10, &mut pool);
1103
1104 let outer = Rect::new(1, 1, 8, 8);
1105 frame.buffer.push_scissor(outer);
1106 assert_eq!(frame.buffer.current_scissor(), outer);
1107
1108 let inner = Rect::new(4, 4, 10, 10);
1109 frame.buffer.push_scissor(inner);
1110 let clipped = outer.intersection(&inner);
1111 let current = frame.buffer.current_scissor();
1112 assert_eq!(current, clipped);
1113
1114 assert!(outer.contains(current.x, current.y));
1116 assert!(outer.contains(
1117 current.right().saturating_sub(1),
1118 current.bottom().saturating_sub(1)
1119 ));
1120
1121 frame.register_hit(
1122 Rect::new(0, 0, 10, 10),
1123 HitId::new(3),
1124 HitRegion::Button,
1125 99,
1126 );
1127
1128 assert_eq!(
1129 frame.hit_test(4, 4),
1130 Some((HitId::new(3), HitRegion::Button, 99))
1131 );
1132 assert_eq!(
1133 frame.hit_test(8, 8),
1134 Some((HitId::new(3), HitRegion::Button, 99))
1135 );
1136 assert!(frame.hit_test(3, 3).is_none()); assert!(frame.hit_test(0, 0).is_none()); frame.buffer.pop_scissor();
1140 assert_eq!(frame.buffer.current_scissor(), outer);
1141 }
1142
1143 #[test]
1144 fn hit_grid_hits_in_area() {
1145 let mut grid = HitGrid::new(5, 5);
1146 grid.register(Rect::new(0, 0, 2, 2), HitId::new(1), HitRegion::Content, 10);
1147 grid.register(Rect::new(1, 1, 2, 2), HitId::new(2), HitRegion::Button, 20);
1148
1149 let hits = grid.hits_in(Rect::new(0, 0, 3, 3));
1150 assert!(hits.contains(&(HitId::new(1), HitRegion::Content, 10)));
1151 assert!(hits.contains(&(HitId::new(2), HitRegion::Button, 20)));
1152 }
1153
1154 #[test]
1155 fn frame_intern() {
1156 let mut pool = GraphemePool::new();
1157 let mut frame = Frame::new(10, 10, &mut pool);
1158
1159 let id = frame.intern("👋");
1160 assert_eq!(frame.pool.get(id), Some("👋"));
1161 }
1162
1163 #[test]
1164 fn frame_intern_with_width() {
1165 let mut pool = GraphemePool::new();
1166 let mut frame = Frame::new(10, 10, &mut pool);
1167
1168 let id = frame.intern_with_width("🧪", 2);
1169 assert_eq!(id.width(), 2);
1170 assert_eq!(frame.pool.get(id), Some("🧪"));
1171 }
1172
1173 #[test]
1174 fn frame_print_text_emoji_presentation_sets_continuation() {
1175 let mut pool = GraphemePool::new();
1176 let mut frame = Frame::new(5, 1, &mut pool);
1177
1178 frame.print_text(0, 0, "👍🏽", Cell::from_char(' '));
1181
1182 let head = frame.buffer.get(0, 0).unwrap();
1183 let tail = frame.buffer.get(1, 0).unwrap();
1184
1185 assert_eq!(head.content.width(), 2);
1186 assert!(tail.content.is_continuation());
1187 }
1188
1189 #[test]
1190 fn frame_enable_hit_testing() {
1191 let mut pool = GraphemePool::new();
1192 let mut frame = Frame::new(10, 10, &mut pool);
1193 assert!(frame.hit_grid.is_none());
1194
1195 frame.enable_hit_testing();
1196 assert!(frame.hit_grid.is_some());
1197
1198 frame.enable_hit_testing();
1200 assert!(frame.hit_grid.is_some());
1201 }
1202
1203 #[test]
1204 fn frame_enable_hit_testing_then_register() {
1205 let mut pool = GraphemePool::new();
1206 let mut frame = Frame::new(10, 10, &mut pool);
1207 frame.enable_hit_testing();
1208
1209 let registered = frame.register_hit_region(Rect::new(0, 0, 5, 5), HitId::new(1));
1210 assert!(registered);
1211 assert_eq!(
1212 frame.hit_test(2, 2),
1213 Some((HitId::new(1), HitRegion::Content, 0))
1214 );
1215 }
1216
1217 #[test]
1218 fn hit_cell_default_is_empty() {
1219 let cell = HitCell::default();
1220 assert!(cell.is_empty());
1221 assert_eq!(cell.widget_id, None);
1222 assert_eq!(cell.region, HitRegion::None);
1223 assert_eq!(cell.data, 0);
1224 }
1225
1226 #[test]
1227 fn hit_cell_new_is_not_empty() {
1228 let cell = HitCell::new(HitId::new(1), HitRegion::Button, 42);
1229 assert!(!cell.is_empty());
1230 assert_eq!(cell.widget_id, Some(HitId::new(1)));
1231 assert_eq!(cell.region, HitRegion::Button);
1232 assert_eq!(cell.data, 42);
1233 }
1234
1235 #[test]
1236 fn hit_region_variants() {
1237 assert_eq!(HitRegion::default(), HitRegion::None);
1238
1239 let variants = [
1241 HitRegion::None,
1242 HitRegion::Content,
1243 HitRegion::Border,
1244 HitRegion::Scrollbar,
1245 HitRegion::Handle,
1246 HitRegion::Button,
1247 HitRegion::Link,
1248 HitRegion::Custom(0),
1249 HitRegion::Custom(1),
1250 HitRegion::Custom(255),
1251 ];
1252 for i in 0..variants.len() {
1253 for j in (i + 1)..variants.len() {
1254 assert_ne!(
1255 variants[i], variants[j],
1256 "variants {i} and {j} should differ"
1257 );
1258 }
1259 }
1260 }
1261
1262 #[test]
1263 fn hit_id_default() {
1264 let id = HitId::default();
1265 assert_eq!(id.id(), 0);
1266 }
1267
1268 #[test]
1269 fn hit_grid_initial_cells_empty() {
1270 let grid = HitGrid::new(5, 5);
1271 for y in 0..5 {
1272 for x in 0..5 {
1273 let cell = grid.get(x, y).unwrap();
1274 assert!(cell.is_empty());
1275 }
1276 }
1277 }
1278
1279 #[test]
1280 fn hit_grid_zero_dimensions() {
1281 let grid = HitGrid::new(0, 0);
1282 assert_eq!(grid.width(), 0);
1283 assert_eq!(grid.height(), 0);
1284 assert!(grid.get(0, 0).is_none());
1285 assert!(grid.hit_test(0, 0).is_none());
1286 }
1287
1288 #[test]
1289 fn hit_grid_hits_in_empty_area() {
1290 let grid = HitGrid::new(10, 10);
1291 let hits = grid.hits_in(Rect::new(0, 0, 5, 5));
1292 assert!(hits.is_empty());
1294 }
1295
1296 #[test]
1297 fn hit_grid_hits_in_clipped_area() {
1298 let mut grid = HitGrid::new(5, 5);
1299 grid.register(Rect::new(0, 0, 5, 5), HitId::new(1), HitRegion::Content, 0);
1300
1301 let hits = grid.hits_in(Rect::new(3, 3, 10, 10));
1303 assert_eq!(hits.len(), 4); }
1305
1306 #[test]
1307 fn hit_test_no_grid_returns_none() {
1308 let mut pool = GraphemePool::new();
1309 let frame = Frame::new(10, 10, &mut pool);
1310 assert!(frame.hit_test(0, 0).is_none());
1311 }
1312
1313 #[test]
1314 fn frame_cursor_operations() {
1315 let mut pool = GraphemePool::new();
1316 let mut frame = Frame::new(80, 24, &mut pool);
1317
1318 frame.set_cursor(Some((79, 23)));
1320 assert_eq!(frame.cursor_position, Some((79, 23)));
1321
1322 frame.set_cursor(Some((0, 0)));
1324 assert_eq!(frame.cursor_position, Some((0, 0)));
1325
1326 frame.set_cursor_visible(false);
1328 assert!(!frame.cursor_visible);
1329 frame.set_cursor_visible(true);
1330 assert!(frame.cursor_visible);
1331 }
1332
1333 #[test]
1334 fn hit_data_large_values() {
1335 let mut grid = HitGrid::new(5, 5);
1336 grid.register(
1338 Rect::new(0, 0, 1, 1),
1339 HitId::new(1),
1340 HitRegion::Content,
1341 u64::MAX,
1342 );
1343 let result = grid.hit_test(0, 0);
1344 assert_eq!(result, Some((HitId::new(1), HitRegion::Content, u64::MAX)));
1345 }
1346
1347 #[test]
1348 fn hit_id_large_value() {
1349 let id = HitId::new(u32::MAX);
1350 assert_eq!(id.id(), u32::MAX);
1351 }
1352
1353 #[test]
1354 fn frame_print_text_interns_complex_graphemes() {
1355 let mut pool = GraphemePool::new();
1356 let mut frame = Frame::new(10, 1, &mut pool);
1357
1358 let flag = "🇺🇸";
1360 assert!(flag.chars().count() > 1);
1361
1362 frame.print_text(0, 0, flag, Cell::default());
1363
1364 let cell = frame.buffer.get(0, 0).unwrap();
1365 assert!(cell.content.is_grapheme());
1366
1367 let id = cell.content.grapheme_id().unwrap();
1368 assert_eq!(frame.pool.get(id), Some(flag));
1369 }
1370
1371 #[test]
1374 fn hit_id_debug_clone_copy_hash() {
1375 let id = HitId::new(99);
1376 let dbg = format!("{:?}", id);
1377 assert!(dbg.contains("99"), "Debug: {dbg}");
1378 let copied: HitId = id; assert_eq!(id, copied);
1380 use std::collections::HashSet;
1382 let mut set = HashSet::new();
1383 set.insert(id);
1384 set.insert(HitId::new(99));
1385 assert_eq!(set.len(), 1);
1386 set.insert(HitId::new(100));
1387 assert_eq!(set.len(), 2);
1388 }
1389
1390 #[test]
1391 fn hit_id_eq_and_ne() {
1392 assert_eq!(HitId::new(0), HitId::new(0));
1393 assert_ne!(HitId::new(0), HitId::new(1));
1394 assert_ne!(HitId::new(u32::MAX), HitId::default());
1395 }
1396
1397 #[test]
1400 fn hit_region_debug_clone_copy_hash() {
1401 let r = HitRegion::Custom(42);
1402 let dbg = format!("{:?}", r);
1403 assert!(dbg.contains("Custom"), "Debug: {dbg}");
1404 let copied: HitRegion = r; assert_eq!(r, copied);
1406 use std::collections::HashSet;
1407 let mut set = HashSet::new();
1408 set.insert(r);
1409 set.insert(HitRegion::Custom(42));
1410 assert_eq!(set.len(), 1);
1411 }
1412
1413 #[test]
1416 fn hit_cell_debug_clone_copy_eq() {
1417 let cell = HitCell::new(HitId::new(5), HitRegion::Link, 123);
1418 let dbg = format!("{:?}", cell);
1419 assert!(dbg.contains("Link"), "Debug: {dbg}");
1420 let copied: HitCell = cell; assert_eq!(cell, copied);
1422 assert_ne!(cell, HitCell::default());
1424 }
1425
1426 #[test]
1429 fn hit_grid_clone() {
1430 let mut grid = HitGrid::new(5, 5);
1431 grid.register(Rect::new(0, 0, 2, 2), HitId::new(1), HitRegion::Content, 7);
1432 let clone = grid.clone();
1433 assert_eq!(clone.width(), 5);
1434 assert_eq!(
1435 clone.hit_test(0, 0),
1436 Some((HitId::new(1), HitRegion::Content, 7))
1437 );
1438 }
1439
1440 #[test]
1441 fn hit_grid_get_mut() {
1442 let mut grid = HitGrid::new(5, 5);
1443 if let Some(cell) = grid.get_mut(2, 3) {
1445 *cell = HitCell::new(HitId::new(77), HitRegion::Handle, 55);
1446 }
1447 assert_eq!(
1448 grid.hit_test(2, 3),
1449 Some((HitId::new(77), HitRegion::Handle, 55))
1450 );
1451 assert!(grid.get_mut(5, 5).is_none());
1453 }
1454
1455 #[test]
1456 fn hit_grid_zero_width_nonzero_height() {
1457 let grid = HitGrid::new(0, 10);
1458 assert_eq!(grid.width(), 0);
1459 assert_eq!(grid.height(), 10);
1460 assert!(grid.get(0, 0).is_none());
1461 assert!(grid.hit_test(0, 5).is_none());
1462 }
1463
1464 #[test]
1465 fn hit_grid_nonzero_width_zero_height() {
1466 let grid = HitGrid::new(10, 0);
1467 assert_eq!(grid.width(), 10);
1468 assert_eq!(grid.height(), 0);
1469 assert!(grid.get(0, 0).is_none());
1470 }
1471
1472 #[test]
1473 fn hit_grid_register_zero_width_rect() {
1474 let mut grid = HitGrid::new(10, 10);
1475 grid.register(Rect::new(2, 2, 0, 5), HitId::new(1), HitRegion::Content, 0);
1476 assert!(grid.hit_test(2, 2).is_none());
1478 }
1479
1480 #[test]
1481 fn hit_grid_register_zero_height_rect() {
1482 let mut grid = HitGrid::new(10, 10);
1483 grid.register(Rect::new(2, 2, 5, 0), HitId::new(1), HitRegion::Content, 0);
1484 assert!(grid.hit_test(2, 2).is_none());
1485 }
1486
1487 #[test]
1488 fn hit_grid_register_past_bounds() {
1489 let mut grid = HitGrid::new(10, 10);
1490 grid.register(
1492 Rect::new(10, 10, 5, 5),
1493 HitId::new(1),
1494 HitRegion::Content,
1495 0,
1496 );
1497 assert!(grid.hit_test(9, 9).is_none());
1498 }
1499
1500 #[test]
1501 fn hit_grid_full_coverage() {
1502 let mut grid = HitGrid::new(3, 3);
1503 grid.register(Rect::new(0, 0, 3, 3), HitId::new(1), HitRegion::Content, 0);
1504 for y in 0..3 {
1506 for x in 0..3 {
1507 assert_eq!(
1508 grid.hit_test(x, y),
1509 Some((HitId::new(1), HitRegion::Content, 0))
1510 );
1511 }
1512 }
1513 }
1514
1515 #[test]
1516 fn hit_grid_single_cell() {
1517 let mut grid = HitGrid::new(1, 1);
1518 grid.register(Rect::new(0, 0, 1, 1), HitId::new(1), HitRegion::Button, 42);
1519 assert_eq!(
1520 grid.hit_test(0, 0),
1521 Some((HitId::new(1), HitRegion::Button, 42))
1522 );
1523 assert!(grid.hit_test(1, 0).is_none());
1524 assert!(grid.hit_test(0, 1).is_none());
1525 }
1526
1527 #[test]
1528 fn hit_grid_hits_in_outside_rect() {
1529 let mut grid = HitGrid::new(5, 5);
1530 grid.register(Rect::new(0, 0, 2, 2), HitId::new(1), HitRegion::Content, 0);
1531 let hits = grid.hits_in(Rect::new(3, 3, 2, 2));
1533 assert!(hits.is_empty());
1534 }
1535
1536 #[test]
1537 fn hit_grid_hits_in_zero_rect() {
1538 let mut grid = HitGrid::new(5, 5);
1539 grid.register(Rect::new(0, 0, 5, 5), HitId::new(1), HitRegion::Content, 0);
1540 let hits = grid.hits_in(Rect::new(2, 2, 0, 0));
1541 assert!(hits.is_empty());
1542 }
1543
1544 #[test]
1547 fn cost_estimate_source_traits() {
1548 let a = CostEstimateSource::Measured;
1549 let b = CostEstimateSource::AreaFallback;
1550 let c = CostEstimateSource::FixedDefault;
1551 let dbg = format!("{:?}", a);
1552 assert!(dbg.contains("Measured"), "Debug: {dbg}");
1553
1554 assert_eq!(
1556 CostEstimateSource::default(),
1557 CostEstimateSource::FixedDefault
1558 );
1559
1560 let copied: CostEstimateSource = a;
1562 assert_eq!(a, copied);
1563
1564 assert_ne!(a, b);
1566 assert_ne!(b, c);
1567 assert_ne!(a, c);
1568 }
1569
1570 #[test]
1573 fn widget_signal_default() {
1574 let sig = WidgetSignal::default();
1575 assert_eq!(sig.widget_id, 0);
1576 assert!(!sig.essential);
1577 assert!((sig.priority - 0.5).abs() < f32::EPSILON);
1578 assert_eq!(sig.staleness_ms, 0);
1579 assert!((sig.focus_boost - 0.0).abs() < f32::EPSILON);
1580 assert!((sig.interaction_boost - 0.0).abs() < f32::EPSILON);
1581 assert_eq!(sig.area_cells, 1);
1582 assert!((sig.cost_estimate_us - 5.0).abs() < f32::EPSILON);
1583 assert!((sig.recent_cost_us - 5.0).abs() < f32::EPSILON);
1584 assert_eq!(sig.estimate_source, CostEstimateSource::FixedDefault);
1585 }
1586
1587 #[test]
1588 fn widget_signal_new() {
1589 let sig = WidgetSignal::new(42);
1590 assert_eq!(sig.widget_id, 42);
1591 assert!(!sig.essential);
1593 assert!((sig.priority - 0.5).abs() < f32::EPSILON);
1594 }
1595
1596 #[test]
1597 fn widget_signal_debug_clone() {
1598 let sig = WidgetSignal::new(7);
1599 let dbg = format!("{:?}", sig);
1600 assert!(dbg.contains("widget_id"), "Debug: {dbg}");
1601 let cloned = sig.clone();
1602 assert_eq!(cloned.widget_id, 7);
1603 }
1604
1605 #[test]
1608 fn widget_budget_default_is_allow_all() {
1609 let budget = WidgetBudget::default();
1610 assert!(budget.allows(0, false));
1611 assert!(budget.allows(u64::MAX, false));
1612 assert!(budget.allows(42, true));
1613 }
1614
1615 #[test]
1616 fn widget_budget_allow_only() {
1617 let budget = WidgetBudget::allow_only(vec![10, 20, 30]);
1618 assert!(budget.allows(10, false));
1619 assert!(budget.allows(20, false));
1620 assert!(budget.allows(30, false));
1621 assert!(!budget.allows(15, false));
1622 assert!(!budget.allows(0, false));
1623 }
1624
1625 #[test]
1626 fn widget_budget_essential_always_allowed() {
1627 let budget = WidgetBudget::allow_only(vec![10]);
1628 assert!(budget.allows(999, true));
1630 assert!(budget.allows(0, true));
1631 }
1632
1633 #[test]
1634 fn widget_budget_allow_only_dedup() {
1635 let budget = WidgetBudget::allow_only(vec![5, 5, 5, 10, 10]);
1636 assert!(budget.allows(5, false));
1637 assert!(budget.allows(10, false));
1638 assert!(!budget.allows(7, false));
1639 }
1640
1641 #[test]
1642 fn widget_budget_allow_only_empty() {
1643 let budget = WidgetBudget::allow_only(vec![]);
1644 assert!(!budget.allows(0, false));
1646 assert!(!budget.allows(1, false));
1647 assert!(budget.allows(1, true)); }
1649
1650 #[test]
1651 fn widget_budget_debug_clone() {
1652 let budget = WidgetBudget::allow_only(vec![1, 2, 3]);
1653 let dbg = format!("{:?}", budget);
1654 assert!(dbg.contains("allow_list"), "Debug: {dbg}");
1655 let cloned = budget.clone();
1656 assert!(cloned.allows(2, false));
1657 }
1658
1659 #[test]
1662 fn frame_zero_dimensions_clamped_to_one() {
1663 let mut pool = GraphemePool::new();
1664 let frame = Frame::new(0, 0, &mut pool);
1665 assert_eq!(frame.buffer.width(), 1);
1666 assert_eq!(frame.buffer.height(), 1);
1667 }
1668
1669 #[test]
1670 fn frame_from_buffer() {
1671 let mut pool = GraphemePool::new();
1672 let mut buf = Buffer::new(20, 10);
1673 buf.set_raw(5, 5, Cell::from_char('Z'));
1674 let frame = Frame::from_buffer(buf, &mut pool);
1675 assert_eq!(frame.width(), 20);
1676 assert_eq!(frame.height(), 10);
1677 assert_eq!(frame.buffer.get(5, 5).unwrap().content.as_char(), Some('Z'));
1678 assert!(frame.hit_grid.is_none());
1679 assert!(frame.cursor_visible);
1680 }
1681
1682 #[test]
1683 fn frame_with_links() {
1684 let mut pool = GraphemePool::new();
1685 let mut links = LinkRegistry::new();
1686 let frame = Frame::with_links(10, 5, &mut pool, &mut links);
1687 assert!(frame.links.is_some());
1688 assert_eq!(frame.width(), 10);
1689 assert_eq!(frame.height(), 5);
1690 }
1691
1692 #[test]
1693 fn frame_set_links() {
1694 let mut pool = GraphemePool::new();
1695 let mut links = LinkRegistry::new();
1696 let mut frame = Frame::new(10, 5, &mut pool);
1697 assert!(frame.links.is_none());
1698 frame.set_links(&mut links);
1699 assert!(frame.links.is_some());
1700 }
1701
1702 #[test]
1703 fn frame_register_link_no_registry() {
1704 let mut pool = GraphemePool::new();
1705 let mut frame = Frame::new(10, 5, &mut pool);
1706 let id = frame.register_link("https://example.com");
1708 assert_eq!(id, 0);
1709 }
1710
1711 #[test]
1712 fn frame_register_link_with_registry() {
1713 let mut pool = GraphemePool::new();
1714 let mut links = LinkRegistry::new();
1715 let mut frame = Frame::with_links(10, 5, &mut pool, &mut links);
1716 let id = frame.register_link("https://example.com");
1717 assert!(id > 0);
1718 let id2 = frame.register_link("https://example.com");
1720 assert_eq!(id, id2);
1721 let id3 = frame.register_link("https://other.com");
1723 assert_ne!(id, id3);
1724 }
1725
1726 #[test]
1729 fn frame_set_widget_budget() {
1730 let mut pool = GraphemePool::new();
1731 let mut frame = Frame::new(10, 10, &mut pool);
1732
1733 assert!(frame.should_render_widget(42, false));
1735
1736 frame.set_widget_budget(WidgetBudget::allow_only(vec![1, 2]));
1738 assert!(frame.should_render_widget(1, false));
1739 assert!(!frame.should_render_widget(42, false));
1740 assert!(frame.should_render_widget(42, true)); }
1742
1743 #[test]
1746 fn frame_widget_signals_lifecycle() {
1747 let mut pool = GraphemePool::new();
1748 let mut frame = Frame::new(10, 10, &mut pool);
1749 assert!(frame.widget_signals().is_empty());
1750
1751 frame.register_widget_signal(WidgetSignal::new(1));
1752 frame.register_widget_signal(WidgetSignal::new(2));
1753 assert_eq!(frame.widget_signals().len(), 2);
1754 assert_eq!(frame.widget_signals()[0].widget_id, 1);
1755 assert_eq!(frame.widget_signals()[1].widget_id, 2);
1756
1757 let taken = frame.take_widget_signals();
1758 assert_eq!(taken.len(), 2);
1759 assert!(frame.widget_signals().is_empty());
1760 }
1761
1762 #[test]
1763 fn frame_clear_resets_signals_and_cursor() {
1764 let mut pool = GraphemePool::new();
1765 let mut frame = Frame::new(10, 10, &mut pool);
1766 frame.set_cursor(Some((5, 5)));
1767 frame.register_widget_signal(WidgetSignal::new(1));
1768 assert!(frame.cursor_position.is_some());
1769 assert!(!frame.widget_signals().is_empty());
1770
1771 frame.clear();
1772 assert!(frame.cursor_position.is_none());
1773 assert!(frame.widget_signals().is_empty());
1774 }
1775
1776 #[test]
1779 fn frame_set_degradation_propagates_to_buffer() {
1780 let mut pool = GraphemePool::new();
1781 let mut frame = Frame::new(10, 10, &mut pool);
1782 assert_eq!(frame.degradation, DegradationLevel::Full);
1783 assert_eq!(frame.buffer.degradation, DegradationLevel::Full);
1784
1785 frame.set_degradation(DegradationLevel::SimpleBorders);
1786 assert_eq!(frame.degradation, DegradationLevel::SimpleBorders);
1787 assert_eq!(frame.buffer.degradation, DegradationLevel::SimpleBorders);
1788
1789 frame.set_degradation(DegradationLevel::EssentialOnly);
1790 assert_eq!(frame.degradation, DegradationLevel::EssentialOnly);
1791 assert_eq!(frame.buffer.degradation, DegradationLevel::EssentialOnly);
1792 }
1793
1794 #[test]
1797 fn frame_with_hit_grid_zero_size_clamped_to_one() {
1798 let mut pool = GraphemePool::new();
1799 let frame = Frame::with_hit_grid(0, 0, &mut pool);
1800 assert_eq!(frame.buffer.width(), 1);
1801 assert_eq!(frame.buffer.height(), 1);
1802 }
1803
1804 #[test]
1807 fn frame_register_hit_with_all_regions() {
1808 let mut pool = GraphemePool::new();
1809 let mut frame = Frame::with_hit_grid(20, 20, &mut pool);
1810 let regions = [
1811 HitRegion::Content,
1812 HitRegion::Border,
1813 HitRegion::Scrollbar,
1814 HitRegion::Handle,
1815 HitRegion::Button,
1816 HitRegion::Link,
1817 HitRegion::Custom(0),
1818 HitRegion::Custom(255),
1819 ];
1820 for (i, ®ion) in regions.iter().enumerate() {
1821 let y = i as u16;
1822 frame.register_hit(Rect::new(0, y, 1, 1), HitId::new(i as u32), region, 0);
1823 }
1824 for (i, ®ion) in regions.iter().enumerate() {
1825 let y = i as u16;
1826 assert_eq!(
1827 frame.hit_test(0, y),
1828 Some((HitId::new(i as u32), region, 0))
1829 );
1830 }
1831 }
1832
1833 #[test]
1836 fn frame_draw_horizontal_line() {
1837 let mut pool = GraphemePool::new();
1838 let mut frame = Frame::new(10, 5, &mut pool);
1839 let cell = Cell::from_char('-');
1840 frame.draw_horizontal_line(2, 1, 5, cell);
1841 for x in 2..7 {
1842 assert_eq!(frame.buffer.get(x, 1).unwrap().content.as_char(), Some('-'));
1843 }
1844 assert!(frame.buffer.get(1, 1).unwrap().is_empty());
1846 assert!(frame.buffer.get(7, 1).unwrap().is_empty());
1847 }
1848
1849 #[test]
1850 fn frame_draw_vertical_line() {
1851 let mut pool = GraphemePool::new();
1852 let mut frame = Frame::new(10, 10, &mut pool);
1853 let cell = Cell::from_char('|');
1854 frame.draw_vertical_line(3, 2, 4, cell);
1855 for y in 2..6 {
1856 assert_eq!(frame.buffer.get(3, y).unwrap().content.as_char(), Some('|'));
1857 }
1858 assert!(frame.buffer.get(3, 1).unwrap().is_empty());
1859 assert!(frame.buffer.get(3, 6).unwrap().is_empty());
1860 }
1861
1862 #[test]
1863 fn frame_draw_rect_filled() {
1864 let mut pool = GraphemePool::new();
1865 let mut frame = Frame::new(10, 10, &mut pool);
1866 let cell = Cell::from_char('#');
1867 frame.draw_rect_filled(Rect::new(1, 1, 3, 3), cell);
1868 for y in 1..4 {
1869 for x in 1..4 {
1870 assert_eq!(frame.buffer.get(x, y).unwrap().content.as_char(), Some('#'));
1871 }
1872 }
1873 assert!(frame.buffer.get(0, 0).unwrap().is_empty());
1875 assert!(frame.buffer.get(4, 4).unwrap().is_empty());
1876 }
1877
1878 #[test]
1879 fn frame_paint_area() {
1880 use crate::cell::PackedRgba;
1881 let mut pool = GraphemePool::new();
1882 let mut frame = Frame::new(5, 5, &mut pool);
1883 let red = PackedRgba::rgb(255, 0, 0);
1884 frame.paint_area(Rect::new(0, 0, 2, 2), Some(red), None);
1885 let cell = frame.buffer.get(0, 0).unwrap();
1886 assert_eq!(cell.fg, red);
1887 }
1888
1889 #[test]
1892 fn frame_print_text_clipped_at_boundary() {
1893 let mut pool = GraphemePool::new();
1894 let mut frame = Frame::new(5, 1, &mut pool);
1895 let end = frame.print_text(0, 0, "Hello World", Cell::from_char(' '));
1897 assert_eq!(end, 5);
1898 for x in 0..5 {
1899 assert!(!frame.buffer.get(x, 0).unwrap().is_empty());
1900 }
1901 }
1902
1903 #[test]
1904 fn frame_print_text_empty_string() {
1905 let mut pool = GraphemePool::new();
1906 let mut frame = Frame::new(10, 1, &mut pool);
1907 let end = frame.print_text(0, 0, "", Cell::from_char(' '));
1908 assert_eq!(end, 0);
1909 }
1910
1911 #[test]
1912 fn frame_print_text_at_right_edge() {
1913 let mut pool = GraphemePool::new();
1914 let mut frame = Frame::new(5, 1, &mut pool);
1915 let end = frame.print_text(4, 0, "AB", Cell::from_char(' '));
1917 assert_eq!(end, 5);
1918 assert_eq!(frame.buffer.get(4, 0).unwrap().content.as_char(), Some('A'));
1919 }
1920
1921 #[test]
1924 fn frame_debug() {
1925 let mut pool = GraphemePool::new();
1926 let frame = Frame::new(5, 3, &mut pool);
1927 let dbg = format!("{:?}", frame);
1928 assert!(dbg.contains("Frame"), "Debug: {dbg}");
1929 }
1930
1931 #[test]
1934 fn hit_grid_debug() {
1935 let grid = HitGrid::new(3, 3);
1936 let dbg = format!("{:?}", grid);
1937 assert!(dbg.contains("HitGrid"), "Debug: {dbg}");
1938 }
1939
1940 #[test]
1943 fn frame_cursor_beyond_bounds() {
1944 let mut pool = GraphemePool::new();
1945 let mut frame = Frame::new(10, 10, &mut pool);
1946 frame.set_cursor(Some((100, 200)));
1948 assert_eq!(frame.cursor_position, Some((100, 200)));
1949 }
1950
1951 #[test]
1954 fn hit_grid_register_overwrite() {
1955 let mut grid = HitGrid::new(5, 5);
1956 grid.register(Rect::new(0, 0, 3, 3), HitId::new(1), HitRegion::Content, 10);
1957 grid.register(Rect::new(0, 0, 3, 3), HitId::new(2), HitRegion::Button, 20);
1958 assert_eq!(
1960 grid.hit_test(1, 1),
1961 Some((HitId::new(2), HitRegion::Button, 20))
1962 );
1963 }
1964
1965 #[test]
1966 fn frame_hit_test_detailed_preserves_owner() {
1967 let mut pool = GraphemePool::new();
1968 let mut frame = Frame::with_hit_grid(4, 4, &mut pool);
1969
1970 frame.with_hit_owner(77, |frame| {
1971 frame.register_hit(Rect::new(1, 1, 2, 2), HitId::new(5), HitRegion::Button, 9);
1972 });
1973
1974 assert_eq!(
1975 frame.hit_test_detailed(1, 1),
1976 Some(HitTestResult::new(
1977 HitId::new(5),
1978 HitRegion::Button,
1979 9,
1980 Some(77),
1981 ))
1982 );
1983 assert_eq!(
1984 frame.hit_test(1, 1),
1985 Some((HitId::new(5), HitRegion::Button, 9))
1986 );
1987 }
1988
1989 #[test]
1990 fn frame_hit_owner_scope_restores_previous_owner() {
1991 let mut pool = GraphemePool::new();
1992 let mut frame = Frame::with_hit_grid(4, 4, &mut pool);
1993
1994 frame.with_hit_owner(10, |frame| {
1995 frame.register_hit(Rect::new(0, 0, 1, 1), HitId::new(1), HitRegion::Content, 1);
1996 frame.with_hit_owner(20, |frame| {
1997 frame.register_hit(Rect::new(1, 0, 1, 1), HitId::new(2), HitRegion::Content, 2);
1998 });
1999 frame.register_hit(Rect::new(2, 0, 1, 1), HitId::new(3), HitRegion::Content, 3);
2000 });
2001
2002 assert_eq!(frame.hit_test_detailed(0, 0).unwrap().owner, Some(10));
2003 assert_eq!(frame.hit_test_detailed(1, 0).unwrap().owner, Some(20));
2004 assert_eq!(frame.hit_test_detailed(2, 0).unwrap().owner, Some(10));
2005 }
2006
2007 #[test]
2008 fn frame_hit_owner_scope_restores_after_panic() {
2009 let mut pool = GraphemePool::new();
2010 let mut frame = Frame::with_hit_grid(4, 4, &mut pool);
2011
2012 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2013 frame.with_hit_owner(55, |_frame| panic!("boom"));
2014 }));
2015 assert!(result.is_err());
2016
2017 frame.register_hit(Rect::new(0, 0, 1, 1), HitId::new(9), HitRegion::Content, 0);
2018 assert_eq!(frame.hit_test_detailed(0, 0).unwrap().owner, None);
2019 }
2020}