94 lines
3 KiB
Dart
94 lines
3 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter/rendering.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'markdown_editing.dart';
|
|
|
|
/// Highlighted markdown source that is either read-only or editable.
|
|
class MarkdownView extends StatefulWidget {
|
|
final MarkdownController controller;
|
|
final bool readOnly;
|
|
final double fontSize;
|
|
final FocusNode? focusNode;
|
|
final ScrollController? scrollController;
|
|
|
|
const MarkdownView({
|
|
super.key,
|
|
required this.controller,
|
|
this.readOnly = true,
|
|
this.fontSize = 16,
|
|
this.focusNode,
|
|
this.scrollController,
|
|
});
|
|
|
|
@override
|
|
MarkdownViewState createState() => MarkdownViewState();
|
|
}
|
|
|
|
class MarkdownViewState extends State<MarkdownView> {
|
|
final _fieldKey = GlobalKey();
|
|
final _formatters = [MarkdownInputFormatter()];
|
|
|
|
/// Scrolls [offset] into view without focusing the field, so read mode
|
|
/// doesn't bring up the keyboard.
|
|
void revealOffset(int offset) {
|
|
final editable = _findEditable(_fieldKey.currentContext?.findRenderObject());
|
|
if (editable == null) return;
|
|
final caret = editable.getLocalRectForCaret(TextPosition(offset: offset));
|
|
editable.showOnScreen(
|
|
rect: Rect.fromLTRB(
|
|
caret.left, caret.top - 80, caret.right, caret.bottom + 80),
|
|
duration: const Duration(milliseconds: 200),
|
|
curve: Curves.easeOut,
|
|
);
|
|
}
|
|
|
|
/// Text offset at the top-left of the visible area.
|
|
int firstVisibleOffset() {
|
|
final box = _fieldKey.currentContext?.findRenderObject() as RenderBox?;
|
|
final editable = _findEditable(box);
|
|
if (box == null || editable == null) return 0;
|
|
return editable
|
|
.getPositionForPoint(box.localToGlobal(const Offset(1, 1)))
|
|
.offset;
|
|
}
|
|
|
|
RenderEditable? _findEditable(RenderObject? node) {
|
|
if (node is RenderEditable) return node;
|
|
RenderEditable? found;
|
|
node?.visitChildren((child) => found ??= _findEditable(child));
|
|
return found;
|
|
}
|
|
|
|
void _indent({required bool outdent}) {
|
|
final controller = widget.controller;
|
|
controller.value = indentLines(controller.value, outdent: outdent);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
// Tab would otherwise move focus out of the editor on desktop.
|
|
return CallbackShortcuts(
|
|
bindings: widget.readOnly
|
|
? const {}
|
|
: {
|
|
const SingleActivator(LogicalKeyboardKey.tab): () =>
|
|
_indent(outdent: false),
|
|
const SingleActivator(LogicalKeyboardKey.tab, shift: true): () =>
|
|
_indent(outdent: true),
|
|
},
|
|
child: TextField(
|
|
key: _fieldKey,
|
|
controller: widget.controller,
|
|
focusNode: widget.focusNode,
|
|
scrollController: widget.scrollController,
|
|
readOnly: widget.readOnly,
|
|
showCursor: !widget.readOnly,
|
|
expands: true,
|
|
maxLines: null,
|
|
decoration: null,
|
|
inputFormatters: widget.readOnly ? null : _formatters,
|
|
style: TextStyle(fontSize: widget.fontSize),
|
|
),
|
|
);
|
|
}
|
|
}
|