// SPDX-License-Identifier: LGPL-2.1-only // Copyright (C) 2026 Liberux Labs, S. L. //! Element-tree recursive walker. //! //! [`layout_and_draw`] is the single bottom of the draw pipeline: //! given an [`Element`] + its allocated rect, it lays out children, //! paints leaves, threads scroll sub-canvases, and records the //! [`LaidOutWidget`] entries the input layer will hit-test against. //! Both software and GPU paths funnel every widget through this one //! function. use crate::render::Canvas; use crate::types::{ Corners, Rect }; use crate::widget::{ Element, LaidOutWidget, WidgetHandlers }; use super::DrawCtx; use super::damage::clamp_rect_to; pub( crate ) fn layout_and_draw( element: &Element, canvas: &mut Canvas, rect: Rect, ctx: &mut DrawCtx, flat_idx: usize, ) -> usize { match element { Element::Column( col ) => { let child_rects = col.layout( rect, canvas ); let mut idx = flat_idx; for ( child_rect, child_i ) in child_rects { idx = layout_and_draw::( &col.children[child_i], canvas, child_rect, ctx, idx ); } idx } Element::Row( r ) => { let child_rects = r.layout( rect, canvas ); let mut idx = flat_idx; for ( child_rect, child_i ) in child_rects { idx = layout_and_draw::( &r.children[child_i], canvas, child_rect, ctx, idx ); } idx } Element::Stack( s ) => { let child_rects = s.layout( rect, canvas ); let mut idx = flat_idx; for ( child_rect, child_i ) in child_rects { 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 } Element::WrapGrid( g ) => { let child_rects = g.layout( rect, canvas ); let mut idx = flat_idx; for ( child_rect, child_i ) in child_rects { idx = layout_and_draw::( &g.children[child_i], canvas, child_rect, ctx, idx ); } idx } Element::Carousel( car ) => { let child_rects = car.layout( rect, canvas ); let mut idx = flat_idx; for ( child_rect, child_i ) in child_rects { idx = layout_and_draw::( &car.children[child_i], canvas, child_rect, ctx, idx ); } idx } Element::TabRow( r ) => { // The child is the scroll of the tabs, whose offset is keyed by this same index. let inner = r.arrange( rect, canvas ); let offset = ctx.scroll_offsets.get( &flat_idx ).copied().unwrap_or( ( 0.0, 0.0 ) ); if let Some( x ) = r.scroll_to( offset.0, inner.width, canvas ) { ctx.scroll_offsets.insert( flat_idx, ( x, offset.1 ) ); } layout_and_draw::( r.child.as_ref(), canvas, inner, ctx, flat_idx ) } Element::TabCells( cells ) => { cells.motion.set_origin( ctx.origin ); let child_rects = cells.layout( rect, canvas ); let mut idx = flat_idx; for ( child_rect, child_i ) in child_rects { idx = layout_and_draw::( cells.child( child_i ), canvas, child_rect, ctx, idx ); } idx } Element::TabCards( cards ) => { cards.motion.set_origin( ctx.origin ); let child_rects = cards.layout( rect, canvas ); let mut idx = flat_idx; for ( child_rect, child_i ) in child_rects { idx = layout_and_draw::( cards.child( child_i ), canvas, child_rect, ctx, idx ); } idx } Element::Flex( f ) => { // `Flex` is invisible chrome: it claimed leftover width up at // `Row::layout`, here we just unwrap it and draw the child // inside the allocated rect. No flat-index of its own. layout_and_draw::( f.child.as_ref(), canvas, rect, ctx, flat_idx ) } Element::AnchoredOverlay( a ) => { // Look up the anchor's rect from the previous frame's // `widget_rects` snapshot. If found, place the child flush // below the anchor at the child's intrinsic size; if not, // fall back to the parent-supplied rect so the child still // renders (modal-style, typically a one-frame artefact on // the first frame after open). let anchor_rect = ctx.previous_widget_rects.iter() .find( |w| w.id == Some( a.anchor_id ) ) .map( |w| w.rect ); let target = match anchor_rect { Some( anchor ) => { // Use the child's intrinsic preferred size so the // popup keeps its design width / height regardless // of how wide the trigger pill happens to be. let ( w, h ) = a.child.preferred_size( rect.width, canvas ); Rect { x: a.left( anchor, w, rect ), y: anchor.y + anchor.height + a.gap, width: w, height: h, } } None => rect, }; layout_and_draw::( a.child.as_ref(), canvas, target, ctx, flat_idx ) } Element::Pressable( p ) => { // Push the wrapper's hit rect *before* recursing so that any // interactive child pushed during recursion sits later in // `widget_rects` and wins under `iter().rev()` hit testing. let my_idx = flat_idx; if p.has_handler() { ctx.widget_rects.push( LaidOutWidget { rect, flat_idx: my_idx, id: p.id, paint_rect: rect, handlers: WidgetHandlers::Button { on_press: p.on_press.clone(), on_long_press: p.on_long_press.clone(), on_drag_start: p.on_drag_start.clone(), on_escape: p.on_escape.clone(), repeating: false, }, keyboard_focusable: false, focus_on_press: p.focus_on_press, cursor: p.cursor.unwrap_or( crate::types::CursorShape::Pointer ), tooltip: None, accessible_label: None, is_live_region: ctx.live_depth > 0, } ); } layout_and_draw::( p.child.as_ref(), canvas, rect, ctx, my_idx + 1 ) } Element::Container( c ) => { let saved_alpha = canvas.global_alpha(); canvas.set_global_alpha( saved_alpha * c.opacity ); let live_inc = if c.a11y_live { 1 } else { 0 }; ctx.live_depth += live_inc; let rect = if let Some( mw ) = c.max_width { crate::types::Rect { width: rect.width.min( mw ), ..rect } } else { rect }; // Surface slot takes precedence over flat background; falls // through to `c.background` when the slot is absent (third- // party theme without the named surface — content still // renders, just without the themed chrome). let painted = match c.surface.as_deref() { Some( slot ) => match crate::theme::resolve_surface( slot ) { Some( ( surf, outer ) ) => { canvas.fill_surface ( rect, &surf.fill, &outer, &surf.inset_shadows, c.corners, ); true } None => false, }, None => false, }; if !painted { if let Some( ref bg ) = c.background { canvas.fill_paint_rect( rect, bg, c.corners ); } } if let Some( ( color, width ) ) = c.border { canvas.stroke_rect( rect, color, width, c.corners ); } if c.blur_behind { let Corners { tl, tr, br, bl } = c.corners; let radius = tl.max( tr ).max( br ).max( bl ).min( rect.width * 0.5 ).min( rect.height * 0.5 ); let on_surface = Rect { x: rect.x + ctx.origin.0, y: rect.y + ctx.origin.1, ..rect }; ctx.blur_regions.push( ( on_surface, radius ) ); } let pad_l = canvas.resolve_geom( c.pad_left ); let pad_r = canvas.resolve_geom( c.pad_right ); let pad_t = canvas.resolve_geom( c.pad_top ); let pad_b = canvas.resolve_geom( c.pad_bottom ); let inner = crate::types::Rect { x: rect.x + pad_l, y: rect.y + pad_t, width: ( rect.width - pad_l - pad_r ).max( 0.0 ), height: ( rect.height - pad_t - pad_b ).max( 0.0 ), }; let result = layout_and_draw::( c.child.as_ref(), canvas, inner, ctx, flat_idx ); ctx.live_depth -= live_inc; canvas.set_global_alpha( saved_alpha ); result } Element::Scroll( s ) => { let my_idx = flat_idx; let axis = s.axis; let ( ox_raw, oy_raw ) = ctx.scroll_offsets.get( &my_idx ).copied().unwrap_or( ( 0.0, 0.0 ) ); // Width-aware preferred size: when the axis allows horizontal // overflow we let the child report its natural width by // asking with `f32::INFINITY`; otherwise the child stays // clamped to the viewport so wraps still happen. let child_w_max = if axis.allows_x() { f32::INFINITY } else { rect.width }; let ( child_w, child_h ) = s.child.preferred_size( child_w_max, canvas ); let ox = if axis.allows_x() { crate::widget::scroll::clamp_offset( ox_raw, child_w, rect.width ) } else { 0.0 }; let oy = if axis.allows_y() { crate::widget::scroll::clamp_offset( oy_raw, child_h, rect.height ) } else { 0.0 }; // Write the clamped offsets back so input handlers (wheel / // drag) cannot accumulate past the content extents. Without // this, repeated wheel ticks past the edge keep growing // `*_raw` and the user has to "undo" that excess before the // content scrolls back. The clamp is idempotent for in-range // values, so the only effect is collapsing out-of-range // entries to the legitimate maximum. if ox != ox_raw || oy != oy_raw { ctx.scroll_offsets.insert( my_idx, ( ox, oy ) ); } // Reuse the sub-canvas from the previous frame if its size matches; // only reallocate when the viewport is resized. let sw = (rect.width.ceil() as u32).max( 1 ); let sh = (rect.height.ceil() as u32).max( 1 ); let mut sub = ctx.scroll_canvases.remove( &my_idx ) .map( |mut c| { if c.size() != ( sw, sh ) { c.resize( sw, sh ); } c } ) .unwrap_or_else( || canvas.sub_canvas( sw, sh ) ); sub.clear(); // Child content fills at least the full viewport on each // allowed axis so that children with `Bottom`/`Right` // alignment are positioned correctly when the content is // shorter than the viewport along that axis. let effective_w = if axis.allows_x() { child_w.max( rect.width ) } else { rect.width }; let effective_h = if axis.allows_y() { child_h.max( rect.height ) } else { rect.height }; // Shift child by `(-ox, -oy)` so scrolled content appears at // `(0, 0)` of the sub-canvas. let child_rect = Rect { x: -ox, y: -oy, width: effective_w, height: effective_h }; let rects_before = ctx.widget_rects.len(); let scroll_rects_before = ctx.scroll_rects.len(); let origin = ctx.origin; ctx.origin = ( origin.0 + rect.x, origin.1 + rect.y ); let next_idx = layout_and_draw::( s.child.as_ref(), &mut sub, child_rect, ctx, my_idx + 1 ); ctx.origin = origin; // Translate widget_rects from sub-canvas space to global space; drop // clipped ones. Also clamp `paint_rect` to the scroll viewport so a // widget painting outside the sub-canvas does not invalidate pixels // it cannot actually reach (the sub-canvas is blitted as a whole, // so nothing outside the viewport appears on screen anyway). let new_rects: Vec> = ctx.widget_rects.drain( rects_before.. ).collect(); // Capture every interactive item the child laid out — including // items that the visibility filter below will discard — so the // keyboard handler can step `hovered_idx` item-by-item without // caring about which items are currently scrolled into view. // The recorded Y is in pre-translation, pre-offset coordinates // (i.e. `child_y` relative to the start of the scroll content), // recovered by undoing the `-oy` shift the child layout pass // applied: `content_y = w.rect.y - child_rect.y = w.rect.y + oy`. let navigable: Vec<( usize, f32, f32 )> = new_rects.iter() .filter( |w| w.handlers.is_navigable_list_item() ) .map( |w| ( w.flat_idx, w.rect.y + oy, w.rect.height ) ) .collect(); if !navigable.is_empty() { ctx.scroll_navigable_items.insert( my_idx, navigable ); } for mut w in new_rects { let content = Rect { x: w.rect.x + ox, y: w.rect.y + oy, ..w.rect }; // Sub-canvas origin (0,0) maps to global (rect.x, rect.y). w.rect.x += rect.x; w.rect.y += rect.y; w.paint_rect.x += rect.x; w.paint_rect.y += rect.y; w.paint_rect = clamp_rect_to( w.paint_rect, rect ); // Only keep widgets at least partially visible inside the // viewport — now bi-axial because Scroll::horizontal / // Scroll::both push content off-screen along x too. let visible_y = w.rect.y + w.rect.height > rect.y && w.rect.y < rect.y + rect.height; let visible_x = w.rect.x + w.rect.width > rect.x && w.rect.x < rect.x + rect.width; if visible_x && visible_y { ctx.widget_rects.push( w ); } else if let Some( id ) = w.id { ctx.scroll_clipped.push( ( id, my_idx, content ) ); } } // Same translation for scroll_rects pushed by any nested // scroll widgets — without this, the wheel handler hit-tests // in surface coords against rects in sub-canvas coords and // only matches when the sub-canvas happens to start at // (0, 0) of the surface. Clamp to the outer scroll rect so // the inner scroll only captures wheel events inside its // visible region. let new_scroll_rects: Vec<( Rect, usize, crate::widget::scroll::ScrollAxis )> = ctx.scroll_rects.drain( scroll_rects_before.. ).collect(); for ( mut r, idx, ax ) in new_scroll_rects { r.x += rect.x; r.y += rect.y; let clamped = clamp_rect_to( r, rect ); if clamped.width > 0.0 && clamped.height > 0.0 { ctx.scroll_rects.push( ( clamped, idx, ax ) ); } } ctx.scroll_rects.push( ( rect, my_idx, axis ) ); canvas.blit( &sub, rect.x as i32, rect.y as i32 ); ctx.scroll_canvases.insert( my_idx, sub ); next_idx } // Without a fade the clip is a plain scissor on the same canvas: no // offscreen pass, no blit, and the child's textures stay in the // parent's caches. Element::Viewport( v ) if v.fade_bottom <= 0.0 => { let child_h = v.child.preferred_size( rect.width, canvas ).1; let effective_h = child_h.max( rect.height ); let saved_clip = canvas.clip_bounds(); let clip = if saved_clip.is_empty() { rect } else { let ( mut x0, mut y0, mut x1, mut y1 ) = ( f32::INFINITY, f32::INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY ); for r in &saved_clip { x0 = x0.min( r.x ); y0 = y0.min( r.y ); x1 = x1.max( r.x + r.width ); y1 = y1.max( r.y + r.height ); } clamp_rect_to( rect, Rect { x: x0, y: y0, width: x1 - x0, height: y1 - y0 } ) }; canvas.set_clip_rects( &[ clip ] ); let prev_viewport = if v.local_viewport { canvas.layout_viewport_override( Some( ( rect.width, rect.height ) ) ) } else { None }; let child_rect = Rect { x: rect.x, y: rect.y, width: rect.width, height: effective_h }; let rects_before = ctx.widget_rects.len(); let scroll_rects_before = ctx.scroll_rects.len(); let next_idx = layout_and_draw::( v.child.as_ref(), canvas, child_rect, ctx, flat_idx ); if v.local_viewport { canvas.layout_viewport_override( prev_viewport ); } canvas.set_clip_rects( &saved_clip ); let new_rects: Vec> = ctx.widget_rects.drain( rects_before.. ).collect(); for mut w in new_rects { w.paint_rect = clamp_rect_to( w.paint_rect, rect ); if w.rect.y + w.rect.height > rect.y && w.rect.y < rect.y + rect.height { ctx.widget_rects.push( w ); } } let new_scroll_rects: Vec<( Rect, usize, crate::widget::scroll::ScrollAxis )> = ctx.scroll_rects.drain( scroll_rects_before.. ).collect(); for ( r, idx, ax ) in new_scroll_rects { let clamped = clamp_rect_to( r, rect ); if clamped.width > 0.0 && clamped.height > 0.0 { ctx.scroll_rects.push( ( clamped, idx, ax ) ); } } next_idx } Element::Viewport( v ) => { let child_h = v.child.preferred_size( rect.width, canvas ).1; let effective_h = child_h.max( rect.height ); let vw = ( rect.width.ceil() as u32 ).max( 1 ); let vh = ( rect.height.ceil() as u32 ).max( 1 ); let seq = ctx.viewport_seq; ctx.viewport_seq += 1; let mut sub = ctx.viewport_canvases.remove( &seq ) .map( |mut c| { if c.size() != ( vw, vh ) { c.resize( vw, vh ); } c } ) .unwrap_or_else( || canvas.sub_canvas( vw, vh ) ); if v.local_viewport { sub.set_local_layout_viewport(); } sub.clear(); let child_rect = Rect { x: 0.0, y: 0.0, width: rect.width, height: effective_h }; let rects_before = ctx.widget_rects.len(); let scroll_rects_before = ctx.scroll_rects.len(); let blur_before = ctx.blur_regions.len(); let origin = ctx.origin; ctx.origin = ( origin.0 + rect.x, origin.1 + rect.y ); let next_idx = layout_and_draw::( v.child.as_ref(), &mut sub, child_rect, ctx, flat_idx ); ctx.origin = origin; let view_on_surface = Rect { x: rect.x + origin.0, y: rect.y + origin.1, ..rect }; let clipped: Vec<( Rect, f32 )> = ctx.blur_regions.drain( blur_before.. ) .map( |( r, radius )| ( clamp_rect_to( r, view_on_surface ), radius ) ) .filter( |( r, _ )| r.width > 0.0 && r.height > 0.0 ) .collect(); ctx.blur_regions.extend( clipped ); let new_rects: Vec> = ctx.widget_rects.drain( rects_before.. ).collect(); for mut w in new_rects { w.rect.x += rect.x; w.rect.y += rect.y; w.paint_rect.x += rect.x; w.paint_rect.y += rect.y; w.paint_rect = clamp_rect_to( w.paint_rect, rect ); if w.rect.y + w.rect.height > rect.y && w.rect.y < rect.y + rect.height { ctx.widget_rects.push( w ); } } // Translate scroll_rects pushed by nested scroll widgets // from sub-canvas to surface space. Same reasoning as the // scroll-inside-scroll case above. let new_scroll_rects: Vec<( Rect, usize, crate::widget::scroll::ScrollAxis )> = ctx.scroll_rects.drain( scroll_rects_before.. ).collect(); for ( mut r, idx, ax ) in new_scroll_rects { r.x += rect.x; r.y += rect.y; let clamped = clamp_rect_to( r, rect ); if clamped.width > 0.0 && clamped.height > 0.0 { ctx.scroll_rects.push( ( clamped, idx, ax ) ); } } canvas.blit_fade_bottom( &sub, rect.x as i32, rect.y as i32, v.fade_bottom ); ctx.viewport_canvases.insert( seq, sub ); next_idx } Element::RichText( r ) => { r.draw( canvas, rect ); // One hit rect per link line, each its own flat_idx so hover/press // state does not bleed between links sharing a paragraph. let link_rects = r.link_rects( rect, canvas ); let consumed = link_rects.len().max( 1 ); for ( k, ( lr, msg ) ) in link_rects.into_iter().enumerate() { ctx.widget_rects.push( LaidOutWidget { rect: lr, flat_idx: flat_idx + k, id: None, paint_rect: lr, handlers: WidgetHandlers::Button { on_press: Some( msg ), on_long_press: None, on_drag_start: None, on_escape: None, repeating: false, }, keyboard_focusable: false, focus_on_press: true, cursor: crate::types::CursorShape::Pointer, tooltip: None, accessible_label: None, is_live_region: ctx.live_depth > 0, } ); } flat_idx + consumed } other => { // Gate the focus ring by `is_focusable`: a widget that opts out // of keyboard focus (e.g. `Button::focusable(false)`) should not // paint a ring even if it ends up in `focused_idx` after a tap. let is_focused = ctx.focused_idx == Some( flat_idx ) && other.is_focusable(); let is_hovered = ctx.hovered_idx == Some( flat_idx ); let is_pressed = ctx.pressed_idx == Some( flat_idx ); let cursor_pos = ctx.cursor_state.get( &flat_idx ).copied().unwrap_or( 0 ); let sel_anchor = ctx.selection_anchor.get( &flat_idx ).copied().unwrap_or( cursor_pos ); let text_scroll = ctx.text_scroll.get( &flat_idx ).copied().unwrap_or_default(); let widget_id = match other { Element::Button( b ) => b.id, Element::TextEdit( t ) => t.id, Element::Toggle( t ) => t.id, Element::Checkbox( c ) => c.id, Element::Radio( r ) => r.id, Element::ListItem( l ) => l.id, Element::WindowButton( b ) => b.id, _ => None, }; if let Some( shown ) = other.draw( canvas, rect, is_focused, is_hovered, is_pressed, cursor_pos, sel_anchor, text_scroll ) { if shown != text_scroll { ctx.text_scroll.insert( flat_idx, shown ); } } if other.is_interactive() { let mut handlers = other.handlers(); // Horizontal sliders resolve their thumb size through the // widget-scaling mode here — layout has the canvas — so the // input path maps clicks against the same thumb the renderer // drew (see `slider::value_from_x_in_rect`). if matches!( other, Element::Slider( _ ) ) { handlers.set_slider_thumb_px( crate::widget::slider::resolved_thumb_px( canvas ) ); } if matches!( other, Element::PatternGrid( _ ) ) { handlers.set_pattern_snap_px( canvas.geom_px( crate::widget::pattern_grid::snap_design_px() ) ); } ctx.widget_rects.push( LaidOutWidget { rect, flat_idx, id: widget_id, paint_rect: other.paint_bounds( rect ), handlers, keyboard_focusable: other.is_focusable(), focus_on_press: other.focus_on_press(), cursor: other.cursor_shape(), tooltip: other.tooltip().map( str::to_string ), accessible_label: other.accessible_label(), is_live_region: ctx.live_depth > 0, } ); } else { let live = ctx.live_depth > 0; match other { Element::Text( t ) if !t.content.is_empty() => { ctx.accessible_extras.push( crate::a11y::tree::AccessibleExtra { rect, label: Some( t.content.clone() ), live, kind: crate::a11y::tree::AccessibleExtraKind::Label, } ); } Element::Image( _ ) => { ctx.accessible_extras.push( crate::a11y::tree::AccessibleExtra { rect, label: None, live, kind: crate::a11y::tree::AccessibleExtraKind::Image, } ); } Element::Separator( _ ) => { ctx.accessible_extras.push( crate::a11y::tree::AccessibleExtra { rect, label: None, live, kind: crate::a11y::tree::AccessibleExtraKind::Separator, } ); } Element::ProgressBar( p ) => { ctx.accessible_extras.push( crate::a11y::tree::AccessibleExtra { rect, label: None, live, kind: crate::a11y::tree::AccessibleExtraKind::Progress( p.value ), } ); } _ => {} } } flat_idx + 1 } } }