From 1550f7f8b33ab0388d6186f0b4ecf485c32ef568 Mon Sep 17 00:00:00 2001 From: Bruno Salmon Date: Mon, 22 Jul 2024 11:00:19 +0100 Subject: [PATCH 01/25] Allowed null font in Font constructor (stopped forcing default font) --- .../src/main/java/javafx/scene/text/Font.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webfx-kit/webfx-kit-javafxgraphics-emul/src/main/java/javafx/scene/text/Font.java b/webfx-kit/webfx-kit-javafxgraphics-emul/src/main/java/javafx/scene/text/Font.java index 3cf12a5de..accc5cbef 100644 --- a/webfx-kit/webfx-kit-javafxgraphics-emul/src/main/java/javafx/scene/text/Font.java +++ b/webfx-kit/webfx-kit-javafxgraphics-emul/src/main/java/javafx/scene/text/Font.java @@ -68,7 +68,7 @@ public Font(String name, String family, FontWeight weight, FontPosture posture, private Font(String name, String family, FontWeight weight, FontPosture posture, double size, String url) { this.name = name; - this.family = family != null ? family : DEFAULT_FAMILY; + this.family = family; //family != null ? family : DEFAULT_FAMILY; this.weight = weight != null ? weight : FontWeight.NORMAL; this.posture = posture != null ? posture : FontPosture.REGULAR; this.size = size; From 3ebfff52ee08ac14d7fd5fbbba5db97c5c905454 Mon Sep 17 00:00:00 2001 From: Bruno Salmon Date: Mon, 29 Jul 2024 19:06:18 +0100 Subject: [PATCH 02/25] Introduced CSS variable --fx-svg-path-fill --- .../main/webfx/css/webfx-kit-javafxgraphics-web@main.css | 8 ++++++-- .../main/webfx/css/webfx-kit-javafxgraphics-web@main.css | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/webfx-kit/webfx-kit-javafxgraphics-fat-j2cl/src/main/webfx/css/webfx-kit-javafxgraphics-web@main.css b/webfx-kit/webfx-kit-javafxgraphics-fat-j2cl/src/main/webfx/css/webfx-kit-javafxgraphics-web@main.css index 28553a638..f68e5f8fc 100644 --- a/webfx-kit/webfx-kit-javafxgraphics-fat-j2cl/src/main/webfx/css/webfx-kit-javafxgraphics-web@main.css +++ b/webfx-kit/webfx-kit-javafxgraphics-fat-j2cl/src/main/webfx/css/webfx-kit-javafxgraphics-web@main.css @@ -1,3 +1,7 @@ +:root { + --fx-svg-path-fill: black; +} + /* Mocking some basic JavaFX behaviours */ body { overflow: hidden; /* Disabling browser horizontal and vertical scroll bars */ @@ -17,9 +21,9 @@ body { /* Applying the default JavaFX behaviour for SVGPath */ fx-svgpath svg path:not([fill]):not([stroke]) { /* if the application code didn't set neither fill nor stroke */ - fill: black; /* then the fill is black */ + fill: var(--fx-svg-path-fill); /* then the fill is black */ } fx-svgpath svg path:not([fill])[stroke] { /* if the application code set the stroke but not the fill */ - fill: transparent; /* then the fill is transparent */ + --fx-svg-path-fill: transparent; /* then the fill is transparent */ } \ No newline at end of file diff --git a/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/webfx/css/webfx-kit-javafxgraphics-web@main.css b/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/webfx/css/webfx-kit-javafxgraphics-web@main.css index 28553a638..f68e5f8fc 100644 --- a/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/webfx/css/webfx-kit-javafxgraphics-web@main.css +++ b/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/webfx/css/webfx-kit-javafxgraphics-web@main.css @@ -1,3 +1,7 @@ +:root { + --fx-svg-path-fill: black; +} + /* Mocking some basic JavaFX behaviours */ body { overflow: hidden; /* Disabling browser horizontal and vertical scroll bars */ @@ -17,9 +21,9 @@ body { /* Applying the default JavaFX behaviour for SVGPath */ fx-svgpath svg path:not([fill]):not([stroke]) { /* if the application code didn't set neither fill nor stroke */ - fill: black; /* then the fill is black */ + fill: var(--fx-svg-path-fill); /* then the fill is black */ } fx-svgpath svg path:not([fill])[stroke] { /* if the application code set the stroke but not the fill */ - fill: transparent; /* then the fill is transparent */ + --fx-svg-path-fill: transparent; /* then the fill is transparent */ } \ No newline at end of file From db5596be1aee197e7e448f750590a50f07c20d7a Mon Sep 17 00:00:00 2001 From: Bruno Salmon Date: Tue, 30 Jul 2024 08:55:31 +0100 Subject: [PATCH 03/25] Fixed SVGPath CSS rule not working --- .../src/main/webfx/css/webfx-kit-javafxgraphics-web@main.css | 1 + 1 file changed, 1 insertion(+) diff --git a/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/webfx/css/webfx-kit-javafxgraphics-web@main.css b/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/webfx/css/webfx-kit-javafxgraphics-web@main.css index f68e5f8fc..035a39c86 100644 --- a/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/webfx/css/webfx-kit-javafxgraphics-web@main.css +++ b/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/webfx/css/webfx-kit-javafxgraphics-web@main.css @@ -26,4 +26,5 @@ fx-svgpath svg path:not([fill]):not([stroke]) { /* if the application code didn' fx-svgpath svg path:not([fill])[stroke] { /* if the application code set the stroke but not the fill */ --fx-svg-path-fill: transparent; /* then the fill is transparent */ + fill: var(--fx-svg-path-fill); } \ No newline at end of file From c812cefc56ad7d3981ef9299e25824fa8f554680 Mon Sep 17 00:00:00 2001 From: Bruno Salmon Date: Fri, 2 Aug 2024 16:30:39 +0100 Subject: [PATCH 04/25] Added support of Node.setClip(Shape.subtract(shape1, shape2)) with rectangles & circles --- .../main/java/javafx/scene/shape/Shape.java | 4 ++ .../javafx/scene/shape/SubtractShape.java | 38 ++++++++++ .../javafxgraphics/base/ShapePeerBase.java | 2 +- .../gwtj2cl/html/HtmlCirclePeer.java | 10 +-- .../gwtj2cl/html/HtmlNodePeer.java | 15 ++-- .../gwtj2cl/html/HtmlRectanglePeer.java | 12 ++-- .../gwtj2cl/html/HtmlScenePeer.java | 19 +++++ .../gwtj2cl/html/HtmlSubtractShapePeer.java | 70 +++++++++++++++++++ .../gwtj2cl/shared/HtmlSvgNodePeer.java | 67 +++++++++++++++--- .../gwtj2cl/svg/SvgNodePeer.java | 34 +++++---- .../javafxgraphics/gwtj2cl/util/SvgUtil.java | 7 +- .../JavaFxGraphicsRegistry.java | 4 ++ .../JavaFxGraphicsRegistry.java | 2 + 13 files changed, 242 insertions(+), 42 deletions(-) create mode 100644 webfx-kit/webfx-kit-javafxgraphics-emul/src/main/java/javafx/scene/shape/SubtractShape.java create mode 100644 webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/html/HtmlSubtractShapePeer.java diff --git a/webfx-kit/webfx-kit-javafxgraphics-emul/src/main/java/javafx/scene/shape/Shape.java b/webfx-kit/webfx-kit-javafxgraphics-emul/src/main/java/javafx/scene/shape/Shape.java index eac2913a0..48ac533a8 100644 --- a/webfx-kit/webfx-kit-javafxgraphics-emul/src/main/java/javafx/scene/shape/Shape.java +++ b/webfx-kit/webfx-kit-javafxgraphics-emul/src/main/java/javafx/scene/shape/Shape.java @@ -79,4 +79,8 @@ public DoubleProperty strokeDashOffsetProperty() { public ObservableList getStrokeDashArray() { return getStrokeDashArray; } + + public static Shape subtract(final Shape shape1, final Shape shape2) { + return new SubtractShape(shape1, shape2); + } } diff --git a/webfx-kit/webfx-kit-javafxgraphics-emul/src/main/java/javafx/scene/shape/SubtractShape.java b/webfx-kit/webfx-kit-javafxgraphics-emul/src/main/java/javafx/scene/shape/SubtractShape.java new file mode 100644 index 000000000..c78b68c38 --- /dev/null +++ b/webfx-kit/webfx-kit-javafxgraphics-emul/src/main/java/javafx/scene/shape/SubtractShape.java @@ -0,0 +1,38 @@ +package javafx.scene.shape; + +import com.sun.javafx.geom.BaseBounds; +import com.sun.javafx.geom.transform.BaseTransform; +import dev.webfx.kit.registry.javafxgraphics.JavaFxGraphicsRegistry; + +/** + * SubtractShape is a pure WebFX class created by Shape.subtract() and the only supported usage so far is clipping. + * + * @author Bruno Salmon + */ +public class SubtractShape extends Shape { + + private final Shape shape1; + private final Shape shape2; + + public SubtractShape(Shape shape1, Shape shape2) { + this.shape1 = shape1; + this.shape2 = shape2; + } + + @Override + public BaseBounds impl_computeGeomBounds(BaseBounds bounds, BaseTransform tx) { + return shape1.impl_computeGeomBounds(bounds, tx); + } + + public Shape getShape1() { + return shape1; + } + + public Shape getShape2() { + return shape2; + } + + static { + JavaFxGraphicsRegistry.registerSubtractShape(); + } +} diff --git a/webfx-kit/webfx-kit-javafxgraphics-peers-base/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/base/ShapePeerBase.java b/webfx-kit/webfx-kit-javafxgraphics-peers-base/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/base/ShapePeerBase.java index 8707d0ace..80aaf7cb2 100644 --- a/webfx-kit/webfx-kit-javafxgraphics-peers-base/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/base/ShapePeerBase.java +++ b/webfx-kit/webfx-kit-javafxgraphics-peers-base/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/base/ShapePeerBase.java @@ -9,7 +9,7 @@ /** * @author Bruno Salmon */ -public abstract class ShapePeerBase +public class ShapePeerBase , NM extends ShapePeerMixin> extends NodePeerBase { diff --git a/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/html/HtmlCirclePeer.java b/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/html/HtmlCirclePeer.java index 8e480b034..14a329c37 100644 --- a/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/html/HtmlCirclePeer.java +++ b/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/html/HtmlCirclePeer.java @@ -26,15 +26,15 @@ public HtmlCirclePeer(NB base, HTMLElement element) { } @Override - protected String computeClipPath() { + public String computeClipPath() { Circle c = getNode(); - return "circle(" + toPx(c.getRadius()) + " at " + toPx(c.getCenterX()) + " " + toPx(c.getCenterY()); + return "circle(" + toPx(c.getRadius()) + " at " + toPx(c.getCenterX()) + " " + toPx(c.getCenterY()) + ")"; } @Override public void updateCenterX(Double centerX) { if (isClip()) - applyClipPathToClipNodes(); + applyClipClipNodes(); else getElement().style.left = (centerX - getNode().getRadius()) + "px"; } @@ -42,7 +42,7 @@ public void updateCenterX(Double centerX) { @Override public void updateCenterY(Double centerY) { if (isClip()) - applyClipPathToClipNodes(); + applyClipClipNodes(); else getElement().style.top = toPx(centerY - getNode().getRadius()); } @@ -50,7 +50,7 @@ public void updateCenterY(Double centerY) { @Override public void updateRadius(Double radius) { if (isClip()) - applyClipPathToClipNodes(); + applyClipClipNodes(); else { CSSStyleDeclaration style = getElement().style; String px = toPx(2 * radius); diff --git a/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/html/HtmlNodePeer.java b/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/html/HtmlNodePeer.java index 651da13ff..d0685e430 100644 --- a/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/html/HtmlNodePeer.java +++ b/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/html/HtmlNodePeer.java @@ -3,6 +3,7 @@ import dev.webfx.kit.mapper.peers.javafxgraphics.base.NodePeerBase; import dev.webfx.kit.mapper.peers.javafxgraphics.base.NodePeerMixin; import dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.shared.HtmlSvgNodePeer; +import dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.shared.SvgRoot; import dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.util.HtmlPaints; import dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.util.HtmlTransforms; import dev.webfx.platform.util.Strings; @@ -31,6 +32,16 @@ public HtmlNodePeer(NB base, HTMLElement element) { super(base, element); } + @Override + protected HtmlScenePeer getScenePeer() { + return (HtmlScenePeer) super.getScenePeer(); + } + + @Override + protected SvgRoot getSvgRoot() { + return getScenePeer().getSvgRoot(); + } + @Override public void updateAllNodeTransforms(List allNodeTransforms) { Element container = getVisibleContainer(); @@ -122,8 +133,4 @@ public static String toPx(double position) { return position + "px"; } - public static double fromPx(String px) { - return px == null || !px.endsWith("px") ? 0 : Double.parseDouble(px.substring(0, px.length() - 2)); - } - } diff --git a/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/html/HtmlRectanglePeer.java b/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/html/HtmlRectanglePeer.java index 3022093b8..9a544da3b 100644 --- a/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/html/HtmlRectanglePeer.java +++ b/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/html/HtmlRectanglePeer.java @@ -29,7 +29,7 @@ public HtmlRectanglePeer(NB base, HTMLElement element) { } @Override - protected String computeClipPath() { + public String computeClipPath() { Rectangle r = getNode(); double width = r.getWidth(); double height = r.getHeight(); @@ -56,7 +56,7 @@ protected String computeClipPath() { @Override public void updateX(Double x) { if (isClip()) - applyClipPathToClipNodes(); + applyClipClipNodes(); else getElement().style.left = toPx(x); } @@ -64,7 +64,7 @@ public void updateX(Double x) { @Override public void updateY(Double y) { if (isClip()) - applyClipPathToClipNodes(); + applyClipClipNodes(); else getElement().style.top = toPx(y); } @@ -72,7 +72,7 @@ public void updateY(Double y) { @Override public void updateWidth(Double width) { if (isClip()) - applyClipPathToClipNodes(); + applyClipClipNodes(); else getElement().style.width = CSSProperties.WidthUnionType.of(toPx(width)); } @@ -80,7 +80,7 @@ public void updateWidth(Double width) { @Override public void updateHeight(Double height) { if (isClip()) - applyClipPathToClipNodes(); + applyClipClipNodes(); else getElement().style.height = CSSProperties.HeightUnionType.of(toPx(height)); } @@ -97,7 +97,7 @@ public void updateArcHeight(Double arcHeight) { private void updateBorderRadius() { if (isClip()) - applyClipPathToClipNodes(); + applyClipClipNodes(); else { Rectangle r = getNode(); getElement().style.borderRadius = CSSProperties.BorderRadiusUnionType.of(toPx(r.getArcWidth()/2) + " " + toPx(r.getArcHeight()/2)); diff --git a/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/html/HtmlScenePeer.java b/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/html/HtmlScenePeer.java index a75e98565..b142b1d9e 100644 --- a/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/html/HtmlScenePeer.java +++ b/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/html/HtmlScenePeer.java @@ -4,9 +4,12 @@ import dev.webfx.kit.mapper.peers.javafxgraphics.NodePeer; import dev.webfx.kit.mapper.peers.javafxgraphics.emul_coupling.base.ScenePeerBase; import dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.shared.HtmlSvgNodePeer; +import dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.shared.SvgRoot; +import dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.shared.SvgRootBase; import dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.util.FxEvents; import dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.util.HtmlPaints; import dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.util.HtmlUtil; +import dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.util.SvgUtil; import dev.webfx.kit.util.properties.FXProperties; import dev.webfx.platform.console.Console; import dev.webfx.platform.uischeduler.UiScheduler; @@ -14,6 +17,7 @@ import dev.webfx.platform.util.Strings; import dev.webfx.platform.util.collection.Collections; import elemental2.dom.*; +import elemental2.svg.SVGSVGElement; import elemental2.webstorage.WebStorageWindow; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; @@ -524,6 +528,21 @@ else if (htmlKey.startsWith("Numpad")) } } + private SvgRoot svgRoot; + + public SvgRoot getSvgRoot() { + if (svgRoot == null) { + SVGSVGElement svgRootBaseSvg = SvgUtil.createSvgElement(); + svgRootBaseSvg.setAttribute("width", "0"); + svgRootBaseSvg.setAttribute("height", "0"); + svgRoot = new SvgRootBase(); + svgRootBaseSvg.append(svgRoot.getDefsElement()); + document.body.appendChild(svgRootBaseSvg); + Console.log("svgRootBaseSvg added to document.body"); + } + return svgRoot; + } + // Utility method to help mapping observable lists private static void mapObservableList(ObservableList ol, Consumer> adder, Consumer> remover) { diff --git a/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/html/HtmlSubtractShapePeer.java b/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/html/HtmlSubtractShapePeer.java new file mode 100644 index 000000000..900b3f24b --- /dev/null +++ b/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/html/HtmlSubtractShapePeer.java @@ -0,0 +1,70 @@ +package dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.html; + +import dev.webfx.kit.mapper.peers.javafxgraphics.base.ShapePeerBase; +import dev.webfx.kit.mapper.peers.javafxgraphics.base.ShapePeerMixin; +import dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.svg.SvgCirclePeer; +import dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.svg.SvgRectanglePeer; +import dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.util.HtmlUtil; +import dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.util.SvgUtil; +import elemental2.dom.Element; +import elemental2.dom.HTMLElement; +import javafx.scene.shape.Circle; +import javafx.scene.shape.Rectangle; +import javafx.scene.shape.Shape; +import javafx.scene.shape.SubtractShape; + +/** + * + * + * @author Bruno Salmon + */ +public final class HtmlSubtractShapePeer + , NM extends ShapePeerMixin> + + extends HtmlShapePeer + implements ShapePeerMixin { + + public HtmlSubtractShapePeer() { + this((NB) new ShapePeerBase(), HtmlUtil.createElement("fx-subtractshape")); + } + + public HtmlSubtractShapePeer(NB base, HTMLElement element) { + super(base, element); + } + + @Override + public Element computeClipMask() { + Element element1 = createSvgShape(getNode().getShape1()); + if (element1 == null) + return null; + Element element2 = createSvgShape(getNode().getShape2()); + if (element2 == null) + return null; + element1.setAttribute("fill", "white"); + element2.setAttribute("fill", "black"); + Element mask = SvgUtil.createSvgElement("mask"); + mask.append(element1, element2); + return mask; + } + + private Element createSvgShape(Shape shape) { + if (shape instanceof Rectangle) { + Rectangle r = (Rectangle) shape; + SvgRectanglePeer svgPeer = new SvgRectanglePeer<>(); + svgPeer.updateX(r.getX()); + svgPeer.updateY(r.getY()); + svgPeer.updateWidth(r.getWidth()); + svgPeer.updateHeight(r.getHeight()); + return svgPeer.getElement(); + } + if (shape instanceof Circle) { + Circle c = (Circle) shape; + SvgCirclePeer svgPeer = new SvgCirclePeer<>(); + svgPeer.updateCenterX(c.getCenterX()); + svgPeer.updateCenterY(c.getCenterY()); + svgPeer.updateRadius(c.getRadius()); + return svgPeer.getElement(); + } + return null; + } +} diff --git a/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/shared/HtmlSvgNodePeer.java b/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/shared/HtmlSvgNodePeer.java index 281aeb638..f6cbd26a2 100644 --- a/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/shared/HtmlSvgNodePeer.java +++ b/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/shared/HtmlSvgNodePeer.java @@ -11,6 +11,7 @@ import dev.webfx.kit.mapper.peers.javafxgraphics.base.NodePeerImpl; import dev.webfx.kit.mapper.peers.javafxgraphics.base.NodePeerMixin; import dev.webfx.kit.mapper.peers.javafxgraphics.emul_coupling.LayoutMeasurable; +import dev.webfx.kit.mapper.peers.javafxgraphics.emul_coupling.ScenePeer; import dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.html.UserInteraction; import dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.svg.SvgNodePeer; import dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.util.*; @@ -134,6 +135,11 @@ public void bind(N node, SceneRequester sceneRequester) { installFocusListeners(); } + protected ScenePeer getScenePeer() { + Scene scene = getNode().getScene(); + return scene == null ? null : scene.impl_getPeer(); + } + /******************************************** Drag & drop support *************************************************/ private EventListener dragStartListener; @@ -591,17 +597,27 @@ public void updateDisabled(Boolean disabled) { } } + private HtmlSvgNodePeer clipPeer; + @Override public void updateClip(Node clip) { + if (clipPeer != null) { + clipPeer.clipNodes.remove(getNode()); + clipPeer.cleanClipMaskIfUnused(); + } if (clip == null) applyClipPath(null); - else - ((HtmlSvgNodePeer) clip.getOrCreateAndBindNodePeer()).bindAsClip(getNode()); + else { + clipPeer = (HtmlSvgNodePeer) clip.getOrCreateAndBindNodePeer(); + clipPeer.bindAsClip(getNode()); + } } protected boolean clip; // true when this node is actually used as a clip (=> not part of the scene graph) - protected List clipNodes; // Contains the list of nodes that use this node as a clip protected String clipPath; + protected Element clipMask; + private static int clipMaskSeq; + protected List clipNodes; // Contains the list of nodes that use this node as a clip private void bindAsClip(Node clipNode) { clip = true; @@ -609,27 +625,39 @@ private void bindAsClip(Node clipNode) { clipNodes = new ArrayList<>(); if (!clipNodes.contains(clipNode)) clipNodes.add(clipNode); - applyClipPathToClipNode(clipNode); + applyClipToClipNode(clipNode); } protected final boolean isClip() { return clip; } - protected final void applyClipPathToClipNodes() { // Should be called when this node is a clip and that its properties has changed + protected final void applyClipClipNodes() { // Should be called when this node is a clip and that its properties has changed clipPath = null; // To force computation N thisClip = getNode(); for (Iterator it = clipNodes.iterator(); it.hasNext(); ) { Node clipNode = it.next(); if (clipNode.getClip() == thisClip) // checking the node is still using that clip - applyClipPathToClipNode(clipNode); + applyClipToClipNode(clipNode); else // Otherwise we remove that node from the clip nodes it.remove(); } + cleanClipMaskIfUnused(); + } + + private void cleanClipMaskIfUnused() { + if (clipMask != null && clipNodes.isEmpty()) { + getSvgRoot().getDefsElement().removeChild(clipMask); + clipMask = null; + } } - private void applyClipPathToClipNode(Node clipNode) { - ((HtmlSvgNodePeer) clipNode.getNodePeer()).applyClipPath(getClipPath()); + private void applyClipToClipNode(Node clipNode) { + getNode().setScene(clipNode.getScene()); // Ensuring this clip node as the same scene as the node it is applied + // A clip can be applied either through a clip path or through a svg mask + HtmlSvgNodePeer clipPeer = (HtmlSvgNodePeer) clipNode.getNodePeer(); + clipPeer.applyClipPath(getClipPath()); + clipPeer.applyClipMask(getClipMask()); } private String getClipPath() { @@ -638,7 +666,7 @@ private String getClipPath() { return clipPath; } - protected String computeClipPath() { // To override for nodes that can be used as clip (ex: rectangle, circle, etc...) + public String computeClipPath() { // To override for nodes that can be used as clip (ex: rectangle, circle, etc...) return null; } @@ -646,6 +674,27 @@ protected void applyClipPath(String clipPah) { setElementAttribute("clip-path", clipPah); } + private Element getClipMask() { + if (clipMask == null) { + clipMask = computeClipMask(); + if (clipMask != null) { + clipMask.setAttribute("id", "mask-" + ++clipMaskSeq); + getSvgRoot().addDef(clipMask); + } + } + return clipMask; + } + + protected abstract SvgRoot getSvgRoot(); + + public Element computeClipMask() { // To override for nodes that can be used as clip (ex: rectangle, circle, etc...) + return null; + } + + protected void applyClipMask(Element clipMask) { + setElementStyleAttribute("mask", SvgUtil.getDefUrl(clipMask)); + } + @Override public void updateCursor(Cursor cursor) { setElementStyleAttribute("cursor", toCssCursor(cursor)); diff --git a/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/svg/SvgNodePeer.java b/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/svg/SvgNodePeer.java index 5589eae37..947288514 100644 --- a/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/svg/SvgNodePeer.java +++ b/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/svg/SvgNodePeer.java @@ -1,5 +1,13 @@ package dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.svg; +import dev.webfx.kit.mapper.peers.javafxgraphics.base.NodePeerBase; +import dev.webfx.kit.mapper.peers.javafxgraphics.base.NodePeerMixin; +import dev.webfx.kit.mapper.peers.javafxgraphics.emul_coupling.ScenePeer; +import dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.shared.HtmlSvgNodePeer; +import dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.shared.SvgRoot; +import dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.util.HtmlPaints; +import dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.util.HtmlUtil; +import dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.util.SvgUtil; import elemental2.dom.Element; import javafx.geometry.VPos; import javafx.scene.Node; @@ -9,14 +17,6 @@ import javafx.scene.paint.Paint; import javafx.scene.paint.RadialGradient; import javafx.scene.text.TextAlignment; -import dev.webfx.kit.mapper.peers.javafxgraphics.base.NodePeerBase; -import dev.webfx.kit.mapper.peers.javafxgraphics.base.NodePeerMixin; -import dev.webfx.kit.mapper.peers.javafxgraphics.emul_coupling.ScenePeer; -import dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.shared.HtmlSvgNodePeer; -import dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.shared.SvgRoot; -import dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.util.HtmlPaints; -import dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.util.HtmlUtil; -import dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.util.SvgUtil; import java.util.*; @@ -36,20 +36,26 @@ public abstract class SvgNodePeer } @Override - protected String computeClipPath() { - if (svgClipPath == null) - svgClipPath = getSvgRoot().addDef(SvgUtil.createClipPath()); - HtmlUtil.setChild(svgClipPath, getElement()); - return SvgUtil.getDefUrl(svgClipPath); + protected SvgScenePeer getScenePeer() { + return (SvgScenePeer) super.getScenePeer(); } - private SvgRoot getSvgRoot() { + @Override + protected SvgRoot getSvgRoot() { ScenePeer scenePeer = getNode().getScene().impl_getPeer(); if (scenePeer instanceof SvgRoot) return (SvgRoot) scenePeer; return (SvgRoot) getNode().getProperties().get("svgRoot"); } + @Override + public String computeClipPath() { + if (svgClipPath == null) + svgClipPath = getSvgRoot().addDef(SvgUtil.createClipPath()); + HtmlUtil.setChild(svgClipPath, getElement()); + return SvgUtil.getDefUrl(svgClipPath); + } + @Override protected String toFilter(Effect effect) { return SvgUtil.getDefUrl(toSvgEffectFilter(effect)); diff --git a/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/util/SvgUtil.java b/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/util/SvgUtil.java index 02d2314d7..f3d26c458 100644 --- a/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/util/SvgUtil.java +++ b/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/util/SvgUtil.java @@ -1,11 +1,12 @@ package dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.util; +import dev.webfx.platform.util.collection.Collections; import elemental2.dom.Element; +import elemental2.svg.SVGSVGElement; import javafx.scene.paint.*; import javafx.scene.shape.StrokeLineCap; import javafx.scene.shape.StrokeLineJoin; import javafx.scene.shape.StrokeType; -import dev.webfx.platform.util.collection.Collections; import static elemental2.dom.DomGlobal.document; @@ -20,8 +21,8 @@ public static Element createSvgElement(String tag) { return document.createElementNS(svgNS, tag); } - public static /*SVGElement Elemental2 compilation error */ Element createSvgElement() { - return /*SVGElement*/ createSvgElement("svg"); + public static SVGSVGElement createSvgElement() { + return (SVGSVGElement) createSvgElement("svg"); } public static Element createSvgDefs() { diff --git a/webfx-kit/webfx-kit-javafxgraphics-registry-gwt-j2cl/src/main/java/dev/webfx/kit/registry/javafxgraphics/JavaFxGraphicsRegistry.java b/webfx-kit/webfx-kit-javafxgraphics-registry-gwt-j2cl/src/main/java/dev/webfx/kit/registry/javafxgraphics/JavaFxGraphicsRegistry.java index d9f52b7c2..e44263df5 100644 --- a/webfx-kit/webfx-kit-javafxgraphics-registry-gwt-j2cl/src/main/java/dev/webfx/kit/registry/javafxgraphics/JavaFxGraphicsRegistry.java +++ b/webfx-kit/webfx-kit-javafxgraphics-registry-gwt-j2cl/src/main/java/dev/webfx/kit/registry/javafxgraphics/JavaFxGraphicsRegistry.java @@ -66,6 +66,10 @@ public static void registerLine() { registerNodePeerFactory(Line.class, HtmlLinePeer::new); } + public static void registerSubtractShape() { + registerNodePeerFactory(SubtractShape.class, HtmlSubtractShapePeer::new); + } + public static void registerText() { registerNodePeerFactory(Text.class, HtmlTextPeer::new); } diff --git a/webfx-kit/webfx-kit-javafxgraphics-registry/src/main/java/dev/webfx/kit/registry/javafxgraphics/JavaFxGraphicsRegistry.java b/webfx-kit/webfx-kit-javafxgraphics-registry/src/main/java/dev/webfx/kit/registry/javafxgraphics/JavaFxGraphicsRegistry.java index f01f0763c..640d075c9 100644 --- a/webfx-kit/webfx-kit-javafxgraphics-registry/src/main/java/dev/webfx/kit/registry/javafxgraphics/JavaFxGraphicsRegistry.java +++ b/webfx-kit/webfx-kit-javafxgraphics-registry/src/main/java/dev/webfx/kit/registry/javafxgraphics/JavaFxGraphicsRegistry.java @@ -12,6 +12,8 @@ public class JavaFxGraphicsRegistry { public static native void registerLine(); + public static native void registerSubtractShape(); + public static native void registerText(); public static native void registerImageView(); From 6ab33c31b6b4e45478ed16fed23eae01c1e72bdb Mon Sep 17 00:00:00 2001 From: Bruno Salmon Date: Sat, 3 Aug 2024 21:41:26 +0100 Subject: [PATCH 05/25] Fixed NPE in Font when family is null --- .../src/main/java/javafx/scene/text/Font.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/webfx-kit/webfx-kit-javafxgraphics-emul/src/main/java/javafx/scene/text/Font.java b/webfx-kit/webfx-kit-javafxgraphics-emul/src/main/java/javafx/scene/text/Font.java index accc5cbef..71f36cfb9 100644 --- a/webfx-kit/webfx-kit-javafxgraphics-emul/src/main/java/javafx/scene/text/Font.java +++ b/webfx-kit/webfx-kit-javafxgraphics-emul/src/main/java/javafx/scene/text/Font.java @@ -189,7 +189,7 @@ public boolean equals(Object o) { if (Double.compare(font.size, size) != 0) return false; if (!Objects.equals(name, font.name)) return false; - if (!family.equals(font.family)) return false; + if (!Objects.equals(family, font.family)) return false; if (weight != font.weight) return false; if (posture != font.posture) return false; return Objects.equals(url, font.url); @@ -198,13 +198,11 @@ public boolean equals(Object o) { @Override public int hashCode() { int result; - long temp; result = name != null ? name.hashCode() : 0; - result = 31 * result + family.hashCode(); + result = family != null ? 31 * result + family.hashCode() : 0; result = 31 * result + weight.hashCode(); result = 31 * result + posture.hashCode(); - temp = Double.doubleToLongBits(size); - result = 31 * result + (int) (temp ^ (temp >>> 32)); + result = 31 * result + Double.hashCode(size); result = 31 * result + (url != null ? url.hashCode() : 0); return result; } From 6b5f4cca41b2e3140ae207e6e26e112eb5302583 Mon Sep 17 00:00:00 2001 From: Bruno Salmon Date: Mon, 5 Aug 2024 23:04:32 +0100 Subject: [PATCH 06/25] Fixed possible exceptions in Media & GwtJ2clMediaPlayerPeer --- .../main/java/javafx/scene/media/Media.java | 2 +- .../spi/gwtj2cl/GwtJ2clMediaPlayerPeer.java | 40 ++++++++++--------- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/webfx-kit/webfx-kit-javafxmedia-emul/src/main/java/javafx/scene/media/Media.java b/webfx-kit/webfx-kit-javafxmedia-emul/src/main/java/javafx/scene/media/Media.java index 56b1b315d..214d1e78d 100644 --- a/webfx-kit/webfx-kit-javafxmedia-emul/src/main/java/javafx/scene/media/Media.java +++ b/webfx-kit/webfx-kit-javafxmedia-emul/src/main/java/javafx/scene/media/Media.java @@ -30,7 +30,7 @@ public ReadOnlyObjectProperty durationProperty() { } public Duration getDuration() { - return durationProperty.get(); + return durationProperty().get(); } // For WebFX internal usage only diff --git a/webfx-kit/webfx-kit-javafxmedia-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxmedia/spi/gwtj2cl/GwtJ2clMediaPlayerPeer.java b/webfx-kit/webfx-kit-javafxmedia-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxmedia/spi/gwtj2cl/GwtJ2clMediaPlayerPeer.java index 336dc4bec..b42f0a3a1 100644 --- a/webfx-kit/webfx-kit-javafxmedia-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxmedia/spi/gwtj2cl/GwtJ2clMediaPlayerPeer.java +++ b/webfx-kit/webfx-kit-javafxmedia-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxmedia/spi/gwtj2cl/GwtJ2clMediaPlayerPeer.java @@ -112,8 +112,7 @@ public void setMediaElement(HTMLMediaElement mediaElement) { // GwtMediaViewPeer this.mediaElement = mediaElement; if (!audioClip) { mediaElement.onloadedmetadata = e -> { - setMediaDuration(mediaElement.duration); - mediaPlayer.setStatus(MediaPlayer.Status.READY); + readAndSetMediaDuration(true); return null; }; } @@ -136,12 +135,17 @@ public HTMLMediaElement getMediaElement() { return mediaElement; } - private void setMediaDuration(double seconds) { - setMediaDuration(Duration.seconds(seconds)); - } - - private void setMediaDuration(Duration duration) { - mediaPlayer.getMedia().setDuration(duration); + private double readAndSetMediaDuration(boolean setMediaPlayerStatusToReady) { + double durationSeconds = hasMediaElement() ? mediaElement.duration : audioBuffer.duration; + if (Double.isFinite(durationSeconds)) { + Duration duration = Duration.seconds(durationSeconds); + Media media = mediaPlayer.getMedia(); + if (!Objects.equals(media.getDuration(), duration)) + media.setDuration(duration); + } + if (setMediaPlayerStatusToReady) + mediaPlayer.setStatus(MediaPlayer.Status.READY); + return durationSeconds; } private void setMediaPlayerCurrentTime(double seconds) { @@ -199,10 +203,8 @@ private void fetchAudioBuffer(boolean resumeIfSuspended) { .then(getAudioContext()::decodeAudioData) .then(buffer -> { audioBuffer = buffer; - if (!audioClip) { - mediaPlayer.setStatus(MediaPlayer.Status.READY); - setMediaDuration(audioBuffer.duration); - } + if (!audioClip) + readAndSetMediaDuration(true); onAudioBufferReady(); return null; }) @@ -543,15 +545,17 @@ private void doOnEnded() { @Override public void seek(Duration duration) { // This method is never called for AudioClip - double jsDuration = Math.max(0, duration.toSeconds()); // Can't be negative - jsDuration = Math.min(jsDuration, mediaPlayer.getMedia().getDuration().toSeconds()); - setMediaPlayerCurrentTime(jsDuration); + double durationSeconds = Math.max(0, duration.toSeconds()); // Can't be negative + double mediaDurationSeconds = readAndSetMediaDuration(false); + if (Double.isFinite(mediaDurationSeconds)) // Sometimes mediaElement.duration returns infinite for some unknown reason + durationSeconds = Math.min(durationSeconds, mediaDurationSeconds); + setMediaPlayerCurrentTime(durationSeconds); if (hasMediaElement()) - mediaElement.currentTime = jsDuration; + mediaElement.currentTime = durationSeconds; else { - bufferSourceStopWatchMillis.startAt(secondsDoubleToMillisLong(jsDuration)); + bufferSourceStopWatchMillis.startAt(secondsDoubleToMillisLong(durationSeconds)); bufferSourceStopWatchMillis.pause(); - bufferSourceStartOffset = jsDuration; + bufferSourceStartOffset = durationSeconds; if (bufferSource != null && bufferSourcePlayed) { seekingBufferSource = true; bufferSourceWasPlayingOnSeeking = !isBufferSourcePaused(); From 2e465d9ef06194f2bcc4cb128c697409061a735d Mon Sep 17 00:00:00 2001 From: Bruno Salmon Date: Sun, 11 Aug 2024 22:30:00 +0100 Subject: [PATCH 07/25] Hided html images until loaded to prevent initial layout issues --- .../javafxgraphics/gwtj2cl/html/HtmlImageViewPeer.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/html/HtmlImageViewPeer.java b/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/html/HtmlImageViewPeer.java index c5f158948..0a18b817a 100644 --- a/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/html/HtmlImageViewPeer.java +++ b/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/html/HtmlImageViewPeer.java @@ -76,8 +76,8 @@ public void updateImage(Image image) { setElementAttribute("alt", imageUrl); // But removing the alt text and hiding the image if the link is broken (to align with JavaFX behaviour which doesn't display such things) setElementAttribute("onerror", "this.style.display='none'; this.alt=''"); - // When we change the image url, we remove the possible display='none' of the previous image (if it was on error) - setElementStyleAttribute("display", null); + // Better to not display the image until it is loaded to prevent initial layout issues + setElementStyleAttribute("display", "none"); // Special case of a canvas image (ex: the WebFX WritableImage emulation code stored the image in a canvas) HTMLCanvasElement canvasElement = CanvasElementHelper.getCanvasElementAssociatedWithImage(image); if (canvasElement != null) { @@ -104,6 +104,8 @@ private void onLoaded() { if (sizeChangedCallback != null) sizeChangedCallback.run(); loaded = true; + // Now that it's loaded, we can display it + setElementStyleAttribute("display", null); } public static void onHTMLImageLoaded(HTMLImageElement imageElement, Image image) { From 284409168b16cdf39142efeccd4b40af54ad579b Mon Sep 17 00:00:00 2001 From: Bruno Salmon Date: Wed, 14 Aug 2024 15:16:29 +0100 Subject: [PATCH 08/25] Fixed ScrollPane min/pref/max computation issues --- .../java/javafx/scene/control/ScrollPane.java | 38 +++++++++++++++++-- .../gwtj2cl/html/HtmlScrollPanePeer.java | 3 +- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/webfx-kit/webfx-kit-javafxcontrols-emul/src/main/java/javafx/scene/control/ScrollPane.java b/webfx-kit/webfx-kit-javafxcontrols-emul/src/main/java/javafx/scene/control/ScrollPane.java index 55dc245be..2d97f90b9 100644 --- a/webfx-kit/webfx-kit-javafxcontrols-emul/src/main/java/javafx/scene/control/ScrollPane.java +++ b/webfx-kit/webfx-kit-javafxcontrols-emul/src/main/java/javafx/scene/control/ScrollPane.java @@ -15,8 +15,8 @@ public class ScrollPane extends Control { private Runnable onChildrenLayout; public ScrollPane() { - // The purpose of this code is to register the mouse handler so it captures focus on mouse click - new BehaviorSkinBase(this, new ScrollPaneBehavior(this)) {}; + // The purpose of this code is to register the mouse handler, so it captures focus on mouse click + new BehaviorSkinBase<>(this, new ScrollPaneBehavior(this)) {}; } public ScrollPane(Node content) { @@ -24,6 +24,36 @@ public ScrollPane(Node content) { setContent(content); } + public boolean shouldUseLayoutMeasurable() { + return false; + } + + @Override protected double computeMinWidth(double height) { + return 0; + } + + @Override protected double computePrefWidth(double height) { + Node content = getContent(); + return content == null ? 0 : content.prefWidth(height); + } + + @Override protected double computeMaxWidth(double height) { + return Double.MAX_VALUE; + } + + @Override protected double computeMinHeight(double width) { + return 0; + } + + @Override protected double computePrefHeight(double width) { + Node content = getContent(); + return content == null ? 0 : content.prefHeight(width); + } + + @Override protected double computeMaxHeight(double width) { + return Double.MAX_VALUE; + } + @Override protected void sceneToLocal(com.sun.javafx.geom.Point2D pt) { super.sceneToLocal(pt); @@ -40,7 +70,7 @@ protected void localToScene(com.sun.javafx.geom.Point2D pt) { super.localToScene(pt); } - private Property hbarPolicyProperty = new SimpleObjectProperty<>(ScrollBarPolicy.AS_NEEDED); + private final Property hbarPolicyProperty = new SimpleObjectProperty<>(ScrollBarPolicy.AS_NEEDED); public Property hbarPolicyProperty() { return hbarPolicyProperty; @@ -54,7 +84,7 @@ public ScrollBarPolicy getHbarPolicy() { return hbarPolicyProperty.getValue(); } - private Property vbarPolicyProperty = new SimpleObjectProperty<>(ScrollBarPolicy.AS_NEEDED); + private final Property vbarPolicyProperty = new SimpleObjectProperty<>(ScrollBarPolicy.AS_NEEDED); public Property vbarPolicyProperty() { return vbarPolicyProperty; diff --git a/webfx-kit/webfx-kit-javafxcontrols-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxcontrols/gwtj2cl/html/HtmlScrollPanePeer.java b/webfx-kit/webfx-kit-javafxcontrols-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxcontrols/gwtj2cl/html/HtmlScrollPanePeer.java index 097e31b63..507de883e 100644 --- a/webfx-kit/webfx-kit-javafxcontrols-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxcontrols/gwtj2cl/html/HtmlScrollPanePeer.java +++ b/webfx-kit/webfx-kit-javafxcontrols-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxcontrols/gwtj2cl/html/HtmlScrollPanePeer.java @@ -4,7 +4,6 @@ import dev.webfx.kit.mapper.peers.javafxcontrols.base.ScrollPanePeerMixin; import dev.webfx.kit.mapper.peers.javafxgraphics.SceneRequester; import dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.html.HtmlRegionPeer; -import dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.html.layoutmeasurable.HtmlLayoutMeasurable; import dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.util.HtmlUtil; import dev.webfx.kit.util.properties.FXProperties; import dev.webfx.platform.uischeduler.UiScheduler; @@ -24,7 +23,7 @@ public final class HtmlScrollPanePeer , NM extends ScrollPanePeerMixin> extends HtmlRegionPeer - implements ScrollPanePeerMixin, HtmlLayoutMeasurable { + implements ScrollPanePeerMixin { public HtmlScrollPanePeer() { this((NB) new ScrollPanePeerBase(), HtmlUtil.createElement("fx-scrollpane")); From ee9b589387614c3b036c33eb59b5325f3e3e8c89 Mon Sep 17 00:00:00 2001 From: Bruno Salmon Date: Wed, 14 Aug 2024 15:17:12 +0100 Subject: [PATCH 09/25] Fixed mask not reset on clip reset --- .../peers/javafxgraphics/gwtj2cl/shared/HtmlSvgNodePeer.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/shared/HtmlSvgNodePeer.java b/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/shared/HtmlSvgNodePeer.java index f6cbd26a2..6577e629a 100644 --- a/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/shared/HtmlSvgNodePeer.java +++ b/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/shared/HtmlSvgNodePeer.java @@ -605,9 +605,10 @@ public void updateClip(Node clip) { clipPeer.clipNodes.remove(getNode()); clipPeer.cleanClipMaskIfUnused(); } - if (clip == null) + if (clip == null) { applyClipPath(null); - else { + applyClipMask(null); + } else { clipPeer = (HtmlSvgNodePeer) clip.getOrCreateAndBindNodePeer(); clipPeer.bindAsClip(getNode()); } From f8b636f7cf6de33b197eeb9664da040190c76bd5 Mon Sep 17 00:00:00 2001 From: Bruno Salmon Date: Thu, 15 Aug 2024 15:59:34 +0100 Subject: [PATCH 10/25] Checked audio player status is unknown before transiting to ready --- .../peers/javafxmedia/spi/gwtj2cl/GwtJ2clMediaPlayerPeer.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/webfx-kit/webfx-kit-javafxmedia-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxmedia/spi/gwtj2cl/GwtJ2clMediaPlayerPeer.java b/webfx-kit/webfx-kit-javafxmedia-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxmedia/spi/gwtj2cl/GwtJ2clMediaPlayerPeer.java index b42f0a3a1..748d8c6be 100644 --- a/webfx-kit/webfx-kit-javafxmedia-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxmedia/spi/gwtj2cl/GwtJ2clMediaPlayerPeer.java +++ b/webfx-kit/webfx-kit-javafxmedia-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxmedia/spi/gwtj2cl/GwtJ2clMediaPlayerPeer.java @@ -143,7 +143,8 @@ private double readAndSetMediaDuration(boolean setMediaPlayerStatusToReady) { if (!Objects.equals(media.getDuration(), duration)) media.setDuration(duration); } - if (setMediaPlayerStatusToReady) + // if requested, we set the player status to READY, checking however its status was UNKNOWN (initial state) + if (setMediaPlayerStatusToReady && mediaPlayer.getStatus() == MediaPlayer.Status.UNKNOWN) mediaPlayer.setStatus(MediaPlayer.Status.READY); return durationSeconds; } From a020319b302010980a4422c1a804dbfffd9841a8 Mon Sep 17 00:00:00 2001 From: Bruno Salmon Date: Thu, 15 Aug 2024 23:44:50 +0100 Subject: [PATCH 11/25] Added getSafeAreaInsets() support in WebFxKitLauncher (GWT/J2CL implementation) --- .../css/webfx-kit-javafxgraphics-web@main.css | 5 ++++ .../GwtJ2clWebFxKitLauncherProvider.java | 25 +++++++++++++++++++ .../css/webfx-kit-javafxgraphics-web@main.css | 4 +++ .../webfx/kit/launcher/WebFxKitLauncher.java | 5 ++++ .../spi/WebFxKitLauncherProvider.java | 5 ++++ 5 files changed, 44 insertions(+) diff --git a/webfx-kit/webfx-kit-javafxgraphics-fat-j2cl/src/main/webfx/css/webfx-kit-javafxgraphics-web@main.css b/webfx-kit/webfx-kit-javafxgraphics-fat-j2cl/src/main/webfx/css/webfx-kit-javafxgraphics-web@main.css index f68e5f8fc..2f7749676 100644 --- a/webfx-kit/webfx-kit-javafxgraphics-fat-j2cl/src/main/webfx/css/webfx-kit-javafxgraphics-web@main.css +++ b/webfx-kit/webfx-kit-javafxgraphics-fat-j2cl/src/main/webfx/css/webfx-kit-javafxgraphics-web@main.css @@ -1,4 +1,8 @@ :root { + --safe-area-inset-top: env(safe-area-inset-top); + --safe-area-inset-right: env(safe-area-inset-right); + --safe-area-inset-bottom: env(safe-area-inset-bottom); + --safe-area-inset-left: env(safe-area-inset-left); --fx-svg-path-fill: black; } @@ -26,4 +30,5 @@ fx-svgpath svg path:not([fill]):not([stroke]) { /* if the application code didn' fx-svgpath svg path:not([fill])[stroke] { /* if the application code set the stroke but not the fill */ --fx-svg-path-fill: transparent; /* then the fill is transparent */ + fill: var(--fx-svg-path-fill); } \ No newline at end of file diff --git a/webfx-kit/webfx-kit-javafxgraphics-gwt-j2cl/src/main/java/dev/webfx/kit/launcher/spi/impl/gwtj2cl/GwtJ2clWebFxKitLauncherProvider.java b/webfx-kit/webfx-kit-javafxgraphics-gwt-j2cl/src/main/java/dev/webfx/kit/launcher/spi/impl/gwtj2cl/GwtJ2clWebFxKitLauncherProvider.java index 1e4976431..5aadb4489 100644 --- a/webfx-kit/webfx-kit-javafxgraphics-gwt-j2cl/src/main/java/dev/webfx/kit/launcher/spi/impl/gwtj2cl/GwtJ2clWebFxKitLauncherProvider.java +++ b/webfx-kit/webfx-kit-javafxgraphics-gwt-j2cl/src/main/java/dev/webfx/kit/launcher/spi/impl/gwtj2cl/GwtJ2clWebFxKitLauncherProvider.java @@ -14,6 +14,7 @@ import dev.webfx.kit.util.properties.FXProperties; import dev.webfx.platform.console.Console; import dev.webfx.platform.useragent.UserAgent; +import dev.webfx.platform.util.Numbers; import dev.webfx.platform.util.Strings; import dev.webfx.platform.util.collection.Collections; import dev.webfx.platform.util.function.Factory; @@ -25,6 +26,7 @@ import javafx.collections.ObservableList; import javafx.geometry.BoundingBox; import javafx.geometry.Bounds; +import javafx.geometry.Insets; import javafx.geometry.Rectangle2D; import javafx.scene.Scene; import javafx.scene.canvas.Canvas; @@ -254,4 +256,27 @@ public void launchApplication(Factory applicationFactory, String... public Application getApplication() { return application; } + + @Override + public Insets getSafeAreaInsets() { + /* The following code is relying on this CSS rule present in webfx-kit-javafxgraphics-web@main.css + :root { + --safe-area-inset-top: env(safe-area-inset-top); + --safe-area-inset-right: env(safe-area-inset-right); + --safe-area-inset-bottom: env(safe-area-inset-bottom); + --safe-area-inset-left: env(safe-area-inset-left); + } + */ + CSSStyleDeclaration computedStyle = Js.uncheckedCast(DomGlobal.window).getComputedStyle(DomGlobal.document.documentElement); + String top = computedStyle.getPropertyValue("--safe-area-inset-top"); + String right = computedStyle.getPropertyValue("--safe-area-inset-right"); + String bottom = computedStyle.getPropertyValue("--safe-area-inset-bottom"); + String left = computedStyle.getPropertyValue("--safe-area-inset-left"); + return new Insets( + Numbers.doubleValue(Strings.removeSuffix(top, "px")), + Numbers.doubleValue(Strings.removeSuffix(right, "px")), + Numbers.doubleValue(Strings.removeSuffix(bottom, "px")), + Numbers.doubleValue(Strings.removeSuffix(left, "px")) + ); + } } \ No newline at end of file diff --git a/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/webfx/css/webfx-kit-javafxgraphics-web@main.css b/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/webfx/css/webfx-kit-javafxgraphics-web@main.css index 035a39c86..2f7749676 100644 --- a/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/webfx/css/webfx-kit-javafxgraphics-web@main.css +++ b/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/webfx/css/webfx-kit-javafxgraphics-web@main.css @@ -1,4 +1,8 @@ :root { + --safe-area-inset-top: env(safe-area-inset-top); + --safe-area-inset-right: env(safe-area-inset-right); + --safe-area-inset-bottom: env(safe-area-inset-bottom); + --safe-area-inset-left: env(safe-area-inset-left); --fx-svg-path-fill: black; } diff --git a/webfx-kit/webfx-kit-launcher/src/main/java/dev/webfx/kit/launcher/WebFxKitLauncher.java b/webfx-kit/webfx-kit-launcher/src/main/java/dev/webfx/kit/launcher/WebFxKitLauncher.java index 7a21011d3..1835f7c61 100644 --- a/webfx-kit/webfx-kit-launcher/src/main/java/dev/webfx/kit/launcher/WebFxKitLauncher.java +++ b/webfx-kit/webfx-kit-launcher/src/main/java/dev/webfx/kit/launcher/WebFxKitLauncher.java @@ -8,6 +8,7 @@ import javafx.application.Application; import javafx.collections.ObservableList; import javafx.geometry.Bounds; +import javafx.geometry.Insets; import javafx.scene.canvas.Canvas; import javafx.scene.canvas.GraphicsContext; import javafx.scene.image.Image; @@ -133,4 +134,8 @@ else if (webFxCssPath.contains(":")) return "dev/webfx/kit/css/" + webFxCssPath; } + public static Insets getSafeAreaInsets() { + return getProvider().getSafeAreaInsets(); + } + } diff --git a/webfx-kit/webfx-kit-launcher/src/main/java/dev/webfx/kit/launcher/spi/WebFxKitLauncherProvider.java b/webfx-kit/webfx-kit-launcher/src/main/java/dev/webfx/kit/launcher/spi/WebFxKitLauncherProvider.java index d052f25e2..7beb6bd62 100644 --- a/webfx-kit/webfx-kit-launcher/src/main/java/dev/webfx/kit/launcher/spi/WebFxKitLauncherProvider.java +++ b/webfx-kit/webfx-kit-launcher/src/main/java/dev/webfx/kit/launcher/spi/WebFxKitLauncherProvider.java @@ -6,6 +6,7 @@ import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.geometry.Bounds; +import javafx.geometry.Insets; import javafx.scene.Scene; import javafx.scene.canvas.Canvas; import javafx.scene.canvas.GraphicsContext; @@ -95,4 +96,8 @@ default double getDefaultCanvasPixelDensity() { default ObservableList loadingFonts() { return FXCollections.emptyObservableList(); // Default implementation fpr synchronous font loading toolkits (such as OpenJFX) } + + default Insets getSafeAreaInsets() { + return Insets.EMPTY; + } } From b35759880dddef1acde903b893894a7eded04bb9 Mon Sep 17 00:00:00 2001 From: Bruno Salmon Date: Sun, 18 Aug 2024 22:59:02 +0100 Subject: [PATCH 12/25] Add OpenJFX implementation of new UiScheduler method --- .../uischeduler/spi/impl/openjfx/FxUiSchedulerProvider.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/webfx-kit/webfx-kit-javafxgraphics-openjfx/src/main/java/dev/webfx/platform/uischeduler/spi/impl/openjfx/FxUiSchedulerProvider.java b/webfx-kit/webfx-kit-javafxgraphics-openjfx/src/main/java/dev/webfx/platform/uischeduler/spi/impl/openjfx/FxUiSchedulerProvider.java index 4cc65c8e2..6331a2a4b 100644 --- a/webfx-kit/webfx-kit-javafxgraphics-openjfx/src/main/java/dev/webfx/platform/uischeduler/spi/impl/openjfx/FxUiSchedulerProvider.java +++ b/webfx-kit/webfx-kit-javafxgraphics-openjfx/src/main/java/dev/webfx/platform/uischeduler/spi/impl/openjfx/FxUiSchedulerProvider.java @@ -52,6 +52,12 @@ public void handle(long now) { } }; + @Override + protected boolean isSystemAnimationFrameRunning() { + // As opposed to the browser, OpenJFX never stops running animation frames + return true; + } + @Override protected void requestAnimationFrame(Runnable runnable) { animationRunnable = runnable; From e853fb42445b35714d6bc0d3291915f3d59c321c Mon Sep 17 00:00:00 2001 From: Bruno Salmon Date: Mon, 19 Aug 2024 16:31:20 +0100 Subject: [PATCH 13/25] Fixed SVGPath.impl_computeGeomBounds(): was not forcing peer creation --- .../src/main/java/javafx/scene/shape/SVGPath.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webfx-kit/webfx-kit-javafxgraphics-emul/src/main/java/javafx/scene/shape/SVGPath.java b/webfx-kit/webfx-kit-javafxgraphics-emul/src/main/java/javafx/scene/shape/SVGPath.java index fc1df5ca9..6a349552a 100644 --- a/webfx-kit/webfx-kit-javafxgraphics-emul/src/main/java/javafx/scene/shape/SVGPath.java +++ b/webfx-kit/webfx-kit-javafxgraphics-emul/src/main/java/javafx/scene/shape/SVGPath.java @@ -181,7 +181,7 @@ public String getName() { private static boolean warned; @Override public BaseBounds impl_computeGeomBounds(BaseBounds bounds, BaseTransform tx) { - NodePeer nodePeer = getNodePeer(); + NodePeer nodePeer = getOrCreateAndBindNodePeer(); if (nodePeer instanceof LayoutMeasurable) { Bounds layoutBounds = ((LayoutMeasurable) nodePeer).getLayoutBounds(); return new BoxBounds((float) layoutBounds.getMinX(), (float) layoutBounds.getMinY(), 0, (float) layoutBounds.getMaxX(), (float) layoutBounds.getMaxY(), 0); From ced921195d80476e7ef148c04f2990246f038795 Mon Sep 17 00:00:00 2001 From: Bruno Salmon Date: Mon, 19 Aug 2024 16:33:18 +0100 Subject: [PATCH 14/25] Moved --fx-border-xxx CSS variables declaration from javafx-controls to javafx-graphics --- .../webfx/css/webfx-kit-javafxcontrols-web@main.css | 4 ---- .../webfx/css/webfx-kit-javafxgraphics-web@main.css | 12 ++++++++++++ .../webfx/css/webfx-kit-javafxgraphics-web@main.css | 12 ++++++++++++ 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/webfx-kit/webfx-kit-javafxcontrols-peers-gwt-j2cl/src/main/webfx/css/webfx-kit-javafxcontrols-web@main.css b/webfx-kit/webfx-kit-javafxcontrols-peers-gwt-j2cl/src/main/webfx/css/webfx-kit-javafxcontrols-web@main.css index effe580ca..38c9e8e25 100644 --- a/webfx-kit/webfx-kit-javafxcontrols-peers-gwt-j2cl/src/main/webfx/css/webfx-kit-javafxcontrols-web@main.css +++ b/webfx-kit/webfx-kit-javafxcontrols-peers-gwt-j2cl/src/main/webfx/css/webfx-kit-javafxcontrols-web@main.css @@ -1,9 +1,5 @@ /***** Global variables *****/ :root { - --fx-border-color: #c0c0c0; - --fx-border-radius: 5px; - --fx-border-style: solid; - --fx-border-width: 1px; --fx-border-color-focus: #0096D6; --fx-textfield-background: white; } diff --git a/webfx-kit/webfx-kit-javafxgraphics-fat-j2cl/src/main/webfx/css/webfx-kit-javafxgraphics-web@main.css b/webfx-kit/webfx-kit-javafxgraphics-fat-j2cl/src/main/webfx/css/webfx-kit-javafxgraphics-web@main.css index 2f7749676..3e50d1b01 100644 --- a/webfx-kit/webfx-kit-javafxgraphics-fat-j2cl/src/main/webfx/css/webfx-kit-javafxgraphics-web@main.css +++ b/webfx-kit/webfx-kit-javafxgraphics-fat-j2cl/src/main/webfx/css/webfx-kit-javafxgraphics-web@main.css @@ -3,6 +3,11 @@ --safe-area-inset-right: env(safe-area-inset-right); --safe-area-inset-bottom: env(safe-area-inset-bottom); --safe-area-inset-left: env(safe-area-inset-left); + --fx-border-color: #c0c0c0; + --fx-border-radius: 5px; + --fx-border-style: solid; + --fx-border-width: 1px; + --fx-border-color-focus: #0096D6; --fx-svg-path-fill: black; } @@ -23,6 +28,13 @@ body { opacity: 50%; } +.fx-border > fx-border { + border-color: var(--fx-border-color); + border-style: var(--fx-border-style); + border-width: var(--fx-border-width); + border-radius: var(--fx-border-radius); +} + /* Applying the default JavaFX behaviour for SVGPath */ fx-svgpath svg path:not([fill]):not([stroke]) { /* if the application code didn't set neither fill nor stroke */ fill: var(--fx-svg-path-fill); /* then the fill is black */ diff --git a/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/webfx/css/webfx-kit-javafxgraphics-web@main.css b/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/webfx/css/webfx-kit-javafxgraphics-web@main.css index 2f7749676..3e50d1b01 100644 --- a/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/webfx/css/webfx-kit-javafxgraphics-web@main.css +++ b/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/webfx/css/webfx-kit-javafxgraphics-web@main.css @@ -3,6 +3,11 @@ --safe-area-inset-right: env(safe-area-inset-right); --safe-area-inset-bottom: env(safe-area-inset-bottom); --safe-area-inset-left: env(safe-area-inset-left); + --fx-border-color: #c0c0c0; + --fx-border-radius: 5px; + --fx-border-style: solid; + --fx-border-width: 1px; + --fx-border-color-focus: #0096D6; --fx-svg-path-fill: black; } @@ -23,6 +28,13 @@ body { opacity: 50%; } +.fx-border > fx-border { + border-color: var(--fx-border-color); + border-style: var(--fx-border-style); + border-width: var(--fx-border-width); + border-radius: var(--fx-border-radius); +} + /* Applying the default JavaFX behaviour for SVGPath */ fx-svgpath svg path:not([fill]):not([stroke]) { /* if the application code didn't set neither fill nor stroke */ fill: var(--fx-svg-path-fill); /* then the fill is black */ From 7b47f7f6d5e2989bb5fdac9464213438ea74e1f0 Mon Sep 17 00:00:00 2001 From: Bruno Salmon Date: Tue, 20 Aug 2024 22:42:17 +0100 Subject: [PATCH 15/25] Fixed EventHandlerManager webfx custom code: early event handlers could be missed --- webfx-kit/webfx-kit-javafxbase-emul/pom.xml | 6 ++++++ .../sun/javafx/event/EventHandlerManager.java | 21 +++++++++++++------ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/webfx-kit/webfx-kit-javafxbase-emul/pom.xml b/webfx-kit/webfx-kit-javafxbase-emul/pom.xml index 1d57091f4..37d951e90 100644 --- a/webfx-kit/webfx-kit-javafxbase-emul/pom.xml +++ b/webfx-kit/webfx-kit-javafxbase-emul/pom.xml @@ -23,6 +23,12 @@ true + + dev.webfx + webfx-platform-util + 0.1.0-SNAPSHOT + + \ No newline at end of file diff --git a/webfx-kit/webfx-kit-javafxbase-emul/src/main/java/com/sun/javafx/event/EventHandlerManager.java b/webfx-kit/webfx-kit-javafxbase-emul/src/main/java/com/sun/javafx/event/EventHandlerManager.java index ec8c2e8f3..d71f4be23 100644 --- a/webfx-kit/webfx-kit-javafxbase-emul/src/main/java/com/sun/javafx/event/EventHandlerManager.java +++ b/webfx-kit/webfx-kit-javafxbase-emul/src/main/java/com/sun/javafx/event/EventHandlerManager.java @@ -1,10 +1,13 @@ package com.sun.javafx.event; +import dev.webfx.platform.util.tuples.Pair; import javafx.event.Event; import javafx.event.EventHandler; import javafx.event.EventType; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; /** @@ -229,15 +232,21 @@ private static void validateEventFilter( public interface EventSourcesListener { void onEventSource(EventType eventType, Object eventSource); } - private static EventSourcesListener eventSourcesListener; + private static EventSourcesListener EVENT_SOURCES_LISTENER; + private static final List, Object>> PENDING_NOTIFY_EVENTS = new ArrayList<>(); - public static void setEventSourcesListener(EventSourcesListener eventSourcesListener) { - EventHandlerManager.eventSourcesListener = eventSourcesListener; + private static void notifyEventSourcesListener(EventType eventType, Object eventSource) { + if (EVENT_SOURCES_LISTENER != null) { + EVENT_SOURCES_LISTENER.onEventSource(eventType, eventSource); + } else { + PENDING_NOTIFY_EVENTS.add(new Pair<>(eventType, eventSource)); + } } - private static void notifyEventSourcesListener(EventType eventType, Object eventSource) { - if (eventSourcesListener != null) - eventSourcesListener.onEventSource(eventType, eventSource); + public static void setEventSourcesListener(EventSourcesListener eventSourcesListener) { + EVENT_SOURCES_LISTENER = eventSourcesListener; + PENDING_NOTIFY_EVENTS.forEach(pair -> eventSourcesListener.onEventSource(pair.get1(), pair.get2())); + PENDING_NOTIFY_EVENTS.clear(); } } From a18c19b8715fab413465a1bebae697ec8cfd7ac1 Mon Sep 17 00:00:00 2001 From: Bruno Salmon Date: Tue, 20 Aug 2024 22:44:56 +0100 Subject: [PATCH 16/25] Added possible passive mode on scene touch events (used by HtmlScrollPanePeer on mobiles) --- .../gwtj2cl/shared/HtmlSvgNodePeer.java | 52 +++++++++++++++---- 1 file changed, 42 insertions(+), 10 deletions(-) diff --git a/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/shared/HtmlSvgNodePeer.java b/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/shared/HtmlSvgNodePeer.java index 6577e629a..48ecbf9d0 100644 --- a/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/shared/HtmlSvgNodePeer.java +++ b/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/shared/HtmlSvgNodePeer.java @@ -15,6 +15,7 @@ import dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.html.UserInteraction; import dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.svg.SvgNodePeer; import dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.util.*; +import dev.webfx.platform.console.Console; import dev.webfx.platform.uischeduler.UiScheduler; import dev.webfx.platform.util.Booleans; import dev.webfx.platform.util.Strings; @@ -395,6 +396,16 @@ private void installTouchListeners(boolean swipe) { } } + private static boolean SCENE_TOUCH_PASSIVE_MODE = false; + + public static void setSceneTouchPassiveMode(boolean passive) { + SCENE_TOUCH_PASSIVE_MODE = passive; + } + + public static boolean isSceneTouchPassiveMode() { + return SCENE_TOUCH_PASSIVE_MODE; + } + private static final GestureRecognizers gestureRecognizers = new GestureRecognizers(); @@ -406,28 +417,49 @@ public static void installTouchListeners(EventTarget htmlTarget, javafx.event.Ev } private static void registerTouchListener(EventTarget htmlTarget, String type, javafx.event.EventTarget fxTarget) { - // We don't enable the browsers built-in touch scrolling features, because this is not a standard behaviour in - // JavaFX, and this can interfere with the user experience, especially with games. - // Note that this will cause a downgrade in Lighthouse. - AddEventListenerOptions passiveOption = AddEventListenerOptions.create(); - passiveOption.setPassive(false); // May be set to true in some cases to improve Lighthouse score - htmlTarget.addEventListener(type, e -> { + EventListener touchListener = e -> { UserInteraction.setUserInteracting(true); boolean fxConsumed = passHtmlTouchEventOnToFx((TouchEvent) e, type, fxTarget); if (fxConsumed) { - e.stopPropagation(); + if (SCENE_TOUCH_PASSIVE_MODE) { + Console.log("[WARNING] ⚠️ a touch event has been consumed by the JavaFX application code while a ScrollPane entered the JS passive mode, so its propagation couldn't be stopped as requested"); + } + e.stopPropagation(); // doesn't work in passive mode if (!UserInteraction.nextUserRunnableRequiresTouchEventDefault()) - e.preventDefault(); + e.preventDefault(); // doesn't work in passive mode } UserInteraction.setUserInteracting(false); - }, passiveOption); + }; + if (fxTarget instanceof Scene) { + AddEventListenerOptions passiveOption = AddEventListenerOptions.create(); + passiveOption.setPassive(true); + htmlTarget.addEventListener(type, e -> { + if (SCENE_TOUCH_PASSIVE_MODE) { + touchListener.handleEvent(e); + } + }, passiveOption); + passiveOption = AddEventListenerOptions.create(); + passiveOption.setPassive(false); // May be set to true in some cases to improve Lighthouse score + htmlTarget.addEventListener(type, e -> { + if (!SCENE_TOUCH_PASSIVE_MODE) { + touchListener.handleEvent(e); + } + }, passiveOption); + } else { + // We don't enable the browsers built-in touch scrolling features, because this is not a standard behaviour in + // JavaFX, and this can interfere with the user experience, especially with games. + // Note that this will cause a downgrade in Lighthouse. + AddEventListenerOptions passiveOption = AddEventListenerOptions.create(); + passiveOption.setPassive(false); // May be set to true in some cases to improve Lighthouse score + htmlTarget.addEventListener(type, touchListener, passiveOption); + } } protected static boolean passHtmlTouchEventOnToFx(TouchEvent e, String type, javafx.event.EventTarget fxTarget) { javafx.scene.input.TouchEvent fxTouchEvent = toFxTouchEvent(e, type, fxTarget); boolean consumed = passOnToFx(fxTarget, fxTouchEvent); // We simulate the JavaFX behaviour where unconsumed touch events are fired again as mouse events. - if (!consumed && fxTarget instanceof Scene) { // Only at the scene level + if (!consumed && fxTarget instanceof Scene && !SCENE_TOUCH_PASSIVE_MODE) { // Only at the scene level, and not in passive mode javafx.scene.input.TouchPoint p = fxTouchEvent.getTouchPoint(); EventType fxType = fxTouchEvent.getEventType(); EventType eventType = fxType == javafx.scene.input.TouchEvent.TOUCH_PRESSED ? javafx.scene.input.MouseEvent.MOUSE_PRESSED : fxType == javafx.scene.input.TouchEvent.TOUCH_MOVED ? javafx.scene.input.MouseEvent.MOUSE_DRAGGED : javafx.scene.input.MouseEvent.MOUSE_RELEASED; From eb9d4b4f34f82319b381e82190c4e4eb911337b7 Mon Sep 17 00:00:00 2001 From: Bruno Salmon Date: Tue, 20 Aug 2024 22:46:09 +0100 Subject: [PATCH 17/25] Added simple CSS mode for mobiles in HtmlScrollPanePeer (requires passive mode) --- .../pom.xml | 12 ++ .../gwtj2cl/html/HtmlScrollPanePeer.java | 120 +++++++++++++++--- 2 files changed, 114 insertions(+), 18 deletions(-) diff --git a/webfx-kit/webfx-kit-javafxcontrols-peers-gwt-j2cl/pom.xml b/webfx-kit/webfx-kit-javafxcontrols-peers-gwt-j2cl/pom.xml index 659e5c4f2..8fcf6b99b 100644 --- a/webfx-kit/webfx-kit-javafxcontrols-peers-gwt-j2cl/pom.xml +++ b/webfx-kit/webfx-kit-javafxcontrols-peers-gwt-j2cl/pom.xml @@ -80,6 +80,18 @@ true + + dev.webfx + webfx-platform-os + 0.1.0-SNAPSHOT + + + + dev.webfx + webfx-platform-scheduler + 0.1.0-SNAPSHOT + + dev.webfx webfx-platform-uischeduler diff --git a/webfx-kit/webfx-kit-javafxcontrols-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxcontrols/gwtj2cl/html/HtmlScrollPanePeer.java b/webfx-kit/webfx-kit-javafxcontrols-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxcontrols/gwtj2cl/html/HtmlScrollPanePeer.java index 507de883e..64e5160c3 100644 --- a/webfx-kit/webfx-kit-javafxcontrols-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxcontrols/gwtj2cl/html/HtmlScrollPanePeer.java +++ b/webfx-kit/webfx-kit-javafxcontrols-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxcontrols/gwtj2cl/html/HtmlScrollPanePeer.java @@ -4,8 +4,11 @@ import dev.webfx.kit.mapper.peers.javafxcontrols.base.ScrollPanePeerMixin; import dev.webfx.kit.mapper.peers.javafxgraphics.SceneRequester; import dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.html.HtmlRegionPeer; +import dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.shared.HtmlSvgNodePeer; import dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.util.HtmlUtil; import dev.webfx.kit.util.properties.FXProperties; +import dev.webfx.platform.os.OperatingSystem; +import dev.webfx.platform.scheduler.Scheduled; import dev.webfx.platform.uischeduler.UiScheduler; import elemental2.dom.Element; import elemental2.dom.HTMLElement; @@ -13,6 +16,7 @@ import javafx.geometry.Bounds; import javafx.scene.Node; import javafx.scene.control.ScrollPane; +import javafx.scene.input.TouchEvent; import jsinterop.base.Js; import jsinterop.base.JsPropertyMap; @@ -20,10 +24,16 @@ * @author Bruno Salmon */ public final class HtmlScrollPanePeer - , NM extends ScrollPanePeerMixin> + , NM extends ScrollPanePeerMixin> - extends HtmlRegionPeer - implements ScrollPanePeerMixin { + extends HtmlRegionPeer + implements ScrollPanePeerMixin { + + private static final boolean USE_CSS = OperatingSystem.isMobile(); // much smoother experience on mobiles but requires passive mode + private static final boolean USE_PERFECT_SCROLLBAR = !USE_CSS; // the perfect scrollbars looks better on desktops than in CSS mode + + private Scheduled cssScrollEndDetector; + private boolean cssScrollDetected; public HtmlScrollPanePeer() { this((NB) new ScrollPanePeerBase(), HtmlUtil.createElement("fx-scrollpane")); @@ -36,17 +46,63 @@ public HtmlScrollPanePeer(NB base, HTMLElement element) { @Override public void bind(N node, SceneRequester sceneRequester) { super.bind(node, sceneRequester); - // Important: perfect scrollbar expect a standard HTML container (won't work with fx-scrollpane) - HTMLElement psContainer = HtmlUtil.setStyle(HtmlUtil.createDivElement(), "position: absolute; width: 100%; height: 100%"); - setChildrenContainer(psContainer); - HtmlUtil.setChildren(getElement(), psContainer); - node.setOnChildrenLayout(HtmlScrollPanePeer.this::scheduleUpdate); + HTMLElement element = getElement(); + if (USE_PERFECT_SCROLLBAR) { + // Important: perfect scrollbar expect a standard HTML container (won't work with fx-scrollpane) + HTMLElement psContainer = HtmlUtil.setStyle(HtmlUtil.createDivElement(), "position: absolute; width: 100%; height: 100%"); + setChildrenContainer(psContainer); + HtmlUtil.setChildren(element, psContainer); + } else { // CSS mode which relies on the overflow-x & overflow-y values + // Important: the CSS mode will react to touch events only if the Scene touch events thar are passed to + // JavaFX originates from an event listener in passive mode, which is not the case by default with webfx, + // because the passive mode doesn't let us stop the propagation which is a feature that the application code + // requests when calling event.consume(). But it's very likely ok to disable this feature during a touch + // scroll to enjoy the smoothness of the scroll in passive mode on mobiles. + node.addEventHandler(TouchEvent.ANY, e -> { + // The purpose of this handler is to detect any touch event that targets the scroll pane while still in + // standard mode (non-passive). By chance, this detection happens just before it is passed to the scene, + // and it's the only and last opportunity to activate the passive mode. Without catching this event + // and switching to passive mode, the CSS overflow scrollbars wouldn't react to the touch events. + if (!HtmlSvgNodePeer.isSceneTouchPassiveMode()) { + HtmlSvgNodePeer.setSceneTouchPassiveMode(true); + // It's also important to detect the end of this touch scroll to go back to the standard mode + rescheduleCssScrollEndDetector(); + } + }); + // We intercept the JS scroll events to update the ScrollPane position in JavaFX when the html one changes + element.addEventListener("scroll", e -> { + if (element.scrollTop != scrollTop) + setScrollTop(element.scrollTop); + if (element.scrollLeft != scrollLeft) + setScrollLeft(element.scrollLeft); + // Also if this happens during a touch scroll, we report the detection of the scroll and reschedule the + // css scroll end detector. + if (HtmlSvgNodePeer.isSceneTouchPassiveMode()) { + cssScrollDetected = true; + rescheduleCssScrollEndDetector(); + } + }); + } + node.setOnChildrenLayout(HtmlScrollPanePeer.this::scheduleUiUpdate); // The following listener is to reestablish the scroll position on scene change. For ex when the user 1) switches // to another page through UI routing and then 2) come back, this node is removed from the scene graph at 1) => // scene = null until 2) => scene reestablished, but Perfect scrollbar lost its state when removed from the DOM. // This listener will trigger a schedule update at 2) which will restore the perfect scrollbar state (scrollTop // & scrollLeft will be reapplied). - FXProperties.runOnPropertiesChange(this::scheduleUpdate, node.sceneProperty()); + FXProperties.runOnPropertiesChange(this::scheduleUiUpdate, node.sceneProperty()); + } + + private void rescheduleCssScrollEndDetector() { + if (cssScrollEndDetector != null) + cssScrollEndDetector.cancel(); + cssScrollEndDetector = UiScheduler.scheduleDelay(200, () -> { + if (!cssScrollDetected) { + HtmlSvgNodePeer.setSceneTouchPassiveMode(false); + } else { + cssScrollDetected = false; + rescheduleCssScrollEndDetector(); + } + }); } private double scrollTop, scrollLeft; @@ -140,22 +196,28 @@ private void syncUiFromModel(boolean horizontal, boolean vertical) { scrollTop = 0; } } - scheduleUpdate(); + scheduleUiUpdate(); syncing = false; } private boolean pending, psInitialized; - private void scheduleUpdate() { + private void scheduleUiUpdate() { if (!pending) { pending = true; UiScheduler.scheduleDeferred(() -> { - Element psContainer = getChildrenContainer(); - if (!psInitialized) { - N node = getNode(); - callPerfectScrollbarInitialize(psContainer, node.getHbarPolicy() == ScrollPane.ScrollBarPolicy.NEVER, node.getvbarPolicy() == ScrollPane.ScrollBarPolicy.NEVER); - psInitialized = true; + if (USE_PERFECT_SCROLLBAR) { + Element psContainer = getChildrenContainer(); + if (!psInitialized) { + N node = getNode(); + callPerfectScrollbarInitialize(psContainer, node.getHbarPolicy() == ScrollPane.ScrollBarPolicy.NEVER, node.getvbarPolicy() == ScrollPane.ScrollBarPolicy.NEVER); + psInitialized = true; + } + callPerfectScrollbarUpdate(psContainer); + } else { + HTMLElement element = getElement(); + element.scrollTop = scrollTop; + element.scrollLeft = scrollLeft; } - callPerfectScrollbarUpdate(psContainer); pending = false; }); } @@ -166,7 +228,7 @@ private void callPerfectScrollbarInitialize(Element psContainer, boolean suppres Js.asPropertyMap(psContainer).set("ps", ps); psContainer.addEventListener("ps-scroll-x", e -> setScrollLeft(psContainer.scrollLeft)); psContainer.addEventListener("ps-scroll-y", e -> setScrollTop(psContainer.scrollTop)); - }; + } private void callPerfectScrollbarUpdate(Element psContainer) { psContainer.scrollLeft = scrollLeft; @@ -187,12 +249,34 @@ public void updateHeight(Number height) { vSyncUiFromModel(); } + private String scrollbarPolicyToOverflow(ScrollPane.ScrollBarPolicy scrollBarPolicy) { + if (scrollBarPolicy != null) { + switch (scrollBarPolicy) { + case ALWAYS: + return "scroll"; + case AS_NEEDED: + return "auto"; + case NEVER: + return "hidden"; + } + } + return "hidden"; + } + @Override public void updateHbarPolicy(ScrollPane.ScrollBarPolicy hbarPolicy) { + if (USE_CSS) { + setElementStyleAttribute("overflow-x", scrollbarPolicyToOverflow(hbarPolicy)); + } + // Note: with PerfectScrollbar, This value is considered during initialisation only } @Override public void updateVbarPolicy(ScrollPane.ScrollBarPolicy vbarPolicy) { + if (USE_CSS) { + setElementStyleAttribute("overflow-y", scrollbarPolicyToOverflow(vbarPolicy)); + } + // Note: with PerfectScrollbar, This value is considered during initialisation only } @Override From 63826ad394e97699506ca1f238f304315e964bf9 Mon Sep 17 00:00:00 2001 From: Bruno Salmon Date: Wed, 21 Aug 2024 01:18:32 +0100 Subject: [PATCH 18/25] Fixed possible NPE in HtmlWebViewPeer --- .../kit/mapper/peers/javafxweb/spi/gwt/HtmlWebViewPeer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webfx-kit/webfx-kit-javafxweb-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxweb/spi/gwt/HtmlWebViewPeer.java b/webfx-kit/webfx-kit-javafxweb-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxweb/spi/gwt/HtmlWebViewPeer.java index d7aede8ac..0776831f4 100644 --- a/webfx-kit/webfx-kit-javafxweb-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxweb/spi/gwt/HtmlWebViewPeer.java +++ b/webfx-kit/webfx-kit-javafxweb-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxweb/spi/gwt/HtmlWebViewPeer.java @@ -128,7 +128,7 @@ public void updateUrl(String url) { return null; }); } else { // Standard or replace mode - if (!"replace".equals(webfxLoadingMode)) { // Standard mode + if (!"replace".equals(webfxLoadingMode) || iFrame.contentWindow == null) { // Standard mode iFrame.src = url; // Standard way to load an iFrame // But it has 2 downsides (which is why webfx proposes alternative loading modes): // 1) it doesn't report any network errors (iFrame.onerror not called). Issue addressed by the webfx From 1ee65ce980957c1d27f1ee5e8eebe9a4b1ef4400 Mon Sep 17 00:00:00 2001 From: Bruno Salmon Date: Wed, 21 Aug 2024 11:27:02 +0100 Subject: [PATCH 19/25] Fixed possible exception in GwtJ2clMediaPlayerPeer (wrong way to stringify json) --- .../spi/gwtj2cl/GwtJ2clMediaPlayerPeer.java | 55 +++++++++++-------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/webfx-kit/webfx-kit-javafxmedia-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxmedia/spi/gwtj2cl/GwtJ2clMediaPlayerPeer.java b/webfx-kit/webfx-kit-javafxmedia-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxmedia/spi/gwtj2cl/GwtJ2clMediaPlayerPeer.java index 748d8c6be..79989dd23 100644 --- a/webfx-kit/webfx-kit-javafxmedia-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxmedia/spi/gwtj2cl/GwtJ2clMediaPlayerPeer.java +++ b/webfx-kit/webfx-kit-javafxmedia-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxmedia/spi/gwtj2cl/GwtJ2clMediaPlayerPeer.java @@ -7,14 +7,13 @@ import dev.webfx.platform.scheduler.Scheduled; import dev.webfx.platform.scheduler.Scheduler; import dev.webfx.platform.uischeduler.UiScheduler; -import elemental2.core.Global; import elemental2.core.JsObject; import elemental2.core.Uint8Array; import elemental2.dom.*; import elemental2.media.*; +import elemental2.promise.Promise; import elemental2.webstorage.Storage; import elemental2.webstorage.WebStorageWindow; -import elemental2.promise.Promise; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.scene.media.AudioSpectrumListener; @@ -25,6 +24,8 @@ import java.util.Objects; +import static elemental2.core.Global.JSON; + /** * @author Bruno Salmon */ @@ -196,24 +197,24 @@ private void fetchAudioBuffer(boolean resumeIfSuspended) { init.setMode("no-cors"); Request request = new Request(mediaUrl, init); DomGlobal.window.fetch(request) - .then(response -> { - if (!response.ok) - Console.log("HTTP error when fetching '" + mediaUrl + "', status = " + response.status); - return response.arrayBuffer(); - }) - .then(getAudioContext()::decodeAudioData) - .then(buffer -> { - audioBuffer = buffer; - if (!audioClip) - readAndSetMediaDuration(true); - onAudioBufferReady(); - return null; - }) - .catch_((Promise.CatchOnRejectedCallbackFn) error -> { - Console.log("Error while fetching '" + mediaUrl + "'"); - Console.logNative(error); - return null; - }); + .then(response -> { + if (!response.ok) + Console.log("HTTP error when fetching '" + mediaUrl + "', status = " + response.status); + return response.arrayBuffer(); + }) + .then(getAudioContext()::decodeAudioData) + .then(buffer -> { + audioBuffer = buffer; + if (!audioClip) + readAndSetMediaDuration(true); + onAudioBufferReady(); + return null; + }) + .catch_((Promise.CatchOnRejectedCallbackFn) error -> { + Console.log("Error while fetching '" + mediaUrl + "'"); + Console.logNative(error); + return null; + }); fetched = true; } else if (UserInteraction.hasUserNotInteractedYet()) UserInteraction.runOnNextUserInteraction(() -> fetchAudioBuffer(true)); @@ -315,12 +316,18 @@ private void callMediaElementPlay() { setUpCors(); // Now we try to play, and call onMediaElementPlaySuccess() on success (implying we were not blocked by CORS) mediaElement.play() - .then(e -> { onMediaElementPlaySuccess(mediaElement); return null; }); + .then(e -> { + onMediaElementPlaySuccess(mediaElement); + return null; + }); // If the CORS strategy was unknown, the previous play was in cors mode, and we try a second play in no-cors mode // and if it succeeds, we call onMediaElementPlaySuccess() which will understand that the working strategy is no-cors if (noCorsMediaElement != null) noCorsMediaElement.play() - .then(e -> { onMediaElementPlaySuccess(noCorsMediaElement); return null; }); + .then(e -> { + onMediaElementPlaySuccess(noCorsMediaElement); + return null; + }); } private void onMediaElementPlaySuccess(HTMLMediaElement mediaElement) { @@ -676,7 +683,7 @@ private String getMemorisedWorkingCrossOrigin() { Storage localStorage = WebStorageWindow.of(DomGlobal.window).localStorage; if (localStorage != null) { String item = localStorage.getItem(LOCAL_STORAGE_WORKING_CROSS_ORIGINS_KEY); - WORKING_CROSS_ORIGINS = Js.cast(Global.JSON.parse(item)); + WORKING_CROSS_ORIGINS = Js.cast(JSON.parse(item)); // ok to pass null (will return null) } if (WORKING_CROSS_ORIGINS == null) WORKING_CROSS_ORIGINS = JsObject.create(null); @@ -706,7 +713,7 @@ private void memoriseWorkingCrossOrigin(HTMLMediaElement mediaElement) { HtmlUtil.setJsJavaObjectAttribute(WORKING_CROSS_ORIGINS, mediaOrigin, workingCrossOrigin); Storage localStorage = WebStorageWindow.of(DomGlobal.window).localStorage; if (localStorage != null) { - localStorage.setItem(LOCAL_STORAGE_WORKING_CROSS_ORIGINS_KEY, "" + WORKING_CROSS_ORIGINS.toJSON()); + localStorage.setItem(LOCAL_STORAGE_WORKING_CROSS_ORIGINS_KEY, JSON.stringify(WORKING_CROSS_ORIGINS)); } } } From e7e76be3183c3618a844add394cf973c62f6713f Mon Sep 17 00:00:00 2001 From: Bruno Salmon Date: Thu, 22 Aug 2024 11:11:53 +0100 Subject: [PATCH 20/25] Improved implementation of css scroll end detector in HtmlScrollPanePeer --- .../gwtj2cl/html/HtmlScrollPanePeer.java | 36 +++++++------------ 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/webfx-kit/webfx-kit-javafxcontrols-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxcontrols/gwtj2cl/html/HtmlScrollPanePeer.java b/webfx-kit/webfx-kit-javafxcontrols-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxcontrols/gwtj2cl/html/HtmlScrollPanePeer.java index 64e5160c3..cefaf0aba 100644 --- a/webfx-kit/webfx-kit-javafxcontrols-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxcontrols/gwtj2cl/html/HtmlScrollPanePeer.java +++ b/webfx-kit/webfx-kit-javafxcontrols-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxcontrols/gwtj2cl/html/HtmlScrollPanePeer.java @@ -8,7 +8,6 @@ import dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.util.HtmlUtil; import dev.webfx.kit.util.properties.FXProperties; import dev.webfx.platform.os.OperatingSystem; -import dev.webfx.platform.scheduler.Scheduled; import dev.webfx.platform.uischeduler.UiScheduler; import elemental2.dom.Element; import elemental2.dom.HTMLElement; @@ -32,7 +31,8 @@ public final class HtmlScrollPanePeer private static final boolean USE_CSS = OperatingSystem.isMobile(); // much smoother experience on mobiles but requires passive mode private static final boolean USE_PERFECT_SCROLLBAR = !USE_CSS; // the perfect scrollbars looks better on desktops than in CSS mode - private Scheduled cssScrollEndDetector; + private double scrollTop, scrollLeft; + private boolean syncing; private boolean cssScrollDetected; public HtmlScrollPanePeer() { @@ -66,7 +66,16 @@ public void bind(N node, SceneRequester sceneRequester) { if (!HtmlSvgNodePeer.isSceneTouchPassiveMode()) { HtmlSvgNodePeer.setSceneTouchPassiveMode(true); // It's also important to detect the end of this touch scroll to go back to the standard mode - rescheduleCssScrollEndDetector(); + UiScheduler.schedulePeriodic(200, scheduled -> { // every 200ms + // if since the last 200ms no scroll events have been generated and the element looks stationary + if (!cssScrollDetected && element.scrollLeft == scrollLeft && element.scrollTop == scrollTop) { + // we consider it's the end of the touch scroll and go back to the standard mode + HtmlSvgNodePeer.setSceneTouchPassiveMode(false); + scheduled.cancel(); // we can stop this periodic check + } else { // otherwise the scroll is still happening + cssScrollDetected = false; // for next check in 200ms + } + }); } }); // We intercept the JS scroll events to update the ScrollPane position in JavaFX when the html one changes @@ -77,10 +86,7 @@ public void bind(N node, SceneRequester sceneRequester) { setScrollLeft(element.scrollLeft); // Also if this happens during a touch scroll, we report the detection of the scroll and reschedule the // css scroll end detector. - if (HtmlSvgNodePeer.isSceneTouchPassiveMode()) { - cssScrollDetected = true; - rescheduleCssScrollEndDetector(); - } + cssScrollDetected = true; }); } node.setOnChildrenLayout(HtmlScrollPanePeer.this::scheduleUiUpdate); @@ -92,22 +98,6 @@ public void bind(N node, SceneRequester sceneRequester) { FXProperties.runOnPropertiesChange(this::scheduleUiUpdate, node.sceneProperty()); } - private void rescheduleCssScrollEndDetector() { - if (cssScrollEndDetector != null) - cssScrollEndDetector.cancel(); - cssScrollEndDetector = UiScheduler.scheduleDelay(200, () -> { - if (!cssScrollDetected) { - HtmlSvgNodePeer.setSceneTouchPassiveMode(false); - } else { - cssScrollDetected = false; - rescheduleCssScrollEndDetector(); - } - }); - } - - private double scrollTop, scrollLeft; - private boolean syncing; - private void setScrollTop(double scrollTop) { this.scrollTop = scrollTop; vSyncModelFromUi(); From 6f5273546d3c8be96afca3947b86463350be8d2e Mon Sep 17 00:00:00 2001 From: Bruno Salmon Date: Thu, 22 Aug 2024 13:50:37 +0100 Subject: [PATCH 21/25] Adjusted css touch scroll stop detection periodicity from 200 to 300ms --- .../peers/javafxcontrols/gwtj2cl/html/HtmlScrollPanePeer.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webfx-kit/webfx-kit-javafxcontrols-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxcontrols/gwtj2cl/html/HtmlScrollPanePeer.java b/webfx-kit/webfx-kit-javafxcontrols-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxcontrols/gwtj2cl/html/HtmlScrollPanePeer.java index cefaf0aba..d08cab143 100644 --- a/webfx-kit/webfx-kit-javafxcontrols-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxcontrols/gwtj2cl/html/HtmlScrollPanePeer.java +++ b/webfx-kit/webfx-kit-javafxcontrols-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxcontrols/gwtj2cl/html/HtmlScrollPanePeer.java @@ -66,14 +66,14 @@ public void bind(N node, SceneRequester sceneRequester) { if (!HtmlSvgNodePeer.isSceneTouchPassiveMode()) { HtmlSvgNodePeer.setSceneTouchPassiveMode(true); // It's also important to detect the end of this touch scroll to go back to the standard mode - UiScheduler.schedulePeriodic(200, scheduled -> { // every 200ms + UiScheduler.schedulePeriodic(300, scheduled -> { // every 300ms // if since the last 200ms no scroll events have been generated and the element looks stationary if (!cssScrollDetected && element.scrollLeft == scrollLeft && element.scrollTop == scrollTop) { // we consider it's the end of the touch scroll and go back to the standard mode HtmlSvgNodePeer.setSceneTouchPassiveMode(false); scheduled.cancel(); // we can stop this periodic check } else { // otherwise the scroll is still happening - cssScrollDetected = false; // for next check in 200ms + cssScrollDetected = false; // for next check in 300ms } }); } From d59bb9ab40afde405a4ff63e1713da2d835c9b42 Mon Sep 17 00:00:00 2001 From: Bruno Salmon Date: Thu, 22 Aug 2024 13:50:37 +0100 Subject: [PATCH 22/25] Adjusted css touch scroll stop detection periodicity from 200 to 400ms --- .../gwtj2cl/html/HtmlScrollPanePeer.java | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/webfx-kit/webfx-kit-javafxcontrols-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxcontrols/gwtj2cl/html/HtmlScrollPanePeer.java b/webfx-kit/webfx-kit-javafxcontrols-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxcontrols/gwtj2cl/html/HtmlScrollPanePeer.java index cefaf0aba..ef933929f 100644 --- a/webfx-kit/webfx-kit-javafxcontrols-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxcontrols/gwtj2cl/html/HtmlScrollPanePeer.java +++ b/webfx-kit/webfx-kit-javafxcontrols-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxcontrols/gwtj2cl/html/HtmlScrollPanePeer.java @@ -65,15 +65,20 @@ public void bind(N node, SceneRequester sceneRequester) { // and switching to passive mode, the CSS overflow scrollbars wouldn't react to the touch events. if (!HtmlSvgNodePeer.isSceneTouchPassiveMode()) { HtmlSvgNodePeer.setSceneTouchPassiveMode(true); - // It's also important to detect the end of this touch scroll to go back to the standard mode - UiScheduler.schedulePeriodic(200, scheduled -> { // every 200ms - // if since the last 200ms no scroll events have been generated and the element looks stationary + // It's also important to detect the end of this touch scroll to go back to the standard mode, so we + // set up a periodic timer for that. It's important to choose a good value for the periodicity. If + // it's too short, this can lead to false stop detection, and switching off the passive mode will + // immediately interrupt the scroll, which is not a nice user experience. But a too long value will + // prevent the user to have standard interaction with JavaFX buttons for example just after the + // scroll stops. 400ms seems a good compromise. + UiScheduler.schedulePeriodic(400, scheduled -> { + // if since the last 400ms no scroll events have been generated and the element looks stationary if (!cssScrollDetected && element.scrollLeft == scrollLeft && element.scrollTop == scrollTop) { // we consider it's the end of the touch scroll and go back to the standard mode HtmlSvgNodePeer.setSceneTouchPassiveMode(false); scheduled.cancel(); // we can stop this periodic check } else { // otherwise the scroll is still happening - cssScrollDetected = false; // for next check in 200ms + cssScrollDetected = false; // for next check in 400ms } }); } From 634a932005cbdb984824bc96618ffeaa475634c9 Mon Sep 17 00:00:00 2001 From: Bruno Salmon Date: Fri, 23 Aug 2024 16:25:52 +0100 Subject: [PATCH 23/25] Prevented false scroll end detection using inertia --- .../gwtj2cl/html/HtmlScrollPanePeer.java | 56 +++++++++---- .../gwtj2cl/shared/HtmlSvgNodePeer.java | 78 ++++++++----------- 2 files changed, 71 insertions(+), 63 deletions(-) diff --git a/webfx-kit/webfx-kit-javafxcontrols-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxcontrols/gwtj2cl/html/HtmlScrollPanePeer.java b/webfx-kit/webfx-kit-javafxcontrols-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxcontrols/gwtj2cl/html/HtmlScrollPanePeer.java index ef933929f..c1a35cd69 100644 --- a/webfx-kit/webfx-kit-javafxcontrols-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxcontrols/gwtj2cl/html/HtmlScrollPanePeer.java +++ b/webfx-kit/webfx-kit-javafxcontrols-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxcontrols/gwtj2cl/html/HtmlScrollPanePeer.java @@ -8,7 +8,9 @@ import dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.util.HtmlUtil; import dev.webfx.kit.util.properties.FXProperties; import dev.webfx.platform.os.OperatingSystem; +import dev.webfx.platform.scheduler.Scheduled; import dev.webfx.platform.uischeduler.UiScheduler; +import elemental2.dom.AddEventListenerOptions; import elemental2.dom.Element; import elemental2.dom.HTMLElement; import javafx.geometry.BoundingBox; @@ -19,6 +21,8 @@ import jsinterop.base.Js; import jsinterop.base.JsPropertyMap; +import java.util.function.Consumer; + /** * @author Bruno Salmon */ @@ -63,26 +67,46 @@ public void bind(N node, SceneRequester sceneRequester) { // standard mode (non-passive). By chance, this detection happens just before it is passed to the scene, // and it's the only and last opportunity to activate the passive mode. Without catching this event // and switching to passive mode, the CSS overflow scrollbars wouldn't react to the touch events. - if (!HtmlSvgNodePeer.isSceneTouchPassiveMode()) { - HtmlSvgNodePeer.setSceneTouchPassiveMode(true); + if (!HtmlSvgNodePeer.isScrolling()) { + HtmlSvgNodePeer.setScrolling(true); // It's also important to detect the end of this touch scroll to go back to the standard mode, so we - // set up a periodic timer for that. It's important to choose a good value for the periodicity. If - // it's too short, this can lead to false stop detection, and switching off the passive mode will - // immediately interrupt the scroll, which is not a nice user experience. But a too long value will - // prevent the user to have standard interaction with JavaFX buttons for example just after the - // scroll stops. 400ms seems a good compromise. - UiScheduler.schedulePeriodic(400, scheduled -> { - // if since the last 400ms no scroll events have been generated and the element looks stationary - if (!cssScrollDetected && element.scrollLeft == scrollLeft && element.scrollTop == scrollTop) { - // we consider it's the end of the touch scroll and go back to the standard mode - HtmlSvgNodePeer.setSceneTouchPassiveMode(false); - scheduled.cancel(); // we can stop this periodic check - } else { // otherwise the scroll is still happening - cssScrollDetected = false; // for next check in 400ms + // set up a periodic timer for that. + UiScheduler.schedulePeriodicInAnimationFrame(100, new Consumer<>() { + private long lastMillis = System.currentTimeMillis(); + private double lastScrollTop = element.scrollTop, lastScrollLeft = element.scrollLeft; + private double scrollTopInertia, scrollLeftInertia; + @Override + public void accept(Scheduled scheduled) { + // We check if the scroll has become stationary since last call + double deltaTop = element.scrollTop - lastScrollTop; + double deltaLeft = element.scrollLeft - lastScrollLeft; + boolean stationary = deltaTop == 0 && deltaLeft == 0 && !cssScrollDetected; // also considering any scroll event + // Note: + long nowMillis = System.currentTimeMillis(); + long delayMillis = nowMillis - lastMillis; + // Skipping suspicious false stop detection for 2s + if (stationary && delayMillis < 2000 && (Math.abs(scrollTopInertia) > 0.05 || Math.abs(scrollLeftInertia) > 0.05)) { + return; + } + scrollTopInertia = deltaTop / delayMillis; + scrollLeftInertia = deltaLeft / delayMillis; + // if since the last period no scroll events have been generated and the element looks stationary + if (stationary) { + // we consider it's the end of the touch scroll and go back to the standard mode + HtmlSvgNodePeer.setScrolling(false); + scheduled.cancel(); // we can stop this periodic check + } else { + lastScrollLeft = element.scrollLeft; + lastScrollTop = element.scrollTop; + lastMillis = nowMillis; + cssScrollDetected = false; + } } }); } }); + AddEventListenerOptions passiveOption = AddEventListenerOptions.create(); + passiveOption.setPassive(true); // We intercept the JS scroll events to update the ScrollPane position in JavaFX when the html one changes element.addEventListener("scroll", e -> { if (element.scrollTop != scrollTop) @@ -92,7 +116,7 @@ public void bind(N node, SceneRequester sceneRequester) { // Also if this happens during a touch scroll, we report the detection of the scroll and reschedule the // css scroll end detector. cssScrollDetected = true; - }); + }, passiveOption); } node.setOnChildrenLayout(HtmlScrollPanePeer.this::scheduleUiUpdate); // The following listener is to reestablish the scroll position on scene change. For ex when the user 1) switches diff --git a/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/shared/HtmlSvgNodePeer.java b/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/shared/HtmlSvgNodePeer.java index 48ecbf9d0..44e4792c4 100644 --- a/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/shared/HtmlSvgNodePeer.java +++ b/webfx-kit/webfx-kit-javafxgraphics-peers-gwt-j2cl/src/main/java/dev/webfx/kit/mapper/peers/javafxgraphics/gwtj2cl/shared/HtmlSvgNodePeer.java @@ -396,19 +396,6 @@ private void installTouchListeners(boolean swipe) { } } - private static boolean SCENE_TOUCH_PASSIVE_MODE = false; - - public static void setSceneTouchPassiveMode(boolean passive) { - SCENE_TOUCH_PASSIVE_MODE = passive; - } - - public static boolean isSceneTouchPassiveMode() { - return SCENE_TOUCH_PASSIVE_MODE; - } - - private static final GestureRecognizers gestureRecognizers = new GestureRecognizers(); - - public static void installTouchListeners(EventTarget htmlTarget, javafx.event.EventTarget fxTarget) { registerTouchListener(htmlTarget, "touchstart", fxTarget); registerTouchListener(htmlTarget, "touchmove", fxTarget); @@ -417,49 +404,44 @@ public static void installTouchListeners(EventTarget htmlTarget, javafx.event.Ev } private static void registerTouchListener(EventTarget htmlTarget, String type, javafx.event.EventTarget fxTarget) { - EventListener touchListener = e -> { + // We don't enable the browsers built-in touch scrolling features, because this is not a standard behaviour in + // JavaFX, and this can interfere with the user experience, especially with games. + // Note that this will cause a downgrade in Lighthouse. + boolean passive = false; // May be set to true in some cases to improve Lighthouse score + AddEventListenerOptions passiveOption = AddEventListenerOptions.create(); + passiveOption.setPassive(passive); + htmlTarget.addEventListener(type, e -> { UserInteraction.setUserInteracting(true); boolean fxConsumed = passHtmlTouchEventOnToFx((TouchEvent) e, type, fxTarget); if (fxConsumed) { - if (SCENE_TOUCH_PASSIVE_MODE) { - Console.log("[WARNING] ⚠️ a touch event has been consumed by the JavaFX application code while a ScrollPane entered the JS passive mode, so its propagation couldn't be stopped as requested"); + e.stopPropagation(); + if (!UserInteraction.nextUserRunnableRequiresTouchEventDefault()) { + if (passive) { + Console.log("Couldn't prevent event default in passive mode"); + } else { + e.preventDefault(); // doesn't work in passive mode + } } - e.stopPropagation(); // doesn't work in passive mode - if (!UserInteraction.nextUserRunnableRequiresTouchEventDefault()) - e.preventDefault(); // doesn't work in passive mode } UserInteraction.setUserInteracting(false); - }; - if (fxTarget instanceof Scene) { - AddEventListenerOptions passiveOption = AddEventListenerOptions.create(); - passiveOption.setPassive(true); - htmlTarget.addEventListener(type, e -> { - if (SCENE_TOUCH_PASSIVE_MODE) { - touchListener.handleEvent(e); - } - }, passiveOption); - passiveOption = AddEventListenerOptions.create(); - passiveOption.setPassive(false); // May be set to true in some cases to improve Lighthouse score - htmlTarget.addEventListener(type, e -> { - if (!SCENE_TOUCH_PASSIVE_MODE) { - touchListener.handleEvent(e); - } - }, passiveOption); - } else { - // We don't enable the browsers built-in touch scrolling features, because this is not a standard behaviour in - // JavaFX, and this can interfere with the user experience, especially with games. - // Note that this will cause a downgrade in Lighthouse. - AddEventListenerOptions passiveOption = AddEventListenerOptions.create(); - passiveOption.setPassive(false); // May be set to true in some cases to improve Lighthouse score - htmlTarget.addEventListener(type, touchListener, passiveOption); - } + }, passiveOption); + } + + private static boolean SCROLLING = false; + + public static void setScrolling(boolean scrolling) { + SCROLLING = scrolling; + } + + public static boolean isScrolling() { + return SCROLLING; } protected static boolean passHtmlTouchEventOnToFx(TouchEvent e, String type, javafx.event.EventTarget fxTarget) { javafx.scene.input.TouchEvent fxTouchEvent = toFxTouchEvent(e, type, fxTarget); boolean consumed = passOnToFx(fxTarget, fxTouchEvent); // We simulate the JavaFX behaviour where unconsumed touch events are fired again as mouse events. - if (!consumed && fxTarget instanceof Scene && !SCENE_TOUCH_PASSIVE_MODE) { // Only at the scene level, and not in passive mode + if (!consumed && fxTarget instanceof Scene && !SCROLLING) { // Not during scrolling javafx.scene.input.TouchPoint p = fxTouchEvent.getTouchPoint(); EventType fxType = fxTouchEvent.getEventType(); EventType eventType = fxType == javafx.scene.input.TouchEvent.TOUCH_PRESSED ? javafx.scene.input.MouseEvent.MOUSE_PRESSED : fxType == javafx.scene.input.TouchEvent.TOUCH_MOVED ? javafx.scene.input.MouseEvent.MOUSE_DRAGGED : javafx.scene.input.MouseEvent.MOUSE_RELEASED; @@ -494,6 +476,8 @@ private static javafx.scene.input.TouchEvent toFxTouchEvent(TouchEvent e, String 0, e.shiftKey, e.ctrlKey, e.altKey, e.metaKey); } + private static final GestureRecognizers GESTURE_RECOGNIZERS = new GestureRecognizers(); + private static TouchPoint toFxTouchPoint(Touch touch, TouchEvent e, javafx.event.EventTarget fxTarget) { TouchPoint.State state = TouchPoint.State.STATIONARY; //if (e.changedTouches.asList().contains(touch)) { @@ -511,10 +495,10 @@ private static TouchPoint toFxTouchPoint(Touch touch, TouchEvent e, javafx.event long time = (long) (e.timeStamp * 1_000_000); SwipeGestureRecognizer.CURRENT_TARGET = fxTarget; if (state == TouchPoint.State.PRESSED) - gestureRecognizers.notifyBeginTouchEvent(time, 0, false, 1); - gestureRecognizers.notifyNextTouchEvent(time, state.name(), touchId, (int) touchX, (int) touchY, (int) touch.screenX, (int) touch.screenY); + GESTURE_RECOGNIZERS.notifyBeginTouchEvent(time, 0, false, 1); + GESTURE_RECOGNIZERS.notifyNextTouchEvent(time, state.name(), touchId, (int) touchX, (int) touchY, (int) touch.screenX, (int) touch.screenY); if (state == TouchPoint.State.RELEASED) - gestureRecognizers.notifyEndTouchEvent(time); + GESTURE_RECOGNIZERS.notifyEndTouchEvent(time); } PickResult pickResult = new PickResult(fxTarget, touchX, touchY); return new TouchPoint(touchId, state, touchX, touchY, touch.screenX, touch.screenY, fxTarget, pickResult); From 85d22b0e84c0edc652a462caf953aca96339a4b0 Mon Sep 17 00:00:00 2001 From: Bruno Salmon Date: Thu, 29 Aug 2024 23:05:00 +0100 Subject: [PATCH 24/25] Turned safeAreaInsets into a JavaFX property --- .../GwtJ2clWebFxKitLauncherProvider.java | 28 +++++++++++++------ .../JavaFxWebFxKitLauncherProvider.java | 11 +++++++- .../webfx/kit/launcher/WebFxKitLauncher.java | 7 ++++- .../spi/WebFxKitLauncherProvider.java | 5 +++- 4 files changed, 39 insertions(+), 12 deletions(-) diff --git a/webfx-kit/webfx-kit-javafxgraphics-gwt-j2cl/src/main/java/dev/webfx/kit/launcher/spi/impl/gwtj2cl/GwtJ2clWebFxKitLauncherProvider.java b/webfx-kit/webfx-kit-javafxgraphics-gwt-j2cl/src/main/java/dev/webfx/kit/launcher/spi/impl/gwtj2cl/GwtJ2clWebFxKitLauncherProvider.java index 5aadb4489..b0035c97f 100644 --- a/webfx-kit/webfx-kit-javafxgraphics-gwt-j2cl/src/main/java/dev/webfx/kit/launcher/spi/impl/gwtj2cl/GwtJ2clWebFxKitLauncherProvider.java +++ b/webfx-kit/webfx-kit-javafxgraphics-gwt-j2cl/src/main/java/dev/webfx/kit/launcher/spi/impl/gwtj2cl/GwtJ2clWebFxKitLauncherProvider.java @@ -21,8 +21,7 @@ import elemental2.dom.*; import javafx.application.Application; import javafx.application.HostServices; -import javafx.beans.property.DoubleProperty; -import javafx.beans.property.SimpleDoubleProperty; +import javafx.beans.property.*; import javafx.collections.ObservableList; import javafx.geometry.BoundingBox; import javafx.geometry.Bounds; @@ -257,8 +256,19 @@ public Application getApplication() { return application; } + private ObjectProperty safeAreaInsetsProperty = null; + @Override - public Insets getSafeAreaInsets() { + public ReadOnlyObjectProperty safeAreaInsetsProperty() { + if (safeAreaInsetsProperty == null) { + safeAreaInsetsProperty = new SimpleObjectProperty<>(Insets.EMPTY); + FXProperties.runNowAndOnPropertiesChange(this::updateSafeAreaInsets, + getPrimaryStage().widthProperty(), getPrimaryStage().heightProperty()); + } + return safeAreaInsetsProperty; + } + + public void updateSafeAreaInsets() { /* The following code is relying on this CSS rule present in webfx-kit-javafxgraphics-web@main.css :root { --safe-area-inset-top: env(safe-area-inset-top); @@ -272,11 +282,11 @@ public Insets getSafeAreaInsets() { String right = computedStyle.getPropertyValue("--safe-area-inset-right"); String bottom = computedStyle.getPropertyValue("--safe-area-inset-bottom"); String left = computedStyle.getPropertyValue("--safe-area-inset-left"); - return new Insets( - Numbers.doubleValue(Strings.removeSuffix(top, "px")), - Numbers.doubleValue(Strings.removeSuffix(right, "px")), - Numbers.doubleValue(Strings.removeSuffix(bottom, "px")), - Numbers.doubleValue(Strings.removeSuffix(left, "px")) - ); + safeAreaInsetsProperty.set(new Insets( + Numbers.doubleValue(Strings.removeSuffix(top, "px")), + Numbers.doubleValue(Strings.removeSuffix(right, "px")), + Numbers.doubleValue(Strings.removeSuffix(bottom, "px")), + Numbers.doubleValue(Strings.removeSuffix(left, "px")) + )); } } \ No newline at end of file diff --git a/webfx-kit/webfx-kit-javafxgraphics-openjfx/src/main/java/dev/webfx/kit/launcher/spi/impl/openjfx/JavaFxWebFxKitLauncherProvider.java b/webfx-kit/webfx-kit-javafxgraphics-openjfx/src/main/java/dev/webfx/kit/launcher/spi/impl/openjfx/JavaFxWebFxKitLauncherProvider.java index d740bcc8a..a0b63a15b 100644 --- a/webfx-kit/webfx-kit-javafxgraphics-openjfx/src/main/java/dev/webfx/kit/launcher/spi/impl/openjfx/JavaFxWebFxKitLauncherProvider.java +++ b/webfx-kit/webfx-kit-javafxgraphics-openjfx/src/main/java/dev/webfx/kit/launcher/spi/impl/openjfx/JavaFxWebFxKitLauncherProvider.java @@ -7,7 +7,10 @@ import dev.webfx.platform.util.function.Factory; import javafx.application.Application; import javafx.application.HostServices; +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.SimpleObjectProperty; import javafx.geometry.Bounds; +import javafx.geometry.Insets; import javafx.scene.image.Image; import javafx.scene.text.Font; import javafx.scene.text.Text; @@ -122,7 +125,6 @@ public void start(Stage primaryStage) throws Exception { application.start(primaryStage); } } - } @Override @@ -144,4 +146,11 @@ public double measureBaselineOffset(Font font) { measurementText.setFont(font); return measurementText.getBaselineOffset(); } + + private final ReadOnlyObjectProperty safeAreaInsetsProperty = new SimpleObjectProperty<>(Insets.EMPTY); + + @Override + public ReadOnlyObjectProperty safeAreaInsetsProperty() { + return safeAreaInsetsProperty; + } } diff --git a/webfx-kit/webfx-kit-launcher/src/main/java/dev/webfx/kit/launcher/WebFxKitLauncher.java b/webfx-kit/webfx-kit-launcher/src/main/java/dev/webfx/kit/launcher/WebFxKitLauncher.java index 1835f7c61..f9eceea3d 100644 --- a/webfx-kit/webfx-kit-launcher/src/main/java/dev/webfx/kit/launcher/WebFxKitLauncher.java +++ b/webfx-kit/webfx-kit-launcher/src/main/java/dev/webfx/kit/launcher/WebFxKitLauncher.java @@ -3,9 +3,10 @@ import dev.webfx.kit.launcher.spi.FastPixelReaderWriter; import dev.webfx.kit.launcher.spi.WebFxKitLauncherProvider; import dev.webfx.platform.console.Console; -import dev.webfx.platform.util.function.Factory; import dev.webfx.platform.service.SingleServiceProvider; +import dev.webfx.platform.util.function.Factory; import javafx.application.Application; +import javafx.beans.property.ReadOnlyObjectProperty; import javafx.collections.ObservableList; import javafx.geometry.Bounds; import javafx.geometry.Insets; @@ -134,6 +135,10 @@ else if (webFxCssPath.contains(":")) return "dev/webfx/kit/css/" + webFxCssPath; } + public static ReadOnlyObjectProperty safeAreaInsetsProperty() { + return getProvider().safeAreaInsetsProperty(); + } + public static Insets getSafeAreaInsets() { return getProvider().getSafeAreaInsets(); } diff --git a/webfx-kit/webfx-kit-launcher/src/main/java/dev/webfx/kit/launcher/spi/WebFxKitLauncherProvider.java b/webfx-kit/webfx-kit-launcher/src/main/java/dev/webfx/kit/launcher/spi/WebFxKitLauncherProvider.java index 7beb6bd62..60a86482a 100644 --- a/webfx-kit/webfx-kit-launcher/src/main/java/dev/webfx/kit/launcher/spi/WebFxKitLauncherProvider.java +++ b/webfx-kit/webfx-kit-launcher/src/main/java/dev/webfx/kit/launcher/spi/WebFxKitLauncherProvider.java @@ -3,6 +3,7 @@ import javafx.application.Application; import javafx.application.HostServices; import javafx.beans.property.DoubleProperty; +import javafx.beans.property.ReadOnlyObjectProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.geometry.Bounds; @@ -97,7 +98,9 @@ default ObservableList loadingFonts() { return FXCollections.emptyObservableList(); // Default implementation fpr synchronous font loading toolkits (such as OpenJFX) } + ReadOnlyObjectProperty safeAreaInsetsProperty(); + default Insets getSafeAreaInsets() { - return Insets.EMPTY; + return safeAreaInsetsProperty().get(); } } From 3ab658ccf6c9193d807aaded63e6876e3b15941b Mon Sep 17 00:00:00 2001 From: Bruno Salmon Date: Thu, 29 Aug 2024 23:48:22 +0100 Subject: [PATCH 25/25] Added a 500ms subsequent update of safe area insets (workaround for iPad bug) --- .../spi/impl/gwtj2cl/GwtJ2clWebFxKitLauncherProvider.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/webfx-kit/webfx-kit-javafxgraphics-gwt-j2cl/src/main/java/dev/webfx/kit/launcher/spi/impl/gwtj2cl/GwtJ2clWebFxKitLauncherProvider.java b/webfx-kit/webfx-kit-javafxgraphics-gwt-j2cl/src/main/java/dev/webfx/kit/launcher/spi/impl/gwtj2cl/GwtJ2clWebFxKitLauncherProvider.java index b0035c97f..b4a230254 100644 --- a/webfx-kit/webfx-kit-javafxgraphics-gwt-j2cl/src/main/java/dev/webfx/kit/launcher/spi/impl/gwtj2cl/GwtJ2clWebFxKitLauncherProvider.java +++ b/webfx-kit/webfx-kit-javafxgraphics-gwt-j2cl/src/main/java/dev/webfx/kit/launcher/spi/impl/gwtj2cl/GwtJ2clWebFxKitLauncherProvider.java @@ -13,6 +13,7 @@ import dev.webfx.kit.mapper.peers.javafxgraphics.gwtj2cl.util.HtmlUtil; import dev.webfx.kit.util.properties.FXProperties; import dev.webfx.platform.console.Console; +import dev.webfx.platform.uischeduler.UiScheduler; import dev.webfx.platform.useragent.UserAgent; import dev.webfx.platform.util.Numbers; import dev.webfx.platform.util.Strings; @@ -264,6 +265,9 @@ public ReadOnlyObjectProperty safeAreaInsetsProperty() { safeAreaInsetsProperty = new SimpleObjectProperty<>(Insets.EMPTY); FXProperties.runNowAndOnPropertiesChange(this::updateSafeAreaInsets, getPrimaryStage().widthProperty(), getPrimaryStage().heightProperty()); + // Workaround for a bug observed in the Gmail internal browser on iPad where the window width/height + // are still not final at the first opening. So we schedule a subsequent update to get final values. + UiScheduler.scheduleDelay(500, this::updateSafeAreaInsets); // 500ms seem enough } return safeAreaInsetsProperty; }