| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568 |
- import 'dart:math' show max;
- import 'dart:ui';
- import 'package:flutter/rendering.dart';
- import 'package:flutter/widgets.dart';
- import 'package:xterm/xterm.dart';
- import 'package:xterm/src/ui/painter.dart';
- import 'package:xterm/src/ui/terminal_size.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 TextScaler textScaler,
- 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,
- _focusNode = focusNode,
- _cursorType = cursorType,
- _alwaysShowCursor = alwaysShowCursor,
- _onEditableRect = onEditableRect,
- _composingText = composingText,
- _painter = TerminalPainter(
- theme: theme,
- textStyle: textStyle,
- textScaler: textScaler,
- );
- 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();
- }
- set textStyle(TerminalStyle value) {
- if (value == _painter.textStyle) return;
- _painter.textStyle = value;
- markNeedsLayout();
- }
- set textScaler(TextScaler value) {
- if (value == _painter.textScaler) return;
- _painter.textScaler = value;
- markNeedsLayout();
- }
- set theme(TerminalTheme value) {
- if (value == _painter.theme) return;
- _painter.theme = value;
- 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();
- }
- TerminalSize? _viewportSize;
- final TerminalPainter _painter;
- var _stickToBottom = true;
- void _onScroll() {
- _stickToBottom = _scrollOffset >= _maxScrollExtent;
- markNeedsLayout();
- _notifyEditableRect();
- }
- void _onFocusChange() {
- markNeedsPaint();
- }
- void _onTerminalChange() {
- markNeedsLayout();
- _notifyEditableRect();
- }
- 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() {
- _painter.clearFontCache();
- super.systemFontsDidChange();
- }
- @override
- void performLayout() {
- size = constraints.biggest;
- _updateViewportSize();
- _updateScrollOffset();
- if (_stickToBottom) {
- _offset.correctBy(_maxScrollExtent - _scrollOffset);
- }
- }
- /// Total height of the terminal in pixels. Includes scrollback buffer.
- double get _terminalHeight =>
- _terminal.buffer.lines.length * _painter.cellSize.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 ~/ _painter.cellSize.height * _painter.cellSize.height;
- return _offset.pixels;
- }
- /// The height of a terminal line in pixels. This includes the line spacing.
- /// Height of the entire terminal is expected to be a multiple of this value.
- double get lineHeight => _painter.cellSize.height;
- /// 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 * _painter.cellSize.width;
- final y = row * _painter.cellSize.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 ~/ _painter.cellSize.height;
- final col = x ~/ _painter.cellSize.width;
- return CellOffset(
- col.clamp(0, _terminal.viewWidth - 1),
- row.clamp(0, _terminal.buffer.lines.length - 1),
- );
- }
- /// Selects entire words in the terminal that contains [from] and [to].
- void selectWord(Offset from, [Offset? to]) {
- final fromOffset = getCellOffset(from);
- final toOffset = to != null ? getCellOffset(to) : null;
- selectWordByCell(fromOffset, toOffset);
- }
- void selectWordByCell(CellOffset from, [CellOffset? to]) {
- final fromBoundary = _terminal.buffer.getWordBoundary(from);
- if (fromBoundary == null) return;
- if (to == null) {
- _controller.setSelection(
- _terminal.buffer.createAnchorFromOffset(fromBoundary.begin),
- _terminal.buffer.createAnchorFromOffset(fromBoundary.end),
- mode: SelectionMode.line,
- );
- } else {
- final toBoundary = _terminal.buffer.getWordBoundary(to);
- if (toBoundary == null) return;
- final range = fromBoundary.merge(toBoundary);
- _controller.setSelection(
- _terminal.buffer.createAnchorFromOffset(range.begin),
- _terminal.buffer.createAnchorFromOffset(range.end),
- mode: SelectionMode.line,
- );
- }
- }
- /// 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);
- final toPosition = to != null ? getCellOffset(to) : null;
- selectCharactersByCell(fromPosition, toPosition);
- }
- void selectCharactersByCell(CellOffset from, [CellOffset? to]) {
- if (to == null) {
- _controller.setSelection(
- _terminal.buffer.createAnchorFromOffset(from),
- _terminal.buffer.createAnchorFromOffset(from),
- );
- } else {
- var toPosition = to;
- if (toPosition.x >= from.x) {
- toPosition = CellOffset(toPosition.x + 1, toPosition.y);
- }
- _controller.setSelection(
- _terminal.buffer.createAnchorFromOffset(from),
- _terminal.buffer.createAnchorFromOffset(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 + _painter.cellSize.height,
- );
- final caretRect = cursor & _painter.cellSize;
- _onEditableRect?.call(rect, caretRect);
- }
- /// Update the viewport size in cells based on the current widget size in
- /// pixels.
- void _updateViewportSize() {
- if (size <= _painter.cellSize) {
- return;
- }
- final viewportSize = TerminalSize(
- size.width ~/ _painter.cellSize.width,
- _viewportHeight ~/ _painter.cellSize.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,
- _painter.cellSize.width.round(),
- _painter.cellSize.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;
- }
- /// The offset of the cursor from the top left corner of this render object.
- Offset get cursorOffset {
- return Offset(
- _terminal.buffer.cursorX * _painter.cellSize.width,
- _terminal.buffer.absoluteCursorY * _painter.cellSize.height + _lineOffset,
- );
- }
- Size get cellSize {
- return _painter.cellSize;
- }
- @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 = _painter.cellSize.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++) {
- _painter.paintLine(
- canvas,
- offset.translate(0, (i * charHeight + _lineOffset).truncateToDouble()),
- lines[i],
- );
- }
- if (_terminal.buffer.absoluteCursorY >= effectFirstLine &&
- _terminal.buffer.absoluteCursorY <= effectLastLine) {
- if (_isComposingText) {
- _paintComposingText(canvas, offset + cursorOffset);
- }
- if (_shouldShowCursor) {
- _painter.paintCursor(
- canvas,
- offset + cursorOffset,
- cursorType: _cursorType,
- hasFocus: _focusNode.hasFocus,
- );
- }
- }
- _paintHighlights(
- canvas,
- _controller.highlights,
- effectFirstLine,
- effectLastLine,
- );
- if (_controller.selection != null) {
- _paintSelection(
- canvas,
- _controller.selection!,
- effectFirstLine,
- effectLastLine,
- );
- }
- }
- /// 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 = _painter.textStyle.toTextStyle(
- color: _painter.resolveForegroundColor(_terminal.cursor.foreground),
- backgroundColor: _painter.theme.background,
- underline: true,
- );
- final builder = ParagraphBuilder(style.getParagraphStyle());
- builder.addPlaceholder(
- offset.dx,
- _painter.cellSize.height,
- PlaceholderAlignment.middle,
- );
- builder.pushStyle(
- style.getTextStyle(textScaler: _painter.textScaler),
- );
- builder.addText(composingText);
- final paragraph = builder.build();
- paragraph.layout(ParagraphConstraints(width: size.width));
- canvas.drawParagraph(paragraph, Offset(0, offset.dy));
- }
- 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;
- }
- _paintSegment(
- canvas,
- segment,
- _painter.theme.selection,
- _painter.theme.selectionForeground,
- );
- }
- }
- void _paintHighlights(
- Canvas canvas,
- List<TerminalHighlight> highlights,
- int firstLine,
- int lastLine,
- ) {
- for (var highlight in _controller.highlights) {
- final range = highlight.range?.normalized;
- if (range == null ||
- range.begin.y > lastLine ||
- range.end.y < firstLine) {
- continue;
- }
- for (var segment in range.toSegments()) {
- if (segment.line < firstLine) {
- continue;
- }
- if (segment.line > lastLine) {
- break;
- }
- _paintSegment(canvas, segment, highlight.color);
- }
- }
- }
- @pragma('vm:prefer-inline')
- void _paintSegment(Canvas canvas, BufferSegment segment, Color color, [Color? textColor]) {
- final start = segment.start ?? 0;
- final end = segment.end ?? _terminal.viewWidth;
- final startOffset = Offset(
- start * _painter.cellSize.width,
- segment.line * _painter.cellSize.height + _lineOffset,
- );
- _painter.paintHighlight(canvas, startOffset, end - start, color);
-
- if (textColor != null) {
- final line = _terminal.buffer.lines[segment.line];
- final cellWidth = _painter.cellSize.width;
- final cellData = CellData.empty();
- for (var i = start; i < end; i++) {
- line.getCellData(i, cellData);
- final cellOffset = startOffset.translate((i - start) * cellWidth, 0);
- _painter.paintCellForeground(canvas, cellOffset, cellData, textColor);
- }
- }
- }
- }
|