Modular Web Browser Engine Built from Scratch in Pure Rust | GPU Accelerated | Custom JS Runtime
SPEED DIAL & QUICK LINKS
+SPEED DIAL & QUICK LINKS
[ Google ] [ GitHub ] @@ -40,6 +45,8 @@ impl Tab { "# ), scroll_y: 0.0, + target_scroll_y: 0.0, + scroll_velocity: 0.0, zoom_level: 1.0, history, } @@ -50,6 +57,8 @@ impl Tab { self.url = url.to_string(); self.page_body = body.to_string(); self.scroll_y = 0.0; + self.target_scroll_y = 0.0; + self.scroll_velocity = 0.0; self.history.navigate_to(url); } @@ -73,6 +82,48 @@ impl Tab { let body = self.page_body.to_ascii_lowercase(); body.matches(&q).count() } + + // ── Smooth scrolling ──────────────────────────────────────────────────────── + + /// Called by the `MouseWheel` event handler. `delta_y` is the raw + /// wheel delta in pixels (positive = scroll down, negative = scroll up). + /// `max_scroll` is the document height minus viewport height so we can + /// clamp the target immediately. + pub fn apply_wheel_delta(&mut self, delta_y: f32, max_scroll: f32) { + // Accumulate velocity from wheel input (scaled for comfortable speed) + self.scroll_velocity += delta_y * 0.8; + // Advance the target by the new velocity and clamp + self.target_scroll_y = + (self.target_scroll_y + self.scroll_velocity).clamp(0.0, max_scroll.max(0.0)); + } + + /// Called once per frame (on `RedrawRequested`). + /// Exponentially decays velocity and lerps `scroll_y` toward `target_scroll_y`. + /// Returns `true` if the frame needs a follow-up redraw (animation still running). + pub fn tick_scroll(&mut self) -> bool { + // Decay inertia — friction coefficient ~0.85 gives comfortable momentum + self.scroll_velocity *= 0.85; + if self.scroll_velocity.abs() < 0.05 { + self.scroll_velocity = 0.0; + } + + // Lerp toward target (easing factor 0.18 → snappy but smooth) + let diff = self.target_scroll_y - self.scroll_y; + if diff.abs() < 0.5 { + self.scroll_y = self.target_scroll_y; + return false; // settled — no further redraws needed + } + self.scroll_y += diff * 0.18; + true // still animating + } + + /// Hard-clamp both `scroll_y` and `target_scroll_y` to `[0, max_scroll]`. + /// Called whenever viewport size changes or a new page loads. + pub fn clamp_scroll(&mut self, max_scroll: f32) { + let upper = max_scroll.max(0.0); + self.target_scroll_y = self.target_scroll_y.clamp(0.0, upper); + self.scroll_y = self.scroll_y.clamp(0.0, upper); + } } pub struct TabManager { @@ -188,3 +239,78 @@ impl TabManager { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_scroll_clamp_prevents_negative() { + let mut tab = Tab::new(1, "Test", "https://example.com"); + tab.target_scroll_y = -50.0; + tab.scroll_y = -50.0; + tab.clamp_scroll(500.0); + assert_eq!(tab.scroll_y, 0.0); + assert_eq!(tab.target_scroll_y, 0.0); + } + + #[test] + fn test_scroll_clamp_prevents_overflow() { + let mut tab = Tab::new(1, "Test", "https://example.com"); + tab.target_scroll_y = 9999.0; + tab.scroll_y = 9999.0; + tab.clamp_scroll(800.0); + assert_eq!(tab.scroll_y, 800.0); + assert_eq!(tab.target_scroll_y, 800.0); + } + + #[test] + fn test_tick_scroll_lerps_toward_target() { + let mut tab = Tab::new(1, "Test", "https://example.com"); + tab.scroll_y = 0.0; + tab.target_scroll_y = 100.0; + let still_animating = tab.tick_scroll(); + assert!(still_animating); + // scroll_y should have moved toward 100 but not reached it yet + assert!(tab.scroll_y > 0.0 && tab.scroll_y < 100.0); + } + + #[test] + fn test_tick_scroll_settles_when_close() { + let mut tab = Tab::new(1, "Test", "https://example.com"); + tab.scroll_y = 99.6; + tab.target_scroll_y = 100.0; + let still_animating = tab.tick_scroll(); + assert!(!still_animating); + assert_eq!(tab.scroll_y, 100.0); + } + + #[test] + fn test_apply_wheel_delta_clamps_to_max() { + let mut tab = Tab::new(1, "Test", "https://example.com"); + tab.apply_wheel_delta(10_000.0, 500.0); + assert!(tab.target_scroll_y <= 500.0); + } + + #[test] + fn test_velocity_decays_to_zero() { + let mut tab = Tab::new(1, "Test", "https://example.com"); + tab.scroll_velocity = 100.0; + for _ in 0..60 { + tab.tick_scroll(); + } + assert!(tab.scroll_velocity.abs() < 0.1); + } + + #[test] + fn test_navigate_to_resets_scroll() { + let mut tab = Tab::new(1, "Test", "https://example.com"); + tab.scroll_y = 300.0; + tab.target_scroll_y = 300.0; + tab.scroll_velocity = 12.0; + tab.navigate_to("New Page", "https://new.com", "
hello
"); + assert_eq!(tab.scroll_y, 0.0); + assert_eq!(tab.target_scroll_y, 0.0); + assert_eq!(tab.scroll_velocity, 0.0); + } +}