terminal_painters.dart 12 KB

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