// SPDX-License-Identifier: LGPL-2.1-only // Copyright (C) 2026 Liberux Labs, S. L. //! Painting a surface's view on its canvas, the same for every backend and renderer. The backend prepares the canvas before and presents it after; see [`crate::platform::Platform`]. use std::collections::HashMap; use std::sync::Arc; use crate::event_loop::SurfaceState; use crate::render::Canvas; use crate::types::{ Color, Rect }; use crate::widget::Element; use super::{ build_draw_ctx, commit_draw_ctx, compute_damage, layout_and_draw, DrawCtx }; use super::chrome::{ draw_fallback_banner, draw_titlebar }; /// The canvas in `slot` at `pw × ph`, built by `create` with the theme's fonts and `scale` on first use. pub( crate ) fn fit_canvas( slot: &mut Option, pw: u32, ph: u32, scale: f32, create: impl FnOnce() -> Canvas ) -> &mut Canvas { let canvas = slot.get_or_insert_with( || { let mut c = create(); c.set_dpi_scale( scale ); if let Some( reg ) = crate::theme::build_font_registry() { c.set_font_registry( Arc::new( reg ) ); } c } ); if canvas.size() != ( pw, ph ) { canvas.resize( pw, ph ); canvas.set_dpi_scale( scale ); } canvas } /// Paint `view` over the whole of `canvas`, cleared to transparent, with no focus, hover or press, for a surface that takes no input such as a subsurface. `scroll` and `viewport` keep its sub-canvases between paints. pub( crate ) fn paint_detached( canvas: &mut Canvas, view: &Element, scroll: &mut HashMap, viewport: &mut HashMap ) { canvas.clear_clip(); canvas.clear(); let ( pw, ph ) = canvas.size(); let screen_rect = Rect { x: 0.0, y: 0.0, width: pw as f32, height: ph as f32 }; let mut ctx = DrawCtx { focused_idx: None, hovered_idx: None, pressed_idx: None, cursor_state: HashMap::new(), selection_anchor: HashMap::new(), text_scroll: HashMap::new(), widget_rects: Vec::new(), debug_layout: false, scroll_offsets: HashMap::new(), scroll_rects: Vec::new(), scroll_canvases: std::mem::take( scroll ), viewport_canvases: std::mem::take( viewport ), viewport_seq: 0, scroll_navigable_items: HashMap::new(), scroll_clipped: Vec::new(), previous_widget_rects: Vec::new(), accessible_extras: Vec::new(), live_depth: 0, origin: ( 0.0, 0.0 ), blur_regions: Vec::new(), }; layout_and_draw::( view, canvas, screen_rect, &mut ctx, 0 ); *scroll = ctx.scroll_canvases; *viewport = ctx.viewport_canvases; } /// Repaint the whole surface. Returns the rects that changed when `precise_damage` asks for them and the content did not change wholesale, else none, meaning the whole surface. pub( crate ) fn paint_full( ss: &mut SurfaceState, view: &Element, bg: Color, debug_layout: bool, precise_damage: bool, pw: u32, ph: u32, scale: f32, ) -> Vec { let canvas = ss.canvas.as_mut().expect( "canvas prepared" ); canvas.clear_clip(); if bg.a > 0.0 { canvas.fill( bg ); } else { canvas.clear(); } let ctx = paint_view( ss, view, debug_layout, pw, ph, scale ); let damage = if ss.content_dirty || !precise_damage { Vec::new() } else { compute_damage( &ss.frame.widget_rects, &ctx.widget_rects, ss.frame.prev_focused, ss.frame.prev_hovered, ss.frame.prev_pressed, ss.focused_idx, ss.hovered_idx, ss.gesture.pressed_idx, pw, ph, ) }; commit_draw_ctx( &mut ss.frame, ss.focused_idx, ss.hovered_idx, ss.gesture.pressed_idx, ctx ); ss.content_dirty = false; damage } /// Repaint only under `dirty_rects`, keeping the rest of the previous frame's pixels, and return them as the damage. pub( crate ) fn paint_partial( ss: &mut SurfaceState, view: &Element, bg: Color, dirty_rects: Vec, pw: u32, ph: u32, scale: f32, ) -> Vec { let canvas = ss.canvas.as_mut().expect( "canvas prepared" ); canvas.set_clip_rects( &dirty_rects ); // Start the dirty rects from the background rather than the previous widget's pixels. if bg.a > 0.0 { canvas.fill( bg ); } else { canvas.clear_rects_transparent( &dirty_rects ); } let ctx = paint_view( ss, view, false, pw, ph, scale ); if let Some( canvas ) = ss.canvas.as_mut() { canvas.clear_clip(); } commit_draw_ctx( &mut ss.frame, ss.focused_idx, ss.hovered_idx, ss.gesture.pressed_idx, ctx ); ss.content_dirty = false; dirty_rects } /// The title bar, the widget tree and what the runtime draws over it, on a canvas already cleared where needed. fn paint_view( ss: &mut SurfaceState, view: &Element, debug_layout: bool, pw: u32, ph: u32, scale: f32, ) -> DrawCtx { let canvas = ss.canvas.as_mut().expect( "canvas prepared" ); let sf = scale; let tb_h = ss.titlebar_height * sf; let inset = ss.insets; let osk_h = crate::event_loop::osk::place_osk( &mut ss.osk, pw, ph, sf, inset ); let top = tb_h + inset.top * sf; let screen_rect = Rect { x: inset.left * sf, y: top, width: ( pw as f32 - ( inset.left + inset.right ) * sf ).max( 0.0 ), height: ( ph as f32 - top - osk_h - inset.bottom * sf ).max( 0.0 ), }; ss.titlebar_close_rect = draw_titlebar( canvas, &ss.titlebar_title, pw, tb_h, sf ); // A text field draws its cursor when focused however the focus came. let drawn_focus = ss.focused_idx.filter( |&idx| ss.focus_visible || crate::tree::find_handlers( &ss.frame.widget_rects, idx ).is_some_and( |h| h.is_text_input() ) ); let mut ctx = build_draw_ctx( &mut ss.frame, drawn_focus, ss.hovered_idx, ss.gesture.pressed_idx, debug_layout ); layout_and_draw::( view, canvas, screen_rect, &mut ctx, 0 ); if ctx.debug_layout { for w in &ctx.widget_rects { canvas.stroke_rect( w.rect, Color::rgb( 1.0, 0.0, 0.0 ), 1.5, 0.0 ); } } if let Some( panel ) = ss.osk.as_mut() { panel.draw( canvas ); } if let Some( ref menu ) = ss.context_menu { super::draw_context_menu( canvas, menu ); } // Last, so it sits over every widget. Outside a partial repaint's clip the previous frame's banner stays. draw_fallback_banner( canvas, pw, sf ); ctx }