ltk/draw/
layout.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
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>

//! Element-tree recursive walker.
//!
//! [`layout_and_draw`] is the single bottom of the draw pipeline:
//! given an [`Element`] + its allocated rect, it lays out children,
//! paints leaves, threads scroll sub-canvases, and records the
//! [`LaidOutWidget`] entries the input layer will hit-test against.
//! Both software and GPU paths funnel every widget through this one
//! function.

use crate::render::Canvas;
use crate::types::Rect;
use crate::widget::{ Element, LaidOutWidget, WidgetHandlers };

use super::DrawCtx;
use super::damage::clamp_rect_to;

pub( crate ) fn layout_and_draw<Msg: Clone>(
	element:  &Element<Msg>,
	canvas:   &mut Canvas,
	rect:     Rect,
	ctx:      &mut DrawCtx<Msg>,
	flat_idx: usize,
) -> usize
{
	match element
	{
		Element::Column( col ) =>
		{
			let child_rects = col.layout( rect, canvas );
			let mut idx = flat_idx;
			for ( child_rect, child_i ) in child_rects
			{
				idx = layout_and_draw::<Msg>( &col.children[child_i], canvas, child_rect, ctx, idx );
			}
			idx
		}
		Element::Row( r ) =>
		{
			let child_rects = r.layout( rect, canvas );
			let mut idx = flat_idx;
			for ( child_rect, child_i ) in child_rects
			{
				idx = layout_and_draw::<Msg>( &r.children[child_i], canvas, child_rect, ctx, idx );
			}
			idx
		}
		Element::Stack( s ) =>
		{
			let child_rects = s.layout( rect, canvas );
			let mut idx = flat_idx;
			for ( child_rect, child_i ) in child_rects
			{
				idx = layout_and_draw::<Msg>( &s.children[ child_i ].0, canvas, child_rect, ctx, idx );
			}
			idx
		}
		Element::WrapGrid( g ) =>
		{
			let child_rects = g.layout( rect, canvas );
			let mut idx = flat_idx;
			for ( child_rect, child_i ) in child_rects
			{
				idx = layout_and_draw::<Msg>( &g.children[child_i], canvas, child_rect, ctx, idx );
			}
			idx
		}
		Element::Carousel( car ) =>
		{
			let child_rects = car.layout( rect, canvas );
			let mut idx = flat_idx;
			for ( child_rect, child_i ) in child_rects
			{
				idx = layout_and_draw::<Msg>( &car.children[child_i], canvas, child_rect, ctx, idx );
			}
			idx
		}
		Element::Flex( f ) =>
		{
			// `Flex` is invisible chrome: it claimed leftover width up at
			// `Row::layout`, here we just unwrap it and draw the child
			// inside the allocated rect. No flat-index of its own.
			layout_and_draw::<Msg>( f.child.as_ref(), canvas, rect, ctx, flat_idx )
		}
		Element::AnchoredOverlay( a ) =>
		{
			// Look up the anchor's rect from the previous frame's
			// `widget_rects` snapshot. If found, place the child flush
			// below the anchor at the child's intrinsic size; if not,
			// fall back to the parent-supplied rect so the child still
			// renders (modal-style, typically a one-frame artefact on
			// the first frame after open).
			let anchor_rect = ctx.previous_widget_rects.iter()
				.find( |w| w.id == Some( a.anchor_id ) )
				.map( |w| w.rect );
			let target = match anchor_rect
			{
				Some( anchor ) =>
				{
					// Use the child's intrinsic preferred size so the
					// popup keeps its design width / height regardless
					// of how wide the trigger pill happens to be.
					let ( w, h ) = a.child.preferred_size( rect.width, canvas );
					Rect
					{
						x:      anchor.x,
						y:      anchor.y + anchor.height + a.gap,
						width:  w,
						height: h,
					}
				}
				None => rect,
			};
			layout_and_draw::<Msg>( a.child.as_ref(), canvas, target, ctx, flat_idx )
		}
		Element::Pressable( p ) =>
		{
			// Push the wrapper's hit rect *before* recursing so that any
			// interactive child pushed during recursion sits later in
			// `widget_rects` and wins under `iter().rev()` hit testing.
			let my_idx = flat_idx;
			if p.has_handler()
			{
				ctx.widget_rects.push( LaidOutWidget
				{
					rect,
					flat_idx:           my_idx,
					id:                 p.id,
					paint_rect:         rect,
					handlers:           WidgetHandlers::Button
					{
						on_press:      p.on_press.clone(),
						on_long_press: p.on_long_press.clone(),
						on_drag_start: p.on_drag_start.clone(),
						on_escape:     p.on_escape.clone(),
						repeating:     false,
					},
					keyboard_focusable: false,
					cursor:             p.cursor.unwrap_or( crate::types::CursorShape::Pointer ),
					tooltip:            None,
					accessible_label:   None,
					is_live_region:     ctx.live_depth > 0,
				} );
			}
			layout_and_draw::<Msg>( p.child.as_ref(), canvas, rect, ctx, my_idx + 1 )
		}
		Element::Container( c ) =>
		{
			let saved_alpha = canvas.global_alpha();
			canvas.set_global_alpha( saved_alpha * c.opacity );
			let live_inc = if c.a11y_live { 1 } else { 0 };
			ctx.live_depth += live_inc;

			let rect = if let Some( mw ) = c.max_width
			{
				crate::types::Rect { width: rect.width.min( mw ), ..rect }
			} else {
				rect
			};

			// Surface slot takes precedence over flat background; falls
			// through to `c.background` when the slot is absent (third-
			// party theme without the named surface — content still
			// renders, just without the themed chrome).
			let painted = match c.surface.as_deref()
			{
				Some( slot ) => match crate::theme::resolve_surface( slot )
				{
					Some( ( surf, outer ) ) =>
					{
						canvas.fill_surface
						(
							rect,
							&surf.fill,
							&outer,
							&surf.inset_shadows,
							c.corners,
						);
						true
					}
					None => false,
				},
				None => false,
			};
			if !painted
			{
				if let Some( ref bg ) = c.background
				{
					canvas.fill_paint_rect( rect, bg, c.corners );
				}
			}
			if let Some( ( color, width ) ) = c.border
			{
				canvas.stroke_rect( rect, color, width, c.corners );
			}
			let vp = canvas.viewport_layout();
			let em = crate::types::Length::EM_BASE_DEFAULT;
			let pad_l = c.pad_left.resolve(   vp, em );
			let pad_r = c.pad_right.resolve(  vp, em );
			let pad_t = c.pad_top.resolve(    vp, em );
			let pad_b = c.pad_bottom.resolve( vp, em );
			let inner = crate::types::Rect
			{
				x:      rect.x + pad_l,
				y:      rect.y + pad_t,
				width:  ( rect.width  - pad_l - pad_r ).max( 0.0 ),
				height: ( rect.height - pad_t - pad_b ).max( 0.0 ),
			};
			let result = layout_and_draw::<Msg>( c.child.as_ref(), canvas, inner, ctx, flat_idx );

			ctx.live_depth -= live_inc;
			canvas.set_global_alpha( saved_alpha );
			result
		}
		Element::Scroll( s ) =>
		{
			let my_idx     = flat_idx;
			let axis       = s.axis;
			let ( ox_raw, oy_raw ) = ctx.scroll_offsets.get( &my_idx ).copied().unwrap_or( ( 0.0, 0.0 ) );
			// Width-aware preferred size: when the axis allows horizontal
			// overflow we let the child report its natural width by
			// asking with `f32::INFINITY`; otherwise the child stays
			// clamped to the viewport so wraps still happen.
			let child_w_max = if axis.allows_x() { f32::INFINITY } else { rect.width };
			let ( child_w, child_h ) = s.child.preferred_size( child_w_max, canvas );
			let ox = if axis.allows_x() { crate::widget::scroll::clamp_offset( ox_raw, child_w, rect.width  ) } else { 0.0 };
			let oy = if axis.allows_y() { crate::widget::scroll::clamp_offset( oy_raw, child_h, rect.height ) } else { 0.0 };
			// Write the clamped offsets back so input handlers (wheel /
			// drag) cannot accumulate past the content extents. Without
			// this, repeated wheel ticks past the edge keep growing
			// `*_raw` and the user has to "undo" that excess before the
			// content scrolls back. The clamp is idempotent for in-range
			// values, so the only effect is collapsing out-of-range
			// entries to the legitimate maximum.
			if ox != ox_raw || oy != oy_raw
			{
				ctx.scroll_offsets.insert( my_idx, ( ox, oy ) );
			}

			// Reuse the sub-canvas from the previous frame if its size matches;
			// only reallocate when the viewport is resized.
			let sw  = (rect.width.ceil()  as u32).max( 1 );
			let sh  = (rect.height.ceil() as u32).max( 1 );
			let mut sub = ctx.scroll_canvases.remove( &my_idx )
				.filter( |c| c.size() == ( sw, sh ) )
				.unwrap_or_else( || canvas.sub_canvas( sw, sh ) );
			sub.clear();

			// Child content fills at least the full viewport on each
			// allowed axis so that children with `Bottom`/`Right`
			// alignment are positioned correctly when the content is
			// shorter than the viewport along that axis.
			let effective_w = if axis.allows_x() { child_w.max( rect.width  ) } else { rect.width  };
			let effective_h = if axis.allows_y() { child_h.max( rect.height ) } else { rect.height };
			// Shift child by `(-ox, -oy)` so scrolled content appears at
			// `(0, 0)` of the sub-canvas.
			let child_rect = Rect { x: -ox, y: -oy, width: effective_w, height: effective_h };

			let rects_before        = ctx.widget_rects.len();
			let scroll_rects_before = ctx.scroll_rects.len();
			let next_idx = layout_and_draw::<Msg>( s.child.as_ref(), &mut sub, child_rect, ctx, my_idx + 1 );

			// Translate widget_rects from sub-canvas space to global space; drop
			// clipped ones. Also clamp `paint_rect` to the scroll viewport so a
			// widget painting outside the sub-canvas does not invalidate pixels
			// it cannot actually reach (the sub-canvas is blitted as a whole,
			// so nothing outside the viewport appears on screen anyway).
			let new_rects: Vec<LaidOutWidget<Msg>> = ctx.widget_rects.drain( rects_before.. ).collect();

			// Capture every interactive item the child laid out — including
			// items that the visibility filter below will discard — so the
			// keyboard handler can step `hovered_idx` item-by-item without
			// caring about which items are currently scrolled into view.
			// The recorded Y is in pre-translation, pre-offset coordinates
			// (i.e. `child_y` relative to the start of the scroll content),
			// recovered by undoing the `-oy` shift the child layout pass
			// applied: `content_y = w.rect.y - child_rect.y = w.rect.y + oy`.
			let navigable: Vec<( usize, f32, f32 )> = new_rects.iter()
				.filter( |w| w.handlers.is_navigable_list_item() )
				.map( |w| ( w.flat_idx, w.rect.y + oy, w.rect.height ) )
				.collect();
			if !navigable.is_empty()
			{
				ctx.scroll_navigable_items.insert( my_idx, navigable );
			}

			for mut w in new_rects
			{
				// Sub-canvas origin (0,0) maps to global (rect.x, rect.y).
				w.rect.x += rect.x;
				w.rect.y += rect.y;
				w.paint_rect.x += rect.x;
				w.paint_rect.y += rect.y;
				w.paint_rect = clamp_rect_to( w.paint_rect, rect );
				// Only keep widgets at least partially visible inside the
				// viewport — now bi-axial because Scroll::horizontal /
				// Scroll::both push content off-screen along x too.
				let visible_y = w.rect.y + w.rect.height > rect.y && w.rect.y < rect.y + rect.height;
				let visible_x = w.rect.x + w.rect.width  > rect.x && w.rect.x < rect.x + rect.width;
				if visible_x && visible_y
				{
					ctx.widget_rects.push( w );
				}
			}

			// Same translation for scroll_rects pushed by any nested
			// scroll widgets — without this, the wheel handler hit-tests
			// in surface coords against rects in sub-canvas coords and
			// only matches when the sub-canvas happens to start at
			// (0, 0) of the surface. Clamp to the outer scroll rect so
			// the inner scroll only captures wheel events inside its
			// visible region.
			let new_scroll_rects: Vec<( Rect, usize, crate::widget::scroll::ScrollAxis )> =
				ctx.scroll_rects.drain( scroll_rects_before.. ).collect();
			for ( mut r, idx, ax ) in new_scroll_rects
			{
				r.x += rect.x;
				r.y += rect.y;
				let clamped = clamp_rect_to( r, rect );
				if clamped.width > 0.0 && clamped.height > 0.0
				{
					ctx.scroll_rects.push( ( clamped, idx, ax ) );
				}
			}

			ctx.scroll_rects.push( ( rect, my_idx, axis ) );

			canvas.blit( &sub, rect.x as i32, rect.y as i32 );

			ctx.scroll_canvases.insert( my_idx, sub );

			next_idx
		}
		Element::Viewport( v ) =>
		{
			let child_h = v.child.preferred_size( rect.width, canvas ).1;
			let effective_h = child_h.max( rect.height );
			let vw = ( rect.width.ceil() as u32 ).max( 1 );
			let vh = ( rect.height.ceil() as u32 ).max( 1 );
			let mut sub = canvas.sub_canvas( vw, vh );
			sub.clear();

			let child_rect = Rect { x: 0.0, y: 0.0, width: rect.width, height: effective_h };
			let rects_before        = ctx.widget_rects.len();
			let scroll_rects_before = ctx.scroll_rects.len();
			let next_idx = layout_and_draw::<Msg>( v.child.as_ref(), &mut sub, child_rect, ctx, flat_idx );

			let new_rects: Vec<LaidOutWidget<Msg>> = ctx.widget_rects.drain( rects_before.. ).collect();
			for mut w in new_rects
			{
				w.rect.x += rect.x;
				w.rect.y += rect.y;
				w.paint_rect.x += rect.x;
				w.paint_rect.y += rect.y;
				w.paint_rect = clamp_rect_to( w.paint_rect, rect );
				if w.rect.y + w.rect.height > rect.y && w.rect.y < rect.y + rect.height
				{
					ctx.widget_rects.push( w );
				}
			}

			// Translate scroll_rects pushed by nested scroll widgets
			// from sub-canvas to surface space. Same reasoning as the
			// scroll-inside-scroll case above.
			let new_scroll_rects: Vec<( Rect, usize, crate::widget::scroll::ScrollAxis )> =
				ctx.scroll_rects.drain( scroll_rects_before.. ).collect();
			for ( mut r, idx, ax ) in new_scroll_rects
			{
				r.x += rect.x;
				r.y += rect.y;
				let clamped = clamp_rect_to( r, rect );
				if clamped.width > 0.0 && clamped.height > 0.0
				{
					ctx.scroll_rects.push( ( clamped, idx, ax ) );
				}
			}

			canvas.blit_fade_bottom( &sub, rect.x as i32, rect.y as i32, v.fade_bottom );
			next_idx
		}
		other =>
		{
			// Gate the focus ring by `is_focusable`: a widget that opts out
			// of keyboard focus (e.g. `Button::focusable(false)`) should not
			// paint a ring even if it ends up in `focused_idx` after a tap.
			let is_focused  = ctx.focused_idx == Some( flat_idx ) && other.is_focusable();
			let is_hovered  = ctx.hovered_idx == Some( flat_idx );
			let is_pressed  = ctx.pressed_idx == Some( flat_idx );
			let cursor_pos  = ctx.cursor_state.get( &flat_idx ).copied().unwrap_or( 0 );
			let sel_anchor  = ctx.selection_anchor.get( &flat_idx ).copied().unwrap_or( cursor_pos );
			let widget_id   = match other
			{
				Element::Button( b )   => b.id,
				Element::TextEdit( t ) => t.id,
				Element::Toggle( t )   => t.id,
				Element::Checkbox( c ) => c.id,
				Element::Radio( r )    => r.id,
				Element::ListItem( l ) => l.id,
				Element::WindowButton( b ) => b.id,
				_                      => None,
			};
			other.draw( canvas, rect, is_focused, is_hovered, is_pressed, cursor_pos, sel_anchor );
			if other.is_interactive()
			{
				ctx.widget_rects.push( LaidOutWidget
				{
					rect,
					flat_idx,
					id:                 widget_id,
					paint_rect:         other.paint_bounds( rect ),
					handlers:           other.handlers(),
					keyboard_focusable: other.is_focusable(),
					cursor:             other.cursor_shape(),
					tooltip:            other.tooltip().map( str::to_string ),
					accessible_label:   other.accessible_label(),
					is_live_region:     ctx.live_depth > 0,
				} );
			}
			else
			{
				let live = ctx.live_depth > 0;
				match other
				{
					Element::Text( t ) if !t.content.is_empty() =>
					{
						ctx.accessible_extras.push( crate::a11y::tree::AccessibleExtra
						{
							rect, label: Some( t.content.clone() ), live,
							kind: crate::a11y::tree::AccessibleExtraKind::Label,
						} );
					}
					Element::Image( _ ) =>
					{
						ctx.accessible_extras.push( crate::a11y::tree::AccessibleExtra
						{
							rect, label: None, live,
							kind: crate::a11y::tree::AccessibleExtraKind::Image,
						} );
					}
					Element::Separator( _ ) =>
					{
						ctx.accessible_extras.push( crate::a11y::tree::AccessibleExtra
						{
							rect, label: None, live,
							kind: crate::a11y::tree::AccessibleExtraKind::Separator,
						} );
					}
					Element::ProgressBar( p ) =>
					{
						ctx.accessible_extras.push( crate::a11y::tree::AccessibleExtra
						{
							rect, label: None, live,
							kind: crate::a11y::tree::AccessibleExtraKind::Progress( p.value ),
						} );
					}
					_ => {}
				}
			}
			flat_idx + 1
		}
	}
}