diff --git a/src/render/clip.rs b/src/render/clip.rs index 9809e31..78b8bc9 100644 --- a/src/render/clip.rs +++ b/src/render/clip.rs @@ -42,9 +42,13 @@ impl SoftwareCanvas mask.fill_path( &path, FillRule::Winding, false, Transform::identity() ); self.clip_mask = Some( mask ); self.clip_bounds = rects.to_vec(); - } else { + } else if rects.is_empty() { self.clip_mask = None; self.clip_bounds = Vec::new(); + } else { + // Only zero-area rects: clip everything, like the GLES zero scissor. + self.clip_mask = Some( mask ); + self.clip_bounds = Vec::new(); } } diff --git a/src/tree.rs b/src/tree.rs index 1ae896e..f4f1695 100755 --- a/src/tree.rs +++ b/src/tree.rs @@ -16,7 +16,9 @@ pub fn find_widget_at( { for w in widget_rects.iter().rev() { - if w.rect.contains( pos ) { return Some( w.flat_idx ); } + // `paint_rect` is clamped to the viewport by Scroll / Viewport, so a + // row scrolled half out of view is only hit on its visible part. + if w.rect.contains( pos ) && w.paint_rect.contains( pos ) { return Some( w.flat_idx ); } } None } @@ -88,3 +90,51 @@ pub fn next_focusable_index( }; Some( next ) } + +#[ cfg( test ) ] +mod tests +{ + use super::*; + use crate::types::Rect; + + 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 find_widget_at_ignores_the_part_of_a_row_scrolled_out_of_view() + { + // A row whose top 60 px sit above a viewport starting at y = 100. + let row = lw( 3, r( 0.0, 40.0, 200.0, 80.0 ), r( 0.0, 100.0, 200.0, 20.0 ) ); + let widgets = [ row ]; + assert_eq!( find_widget_at( &widgets, Point { x: 50.0, y: 70.0 } ), None ); + assert_eq!( find_widget_at( &widgets, Point { x: 50.0, y: 110.0 } ), Some( 3 ) ); + } + + #[ test ] + fn find_widget_at_keeps_the_full_rect_when_paint_rect_encloses_it() + { + let button = lw( 1, r( 10.0, 10.0, 100.0, 40.0 ), r( 8.0, 8.0, 104.0, 44.0 ) ); + let widgets = [ button ]; + assert_eq!( find_widget_at( &widgets, Point { x: 10.0, y: 10.0 } ), Some( 1 ) ); + assert_eq!( find_widget_at( &widgets, Point { x: 9.0, y: 9.0 } ), None ); + } +} diff --git a/src/widget/text_edit/draw.rs b/src/widget/text_edit/draw.rs index 1230147..655fc48 100644 --- a/src/widget/text_edit/draw.rs +++ b/src/widget/text_edit/draw.rs @@ -13,6 +13,25 @@ use super::wrapping::compute_visual_lines; /// do not overlap (zero-area / negative-extent results count as /// "no overlap" so the caller can `filter_map` straight into a clip /// list). +/// Clip for the field interior. An empty intersection with the parent clip +/// must still clip everything: an empty rect list would lift the clip. +pub( super ) fn interior_clip( outer: &[ Rect ], inner: Rect ) -> Vec +{ + if outer.is_empty() + { + return vec![ inner ]; + } + let clip: Vec = outer.iter() + .filter_map( |o| rect_intersect( *o, inner ) ) + .collect(); + if clip.is_empty() + { + vec![ Rect { x: 0.0, y: 0.0, width: 0.0, height: 0.0 } ] + } else { + clip + } +} + fn rect_intersect( a: Rect, b: Rect ) -> Option { let x0 = a.x.max( b.x ); @@ -118,15 +137,7 @@ impl TextEdit width: ( rect.width - theme::PAD_H - toggle_reserve ).max( 0.0 ), height: rect.height, }; - let composed_clip: Vec = if outer_clip.is_empty() - { - vec![ inner_rect ] - } else { - outer_clip.iter() - .filter_map( |o| rect_intersect( *o, inner_rect ) ) - .collect() - }; - canvas.set_clip_rects( &composed_clip ); + canvas.set_clip_rects( &interior_clip( &outer_clip, inner_rect ) ); // Selection highlight (single-line). Painted before the text // so the glyphs sit on top of the tinted band. @@ -300,15 +311,7 @@ impl TextEdit width: ( rect.width - theme::PAD_H ).max( 0.0 ), height: ( rect.height - theme::PAD_V_MULTI ).max( 0.0 ), }; - let composed_clip: Vec = if outer_clip.is_empty() - { - vec![ inner_rect ] - } else { - outer_clip.iter() - .filter_map( |o| rect_intersect( *o, inner_rect ) ) - .collect() - }; - canvas.set_clip_rects( &composed_clip ); + canvas.set_clip_rects( &interior_clip( &outer_clip, inner_rect ) ); // Selection highlight (multiline). Painted before the text so // the glyphs sit on top of the tinted band(s). Iterates diff --git a/src/widget/text_edit/tests.rs b/src/widget/text_edit/tests.rs index a2069be..a56084a 100644 --- a/src/widget/text_edit/tests.rs +++ b/src/widget/text_edit/tests.rs @@ -735,3 +735,28 @@ fn password_toggle_drop_zeros_underlying_string_buffer() // path runs unconditionally for password_toggle fields. let _ = ( ptr, len ); } + +// ── Interior clip ───────────────────────────────────────────────────────── + +#[ test ] +fn interior_clip_outside_the_parent_clip_clips_everything() +{ + use super::draw::interior_clip; + use crate::types::Rect; + + let inner = Rect { x: 10.0, y: 10.0, width: 100.0, height: 20.0 }; + assert_eq!( interior_clip( &[], inner ), vec![ inner ] ); + + let overlapping = Rect { x: 0.0, y: 0.0, width: 50.0, height: 50.0 }; + assert_eq!( + interior_clip( &[ overlapping ], inner ), + vec![ Rect { x: 10.0, y: 10.0, width: 40.0, height: 20.0 } ], + ); + + // A partial repaint of a row below the field: the intersection is empty + // and the clip must not be lifted, or the text paints over itself. + let below = Rect { x: 0.0, y: 200.0, width: 300.0, height: 40.0 }; + let clip = interior_clip( &[ below ], inner ); + assert_eq!( clip.len(), 1 ); + assert_eq!( clip[ 0 ].width * clip[ 0 ].height, 0.0 ); +}