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.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 let event = BlitzScrollEvent {
337 scroll_top: offset.y,
338 scroll_left: offset.x,
339 scroll_width: layout.scroll_width() as i32,
340 scroll_height: layout.scroll_height() as i32,
341 client_width: layout.size.width as i32,
342 client_height: layout.size.height as i32,
343 };
344 dispatch_event(DomEvent::new(node_id, DomEventData::Scroll(event)));
345
346 self.show_scrollbars(node_id);
347 self.shell_provider.request_redraw();
348 true
349 }
350 }
351 }
352
353 pub fn scroll_node_by<F: FnMut(DomEvent)>(
354 &mut self,
355 node_id: NodeId,
356 x: f64,
357 y: f64,
358 dispatch_event: F,
359 ) {
360 self.scroll_node_by_has_changed(node_id, x, y, dispatch_event);
361 }
362
363 pub fn scroll_node_by_has_changed<F: FnMut(DomEvent)>(
367 &mut self,
368 node_id: NodeId,
369 x: f64,
370 y: f64,
371 mut dispatch_event: F,
372 ) -> bool {
373 self.scroll(
374 ScrollRequest {
375 target: ScrollTarget::Node(node_id),
376 amount: ScrollAmount::By(Point { x: -x, y: -y }),
379 overflow: ScrollOverflow::Chain,
380 source: ScrollSource::User,
381 behavior: ScrollBehavior::Instant,
382 interrupt_animation: false,
383 },
384 &mut dispatch_event,
385 )
386 }
387
388 pub fn scroll_viewport_by(&mut self, x: f64, y: f64) {
389 self.scroll_viewport_by_has_changed(x, y);
390 }
391
392 pub fn scroll_viewport_by_has_changed(&mut self, x: f64, y: f64) -> bool {
394 self.scroll(
395 ScrollRequest {
396 target: ScrollTarget::Viewport,
397 amount: ScrollAmount::By(Point { x: -x, y: -y }),
398 overflow: ScrollOverflow::Clamp,
399 source: ScrollSource::User,
400 behavior: ScrollBehavior::Instant,
401 interrupt_animation: false,
402 },
403 &mut |_| {},
404 )
405 }
406
407 pub(crate) fn scroll_chain_by(
408 &mut self,
409 anchor_node_id: Option<NodeId>,
410 scroll_x: f64,
411 scroll_y: f64,
412 dispatch_event: &mut dyn FnMut(DomEvent),
413 ) -> bool {
414 self.scroll(
415 ScrollRequest {
416 target: anchor_node_id.map_or(ScrollTarget::Viewport, ScrollTarget::Node),
417 amount: ScrollAmount::By(Point {
418 x: -scroll_x,
419 y: -scroll_y,
420 }),
421 overflow: ScrollOverflow::Chain,
422 source: ScrollSource::User,
423 behavior: ScrollBehavior::Instant,
424 interrupt_animation: true,
427 },
428 dispatch_event,
429 )
430 }
431
432 const SMOOTH_SCROLL_DURATION_MS: f64 = 300.0;
434
435 pub(crate) fn scroll_state(
440 &self,
441 target: ScrollTarget,
442 include_hidden: bool,
443 ) -> (Point<f64>, Point<f64>) {
444 match self.canonical_scroll_target(target) {
445 ScrollTarget::Viewport => {
446 let (content_width, content_height) = match self.try_root_element() {
451 Some(root) => {
452 let layout = root.final_layout();
453 (
454 layout.size.width.max(layout.scrollable_overflow_rect.right) as f64,
455 layout
456 .size
457 .height
458 .max(layout.scrollable_overflow_rect.bottom)
459 as f64,
460 )
461 }
462 None => (0.0, 0.0),
463 };
464 let scale = self.viewport.scale() as f64;
465 let window_width = self.viewport.window_size.0 as f64 / scale;
466 let window_height = self.viewport.window_size.1 as f64 / scale;
467 let max = Point {
468 x: (content_width - window_width).max(0.0),
469 y: (content_height - window_height).max(0.0),
470 };
471 (self.viewport_scroll, max)
472 }
473 ScrollTarget::Node(node_id) => {
474 let Some(node) = self.nodes.get(node_id) else {
475 return (Point::ZERO, Point::ZERO);
476 };
477
478 let scrollable = |overflow| match overflow {
481 Overflow::Scroll | Overflow::Auto => true,
482 Overflow::Hidden => include_hidden,
483 _ => false,
484 };
485 let (can_x_scroll, can_y_scroll) = node
486 .primary_styles()
487 .map(|styles| {
488 (
489 scrollable(styles.clone_overflow_x()),
490 scrollable(styles.clone_overflow_y()),
491 )
492 })
493 .unwrap_or((false, false));
494 let max = Point {
495 x: match can_x_scroll {
496 true => node.final_layout().scroll_width() as f64,
497 false => 0.0,
498 },
499 y: match can_y_scroll {
500 true => node.final_layout().scroll_height() as f64,
501 false => 0.0,
502 },
503 };
504 (*node.scroll_offset(), max)
505 }
506 }
507 }
508
509 fn start_scroll_animation(&mut self, target: ScrollTarget, end: Point<f64>) {
512 let start = self.scroll_state(target, true).0;
513
514 let start_time = SystemTime::now()
515 .duration_since(UNIX_EPOCH)
516 .unwrap()
517 .as_millis() as u64 as f64;
518
519 self.scroll_animation = ScrollAnimationState::ScrollTo(ScrollToState {
520 target,
521 start,
522 end,
523 start_time,
524 duration: Self::SMOOTH_SCROLL_DURATION_MS,
525 });
526
527 self.shell_provider.request_redraw();
529 }
530
531 fn should_scroll_smoothly(&self, target: ScrollTarget, behavior: ScrollBehavior) -> bool {
532 match behavior {
533 ScrollBehavior::Auto => {
534 let styled_node = match target {
535 ScrollTarget::Node(node_id) => self.nodes.get(node_id),
536 ScrollTarget::Viewport => self.try_root_element(),
537 };
538 styled_node.is_some_and(|node| {
539 node.primary_styles().is_some_and(|style| {
540 style.clone_scroll_behavior()
541 == style::computed_values::scroll_behavior::T::Smooth
542 })
543 })
544 }
545 ScrollBehavior::Instant => false,
546 ScrollBehavior::Smooth => true,
547 }
548 }
549
550 pub fn scroll_to(&mut self, node_id: NodeId, x: f64, y: f64, behavior: ScrollBehavior) {
555 self.scroll_programmatically(node_id, ScrollAmount::To(Point { x, y }), behavior);
556 }
557
558 pub fn scroll_by(&mut self, node_id: NodeId, x: f64, y: f64, behavior: ScrollBehavior) {
560 self.scroll_programmatically(node_id, ScrollAmount::By(Point { x, y }), behavior);
561 }
562
563 fn scroll_programmatically(
564 &mut self,
565 node_id: NodeId,
566 amount: ScrollAmount,
567 behavior: ScrollBehavior,
568 ) {
569 if self.nodes.get(node_id).is_none() {
570 return;
571 }
572
573 self.scroll(
575 ScrollRequest {
576 target: ScrollTarget::Node(node_id),
577 amount,
578 overflow: ScrollOverflow::Clamp,
579 source: ScrollSource::Programmatic,
580 behavior,
581 interrupt_animation: true,
582 },
583 &mut |_| {},
584 );
585 }
586
587 fn aligned_scroll_offset(
588 current: f64,
589 viewport_size: f64,
590 target_start: f64,
591 target_size: f64,
592 position: ScrollLogicalPosition,
593 ) -> f64 {
594 let target_end = target_start + target_size;
595 match position {
596 ScrollLogicalPosition::Start => target_start,
597 ScrollLogicalPosition::Center => target_start - (viewport_size - target_size) / 2.0,
598 ScrollLogicalPosition::End => target_end - viewport_size,
599 ScrollLogicalPosition::Nearest => {
600 let viewport_end = current + viewport_size;
601 if (target_start >= current && target_end <= viewport_end)
602 || (target_start <= current && target_end >= viewport_end)
603 {
604 current
605 } else {
606 let start_offset = target_start;
607 let end_offset = target_end - viewport_size;
608 if (start_offset - current).abs() < (end_offset - current).abs() {
609 start_offset
610 } else {
611 end_offset
612 }
613 }
614 }
615 }
616 }
617
618 pub fn scroll_into_view(
620 &mut self,
621 node_id: NodeId,
622 behavior: ScrollBehavior,
623 vertical: ScrollLogicalPosition,
624 horizontal: ScrollLogicalPosition,
625 ) {
626 let Some(node) = self.nodes.get(node_id) else {
627 return;
628 };
629 let target =
630 node.absolute_position(node.scroll_offset().x as f32, node.scroll_offset().y as f32);
631 let target_size = node.final_layout().size;
632 let Some(root_id) = self.try_root_element().map(|root| root.id) else {
633 return;
634 };
635 let scale = self.viewport.scale() as f64;
636 let viewport_width = self.viewport.window_size.0 as f64 / scale;
637 let viewport_height = self.viewport.window_size.1 as f64 / scale;
638 let x = Self::aligned_scroll_offset(
639 self.viewport_scroll.x,
640 viewport_width,
641 target.x as f64,
642 target_size.width as f64,
643 horizontal,
644 );
645 let y = Self::aligned_scroll_offset(
646 self.viewport_scroll.y,
647 viewport_height,
648 target.y as f64,
649 target_size.height as f64,
650 vertical,
651 );
652 self.scroll_to(root_id, x, y, behavior);
653 }
654
655 fn resolve_fragment_scroll_target(&self, fragment: &str) -> Option<Option<NodeId>> {
662 let decoded = percent_encoding::percent_decode_str(fragment)
664 .decode_utf8_lossy()
665 .into_owned();
666
667 if !decoded.is_empty() {
668 if let Some(node_id) = self.get_fragment_target(&decoded) {
669 return Some(Some(node_id));
670 }
671 }
672
673 if decoded.is_empty() || decoded.eq_ignore_ascii_case("top") {
676 return Some(None);
677 }
678
679 None
680 }
681
682 fn scroll_to_fragment_with_behavior(
683 &mut self,
684 fragment: &str,
685 behavior: ScrollBehavior,
686 ) -> bool {
687 match self.resolve_fragment_scroll_target(fragment) {
688 Some(Some(node_id)) => {
689 self.scroll_into_view(
690 node_id,
691 behavior,
692 ScrollLogicalPosition::Start,
693 ScrollLogicalPosition::Nearest,
694 );
695 true
696 }
697 Some(None) => {
698 let root_id = self.root_element().id;
699 self.scroll_to(root_id, 0.0, 0.0, behavior);
700 true
701 }
702 None => false,
703 }
704 }
705
706 pub fn scroll_to_fragment(&mut self, fragment: &str) -> bool {
712 self.scroll_to_fragment_with_behavior(fragment, ScrollBehavior::Auto)
713 }
714
715 pub fn scroll_to_fragment_smooth(&mut self, fragment: &str) -> bool {
718 self.scroll_to_fragment_with_behavior(fragment, ScrollBehavior::Smooth)
719 }
720
721 pub fn resolve_scroll_animation(&mut self) {
722 match &mut self.scroll_animation {
723 ScrollAnimationState::Fling(fling_state) => {
724 let time_ms = SystemTime::now()
725 .duration_since(UNIX_EPOCH)
726 .unwrap()
727 .as_millis() as u64 as f64;
728
729 let time_diff_ms = time_ms - fling_state.last_seen_time;
730
731 let deceleration = 1.0 - ((0.05 / 16.66666) * time_diff_ms);
733
734 fling_state.x_velocity *= deceleration;
735 fling_state.y_velocity *= deceleration;
736 fling_state.last_seen_time = time_ms;
737 let fling_state = fling_state.clone();
738
739 let dx = fling_state.x_velocity * time_diff_ms;
740 let dy = fling_state.y_velocity * time_diff_ms;
741
742 self.scroll_chain_by(Some(fling_state.target), dx, dy, &mut |_| {});
743 if fling_state.x_velocity.abs() < 0.1 && fling_state.y_velocity.abs() < 0.1 {
744 self.scroll_animation = ScrollAnimationState::None;
745 }
746 }
747 ScrollAnimationState::ScrollTo(scroll_to) => {
748 let scroll_to = scroll_to.clone();
749 let time_ms = SystemTime::now()
750 .duration_since(UNIX_EPOCH)
751 .unwrap()
752 .as_millis() as u64 as f64;
753
754 let progress = if scroll_to.duration <= 0.0 {
756 1.0
757 } else {
758 ((time_ms - scroll_to.start_time) / scroll_to.duration).clamp(0.0, 1.0)
759 };
760 let eased = ease_in_out_cubic(progress);
761
762 let target = Point {
764 x: scroll_to.start.x + (scroll_to.end.x - scroll_to.start.x) * eased,
765 y: scroll_to.start.y + (scroll_to.end.y - scroll_to.start.y) * eased,
766 };
767 self.write_scroll_offset(scroll_to.target, target, &mut |_| {});
769
770 if progress >= 1.0 {
771 self.scroll_animation = ScrollAnimationState::None;
772 }
773 }
774 ScrollAnimationState::None => {
775 }
777 }
778 }
779}