terminal_painters.dart 12 KB

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