terminal_painters.dart 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  1. import 'dart:math';
  2. import 'package:flutter/material.dart';
  3. import 'package:xterm/buffer/cell_flags.dart';
  4. import 'package:xterm/buffer/line/line.dart';
  5. import 'package:xterm/mouse/position.dart';
  6. import 'package:xterm/terminal/terminal_search.dart';
  7. import 'package:xterm/terminal/terminal_ui_interaction.dart';
  8. import 'package:xterm/theme/terminal_style.dart';
  9. import 'package:xterm/util/bit_flags.dart';
  10. import 'cache.dart';
  11. import 'char_size.dart';
  12. class TerminalPainter extends CustomPainter {
  13. TerminalPainter({
  14. required this.terminal,
  15. required this.style,
  16. required this.charSize,
  17. required this.textLayoutCache,
  18. });
  19. final TerminalUiInteraction terminal;
  20. final TerminalStyle style;
  21. final CellSize charSize;
  22. final TextLayoutCache textLayoutCache;
  23. @override
  24. void paint(Canvas canvas, Size size) {
  25. _paintBackground(canvas);
  26. _paintText(canvas);
  27. _paintUserSearchResult(canvas, size);
  28. _paintSelection(canvas);
  29. }
  30. void _paintBackground(Canvas canvas) {
  31. final lines = terminal.getVisibleLines();
  32. for (var row = 0; row < lines.length; row++) {
  33. final line = lines[row];
  34. final offsetY = row * charSize.cellHeight;
  35. final cellCount = terminal.terminalWidth;
  36. for (var col = 0; col < cellCount; col++) {
  37. final cellWidth = line.cellGetWidth(col);
  38. if (cellWidth == 0) {
  39. continue;
  40. }
  41. final cellFgColor = line.cellGetFgColor(col);
  42. final cellBgColor = line.cellGetBgColor(col);
  43. final effectBgColor = line.cellHasFlag(col, CellFlags.inverse)
  44. ? cellFgColor
  45. : cellBgColor;
  46. if (effectBgColor == 0x00) {
  47. continue;
  48. }
  49. // when a program reports black as background then it "really" means transparent
  50. if (effectBgColor == 0xFF000000) {
  51. continue;
  52. }
  53. final offsetX = col * charSize.cellWidth;
  54. final effectWidth = charSize.cellWidth * cellWidth + 1;
  55. final effectHeight = charSize.cellHeight + 1;
  56. // background color is already painted with opacity by the Container of
  57. // TerminalPainter so wo don't need to fallback to
  58. // terminal.theme.background here.
  59. final paint = Paint()..color = Color(effectBgColor);
  60. canvas.drawRect(
  61. Rect.fromLTWH(offsetX, offsetY, effectWidth, effectHeight),
  62. paint,
  63. );
  64. }
  65. }
  66. }
  67. void _paintUserSearchResult(Canvas canvas, Size size) {
  68. final searchResult = terminal.userSearchResult;
  69. //when there is no ongoing user search then directly return
  70. if (!terminal.isUserSearchActive) {
  71. return;
  72. }
  73. //make everything dim so that the search result can be seen better
  74. final dimPaint = Paint()
  75. ..color = Color(terminal.theme.background).withAlpha(128)
  76. ..style = PaintingStyle.fill;
  77. canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), dimPaint);
  78. for (int i = 1; i <= searchResult.allHits.length; i++) {
  79. _paintSearchHit(canvas, searchResult.allHits[i - 1], i);
  80. }
  81. }
  82. void _paintSearchHit(Canvas canvas, TerminalSearchHit hit, int hitNum) {
  83. //check if the hit is visible
  84. if (hit.startLineIndex >=
  85. terminal.scrollOffsetFromTop + terminal.terminalHeight ||
  86. hit.endLineIndex < terminal.scrollOffsetFromTop) {
  87. return;
  88. }
  89. final paint = Paint()
  90. ..color = Color(terminal.currentSearchHit == hitNum
  91. ? terminal.theme.searchHitBackgroundCurrent
  92. : terminal.theme.searchHitBackground)
  93. ..style = PaintingStyle.fill;
  94. if (hit.startLineIndex == hit.endLineIndex) {
  95. final double y =
  96. (hit.startLineIndex.toDouble() - terminal.scrollOffsetFromTop) *
  97. charSize.cellHeight;
  98. final startX = charSize.cellWidth * hit.startIndex;
  99. final endX = charSize.cellWidth * hit.endIndex;
  100. canvas.drawRect(
  101. Rect.fromLTRB(startX, y, endX, y + charSize.cellHeight), paint);
  102. } else {
  103. //draw first row: start - line end
  104. final double yFirstRow =
  105. (hit.startLineIndex.toDouble() - terminal.scrollOffsetFromTop) *
  106. charSize.cellHeight;
  107. final startXFirstRow = charSize.cellWidth * hit.startIndex;
  108. final endXFirstRow = charSize.cellWidth * terminal.terminalWidth;
  109. canvas.drawRect(
  110. Rect.fromLTRB(startXFirstRow, yFirstRow, endXFirstRow,
  111. yFirstRow + charSize.cellHeight),
  112. paint);
  113. //draw middle rows
  114. final middleRowCount = hit.endLineIndex - hit.startLineIndex - 1;
  115. if (middleRowCount > 0) {
  116. final startYMiddleRows =
  117. (hit.startLineIndex + 1 - terminal.scrollOffsetFromTop) *
  118. charSize.cellHeight;
  119. final startXMiddleRows = 0.toDouble();
  120. final endYMiddleRows = min(
  121. hit.endLineIndex - terminal.scrollOffsetFromTop,
  122. terminal.terminalHeight) *
  123. charSize.cellHeight;
  124. final endXMiddleRows = terminal.terminalWidth * charSize.cellWidth;
  125. canvas.drawRect(
  126. Rect.fromLTRB(startXMiddleRows, startYMiddleRows, endXMiddleRows,
  127. endYMiddleRows),
  128. paint);
  129. }
  130. //draw end row: line start - end
  131. if (hit.endLineIndex - terminal.scrollOffsetFromTop <
  132. terminal.terminalHeight) {
  133. final startXEndRow = 0.toDouble();
  134. final startYEndRow = (hit.endLineIndex - terminal.scrollOffsetFromTop) *
  135. charSize.cellHeight;
  136. final endXEndRow = hit.endIndex * charSize.cellWidth;
  137. final endYEndRow = startYEndRow + charSize.cellHeight;
  138. canvas.drawRect(
  139. Rect.fromLTRB(startXEndRow, startYEndRow, endXEndRow, endYEndRow),
  140. paint);
  141. }
  142. }
  143. final visibleLines = terminal.getVisibleLines();
  144. //paint text
  145. for (var rawRow = hit.startLineIndex;
  146. rawRow <= hit.endLineIndex;
  147. rawRow++) {
  148. final start = rawRow == hit.startLineIndex ? hit.startIndex : 0;
  149. final end =
  150. rawRow == hit.endLineIndex ? hit.endIndex : terminal.terminalWidth;
  151. final row = rawRow - terminal.scrollOffsetFromTop;
  152. final offsetY = row * charSize.cellHeight;
  153. if (row >= visibleLines.length || row < 0) {
  154. continue;
  155. }
  156. final line = visibleLines[row];
  157. for (var col = start; col < end; col++) {
  158. final offsetX = col * charSize.cellWidth;
  159. _paintCell(
  160. canvas,
  161. line,
  162. col,
  163. offsetX,
  164. offsetY,
  165. fgColorOverride: terminal.theme.searchHitForeground,
  166. bgColorOverride: terminal.theme.searchHitForeground,
  167. );
  168. }
  169. }
  170. }
  171. void _paintSelection(Canvas canvas) {
  172. final paint = Paint()..color = Colors.white.withOpacity(0.3);
  173. for (var y = 0; y < terminal.terminalHeight; y++) {
  174. final offsetY = y * charSize.cellHeight;
  175. final absoluteY = terminal.convertViewLineToRawLine(y) -
  176. terminal.scrollOffsetFromBottom;
  177. for (var x = 0; x < terminal.terminalWidth; x++) {
  178. var cellCount = 0;
  179. while (
  180. (terminal.selection?.contains(Position(x + cellCount, absoluteY)) ??
  181. false) &&
  182. x + cellCount < terminal.terminalWidth) {
  183. cellCount++;
  184. }
  185. if (cellCount == 0) {
  186. continue;
  187. }
  188. final offsetX = x * charSize.cellWidth;
  189. final effectWidth = cellCount * charSize.cellWidth;
  190. final effectHeight = charSize.cellHeight;
  191. canvas.drawRect(
  192. Rect.fromLTWH(offsetX, offsetY, effectWidth, effectHeight),
  193. paint,
  194. );
  195. x += cellCount;
  196. }
  197. }
  198. }
  199. void _paintText(Canvas canvas) {
  200. final lines = terminal.getVisibleLines();
  201. for (var row = 0; row < lines.length; row++) {
  202. final line = lines[row];
  203. final offsetY = row * charSize.cellHeight;
  204. // final cellCount = math.min(terminal.viewWidth, line.length);
  205. final cellCount = terminal.terminalWidth;
  206. for (var col = 0; col < cellCount; col++) {
  207. final width = line.cellGetWidth(col);
  208. if (width == 0) {
  209. continue;
  210. }
  211. final offsetX = col * charSize.cellWidth;
  212. _paintCell(
  213. canvas,
  214. line,
  215. col,
  216. offsetX,
  217. offsetY,
  218. );
  219. }
  220. }
  221. }
  222. int _getColor(int colorCode) {
  223. return (colorCode == 0) ? 0xFF000000 : colorCode;
  224. }
  225. void _paintCell(
  226. Canvas canvas,
  227. BufferLine line,
  228. int cell,
  229. double offsetX,
  230. double offsetY, {
  231. int? fgColorOverride,
  232. int? bgColorOverride,
  233. }) {
  234. final codePoint = line.cellGetContent(cell);
  235. final fgColor = fgColorOverride ?? _getColor(line.cellGetFgColor(cell));
  236. final bgColor = bgColorOverride ?? _getColor(line.cellGetBgColor(cell));
  237. final flags = line.cellGetFlags(cell);
  238. if (codePoint == 0 || flags.hasFlag(CellFlags.invisible)) {
  239. return;
  240. }
  241. // final cellHash = line.cellGetHash(cell);
  242. final cellHash = hashValues(codePoint, fgColor, bgColor, flags);
  243. var character = textLayoutCache.getLayoutFromCache(cellHash);
  244. if (character != null) {
  245. canvas.drawParagraph(character, Offset(offsetX, offsetY));
  246. return;
  247. }
  248. final cellColor = flags.hasFlag(CellFlags.inverse) ? bgColor : fgColor;
  249. var color = Color(cellColor);
  250. if (flags & CellFlags.faint != 0) {
  251. color = color.withOpacity(0.5);
  252. }
  253. final styleToUse = PaintHelper.getStyleToUse(
  254. style,
  255. color,
  256. bold: flags.hasFlag(CellFlags.bold),
  257. italic: flags.hasFlag(CellFlags.italic),
  258. underline: flags.hasFlag(CellFlags.underline),
  259. );
  260. character = textLayoutCache.performAndCacheLayout(
  261. String.fromCharCode(codePoint), styleToUse, cellHash);
  262. canvas.drawParagraph(character, Offset(offsetX, offsetY));
  263. }
  264. @override
  265. bool shouldRepaint(CustomPainter oldDelegate) {
  266. /// paint only when the terminal has changed since last paint.
  267. return terminal.dirty;
  268. }
  269. }
  270. class CursorPainter extends CustomPainter {
  271. final bool visible;
  272. final CellSize charSize;
  273. final bool focused;
  274. final bool blinkVisible;
  275. final int cursorColor;
  276. final int textColor;
  277. final String composingString;
  278. final TextLayoutCache textLayoutCache;
  279. final TerminalStyle style;
  280. CursorPainter({
  281. required this.visible,
  282. required this.charSize,
  283. required this.focused,
  284. required this.blinkVisible,
  285. required this.cursorColor,
  286. required this.textColor,
  287. required this.composingString,
  288. required this.textLayoutCache,
  289. required this.style,
  290. });
  291. @override
  292. void paint(Canvas canvas, Size size) {
  293. bool isVisible =
  294. visible && (blinkVisible || composingString != '' || !focused);
  295. if (isVisible) {
  296. _paintCursor(canvas);
  297. }
  298. }
  299. @override
  300. bool shouldRepaint(covariant CustomPainter oldDelegate) {
  301. if (oldDelegate is CursorPainter) {
  302. return blinkVisible != oldDelegate.blinkVisible ||
  303. focused != oldDelegate.focused ||
  304. visible != oldDelegate.visible ||
  305. charSize.cellWidth != oldDelegate.charSize.cellWidth ||
  306. charSize.cellHeight != oldDelegate.charSize.cellHeight ||
  307. composingString != oldDelegate.composingString;
  308. }
  309. return true;
  310. }
  311. void _paintCursor(Canvas canvas) {
  312. final paint = Paint()
  313. ..color = Color(cursorColor)
  314. ..strokeWidth = focused ? 0.0 : 1.0
  315. ..style = focused ? PaintingStyle.fill : PaintingStyle.stroke;
  316. canvas.drawRect(
  317. Rect.fromLTWH(0, 0, charSize.cellWidth, charSize.cellHeight), paint);
  318. if (composingString != '') {
  319. final styleToUse = PaintHelper.getStyleToUse(style, Color(textColor));
  320. final character = textLayoutCache.performAndCacheLayout(
  321. composingString, styleToUse, null);
  322. canvas.drawParagraph(character, Offset(0, 0));
  323. }
  324. }
  325. }
  326. class PaintHelper {
  327. static TextStyle getStyleToUse(
  328. TerminalStyle style,
  329. Color color, {
  330. bool bold = false,
  331. bool italic = false,
  332. bool underline = false,
  333. }) {
  334. return (style.textStyleProvider != null)
  335. ? style.textStyleProvider!(
  336. color: color,
  337. fontSize: style.fontSize,
  338. fontWeight: bold && !style.ignoreBoldFlag
  339. ? FontWeight.bold
  340. : FontWeight.normal,
  341. fontStyle: italic ? FontStyle.italic : FontStyle.normal,
  342. decoration:
  343. underline ? TextDecoration.underline : TextDecoration.none,
  344. )
  345. : TextStyle(
  346. color: color,
  347. fontSize: style.fontSize,
  348. fontWeight: bold && !style.ignoreBoldFlag
  349. ? FontWeight.bold
  350. : FontWeight.normal,
  351. fontStyle: italic ? FontStyle.italic : FontStyle.normal,
  352. decoration:
  353. underline ? TextDecoration.underline : TextDecoration.none,
  354. fontFamily: 'monospace',
  355. fontFamilyFallback: style.fontFamily,
  356. );
  357. }
  358. }