// 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`]; routing and surface-level orchestration //! live in [`crate::event_loop::frame`]. //! //! # 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 crate::render::Canvas; use crate::types::{ Rect, WidgetId }; use crate::widget::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; /// 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, crate::widget::scroll::ScrollAxis )>, 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>, /// Widgets carrying a [`WidgetId`] that a scroll viewport clipped out /// of `widget_rects` this frame: `(id, scroll flat_idx, rect)` with the /// rect in the scroll's content coordinates, so a focus request can /// move that scroll until the widget is in view. pub scroll_clipped: Vec<( WidgetId, usize, Rect )>, /// 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>, /// Non-interactive widgets exposed only to the accessibility /// tree (text labels, images, separators, progress bars). pub accessible_extras: Vec, /// Depth counter for containers marked `a11y_live`. pub live_depth: u32, } /// Build the per-frame [`DrawCtx`] from the surface's [`FrameState`], moving the /// carried maps (cursor / selection / scroll) out of `frame` into the context. /// The interaction indices are passed by value (they live on `SurfaceState`, not /// `frame`). `debug_layout` is `false` on the partial paths. Shared by all four /// frame paths; [`commit_draw_ctx`] writes the produced state back. pub( crate ) fn build_draw_ctx( frame: &mut crate::event_loop::FrameState, focused_idx: Option, hovered_idx: Option, pressed_idx: Option, debug_layout: bool, ) -> DrawCtx { DrawCtx { focused_idx, hovered_idx, pressed_idx, cursor_state: std::mem::take( &mut frame.cursor_state ), selection_anchor: std::mem::take( &mut frame.selection_anchor ), widget_rects: Vec::new(), debug_layout, scroll_offsets: std::mem::take( &mut frame.scroll_offsets ), scroll_rects: Vec::new(), scroll_canvases: std::mem::take( &mut frame.scroll_canvases ), scroll_navigable_items: std::mem::take( &mut frame.scroll_navigable_items ), scroll_clipped: Vec::new(), previous_widget_rects: frame.widget_rects.clone(), accessible_extras: Vec::new(), live_depth: 0, } } /// Write the finished frame's [`DrawCtx`] back into the [`FrameState`]: snapshot /// the interaction indices into `prev_*` and move the produced rect lists and /// carried maps back. Shared by all four frame paths; the caller must have /// already consumed anything it needs from `ctx` (e.g. `compute_damage`) before /// calling this, and resets `content_dirty` itself (it lives on `SurfaceState`). pub( crate ) fn commit_draw_ctx( frame: &mut crate::event_loop::FrameState, focused_idx: Option, hovered_idx: Option, pressed_idx: Option, mut ctx: DrawCtx, ) { frame.prev_focused = focused_idx; frame.prev_hovered = hovered_idx; frame.prev_pressed = pressed_idx; frame.widget_rects = ctx.widget_rects; frame.scroll_rects = ctx.scroll_rects; frame.scroll_canvases = std::mem::take( &mut ctx.scroll_canvases ); frame.cursor_state = ctx.cursor_state; frame.selection_anchor = ctx.selection_anchor; frame.scroll_offsets = ctx.scroll_offsets; frame.accessible_extras = ctx.accessible_extras; frame.scroll_navigable_items = ctx.scroll_navigable_items; frame.scroll_clipped = ctx.scroll_clipped; } /// 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 ); } }