javafx

How to disable smoothing for the Line node in TextFlow in JavaFX?


I have two TextFlow and in I need one vertical line across them (thanks to James_D who helped me with it. However, I need a line with width 1px, but it seems that line is smoothed, so the rendered line width is 2px This is my code:

public class Test extends Application {

    @Override
    public void start(Stage primaryStage) throws IOException {
        var textFlow1 = createTextFlow();
        textFlow1.setStyle("-fx-background-color: cyan");
        var textFlow2 = createTextFlow();
        textFlow2.setStyle("-fx-background-color: yellow");

        VBox vbox = new VBox(textFlow1, textFlow2);
        Scene scene = new Scene(vbox, 300, 100);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private TextFlow createTextFlow() {
        Text textA = new Text("ABC ");
        textA.setStyle("-fx-font-family: 'monospace'; -fx-font-size: 14;");
        Line line = new Line();
        line.setManaged(false);
        line.setSmooth(false);
        Text textB = new Text(" DEF");
        textB.setStyle("-fx-font-family: 'monospace'; -fx-font-size: 14;");
        TextFlow textFlow = new TextFlow(textA, line, textB) {
            @Override
            protected void layoutChildren() {
                super.layoutChildren();
                double x = textB.getBoundsInParent().getMinX();
                line.setStartX(x);
                line.setEndX(x);
                line.setEndY(getHeight());
            }
        };
        return textFlow;
    }

    public static void main(String[] args) {
        launch(args);
    }
}

And this is the result:

enter image description here

Could anyone say how to disable this smoothing for the line?


Solution

  • Round and center the line to cover the pixels it traverses

    To get a crisp line, round the endpoint coordinates and modify them by half the stroke width. As your stroke is 1 pixel, adjust by 0.5.

    For example:

    double x = Math.round(textB.getBoundsInParent().getMinX()) - 0.5;
    line.setStartX(x);
    line.setEndX(x);
    

    Taking a snapshot and zooming in the sample output:

    snapshot

    Executable Sample

    import javafx.application.Application;
    import javafx.geometry.Insets;
    import javafx.scene.Scene;
    import javafx.scene.layout.VBox;
    import javafx.scene.shape.Line;
    import javafx.scene.text.Text;
    import javafx.scene.text.TextFlow;
    import javafx.stage.Stage;
    
    import java.io.IOException;
    
    public class Test extends Application {
    
        @Override
        public void start(Stage primaryStage) throws IOException {
            var textFlow1 = createTextFlow();
            textFlow1.setStyle("-fx-background-color: cyan");
            var textFlow2 = createTextFlow();
            textFlow2.setStyle("-fx-background-color: yellow");
    
            VBox vbox = new VBox(textFlow1, textFlow2);
            vbox.setPadding(new Insets(5));
            Scene scene = new Scene(vbox);
            primaryStage.setScene(scene);
            primaryStage.show();
        }
    
        private TextFlow createTextFlow() {
            Text textA = new Text("ABC ");
            textA.setStyle("-fx-font-family: 'monospace'; -fx-font-size: 14;");
            Line line = new Line();
            line.setManaged(false);
            Text textB = new Text(" DEF");
            textB.setStyle("-fx-font-family: 'monospace'; -fx-font-size: 14;");
    
            return new TextFlow(textA, line, textB) {
                @Override
                protected void layoutChildren() {
                    super.layoutChildren();
                    double x = Math.round(textB.getBoundsInParent().getMinX()) - 0.5;
                    line.setStartX(x);
                    line.setEndX(x);
                    line.setStartY(0.5);
                    line.setEndY(Math.round(getHeight()) - 0.5);
                }
            };
        }
    
        public static void main(String[] args) {
            launch(args);
        }
    }
    

    Explanation

    For further info see the section titled "Interaction with coordinate systems" for Shape.

    Most nodes tend to have only integer translations applied to them and quite often they are defined using integer coordinates as well. For this common case, fills of shapes with straight line edges tend to be crisp since they line up with the cracks between pixels that fall on integer device coordinates and thus tend to naturally cover entire pixels. On the other hand, stroking those same shapes can often lead to fuzzy outlines because the default stroking attributes specify both that the default stroke width is 1.0 coordinates which often maps to exactly 1 device pixel and also that the stroke should straddle the border of the shape, falling half on either side of the border. Since the borders in many common shapes tend to fall directly on integer coordinates and those integer coordinates often map precisely to integer device locations, the borders tend to result in 50% coverage over the pixel rows and columns on either side of the border of the shape rather than 100% coverage on one or the other. Thus, fills may typically be crisp, but strokes are often fuzzy.

    Two common solutions to avoid these fuzzy outlines are to use wider strokes that cover more pixels completely - typically a stroke width of 2.0 will achieve this if there are no scale transforms in effect - or to specify either the StrokeType.INSIDE or StrokeType.OUTSIDE stroke styles - which will bias the default single unit stroke onto one of the full pixel rows or columns just inside or outside the border of the shape.

    For a line, unlike a filled shape, adjusting StrokeType, will not give you the desired result as a line has no width, only a length and position. StrokeType.INSIDE for a line will result in the line not being displayed, and StrokeType.OUTSIDE will result in a line twice the stroke width wide.

    For those reasons, I adjust the coordinates for the line start and end positions, assuming the default StrokeType.CENTERED rather than adjusting the stroke type. This makes the line stroke become centered on the center of the pixels the line is traversing, and, as the stroke is one pixel wide and the line is straight vertical, the line completely covers the pixels it traverses and no others.

    Alternate solution: Use a rectangle instead of a line

    As noted by Stefman1987 in comments:

    Another solution is to use Rectangle instead of Line with width 1px - it works without any "fuzziness"

    This matches up with the potential solution provided in the Javadoc comment quoted earlier:

    fills of shapes with straight line edges tend to be crisp since they line up with the cracks between pixels that fall on integer device coordinates and thus tend to naturally cover entire pixels