terminal_view.dart 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  1. import 'package:flutter/cupertino.dart';
  2. import 'package:flutter/material.dart';
  3. import 'package:flutter/rendering.dart';
  4. import 'package:flutter/services.dart';
  5. import 'package:xterm/core/terminal.dart';
  6. import 'package:xterm/ui/cursor_type.dart';
  7. import 'package:xterm/ui/input_map.dart';
  8. import 'package:xterm/core/input/keys.dart';
  9. import 'package:xterm/ui/char_metrics.dart';
  10. import 'package:xterm/ui/controller.dart';
  11. import 'package:xterm/ui/custom_text_edit.dart';
  12. import 'package:xterm/ui/gesture/gesture_handler.dart';
  13. import 'package:xterm/ui/keyboard_visibility.dart';
  14. import 'package:xterm/ui/render.dart';
  15. import 'package:xterm/ui/terminal_text_style.dart';
  16. import 'package:xterm/ui/terminal_theme.dart';
  17. import 'package:xterm/ui/themes.dart';
  18. class TerminalView extends StatefulWidget {
  19. const TerminalView(
  20. this.terminal, {
  21. Key? key,
  22. this.controller,
  23. this.theme = TerminalThemes.defaultTheme,
  24. this.textStyle = const TerminalStyle(),
  25. this.padding,
  26. this.scrollController,
  27. this.autoResize = true,
  28. this.backgroundOpacity = 1,
  29. this.focusNode,
  30. this.autofocus = false,
  31. this.onTap,
  32. this.mouseCursor = SystemMouseCursors.text,
  33. this.keyboardType = TextInputType.emailAddress,
  34. this.keyboardAppearance = Brightness.dark,
  35. this.cursorType = TerminalCursorType.block,
  36. this.alwaysShowCursor = false,
  37. this.deleteDetection = false,
  38. }) : super(key: key);
  39. /// The underlying terminal that this widget renders.
  40. final Terminal terminal;
  41. final TerminalController? controller;
  42. /// The theme to use for this terminal.
  43. final TerminalTheme theme;
  44. /// The style to use for painting characters.
  45. final TerminalStyle textStyle;
  46. /// Padding around the inner [Scrollable] widget.
  47. final EdgeInsets? padding;
  48. /// Scroll controller for the inner [Scrollable] widget.
  49. final ScrollController? scrollController;
  50. /// Should this widget automatically notify the underlying terminal when its
  51. /// size changes. [true] by default.
  52. final bool autoResize;
  53. /// Opacity of the terminal background. Set to 0 to make the terminal
  54. /// background transparent.
  55. final double backgroundOpacity;
  56. /// An optional focus node to use as the focus node for this widget.
  57. final FocusNode? focusNode;
  58. /// True if this widget will be selected as the initial focus when no other
  59. /// node in its scope is currently focused.
  60. final bool autofocus;
  61. /// Callback for when the user taps on the terminal.
  62. ///
  63. /// This exists because [TerminalView] builds a [GestureDetector] internally
  64. /// to to trigger focus requests, adjust the selection, etc. Handling some of
  65. /// those events by wrapping [TerminalView] with a competingGestureDetector is
  66. /// problematic.
  67. final VoidCallback? onTap;
  68. /// The mouse cursor for mouse pointers that are hovering over the terminal.
  69. /// [SystemMouseCursors.text] by default.
  70. final MouseCursor mouseCursor;
  71. /// The type of information for which to optimize the text input control.
  72. /// [TextInputType.emailAddress] by default.
  73. final TextInputType keyboardType;
  74. /// The appearance of the keyboard. [Brightness.dark] by default.
  75. ///
  76. /// This setting is only honored on iOS devices.
  77. final Brightness keyboardAppearance;
  78. /// The type of cursor to use. [TerminalCursorType.block] by default.
  79. final TerminalCursorType cursorType;
  80. /// Whether to always show the cursor. This is useful for debugging.
  81. /// [false] by default.
  82. final bool alwaysShowCursor;
  83. /// Workaround to detect delete key for platforms and IMEs that does not
  84. /// emit hardware delete event. Prefered on mobile platforms. [false] by
  85. /// default.
  86. final bool deleteDetection;
  87. @override
  88. State<TerminalView> createState() => TerminalViewState();
  89. }
  90. class TerminalViewState extends State<TerminalView> {
  91. late final FocusNode _focusNode;
  92. final _customTextEditKey = GlobalKey<CustomTextEditState>();
  93. final _scrollableKey = GlobalKey<ScrollableState>();
  94. final _viewportKey = GlobalKey();
  95. String? _composingText;
  96. late TerminalController _controller;
  97. late ScrollController _scrollController;
  98. RenderTerminal get renderTerminal =>
  99. _viewportKey.currentContext!.findRenderObject() as RenderTerminal;
  100. @override
  101. void initState() {
  102. _focusNode = widget.focusNode ?? FocusNode();
  103. _controller = widget.controller ?? TerminalController();
  104. _scrollController = widget.scrollController ?? ScrollController();
  105. super.initState();
  106. }
  107. @override
  108. void didUpdateWidget(TerminalView oldWidget) {
  109. if (oldWidget.focusNode != widget.focusNode) {
  110. _focusNode = widget.focusNode ?? FocusNode();
  111. }
  112. super.didUpdateWidget(oldWidget);
  113. }
  114. @override
  115. void dispose() {
  116. _focusNode.dispose();
  117. super.dispose();
  118. }
  119. void requestKeyboard() {
  120. _customTextEditKey.currentState?.requestKeyboard();
  121. }
  122. void closeKeyboard() {
  123. _customTextEditKey.currentState?.closeKeyboard();
  124. }
  125. bool get hasInputConnection {
  126. return _customTextEditKey.currentState?.hasInputConnection == true;
  127. }
  128. KeyEventResult _onKeyEvent(RawKeyEvent event) {
  129. if (event is! RawKeyDownEvent) {
  130. return KeyEventResult.ignored;
  131. }
  132. final key = inputMap(event.logicalKey);
  133. if (key == null) {
  134. return KeyEventResult.ignored;
  135. }
  136. final handled = widget.terminal.keyInput(
  137. key,
  138. ctrl: event.isControlPressed,
  139. alt: event.isAltPressed,
  140. shift: event.isShiftPressed,
  141. );
  142. if (handled) {
  143. _scrollToBottom();
  144. }
  145. return handled ? KeyEventResult.handled : KeyEventResult.ignored;
  146. }
  147. void _onKeyboardShow() {
  148. if (_focusNode.hasFocus) {
  149. WidgetsBinding.instance.addPostFrameCallback((_) {
  150. _scrollToBottom();
  151. });
  152. }
  153. }
  154. void _onEditableRect(Rect rect, Rect caretRect) {
  155. _customTextEditKey.currentState?.setEditableRect(rect, caretRect);
  156. }
  157. void _scrollToBottom() {
  158. final position = _scrollableKey.currentState?.position;
  159. if (position != null) {
  160. position.jumpTo(position.maxScrollExtent);
  161. }
  162. }
  163. @override
  164. Widget build(BuildContext context) {
  165. // Calculate everytime build happens, because some fonts library
  166. // lazily load fonts (such as google_fonts) and this can change the
  167. // font metrics while textStyle is still the same.
  168. final charMetrics = calcCharMetrics(widget.textStyle);
  169. Widget child = Scrollable(
  170. key: _scrollableKey,
  171. controller: _scrollController,
  172. viewportBuilder: (context, offset) {
  173. return _TerminalView(
  174. key: _viewportKey,
  175. terminal: widget.terminal,
  176. controller: _controller,
  177. offset: offset,
  178. padding: MediaQuery.of(context).padding,
  179. autoResize: widget.autoResize,
  180. charMetrics: charMetrics,
  181. textStyle: widget.textStyle,
  182. theme: widget.theme,
  183. focusNode: _focusNode,
  184. cursorType: widget.cursorType,
  185. alwaysShowCursor: widget.alwaysShowCursor,
  186. onEditableRect: _onEditableRect,
  187. composingText: _composingText,
  188. );
  189. },
  190. );
  191. child = Container(
  192. color: widget.theme.background.withOpacity(widget.backgroundOpacity),
  193. padding: widget.padding,
  194. child: child,
  195. );
  196. child = CustomTextEdit(
  197. key: _customTextEditKey,
  198. focusNode: _focusNode,
  199. inputType: widget.keyboardType,
  200. keyboardAppearance: widget.keyboardAppearance,
  201. deleteDetection: widget.deleteDetection,
  202. onInsert: (text) {
  203. _scrollToBottom();
  204. widget.terminal.textInput(text);
  205. },
  206. onDelete: () {
  207. _scrollToBottom();
  208. widget.terminal.keyInput(TerminalKey.backspace);
  209. },
  210. onComposing: (text) {
  211. setState(() => _composingText = text);
  212. },
  213. onAction: (action) {
  214. _scrollToBottom();
  215. if (action == TextInputAction.done) {
  216. widget.terminal.keyInput(TerminalKey.enter);
  217. }
  218. },
  219. onKey: _onKeyEvent,
  220. child: child,
  221. );
  222. child = KeyboardVisibilty(
  223. onKeyboardShow: _onKeyboardShow,
  224. child: child,
  225. );
  226. child = TerminalGestureHandler(
  227. terminalView: this,
  228. onTapUp: _onTapUp,
  229. onTapDown: _onTapDown,
  230. onSecondaryTapDown: _onSecondaryTapDown,
  231. child: child,
  232. );
  233. child = MouseRegion(
  234. cursor: widget.mouseCursor,
  235. child: child,
  236. );
  237. return child;
  238. }
  239. void _onTapUp(_) {
  240. widget.onTap?.call();
  241. }
  242. void _onTapDown(_) {
  243. if (_controller.hasSelection) {
  244. _controller.clearSelection();
  245. } else {
  246. _customTextEditKey.currentState?.requestKeyboard();
  247. }
  248. }
  249. void _onSecondaryTapDown(TapDownDetails details) {
  250. final position = renderTerminal.positionFromOffset(
  251. renderTerminal.globalToLocal(details.globalPosition),
  252. );
  253. final selection = _controller.selection;
  254. if (selection == null || !selection.isWithin(position)) {
  255. renderTerminal.selectWord(details.globalPosition);
  256. }
  257. }
  258. }
  259. class _TerminalView extends LeafRenderObjectWidget {
  260. const _TerminalView({
  261. Key? key,
  262. required this.terminal,
  263. required this.controller,
  264. required this.offset,
  265. required this.padding,
  266. required this.autoResize,
  267. required this.charMetrics,
  268. required this.textStyle,
  269. required this.theme,
  270. required this.focusNode,
  271. required this.cursorType,
  272. required this.alwaysShowCursor,
  273. this.onEditableRect,
  274. this.composingText,
  275. }) : super(key: key);
  276. final Terminal terminal;
  277. final TerminalController controller;
  278. final ViewportOffset offset;
  279. final EdgeInsets padding;
  280. final bool autoResize;
  281. final Size charMetrics;
  282. final TerminalStyle textStyle;
  283. final TerminalTheme theme;
  284. final FocusNode focusNode;
  285. final TerminalCursorType cursorType;
  286. final bool alwaysShowCursor;
  287. final EditableRectCallback? onEditableRect;
  288. final String? composingText;
  289. @override
  290. RenderTerminal createRenderObject(BuildContext context) {
  291. return RenderTerminal(
  292. terminal: terminal,
  293. controller: controller,
  294. offset: offset,
  295. padding: padding,
  296. autoResize: autoResize,
  297. charMetrics: charMetrics,
  298. textStyle: textStyle,
  299. theme: theme,
  300. focusNode: focusNode,
  301. cursorType: cursorType,
  302. alwaysShowCursor: alwaysShowCursor,
  303. onEditableRect: onEditableRect,
  304. composingText: composingText,
  305. );
  306. }
  307. @override
  308. void updateRenderObject(BuildContext context, RenderTerminal renderObject) {
  309. renderObject
  310. ..terminal = terminal
  311. ..controller = controller
  312. ..offset = offset
  313. ..padding = padding
  314. ..autoResize = autoResize
  315. ..charMetrics = charMetrics
  316. ..textStyle = textStyle
  317. ..theme = theme
  318. ..focusNode = focusNode
  319. ..cursorType = cursorType
  320. ..alwaysShowCursor = alwaysShowCursor
  321. ..onEditableRect = onEditableRect
  322. ..composingText = composingText;
  323. }
  324. }