#![forbid(unsafe_code)]
use crate::arena::FrameArena;
use crate::budget::DegradationLevel;
use crate::buffer::Buffer;
use crate::cell::{Cell, CellContent, GraphemeId};
use crate::drawing::{BorderChars, Draw};
use crate::grapheme_pool::GraphemePool;
use crate::{display_width, grapheme_width};
use ftui_core::geometry::Rect;
use unicode_segmentation::UnicodeSegmentation;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct HitId(pub u32);
impl HitId {
#[inline]
pub const fn new(id: u32) -> Self {
Self(id)
}
#[inline]
pub const fn id(self) -> u32 {
self.0
}
}
pub type HitData = u64;
pub type HitOwner = u64;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum HitRegion {
#[default]
None,
Content,
Border,
Scrollbar,
Handle,
Button,
Link,
Custom(u8),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HitTestResult {
pub id: HitId,
pub region: HitRegion,
pub data: HitData,
pub owner: Option<HitOwner>,
}
impl HitTestResult {
#[inline]
pub const fn new(id: HitId, region: HitRegion, data: HitData, owner: Option<HitOwner>) -> Self {
Self {
id,
region,
data,
owner,
}
}
#[inline]
pub const fn into_tuple(self) -> (HitId, HitRegion, HitData) {
(self.id, self.region, self.data)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct HitCell {
pub widget_id: Option<HitId>,
pub region: HitRegion,
pub data: HitData,
pub owner: Option<HitOwner>,
}
impl HitCell {
#[inline]
pub const fn new(widget_id: HitId, region: HitRegion, data: HitData) -> Self {
Self {
widget_id: Some(widget_id),
region,
data,
owner: None,
}
}
#[inline]
pub const fn new_with_owner(
widget_id: HitId,
region: HitRegion,
data: HitData,
owner: Option<HitOwner>,
) -> Self {
Self {
widget_id: Some(widget_id),
region,
data,
owner,
}
}
#[inline]
pub const fn is_empty(&self) -> bool {
self.widget_id.is_none()
}
}
#[derive(Debug, Clone)]
pub struct HitGrid {
width: u16,
height: u16,
cells: Vec<HitCell>,
}
impl HitGrid {
pub fn new(width: u16, height: u16) -> Self {
let size = width as usize * height as usize;
Self {
width,
height,
cells: vec![HitCell::default(); size],
}
}
#[inline]
pub const fn width(&self) -> u16 {
self.width
}
#[inline]
pub const fn height(&self) -> u16 {
self.height
}
#[inline]
fn index(&self, x: u16, y: u16) -> Option<usize> {
if x < self.width && y < self.height {
Some(y as usize * self.width as usize + x as usize)
} else {
None
}
}
#[inline]
#[must_use]
pub fn get(&self, x: u16, y: u16) -> Option<&HitCell> {
self.index(x, y).map(|i| &self.cells[i])
}
#[inline]
#[must_use]
pub fn get_mut(&mut self, x: u16, y: u16) -> Option<&mut HitCell> {
self.index(x, y).map(|i| &mut self.cells[i])
}
pub fn register(&mut self, rect: Rect, widget_id: HitId, region: HitRegion, data: HitData) {
self.register_with_owner(rect, widget_id, region, data, None);
}
pub fn register_with_owner(
&mut self,
rect: Rect,
widget_id: HitId,
region: HitRegion,
data: HitData,
owner: Option<HitOwner>,
) {
let x_end = (rect.x as usize + rect.width as usize).min(self.width as usize);
let y_end = (rect.y as usize + rect.height as usize).min(self.height as usize);
if rect.x as usize >= x_end || rect.y as usize >= y_end {
return;
}
let hit_cell = HitCell::new_with_owner(widget_id, region, data, owner);
for y in rect.y as usize..y_end {
let row_start = y * self.width as usize;
let start = row_start + rect.x as usize;
let end = row_start + x_end;
self.cells[start..end].fill(hit_cell);
}
}
#[must_use]
pub fn hit_test(&self, x: u16, y: u16) -> Option<(HitId, HitRegion, HitData)> {
self.hit_test_detailed(x, y).map(HitTestResult::into_tuple)
}
#[must_use]
pub fn hit_test_detailed(&self, x: u16, y: u16) -> Option<HitTestResult> {
self.get(x, y).and_then(|cell| {
cell.widget_id
.map(|id| HitTestResult::new(id, cell.region, cell.data, cell.owner))
})
}
pub fn hits_in(&self, rect: Rect) -> Vec<(HitId, HitRegion, HitData)> {
let x_end = (rect.x as usize + rect.width as usize).min(self.width as usize) as u16;
let y_end = (rect.y as usize + rect.height as usize).min(self.height as usize) as u16;
let mut hits = Vec::new();
for y in rect.y..y_end {
for x in rect.x..x_end {
if let Some((id, region, data)) = self.hit_test(x, y) {
hits.push((id, region, data));
}
}
}
hits
}
pub fn clear(&mut self) {
self.cells.fill(HitCell::default());
}
}
use crate::link_registry::LinkRegistry;
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum CostEstimateSource {
Measured,
AreaFallback,
#[default]
FixedDefault,
}
#[derive(Debug, Clone)]
pub struct WidgetSignal {
pub widget_id: u64,
pub essential: bool,
pub priority: f32,
pub staleness_ms: u64,
pub focus_boost: f32,
pub interaction_boost: f32,
pub area_cells: u32,
pub cost_estimate_us: f32,
pub recent_cost_us: f32,
pub estimate_source: CostEstimateSource,
}
impl Default for WidgetSignal {
fn default() -> Self {
Self {
widget_id: 0,
essential: false,
priority: 0.5,
staleness_ms: 0,
focus_boost: 0.0,
interaction_boost: 0.0,
area_cells: 1,
cost_estimate_us: 5.0,
recent_cost_us: 5.0,
estimate_source: CostEstimateSource::FixedDefault,
}
}
}
impl WidgetSignal {
#[must_use]
pub fn new(widget_id: u64) -> Self {
Self {
widget_id,
..Self::default()
}
}
}
#[derive(Debug, Clone)]
pub struct WidgetBudget {
allow_list: Option<Vec<u64>>,
}
impl Default for WidgetBudget {
fn default() -> Self {
Self::allow_all()
}
}
impl WidgetBudget {
#[must_use]
pub fn allow_all() -> Self {
Self { allow_list: None }
}
#[must_use]
pub fn allow_only(mut ids: Vec<u64>) -> Self {
ids.sort_unstable();
ids.dedup();
Self {
allow_list: Some(ids),
}
}
#[inline]
pub fn allows(&self, widget_id: u64, essential: bool) -> bool {
if essential {
return true;
}
match &self.allow_list {
None => true,
Some(ids) => ids.binary_search(&widget_id).is_ok(),
}
}
}
#[derive(Debug)]
pub struct Frame<'a> {
pub buffer: Buffer,
pub pool: &'a mut GraphemePool,
pub links: Option<&'a mut LinkRegistry>,
pub hit_grid: Option<HitGrid>,
hit_owner_stack: Vec<HitOwner>,
pub widget_budget: WidgetBudget,
pub widget_signals: Vec<WidgetSignal>,
pub cursor_position: Option<(u16, u16)>,
pub cursor_visible: bool,
pub degradation: DegradationLevel,
pub arena: Option<&'a FrameArena>,
}
impl<'a> Frame<'a> {
pub fn new(width: u16, height: u16, pool: &'a mut GraphemePool) -> Self {
Self {
buffer: Buffer::new(width, height),
pool,
links: None,
hit_grid: None,
hit_owner_stack: Vec::new(),
widget_budget: WidgetBudget::default(),
widget_signals: Vec::new(),
cursor_position: None,
cursor_visible: true,
degradation: DegradationLevel::Full,
arena: None,
}
}
pub fn from_buffer(buffer: Buffer, pool: &'a mut GraphemePool) -> Self {
Self {
buffer,
pool,
links: None,
hit_grid: None,
hit_owner_stack: Vec::new(),
widget_budget: WidgetBudget::default(),
widget_signals: Vec::new(),
cursor_position: None,
cursor_visible: true,
degradation: DegradationLevel::Full,
arena: None,
}
}
pub fn with_links(
width: u16,
height: u16,
pool: &'a mut GraphemePool,
links: &'a mut LinkRegistry,
) -> Self {
Self {
buffer: Buffer::new(width, height),
pool,
links: Some(links),
hit_grid: None,
hit_owner_stack: Vec::new(),
widget_budget: WidgetBudget::default(),
widget_signals: Vec::new(),
cursor_position: None,
cursor_visible: true,
degradation: DegradationLevel::Full,
arena: None,
}
}
pub fn with_hit_grid(width: u16, height: u16, pool: &'a mut GraphemePool) -> Self {
Self {
buffer: Buffer::new(width, height),
pool,
links: None,
hit_grid: Some(HitGrid::new(width, height)),
hit_owner_stack: Vec::new(),
widget_budget: WidgetBudget::default(),
widget_signals: Vec::new(),
cursor_position: None,
cursor_visible: true,
degradation: DegradationLevel::Full,
arena: None,
}
}
pub fn set_links(&mut self, links: &'a mut LinkRegistry) {
self.links = Some(links);
}
pub fn set_arena(&mut self, arena: &'a FrameArena) {
self.arena = Some(arena);
}
pub fn arena(&self) -> Option<&FrameArena> {
self.arena
}
pub fn register_link(&mut self, url: &str) -> u32 {
if let Some(ref mut links) = self.links {
links.register(url)
} else {
0
}
}
pub fn set_widget_budget(&mut self, budget: WidgetBudget) {
self.widget_budget = budget;
}
#[inline]
pub fn should_render_widget(&self, widget_id: u64, essential: bool) -> bool {
self.widget_budget.allows(widget_id, essential)
}
pub fn register_widget_signal(&mut self, signal: WidgetSignal) {
self.widget_signals.push(signal);
}
#[inline]
pub fn widget_signals(&self) -> &[WidgetSignal] {
&self.widget_signals
}
#[inline]
pub fn take_widget_signals(&mut self) -> Vec<WidgetSignal> {
std::mem::take(&mut self.widget_signals)
}
pub fn intern(&mut self, text: &str) -> GraphemeId {
let width = display_width(text).min(GraphemeId::MAX_WIDTH as usize) as u8;
self.pool.intern(text, width)
}
pub fn intern_with_width(&mut self, text: &str, width: u8) -> GraphemeId {
self.pool.intern(text, width)
}
pub fn enable_hit_testing(&mut self) {
if self.hit_grid.is_none() {
self.hit_grid = Some(HitGrid::new(self.width(), self.height()));
}
}
#[inline]
pub fn width(&self) -> u16 {
self.buffer.width()
}
#[inline]
pub fn height(&self) -> u16 {
self.buffer.height()
}
pub fn clear(&mut self) {
self.buffer.clear();
if let Some(ref mut grid) = self.hit_grid {
grid.clear();
}
self.cursor_position = None;
self.widget_signals.clear();
}
#[inline]
pub fn set_cursor(&mut self, position: Option<(u16, u16)>) {
self.cursor_position = position;
}
#[inline]
pub fn set_cursor_visible(&mut self, visible: bool) {
self.cursor_visible = visible;
}
#[inline]
pub fn set_degradation(&mut self, level: DegradationLevel) {
self.degradation = level;
self.buffer.degradation = level;
}
#[inline]
pub fn bounds(&self) -> Rect {
self.buffer.bounds()
}
pub fn register_hit(
&mut self,
rect: Rect,
id: HitId,
region: HitRegion,
data: HitData,
) -> bool {
let owner = self.current_hit_owner();
if let Some(ref mut grid) = self.hit_grid {
let clipped = rect.intersection(&self.buffer.current_scissor());
if !clipped.is_empty() {
grid.register_with_owner(clipped, id, region, data, owner);
}
true
} else {
false
}
}
pub fn with_hit_owner<R>(&mut self, owner: HitOwner, f: impl FnOnce(&mut Self) -> R) -> R {
self.hit_owner_stack.push(owner);
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| f(self)));
self.hit_owner_stack.pop();
match result {
Ok(result) => result,
Err(payload) => std::panic::resume_unwind(payload),
}
}
#[must_use]
pub fn hit_test(&self, x: u16, y: u16) -> Option<(HitId, HitRegion, HitData)> {
self.hit_grid.as_ref().and_then(|grid| grid.hit_test(x, y))
}
#[must_use]
pub fn hit_test_detailed(&self, x: u16, y: u16) -> Option<HitTestResult> {
self.hit_grid
.as_ref()
.and_then(|grid| grid.hit_test_detailed(x, y))
}
pub fn register_hit_region(&mut self, rect: Rect, id: HitId) -> bool {
self.register_hit(rect, id, HitRegion::Content, 0)
}
#[inline]
fn current_hit_owner(&self) -> Option<HitOwner> {
self.hit_owner_stack.last().copied()
}
}
impl<'a> Draw for Frame<'a> {
fn draw_horizontal_line(&mut self, x: u16, y: u16, width: u16, cell: Cell) {
self.buffer.draw_horizontal_line(x, y, width, cell);
}
fn draw_vertical_line(&mut self, x: u16, y: u16, height: u16, cell: Cell) {
self.buffer.draw_vertical_line(x, y, height, cell);
}
fn draw_rect_filled(&mut self, rect: Rect, cell: Cell) {
self.buffer.draw_rect_filled(rect, cell);
}
fn draw_rect_outline(&mut self, rect: Rect, cell: Cell) {
self.buffer.draw_rect_outline(rect, cell);
}
fn print_text(&mut self, x: u16, y: u16, text: &str, base_cell: Cell) -> u16 {
self.print_text_clipped(x, y, text, base_cell, self.width())
}
fn print_text_clipped(
&mut self,
x: u16,
y: u16,
text: &str,
base_cell: Cell,
max_x: u16,
) -> u16 {
let mut cx = x;
for grapheme in text.graphemes(true) {
let width = grapheme_width(grapheme);
if width == 0 {
continue;
}
if cx >= max_x {
break;
}
if cx as u32 + width as u32 > max_x as u32 {
break;
}
let content = if width > 1 || grapheme.chars().count() > 1 {
let id = self.intern_with_width(grapheme, width as u8);
CellContent::from_grapheme(id)
} else if let Some(c) = grapheme.chars().next() {
CellContent::from_char(c)
} else {
continue;
};
let cell = Cell {
content,
fg: base_cell.fg,
bg: base_cell.bg,
attrs: base_cell.attrs,
};
self.buffer.set_fast(cx, y, cell);
cx = cx.saturating_add(width as u16);
}
cx
}
fn draw_border(&mut self, rect: Rect, chars: BorderChars, base_cell: Cell) {
self.buffer.draw_border(rect, chars, base_cell);
}
fn draw_box(&mut self, rect: Rect, chars: BorderChars, border_cell: Cell, fill_cell: Cell) {
self.buffer.draw_box(rect, chars, border_cell, fill_cell);
}
fn paint_area(
&mut self,
rect: Rect,
fg: Option<crate::cell::PackedRgba>,
bg: Option<crate::cell::PackedRgba>,
) {
self.buffer.paint_area(rect, fg, bg);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cell::Cell;
#[test]
fn frame_creation() {
let mut pool = GraphemePool::new();
let frame = Frame::new(80, 24, &mut pool);
assert_eq!(frame.width(), 80);
assert_eq!(frame.height(), 24);
assert!(frame.hit_grid.is_none());
assert!(frame.cursor_position.is_none());
assert!(frame.cursor_visible);
}
#[test]
fn frame_with_hit_grid() {
let mut pool = GraphemePool::new();
let frame = Frame::with_hit_grid(80, 24, &mut pool);
assert!(frame.hit_grid.is_some());
assert_eq!(frame.width(), 80);
assert_eq!(frame.height(), 24);
}
#[test]
fn frame_cursor() {
let mut pool = GraphemePool::new();
let mut frame = Frame::new(80, 24, &mut pool);
assert!(frame.cursor_position.is_none());
assert!(frame.cursor_visible);
frame.set_cursor(Some((10, 5)));
assert_eq!(frame.cursor_position, Some((10, 5)));
frame.set_cursor_visible(false);
assert!(!frame.cursor_visible);
frame.set_cursor(None);
assert!(frame.cursor_position.is_none());
}
#[test]
fn frame_clear() {
let mut pool = GraphemePool::new();
let mut frame = Frame::with_hit_grid(10, 10, &mut pool);
frame.buffer.set_raw(5, 5, Cell::from_char('X'));
frame.register_hit_region(Rect::new(0, 0, 5, 5), HitId::new(1));
assert_eq!(frame.buffer.get(5, 5).unwrap().content.as_char(), Some('X'));
assert_eq!(
frame.hit_test(2, 2),
Some((HitId::new(1), HitRegion::Content, 0))
);
frame.clear();
assert!(frame.buffer.get(5, 5).unwrap().is_empty());
assert!(frame.hit_test(2, 2).is_none());
}
#[test]
fn frame_bounds() {
let mut pool = GraphemePool::new();
let frame = Frame::new(80, 24, &mut pool);
let bounds = frame.bounds();
assert_eq!(bounds.x, 0);
assert_eq!(bounds.y, 0);
assert_eq!(bounds.width, 80);
assert_eq!(bounds.height, 24);
}
#[test]
fn hit_grid_creation() {
let grid = HitGrid::new(80, 24);
assert_eq!(grid.width(), 80);
assert_eq!(grid.height(), 24);
}
#[test]
fn hit_grid_registration() {
let mut pool = GraphemePool::new();
let mut frame = Frame::with_hit_grid(80, 24, &mut pool);
let hit_id = HitId::new(42);
let rect = Rect::new(10, 5, 20, 3);
frame.register_hit(rect, hit_id, HitRegion::Button, 99);
assert_eq!(frame.hit_test(15, 6), Some((hit_id, HitRegion::Button, 99)));
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()); }
#[test]
fn hit_grid_overlapping_regions() {
let mut pool = GraphemePool::new();
let mut frame = Frame::with_hit_grid(20, 20, &mut pool);
frame.register_hit(
Rect::new(0, 0, 10, 10),
HitId::new(1),
HitRegion::Content,
1,
);
frame.register_hit(Rect::new(5, 5, 10, 10), HitId::new(2), HitRegion::Border, 2);
assert_eq!(
frame.hit_test(2, 2),
Some((HitId::new(1), HitRegion::Content, 1))
);
assert_eq!(
frame.hit_test(7, 7),
Some((HitId::new(2), HitRegion::Border, 2))
);
assert_eq!(
frame.hit_test(12, 12),
Some((HitId::new(2), HitRegion::Border, 2))
);
}
#[test]
fn hit_grid_out_of_bounds() {
let mut pool = GraphemePool::new();
let frame = Frame::with_hit_grid(10, 10, &mut pool);
assert!(frame.hit_test(100, 100).is_none());
assert!(frame.hit_test(10, 0).is_none()); assert!(frame.hit_test(0, 10).is_none()); }
#[test]
fn hit_id_properties() {
let id = HitId::new(42);
assert_eq!(id.id(), 42);
assert_eq!(id, HitId(42));
}
#[test]
fn register_hit_region_no_grid() {
let mut pool = GraphemePool::new();
let mut frame = Frame::new(10, 10, &mut pool);
let result = frame.register_hit_region(Rect::new(0, 0, 5, 5), HitId::new(1));
assert!(!result); }
#[test]
fn register_hit_region_with_grid() {
let mut pool = GraphemePool::new();
let mut frame = Frame::with_hit_grid(10, 10, &mut pool);
let result = frame.register_hit_region(Rect::new(0, 0, 5, 5), HitId::new(1));
assert!(result); }
#[test]
fn hit_grid_clear() {
let mut grid = HitGrid::new(10, 10);
grid.register(Rect::new(0, 0, 5, 5), HitId::new(1), HitRegion::Content, 0);
assert_eq!(
grid.hit_test(2, 2),
Some((HitId::new(1), HitRegion::Content, 0))
);
grid.clear();
assert!(grid.hit_test(2, 2).is_none());
}
#[test]
fn hit_grid_boundary_clipping() {
let mut grid = HitGrid::new(10, 10);
grid.register(
Rect::new(8, 8, 10, 10),
HitId::new(1),
HitRegion::Content,
0,
);
assert_eq!(
grid.hit_test(9, 9),
Some((HitId::new(1), HitRegion::Content, 0))
);
assert!(grid.hit_test(10, 10).is_none());
}
#[test]
fn hit_grid_edge_and_corner_cells() {
let mut grid = HitGrid::new(4, 4);
grid.register(Rect::new(3, 0, 1, 4), HitId::new(7), HitRegion::Border, 11);
assert_eq!(
grid.hit_test(3, 0),
Some((HitId::new(7), HitRegion::Border, 11))
);
assert_eq!(
grid.hit_test(3, 3),
Some((HitId::new(7), HitRegion::Border, 11))
);
assert!(grid.hit_test(2, 0).is_none());
assert!(grid.hit_test(4, 0).is_none());
assert!(grid.hit_test(3, 4).is_none());
let mut grid = HitGrid::new(4, 4);
grid.register(Rect::new(0, 3, 4, 1), HitId::new(9), HitRegion::Content, 21);
assert_eq!(
grid.hit_test(0, 3),
Some((HitId::new(9), HitRegion::Content, 21))
);
assert_eq!(
grid.hit_test(3, 3),
Some((HitId::new(9), HitRegion::Content, 21))
);
assert!(grid.hit_test(0, 2).is_none());
assert!(grid.hit_test(0, 4).is_none());
}
#[test]
fn frame_register_hit_respects_nested_scissor() {
let mut pool = GraphemePool::new();
let mut frame = Frame::with_hit_grid(10, 10, &mut pool);
let outer = Rect::new(1, 1, 8, 8);
frame.buffer.push_scissor(outer);
assert_eq!(frame.buffer.current_scissor(), outer);
let inner = Rect::new(4, 4, 10, 10);
frame.buffer.push_scissor(inner);
let clipped = outer.intersection(&inner);
let current = frame.buffer.current_scissor();
assert_eq!(current, clipped);
assert!(outer.contains(current.x, current.y));
assert!(outer.contains(
current.right().saturating_sub(1),
current.bottom().saturating_sub(1)
));
frame.register_hit(
Rect::new(0, 0, 10, 10),
HitId::new(3),
HitRegion::Button,
99,
);
assert_eq!(
frame.hit_test(4, 4),
Some((HitId::new(3), HitRegion::Button, 99))
);
assert_eq!(
frame.hit_test(8, 8),
Some((HitId::new(3), HitRegion::Button, 99))
);
assert!(frame.hit_test(3, 3).is_none()); assert!(frame.hit_test(0, 0).is_none());
frame.buffer.pop_scissor();
assert_eq!(frame.buffer.current_scissor(), outer);
}
#[test]
fn hit_grid_hits_in_area() {
let mut grid = HitGrid::new(5, 5);
grid.register(Rect::new(0, 0, 2, 2), HitId::new(1), HitRegion::Content, 10);
grid.register(Rect::new(1, 1, 2, 2), HitId::new(2), HitRegion::Button, 20);
let hits = grid.hits_in(Rect::new(0, 0, 3, 3));
assert!(hits.contains(&(HitId::new(1), HitRegion::Content, 10)));
assert!(hits.contains(&(HitId::new(2), HitRegion::Button, 20)));
}
#[test]
fn frame_intern() {
let mut pool = GraphemePool::new();
let mut frame = Frame::new(10, 10, &mut pool);
let id = frame.intern("👋");
assert_eq!(frame.pool.get(id), Some("👋"));
}
#[test]
fn frame_intern_with_width() {
let mut pool = GraphemePool::new();
let mut frame = Frame::new(10, 10, &mut pool);
let id = frame.intern_with_width("🧪", 2);
assert_eq!(id.width(), 2);
assert_eq!(frame.pool.get(id), Some("🧪"));
}
#[test]
fn frame_print_text_emoji_presentation_sets_continuation() {
let mut pool = GraphemePool::new();
let mut frame = Frame::new(5, 1, &mut pool);
frame.print_text(0, 0, "👍🏽", Cell::from_char(' '));
let head = frame.buffer.get(0, 0).unwrap();
let tail = frame.buffer.get(1, 0).unwrap();
assert_eq!(head.content.width(), 2);
assert!(tail.content.is_continuation());
}
#[test]
fn frame_enable_hit_testing() {
let mut pool = GraphemePool::new();
let mut frame = Frame::new(10, 10, &mut pool);
assert!(frame.hit_grid.is_none());
frame.enable_hit_testing();
assert!(frame.hit_grid.is_some());
frame.enable_hit_testing();
assert!(frame.hit_grid.is_some());
}
#[test]
fn frame_enable_hit_testing_then_register() {
let mut pool = GraphemePool::new();
let mut frame = Frame::new(10, 10, &mut pool);
frame.enable_hit_testing();
let registered = frame.register_hit_region(Rect::new(0, 0, 5, 5), HitId::new(1));
assert!(registered);
assert_eq!(
frame.hit_test(2, 2),
Some((HitId::new(1), HitRegion::Content, 0))
);
}
#[test]
fn hit_cell_default_is_empty() {
let cell = HitCell::default();
assert!(cell.is_empty());
assert_eq!(cell.widget_id, None);
assert_eq!(cell.region, HitRegion::None);
assert_eq!(cell.data, 0);
}
#[test]
fn hit_cell_new_is_not_empty() {
let cell = HitCell::new(HitId::new(1), HitRegion::Button, 42);
assert!(!cell.is_empty());
assert_eq!(cell.widget_id, Some(HitId::new(1)));
assert_eq!(cell.region, HitRegion::Button);
assert_eq!(cell.data, 42);
}
#[test]
fn hit_region_variants() {
assert_eq!(HitRegion::default(), HitRegion::None);
let variants = [
HitRegion::None,
HitRegion::Content,
HitRegion::Border,
HitRegion::Scrollbar,
HitRegion::Handle,
HitRegion::Button,
HitRegion::Link,
HitRegion::Custom(0),
HitRegion::Custom(1),
HitRegion::Custom(255),
];
for i in 0..variants.len() {
for j in (i + 1)..variants.len() {
assert_ne!(
variants[i], variants[j],
"variants {i} and {j} should differ"
);
}
}
}
#[test]
fn hit_id_default() {
let id = HitId::default();
assert_eq!(id.id(), 0);
}
#[test]
fn hit_grid_initial_cells_empty() {
let grid = HitGrid::new(5, 5);
for y in 0..5 {
for x in 0..5 {
let cell = grid.get(x, y).unwrap();
assert!(cell.is_empty());
}
}
}
#[test]
fn hit_grid_zero_dimensions() {
let grid = HitGrid::new(0, 0);
assert_eq!(grid.width(), 0);
assert_eq!(grid.height(), 0);
assert!(grid.get(0, 0).is_none());
assert!(grid.hit_test(0, 0).is_none());
}
#[test]
fn hit_grid_hits_in_empty_area() {
let grid = HitGrid::new(10, 10);
let hits = grid.hits_in(Rect::new(0, 0, 5, 5));
assert!(hits.is_empty());
}
#[test]
fn hit_grid_hits_in_clipped_area() {
let mut grid = HitGrid::new(5, 5);
grid.register(Rect::new(0, 0, 5, 5), HitId::new(1), HitRegion::Content, 0);
let hits = grid.hits_in(Rect::new(3, 3, 10, 10));
assert_eq!(hits.len(), 4); }
#[test]
fn hit_test_no_grid_returns_none() {
let mut pool = GraphemePool::new();
let frame = Frame::new(10, 10, &mut pool);
assert!(frame.hit_test(0, 0).is_none());
}
#[test]
fn frame_cursor_operations() {
let mut pool = GraphemePool::new();
let mut frame = Frame::new(80, 24, &mut pool);
frame.set_cursor(Some((79, 23)));
assert_eq!(frame.cursor_position, Some((79, 23)));
frame.set_cursor(Some((0, 0)));
assert_eq!(frame.cursor_position, Some((0, 0)));
frame.set_cursor_visible(false);
assert!(!frame.cursor_visible);
frame.set_cursor_visible(true);
assert!(frame.cursor_visible);
}
#[test]
fn hit_data_large_values() {
let mut grid = HitGrid::new(5, 5);
grid.register(
Rect::new(0, 0, 1, 1),
HitId::new(1),
HitRegion::Content,
u64::MAX,
);
let result = grid.hit_test(0, 0);
assert_eq!(result, Some((HitId::new(1), HitRegion::Content, u64::MAX)));
}
#[test]
fn hit_id_large_value() {
let id = HitId::new(u32::MAX);
assert_eq!(id.id(), u32::MAX);
}
#[test]
fn frame_print_text_interns_complex_graphemes() {
let mut pool = GraphemePool::new();
let mut frame = Frame::new(10, 1, &mut pool);
let flag = "🇺🇸";
assert!(flag.chars().count() > 1);
frame.print_text(0, 0, flag, Cell::default());
let cell = frame.buffer.get(0, 0).unwrap();
assert!(cell.content.is_grapheme());
let id = cell.content.grapheme_id().unwrap();
assert_eq!(frame.pool.get(id), Some(flag));
}
#[test]
fn hit_id_debug_clone_copy_hash() {
let id = HitId::new(99);
let dbg = format!("{:?}", id);
assert!(dbg.contains("99"), "Debug: {dbg}");
let copied: HitId = id; assert_eq!(id, copied);
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(id);
set.insert(HitId::new(99));
assert_eq!(set.len(), 1);
set.insert(HitId::new(100));
assert_eq!(set.len(), 2);
}
#[test]
fn hit_id_eq_and_ne() {
assert_eq!(HitId::new(0), HitId::new(0));
assert_ne!(HitId::new(0), HitId::new(1));
assert_ne!(HitId::new(u32::MAX), HitId::default());
}
#[test]
fn hit_region_debug_clone_copy_hash() {
let r = HitRegion::Custom(42);
let dbg = format!("{:?}", r);
assert!(dbg.contains("Custom"), "Debug: {dbg}");
let copied: HitRegion = r; assert_eq!(r, copied);
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(r);
set.insert(HitRegion::Custom(42));
assert_eq!(set.len(), 1);
}
#[test]
fn hit_cell_debug_clone_copy_eq() {
let cell = HitCell::new(HitId::new(5), HitRegion::Link, 123);
let dbg = format!("{:?}", cell);
assert!(dbg.contains("Link"), "Debug: {dbg}");
let copied: HitCell = cell; assert_eq!(cell, copied);
assert_ne!(cell, HitCell::default());
}
#[test]
fn hit_grid_clone() {
let mut grid = HitGrid::new(5, 5);
grid.register(Rect::new(0, 0, 2, 2), HitId::new(1), HitRegion::Content, 7);
let clone = grid.clone();
assert_eq!(clone.width(), 5);
assert_eq!(
clone.hit_test(0, 0),
Some((HitId::new(1), HitRegion::Content, 7))
);
}
#[test]
fn hit_grid_get_mut() {
let mut grid = HitGrid::new(5, 5);
if let Some(cell) = grid.get_mut(2, 3) {
*cell = HitCell::new(HitId::new(77), HitRegion::Handle, 55);
}
assert_eq!(
grid.hit_test(2, 3),
Some((HitId::new(77), HitRegion::Handle, 55))
);
assert!(grid.get_mut(5, 5).is_none());
}
#[test]
fn hit_grid_zero_width_nonzero_height() {
let grid = HitGrid::new(0, 10);
assert_eq!(grid.width(), 0);
assert_eq!(grid.height(), 10);
assert!(grid.get(0, 0).is_none());
assert!(grid.hit_test(0, 5).is_none());
}
#[test]
fn hit_grid_nonzero_width_zero_height() {
let grid = HitGrid::new(10, 0);
assert_eq!(grid.width(), 10);
assert_eq!(grid.height(), 0);
assert!(grid.get(0, 0).is_none());
}
#[test]
fn hit_grid_register_zero_width_rect() {
let mut grid = HitGrid::new(10, 10);
grid.register(Rect::new(2, 2, 0, 5), HitId::new(1), HitRegion::Content, 0);
assert!(grid.hit_test(2, 2).is_none());
}
#[test]
fn hit_grid_register_zero_height_rect() {
let mut grid = HitGrid::new(10, 10);
grid.register(Rect::new(2, 2, 5, 0), HitId::new(1), HitRegion::Content, 0);
assert!(grid.hit_test(2, 2).is_none());
}
#[test]
fn hit_grid_register_past_bounds() {
let mut grid = HitGrid::new(10, 10);
grid.register(
Rect::new(10, 10, 5, 5),
HitId::new(1),
HitRegion::Content,
0,
);
assert!(grid.hit_test(9, 9).is_none());
}
#[test]
fn hit_grid_full_coverage() {
let mut grid = HitGrid::new(3, 3);
grid.register(Rect::new(0, 0, 3, 3), HitId::new(1), HitRegion::Content, 0);
for y in 0..3 {
for x in 0..3 {
assert_eq!(
grid.hit_test(x, y),
Some((HitId::new(1), HitRegion::Content, 0))
);
}
}
}
#[test]
fn hit_grid_single_cell() {
let mut grid = HitGrid::new(1, 1);
grid.register(Rect::new(0, 0, 1, 1), HitId::new(1), HitRegion::Button, 42);
assert_eq!(
grid.hit_test(0, 0),
Some((HitId::new(1), HitRegion::Button, 42))
);
assert!(grid.hit_test(1, 0).is_none());
assert!(grid.hit_test(0, 1).is_none());
}
#[test]
fn hit_grid_hits_in_outside_rect() {
let mut grid = HitGrid::new(5, 5);
grid.register(Rect::new(0, 0, 2, 2), HitId::new(1), HitRegion::Content, 0);
let hits = grid.hits_in(Rect::new(3, 3, 2, 2));
assert!(hits.is_empty());
}
#[test]
fn hit_grid_hits_in_zero_rect() {
let mut grid = HitGrid::new(5, 5);
grid.register(Rect::new(0, 0, 5, 5), HitId::new(1), HitRegion::Content, 0);
let hits = grid.hits_in(Rect::new(2, 2, 0, 0));
assert!(hits.is_empty());
}
#[test]
fn cost_estimate_source_traits() {
let a = CostEstimateSource::Measured;
let b = CostEstimateSource::AreaFallback;
let c = CostEstimateSource::FixedDefault;
let dbg = format!("{:?}", a);
assert!(dbg.contains("Measured"), "Debug: {dbg}");
assert_eq!(
CostEstimateSource::default(),
CostEstimateSource::FixedDefault
);
let copied: CostEstimateSource = a;
assert_eq!(a, copied);
assert_ne!(a, b);
assert_ne!(b, c);
assert_ne!(a, c);
}
#[test]
fn widget_signal_default() {
let sig = WidgetSignal::default();
assert_eq!(sig.widget_id, 0);
assert!(!sig.essential);
assert!((sig.priority - 0.5).abs() < f32::EPSILON);
assert_eq!(sig.staleness_ms, 0);
assert!((sig.focus_boost - 0.0).abs() < f32::EPSILON);
assert!((sig.interaction_boost - 0.0).abs() < f32::EPSILON);
assert_eq!(sig.area_cells, 1);
assert!((sig.cost_estimate_us - 5.0).abs() < f32::EPSILON);
assert!((sig.recent_cost_us - 5.0).abs() < f32::EPSILON);
assert_eq!(sig.estimate_source, CostEstimateSource::FixedDefault);
}
#[test]
fn widget_signal_new() {
let sig = WidgetSignal::new(42);
assert_eq!(sig.widget_id, 42);
assert!(!sig.essential);
assert!((sig.priority - 0.5).abs() < f32::EPSILON);
}
#[test]
fn widget_signal_debug_clone() {
let sig = WidgetSignal::new(7);
let dbg = format!("{:?}", sig);
assert!(dbg.contains("widget_id"), "Debug: {dbg}");
let cloned = sig.clone();
assert_eq!(cloned.widget_id, 7);
}
#[test]
fn widget_budget_default_is_allow_all() {
let budget = WidgetBudget::default();
assert!(budget.allows(0, false));
assert!(budget.allows(u64::MAX, false));
assert!(budget.allows(42, true));
}
#[test]
fn widget_budget_allow_only() {
let budget = WidgetBudget::allow_only(vec![10, 20, 30]);
assert!(budget.allows(10, false));
assert!(budget.allows(20, false));
assert!(budget.allows(30, false));
assert!(!budget.allows(15, false));
assert!(!budget.allows(0, false));
}
#[test]
fn widget_budget_essential_always_allowed() {
let budget = WidgetBudget::allow_only(vec![10]);
assert!(budget.allows(999, true));
assert!(budget.allows(0, true));
}
#[test]
fn widget_budget_allow_only_dedup() {
let budget = WidgetBudget::allow_only(vec![5, 5, 5, 10, 10]);
assert!(budget.allows(5, false));
assert!(budget.allows(10, false));
assert!(!budget.allows(7, false));
}
#[test]
fn widget_budget_allow_only_empty() {
let budget = WidgetBudget::allow_only(vec![]);
assert!(!budget.allows(0, false));
assert!(!budget.allows(1, false));
assert!(budget.allows(1, true)); }
#[test]
fn widget_budget_debug_clone() {
let budget = WidgetBudget::allow_only(vec![1, 2, 3]);
let dbg = format!("{:?}", budget);
assert!(dbg.contains("allow_list"), "Debug: {dbg}");
let cloned = budget.clone();
assert!(cloned.allows(2, false));
}
#[test]
fn frame_zero_dimensions_clamped_to_one() {
let mut pool = GraphemePool::new();
let frame = Frame::new(0, 0, &mut pool);
assert_eq!(frame.buffer.width(), 1);
assert_eq!(frame.buffer.height(), 1);
}
#[test]
fn frame_from_buffer() {
let mut pool = GraphemePool::new();
let mut buf = Buffer::new(20, 10);
buf.set_raw(5, 5, Cell::from_char('Z'));
let frame = Frame::from_buffer(buf, &mut pool);
assert_eq!(frame.width(), 20);
assert_eq!(frame.height(), 10);
assert_eq!(frame.buffer.get(5, 5).unwrap().content.as_char(), Some('Z'));
assert!(frame.hit_grid.is_none());
assert!(frame.cursor_visible);
}
#[test]
fn frame_with_links() {
let mut pool = GraphemePool::new();
let mut links = LinkRegistry::new();
let frame = Frame::with_links(10, 5, &mut pool, &mut links);
assert!(frame.links.is_some());
assert_eq!(frame.width(), 10);
assert_eq!(frame.height(), 5);
}
#[test]
fn frame_set_links() {
let mut pool = GraphemePool::new();
let mut links = LinkRegistry::new();
let mut frame = Frame::new(10, 5, &mut pool);
assert!(frame.links.is_none());
frame.set_links(&mut links);
assert!(frame.links.is_some());
}
#[test]
fn frame_register_link_no_registry() {
let mut pool = GraphemePool::new();
let mut frame = Frame::new(10, 5, &mut pool);
let id = frame.register_link("https://example.com");
assert_eq!(id, 0);
}
#[test]
fn frame_register_link_with_registry() {
let mut pool = GraphemePool::new();
let mut links = LinkRegistry::new();
let mut frame = Frame::with_links(10, 5, &mut pool, &mut links);
let id = frame.register_link("https://example.com");
assert!(id > 0);
let id2 = frame.register_link("https://example.com");
assert_eq!(id, id2);
let id3 = frame.register_link("https://other.com");
assert_ne!(id, id3);
}
#[test]
fn frame_set_widget_budget() {
let mut pool = GraphemePool::new();
let mut frame = Frame::new(10, 10, &mut pool);
assert!(frame.should_render_widget(42, false));
frame.set_widget_budget(WidgetBudget::allow_only(vec![1, 2]));
assert!(frame.should_render_widget(1, false));
assert!(!frame.should_render_widget(42, false));
assert!(frame.should_render_widget(42, true)); }
#[test]
fn frame_widget_signals_lifecycle() {
let mut pool = GraphemePool::new();
let mut frame = Frame::new(10, 10, &mut pool);
assert!(frame.widget_signals().is_empty());
frame.register_widget_signal(WidgetSignal::new(1));
frame.register_widget_signal(WidgetSignal::new(2));
assert_eq!(frame.widget_signals().len(), 2);
assert_eq!(frame.widget_signals()[0].widget_id, 1);
assert_eq!(frame.widget_signals()[1].widget_id, 2);
let taken = frame.take_widget_signals();
assert_eq!(taken.len(), 2);
assert!(frame.widget_signals().is_empty());
}
#[test]
fn frame_clear_resets_signals_and_cursor() {
let mut pool = GraphemePool::new();
let mut frame = Frame::new(10, 10, &mut pool);
frame.set_cursor(Some((5, 5)));
frame.register_widget_signal(WidgetSignal::new(1));
assert!(frame.cursor_position.is_some());
assert!(!frame.widget_signals().is_empty());
frame.clear();
assert!(frame.cursor_position.is_none());
assert!(frame.widget_signals().is_empty());
}
#[test]
fn frame_set_degradation_propagates_to_buffer() {
let mut pool = GraphemePool::new();
let mut frame = Frame::new(10, 10, &mut pool);
assert_eq!(frame.degradation, DegradationLevel::Full);
assert_eq!(frame.buffer.degradation, DegradationLevel::Full);
frame.set_degradation(DegradationLevel::SimpleBorders);
assert_eq!(frame.degradation, DegradationLevel::SimpleBorders);
assert_eq!(frame.buffer.degradation, DegradationLevel::SimpleBorders);
frame.set_degradation(DegradationLevel::EssentialOnly);
assert_eq!(frame.degradation, DegradationLevel::EssentialOnly);
assert_eq!(frame.buffer.degradation, DegradationLevel::EssentialOnly);
}
#[test]
fn frame_with_hit_grid_zero_size_clamped_to_one() {
let mut pool = GraphemePool::new();
let frame = Frame::with_hit_grid(0, 0, &mut pool);
assert_eq!(frame.buffer.width(), 1);
assert_eq!(frame.buffer.height(), 1);
}
#[test]
fn frame_register_hit_with_all_regions() {
let mut pool = GraphemePool::new();
let mut frame = Frame::with_hit_grid(20, 20, &mut pool);
let regions = [
HitRegion::Content,
HitRegion::Border,
HitRegion::Scrollbar,
HitRegion::Handle,
HitRegion::Button,
HitRegion::Link,
HitRegion::Custom(0),
HitRegion::Custom(255),
];
for (i, ®ion) in regions.iter().enumerate() {
let y = i as u16;
frame.register_hit(Rect::new(0, y, 1, 1), HitId::new(i as u32), region, 0);
}
for (i, ®ion) in regions.iter().enumerate() {
let y = i as u16;
assert_eq!(
frame.hit_test(0, y),
Some((HitId::new(i as u32), region, 0))
);
}
}
#[test]
fn frame_draw_horizontal_line() {
let mut pool = GraphemePool::new();
let mut frame = Frame::new(10, 5, &mut pool);
let cell = Cell::from_char('-');
frame.draw_horizontal_line(2, 1, 5, cell);
for x in 2..7 {
assert_eq!(frame.buffer.get(x, 1).unwrap().content.as_char(), Some('-'));
}
assert!(frame.buffer.get(1, 1).unwrap().is_empty());
assert!(frame.buffer.get(7, 1).unwrap().is_empty());
}
#[test]
fn frame_draw_vertical_line() {
let mut pool = GraphemePool::new();
let mut frame = Frame::new(10, 10, &mut pool);
let cell = Cell::from_char('|');
frame.draw_vertical_line(3, 2, 4, cell);
for y in 2..6 {
assert_eq!(frame.buffer.get(3, y).unwrap().content.as_char(), Some('|'));
}
assert!(frame.buffer.get(3, 1).unwrap().is_empty());
assert!(frame.buffer.get(3, 6).unwrap().is_empty());
}
#[test]
fn frame_draw_rect_filled() {
let mut pool = GraphemePool::new();
let mut frame = Frame::new(10, 10, &mut pool);
let cell = Cell::from_char('#');
frame.draw_rect_filled(Rect::new(1, 1, 3, 3), cell);
for y in 1..4 {
for x in 1..4 {
assert_eq!(frame.buffer.get(x, y).unwrap().content.as_char(), Some('#'));
}
}
assert!(frame.buffer.get(0, 0).unwrap().is_empty());
assert!(frame.buffer.get(4, 4).unwrap().is_empty());
}
#[test]
fn frame_paint_area() {
use crate::cell::PackedRgba;
let mut pool = GraphemePool::new();
let mut frame = Frame::new(5, 5, &mut pool);
let red = PackedRgba::rgb(255, 0, 0);
frame.paint_area(Rect::new(0, 0, 2, 2), Some(red), None);
let cell = frame.buffer.get(0, 0).unwrap();
assert_eq!(cell.fg, red);
}
#[test]
fn frame_print_text_clipped_at_boundary() {
let mut pool = GraphemePool::new();
let mut frame = Frame::new(5, 1, &mut pool);
let end = frame.print_text(0, 0, "Hello World", Cell::from_char(' '));
assert_eq!(end, 5);
for x in 0..5 {
assert!(!frame.buffer.get(x, 0).unwrap().is_empty());
}
}
#[test]
fn frame_print_text_empty_string() {
let mut pool = GraphemePool::new();
let mut frame = Frame::new(10, 1, &mut pool);
let end = frame.print_text(0, 0, "", Cell::from_char(' '));
assert_eq!(end, 0);
}
#[test]
fn frame_print_text_at_right_edge() {
let mut pool = GraphemePool::new();
let mut frame = Frame::new(5, 1, &mut pool);
let end = frame.print_text(4, 0, "AB", Cell::from_char(' '));
assert_eq!(end, 5);
assert_eq!(frame.buffer.get(4, 0).unwrap().content.as_char(), Some('A'));
}
#[test]
fn frame_debug() {
let mut pool = GraphemePool::new();
let frame = Frame::new(5, 3, &mut pool);
let dbg = format!("{:?}", frame);
assert!(dbg.contains("Frame"), "Debug: {dbg}");
}
#[test]
fn hit_grid_debug() {
let grid = HitGrid::new(3, 3);
let dbg = format!("{:?}", grid);
assert!(dbg.contains("HitGrid"), "Debug: {dbg}");
}
#[test]
fn frame_cursor_beyond_bounds() {
let mut pool = GraphemePool::new();
let mut frame = Frame::new(10, 10, &mut pool);
frame.set_cursor(Some((100, 200)));
assert_eq!(frame.cursor_position, Some((100, 200)));
}
#[test]
fn hit_grid_register_overwrite() {
let mut grid = HitGrid::new(5, 5);
grid.register(Rect::new(0, 0, 3, 3), HitId::new(1), HitRegion::Content, 10);
grid.register(Rect::new(0, 0, 3, 3), HitId::new(2), HitRegion::Button, 20);
assert_eq!(
grid.hit_test(1, 1),
Some((HitId::new(2), HitRegion::Button, 20))
);
}
#[test]
fn frame_hit_test_detailed_preserves_owner() {
let mut pool = GraphemePool::new();
let mut frame = Frame::with_hit_grid(4, 4, &mut pool);
frame.with_hit_owner(77, |frame| {
frame.register_hit(Rect::new(1, 1, 2, 2), HitId::new(5), HitRegion::Button, 9);
});
assert_eq!(
frame.hit_test_detailed(1, 1),
Some(HitTestResult::new(
HitId::new(5),
HitRegion::Button,
9,
Some(77),
))
);
assert_eq!(
frame.hit_test(1, 1),
Some((HitId::new(5), HitRegion::Button, 9))
);
}
#[test]
fn frame_hit_owner_scope_restores_previous_owner() {
let mut pool = GraphemePool::new();
let mut frame = Frame::with_hit_grid(4, 4, &mut pool);
frame.with_hit_owner(10, |frame| {
frame.register_hit(Rect::new(0, 0, 1, 1), HitId::new(1), HitRegion::Content, 1);
frame.with_hit_owner(20, |frame| {
frame.register_hit(Rect::new(1, 0, 1, 1), HitId::new(2), HitRegion::Content, 2);
});
frame.register_hit(Rect::new(2, 0, 1, 1), HitId::new(3), HitRegion::Content, 3);
});
assert_eq!(frame.hit_test_detailed(0, 0).unwrap().owner, Some(10));
assert_eq!(frame.hit_test_detailed(1, 0).unwrap().owner, Some(20));
assert_eq!(frame.hit_test_detailed(2, 0).unwrap().owner, Some(10));
}
#[test]
fn frame_hit_owner_scope_restores_after_panic() {
let mut pool = GraphemePool::new();
let mut frame = Frame::with_hit_grid(4, 4, &mut pool);
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
frame.with_hit_owner(55, |_frame| panic!("boom"));
}));
assert!(result.is_err());
frame.register_hit(Rect::new(0, 0, 1, 1), HitId::new(9), HitRegion::Content, 0);
assert_eq!(frame.hit_test_detailed(0, 0).unwrap().owner, None);
}
}