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 span = TuiTrace.begin(
'TextInputModel.view',
tag: TraceTag.render,
extra: 'len=${_value.length} offset=$_offset offsetR=$_offsetRight',
);
// Placeholder text
if (_value.isEmpty && placeholder.isNotEmpty) {
span.end(extra: 'placeholder');
return _placeholderView();
}
// Multi-line rendering path.
if (multiline) {
final result = _multilineView();
span.end(extra: 'multiline lines=$lineCount');
return result;
}
final styles = activeStyle();
final textInlineStyle = styles.text.inline(true);
String styleText(String s) => textInlineStyle.render(s);
final visibleValue = _value.sublist(_offset, _offsetRight);
final pos = math.max(0, _pos - _offset);
// Selection range in visible space
int? selStart, selEnd;
if (selectionStart != null && selectionEnd != null) {
final start = math.min(selectionStart!, selectionEnd!);
final end = math.max(selectionStart!, selectionEnd!);
selStart = math.max(0, start - _offset);
selEnd = math.min(visibleValue.length, end - _offset);
if (selStart >= visibleValue.length || selEnd <= 0) {
selStart = null;
selEnd = null;
}
}
final v = StringBuffer();
final selectionStyle = styles.selection;
final normalEcho = echoMode == EchoMode.normal;
final hasSelection = selStart != null && selEnd != null;
final visibleSelStart = selStart ?? -1;
final visibleSelEnd = selEnd ?? -1;
var valWidth = 0;
for (var i = 0; i < visibleValue.length; i++) {
final raw = visibleValue[i];
final char = normalEcho ? raw : _echoTransform(raw);
valWidth += normalEcho
? runeWidth(uni.firstCodePoint(raw))
: stringWidth(char);
final isSelected =
hasSelection && i >= visibleSelStart && i < visibleSelEnd;
if (i == pos) {
cursor = cursor.setChar(char);
var cv = cursor.view();
if (isSelected) {
cv = selectionStyle.render(cv);
}
v.write(cv);
} else {
final rendered = styleText(char);
v.write(isSelected ? selectionStyle.render(rendered) : rendered);
}
}
if (pos >= visibleValue.length) {
if (_focused && _canAcceptSuggestion()) {
final suggestion = _matchedSuggestions[_currentSuggestionIndex];
if (_value.length < suggestion.length) {
cursor = cursor.setChar(_echoTransform(suggestion[_value.length]));
v.write(cursor.view());
v.write(_completionView(1));
} else {
cursor = cursor.setChar(' ');
v.write(cursor.view());
}
} else {
cursor = cursor.setChar(' ');
v.write(cursor.view());
}
}
// Padding for fixed width
if (width > 0 && valWidth <= width) {
var padding = math.max(0, width - valWidth);
if (valWidth + padding <= width && pos < visibleValue.length) {
padding++;
}
v.write(_renderPadding(styles.text, styles.prompt, padding));
}
final styledPrompt = styles.prompt.render(prompt);
final content = '$styledPrompt${v.toString()}';
if (useVirtualCursor || !_focused) {
span.end(extra: 'chars=${visibleValue.length}');
return content;
}
span.end(extra: 'chars=${visibleValue.length}');
return View(content: content, cursor: terminalCursor);
}