在基于 Swings 文本的组件中选择时的字体连字和字距调整

cal*_*edo 7 java fonts swing kerning ligature

连字

在 java 17 上玩 swing 时,我遇到了一些奇怪的故障:当启用字体连字并切换到连字重字体时,例如Fira Code,选择部分连字文本会产生以下效果:

  • 所选文本似乎是脱离上下文重新渲染的,但是连字之外的部分仍然没有改变。

断线示例

  • 退出选择时,插入符号(文本光标)会在字母之间留下一些未渲染的空格。

取消选择部分连字会留下未渲染的空间

  • 这两个问题也可能同时发生,可能导致内容不可读。

两个问题同时出现

字距调整

当考虑字距调整密集型字体(例如Microsoft Windows 11 的新默认字体Segoe UI或 )时,会发生以下故障:Segoe UI Variable

  • 重新呈现所选文本,而不在全文上下文中计算字距调整。

紧排重新渲染选择的动画

  • 尝试将插入符号放在一长行字距调整文本中(在这种情况下,文本呈现得更加压缩)几乎是不可能的 - 似乎内部选择逻辑正在根据未字距调整的版本计算目标。

字距调整文本的错误选择偏移的动画

  • 字距调整文本似乎还存在取消选择时无法正确重新渲染的问题。

取消选择字距调整文本的动画会留下未渲染空间的痕迹

分析

这种行为似乎至少从 Java 1.8 开始就表现出来了,我节省了时间和硬盘空间检查以前的版本。而且不同的 LAF 似乎不会以任何方式影响故障。我认为罪魁祸首似乎是java.desktop/javax.swing.text.PlainView,尤其是其将文本视为可分割片段的方式,仅重新渲染“损坏”的文本updateDamage#668及其更改边界框的计算。

解决方法

我发现一直有效的唯一一致的解决方法是停用字距调整和连字。我还尝试将我自己的 PlainView 类挂接到 swing 中,但其架构积极尝试阻止此类修复。

问题

最后我想问一些问题,这对于理解问题非常有帮助:

  • 你是否也遇到过这样的问题,还是我编造的例子太综合了?
  • 其他渲染管线是否也受到影响?我认为是这样,但我只能在 Windows 11 上进行测试。
  • 你知道这是 JVM 标准库的错误还是存储的字体规格不标准吗?
  • 您知道有什么解决方法可以改善这种情况吗?
  • 有没有办法交换 Swing 中的默认字体渲染管道?

感谢您的帮助!我在下面附上了源代码以及我当前的机器配置。


编辑: 该问题似乎也存在于 Mac OSX Big Sur 和 Java 17 上,导致使用字距调整和连字时出现伪影。为了重现,我使用了默认Helvetica字体。这可能暗示了 jdk 故障的原因。

import javax.swing.*;
import java.awt.*;
import java.awt.font.*;
import java.util.*;


/**
        Tested on: Edition Windows 11 Pro
        Version    22H2
        OS build   22623.875
        Experience Windows Feature Experience Pack 1000.22636.1000.0
        
        openjdk 17.0.4.1 2022-08-12
        OpenJDK Runtime Environment Temurin-17.0.4.1+1 (build 17.0.4.1+1)
        OpenJDK 64-Bit Server VM Temurin-17.0.4.1+1 (build 17.0.4.1+1, mixed mode, sharing)
**/
public class Main {
    private static final boolean TEST_KERNING = true;
    private static final boolean TEST_LIGATURES = true;
    private static final String TEST_FONT = "Segoe UI Variable";
    private static final Number TEST_FONT_SIZE = 40;
    private static final Number TEST_FONT_WIDTH = TextAttribute.WEIGHT_BOLD;

    public static void main(String[] args) throws Exception {
        UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());

        Map<TextAttribute, Object> textAttributes = new HashMap<>();
        textAttributes.put(TextAttribute.FAMILY, TEST_FONT);
        textAttributes.put(TextAttribute.SIZE, TEST_FONT_SIZE);
        textAttributes.put(TextAttribute.WEIGHT, TEST_FONT_WIDTH);
        if(TEST_KERNING)
            textAttributes.put(TextAttribute.KERNING, TextAttribute.KERNING_ON);
        if(TEST_LIGATURES)
            textAttributes.put(TextAttribute.LIGATURES, TextAttribute.LIGATURES_ON);
        Font font = Font.getFont(textAttributes);

        JTextField txtInput = new JTextField();
        txtInput.setFont(font);

        JFrame frame = new JFrame("Demo");
        frame.setSize(400, 100);
        frame.add(txtInput);
        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

        SwingUtilities.invokeAndWait(() ->
            frame.setVisible(true)
        );
    }
}
Run Code Online (Sandbox Code Playgroud)