terminal_view.dart 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551
  1. import 'dart:math' as math;
  2. import 'dart:ui';
  3. import 'package:flutter/gestures.dart';
  4. import 'package:flutter/material.dart';
  5. import 'package:flutter/rendering.dart';
  6. import 'package:flutter/scheduler.dart';
  7. import 'package:flutter/services.dart';
  8. import 'package:meta/meta.dart';
  9. import 'package:xterm/buffer/cell.dart';
  10. import 'package:xterm/frontend/char_size.dart';
  11. import 'package:xterm/frontend/helpers.dart';
  12. import 'package:xterm/frontend/input_behavior.dart';
  13. import 'package:xterm/frontend/input_behaviors.dart';
  14. import 'package:xterm/frontend/input_listener.dart';
  15. import 'package:xterm/frontend/oscillator.dart';
  16. import 'package:xterm/frontend/cache.dart';
  17. import 'package:xterm/mouse/position.dart';
  18. import 'package:xterm/terminal/terminal.dart';
  19. import 'package:xterm/utli/hash_values.dart';
  20. typedef ResizeHandler = void Function(int width, int height);
  21. const _kDefaultFontFamily = [
  22. 'Droid Sans Mono',
  23. 'Noto Sans Mono',
  24. 'Roboto Mono',
  25. 'Consolas',
  26. 'Noto Sans Mono CJK SC',
  27. 'Noto Sans Mono CJK TC',
  28. 'Noto Sans Mono CJK KR',
  29. 'Noto Sans Mono CJK JP',
  30. 'Noto Sans Mono CJK HK',
  31. 'monospace',
  32. 'Noto Color Emoji',
  33. 'Noto Sans Symbols',
  34. 'Roboto',
  35. 'Ubuntu',
  36. 'Cantarell',
  37. 'DejaVu Sans',
  38. 'Liberation Sans',
  39. 'Arial',
  40. 'Droid Sans Fallback',
  41. 'sans-serif',
  42. ];
  43. class TerminalView extends StatefulWidget {
  44. TerminalView({
  45. Key key,
  46. @required this.terminal,
  47. this.onResize,
  48. this.fontSize = 16,
  49. this.fontFamily = _kDefaultFontFamily,
  50. this.fontWidthScaleFactor = 1.0,
  51. this.fontHeightScaleFactor = 1.1,
  52. FocusNode focusNode,
  53. ScrollController scrollController,
  54. InputBehavior inputBehavior,
  55. }) : assert(terminal != null),
  56. focusNode = focusNode ?? FocusNode(),
  57. scrollController = scrollController ?? ScrollController(),
  58. inputBehavior = inputBehavior ?? InputBehaviors.platform,
  59. super(key: key ?? ValueKey(terminal));
  60. final Terminal terminal;
  61. final ResizeHandler onResize;
  62. final FocusNode focusNode;
  63. final ScrollController scrollController;
  64. final double fontSize;
  65. final double fontWidthScaleFactor;
  66. final double fontHeightScaleFactor;
  67. final List<String> fontFamily;
  68. final InputBehavior inputBehavior;
  69. CellSize measureCellSize() {
  70. final testString = 'xxxxxxxxxx' * 1000;
  71. final text = Text(
  72. testString,
  73. style: TextStyle(
  74. fontFamilyFallback: _kDefaultFontFamily,
  75. fontSize: fontSize,
  76. ),
  77. );
  78. final size = textSize(text);
  79. final charWidth = (size.width / testString.length);
  80. final charHeight = size.height;
  81. final cellWidth = charWidth * fontWidthScaleFactor;
  82. final cellHeight = size.height * fontHeightScaleFactor;
  83. return CellSize(
  84. charWidth: charWidth,
  85. charHeight: charHeight,
  86. cellWidth: cellWidth,
  87. cellHeight: cellHeight,
  88. letterSpacing: cellWidth - charWidth,
  89. lineSpacing: cellHeight - charHeight,
  90. );
  91. }
  92. @override
  93. _TerminalViewState createState() => _TerminalViewState();
  94. }
  95. class _TerminalViewState extends State<TerminalView> {
  96. final oscillator = Oscillator.ms(600);
  97. bool get focused {
  98. return widget.focusNode.hasFocus;
  99. }
  100. int _lastTerminalWidth;
  101. int _lastTerminalHeight;
  102. CellSize _cellSize;
  103. ViewportOffset _offset;
  104. var _minScrollExtent = 0.0;
  105. var _maxScrollExtent = 0.0;
  106. void onTerminalChange() {
  107. // if (_offset != null) {
  108. // final currentScrollExtent =
  109. // _cellSize.cellHeight * widget.terminal.buffer.scrollOffsetFromTop;
  110. // if (_offset.pixels != currentScrollExtent) {
  111. // _offset.correctBy(currentScrollExtent - _offset.pixels - 1);
  112. // }
  113. // }
  114. if (mounted) {
  115. setState(() {});
  116. }
  117. }
  118. void onTick() {
  119. widget.terminal.refresh();
  120. }
  121. @override
  122. void initState() {
  123. // oscillator.start();
  124. // oscillator.addListener(onTick);
  125. _cellSize = widget.measureCellSize();
  126. widget.terminal.addListener(onTerminalChange);
  127. super.initState();
  128. }
  129. @override
  130. void didUpdateWidget(TerminalView oldWidget) {
  131. widget.terminal.addListener(onTerminalChange);
  132. super.didUpdateWidget(oldWidget);
  133. }
  134. @override
  135. void dispose() {
  136. // oscillator.stop();
  137. // oscillator.removeListener(onTick);
  138. widget.terminal.removeListener(onTerminalChange);
  139. super.dispose();
  140. }
  141. @override
  142. Widget build(BuildContext context) {
  143. return InputListener(
  144. listenKeyStroke: widget.inputBehavior.acceptKeyStroke,
  145. onKeyStroke: onKeyStroke,
  146. onTextInput: onInput,
  147. onAction: onAction,
  148. onFocus: onFocus,
  149. focusNode: widget.focusNode,
  150. autofocus: true,
  151. child: MouseRegion(
  152. cursor: SystemMouseCursors.text,
  153. child: LayoutBuilder(builder: (context, constraints) {
  154. onResize(constraints.maxWidth, constraints.maxHeight);
  155. return Scrollable(
  156. viewportBuilder: (context, offset) {
  157. offset.applyViewportDimension(constraints.maxHeight);
  158. _minScrollExtent = 0.0;
  159. _maxScrollExtent = math.max(
  160. 0.0,
  161. _cellSize.cellHeight * widget.terminal.buffer.height -
  162. constraints.maxHeight);
  163. // final currentScrollExtent = _cellSize.cellHeight *
  164. // widget.terminal.buffer.scrollOffsetFromTop;
  165. // offset.correctBy(currentScrollExtent - offset.pixels - 1);
  166. offset.applyContentDimensions(_minScrollExtent, _maxScrollExtent);
  167. _offset = offset;
  168. _offset.addListener(onScroll);
  169. return buildTerminal(context);
  170. },
  171. );
  172. }),
  173. ),
  174. );
  175. }
  176. Widget buildTerminal(BuildContext context) {
  177. return GestureDetector(
  178. behavior: HitTestBehavior.deferToChild,
  179. dragStartBehavior: DragStartBehavior.down,
  180. onTapDown: (detail) {
  181. if (widget.terminal.selection.isEmpty) {
  182. InputListener.of(context).requestKeyboard();
  183. } else {
  184. widget.terminal.selection.clear();
  185. }
  186. final pos = detail.localPosition;
  187. final offset = getMouseOffset(pos.dx, pos.dy);
  188. widget.terminal.mouseMode.onTap(widget.terminal, offset);
  189. widget.terminal.refresh();
  190. },
  191. onPanStart: (detail) {
  192. final pos = detail.localPosition;
  193. final offset = getMouseOffset(pos.dx, pos.dy);
  194. widget.terminal.mouseMode.onPanStart(widget.terminal, offset);
  195. widget.terminal.refresh();
  196. },
  197. onPanUpdate: (detail) {
  198. final pos = detail.localPosition;
  199. final offset = getMouseOffset(pos.dx, pos.dy);
  200. widget.terminal.mouseMode.onPanUpdate(widget.terminal, offset);
  201. widget.terminal.refresh();
  202. },
  203. child: Container(
  204. constraints: BoxConstraints.expand(),
  205. color: Color(widget.terminal.colorScheme.background.value),
  206. child: CustomPaint(
  207. painter: TerminalPainter(
  208. terminal: widget.terminal,
  209. view: widget,
  210. oscillator: oscillator,
  211. focused: focused,
  212. charSize: _cellSize,
  213. ),
  214. ),
  215. ),
  216. );
  217. }
  218. Position getMouseOffset(double px, double py) {
  219. final col = (px / _cellSize.cellWidth).floor();
  220. final row = (py / _cellSize.cellHeight).floor();
  221. final x = col;
  222. final y = widget.terminal.buffer.convertViewLineToRawLine(row) -
  223. widget.terminal.buffer.scrollOffsetFromBottom;
  224. return Position(x, y);
  225. }
  226. void onResize(double width, double height) {
  227. final termWidth = (width / _cellSize.cellWidth).floor();
  228. final termHeight = (height / _cellSize.cellHeight).floor();
  229. if (_lastTerminalWidth != termWidth || _lastTerminalHeight != termHeight) {
  230. _lastTerminalWidth = termWidth;
  231. _lastTerminalHeight = termHeight;
  232. // print('($termWidth, $termHeight)');
  233. if (widget.onResize != null) {
  234. widget.onResize(termWidth, termHeight);
  235. }
  236. SchedulerBinding.instance.addPostFrameCallback((_) {
  237. widget.terminal.resize(termWidth, termHeight);
  238. });
  239. // Future.delayed(Duration.zero).then((_) {
  240. // widget.terminal.resize(termWidth, termHeight);
  241. // });
  242. }
  243. }
  244. TextEditingValue onInput(TextEditingValue value) {
  245. return widget.inputBehavior.onTextEdit(value, widget.terminal);
  246. }
  247. void onKeyStroke(RawKeyEvent event) {
  248. widget.inputBehavior.onKeyStroke(event, widget.terminal);
  249. _offset.moveTo(_maxScrollExtent);
  250. }
  251. void onFocus(bool focused) {
  252. SchedulerBinding.instance.addPostFrameCallback((_) {
  253. widget.terminal.refresh();
  254. });
  255. }
  256. void onAction(TextInputAction action) {
  257. widget.inputBehavior.onAction(action, widget.terminal);
  258. }
  259. void onScroll() {
  260. final charOffset = (_offset.pixels / _cellSize.cellHeight).ceil();
  261. final offset = widget.terminal.invisibleHeight - charOffset;
  262. widget.terminal.buffer.setScrollOffsetFromBottom(offset);
  263. }
  264. }
  265. class TerminalPainter extends CustomPainter {
  266. TerminalPainter({
  267. this.terminal,
  268. this.view,
  269. this.oscillator,
  270. this.focused,
  271. this.charSize,
  272. });
  273. final Terminal terminal;
  274. final TerminalView view;
  275. final Oscillator oscillator;
  276. final bool focused;
  277. final CellSize charSize;
  278. @override
  279. void paint(Canvas canvas, Size size) {
  280. paintBackground(canvas);
  281. // if (oscillator.value) {
  282. // }
  283. if (terminal.showCursor) {
  284. paintCursor(canvas);
  285. }
  286. paintText(canvas);
  287. // print(textLayoutCacheV2.length);
  288. // or paintTextFast(canvas);
  289. paintSelection(canvas);
  290. }
  291. void paintBackground(Canvas canvas) {
  292. final lines = terminal.getVisibleLines();
  293. for (var i = 0; i < lines.length; i++) {
  294. final line = lines[i];
  295. final offsetY = i * charSize.cellHeight;
  296. final cellCount = math.min(terminal.viewWidth, line.length);
  297. for (var i = 0; i < cellCount; i++) {
  298. final cell = line.getCell(i);
  299. if (cell.attr == null || cell.width == 0) {
  300. continue;
  301. }
  302. final offsetX = i * charSize.cellWidth;
  303. final effectWidth = charSize.cellWidth * cell.width + 1;
  304. final effectHeight = charSize.cellHeight + 1;
  305. final bgColor =
  306. cell.attr.inverse ? cell.attr.fgColor : cell.attr.bgColor;
  307. if (bgColor == null) {
  308. continue;
  309. }
  310. final paint = Paint()..color = Color(bgColor.value);
  311. canvas.drawRect(
  312. Rect.fromLTWH(offsetX, offsetY, effectWidth, effectHeight),
  313. paint,
  314. );
  315. }
  316. }
  317. }
  318. void paintSelection(Canvas canvas) {
  319. for (var y = 0; y < terminal.viewHeight; y++) {
  320. final offsetY = y * charSize.cellHeight;
  321. final absoluteY = terminal.buffer.convertViewLineToRawLine(y) -
  322. terminal.buffer.scrollOffsetFromBottom;
  323. for (var x = 0; x < terminal.viewWidth; x++) {
  324. var cellCount = 0;
  325. while (
  326. terminal.selection.contains(Position(x + cellCount, absoluteY)) &&
  327. x + cellCount < terminal.viewWidth) {
  328. cellCount++;
  329. }
  330. if (cellCount == 0) {
  331. continue;
  332. }
  333. final offsetX = x * charSize.cellWidth;
  334. final effectWidth = cellCount * charSize.cellWidth;
  335. final effectHeight = charSize.cellHeight;
  336. final paint = Paint()..color = Colors.white.withOpacity(0.3);
  337. canvas.drawRect(
  338. Rect.fromLTWH(offsetX, offsetY, effectWidth, effectHeight),
  339. paint,
  340. );
  341. x += cellCount;
  342. }
  343. }
  344. }
  345. void paintText(Canvas canvas) {
  346. final lines = terminal.getVisibleLines();
  347. for (var i = 0; i < lines.length; i++) {
  348. final line = lines[i];
  349. final offsetY = i * charSize.cellHeight;
  350. final cellCount = math.min(terminal.viewWidth, line.length);
  351. for (var i = 0; i < cellCount; i++) {
  352. final cell = line.getCell(i);
  353. if (cell.attr == null || cell.width == 0) {
  354. continue;
  355. }
  356. final offsetX = i * charSize.cellWidth;
  357. paintCell(canvas, cell, offsetX, offsetY);
  358. }
  359. }
  360. }
  361. void paintTextFast(Canvas canvas) {
  362. final lines = terminal.getVisibleLines();
  363. for (var i = 0; i < lines.length; i++) {
  364. final line = lines[i];
  365. final offsetY = i * charSize.cellHeight;
  366. final cellCount = math.min(terminal.viewWidth, line.length);
  367. final builder = StringBuffer();
  368. for (var i = 0; i < cellCount; i++) {
  369. final cell = line.getCell(i);
  370. if (cell.attr == null || cell.width == 0) {
  371. continue;
  372. }
  373. if (cell.codePoint == null) {
  374. builder.write(' ');
  375. } else {
  376. builder.writeCharCode(cell.codePoint);
  377. }
  378. // final offsetX = i * charSize.effectWidth;
  379. // paintCell(canvas, cell, offsetX, offsetY);
  380. }
  381. final style = TextStyle(
  382. // color: color,
  383. // fontWeight: cell.attr.bold ? FontWeight.bold : FontWeight.normal,
  384. // fontStyle: cell.attr.italic ? FontStyle.italic : FontStyle.normal,
  385. fontSize: view.fontSize,
  386. letterSpacing: charSize.letterSpacing,
  387. fontFeatures: [FontFeature.tabularFigures()],
  388. // decoration:
  389. // cell.attr.underline ? TextDecoration.underline : TextDecoration.none,
  390. fontFamilyFallback: _kDefaultFontFamily,
  391. );
  392. final span = TextSpan(
  393. text: builder.toString(),
  394. style: style,
  395. );
  396. final tp = textLayoutCache.getOrPerformLayout(span);
  397. tp.paint(canvas, Offset(0, offsetY));
  398. }
  399. }
  400. void paintCell(Canvas canvas, Cell cell, double offsetX, double offsetY) {
  401. if (cell.codePoint == null || cell.attr.invisible) {
  402. return;
  403. }
  404. final cellColor = cell.attr.inverse
  405. ? cell.attr.bgColor ?? terminal.colorScheme.background
  406. : cell.attr.fgColor ?? terminal.colorScheme.foreground;
  407. var color = Color(cellColor.value);
  408. if (cell.attr.faint) {
  409. color = color.withOpacity(0.5);
  410. }
  411. final style = TextStyle(
  412. color: color,
  413. fontWeight: cell.attr.bold ? FontWeight.bold : FontWeight.normal,
  414. fontStyle: cell.attr.italic ? FontStyle.italic : FontStyle.normal,
  415. fontSize: view.fontSize,
  416. decoration:
  417. cell.attr.underline ? TextDecoration.underline : TextDecoration.none,
  418. fontFamilyFallback: _kDefaultFontFamily,
  419. );
  420. final span = TextSpan(
  421. text: String.fromCharCode(cell.codePoint),
  422. // text: codePointCache.getOrConstruct(cell.codePoint),
  423. style: style,
  424. );
  425. // final tp = textLayoutCache.getOrPerformLayout(span);
  426. final tp = textLayoutCacheV2.getOrPerformLayout(
  427. span, hashValues(cell.codePoint, cell.attr));
  428. tp.paint(canvas, Offset(offsetX, offsetY));
  429. }
  430. void paintCursor(Canvas canvas) {
  431. final screenCursorY = terminal.cursorY + terminal.scrollOffset;
  432. if (screenCursorY < 0 || screenCursorY >= terminal.viewHeight) {
  433. return;
  434. }
  435. final char = terminal.buffer.getCellUnderCursor();
  436. final width =
  437. char != null ? charSize.cellWidth * char.width : charSize.cellWidth;
  438. final offsetX = charSize.cellWidth * terminal.cursorX;
  439. final offsetY = charSize.cellHeight * screenCursorY;
  440. final paint = Paint()
  441. ..color = Color(terminal.colorScheme.cursor.value)
  442. ..strokeWidth = focused ? 0.0 : 1.0
  443. ..style = focused ? PaintingStyle.fill : PaintingStyle.stroke;
  444. canvas.drawRect(
  445. Rect.fromLTWH(offsetX, offsetY, width, charSize.cellHeight), paint);
  446. }
  447. @override
  448. bool shouldRepaint(CustomPainter oldDelegate) {
  449. // print('shouldRepaint');
  450. return terminal.dirty;
  451. }
  452. }