Skip to content

Commit 4c4da48

Browse files
committed
[JEWEL-830] Fix Editor TextStyle line height
Swing is not using the font size when computing the base lineHeight, that is then multiplied by the user-set factor (default 1.2). Instead, they use FontMetrics.getHeight(), which computes it as leading + ascent + descent. This changes the Jewel line height computation for the editor text style to use the same logic. It requires a Graphics2D instance, which is obtained from a Swing BufferedImage on demand, to obtain the same font metrics as the Swing UI uses. This gets us a much closer result, but is still not 100% matching. It looks like this introduces a ~10% extra line height in Compose compared to Swing. I will make a follow up change when I figure out where in Skia's line height usage there is a divergence from Swing's, but for now this is already much better than what we did before. This change also fixes the Jewel sample plugin.xml so it doesn't crash at startup when run in the IJP build.
1 parent 579c97c commit 4c4da48

File tree

6 files changed

+126
-47
lines changed

6 files changed

+126
-47
lines changed

platform/jewel/ide-laf-bridge/api/ide-laf-bridge.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ public final class org/jetbrains/jewel/bridge/theme/IntUiBridgeSplitButtonKt {
173173
public final class org/jetbrains/jewel/bridge/theme/IntUiBridgeTextKt {
174174
public static final fun retrieveConsoleTextStyle ()Landroidx/compose/ui/text/TextStyle;
175175
public static final fun retrieveDefaultTextStyle ()Landroidx/compose/ui/text/TextStyle;
176+
public static final fun retrieveDefaultTextStyle (F)Landroidx/compose/ui/text/TextStyle;
176177
public static final fun retrieveEditorTextStyle ()Landroidx/compose/ui/text/TextStyle;
177178
}
178179

platform/jewel/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/theme/IntUiBridgeText.kt

Lines changed: 67 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,69 @@ import androidx.compose.ui.text.platform.asComposeFontFamily
66
import androidx.compose.ui.unit.sp
77
import com.intellij.openapi.editor.colors.ColorKey
88
import com.intellij.openapi.editor.colors.EditorFontType
9+
import com.intellij.openapi.editor.impl.FontInfo
10+
import com.intellij.openapi.editor.impl.view.FontLayoutService
11+
import com.intellij.util.ui.ImageUtil
12+
import com.intellij.util.ui.JBFont
13+
import java.awt.Font
14+
import java.awt.image.BufferedImage.TYPE_INT_ARGB
15+
import javax.swing.UIManager
16+
import org.jetbrains.jewel.bridge.keyNotFound
917
import org.jetbrains.jewel.bridge.retrieveEditorColorScheme
1018
import org.jetbrains.jewel.bridge.retrieveTextStyle
1119
import org.jetbrains.jewel.bridge.toComposeColor
20+
import org.jetbrains.jewel.foundation.ExperimentalJewelApi
1221

13-
public fun retrieveDefaultTextStyle(): TextStyle = retrieveTextStyle("Label.font", "Label.foreground")
22+
private const val KEY_LABEL_FONT = "Label.font"
1423

24+
/**
25+
* Retrieves the default text style from the Swing LaF.
26+
*
27+
* This is the font set in _Settings | Appearance & Behavior | Appearance_, and defaults to 13px Inter.
28+
*/
29+
public fun retrieveDefaultTextStyle(): TextStyle = retrieveTextStyle(KEY_LABEL_FONT, "Label.foreground")
30+
31+
/**
32+
* Retrieves the default text style from the Swing LaF, applying a line height multiplier.
33+
*
34+
* This is the font set in _Settings | Appearance & Behavior | Appearance_, and defaults to 13px Inter.
35+
*/
36+
@ExperimentalJewelApi
37+
public fun retrieveDefaultTextStyle(lineHeightMultiplier: Float): TextStyle {
38+
val lafFont = UIManager.getFont(KEY_LABEL_FONT) ?: keyNotFound(KEY_LABEL_FONT, "Font")
39+
val font = JBFont.create(lafFont, false)
40+
val baseLineHeight = computeBaseLineHeightFor(font)
41+
return retrieveDefaultTextStyle().copy(lineHeight = (baseLineHeight * lineHeightMultiplier).sp)
42+
}
43+
44+
/**
45+
* Retrieves the editor style from the Swing LaF.
46+
*
47+
* This is the font set in _Settings | Editor | Font_, or _Settings | Editor | Color Scheme | Color Scheme Font_, and
48+
* defaults to 13px JetBrains Mono, with a line height factor of 1.2.
49+
*/
1550
@OptIn(ExperimentalTextApi::class)
1651
public fun retrieveEditorTextStyle(): TextStyle {
1752
val editorColorScheme = retrieveEditorColorScheme()
1853

19-
val fontSize = editorColorScheme.editorFontSize.sp
54+
val font = editorColorScheme.getFont(EditorFontType.PLAIN)
55+
val baseLineHeight = computeBaseLineHeightFor(font)
2056
return retrieveDefaultTextStyle()
2157
.copy(
2258
color = editorColorScheme.defaultForeground.toComposeColor(),
23-
fontFamily = editorColorScheme.getFont(EditorFontType.PLAIN).asComposeFontFamily(),
24-
fontSize = fontSize,
25-
lineHeight = fontSize * editorColorScheme.lineSpacing,
59+
fontFamily = font.asComposeFontFamily(),
60+
fontSize = editorColorScheme.editorFontSize.sp,
61+
lineHeight = (baseLineHeight * editorColorScheme.lineSpacing).coerceAtLeast(1f).sp,
2662
fontFeatureSettings = if (!editorColorScheme.isUseLigatures) "liga 0" else "liga 1",
2763
)
2864
}
2965

66+
/**
67+
* Retrieves the editor style from the Swing LaF.
68+
*
69+
* This is the font set in _Settings | Editor | Color Scheme | Console Font_, and defaults to the same as the
70+
* [editor font][retrieveEditorTextStyle].
71+
*/
3072
@OptIn(ExperimentalTextApi::class)
3173
public fun retrieveConsoleTextStyle(): TextStyle {
3274
val editorColorScheme = retrieveEditorColorScheme()
@@ -37,12 +79,30 @@ public fun retrieveConsoleTextStyle(): TextStyle {
3779
editorColorScheme.getColor(ColorKey.createColorKey("BLOCK_TERMINAL_DEFAULT_FOREGROUND"))
3880
?: editorColorScheme.defaultForeground
3981

82+
val font = editorColorScheme.getFont(EditorFontType.CONSOLE_PLAIN)
83+
val baseLineHeight = computeBaseLineHeightFor(font)
4084
return retrieveDefaultTextStyle()
4185
.copy(
4286
color = fontColor.toComposeColor(),
43-
fontFamily = editorColorScheme.getFont(EditorFontType.CONSOLE_PLAIN).asComposeFontFamily(),
87+
fontFamily = font.asComposeFontFamily(),
4488
fontSize = fontSize,
45-
lineHeight = fontSize * editorColorScheme.lineSpacing,
89+
lineHeight = (baseLineHeight * editorColorScheme.lineSpacing).coerceAtLeast(1f).sp,
4690
fontFeatureSettings = if (!editorColorScheme.isUseLigatures) "liga 0" else "liga 1",
4791
)
4892
}
93+
94+
private val image = ImageUtil.createImage(1, 1, TYPE_INT_ARGB)
95+
96+
/**
97+
* Computes the "base" line height with the same logic used by
98+
* [com.intellij.openapi.editor.impl.view.EditorView.initMetricsIfNeeded].
99+
*/
100+
private fun computeBaseLineHeightFor(font: Font): Int {
101+
// We need to create a Graphics2D to get its FontRendererContext. The only way we have is
102+
// by requesting a BufferedImage to create one for us. We can't reuse it because the FRC
103+
// instance is cached inside the Graphics2D and may lead to incorrect scales being applied.
104+
val graphics2D = image.createGraphics()
105+
val fm = FontInfo.getFontMetrics(font, graphics2D.fontRenderContext)
106+
107+
return FontLayoutService.getInstance().getHeight(fm)
108+
}

platform/jewel/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/SwingComparisonTabPanel.kt

Lines changed: 55 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import androidx.compose.ui.unit.Dp
2626
import androidx.compose.ui.unit.LayoutDirection
2727
import androidx.compose.ui.unit.dp
2828
import com.intellij.ide.ui.laf.darcula.ui.DarculaButtonUI
29+
import com.intellij.openapi.editor.colors.EditorFontType
2930
import com.intellij.openapi.ui.ComboBox
3031
import com.intellij.ui.JBColor
3132
import com.intellij.ui.components.JBLabel
@@ -47,9 +48,12 @@ import javax.swing.JLabel
4748
import javax.swing.JPanel
4849
import org.jetbrains.jewel.bridge.JewelComposePanel
4950
import org.jetbrains.jewel.bridge.medium
51+
import org.jetbrains.jewel.bridge.retrieveEditorColorScheme
5052
import org.jetbrains.jewel.foundation.theme.JewelTheme
5153
import org.jetbrains.jewel.ui.component.DefaultButton
54+
import org.jetbrains.jewel.ui.component.EditableListComboBox
5255
import org.jetbrains.jewel.ui.component.Icon
56+
import org.jetbrains.jewel.ui.component.ListComboBox
5357
import org.jetbrains.jewel.ui.component.OutlinedButton
5458
import org.jetbrains.jewel.ui.component.Text
5559
import org.jetbrains.jewel.ui.component.TextArea
@@ -141,6 +145,30 @@ internal class SwingComparisonTabPanel : BorderLayoutPanel() {
141145
}
142146
}
143147

148+
row("Long editor text (Swing)") {
149+
text(longText, maxLineLength = 100).applyToComponent {
150+
font = retrieveEditorColorScheme().getFont(EditorFontType.PLAIN)
151+
}
152+
}
153+
row("Long editor text (Compose)") {
154+
compose {
155+
Box {
156+
Text(
157+
longText,
158+
style = JewelTheme.editorTextStyle,
159+
modifier =
160+
Modifier.width(
161+
with(LocalDensity.current) {
162+
// Guesstimate how wide this should be ? we can't tell it to be
163+
// "fill", as it crashes natively
164+
JewelTheme.defaultTextStyle.fontSize.toDp() * 60
165+
}
166+
),
167+
)
168+
}
169+
}
170+
}
171+
144172
row("Titles (Swing)") {
145173
text("This will wrap over a couple rows", maxLineLength = 30).component.font = JBFont.h1()
146174
}
@@ -276,12 +304,12 @@ internal class SwingComparisonTabPanel : BorderLayoutPanel() {
276304
Text("Not editable")
277305
Text(text = "Selected item: $selectedText")
278306

279-
// ListComboBox(
280-
// items = comboBoxItems,
281-
// selectedIndex = selectedIndex,
282-
// onSelectedItemChange = { selectedIndex = it },
283-
// modifier = Modifier.width(200.dp),
284-
// )
307+
ListComboBox(
308+
items = comboBoxItems,
309+
selectedIndex = selectedIndex,
310+
onSelectedItemChange = { selectedIndex = it },
311+
modifier = Modifier.width(200.dp),
312+
)
285313
}
286314

287315
Column {
@@ -291,13 +319,13 @@ internal class SwingComparisonTabPanel : BorderLayoutPanel() {
291319
Text("Not editable + disabled")
292320
Text(text = "Selected item: $selectedText")
293321

294-
// ListComboBox(
295-
// items = comboBoxItems,
296-
// selectedIndex = selectedIndex,
297-
// onSelectedItemChange = { selectedIndex = it },
298-
// modifier = Modifier.width(200.dp),
299-
// enabled = false,
300-
// )
322+
ListComboBox(
323+
items = comboBoxItems,
324+
selectedIndex = selectedIndex,
325+
onSelectedItemChange = { selectedIndex = it },
326+
modifier = Modifier.width(200.dp),
327+
enabled = false,
328+
)
301329
}
302330

303331
Column {
@@ -307,13 +335,13 @@ internal class SwingComparisonTabPanel : BorderLayoutPanel() {
307335
Text("Editable")
308336
Text(text = "Selected item: $selectedText")
309337

310-
// EditableListComboBox(
311-
// items = comboBoxItems,
312-
// selectedIndex = selectedIndex,
313-
// onSelectedItemChange = { selectedIndex = it },
314-
// modifier = Modifier.width(200.dp),
315-
// maxPopupHeight = 150.dp,
316-
// )
338+
EditableListComboBox(
339+
items = comboBoxItems,
340+
selectedIndex = selectedIndex,
341+
onSelectedItemChange = { selectedIndex = it },
342+
modifier = Modifier.width(200.dp),
343+
maxPopupHeight = 150.dp,
344+
)
317345
}
318346

319347
Column {
@@ -323,13 +351,13 @@ internal class SwingComparisonTabPanel : BorderLayoutPanel() {
323351
Text("Editable + disabled")
324352
Text(text = "Selected item: $selectedText")
325353

326-
// EditableListComboBox(
327-
// items = comboBoxItems,
328-
// selectedIndex = selectedIndex,
329-
// onSelectedItemChange = { selectedIndex = it },
330-
// modifier = Modifier.width(200.dp),
331-
// enabled = false,
332-
// )
354+
EditableListComboBox(
355+
items = comboBoxItems,
356+
selectedIndex = selectedIndex,
357+
onSelectedItemChange = { selectedIndex = it },
358+
modifier = Modifier.width(200.dp),
359+
enabled = false,
360+
)
333361
}
334362
}
335363
}

platform/jewel/samples/ide-plugin/src/main/resources/META-INF/plugin.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
<!-- Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
12
<idea-plugin>
23
<id>org.jetbrains.jewel.demo</id>
34
<name>Jewel Demo</name>
@@ -23,7 +24,7 @@ See the <a href="https://github.com/JetBrains/jewel">Jewel repository</a> for mo
2324

2425
<extensions defaultExtensionNs="com.intellij">
2526

26-
<toolWindow id="JewelDemo" anchor="bottom" secondary="false" canCloseContents="false"
27+
<toolWindow id="JewelDemoToolWindow" anchor="bottom" secondary="false" canCloseContents="false"
2728
icon="JewelIcons.ToolWindowIcon"
2829
factoryClass="org.jetbrains.jewel.samples.ideplugin.JewelDemoToolWindowFactory"/>
2930

platform/jewel/samples/ide-plugin/src/main/resources/intellij.platform.jewel.samples.idePlugin.xml

Lines changed: 0 additions & 11 deletions
This file was deleted.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
action.JewelActionSystemTest.text=Jewel Action System Test
22
action.JewelWizardDialog.text=Jewel Wizard Dialog
33
action.JewelComponentShowcaseDialog.text=Jewel Components Showcase
4-
toolwindow.stripe.JewelDemo=Jewel
4+
toolwindow.stripe.JewelDemoToolWindow=Jewel

0 commit comments

Comments
 (0)