diff --git a/package.json b/package.json index 8aee536be..7436b187d 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "build": [ "dune build -p Revery -j4" ], + "buildsInSource": "_build", "install": [ "esy-installer Revery.install", "bash -c \"#{os == 'windows' ? 'cp /usr/x86_64-w64-mingw32/sys-root/mingw/bin/*.dll \\'$cur__bin\\'': ':'}\"" @@ -47,15 +48,20 @@ "reason-font-manager": "^2.1.1", "reason-harfbuzz": "^1.91.5007", "rench": "^1.9.1", + "reason-sdl2": "*", + "libscroll": "*", "rebez": "jchavarri/rebez#03fa3b7", - "reason-sdl2": "^2.10.3021", "reason-skia": "revery-ui/reason-skia#2352b85", "revery-text-wrap": "revery-ui/revery-text-wrap#966383e", "@glennsl/timber": "1.0.0" }, "resolutions": { "esy-cmake": "prometheansacrifice/esy-cmake#2a47392def755", - "@opam/cmdliner": "1.0.2" + "@opam/cmdliner": "1.0.2", + "timber": "glennsl/timber#ae065bb", + "libscroll": "link:../libscroll-re", + "esy-sdl2": "link:../esy-sdl2", + "reason-sdl2": "link:../reason-sdl2" }, "devDependencies": { "ocaml": "~4.8", diff --git a/src/Core/App.re b/src/Core/App.re index a70e6ef04..f127b07c4 100644 --- a/src/Core/App.re +++ b/src/Core/App.re @@ -183,6 +183,7 @@ let start = init => { | Sdl2.Event.MouseButtonDown({windowID, _}) => handleEvent(windowID) | Sdl2.Event.MouseMotion({windowID, _}) => handleEvent(windowID) | Sdl2.Event.MouseWheel({windowID, _}) => handleEvent(windowID) + | Sdl2.Event.Pan({windowID, _}) => handleEvent(windowID) | Sdl2.Event.KeyDown({windowID, _}) => handleEvent(windowID) | Sdl2.Event.KeyUp({windowID, _}) => handleEvent(windowID) | Sdl2.Event.TextInput({windowID, _}) => handleEvent(windowID) diff --git a/src/Core/Events.re b/src/Core/Events.re index 611b009c2..5aa7d0241 100644 --- a/src/Core/Events.re +++ b/src/Core/Events.re @@ -22,9 +22,20 @@ type mouseMoveEvent = { mouseY: float, }; +module MousePanAction = { + type t = + | Interrupt + | Fling + | Pan(float); + + let pp : Format.formatter => t => unit = (_: Format.formatter) => (_: t) => () +} + type mouseWheelEvent = { - deltaX: float, - deltaY: float, + source: Libscroll.Source.t, + axis: Libscroll.Axis.t, + action: MousePanAction.t, + timestamp: int, }; type mouseButtonEvent = {button: MouseButton.t}; diff --git a/src/Core/Window.re b/src/Core/Window.re index 1c4ba8f08..7c0067004 100644 --- a/src/Core/Window.re +++ b/src/Core/Window.re @@ -298,13 +298,57 @@ let render = (w: t) => { w.isRendering = false; }; +let convertWheelType = (intype: Sdl2.WheelType.t) => { + open Libscroll; + open Sdl2; + switch (intype) { + | WheelType.Last => Source.Previous + | WheelType.Undefined => Source.Undefined + | WheelType.Touchscreen => Source.Touchscreen + | WheelType.Touchpad => Source.Touchpad + | WheelType.Wheel => Source.Mousewheel + | WheelType.WheelPrecise => Source.PreciseMousewheel + | WheelType.OtherNonKinetic => KineticPassthrough + | WheelType.OtherKinetic => KineticPassthrough + } +} + +let mapAxis = (sdlAxis: Sdl2.Axis.t) => switch (sdlAxis) { + | Sdl2.Axis.Vertical => Libscroll.Axis.Vertical + | Sdl2.Axis.Horizontal => Libscroll.Axis.Horizontal +} + +let mapPanAction = (sdlAction: Sdl2.Event.PanElements.t) => switch (sdlAction) { + | Sdl2.Event.PanElements.Fling => Events.MousePanAction.Fling + | Sdl2.Event.PanElements.Interrupt => Events.MousePanAction.Interrupt + | Sdl2.Event.PanElements.Pan(amount) => Events.MousePanAction.Pan(amount) +} + let handleEvent = (sdlEvent: Sdl2.Event.t, v: t) => { switch (sdlEvent) { | Sdl2.Event.MouseWheel({deltaX, deltaY, _}) => - let wheelEvent: Events.mouseWheelEvent = { - deltaX: float_of_int(deltaX), - deltaY: float_of_int(deltaY), + let xEvent: Events.mouseWheelEvent = { + source: Libscroll.Source.Mousewheel, + timestamp: Sdl2.Timekeeping.getTicks(), // TODO: try to migrate completely to pan events for more precise timestamps + action: Events.MousePanAction.Pan(float_of_int(deltaX)), + axis: Libscroll.Axis.Horizontal, }; + Event.dispatch(v.onMouseWheel, xEvent); + + let yEvent: Events.mouseWheelEvent = { + source: Libscroll.Source.Mousewheel, + timestamp: Sdl2.Timekeeping.getTicks(), // TODO: try to migrate completely to pan events for more precise timestamps + action: Events.MousePanAction.Pan(float_of_int(deltaY)), + axis: Libscroll.Axis.Vertical, + }; + Event.dispatch(v.onMouseWheel, yEvent); + | Sdl2.Event.Pan({timestamp, source, axis, action}) => + let wheelEvent: Events.mouseWheelEvent = { + source: convertWheelType(source), + timestamp: timestamp, + action: mapPanAction(action), + axis: mapAxis(axis), + } Event.dispatch(v.onMouseWheel, wheelEvent); | Sdl2.Event.MouseMotion({x, y, _}) => let mouseEvent: Events.mouseMoveEvent = { diff --git a/src/Core/dune b/src/Core/dune index 67f205386..92d2fd852 100644 --- a/src/Core/dune +++ b/src/Core/dune @@ -5,4 +5,4 @@ (c_names file) (c_flags :standard -Wall -Wextra -Werror) (preprocess (pps ppx_deriving.show)) - (libraries threads console.lib str lwt sdl2 skia flex Rench re Revery_Native revery-text-wrap timber)) + (libraries threads console.lib str lwt sdl2 skia flex Rench re Revery_Native revery-text-wrap timber Libscroll)) diff --git a/src/UI/Mouse.re b/src/UI/Mouse.re index dfbf18b28..b5ed53c67 100644 --- a/src/UI/Mouse.re +++ b/src/UI/Mouse.re @@ -287,7 +287,12 @@ let internalToExternalEvent = (c: Cursor.t, evt: Events.internalMouseEvents) => | InternalMouseMove(evt) => MouseMove({mouseX: evt.mouseX, mouseY: evt.mouseY}) | InternalMouseWheel(evt) => - MouseWheel({deltaX: evt.deltaX, deltaY: evt.deltaY}) + MouseWheel({ + source: evt.source, + axis: evt.axis, + action: evt.action, + timestamp: evt.timestamp + }) | InternalMouseEnter(evt) => MouseEnter({mouseX: evt.mouseX, mouseY: evt.mouseY}) | InternalMouseLeave(evt) => diff --git a/src/UI/NodeEvents.re b/src/UI/NodeEvents.re index 6a408519a..80aafbab1 100644 --- a/src/UI/NodeEvents.re +++ b/src/UI/NodeEvents.re @@ -20,8 +20,10 @@ type mouseButtonEventParams = { [@deriving show({with_path: false})] type mouseWheelEventParams = { - deltaX: float, - deltaY: float, + source: Libscroll.Source.t, + axis: Libscroll.Axis.t, + action: Events.MousePanAction.t, + timestamp: int, }; [@deriving show({with_path: false})] diff --git a/src/UI/dune b/src/UI/dune index fea1cf8b5..77f2b80c5 100644 --- a/src/UI/dune +++ b/src/UI/dune @@ -2,4 +2,4 @@ (name Revery_UI) (public_name Revery.UI) (preprocess (pps lwt_ppx ppx_deriving.show)) - (libraries brisk-reconciler lwt lwt.unix sdl2 skia flex rebez.lib Revery_Core Revery_Draw Revery_Math)) + (libraries brisk-reconciler lwt lwt.unix sdl2 skia flex rebez.lib Revery_Core Revery_Draw Revery_Math Libscroll)) diff --git a/src/UI_Components/ScrollView.re b/src/UI_Components/ScrollView.re index 01e3ccef2..76063ffd6 100644 --- a/src/UI_Components/ScrollView.re +++ b/src/UI_Components/ScrollView.re @@ -5,6 +5,8 @@ open Revery_UI_Primitives; module Hooks = Revery_UI_Hooks; +module Log = (val Log.withNamespace("Revery.ScrollView")); + type bouncingState = | Bouncing(int) | Idle; @@ -24,29 +26,12 @@ let reducer = (action, _state) => { }; }; -let bounceAnimation = (~origin, ~force) => - Animation.( - { - let bounceAway = - animate(Time.ms(100)) - |> ease(Easing.cubicBezier(0.23, 1., 0.32, 1.)) - |> tween(float(origin), float(origin + force)); - - let bounceBack = - Animation.animate(Time.ms(800)) - |> ease(Easing.cubicBezier(0.23, 1., 0.32, 1.)) - |> tween(float(origin + force), float(origin)); - - bounceAway |> andThen(~next=bounceBack) |> map(int_of_float); - } - ); - let%component make = ( ~style, ~scrollLeft=0, ~scrollTop=0, - ~bounce=defaultBounce, + ~bounce=defaultBounce, // TODO: pass bounce hint to libscroll ~children=React.empty, (), ) => { @@ -57,22 +42,43 @@ let%component make = let%hook (actualScrollLeft, setScrollLeft) = Hooks.state(scrollLeft); let%hook (bouncingState, setBouncingState) = Hooks.state(Idle); - let%hook (actualScrollTop, _bounceAnimationState, resetBouncingAnimation) = - switch (bouncingState) { - | Idle => - // TODO: Why isn't Animation.const always sufficient to stop the timer? - Hooks.animation(~active=false, Animation.const(actualScrollTop)) - - | Bouncing(force) => - Hooks.animation( - bounceAnimation(~origin=actualScrollTop, ~force), ~onComplete=() => - setBouncingState(_ => Idle) - ) - }; - let setBouncingState = state => { - resetBouncingAnimation(); - setBouncingState(state); - }; + let%hook (scrollViewRef) = Hooks.ref(None); + + let%hook (kickAnimating, setKickAnimating) = Hooks.state(0); + + let scrollviewActive = () => { + let r = switch (scrollViewRef^) { + | None => { + Log.error("Scrollview was none"); + false + } + | Some(scrollview) => { + let v = Libscroll.animating(scrollview) + if (v) { + setKickAnimating(_ => kickAnimating + 1); + } else { + setKickAnimating(_ => kickAnimating); + } + + v + } + } + + r + } + + let%hook (animationTimerTime, resetAnimationTimer) = Hooks.timer(~active=scrollviewActive(), ()); + + + let%hook () = Hooks.effect(OnMount, () => { + let scrollView = Libscroll.scrollview_new(); + scrollViewRef := Some(scrollView); + + let dispose = () => { + scrollViewRef := None; + }; + Some(dispose); + }); let scrollBarThickness = 10; @@ -81,6 +87,21 @@ let%component make = TranslateY((-1.) *. float_of_int(actualScrollTop)), ]; + let (scrollX, scrollY) = switch (scrollViewRef^) { + | None => { + Log.error("Can't sample, sv null"); + + (0.0, 0.0) + } + | Some(scrollview) => { + let (x, y) = Libscroll.sample(scrollview, Sdl2.Timekeeping.getTicks()); + + (x, y) + } + } + dispatch(ScrollUpdated(int_of_float(scrollY))); + + let (horizontalScrollBar, verticalScrollBar, scroll) = switch (outerRef) { | Some(outer) => @@ -156,26 +177,24 @@ let%component make = : empty; let scroll = (wheelEvent: NodeEvents.mouseWheelEventParams) => { - let delta = int_of_float(wheelEvent.deltaY *. 25.); - let newScrollTop = actualScrollTop - delta; - - let isAtTop = newScrollTop < 0; - let isAtBottom = newScrollTop > maxHeight; - - switch (bouncingState) { - | Bouncing(force) when force < 0 && wheelEvent.deltaY < 0. => - setBouncingState(_ => Idle) - | Bouncing(force) when force > 0 && wheelEvent.deltaY > 0. => - setBouncingState(_ => Idle) - | Bouncing(_) => () - | Idle when !bounce && (isAtTop || isAtBottom) => - let clampedScrollTop = isAtTop ? 0 : maxHeight; - dispatch(ScrollUpdated(clampedScrollTop)); - | Idle when bounce && (isAtTop || isAtBottom) => - setBouncingState(_ => Bouncing(- delta * 2)); - dispatch(ScrollUpdated(isAtTop ? 0 : maxHeight)); - | Idle => dispatch(ScrollUpdated(newScrollTop)) - }; + switch (scrollViewRef^) { + | Some(scrollview) => { + Libscroll.set_geometry(scrollview, float_of_int(maxHeight), 0.0, 0.0, 0.0); + + Libscroll.set_source(scrollview, wheelEvent.source); + + switch (wheelEvent.action) { + | Events.MousePanAction.Fling => Libscroll.push_fling(scrollview, wheelEvent.axis, wheelEvent.timestamp) + | Events.MousePanAction.Interrupt => Libscroll.push_interrupt(scrollview, wheelEvent.axis, wheelEvent.timestamp) + | Events.MousePanAction.Pan(delta) => Libscroll.push_pan(scrollview, wheelEvent.axis, delta, wheelEvent.timestamp) + } + + setKickAnimating(_ => kickAnimating + 1); + resetAnimationTimer(); + + } + | None => Log.error("Scrollview not present on event dispatch"); + } }; (horizontalScrollbar, verticalScrollBar, scroll); | _ => (empty, empty, (_ => ()))