terminal_view.dart 13 KB

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