terminal_view.dart 14 KB

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