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(); + } }