Skip to content

Commit

Permalink
Failing test + initialCssState impl
Browse files Browse the repository at this point in the history
  • Loading branch information
mstr2 committed Oct 21, 2024
1 parent 6ac2dd3 commit b993d4e
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,10 @@ public static void reapplyCSS(Node node) {
nodeAccessor.reapplyCSS(node);
}

public static boolean isInitialCssState(Node node) {
return nodeAccessor.isInitialCssState(node);
}

public static void recalculateRelativeSizeProperties(Node node, Font fontForRelativeSizes) {
nodeAccessor.recalculateRelativeSizeProperties(node, fontForRelativeSizes);
}
Expand Down Expand Up @@ -391,6 +395,7 @@ boolean doComputeIntersects(Node node, PickRay pickRay,
void setLabeledBy(Node node, Node labeledBy);
Accessible getAccessible(Node node);
void reapplyCSS(Node node);
boolean isInitialCssState(Node node);
void recalculateRelativeSizeProperties(Node node, Font fontForRelativeSizes);
boolean isTreeVisible(Node node);
BooleanExpression treeVisibleProperty(Node node);
Expand Down
28 changes: 28 additions & 0 deletions modules/javafx.graphics/src/main/java/javafx/scene/Node.java
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,11 @@ public void reapplyCSS(Node node) {
node.reapplyCSS();
}

@Override
public boolean isInitialCssState(Node node) {
return node.initialCssState;
}

@Override
public void recalculateRelativeSizeProperties(Node node, Font fontForRelativeSizes) {
node.recalculateRelativeSizeProperties(fontForRelativeSizes);
Expand Down Expand Up @@ -1010,6 +1015,8 @@ protected void invalidated() {
}
updateDisabled();
computeDerivedDepthTest();
resetInitialCssStateFlag();

final Parent newParent = get();

// Update the focus bits before calling reapplyCss(), as the focus bits can affect CSS styling.
Expand Down Expand Up @@ -1106,6 +1113,8 @@ private void invalidatedScenes(Scene oldScene, SubScene oldSubScene) {
if (sceneChanged) {
if (newScene == null) {
completeTransitionTimers();
} else {
resetInitialCssStateFlag();
}
updateCanReceiveFocus();
if (isFocusTraversable()) {
Expand Down Expand Up @@ -10066,6 +10075,25 @@ private void doProcessCSS() {
}
}

/**
* A node is considered to be in its initial CSS state if it wasn't shown in a scene graph before.
* This flag is cleared after CSS processing was completed in a Scene pulse event. Note that manual
* calls to {@link #applyCss()} or similar methods will not clear this flag, since we consider all
* CSS processing before the Scene pulse to be part of the node's initial state.
*/
private boolean initialCssState = true;

private void resetInitialCssStateFlag() {
initialCssState = true;
Scene scene = getScene();
if (scene != null) {
scene.registerClearInitialCssStateFlag(this);
}
}

void clearInitialCssStateFlag() {
initialCssState = false;
}

/**
* A StyleHelper for this node.
Expand Down
19 changes: 19 additions & 0 deletions modules/javafx.graphics/src/main/java/javafx/scene/Scene.java
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,25 @@ private void doCSSPass() {
sceneRoot.clearDirty(com.sun.javafx.scene.DirtyBits.NODE_CSS);
sceneRoot.processCSS();
}

if (!clearInitialCssStateNodes.isEmpty()) {
for (var node : clearInitialCssStateNodes) {
node.clearInitialCssStateFlag();
}

clearInitialCssStateNodes.clear();
}
}

/**
* A list of nodes that have expressed their interest to be notified when the next CSS pass
* is completed. Nodes will use this event to determine whether they are in their initial
* CSS state (see {@link Node#initialCssState}.
*/
private final List<Node> clearInitialCssStateNodes = new ArrayList<>();

void registerClearInitialCssStateFlag(Node node) {
clearInitialCssStateNodes.add(node);
}

void doLayoutPass() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ public void startup() {
scene = new Scene(new Group(node));
stage = new Stage();
stage.setScene(scene);
stage.show();
}

@AfterEach
Expand Down Expand Up @@ -188,6 +187,10 @@ public void testRunningTimersAreTrackedInNode() {
Map<String, TransitionTimer> timers = NodeShim.getTransitionTimers(node);
assertNull(timers);

// Showing the stage causes the first Scene CSS pass, after which the node is
// eligible for CSS transitions.
stage.show();

// The hover state starts the timer.
node.pseudoClassStateChanged(PseudoClass.getPseudoClass("hover"), true);
node.applyCss();
Expand All @@ -213,6 +216,10 @@ public void testRunningTimerIsCompletedWhenNodeIsRemovedFromSceneGraph() {
node.applyCss();
assertNull(NodeShim.getTransitionTimers(node));

// Showing the stage causes the first Scene CSS pass, after which the node is
// eligible for CSS transitions.
stage.show();

// The hover state starts the timer.
node.pseudoClassStateChanged(PseudoClass.getPseudoClass("hover"), true);
node.applyCss();
Expand All @@ -238,6 +245,10 @@ public void testRunningTimerIsCompletedWhenNodeBecomesInvisible() {
node.applyCss();
assertNull(NodeShim.getTransitionTimers(node));

// Showing the stage causes the first Scene CSS pass, after which the node is
// eligible for CSS transitions.
stage.show();

// The hover state starts the timer.
node.pseudoClassStateChanged(PseudoClass.getPseudoClass("hover"), true);
node.applyCss();
Expand All @@ -251,4 +262,71 @@ public void testRunningTimerIsCompletedWhenNodeBecomesInvisible() {
assertEquals(1, node.getOpacity(), 0.001);
}

@Test
public void testTransitionIsStartedWhenInitialPropertyIsNotSpecifiedInStylesheet() {
String url = "data:text/css;base64," + Base64.getUrlEncoder().encodeToString("""
.testClass { transition: all 1s; }
.testClass:hover { -fx-opacity: 0.5; }
""".getBytes(StandardCharsets.UTF_8));

scene.getStylesheets().add(url);
node.getStyleClass().add("testClass");
node.applyCss();
assertNull(NodeShim.getTransitionTimers(node));

stage.show();
node.pseudoClassStateChanged(PseudoClass.getPseudoClass("hover"), true);
toolkit.firePulse();
assertEquals(1, NodeShim.getTransitionTimers(node).size());
}

@Test
public void testTransitionIsOnlyStartedAfterSceneHasProcessedCSS() {
String url = "data:text/css;base64," + Base64.getUrlEncoder().encodeToString("""
.testClass { -fx-opacity: 0; transition: all 1s; }
.testClass:hover { -fx-opacity: 1; }
""".getBytes(StandardCharsets.UTF_8));

scene.getStylesheets().add(url);
node.getStyleClass().add("testClass");
node.applyCss();
assertNull(NodeShim.getTransitionTimers(node));

// Even though we apply CSS here, no transition is started because the Scene has not
// processed CSS in response to a pulse event yet.
node.pseudoClassStateChanged(PseudoClass.getPseudoClass("hover"), true);
node.applyCss();
assertNull(NodeShim.getTransitionTimers(node));

// This will fire the first pulse event, which also does not start the transition
// because the initial CSS pass only establishes the initial CSS state for the node.
node.pseudoClassStateChanged(PseudoClass.getPseudoClass("hover"), false);
stage.show();
assertNull(NodeShim.getTransitionTimers(node));

// This will fire the second pulse event, which will start the transition.
node.pseudoClassStateChanged(PseudoClass.getPseudoClass("hover"), true);
toolkit.firePulse();
assertEquals(1, NodeShim.getTransitionTimers(node).size());
}

@Test
public void testInitialCssStateFlagIsSetWhenAddedToOrRemovedFromScene() {
stage.show();

// The initial CSS pass is already done, so the initialCssState flag is cleared.
assertFalse(NodeHelper.isInitialCssState(node));

// Removing the node from the scene resets the initialCssState flag.
((Group)scene.getRoot()).getChildren().remove(0);
assertTrue(NodeHelper.isInitialCssState(node));

// The initialCssState flag is set on a newly-created node.
var node2 = new Rectangle();
assertTrue(NodeHelper.isInitialCssState(node2));

// The flag is also set when the new node is added to the scene.
((Group)scene.getRoot()).getChildren().add(node2);
assertTrue(NodeHelper.isInitialCssState(node));
}
}

0 comments on commit b993d4e

Please sign in to comment.