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