painter.dart 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. import 'dart:ui';
  2. import 'package:flutter/painting.dart';
  3. import 'package:xterm/src/ui/palette_builder.dart';
  4. import 'package:xterm/src/ui/paragraph_cache.dart';
  5. import 'package:xterm/xterm.dart';
  6. /// Encapsulates the logic for painting various terminal elements.
  7. class TerminalPainter {
  8. TerminalPainter({
  9. required TerminalTheme theme,
  10. required TerminalStyle textStyle,
  11. required TextScaler textScaler,
  12. }) : _textStyle = textStyle,
  13. _theme = theme,
  14. _textScaler = textScaler;
  15. /// A lookup table from terminal colors to Flutter colors.
  16. late var _colorPalette = PaletteBuilder(_theme).build();
  17. /// Size of each character in the terminal.
  18. late var _cellSize = _measureCharSize();
  19. /// The cached for cells in the terminal. Should be cleared when the same
  20. /// cell no longer produces the same visual output. For example, when
  21. /// [_textStyle] is changed, or when the system font changes.
  22. final _paragraphCache = ParagraphCache(10240);
  23. TerminalStyle get textStyle => _textStyle;
  24. TerminalStyle _textStyle;
  25. set textStyle(TerminalStyle value) {
  26. if (value == _textStyle) return;
  27. _textStyle = value;
  28. _cellSize = _measureCharSize();
  29. _paragraphCache.clear();
  30. }
  31. TextScaler get textScaler => _textScaler;
  32. TextScaler _textScaler = TextScaler.linear(1.0);
  33. set textScaler(TextScaler value) {
  34. if (value == _textScaler) return;
  35. _textScaler = value;
  36. _cellSize = _measureCharSize();
  37. _paragraphCache.clear();
  38. }
  39. TerminalTheme get theme => _theme;
  40. TerminalTheme _theme;
  41. set theme(TerminalTheme value) {
  42. if (value == _theme) return;
  43. _theme = value;
  44. _colorPalette = PaletteBuilder(value).build();
  45. _paragraphCache.clear();
  46. }
  47. Size _measureCharSize() {
  48. const test = 'mmmmmmmmmm';
  49. final textStyle = _textStyle.toTextStyle();
  50. final builder = ParagraphBuilder(textStyle.getParagraphStyle());
  51. builder.pushStyle(
  52. textStyle.getTextStyle(textScaler: _textScaler),
  53. );
  54. builder.addText(test);
  55. final paragraph = builder.build();
  56. paragraph.layout(ParagraphConstraints(width: double.infinity));
  57. final result = Size(
  58. paragraph.maxIntrinsicWidth / test.length,
  59. paragraph.height,
  60. );
  61. paragraph.dispose();
  62. return result;
  63. }
  64. /// The size of each character in the terminal.
  65. Size get cellSize => _cellSize;
  66. /// When the set of font available to the system changes, call this method to
  67. /// clear cached state related to font rendering.
  68. void clearFontCache() {
  69. _cellSize = _measureCharSize();
  70. _paragraphCache.clear();
  71. }
  72. /// Paints the cursor based on the current cursor type.
  73. void paintCursor(
  74. Canvas canvas,
  75. Offset offset, {
  76. required TerminalCursorType cursorType,
  77. bool hasFocus = true,
  78. }) {
  79. final paint = Paint()
  80. ..color = _theme.cursor
  81. ..strokeWidth = 1;
  82. if (!hasFocus) {
  83. paint.style = PaintingStyle.stroke;
  84. canvas.drawRect(offset & _cellSize, paint);
  85. return;
  86. }
  87. switch (cursorType) {
  88. case TerminalCursorType.block:
  89. paint.style = PaintingStyle.fill;
  90. canvas.drawRect(offset & _cellSize, paint);
  91. return;
  92. case TerminalCursorType.underline:
  93. return canvas.drawLine(
  94. Offset(offset.dx, _cellSize.height - 1),
  95. Offset(offset.dx + _cellSize.width, _cellSize.height - 1),
  96. paint,
  97. );
  98. case TerminalCursorType.verticalBar:
  99. return canvas.drawLine(
  100. Offset(offset.dx, 0),
  101. Offset(offset.dx, _cellSize.height),
  102. paint,
  103. );
  104. }
  105. }
  106. @pragma('vm:prefer-inline')
  107. void paintHighlight(Canvas canvas, Offset offset, int length, Color color) {
  108. final endOffset =
  109. offset.translate(length * _cellSize.width, _cellSize.height);
  110. final paint = Paint()
  111. ..color = color
  112. ..strokeWidth = 1;
  113. canvas.drawRect(
  114. Rect.fromPoints(offset, endOffset),
  115. paint,
  116. );
  117. }
  118. /// Paints [line] to [canvas] at [offset]. The x offset of [offset] is usually
  119. /// 0, and the y offset is the top of the line.
  120. void paintLine(
  121. Canvas canvas,
  122. Offset offset,
  123. BufferLine line,
  124. ) {
  125. final cellData = CellData.empty();
  126. final cellWidth = _cellSize.width;
  127. for (var i = 0; i < line.length; i++) {
  128. line.getCellData(i, cellData);
  129. final charWidth = cellData.content >> CellContent.widthShift;
  130. final cellOffset = offset.translate(i * cellWidth, 0);
  131. paintCell(canvas, cellOffset, cellData);
  132. if (charWidth == 2) {
  133. i++;
  134. }
  135. }
  136. }
  137. @pragma('vm:prefer-inline')
  138. void paintCell(Canvas canvas, Offset offset, CellData cellData) {
  139. paintCellBackground(canvas, offset, cellData);
  140. paintCellForeground(canvas, offset, cellData);
  141. }
  142. /// Paints the character in the cell represented by [cellData] to [canvas] at
  143. /// [offset].
  144. @pragma('vm:prefer-inline')
  145. void paintCellForeground(Canvas canvas, Offset offset, CellData cellData) {
  146. final charCode = cellData.content & CellContent.codepointMask;
  147. if (charCode == 0) return;
  148. final cacheKey = cellData.getHash() ^ _textScaler.hashCode;
  149. var paragraph = _paragraphCache.getLayoutFromCache(cacheKey);
  150. if (paragraph == null) {
  151. final cellFlags = cellData.flags;
  152. var color = cellFlags & CellFlags.inverse == 0
  153. ? resolveForegroundColor(cellData.foreground)
  154. : resolveBackgroundColor(cellData.background);
  155. if (cellData.flags & CellFlags.faint != 0) {
  156. color = color.withOpacity(0.5);
  157. }
  158. final style = _textStyle.toTextStyle(
  159. color: color,
  160. bold: cellFlags & CellFlags.bold != 0,
  161. italic: cellFlags & CellFlags.italic != 0,
  162. underline: cellFlags & CellFlags.underline != 0,
  163. );
  164. // Flutter does not draw an underline below a space which is not between
  165. // other regular characters. As only single characters are drawn, this
  166. // will never produce an underline below a space in the terminal. As a
  167. // workaround the regular space CodePoint 0x20 is replaced with
  168. // the CodePoint 0xA0. This is a non breaking space and a underline can be
  169. // drawn below it.
  170. var char = String.fromCharCode(charCode);
  171. if (cellFlags & CellFlags.underline != 0 && charCode == 0x20) {
  172. char = String.fromCharCode(0xA0);
  173. }
  174. paragraph = _paragraphCache.performAndCacheLayout(
  175. char,
  176. style,
  177. _textScaler,
  178. cacheKey,
  179. );
  180. }
  181. canvas.drawParagraph(paragraph, offset);
  182. }
  183. /// Paints the background of a cell represented by [cellData] to [canvas] at
  184. /// [offset].
  185. @pragma('vm:prefer-inline')
  186. void paintCellBackground(Canvas canvas, Offset offset, CellData cellData) {
  187. late Color color;
  188. final colorType = cellData.background & CellColor.typeMask;
  189. if (cellData.flags & CellFlags.inverse != 0) {
  190. color = resolveForegroundColor(cellData.foreground);
  191. } else if (colorType == CellColor.normal) {
  192. return;
  193. } else {
  194. color = resolveBackgroundColor(cellData.background);
  195. }
  196. final paint = Paint()..color = color;
  197. final doubleWidth = cellData.content >> CellContent.widthShift == 2;
  198. final widthScale = doubleWidth ? 2 : 1;
  199. final size = Size(_cellSize.width * widthScale + 1, _cellSize.height);
  200. canvas.drawRect(offset & size, paint);
  201. }
  202. /// Get the effective foreground color for a cell from information encoded in
  203. /// [cellColor].
  204. @pragma('vm:prefer-inline')
  205. Color resolveForegroundColor(int cellColor) {
  206. final colorType = cellColor & CellColor.typeMask;
  207. final colorValue = cellColor & CellColor.valueMask;
  208. switch (colorType) {
  209. case CellColor.normal:
  210. return _theme.foreground;
  211. case CellColor.named:
  212. case CellColor.palette:
  213. return _colorPalette[colorValue];
  214. case CellColor.rgb:
  215. default:
  216. return Color(colorValue | 0xFF000000);
  217. }
  218. }
  219. /// Get the effective background color for a cell from information encoded in
  220. /// [cellColor].
  221. @pragma('vm:prefer-inline')
  222. Color resolveBackgroundColor(int cellColor) {
  223. final colorType = cellColor & CellColor.typeMask;
  224. final colorValue = cellColor & CellColor.valueMask;
  225. switch (colorType) {
  226. case CellColor.normal:
  227. return _theme.background;
  228. case CellColor.named:
  229. case CellColor.palette:
  230. return _colorPalette[colorValue];
  231. case CellColor.rgb:
  232. default:
  233. return Color(colorValue | 0xFF000000);
  234. }
  235. }
  236. }