Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions lib/core/theme/querya_typography.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ abstract class QueryaTypography {
/// - macOS: Menlo, SF Mono, Monaco
/// - Linux / BSD: Fira Code, Ubuntu Mono, DejaVu Sans Mono
/// - Generic system fallback: monospace
///
/// Ends with [cjkFontFamilyFallback], so CJK text in the editor, inspectors
/// and badges does not go through the engine's per-paragraph search.
static const List<String> monoFontFamilyFallback = <String>[
'Cascadia Code',
'Consolas',
Expand All @@ -24,5 +27,33 @@ abstract class QueryaTypography {
'DejaVu Sans Mono',
'Courier New',
'monospace',
...cjkFontFamilyFallback,
];

/// Families that have CJK glyphs, to put after the primary family (#1553).
///
/// When the primary family has none, the engine asks fontconfig (or the OS)
/// for a fallback for every paragraph and does not reuse the answer: about
/// 400 us per 18-character string instead of 10-30 us measured with an
/// explicit family. A family that is not installed costs 2-5 us, so the
/// list can be long. Use it alone where the primary family must stay the
/// generic `monospace` (result grid cells).
static const List<String> cjkFontFamilyFallback = <String>[
// Linux
'Noto Sans Mono CJK SC',
'Noto Sans Mono CJK JP',
'Noto Sans Mono CJK KR',
'Noto Sans CJK SC',
'Noto Sans CJK JP',
'Noto Sans CJK KR',
'WenQuanYi Micro Hei',
// macOS
'PingFang SC',
'Hiragino Sans',
'Apple SD Gothic Neo',
// Windows
'Microsoft YaHei',
'Yu Gothic',
'Malgun Gothic',
];
}
3 changes: 3 additions & 0 deletions lib/features/workspace/data_grid_groupings_view.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart' as material;
import 'package:flutter/services.dart';
import 'package:querya_desktop/core/theme/querya_typography.dart';
import 'package:querya_desktop/core/ui/querya_control_tokens.dart';
import 'package:querya_desktop/features/workspace/grid_groupings_engine.dart';
import 'package:querya_desktop/features/workspace/result_grid_view.dart';
Expand Down Expand Up @@ -320,6 +321,8 @@ class _DataGridGroupingsViewState
fontSize: 13,
fontWeight: FontWeight.w600,
fontFamily: 'monospace',
fontFamilyFallback:
QueryaTypography.cjkFontFamilyFallback,
),
),
),
Expand Down
156 changes: 111 additions & 45 deletions lib/features/workspace/grid/grid_cell_renderer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -109,30 +109,29 @@ class _GridCell extends material.StatelessWidget {
final isInserted = rowStatus == StagedRowStatus.inserted;
final isModified = cellStatus == StagedCellStatus.modified;

final style = material.TextStyle(
fontSize: 12,
fontWeight: material.FontWeight.normal,
fontFamily: 'monospace',
color: isDeleted
? colorScheme.destructive.withValues(alpha: 0.7)
: (isNull
? colorScheme.mutedForeground.withValues(alpha: 0.5)
: colorScheme.foreground),
decoration: isDeleted ? material.TextDecoration.lineThrough : null,
fontStyle: isNull ? material.FontStyle.italic : material.FontStyle.normal,
final style = _CellLook.style(
colorScheme,
isNull: isNull,
isDeleted: isDeleted,
);
final decoration = _CellLook.decoration(
colorScheme,
background: isSelected
? _CellBackground.selected
: (isDeleted
? _CellBackground.deleted
: (isModified
? _CellBackground.modified
: (isInserted
? _CellBackground.inserted
: (striped
? _CellBackground.striped
: _CellBackground.none)))),
right: isSelectionRight,
left: isSelectionLeft,
top: isSelectionTop,
bottom: isSelectionBottom,
);

final bg = isSelected
? colorScheme.primary.withValues(alpha: 0.18)
: (isDeleted
? colorScheme.destructive.withValues(alpha: 0.08)
: (isModified
? colorScheme.primary.withValues(alpha: 0.14)
: (isInserted
? colorScheme.primary.withValues(alpha: 0.08)
: (striped
? colorScheme.muted.withValues(alpha: 0.12)
: material.Colors.transparent))));

// Primary/secondary gestures and hover tooltips are hoisted to the grid
// level (#983, #1346). _GridCell is a lightweight leaf widget.
Expand All @@ -144,28 +143,7 @@ class _GridCell extends material.StatelessWidget {
height: double.infinity,
padding: const material.EdgeInsets.symmetric(horizontal: 10),
alignment: material.Alignment.centerLeft,
decoration: material.BoxDecoration(
color: bg,
border: material.Border(
right: material.BorderSide(
color: isSelectionRight
? colorScheme.primary
: colorScheme.border.withValues(alpha: 0.3),
width: isSelectionRight ? 1.5 : 1.0,
),
left: isSelectionLeft
? material.BorderSide(color: colorScheme.primary, width: 1.5)
: material.BorderSide.none,
top: isSelectionTop
? material.BorderSide(color: colorScheme.primary, width: 1.5)
: material.BorderSide.none,
bottom: isSelectionBottom
? material.BorderSide(color: colorScheme.primary, width: 1.5)
: material.BorderSide(
color: colorScheme.border.withValues(alpha: 0.15),
),
),
),
decoration: decoration,
child: material.Stack(
clipBehavior: material.Clip.none,
alignment: material.Alignment.centerLeft,
Expand All @@ -192,6 +170,94 @@ class _GridCell extends material.StatelessWidget {
}
}

enum _CellBackground { selected, deleted, modified, inserted, striped, none }

/// The looks a cell can have are few, so each is built once per colour scheme
/// and shared (#1519): a scroll that builds a new row reuses them instead of
/// allocating a style, a decoration, a border and four sides per cell.
abstract final class _CellLook {
static ColorScheme? _scheme;
static final _styles = List<material.TextStyle?>.filled(4, null);
static final _decorations = <int, material.BoxDecoration>{};

/// A new colour scheme (theme switch) drops what was built for the old one.
static void _forget(ColorScheme scheme) {
if (identical(_scheme, scheme)) return;
_scheme = scheme;
_styles.fillRange(0, _styles.length, null);
_decorations.clear();
}

static material.TextStyle style(
ColorScheme scheme, {
required bool isNull,
required bool isDeleted,
}) {
_forget(scheme);
final key = (isNull ? 1 : 0) | (isDeleted ? 2 : 0);
return _styles[key] ??= material.TextStyle(
fontSize: 12,
fontWeight: material.FontWeight.normal,
fontFamily: 'monospace',
// Without a CJK family the engine searches for a fallback per paragraph
// and is ~40x slower on CJK text (#1553).
fontFamilyFallback: QueryaTypography.cjkFontFamilyFallback,
color: isDeleted
? scheme.destructive.withValues(alpha: 0.7)
: (isNull
? scheme.mutedForeground.withValues(alpha: 0.5)
: scheme.foreground),
decoration: isDeleted ? material.TextDecoration.lineThrough : null,
fontStyle: isNull ? material.FontStyle.italic : material.FontStyle.normal,
);
}

static material.BoxDecoration decoration(
ColorScheme scheme, {
required _CellBackground background,
required bool right,
required bool left,
required bool top,
required bool bottom,
}) {
_forget(scheme);
final key = background.index |
(right ? 1 << 3 : 0) |
(left ? 1 << 4 : 0) |
(top ? 1 << 5 : 0) |
(bottom ? 1 << 6 : 0);
return _decorations[key] ??= material.BoxDecoration(
color: switch (background) {
_CellBackground.selected => scheme.primary.withValues(alpha: 0.18),
_CellBackground.deleted => scheme.destructive.withValues(alpha: 0.08),
_CellBackground.modified => scheme.primary.withValues(alpha: 0.14),
_CellBackground.inserted => scheme.primary.withValues(alpha: 0.08),
_CellBackground.striped => scheme.muted.withValues(alpha: 0.12),
_CellBackground.none => material.Colors.transparent,
},
border: material.Border(
right: material.BorderSide(
color: right
? scheme.primary
: scheme.border.withValues(alpha: 0.3),
width: right ? 1.5 : 1.0,
),
left: left
? material.BorderSide(color: scheme.primary, width: 1.5)
: material.BorderSide.none,
top: top
? material.BorderSide(color: scheme.primary, width: 1.5)
: material.BorderSide.none,
bottom: bottom
? material.BorderSide(color: scheme.primary, width: 1.5)
: material.BorderSide(
color: scheme.border.withValues(alpha: 0.15),
),
),
);
}
}

class _TriangleCornerPainter extends material.CustomPainter {
const _TriangleCornerPainter({required this.color});
final material.Color color;
Expand Down
2 changes: 2 additions & 0 deletions lib/features/workspace/grid_cell_editor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:flutter/material.dart' as material;
import 'package:flutter/services.dart';
import 'package:querya_desktop/core/motion/querya_motion.dart';
import 'package:querya_desktop/core/motion/querya_motion_context.dart';
import 'package:querya_desktop/core/theme/querya_typography.dart';
import 'package:querya_desktop/features/workspace/grid_data_type_validator.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart' hide TextExtension;

Expand Down Expand Up @@ -188,6 +189,7 @@ class _GridCellEditorState extends material.State<GridCellEditor> {
style: const material.TextStyle(
fontSize: 12,
fontFamily: 'monospace',
fontFamilyFallback: QueryaTypography.cjkFontFamilyFallback,
),
decoration: const material.InputDecoration(
border: material.InputBorder.none,
Expand Down
1 change: 1 addition & 0 deletions lib/features/workspace/result_grid_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import 'package:querya_desktop/core/motion/querya_motion.dart';
import 'package:querya_desktop/core/motion/querya_motion_context.dart';
import 'package:querya_desktop/core/security/pii_masker.dart';
import 'package:querya_desktop/core/security/pii_masking_controller.dart';
import 'package:querya_desktop/core/theme/querya_typography.dart';
import 'package:querya_desktop/core/ui/querya_tooltip.dart';
import 'package:querya_desktop/features/workspace/data_grid_staging_buffer.dart';
import 'package:querya_desktop/features/workspace/grid_cell_editor.dart';
Expand Down
16 changes: 14 additions & 2 deletions test/core/theme/querya_typography_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,20 @@ void main() {
expect(stack, contains('DejaVu Sans Mono'));
expect(stack, contains('monospace'));

// Ensure fallback ends with generic 'monospace'
expect(stack.last, equals('monospace'));
// The generic 'monospace' closes the Latin stack; the CJK tail follows it
// (#1553), so Latin text keeps rendering in the monospace fonts.
const cjk = QueryaTypography.cjkFontFamilyFallback;
expect(stack.sublist(stack.length - cjk.length), equals(cjk));
expect(stack[stack.length - cjk.length - 1], equals('monospace'));
});

test('CJK fallback names a family for each desktop platform (#1553)', () {
const cjk = QueryaTypography.cjkFontFamilyFallback;
expect(cjk, contains('Noto Sans CJK SC'));
expect(cjk, contains('PingFang SC'));
expect(cjk, contains('Microsoft YaHei'));
expect(cjk, isNot(contains('monospace')));
expect(cjk.toSet().length, cjk.length); // no duplicates
});

test('QueryaEditorTheme wires monospace typography stack by default', () {
Expand Down
Loading