diff --git a/webfx-kit/webfx-kit-javafxbase-emul/pom.xml b/webfx-kit/webfx-kit-javafxbase-emul/pom.xml
index 1d57091f46..37d951e90d 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 ec8c2e8f3c..d71f4be238 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();
}
}
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 55dc245be0..2d97f90b90 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/pom.xml b/webfx-kit/webfx-kit-javafxcontrols-peers-gwt-j2cl/pom.xml
index 659e5c4f2f..8fcf6b99bd 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 097e31b633..c1a35cd69b 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,27 +4,40 @@
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.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.AddEventListenerOptions;
import elemental2.dom.Element;
import elemental2.dom.HTMLElement;
import javafx.geometry.BoundingBox;
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;
+import java.util.function.Consumer;
+
/**
* @author Bruno Salmon
*/
public final class HtmlScrollPanePeer
- , NM extends ScrollPanePeerMixin>
+ , NM extends 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
- extends HtmlRegionPeer
- implements ScrollPanePeerMixin, HtmlLayoutMeasurable {
+ private double scrollTop, scrollLeft;
+ private boolean syncing;
+ private boolean cssScrollDetected;
public HtmlScrollPanePeer() {
this((NB) new ScrollPanePeerBase(), HtmlUtil.createElement("fx-scrollpane"));
@@ -37,22 +50,83 @@ 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.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.
+ 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)
+ 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.
+ 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
// 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 double scrollTop, scrollLeft;
- private boolean syncing;
-
private void setScrollTop(double scrollTop) {
this.scrollTop = scrollTop;
vSyncModelFromUi();
@@ -141,22 +215,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;
});
}
@@ -167,7 +247,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;
@@ -188,12 +268,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
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 effe580cac..38c9e8e258 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-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 fc1df5ca97..6a349552af 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);
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 eac2913a02..48ac533a89 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 0000000000..c78b68c38a
--- /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-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 3cf12a5de6..71f36cfb9d 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;
@@ -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;
}
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 28553a638b..3e50d1b014 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,16 @@
+: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-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;
+}
+
/* Mocking some basic JavaFX behaviours */
body {
overflow: hidden; /* Disabling browser horizontal and vertical scroll bars */
@@ -15,11 +28,19 @@ 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: 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 */
+ 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 1e4976431e..b4a2302544 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,18 +13,20 @@
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;
import dev.webfx.platform.util.collection.Collections;
import dev.webfx.platform.util.function.Factory;
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;
+import javafx.geometry.Insets;
import javafx.geometry.Rectangle2D;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
@@ -254,4 +256,41 @@ public void launchApplication(Factory applicationFactory, String...
public Application getApplication() {
return application;
}
+
+ private ObjectProperty safeAreaInsetsProperty = null;
+
+ @Override
+ public ReadOnlyObjectProperty safeAreaInsetsProperty() {
+ if (safeAreaInsetsProperty == null) {
+ 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;
+ }
+
+ 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);
+ --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");
+ 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 d740bcc8a1..a0b63a15ba 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-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 4cc65c8e20..6331a2a4b1 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;
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 8707d0acee..80aaf7cb25 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 8e480b0341..14a329c37c 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/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 c5f1589483..0a18b817a1 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) {
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 651da13ffd..d0685e430a 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 3022093b84..9a544da3b6 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 a75e98565c..b142b1d9ed 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 0000000000..900b3f24b7
--- /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 281aeb638c..44e4792c4d 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,9 +11,11 @@
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.*;
+import dev.webfx.platform.console.Console;
import dev.webfx.platform.uischeduler.UiScheduler;
import dev.webfx.platform.util.Booleans;
import dev.webfx.platform.util.Strings;
@@ -134,6 +136,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;
@@ -389,9 +396,6 @@ private void installTouchListeners(boolean swipe) {
}
}
- 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);
@@ -403,25 +407,41 @@ private static void registerTouchListener(EventTarget htmlTarget, String type, j
// 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(false); // May be set to true in some cases to improve Lighthouse score
+ passiveOption.setPassive(passive);
htmlTarget.addEventListener(type, e -> {
UserInteraction.setUserInteracting(true);
boolean fxConsumed = passHtmlTouchEventOnToFx((TouchEvent) e, type, fxTarget);
if (fxConsumed) {
e.stopPropagation();
- if (!UserInteraction.nextUserRunnableRequiresTouchEventDefault())
- e.preventDefault();
+ if (!UserInteraction.nextUserRunnableRequiresTouchEventDefault()) {
+ if (passive) {
+ Console.log("Couldn't prevent event default in passive mode");
+ } else {
+ e.preventDefault(); // doesn't work in passive mode
+ }
+ }
}
UserInteraction.setUserInteracting(false);
}, 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) { // Only at the scene level
+ 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;
@@ -456,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)) {
@@ -473,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);
@@ -591,17 +613,28 @@ public void updateDisabled(Boolean disabled) {
}
}
+ private HtmlSvgNodePeer clipPeer;
+
@Override
public void updateClip(Node clip) {
- if (clip == null)
+ if (clipPeer != null) {
+ clipPeer.clipNodes.remove(getNode());
+ clipPeer.cleanClipMaskIfUnused();
+ }
+ if (clip == null) {
applyClipPath(null);
- else
- ((HtmlSvgNodePeer) clip.getOrCreateAndBindNodePeer()).bindAsClip(getNode());
+ applyClipMask(null);
+ } 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 +642,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 +683,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 +691,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 5589eae373..9472885146 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 02d2314d74..f3d26c458f 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-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 28553a638b..3e50d1b014 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,16 @@
+: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-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;
+}
+
/* Mocking some basic JavaFX behaviours */
body {
overflow: hidden; /* Disabling browser horizontal and vertical scroll bars */
@@ -15,11 +28,19 @@ 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: 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 */
+ fill: var(--fx-svg-path-fill);
}
\ No newline at end of file
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 d9f52b7c20..e44263df5f 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 f01f0763c3..640d075c94 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();
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 56b1b315df..214d1e78dd 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 336dc4bec7..79989dd238 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
*/
@@ -112,8 +113,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 +136,18 @@ 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 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;
}
private void setMediaPlayerCurrentTime(double seconds) {
@@ -191,26 +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) {
- mediaPlayer.setStatus(MediaPlayer.Status.READY);
- setMediaDuration(audioBuffer.duration);
- }
- 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));
@@ -312,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) {
@@ -543,15 +553,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();
@@ -671,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);
@@ -701,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));
}
}
}
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 d7aede8acb..0776831f47 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
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 7a21011d33..f9eceea3db 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,11 +3,13 @@
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;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.image.Image;
@@ -133,4 +135,12 @@ 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 d052f25e2e..60a86482aa 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,9 +3,11 @@
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;
+import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
@@ -95,4 +97,10 @@ default double getDefaultCanvasPixelDensity() {
default ObservableList loadingFonts() {
return FXCollections.emptyObservableList(); // Default implementation fpr synchronous font loading toolkits (such as OpenJFX)
}
+
+ ReadOnlyObjectProperty safeAreaInsetsProperty();
+
+ default Insets getSafeAreaInsets() {
+ return safeAreaInsetsProperty().get();
+ }
}