Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

/**
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -161,6 +171,10 @@ public interface SceneAccessor {
Accessible getAccessible(Scene scene);

SceneContext getSceneContext(Scene scene);

Snapper getSnapper(Scene scene);

RenderScaleContext getRenderScaleContext(Scene scene);
}

}
Original file line number Diff line number Diff line change
@@ -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 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really like the idea of the Snapper.

What I would really like to see documented is what values developers should snap.
Maybe we could add all the information we gathered over the years here.
So the conclusion of the mailing list entries, #1948, #1111 (maybe even revive this one after) and there are probably more.

Especially: Snap only final values once (before they are returned or used as x/y/w/h (If I understood that right).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, we can add documentation here, just like how Measurable explains the bias system a bit.

Snap only final values once (before they are returned or used as x/y/w/h (If I understood that right).

That's probably best indeed; it depends on what's using those values again whether or not the snapping proved important or not (often the value gets resnapped again, depending on the container, but you shouldn't rely on that).

I also discovered a slight bug in how ceil works. We shouldn't subtract 1 ulp from the values, as 1 ulp (at Double.MAX_VALUE) can be a huge number. I was wrong when I implemented that (although it works for most "normal" values).

Instead I propose that we subtract 1 millionth of a pixel. At Double.MAX_VALUE that rounds to 0, while at more reasonable values it will remove any slight floating point errors that could cause a small 1 pixel misalignment.


/**
* A default snapper for 1.0 scaling.
*/
static final Snapper DEFAULT = new Snapper() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DEFAULT might be too generic. IDENTITY_SNAPPED or something like that?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, maybe just IDENTITY or SCALE_1X or UNIT_SCALE


@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<RenderScaleContext, Snapper> 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);
Comment on lines +169 to +172

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we perhaps have just snapPositionX/Y? Since snapSpaceX/Y is always doing the same, I don't see any point to have both (and I already disliked that in the current Region implementation)

@hjohn hjohn Aug 8, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's possible, but they do have distinct purposes:

  • Position for snapping coordinates (x/y)
    • Rounded to align controls to the closest possible display position
  • Space for snapping "empty" areas (borders, spacing, margins)
    • Rounded because they don't display important content
  • Size for snapping "content" areas (text, graphics)
    • Important, we don't want a text character or icon to be truncated by one pixel

So the type of snap function you use also tells you something about what you're snapping (and if you're passing a width of a piece of text to position or space, then that's a clear bug).

double snapSizeX(double value);
double snapSizeY(double value);
}
Loading