Embedder primitives: placed-child clipping, offscreen RGBA readback, standalone text measurement
Three additions an embedder needs to drive ltk as the render backend for a retained, externally-owned widget tree, each kept general rather than tied to one consumer. Stack placed-child clipping. `Stack::push_placed_clipped(e, rect, clip)` places a child at an exact rect like `push_placed` but clips its subtree's drawing to `clip` (in the Stack's coordinate space) — Android's clipChildren: content that overflows, such as a scrolled list row reaching above the list or an inner card past a rounded bubble, is not painted. The Stack child tuple grows an 8th `Option<Rect>` for the clip, and `layout_and_draw` brackets a clipped child with `set_clip_rects`/restore around the recursion. The clip is shifted by the Stack's own origin to match the placed rect (which `Stack::layout` already offsets), so a Stack laid out at a non-zero origin clips in the right place rather than off by that origin. Offscreen RGBA readback. `Canvas::read_rgba_pixels(out)` reads any canvas into tightly packed straight-alpha RGBA8, top-left row first. Unlike `read_gles_rgba_pixels` it also serves the software backend, un-premultiplying its pixmap, so an offscreen software canvas (an embedder's scratch bitmap) can be read back into a straight-alpha buffer. `Canvas::is_software()` lets a caller branch on the backend — e.g. to honour a real path clip on software but only a bounding rect on GLES. Standalone text measurement. `measure_text(text, size)`, re-exported at the crate root, measures one line with the default UI font and the system fallback chain, returning `(width, line_height)` in pixels without a live `Canvas` — for an embedder's measure pass that must produce the same metrics the renderer will later use. It is backed by `system_fonts::primary_handle()`, a process-wide cached handle for the primary UI font (the same default a canvas loads, with the bundled-font fallback), which widens `render::helpers::load_default_font_bytes` to `pub(crate)`.
This commit is contained in:
@@ -53,7 +53,24 @@ pub( crate ) fn layout_and_draw<Msg: Clone>(
|
|||||||
let mut idx = flat_idx;
|
let mut idx = flat_idx;
|
||||||
for ( child_rect, child_i ) in child_rects
|
for ( child_rect, child_i ) in child_rects
|
||||||
{
|
{
|
||||||
idx = layout_and_draw::<Msg>( &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::<Msg>( &child.0, canvas, child_rect, ctx, idx );
|
||||||
|
canvas.set_clip_rects( &saved );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
idx = layout_and_draw::<Msg>( &child.0, canvas, child_rect, ctx, idx );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
idx
|
idx
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,10 +52,11 @@ pub enum VAlign
|
|||||||
pub struct Stack<Msg: Clone>
|
pub struct Stack<Msg: Clone>
|
||||||
{
|
{
|
||||||
/// Children with their alignment, margin, and extra `(x, y)` translation
|
/// 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<Rect>` overrides alignment/sizing with an exact rect (see
|
/// `Option<Rect>` overrides alignment/sizing with an exact rect (see
|
||||||
/// [`push_placed`](Self::push_placed)).
|
/// [`push_placed`](Self::push_placed)); the 8th clips the child's draw to a
|
||||||
pub children: Vec<( Element<Msg>, HAlign, VAlign, f32, f32, f32, Option<Rect> )>,
|
/// rect (Android clipChildren — see [`push_placed_clipped`](Self::push_placed_clipped)).
|
||||||
|
pub children: Vec<( Element<Msg>, HAlign, VAlign, f32, f32, f32, Option<Rect>, Option<Rect> )>,
|
||||||
/// When `true`, [`preferred_size`](Self::preferred_size) reports the max of
|
/// When `true`, [`preferred_size`](Self::preferred_size) reports the max of
|
||||||
/// children's intrinsic widths and heights instead of `(max_width, tallest)`.
|
/// children's intrinsic widths and heights instead of `(max_width, tallest)`.
|
||||||
pub fit_content: bool,
|
pub fit_content: bool,
|
||||||
@@ -102,7 +103,7 @@ impl<Msg: Clone> Stack<Msg>
|
|||||||
margin: f32,
|
margin: f32,
|
||||||
) -> Self
|
) -> 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
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,7 +120,7 @@ impl<Msg: Clone> Stack<Msg>
|
|||||||
offset_y: f32,
|
offset_y: f32,
|
||||||
) -> Self
|
) -> 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
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,7 +134,22 @@ impl<Msg: Clone> Stack<Msg>
|
|||||||
rect: Rect,
|
rect: Rect,
|
||||||
) -> Self
|
) -> 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<Element<Msg>>,
|
||||||
|
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
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,7 +158,7 @@ impl<Msg: Clone> Stack<Msg>
|
|||||||
if self.fit_content
|
if self.fit_content
|
||||||
{
|
{
|
||||||
let content_w = self.children.iter()
|
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::Spacer( s ) => s.resolved_width( canvas ).unwrap_or( 0.0 ),
|
||||||
Element::Separator( _ ) => 0.0,
|
Element::Separator( _ ) => 0.0,
|
||||||
@@ -157,13 +173,13 @@ impl<Msg: Clone> Stack<Msg>
|
|||||||
} )
|
} )
|
||||||
.fold( 0.0_f32, f32::max );
|
.fold( 0.0_f32, f32::max );
|
||||||
let max_h = self.children.iter()
|
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 );
|
.fold( 0.0_f32, f32::max );
|
||||||
return ( content_w.min( max_width ), max_h );
|
return ( content_w.min( max_width ), max_h );
|
||||||
}
|
}
|
||||||
|
|
||||||
let max_h = self.children.iter()
|
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 );
|
.fold( 0.0_f32, f32::max );
|
||||||
( max_width, max_h )
|
( max_width, max_h )
|
||||||
}
|
}
|
||||||
@@ -171,7 +187,7 @@ impl<Msg: Clone> Stack<Msg>
|
|||||||
/// Return `(rect, child_index)` pairs, computing each child's rect from its alignment.
|
/// Return `(rect, child_index)` pairs, computing each child's rect from its alignment.
|
||||||
pub fn layout( &self, rect: Rect, canvas: &Canvas ) -> Vec<(Rect, usize)>
|
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
|
if let Some( p ) = placed
|
||||||
{
|
{
|
||||||
@@ -213,8 +229,8 @@ impl<Msg: Clone> Stack<Msg>
|
|||||||
Stack
|
Stack
|
||||||
{
|
{
|
||||||
children: self.children.into_iter()
|
children: self.children.into_iter()
|
||||||
.map( |( child, 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 ) )
|
( child.map_arc( f ), ha, va, margin, ox, oy, placed, clip ) )
|
||||||
.collect(),
|
.collect(),
|
||||||
fit_content: self.fit_content,
|
fit_content: self.fit_content,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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::{ Color, Corners, CursorShape, Length, LengthBase, PathCmd, Point, Rect, Size, WidgetId };
|
||||||
pub use types::{ design_reference, set_design_reference };
|
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 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::rich_text::{ rich_text, RichText, LinkSpan };
|
||||||
pub use widget::button::ButtonVariant;
|
pub use widget::button::ButtonVariant;
|
||||||
pub use widget::slider::{ Slider, slider, SliderAxis };
|
pub use widget::slider::{ Slider, slider, SliderAxis };
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ pub ( crate ) fn find_font_opt() -> Option<String>
|
|||||||
/// OFL 1.1) when nothing matches or the file cannot be read. Always
|
/// OFL 1.1) when nothing matches or the file cannot be read. Always
|
||||||
/// returns usable bytes so canvas construction never panics on a
|
/// returns usable bytes so canvas construction never panics on a
|
||||||
/// system without the expected fonts.
|
/// system without the expected fonts.
|
||||||
pub ( super ) fn load_default_font_bytes() -> Vec<u8>
|
pub ( crate ) fn load_default_font_bytes() -> Vec<u8>
|
||||||
{
|
{
|
||||||
if let Some( path ) = find_font_opt()
|
if let Some( path ) = find_font_opt()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
/// Composite an externally-owned GL texture into `dest`. No-op on
|
||||||
/// the software backend (no GL state to sample from). Used by
|
/// the software backend (no GL state to sample from). Used by
|
||||||
/// widgets that host content rendered by an external producer —
|
/// widgets that host content rendered by an external producer —
|
||||||
|
|||||||
@@ -147,6 +147,20 @@ pub fn lookup( ch: char ) -> Option<Arc<Font>>
|
|||||||
lookup_handle( ch ).map( |h| h.font )
|
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<FontHandle> = 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
|
/// Bytes-aware variant of [`lookup`]. Returns the full
|
||||||
/// [`FontHandle`] (fontdue handle + raw bytes + face index) so
|
/// [`FontHandle`] (fontdue handle + raw bytes + face index) so
|
||||||
/// callers that need to invoke a HarfBuzz-style shaper can do so
|
/// callers that need to invoke a HarfBuzz-style shaper can do so
|
||||||
|
|||||||
@@ -145,6 +145,34 @@ thread_local!
|
|||||||
/// set of on-screen labels is far smaller than this).
|
/// set of on-screen labels is far smaller than this).
|
||||||
const SHAPE_CACHE_CAP: usize = 8192;
|
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
|
/// Cached wrapper over [`shape_line`]. `font_ctx` identifies the resolver's
|
||||||
/// leading font so two fonts shaping the same text don't collide — pass
|
/// 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
|
/// `std::sync::Arc::as_ptr( &handle.font ) as usize`. `resolve_font` runs only
|
||||||
|
|||||||
Reference in New Issue
Block a user