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
// 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 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;
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<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 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>,
/// 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 )>>,
/// 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,
}
/// 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 );
}
}