Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,15 @@ class MessageColumn extends ColumnData<LogData>
// The current summary is generally the first 200 chars of details.
data.summary!,
theme.regularTextStyle,
backgroundColor: theme.colorScheme.surface,
),
if (hasSummary && hasDetails())
TextSpan(text: ' • ', style: theme.subtleTextStyle),
if (hasDetails())
...textSpansFromAnsi(
detailsComputed ? data.details! : '<fetching>',
theme.subtleTextStyle,
backgroundColor: theme.colorScheme.surface,
),
],
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ class DisplayProvider extends StatelessWidget {
children: textSpansFromAnsi(
variable.text ?? '',
theme.subtleFixedFontStyle,
backgroundColor: theme.colorScheme.surface,
),
),
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ class _ConsoleOutputState extends State<_ConsoleOutput>
children: textSpansFromAnsi(
line.text,
theme.regularTextStyle,
backgroundColor: theme.colorScheme.surface,
),
),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ class _DisplayProviderState extends State<DisplayProvider> {
children: textSpansFromAnsi(
widget.variable.text!,
theme.subtleFixedFontStyle,
backgroundColor: theme.colorScheme.surface,
),
),
),
Expand Down
44 changes: 40 additions & 4 deletions packages/devtools_app/lib/src/shared/primitives/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -545,15 +545,19 @@ class DebugTimingLogger {
}
}

List<TextSpan> textSpansFromAnsi(String input, TextStyle defaultStyle) {
List<TextSpan> textSpansFromAnsi(
String input,
TextStyle defaultStyle, {
Color? backgroundColor,
}) {
final parser = AnsiParser(input);
return parser.parse().map((entry) {
final styled = entry.bold || entry.fgColor != null || entry.bgColor != null;
return TextSpan(
text: entry.text,
style: styled
? TextStyle(
color: ansiToColor(entry.fgColor),
color: ansiToColor(entry.fgColor, background: backgroundColor),
backgroundColor: ansiToColor(entry.bgColor),
fontWeight: entry.bold ? FontWeight.bold : FontWeight.normal,
)
Expand All @@ -562,13 +566,45 @@ List<TextSpan> textSpansFromAnsi(String input, TextStyle defaultStyle) {
}).toList();
}

Color? ansiToColor(List<int>? ansiInput) {
Color? ansiToColor(List<int>? ansiInput, {Color? background}) {
if (ansiInput == null) {
return null;
}

assert(ansiInput.length == 3, 'Ansi color list should contain 3 elements');
return Color.fromRGBO(ansiInput[0], ansiInput[1], ansiInput[2], 1);
final color = Color.fromRGBO(ansiInput[0], ansiInput[1], ansiInput[2], 1);
if (background == null) {
return color;
}
return _adjustForContrast(color, background);
}

/// Returns [color] adjusted so that it remains legible against [background].
///
/// ANSI colors are fixed terminal colors, and some of them (e.g. ansi-black,
/// ansi-white, ansi-bright-white) are effectively invisible against the
/// corresponding light or dark themed background. When the contrast between
/// [color] and [background] is too low, blend the color toward black (on a
/// light background) or white (on a dark background) to restore legibility.
Color _adjustForContrast(Color color, Color background) {
// A 3:1 contrast ratio is sufficient for terminal-style output (WCAG AA
// large-text threshold).
const minContrastRatio = 3.0;
const contrastBlendFactor = 0.6;

final fgLuminance = color.computeLuminance();
final bgLuminance = background.computeLuminance();
final contrastRatio =
(max(fgLuminance, bgLuminance) + 0.05) /
(min(fgLuminance, bgLuminance) + 0.05);
if (contrastRatio >= minContrastRatio) {
return color;
}
return Color.lerp(
color,
bgLuminance > 0.5 ? Colors.black : Colors.white,
contrastBlendFactor,
)!;
}
Comment thread
awaisdev5765 marked this conversation as resolved.
Resolved

/// An extension on [LogicalKeySet] to provide user-facing names for key
Expand Down
53 changes: 53 additions & 0 deletions packages/devtools_app/test/shared/primitives/utils_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,59 @@ import 'package:material_ui/material_ui.dart';

void main() {
group('utils', () {
group('textSpansFromAnsi', () {
const defaultStyle = TextStyle();
// Theme surface colors used by DevTools (see devtools_app_shared).
const lightSurface = Color(0xFFFFFFFF);
const darkSurface = Color(0xFF1B1B1F);

test('adjusts low-contrast foreground for a light background', () {
// ansi-white is invisible on a white background.
final spans = textSpansFromAnsi(
'\x1B[37mhello\x1B[0m',
defaultStyle,
backgroundColor: lightSurface,
);
final color = spans.single.style!.color!;
expect(color.computeLuminance(), lessThan(1.0));
expect(color, isNot(equals(lightSurface)));
});

test('adjusts low-contrast foreground for a dark background', () {
// ansi-black is invisible on a near-black background.
final spans = textSpansFromAnsi(
'\x1B[30mhello\x1B[0m',
defaultStyle,
backgroundColor: darkSurface,
);
final color = spans.single.style!.color!;
expect(
color.computeLuminance(),
greaterThan(darkSurface.computeLuminance()),
);
});

test('keeps colors that already have sufficient contrast', () {
// ansi-red is legible on a white background and must be unchanged.
final spans = textSpansFromAnsi(
'\x1B[31mhello\x1B[0m',
defaultStyle,
backgroundColor: lightSurface,
);
expect(
spans.single.style!.color,
equals(const Color.fromRGBO(187, 0, 0, 1)),
);
});

test('does not adjust colors when no background is provided', () {
final spans = textSpansFromAnsi('\x1B[37mhello\x1B[0m', defaultStyle);
expect(
spans.single.style!.color,
equals(const Color.fromRGBO(255, 255, 255, 1)),
);
});
});
group('durationText', () {
test('infers unit based on duration', () {
expect(durationText(Duration.zero), equals('0 μs'));
Expand Down
Loading