From ef2df2a6b43caa2b29bf5ed69fe928398219ba2e Mon Sep 17 00:00:00 2001 From: John Hendrikx Date: Fri, 31 Jul 2026 21:04:55 +0200 Subject: [PATCH 1/5] Create new interfaces Layoutable and Measurable and use by Node --- .../src/main/java/javafx/scene/Node.java | 183 +++++--------- .../java/javafx/scene/layout/Layoutable.java | 157 ++++++++++++ .../java/javafx/scene/layout/Measurable.java | 225 ++++++++++++++++++ 3 files changed, 441 insertions(+), 124 deletions(-) create mode 100644 modules/javafx.graphics/src/main/java/javafx/scene/layout/Layoutable.java create mode 100644 modules/javafx.graphics/src/main/java/javafx/scene/layout/Measurable.java diff --git a/modules/javafx.graphics/src/main/java/javafx/scene/Node.java b/modules/javafx.graphics/src/main/java/javafx/scene/Node.java index 76b8126fd59..a1300b54abe 100644 --- a/modules/javafx.graphics/src/main/java/javafx/scene/Node.java +++ b/modules/javafx.graphics/src/main/java/javafx/scene/Node.java @@ -100,6 +100,7 @@ import javafx.scene.input.TouchEvent; import javafx.scene.input.TransferMode; import javafx.scene.input.ZoomEvent; +import javafx.scene.layout.Layoutable; import javafx.scene.shape.Shape; import javafx.scene.text.Font; import javafx.scene.transform.Rotate; @@ -412,7 +413,7 @@ */ @IDProperty("id") public abstract sealed class Node - implements EventTarget, Styleable + implements EventTarget, Styleable, Layoutable permits AbstractNode, Camera, LightBase, Parent, SubScene, Canvas, ImageView, Shape, Shape3D { /* @@ -1046,7 +1047,7 @@ private ReadOnlyObjectWrapper parentPropertyImpl() { protected void invalidated() { if (oldParent != null) { if (nodeTransformation != null && nodeTransformation.listenerReasons > 0) { - ((Node) oldParent).localToSceneTransformProperty().removeListener( + oldParent.localToSceneTransformProperty().removeListener( nodeTransformation.getLocalToSceneInvalidationListener()); } } @@ -1061,7 +1062,7 @@ protected void invalidated() { if (newParent != null) { if (nodeTransformation != null && nodeTransformation.listenerReasons > 0) { - ((Node) newParent).localToSceneTransformProperty().addListener( + newParent.localToSceneTransformProperty().addListener( nodeTransformation.getLocalToSceneInvalidationListener()); } // @@ -2958,8 +2959,10 @@ public String getName() { } /** - * Sets the node's layoutX and layoutY translation properties in order to - * relocate this node to the x,y location in the parent. + * {@inheritDoc} + *

+ * This is done by setting the node's layoutX and layoutY translation + * properties, relocating it to the x,y location in the parent. *

* This method does not alter translateX or translateY, which if also set * will be added to layoutX and layoutY, adjusting the final location by @@ -2968,6 +2971,7 @@ public String getName() { * @param x the target x coordinate location * @param y the target y coordinate location */ + @Override public void relocate(double x, double y) { setLayoutX(x - getLayoutBounds().getMinX()); setLayoutY(y - getLayoutBounds().getMinY()); @@ -2979,20 +2983,17 @@ public void relocate(double x, double y) { } /** - * Indicates whether this node is a type which can be resized by its parent. - * If this method returns true, then the parent will resize the node (ideally - * within its size range) by calling node.resize(width,height) during the - * layout pass. All Regions, Controls, and WebView are resizable classes - * which depend on their parents resizing them during layout once all sizing - * and CSS styling information has been applied. + * {@inheritDoc} + *

+ * If this method returns true, then the parent will resize the node + * (ideally within its size range) by calling node.resize(width,height) + * during the layout pass. All Regions, Controls, and WebView are + * resizable classes which depend on their parents resizing them during + * layout once all sizing and CSS styling information has been applied. *

* If this method returns false, then the parent cannot resize it during - * layout (resize() is a no-op) and it should return its layoutBounds for - * minimum, preferred, and maximum sizes. Group, Text, and all Shapes are not - * resizable and hence depend on the application to establish their sizing - * by setting appropriate properties (e.g. width/height for Rectangle, - * text on Text, and so on). Non-resizable nodes may still be relocated - * during layout. + * layout (resize() is a no-op). Group, Text, and all Shapes are not + * resizable classes. * * @see #getContentBias() * @see #minWidth(double) @@ -3006,15 +3007,13 @@ public void relocate(double x, double y) { * * @return whether or not this node type can be resized by its parent during layout */ + @Override public boolean isResizable() { return false; } /** - * Returns the orientation of a node's resizing bias for layout purposes. - * If the node type has no bias, returns null. If the node is resizable and - * it's height depends on its width, returns HORIZONTAL, else if its width - * depends on its height, returns VERTICAL. + * {@inheritDoc} *

* Resizable subclasses should override this method to return an * appropriate value. @@ -3029,28 +3028,18 @@ public boolean isResizable() { * * @return orientation of width/height dependency or null if there is none */ + @Override public Orientation getContentBias() { return null; } /** - * Returns the node's minimum width for use in layout calculations. + * {@inheritDoc} + *

* If the node is resizable, its parent should not resize its width any * smaller than this value. If the node is not resizable, returns its - * layoutBounds width. - *

- * Layout code which calls this method should first check the content-bias - * of the node. If the node has a vertical content-bias, then callers - * should pass in a height value that the minimum width should be based on. - * If the node has either a horizontal or null content-bias, then the caller - * should pass in -1. - *

- * Node subclasses with a vertical content-bias should honor the height - * parameter whether -1 or a positive value. All other subclasses may ignore - * the height parameter (which will likely be -1). - *

- * If Node's {@link #maxWidth(double)} is lower than this number, - * {@code minWidth} takes precedence. This means the Node should never be resized below {@code minWidth}. + * layoutBounds width. This means the Node should never be resized below + * {@code minWidth}. * * @see #isResizable() * @see #getContentBias() @@ -3059,28 +3048,18 @@ public Orientation getContentBias() { * @return the minimum width that the node should be resized to during layout. * The result will never be NaN, nor will it ever be negative. */ + @Override public double minWidth(double height) { return prefWidth(height); } /** - * Returns the node's minimum height for use in layout calculations. + * {@inheritDoc} + *

* If the node is resizable, its parent should not resize its height any * smaller than this value. If the node is not resizable, returns its - * layoutBounds height. - *

- * Layout code which calls this method should first check the content-bias - * of the node. If the node has a horizontal content-bias, then callers - * should pass in a width value that the minimum height should be based on. - * If the node has either a vertical or null content-bias, then the caller - * should pass in -1. - *

- * Node subclasses with a horizontal content-bias should honor the width - * parameter whether -1 or a positive value. All other subclasses may ignore - * the width parameter (which will likely be -1). - *

- * If Node's {@link #maxHeight(double)} is lower than this number, - * {@code minHeight} takes precedence. This means the Node should never be resized below {@code minHeight}. + * layoutBounds height. This means the Node should never be resized below + * {@code minHeight}. * * @see #isResizable() * @see #getContentBias() @@ -3089,26 +3068,18 @@ public double minWidth(double height) { * @return the minimum height that the node should be resized to during layout * The result will never be NaN, nor will it ever be negative. */ + @Override public double minHeight(double width) { return prefHeight(width); } /** - * Returns the node's preferred width for use in layout calculations. + * {@inheritDoc} + *

* If the node is resizable, its parent should treat this value as the * node's ideal width within its range. If the node is not resizable, * just returns its layoutBounds width, which should be treated as the rigid * width of the node. - *

- * Layout code which calls this method should first check the content-bias - * of the node. If the node has a vertical content-bias, then callers - * should pass in a height value that the preferred width should be based on. - * If the node has either a horizontal or null content-bias, then the caller - * should pass in -1. - *

- * Node subclasses with a vertical content-bias should honor the height - * parameter whether -1 or a positive value. All other subclasses may ignore - * the height parameter (which will likely be -1). * * @see #isResizable() * @see #getContentBias() @@ -3118,27 +3089,19 @@ public double minHeight(double width) { * @return the preferred width that the node should be resized to during layout * The result will never be NaN, nor will it ever be negative. */ + @Override public double prefWidth(double height) { final double result = getLayoutBounds().getWidth(); return Double.isNaN(result) || result < 0 ? 0 : result; } /** - * Returns the node's preferred height for use in layout calculations. + * {@inheritDoc} + *

* If the node is resizable, its parent should treat this value as the * node's ideal height within its range. If the node is not resizable, * just returns its layoutBounds height, which should be treated as the rigid * height of the node. - *

- * Layout code which calls this method should first check the content-bias - * of the node. If the node has a horizontal content-bias, then callers - * should pass in a width value that the preferred height should be based on. - * If the node has either a vertical or null content-bias, then the caller - * should pass in -1. - *

- * Node subclasses with a horizontal content-bias should honor the height - * parameter whether -1 or a positive value. All other subclasses may ignore - * the height parameter (which will likely be -1). * * @see #getContentBias() * @see #autosize() @@ -3147,31 +3110,22 @@ public double prefWidth(double height) { * @return the preferred height that the node should be resized to during layout * The result will never be NaN, nor will it ever be negative. */ + @Override public double prefHeight(double width) { final double result = getLayoutBounds().getHeight(); return Double.isNaN(result) || result < 0 ? 0 : result; } /** - * Returns the node's maximum width for use in layout calculations. + * {@inheritDoc} + *

* If the node is resizable, its parent should not resize its width any * larger than this value. A value of Double.MAX_VALUE indicates the * parent may expand the node's width beyond its preferred without limits. *

* If the node is not resizable, returns its layoutBounds width. *

- * Layout code which calls this method should first check the content-bias - * of the node. If the node has a vertical content-bias, then callers - * should pass in a height value that the maximum width should be based on. - * If the node has either a horizontal or null content-bias, then the caller - * should pass in -1. - *

- * Node subclasses with a vertical content-bias should honor the height - * parameter whether -1 or a positive value. All other subclasses may ignore - * the height parameter (which will likely be -1). - *

- * If Node's {@link #minWidth(double)} is greater, it should take precedence - * over the {@code maxWidth}. This means the Node should never be resized below {@code minWidth}. + * This means the Node should never be resized below {@code minWidth}. * * @see #isResizable() * @see #getContentBias() @@ -3180,30 +3134,21 @@ public double prefHeight(double width) { * @return the maximum width that the node should be resized to during layout * The result will never be NaN, nor will it ever be negative. */ + @Override public double maxWidth(double height) { return prefWidth(height); } /** - * Returns the node's maximum height for use in layout calculations. + * {@inheritDoc} + *

* If the node is resizable, its parent should not resize its height any * larger than this value. A value of Double.MAX_VALUE indicates the * parent may expand the node's height beyond its preferred without limits. *

* If the node is not resizable, returns its layoutBounds height. *

- * Layout code which calls this method should first check the content-bias - * of the node. If the node has a horizontal content-bias, then callers - * should pass in a width value that the maximum height should be based on. - * If the node has either a vertical or null content-bias, then the caller - * should pass in -1. - *

- * Node subclasses with a horizontal content-bias should honor the width - * parameter whether -1 or a positive value. All other subclasses may ignore - * the width parameter (which will likely be -1). - *

- * If Node's {@link #minHeight(double)} is greater, it should take precedence - * over the {@code maxHeight}. This means the Node should never be resized below {@code minHeight}. + * This means the Node should never be resized below {@code minHeight}. * * @see #isResizable() * @see #getContentBias() @@ -3212,22 +3157,21 @@ public double maxWidth(double height) { * @return the maximum height that the node should be resized to during layout * The result will never be NaN, nor will it ever be negative. */ + @Override public double maxHeight(double width) { return prefHeight(width); } /** - * If the node is resizable, will set its layout bounds to the specified - * width and height. If the node is not resizable, this method is a no-op. + * {@inheritDoc} *

* This method should generally only be called by parent nodes from their * layoutChildren() methods. All Parent classes will automatically resize * resizable children, so resizing done directly by the application will be * overridden by the node's parent, unless the child is unmanaged. *

- * Parents are responsible for ensuring the width and height values fall - * within the resizable node's preferred range. The autosize() method may - * be used if the parent just needs to resize the node to its preferred size. + * The autosize() method may be used if the parent just needs to resize the + * node to its preferred size. * * @see #isResizable() * @see #getContentBias() @@ -3243,6 +3187,7 @@ public double maxHeight(double width) { * @param width the target layout bounds width * @param height the target layout bounds height */ + @Override public void resize(double width, double height) { } @@ -3286,22 +3231,18 @@ public final void autosize() { } /** - * If the node is resizable, will set its layout bounds to the specified - * width and height. If the node is not resizable, the resize step is skipped. + * {@inheritDoc} *

- * Once the node has been resized (if resizable) then sets the node's layoutX - * and layoutY translation properties in order to relocate it to x,y in the - * parent's coordinate space. + * This relocation is performed by setting the node's layoutX and layoutY + * translation properties, in the parent's coordinate space. *

* This method should generally only be called by parent nodes from their * layoutChildren() methods. All Parent classes will automatically resize * resizable children, so resizing done directly by the application will be * overridden by the node's parent, unless the child is unmanaged. *

- * Parents are responsible for ensuring the width and height values fall - * within the resizable node's preferred range. The autosize() and relocate() - * methods may be used if the parent just needs to resize the node to its - * preferred size and reposition it. + * The autosize() and relocate() methods may be used if the parent just + * needs to resize the node to its preferred size and reposition it. * * @see #isResizable() * @see #getContentBias() @@ -3319,26 +3260,20 @@ public final void autosize() { * @param height the target layout bounds height * */ + @Override public void resizeRelocate(double x, double y, double width, double height) { resize(width, height); relocate(x,y); } /** - * This is a special value that might be returned by {@link #getBaselineOffset()}. - * This means that the Parent (layout Pane) of this Node should use the height of this Node as a baseline. - */ - public static final double BASELINE_OFFSET_SAME_AS_HEIGHT = Double.NEGATIVE_INFINITY; - - /** - * The 'alphabetic' (or 'roman') baseline offset from the node's layoutBounds.minY location - * that should be used when this node is being vertically aligned by baseline with - * other nodes. By default this returns {@link #BASELINE_OFFSET_SAME_AS_HEIGHT} for resizable Nodes - * and layoutBounds height for non-resizable. Subclasses - * which contain text should override this method to return their actual text baseline offset. + * {@inheritDoc} + *

+ * For a Node, this offset is measured from {@code layoutBounds.minY}. * * @return offset of text baseline from layoutBounds.minY for non-resizable Nodes or {@link #BASELINE_OFFSET_SAME_AS_HEIGHT} otherwise */ + @Override public double getBaselineOffset() { if (isResizable()) { return BASELINE_OFFSET_SAME_AS_HEIGHT; diff --git a/modules/javafx.graphics/src/main/java/javafx/scene/layout/Layoutable.java b/modules/javafx.graphics/src/main/java/javafx/scene/layout/Layoutable.java new file mode 100644 index 00000000000..db48861ad3a --- /dev/null +++ b/modules/javafx.graphics/src/main/java/javafx/scene/layout/Layoutable.java @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package javafx.scene.layout; + +import javafx.geometry.Bounds; + +/** + * Interface for elements that can be resized and relocated. + *

+ * Extends {@link Measurable} with the ability to report whether an element + * supports being resized, to expose the bounds that should be used for + * layout calculations, and to resize and relocate the element to specific + * dimensions and coordinates. + * + *

Resizability

+ * Whether an element supports being resized is reported by + * {@link #isResizable()}. Calling {@link #resize(double, double)} on a + * resizable element sets its layout bounds to the given size, ideally + * within the range established by {@link Measurable}'s sizing methods. + * Calling it on a non-resizable element has no effect; such an element + * establishes its own size some other way, but can still be relocated. + * + *

Layout Bounds

+ * {@link #getLayoutBounds()} returns the rectangular bounds that should be + * used for layout calculations, which may differ from an element's visual + * bounds. + * + *

Resizing and Relocation

+ * {@link #resize(double, double)}, {@link #relocate(double, double)}, and + * {@link #resizeRelocate(double, double, double, double)} resize and/or + * reposition an element to specific dimensions and coordinates. + * + * @see Measurable + */ +public interface Layoutable extends Measurable { + + /** + * Indicates whether this element supports being resized. + * If this method returns true, calling {@link #resize(double, double)} + * sets this element's layout bounds to the given width and height + * (ideally within its size range). + *

+ * If this method returns false, {@link #resize(double, double)} is a + * no-op, and this element's layoutBounds should be used as its minimum, + * preferred, and maximum sizes. Non-resizable elements depend on the + * application to establish their sizing by setting appropriate properties + * (like setting width/height for a rectangle, or setting a text). A + * non-resizable element may still be relocated during layout. + * + * @see #resize(double, double) + * @see #getLayoutBounds() + * @return whether or not this element supports being resized + */ + boolean isResizable(); + + /** + * The rectangular bounds that should be used for layout calculations for + * this element. {@code layoutBounds} may differ from the visual bounds + * of the element and is computed differently depending on the element type. + *

+ * If the element type is resizable then the layoutBounds will always be {@code 0,0 width x height}. + * If the element type is not resizable then the {@code layoutBounds} + * are computed based on the element's geometric properties. + *

+ * Because the computation of layoutBounds is often tied to an element's + * geometric variables, it is an error to bind any such variables to an + * expression that depends upon {@code layoutBounds}. For example, the + * x or y variables of a shape should never be bound to {@code layoutBounds} + * for the purpose of positioning the element. + *

+ * Note that for 3D shapes, the layout bounds is actually a rectangular box + * with X, Y, and Z values, although only X and Y are used in layout calculations. + *

+ * The {@code layoutBounds} will never be null. + * + * @return the current layout bounds, never {@code null} + */ + Bounds getLayoutBounds(); + + /** + * If this element is resizable, sets its layout bounds to the specified + * width and height. If this element is not resizable, the resize step + * is skipped. + *

+ * Once the element has been resized (if resizable) then relocates it to + * x,y. + *

+ * Callers are responsible for ensuring the width and height values fall + * within this element's preferred range. + * + * @see #isResizable() + * @see #resize(double, double) + * @see #relocate(double, double) + * @param x the target x coordinate location + * @param y the target y coordinate location + * @param width the target layout bounds width + * @param height the target layout bounds height + */ + default void resizeRelocate(double x, double y, double width, double height) { + resize(width, height); + relocate(x,y); + } + + /** + * If this element is resizable, sets its layout bounds to the specified + * width and height. If this element is not resizable, this method is a + * no-op. + *

+ * Callers are responsible for ensuring the width and height values fall + * within this element's preferred range. + * + * @see #isResizable() + * @see #minWidth(double) + * @see #minHeight(double) + * @see #maxWidth(double) + * @see #maxHeight(double) + * @see #getLayoutBounds() + * @param width the target layout bounds width + * @param height the target layout bounds height + */ + void resize(double width, double height); + + /** + * Relocates this element to the given x,y location. + *

+ * This method only affects this element's position; its size (as + * reported by {@link #getLayoutBounds()}) is unaffected. + * + * @see #resizeRelocate(double, double, double, double) + * @param x the target x coordinate location + * @param y the target y coordinate location + */ + void relocate(double x, double y); +} diff --git a/modules/javafx.graphics/src/main/java/javafx/scene/layout/Measurable.java b/modules/javafx.graphics/src/main/java/javafx/scene/layout/Measurable.java new file mode 100644 index 00000000000..cc81119c935 --- /dev/null +++ b/modules/javafx.graphics/src/main/java/javafx/scene/layout/Measurable.java @@ -0,0 +1,225 @@ +/* + * Copyright (c) 2010, 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package javafx.scene.layout; + +import javafx.geometry.Orientation; + +/** + * Interface providing measurement metrics for layout calculations. + *

+ * Implemented by elements that provide intrinsic or calculated size constraints, + * baseline alignment, and content bias. + * + *

Sizing Precedence

+ * Layout algorithms should compute dimensions using the bounds returned by this interface. + * When bounds conflict, the following precedence rules apply: + * + * The preferred size should therefore be clamped within the range {@code [min, max]}. + * + *

Content Bias

+ * An element's measurements may depend on its target size along the opposite axis: + * + * + * Layout engines must always check {@link #getContentBias()} before invoking measurement methods. + * + * @see Layoutable + */ +public interface Measurable { + + /** + * This is a special value that might be returned by {@link #getBaselineOffset()}. + * Indicates that the height of this element should be used as its baseline. + */ + static final double BASELINE_OFFSET_SAME_AS_HEIGHT = Double.NEGATIVE_INFINITY; + + /** + * Returns the minimum width for use in layout calculations. + *

+ * Layout code which calls this method should first check the content-bias. + * For a vertical content-bias callers should pass in a height value that + * the minimum width should be based on. For a horizontal or null content-bias + * the caller should pass in -1. + *

+ * Implementations that have a vertical content-bias should honor the height + * parameter whether -1 or a positive value. All other implementations may ignore + * the height parameter (which will likely be -1). + *

+ * If {@link #maxWidth(double)} is lower than this number, {@code minWidth} takes + * precedence. + * + * @see #getContentBias() + * @param height the height that should be used if minimum width depends on it + * @return the minimum width required by this element; the result will never be NaN, + * nor will it ever be negative. + */ + double minWidth(double height); + + /** + * Returns the minimum height for use in layout calculations. + *

+ * Layout code which calls this method should first check the content-bias. + * For a horizontal content-bias callers should pass in a width value that + * the minimum height should be based on. For a vertical or null content-bias + * the caller should pass in -1. + *

+ * Implementations that have a horizontal content-bias should honor the width + * parameter whether -1 or a positive value. All other implementations may ignore + * the width parameter (which will likely be -1). + *

+ * If {@link #maxHeight(double)} is lower than this number, {@code minHeight} takes + * precedence. + * + * @see #getContentBias() + * @param width the width that should be used if minimum height depends on it + * @return the minimum height required by this element; the result will never be NaN, + * nor will it ever be negative. + */ + double minHeight(double width); + + /** + * Returns the preferred width for use in layout calculations. + *

+ * Layout code which calls this method should first check the content-bias. + * For a vertical content-bias callers should pass in a height value that + * the preferred width should be based on. For a horizontal or null content-bias + * the caller should pass in -1. + *

+ * Implementations that have a vertical content-bias should honor the height + * parameter whether -1 or a positive value. All other implementations may ignore + * the height parameter (which will likely be -1). + * + * @see #getContentBias() + * @param height the height that should be used if preferred width depends on it + * @return the preferred width for this element; the result will never be NaN, + * nor will it ever be negative. + */ + double prefWidth(double height); + + /** + * Returns the preferred height for use in layout calculations. + *

+ * Layout code which calls this method should first check the content-bias. + * For a horizontal content-bias callers should pass in a width value that + * the preferred height should be based on. For a vertical or null content-bias + * the caller should pass in -1. + *

+ * Implementations that have a horizontal content-bias should honor the height + * parameter whether -1 or a positive value. All other implementations may ignore + * the height parameter (which will likely be -1). + * + * @see #getContentBias() + * @param width the width that should be used if preferred height depends on it + * @return the preferred height for this element; the result will never be NaN, + * nor will it ever be negative. + */ + double prefHeight(double width); + + /** + * Returns the maximum width for use in layout calculations. + * A value of {@code Double.MAX_VALUE} indicates that the width may be + * expanded beyond its preferred width without limits. + *

+ * Layout code which calls this method should first check the content-bias. + * For a vertical content-bias callers should pass in a height value that + * the maximum width should be based on. For a horizontal or null content-bias + * the caller should pass in -1. + *

+ * Implementations that have a vertical content-bias should honor the height + * parameter whether -1 or a positive value. All other implementations may ignore + * the height parameter (which will likely be -1). + *

+ * If {@link #minWidth(double)} is greater, it should take precedence + * over the {@code maxWidth}. + * + * @see #getContentBias() + * @param height the height that should be used if maximum width depends on it + * @return the maximum width for this element; the result will never be NaN, + * nor will it ever be negative. + */ + double maxWidth(double height); + + /** + * Returns the maximum height for use in layout calculations. + * A value of {@code Double.MAX_VALUE} indicates that the height may be + * expanded beyond its preferred height without limits. + *

+ * Layout code which calls this method should first check the content-bias. + * For a horizontal content-bias callers should pass in a width value that + * the maximum height should be based on. For a vertical or null content-bias + * the caller should pass in -1. + *

+ * Implementations that have a horizontal content-bias should honor the width + * parameter whether -1 or a positive value. All other implementations may ignore + * the width parameter (which will likely be -1). + *

+ * If {@link #minHeight(double)} is greater, it should take precedence + * over the {@code maxHeight}. + * + * @see #getContentBias() + * @param width the width that should be used if maximum height depends on it + * @return the maximum height for this element; the result will never be NaN, + * nor will it ever be negative. + */ + double maxHeight(double width); + + /** + * The 'alphabetic' (or 'roman') baseline offset from the element's top boundary + * that should be used when this element is being vertically aligned by baseline with + * other elements. By default this returns {@link #BASELINE_OFFSET_SAME_AS_HEIGHT} for resizable elements + * and height for non-resizable elements. Implementations which contain text + * should override this method to return their actual text baseline offset. + * + * @return offset of text baseline from top boundary for non-resizable elements or {@link #BASELINE_OFFSET_SAME_AS_HEIGHT} otherwise + */ + double getBaselineOffset(); + + /** + * Returns the orientation of the resizing bias for layout purposes. + * If there is no bias, returns null. If its height depends on its width, + * returns {@link Orientation#HORIZONTAL}, else if its width depends on its height, + * returns {@link Orientation#VERTICAL}. + * + * @see #minWidth(double) + * @see #minHeight(double) + * @see #prefWidth(double) + * @see #prefHeight(double) + * @see #maxWidth(double) + * @see #maxHeight(double) + * @return orientation of width/height dependency or null if there is none + */ + Orientation getContentBias(); +} \ No newline at end of file From 772ebcddaeebeefb7e57e1da9cef96bfd947a733 Mon Sep 17 00:00:00 2001 From: John Hendrikx Date: Sun, 2 Aug 2026 00:52:02 +0200 Subject: [PATCH 2/5] Add StackPaneLayout, move Region helpers to LayoutSupport --- .../com/sun/javafx/scene/SceneHelper.java | 14 + .../com/sun/javafx/scene/layout/Snapper.java | 175 +++++ .../src/main/java/javafx/scene/Scene.java | 66 ++ .../java/javafx/scene/layout/FlowPane.java | 2 +- .../java/javafx/scene/layout/GridPane.java | 2 +- .../main/java/javafx/scene/layout/HBox.java | 2 +- .../javafx/scene/layout/LayoutSupport.java | 672 ++++++++++++++++++ .../main/java/javafx/scene/layout/Region.java | 373 ++-------- .../scene/layout/RenderScaleContext.java | 43 ++ .../java/javafx/scene/layout/StackPane.java | 111 ++- .../javafx/scene/layout/StackPaneLayout.java | 261 +++++++ .../java/javafx/scene/layout/TilePane.java | 2 +- .../main/java/javafx/scene/layout/VBox.java | 2 +- 13 files changed, 1341 insertions(+), 384 deletions(-) create mode 100644 modules/javafx.graphics/src/main/java/com/sun/javafx/scene/layout/Snapper.java create mode 100644 modules/javafx.graphics/src/main/java/javafx/scene/layout/LayoutSupport.java create mode 100644 modules/javafx.graphics/src/main/java/javafx/scene/layout/RenderScaleContext.java create mode 100644 modules/javafx.graphics/src/main/java/javafx/scene/layout/StackPaneLayout.java diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/scene/SceneHelper.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/scene/SceneHelper.java index b19e300a6f3..8dd5a6c804a 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/scene/SceneHelper.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/scene/SceneHelper.java @@ -26,6 +26,7 @@ package com.sun.javafx.scene; import com.sun.glass.ui.Accessible; +import com.sun.javafx.scene.layout.Snapper; import com.sun.javafx.tk.TKScene; import com.sun.javafx.util.Utils; import javafx.scene.Camera; @@ -34,6 +35,7 @@ import javafx.scene.Scene; import javafx.scene.input.KeyEvent; import javafx.scene.input.MouseEvent; +import javafx.scene.layout.RenderScaleContext; import javafx.stage.Window; /** @@ -114,6 +116,14 @@ public static SceneContext getSceneContext(Scene scene) { return sceneAccessor.getSceneContext(scene); } + public static Snapper getSnapper(Scene scene) { + return sceneAccessor.getSnapper(scene); + } + + public static RenderScaleContext getRenderScaleContext(Scene scene) { + return sceneAccessor.getRenderScaleContext(scene); + } + public static void setSceneAccessor(final SceneAccessor newAccessor) { if (sceneAccessor != null) { throw new IllegalStateException(); @@ -161,6 +171,10 @@ public interface SceneAccessor { Accessible getAccessible(Scene scene); SceneContext getSceneContext(Scene scene); + + Snapper getSnapper(Scene scene); + + RenderScaleContext getRenderScaleContext(Scene scene); } } diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/scene/layout/Snapper.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/scene/layout/Snapper.java new file mode 100644 index 00000000000..961b30aa3b7 --- /dev/null +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/scene/layout/Snapper.java @@ -0,0 +1,175 @@ +/* + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.sun.javafx.scene.layout; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import javafx.scene.layout.RenderScaleContext; + +/** + * Turns a {@link RenderScaleContext}'s raw scale factors into the actual snapping + * operations used by layout math. + */ +public interface Snapper { + + /** + * A default snapper for 1.0 scaling. + */ + static final Snapper DEFAULT = new Snapper() { + + @Override + public double snapPositionX(double value) { + return Math.round(value); + } + + @Override + public double snapPositionY(double value) { + return Math.round(value); + } + + @Override + public double snapSpaceX(double value) { + return Math.round(value); + } + + @Override + public double snapSpaceY(double value) { + return Math.round(value); + } + + @Override + public double snapSizeX(double value) { + return Math.ceil(value - Math.ulp(value)); + } + + @Override + public double snapSizeY(double value) { + return Math.ceil(value - Math.ulp(value)); + } + }; + + /** + * A default snapper that does no snapping at all. + */ + static final Snapper NO_SNAPPING = new Snapper() { + @Override + public double snapPositionX(double value) { + return value; + } + + @Override + public double snapPositionY(double value) { + return value; + } + + @Override + public double snapSpaceX(double value) { + return value; + } + + @Override + public double snapSpaceY(double value) { + return value; + } + + @Override + public double snapSizeX(double value) { + return value; + } + + @Override + public double snapSizeY(double value) { + return value; + } + }; + + class Cache { + private static final Map INSTANCES = new ConcurrentHashMap<>(); + + { + INSTANCES.put(RenderScaleContext.DEFAULT, DEFAULT); + } + } + + static Snapper createSnapper(RenderScaleContext context) { + return Cache.INSTANCES.computeIfAbsent(context, _ -> new Snapper() { + final double ssx = context.snapScaleX(); + final double ssy = context.snapScaleY(); + final double rssx = 1.0 / ssx; + final double rssy = 1.0 / ssy; + + @Override + public double snapPositionX(double value) { + return Math.round(value * ssx) * rssx; + } + + @Override + public double snapPositionY(double value) { + return Math.round(value * ssy) * rssy; + } + + @Override + public double snapSpaceX(double value) { + return Math.round(value * ssx) * rssx; + } + + @Override + public double snapSpaceY(double value) { + return Math.round(value * ssy) * rssy; + } + + @Override + public double snapSizeX(double value) { + double d = value * ssx; + + if (Double.isInfinite(d)) { // Avoids returning NaN for high magnitude inputs + return value; + } + + return Math.ceil(d - Math.ulp(d)) * rssx; + } + + @Override + public double snapSizeY(double value) { + double d = value * ssy; + + if (Double.isInfinite(d)) { // Avoids returning NaN for high magnitude inputs + return value; + } + + return Math.ceil(d - Math.ulp(d)) * rssy; + } + }); + } + + double snapPositionX(double value); + double snapPositionY(double value); + double snapSpaceX(double value); + double snapSpaceY(double value); + double snapSizeX(double value); + double snapSizeY(double value); +} diff --git a/modules/javafx.graphics/src/main/java/javafx/scene/Scene.java b/modules/javafx.graphics/src/main/java/javafx/scene/Scene.java index d9f39c9b5fe..01ee9e01a00 100644 --- a/modules/javafx.graphics/src/main/java/javafx/scene/Scene.java +++ b/modules/javafx.graphics/src/main/java/javafx/scene/Scene.java @@ -50,6 +50,7 @@ import com.sun.javafx.scene.input.ExtendedInputMethodRequests; import com.sun.javafx.scene.input.InputEventUtils; import com.sun.javafx.scene.input.PickResultChooser; +import com.sun.javafx.scene.layout.Snapper; import com.sun.javafx.scene.traversal.Direction; import com.sun.javafx.scene.traversal.SceneTraversalEngine; import com.sun.javafx.scene.traversal.TopMostTraversalEngine; @@ -83,6 +84,7 @@ import javafx.scene.layout.HeaderBar; import javafx.scene.layout.HeaderButtonType; import javafx.scene.layout.HeaderDragType; +import javafx.scene.layout.RenderScaleContext; import javafx.scene.paint.Color; import javafx.scene.paint.Paint; import javafx.stage.PopupWindow; @@ -510,6 +512,16 @@ public Accessible getAccessible(Scene scene) { public SceneContext getSceneContext(Scene scene) { return scene.context; } + + @Override + public Snapper getSnapper(Scene scene) { + return scene.snapper; + } + + @Override + public RenderScaleContext getRenderScaleContext(Scene scene) { + return scene.renderScaleContext; + } }); } @@ -2175,14 +2187,68 @@ null, new Point3D( private void windowForSceneChanged(Window oldWindow, Window newWindow) { if (oldWindow != null) { oldWindow.focusedProperty().removeListener(sceneWindowFocusedListener); + oldWindow.renderScaleXProperty().removeListener(sceneWindowRenderScaleListener); + oldWindow.renderScaleYProperty().removeListener(sceneWindowRenderScaleListener); } if (newWindow != null) { newWindow.focusedProperty().addListener(sceneWindowFocusedListener); + newWindow.renderScaleXProperty().addListener(sceneWindowRenderScaleListener); + newWindow.renderScaleYProperty().addListener(sceneWindowRenderScaleListener); setWindowFocused(newWindow.isFocused()); + requestLayoutRecursive(getRoot()); } else { setWindowFocused(false); } + + updateRenderScaleCache(); + } + + /** + * Trigger a requestLayout() call on the given Node and all its + * descendants in the Scene, in order to invalidate any caches that + * these Nodes may be holding that were calculated using an outdated + * render scale (the main culprit here is Parent's caching of + * min/pref/max sizes but there are many Nodes that follow a similar + * "invalidate on requestLayout()" pattern). + * + * @param parent a start node, cannot be {@code null} + */ + private static void requestLayoutRecursive(Parent parent) { + parent.requestLayout(); + + for (Node child : parent.getChildrenUnmodifiable()) { + if (child instanceof Parent p) { + requestLayoutRecursive(p); + } + } + } + + private final InvalidationListener sceneWindowRenderScaleListener = + _ -> { + updateRenderScaleCache(); + requestLayoutRecursive(getRoot()); + }; + + /** + * Cached {@link Snapper} for this scene's window render scale, refreshed + * whenever the window changes or its render scale changes. + */ + private Snapper snapper = Snapper.DEFAULT; + + /** + * Cached {@link RenderScaleContext} for this scene's window render scale, refreshed + * whenever the window changes or its render scale changes. + */ + private RenderScaleContext renderScaleContext = RenderScaleContext.DEFAULT; + + private void updateRenderScaleCache() { + Window window = getWindow(); + + this.renderScaleContext = window == null + ? RenderScaleContext.DEFAULT + : new RenderScaleContext(window.getRenderScaleX(), window.getRenderScaleY()); + this.snapper = Snapper.createSnapper(renderScaleContext); } private final InvalidationListener sceneWindowFocusedListener = diff --git a/modules/javafx.graphics/src/main/java/javafx/scene/layout/FlowPane.java b/modules/javafx.graphics/src/main/java/javafx/scene/layout/FlowPane.java index 42b69cdb2e7..e7feb15ddc2 100644 --- a/modules/javafx.graphics/src/main/java/javafx/scene/layout/FlowPane.java +++ b/modules/javafx.graphics/src/main/java/javafx/scene/layout/FlowPane.java @@ -176,7 +176,7 @@ public static Insets getMargin(Node child) { return (Insets)getConstraint(child, MARGIN_CONSTRAINT); } - private static final Callback marginAccessor = n -> getMargin(n); + private static final Callback marginAccessor = n -> getMargin((Node)n); /** * Removes all flowpane constraints from the child node. diff --git a/modules/javafx.graphics/src/main/java/javafx/scene/layout/GridPane.java b/modules/javafx.graphics/src/main/java/javafx/scene/layout/GridPane.java index f649a089104..80816ab4d0e 100644 --- a/modules/javafx.graphics/src/main/java/javafx/scene/layout/GridPane.java +++ b/modules/javafx.graphics/src/main/java/javafx/scene/layout/GridPane.java @@ -414,7 +414,7 @@ private double getBaselineComplementForChild(Node child) { return -1; } - private static final Callback marginAccessor = n -> getMargin(n); + private static final Callback marginAccessor = n -> getMargin((Node)n); /** * Sets the horizontal alignment for the child when contained by a gridpane. diff --git a/modules/javafx.graphics/src/main/java/javafx/scene/layout/HBox.java b/modules/javafx.graphics/src/main/java/javafx/scene/layout/HBox.java index 0e622734dd4..ef35ab40bb2 100644 --- a/modules/javafx.graphics/src/main/java/javafx/scene/layout/HBox.java +++ b/modules/javafx.graphics/src/main/java/javafx/scene/layout/HBox.java @@ -210,7 +210,7 @@ public static Insets getMargin(Node child) { return (Insets)getConstraint(child, MARGIN_CONSTRAINT); } - private static final Callback marginAccessor = n -> getMargin(n); + private static final Callback marginAccessor = n -> getMargin((Node)n); /** * Removes all hbox constraints from the child node. diff --git a/modules/javafx.graphics/src/main/java/javafx/scene/layout/LayoutSupport.java b/modules/javafx.graphics/src/main/java/javafx/scene/layout/LayoutSupport.java new file mode 100644 index 00000000000..57a51c1776c --- /dev/null +++ b/modules/javafx.graphics/src/main/java/javafx/scene/layout/LayoutSupport.java @@ -0,0 +1,672 @@ +/* + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package javafx.scene.layout; + +import java.util.List; +import java.util.function.Function; + +import com.sun.javafx.scene.layout.Snapper; + +import javafx.geometry.HPos; +import javafx.geometry.Insets; +import javafx.geometry.Orientation; +import javafx.geometry.VPos; +import javafx.scene.Node; +import javafx.util.Callback; + +/** + * Layout math shared between Region and Layoutable-based layouts. + */ +final class LayoutSupport { + + static double computeChildMinAreaWidth(Snapper snapper, Layoutable child, Insets margin) { + return computeChildMinAreaWidth(snapper, child, -1, margin, -1, false); + } + + static double computeChildMinAreaWidth(Snapper snapper, Layoutable child, double baselineComplement, Insets margin, double availableHeight, boolean fillHeight) { + double left = margin != null ? snapper.snapSpaceX(margin.getLeft()) : 0; + double right = margin != null ? snapper.snapSpaceX(margin.getRight()) : 0; + double alt = -1; + if (availableHeight != -1 && child.isResizable() && child.getContentBias() == Orientation.VERTICAL) { // width depends on height + double top = margin != null ? snapper.snapSpaceY(margin.getTop()) : 0; + double bottom = margin != null ? snapper.snapSpaceY(margin.getBottom()) : 0; + double bo = child.getBaselineOffset(); + final double contentHeight = bo == Measurable.BASELINE_OFFSET_SAME_AS_HEIGHT && baselineComplement != -1 ? + availableHeight - top - bottom - baselineComplement : + availableHeight - top - bottom; + alt = computedBoundedHeight(snapper, child, fillHeight, contentHeight); + } + return left + snapper.snapSizeX(child.minWidth(alt)) + right; + } + + static double computeChildMinAreaHeight(Snapper snapper, Layoutable child, Insets margin) { + return computeChildMinAreaHeight(snapper, child, -1, margin, -1, false); + } + + static double computeChildMinAreaHeight(Snapper snapper, Layoutable child, double minBaselineComplement, Insets margin, double availableWidth, boolean fillWidth) { + double top = margin != null ? snapper.snapSpaceY(margin.getTop()) : 0; + double bottom = margin != null ? snapper.snapSpaceY(margin.getBottom()) : 0; + + double alt = -1; + if (availableWidth != -1 && child.isResizable() && child.getContentBias() == Orientation.HORIZONTAL) { // height depends on width + double contentWidth = computeContentWidth(snapper, margin, availableWidth); + + alt = computedBoundedWidth(snapper, child, fillWidth, contentWidth); + } + + // For explanation, see computeChildPrefAreaHeight + if (minBaselineComplement != -1) { + double baseline = child.getBaselineOffset(); + if (child.isResizable() && baseline == Measurable.BASELINE_OFFSET_SAME_AS_HEIGHT) { + return top + snapper.snapSizeY(child.minHeight(alt)) + bottom + + minBaselineComplement; + } + + return baseline + minBaselineComplement; + } + + return top + snapper.snapSizeY(child.minHeight(alt)) + bottom; + } + + static double computeChildPrefAreaWidth(Snapper snapper, Layoutable child, Insets margin) { + return computeChildPrefAreaWidth(snapper, child, -1, margin, -1, false); + } + + static double computeChildPrefAreaWidth(Snapper snapper, Layoutable child, double baselineComplement, Insets margin, double availableHeight, boolean fillHeight) { + double left = margin != null ? snapper.snapSpaceX(margin.getLeft()) : 0; + double right = margin != null ? snapper.snapSpaceX(margin.getRight()) : 0; + double alt = -1; + if (availableHeight != -1 && child.isResizable() && child.getContentBias() == Orientation.VERTICAL) { + double top = margin != null ? snapper.snapSpaceY(margin.getTop()) : 0; + double bottom = margin != null ? snapper.snapSpaceY(margin.getBottom()) : 0; + double bo = child.getBaselineOffset(); + final double contentHeight = bo == Measurable.BASELINE_OFFSET_SAME_AS_HEIGHT && baselineComplement != -1 ? + availableHeight - top - bottom - baselineComplement : + availableHeight - top - bottom; + alt = computedBoundedHeight(snapper, child, fillHeight, contentHeight); + } + return left + snapper.snapSizeX(boundedSize(child.minWidth(alt), child.prefWidth(alt), child.maxWidth(alt))) + right; + } + + static double computeChildPrefAreaHeight(Snapper snapper, Layoutable child, Insets margin) { + return computeChildPrefAreaHeight(snapper, child, -1, margin, -1, false); + } + + static double computeChildPrefAreaHeight(Snapper snapper, Layoutable child, double prefBaselineComplement, Insets margin, double availableWidth, boolean fillWidth) { + double top = margin != null ? snapper.snapSpaceY(margin.getTop()) : 0; + double bottom = margin != null ? snapper.snapSpaceY(margin.getBottom()) : 0; + + double alt = -1; + if (availableWidth != -1 && child.isResizable() && child.getContentBias() == Orientation.HORIZONTAL) { + double contentWidth = computeContentWidth(snapper, margin, availableWidth); + + alt = computedBoundedWidth(snapper, child, fillWidth, contentWidth); + } + + if (prefBaselineComplement != -1) { + double baseline = child.getBaselineOffset(); + if (child.isResizable() && baseline == Measurable.BASELINE_OFFSET_SAME_AS_HEIGHT) { + // When baseline is same as height, the preferred height of the node will be above the baseline, so we need to add + // the preferred complement to it + return top + snapper.snapSizeY(boundedSize(child.minHeight(alt), child.prefHeight(alt), child.maxHeight(alt))) + bottom + + prefBaselineComplement; + } + + // For all other Nodes, it's just their baseline and the complement. + // Note that the complement already contain the Node's preferred (or fixed) height + return top + baseline + prefBaselineComplement + bottom; + } + + return top + snapper.snapSizeY(boundedSize(child.minHeight(alt), child.prefHeight(alt), child.maxHeight(alt))) + bottom; + } + + static double computeChildMaxAreaWidth(Snapper snapper, Node child, double baselineComplement, Insets margin, double availableHeight, boolean fillHeight) { + double max = child.maxWidth(-1); + if (max == Double.MAX_VALUE) { + return max; + } + double left = margin != null ? snapper.snapSpaceX(margin.getLeft()) : 0; + double right = margin != null ? snapper.snapSpaceX(margin.getRight()) : 0; + double alt = -1; + if (availableHeight != -1 && child.isResizable() && child.getContentBias() == Orientation.VERTICAL) { // width depends on height + double top = margin != null ? snapper.snapSpaceY(margin.getTop()) : 0; + double bottom = (margin != null ? snapper.snapSpaceY(margin.getBottom()) : 0); + double bo = child.getBaselineOffset(); + final double contentHeight = bo == Measurable.BASELINE_OFFSET_SAME_AS_HEIGHT && baselineComplement != -1 ? + availableHeight - top - bottom - baselineComplement : + availableHeight - top - bottom; + + alt = computedBoundedHeight(snapper, child, fillHeight, contentHeight); + max = child.maxWidth(alt); + } + // if min > max, min wins, so still need to call boundedSize() + return left + snapper.snapSizeX(boundedSize(child.minWidth(alt), max, Double.MAX_VALUE)) + right; + } + + static double computeChildMaxAreaHeight(Snapper snapper, Node child, double maxBaselineComplement, Insets margin, double availableWidth, boolean fillWidth) { + double max = child.maxHeight(-1); + if (max == Double.MAX_VALUE) { + return max; + } + double top = margin != null? snapper.snapSpaceY(margin.getTop()) : 0; + double bottom = margin != null? snapper.snapSpaceY(margin.getBottom()) : 0; + double alt = -1; + if (availableWidth != -1 && child.isResizable() && child.getContentBias() == Orientation.HORIZONTAL) { // height depends on width + double contentWidth = computeContentWidth(snapper, margin, availableWidth); + + alt = computedBoundedWidth(snapper, child, fillWidth, contentWidth); + max = child.maxHeight(alt); + } + // For explanation, see computeChildPrefAreaHeight + if (maxBaselineComplement != -1) { + double baseline = child.getBaselineOffset(); + if (child.isResizable() && baseline == Measurable.BASELINE_OFFSET_SAME_AS_HEIGHT) { + return top + snapper.snapSizeY(boundedSize(child.minHeight(alt), max, Double.MAX_VALUE)) + bottom + + maxBaselineComplement; + } + + return top + baseline + maxBaselineComplement + bottom; + } + + // if min > max, min wins, so still need to call boundedSize() + return top + snapper.snapSizeY(boundedSize(child.minHeight(alt), max, Double.MAX_VALUE)) + bottom; + } + + /* + * Definition of used terms: + * + * # available width/heights: + * + * Sizes provided by the container that may be used as a dependent value when + * calculating sizes for biased controls. These may be set to -1 to indicate + * no such information is available. If given, the sizes include the Margin + * of the child. As such the Margin must be removed before passing these + * values as a dependent value to min/pref/max width/height functions. + * + * # content width/heights: + * + * The space allocated to a child, minus its margins. A content size is + * always a real value (not NaN) and never negative. + * + * # bounded width/heights: + * + * The space allocated to a child, minus its margins, adjusted according to + * its constraints (min <= X <= max). A bounded size is always a real value + * (not NaN) and never negative. + */ + + /* + * Given a content width, limits it by the child's constraints. The fill boolean + * controls whether the content width or the child's preferred width is used to compute + * the bounded width. + */ + private static double computedBoundedWidth(Snapper snapper, Layoutable child, boolean fill, double contentWidth) { + double min = child.minWidth(-1); + double max = child.maxWidth(-1); + + if (fill) { + return snapper.snapSizeX(boundedSize(min, contentWidth, max)); + } + + return snapper.snapSizeX(boundedSize(min, child.prefWidth(-1), Math.min(max, contentWidth))); + } + + /* + * Given a content height, limits it by the child's constraints. The fill boolean + * controls whether the content height or the child's preferred height is used to compute + * the bounded height. + */ + private static double computedBoundedHeight(Snapper snapper, Layoutable child, boolean fill, double contentHeight) { + double min = child.minHeight(-1); + double max = child.maxHeight(-1); + + if (fill) { + return snapper.snapSizeY(boundedSize(min, contentHeight, max)); + } + + return snapper.snapSizeY(boundedSize(min, child.prefHeight(-1), Math.min(max, contentHeight))); + } + + /* + * Removes the given Margin (if any) from a width which still includes margins + * to create a content width. + */ + private static double computeContentWidth(Snapper snapper, Insets margin, double width) { + double left = margin != null ? snapper.snapSpaceX(margin.getLeft()) : 0; + double right = margin != null ? snapper.snapSpaceX(margin.getRight()) : 0; + + return width - left - right; + } + + /* Max of children's minimum area widths */ + static double computeMaxMinAreaWidth(Snapper snapper, List children, Callback margins, double height, + boolean fillHeight) { + return getMaxAreaWidth(snapper, children, margins, new double[] { height }, fillHeight, true); + } + + /* Max of children's minimum area heights */ + static double computeMaxMinAreaHeight(Snapper snapper, List children, Callback margins, double width, + boolean fillWidth, VPos valignment) { + return getMaxAreaHeight(snapper, children, margins, new double[] { width }, fillWidth, true, valignment); + } + + /* Max of children's pref area widths */ + static double computeMaxPrefAreaWidth(Snapper snapper, List children, Callback margins, double height, + boolean fillHeight) { + return getMaxAreaWidth(snapper, children, margins, new double[] { height }, fillHeight, false); + } + + /* Max of children's pref area heights */ + static double computeMaxPrefAreaHeight(Snapper snapper, List children, Callback margins, double width, + boolean fillWidth, VPos valignment) { + return getMaxAreaHeight(snapper, children, margins, new double[] { width }, fillWidth, false, valignment); + } + + /* Utility method for computing the max of children's min or pref heights, taking into account baseline alignment. */ + static double getMaxAreaHeight( + Snapper snapper, List children, Callback childMargins, double[] childWidths, + boolean fillWidth, boolean minimum, VPos valignment + ) { + final double singleChildWidth = childWidths == null ? -1 : childWidths.length == 1 ? childWidths[0] : Double.NaN; + if (valignment == VPos.BASELINE) { + double maxAbove = 0; + double maxBelow = 0; + for (int i = 0, maxPos = children.size(); i < maxPos; i++) { + final Layoutable child = children.get(i); + final double childWidth = Double.isNaN(singleChildWidth) ? childWidths[i] : singleChildWidth; + Insets margin = childMargins.call(child); + final double top = margin != null ? snapper.snapSpaceY(margin.getTop()) : 0; + final double bottom = margin != null ? snapper.snapSpaceY(margin.getBottom()) : 0; + final double baseline = child.getBaselineOffset(); + + final double childHeight = minimum ? snapper.snapSizeY(child.minHeight(childWidth)) : snapper.snapSizeY(child.prefHeight(childWidth)); + if (baseline == Measurable.BASELINE_OFFSET_SAME_AS_HEIGHT) { + maxAbove = Math.max(maxAbove, childHeight + top); + } else { + maxAbove = Math.max(maxAbove, baseline + top); + maxBelow = Math.max(maxBelow, + snapper.snapSpaceY(minimum ? snapper.snapSizeY(child.minHeight(childWidth)) : snapper.snapSizeY(child.prefHeight(childWidth))) - + baseline + bottom); + } + } + return maxAbove + maxBelow; //remind(aim): ceil this value? + } + + double max = 0; + for (int i = 0, maxPos = children.size(); i < maxPos; i++) { + final Layoutable child = children.get(i); + Insets margin = childMargins.call(child); + final double childWidth = Double.isNaN(singleChildWidth) ? childWidths[i] : singleChildWidth; + + max = Math.max(max, minimum? + computeChildMinAreaHeight(snapper, child, -1, margin, childWidth, fillWidth) : + computeChildPrefAreaHeight(snapper, child, -1, margin, childWidth, fillWidth)); + } + return max; + } + + /* Utility method for computing the max of children's min or pref width, horizontal alignment is ignored for now. */ + static double getMaxAreaWidth(Snapper snapper, List children, + Callback childMargins, double[] childHeights, boolean fillHeight, boolean minimum) { + final double singleChildHeight = childHeights == null ? -1 : childHeights.length == 1 ? childHeights[0] : Double.NaN; + + double max = 0; + for (int i = 0, maxPos = children.size(); i < maxPos; i++) { + final Layoutable child = children.get(i); + final Insets margin = childMargins.call(child); + final double childHeight = Double.isNaN(singleChildHeight) ? childHeights[i] : singleChildHeight; + max = Math.max(max, minimum? + computeChildMinAreaWidth(snapper, child, -1, margin, childHeight, fillHeight) : + computeChildPrefAreaWidth(snapper, child, -1, margin, childHeight, fillHeight)); + } + + return max; + } + + static double getAreaBaselineOffset( + Snapper snapper, List children, Callback margins, + Function positionToWidth, double areaHeight, boolean fillHeight + ) { + return getAreaBaselineOffset(snapper, children, margins, positionToWidth, areaHeight, _ -> fillHeight, getMinBaselineComplement(children)); + } + + /** + * Returns the baseline offset of provided children, with respect to the minimum complement, computed + * by {@link #getMinBaselineComplement(java.util.List)} from the same set of children. + * + * @param snapper the {@link Snapper} to use, cannot be {@code null} + * @param children the children with baseline alignment + * @param margins their margins (callback) + * @param positionToWidth callback for children widths (can return -1 if no bias is used) + * @param areaHeight height of the area to layout in + * @param fillHeight callback to specify children that has fillHeight constraint + * @param minComplement minimum complement + */ + static double getAreaBaselineOffset( + Snapper snapper, List children, Callback margins, + Function positionToWidth, + double areaHeight, Function fillHeight, double minComplement + ) { + double b = 0; + for (int i = 0; i < children.size(); ++i) { + Layoutable l = children.get(i); + Insets margin = margins.call(l); + double top = margin != null ? snapper.snapSpaceY(margin.getTop()) : 0; + double bottom = margin != null ? snapper.snapSpaceY(margin.getBottom()) : 0; + final double bo = l.getBaselineOffset(); + if (bo == Measurable.BASELINE_OFFSET_SAME_AS_HEIGHT) { + double alt = -1; + if (l.getContentBias() == Orientation.HORIZONTAL) { + alt = positionToWidth.apply(i); + } + if (fillHeight.apply(i)) { + // If the children fills it's height, than it's "preferred" height is the area without the complement and insets + b = Math.max(b, top + boundedSize(l.minHeight(alt), areaHeight - minComplement - top - bottom, + l.maxHeight(alt))); + } else { + // Otherwise, we must use the area without complement and insets as a maximum for the child + b = Math.max(b, top + boundedSize(l.minHeight(alt), l.prefHeight(alt), + Math.min(l.maxHeight(alt), areaHeight - minComplement - top - bottom))); + } + } else { + b = Math.max(b, top + bo); + } + } + return b; + } + + /** + * Return the minimum complement of baseline + * @param children + * @return + */ + static double getMinBaselineComplement(List children) { + return getBaselineComplement(children, true, false); + } + + /** + * Return the preferred complement of baseline + * @param children + * @return + */ + static double getPrefBaselineComplement(List children) { + return getBaselineComplement(children, false, false); + } + + /** + * Return the maximal complement of baseline + * @param children + * @return + */ + static double getMaxBaselineComplement(List children) { + return getBaselineComplement(children, false, true); + } + + private static double getBaselineComplement(List children, boolean min, boolean max) { + double bc = 0; + for (Layoutable l : children) { + final double bo = l.getBaselineOffset(); + if (bo == Measurable.BASELINE_OFFSET_SAME_AS_HEIGHT) { + continue; + } + if (l.isResizable()) { + bc = Math.max(bc, (min ? l.minHeight(-1) : max ? l.maxHeight(-1) : l.prefHeight(-1)) - bo); + } else { + bc = Math.max(bc, l.getLayoutBounds().getHeight() - bo); + } + } + return bc; + } + + /** + * The bounded width and height of a child. + * + * @param width the bounded width + * @param height the bounded height + */ + record Size(double width, double height) {} + + /** + * Returns the size of a Node that should be placed in an area of the specified size, + * bounded in it's min/max size, respecting bias. + * + * @param child the child, cannot be {@code null} + * @param areaWidth the width of the bounding area where the child is going to be placed + * @param areaHeight the height of the bounding area where the child is going to be placed + * @param fillWidth whether the child should try to fill the area width + * @param fillHeight whether the child should try to fill the area height + * @return the bounded size the child should be resized to, never {@code null} + */ + static Size boundedSizeWithBias(Layoutable child, double areaWidth, double areaHeight, boolean fillWidth, boolean fillHeight) { + Orientation bias = child.getContentBias(); + + double childWidth = 0; + double childHeight = 0; + + if (bias == null) { + childWidth = boundedSize( + child.minWidth(-1), fillWidth ? areaWidth + : Math.min(areaWidth, child.prefWidth(-1)), + child.maxWidth(-1)); + childHeight = boundedSize( + child.minHeight(-1), fillHeight ? areaHeight + : Math.min(areaHeight, child.prefHeight(-1)), + child.maxHeight(-1)); + } else if (bias == Orientation.HORIZONTAL) { + childWidth = boundedSize( + child.minWidth(-1), fillWidth ? areaWidth + : Math.min(areaWidth, child.prefWidth(-1)), + child.maxWidth(-1)); + childHeight = boundedSize( + child.minHeight(childWidth), fillHeight ? areaHeight + : Math.min(areaHeight, child.prefHeight(childWidth)), + child.maxHeight(childWidth)); + } else { // bias == VERTICAL + childHeight = boundedSize( + child.minHeight(-1), fillHeight ? areaHeight + : Math.min(areaHeight, child.prefHeight(-1)), + child.maxHeight(-1)); + childWidth = boundedSize( + child.minWidth(childHeight), fillWidth ? areaWidth + : Math.min(areaWidth, child.prefWidth(childHeight)), + child.maxWidth(childHeight)); + } + + return new Size(childWidth, childHeight); + } + + /** + * Utility method which lays out the child within an area of it's + * parent defined by {@code areaX}, {@code areaY}, {@code areaWidth} x {@code areaHeight}, + * with a baseline offset relative to that area. + *

+ * If the child is resizable, this method will use {@code fillWidth} and {@code fillHeight} + * to determine whether to resize it to fill the area or keep the child at its + * preferred dimension. If fillWidth/fillHeight are true, then this method + * will only resize the child up to its max size limits. If the child's maximum + * size preference is less than the area size, the maximum size will be used. + * If child's maximum is greater than the area size, then the child will be + * resized to fit within the area, unless its minimum size prevents it. + *

+ * If the child has a non-null contentBias, then this method will use it when + * resizing the child. If the contentBias is horizontal, it will set its width + * first and then pass that value to compute the child's height. If child's + * contentBias is vertical, then it will set its height first + * and pass that value to compute the child's width. If the child's contentBias + * is null, then it's width and height have no dependencies on each other. + *

+ * If the child is not resizable (Shape, Group, etc) then it will only be + * positioned and not resized. + *

+ * If the child's resulting size differs from the area's size (either + * because it was not resizable or it's sizing preferences prevented it), then + * this function will align the node relative to the area using horizontal and + * vertical alignment values. + * If valignment is {@code VPos.BASELINE} then the node's baseline will be aligned + * with the area baseline offset parameter, otherwise the baseline parameter + * is ignored. + *

+ * If {@code margin} is non-null, then that space will be allocated around the + * child within the layout area. margin may be null. + *

+ * The resulting x,y values will be rounded to their nearest pixel + * boundaries and the width/height values will be ceiled to the next + * pixel boundary, as determined by the given {@code snapper}. + * + * @param snapper the {@link Snapper} to use, cannot be {@code null} + * @param child the child being positioned within the area, cannot be {@code null} + * @param areaX the horizontal offset of the layout area + * @param areaY the vertical offset of the layout area + * @param areaWidth the width of the layout area + * @param areaHeight the height of the layout area + * @param areaBaselineOffset the baseline offset to be used if VPos is BASELINE + * @param margin the margin of space to be allocated around the child + * @param fillWidth whether or not the child should be resized to fill the area width or kept to its preferred width + * @param fillHeight whether or not the child should e resized to fill the area height or kept to its preferred height + * @param halignment the horizontal alignment for the child within the area + * @param valignment the vertical alignment for the child within the area + */ + static void layoutInArea( + Snapper snapper, Layoutable child, double areaX, double areaY, + double areaWidth, double areaHeight, + double areaBaselineOffset, + Insets margin, boolean fillWidth, boolean fillHeight, + HPos halignment, VPos valignment + ) { + + Insets childMargin = margin != null ? margin : Insets.EMPTY; + double top = snapper.snapSpaceY(childMargin.getTop()); + double bottom = snapper.snapSpaceY(childMargin.getBottom()); + double left = snapper.snapSpaceX(childMargin.getLeft()); + double right = snapper.snapSpaceX(childMargin.getRight()); + + if (valignment == VPos.BASELINE) { + double bo = child.getBaselineOffset(); + if (bo == Measurable.BASELINE_OFFSET_SAME_AS_HEIGHT) { + if (child.isResizable()) { + // Everything below the baseline is like an "inset". The Node with BASELINE_OFFSET_SAME_AS_HEIGHT cannot + // be resized to this area + bottom += snapper.snapSpaceY(areaHeight - areaBaselineOffset); + } else { + top = snapper.snapSpaceY(areaBaselineOffset - child.getLayoutBounds().getHeight()); + } + } else { + top = snapper.snapSpaceY(areaBaselineOffset - bo); + } + } + + if (child.isResizable()) { + Size size = boundedSizeWithBias(child, areaWidth - left - right, areaHeight - top - bottom, fillWidth, fillHeight); + + child.resize(snapper.snapSizeX(size.width()), snapper.snapSizeY(size.height())); + } + position(snapper, child, areaX, areaY, areaWidth, areaHeight, areaBaselineOffset, + top, right, bottom, left, halignment, valignment); + } + + private static void position( + Snapper snapper, Layoutable child, double areaX, double areaY, double areaWidth, double areaHeight, + double areaBaselineOffset, + double topMargin, double rightMargin, double bottomMargin, double leftMargin, + HPos hpos, VPos vpos + ) { + final double xoffset = leftMargin + computeXOffset(areaWidth - leftMargin - rightMargin, + child.getLayoutBounds().getWidth(), hpos); + final double yoffset; + if (vpos == VPos.BASELINE) { + double bo = child.getBaselineOffset(); + + if (bo == Measurable.BASELINE_OFFSET_SAME_AS_HEIGHT) { + // We already know the layout bounds at this stage, so we can use them + yoffset = areaBaselineOffset - child.getLayoutBounds().getHeight(); + } else { + yoffset = areaBaselineOffset - bo; + } + } else { + yoffset = topMargin + computeYOffset(areaHeight - topMargin - bottomMargin, + child.getLayoutBounds().getHeight(), vpos); + } + + double x = snapper.snapPositionX(areaX + xoffset); + double y = snapper.snapPositionY(areaY + yoffset); + + child.relocate(x, y); + } + + private static double computeXOffset(double width, double contentWidth, HPos hpos) { + switch(hpos) { + case LEFT: + return 0; + case CENTER: + return (width - contentWidth) / 2; + case RIGHT: + return width - contentWidth; + default: + throw new AssertionError("Unhandled hPos"); + } + } + + private static double computeYOffset(double height, double contentHeight, VPos vpos) { + switch(vpos) { + case BASELINE: + case TOP: + return 0; + case CENTER: + return (height - contentHeight) / 2; + case BOTTOM: + return height - contentHeight; + default: + throw new AssertionError("Unhandled vPos"); + } + } + + /** + * Computes the value based on the given min and max values. We encode in this + * method the logic surrounding various edge cases, such as when the min is + * specified as greater than the max, or the max less than the min, or a pref + * value that exceeds either the max or min in their extremes. + *

+ * If the min is greater than the max, then we want to make sure the returned + * value is the min. In other words, in such a case, the min becomes the only + * acceptable return value. + *

+ * If the min and max values are well ordered, and the pref is less than the min + * then the min is returned. Likewise, if the values are well ordered and the + * pref is greater than the max, then the max is returned. If the pref lies + * between the min and the max, then the pref is returned. + * + * + * @param min The minimum bound + * @param pref The value to be clamped between the min and max + * @param max the maximum bound + * @return the size bounded by min, pref, and max. + */ + private static double boundedSize(double min, double pref, double max) { + double a = pref >= min ? pref : min; + double b = min >= max ? min : max; + return a <= b ? a : b; + } +} diff --git a/modules/javafx.graphics/src/main/java/javafx/scene/layout/Region.java b/modules/javafx.graphics/src/main/java/javafx/scene/layout/Region.java index 972917cc0cb..db8be2be0ba 100644 --- a/modules/javafx.graphics/src/main/java/javafx/scene/layout/Region.java +++ b/modules/javafx.graphics/src/main/java/javafx/scene/layout/Region.java @@ -72,9 +72,11 @@ import com.sun.javafx.scene.DirtyBits; import com.sun.javafx.scene.NodeHelper; import com.sun.javafx.scene.ParentHelper; +import com.sun.javafx.scene.SceneHelper; import com.sun.javafx.scene.input.PickResultChooser; import com.sun.javafx.scene.layout.RegionHelper; import com.sun.javafx.scene.layout.ScaledMath; +import com.sun.javafx.scene.layout.Snapper; import com.sun.javafx.scene.shape.ShapeHelper; import com.sun.javafx.sg.prism.NGNode; import com.sun.javafx.sg.prism.NGRegion; @@ -288,6 +290,31 @@ private double getSnapScaleY() { return _getSnapScaleYimpl(getScene()); } + /** + * Returns the {@link RenderScaleContext} for this region's current + * scene/window, or {@link RenderScaleContext#DEFAULT} if unattached. + * + * @return the {@link RenderScaleContext}, never {@code null} + */ + RenderScaleContext renderScaleContext() { + Scene scene = getScene(); + + return scene == null ? RenderScaleContext.DEFAULT : SceneHelper.getRenderScaleContext(scene); + } + + /* + * Returns the {@link Snapper} matching this region's {@link #isSnapToPixel()} state. + */ + private Snapper snapper() { + if (!isSnapToPixel()) { + return Snapper.NO_SNAPPING; + } + + Scene scene = getScene(); + + return scene == null ? Snapper.DEFAULT : SceneHelper.getSnapper(scene); + } + /** * If snapToPixel is true, then the value is rounded using Math.round. Otherwise, * the value is simply returned. This method will surely be JIT'd under normal @@ -393,35 +420,16 @@ private double snapPortionY(double value, boolean snapToPixel) { return value > 0 ? ScaledMath.floor(value, s) : ScaledMath.ceil(value, s); } - double getAreaBaselineOffset(List children, Callback margins, + double getAreaBaselineOffset(List children, Callback margins, Function positionToWidth, double areaHeight, boolean fillHeight) { - return getAreaBaselineOffset(children, margins, positionToWidth, areaHeight, fillHeight, isSnapToPixel()); + return LayoutSupport.getAreaBaselineOffset(snapper(), children, margins, positionToWidth, areaHeight, fillHeight); } - static double getAreaBaselineOffset(List children, Callback margins, - Function positionToWidth, - double areaHeight, boolean fillHeight, boolean snapToPixel) { - return getAreaBaselineOffset(children, margins, positionToWidth, areaHeight, fillHeight, - getMinBaselineComplement(children), snapToPixel); - } - - double getAreaBaselineOffset(List children, Callback margins, + double getAreaBaselineOffset(List children, Callback margins, Function positionToWidth, double areaHeight, final boolean fillHeight, double minComplement) { - return getAreaBaselineOffset(children, margins, positionToWidth, areaHeight, fillHeight, minComplement, isSnapToPixel()); - } - - static double getAreaBaselineOffset(List children, Callback margins, - Function positionToWidth, - double areaHeight, final boolean fillHeight, double minComplement, boolean snapToPixel) { - return getAreaBaselineOffset(children, margins, positionToWidth, areaHeight, t -> fillHeight, minComplement, snapToPixel); - } - - double getAreaBaselineOffset(List children, Callback margins, - Function positionToWidth, - double areaHeight, Function fillHeight, double minComplement) { - return getAreaBaselineOffset(children, margins, positionToWidth, areaHeight, fillHeight, minComplement, isSnapToPixel()); + return getAreaBaselineOffset(children, margins, positionToWidth, areaHeight, _ -> fillHeight, minComplement); } /** @@ -434,38 +442,10 @@ static double getAreaBaselineOffset(List children, Callback * @param fillHeight callback to specify children that has fillHeight constraint * @param minComplement minimum complement */ - static double getAreaBaselineOffset(List children, Callback margins, + double getAreaBaselineOffset(List children, Callback margins, Function positionToWidth, - double areaHeight, Function fillHeight, double minComplement, boolean snapToPixel) { - double b = 0; - double snapScaleV = 0.0; - for (int i = 0;i < children.size(); ++i) { - Node n = children.get(i); - // Note: all children should be coming from the same parent so they should all have the same snapScale - if (snapToPixel && i == 0) snapScaleV = getSnapScaleY(n.getParent()); - Insets margin = margins.call(n); - double top = margin != null ? snapSpace(margin.getTop(), snapToPixel, snapScaleV) : 0; - double bottom = (margin != null ? snapSpace(margin.getBottom(), snapToPixel, snapScaleV) : 0); - final double bo = n.getBaselineOffset(); - if (bo == BASELINE_OFFSET_SAME_AS_HEIGHT) { - double alt = -1; - if (n.getContentBias() == Orientation.HORIZONTAL) { - alt = positionToWidth.apply(i); - } - if (fillHeight.apply(i)) { - // If the children fills it's height, than it's "preferred" height is the area without the complement and insets - b = Math.max(b, top + boundedSize(n.minHeight(alt), areaHeight - minComplement - top - bottom, - n.maxHeight(alt))); - } else { - // Otherwise, we must use the area without complement and insets as a maximum for the Node - b = Math.max(b, top + boundedSize(n.minHeight(alt), n.prefHeight(alt), - Math.min(n.maxHeight(alt), areaHeight - minComplement - top - bottom))); - } - } else { - b = Math.max(b, top + bo); - } - } - return b; + double areaHeight, Function fillHeight, double minComplement) { + return LayoutSupport.getAreaBaselineOffset(snapper(), children, margins, positionToWidth, areaHeight, fillHeight, minComplement); } /** @@ -474,7 +454,7 @@ static double getAreaBaselineOffset(List children, Callback * @return */ static double getMinBaselineComplement(List children) { - return getBaselineComplement(children, true, false); + return LayoutSupport.getMinBaselineComplement(children); } /** @@ -483,7 +463,7 @@ static double getMinBaselineComplement(List children) { * @return */ static double getPrefBaselineComplement(List children) { - return getBaselineComplement(children, false, false); + return LayoutSupport.getPrefBaselineComplement(children); } /** @@ -492,23 +472,7 @@ static double getPrefBaselineComplement(List children) { * @return */ static double getMaxBaselineComplement(List children) { - return getBaselineComplement(children, false, true); - } - - private static double getBaselineComplement(List children, boolean min, boolean max) { - double bc = 0; - for (Node n : children) { - final double bo = n.getBaselineOffset(); - if (bo == BASELINE_OFFSET_SAME_AS_HEIGHT) { - continue; - } - if (n.isResizable()) { - bc = Math.max(bc, (min ? n.minHeight(-1) : max ? n.maxHeight(-1) : n.prefHeight(-1)) - bo); - } else { - bc = Math.max(bc, n.getLayoutBounds().getHeight() - bo); - } - } - return bc; + return LayoutSupport.getMaxBaselineComplement(children); } @@ -1873,20 +1837,7 @@ public final double snappedRightInset() { } double computeChildMinAreaWidth(Node child, double baselineComplement, Insets margin, double availableHeight, boolean fillHeight) { - final boolean snap = isSnapToPixel(); - double left = margin != null? snapSpaceX(margin.getLeft(), snap) : 0; - double right = margin != null? snapSpaceX(margin.getRight(), snap) : 0; - double alt = -1; - if (availableHeight != -1 && child.isResizable() && child.getContentBias() == Orientation.VERTICAL) { // width depends on height - double top = margin != null? snapSpaceY(margin.getTop(), snap) : 0; - double bottom = (margin != null? snapSpaceY(margin.getBottom(), snap) : 0); - double bo = child.getBaselineOffset(); - final double contentHeight = bo == BASELINE_OFFSET_SAME_AS_HEIGHT && baselineComplement != -1 ? - availableHeight - top - bottom - baselineComplement : - availableHeight - top - bottom; - alt = computedBoundedHeight(child, fillHeight, contentHeight); - } - return left + snapSizeX(child.minWidth(alt)) + right; + return LayoutSupport.computeChildMinAreaWidth(snapper(), child, baselineComplement, margin, availableHeight, fillHeight); } double computeChildMinAreaHeight(Node child, Insets margin) { @@ -1894,29 +1845,7 @@ public final double snappedRightInset() { } double computeChildMinAreaHeight(Node child, double minBaselineComplement, Insets margin, double availableWidth, boolean fillWidth) { - final boolean snap = isSnapToPixel(); - double top =margin != null? snapSpaceY(margin.getTop(), snap) : 0; - double bottom = margin != null? snapSpaceY(margin.getBottom(), snap) : 0; - - double alt = -1; - if (availableWidth != -1 && child.isResizable() && child.getContentBias() == Orientation.HORIZONTAL) { // height depends on width - double contentWidth = computeContentWidth(margin, availableWidth); - - alt = computeBoundedWidth(child, fillWidth, contentWidth); - } - - // For explanation, see computeChildPrefAreaHeight - if (minBaselineComplement != -1) { - double baseline = child.getBaselineOffset(); - if (child.isResizable() && baseline == BASELINE_OFFSET_SAME_AS_HEIGHT) { - return top + snapSizeY(child.minHeight(alt)) + bottom - + minBaselineComplement; - } else { - return baseline + minBaselineComplement; - } - } else { - return top + snapSizeY(child.minHeight(alt)) + bottom; - } + return LayoutSupport.computeChildMinAreaHeight(snapper(), child, minBaselineComplement, margin, availableWidth, fillWidth); } double computeChildPrefAreaWidth(Node child, Insets margin) { @@ -1924,20 +1853,7 @@ public final double snappedRightInset() { } double computeChildPrefAreaWidth(Node child, double baselineComplement, Insets margin, double availableHeight, boolean fillHeight) { - final boolean snap = isSnapToPixel(); - double left = margin != null? snapSpaceX(margin.getLeft(), snap) : 0; - double right = margin != null? snapSpaceX(margin.getRight(), snap) : 0; - double alt = -1; - if (availableHeight != -1 && child.isResizable() && child.getContentBias() == Orientation.VERTICAL) { // width depends on height - double top = margin != null? snapSpaceY(margin.getTop(), snap) : 0; - double bottom = margin != null? snapSpaceY(margin.getBottom(), snap) : 0; - double bo = child.getBaselineOffset(); - final double contentHeight = bo == BASELINE_OFFSET_SAME_AS_HEIGHT && baselineComplement != -1 ? - availableHeight - top - bottom - baselineComplement : - availableHeight - top - bottom; - alt = computedBoundedHeight(child, fillHeight, contentHeight); - } - return left + snapSizeX(boundedSize(child.minWidth(alt), child.prefWidth(alt), child.maxWidth(alt))) + right; + return LayoutSupport.computeChildPrefAreaWidth(snapper(), child, baselineComplement, margin, availableHeight, fillHeight); } double computeChildPrefAreaHeight(Node child, Insets margin) { @@ -1945,87 +1861,15 @@ public final double snappedRightInset() { } double computeChildPrefAreaHeight(Node child, double prefBaselineComplement, Insets margin, double availableWidth, boolean fillWidth) { - final boolean snap = isSnapToPixel(); - double top = margin != null? snapSpaceY(margin.getTop(), snap) : 0; - double bottom = margin != null? snapSpaceY(margin.getBottom(), snap) : 0; - - double alt = -1; - if (availableWidth != -1 && child.isResizable() && child.getContentBias() == Orientation.HORIZONTAL) { // height depends on width - double contentWidth = computeContentWidth(margin, availableWidth); - - alt = computeBoundedWidth(child, fillWidth, contentWidth); - } - - if (prefBaselineComplement != -1) { - double baseline = child.getBaselineOffset(); - if (child.isResizable() && baseline == BASELINE_OFFSET_SAME_AS_HEIGHT) { - // When baseline is same as height, the preferred height of the node will be above the baseline, so we need to add - // the preferred complement to it - return top + snapSizeY(boundedSize(child.minHeight(alt), child.prefHeight(alt), child.maxHeight(alt))) + bottom - + prefBaselineComplement; - } else { - // For all other Nodes, it's just their baseline and the complement. - // Note that the complement already contain the Node's preferred (or fixed) height - return top + baseline + prefBaselineComplement + bottom; - } - } else { - return top + snapSizeY(boundedSize(child.minHeight(alt), child.prefHeight(alt), child.maxHeight(alt))) + bottom; - } + return LayoutSupport.computeChildPrefAreaHeight(snapper(), child, prefBaselineComplement, margin, availableWidth, fillWidth); } double computeChildMaxAreaWidth(Node child, double baselineComplement, Insets margin, double availableHeight, boolean fillHeight) { - double max = child.maxWidth(-1); - if (max == Double.MAX_VALUE) { - return max; - } - final boolean snap = isSnapToPixel(); - double left = margin != null? snapSpaceX(margin.getLeft(), snap) : 0; - double right = margin != null? snapSpaceX(margin.getRight(), snap) : 0; - double alt = -1; - if (availableHeight != -1 && child.isResizable() && child.getContentBias() == Orientation.VERTICAL) { // width depends on height - double top = margin != null? snapSpaceY(margin.getTop(), snap) : 0; - double bottom = (margin != null? snapSpaceY(margin.getBottom(), snap) : 0); - double bo = child.getBaselineOffset(); - final double contentHeight = bo == BASELINE_OFFSET_SAME_AS_HEIGHT && baselineComplement != -1 ? - availableHeight - top - bottom - baselineComplement : - availableHeight - top - bottom; - - alt = computedBoundedHeight(child, fillHeight, contentHeight); - max = child.maxWidth(alt); - } - // if min > max, min wins, so still need to call boundedSize() - return left + snapSizeX(boundedSize(child.minWidth(alt), max, Double.MAX_VALUE)) + right; + return LayoutSupport.computeChildMaxAreaWidth(snapper(), child, baselineComplement, margin, availableHeight, fillHeight); } double computeChildMaxAreaHeight(Node child, double maxBaselineComplement, Insets margin, double availableWidth, boolean fillWidth) { - double max = child.maxHeight(-1); - if (max == Double.MAX_VALUE) { - return max; - } - - final boolean snap = isSnapToPixel(); - double top = margin != null? snapSpaceY(margin.getTop(), snap) : 0; - double bottom = margin != null? snapSpaceY(margin.getBottom(), snap) : 0; - double alt = -1; - if (availableWidth != -1 && child.isResizable() && child.getContentBias() == Orientation.HORIZONTAL) { // height depends on width - double contentWidth = computeContentWidth(margin, availableWidth); - - alt = computeBoundedWidth(child, fillWidth, contentWidth); - max = child.maxHeight(alt); - } - // For explanation, see computeChildPrefAreaHeight - if (maxBaselineComplement != -1) { - double baseline = child.getBaselineOffset(); - if (child.isResizable() && baseline == BASELINE_OFFSET_SAME_AS_HEIGHT) { - return top + snapSizeY(boundedSize(child.minHeight(alt), max, Double.MAX_VALUE)) + bottom - + maxBaselineComplement; - } else { - return top + baseline + maxBaselineComplement + bottom; - } - } else { - // if min > max, min wins, so still need to call boundedSize() - return top + snapSizeY(boundedSize(child.minHeight(alt), max, Double.MAX_VALUE)) + bottom; - } + return LayoutSupport.computeChildMaxAreaHeight(snapper(), child, maxBaselineComplement, margin, availableWidth, fillWidth); } /* @@ -2051,110 +1895,66 @@ public final double snappedRightInset() { * (not NaN) and never negative. */ - /* - * Given a content width, limits it by the child's constraints. The fill boolean - * controls whether the content width or the child's preferred width is used to compute - * the bounded width. - */ - private double computeBoundedWidth(Node child, boolean fill, double contentWidth) { - double min = child.minWidth(-1); - double max = child.maxWidth(-1); - - if (fill) { - return snapSizeX(boundedSize(min, contentWidth, max)); - } - - return snapSizeX(boundedSize(min, child.prefWidth(-1), Math.min(max, contentWidth))); - } - - /* - * Given a content height, limits it by the child's constraints. The fill boolean - * controls whether the content height or the child's preferred height is used to compute - * the bounded height. - */ - private double computedBoundedHeight(Node child, boolean fill, double contentHeight) { - double min = child.minHeight(-1); - double max = child.maxHeight(-1); - - if (fill) { - return snapSizeY(boundedSize(min, contentHeight, max)); - } - - return snapSizeY(boundedSize(min, child.prefHeight(-1), Math.min(max, contentHeight))); - } - - /* - * Removes the given Margin (if any) from a width which still includes margins - * to create a content width. - */ - private double computeContentWidth(Insets margin, double width) { - boolean snap = isSnapToPixel(); - double left = margin != null ? snapSpaceX(margin.getLeft(), snap) : 0; - double right = margin != null ? snapSpaceX(margin.getRight(), snap) : 0; - - return width - left - right; - } - /* Max of children's minimum area widths */ - double computeMaxMinAreaWidth(List children, Callback margins) { + double computeMaxMinAreaWidth(List children, Callback margins) { return getMaxAreaWidth(children, margins, new double[] { -1 }, false, true); } - double computeMaxMinAreaWidth(List children, Callback margins, double height, + double computeMaxMinAreaWidth(List children, Callback margins, double height, boolean fillHeight) { - return getMaxAreaWidth(children, margins, new double[] { height }, fillHeight, true); + return LayoutSupport.computeMaxMinAreaWidth(snapper(), children, margins, height, fillHeight); } - double computeMaxMinAreaWidth(List children, Callback childMargins, + double computeMaxMinAreaWidth(List children, Callback childMargins, double[] childHeights, boolean fillHeight) { return getMaxAreaWidth(children, childMargins, childHeights, fillHeight, true); } /* Max of children's minimum area heights */ - double computeMaxMinAreaHeight(List children, Callback margins, VPos valignment) { + double computeMaxMinAreaHeight(List children, Callback margins, VPos valignment) { return getMaxAreaHeight(children, margins, null, false, true, valignment); } - double computeMaxMinAreaHeight(List children, Callback margins, double width, + double computeMaxMinAreaHeight(List children, Callback margins, double width, boolean fillWidth, VPos valignment) { - return getMaxAreaHeight(children, margins, new double[] { width }, fillWidth, true, valignment); + return LayoutSupport.computeMaxMinAreaHeight(snapper(), children, margins, width, fillWidth, valignment); } - double computeMaxMinAreaHeight(List children, Callback childMargins, + double computeMaxMinAreaHeight(List children, Callback childMargins, double[] childWidths, boolean fillWidth, VPos valignment) { return getMaxAreaHeight(children, childMargins, childWidths, fillWidth, true, valignment); } /* Max of children's pref area widths */ - double computeMaxPrefAreaWidth(List children, Callback margins) { + double computeMaxPrefAreaWidth(List children, Callback margins) { return getMaxAreaWidth(children, margins, new double[] { -1 }, false, false); } - double computeMaxPrefAreaWidth(List children, Callback margins, double height, + double computeMaxPrefAreaWidth(List children, Callback margins, double height, boolean fillHeight) { - return getMaxAreaWidth(children, margins, new double[] { height }, fillHeight, false); + return LayoutSupport.computeMaxPrefAreaWidth(snapper(), children, margins, height, fillHeight); } - double computeMaxPrefAreaWidth(List children, Callback childMargins, + double computeMaxPrefAreaWidth(List children, Callback childMargins, double[] childHeights, boolean fillHeight) { return getMaxAreaWidth(children, childMargins, childHeights, fillHeight, false); } /* Max of children's pref area heights */ - double computeMaxPrefAreaHeight(List children, Callback margins, VPos valignment) { + double computeMaxPrefAreaHeight(List children, Callback margins, VPos valignment) { return getMaxAreaHeight(children, margins, null, false, false, valignment); } - double computeMaxPrefAreaHeight(List children, Callback margins, double width, + double computeMaxPrefAreaHeight(List children, Callback margins, double width, boolean fillWidth, VPos valignment) { - return getMaxAreaHeight(children, margins, new double[] { width }, fillWidth, false, valignment); + return LayoutSupport.computeMaxPrefAreaHeight(snapper(), children, margins, width, fillWidth, valignment); } - double computeMaxPrefAreaHeight(List children, Callback childMargins, + double computeMaxPrefAreaHeight(List children, Callback childMargins, double[] childWidths, boolean fillWidth, VPos valignment) { return getMaxAreaHeight(children, childMargins, childWidths, fillWidth, false, valignment); } @@ -2219,61 +2019,16 @@ static Vec2d boundedNodeSizeWithBias(Node node, double areaWidth, double areaHei /* utility method for computing the max of children's min or pref heights, taking into account baseline alignment */ private double getMaxAreaHeight( - List children, Callback childMargins, double[] childWidths, + List children, Callback childMargins, double[] childWidths, boolean fillWidth, boolean minimum, VPos valignment ) { - final double singleChildWidth = childWidths == null ? -1 : childWidths.length == 1 ? childWidths[0] : Double.NaN; - if (valignment == VPos.BASELINE) { - double maxAbove = 0; - double maxBelow = 0; - for (int i = 0, maxPos = children.size(); i < maxPos; i++) { - final Node child = children.get(i); - final double childWidth = Double.isNaN(singleChildWidth) ? childWidths[i] : singleChildWidth; - Insets margin = childMargins.call(child); - final double top = margin != null? snapSpaceY(margin.getTop()) : 0; - final double bottom = margin != null? snapSpaceY(margin.getBottom()) : 0; - final double baseline = child.getBaselineOffset(); - - final double childHeight = minimum? snapSizeY(child.minHeight(childWidth)) : snapSizeY(child.prefHeight(childWidth)); - if (baseline == BASELINE_OFFSET_SAME_AS_HEIGHT) { - maxAbove = Math.max(maxAbove, childHeight + top); - } else { - maxAbove = Math.max(maxAbove, baseline + top); - maxBelow = Math.max(maxBelow, - snapSpaceY(minimum?snapSizeY(child.minHeight(childWidth)) : snapSizeY(child.prefHeight(childWidth))) - - baseline + bottom); - } - } - return maxAbove + maxBelow; //remind(aim): ceil this value? - } else { - double max = 0; - for (int i = 0, maxPos = children.size(); i < maxPos; i++) { - final Node child = children.get(i); - Insets margin = childMargins.call(child); - final double childWidth = Double.isNaN(singleChildWidth) ? childWidths[i] : singleChildWidth; - max = Math.max(max, minimum? - computeChildMinAreaHeight(child, -1, margin, childWidth, fillWidth) : - computeChildPrefAreaHeight(child, -1, margin, childWidth, fillWidth)); - } - return max; - } + return LayoutSupport.getMaxAreaHeight(snapper(), children, childMargins, childWidths, fillWidth, minimum, valignment); } /* utility method for computing the max of children's min or pref width, horizontal alignment is ignored for now */ private double getMaxAreaWidth(List children, - Callback childMargins, double[] childHeights, boolean fillHeight, boolean minimum) { - final double singleChildHeight = childHeights == null ? -1 : childHeights.length == 1 ? childHeights[0] : Double.NaN; - - double max = 0; - for (int i = 0, maxPos = children.size(); i < maxPos; i++) { - final Node child = children.get(i); - final Insets margin = childMargins.call(child); - final double childHeight = Double.isNaN(singleChildHeight) ? childHeights[i] : singleChildHeight; - max = Math.max(max, minimum? - computeChildMinAreaWidth(child, -1, margin, childHeight, fillHeight) : - computeChildPrefAreaWidth(child, -1, margin, childHeight, fillHeight)); - } - return max; + Callback childMargins, double[] childHeights, boolean fillHeight, boolean minimum) { + return LayoutSupport.getMaxAreaWidth(snapper(), children, childMargins, childHeights, fillHeight, minimum); } /** @@ -2515,7 +2270,7 @@ protected void layoutInArea(Node child, double areaX, double areaY, double areaBaselineOffset, Insets margin, boolean fillWidth, boolean fillHeight, HPos halignment, VPos valignment) { - layoutInArea(child, areaX, areaY, areaWidth, areaHeight, areaBaselineOffset, margin, fillWidth, fillHeight, halignment, valignment, isSnapToPixel()); + LayoutSupport.layoutInArea(snapper(), child, areaX, areaY, areaWidth, areaHeight, areaBaselineOffset, margin, fillWidth, fillHeight, halignment, valignment); } /** diff --git a/modules/javafx.graphics/src/main/java/javafx/scene/layout/RenderScaleContext.java b/modules/javafx.graphics/src/main/java/javafx/scene/layout/RenderScaleContext.java new file mode 100644 index 00000000000..5b7d40166e6 --- /dev/null +++ b/modules/javafx.graphics/src/main/java/javafx/scene/layout/RenderScaleContext.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package javafx.scene.layout; + +/** + * Provides the horizontal and vertical pixel scale factors for a rendering + * surface. + * + * @param snapScaleX a positive horizontal scale factor, expressed as the + * ratio of device pixels to logical pixels on the X axis + * @param snapScaleY a positive vertical scale factor, expressed as the + * ratio of device pixels to logical pixels on the Y axis + */ +public record RenderScaleContext(double snapScaleX, double snapScaleY) { + + /** + * A render scale context that returns 1.0 for both axes, useful for testing. + */ + public static final RenderScaleContext DEFAULT = new RenderScaleContext(1.0, 1.0); +} diff --git a/modules/javafx.graphics/src/main/java/javafx/scene/layout/StackPane.java b/modules/javafx.graphics/src/main/java/javafx/scene/layout/StackPane.java index 854069dc533..576f0a23557 100644 --- a/modules/javafx.graphics/src/main/java/javafx/scene/layout/StackPane.java +++ b/modules/javafx.graphics/src/main/java/javafx/scene/layout/StackPane.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -35,11 +35,9 @@ import javafx.geometry.Insets; import javafx.geometry.Orientation; import javafx.geometry.Pos; -import javafx.geometry.VPos; import javafx.scene.Node; import javafx.css.converter.EnumConverter; import javafx.css.Styleable; -import javafx.geometry.HPos; import javafx.util.Callback; /** @@ -137,8 +135,11 @@ public class StackPane extends Pane { - private boolean biasDirty = true; - private Orientation bias; + private static final Callback MARGIN_LOOKUP = child -> getMargin((Node) child); + private static final Callback ALIGNMENT_LOOKUP = child -> StackPane.getAlignment((Node) child); + + private final StackPaneLayout stackPaneLayout = new StackPaneLayout(MARGIN_LOOKUP, ALIGNMENT_LOOKUP); + private boolean layoutSynced; /* ****************************************************************** * BEGIN static methods @@ -187,8 +188,6 @@ public static Insets getMargin(Node child) { return (Insets)getConstraint(child, MARGIN_CONSTRAINT); } - private static final Callback marginAccessor = n -> getMargin(n); - /** * Removes all stackpane constraints from the child node. * @param child the child node @@ -265,88 +264,60 @@ private Pos getAlignmentInternal() { * have a content bias. */ @Override public Orientation getContentBias() { - if (biasDirty) { - bias = null; - final List children = getManagedChildren(); - for (Node child : children) { - Orientation contentBias = child.getContentBias(); - if (contentBias != null) { - bias = contentBias; - if (contentBias == Orientation.HORIZONTAL) { - break; - } - } - } - biasDirty = false; - } - return bias; + syncLayout(); + + return stackPaneLayout.getContentBias(); } @Override protected double computeMinWidth(double height) { - Listmanaged = getManagedChildren(); - return getInsets().getLeft() + - computeMaxMinAreaWidth(managed, marginAccessor, height, true) + - getInsets().getRight(); + syncLayout(); + + return stackPaneLayout.minWidth(height); } @Override protected double computeMinHeight(double width) { - Listmanaged = getManagedChildren(); - return getInsets().getTop() + - computeMaxMinAreaHeight(managed, marginAccessor, width, true, getAlignmentInternal().getVpos()) + - getInsets().getBottom(); + syncLayout(); + + return stackPaneLayout.minHeight(width); } @Override protected double computePrefWidth(double height) { - Listmanaged = getManagedChildren(); - Insets padding = getInsets(); - return padding.getLeft() + - computeMaxPrefAreaWidth(managed, marginAccessor, - (height == -1) ? -1 : (height - padding.getTop() - padding.getBottom()), true) + - padding.getRight(); + syncLayout(); + + return stackPaneLayout.prefWidth(height); } @Override protected double computePrefHeight(double width) { - Listmanaged = getManagedChildren(); - Insets padding = getInsets(); - return padding.getTop() + - computeMaxPrefAreaHeight(managed, marginAccessor, - (width == -1) ? -1 : (width - padding.getLeft() - padding.getRight()), true, - getAlignmentInternal().getVpos()) + - padding.getBottom(); - } + syncLayout(); + return stackPaneLayout.prefHeight(width); + } @Override public void requestLayout() { - biasDirty = true; - bias = null; + stackPaneLayout.invalidate(); + layoutSynced = false; super.requestLayout(); } - @Override protected void layoutChildren() { - List managed = getManagedChildren(); - Pos align = getAlignmentInternal(); - HPos alignHpos = align.getHpos(); - VPos alignVpos = align.getVpos(); - final double width = getWidth(); - double height = getHeight(); - double top = getInsets().getTop(); - double right = getInsets().getRight(); - double left = getInsets().getLeft(); - double bottom = getInsets().getBottom(); - double contentWidth = width - left - right; - double contentHeight = height - top - bottom; - double baselineOffset = alignVpos == VPos.BASELINE ? - getAreaBaselineOffset(managed, marginAccessor, i -> width, contentHeight, true) - : 0; - for (int i = 0, size = managed.size(); i < size; i++) { - Node child = managed.get(i); - Pos childAlignment = StackPane.getAlignment(child); - layoutInArea(child, left, top, - contentWidth, contentHeight, - baselineOffset, getMargin(child), - childAlignment != null? childAlignment.getHpos() : alignHpos, - childAlignment != null? childAlignment.getVpos() : alignVpos); + private void syncLayout() { + if (layoutSynced) { + return; } + + stackPaneLayout.setChildren(getManagedChildren()); + stackPaneLayout.setAlignment(getAlignmentInternal()); + stackPaneLayout.setInsets(getInsets()); + stackPaneLayout.setSnapToPixel(isSnapToPixel()); + stackPaneLayout.setRenderScaleContext(renderScaleContext()); + + layoutSynced = true; + } + + @Override + protected void layoutChildren() { + syncLayout(); + + stackPaneLayout.resizeRelocate(0, 0, getWidth(), getHeight()); } /* ************************************************************************* diff --git a/modules/javafx.graphics/src/main/java/javafx/scene/layout/StackPaneLayout.java b/modules/javafx.graphics/src/main/java/javafx/scene/layout/StackPaneLayout.java new file mode 100644 index 00000000000..a0b888f8d52 --- /dev/null +++ b/modules/javafx.graphics/src/main/java/javafx/scene/layout/StackPaneLayout.java @@ -0,0 +1,261 @@ +/* + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package javafx.scene.layout; + +import java.util.List; +import java.util.Objects; + +import com.sun.javafx.scene.layout.Snapper; + +import javafx.geometry.Bounds; +import javafx.geometry.BoundingBox; +import javafx.geometry.HPos; +import javafx.geometry.Insets; +import javafx.geometry.Orientation; +import javafx.geometry.Pos; +import javafx.geometry.VPos; +import javafx.util.Callback; + +/** + * A layout which lays out its children in a back-to-front stack. + *

+ * The z-order of the children is defined by the order of the children list + * with the 0th child being the bottom and last child on top. If a border and/or + * padding have been set, the children will be laid out within those insets. + *

+ * This layout will attempt to resize each child to fill its content area. + * If the child could not be sized to fill the layout (either because it was + * not resizable or its max size prevented it) then it will be aligned within + * the area using the alignment property, which defaults to Pos.CENTER. + * + *

Resizable Range

+ * + * This layout computes its valid range based on its content as outlined in the table below. + * + * + * + * + * + * + * + * + * + * + * + * + *
Resize Table
widthheight
minimumleft/right insets plus the largest of the children's min widths.top/bottom insets plus the largest of the children's min heights.
preferredleft/right insets plus the largest of the children's pref widths.top/bottom insets plus the largest of the children's pref heights.
maximumDouble.MAX_VALUEDouble.MAX_VALUE
+ *

+ * This layout's unbounded maximum width and height are an indication that + * it may be resized beyond its preferred size to fill whatever space is assigned + * to it. + */ +class StackPaneLayout implements Layoutable { + private final Callback marginLookup; + private final Callback alignmentLookup; + + private RenderScaleContext renderScaleContext = RenderScaleContext.DEFAULT; + private List children = List.of(); + private Pos alignment = Pos.CENTER; + private Insets insets = Insets.EMPTY; + private boolean snapToPixel = true; + private Snapper snapper = Snapper.createSnapper(renderScaleContext); + + private double x; + private double y; + private double w; + private double h; + + private boolean biasDirty = true; + private Orientation bias; + + StackPaneLayout(Callback marginLookup, Callback alignmentLookup) { + this.marginLookup = Objects.requireNonNull(marginLookup, "marginLookup"); + this.alignmentLookup = Objects.requireNonNull(alignmentLookup, "alignmentLookup"); + } + + void setChildren(List children) { + this.children = Objects.requireNonNull(children, "children"); + + invalidate(); + } + + void setAlignment(Pos alignment) { + this.alignment = alignment == null ? Pos.CENTER : alignment; + } + + void setInsets(Insets insets) { + this.insets = insets == null ? Insets.EMPTY : insets; + } + + void setSnapToPixel(boolean snapToPixel) { + if (this.snapToPixel != snapToPixel) { + this.snapToPixel = snapToPixel; + + updateSnapper(); + } + } + + void setRenderScaleContext(RenderScaleContext renderScaleContext) { + if (!this.renderScaleContext.equals(renderScaleContext)) { + this.renderScaleContext = Objects.requireNonNull(renderScaleContext, "renderScaleContext"); + + updateSnapper(); + } + } + + void invalidate() { + this.biasDirty = true; + this.bias = null; + } + + @Override + public boolean isResizable() { + return true; + } + + @Override + public Orientation getContentBias() { + if (biasDirty) { + bias = null; + + for (Layoutable child : children) { + Orientation contentBias = child.getContentBias(); + + if (contentBias != null) { + bias = contentBias; + + if (contentBias == Orientation.HORIZONTAL) { + break; + } + } + } + + biasDirty = false; + } + + return bias; + } + + @Override + public double minWidth(double height) { + // TODO pre-existing bug, insets not snapped anywhere + return insets.getLeft() + + LayoutSupport.computeMaxMinAreaWidth(snapper, children, marginLookup, height, true) + + insets.getRight(); + } + + @Override + public double minHeight(double width) { + return insets.getTop() + + LayoutSupport.computeMaxMinAreaHeight(snapper, children, marginLookup, width, true, alignment.getVpos()) + + insets.getBottom(); + } + + @Override + public double prefWidth(double height) { + return insets.getLeft() + + LayoutSupport.computeMaxPrefAreaWidth(snapper, children, marginLookup, (height == -1) ? -1 : (height - insets.getTop() - insets.getBottom()), true) + + insets.getRight(); + } + + @Override + public double prefHeight(double width) { + return insets.getTop() + + LayoutSupport.computeMaxPrefAreaHeight(snapper, children, marginLookup, (width == -1) ? -1 : (width - insets.getLeft() - insets.getRight()), true, alignment.getVpos()) + + insets.getBottom(); + } + + @Override + public double maxWidth(double height) { + return Double.MAX_VALUE; + } + + @Override + public double maxHeight(double width) { + return Double.MAX_VALUE; + } + + @Override + public double getBaselineOffset() { + return Measurable.BASELINE_OFFSET_SAME_AS_HEIGHT; + } + + @Override + public Bounds getLayoutBounds() { + return new BoundingBox(x, y, w, h); + } + + @Override + public void resize(double width, double height) { + resizeRelocate(x, y, w, h); + } + + @Override + public void relocate(double x, double y) { + resizeRelocate(x, y, w, h); + } + + @Override + public void resizeRelocate(double x, double y, double width, double height) { + this.x = x; + this.y = y; + this.w = width; + this.h = height; + + layoutChildren(); + } + + private void layoutChildren() { + HPos alignHpos = alignment.getHpos(); + VPos alignVpos = alignment.getVpos(); + double top = insets.getTop(); + double right = insets.getRight(); + double left = insets.getLeft(); + double bottom = insets.getBottom(); + double contentWidth = w - left - right; + double contentHeight = h - top - bottom; + double baselineOffset = alignVpos == VPos.BASELINE + ? LayoutSupport.getAreaBaselineOffset(snapper, children, marginLookup, _ -> contentWidth, contentHeight, true) + : 0; + + for (int i = 0, size = children.size(); i < size; i++) { + Layoutable child = children.get(i); + Pos childAlignment = alignmentLookup.call(child); + + LayoutSupport.layoutInArea( + snapper, child, x + left, y + top, + contentWidth, contentHeight, + baselineOffset, marginLookup.call(child), true, true, + childAlignment != null ? childAlignment.getHpos() : alignHpos, + childAlignment != null ? childAlignment.getVpos() : alignVpos + ); + } + } + + private void updateSnapper() { + this.snapper = snapToPixel ? Snapper.createSnapper(renderScaleContext) : Snapper.NO_SNAPPING; + } +} diff --git a/modules/javafx.graphics/src/main/java/javafx/scene/layout/TilePane.java b/modules/javafx.graphics/src/main/java/javafx/scene/layout/TilePane.java index f916531a984..13ede5b281f 100644 --- a/modules/javafx.graphics/src/main/java/javafx/scene/layout/TilePane.java +++ b/modules/javafx.graphics/src/main/java/javafx/scene/layout/TilePane.java @@ -230,7 +230,7 @@ public static Insets getMargin(Node node) { return (Insets)getConstraint(node, MARGIN_CONSTRAINT); } - private static final Callback marginAccessor = n -> getMargin(n); + private static final Callback marginAccessor = n -> getMargin((Node)n); /** * Removes all tilepane constraints from the child node. diff --git a/modules/javafx.graphics/src/main/java/javafx/scene/layout/VBox.java b/modules/javafx.graphics/src/main/java/javafx/scene/layout/VBox.java index 3d0449a2323..de70a28ebf2 100644 --- a/modules/javafx.graphics/src/main/java/javafx/scene/layout/VBox.java +++ b/modules/javafx.graphics/src/main/java/javafx/scene/layout/VBox.java @@ -199,7 +199,7 @@ public static Insets getMargin(Node child) { return (Insets)getConstraint(child, MARGIN_CONSTRAINT); } - private static final Callback marginAccessor = n -> getMargin(n); + private static final Callback marginAccessor = n -> getMargin((Node)n); /** * Removes all vbox constraints from the child node. From 4885655a418f1631681bdf0ba085b90eb46121c7 Mon Sep 17 00:00:00 2001 From: John Hendrikx Date: Mon, 3 Aug 2026 23:01:29 +0200 Subject: [PATCH 3/5] Remove white space at end of line --- .../src/main/java/javafx/scene/layout/Region.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/javafx.graphics/src/main/java/javafx/scene/layout/Region.java b/modules/javafx.graphics/src/main/java/javafx/scene/layout/Region.java index db8be2be0ba..11fa230695b 100644 --- a/modules/javafx.graphics/src/main/java/javafx/scene/layout/Region.java +++ b/modules/javafx.graphics/src/main/java/javafx/scene/layout/Region.java @@ -293,7 +293,7 @@ private double getSnapScaleY() { /** * Returns the {@link RenderScaleContext} for this region's current * scene/window, or {@link RenderScaleContext#DEFAULT} if unattached. - * + * * @return the {@link RenderScaleContext}, never {@code null} */ RenderScaleContext renderScaleContext() { From 5735b967ea1c382161b70ff884cd907653824c46 Mon Sep 17 00:00:00 2001 From: John Hendrikx Date: Sun, 9 Aug 2026 11:55:21 +0200 Subject: [PATCH 4/5] Address some review comments --- .../src/main/java/javafx/scene/layout/Measurable.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/javafx.graphics/src/main/java/javafx/scene/layout/Measurable.java b/modules/javafx.graphics/src/main/java/javafx/scene/layout/Measurable.java index cc81119c935..1feb611fcd7 100644 --- a/modules/javafx.graphics/src/main/java/javafx/scene/layout/Measurable.java +++ b/modules/javafx.graphics/src/main/java/javafx/scene/layout/Measurable.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2026, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -61,10 +61,10 @@ public interface Measurable { /** - * This is a special value that might be returned by {@link #getBaselineOffset()}. + * A special value that might be returned by {@link #getBaselineOffset()}. * Indicates that the height of this element should be used as its baseline. */ - static final double BASELINE_OFFSET_SAME_AS_HEIGHT = Double.NEGATIVE_INFINITY; + double BASELINE_OFFSET_SAME_AS_HEIGHT = Double.NEGATIVE_INFINITY; /** * Returns the minimum width for use in layout calculations. @@ -222,4 +222,4 @@ public interface Measurable { * @return orientation of width/height dependency or null if there is none */ Orientation getContentBias(); -} \ No newline at end of file +} From 2c575867307f89b88b544691cdb3cabe49e21a8b Mon Sep 17 00:00:00 2001 From: John Hendrikx Date: Mon, 10 Aug 2026 04:52:52 +0200 Subject: [PATCH 5/5] Fix types and removed redundant isResizable checks --- .../javafx/scene/layout/LayoutSupport.java | 43 +++++++++---------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/modules/javafx.graphics/src/main/java/javafx/scene/layout/LayoutSupport.java b/modules/javafx.graphics/src/main/java/javafx/scene/layout/LayoutSupport.java index 57a51c1776c..4f6b808f483 100644 --- a/modules/javafx.graphics/src/main/java/javafx/scene/layout/LayoutSupport.java +++ b/modules/javafx.graphics/src/main/java/javafx/scene/layout/LayoutSupport.java @@ -34,7 +34,6 @@ import javafx.geometry.Insets; import javafx.geometry.Orientation; import javafx.geometry.VPos; -import javafx.scene.Node; import javafx.util.Callback; /** @@ -42,15 +41,15 @@ */ final class LayoutSupport { - static double computeChildMinAreaWidth(Snapper snapper, Layoutable child, Insets margin) { + static double computeChildMinAreaWidth(Snapper snapper, Measurable child, Insets margin) { return computeChildMinAreaWidth(snapper, child, -1, margin, -1, false); } - static double computeChildMinAreaWidth(Snapper snapper, Layoutable child, double baselineComplement, Insets margin, double availableHeight, boolean fillHeight) { + static double computeChildMinAreaWidth(Snapper snapper, Measurable child, double baselineComplement, Insets margin, double availableHeight, boolean fillHeight) { double left = margin != null ? snapper.snapSpaceX(margin.getLeft()) : 0; double right = margin != null ? snapper.snapSpaceX(margin.getRight()) : 0; double alt = -1; - if (availableHeight != -1 && child.isResizable() && child.getContentBias() == Orientation.VERTICAL) { // width depends on height + if (availableHeight != -1 && child.getContentBias() == Orientation.VERTICAL) { // width depends on height double top = margin != null ? snapper.snapSpaceY(margin.getTop()) : 0; double bottom = margin != null ? snapper.snapSpaceY(margin.getBottom()) : 0; double bo = child.getBaselineOffset(); @@ -62,16 +61,16 @@ static double computeChildMinAreaWidth(Snapper snapper, Layoutable child, double return left + snapper.snapSizeX(child.minWidth(alt)) + right; } - static double computeChildMinAreaHeight(Snapper snapper, Layoutable child, Insets margin) { + static double computeChildMinAreaHeight(Snapper snapper, Measurable child, Insets margin) { return computeChildMinAreaHeight(snapper, child, -1, margin, -1, false); } - static double computeChildMinAreaHeight(Snapper snapper, Layoutable child, double minBaselineComplement, Insets margin, double availableWidth, boolean fillWidth) { + static double computeChildMinAreaHeight(Snapper snapper, Measurable child, double minBaselineComplement, Insets margin, double availableWidth, boolean fillWidth) { double top = margin != null ? snapper.snapSpaceY(margin.getTop()) : 0; double bottom = margin != null ? snapper.snapSpaceY(margin.getBottom()) : 0; double alt = -1; - if (availableWidth != -1 && child.isResizable() && child.getContentBias() == Orientation.HORIZONTAL) { // height depends on width + if (availableWidth != -1 && child.getContentBias() == Orientation.HORIZONTAL) { // height depends on width double contentWidth = computeContentWidth(snapper, margin, availableWidth); alt = computedBoundedWidth(snapper, child, fillWidth, contentWidth); @@ -80,7 +79,7 @@ static double computeChildMinAreaHeight(Snapper snapper, Layoutable child, doubl // For explanation, see computeChildPrefAreaHeight if (minBaselineComplement != -1) { double baseline = child.getBaselineOffset(); - if (child.isResizable() && baseline == Measurable.BASELINE_OFFSET_SAME_AS_HEIGHT) { + if (baseline == Measurable.BASELINE_OFFSET_SAME_AS_HEIGHT) { return top + snapper.snapSizeY(child.minHeight(alt)) + bottom + minBaselineComplement; } @@ -91,15 +90,15 @@ static double computeChildMinAreaHeight(Snapper snapper, Layoutable child, doubl return top + snapper.snapSizeY(child.minHeight(alt)) + bottom; } - static double computeChildPrefAreaWidth(Snapper snapper, Layoutable child, Insets margin) { + static double computeChildPrefAreaWidth(Snapper snapper, Measurable child, Insets margin) { return computeChildPrefAreaWidth(snapper, child, -1, margin, -1, false); } - static double computeChildPrefAreaWidth(Snapper snapper, Layoutable child, double baselineComplement, Insets margin, double availableHeight, boolean fillHeight) { + static double computeChildPrefAreaWidth(Snapper snapper, Measurable child, double baselineComplement, Insets margin, double availableHeight, boolean fillHeight) { double left = margin != null ? snapper.snapSpaceX(margin.getLeft()) : 0; double right = margin != null ? snapper.snapSpaceX(margin.getRight()) : 0; double alt = -1; - if (availableHeight != -1 && child.isResizable() && child.getContentBias() == Orientation.VERTICAL) { + if (availableHeight != -1 && child.getContentBias() == Orientation.VERTICAL) { double top = margin != null ? snapper.snapSpaceY(margin.getTop()) : 0; double bottom = margin != null ? snapper.snapSpaceY(margin.getBottom()) : 0; double bo = child.getBaselineOffset(); @@ -111,16 +110,16 @@ static double computeChildPrefAreaWidth(Snapper snapper, Layoutable child, doubl return left + snapper.snapSizeX(boundedSize(child.minWidth(alt), child.prefWidth(alt), child.maxWidth(alt))) + right; } - static double computeChildPrefAreaHeight(Snapper snapper, Layoutable child, Insets margin) { + static double computeChildPrefAreaHeight(Snapper snapper, Measurable child, Insets margin) { return computeChildPrefAreaHeight(snapper, child, -1, margin, -1, false); } - static double computeChildPrefAreaHeight(Snapper snapper, Layoutable child, double prefBaselineComplement, Insets margin, double availableWidth, boolean fillWidth) { + static double computeChildPrefAreaHeight(Snapper snapper, Measurable child, double prefBaselineComplement, Insets margin, double availableWidth, boolean fillWidth) { double top = margin != null ? snapper.snapSpaceY(margin.getTop()) : 0; double bottom = margin != null ? snapper.snapSpaceY(margin.getBottom()) : 0; double alt = -1; - if (availableWidth != -1 && child.isResizable() && child.getContentBias() == Orientation.HORIZONTAL) { + if (availableWidth != -1 && child.getContentBias() == Orientation.HORIZONTAL) { double contentWidth = computeContentWidth(snapper, margin, availableWidth); alt = computedBoundedWidth(snapper, child, fillWidth, contentWidth); @@ -128,7 +127,7 @@ static double computeChildPrefAreaHeight(Snapper snapper, Layoutable child, doub if (prefBaselineComplement != -1) { double baseline = child.getBaselineOffset(); - if (child.isResizable() && baseline == Measurable.BASELINE_OFFSET_SAME_AS_HEIGHT) { + if (baseline == Measurable.BASELINE_OFFSET_SAME_AS_HEIGHT) { // When baseline is same as height, the preferred height of the node will be above the baseline, so we need to add // the preferred complement to it return top + snapper.snapSizeY(boundedSize(child.minHeight(alt), child.prefHeight(alt), child.maxHeight(alt))) + bottom @@ -143,7 +142,7 @@ static double computeChildPrefAreaHeight(Snapper snapper, Layoutable child, doub return top + snapper.snapSizeY(boundedSize(child.minHeight(alt), child.prefHeight(alt), child.maxHeight(alt))) + bottom; } - static double computeChildMaxAreaWidth(Snapper snapper, Node child, double baselineComplement, Insets margin, double availableHeight, boolean fillHeight) { + static double computeChildMaxAreaWidth(Snapper snapper, Measurable child, double baselineComplement, Insets margin, double availableHeight, boolean fillHeight) { double max = child.maxWidth(-1); if (max == Double.MAX_VALUE) { return max; @@ -151,7 +150,7 @@ static double computeChildMaxAreaWidth(Snapper snapper, Node child, double basel double left = margin != null ? snapper.snapSpaceX(margin.getLeft()) : 0; double right = margin != null ? snapper.snapSpaceX(margin.getRight()) : 0; double alt = -1; - if (availableHeight != -1 && child.isResizable() && child.getContentBias() == Orientation.VERTICAL) { // width depends on height + if (availableHeight != -1 && child.getContentBias() == Orientation.VERTICAL) { // width depends on height double top = margin != null ? snapper.snapSpaceY(margin.getTop()) : 0; double bottom = (margin != null ? snapper.snapSpaceY(margin.getBottom()) : 0); double bo = child.getBaselineOffset(); @@ -166,7 +165,7 @@ static double computeChildMaxAreaWidth(Snapper snapper, Node child, double basel return left + snapper.snapSizeX(boundedSize(child.minWidth(alt), max, Double.MAX_VALUE)) + right; } - static double computeChildMaxAreaHeight(Snapper snapper, Node child, double maxBaselineComplement, Insets margin, double availableWidth, boolean fillWidth) { + static double computeChildMaxAreaHeight(Snapper snapper, Measurable child, double maxBaselineComplement, Insets margin, double availableWidth, boolean fillWidth) { double max = child.maxHeight(-1); if (max == Double.MAX_VALUE) { return max; @@ -174,7 +173,7 @@ static double computeChildMaxAreaHeight(Snapper snapper, Node child, double maxB double top = margin != null? snapper.snapSpaceY(margin.getTop()) : 0; double bottom = margin != null? snapper.snapSpaceY(margin.getBottom()) : 0; double alt = -1; - if (availableWidth != -1 && child.isResizable() && child.getContentBias() == Orientation.HORIZONTAL) { // height depends on width + if (availableWidth != -1 && child.getContentBias() == Orientation.HORIZONTAL) { // height depends on width double contentWidth = computeContentWidth(snapper, margin, availableWidth); alt = computedBoundedWidth(snapper, child, fillWidth, contentWidth); @@ -183,7 +182,7 @@ static double computeChildMaxAreaHeight(Snapper snapper, Node child, double maxB // For explanation, see computeChildPrefAreaHeight if (maxBaselineComplement != -1) { double baseline = child.getBaselineOffset(); - if (child.isResizable() && baseline == Measurable.BASELINE_OFFSET_SAME_AS_HEIGHT) { + if (baseline == Measurable.BASELINE_OFFSET_SAME_AS_HEIGHT) { return top + snapper.snapSizeY(boundedSize(child.minHeight(alt), max, Double.MAX_VALUE)) + bottom + maxBaselineComplement; } @@ -223,7 +222,7 @@ static double computeChildMaxAreaHeight(Snapper snapper, Node child, double maxB * controls whether the content width or the child's preferred width is used to compute * the bounded width. */ - private static double computedBoundedWidth(Snapper snapper, Layoutable child, boolean fill, double contentWidth) { + private static double computedBoundedWidth(Snapper snapper, Measurable child, boolean fill, double contentWidth) { double min = child.minWidth(-1); double max = child.maxWidth(-1); @@ -239,7 +238,7 @@ private static double computedBoundedWidth(Snapper snapper, Layoutable child, bo * controls whether the content height or the child's preferred height is used to compute * the bounded height. */ - private static double computedBoundedHeight(Snapper snapper, Layoutable child, boolean fill, double contentHeight) { + private static double computedBoundedHeight(Snapper snapper, Measurable child, boolean fill, double contentHeight) { double min = child.minHeight(-1); double max = child.maxHeight(-1);