Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP/POC] Libscroll Integration #766

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 9 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,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\\'': ':'}\""
Expand All @@ -53,16 +54,21 @@
"reason-harfbuzz": "^1.91.5004",
"rench": "^1.9.1",
"rebez": "github:jchavarri/rebez#03fa3b7",
"reason-sdl2": "^2.10.3016",
"reason-sdl2": "github:revery-ui/reason-sdl2",
"reason-skia": "github:revery-ui/reason-skia#69743dc",
"revery-text-wrap": "github:revery-ui/revery-text-wrap#005385c",
"timber": "*"
"timber": "*",
"libscroll": "*"
},
"resolutions": {
"@esy-ocaml/libffi": "esy-ocaml/libffi#c61127d",
"esy-cmake": "prometheansacrifice/esy-cmake#2a47392def755",
"@opam/cmdliner": "1.0.2",
"timber": "glennsl/timber#ae065bb"
"timber": "glennsl/timber#ae065bb",
"libscroll": "link:../libscroll-re",
"reason-sdl2": "revery-ui/reason-sdl2#a52eee7",
"esy-sdl2": "link:../esy-sdl2",
"reason-sdl2": "link:../reason-sdl2"
},
"devDependencies": {
"ocaml": "~4.8",
Expand Down
1 change: 1 addition & 0 deletions src/Core/App.re
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ let start = (~onIdle=noop, initFunc: appInitFunc) => {
| Sdl2.Event.MouseButtonDown({windowID, _}) => handleEvent(windowID)
| Sdl2.Event.MouseMotion({windowID, _}) => handleEvent(windowID)
| Sdl2.Event.MouseWheel({windowID, _}) => handleEvent(windowID)
| Sdl2.Event.MousePan({windowID, _}) => handleEvent(windowID)
| Sdl2.Event.KeyDown({windowID, _}) => handleEvent(windowID)
| Sdl2.Event.KeyUp({windowID, _}) => handleEvent(windowID)
| Sdl2.Event.TextInput({windowID, _}) => handleEvent(windowID)
Expand Down
8 changes: 6 additions & 2 deletions src/Core/Events.re
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,12 @@ type mouseMoveEvent = {
};

type mouseWheelEvent = {
deltaX: float,
deltaY: float,
deltaX: option(float),
deltaY: option(float),
isFling: bool,
isInterrupt: bool,
source: Libscroll.Source.t,
timestamp: int,
};

type mouseButtonEvent = {button: MouseButton.t};
Expand Down
34 changes: 32 additions & 2 deletions src/Core/Window.re
Original file line number Diff line number Diff line change
Expand Up @@ -297,14 +297,44 @@ 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 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),
deltaX: Some(float_of_int(deltaX)),
deltaY: Some(float_of_int(deltaY)),
source: Libscroll.Source.Mousewheel,
timestamp: 0, // TODO: add timestamps to mousewheel events in sdl OR completely switch to pan
isFling: false,
isInterrupt: false,
};
Event.dispatch(v.onMouseWheel, wheelEvent);
| Sdl2.Event.MousePan({deltaX, deltaY, containsX, containsY, isFling, isInterrupt, source, timestamp}) =>
Log.info("Got pan event");
let wheelEvent: Events.mouseWheelEvent = {
deltaX: containsX ? Some(float_of_int(deltaX)) : None,
deltaY: containsY ? Some(float_of_int(deltaY)) : None,
isFling: isFling,
isInterrupt: isInterrupt,
source: convertWheelType(source),
timestamp: timestamp,
}
Event.dispatch(v.onMouseWheel, wheelEvent);
| Sdl2.Event.MouseMotion({x, y, _}) =>
let mouseEvent: Events.mouseMoveEvent = {
mouseX: float_of_int(x),
Expand Down
2 changes: 1 addition & 1 deletion src/Core/dune
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're taking out skia and putting fontkit back?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@glennsl whoops, not intentionally. I think that file got left behind as I was jumping between branches

(libraries threads console.lib str lwt sdl2 skia flex Rench re Revery_Native revery-text-wrap timber Libscroll))
9 changes: 8 additions & 1 deletion src/UI/Mouse.re
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,14 @@ 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({
deltaX: evt.deltaX,
deltaY: evt.deltaY,
isFling: evt.isFling,
isInterrupt: evt.isInterrupt,
source: evt.source,
timestamp: evt.timestamp
})
| InternalMouseEnter(evt) =>
MouseEnter({mouseX: evt.mouseX, mouseY: evt.mouseY})
| InternalMouseLeave(evt) =>
Expand Down
8 changes: 6 additions & 2 deletions src/UI/NodeEvents.re
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,12 @@ type mouseButtonEventParams = {

[@deriving show({with_path: false})]
type mouseWheelEventParams = {
deltaX: float,
deltaY: float,
deltaX: option(float),
deltaY: option(float),
isFling: bool,
isInterrupt: bool,
source: Libscroll.Source.t,
timestamp: int,
};

[@deriving show({with_path: false})]
Expand Down
2 changes: 1 addition & 1 deletion src/UI/dune
Original file line number Diff line number Diff line change
Expand Up @@ -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))
82 changes: 76 additions & 6 deletions src/UI_Components/ScrollView.re
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -57,6 +59,19 @@ let%component make =
let%hook (actualScrollLeft, setScrollLeft) = Hooks.state(scrollLeft);
let%hook (bouncingState, setBouncingState) = Hooks.state(Idle);

//let%hook (scrollview, setScrollview) = Hooks.state(() => Libscroll.scrollview_new());
//setScrollview(_ => scrollview);
let%hook (scrollViewRef) = Hooks.ref(None);
let%hook () = Hooks.effect(OnMount, () => {
let scrollView = Libscroll.scrollview_new();
scrollViewRef := Some(scrollView);

let dispose = () => {
scrollViewRef := None;
};
Some(dispose);
});

let%hook (actualScrollTop, _bounceAnimationState, resetBouncingAnimation) =
switch (bouncingState) {
| Idle =>
Expand Down Expand Up @@ -154,25 +169,80 @@ let%component make =
thumbColor=scrollThumbColor
/>
: empty;

/*let pan = (panEvent: NodeEvents.panEventParams) => {
switch (scrollViewRef^) {
| None => ()
| Some(scrollview) => {
let timestamp = wheelEvent.timestamp;
let delta = wheelEvent.delta;
let axis = wheelEvent.axis;
}
}*/

let scrollForFrameFlip = (timestamp: int) => {
// call into libscroll to sample new position
}

let scroll = (wheelEvent: NodeEvents.mouseWheelEventParams) => {
let delta = int_of_float(wheelEvent.deltaY *. 25.);
switch (scrollViewRef^) {
| Some(scrollview) => {//Libscroll.push_pan(scrollview, Libscroll.Axis.Vertical, 10.0, 0)
Log.info("Scrollview existed");

Libscroll.set_source(scrollview, wheelEvent.source);

switch(wheelEvent.deltaX) {
| Some(delta) => {
Libscroll.push_pan(scrollview, Libscroll.Axis.Horizontal, delta, wheelEvent.timestamp);
}
| None => ()
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this an option? Wouldn't a delta of 0 be "neutral" and effectively do nothing anyway?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it wasn't doing interpolation/prediction then yes, but with both enabled it would mean that acceleration goes to infinity and velocity drops to zero, which would cause weird stuttering if event ordering isn't consistent (and also make it so that fling sometimes wouldn't work)

Copy link
Member

@glennsl glennsl Mar 8, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you not just weed out the 0s from the interpolation? Or does the None have some other significance?

Like, if it was not an option, would this not be equivalent?

if (wheelEvent.deltaX != 0) {
  Libscroll.push_pan(scrollview, Libscroll.Axis.Horizontal, wheelEvent.deltaX, wheelEvent.timestamp);
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably, at least on Wayland I never seem to get a zero delta. I still need to figure out how to drag prediction toward zero if velocity drops but no zero event or fling event comes in.

It seems semantically odd to special case zero delta to me but it might be fine

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find it odd that it needs to be special-cased too, but I would consider both using an option and weeding out 0s special-casing. A delta of 0 seems completely normal to me, so I'm wondering if the problem is somewhere else in the model. Unless you work with imperfect sensor data, which of course happens sometimes.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If these were polymorphic variants, you wouldn't even need to do the mapping. It's a good use case for it.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, how would that work? Am not super familiar with them

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Polymorphic variants are the structurally typed equivalents of ordinary variants. Their identity is derived from the type itself, not from any definition. In this case specifically that means SDL and libscroll can share a polymorphic variant type without either library knowing about the other.

By example, it looks like this:

let functionTakingPolyVariant = (poly: [`Number(int) | `Text(string)]) =>
  switch (poly) {
  | `Number(num) => "Number: " ++ string_of_int(num)
  | `Text(text) => "Text: " ++ text
  };

let str = functionTakingPolyVariant(`Number(42));

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something I'm also remembering here, is that wayland sends events for when the source axis changes for a pan, and that contains an undefined delta. I probably need a way of excluding the delta as well. Might make sense to have a more general axis enum indicating horizontal, vertical, or undefined (undefined meaning the delta is meaningless, and shouldn't be interpreted)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at how this interacts with scroll stop events makes me want to bring back the guard, for single accesses.

Effectively what I want to encode is the variant/enum

Info(Source)
Fling(Axis, Source) |
Interrupt(Axis, Source) |
Pan(Axis, Source, delta)

in a way that is fairly ideomatic in c and translates well across to reasonml/rust code. What are your opinions on doing that nicely?

I might be overfitting for the linux way of handling input, but afaik it's pretty similar on OSX and can be fully described for the Windows model (just compose multiple events)


switch(wheelEvent.deltaY) {
| Some(delta) => {
Libscroll.push_pan(scrollview, Libscroll.Axis.Vertical, delta, wheelEvent.timestamp);
}
| None => ()
};

switch(wheelEvent.isFling) {
| true => Libscroll.push_fling(scrollview, wheelEvent.timestamp);
| false => ()
};
Copy link
Member

@glennsl glennsl Mar 8, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't use pattern matching for boolean conditionals. That's what if expressions are for. They communicate intent better, are less noisy, and also shorter in cases like this where you can skip the else branch:

Suggested change
switch(wheelEvent.isFling) {
| true => Libscroll.push_fling(scrollview, wheelEvent.timestamp);
| false => ()
};
if (wheelEvent.isFling) {
Libscroll.push_fling(scrollview, wheelEvent.timestamp);
};

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, will change


switch(wheelEvent.isInterrupt) {
| true => Libscroll.push_interrupt(scrollview, wheelEvent.timestamp);
| false => ()
};

}
| None => Log.error("Scrollview not present on event dispatch");
}
let delta = switch(wheelEvent.deltaY) {
| Some(value) => value *. 25.
| None => 0.0
};
//let delta = int_of_float(wheelEvent.deltaY *. 25.);
let delta_s = delta;
let delta = int_of_float(delta /. -400.0);
let newScrollTop = actualScrollTop - delta;

let isAtTop = newScrollTop < 0;
let isAtBottom = newScrollTop > maxHeight;

switch (bouncingState) {
| Bouncing(force) when force < 0 && wheelEvent.deltaY < 0. =>
| Bouncing(force) when force < 0 && delta_s < 0. =>
setBouncingState(_ => Idle)
| Bouncing(force) when force > 0 && wheelEvent.deltaY > 0. =>
| Bouncing(force) when force > 0 && delta_s > 0. =>
setBouncingState(_ => Idle)
| Bouncing(_) => ()
| Idle when !bounce && (isAtTop || isAtBottom) =>
let clampedScrollTop = isAtTop ? 0 : maxHeight;
dispatch(ScrollUpdated(clampedScrollTop));
dispatch(ScrollUpdated(newScrollTop));
//let clampedScrollTop = isAtTop ? 0 : maxHeight;
//dispatch(ScrollUpdated(clampedScrollTop));
()
| Idle when bounce && (isAtTop || isAtBottom) =>
setBouncingState(_ => Bouncing(- delta * 2));
//setBouncingState(_ => Bouncing(- delta * 2));
dispatch(ScrollUpdated(isAtTop ? 0 : maxHeight));
| Idle => dispatch(ScrollUpdated(newScrollTop))
};
Expand Down