use crate::types::Rect;
use crate::widget::LaidOutWidget;
pub( super ) fn clamp_rect_to( r: Rect, bounds: Rect ) -> Rect
{
let x0 = r.x.max( bounds.x );
let y0 = r.y.max( bounds.y );
let x1 = ( r.x + r.width ).min( bounds.x + bounds.width );
let y1 = ( r.y + r.height ).min( bounds.y + bounds.height );
if x1 <= x0 || y1 <= y0
{
Rect { x: x0, y: y0, width: 0.0, height: 0.0 }
} else {
Rect { x: x0, y: y0, width: x1 - x0, height: y1 - y0 }
}
}
pub( crate ) fn compute_interaction_dirty_rects<Msg: Clone>(
widget_rects: &[ LaidOutWidget<Msg> ],
prev_focused: Option<usize>, prev_hovered: Option<usize>, prev_pressed: Option<usize>,
new_focused: Option<usize>, new_hovered: Option<usize>, new_pressed: Option<usize>,
pw: u32,
ph: u32,
) -> Vec<Rect>
{
let mut rects: Vec<Rect> = Vec::new();
let visit = |idx_opt: Option<usize>, sink: &mut Vec<Rect>|
{
if let Some( idx ) = idx_opt
{
if let Some( w ) = widget_rects.iter().find( |w| w.flat_idx == idx )
{
if !w.handlers.is_slider()
{
sink.push( w.paint_rect );
}
}
}
};
if prev_focused != new_focused
{
visit( prev_focused, &mut rects );
visit( new_focused, &mut rects );
}
if prev_hovered != new_hovered
{
visit( prev_hovered, &mut rects );
visit( new_hovered, &mut rects );
}
if prev_pressed != new_pressed
{
visit( prev_pressed, &mut rects );
visit( new_pressed, &mut rects );
}
let sw = pw as f32;
let sh = ph as f32;
for r in &mut rects
{
let x0 = r.x.floor().max( 0.0 );
let y0 = r.y.floor().max( 0.0 );
let x1 = ( r.x + r.width ).ceil().min( sw );
let y1 = ( r.y + r.height ).ceil().min( sh );
r.x = x0;
r.y = y0;
r.width = ( x1 - x0 ).max( 0.0 );
r.height = ( y1 - y0 ).max( 0.0 );
}
rects.retain( |r| r.width > 0.0 && r.height > 0.0 );
rects
}
pub( crate ) fn compute_damage<Msg: Clone>(
old_rects: &[ LaidOutWidget<Msg> ],
new_rects: &[ LaidOutWidget<Msg> ],
old_focused: Option<usize>,
old_hovered: Option<usize>,
old_pressed: Option<usize>,
new_focused: Option<usize>,
new_hovered: Option<usize>,
new_pressed: Option<usize>,
screen_w: u32,
screen_h: u32,
) -> Vec<Rect>
{
if old_rects.len() != new_rects.len()
{
return Vec::new();
}
let mut damage = Vec::new();
let changed_indices: Vec<usize> = [
old_focused, new_focused,
old_hovered, new_hovered,
old_pressed, new_pressed,
].iter().filter_map( |&idx| idx ).collect();
for &idx in &changed_indices
{
if let Some( w ) = old_rects.iter().find( |w| w.flat_idx == idx )
{
damage.push( w.paint_rect );
}
if let Some( w ) = new_rects.iter().find( |w| w.flat_idx == idx )
{
damage.push( w.paint_rect );
}
}
for ( i, old_w ) in old_rects.iter().enumerate()
{
if let Some( new_w ) = new_rects.get( i )
{
let old_r = old_w.rect;
let new_r = new_w.rect;
if ( old_r.x - new_r.x ).abs() > 0.5
|| ( old_r.y - new_r.y ).abs() > 0.5
|| ( old_r.width - new_r.width ).abs() > 0.5
|| ( old_r.height - new_r.height ).abs() > 0.5
{
return Vec::new();
}
}
}
if damage.is_empty()
{
return Vec::new();
}
for r in &mut damage
{
r.x -= 1.0;
r.y -= 1.0;
r.width += 2.0;
r.height += 2.0;
}
let sw = screen_w as f32;
let sh = screen_h as f32;
for r in &mut damage
{
r.x = r.x.max( 0.0 );
r.y = r.y.max( 0.0 );
r.width = r.width.min( sw - r.x );
r.height = r.height.min( sh - r.y );
}
let total_damage: f32 = damage.iter().map( |r| r.width * r.height ).sum();
if total_damage > sw * sh * 0.5
{
return Vec::new();
}
damage
}
#[ cfg( test ) ]
mod tests
{
use super::*;
use crate::widget::WidgetHandlers;
fn r( x: f32, y: f32, w: f32, h: f32 ) -> Rect
{
Rect { x, y, width: w, height: h }
}
fn lw( idx: usize, rect: Rect, paint: Rect ) -> LaidOutWidget<()>
{
LaidOutWidget
{
rect,
flat_idx: idx,
id: None,
paint_rect: paint,
handlers: WidgetHandlers::None,
keyboard_focusable: true,
cursor: crate::types::CursorShape::Default,
tooltip: None,
accessible_label: None,
is_live_region: false,
}
}
#[ test ]
fn clamp_rect_to_returns_intersection()
{
let bounds = r( 0.0, 0.0, 100.0, 100.0 );
let inside = r( 10.0, 10.0, 20.0, 20.0 );
assert_eq!( clamp_rect_to( inside, bounds ), inside );
}
#[ test ]
fn clamp_rect_to_clips_to_bounds()
{
let bounds = r( 0.0, 0.0, 100.0, 100.0 );
let bleed = r( 80.0, 80.0, 50.0, 50.0 );
assert_eq!( clamp_rect_to( bleed, bounds ), r( 80.0, 80.0, 20.0, 20.0 ) );
}
#[ test ]
fn clamp_rect_to_disjoint_returns_zero_size()
{
let bounds = r( 0.0, 0.0, 100.0, 100.0 );
let off = r( 200.0, 200.0, 50.0, 50.0 );
let out = clamp_rect_to( off, bounds );
assert_eq!( ( out.width, out.height ), ( 0.0, 0.0 ) );
}
#[ test ]
fn no_state_change_yields_empty_dirty_rects()
{
let widgets = vec![ lw( 1, r( 0.0, 0.0, 50.0, 50.0 ), r( 0.0, 0.0, 50.0, 50.0 ) ) ];
let rects = compute_interaction_dirty_rects(
&widgets,
Some( 1 ), None, None,
Some( 1 ), None, None,
800, 600,
);
assert!( rects.is_empty() );
}
#[ test ]
fn focus_change_emits_both_old_and_new_paint_rects()
{
let widgets = vec![
lw( 1, r( 0.0, 0.0, 50.0, 50.0 ), r( 0.0, 0.0, 50.0, 50.0 ) ),
lw( 2, r( 100.0, 0.0, 50.0, 50.0 ), r( 100.0, 0.0, 50.0, 50.0 ) ),
];
let rects = compute_interaction_dirty_rects(
&widgets,
Some( 1 ), None, None,
Some( 2 ), None, None,
800, 600,
);
assert_eq!( rects.len(), 2 );
}
#[ test ]
fn hover_change_emits_paint_rects_independent_of_focus()
{
let widgets = vec![
lw( 1, r( 0.0, 0.0, 50.0, 50.0 ), r( 0.0, 0.0, 50.0, 50.0 ) ),
lw( 2, r( 100.0, 0.0, 50.0, 50.0 ), r( 100.0, 0.0, 50.0, 50.0 ) ),
];
let rects = compute_interaction_dirty_rects(
&widgets,
Some( 1 ), Some( 1 ), None,
Some( 1 ), Some( 2 ), None,
800, 600,
);
assert_eq!( rects.len(), 2 );
}
#[ test ]
fn dirty_rects_are_snapped_and_clamped_to_screen()
{
let widgets = vec![
lw( 1, r( -10.0, -10.0, 30.5, 40.5 ), r( -10.0, -10.0, 30.5, 40.5 ) ),
];
let rects = compute_interaction_dirty_rects(
&widgets,
None, None, None,
Some( 1 ), None, None,
100, 100,
);
assert_eq!( rects.len(), 1 );
assert_eq!( rects[ 0 ].x, 0.0 );
assert_eq!( rects[ 0 ].y, 0.0 );
assert_eq!( rects[ 0 ].width, 21.0 );
assert_eq!( rects[ 0 ].height, 31.0 );
}
#[ test ]
fn dirty_rects_outside_screen_are_dropped()
{
let widgets = vec![
lw( 1, r( 1000.0, 1000.0, 50.0, 50.0 ), r( 1000.0, 1000.0, 50.0, 50.0 ) ),
];
let rects = compute_interaction_dirty_rects(
&widgets,
None, None, None,
Some( 1 ), None, None,
100, 100,
);
assert!( rects.is_empty() );
}
#[ test ]
fn missing_widget_idx_silently_skipped()
{
let widgets = vec![ lw( 1, r( 0.0, 0.0, 50.0, 50.0 ), r( 0.0, 0.0, 50.0, 50.0 ) ) ];
let rects = compute_interaction_dirty_rects(
&widgets,
Some( 99 ), None, None,
Some( 1 ), None, None,
800, 600,
);
assert_eq!( rects.len(), 1 );
}
#[ test ]
fn tree_size_change_returns_full_redraw()
{
let old = vec![ lw( 1, r( 0.0, 0.0, 50.0, 50.0 ), r( 0.0, 0.0, 50.0, 50.0 ) ) ];
let new = vec![
lw( 1, r( 0.0, 0.0, 50.0, 50.0 ), r( 0.0, 0.0, 50.0, 50.0 ) ),
lw( 2, r( 0.0, 60.0, 50.0, 50.0 ), r( 0.0, 60.0, 50.0, 50.0 ) ),
];
let damage = compute_damage(
&old, &new,
None, None, None,
None, None, None,
800, 600,
);
assert!( damage.is_empty(), "tree size change must signal full redraw via empty vec" );
}
#[ test ]
fn layout_shift_returns_full_redraw()
{
let old = vec![ lw( 1, r( 0.0, 0.0, 50.0, 50.0 ), r( 0.0, 0.0, 50.0, 50.0 ) ) ];
let new = vec![ lw( 1, r( 0.0, 60.0, 50.0, 50.0 ), r( 0.0, 60.0, 50.0, 50.0 ) ) ];
let damage = compute_damage(
&old, &new,
Some( 1 ), None, None,
Some( 1 ), None, None,
800, 600,
);
assert!( damage.is_empty() );
}
#[ test ]
fn focus_change_emits_partial_damage()
{
let widgets = vec![
lw( 1, r( 0.0, 0.0, 50.0, 50.0 ), r( 0.0, 0.0, 50.0, 50.0 ) ),
lw( 2, r( 100.0, 0.0, 50.0, 50.0 ), r( 100.0, 0.0, 50.0, 50.0 ) ),
];
let damage = compute_damage(
&widgets, &widgets,
Some( 1 ), None, None,
Some( 2 ), None, None,
800, 600,
);
assert!( !damage.is_empty(), "focus change must produce non-empty damage list" );
assert!(
damage.len() >= 2,
"both old and new focused widgets contribute their paint_rects"
);
}
#[ test ]
fn no_change_with_redraw_request_returns_full_redraw()
{
let widgets = vec![ lw( 1, r( 0.0, 0.0, 50.0, 50.0 ), r( 0.0, 0.0, 50.0, 50.0 ) ) ];
let damage = compute_damage(
&widgets, &widgets,
None, None, None,
None, None, None,
800, 600,
);
assert!( damage.is_empty() );
}
#[ test ]
fn damage_rects_are_dilated_by_one_pixel_each_side()
{
let widgets = vec![ lw( 1, r( 100.0, 100.0, 50.0, 50.0 ), r( 100.0, 100.0, 50.0, 50.0 ) ) ];
let damage = compute_damage(
&widgets, &widgets,
None, None, None,
Some( 1 ), None, None,
800, 600,
);
assert_eq!( damage.len(), 2 );
for d in &damage
{
assert_eq!( d.x, 99.0 );
assert_eq!( d.y, 99.0 );
assert_eq!( d.width, 52.0 );
assert_eq!( d.height, 52.0 );
}
}
#[ test ]
fn damage_above_fifty_percent_collapses_to_full_redraw()
{
let widgets = vec![
lw( 1, r( 0.0, 0.0, 80.0, 80.0 ), r( 0.0, 0.0, 80.0, 80.0 ) ),
];
let damage = compute_damage(
&widgets, &widgets,
None, None, None,
Some( 1 ), None, None,
100, 100,
);
assert!( damage.is_empty(), "damage > 50 % of surface must signal full redraw" );
}
#[ test ]
fn damage_below_fifty_percent_returns_partial()
{
let widgets = vec![
lw( 1, r( 5.0, 5.0, 10.0, 10.0 ), r( 5.0, 5.0, 10.0, 10.0 ) ),
];
let damage = compute_damage(
&widgets, &widgets,
None, None, None,
Some( 1 ), None, None,
100, 100,
);
assert!( !damage.is_empty() );
}
#[ test ]
fn damage_clamped_to_surface_extents()
{
let widgets = vec![
lw( 1, r( 90.0, 90.0, 5.0, 5.0 ), r( 90.0, 90.0, 5.0, 5.0 ) ),
];
let damage = compute_damage(
&widgets, &widgets,
None, None, None,
Some( 1 ), None, None,
100, 100,
);
for d in &damage
{
assert!( d.x + d.width <= 100.0 + f32::EPSILON );
assert!( d.y + d.height <= 100.0 + f32::EPSILON );
}
}
}