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
2 changes: 2 additions & 0 deletions lib/src/terminal_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ class TerminalView extends StatefulWidget {
/// emulators. True by default.
final bool simulateScroll;



@override
State<TerminalView> createState() => TerminalViewState();
}
Expand Down
149 changes: 126 additions & 23 deletions lib/src/ui/painter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -147,16 +147,40 @@ class TerminalPainter {
final cellData = CellData.empty();
final cellWidth = _cellSize.width;

for (var i = 0; i < line.length; i++) {
line.getCellData(i, cellData);
if (_textStyle.ligatures) {
// When ligatures are enabled, group cells with the same style to allow ligature formation
int i = 0;
while (i < line.length) {
line.getCellData(i, cellData);
final start = i;
final style = _getCellTextStyle(cellData);

// Find the end of the segment with the same style
i++;
while (i < line.length) {
line.getCellData(i, cellData);
if (!_textStylesEqual(style, _getCellTextStyle(cellData))) {
break;
}
i++;
}

// Paint the segment
_paintCellSegment(canvas, offset, line, start, i, style);
}
} else {
// Original behavior: paint each cell individually
for (var i = 0; i < line.length; i++) {
line.getCellData(i, cellData);

final charWidth = cellData.content >> CellContent.widthShift;
final cellOffset = offset.translate(i * cellWidth, 0);
final charWidth = cellData.content >> CellContent.widthShift;
final cellOffset = offset.translate(i * cellWidth, 0);

paintCell(canvas, cellOffset, cellData);
paintCell(canvas, cellOffset, cellData);

if (charWidth == 2) {
i++;
if (charWidth == 2) {
i++;
}
}
}
}
Expand All @@ -167,33 +191,111 @@ class TerminalPainter {
paintCellForeground(canvas, offset, cellData);
}

TextStyle _getCellTextStyle(CellData cellData) {
final cellFlags = cellData.flags;

var color = cellFlags & CellFlags.inverse == 0
? resolveForegroundColor(cellData.foreground)
: resolveBackgroundColor(cellData.background);

if (cellData.flags & CellFlags.faint != 0) {
color = color.withValues(alpha: 0.5);
}

return _textStyle.toTextStyle(
color: color,
bold: cellFlags & CellFlags.bold != 0,
italic: cellFlags & CellFlags.italic != 0,
underline: cellFlags & CellFlags.underline != 0,
);
}

bool _textStylesEqual(TextStyle a, TextStyle b) {
// Compare font features more carefully since they're lists
final fontFeaturesEqual =
(a.fontFeatures == null && b.fontFeatures == null) ||
(a.fontFeatures != null &&
b.fontFeatures != null &&
a.fontFeatures!.length == b.fontFeatures!.length &&
a.fontFeatures!
.every((feature) => b.fontFeatures!.contains(feature)));

return a.color == b.color &&
a.fontWeight == b.fontWeight &&
a.fontStyle == b.fontStyle &&
a.decoration == b.decoration &&
a.fontFamily == b.fontFamily &&
a.fontFamilyFallback == b.fontFamilyFallback &&
a.fontSize == b.fontSize &&
a.height == b.height &&
fontFeaturesEqual;
}

void _paintCellSegment(Canvas canvas, Offset lineOffset, BufferLine line,
int start, int end, TextStyle style) {
final cellData = CellData.empty();
final cellWidth = _cellSize.width;
final buffer = StringBuffer();

for (int i = start; i < end; i++) {
line.getCellData(i, cellData);
final charCode = cellData.content & CellContent.codepointMask;
final cellFlags = cellData.flags;
final charWidth = cellData.content >> CellContent.widthShift;

if (charCode != 0) {
var char = String.fromCharCode(charCode);
if (cellFlags & CellFlags.underline != 0 && charCode == 0x20) {
char = String.fromCharCode(0xA0);
}
buffer.write(char);
} else {
buffer.write(' '); // For empty cells
}

// Paint background for each cell
final cellOffset = lineOffset.translate(i * cellWidth, 0);
paintCellBackground(canvas, cellOffset, cellData);

if (charWidth == 2) {
i++; // Skip the next cell for wide characters
}
}

final text = buffer.toString();
if (text.isNotEmpty) {
final segmentOffset = lineOffset.translate(start * cellWidth, 0);
final cacheKey = text.hashCode ^ style.hashCode ^ _textScaler.hashCode;
var paragraph = _paragraphCache.getLayoutFromCache(cacheKey);

if (paragraph == null) {
paragraph = _paragraphCache.performAndCacheLayout(
text,
style,
_textScaler,
double.infinity,
cacheKey,
);
}

canvas.drawParagraph(paragraph, segmentOffset);
}
}

/// Paints the character in the cell represented by [cellData] to [canvas] at
/// [offset].
@pragma('vm:prefer-inline')
void paintCellForeground(Canvas canvas, Offset offset, CellData cellData) {
final charCode = cellData.content & CellContent.codepointMask;
if (charCode == 0) return;

final cacheKey = cellData.getHash() ^ _textScaler.hashCode;
final cacheKey =
cellData.getHash() ^ _textScaler.hashCode ^ _cellSize.width.hashCode;
var paragraph = _paragraphCache.getLayoutFromCache(cacheKey);

if (paragraph == null) {
final cellFlags = cellData.flags;

var color = cellFlags & CellFlags.inverse == 0
? resolveForegroundColor(cellData.foreground)
: resolveBackgroundColor(cellData.background);

if (cellData.flags & CellFlags.faint != 0) {
color = color.withOpacity(0.5);
}

final style = _textStyle.toTextStyle(
color: color,
bold: cellFlags & CellFlags.bold != 0,
italic: cellFlags & CellFlags.italic != 0,
underline: cellFlags & CellFlags.underline != 0,
);
final style = _getCellTextStyle(cellData);

// Flutter does not draw an underline below a space which is not between
// other regular characters. As only single characters are drawn, this
Expand All @@ -210,6 +312,7 @@ class TerminalPainter {
char,
style,
_textScaler,
_cellSize.width,
cacheKey,
);
}
Expand Down
3 changes: 2 additions & 1 deletion lib/src/ui/paragraph_cache.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,15 @@ class ParagraphCache {
String text,
TextStyle style,
TextScaler textScaler,
double width,
int key,
) {
final builder = ParagraphBuilder(style.getParagraphStyle());
builder.pushStyle(style.getTextStyle(textScaler: textScaler));
builder.addText(text);

final paragraph = builder.build();
paragraph.layout(ParagraphConstraints(width: double.infinity));
paragraph.layout(ParagraphConstraints(width: width));

_cache[key] = paragraph;
return paragraph;
Expand Down
6 changes: 6 additions & 0 deletions lib/src/ui/terminal_text_style.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class TerminalStyle {
this.height = _kDefaultHeight,
this.fontFamily = _kDefaultFontFamily,
this.fontFamilyFallback = _kDefaultFontFamilyFallback,
this.ligatures = false,
});

factory TerminalStyle.fromTextStyle(TextStyle textStyle) {
Expand All @@ -51,6 +52,8 @@ class TerminalStyle {

final List<String> fontFamilyFallback;

final bool ligatures;

TextStyle toTextStyle({
Color? color,
Color? backgroundColor,
Expand All @@ -68,6 +71,7 @@ class TerminalStyle {
fontWeight: bold ? FontWeight.bold : FontWeight.normal,
fontStyle: italic ? FontStyle.italic : FontStyle.normal,
decoration: underline ? TextDecoration.underline : TextDecoration.none,
fontFeatures: ligatures ? [const FontFeature.enable('liga')] : null,
);
}

Expand All @@ -76,12 +80,14 @@ class TerminalStyle {
double? height,
String? fontFamily,
List<String>? fontFamilyFallback,
bool? ligatures,
}) {
return TerminalStyle(
fontSize: fontSize ?? this.fontSize,
height: height ?? this.height,
fontFamily: fontFamily ?? this.fontFamily,
fontFamilyFallback: fontFamilyFallback ?? this.fontFamilyFallback,
ligatures: ligatures ?? this.ligatures,
);
}
}