render.dart 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696
  1. import 'dart:math' show min, max;
  2. import 'dart:ui';
  3. import 'package:flutter/cupertino.dart';
  4. import 'package:flutter/material.dart';
  5. import 'package:flutter/rendering.dart';
  6. import 'package:flutter/scheduler.dart';
  7. import 'package:xterm/src/core/buffer/cell_flags.dart';
  8. import 'package:xterm/src/core/buffer/cell_offset.dart';
  9. import 'package:xterm/src/core/buffer/range.dart';
  10. import 'package:xterm/src/core/cell.dart';
  11. import 'package:xterm/src/core/buffer/line.dart';
  12. import 'package:xterm/src/core/mouse/button.dart';
  13. import 'package:xterm/src/core/mouse/button_state.dart';
  14. import 'package:xterm/src/terminal.dart';
  15. import 'package:xterm/src/ui/char_metrics.dart';
  16. import 'package:xterm/src/ui/controller.dart';
  17. import 'package:xterm/src/ui/cursor_type.dart';
  18. import 'package:xterm/src/ui/palette_builder.dart';
  19. import 'package:xterm/src/ui/paragraph_cache.dart';
  20. import 'package:xterm/src/ui/terminal_size.dart';
  21. import 'package:xterm/src/ui/terminal_text_style.dart';
  22. import 'package:xterm/src/ui/terminal_theme.dart';
  23. typedef EditableRectCallback = void Function(Rect rect, Rect caretRect);
  24. class RenderTerminal extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
  25. RenderTerminal({
  26. required Terminal terminal,
  27. required TerminalController controller,
  28. required ViewportOffset offset,
  29. required EdgeInsets padding,
  30. required bool autoResize,
  31. required TerminalStyle textStyle,
  32. required double textScaleFactor,
  33. required TerminalTheme theme,
  34. required FocusNode focusNode,
  35. required TerminalCursorType cursorType,
  36. required bool alwaysShowCursor,
  37. EditableRectCallback? onEditableRect,
  38. String? composingText,
  39. }) : _terminal = terminal,
  40. _controller = controller,
  41. _offset = offset,
  42. _padding = padding,
  43. _autoResize = autoResize,
  44. _textStyle = textStyle,
  45. _textScaleFactor = textScaleFactor,
  46. _theme = theme,
  47. _focusNode = focusNode,
  48. _cursorType = cursorType,
  49. _alwaysShowCursor = alwaysShowCursor,
  50. _onEditableRect = onEditableRect,
  51. _composingText = composingText {
  52. _updateColorPalette();
  53. _updateCharSize();
  54. }
  55. Terminal _terminal;
  56. set terminal(Terminal terminal) {
  57. if (_terminal == terminal) return;
  58. if (attached) _terminal.removeListener(_onTerminalChange);
  59. _terminal = terminal;
  60. if (attached) _terminal.addListener(_onTerminalChange);
  61. _resizeTerminalIfNeeded();
  62. markNeedsLayout();
  63. }
  64. TerminalController _controller;
  65. set controller(TerminalController controller) {
  66. if (_controller == controller) return;
  67. if (attached) _controller.removeListener(_onControllerUpdate);
  68. _controller = controller;
  69. if (attached) _controller.addListener(_onControllerUpdate);
  70. markNeedsLayout();
  71. }
  72. ViewportOffset _offset;
  73. set offset(ViewportOffset value) {
  74. if (value == _offset) return;
  75. if (attached) _offset.removeListener(_onScroll);
  76. _offset = value;
  77. if (attached) _offset.addListener(_onScroll);
  78. markNeedsLayout();
  79. }
  80. EdgeInsets _padding;
  81. set padding(EdgeInsets value) {
  82. if (value == _padding) return;
  83. _padding = value;
  84. markNeedsLayout();
  85. }
  86. bool _autoResize;
  87. set autoResize(bool value) {
  88. if (value == _autoResize) return;
  89. _autoResize = value;
  90. markNeedsLayout();
  91. }
  92. TerminalStyle _textStyle;
  93. set textStyle(TerminalStyle value) {
  94. if (value == _textStyle) return;
  95. _textStyle = value;
  96. _updateCharSize();
  97. _paragraphCache.clear();
  98. markNeedsLayout();
  99. }
  100. double _textScaleFactor;
  101. set textScaleFactor(double value) {
  102. if (value == _textScaleFactor) return;
  103. _textScaleFactor = value;
  104. _updateCharSize();
  105. markNeedsLayout();
  106. }
  107. TerminalTheme _theme;
  108. set theme(TerminalTheme value) {
  109. if (value == _theme) return;
  110. _theme = value;
  111. _updateColorPalette();
  112. markNeedsPaint();
  113. }
  114. FocusNode _focusNode;
  115. set focusNode(FocusNode value) {
  116. if (value == _focusNode) return;
  117. if (attached) _focusNode.removeListener(_onFocusChange);
  118. _focusNode = value;
  119. if (attached) _focusNode.addListener(_onFocusChange);
  120. markNeedsPaint();
  121. }
  122. TerminalCursorType _cursorType;
  123. set cursorType(TerminalCursorType value) {
  124. if (value == _cursorType) return;
  125. _cursorType = value;
  126. markNeedsPaint();
  127. }
  128. bool _alwaysShowCursor;
  129. set alwaysShowCursor(bool value) {
  130. if (value == _alwaysShowCursor) return;
  131. _alwaysShowCursor = value;
  132. markNeedsPaint();
  133. }
  134. EditableRectCallback? _onEditableRect;
  135. set onEditableRect(EditableRectCallback? value) {
  136. if (value == _onEditableRect) return;
  137. _onEditableRect = value;
  138. markNeedsLayout();
  139. }
  140. String? _composingText;
  141. set composingText(String? value) {
  142. if (value == _composingText) return;
  143. _composingText = value;
  144. markNeedsPaint();
  145. }
  146. /// The lookup table for converting terminal colors to Flutter colors. This is
  147. /// generated from the [_theme].
  148. late List<Color> _colorPalette;
  149. /// The size of a single character in [_textStyle] in pixels. [_textStyle] is
  150. /// expected to be monospace.
  151. late Size _charSize;
  152. TerminalSize? _viewportSize;
  153. /// Updates [_colorPalette] based on the current [_theme]. This should be
  154. /// called whenever the [_theme] changes.
  155. void _updateColorPalette() {
  156. _colorPalette = PaletteBuilder(_theme).build();
  157. }
  158. /// Updates [_charSize] based on the current [_textStyle]. This should be
  159. /// called whenever the [_textStyle] changes or the system font changes.
  160. void _updateCharSize() {
  161. _charSize = calcCharSize(_textStyle, _textScaleFactor);
  162. }
  163. var _stickToBottom = true;
  164. void _onScroll() {
  165. _stickToBottom = _scrollOffset >= _maxScrollExtent;
  166. markNeedsLayout();
  167. }
  168. void _onFocusChange() {
  169. markNeedsPaint();
  170. }
  171. void _onTerminalChange() {
  172. markNeedsLayout();
  173. }
  174. void _onControllerUpdate() {
  175. markNeedsLayout();
  176. }
  177. @override
  178. final isRepaintBoundary = true;
  179. @override
  180. void attach(PipelineOwner owner) {
  181. super.attach(owner);
  182. _offset.addListener(_onScroll);
  183. _terminal.addListener(_onTerminalChange);
  184. _controller.addListener(_onControllerUpdate);
  185. _focusNode.addListener(_onFocusChange);
  186. }
  187. @override
  188. void detach() {
  189. super.detach();
  190. _offset.removeListener(_onScroll);
  191. _terminal.removeListener(_onTerminalChange);
  192. _controller.removeListener(_onControllerUpdate);
  193. _focusNode.removeListener(_onFocusChange);
  194. }
  195. @override
  196. bool hitTestSelf(Offset position) {
  197. return true;
  198. }
  199. @override
  200. void systemFontsDidChange() {
  201. _updateCharSize();
  202. _paragraphCache.clear();
  203. super.systemFontsDidChange();
  204. }
  205. @override
  206. void performLayout() {
  207. size = constraints.biggest;
  208. _updateViewportSize();
  209. _updateScrollOffset();
  210. if (_stickToBottom) {
  211. _offset.correctBy(_maxScrollExtent - _scrollOffset);
  212. }
  213. SchedulerBinding.instance
  214. .addPostFrameCallback((_) => _notifyEditableRect());
  215. }
  216. /// Total height of the terminal in pixels. Includes scrollback buffer.
  217. double get _terminalHeight =>
  218. _terminal.buffer.lines.length * _charSize.height;
  219. /// The distance from the top of the terminal to the top of the viewport.
  220. // double get _scrollOffset => _offset.pixels;
  221. double get _scrollOffset {
  222. // return _offset.pixels ~/ _charSize.height * _charSize.height;
  223. return _offset.pixels;
  224. }
  225. /// Get the top-left corner of the cell at [cellOffset] in pixels.
  226. Offset getOffset(CellOffset cellOffset) {
  227. final row = cellOffset.y;
  228. final col = cellOffset.x;
  229. final x = col * _charSize.width;
  230. final y = row * _charSize.height;
  231. return Offset(x + _padding.left, y + _padding.top - _scrollOffset);
  232. }
  233. /// Get the [CellOffset] of the cell that [offset] is in.
  234. CellOffset getCellOffset(Offset offset) {
  235. final x = offset.dx - _padding.left;
  236. final y = offset.dy - _padding.top + _scrollOffset;
  237. final row = y ~/ _charSize.height;
  238. final col = x ~/ _charSize.width;
  239. return CellOffset(col, row);
  240. }
  241. /// Selects entire words in the terminal that contains [from] and [to].
  242. void selectWord(Offset from, [Offset? to]) {
  243. final fromOffset = getCellOffset(from);
  244. final fromBoundary = _terminal.buffer.getWordBoundary(fromOffset);
  245. if (fromBoundary == null) return;
  246. if (to == null) {
  247. _controller.setSelection(fromBoundary);
  248. } else {
  249. final toOffset = getCellOffset(to);
  250. final toBoundary = _terminal.buffer.getWordBoundary(toOffset);
  251. if (toBoundary == null) return;
  252. _controller.setSelection(fromBoundary.merge(toBoundary));
  253. }
  254. }
  255. /// Selects characters in the terminal that starts from [from] to [to]. At
  256. /// least one cell is selected even if [from] and [to] are same.
  257. void selectCharacters(Offset from, [Offset? to]) {
  258. final fromPosition = getCellOffset(from);
  259. if (to == null) {
  260. _controller.setSelectionRange(fromPosition, fromPosition);
  261. } else {
  262. var toPosition = getCellOffset(to);
  263. if (toPosition.x >= fromPosition.x) {
  264. toPosition = CellOffset(toPosition.x + 1, toPosition.y);
  265. }
  266. _controller.setSelectionRange(fromPosition, toPosition);
  267. }
  268. }
  269. /// Send a mouse event at [offset] with [button] being currently in [buttonState].
  270. bool mouseEvent(
  271. TerminalMouseButton button,
  272. TerminalMouseButtonState buttonState,
  273. Offset offset,
  274. ) {
  275. final position = getCellOffset(offset);
  276. return _terminal.mouseInput(button, buttonState, position);
  277. }
  278. void _notifyEditableRect() {
  279. final cursor = localToGlobal(_cursorOffset);
  280. final rect = Rect.fromLTRB(
  281. cursor.dx,
  282. cursor.dy,
  283. size.width,
  284. cursor.dy + _charSize.height,
  285. );
  286. final caretRect = cursor & _charSize;
  287. _onEditableRect?.call(rect, caretRect);
  288. }
  289. /// Update the viewport size in cells based on the current widget size in
  290. /// pixels.
  291. void _updateViewportSize() {
  292. if (size <= _charSize) {
  293. return;
  294. }
  295. final viewportSize = TerminalSize(
  296. size.width ~/ _charSize.width,
  297. _viewportHeight ~/ _charSize.height,
  298. );
  299. if (_viewportSize != viewportSize) {
  300. _viewportSize = viewportSize;
  301. _resizeTerminalIfNeeded();
  302. }
  303. }
  304. /// Notify the underlying terminal that the viewport size has changed.
  305. void _resizeTerminalIfNeeded() {
  306. if (_autoResize && _viewportSize != null) {
  307. _terminal.resize(
  308. _viewportSize!.width,
  309. _viewportSize!.height,
  310. _charSize.width.round(),
  311. _charSize.height.round(),
  312. );
  313. }
  314. }
  315. /// Update the scroll offset based on the current terminal state. This should
  316. /// be called in [performLayout] after the viewport size has been updated.
  317. void _updateScrollOffset() {
  318. _offset.applyViewportDimension(_viewportHeight);
  319. _offset.applyContentDimensions(0, _maxScrollExtent);
  320. }
  321. bool get _isComposingText {
  322. return _composingText != null && _composingText!.isNotEmpty;
  323. }
  324. bool get _shouldShowCursor {
  325. return _terminal.cursorVisibleMode || _alwaysShowCursor || _isComposingText;
  326. }
  327. double get _viewportHeight {
  328. return size.height - _padding.vertical;
  329. }
  330. double get _maxScrollExtent {
  331. return max(_terminalHeight - _viewportHeight, 0.0);
  332. }
  333. double get _lineOffset {
  334. return -_scrollOffset + _padding.top;
  335. }
  336. Offset get _cursorOffset {
  337. return Offset(
  338. _terminal.buffer.cursorX * _charSize.width,
  339. _terminal.buffer.absoluteCursorY * _charSize.height + _lineOffset,
  340. );
  341. }
  342. /// The cached for cells in the terminal. Should be cleared when the same
  343. /// cell no longer produces the same visual output. For example, when
  344. /// [_textStyle] is changed, or when the system font changes.
  345. final _paragraphCache = ParagraphCache(10240);
  346. @override
  347. void paint(PaintingContext context, Offset offset) {
  348. _paint(context, offset);
  349. context.setWillChangeHint();
  350. }
  351. void _paint(PaintingContext context, Offset offset) {
  352. final canvas = context.canvas;
  353. final lines = _terminal.buffer.lines;
  354. final charHeight = _charSize.height;
  355. final firstLineOffset = _scrollOffset - _padding.top;
  356. final lastLineOffset = _scrollOffset + size.height + _padding.bottom;
  357. final firstLine = firstLineOffset ~/ charHeight;
  358. final lastLine = lastLineOffset ~/ charHeight;
  359. final effectFirstLine = firstLine.clamp(0, lines.length - 1);
  360. final effectLastLine = lastLine.clamp(0, lines.length - 1);
  361. for (var i = effectFirstLine; i <= effectLastLine; i++) {
  362. _paintLine(
  363. canvas,
  364. lines[i],
  365. offset.translate(0, (i * charHeight + _lineOffset).truncateToDouble()),
  366. );
  367. }
  368. if (_terminal.buffer.absoluteCursorY >= effectFirstLine &&
  369. _terminal.buffer.absoluteCursorY <= effectLastLine) {
  370. final cursorOffset = offset + _cursorOffset;
  371. if (_isComposingText) {
  372. _paintComposingText(canvas, cursorOffset);
  373. }
  374. if (_shouldShowCursor) {
  375. _paintCursor(canvas, cursorOffset);
  376. }
  377. }
  378. if (_controller.selection != null) {
  379. _paintSelection(
  380. canvas,
  381. _controller.selection!,
  382. effectFirstLine,
  383. effectLastLine,
  384. );
  385. }
  386. }
  387. /// Paints the cursor based on the current cursor type.
  388. void _paintCursor(Canvas canvas, Offset offset) {
  389. final paint = Paint()
  390. ..color = _theme.cursor
  391. ..strokeWidth = 1;
  392. if (!_focusNode.hasFocus) {
  393. paint.style = PaintingStyle.stroke;
  394. canvas.drawRect(offset & _charSize, paint);
  395. return;
  396. }
  397. switch (_cursorType) {
  398. case TerminalCursorType.block:
  399. paint.style = PaintingStyle.fill;
  400. canvas.drawRect(offset & _charSize, paint);
  401. return;
  402. case TerminalCursorType.underline:
  403. return canvas.drawLine(
  404. Offset(offset.dx, _charSize.height - 1),
  405. Offset(offset.dx + _charSize.width, _charSize.height - 1),
  406. paint,
  407. );
  408. case TerminalCursorType.verticalBar:
  409. return canvas.drawLine(
  410. Offset(offset.dx, 0),
  411. Offset(offset.dx, _charSize.height),
  412. paint,
  413. );
  414. }
  415. }
  416. /// Paints the text that is currently being composed in IME to [canvas] at
  417. /// [offset]. [offset] is usually the cursor position.
  418. void _paintComposingText(Canvas canvas, Offset offset) {
  419. final composingText = _composingText;
  420. if (composingText == null) {
  421. return;
  422. }
  423. final style = _textStyle.toTextStyle(
  424. color: _resolveForegroundColor(_terminal.cursor.foreground),
  425. backgroundColor: _theme.background,
  426. underline: true,
  427. );
  428. final builder = ParagraphBuilder(style.getParagraphStyle());
  429. builder.addPlaceholder(
  430. offset.dx,
  431. _charSize.height,
  432. PlaceholderAlignment.middle,
  433. );
  434. builder.pushStyle(style.getTextStyle(textScaleFactor: _textScaleFactor));
  435. builder.addText(composingText);
  436. final paragraph = builder.build();
  437. paragraph.layout(ParagraphConstraints(width: size.width));
  438. canvas.drawParagraph(paragraph, Offset(0, offset.dy));
  439. }
  440. /// Paints [line] to [canvas] at [offset]. The x offset of [offset] is usually
  441. /// 0, and the y offset is the top of the line.
  442. void _paintLine(Canvas canvas, BufferLine line, Offset offset) {
  443. final cellData = CellData.empty();
  444. final cellWidth = _charSize.width;
  445. final visibleCells = size.width ~/ cellWidth + 1;
  446. final effectCells = min(visibleCells, line.length);
  447. for (var i = 0; i < effectCells; i++) {
  448. line.getCellData(i, cellData);
  449. final charWidth = cellData.content >> CellContent.widthShift;
  450. final cellOffset = offset.translate(i * cellWidth, 0);
  451. _paintCellBackground(canvas, cellOffset, cellData);
  452. _paintCellForeground(canvas, cellOffset, cellData);
  453. if (charWidth == 2) {
  454. i++;
  455. }
  456. }
  457. }
  458. void _paintSelection(
  459. Canvas canvas,
  460. BufferRange selection,
  461. int firstLine,
  462. int lastLine,
  463. ) {
  464. for (final segment in selection.toSegments()) {
  465. if (segment.line >= _terminal.buffer.lines.length) {
  466. break;
  467. }
  468. if (segment.line < firstLine) {
  469. continue;
  470. }
  471. if (segment.line > lastLine) {
  472. break;
  473. }
  474. final start = segment.start ?? 0;
  475. final end = segment.end ?? _terminal.viewWidth;
  476. final startOffset = Offset(
  477. start * _charSize.width,
  478. segment.line * _charSize.height + _lineOffset,
  479. );
  480. final endOffset = Offset(
  481. end * _charSize.width,
  482. (segment.line + 1) * _charSize.height + _lineOffset,
  483. );
  484. final paint = Paint()
  485. ..color = _theme.selection
  486. ..strokeWidth = 1;
  487. canvas.drawRect(
  488. Rect.fromPoints(startOffset, endOffset),
  489. paint,
  490. );
  491. }
  492. }
  493. /// Paints the character in the cell represented by [cellData] to [canvas] at
  494. /// [offset].
  495. @pragma('vm:prefer-inline')
  496. void _paintCellForeground(Canvas canvas, Offset offset, CellData cellData) {
  497. final charCode = cellData.content & CellContent.codepointMask;
  498. if (charCode == 0) return;
  499. final cacheKey = cellData.getHash() ^ _textScaleFactor.hashCode;
  500. var paragraph = _paragraphCache.getLayoutFromCache(cacheKey);
  501. if (paragraph == null) {
  502. final cellFlags = cellData.flags;
  503. var color = cellFlags & CellFlags.inverse == 0
  504. ? _resolveForegroundColor(cellData.foreground)
  505. : _resolveBackgroundColor(cellData.background);
  506. if (cellData.flags & CellFlags.faint != 0) {
  507. color = color.withOpacity(0.5);
  508. }
  509. final style = _textStyle.toTextStyle(
  510. color: color,
  511. bold: cellFlags & CellFlags.bold != 0,
  512. italic: cellFlags & CellFlags.italic != 0,
  513. underline: cellFlags & CellFlags.underline != 0,
  514. );
  515. // Flutter does not draw an underline below a space which is not between
  516. // other regular characters. As only single characters are drawn, this
  517. // will never produce an underline below a space in the terminal. As a
  518. // workaround the regular space CodePoint 0x20 is replaced with
  519. // the CodePoint 0xA0. This is a non breaking space and a underline can be
  520. // drawn below it.
  521. var char = String.fromCharCode(charCode);
  522. if (cellFlags & CellFlags.underline != 0 && charCode == 0x20) {
  523. char = String.fromCharCode(0xA0);
  524. }
  525. paragraph = _paragraphCache.performAndCacheLayout(
  526. char,
  527. style,
  528. _textScaleFactor,
  529. cacheKey,
  530. );
  531. }
  532. canvas.drawParagraph(paragraph, offset);
  533. }
  534. /// Paints the background of a cell represented by [cellData] to [canvas] at
  535. /// [offset].
  536. @pragma('vm:prefer-inline')
  537. void _paintCellBackground(Canvas canvas, Offset offset, CellData cellData) {
  538. late Color color;
  539. final colorType = cellData.background & CellColor.typeMask;
  540. if (cellData.flags & CellFlags.inverse != 0) {
  541. color = _resolveForegroundColor(cellData.foreground);
  542. } else if (colorType == CellColor.normal) {
  543. return;
  544. } else {
  545. color = _resolveBackgroundColor(cellData.background);
  546. }
  547. final paint = Paint()..color = color;
  548. final doubleWidth = cellData.content >> CellContent.widthShift == 2;
  549. final widthScale = doubleWidth ? 2 : 1;
  550. final size = Size(_charSize.width * widthScale + 1, _charSize.height);
  551. canvas.drawRect(offset & size, paint);
  552. }
  553. /// Get the effective foreground color for a cell from information encoded in
  554. /// [cellColor].
  555. @pragma('vm:prefer-inline')
  556. Color _resolveForegroundColor(int cellColor) {
  557. final colorType = cellColor & CellColor.typeMask;
  558. final colorValue = cellColor & CellColor.valueMask;
  559. switch (colorType) {
  560. case CellColor.normal:
  561. return _theme.foreground;
  562. case CellColor.named:
  563. case CellColor.palette:
  564. return _colorPalette[colorValue];
  565. case CellColor.rgb:
  566. default:
  567. return Color(colorValue | 0xFF000000);
  568. }
  569. }
  570. /// Get the effective background color for a cell from information encoded in
  571. /// [cellColor].
  572. @pragma('vm:prefer-inline')
  573. Color _resolveBackgroundColor(int cellColor) {
  574. final colorType = cellColor & CellColor.typeMask;
  575. final colorValue = cellColor & CellColor.valueMask;
  576. switch (colorType) {
  577. case CellColor.normal:
  578. return _theme.background;
  579. case CellColor.named:
  580. case CellColor.palette:
  581. return _colorPalette[colorValue];
  582. case CellColor.rgb:
  583. default:
  584. return Color(colorValue | 0xFF000000);
  585. }
  586. }
  587. }