ltk/
core.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
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
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>

//! Runtime-free UI surface primitives.
//!
//! This module exposes the part of ltk that is useful outside `ltk::run`:
//! widget tree layout, drawing into a `Canvas`, hit-test rect snapshots, and
//! damage tracking. It deliberately contains no Wayland client event loop,
//! layer-shell, xdg-shell, SHM pool, or frame-callback logic.

use std::collections::HashMap;
use std::ffi::c_void;
use std::sync::Arc;

pub use crate::gles_render::{ BorrowedGlesTexture, GlesVersion };
pub use crate::render::Canvas;
pub use crate::widget::{ LaidOutWidget, WidgetHandlers };

use crate::draw::{ self, DrawCtx };
use crate::egl_context::EglOffscreenContext;
use crate::types::{ Color, Point, Rect };
use crate::widget::Element;

/// Options for one runtime-free render pass.
#[ derive( Debug, Clone, Copy ) ]
pub struct RenderOptions
{
	/// Physical-pixel bounds to lay out the tree into.
	pub bounds:       Rect,
	/// Background fill. Use [`Color::TRANSPARENT`] for a transparent target.
	pub background:   Color,
	/// Draw red debug rectangles around laid-out interactive widgets.
	pub debug_layout: bool,
}

impl RenderOptions
{
	/// Render into the full `width × height` canvas with a transparent
	/// background and debug layout disabled.
	pub fn full_canvas( width: u32, height: u32 ) -> Self
	{
		Self
		{
			bounds:       Rect { x: 0.0, y: 0.0, width: width as f32, height: height as f32 },
			background:   Color::TRANSPARENT,
			debug_layout: false,
		}
	}

	/// Set the background fill for the render pass.
	pub fn background( mut self, color: Color ) -> Self
	{
		self.background = color;
		self
	}

	/// Enable or disable debug layout rectangles.
	pub fn debug_layout( mut self, yes: bool ) -> Self
	{
		self.debug_layout = yes;
		self
	}
}

/// Result of rendering a tree into a [`UiSurface`].
#[ derive( Debug, Clone ) ]
pub struct RenderOutput
{
	/// Damage rects in physical pixels. Empty means "damage the full bounds".
	pub damage_rects: Vec<Rect>,
	/// True when the caller should treat the whole render bounds as dirty.
	pub full_redraw:  bool,
}

/// A retained rendering target for code that wants ltk widgets without
/// `ltk::run`.
///
/// A compositor can keep one `UiSurface` per decoration or panel, mutate
/// focus/hover/pressed state from its own input routing, then call
/// [`Self::render`] whenever it decides a repaint is needed.
// Field declaration order is load-bearing: Rust drops fields top-to-bottom,
// so `canvas` is dropped before `egl_context`. That matters because
// `Canvas::Gles::Drop` releases textures / FBOs / shader programs through
// glow, which requires the matching GL context to be current — which is what
// `Drop for UiSurface` ensures by calling `make_owned_gles_current()` first.
// If `egl_context` were declared above `canvas`, the EGL context would be
// torn down first and `canvas`'s GL releases would silently leak / corrupt
// state. Do not reorder these two fields without preserving that property.
pub struct UiSurface<Msg: Clone>
{
	canvas:          Canvas,
	egl_context:     Option<EglOffscreenContext>,
	focused_idx:     Option<usize>,
	hovered_idx:     Option<usize>,
	pressed_idx:     Option<usize>,
	prev_focused:    Option<usize>,
	prev_hovered:    Option<usize>,
	prev_pressed:    Option<usize>,
	widget_rects:    Vec<LaidOutWidget<Msg>>,
	cursor_state:    HashMap<usize, usize>,
	selection_anchor: HashMap<usize, usize>,
	scroll_offsets:  HashMap<usize, ( f32, f32 )>,
	scroll_canvases: HashMap<usize, Canvas>,
	scroll_navigable_items: HashMap<usize, Vec<( usize, f32, f32 )>>,
	content_dirty:   bool,
}

impl<Msg: Clone> UiSurface<Msg>
{
	/// Create a software-backed surface of the given physical size.
	///
	/// This is intentionally conservative for compositor integrations: an
	/// embedder typically already owns an EGL/GLES context, so `new` must
	/// not allocate a hidden offscreen context per decoration. Use
	/// [`Self::from_gles_context`] when the caller can provide an
	/// already-current GL context.
	pub fn new( width: u32, height: u32 ) -> Self
	{
		Self::new_software( width, height )
	}

	/// Create a software-backed surface explicitly.
	pub fn new_software( width: u32, height: u32 ) -> Self
	{
		Self::from_canvas( Canvas::new( width, height ) )
	}

	/// Create a GLES-backed surface with an ltk-owned offscreen EGL context.
	///
	/// The context is made current before render/resize operations. This is
	/// useful for runtime-free rendering; compositors that already own a GL
	/// context can instead build a `Canvas::new_gles(...)` and pass it to
	/// [`Self::from_canvas`].
	pub fn try_new_gles( width: u32, height: u32 ) -> Result<Self, String>
	{
		let egl_context = EglOffscreenContext::new()?;
		egl_context.make_current()?;
		let canvas = Canvas::new_gles(
			Arc::clone( egl_context.gl() ),
			egl_context.version(),
			width,
			height,
		);
		Ok( Self::from_canvas_with_egl_context( canvas, Some( egl_context ) ) )
	}

	/// Create a GLES-backed surface from a caller-owned, already-current GL
	/// context.
	///
	/// This is the path intended for compositor embedders: no EGL display,
	/// EGL context, or pbuffer is allocated by ltk. The caller must keep
	/// the underlying GL context alive and current whenever this surface
	/// is created, rendered, resized, or dropped.
	pub fn from_gles_context(
		gl:      Arc<glow::Context>,
		version: GlesVersion,
		width:   u32,
		height:  u32,
	) -> Self
	{
		Self::from_canvas( Canvas::new_gles( gl, version, width, height ) )
	}

	/// Create a GLES-backed surface from the GL function loader of the current
	/// context.
	///
	/// This is useful for renderers such as Smithay's `GlesRenderer`, which
	/// keep their own EGL context and expose custom GL access through a
	/// callback. This constructor does not allocate an EGL context; it only
	/// builds ltk's `glow` dispatch table and GPU canvas resources in the
	/// context that is current while this function runs.
	///
	/// # Safety
	///
	/// `loader` must resolve symbols for the GL context that is current on this
	/// thread. That same context must remain alive, and must be made current
	/// before any call that touches the returned surface — **including the
	/// implicit destructor**: dropping the `UiSurface` releases GPU resources
	/// (textures, FBOs, programs) through the caller-owned context and will
	/// leak / corrupt state if that context is not current at drop time.
	pub unsafe fn from_current_gles_loader<F>(
		mut loader: F,
		version:    GlesVersion,
		width:      u32,
		height:     u32,
	) -> Self
	where
		F: FnMut( &str ) -> *const c_void,
	{
		// SAFETY: forwarded from `from_current_gles_loader`'s own `# Safety`
		// contract — `loader` resolves symbols for a context current on this
		// thread, so `glGetString( GL_VERSION )` (called inside
		// `from_loader_function`) is well-defined.
		let gl = Arc::new( unsafe { glow::Context::from_loader_function( move |name| loader( name ) ) } );
		Self::from_gles_context( gl, version, width, height )
	}

	/// Wrap an existing canvas. This is the hook for compositor-owned GPU
	/// targets once the caller provides an already-current GL context. If
	/// `canvas` is `Canvas::Gles`, the caller remains responsible for making the
	/// matching GL context current before calling methods that touch the
	/// canvas — **including the implicit destructor**: dropping the
	/// `UiSurface` releases GPU resources (textures, FBOs, programs) through
	/// the caller-owned context and will leak / corrupt state if that context
	/// is not current at drop time.
	pub fn from_canvas( canvas: Canvas ) -> Self
	{
		Self::from_canvas_with_egl_context( canvas, None )
	}

	fn from_canvas_with_egl_context(
		canvas:      Canvas,
		egl_context: Option<EglOffscreenContext>,
	) -> Self
	{
		Self
		{
			canvas,
			egl_context,
			focused_idx:     None,
			hovered_idx:     None,
			pressed_idx:     None,
			prev_focused:    None,
			prev_hovered:    None,
			prev_pressed:    None,
			widget_rects:    Vec::new(),
			cursor_state:    HashMap::new(),
			selection_anchor: HashMap::new(),
			scroll_offsets:  HashMap::new(),
			scroll_canvases: HashMap::new(),
			scroll_navigable_items: HashMap::new(),
			content_dirty:   true,
		}
	}

	/// Access the backing canvas after a render pass.
	pub fn canvas( &self ) -> &Canvas
	{
		&self.canvas
	}

	/// Mutable access to the backing canvas for compositor-specific upload or
	/// presentation code. Mark content dirty afterwards if external drawing
	/// changes what ltk should preserve across partial redraws.
	pub fn canvas_mut( &mut self ) -> &mut Canvas
	{
		self.make_owned_gles_current();
		&mut self.canvas
	}

	/// Current canvas size in physical pixels.
	pub fn size( &self ) -> ( u32, u32 )
	{
		self.canvas.size()
	}

	/// Resize the backing canvas and force the next render to redraw fully.
	pub fn resize( &mut self, width: u32, height: u32 )
	{
		self.make_owned_gles_current();
		self.canvas.resize( width, height );
		self.mark_content_dirty();
	}

	/// Set the DPI scale used for text and font metrics.
	pub fn set_dpi_scale( &mut self, scale: f32 )
	{
		self.make_owned_gles_current();
		self.canvas.set_dpi_scale( scale );
		self.mark_content_dirty();
	}

	/// Mark the next render as content-changing, forcing a full repaint.
	pub fn mark_content_dirty( &mut self )
	{
		self.content_dirty = true;
	}

	/// Current laid-out interactive widgets from the last render.
	pub fn widget_rects( &self ) -> &[ LaidOutWidget<Msg> ]
	{
		&self.widget_rects
	}

	/// Hit-test a physical point against the last rendered widget rects.
	pub fn hit_test( &self, pos: Point ) -> Option<usize>
	{
		crate::tree::find_widget_at( &self.widget_rects, pos )
	}

	/// Lookup a laid-out widget by flat index.
	pub fn widget( &self, flat_idx: usize ) -> Option<&LaidOutWidget<Msg>>
	{
		crate::tree::find_widget( &self.widget_rects, flat_idx )
	}

	/// Lookup the handler snapshot for a laid-out widget.
	pub fn handlers( &self, flat_idx: usize ) -> Option<&WidgetHandlers<Msg>>
	{
		crate::tree::find_handlers( &self.widget_rects, flat_idx )
	}

	/// Update keyboard focus state. This is interaction-only, so the next
	/// render can use partial damage when layout/content did not change.
	pub fn set_focused( &mut self, idx: Option<usize> )
	{
		self.focused_idx = idx;
	}

	/// Update hover state. This is interaction-only.
	pub fn set_hovered( &mut self, idx: Option<usize> )
	{
		self.hovered_idx = idx;
	}

	/// Update pressed state. This is interaction-only.
	pub fn set_pressed( &mut self, idx: Option<usize> )
	{
		self.pressed_idx = idx;
	}

	pub fn focused( &self ) -> Option<usize> { self.focused_idx }
	pub fn hovered( &self ) -> Option<usize> { self.hovered_idx }
	pub fn pressed( &self ) -> Option<usize> { self.pressed_idx }

	/// Render `element` into the backing canvas.
	///
	/// This does not commit, swap buffers, request frame callbacks, or talk to
	/// Wayland. The caller owns presentation and frame pacing.
	pub fn render( &mut self, element: &Element<Msg>, options: RenderOptions ) -> RenderOutput
	{
		self.make_owned_gles_current();
		let ( width, height ) = self.canvas.size();

		let damage_rects = if self.content_dirty || self.widget_rects.is_empty()
		{
			Vec::new()
		}
		else
		{
			draw::compute_interaction_dirty_rects(
				&self.widget_rects,
				self.prev_focused, self.prev_hovered, self.prev_pressed,
				self.focused_idx,  self.hovered_idx,  self.pressed_idx,
				width,
				height,
			)
		};

		let use_partial = !self.content_dirty && !damage_rects.is_empty();
		// Only `compute_damage` (called below in the !use_partial branch)
		// needs the previous frame's rects, and it consumes them by reference.
		// The clone is therefore deferred to that branch — the partial-damage
		// path (typical for hover / focus / press transitions) skips it
		// entirely, saving one Vec<LaidOutWidget> clone per frame.
		let old_rects: Vec<LaidOutWidget<Msg>> = if use_partial
		{
			Vec::new()
		}
		else
		{
			self.widget_rects.clone()
		};
		if use_partial
		{
			self.canvas.set_clip_rects( &damage_rects );
			if options.background.a > 0.0
			{
				self.canvas.fill( options.background );
			}
			else
			{
				self.canvas.clear_rects_transparent( &damage_rects );
			}
		}
		else
		{
			self.canvas.clear_clip();
			if options.background.a > 0.0
			{
				self.canvas.fill( options.background );
			}
			else
			{
				self.canvas.clear();
			}
		}

		let mut ctx: DrawCtx<Msg> = DrawCtx
		{
			focused_idx:     self.focused_idx,
			hovered_idx:     self.hovered_idx,
			pressed_idx:     self.pressed_idx,
			cursor_state:    std::mem::take( &mut self.cursor_state ),
			selection_anchor: std::mem::take( &mut self.selection_anchor ),
			widget_rects:    Vec::new(),
			debug_layout:    options.debug_layout && !use_partial,
			scroll_offsets:  std::mem::take( &mut self.scroll_offsets ),
			scroll_rects:    Vec::new(),
			scroll_canvases: std::mem::take( &mut self.scroll_canvases ),
			scroll_navigable_items: std::mem::take( &mut self.scroll_navigable_items ),
			previous_widget_rects: self.widget_rects.clone(),
			accessible_extras: Vec::new(),
			live_depth: 0,
		};

		draw::layout_and_draw( element, &mut self.canvas, options.bounds, &mut ctx, 0 );

		if ctx.debug_layout && !use_partial
		{
			for w in &ctx.widget_rects
			{
				self.canvas.stroke_rect( w.rect, Color::rgb( 1.0, 0.0, 0.0 ), 1.5, 0.0 );
			}
		}
		self.canvas.clear_clip();

		let output_damage = if use_partial
		{
			damage_rects
		}
		else
		{
			draw::compute_damage(
				&old_rects,
				&ctx.widget_rects,
				self.prev_focused, self.prev_hovered, self.prev_pressed,
				self.focused_idx,  self.hovered_idx,  self.pressed_idx,
				width,
				height,
			)
		};

		self.prev_focused    = self.focused_idx;
		self.prev_hovered    = self.hovered_idx;
		self.prev_pressed    = self.pressed_idx;
		self.widget_rects    = ctx.widget_rects;
		self.cursor_state    = ctx.cursor_state;
		self.selection_anchor = ctx.selection_anchor;
		self.scroll_offsets  = ctx.scroll_offsets;
		// `ctx.scroll_rects` is intentionally dropped here. The `Scroll` widget
		// pushes its hit-test rects into the DrawCtx during the draw pass, but
		// `UiSurface` exposes no wheel-event routing API, so persisting them
		// across frames serves no consumer. If a future embedder needs to
		// dispatch wheel events through `UiSurface`, add a `scroll_rects()`
		// getter together with a `scroll_by( flat_idx, dy )` mutator and start
		// keeping the field again.
		self.scroll_canvases = ctx.scroll_canvases;
		self.scroll_navigable_items = ctx.scroll_navigable_items;
		self.content_dirty   = false;

		RenderOutput
		{
			full_redraw:  output_damage.is_empty(),
			damage_rects: output_damage,
		}
	}

	fn make_owned_gles_current( &self )
	{
		if let Some( egl_context ) = &self.egl_context
		{
			if let Err( e ) = egl_context.make_current()
			{
				log_make_current_failure_once( &e );
			}
		}
	}
}

fn log_make_current_failure_once( reason: &str )
{
	use std::sync::Once;
	static ONCE: Once = Once::new();
	ONCE.call_once( ||
	{
		eprintln!( "[ltk] core: eglMakeCurrent failed: {reason} (subsequent GL ops will silently no-op or render garbage)" );
	} );
}

impl<Msg: Clone> Drop for UiSurface<Msg>
{
	fn drop( &mut self )
	{
		self.make_owned_gles_current();
	}
}