render.dart 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618
  1. import 'dart:math' show min, max;
  2. import 'dart:ui';
  3. import 'package:flutter/cupertino.dart';
  4. import 'package:flutter/material.dart';
  5. import 'package:flutter/rendering.dart';
  6. import 'package:flutter/scheduler.dart';
  7. import 'package:xterm/core/buffer/cell_flags.dart';
  8. import 'package:xterm/core/buffer/position.dart';
  9. import 'package:xterm/core/buffer/range.dart';
  10. import 'package:xterm/core/cell.dart';
  11. import 'package:xterm/core/buffer/line.dart';
  12. import 'package:xterm/core/terminal.dart';
  13. import 'package:xterm/ui/controller.dart';
  14. import 'package:xterm/ui/cursor_type.dart';
  15. import 'package:xterm/ui/palette_builder.dart';
  16. import 'package:xterm/ui/paragraph_cache.dart';
  17. import 'package:xterm/ui/terminal_size.dart';
  18. import 'package:xterm/ui/terminal_text_style.dart';
  19. import 'package:xterm/ui/terminal_theme.dart';
  20. typedef EditableRectCallback = void Function(Rect rect, Rect caretRect);
  21. class RenderTerminal extends RenderBox {
  22. RenderTerminal({
  23. required Terminal terminal,
  24. required TerminalController controller,
  25. required ViewportOffset offset,
  26. required EdgeInsets padding,
  27. required bool autoResize,
  28. required Size charMetrics,
  29. required TerminalStyle textStyle,
  30. required TerminalTheme theme,
  31. required FocusNode focusNode,
  32. required TerminalCursorType cursorType,
  33. required bool alwaysShowCursor,
  34. EditableRectCallback? onEditableRect,
  35. String? composingText,
  36. }) : _terminal = terminal,
  37. _controller = controller,
  38. _offset = offset,
  39. _padding = padding,
  40. _autoResize = autoResize,
  41. _charMetrics = charMetrics,
  42. _textStyle = textStyle,
  43. _theme = theme,
  44. _focusNode = focusNode,
  45. _cursorType = cursorType,
  46. _alwaysShowCursor = alwaysShowCursor,
  47. _onEditableRect = onEditableRect,
  48. _composingText = composingText {
  49. _updateColorPalette();
  50. }
  51. Terminal _terminal;
  52. set terminal(Terminal terminal) {
  53. if (_terminal == terminal) return;
  54. if (attached) _terminal.removeListener(_onTerminalChange);
  55. _terminal = terminal;
  56. if (attached) _terminal.addListener(_onTerminalChange);
  57. _resizeTerminalIfNeeded();
  58. markNeedsLayout();
  59. }
  60. TerminalController _controller;
  61. set controller(TerminalController controller) {
  62. if (_controller == controller) return;
  63. if (attached) _controller.removeListener(_onControllerUpdate);
  64. _controller = controller;
  65. if (attached) _controller.addListener(_onControllerUpdate);
  66. markNeedsLayout();
  67. }
  68. ViewportOffset _offset;
  69. set offset(ViewportOffset value) {
  70. if (value == _offset) return;
  71. if (attached) _offset.removeListener(_hasScrolled);
  72. _offset = value;
  73. if (attached) _offset.addListener(_hasScrolled);
  74. markNeedsLayout();
  75. }
  76. EdgeInsets _padding;
  77. set padding(EdgeInsets value) {
  78. if (value == _padding) return;
  79. _padding = value;
  80. markNeedsLayout();
  81. }
  82. bool _autoResize;
  83. set autoResize(bool value) {
  84. if (value == _autoResize) return;
  85. _autoResize = value;
  86. markNeedsLayout();
  87. }
  88. Size _charMetrics;
  89. set charMetrics(Size value) {
  90. if (value == _charMetrics) return;
  91. _charMetrics = value;
  92. markNeedsLayout();
  93. }
  94. TerminalStyle _textStyle;
  95. set textStyle(TerminalStyle value) {
  96. if (value == _textStyle) return;
  97. _textStyle = value;
  98. markNeedsLayout();
  99. }
  100. TerminalTheme _theme;
  101. set theme(TerminalTheme value) {
  102. if (value == _theme) return;
  103. _theme = value;
  104. _updateColorPalette();
  105. markNeedsPaint();
  106. }
  107. FocusNode _focusNode;
  108. set focusNode(FocusNode value) {
  109. if (value == _focusNode) return;
  110. if (attached) _focusNode.removeListener(_onFocusChange);
  111. _focusNode = value;
  112. if (attached) _focusNode.addListener(_onFocusChange);
  113. markNeedsPaint();
  114. }
  115. TerminalCursorType _cursorType;
  116. set cursorType(TerminalCursorType value) {
  117. if (value == _cursorType) return;
  118. _cursorType = value;
  119. markNeedsPaint();
  120. }
  121. bool _alwaysShowCursor;
  122. set alwaysShowCursor(bool value) {
  123. if (value == _alwaysShowCursor) return;
  124. _alwaysShowCursor = value;
  125. markNeedsPaint();
  126. }
  127. EditableRectCallback? _onEditableRect;
  128. set onEditableRect(EditableRectCallback? value) {
  129. if (value == _onEditableRect) return;
  130. _onEditableRect = value;
  131. markNeedsLayout();
  132. }
  133. String? _composingText;
  134. set composingText(String? value) {
  135. if (value == _composingText) return;
  136. _composingText = value;
  137. markNeedsPaint();
  138. }
  139. final _paragraphCache = ParagraphCache(10240);
  140. late List<Color> _colorPalette;
  141. TerminalSize? _viewportSize;
  142. void _updateColorPalette() {
  143. _colorPalette = PaletteBuilder(_theme).build();
  144. }
  145. var _stickToBottom = true;
  146. void _hasScrolled() {
  147. _stickToBottom = _offset.pixels >= _maxScrollExtent;
  148. markNeedsLayout();
  149. }
  150. void _onFocusChange() {
  151. markNeedsPaint();
  152. }
  153. void _onTerminalChange() {
  154. markNeedsLayout();
  155. }
  156. void _onControllerUpdate() {
  157. markNeedsLayout();
  158. }
  159. @override
  160. final isRepaintBoundary = true;
  161. @override
  162. void attach(PipelineOwner owner) {
  163. super.attach(owner);
  164. _offset.addListener(_hasScrolled);
  165. _terminal.addListener(_onTerminalChange);
  166. _controller.addListener(_onControllerUpdate);
  167. _focusNode.addListener(_onFocusChange);
  168. }
  169. @override
  170. void detach() {
  171. super.detach();
  172. _offset.removeListener(_hasScrolled);
  173. _terminal.removeListener(_onTerminalChange);
  174. _controller.removeListener(_onControllerUpdate);
  175. _focusNode.removeListener(_onFocusChange);
  176. }
  177. @override
  178. bool hitTestSelf(Offset position) {
  179. return true;
  180. }
  181. @override
  182. void performLayout() {
  183. size = constraints.biggest;
  184. _updateViewportSize();
  185. _updateScrollOffset();
  186. if (_stickToBottom) {
  187. _offset.correctBy(_maxScrollExtent - _offset.pixels);
  188. }
  189. SchedulerBinding.instance
  190. .addPostFrameCallback((_) => _notifyEditableRect());
  191. }
  192. double get lineHeight => _charMetrics.height;
  193. double get terminalHeight =>
  194. _terminal.buffer.lines.length * _charMetrics.height;
  195. double get scrollOffset => _offset.pixels;
  196. BufferPosition positionFromOffset(Offset offset) {
  197. final x = offset.dx - _padding.left;
  198. final y = offset.dy - _padding.top + _offset.pixels;
  199. final row = y ~/ _charMetrics.height;
  200. final col = x ~/ _charMetrics.width;
  201. return BufferPosition(col, row);
  202. }
  203. Offset offsetFromPosition(BufferPosition position) {
  204. final row = position.y;
  205. final col = position.x;
  206. final x = col * _charMetrics.width;
  207. final y = row * _charMetrics.height;
  208. return Offset(x + _padding.left, y + _padding.top - _offset.pixels);
  209. }
  210. void selectWord(Offset from, [Offset? to]) {
  211. final fromOffset = positionFromOffset(globalToLocal(from));
  212. final fromBoundary = _terminal.buffer.getWordBoundary(fromOffset);
  213. if (fromBoundary == null) return;
  214. if (to == null) {
  215. _controller.setSelection(fromBoundary);
  216. } else {
  217. final toOffset = positionFromOffset(globalToLocal(to));
  218. final toBoundary = _terminal.buffer.getWordBoundary(toOffset);
  219. if (toBoundary == null) return;
  220. _controller.setSelection(fromBoundary.merge(toBoundary));
  221. }
  222. }
  223. void selectPosition(Offset from, [Offset? to]) {
  224. final fromPosition = positionFromOffset(globalToLocal(from));
  225. if (to == null) {
  226. _controller.setSelection(BufferRange.collapsed(fromPosition));
  227. } else {
  228. final toPosition = positionFromOffset(globalToLocal(to));
  229. _controller.setSelection(BufferRange(fromPosition, toPosition));
  230. }
  231. }
  232. void _notifyEditableRect() {
  233. final cursor = localToGlobal(_cursorOffset);
  234. final rect = Rect.fromLTRB(
  235. cursor.dx,
  236. cursor.dy,
  237. size.width,
  238. cursor.dy + _charMetrics.height,
  239. );
  240. final caretRect = cursor & _charMetrics;
  241. _onEditableRect?.call(rect, caretRect);
  242. }
  243. void _updateViewportSize() {
  244. if (size <= _charMetrics) {
  245. return;
  246. }
  247. final viewportSize = TerminalSize(
  248. size.width ~/ _charMetrics.width,
  249. _viewportHeight ~/ _charMetrics.height,
  250. );
  251. if (_viewportSize != viewportSize) {
  252. _viewportSize = viewportSize;
  253. _resizeTerminalIfNeeded();
  254. }
  255. }
  256. void _resizeTerminalIfNeeded() {
  257. if (_autoResize && _viewportSize != null) {
  258. _terminal.resize(
  259. _viewportSize!.width,
  260. _viewportSize!.height,
  261. _charMetrics.width.round(),
  262. _charMetrics.height.round(),
  263. );
  264. }
  265. }
  266. bool get _isComposingText {
  267. return _composingText != null && _composingText!.isNotEmpty;
  268. }
  269. bool get _shouldShowCursor {
  270. return _terminal.cursorVisibleMode || _alwaysShowCursor || _isComposingText;
  271. }
  272. double get _viewportHeight {
  273. return size.height - _padding.vertical;
  274. }
  275. double get _maxScrollExtent {
  276. return max(terminalHeight - _viewportHeight, 0.0);
  277. }
  278. double get _lineOffset {
  279. return -_offset.pixels + _padding.top;
  280. }
  281. Offset get _cursorOffset {
  282. return Offset(
  283. _terminal.buffer.cursorX * _charMetrics.width,
  284. _terminal.buffer.absoluteCursorY * _charMetrics.height + _lineOffset,
  285. );
  286. }
  287. void _updateScrollOffset() {
  288. _offset.applyViewportDimension(_viewportHeight);
  289. _offset.applyContentDimensions(0, _maxScrollExtent);
  290. }
  291. @override
  292. void paint(PaintingContext context, Offset offset) {
  293. _paint(context, offset);
  294. context.setWillChangeHint();
  295. }
  296. void _paint(PaintingContext context, Offset offset) {
  297. final canvas = context.canvas;
  298. final lines = _terminal.buffer.lines;
  299. final charHeight = _charMetrics.height;
  300. final firstLineOffset = _offset.pixels - _padding.top;
  301. final lastLineOffset = _offset.pixels + size.height + _padding.bottom;
  302. final firstLine = firstLineOffset ~/ charHeight;
  303. final lastLine = lastLineOffset ~/ charHeight;
  304. final effectFirstLine = firstLine.clamp(0, lines.length - 1);
  305. final effectLastLine = lastLine.clamp(0, lines.length - 1);
  306. for (var i = effectFirstLine; i <= effectLastLine; i++) {
  307. _paintLine(
  308. canvas,
  309. lines[i],
  310. offset.translate(0, (i * charHeight + _lineOffset).truncateToDouble()),
  311. );
  312. }
  313. if (_terminal.buffer.absoluteCursorY >= effectFirstLine &&
  314. _terminal.buffer.absoluteCursorY <= effectLastLine) {
  315. final cursorOffset = offset + _cursorOffset;
  316. if (_isComposingText) {
  317. _paintComposingText(canvas, cursorOffset);
  318. }
  319. if (_shouldShowCursor) {
  320. _paintCursor(canvas, cursorOffset);
  321. }
  322. }
  323. if (_controller.selection != null) {
  324. _paintSelection(
  325. canvas,
  326. _controller.selection!,
  327. effectFirstLine,
  328. effectLastLine,
  329. );
  330. }
  331. }
  332. void _paintCursor(Canvas canvas, Offset offset) {
  333. final paint = Paint()
  334. ..color = _theme.cursor
  335. ..strokeWidth = 1;
  336. if (!_focusNode.hasFocus) {
  337. paint.style = PaintingStyle.stroke;
  338. canvas.drawRect(offset & _charMetrics, paint);
  339. return;
  340. }
  341. switch (_cursorType) {
  342. case TerminalCursorType.block:
  343. paint.style = PaintingStyle.fill;
  344. canvas.drawRect(offset & _charMetrics, paint);
  345. return;
  346. case TerminalCursorType.underline:
  347. return canvas.drawLine(
  348. Offset(offset.dx, _charMetrics.height - 1),
  349. Offset(offset.dx + _charMetrics.width, _charMetrics.height - 1),
  350. paint,
  351. );
  352. case TerminalCursorType.verticalBar:
  353. return canvas.drawLine(
  354. Offset(offset.dx, 0),
  355. Offset(offset.dx, _charMetrics.height),
  356. paint,
  357. );
  358. }
  359. }
  360. void _paintComposingText(Canvas canvas, Offset offset) {
  361. final composingText = _composingText;
  362. if (composingText == null) {
  363. return;
  364. }
  365. final style = _textStyle.toTextStyle(
  366. color: _resolveForegroundColor(_terminal.cursor.foreground),
  367. backgroundColor: _theme.background,
  368. underline: true,
  369. );
  370. final builder = ParagraphBuilder(style.getParagraphStyle());
  371. builder.addPlaceholder(
  372. offset.dx,
  373. _charMetrics.height,
  374. PlaceholderAlignment.middle,
  375. );
  376. builder.pushStyle(style.getTextStyle());
  377. builder.addText(composingText);
  378. final paragraph = builder.build();
  379. paragraph.layout(ParagraphConstraints(width: size.width));
  380. canvas.drawParagraph(paragraph, Offset(0, offset.dy));
  381. }
  382. void _paintLine(Canvas canvas, BufferLine line, Offset offset) {
  383. final cellData = CellData.empty();
  384. final cellWidth = _charMetrics.width;
  385. final visibleCells = size.width ~/ cellWidth + 1;
  386. final effectCells = min(visibleCells, line.length);
  387. for (var i = 0; i < effectCells; i++) {
  388. line.getCellData(i, cellData);
  389. final charWidth = cellData.content >> CellContent.widthShift;
  390. final cellOffset = offset.translate(i * cellWidth, 0);
  391. _paintCellBackground(canvas, cellOffset, cellData);
  392. _paintCellForeground(canvas, cellOffset, line, cellData);
  393. if (charWidth == 2) {
  394. i++;
  395. }
  396. }
  397. }
  398. void _paintSelection(
  399. Canvas canvas,
  400. BufferRange selection,
  401. int firstLine,
  402. int lastLine,
  403. ) {
  404. for (final segment in selection.toSegments()) {
  405. if (segment.line >= _terminal.buffer.lines.length) {
  406. break;
  407. }
  408. if (segment.line < firstLine) {
  409. continue;
  410. }
  411. if (segment.line > lastLine) {
  412. break;
  413. }
  414. final start = segment.start ?? 0;
  415. final end = segment.end ?? _terminal.viewWidth;
  416. final startOffset = Offset(
  417. start * _charMetrics.width,
  418. segment.line * _charMetrics.height + _lineOffset,
  419. );
  420. final endOffset = Offset(
  421. end * _charMetrics.width,
  422. (segment.line + 1) * _charMetrics.height + _lineOffset,
  423. );
  424. final paint = Paint()
  425. ..color = _theme.cursor
  426. ..strokeWidth = 1;
  427. canvas.drawRect(
  428. Rect.fromPoints(startOffset, endOffset),
  429. paint,
  430. );
  431. }
  432. }
  433. @pragma('vm:prefer-inline')
  434. void _paintCellForeground(
  435. Canvas canvas,
  436. Offset offset,
  437. BufferLine line,
  438. CellData cellData,
  439. ) {
  440. final charCode = cellData.content & CellContent.codepointMask;
  441. if (charCode == 0) return;
  442. final hash = cellData.getHash();
  443. // final hash = cellData.getHash() + line.hashCode;
  444. var paragraph = _paragraphCache.getLayoutFromCache(hash);
  445. if (paragraph == null) {
  446. final cellFlags = cellData.flags;
  447. var color = cellFlags & CellFlags.inverse == 0
  448. ? _resolveForegroundColor(cellData.foreground)
  449. : _resolveBackgroundColor(cellData.background);
  450. if (cellData.flags & CellFlags.faint != 0) {
  451. color = color.withOpacity(0.5);
  452. }
  453. final style = _textStyle.toTextStyle(
  454. color: color,
  455. bold: cellFlags & CellFlags.bold != 0,
  456. italic: cellFlags & CellFlags.italic != 0,
  457. underline: cellFlags & CellFlags.underline != 0,
  458. );
  459. paragraph = _paragraphCache.performAndCacheLayout(
  460. String.fromCharCode(charCode),
  461. style,
  462. hash,
  463. );
  464. }
  465. canvas.drawParagraph(paragraph, offset);
  466. }
  467. @pragma('vm:prefer-inline')
  468. void _paintCellBackground(Canvas canvas, Offset offset, CellData cellData) {
  469. late Color color;
  470. final colorType = cellData.background & CellColor.typeMask;
  471. if (cellData.flags & CellFlags.inverse != 0) {
  472. color = _resolveForegroundColor(cellData.foreground);
  473. } else if (colorType == CellColor.normal) {
  474. return;
  475. } else {
  476. color = _resolveBackgroundColor(cellData.background);
  477. }
  478. final paint = Paint()..color = color;
  479. final doubleWidth = cellData.content >> CellContent.widthShift == 2;
  480. final widthScale = doubleWidth ? 2 : 1;
  481. final size = Size(_charMetrics.width * widthScale + 1, _charMetrics.height);
  482. canvas.drawRect(offset & size, paint);
  483. }
  484. @pragma('vm:prefer-inline')
  485. Color _resolveForegroundColor(int cellColor) {
  486. final colorType = cellColor & CellColor.typeMask;
  487. final colorValue = cellColor & CellColor.valueMask;
  488. switch (colorType) {
  489. case CellColor.normal:
  490. return _theme.foreground;
  491. case CellColor.named:
  492. case CellColor.palette:
  493. return _colorPalette[colorValue];
  494. case CellColor.rgb:
  495. default:
  496. return Color(colorValue | 0xFF000000);
  497. }
  498. }
  499. @pragma('vm:prefer-inline')
  500. Color _resolveBackgroundColor(int cellColor) {
  501. final colorType = cellColor & CellColor.typeMask;
  502. final colorValue = cellColor & CellColor.valueMask;
  503. switch (colorType) {
  504. case CellColor.normal:
  505. return _theme.background;
  506. case CellColor.named:
  507. case CellColor.palette:
  508. return _colorPalette[colorValue];
  509. case CellColor.rgb:
  510. default:
  511. return Color(colorValue | 0xFF000000);
  512. }
  513. }
  514. }