diff --git a/.pql/pql-plan.json b/.pql/pql-plan.json index 9d1dab87..fd1df1e9 100644 --- a/.pql/pql-plan.json +++ b/.pql/pql-plan.json @@ -1,5 +1,5 @@ { - "exported_at": "2026-05-06T20:29:05Z", + "exported_at": "2026-05-06T20:29:23Z", "decisions": [ { "id": "D-1", diff --git a/CHANGELOG.md b/CHANGELOG.md index f35ca2e3..6e83cc6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,11 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit. ### Changed +- Terminal panes now render bold attributes with a real bold weight — + bundled JetBrainsMono Bold + BoldItalic are registered with the + `JetBrainsMono` family at `weight: 700`. The painter's bold + suppression workaround (added when only Regular + Italic were wired + and Flutter's synthetic bold drifted advance widths) is gone. - Claude pane uses `MultitabPane` for primary + secondaries — drops ~100 lines of bespoke tab-strip code, gains drag-to-reorder. - UI spacing constants live in `lib/widgets/src/spacing.dart` — diff --git a/assets/licenses.yaml b/assets/licenses.yaml index 70284287..ba27e0ff 100644 --- a/assets/licenses.yaml +++ b/assets/licenses.yaml @@ -53,7 +53,7 @@ dependencies: purpose: >- Monospace face for terminal panes, diff views, code editors, and any other monospace surface. - weights_bundled: [Regular, Italic] + weights_bundled: [Regular, Italic, Bold, BoldItalic] - name: Josefin Sans kind: font diff --git a/lib/src/terminal/src/ui/painter.dart b/lib/src/terminal/src/ui/painter.dart index 500f3221..a4376257 100644 --- a/lib/src/terminal/src/ui/painter.dart +++ b/lib/src/terminal/src/ui/painter.dart @@ -126,8 +126,7 @@ class TerminalPainter { @pragma('vm:prefer-inline') void paintHighlight(Canvas canvas, Offset offset, int length, Color color) { - final endOffset = - offset.translate(length * _cellSize.width, _cellSize.height); + final endOffset = offset.translate(length * _cellSize.width, _cellSize.height); final paint = Paint() ..color = color @@ -182,20 +181,15 @@ class TerminalPainter { if (paragraph == null) { final cellFlags = cellData.flags; - var color = cellFlags & CellFlags.inverse == 0 - ? resolveForegroundColor(cellData.foreground) - : resolveBackgroundColor(cellData.background); + var color = cellFlags & CellFlags.inverse == 0 ? resolveForegroundColor(cellData.foreground) : resolveBackgroundColor(cellData.background); if (cellData.flags & CellFlags.faint != 0) { color = color.withOpacity(0.5); } - // Skip bold rendering — Flutter's synthetic bold (no Bold.ttf - // registered) drifts glyph advance widths slightly, breaking - // the cell grid. Color is enough to convey emphasis in TUIs. final style = _textStyle.toTextStyle( color: color, - bold: false, + bold: cellFlags & CellFlags.bold != 0, italic: cellFlags & CellFlags.italic != 0, underline: cellFlags & CellFlags.underline != 0, ); diff --git a/pubspec.yaml b/pubspec.yaml index dc56967d..d76df55b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -95,3 +95,8 @@ flutter: - asset: assets/fonts/jetbrains_mono/JetBrainsMono-Regular.ttf - asset: assets/fonts/jetbrains_mono/JetBrainsMono-Italic.ttf style: italic + - asset: assets/fonts/jetbrains_mono/JetBrainsMono-Bold.ttf + weight: 700 + - asset: assets/fonts/jetbrains_mono/JetBrainsMono-BoldItalic.ttf + weight: 700 + style: italic diff --git a/test/goldens/goldens/ci/clide_button.png b/test/goldens/goldens/ci/clide_button.png index 008f3e4e..7c8b9051 100644 Binary files a/test/goldens/goldens/ci/clide_button.png and b/test/goldens/goldens/ci/clide_button.png differ diff --git a/test/goldens/goldens/linux/clide_button.png b/test/goldens/goldens/linux/clide_button.png index 4dfd29c1..17998f4c 100644 Binary files a/test/goldens/goldens/linux/clide_button.png and b/test/goldens/goldens/linux/clide_button.png differ diff --git a/test/terminal/painter_bold_metrics_test.dart b/test/terminal/painter_bold_metrics_test.dart new file mode 100644 index 00000000..80d8b5f1 --- /dev/null +++ b/test/terminal/painter_bold_metrics_test.dart @@ -0,0 +1,55 @@ +/// Verifies that the bundled JetBrainsMono Bold face has identical +/// advance widths to Regular at our render size — required for the +/// terminal cell grid to stay stable when bold attributes flip on. +/// +/// If this fails, the workaround in painter.dart was deactivated +/// against a font that drifts; pick one of the alternatives in T-73 +/// (variable JetBrainsMono / Berkeley Mono / IBM Plex Mono). +library; + +import 'dart:io'; +import 'dart:ui' as ui; + +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +const _family = 'JetBrainsMonoTest'; +const _fontSize = 14.0; +const _sample = 'mmmmmmmmmm'; + +void main() { + setUpAll(() async { + final regular = await File('assets/fonts/jetbrains_mono/JetBrainsMono-Regular.ttf').readAsBytes(); + final bold = await File('assets/fonts/jetbrains_mono/JetBrainsMono-Bold.ttf').readAsBytes(); + + final loader = FontLoader(_family) + ..addFont(Future.value(ByteData.sublistView(regular))) + ..addFont(Future.value(ByteData.sublistView(bold))); + await loader.load(); + }); + + test('JetBrainsMono Bold advance width matches Regular (cell drift = 0)', () { + final regularWidth = _measure(FontWeight.normal); + final boldWidth = _measure(FontWeight.bold); + expect(boldWidth, regularWidth, + reason: 'Bold advance width must equal Regular at $_fontSize px ' + 'or the terminal cell grid drifts when bold flips on.'); + }); +} + +double _measure(FontWeight weight) { + final builder = ui.ParagraphBuilder(ui.ParagraphStyle( + fontFamily: _family, + fontSize: _fontSize, + )) + ..pushStyle(ui.TextStyle( + fontFamily: _family, + fontWeight: weight, + fontSize: _fontSize, + )) + ..addText(_sample); + final paragraph = builder.build()..layout(const ui.ParagraphConstraints(width: double.infinity)); + final width = paragraph.maxIntrinsicWidth; + paragraph.dispose(); + return width; +}