Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions super_editor/example/lib/demos/example_editor/example_editor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,7 @@ class _ExampleEditorState extends State<ExampleEditor> {
DefaultCaretOverlayBuilder(
const CaretStyle().copyWith(color: isLight ? Colors.black : Colors.redAccent),
),
_TokenBoundsOverlay(),
],
selectionStyle: isLight
? defaultSelectionStyle
Expand Down Expand Up @@ -524,3 +525,106 @@ final _darkModeStyles = [
},
),
];

class _TokenBoundsOverlay implements DocumentLayerBuilder {
@override
Widget build(BuildContext context, SuperEditorContext editContext) {
print("Building token bounds overlay");
return _AttributionBounds(
layout: editContext.documentLayout,
document: editContext.document,
selector: (a) => a == boldAttribution,
);
}
}

class _AttributionBounds extends StatefulWidget {
const _AttributionBounds({
Key? key,
required this.layout,
required this.document,
required this.selector,
}) : super(key: key);

final DocumentLayout layout;
final Document document;
final AttributionBoundsSelector selector;

@override
State<_AttributionBounds> createState() => _AttributionBoundsState();
}

class _AttributionBoundsState extends State<_AttributionBounds> {
final _bounds = <Rect>{};

@override
void initState() {
super.initState();

_findBounds();
widget.document.addListener(_onDocumentChange);
}

@override
void dispose() {
widget.document.removeListener(_onDocumentChange);
super.dispose();
}

void _onDocumentChange(changeLog) {
if (!mounted) {
return;
}

setState(() {
_findBounds();
});
}

void _findBounds() {
_bounds.clear();

for (final node in widget.document.nodes) {
if (node is! TextNode) {
continue;
}

final spans = node.text.getAttributionSpansInRange(
attributionFilter: widget.selector,
range: SpanRange(start: 0, end: node.text.text.length - 1),
);

final documentRanges = spans.map(
(span) => DocumentRange(
start: DocumentPosition(nodeId: node.id, nodePosition: TextNodePosition(offset: span.start)),
end: DocumentPosition(nodeId: node.id, nodePosition: TextNodePosition(offset: span.end + 1)),
),
);

_bounds.addAll(documentRanges.map(
(range) => widget.layout.getRectForSelection(range.start, range.end) ?? Rect.zero,
));
}
}

@override
Widget build(BuildContext context) {
return IgnorePointer(
child: Stack(
children: [
for (final bound in _bounds) //
Positioned.fromRect(
rect: bound,
child: DecoratedBox(
decoration: BoxDecoration(
border: Border.all(color: Colors.blue),
),
),
),
],
),
);
}
}

typedef AttributionBoundsSelector = bool Function(Attribution attribution);
2 changes: 1 addition & 1 deletion super_editor/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ Future<void> main() async {
// editorDocLog,
// editorStyleLog,
// textFieldLog,
appLog,
// appLog,
});

runApp(SuperEditorDemoApp());
Expand Down
5 changes: 3 additions & 2 deletions super_editor/lib/src/default_editor/super_editor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -707,8 +707,9 @@ class SuperEditorState extends State<SuperEditor> {
showDebugPaint: widget.debugPaint.layout,
),
overlays: [
for (final overlayBuilder in widget.documentOverlayBuilders) //
overlayBuilder.build(context, editContext),
if (_docLayoutKey.currentState != null) //
for (final overlayBuilder in widget.documentOverlayBuilders) //
overlayBuilder.build(context, editContext),
],
);
case DocumentGestureMode.android:
Expand Down
189 changes: 189 additions & 0 deletions super_text_layout/example/lib/attributed_follower.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import 'package:attributed_text/attributed_text.dart';
import 'package:flutter/material.dart';
import 'package:follow_the_leader/follow_the_leader.dart';
import 'package:overlord/follow_the_leader.dart';
import 'package:overlord/overlord.dart';
import 'package:super_text_layout/super_text_layout.dart';

class AttributedFollowerDemo extends StatefulWidget {
const AttributedFollowerDemo({super.key});

@override
State<AttributedFollowerDemo> createState() => _AttributedFollowerDemoState();
}

class _AttributedFollowerDemoState extends State<AttributedFollowerDemo> {
static const token = NamedAttribution("token");

static const _textStyle = TextStyle(
color: Color(0xFF444444),
fontFamily: 'Roboto',
fontSize: 20,
height: 1.4,
);

final _attributedText = AttributedText(
text: "This is some text that's tokenized like tags and mentions",
spans: AttributedSpans(attributions: [
const SpanMarker(attribution: token, offset: 8, markerType: SpanMarkerType.start),
const SpanMarker(attribution: token, offset: 16, markerType: SpanMarkerType.end),
const SpanMarker(attribution: token, offset: 25, markerType: SpanMarkerType.start),
const SpanMarker(attribution: token, offset: 33, markerType: SpanMarkerType.end),
const SpanMarker(attribution: token, offset: 40, markerType: SpanMarkerType.start),
const SpanMarker(attribution: token, offset: 43, markerType: SpanMarkerType.end),
const SpanMarker(attribution: token, offset: 49, markerType: SpanMarkerType.start),
const SpanMarker(attribution: token, offset: 56, markerType: SpanMarkerType.end),
]),
);
late final TextSpan _richText;
Set<TextRange> _tokenRanges = {};

final _leaderLink = LeaderLink();

int _spanToFollow = 0;

@override
void initState() {
super.initState();

_createRichText();
_calculateTokenRanges();
}

void _createRichText() {
_richText = TextSpan(children: [], style: _textStyle);
int textOffset = 0;

final spans = _attributedText.getAttributionSpansInRange(
attributionFilter: (a) => a == token,
range: SpanRange(start: 0, end: _attributedText.text.length),
);

for (final span in spans) {
if (span.start > textOffset) {
_richText.children!.add(
TextSpan(text: _attributedText.text.substring(textOffset, span.start)),
);
textOffset = span.start;
}

_richText.children!.add(TextSpan(
text: _attributedText.text.substring(span.start, span.end + 1),
style: const TextStyle(color: Colors.blue),
));
textOffset = span.end + 1;
}
}

void _calculateTokenRanges() {
_tokenRanges = _attributedText
.getAttributionSpansInRange(
attributionFilter: (a) => a == token, range: SpanRange(start: 0, end: _attributedText.text.length))
.map((span) => TextSelection(baseOffset: span.start, extentOffset: span.end + 1))
.toSet();
}

@override
Widget build(BuildContext context) {
return Stack(
children: [
Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildText(),
_buildSpanSelectionControls(),
],
),
Follower.withAligner(
link: _leaderLink,
aligner: CupertinoPopoverToolbarAligner(),
child: CupertinoPopoverToolbar(
focalPoint: LeaderMenuFocalPoint(link: _leaderLink),
children: [
CupertinoPopoverToolbarMenuItem(label: "Copy"),
CupertinoPopoverToolbarMenuItem(label: "Cut"),
CupertinoPopoverToolbarMenuItem(label: "Paste"),
],
),
),
],
);
}

Widget _buildText() {
return SuperText(
richText: _richText,
layerBeneathBuilder: boundingBoxesLayer(_tokenRanges, (i) => i == _spanToFollow ? _leaderLink : null),
);
}

Widget _buildSpanSelectionControls() {
return Row(
children: [
const Spacer(),
IconButton(
icon: const Icon(Icons.chevron_left),
onPressed: _spanToFollow > 0
? () {
setState(() {
_spanToFollow -= 1;
});
}
: null,
),
IconButton(
icon: const Icon(Icons.chevron_right),
onPressed: _spanToFollow < _tokenRanges.length - 1
? () {
setState(() {
_spanToFollow += 1;
});
}
: null,
),
],
);
}
}

/// Returns a [SuperTextLayerBuilder] that builds invisible bounding box widgets around
/// each of the given [ranges].
SuperTextLayerBuilder boundingBoxesLayer(Set<TextRange> ranges, BoundingBoxLinker linker) {
return (context, textLayout) {
final tokenRectsByAttribution = ranges
.map((range) => textLayout.getBoxesForSelection(
TextSelection(baseOffset: range.start, extentOffset: range.end),
))
.map((boxes) => boxes.map((box) => box.toRect()));

final tokenRects = [
for (final rects in tokenRectsByAttribution) //
...rects,
];

final rectWidgets = <Widget>[];
for (int i = 0; i < tokenRects.length; i += 1) {
final link = linker(i);
rectWidgets.add(
link != null
? Leader(
link: link,
child: const SizedBox(),
)
: const SizedBox(),
);
}

return Stack(
children: [
for (int i = 0; i < rectWidgets.length; i += 1)
Positioned.fromRect(
rect: tokenRects[i],
child: rectWidgets[i],
),
],
);
};
}

typedef BoundingBoxLinker = LeaderLink? Function(int spanIndex);
Loading