| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696 |
- import 'dart:math' show min, max;
- import 'dart:ui';
- import 'package:flutter/cupertino.dart';
- import 'package:flutter/material.dart';
- import 'package:flutter/rendering.dart';
- import 'package:flutter/scheduler.dart';
- import 'package:xterm/src/core/buffer/cell_flags.dart';
- import 'package:xterm/src/core/buffer/cell_offset.dart';
- import 'package:xterm/src/core/buffer/range.dart';
- import 'package:xterm/src/core/cell.dart';
- import 'package:xterm/src/core/buffer/line.dart';
- import 'package:xterm/src/core/mouse/button.dart';
- import 'package:xterm/src/core/mouse/button_state.dart';
- import 'package:xterm/src/terminal.dart';
- import 'package:xterm/src/ui/char_metrics.dart';
- import 'package:xterm/src/ui/controller.dart';
- import 'package:xterm/src/ui/cursor_type.dart';
- import 'package:xterm/src/ui/palette_builder.dart';
- import 'package:xterm/src/ui/paragraph_cache.dart';
- import 'package:xterm/src/ui/terminal_size.dart';
- import 'package:xterm/src/ui/terminal_text_style.dart';
- import 'package:xterm/src/ui/terminal_theme.dart';
- typedef EditableRectCallback = void Function(Rect rect, Rect caretRect);
- class RenderTerminal extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
- RenderTerminal({
- required Terminal terminal,
- required TerminalController controller,
- required ViewportOffset offset,
- required EdgeInsets padding,
- required bool autoResize,
- required TerminalStyle textStyle,
- required double textScaleFactor,
- required TerminalTheme theme,
- required FocusNode focusNode,
- required TerminalCursorType cursorType,
- required bool alwaysShowCursor,
- EditableRectCallback? onEditableRect,
- String? composingText,
- }) : _terminal = terminal,
- _controller = controller,
- _offset = offset,
- _padding = padding,
- _autoResize = autoResize,
- _textStyle = textStyle,
- _textScaleFactor = textScaleFactor,
- _theme = theme,
- _focusNode = focusNode,
- _cursorType = cursorType,
- _alwaysShowCursor = alwaysShowCursor,
- _onEditableRect = onEditableRect,
- _composingText = composingText {
- _updateColorPalette();
- _updateCharSize();
- }
- Terminal _terminal;
- set terminal(Terminal terminal) {
- if (_terminal == terminal) return;
- if (attached) _terminal.removeListener(_onTerminalChange);
- _terminal = terminal;
- if (attached) _terminal.addListener(_onTerminalChange);
- _resizeTerminalIfNeeded();
- markNeedsLayout();
- }
- TerminalController _controller;
- set controller(TerminalController controller) {
- if (_controller == controller) return;
- if (attached) _controller.removeListener(_onControllerUpdate);
- _controller = controller;
- if (attached) _controller.addListener(_onControllerUpdate);
- markNeedsLayout();
- }
- ViewportOffset _offset;
- set offset(ViewportOffset value) {
- if (value == _offset) return;
- if (attached) _offset.removeListener(_onScroll);
- _offset = value;
- if (attached) _offset.addListener(_onScroll);
- markNeedsLayout();
- }
- EdgeInsets _padding;
- set padding(EdgeInsets value) {
- if (value == _padding) return;
- _padding = value;
- markNeedsLayout();
- }
- bool _autoResize;
- set autoResize(bool value) {
- if (value == _autoResize) return;
- _autoResize = value;
- markNeedsLayout();
- }
- TerminalStyle _textStyle;
- set textStyle(TerminalStyle value) {
- if (value == _textStyle) return;
- _textStyle = value;
- _updateCharSize();
- _paragraphCache.clear();
- markNeedsLayout();
- }
- double _textScaleFactor;
- set textScaleFactor(double value) {
- if (value == _textScaleFactor) return;
- _textScaleFactor = value;
- _updateCharSize();
- markNeedsLayout();
- }
- TerminalTheme _theme;
- set theme(TerminalTheme value) {
- if (value == _theme) return;
- _theme = value;
- _updateColorPalette();
- markNeedsPaint();
- }
- FocusNode _focusNode;
- set focusNode(FocusNode value) {
- if (value == _focusNode) return;
- if (attached) _focusNode.removeListener(_onFocusChange);
- _focusNode = value;
- if (attached) _focusNode.addListener(_onFocusChange);
- markNeedsPaint();
- }
- TerminalCursorType _cursorType;
- set cursorType(TerminalCursorType value) {
- if (value == _cursorType) return;
- _cursorType = value;
- markNeedsPaint();
- }
- bool _alwaysShowCursor;
- set alwaysShowCursor(bool value) {
- if (value == _alwaysShowCursor) return;
- _alwaysShowCursor = value;
- markNeedsPaint();
- }
- EditableRectCallback? _onEditableRect;
- set onEditableRect(EditableRectCallback? value) {
- if (value == _onEditableRect) return;
- _onEditableRect = value;
- markNeedsLayout();
- }
- String? _composingText;
- set composingText(String? value) {
- if (value == _composingText) return;
- _composingText = value;
- markNeedsPaint();
- }
- /// The lookup table for converting terminal colors to Flutter colors. This is
- /// generated from the [_theme].
- late List<Color> _colorPalette;
- /// The size of a single character in [_textStyle] in pixels. [_textStyle] is
- /// expected to be monospace.
- late Size _charSize;
- TerminalSize? _viewportSize;
- /// Updates [_colorPalette] based on the current [_theme]. This should be
- /// called whenever the [_theme] changes.
- void _updateColorPalette() {
- _colorPalette = PaletteBuilder(_theme).build();
- }
- /// Updates [_charSize] based on the current [_textStyle]. This should be
- /// called whenever the [_textStyle] changes or the system font changes.
- void _updateCharSize() {
- _charSize = calcCharSize(_textStyle, _textScaleFactor);
- }
- var _stickToBottom = true;
- void _onScroll() {
- _stickToBottom = _scrollOffset >= _maxScrollExtent;
- markNeedsLayout();
- }
- void _onFocusChange() {
- markNeedsPaint();
- }
- void _onTerminalChange() {
- markNeedsLayout();
- }
- void _onControllerUpdate() {
- markNeedsLayout();
- }
- @override
- final isRepaintBoundary = true;
- @override
- void attach(PipelineOwner owner) {
- super.attach(owner);
- _offset.addListener(_onScroll);
- _terminal.addListener(_onTerminalChange);
- _controller.addListener(_onControllerUpdate);
- _focusNode.addListener(_onFocusChange);
- }
- @override
- void detach() {
- super.detach();
- _offset.removeListener(_onScroll);
- _terminal.removeListener(_onTerminalChange);
- _controller.removeListener(_onControllerUpdate);
- _focusNode.removeListener(_onFocusChange);
- }
- @override
- bool hitTestSelf(Offset position) {
- return true;
- }
- @override
- void systemFontsDidChange() {
- _updateCharSize();
- _paragraphCache.clear();
- super.systemFontsDidChange();
- }
- @override
- void performLayout() {
- size = constraints.biggest;
- _updateViewportSize();
- _updateScrollOffset();
- if (_stickToBottom) {
- _offset.correctBy(_maxScrollExtent - _scrollOffset);
- }
- SchedulerBinding.instance
- .addPostFrameCallback((_) => _notifyEditableRect());
- }
- /// Total height of the terminal in pixels. Includes scrollback buffer.
- double get _terminalHeight =>
- _terminal.buffer.lines.length * _charSize.height;
- /// The distance from the top of the terminal to the top of the viewport.
- // double get _scrollOffset => _offset.pixels;
- double get _scrollOffset {
- // return _offset.pixels ~/ _charSize.height * _charSize.height;
- return _offset.pixels;
- }
- /// Get the top-left corner of the cell at [cellOffset] in pixels.
- Offset getOffset(CellOffset cellOffset) {
- final row = cellOffset.y;
- final col = cellOffset.x;
- final x = col * _charSize.width;
- final y = row * _charSize.height;
- return Offset(x + _padding.left, y + _padding.top - _scrollOffset);
- }
- /// Get the [CellOffset] of the cell that [offset] is in.
- CellOffset getCellOffset(Offset offset) {
- final x = offset.dx - _padding.left;
- final y = offset.dy - _padding.top + _scrollOffset;
- final row = y ~/ _charSize.height;
- final col = x ~/ _charSize.width;
- return CellOffset(col, row);
- }
- /// Selects entire words in the terminal that contains [from] and [to].
- void selectWord(Offset from, [Offset? to]) {
- final fromOffset = getCellOffset(from);
- final fromBoundary = _terminal.buffer.getWordBoundary(fromOffset);
- if (fromBoundary == null) return;
- if (to == null) {
- _controller.setSelection(fromBoundary);
- } else {
- final toOffset = getCellOffset(to);
- final toBoundary = _terminal.buffer.getWordBoundary(toOffset);
- if (toBoundary == null) return;
- _controller.setSelection(fromBoundary.merge(toBoundary));
- }
- }
- /// Selects characters in the terminal that starts from [from] to [to]. At
- /// least one cell is selected even if [from] and [to] are same.
- void selectCharacters(Offset from, [Offset? to]) {
- final fromPosition = getCellOffset(from);
- if (to == null) {
- _controller.setSelectionRange(fromPosition, fromPosition);
- } else {
- var toPosition = getCellOffset(to);
- if (toPosition.x >= fromPosition.x) {
- toPosition = CellOffset(toPosition.x + 1, toPosition.y);
- }
- _controller.setSelectionRange(fromPosition, toPosition);
- }
- }
- /// Send a mouse event at [offset] with [button] being currently in [buttonState].
- bool mouseEvent(
- TerminalMouseButton button,
- TerminalMouseButtonState buttonState,
- Offset offset,
- ) {
- final position = getCellOffset(offset);
- return _terminal.mouseInput(button, buttonState, position);
- }
- void _notifyEditableRect() {
- final cursor = localToGlobal(_cursorOffset);
- final rect = Rect.fromLTRB(
- cursor.dx,
- cursor.dy,
- size.width,
- cursor.dy + _charSize.height,
- );
- final caretRect = cursor & _charSize;
- _onEditableRect?.call(rect, caretRect);
- }
- /// Update the viewport size in cells based on the current widget size in
- /// pixels.
- void _updateViewportSize() {
- if (size <= _charSize) {
- return;
- }
- final viewportSize = TerminalSize(
- size.width ~/ _charSize.width,
- _viewportHeight ~/ _charSize.height,
- );
- if (_viewportSize != viewportSize) {
- _viewportSize = viewportSize;
- _resizeTerminalIfNeeded();
- }
- }
- /// Notify the underlying terminal that the viewport size has changed.
- void _resizeTerminalIfNeeded() {
- if (_autoResize && _viewportSize != null) {
- _terminal.resize(
- _viewportSize!.width,
- _viewportSize!.height,
- _charSize.width.round(),
- _charSize.height.round(),
- );
- }
- }
- /// Update the scroll offset based on the current terminal state. This should
- /// be called in [performLayout] after the viewport size has been updated.
- void _updateScrollOffset() {
- _offset.applyViewportDimension(_viewportHeight);
- _offset.applyContentDimensions(0, _maxScrollExtent);
- }
- bool get _isComposingText {
- return _composingText != null && _composingText!.isNotEmpty;
- }
- bool get _shouldShowCursor {
- return _terminal.cursorVisibleMode || _alwaysShowCursor || _isComposingText;
- }
- double get _viewportHeight {
- return size.height - _padding.vertical;
- }
- double get _maxScrollExtent {
- return max(_terminalHeight - _viewportHeight, 0.0);
- }
- double get _lineOffset {
- return -_scrollOffset + _padding.top;
- }
- Offset get _cursorOffset {
- return Offset(
- _terminal.buffer.cursorX * _charSize.width,
- _terminal.buffer.absoluteCursorY * _charSize.height + _lineOffset,
- );
- }
- /// The cached for cells in the terminal. Should be cleared when the same
- /// cell no longer produces the same visual output. For example, when
- /// [_textStyle] is changed, or when the system font changes.
- final _paragraphCache = ParagraphCache(10240);
- @override
- void paint(PaintingContext context, Offset offset) {
- _paint(context, offset);
- context.setWillChangeHint();
- }
- void _paint(PaintingContext context, Offset offset) {
- final canvas = context.canvas;
- final lines = _terminal.buffer.lines;
- final charHeight = _charSize.height;
- final firstLineOffset = _scrollOffset - _padding.top;
- final lastLineOffset = _scrollOffset + size.height + _padding.bottom;
- final firstLine = firstLineOffset ~/ charHeight;
- final lastLine = lastLineOffset ~/ charHeight;
- final effectFirstLine = firstLine.clamp(0, lines.length - 1);
- final effectLastLine = lastLine.clamp(0, lines.length - 1);
- for (var i = effectFirstLine; i <= effectLastLine; i++) {
- _paintLine(
- canvas,
- lines[i],
- offset.translate(0, (i * charHeight + _lineOffset).truncateToDouble()),
- );
- }
- if (_terminal.buffer.absoluteCursorY >= effectFirstLine &&
- _terminal.buffer.absoluteCursorY <= effectLastLine) {
- final cursorOffset = offset + _cursorOffset;
- if (_isComposingText) {
- _paintComposingText(canvas, cursorOffset);
- }
- if (_shouldShowCursor) {
- _paintCursor(canvas, cursorOffset);
- }
- }
- if (_controller.selection != null) {
- _paintSelection(
- canvas,
- _controller.selection!,
- effectFirstLine,
- effectLastLine,
- );
- }
- }
- /// Paints the cursor based on the current cursor type.
- void _paintCursor(Canvas canvas, Offset offset) {
- final paint = Paint()
- ..color = _theme.cursor
- ..strokeWidth = 1;
- if (!_focusNode.hasFocus) {
- paint.style = PaintingStyle.stroke;
- canvas.drawRect(offset & _charSize, paint);
- return;
- }
- switch (_cursorType) {
- case TerminalCursorType.block:
- paint.style = PaintingStyle.fill;
- canvas.drawRect(offset & _charSize, paint);
- return;
- case TerminalCursorType.underline:
- return canvas.drawLine(
- Offset(offset.dx, _charSize.height - 1),
- Offset(offset.dx + _charSize.width, _charSize.height - 1),
- paint,
- );
- case TerminalCursorType.verticalBar:
- return canvas.drawLine(
- Offset(offset.dx, 0),
- Offset(offset.dx, _charSize.height),
- paint,
- );
- }
- }
- /// Paints the text that is currently being composed in IME to [canvas] at
- /// [offset]. [offset] is usually the cursor position.
- void _paintComposingText(Canvas canvas, Offset offset) {
- final composingText = _composingText;
- if (composingText == null) {
- return;
- }
- final style = _textStyle.toTextStyle(
- color: _resolveForegroundColor(_terminal.cursor.foreground),
- backgroundColor: _theme.background,
- underline: true,
- );
- final builder = ParagraphBuilder(style.getParagraphStyle());
- builder.addPlaceholder(
- offset.dx,
- _charSize.height,
- PlaceholderAlignment.middle,
- );
- builder.pushStyle(style.getTextStyle(textScaleFactor: _textScaleFactor));
- builder.addText(composingText);
- final paragraph = builder.build();
- paragraph.layout(ParagraphConstraints(width: size.width));
- canvas.drawParagraph(paragraph, Offset(0, offset.dy));
- }
- /// Paints [line] to [canvas] at [offset]. The x offset of [offset] is usually
- /// 0, and the y offset is the top of the line.
- void _paintLine(Canvas canvas, BufferLine line, Offset offset) {
- final cellData = CellData.empty();
- final cellWidth = _charSize.width;
- final visibleCells = size.width ~/ cellWidth + 1;
- final effectCells = min(visibleCells, line.length);
- for (var i = 0; i < effectCells; i++) {
- line.getCellData(i, cellData);
- final charWidth = cellData.content >> CellContent.widthShift;
- final cellOffset = offset.translate(i * cellWidth, 0);
- _paintCellBackground(canvas, cellOffset, cellData);
- _paintCellForeground(canvas, cellOffset, cellData);
- if (charWidth == 2) {
- i++;
- }
- }
- }
- void _paintSelection(
- Canvas canvas,
- BufferRange selection,
- int firstLine,
- int lastLine,
- ) {
- for (final segment in selection.toSegments()) {
- if (segment.line >= _terminal.buffer.lines.length) {
- break;
- }
- if (segment.line < firstLine) {
- continue;
- }
- if (segment.line > lastLine) {
- break;
- }
- final start = segment.start ?? 0;
- final end = segment.end ?? _terminal.viewWidth;
- final startOffset = Offset(
- start * _charSize.width,
- segment.line * _charSize.height + _lineOffset,
- );
- final endOffset = Offset(
- end * _charSize.width,
- (segment.line + 1) * _charSize.height + _lineOffset,
- );
- final paint = Paint()
- ..color = _theme.selection
- ..strokeWidth = 1;
- canvas.drawRect(
- Rect.fromPoints(startOffset, endOffset),
- paint,
- );
- }
- }
- /// 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() ^ _textScaleFactor.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,
- );
- // Flutter does not draw an underline below a space which is not between
- // other regular characters. As only single characters are drawn, this
- // will never produce an underline below a space in the terminal. As a
- // workaround the regular space CodePoint 0x20 is replaced with
- // the CodePoint 0xA0. This is a non breaking space and a underline can be
- // drawn below it.
- var char = String.fromCharCode(charCode);
- if (cellFlags & CellFlags.underline != 0 && charCode == 0x20) {
- char = String.fromCharCode(0xA0);
- }
- paragraph = _paragraphCache.performAndCacheLayout(
- char,
- style,
- _textScaleFactor,
- cacheKey,
- );
- }
- canvas.drawParagraph(paragraph, offset);
- }
- /// Paints the background of a cell represented by [cellData] to [canvas] at
- /// [offset].
- @pragma('vm:prefer-inline')
- void _paintCellBackground(Canvas canvas, Offset offset, CellData cellData) {
- late Color color;
- final colorType = cellData.background & CellColor.typeMask;
- if (cellData.flags & CellFlags.inverse != 0) {
- color = _resolveForegroundColor(cellData.foreground);
- } else if (colorType == CellColor.normal) {
- return;
- } else {
- color = _resolveBackgroundColor(cellData.background);
- }
- final paint = Paint()..color = color;
- final doubleWidth = cellData.content >> CellContent.widthShift == 2;
- final widthScale = doubleWidth ? 2 : 1;
- final size = Size(_charSize.width * widthScale + 1, _charSize.height);
- canvas.drawRect(offset & size, paint);
- }
- /// Get the effective foreground color for a cell from information encoded in
- /// [cellColor].
- @pragma('vm:prefer-inline')
- Color _resolveForegroundColor(int cellColor) {
- final colorType = cellColor & CellColor.typeMask;
- final colorValue = cellColor & CellColor.valueMask;
- switch (colorType) {
- case CellColor.normal:
- return _theme.foreground;
- case CellColor.named:
- case CellColor.palette:
- return _colorPalette[colorValue];
- case CellColor.rgb:
- default:
- return Color(colorValue | 0xFF000000);
- }
- }
- /// Get the effective background color for a cell from information encoded in
- /// [cellColor].
- @pragma('vm:prefer-inline')
- Color _resolveBackgroundColor(int cellColor) {
- final colorType = cellColor & CellColor.typeMask;
- final colorValue = cellColor & CellColor.valueMask;
- switch (colorType) {
- case CellColor.normal:
- return _theme.background;
- case CellColor.named:
- case CellColor.palette:
- return _colorPalette[colorValue];
- case CellColor.rgb:
- default:
- return Color(colorValue | 0xFF000000);
- }
- }
- }
|