ltk/draw/mod.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
//! 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 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.
//!
//! Painting is the same for the software canvas (a CPU pixmap) and the GLES one (an FBO). The backend prepares the canvas before and presents it after, through [`crate::platform::Platform`]; routing lives in [`crate::event_loop::frame`].
//!
//! # Submodule layout
//!
//! * [`paint`] — `fit_canvas`, `paint_full` / `paint_partial`
//! * [`damage`] — `compute_interaction_dirty_rects`, `compute_damage`,
//! `clamp_rect_to`
//! * [`chrome`] — `draw_titlebar`, `draw_fallback_banner`
//! * [`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;
#[ cfg( backend ) ]
pub( crate ) mod paint;
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;
#[ cfg( backend ) ]
pub( crate ) use paint::{ fit_canvas, paint_full, paint_partial };
#[ cfg( any( feature = "wayland", feature = "web" ) ) ]
pub( crate ) use paint::paint_detached;
/// 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<Msg: Clone>
{
pub focused_idx: Option<usize>,
pub hovered_idx: Option<usize>,
pub pressed_idx: Option<usize>,
pub cursor_state: HashMap<usize, usize>,
pub selection_anchor: HashMap<usize, usize>,
pub text_scroll: HashMap<usize, crate::widget::text_edit::TextScroll>,
pub widget_rects: Vec<LaidOutWidget<Msg>>,
pub debug_layout: bool,
pub scroll_offsets: HashMap<usize, ( f32, f32 )>,
pub scroll_rects: Vec<( Rect, usize, crate::widget::scroll::ScrollAxis )>,
pub scroll_canvases: HashMap<usize, Canvas>,
/// Sub-canvases of `Viewport` elements, keyed by their order of
/// appearance in the walk (a viewport owns no flat index of its own).
pub viewport_canvases: HashMap<usize, Canvas>,
pub viewport_seq: usize,
/// 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<usize, Vec<( usize, f32, f32 )>>,
/// 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<LaidOutWidget<Msg>>,
/// Non-interactive widgets exposed only to the accessibility
/// tree (text labels, images, separators, progress bars).
pub accessible_extras: Vec<crate::a11y::tree::AccessibleExtra>,
/// Depth counter for containers marked `a11y_live`.
pub live_depth: u32,
/// Where the canvas being drawn on sits on the surface: a scroll or a viewport draws its child on a canvas of its own.
pub origin: ( f32, f32 ),
/// `( rect, radius )` of every `blur_behind` container, in surface coordinates.
pub blur_regions: Vec<( Rect, f32 )>,
}
/// 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 paint. Shared by the full
/// and partial paints; [`commit_draw_ctx`] writes the produced state back.
#[ cfg( backend ) ]
pub( crate ) fn build_draw_ctx<Msg: Clone>(
frame: &mut crate::event_loop::FrameState<Msg>,
focused_idx: Option<usize>,
hovered_idx: Option<usize>,
pressed_idx: Option<usize>,
debug_layout: bool,
) -> DrawCtx<Msg>
{
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 ),
text_scroll: std::mem::take( &mut frame.text_scroll ),
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 ),
viewport_canvases: std::mem::take( &mut frame.viewport_canvases ),
viewport_seq: 0,
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,
origin: ( 0.0, 0.0 ),
blur_regions: Vec::new(),
}
}
/// 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 the full and partial paints; 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`).
#[ cfg( backend ) ]
pub( crate ) fn commit_draw_ctx<Msg: Clone>(
frame: &mut crate::event_loop::FrameState<Msg>,
focused_idx: Option<usize>,
hovered_idx: Option<usize>,
pressed_idx: Option<usize>,
mut ctx: DrawCtx<Msg>,
)
{
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.viewport_canvases = std::mem::take( &mut ctx.viewport_canvases );
frame.cursor_state = ctx.cursor_state;
frame.selection_anchor = ctx.selection_anchor;
frame.text_scroll = ctx.text_scroll;
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;
if ctx.blur_regions != frame.blur_regions
{
frame.blur_regions = ctx.blur_regions;
frame.blur_changed = true;
}
}
/// Paint the built-in Copy / Cut / Paste context menu on top of the
/// finished surface content. Called from [`paint`] after the widget
/// tree so the menu sits above everything
/// the widget tree painted, matching the convention every other
/// toolkit follows for runtime-internal popups.
#[ cfg( backend ) ]
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 );
}
}