view method
Renders the current model state for display.
This method is called after every update to refresh the screen. It should return either a String or a View object.
Guidelines
- Keep view functions pure - no side effects
- View should only depend on model state
- Use string interpolation or StringBuffer for complex views
- Consider terminal width/height for responsive layouts
Example
@override
String view() {
final buffer = StringBuffer();
// Header
buffer.writeln('╔════════════════════════════╗');
buffer.writeln('║ My Application ║');
buffer.writeln('╚════════════════════════════╝');
buffer.writeln();
// Content
if (loading) {
buffer.writeln('Loading...');
} else {
for (final item in items) {
final prefix = item == selectedItem ? '▸ ' : ' ';
buffer.writeln('$prefix$item');
}
}
buffer.writeln();
// Footer
buffer.writeln('↑/↓: Navigate Enter: Select q: Quit');
return buffer.toString();
}
Implementation
@override
Object view() {
final style = activeStyle();
final lineNumberDigits = showLineNumbers ? '$lineCount'.length : 0;
final displayLines = _softWrappedLines(lineNumberDigits);
final buffer = StringBuffer();
if (value.isEmpty && placeholder.isNotEmpty) {
final p =
promptFunc?.call((
lineIndex: 0,
isFocused: _focused,
row: _row,
col: _col,
)) ??
prompt;
final ph = style.computedPlaceholder.render(placeholder);
buffer.write('${style.computedPrompt.render(p)}$ph');
} else {
for (var i = 0; i < displayLines.length; i++) {
final displayLine = displayLines[i];
final lineDecorations = _lineDecorationsForRow(displayLine.rowIndex);
final lineDecorationStyle = _lineDecorationStyleForDecorations(
style,
lineDecorations,
);
final lineNumberDecorationStyle =
_lineNumberDecorationStyleForDecorations(style, lineDecorations);
final lineNumberMarker = _normalizedLineNumberMarker(
displayLine.charOffset == 0
? _lineNumberMarkerForDecorations(lineDecorations)
: null,
);
final p =
promptFunc?.call((
lineIndex: i,
isFocused: _focused,
row: displayLine.rowIndex,
col: _col,
)) ??
prompt;
String lnNumber = '';
if (showLineNumbers) {
final lnText = displayLine.charOffset == 0
? '${(displayLine.rowIndex + 1).toString().padLeft(lineNumberDigits)}$lineNumberMarker'
: ' ' * (lineNumberDigits + 1);
lnNumber = (lineNumberDecorationStyle ?? style.computedLineNumber)
.render(lnText);
}
// Compute selection overlap for this visual segment.
int? selStart;
int? selEnd;
if (_selectionStart != null && _selectionEnd != null) {
final (x1, y1) = _selectionStart!;
final (x2, y2) = _selectionEnd!;
final startY = math.min(y1, y2);
final endY = math.max(y1, y2);
final rowIdx = displayLine.rowIndex;
if (rowIdx >= startY && rowIdx <= endY) {
// Selection range in the original (unwrapped) row coordinates.
int rowStart;
int rowEnd;
if (startY == endY) {
rowStart = math.min(x1, x2);
rowEnd = math.max(x1, x2);
} else if (rowIdx == startY) {
rowStart = y1 < y2 ? x1 : x2;
rowEnd = _document.lineLength(rowIdx);
} else if (rowIdx == endY) {
rowStart = 0;
rowEnd = y1 < y2 ? x2 : x1;
} else {
rowStart = 0;
rowEnd = _document.lineLength(rowIdx);
}
rowStart = rowStart.clamp(0, _document.lineLength(rowIdx));
rowEnd = rowEnd.clamp(0, _document.lineLength(rowIdx));
// Map to this segment via charOffset.
final segStart = displayLine.charOffset;
final segLen = uni.graphemes(displayLine.text).length;
final segEnd = segStart + segLen;
final overlapStart = math.max(rowStart, segStart);
final overlapEnd = math.min(rowEnd, segEnd);
if (overlapStart < overlapEnd) {
selStart = overlapStart - segStart;
selEnd = overlapEnd - segStart;
}
}
}
final gs = uni.graphemes(displayLine.text).toList(growable: false);
final decorationRanges = _segmentDecorationRanges(
displayLine.rowIndex,
displayLine.charOffset,
displayLine.charOffset + gs.length,
);
final cursorCol = displayLine.hasCursor
? (_col - displayLine.charOffset)
: -1;
var renderedBody = '';
for (var j = 0; j < gs.length; j++) {
final isSelected =
selStart != null && selEnd != null && j >= selStart && j < selEnd;
final decorationStyleKey = _decorationStyleKeyForColumn(
decorationRanges,
j,
);
final decorationStyle = decorationStyleKey == null
? null
: style.computedDecorationStyle(decorationStyleKey);
final partStyle = _textCellStyle(
style,
lineDecorationStyle: lineDecorationStyle,
decorationStyle: decorationStyle,
isSelected: isSelected,
useCursorStyle:
displayLine.hasCursor && useVirtualCursor && j == cursorCol,
);
final part = partStyle.render(gs[j]);
renderedBody += part;
}
if (displayLine.hasCursor &&
useVirtualCursor &&
cursorCol >= gs.length) {
final partStyle = _textCellStyle(
style,
lineDecorationStyle: lineDecorationStyle,
isSelected: false,
useCursorStyle: true,
);
renderedBody += partStyle.render(' ');
}
buffer.writeln(
'${style.computedPrompt.render(p)}$lnNumber$renderedBody',
);
}
// end of buffer indicator
final remaining = (_height - displayLines.length);
if (remaining > 0) {
final eob = style.computedEndOfBuffer.render('~');
for (var i = 0; i < remaining; i++) {
buffer.writeln(eob);
}
}
}
final content = buffer.toString().trimRight();
if (useVirtualCursor || !_focused) {
return content;
}
return View(content: content, cursor: terminalCursor);
}