terminal_view.dart 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492
  1. import 'dart:math' as math;
  2. import 'dart:ui';
  3. import 'package:flutter/cupertino.dart';
  4. import 'package:flutter/gestures.dart';
  5. import 'package:flutter/material.dart';
  6. import 'package:flutter/rendering.dart';
  7. import 'package:flutter/scheduler.dart';
  8. import 'package:flutter/services.dart';
  9. import 'package:xterm/frontend/cache.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/terminal_painters.dart';
  17. import 'package:xterm/mouse/position.dart';
  18. import 'package:xterm/terminal/terminal_ui_interaction.dart';
  19. import 'package:xterm/theme/terminal_style.dart';
  20. class TerminalView extends StatefulWidget {
  21. TerminalView({
  22. Key? key,
  23. required this.terminal,
  24. this.style = const TerminalStyle(),
  25. this.opacity = 1.0,
  26. FocusNode? focusNode,
  27. this.autofocus = false,
  28. ScrollController? scrollController,
  29. InputBehavior? inputBehavior,
  30. }) : focusNode = focusNode ?? FocusNode(),
  31. scrollController = scrollController ?? ScrollController(),
  32. inputBehavior = inputBehavior ?? InputBehaviors.platform,
  33. super(key: key ?? ValueKey(terminal));
  34. final TerminalUiInteraction terminal;
  35. final FocusNode focusNode;
  36. final bool autofocus;
  37. final ScrollController scrollController;
  38. final TerminalStyle style;
  39. final double opacity;
  40. final InputBehavior inputBehavior;
  41. // get the dimensions of a rendered character
  42. CellSize measureCellSize(double fontSize) {
  43. final testString = 'xxxxxxxxxx' * 1000;
  44. final text = Text(
  45. testString,
  46. maxLines: 1,
  47. style: (style.textStyleProvider != null)
  48. ? style.textStyleProvider!(
  49. fontSize: fontSize,
  50. )
  51. : TextStyle(
  52. fontFamily: 'monospace',
  53. fontFamilyFallback: style.fontFamily,
  54. fontSize: fontSize,
  55. ),
  56. );
  57. final size = textSize(text);
  58. final charWidth = (size.width / testString.length);
  59. final charHeight = size.height;
  60. final cellWidth = charWidth * style.fontWidthScaleFactor;
  61. final cellHeight = size.height * style.fontHeightScaleFactor;
  62. return CellSize(
  63. charWidth: charWidth,
  64. charHeight: charHeight,
  65. cellWidth: cellWidth,
  66. cellHeight: cellHeight,
  67. letterSpacing: cellWidth - charWidth,
  68. lineSpacing: cellHeight - charHeight,
  69. );
  70. }
  71. @override
  72. _TerminalViewState createState() => _TerminalViewState();
  73. }
  74. class _TerminalViewState extends State<TerminalView> {
  75. /// blinking cursor and blinking character
  76. final blinkOscillator = Oscillator.ms(600);
  77. final textLayoutCache = TextLayoutCache(TextDirection.ltr, 10240);
  78. bool get focused {
  79. return widget.focusNode.hasFocus;
  80. }
  81. late CellSize _cellSize;
  82. Position? _tapPosition;
  83. /// Scroll position from the terminal. Not null if terminal scroll extent has
  84. /// been updated and needs to be syncronized to flutter side.
  85. double? _pendingTerminalScrollExtent;
  86. void onTerminalChange() {
  87. _pendingTerminalScrollExtent =
  88. _cellSize.cellHeight * widget.terminal.scrollOffsetFromTop;
  89. if (mounted) {
  90. setState(() {});
  91. }
  92. }
  93. // listen to oscillator to update mouse blink etc.
  94. // void onTick() {
  95. // widget.terminal.refresh();
  96. // }
  97. @override
  98. void initState() {
  99. blinkOscillator.start();
  100. // oscillator.addListener(onTick);
  101. // measureCellSize is expensive so we cache the result.
  102. _cellSize = widget.measureCellSize(widget.style.fontSize);
  103. widget.terminal.addListener(onTerminalChange);
  104. super.initState();
  105. }
  106. @override
  107. void didUpdateWidget(TerminalView oldWidget) {
  108. oldWidget.terminal.removeListener(onTerminalChange);
  109. widget.terminal.addListener(onTerminalChange);
  110. if (oldWidget.style != widget.style) {
  111. _cellSize = widget.measureCellSize(widget.style.fontSize);
  112. textLayoutCache.clear();
  113. updateTerminalSize();
  114. }
  115. super.didUpdateWidget(oldWidget);
  116. }
  117. @override
  118. void dispose() {
  119. blinkOscillator.stop();
  120. // oscillator.removeListener(onTick);
  121. widget.terminal.removeListener(onTerminalChange);
  122. super.dispose();
  123. }
  124. @override
  125. Widget build(BuildContext context) {
  126. return InputListener(
  127. listenKeyStroke: widget.inputBehavior.acceptKeyStroke,
  128. onKeyStroke: onKeyStroke,
  129. onTextInput: onInput,
  130. onAction: onAction,
  131. onFocus: onFocus,
  132. focusNode: widget.focusNode,
  133. autofocus: widget.autofocus,
  134. initEditingState: widget.inputBehavior.initEditingState,
  135. child: MouseRegion(
  136. cursor: SystemMouseCursors.text,
  137. child: LayoutBuilder(builder: (context, constraints) {
  138. onWidgetSize(constraints.maxWidth, constraints.maxHeight);
  139. // use flutter's Scrollable to manage scrolling to better integrate
  140. // with widgets such as Scrollbar.
  141. return NotificationListener<ScrollNotification>(
  142. onNotification: (notification) {
  143. onScroll(notification.metrics.pixels);
  144. return false;
  145. },
  146. child: Scrollable(
  147. controller: widget.scrollController,
  148. viewportBuilder: (context, offset) {
  149. final position = widget.scrollController.position;
  150. /// use [_EmptyScrollActivity] to suppress unexpected behaviors
  151. /// that come from [applyViewportDimension].
  152. if (position is ScrollActivityDelegate) {
  153. position.beginActivity(
  154. _EmptyScrollActivity(position as ScrollActivityDelegate),
  155. );
  156. }
  157. // set viewport height.
  158. offset.applyViewportDimension(constraints.maxHeight);
  159. if (widget.terminal.isReady) {
  160. final minScrollExtent = 0.0;
  161. final maxScrollExtent = math.max(
  162. 0.0,
  163. _cellSize.cellHeight * widget.terminal.bufferHeight -
  164. constraints.maxHeight);
  165. // set how much the terminal can scroll
  166. offset.applyContentDimensions(
  167. minScrollExtent, maxScrollExtent);
  168. // synchronize pending terminal scroll extent to ScrollController
  169. if (_pendingTerminalScrollExtent != null) {
  170. position.correctPixels(_pendingTerminalScrollExtent!);
  171. _pendingTerminalScrollExtent = null;
  172. }
  173. }
  174. return buildTerminal(context);
  175. },
  176. ),
  177. );
  178. }),
  179. ),
  180. );
  181. }
  182. Widget buildTerminal(BuildContext context) {
  183. return GestureDetector(
  184. behavior: HitTestBehavior.deferToChild,
  185. dragStartBehavior: DragStartBehavior.down,
  186. onDoubleTapDown: (detail) {
  187. final pos = detail.localPosition;
  188. _tapPosition = getMouseOffset(pos.dx, pos.dy);
  189. },
  190. onTapDown: (detail) {
  191. final pos = detail.localPosition;
  192. _tapPosition = getMouseOffset(pos.dx, pos.dy);
  193. },
  194. onDoubleTap: () {
  195. if (_tapPosition != null) {
  196. widget.terminal.onMouseDoubleTap(_tapPosition!);
  197. widget.terminal.refresh();
  198. }
  199. },
  200. onTap: () {
  201. if (widget.terminal.selection?.isEmpty ?? true) {
  202. InputListener.of(context)!.requestKeyboard();
  203. } else {
  204. widget.terminal.clearSelection();
  205. }
  206. if (_tapPosition != null) {
  207. widget.terminal.onMouseTap(_tapPosition!);
  208. widget.terminal.refresh();
  209. }
  210. },
  211. onPanStart: (detail) {
  212. final pos = detail.localPosition;
  213. final offset = getMouseOffset(pos.dx, pos.dy);
  214. widget.terminal.onPanStart(offset);
  215. widget.terminal.refresh();
  216. },
  217. onPanUpdate: (detail) {
  218. final pos = detail.localPosition;
  219. final offset = getMouseOffset(pos.dx, pos.dy);
  220. widget.terminal.onPanUpdate(offset);
  221. widget.terminal.refresh();
  222. },
  223. child: Container(
  224. constraints: BoxConstraints.expand(),
  225. child: Stack(
  226. children: <Widget>[
  227. CustomPaint(
  228. painter: TerminalPainter(
  229. terminal: widget.terminal,
  230. style: widget.style,
  231. charSize: _cellSize,
  232. textLayoutCache: textLayoutCache,
  233. ),
  234. ),
  235. Positioned(
  236. child: CursorView(
  237. terminal: widget.terminal,
  238. cellSize: _cellSize,
  239. focusNode: widget.focusNode,
  240. blinkOscillator: blinkOscillator,
  241. ),
  242. width: _cellSize.cellWidth,
  243. height: _cellSize.cellHeight,
  244. left: _getCursorOffset().dx,
  245. top: _getCursorOffset().dy,
  246. ),
  247. ],
  248. ),
  249. color: Color(widget.terminal.backgroundColor).withOpacity(
  250. widget.opacity,
  251. ),
  252. ),
  253. );
  254. }
  255. Offset _getCursorOffset() {
  256. final screenCursorY = widget.terminal.cursorY;
  257. final offsetX = _cellSize.cellWidth * widget.terminal.cursorX;
  258. final offsetY = _cellSize.cellHeight * screenCursorY;
  259. return Offset(offsetX, offsetY);
  260. }
  261. /// Get global cell position from mouse position.
  262. Position getMouseOffset(double px, double py) {
  263. final col = (px / _cellSize.cellWidth).floor();
  264. final row = (py / _cellSize.cellHeight).floor();
  265. final x = col;
  266. final y = widget.terminal.convertViewLineToRawLine(row) -
  267. widget.terminal.scrollOffsetFromBottom;
  268. return Position(x, y);
  269. }
  270. double? _width;
  271. double? _height;
  272. void onWidgetSize(double width, double height) {
  273. if (!widget.terminal.isReady) {
  274. return;
  275. }
  276. _width = width;
  277. _height = height;
  278. updateTerminalSize();
  279. }
  280. int? _lastTerminalWidth;
  281. int? _lastTerminalHeight;
  282. void updateTerminalSize() {
  283. assert(_width != null);
  284. assert(_height != null);
  285. final termWidth = (_width! / _cellSize.cellWidth).floor();
  286. final termHeight = (_height! / _cellSize.cellHeight).floor();
  287. if (_lastTerminalWidth == termWidth && _lastTerminalHeight == termHeight) {
  288. return;
  289. }
  290. _lastTerminalWidth = termWidth;
  291. _lastTerminalHeight = termHeight;
  292. widget.terminal.resize(
  293. termWidth,
  294. termHeight,
  295. (termWidth * _cellSize.cellWidth).floor(),
  296. (termHeight * _cellSize.cellHeight).floor(),
  297. );
  298. }
  299. TextEditingValue? onInput(TextEditingValue value) {
  300. return widget.inputBehavior.onTextEdit(value, widget.terminal);
  301. }
  302. void onKeyStroke(RawKeyEvent event) {
  303. // TODO: find a way to stop scrolling immediately after key stroke.
  304. widget.inputBehavior.onKeyStroke(event, widget.terminal);
  305. widget.terminal.setScrollOffsetFromBottom(0);
  306. }
  307. void onFocus(bool focused) {
  308. SchedulerBinding.instance!.addPostFrameCallback((_) {
  309. widget.terminal.refresh();
  310. });
  311. }
  312. void onAction(TextInputAction action) {
  313. widget.inputBehavior.onAction(action, widget.terminal);
  314. }
  315. // synchronize flutter scroll offset to terminal
  316. void onScroll(double offset) {
  317. final topOffset = (offset / _cellSize.cellHeight).ceil();
  318. final bottomOffset = widget.terminal.invisibleHeight - topOffset;
  319. widget.terminal.setScrollOffsetFromBottom(bottomOffset);
  320. }
  321. }
  322. class CursorView extends StatefulWidget {
  323. final CellSize cellSize;
  324. final TerminalUiInteraction terminal;
  325. final FocusNode? focusNode;
  326. final Oscillator blinkOscillator;
  327. CursorView({
  328. required this.terminal,
  329. required this.cellSize,
  330. required this.focusNode,
  331. required this.blinkOscillator,
  332. });
  333. @override
  334. State<StatefulWidget> createState() => _CursorViewState();
  335. }
  336. class _CursorViewState extends State<CursorView> {
  337. bool get focused {
  338. return widget.focusNode?.hasFocus ?? false;
  339. }
  340. var _isOscillatorCallbackRegistered = false;
  341. @override
  342. void initState() {
  343. _isOscillatorCallbackRegistered = true;
  344. widget.blinkOscillator.addListener(onOscillatorTick);
  345. widget.terminal.addListener(onTerminalChange);
  346. super.initState();
  347. }
  348. @override
  349. Widget build(BuildContext context) {
  350. return CustomPaint(
  351. painter: CursorPainter(
  352. visible: _isCursorVisible(),
  353. focused: focused,
  354. charSize: widget.cellSize,
  355. blinkVisible: widget.blinkOscillator.value,
  356. cursorColor: widget.terminal.cursorColor,
  357. ),
  358. );
  359. }
  360. bool _isCursorVisible() {
  361. final screenCursorY = widget.terminal.cursorY;
  362. if (screenCursorY < 0 || screenCursorY >= widget.terminal.terminalHeight) {
  363. return false;
  364. }
  365. return widget.terminal.showCursor;
  366. }
  367. @override
  368. void dispose() {
  369. widget.terminal.removeListener(onTerminalChange);
  370. widget.blinkOscillator.removeListener(onOscillatorTick);
  371. super.dispose();
  372. }
  373. void onTerminalChange() {
  374. if (!mounted) {
  375. return;
  376. }
  377. setState(() {
  378. if (_isCursorVisible() /*&& widget.terminal.blinkingCursor*/ && focused) {
  379. if (!_isOscillatorCallbackRegistered) {
  380. _isOscillatorCallbackRegistered = true;
  381. widget.blinkOscillator.addListener(onOscillatorTick);
  382. }
  383. } else {
  384. if (_isOscillatorCallbackRegistered) {
  385. _isOscillatorCallbackRegistered = false;
  386. widget.blinkOscillator.removeListener(onOscillatorTick);
  387. }
  388. }
  389. });
  390. }
  391. void onOscillatorTick() {
  392. setState(() {});
  393. }
  394. }
  395. /// A scroll activity that does nothing. Used to suppress unexpected behaviors
  396. /// from [Scrollable] during viewport building process.
  397. class _EmptyScrollActivity extends IdleScrollActivity {
  398. _EmptyScrollActivity(ScrollActivityDelegate delegate) : super(delegate);
  399. @override
  400. void applyNewDimensions() {}
  401. /// set [isScrolling] to ture to prevent flutter from calling the old scroll
  402. /// activity.
  403. @override
  404. final isScrolling = true;
  405. void dispatchScrollStartNotification(
  406. ScrollMetrics metrics, BuildContext? context) {}
  407. void dispatchScrollUpdateNotification(
  408. ScrollMetrics metrics, BuildContext context, double scrollDelta) {}
  409. void dispatchOverscrollNotification(
  410. ScrollMetrics metrics, BuildContext context, double overscroll) {}
  411. void dispatchScrollEndNotification(
  412. ScrollMetrics metrics, BuildContext context) {}
  413. }