Scale the interface fractionally on Linux

Java2D on X11 (and so under XWayland) truncates its scale to a whole
number, drawing 150 % at 100 %. On Linux, pin Java2D to 1 and hand the
full factor to FlatLaf, taken from XSETTINGS Xft/DPI, KWin's Xwayland
scale or GDK_SCALE, or chosen in Options → Design.

The client's own sizes now go through UIScale: borders, gaps, dialog and
row sizes, icons (rendered at the scaled size, so they stay crisp),
custom-painted components and font sizes in the HTML views.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-23 22:33:27 +00:00
parent 1fddae3f0b
commit 9d996f48a2
57 changed files with 643 additions and 311 deletions

View File

@@ -1,5 +1,7 @@
package com.ts3client.ui;
import com.formdev.flatlaf.util.ScaledEmptyBorder;
import com.formdev.flatlaf.util.UIScale;
import com.ts3client.contacts.Contact;
import com.ts3client.net.ChannelNode;
import com.ts3client.net.ClientEntry;
@@ -65,7 +67,7 @@ public final class InfoPanel extends JPanel {
public InfoPanel() {
super(new BorderLayout());
pane.setBorder(BorderFactory.createEmptyBorder(6, 8, 6, 8));
pane.setBorder(new ScaledEmptyBorder(6, 8, 6, 8));
pane.addHyperlinkListener(e -> {
if (e.getEventType() != javax.swing.event.HyperlinkEvent.EventType.ACTIVATED) return;
Links.open(e.getDescription(), this);
@@ -82,7 +84,7 @@ public final class InfoPanel extends JPanel {
toggle.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
toggle.setToolTipText("Show these details in a chat tab instead");
toggle.setOpaque(true);
toggle.setBorder(BorderFactory.createEmptyBorder(1, 4, 1, 4));
toggle.setBorder(new ScaledEmptyBorder(1, 4, 1, 4));
toggle.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
@@ -188,7 +190,7 @@ public final class InfoPanel extends JPanel {
private String body() {
if (shownChannel != null) return channelBody(shownChannel);
if (shownClient != null) {
renderedAvatarBox = inChatTab ? AvatarHtml.MAX_SIZE : avatarBox();
renderedAvatarBox = inChatTab ? UIScale.scale(AvatarHtml.MAX_SIZE) : avatarBox();
return clientBody(shownClient, shownModel, shownIcons, shownContact, shownAvatar, renderedAvatarBox);
}
return "<i class='muted'>Select a channel or client to see details.</i>";
@@ -201,9 +203,11 @@ public final class InfoPanel extends JPanel {
*/
private int avatarBox() {
int width = scroll.getViewport().getWidth();
if (width <= 0) return AvatarHtml.MAX_SIZE;
int box = width * 2 / 5 / AVATAR_BOX_STEP * AVATAR_BOX_STEP;
return Math.max(AVATAR_MIN_SIZE, Math.min(AvatarHtml.MAX_SIZE, box));
int max = UIScale.scale(AvatarHtml.MAX_SIZE);
if (width <= 0) return max;
int step = UIScale.scale(AVATAR_BOX_STEP);
int box = width * 2 / 5 / step * step;
return Math.max(UIScale.scale(AVATAR_MIN_SIZE), Math.min(max, box));
}
private static String channelBody(ChannelNode ch) {
@@ -231,7 +235,7 @@ public final class InfoPanel extends JPanel {
// The avatar sits top-right beside the details, like the official client's info frame.
return "<table width='100%' cellpadding='0' cellspacing='0'><tr>"
+ "<td valign='top'>" + details + "</td>"
+ "<td valign='top' align='right' style='padding-left:8px'>" + picture + "</td>"
+ "<td valign='top' align='right' style='padding-left:" + HtmlStyles.px(8) + "'>" + picture + "</td>"
+ "</tr></table>";
}
@@ -297,11 +301,11 @@ public final class InfoPanel extends JPanel {
if (group.iconId == 0) return "";
File file = icons.file(group.iconId);
if (file == null) return "";
return "<img src='" + file.toURI() + "' width='14' height='14'> ";
return "<img src='" + file.toURI() + "' width='" + UIScale.scale(14) + "' height='" + UIScale.scale(14) + "'> ";
}
private static String heading(String text) {
return "<div style='font-weight:bold;font-size:13px;margin-bottom:4px'>" + text + "</div>";
return "<div style='font-weight:bold;font-size:13px;margin-bottom:" + HtmlStyles.px(4) + "'>" + text + "</div>";
}
private static void row(StringBuilder sb, String label, String value) {