1use std::collections::HashMap;
21use std::hash::{Hash, Hasher};
22use std::panic::Location;
23
24use fret_core::{Point, Px, Rect, Size};
25use fret_runtime::FrameId;
26use fret_ui::action::{ActionCx, OnTimer, PointerDownCx, PointerMoveCx, UiPointerActionHost};
27use fret_ui::canvas::CanvasPainter;
28use fret_ui::element::{
29 AnyElement, CanvasProps, Length, PointerRegionProps, ScrollAxis, ScrollProps,
30};
31use fret_ui::scroll::ScrollHandle;
32use fret_ui::virtual_list::VirtualListMetrics;
33use fret_ui::{ElementContext, UiHost};
34use tracing::info;
35
36#[derive(Debug, Clone, Copy)]
37pub struct WindowedRowsPaintFrame {
38 pub viewport_height: Px,
39 pub offset_y: Px,
40 pub visible_start: usize,
41 pub visible_end: usize,
42}
43
44pub type OnWindowedRowsPaintFrame =
45 std::sync::Arc<dyn for<'p> Fn(&mut CanvasPainter<'p>, WindowedRowsPaintFrame) + 'static>;
46
47#[derive(Debug, Clone, Copy, PartialEq)]
48pub struct WindowedRowsSurfaceWindowTelemetry {
49 pub callsite_id: u64,
50 pub file: &'static str,
51 pub line: u32,
52 pub column: u32,
53
54 pub len: u64,
55 pub row_height: Px,
56 pub overscan: u64,
57 pub gap: Px,
58 pub scroll_margin: Px,
59
60 pub viewport_height: Px,
61 pub offset_y: Px,
62 pub content_height: Px,
63
64 pub visible_start: Option<u64>,
65 pub visible_end: Option<u64>,
66 pub visible_count: u64,
67}
68
69#[derive(Default)]
70pub struct WindowedRowsSurfaceDiagnosticsStore {
71 per_window: HashMap<fret_core::AppWindowId, WindowedRowsSurfaceDiagnosticsFrame>,
72}
73
74#[derive(Default)]
75struct WindowedRowsSurfaceDiagnosticsFrame {
76 frame_id: FrameId,
77 windows: Vec<WindowedRowsSurfaceWindowTelemetry>,
78}
79
80impl WindowedRowsSurfaceDiagnosticsStore {
81 pub fn begin_frame(&mut self, window: fret_core::AppWindowId, frame_id: FrameId) {
82 let w = self.per_window.entry(window).or_default();
83 if w.frame_id != frame_id {
84 w.frame_id = frame_id;
85 w.windows.clear();
86 }
87 }
88
89 pub fn record_window(
90 &mut self,
91 window: fret_core::AppWindowId,
92 frame_id: FrameId,
93 telemetry: WindowedRowsSurfaceWindowTelemetry,
94 ) {
95 self.begin_frame(window, frame_id);
96 let w = self.per_window.entry(window).or_default();
97 w.windows.push(telemetry);
98 }
99
100 #[allow(dead_code)]
101 pub fn windows_for_window(
102 &self,
103 window: fret_core::AppWindowId,
104 frame_id: FrameId,
105 ) -> Option<&[WindowedRowsSurfaceWindowTelemetry]> {
106 let w = self.per_window.get(&window)?;
107 (w.frame_id == frame_id).then_some(w.windows.as_slice())
108 }
109}
110
111#[derive(Clone)]
116pub struct WindowedRowsSurfaceProps {
117 pub scroll: ScrollProps,
118 pub canvas: CanvasProps,
119 pub len: usize,
120 pub row_height: Px,
121 pub overscan: usize,
122 pub gap: Px,
123 pub scroll_margin: Px,
124 pub scroll_handle: ScrollHandle,
125 pub on_paint_frame: Option<OnWindowedRowsPaintFrame>,
126}
127
128impl Default for WindowedRowsSurfaceProps {
129 fn default() -> Self {
130 let scroll = ScrollProps {
131 axis: ScrollAxis::Y,
132 layout: fret_ui::element::LayoutStyle {
133 size: fret_ui::element::SizeStyle {
134 width: Length::Fill,
135 height: Length::Fill,
136 ..Default::default()
137 },
138 ..Default::default()
139 },
140 windowed_paint: true,
143 ..Default::default()
144 };
145
146 let mut canvas = CanvasProps::default();
147 canvas.layout.size.width = Length::Fill;
148
149 Self {
150 scroll,
151 canvas,
152 len: 0,
153 row_height: Px(0.0),
154 overscan: 0,
155 gap: Px(0.0),
156 scroll_margin: Px(0.0),
157 scroll_handle: ScrollHandle::default(),
158 on_paint_frame: None,
159 }
160 }
161}
162
163#[track_caller]
174pub fn windowed_rows_surface<H: UiHost>(
175 cx: &mut ElementContext<'_, H>,
176 props: WindowedRowsSurfaceProps,
177 paint_row: impl for<'p> Fn(&mut CanvasPainter<'p>, usize, Rect) + 'static,
178) -> AnyElement {
179 let caller = Location::caller();
180 let WindowedRowsSurfaceProps {
181 mut scroll,
182 mut canvas,
183 len,
184 row_height,
185 overscan,
186 gap,
187 scroll_margin,
188 scroll_handle,
189 on_paint_frame,
190 } = props;
191
192 let mut metrics = VirtualListMetrics::default();
193 metrics.ensure_with_mode(
194 fret_ui::element::VirtualListMeasureMode::Fixed,
195 len,
196 row_height,
197 gap,
198 scroll_margin,
199 );
200 let content_h = metrics.total_height();
201
202 let viewport_h = Px(scroll_handle.viewport_size().height.0.max(0.0));
203 let offset_y = Px(scroll_handle.offset().y.0.max(0.0));
204 let offset_y = metrics.clamp_offset(offset_y, viewport_h);
205 let visible = metrics.visible_range(offset_y, viewport_h, overscan);
206
207 let mut hasher = std::collections::hash_map::DefaultHasher::new();
208 caller.file().hash(&mut hasher);
209 caller.line().hash(&mut hasher);
210 caller.column().hash(&mut hasher);
211 let callsite_id = hasher.finish();
212
213 cx.app.with_global_mut_untracked(
214 WindowedRowsSurfaceDiagnosticsStore::default,
215 |store, _app| {
216 let (visible_start, visible_end, visible_count) = visible
217 .map(|visible| {
218 let count = visible.count;
219 if count == 0 {
220 return (None, None, 0u64);
221 }
222 let start = visible.start_index.saturating_sub(visible.overscan);
223 let end = (visible.end_index + visible.overscan).min(count.saturating_sub(1));
224 (
225 Some(start as u64),
226 Some(end as u64),
227 (end.saturating_sub(start) as u64).saturating_add(1),
228 )
229 })
230 .unwrap_or((None, None, 0));
231 store.record_window(
232 cx.window,
233 cx.frame_id,
234 WindowedRowsSurfaceWindowTelemetry {
235 callsite_id,
236 file: caller.file(),
237 line: caller.line(),
238 column: caller.column(),
239 len: len as u64,
240 row_height,
241 overscan: overscan as u64,
242 gap,
243 scroll_margin,
244 viewport_height: viewport_h,
245 offset_y,
246 content_height: content_h,
247 visible_start,
248 visible_end,
249 visible_count,
250 },
251 );
252 },
253 );
254
255 scroll.axis = ScrollAxis::Y;
256 scroll.scroll_handle = Some(scroll_handle.clone());
257 scroll.windowed_paint = true;
260
261 canvas.layout.size.width = Length::Fill;
262 canvas.layout.size.height = Length::Px(content_h);
263
264 cx.scroll(scroll, move |cx| {
265 let scroll_handle = scroll_handle.clone();
266 let metrics = metrics.clone();
267 let paint_row = std::sync::Arc::new(paint_row);
268 let on_paint_frame = on_paint_frame.clone();
269
270 vec![cx.canvas(canvas, move |painter| {
271 let viewport_h = Px(scroll_handle.viewport_size().height.0.max(0.0));
272 let offset_y = Px(scroll_handle.offset().y.0.max(0.0));
273 let offset_y = metrics.clamp_offset(offset_y, viewport_h);
274 let Some(visible) = metrics.visible_range(offset_y, viewport_h, overscan) else {
275 return;
276 };
277
278 let bounds = painter.bounds();
279 let origin_x = bounds.origin.x;
280 let origin_y = bounds.origin.y;
281 let width = Px(bounds.size.width.0.max(0.0));
282 let count = visible.count;
283 if count == 0 {
284 return;
285 }
286
287 let start = visible.start_index.saturating_sub(visible.overscan);
288 let end = (visible.end_index + visible.overscan).min(count.saturating_sub(1));
289
290 if let Some(on_paint_frame) = &on_paint_frame {
291 on_paint_frame(
292 painter,
293 WindowedRowsPaintFrame {
294 viewport_height: viewport_h,
295 offset_y,
296 visible_start: start,
297 visible_end: end,
298 },
299 );
300 }
301
302 for index in start..=end {
303 let y = metrics.offset_for_index(index);
304 let h = metrics.height_at(index);
305 let rect = Rect::new(
306 Point::new(origin_x, Px(origin_y.0 + y.0)),
307 Size::new(width, h),
308 );
309 paint_row(painter, index, rect);
310 }
311 })]
312 })
313}
314
315pub type OnWindowedRowsPointerDown = std::sync::Arc<
316 dyn Fn(&mut dyn UiPointerActionHost, ActionCx, usize, PointerDownCx) -> bool + 'static,
317>;
318
319pub type OnWindowedRowsPointerMove = std::sync::Arc<
320 dyn Fn(&mut dyn UiPointerActionHost, ActionCx, Option<usize>, PointerMoveCx) -> bool + 'static,
321>;
322
323pub type OnWindowedRowsPointerUp = std::sync::Arc<
324 dyn Fn(
325 &mut dyn UiPointerActionHost,
326 ActionCx,
327 Option<usize>,
328 fret_ui::action::PointerUpCx,
329 ) -> bool
330 + 'static,
331>;
332
333pub type OnWindowedRowsPointerCancel = std::sync::Arc<
334 dyn Fn(&mut dyn UiPointerActionHost, ActionCx, fret_ui::action::PointerCancelCx) -> bool
335 + 'static,
336>;
337
338#[derive(Default, Clone)]
339pub struct WindowedRowsSurfacePointerHandlers {
340 pub on_pointer_down: Option<OnWindowedRowsPointerDown>,
341 pub on_pointer_move: Option<OnWindowedRowsPointerMove>,
342 pub on_pointer_up: Option<OnWindowedRowsPointerUp>,
343 pub on_pointer_cancel: Option<OnWindowedRowsPointerCancel>,
344 pub on_timer: Option<OnTimer>,
345}
346
347fn row_index_for_pointer(
348 metrics: &VirtualListMetrics,
349 scroll_handle: &ScrollHandle,
350 bounds: Rect,
351 position: Point,
352 len: usize,
353) -> Option<usize> {
354 if len == 0 {
355 return None;
356 }
357
358 let viewport_h = Px(scroll_handle.viewport_size().height.0.max(0.0));
359 if viewport_h.0 <= 0.0 {
360 return None;
361 }
362
363 let offset_y = Px(scroll_handle.offset().y.0.max(0.0));
364 let offset_y = metrics.clamp_offset(offset_y, viewport_h);
365
366 let local_y = Px(position.y.0 - bounds.origin.y.0);
367
368 if std::env::var_os("FRET_WINDOWED_ROWS_POINTER_DEBUG")
369 .is_some_and(|v| !v.is_empty() && v != "0")
370 {
371 info!(
372 "windowed_rows_pointer bounds_y={} pos_y={} local_y={} offset_y={} viewport_h={}",
373 bounds.origin.y.0, position.y.0, local_y.0, offset_y.0, viewport_h.0
374 );
375 }
376
377 let idx_viewport = metrics.index_for_offset(Px(offset_y.0 + local_y.0));
386 let idx_content = metrics.index_for_offset(local_y);
387
388 let idx = if let Some(visible) = metrics.visible_range(offset_y, viewport_h, 0) {
389 let in_visible = |idx: usize| idx >= visible.start_index && idx <= visible.end_index;
390 match (in_visible(idx_viewport), in_visible(idx_content)) {
391 (true, false) => idx_viewport,
392 (false, true) => idx_content,
393 _ => idx_content,
395 }
396 } else {
397 idx_content
398 };
399
400 Some(idx.min(len.saturating_sub(1)))
401}
402
403#[track_caller]
406pub fn windowed_rows_surface_with_pointer_region<H: UiHost>(
407 cx: &mut ElementContext<'_, H>,
408 props: WindowedRowsSurfaceProps,
409 pointer: PointerRegionProps,
410 handlers: WindowedRowsSurfacePointerHandlers,
411 content_semantics: Option<fret_ui::element::SemanticsProps>,
412 paint_row: impl for<'p> Fn(&mut CanvasPainter<'p>, usize, Rect) + 'static,
413) -> AnyElement {
414 let caller = Location::caller();
415 let WindowedRowsSurfacePointerHandlers {
416 on_pointer_down,
417 on_pointer_move,
418 on_pointer_up,
419 on_pointer_cancel,
420 on_timer,
421 } = handlers;
422
423 let WindowedRowsSurfaceProps {
424 mut scroll,
425 mut canvas,
426 len,
427 row_height,
428 overscan,
429 gap,
430 scroll_margin,
431 scroll_handle,
432 on_paint_frame,
433 } = props;
434
435 let mut metrics = VirtualListMetrics::default();
436 metrics.ensure_with_mode(
437 fret_ui::element::VirtualListMeasureMode::Fixed,
438 len,
439 row_height,
440 gap,
441 scroll_margin,
442 );
443 let content_h = metrics.total_height();
444
445 let viewport_h = Px(scroll_handle.viewport_size().height.0.max(0.0));
446 let offset_y = Px(scroll_handle.offset().y.0.max(0.0));
447 let offset_y = metrics.clamp_offset(offset_y, viewport_h);
448 let visible = metrics.visible_range(offset_y, viewport_h, overscan);
449
450 let mut hasher = std::collections::hash_map::DefaultHasher::new();
451 caller.file().hash(&mut hasher);
452 caller.line().hash(&mut hasher);
453 caller.column().hash(&mut hasher);
454 let callsite_id = hasher.finish();
455
456 cx.app.with_global_mut_untracked(
457 WindowedRowsSurfaceDiagnosticsStore::default,
458 |store, _app| {
459 let (visible_start, visible_end, visible_count) = visible
460 .map(|visible| {
461 let count = visible.count;
462 if count == 0 {
463 return (None, None, 0u64);
464 }
465 let start = visible.start_index.saturating_sub(visible.overscan);
466 let end = (visible.end_index + visible.overscan).min(count.saturating_sub(1));
467 (
468 Some(start as u64),
469 Some(end as u64),
470 (end.saturating_sub(start) as u64).saturating_add(1),
471 )
472 })
473 .unwrap_or((None, None, 0));
474 store.record_window(
475 cx.window,
476 cx.frame_id,
477 WindowedRowsSurfaceWindowTelemetry {
478 callsite_id,
479 file: caller.file(),
480 line: caller.line(),
481 column: caller.column(),
482 len: len as u64,
483 row_height,
484 overscan: overscan as u64,
485 gap,
486 scroll_margin,
487 viewport_height: viewport_h,
488 offset_y,
489 content_height: content_h,
490 visible_start,
491 visible_end,
492 visible_count,
493 },
494 );
495 },
496 );
497
498 scroll.axis = ScrollAxis::Y;
499 scroll.scroll_handle = Some(scroll_handle.clone());
500 scroll.windowed_paint = true;
503
504 canvas.layout.size.width = Length::Fill;
505 canvas.layout.size.height = Length::Px(content_h);
506
507 cx.scroll(scroll, move |cx| {
508 let scroll_handle = scroll_handle.clone();
509 let metrics = metrics.clone();
510 let paint_row = std::sync::Arc::new(paint_row);
511 let on_pointer_down = on_pointer_down.clone();
512 let on_pointer_move = on_pointer_move.clone();
513 let on_pointer_up = on_pointer_up.clone();
514 let on_pointer_cancel = on_pointer_cancel.clone();
515 let content_semantics = content_semantics.clone();
516 let on_paint_frame = on_paint_frame.clone();
517
518 vec![cx.pointer_region(pointer, move |cx| {
519 if let Some(on_timer) = on_timer.clone() {
520 cx.timer_on_timer_for(cx.root_id(), on_timer);
521 }
522
523 if let Some(on_pointer_down) = on_pointer_down.clone() {
524 let scroll_handle = scroll_handle.clone();
525 let metrics = metrics.clone();
526 cx.pointer_region_on_pointer_down(std::sync::Arc::new(
527 move |host, action_cx, down| {
528 let bounds = host.bounds();
529 let idx = row_index_for_pointer(
530 &metrics,
531 &scroll_handle,
532 bounds,
533 down.position,
534 len,
535 );
536 let Some(idx) = idx else {
537 return false;
538 };
539 on_pointer_down(host, action_cx, idx, down)
540 },
541 ));
542 }
543
544 if let Some(on_pointer_move) = on_pointer_move.clone() {
545 let scroll_handle = scroll_handle.clone();
546 let metrics = metrics.clone();
547 cx.pointer_region_on_pointer_move(std::sync::Arc::new(
548 move |host, action_cx, mv| {
549 let bounds = host.bounds();
550 let idx = row_index_for_pointer(
551 &metrics,
552 &scroll_handle,
553 bounds,
554 mv.position,
555 len,
556 );
557 on_pointer_move(host, action_cx, idx, mv)
558 },
559 ));
560 }
561
562 if let Some(on_pointer_up) = on_pointer_up.clone() {
563 let scroll_handle = scroll_handle.clone();
564 let metrics = metrics.clone();
565 cx.pointer_region_on_pointer_up(std::sync::Arc::new(move |host, action_cx, up| {
566 let bounds = host.bounds();
567 let idx =
568 row_index_for_pointer(&metrics, &scroll_handle, bounds, up.position, len);
569 on_pointer_up(host, action_cx, idx, up)
570 }));
571 }
572
573 if let Some(on_pointer_cancel) = on_pointer_cancel.clone() {
574 cx.pointer_region_on_pointer_cancel(on_pointer_cancel);
575 }
576
577 let canvas_children = vec![cx.canvas(canvas, move |painter| {
578 let viewport_h = Px(scroll_handle.viewport_size().height.0.max(0.0));
579 let offset_y = Px(scroll_handle.offset().y.0.max(0.0));
580 let offset_y = metrics.clamp_offset(offset_y, viewport_h);
581 let Some(visible) = metrics.visible_range(offset_y, viewport_h, overscan) else {
582 return;
583 };
584
585 let bounds = painter.bounds();
586 let origin_x = bounds.origin.x;
587 let origin_y = bounds.origin.y;
588 let width = Px(bounds.size.width.0.max(0.0));
589 let count = visible.count;
590 if count == 0 {
591 return;
592 }
593
594 let start = visible.start_index.saturating_sub(visible.overscan);
595 let end = (visible.end_index + visible.overscan).min(count.saturating_sub(1));
596
597 if let Some(on_paint_frame) = &on_paint_frame {
598 on_paint_frame(
599 painter,
600 WindowedRowsPaintFrame {
601 viewport_height: viewport_h,
602 offset_y,
603 visible_start: start,
604 visible_end: end,
605 },
606 );
607 }
608
609 for index in start..=end {
610 let y = metrics.offset_for_index(index);
611 let h = metrics.height_at(index);
612 let rect = Rect::new(
613 Point::new(origin_x, Px(origin_y.0 + y.0)),
614 Size::new(width, h),
615 );
616 paint_row(painter, index, rect);
617 }
618 })];
619
620 if let Some(semantics) = content_semantics.clone() {
621 vec![cx.semantics(semantics, |_cx| canvas_children)]
622 } else {
623 canvas_children
624 }
625 })]
626 })
627}
628
629#[cfg(test)]
630mod tests {
631 use super::*;
632
633 #[test]
634 fn default_props_enable_windowed_paint() {
635 let props = WindowedRowsSurfaceProps::default();
636 assert_eq!(props.scroll.axis, ScrollAxis::Y);
637 assert!(props.scroll.windowed_paint);
638 }
639}