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
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
//! Overlay window that shows the most recent input events.
//!
//! Designed for hardware bring-up: renders a dark rounded-rect panel
//! with a scrolling list of event descriptions. Appears on any input
//! and auto-hides after all entries expire.
use alloc::string::String;
use alloc::vec::Vec;
use core::cell::Cell;
use rlvgl_core::bitmap_font::BitmapFont;
use rlvgl_core::event::Event;
use rlvgl_core::renderer::Renderer;
use rlvgl_core::widget::{Color, Rect, Widget};
use crate::draw_helpers::{draw_rounded_border, fill_rounded_rect};
/// Default expiry if none specified (10 s at 6 Hz — legacy).
const DEFAULT_EXPIRE_TICKS: u32 = 60;
/// Maximum visible lines in the window.
const MAX_LINES: usize = 10;
/// Frames to keep clearing after hiding (double-buffer + 1 margin).
const CLEAR_FRAMES: u8 = 3;
/// A single event log entry.
struct EventEntry {
text: String,
age: u32,
}
/// Themed overlay that displays recent input events.
pub struct EventWindow {
bounds: Rect,
bg_color: Color,
border_color: Color,
border_width: u8,
radius: u8,
text_color: Color,
entries: Vec<EventEntry>,
visible: bool,
/// When `false`, `push_event` is a no-op (events are silently dropped).
enabled: bool,
/// Counts down after hiding to clear stale pixels from both framebuffers.
clear_countdown: u8,
padding: i32,
font: &'static BitmapFont,
/// Ticks before an entry expires (frame-rate dependent).
expire_ticks: u32,
/// Number of text lines rendered during the most recent draw pass.
last_draw_lines: Cell<u8>,
/// Monotonic draw sequence number for telemetry.
draw_seq: Cell<u32>,
/// When true, `handle_event(Tick)` is a no-op — entries don't age or
/// expire. Used during multi-frame dirty renders to ensure both
/// double-buffer frames show identical content.
frozen: bool,
/// When true, `draw()` is a no-op — the DMA2D overlay pipeline
/// handles rendering externally.
dma2d_mode: bool,
}
impl EventWindow {
/// Whether the window is currently visible.
pub fn is_visible(&self) -> bool {
self.visible
}
/// Number of entries currently in the list.
pub fn entry_count(&self) -> usize {
self.entries.len()
}
/// Whether event collection is enabled.
pub fn is_enabled(&self) -> bool {
self.enabled
}
/// Enable or disable event collection. When disabled, `push_event` is a no-op.
pub fn set_enabled(&mut self, val: bool) {
self.enabled = val;
}
/// Packed event-window diagnostic state.
pub fn diag_state(&self) -> u32 {
((self.last_draw_lines.get() as u32) << 24)
| ((self.clear_countdown as u32) << 16)
| ((self.entries.len().min(0xFF) as u32) << 8)
| ((self.visible as u32) << 1)
| (self.enabled as u32)
}
/// Monotonic draw sequence number.
pub fn draw_seq(&self) -> u32 {
self.draw_seq.get()
}
/// Freeze event aging. While frozen, `handle_event(Tick)` is a no-op
/// so entries don't age or expire during multi-frame dirty renders.
pub fn set_frozen(&mut self, val: bool) {
self.frozen = val;
}
/// Whether event aging is currently frozen.
pub fn is_frozen(&self) -> bool {
self.frozen
}
/// Enable DMA2D rendering mode. When true, `draw()` becomes a no-op
/// because the DMA2D overlay pipeline handles rendering externally.
pub fn set_dma2d_mode(&mut self, val: bool) {
self.dma2d_mode = val;
}
/// Whether DMA2D rendering mode is active.
pub fn is_dma2d_mode(&self) -> bool {
self.dma2d_mode
}
/// Iterate visible entries, calling `f(line_index, text)` for each.
pub fn for_each_visible<F: FnMut(usize, &str)>(&self, mut f: F) {
let max_lines = MAX_LINES.min(self.entries.len());
let start = self.entries.len().saturating_sub(MAX_LINES);
for (i, entry) in self.entries[start..].iter().enumerate() {
if i >= max_lines {
break;
}
f(i, &entry.text);
}
}
/// Reference to the font used for text rendering.
pub fn font(&self) -> &'static BitmapFont {
self.font
}
/// Inner padding in pixels.
pub fn padding(&self) -> i32 {
self.padding
}
/// Line height: font scaled_height + gap.
pub fn line_height(&self) -> i32 {
self.font.scaled_height() + 4
}
/// Push a pre-formatted event string into the display list.
pub fn push_event(&mut self, text: String) {
if !self.enabled {
return;
}
self.entries.push(EventEntry { text, age: 0 });
// Cap total entries to prevent unbounded growth.
if self.entries.len() > MAX_LINES * 2 {
self.entries.remove(0);
}
self.visible = true;
}
}
impl Widget for EventWindow {
fn bounds(&self) -> Rect {
self.bounds
}
fn draw(&self, renderer: &mut dyn Renderer) {
if !self.visible || self.dma2d_mode {
return;
}
// Background + border
fill_rounded_rect(renderer, self.bounds, self.bg_color, self.radius);
draw_rounded_border(
renderer,
self.bounds,
self.border_color,
self.border_width,
self.radius,
);
// Text entries stacked vertically
let line_h = self.font.scaled_height() + 4;
let max_lines = MAX_LINES.min(self.entries.len());
let start = self.entries.len().saturating_sub(MAX_LINES);
let inner_x = self.bounds.x + self.padding;
let inner_y = self.bounds.y + self.padding;
self.last_draw_lines.set(max_lines as u8);
self.draw_seq.set(self.draw_seq.get().wrapping_add(1));
for (i, entry) in self.entries[start..].iter().enumerate() {
if i >= max_lines {
break;
}
let y = inner_y + i as i32 * line_h;
self.font
.draw_str(renderer, inner_x, y, &entry.text, self.text_color);
}
}
fn handle_event(&mut self, event: &Event) -> bool {
if event == &Event::Tick {
// Skip aging while frozen (multi-frame dirty render in progress).
if self.frozen {
return false;
}
// Age all entries and remove expired ones.
for entry in &mut self.entries {
entry.age += 1;
}
self.entries.retain(|e| e.age < self.expire_ticks);
if self.entries.is_empty() && self.visible {
// Start clearing stale pixels from both framebuffers.
// The Compositor calls clear_region() to drive the countdown.
self.clear_countdown = CLEAR_FRAMES;
self.visible = false;
}
}
// Input events are pushed by the application via push_event()
// so it can label the source (joystick vs button vs touch).
false // never consume — let other widgets see the event too
}
fn clear_region(&mut self) -> Option<Rect> {
if self.clear_countdown > 0 && !self.visible {
self.clear_countdown -= 1;
Some(self.bounds)
} else {
None
}
}
}
/// Builder for [`EventWindow`] with the dark-overlay theme.
pub struct EventWindowBuilder {
window_w: i32,
window_h: i32,
pos_x: Option<i32>,
pos_y: Option<i32>,
bg_color: Color,
border_color: Color,
border_width: u8,
radius: u8,
text_color: Color,
font: &'static BitmapFont,
expire_ticks: u32,
}
impl EventWindowBuilder {
/// Create a builder with default dark-overlay theme values.
pub fn new(font: &'static BitmapFont) -> Self {
// Window sized to hold MAX_LINES of text at the font's scaled line height.
let line_h = font.scaled_height() + 4;
let padding = 12;
let window_h = MAX_LINES as i32 * line_h + padding * 2;
let window_w = 380;
Self {
window_w,
window_h,
pos_x: None,
pos_y: None,
bg_color: Color(25, 25, 25, 255),
border_color: Color(80, 80, 80, 255),
border_width: 2,
radius: 8,
text_color: Color(220, 220, 220, 255),
font,
expire_ticks: DEFAULT_EXPIRE_TICKS,
}
}
/// Set the number of ticks before entries expire.
///
/// For frame-rate-independent timing, pass `frame_hz * desired_seconds`.
pub fn expire_ticks(mut self, ticks: u32) -> Self {
self.expire_ticks = ticks;
self
}
/// Override the background color.
pub fn bg_color(mut self, c: Color) -> Self {
self.bg_color = c;
self
}
/// Override the border color.
pub fn border_color(mut self, c: Color) -> Self {
self.border_color = c;
self
}
/// Override the corner radius.
pub fn radius(mut self, r: u8) -> Self {
self.radius = r;
self
}
/// Override the window width.
pub fn width(mut self, w: i32) -> Self {
self.window_w = w;
self
}
/// Center the window on a screen of the given dimensions.
pub fn center(mut self, screen_w: i32, screen_h: i32) -> Self {
self.pos_x = Some((screen_w - self.window_w) / 2);
self.pos_y = Some((screen_h - self.window_h) / 2);
self
}
/// Consume the builder and produce an [`EventWindow`].
pub fn build(self) -> EventWindow {
let margin = 10;
EventWindow {
bounds: Rect {
x: self.pos_x.unwrap_or(margin),
y: self.pos_y.unwrap_or(margin),
width: self.window_w,
height: self.window_h,
},
bg_color: self.bg_color,
border_color: self.border_color,
border_width: self.border_width,
radius: self.radius,
text_color: self.text_color,
entries: Vec::new(),
visible: false,
enabled: true,
clear_countdown: 0,
padding: 12,
font: self.font,
expire_ticks: self.expire_ticks,
last_draw_lines: Cell::new(0),
draw_seq: Cell::new(0),
frozen: false,
dma2d_mode: false,
}
}
}