// SPDX-License-Identifier: LGPL-2.1-only // Copyright (C) 2026 Liberux Labs, S. L. //! Per-frame drawing pipeline. //! //! The run loop hands each configured surface to [`draw_frame`] once per //! vblank; this module walks it through a decision tree: //! //! * **Skip** — no content changed and no interaction state moved, so the //! previously committed buffer is still correct. //! * **Partial** — only focus / hover / pressed changed. Install a clip //! mask covering the paint rects of the affected widgets, repaint only //! under the clip, damage Wayland with exactly those rects. //! * **Full** — something substantive changed (app message, animation //! tick, configure, text edit, scroll, slider drag). Clear + redraw //! the entire view, let damage tracking tighten the commit. //! //! Each of those paths has a software variant (CPU + SHM pool) and a //! GLES variant (FBO + EGL swap). The four resulting functions live in //! [`software`] and [`gles`]; this file is just the router plus the //! small shared setup ([`DrawCtx`], [`pick_shm_format`]). //! //! # Submodule layout //! //! * [`software`] — `draw_surface_full` / `draw_surface_partial` //! * [`gles`] — `draw_surface_full_gpu` / `draw_surface_partial_gpu` //! * [`damage`] — `compute_interaction_dirty_rects`, `compute_damage`, //! `clamp_rect_to` //! * [`chrome`] — `draw_titlebar`, `apply_input_region` //! * [`layout`] — `layout_and_draw` (the recursive element walker) use std::collections::HashMap; use std::sync::Arc; use smithay_client_toolkit::reexports::client::protocol::{ wl_shm, wl_surface::WlSurface }; use smithay_client_toolkit::shm::Shm; use crate::app::App; use crate::event_loop::{ AppData, SurfaceFocus, SurfaceState }; use crate::render::Canvas; use crate::types::{ Color, Rect }; use crate::widget::{ Element, LaidOutWidget }; pub( crate ) mod software; pub( crate ) mod gles; pub( crate ) mod damage; pub( crate ) mod chrome; pub( crate ) mod layout; pub( crate ) use damage::{ compute_damage, compute_interaction_dirty_rects }; pub( crate ) use layout::layout_and_draw; /// Pick the best wl_shm format for our RGBA-premultiplied pixmap. /// /// Abgr8888 matches tiny-skia's byteorder on little-endian systems, so we can /// copy with a plain memcpy. If the compositor doesn't advertise it, fall back /// to Argb8888 (mandatory per wl_shm) which requires a per-channel swap. /// /// Returns `(format, swap_rb)`. pub( crate ) fn pick_shm_format( shm: &Shm ) -> ( wl_shm::Format, bool ) { if shm.formats().contains( &wl_shm::Format::Abgr8888 ) { ( wl_shm::Format::Abgr8888, false ) } else { ( wl_shm::Format::Argb8888, true ) } } /// Per-frame draw state threaded through [`layout_and_draw`]. Captures /// the interaction snapshot (focus / hover / pressed), scratch space /// for the widget-rect list the frame will produce, and the scroll /// offsets / sub-canvases carried across frames. /// /// `scroll_canvases` arrives populated (the previous frame's /// sub-canvases) so `layout_and_draw` can re-use them for Scroll /// viewports whose size did not change. `scroll_rects` and /// `widget_rects` start empty and get filled as the element tree is /// walked. pub( crate ) struct DrawCtx { pub focused_idx: Option, pub hovered_idx: Option, pub pressed_idx: Option, pub cursor_state: HashMap, pub selection_anchor: HashMap, pub widget_rects: Vec>, pub debug_layout: bool, pub scroll_offsets: HashMap, pub scroll_rects: Vec<(Rect, usize)>, pub scroll_canvases: HashMap, /// Per-scroll navigation map: list of `(flat_idx, content_y, height)` /// for every interactive item the scroll's child laid out, in /// document order, **including items currently scrolled off-screen**. /// Keyboard arrow handlers read this to step the runtime's /// `hovered_idx` item-by-item without needing to know how the popup /// content was composed. The Y is in pre-translation, pre-offset /// coordinates (i.e. relative to the start of the scroll's child /// content) so the keyboard auto-scroll can compute the offset /// needed to bring an item into view without depending on the /// current scroll position. pub scroll_navigable_items: HashMap>, /// Snapshot of the previous frame's `widget_rects`. Read by /// [`crate::widget::anchored_overlay::AnchoredOverlay`] at draw time /// to look up the rect of an anchor widget by [`crate::WidgetId`] and /// re-position itself relative to that rect. Drivers populate this /// before invoking the recursive layout / draw walk. pub previous_widget_rects: Vec>, } /// Paint the built-in Copy / Cut / Paste context menu on top of the /// finished surface content. Called from the software and GLES draw /// paths right before `present()` so the menu sits above everything /// the widget tree painted, matching the convention every other /// toolkit follows for runtime-internal popups. pub( crate ) fn draw_context_menu( canvas: &mut crate::render::Canvas, menu: &crate::event_loop::context_menu::ContextMenu, ) { let palette = crate::theme::palette(); let bg = palette.surface; let border = palette.divider; let text = palette.text_primary; let muted = palette.text_secondary; let hi = palette.surface_alt; let r = menu.rect; canvas.fill_rect( r, bg, 8.0 ); canvas.stroke_rect( r, border, 1.0, 8.0 ); let ( ys, row_h ) = menu.row_ys(); // Row order: Copy / Cut / Paste / Delete. Labels go through // `rust_i18n::t!()` so the menu picks up the active locale; the // `enabled` flag mirrors the gating in `handle_context_menu_press`. let labels: [ ( String, bool ); 4 ] = [ ( rust_i18n::t!( "context_menu.copy" ).to_string(), menu.has_selection ), ( rust_i18n::t!( "context_menu.cut" ).to_string(), menu.has_selection ), ( rust_i18n::t!( "context_menu.paste" ).to_string(), menu.can_paste ), ( rust_i18n::t!( "context_menu.delete" ).to_string(), menu.has_selection ), ]; // Subtle accent band on the row matching the *primary* action so // the menu reads as "Paste is the default" when there is no // selection (the common case for a paste-into-empty-field click) // and "Copy is the default" when a selection is active. Just a // hint, not a binding — every row still works on its own click. let primary_idx = if menu.has_selection { 0 } else { 2 }; let primary_band = crate::types::Rect { x: r.x + 4.0, y: ys[ primary_idx ] + 2.0, width: r.width - 8.0, height: row_h - 4.0, }; canvas.fill_rect( primary_band, hi, 6.0 ); for ( i, ( label, enabled ) ) in labels.iter().enumerate() { let color = if *enabled { text } else { muted }; canvas.draw_text( label, r.x + 16.0, ys[ i ] + row_h * 0.5 + 5.0, 14.0, color, ); } // Thin separator between every pair of rows. let sep_color = palette.divider; for y in ys.iter().skip( 1 ) { canvas.draw_line( r.x + 8.0, *y, r.x + r.width - 8.0, *y, sep_color, 1.0 ); } } pub( crate ) fn draw_frame( data: &mut AppData ) { // Caches were refreshed by the run loop just before calling us; pull them // by reference instead of re-invoking `App::view` / `App::overlays` each // frame. The two `expect`s document the run loop's contract. let main_view = data.cached_view.as_ref().expect( "view cache populated" ); let overlays = data.cached_overlays.as_ref().expect( "overlays cache populated" ); let main_bg = data.app.background_color(); let main_region = data.app.input_region(); let debug_layout = data.debug_layout; let ( format, swap_rb ) = pick_shm_format( &data.shm ); let egl_ctx = data.egl_context.as_ref(); let qh = &data.qh; if data.main.configured && data.main.needs_redraw && !data.main.frame_pending { let req_frame = | wl: &WlSurface | { let _ = wl.frame( qh, SurfaceFocus::Main ); }; draw_surface::( &mut data.main, &data.compositor_state, egl_ctx, main_view, main_bg, main_region.as_deref(), debug_layout, format, swap_rb, &req_frame, ); data.main.needs_redraw = false; data.main.last_draw = std::time::Instant::now(); } for spec in overlays { if let Some( ss ) = data.overlays.get_mut( &spec.id ) { if !ss.configured || !ss.needs_redraw || ss.frame_pending { continue; } let focus = SurfaceFocus::Overlay( spec.id ); let req_frame = | wl: &WlSurface | { let _ = wl.frame( qh, focus ); }; let bg = Color::rgba( 0.0, 0.0, 0.0, 0.0 ); draw_surface::( ss, &data.compositor_state, egl_ctx, &spec.view, bg, spec.input_region.as_deref(), debug_layout, format, swap_rb, &req_frame, ); ss.needs_redraw = false; ss.last_draw = std::time::Instant::now(); } } } /// Render one surface's current view. Picks between software and GLES, /// and within each picks between full and partial redraw based on what /// changed since the last committed frame. /// /// The decision: /// * **Skip** — `content_dirty` is false and no interaction state /// changed. Previously committed buffer stays on screen. /// * **Partial** — `content_dirty` is false but focus / hover / pressed /// transitioned. Canvas is preserved across frames, so clip to the /// dirty widgets and repaint only under the clip. /// * **Full** — `content_dirty` is true. Clear + redraw + damage. /// /// Partial eligibility also bails out when the total dirty area >50% /// of the surface: at that ratio per-region clipping is no faster than /// a plain full redraw, so the code prefers one big damage rect over /// several small ones. fn draw_surface( ss: &mut SurfaceState, compositor: &smithay_client_toolkit::compositor::CompositorState, egl_ctx: Option<&Arc>, view: &Element, bg: Color, input_region: Option<&[Rect]>, debug_layout: bool, shm_format: wl_shm::Format, swap_rb: bool, request_frame: &dyn Fn( &WlSurface ), ) { let scale = ss.scale_factor.max( 1 ) as u32; let w = ss.width; let h = ss.height; if w == 0 || h == 0 { return; } let pw = w * scale; let ph = h * scale; // Decide partial-redraw eligibility BEFORE allocating a buffer. If we end // up skipping the frame, we want to avoid touching the SHM pool at all. let canvas_ready = ss.canvas.as_ref() .map( |c| c.size() == ( pw, ph ) ) .unwrap_or( false ); let partial_eligible = !ss.content_dirty && canvas_ready && !ss.widget_rects.is_empty(); if partial_eligible { let dirty_rects = compute_interaction_dirty_rects( &ss.widget_rects, ss.prev_focused, ss.prev_hovered, ss.prev_pressed, ss.focused_idx, ss.hovered_idx, ss.gesture.pressed_idx, pw, ph, ); if dirty_rects.is_empty() { // Nothing visible changed — keep the previously committed buffer. return; } // Total dirty area > 50% of screen: a full redraw is no slower than // per-region clipping but emits a single damage rect. let total: f32 = dirty_rects.iter().map( |r| r.width * r.height ).sum(); if total < pw as f32 * ph as f32 * 0.5 { if let ( Some( ctx ), true ) = ( egl_ctx, ss.egl_surface.is_some() ) { gles::draw_surface_partial_gpu( ss, compositor, ctx, view, bg, input_region, dirty_rects, pw, ph, scale, request_frame, ); } else { software::draw_surface_partial( ss, compositor, view, bg, input_region, shm_format, swap_rb, dirty_rects, pw, ph, scale, request_frame, ); } return; } } if let ( Some( ctx ), true ) = ( egl_ctx, ss.egl_surface.is_some() ) { gles::draw_surface_full_gpu( ss, compositor, ctx, view, bg, input_region, debug_layout, pw, ph, scale, request_frame, ); } else { software::draw_surface_full( ss, compositor, view, bg, input_region, debug_layout, shm_format, swap_rb, pw, ph, scale, request_frame, ); } }