muzzle-velocity/lib/markdown_view.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),
),
);
}
}