1use tessera_foundation::gesture::{
7 DragAxis, DragRecognizer, DragSettings, ScrollRecognizer, ScrollSettings,
8};
9use tessera_ui::{
10 AxisConstraint, CallbackWith, ComputedData, Constraint, Dp, FocusProperties, KeyboardInput,
11 KeyboardInputModifierNode, LayoutResult, MeasurementError, Modifier, PointerInput,
12 PointerInputModifierNode, Px, PxPosition, ScrollDeltaUnit, ScrollEventSource, State, key,
13 layout::{MeasureScope, PlacementResult, PlacementScope, layout},
14 modifier::{FocusModifierExt as _, ModifierCapabilityExt as _},
15 normalize_platform_scroll_delta, receive_frame_nanos, remember, tessera, winit,
16};
17
18use crate::{
19 alignment::CrossAxisAlignment,
20 layout_flex::{cross_axis_child_constraint, px_mul},
21 modifier::ModifierExt as _,
22 pos_misc::is_position_inside_bounds,
23};
24
25const DEFAULT_SNAP_THRESHOLD: f32 = 0.5;
26const DEFAULT_SCROLL_SMOOTHING: f32 = 0.12;
27const SNAP_IDLE_TIME_NANOS: u64 = 120_000_000;
28
29#[derive(Clone, Copy, Debug, PartialEq, Default)]
31pub enum PagerPageSize {
32 #[default]
34 Fill,
35 Fixed(Dp),
37}
38
39#[derive(Clone, PartialEq)]
41struct PagerConfig {
42 pub modifier: Modifier,
44 pub page_count: usize,
46 pub initial_page: usize,
48 pub page_size: PagerPageSize,
50 pub page_spacing: Dp,
52 pub content_padding: Dp,
54 pub beyond_viewport_page_count: usize,
56 pub cross_axis_alignment: CrossAxisAlignment,
58 pub user_scroll_enabled: bool,
60 pub snap_threshold: f32,
62 pub scroll_smoothing: f32,
64 pub page_content: CallbackWith<usize>,
66 pub controller: Option<State<PagerController>>,
70}
71
72fn normalize_pager_scroll_delta(
73 delta_x: f32,
74 delta_y: f32,
75 unit: Option<ScrollDeltaUnit>,
76 source: Option<ScrollEventSource>,
77) -> (f32, f32) {
78 match (source, unit) {
79 (Some(source), Some(unit)) => {
80 normalize_platform_scroll_delta(delta_x, delta_y, unit, source)
81 }
82 _ => (delta_x, delta_y),
83 }
84}
85
86impl Default for PagerConfig {
87 fn default() -> Self {
88 Self {
89 modifier: Modifier::new().fill_max_size(),
90 page_count: 0,
91 initial_page: 0,
92 page_size: PagerPageSize::default(),
93 page_spacing: Dp(0.0),
94 content_padding: Dp(0.0),
95 beyond_viewport_page_count: 0,
96 cross_axis_alignment: CrossAxisAlignment::Center,
97 user_scroll_enabled: true,
98 snap_threshold: DEFAULT_SNAP_THRESHOLD,
99 scroll_smoothing: DEFAULT_SCROLL_SMOOTHING,
100 page_content: CallbackWith::default_value(),
101 controller: None,
102 }
103 }
104}
105
106struct PagerParams {
107 modifier: Option<Modifier>,
108 page_count: usize,
109 initial_page: usize,
110 page_size: PagerPageSize,
111 page_spacing: Dp,
112 content_padding: Dp,
113 beyond_viewport_page_count: usize,
114 cross_axis_alignment: CrossAxisAlignment,
115 user_scroll_enabled: bool,
116 snap_threshold: Option<f32>,
117 scroll_smoothing: Option<f32>,
118 page_content: Option<CallbackWith<usize>>,
119 controller: Option<State<PagerController>>,
120}
121
122fn pager_config_from_params(params: PagerParams) -> PagerConfig {
123 let defaults = PagerConfig::default();
124 PagerConfig {
125 modifier: params.modifier.unwrap_or(defaults.modifier),
126 page_count: params.page_count,
127 initial_page: params.initial_page,
128 page_size: params.page_size,
129 page_spacing: params.page_spacing,
130 content_padding: params.content_padding,
131 beyond_viewport_page_count: params.beyond_viewport_page_count,
132 cross_axis_alignment: params.cross_axis_alignment,
133 user_scroll_enabled: params.user_scroll_enabled,
134 snap_threshold: params.snap_threshold.unwrap_or(defaults.snap_threshold),
135 scroll_smoothing: params.scroll_smoothing.unwrap_or(defaults.scroll_smoothing),
136 page_content: params
137 .page_content
138 .unwrap_or_else(CallbackWith::default_value),
139 controller: params.controller,
140 }
141}
142
143#[derive(Clone, PartialEq)]
145pub struct PagerController {
146 current_page: usize,
147 current_page_offset_fraction: f32,
148 page_count: usize,
149 page_size: Px,
150 page_spacing: Px,
151 scroll_offset: f32,
152 target_offset: f32,
153 last_frame_nanos: Option<u64>,
154 last_scroll_frame_nanos: Option<u64>,
155 is_dragging: bool,
156 initialized: bool,
157}
158
159impl PagerController {
160 pub fn new(initial_page: usize) -> Self {
162 Self {
163 current_page: initial_page,
164 current_page_offset_fraction: 0.0,
165 page_count: 0,
166 page_size: Px::ZERO,
167 page_spacing: Px::ZERO,
168 scroll_offset: 0.0,
169 target_offset: 0.0,
170 last_frame_nanos: None,
171 last_scroll_frame_nanos: None,
172 is_dragging: false,
173 initialized: false,
174 }
175 }
176
177 pub fn current_page(&self) -> usize {
179 self.current_page
180 }
181
182 pub fn current_page_offset_fraction(&self) -> f32 {
184 self.current_page_offset_fraction
185 }
186
187 pub fn jump_to_page(&mut self, page: usize) {
189 let page = self.clamp_page(page);
190 self.current_page = page;
191 let offset = self.offset_for_page(page);
192 self.scroll_offset = offset;
193 self.target_offset = offset;
194 self.last_scroll_frame_nanos = None;
195 self.update_current_page_from_offset();
196 }
197
198 pub fn scroll_to_page(&mut self, page: usize) {
200 let page = self.clamp_page(page);
201 self.target_offset = self.offset_for_page(page);
202 self.last_scroll_frame_nanos = None;
203 }
204
205 fn set_page_count(&mut self, page_count: usize) {
206 self.page_count = page_count;
207 if page_count == 0 {
208 self.current_page = 0;
209 self.scroll_offset = 0.0;
210 self.target_offset = 0.0;
211 self.current_page_offset_fraction = 0.0;
212 return;
213 }
214 if self.current_page >= page_count {
215 self.current_page = page_count - 1;
216 }
217 }
218
219 fn update_layout(&mut self, page_size: Px, page_spacing: Px, page_count: usize) {
220 let size_changed = page_size != self.page_size || page_spacing != self.page_spacing;
221 self.page_size = page_size;
222 self.page_spacing = page_spacing;
223 self.page_count = page_count;
224 self.current_page = self.clamp_page(self.current_page);
225
226 if page_count == 0 {
227 self.scroll_offset = 0.0;
228 self.target_offset = 0.0;
229 self.current_page_offset_fraction = 0.0;
230 return;
231 }
232
233 if !self.initialized && page_size > Px::ZERO {
234 let offset = self.offset_for_page(self.current_page);
235 self.scroll_offset = offset;
236 self.target_offset = offset;
237 self.initialized = true;
238 } else if size_changed && page_size > Px::ZERO {
239 let offset = self.offset_for_page(self.current_page);
240 self.scroll_offset = offset;
241 self.target_offset = offset;
242 }
243
244 self.scroll_offset = self.clamp_offset(self.scroll_offset);
245 self.target_offset = self.clamp_offset(self.target_offset);
246 self.update_current_page_from_offset();
247 }
248
249 fn tick(&mut self, frame_nanos: u64, snap_threshold: f32, scroll_smoothing: f32) {
250 if self.page_count == 0 {
251 return;
252 }
253 if self.page_distance() <= f32::EPSILON {
254 return;
255 }
256
257 let snap_threshold = snap_threshold.clamp(0.0, 1.0);
258 let scroll_smoothing = scroll_smoothing.clamp(0.0, 1.0);
259 let idle = self
260 .last_scroll_frame_nanos
261 .map(|last_scroll_frame_nanos| {
262 frame_nanos.saturating_sub(last_scroll_frame_nanos) > SNAP_IDLE_TIME_NANOS
263 })
264 .unwrap_or(true);
265
266 if idle && !self.is_dragging {
267 let target_page = self.snap_target_page(snap_threshold);
268 self.target_offset = self.offset_for_page(target_page);
269 }
270
271 self.update_scroll_offset(frame_nanos, scroll_smoothing);
272 self.scroll_offset = self.clamp_offset(self.scroll_offset);
273 self.update_current_page_from_offset();
274 }
275
276 fn has_pending_animation_frame(&self, frame_nanos: u64) -> bool {
277 if self.page_count == 0 || self.page_distance() <= f32::EPSILON {
278 return false;
279 }
280
281 if self.is_dragging {
282 return true;
283 }
284
285 if (self.target_offset - self.scroll_offset).abs() > f32::EPSILON {
286 return true;
287 }
288
289 self.last_scroll_frame_nanos
290 .map(|last_scroll_frame_nanos| {
291 frame_nanos.saturating_sub(last_scroll_frame_nanos) <= SNAP_IDLE_TIME_NANOS
292 })
293 .unwrap_or(false)
294 }
295
296 fn apply_scroll_delta(&mut self, delta: f32, frame_nanos: u64) {
297 if self.page_distance() <= f32::EPSILON || self.page_count == 0 {
298 return;
299 }
300 self.scroll_offset = self.clamp_offset(self.scroll_offset + delta);
301 self.target_offset = self.scroll_offset;
302 self.last_scroll_frame_nanos = Some(frame_nanos);
303 self.update_current_page_from_offset();
304 }
305
306 fn start_drag(&mut self, frame_nanos: u64) {
307 self.is_dragging = true;
308 self.last_scroll_frame_nanos = Some(frame_nanos);
309 }
310
311 fn end_drag(&mut self) {
312 self.is_dragging = false;
313 }
314
315 fn is_dragging(&self) -> bool {
316 self.is_dragging
317 }
318
319 fn scroll_offset_px(&self) -> Px {
320 Px::saturating_from_f32(self.scroll_offset)
321 }
322
323 fn clamp_page(&self, page: usize) -> usize {
324 if self.page_count == 0 {
325 0
326 } else {
327 page.min(self.page_count.saturating_sub(1))
328 }
329 }
330
331 fn page_distance(&self) -> f32 {
332 (self.page_size + self.page_spacing).to_f32()
333 }
334
335 fn offset_for_page(&self, page: usize) -> f32 {
336 -self.page_distance() * page as f32
337 }
338
339 fn clamp_offset(&self, offset: f32) -> f32 {
340 if self.page_count <= 1 || self.page_distance() <= f32::EPSILON {
341 return 0.0;
342 }
343 let max_offset = 0.0;
344 let min_offset = -self.page_distance() * self.page_count.saturating_sub(1) as f32;
345 offset.clamp(min_offset, max_offset)
346 }
347
348 fn snap_target_page(&self, threshold: f32) -> usize {
349 let distance = self.page_distance();
350 if distance <= f32::EPSILON || self.page_count == 0 {
351 return 0;
352 }
353 let page_float = -self.scroll_offset / distance;
354 let base_page = page_float.floor();
355 let fraction = page_float - base_page;
356 let target = if fraction >= threshold {
357 base_page + 1.0
358 } else {
359 base_page
360 };
361 let max_page = self.page_count.saturating_sub(1) as f32;
362 if target.is_finite() {
363 target.clamp(0.0, max_page) as usize
364 } else {
365 0
366 }
367 }
368
369 fn update_current_page_from_offset(&mut self) {
370 let distance = self.page_distance();
371 if distance <= f32::EPSILON || self.page_count == 0 {
372 self.current_page = 0;
373 self.current_page_offset_fraction = 0.0;
374 return;
375 }
376
377 let page_float = -self.scroll_offset / distance;
378 let mut nearest = page_float.round();
379 if !nearest.is_finite() {
380 nearest = 0.0;
381 }
382 let max_page = self.page_count.saturating_sub(1) as f32;
383 let current_page = nearest.clamp(0.0, max_page) as usize;
384 let snapped_offset = -distance * current_page as f32;
385 let offset_fraction = ((self.scroll_offset - snapped_offset) / distance).clamp(-0.5, 0.5);
386 self.current_page = current_page;
387 self.current_page_offset_fraction = offset_fraction;
388 }
389
390 fn update_scroll_offset(&mut self, frame_nanos: u64, smoothing: f32) {
391 let delta_time = if let Some(last_frame_nanos) = self.last_frame_nanos {
392 frame_nanos.saturating_sub(last_frame_nanos) as f32 / 1_000_000_000.0
393 } else {
394 1.0 / 60.0
395 };
396 self.last_frame_nanos = Some(frame_nanos);
397
398 let diff = self.target_offset - self.scroll_offset;
399 if diff.abs() < 0.5 {
400 self.scroll_offset = self.target_offset;
401 return;
402 }
403
404 let mut movement_factor = (1.0 - smoothing) * delta_time * 60.0;
405 if movement_factor > 1.0 {
406 movement_factor = 1.0;
407 }
408
409 self.scroll_offset += diff * movement_factor;
410 }
411}
412
413impl Default for PagerController {
414 fn default() -> Self {
415 Self::new(0)
416 }
417}
418
419#[derive(Clone, Copy, Debug, PartialEq, Eq)]
420enum PagerAxis {
421 Horizontal,
422 Vertical,
423}
424
425impl PagerAxis {
426 fn cross(self, size: ComputedData) -> Px {
427 match self {
428 Self::Horizontal => size.height,
429 Self::Vertical => size.width,
430 }
431 }
432
433 fn pack_size(self, main: Px, cross: Px) -> ComputedData {
434 match self {
435 Self::Horizontal => ComputedData {
436 width: main,
437 height: cross,
438 },
439 Self::Vertical => ComputedData {
440 width: cross,
441 height: main,
442 },
443 }
444 }
445
446 fn position(self, main: Px, cross: Px) -> PxPosition {
447 match self {
448 Self::Horizontal => PxPosition::new(main, cross),
449 Self::Vertical => PxPosition::new(cross, main),
450 }
451 }
452
453 fn scroll_delta(self, delta_x: f32, delta_y: f32) -> f32 {
454 match self {
455 Self::Horizontal => {
456 if delta_x.abs() >= 0.01 {
457 delta_x
458 } else {
459 delta_y
460 }
461 }
462 Self::Vertical => {
463 if delta_y.abs() >= 0.01 {
464 delta_y
465 } else {
466 delta_x
467 }
468 }
469 }
470 }
471}
472
473#[derive(Clone)]
474struct PagerLayout {
475 axis: PagerAxis,
476 cross_axis_alignment: CrossAxisAlignment,
477 page_size: PagerPageSize,
478 page_spacing: Px,
479 content_padding: Px,
480 page_count: usize,
481 visible_pages: Vec<usize>,
482 scroll_offset: Px,
483 controller: State<PagerController>,
484}
485
486impl PartialEq for PagerLayout {
487 fn eq(&self, other: &Self) -> bool {
488 tessera_ui::LayoutModifierNode::placement_eq(self, other)
489 }
490}
491
492impl tessera_ui::LayoutModifierNode for PagerLayout {
493 fn measure(&self, input: &mut MeasureScope<'_>) -> Result<LayoutResult, MeasurementError> {
494 let mut result = LayoutResult::default();
495 let children = input.children();
496 if self.page_count == 0 {
497 return Ok(result.with_size(ComputedData::min_from_constraint(
498 input.parent_constraint().as_ref(),
499 )));
500 }
501
502 if children.len() != self.visible_pages.len() {
503 return Err(MeasurementError::MeasureFnFailed(
504 "Pager measured child count mismatch".into(),
505 ));
506 }
507
508 let parent = input.parent_constraint();
509 let main_dimension = match self.axis {
510 PagerAxis::Horizontal => parent.width(),
511 PagerAxis::Vertical => parent.height(),
512 };
513 let cross_dimension = match self.axis {
514 PagerAxis::Horizontal => parent.height(),
515 PagerAxis::Vertical => parent.width(),
516 };
517
518 let page_main =
519 resolve_page_main_size(self.page_size, main_dimension, self.content_padding);
520 let page_spacing = self.page_spacing;
521 let padding = self.content_padding;
522 let container_main = main_dimension.clamp(page_main + padding + padding);
523
524 let cross_constraint =
525 cross_dimension_for_alignment(cross_dimension, self.cross_axis_alignment);
526 let child_constraint = match self.axis {
527 PagerAxis::Horizontal => {
528 Constraint::new(AxisConstraint::exact(page_main), cross_constraint)
529 }
530 PagerAxis::Vertical => {
531 Constraint::new(cross_constraint, AxisConstraint::exact(page_main))
532 }
533 };
534
535 let mut max_cross = Px::ZERO;
536 let mut measured_children = Vec::with_capacity(children.len());
537 for &child in &children {
538 let measurement = child.measure(input, &child_constraint)?;
539 max_cross = max_cross.max(self.axis.cross(measurement.size()));
540 measured_children.push((child, measurement.size()));
541 }
542 let container_cross = cross_dimension.clamp(max_cross);
543
544 let should_update_layout = self.controller.with(|controller| {
545 let mut next = controller.clone();
546 next.update_layout(page_main, page_spacing, self.page_count);
547 next != *controller
548 });
549 if should_update_layout {
550 self.controller.with_mut(|controller| {
551 controller.update_layout(page_main, page_spacing, self.page_count);
552 });
553 }
554
555 let scroll_offset = self.controller.with(|c| c.scroll_offset_px());
556 let page_step = page_main + page_spacing;
557
558 for (&child, &page_index) in children.iter().zip(self.visible_pages.iter()) {
559 let measured = measured_children
560 .iter()
561 .find_map(|(measured_child, size)| (*measured_child == child).then_some(*size))
562 .unwrap_or(ComputedData::ZERO);
563 let cross_offset = compute_cross_offset(
564 container_cross,
565 self.axis.cross(measured),
566 self.cross_axis_alignment,
567 );
568 let page_offset = padding + px_mul(page_step, page_index) + scroll_offset;
569 let position = self.axis.position(page_offset, cross_offset);
570 result.place_child(child, position);
571 }
572
573 Ok(result.with_size(self.axis.pack_size(container_main, container_cross)))
574 }
575
576 fn measure_eq(&self, other: &Self) -> bool {
577 self.axis == other.axis
578 && self.cross_axis_alignment == other.cross_axis_alignment
579 && self.page_size == other.page_size
580 && self.page_spacing == other.page_spacing
581 && self.content_padding == other.content_padding
582 && self.page_count == other.page_count
583 && self.visible_pages == other.visible_pages
584 }
585
586 fn placement_eq(&self, other: &Self) -> bool {
587 self.measure_eq(other) && self.scroll_offset == other.scroll_offset
588 }
589
590 fn place_children(
591 &self,
592 input: &PlacementScope<'_>,
593 _inner: &PlacementResult,
594 ) -> Option<PlacementResult> {
595 let mut result = LayoutResult::default();
596 if self.page_count == 0 {
597 return Some(result.into_placement_result());
598 }
599
600 let children = input.children();
601 if children.len() != self.visible_pages.len() {
602 return None;
603 }
604
605 let container_cross = self.axis.cross(input.size());
606 let page_step = self
607 .controller
608 .with(|controller| controller.page_size + controller.page_spacing);
609 let padding = self.content_padding;
610
611 for (&child, &page_index) in children.iter().zip(self.visible_pages.iter()) {
612 let measured = child.size();
613 let cross_offset = compute_cross_offset(
614 container_cross,
615 self.axis.cross(measured),
616 self.cross_axis_alignment,
617 );
618 let page_offset = padding + px_mul(page_step, page_index) + self.scroll_offset;
619 let position = self.axis.position(page_offset, cross_offset);
620 result.place_child(child, position);
621 }
622
623 Some(result.into_placement_result())
624 }
625}
626
627impl tessera_ui::DrawModifierNode for PagerLayout {
628 fn draw(
629 &self,
630 ctx: &mut tessera_ui::DrawModifierContext<'_, '_>,
631 content: &mut dyn tessera_ui::DrawModifierContent,
632 ) {
633 let input = &mut *ctx.render_input;
634 input.metadata_mut().set_clips_children(true);
635 content.draw(input);
636 }
637}
638
639#[derive(Clone, Copy, PartialEq, Eq, Hash)]
640struct ZeroLayout;
641
642impl tessera_ui::LayoutModifierNode for ZeroLayout {
643 fn measure(&self, input: &mut MeasureScope<'_>) -> Result<LayoutResult, MeasurementError> {
644 Ok(LayoutResult::new(ComputedData::min_from_constraint(
645 input.parent_constraint().as_ref(),
646 )))
647 }
648}
649
650fn compute_visible_pages(current_page: usize, page_count: usize, beyond: usize) -> Vec<usize> {
651 if page_count == 0 {
652 return Vec::new();
653 }
654 let extra = beyond.saturating_add(1);
655 let start = current_page.saturating_sub(extra);
656 let end = (current_page + extra + 1).min(page_count);
657 (start..end).collect()
658}
659
660fn resolve_page_main_size(
661 page_size: PagerPageSize,
662 main_dimension: AxisConstraint,
663 padding: Px,
664) -> Px {
665 match page_size {
666 PagerPageSize::Fill => {
667 let max = main_dimension
668 .resolve_max()
669 .expect("Pager page size Fill requires a bounded main-axis constraint");
670 (max - padding - padding).max(Px::ZERO)
671 }
672 PagerPageSize::Fixed(dp) => dp.into(),
673 }
674}
675
676fn cross_dimension_for_alignment(
677 cross_dimension: AxisConstraint,
678 alignment: CrossAxisAlignment,
679) -> AxisConstraint {
680 match alignment {
681 CrossAxisAlignment::Stretch => match cross_dimension.resolve_max() {
685 Some(max) => AxisConstraint::exact(max),
686 None => cross_dimension,
687 },
688 _ => cross_axis_child_constraint(cross_dimension, alignment),
689 }
690}
691
692fn compute_cross_offset(container: Px, child: Px, alignment: CrossAxisAlignment) -> Px {
693 match alignment {
694 CrossAxisAlignment::Start | CrossAxisAlignment::Stretch => Px::ZERO,
695 CrossAxisAlignment::Center => (container - child).max(Px::ZERO) / 2,
696 CrossAxisAlignment::End => (container - child).max(Px::ZERO),
697 }
698}
699
700fn sanitize_spacing(px: Px) -> Px {
701 if px < Px::ZERO { Px::ZERO } else { px }
702}
703
704struct PagerKeyboardModifierNode {
705 controller: State<PagerController>,
706 axis: PagerAxis,
707 user_scroll_enabled: bool,
708}
709
710impl KeyboardInputModifierNode for PagerKeyboardModifierNode {
711 fn on_keyboard_input(&self, mut input: KeyboardInput<'_>) {
712 if !self.user_scroll_enabled
713 || input.key_modifiers.control_key()
714 || input.key_modifiers.alt_key()
715 || input.key_modifiers.super_key()
716 {
717 return;
718 }
719
720 let mut handled = false;
721 for event in input.keyboard_events.iter() {
722 if event.state != winit::event::ElementState::Pressed {
723 continue;
724 }
725
726 let Some(command) = pager_keyboard_command(self.axis, &event.logical_key) else {
727 continue;
728 };
729
730 handled = run_pager_keyboard_command(self.controller, command);
731 if handled {
732 break;
733 }
734 }
735
736 if handled {
737 input.block_keyboard();
738 }
739 }
740}
741
742struct PagerPointerModifierNode {
743 controller: State<PagerController>,
744 axis: PagerAxis,
745 user_scroll_enabled: bool,
746 drag_recognizer: State<DragRecognizer>,
747 scroll_recognizer: State<ScrollRecognizer>,
748}
749
750impl PointerInputModifierNode for PagerPointerModifierNode {
751 fn on_pointer_input(&self, input: PointerInput<'_>) {
752 if !self.user_scroll_enabled {
753 return;
754 }
755
756 let is_cursor_in_component = input
757 .cursor_position_rel
758 .map(|pos| is_position_inside_bounds(input.computed_data, pos))
759 .unwrap_or(false);
760 let is_dragging = self.controller.with(|controller| controller.is_dragging());
761 if !is_cursor_in_component && !is_dragging {
762 return;
763 }
764
765 let frame_nanos = tessera_ui::current_frame_nanos();
766 let scroll_result = self.scroll_recognizer.with_mut(|recognizer| {
767 recognizer.update(input.pass, input.pointer_changes.as_mut_slice())
768 });
769 let (scroll_delta_x, scroll_delta_y) = normalize_pager_scroll_delta(
770 scroll_result.delta_x,
771 scroll_result.delta_y,
772 scroll_result.unit,
773 scroll_result.source,
774 );
775 let scroll_delta = self.axis.scroll_delta(scroll_delta_x, scroll_delta_y);
776
777 if scroll_delta.abs() >= 0.01 {
778 self.controller.with_mut(|controller| {
779 controller.apply_scroll_delta(scroll_delta, frame_nanos);
780 controller.end_drag();
781 });
782 return;
783 }
784
785 let drag_result = self.drag_recognizer.with_mut(|recognizer| {
786 recognizer.update(
787 input.pass,
788 input.pointer_changes.as_mut_slice(),
789 is_cursor_in_component,
790 )
791 });
792 if drag_result.started {
793 self.controller
794 .with_mut(|controller| controller.start_drag(frame_nanos));
795 }
796
797 let drag_delta = self.axis.scroll_delta(
798 input.local_delta(drag_result.delta_x.to_f32(), 0.0).0,
799 input.local_delta(0.0, drag_result.delta_y.to_f32()).1,
800 );
801 if drag_result.updated && drag_delta.abs() >= 0.01 {
802 self.controller
803 .with_mut(|controller| controller.apply_scroll_delta(drag_delta, frame_nanos));
804 }
805
806 if drag_result.ended {
807 self.controller.with_mut(|controller| controller.end_drag());
808 }
809 }
810}
811
812fn apply_pager_input_modifiers(
813 base: Modifier,
814 controller: State<PagerController>,
815 axis: PagerAxis,
816 user_scroll_enabled: bool,
817 drag_recognizer: State<DragRecognizer>,
818 scroll_recognizer: State<ScrollRecognizer>,
819) -> Modifier {
820 base.push_keyboard_input(PagerKeyboardModifierNode {
821 controller,
822 axis,
823 user_scroll_enabled,
824 })
825 .push_pointer_input(PagerPointerModifierNode {
826 controller,
827 axis,
828 user_scroll_enabled,
829 drag_recognizer,
830 scroll_recognizer,
831 })
832}
833
834#[tessera]
873pub fn horizontal_pager(
874 modifier: Option<Modifier>,
875 page_count: Option<usize>,
876 initial_page: Option<usize>,
877 page_size: Option<PagerPageSize>,
878 page_spacing: Option<Dp>,
879 content_padding: Option<Dp>,
880 beyond_viewport_page_count: Option<usize>,
881 cross_axis_alignment: Option<CrossAxisAlignment>,
882 user_scroll_enabled: Option<bool>,
883 snap_threshold: Option<f32>,
884 scroll_smoothing: Option<f32>,
885 page_content: Option<CallbackWith<usize>>,
886 controller: Option<State<PagerController>>,
887) {
888 let page_count = page_count.unwrap_or(PagerConfig::default().page_count);
889 let initial_page = initial_page.unwrap_or(PagerConfig::default().initial_page);
890 let page_size = page_size.unwrap_or(PagerConfig::default().page_size);
891 let page_spacing = page_spacing.unwrap_or(PagerConfig::default().page_spacing);
892 let content_padding = content_padding.unwrap_or(PagerConfig::default().content_padding);
893 let beyond_viewport_page_count =
894 beyond_viewport_page_count.unwrap_or(PagerConfig::default().beyond_viewport_page_count);
895 let cross_axis_alignment =
896 cross_axis_alignment.unwrap_or(PagerConfig::default().cross_axis_alignment);
897 let user_scroll_enabled =
898 user_scroll_enabled.unwrap_or(PagerConfig::default().user_scroll_enabled);
899 let pager_args = pager_config_from_params(PagerParams {
900 modifier,
901 page_count,
902 initial_page,
903 page_size,
904 page_spacing,
905 content_padding,
906 beyond_viewport_page_count,
907 cross_axis_alignment,
908 user_scroll_enabled,
909 snap_threshold,
910 scroll_smoothing,
911 page_content,
912 controller,
913 });
914 let controller = match pager_args.controller {
915 Some(value) => value,
916 None => remember(|| PagerController::new(pager_args.initial_page)),
917 };
918 let drag_recognizer = remember(|| {
919 DragRecognizer::new(DragSettings {
920 axis: Some(DragAxis::Horizontal),
921 ..DragSettings::default()
922 })
923 });
924 let scroll_recognizer = remember(|| ScrollRecognizer::new(ScrollSettings { consume: true }));
925 let frame_nanos = tessera_ui::current_frame_nanos();
926 if controller.with(|current| current.has_pending_animation_frame(frame_nanos)) {
927 let snap_threshold = pager_args.snap_threshold;
928 let scroll_smoothing = pager_args.scroll_smoothing;
929 receive_frame_nanos(move |frame_nanos| {
930 let has_pending_animation_frame = controller.with_mut(|current| {
931 current.tick(frame_nanos, snap_threshold, scroll_smoothing);
932 current.has_pending_animation_frame(frame_nanos)
933 });
934 if has_pending_animation_frame {
935 tessera_ui::FrameNanosControl::Continue
936 } else {
937 tessera_ui::FrameNanosControl::Stop
938 }
939 });
940 }
941 pager_render(
942 pager_args,
943 controller,
944 PagerAxis::Horizontal,
945 drag_recognizer,
946 scroll_recognizer,
947 );
948}
949
950#[tessera]
986pub fn vertical_pager(
987 modifier: Option<Modifier>,
988 page_count: Option<usize>,
989 initial_page: Option<usize>,
990 page_size: Option<PagerPageSize>,
991 page_spacing: Option<Dp>,
992 content_padding: Option<Dp>,
993 beyond_viewport_page_count: Option<usize>,
994 cross_axis_alignment: Option<CrossAxisAlignment>,
995 user_scroll_enabled: Option<bool>,
996 snap_threshold: Option<f32>,
997 scroll_smoothing: Option<f32>,
998 page_content: Option<CallbackWith<usize>>,
999 controller: Option<State<PagerController>>,
1000) {
1001 let page_count = page_count.unwrap_or(PagerConfig::default().page_count);
1002 let initial_page = initial_page.unwrap_or(PagerConfig::default().initial_page);
1003 let page_size = page_size.unwrap_or(PagerConfig::default().page_size);
1004 let page_spacing = page_spacing.unwrap_or(PagerConfig::default().page_spacing);
1005 let content_padding = content_padding.unwrap_or(PagerConfig::default().content_padding);
1006 let beyond_viewport_page_count =
1007 beyond_viewport_page_count.unwrap_or(PagerConfig::default().beyond_viewport_page_count);
1008 let cross_axis_alignment =
1009 cross_axis_alignment.unwrap_or(PagerConfig::default().cross_axis_alignment);
1010 let user_scroll_enabled =
1011 user_scroll_enabled.unwrap_or(PagerConfig::default().user_scroll_enabled);
1012 let pager_args = pager_config_from_params(PagerParams {
1013 modifier,
1014 page_count,
1015 initial_page,
1016 page_size,
1017 page_spacing,
1018 content_padding,
1019 beyond_viewport_page_count,
1020 cross_axis_alignment,
1021 user_scroll_enabled,
1022 snap_threshold,
1023 scroll_smoothing,
1024 page_content,
1025 controller,
1026 });
1027 let controller = match pager_args.controller {
1028 Some(value) => value,
1029 None => remember(|| PagerController::new(pager_args.initial_page)),
1030 };
1031 let drag_recognizer = remember(|| {
1032 DragRecognizer::new(DragSettings {
1033 axis: Some(DragAxis::Vertical),
1034 ..DragSettings::default()
1035 })
1036 });
1037 let scroll_recognizer = remember(|| ScrollRecognizer::new(ScrollSettings { consume: true }));
1038 let frame_nanos = tessera_ui::current_frame_nanos();
1039 if controller.with(|current| current.has_pending_animation_frame(frame_nanos)) {
1040 let snap_threshold = pager_args.snap_threshold;
1041 let scroll_smoothing = pager_args.scroll_smoothing;
1042 receive_frame_nanos(move |frame_nanos| {
1043 let has_pending_animation_frame = controller.with_mut(|current| {
1044 current.tick(frame_nanos, snap_threshold, scroll_smoothing);
1045 current.has_pending_animation_frame(frame_nanos)
1046 });
1047 if has_pending_animation_frame {
1048 tessera_ui::FrameNanosControl::Continue
1049 } else {
1050 tessera_ui::FrameNanosControl::Stop
1051 }
1052 });
1053 }
1054 pager_render(
1055 pager_args,
1056 controller,
1057 PagerAxis::Vertical,
1058 drag_recognizer,
1059 scroll_recognizer,
1060 );
1061}
1062
1063#[tessera]
1064fn pager_render(
1065 args: PagerConfig,
1066 controller: State<PagerController>,
1067 axis: PagerAxis,
1068 drag_recognizer: State<DragRecognizer>,
1069 scroll_recognizer: State<ScrollRecognizer>,
1070) {
1071 let page_content = args.page_content;
1072 let should_set_page_count = controller.with(|current| {
1073 let mut next = current.clone();
1074 next.set_page_count(args.page_count);
1075 next != *current
1076 });
1077 if should_set_page_count {
1078 controller.with_mut(|current| current.set_page_count(args.page_count));
1079 }
1080
1081 let current_page = controller.with(|current| current.current_page());
1082 let visible_pages = compute_visible_pages(
1083 current_page,
1084 args.page_count,
1085 args.beyond_viewport_page_count,
1086 );
1087
1088 if visible_pages.is_empty() {
1089 layout().modifier(args.modifier.clone()).policy(ZeroLayout);
1090 return;
1091 }
1092
1093 let modifier = apply_pager_input_modifiers(
1094 args.modifier.clone().focusable().focus_properties(
1095 FocusProperties::new()
1096 .can_focus(args.user_scroll_enabled && args.page_count > 1)
1097 .can_request_focus(args.user_scroll_enabled && args.page_count > 1),
1098 ),
1099 controller,
1100 axis,
1101 args.user_scroll_enabled,
1102 drag_recognizer,
1103 scroll_recognizer,
1104 );
1105
1106 let policy = PagerLayout {
1107 axis,
1108 cross_axis_alignment: args.cross_axis_alignment,
1109 page_size: args.page_size,
1110 page_spacing: sanitize_spacing(Px::from(args.page_spacing)),
1111 content_padding: sanitize_spacing(Px::from(args.content_padding)),
1112 page_count: args.page_count,
1113 visible_pages: visible_pages.clone(),
1114 scroll_offset: controller.with(|current| current.scroll_offset_px()),
1115 controller,
1116 };
1117 layout()
1118 .modifier(modifier)
1119 .policy(policy.clone())
1120 .draw(policy)
1121 .child(move || {
1122 for page_index in visible_pages.iter().copied() {
1123 key(page_index, || {
1124 page_content.call(page_index);
1125 });
1126 }
1127 });
1128}
1129
1130#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1131enum PagerKeyboardCommand {
1132 Previous,
1133 Next,
1134 First,
1135 Last,
1136}
1137
1138fn pager_keyboard_command(
1139 axis: PagerAxis,
1140 logical_key: &winit::keyboard::Key,
1141) -> Option<PagerKeyboardCommand> {
1142 use winit::keyboard::{Key, NamedKey};
1143
1144 match logical_key {
1145 Key::Named(NamedKey::Home) => Some(PagerKeyboardCommand::First),
1146 Key::Named(NamedKey::End) => Some(PagerKeyboardCommand::Last),
1147 Key::Named(NamedKey::PageUp) => Some(PagerKeyboardCommand::Previous),
1148 Key::Named(NamedKey::PageDown) => Some(PagerKeyboardCommand::Next),
1149 Key::Named(NamedKey::ArrowLeft) if axis == PagerAxis::Horizontal => {
1150 Some(PagerKeyboardCommand::Previous)
1151 }
1152 Key::Named(NamedKey::ArrowRight) if axis == PagerAxis::Horizontal => {
1153 Some(PagerKeyboardCommand::Next)
1154 }
1155 Key::Named(NamedKey::ArrowUp) if axis == PagerAxis::Vertical => {
1156 Some(PagerKeyboardCommand::Previous)
1157 }
1158 Key::Named(NamedKey::ArrowDown) if axis == PagerAxis::Vertical => {
1159 Some(PagerKeyboardCommand::Next)
1160 }
1161 _ => None,
1162 }
1163}
1164
1165fn run_pager_keyboard_command(
1166 controller: State<PagerController>,
1167 command: PagerKeyboardCommand,
1168) -> bool {
1169 controller.with_mut(|controller| {
1170 if controller.page_count <= 1 {
1171 return false;
1172 }
1173
1174 let target = match command {
1175 PagerKeyboardCommand::Previous => {
1176 if controller.current_page == 0 {
1177 return false;
1178 }
1179 controller.current_page - 1
1180 }
1181 PagerKeyboardCommand::Next => {
1182 if controller.current_page + 1 >= controller.page_count {
1183 return false;
1184 }
1185 controller.current_page + 1
1186 }
1187 PagerKeyboardCommand::First => {
1188 if controller.current_page == 0 {
1189 return false;
1190 }
1191 0
1192 }
1193 PagerKeyboardCommand::Last => {
1194 let last_page = controller.page_count - 1;
1195 if controller.current_page == last_page {
1196 return false;
1197 }
1198 last_page
1199 }
1200 };
1201
1202 controller.scroll_to_page(target);
1203 true
1204 })
1205}