diff --git a/src/draw/layout.rs b/src/draw/layout.rs index 2f96a0d..bf8eb26 100644 --- a/src/draw/layout.rs +++ b/src/draw/layout.rs @@ -53,7 +53,24 @@ pub( crate ) fn layout_and_draw( let mut idx = flat_idx; for ( child_rect, child_i ) in child_rects { - idx = layout_and_draw::( &s.children[ child_i ].0, canvas, child_rect, ctx, idx ); + let child = &s.children[ child_i ]; + // A placed child may carry a clip rect (Android clipChildren): clip the + // canvas to it for the child's subtree, then restore the prior clip. + // The clip is in the Stack's coordinate space (like the placed rect), + // so it is shifted by the Stack's origin to match — otherwise a Stack + // laid out at a non-zero origin clips in the wrong place. + if let Some( clip ) = child.7 + { + let saved = canvas.clip_bounds(); + let clip = Rect { x: rect.x + clip.x, y: rect.y + clip.y, width: clip.width, height: clip.height }; + canvas.set_clip_rects( &[ clip ] ); + idx = layout_and_draw::( &child.0, canvas, child_rect, ctx, idx ); + canvas.set_clip_rects( &saved ); + } + else + { + idx = layout_and_draw::( &child.0, canvas, child_rect, ctx, idx ); + } } idx } diff --git a/src/layout/stack.rs b/src/layout/stack.rs index b57fc12..dde407a 100644 --- a/src/layout/stack.rs +++ b/src/layout/stack.rs @@ -52,10 +52,11 @@ pub enum VAlign pub struct Stack { /// Children with their alignment, margin, and extra `(x, y)` translation - /// applied after alignment. Drawn in order — last child is on top. The final + /// applied after alignment. Drawn in order — last child is on top. The 7th /// `Option` overrides alignment/sizing with an exact rect (see - /// [`push_placed`](Self::push_placed)). - pub children: Vec<( Element, HAlign, VAlign, f32, f32, f32, Option )>, + /// [`push_placed`](Self::push_placed)); the 8th clips the child's draw to a + /// rect (Android clipChildren — see [`push_placed_clipped`](Self::push_placed_clipped)). + pub children: Vec<( Element, HAlign, VAlign, f32, f32, f32, Option, Option )>, /// When `true`, [`preferred_size`](Self::preferred_size) reports the max of /// children's intrinsic widths and heights instead of `(max_width, tallest)`. pub fit_content: bool, @@ -102,7 +103,7 @@ impl Stack margin: f32, ) -> Self { - self.children.push( ( e.into(), h_align, v_align, margin, 0.0, 0.0, None ) ); + self.children.push( ( e.into(), h_align, v_align, margin, 0.0, 0.0, None, None ) ); self } @@ -119,7 +120,7 @@ impl Stack offset_y: f32, ) -> Self { - self.children.push( ( e.into(), h_align, v_align, 0.0, offset_x, offset_y, None ) ); + self.children.push( ( e.into(), h_align, v_align, 0.0, offset_x, offset_y, None, None ) ); self } @@ -133,7 +134,22 @@ impl Stack rect: Rect, ) -> Self { - self.children.push( ( e.into(), HAlign::Start, VAlign::Top, 0.0, 0.0, 0.0, Some( rect ) ) ); + self.children.push( ( e.into(), HAlign::Start, VAlign::Top, 0.0, 0.0, 0.0, Some( rect ), None ) ); + self + } + + /// Like [`push_placed`](Self::push_placed) but clips the child's drawing to + /// `clip` (in the Stack's coordinate space). Mirrors Android's clipChildren: + /// content that overflows the clip — e.g. a scrolled list row reaching above + /// the list, or an inner card past a rounded bubble — is not painted. + pub fn push_placed_clipped( + mut self, + e: impl Into>, + rect: Rect, + clip: Rect, + ) -> Self + { + self.children.push( ( e.into(), HAlign::Start, VAlign::Top, 0.0, 0.0, 0.0, Some( rect ), Some( clip ) ) ); self } @@ -142,7 +158,7 @@ impl Stack if self.fit_content { let content_w = self.children.iter() - .map( |( c, _, _, _, _, _, _ )| match c + .map( |( c, .. )| match c { Element::Spacer( s ) => s.resolved_width( canvas ).unwrap_or( 0.0 ), Element::Separator( _ ) => 0.0, @@ -157,13 +173,13 @@ impl Stack } ) .fold( 0.0_f32, f32::max ); let max_h = self.children.iter() - .map( |( c, _, _, _, _, _, _ )| c.preferred_size( max_width, canvas ).1 ) + .map( |( c, .. )| c.preferred_size( max_width, canvas ).1 ) .fold( 0.0_f32, f32::max ); return ( content_w.min( max_width ), max_h ); } let max_h = self.children.iter() - .map( |( c, _, _, _, _, _, _ )| c.preferred_size( max_width, canvas ).1 ) + .map( |( c, .. )| c.preferred_size( max_width, canvas ).1 ) .fold( 0.0_f32, f32::max ); ( max_width, max_h ) } @@ -171,7 +187,7 @@ impl Stack /// Return `(rect, child_index)` pairs, computing each child's rect from its alignment. pub fn layout( &self, rect: Rect, canvas: &Canvas ) -> Vec<(Rect, usize)> { - self.children.iter().enumerate().map( |( i, ( child, h_align, v_align, margin, ox, oy, placed ) )| + self.children.iter().enumerate().map( |( i, ( child, h_align, v_align, margin, ox, oy, placed, .. ) )| { if let Some( p ) = placed { @@ -213,8 +229,8 @@ impl Stack Stack { children: self.children.into_iter() - .map( |( child, ha, va, margin, ox, oy, placed )| - ( child.map_arc( f ), ha, va, margin, ox, oy, placed ) ) + .map( |( child, ha, va, margin, ox, oy, placed, clip )| + ( child.map_arc( f ), ha, va, margin, ox, oy, placed, clip ) ) .collect(), fit_content: self.fit_content, } diff --git a/src/lib.rs b/src/lib.rs index 611e475..15004e0 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -246,6 +246,7 @@ pub use chassis::{ set_default_theme, theme_logo_rgba, theme_icon_tinted, brandi pub use types::{ Color, Corners, CursorShape, Length, LengthBase, PathCmd, Point, Rect, Size, WidgetId }; pub use types::{ design_reference, set_design_reference }; pub use widget::{ Element, button, icon_button, text_edit, image as img_widget, text, container }; +pub use text_shaping::measure_text; pub use widget::rich_text::{ rich_text, RichText, LinkSpan }; pub use widget::button::ButtonVariant; pub use widget::slider::{ Slider, slider, SliderAxis }; diff --git a/src/render/helpers.rs b/src/render/helpers.rs index 557c722..587b70f 100644 --- a/src/render/helpers.rs +++ b/src/render/helpers.rs @@ -118,7 +118,7 @@ pub ( crate ) fn find_font_opt() -> Option /// OFL 1.1) when nothing matches or the file cannot be read. Always /// returns usable bytes so canvas construction never panics on a /// system without the expected fonts. -pub ( super ) fn load_default_font_bytes() -> Vec +pub ( crate ) fn load_default_font_bytes() -> Vec { if let Some( path ) = find_font_opt() { diff --git a/src/render/mod.rs b/src/render/mod.rs index 5ebb43d..6f026c1 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -262,6 +262,45 @@ impl Canvas } } + /// Whether this is the CPU (software) backend. Callers that can honour a real + /// path clip on the software backend but only a bounding rect on GLES branch + /// on this. + pub fn is_software( &self ) -> bool + { + matches!( self, Canvas::Software( _ ) ) + } + + /// Read this canvas into tightly packed straight-alpha RGBA8, top-left row + /// first (`out.len()` must be at least width*height*4). Unlike + /// [`Self::read_gles_rgba_pixels`] this also serves the software backend, by + /// un-premultiplying its pixmap — used to read back an offscreen software + /// canvas (e.g. an Android `Canvas(Bitmap)`) into a straight-alpha buffer. + pub fn read_rgba_pixels( &self, out: &mut [u8] ) -> Result<(), String> + { + match self + { + Canvas::Software( c ) => + { + let pixels = c.pixmap.pixels(); + if out.len() < pixels.len() * 4 + { + return Err( "read_rgba_pixels: output buffer too small".to_string() ); + } + for ( i, p ) in pixels.iter().enumerate() + { + let d = p.demultiply(); + let o = i * 4; + out[ o ] = d.red(); + out[ o + 1 ] = d.green(); + out[ o + 2 ] = d.blue(); + out[ o + 3 ] = d.alpha(); + } + Ok( () ) + } + Canvas::Gles( c ) => c.read_rgba_pixels( out ), + } + } + /// Composite an externally-owned GL texture into `dest`. No-op on /// the software backend (no GL state to sample from). Used by /// widgets that host content rendered by an external producer — diff --git a/src/system_fonts.rs b/src/system_fonts.rs index 51f849e..9deb6af 100644 --- a/src/system_fonts.rs +++ b/src/system_fonts.rs @@ -147,6 +147,20 @@ pub fn lookup( ch: char ) -> Option> lookup_handle( ch ).map( |h| h.font ) } +/// The process-wide primary UI font — the same default a canvas loads — cached +/// for standalone text measurement (e.g. an embedded measure pass with no live +/// canvas). Falls back to the bundled font when no system font is found. +pub fn primary_handle() -> FontHandle +{ + static PRIMARY: OnceLock = OnceLock::new(); + PRIMARY.get_or_init( || + { + let bytes = crate::render::helpers::load_default_font_bytes(); + let font = Font::from_bytes( bytes.as_slice(), FontSettings::default() ).expect( "primary font parses" ); + FontHandle { font: Arc::new( font ), bytes: Arc::new( bytes ), face: 0 } + } ).clone() +} + /// Bytes-aware variant of [`lookup`]. Returns the full /// [`FontHandle`] (fontdue handle + raw bytes + face index) so /// callers that need to invoke a HarfBuzz-style shaper can do so diff --git a/src/text_shaping.rs b/src/text_shaping.rs index a92c81e..39a94aa 100644 --- a/src/text_shaping.rs +++ b/src/text_shaping.rs @@ -145,6 +145,34 @@ thread_local! /// set of on-screen labels is far smaller than this). const SHAPE_CACHE_CAP: usize = 8192; +/// Measure a single line of `text` at `size` px with the default UI font (and +/// the system fallback chain), returning `(width, line_height)` in pixels. For +/// laying text out without a live [`crate::Canvas`] — e.g. an embedded Android +/// measure pass that needs the same metrics the renderer will use. +pub fn measure_text( text: &str, size: f32 ) -> ( f32, f32 ) +{ + let primary = crate::system_fonts::primary_handle(); + let line_h = primary.font.horizontal_line_metrics( size ).map( |m| m.new_line_size ).unwrap_or( size * 1.3 ); + if text.is_empty() + { + return ( 0.0, line_h ); + } + let resolver = primary.clone(); + let glyphs = shape_line( text, size, move | ch | + { + if resolver.font.lookup_glyph_index( ch ) != 0 + { + Some( resolver.clone() ) + } + else + { + crate::system_fonts::lookup_handle( ch ).or_else( || Some( resolver.clone() ) ) + } + } ); + let width: f32 = glyphs.iter().map( | g | g.x_advance ).sum(); + ( width, line_h ) +} + /// Cached wrapper over [`shape_line`]. `font_ctx` identifies the resolver's /// leading font so two fonts shaping the same text don't collide — pass /// `std::sync::Arc::as_ptr( &handle.font ) as usize`. `resolve_font` runs only