1use blitz_traits::events::{BlitzScrollEvent, DomEvent, DomEventData};
5use blitz_traits::node_id::NodeId;
6use style::values::computed::Overflow;
7use web_time::{SystemTime, UNIX_EPOCH};
8
9use crate::BaseDocument;
10use crate::util::Point;
11
12#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
13pub enum ScrollBehavior {
14 #[default]
15 Auto,
16 Instant,
17 Smooth,
18}
19
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub enum ScrollLogicalPosition {
22 Start,
23 Center,
24 End,
25 Nearest,
26}
27
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
34pub(crate) enum ScrollTarget {
35 Node(NodeId),
36 Viewport,
37}
38
39#[derive(Clone, Copy, Debug, PartialEq)]
41pub(crate) enum ScrollAmount {
42 To(Point<f64>),
44 By(Point<f64>),
46}
47
48#[derive(Clone, Copy, Debug, PartialEq, Eq)]
50pub(crate) enum ScrollOverflow {
51 Clamp,
53 Chain,
56}
57
58#[derive(Clone, Copy, Debug, PartialEq, Eq)]
62pub(crate) enum ScrollSource {
63 User,
65 Programmatic,
67}
68
69#[derive(Clone, Copy, Debug, PartialEq)]
70pub(crate) struct ScrollRequest {
71 pub(crate) target: ScrollTarget,
72 pub(crate) amount: ScrollAmount,
73 pub(crate) overflow: ScrollOverflow,
74 pub(crate) source: ScrollSource,
75 pub(crate) behavior: ScrollBehavior,
76 pub(crate) interrupt_animation: bool,
79}
80
81#[derive(Debug, Clone, PartialEq)]
82pub(crate) struct FlingState {
83 pub(crate) target: NodeId,
84 pub(crate) last_seen_time: f64,
85 pub(crate) x_velocity: f64,
86 pub(crate) y_velocity: f64,
87}
88
89#[derive(Debug, Clone, PartialEq)]
92pub(crate) struct ScrollToState {
93 pub(crate) target: ScrollTarget,
95 pub(crate) start: Point<f64>,
97 pub(crate) end: Point<f64>,
99 pub(crate) start_time: f64,
101 pub(crate) duration: f64,
103}
104
105#[derive(Debug, Clone, PartialEq)]
106pub(crate) enum ScrollAnimationState {
107 None,
108 Fling(FlingState),
109 ScrollTo(ScrollToState),
111}
112
113fn ease_in_out_cubic(t: f64) -> f64 {
117 if t < 0.5 {
118 4.0 * t * t * t
119 } else {
120 let f = 2.0 * t - 2.0;
121 1.0 + (f * f * f) / 2.0
122 }
123}
124
125impl BaseDocument {
126 pub(crate) fn scroll(
132 &mut self,
133 request: ScrollRequest,
134 dispatch_event: &mut dyn FnMut(DomEvent),
135 ) -> bool {
136 if request.interrupt_animation
137 && matches!(self.scroll_animation, ScrollAnimationState::ScrollTo(_))
138 {
139 self.scroll_animation = ScrollAnimationState::None;
140 }
141
142 let target = self.canonical_scroll_target(request.target);
143
144 if let (ScrollTarget::Node(node_id), ScrollAmount::By(delta), ScrollOverflow::Chain) =
147 (target, request.amount, request.overflow)
148 {
149 if let Some(has_changed) =
150 self.scroll_inner_content_by(node_id, delta, request, dispatch_event)
151 {
152 return has_changed;
153 }
154 }
155
156 let include_hidden = request.source == ScrollSource::Programmatic;
157 let (current, max) = self.scroll_state(target, include_hidden);
158 let unclamped = match request.amount {
159 ScrollAmount::To(to) => to,
160 ScrollAmount::By(by) => Point {
161 x: current.x + by.x,
162 y: current.y + by.y,
163 },
164 };
165 let end = Point {
166 x: unclamped.x.clamp(0.0, max.x),
167 y: unclamped.y.clamp(0.0, max.y),
168 };
169
170 if self.should_scroll_smoothly(target, request.behavior) {
171 self.start_scroll_animation(target, end);
172 return end != current;
173 }
174
175 let has_changed = self.write_scroll_offset(target, end, dispatch_event);
176
177 if request.overflow == ScrollOverflow::Chain {
179 let remainder = Point {
180 x: unclamped.x - end.x,
181 y: unclamped.y - end.y,
182 };
183 if remainder != Point::ZERO {
184 if let Some(next) = self.next_scroller_in_chain(target) {
185 let request = ScrollRequest {
186 target: next,
187 amount: ScrollAmount::By(remainder),
188 ..request
189 };
190 return has_changed | self.scroll(request, dispatch_event);
191 }
192 }
193 }
194
195 has_changed
196 }
197
198 fn scroll_inner_content_by(
201 &mut self,
202 node_id: NodeId,
203 delta: Point<f64>,
204 request: ScrollRequest,
205 dispatch_event: &mut dyn FnMut(DomEvent),
206 ) -> Option<bool> {
207 let node = self.nodes.get_mut(node_id)?;
208
209 if let Some(mut sub_doc) = node.subdoc_mut().map(|doc| doc.inner_mut()) {
210 let target = sub_doc
211 .get_hover_node_id()
212 .map_or(ScrollTarget::Viewport, ScrollTarget::Node);
213 let request = ScrollRequest {
215 target,
216 amount: ScrollAmount::By(delta),
217 ..request
218 };
219 return Some(sub_doc.scroll(request, dispatch_event));
220 }
221
222 node.element_data()?.text_input_data()?;
224 let content_box_width = node.final_layout().content_box_width();
225 let content_box_height = node.final_layout().content_box_height();
226 let input = node
227 .element_data_mut()
228 .and_then(|el| el.text_input_data_mut())
229 .unwrap();
230
231 let mut remainder = delta;
234 if input.is_multiline {
235 remainder.y =
236 -(input.scroll_by(-delta.y as f32, content_box_width, content_box_height) as f64);
237 } else {
238 remainder.x =
239 -(input.scroll_by(-delta.x as f32, content_box_width, content_box_height) as f64);
240 }
241
242 let has_changed = remainder != delta;
243
244 if remainder != Point::ZERO {
245 if let Some(next) = self.next_scroller_in_chain(ScrollTarget::Node(node_id)) {
246 let request = ScrollRequest {
247 target: next,
248 amount: ScrollAmount::By(remainder),
249 ..request
250 };
251 return Some(has_changed | self.scroll(request, dispatch_event));
252 }
253 }
254
255 Some(has_changed)
256 }
257
258 fn next_scroller_in_chain(&self, target: ScrollTarget) -> Option<ScrollTarget> {
261 match target {
262 ScrollTarget::Viewport => None,
263 ScrollTarget::Node(node_id) => Some(
264 self.nodes
265 .get(node_id)
266 .and_then(|node| node.parent)
267 .map_or(ScrollTarget::Viewport, ScrollTarget::Node),
268 ),
269 }
270 }
271
272 fn canonical_scroll_target(&self, target: ScrollTarget) -> ScrollTarget {
275 match target {
276 ScrollTarget::Node(node_id)
277 if self.try_root_element().is_some_and(|el| el.id == node_id) =>
278 {
279 ScrollTarget::Viewport
280 }
281 target => target,
282 }
283 }
284
285 pub(crate) fn write_scroll_offset(
288 &mut self,
289 target: ScrollTarget,
290 offset: Point<f64>,
291 dispatch_event: &mut dyn FnMut(DomEvent),
292 ) -> bool {
293 match self.canonical_scroll_target(target) {
294 ScrollTarget::Viewport => {
295 let initial = self.viewport_scroll();
296 self.set_viewport_scroll(offset);
297 if offset == initial {
298 return false;
299 }
300
301 if let Some(root) = self.try_root_element() {
302 let root_id = root.id;
303 let layout = *root.final_layout();
304 let scale = self.viewport.scale() as f64;
305 let event = BlitzScrollEvent {
306 scroll_top: offset.y,
307 scroll_left: offset.x,
308 scroll_width: layout.size.width.max(layout.scrollable_overflow_rect.right)
309 as i32,
310 scroll_height: layout
311 .size
312 .height
313 .max(layout.scrollable_overflow_rect.bottom)
314 as i32,
315 client_width: (self.viewport.window_size.0 as f64 / scale) as i32,
316 client_height: (self.viewport.window_size.1 as f64 / scale) as i32,
317 };
318 dispatch_event(DomEvent::new(root_id, DomEventData::Scroll(event)));
319 }
320
321 self.shell_provider.request_redraw();
322 true
323 }
324 ScrollTarget::Node(node_id) => {
325 let Some(node) = self.nodes.get_mut(node_id) else {
326 return false;
327 };
328
329 let initial = *node.scroll_offset();
330 *node.scroll_offset_mut() = offset;
331 if offset == initial {
332 return false;
333 }
334
335 let layout = *node.final_layout();
336 self.nodes.bump_geometry_generation();
337 let event = BlitzScrollEvent {
338 scroll_top: offset.y,
339 scroll_left: offset.x,
340 scroll_width: layout.scroll_width() as i32,
341 scroll_height: layout.scroll_height() as i32,
342 client_width: layout.size.width as i32,
343 client_height: layout.size.height as i32,
344 };
345 dispatch_event(DomEvent::new(node_id, DomEventData::Scroll(event)));
346
347 self.show_scrollbars(node_id);
348 self.shell_provider.request_redraw();
349 true
350 }
351 }
352 }
353
354 pub fn scroll_node_by<F: FnMut(DomEvent)>(
355 &mut self,
356 node_id: NodeId,
357 x: f64,
358 y: f64,
359 dispatch_event: F,
360 ) {
361 self.scroll_node_by_has_changed(node_id, x, y, dispatch_event);
362 }
363
364 pub fn scroll_node_by_has_changed<F: FnMut(DomEvent)>(
368 &mut self,
369 node_id: NodeId,
370 x: f64,
371 y: f64,
372 mut dispatch_event: F,
373 ) -> bool {
374 self.scroll(
375 ScrollRequest {
376 target: ScrollTarget::Node(node_id),
377 amount: ScrollAmount::By(Point { x: -x, y: -y }),
380 overflow: ScrollOverflow::Chain,
381 source: ScrollSource::User,
382 behavior: ScrollBehavior::Instant,
383 interrupt_animation: false,
384 },
385 &mut dispatch_event,
386 )
387 }
388
389 pub fn scroll_viewport_by(&mut self, x: f64, y: f64) {
390 self.scroll_viewport_by_has_changed(x, y);
391 }
392
393 pub fn scroll_viewport_by_has_changed(&mut self, x: f64, y: f64) -> bool {
395 self.scroll(
396 ScrollRequest {
397 target: ScrollTarget::Viewport,
398 amount: ScrollAmount::By(Point { x: -x, y: -y }),
399 overflow: ScrollOverflow::Clamp,
400 source: ScrollSource::User,
401 behavior: ScrollBehavior::Instant,
402 interrupt_animation: false,
403 },
404 &mut |_| {},
405 )
406 }
407
408 pub(crate) fn scroll_chain_by(
409 &mut self,
410 anchor_node_id: Option<NodeId>,
411 scroll_x: f64,
412 scroll_y: f64,
413 dispatch_event: &mut dyn FnMut(DomEvent),
414 ) -> bool {
415 self.scroll(
416 ScrollRequest {
417 target: anchor_node_id.map_or(ScrollTarget::Viewport, ScrollTarget::Node),
418 amount: ScrollAmount::By(Point {
419 x: -scroll_x,
420 y: -scroll_y,
421 }),
422 overflow: ScrollOverflow::Chain,
423 source: ScrollSource::User,
424 behavior: ScrollBehavior::Instant,
425 interrupt_animation: true,
428 },
429 dispatch_event,
430 )
431 }
432
433 const SMOOTH_SCROLL_DURATION_MS: f64 = 300.0;
435
436 pub(crate) fn scroll_state(
441 &self,
442 target: ScrollTarget,
443 include_hidden: bool,
444 ) -> (Point<f64>, Point<f64>) {
445 match self.canonical_scroll_target(target) {
446 ScrollTarget::Viewport => {
447 let (content_width, content_height) = match self.try_root_element() {
452 Some(root) => {
453 let layout = root.final_layout();
454 (
455 layout.size.width.max(layout.scrollable_overflow_rect.right) as f64,
456 layout
457 .size
458 .height
459 .max(layout.scrollable_overflow_rect.bottom)
460 as f64,
461 )
462 }
463 None => (0.0, 0.0),
464 };
465 let scale = self.viewport.scale() as f64;
466 let window_width = self.viewport.window_size.0 as f64 / scale;
467 let window_height = self.viewport.window_size.1 as f64 / scale;
468 let max = Point {
469 x: (content_width - window_width).max(0.0),
470 y: (content_height - window_height).max(0.0),
471 };
472 (self.viewport_scroll(), max)
473 }
474 ScrollTarget::Node(node_id) => {
475 let Some(node) = self.nodes.get(node_id) else {
476 return (Point::ZERO, Point::ZERO);
477 };
478
479 let scrollable = |overflow| match overflow {
482 Overflow::Scroll | Overflow::Auto => true,
483 Overflow::Hidden => include_hidden,
484 _ => false,
485 };
486 let (can_x_scroll, can_y_scroll) = node
487 .primary_styles()
488 .map(|styles| {
489 (
490 scrollable(styles.clone_overflow_x()),
491 scrollable(styles.clone_overflow_y()),
492 )
493 })
494 .unwrap_or((false, false));
495 let max = Point {
496 x: match can_x_scroll {
497 true => node.final_layout().scroll_width() as f64,
498 false => 0.0,
499 },
500 y: match can_y_scroll {
501 true => node.final_layout().scroll_height() as f64,
502 false => 0.0,
503 },
504 };
505 (*node.scroll_offset(), max)
506 }
507 }
508 }
509
510 fn start_scroll_animation(&mut self, target: ScrollTarget, end: Point<f64>) {
513 let start = self.scroll_state(target, true).0;
514
515 let start_time = SystemTime::now()
516 .duration_since(UNIX_EPOCH)
517 .unwrap()
518 .as_millis() as u64 as f64;
519
520 self.scroll_animation = ScrollAnimationState::ScrollTo(ScrollToState {
521 target,
522 start,
523 end,
524 start_time,
525 duration: Self::SMOOTH_SCROLL_DURATION_MS,
526 });
527
528 self.shell_provider.request_redraw();
530 }
531
532 fn should_scroll_smoothly(&self, target: ScrollTarget, behavior: ScrollBehavior) -> bool {
533 match behavior {
534 ScrollBehavior::Auto => {
535 let styled_node = match target {
536 ScrollTarget::Node(node_id) => self.nodes.get(node_id),
537 ScrollTarget::Viewport => self.try_root_element(),
538 };
539 styled_node.is_some_and(|node| {
540 node.primary_styles().is_some_and(|style| {
541 style.clone_scroll_behavior()
542 == style::computed_values::scroll_behavior::T::Smooth
543 })
544 })
545 }
546 ScrollBehavior::Instant => false,
547 ScrollBehavior::Smooth => true,
548 }
549 }
550
551 pub fn scroll_to(&mut self, node_id: NodeId, x: f64, y: f64, behavior: ScrollBehavior) {
556 self.scroll_programmatically(node_id, ScrollAmount::To(Point { x, y }), behavior);
557 }
558
559 pub fn scroll_by(&mut self, node_id: NodeId, x: f64, y: f64, behavior: ScrollBehavior) {
561 self.scroll_programmatically(node_id, ScrollAmount::By(Point { x, y }), behavior);
562 }
563
564 fn scroll_programmatically(
565 &mut self,
566 node_id: NodeId,
567 amount: ScrollAmount,
568 behavior: ScrollBehavior,
569 ) {
570 if self.nodes.get(node_id).is_none() {
571 return;
572 }
573
574 self.scroll(
576 ScrollRequest {
577 target: ScrollTarget::Node(node_id),
578 amount,
579 overflow: ScrollOverflow::Clamp,
580 source: ScrollSource::Programmatic,
581 behavior,
582 interrupt_animation: true,
583 },
584 &mut |_| {},
585 );
586 }
587
588 fn aligned_scroll_offset(
589 current: f64,
590 viewport_size: f64,
591 target_start: f64,
592 target_size: f64,
593 position: ScrollLogicalPosition,
594 ) -> f64 {
595 let target_end = target_start + target_size;
596 match position {
597 ScrollLogicalPosition::Start => target_start,
598 ScrollLogicalPosition::Center => target_start - (viewport_size - target_size) / 2.0,
599 ScrollLogicalPosition::End => target_end - viewport_size,
600 ScrollLogicalPosition::Nearest => {
601 let viewport_end = current + viewport_size;
602 if (target_start >= current && target_end <= viewport_end)
603 || (target_start <= current && target_end >= viewport_end)
604 {
605 current
606 } else {
607 let start_offset = target_start;
608 let end_offset = target_end - viewport_size;
609 if (start_offset - current).abs() < (end_offset - current).abs() {
610 start_offset
611 } else {
612 end_offset
613 }
614 }
615 }
616 }
617 }
618
619 pub fn scroll_into_view(
621 &mut self,
622 node_id: NodeId,
623 behavior: ScrollBehavior,
624 vertical: ScrollLogicalPosition,
625 horizontal: ScrollLogicalPosition,
626 ) {
627 let Some(node) = self.nodes.get(node_id) else {
628 return;
629 };
630 let target =
631 node.absolute_position(node.scroll_offset().x as f32, node.scroll_offset().y as f32);
632 let target_size = node.final_layout().size;
633 let Some(root_id) = self.try_root_element().map(|root| root.id) else {
634 return;
635 };
636 let scale = self.viewport.scale() as f64;
637 let viewport_width = self.viewport.window_size.0 as f64 / scale;
638 let viewport_height = self.viewport.window_size.1 as f64 / scale;
639 let x = Self::aligned_scroll_offset(
640 self.viewport_scroll().x,
641 viewport_width,
642 target.x as f64,
643 target_size.width as f64,
644 horizontal,
645 );
646 let y = Self::aligned_scroll_offset(
647 self.viewport_scroll().y,
648 viewport_height,
649 target.y as f64,
650 target_size.height as f64,
651 vertical,
652 );
653 self.scroll_to(root_id, x, y, behavior);
654 }
655
656 fn resolve_fragment_scroll_target(&self, fragment: &str) -> Option<Option<NodeId>> {
663 let decoded = percent_encoding::percent_decode_str(fragment)
665 .decode_utf8_lossy()
666 .into_owned();
667
668 if !decoded.is_empty() {
669 if let Some(node_id) = self.get_fragment_target(&decoded) {
670 return Some(Some(node_id));
671 }
672 }
673
674 if decoded.is_empty() || decoded.eq_ignore_ascii_case("top") {
677 return Some(None);
678 }
679
680 None
681 }
682
683 fn scroll_to_fragment_with_behavior(
684 &mut self,
685 fragment: &str,
686 behavior: ScrollBehavior,
687 ) -> bool {
688 match self.resolve_fragment_scroll_target(fragment) {
689 Some(Some(node_id)) => {
690 self.scroll_into_view(
691 node_id,
692 behavior,
693 ScrollLogicalPosition::Start,
694 ScrollLogicalPosition::Nearest,
695 );
696 true
697 }
698 Some(None) => {
699 let root_id = self.root_element().id;
700 self.scroll_to(root_id, 0.0, 0.0, behavior);
701 true
702 }
703 None => false,
704 }
705 }
706
707 pub fn scroll_to_fragment(&mut self, fragment: &str) -> bool {
713 self.scroll_to_fragment_with_behavior(fragment, ScrollBehavior::Auto)
714 }
715
716 pub fn scroll_to_fragment_smooth(&mut self, fragment: &str) -> bool {
719 self.scroll_to_fragment_with_behavior(fragment, ScrollBehavior::Smooth)
720 }
721
722 pub fn resolve_scroll_animation(&mut self) {
723 match &mut self.scroll_animation {
724 ScrollAnimationState::Fling(fling_state) => {
725 let time_ms = SystemTime::now()
726 .duration_since(UNIX_EPOCH)
727 .unwrap()
728 .as_millis() as u64 as f64;
729
730 let time_diff_ms = time_ms - fling_state.last_seen_time;
731
732 let deceleration = 1.0 - ((0.05 / 16.66666) * time_diff_ms);
734
735 fling_state.x_velocity *= deceleration;
736 fling_state.y_velocity *= deceleration;
737 fling_state.last_seen_time = time_ms;
738 let fling_state = fling_state.clone();
739
740 let dx = fling_state.x_velocity * time_diff_ms;
741 let dy = fling_state.y_velocity * time_diff_ms;
742
743 self.scroll_chain_by(Some(fling_state.target), dx, dy, &mut |_| {});
744 if fling_state.x_velocity.abs() < 0.1 && fling_state.y_velocity.abs() < 0.1 {
745 self.scroll_animation = ScrollAnimationState::None;
746 }
747 }
748 ScrollAnimationState::ScrollTo(scroll_to) => {
749 let scroll_to = scroll_to.clone();
750 let time_ms = SystemTime::now()
751 .duration_since(UNIX_EPOCH)
752 .unwrap()
753 .as_millis() as u64 as f64;
754
755 let progress = if scroll_to.duration <= 0.0 {
757 1.0
758 } else {
759 ((time_ms - scroll_to.start_time) / scroll_to.duration).clamp(0.0, 1.0)
760 };
761 let eased = ease_in_out_cubic(progress);
762
763 let target = Point {
765 x: scroll_to.start.x + (scroll_to.end.x - scroll_to.start.x) * eased,
766 y: scroll_to.start.y + (scroll_to.end.y - scroll_to.start.y) * eased,
767 };
768 self.write_scroll_offset(scroll_to.target, target, &mut |_| {});
770
771 if progress >= 1.0 {
772 self.scroll_animation = ScrollAnimationState::None;
773 }
774 }
775 ScrollAnimationState::None => {
776 }
778 }
779 }
780}