dev
dart 200 lines 5.77 KB
Raw
1 import 'package:cw_core/utils/text_normalizer.dart';
2 import 'package:flutter/material.dart';
3
4 extension Compare<T> on Comparable<T> {
5 bool operator <=(T other) => compareTo(other) <= 0;
6 bool operator >=(T other) => compareTo(other) >= 0;
7 bool operator <(T other) => compareTo(other) < 0;
8 bool operator >(T other) => compareTo(other) > 0;
9 }
10
11 class Annotation implements Comparable<Annotation> {
12 Annotation({required this.range, required this.style});
13
14 final TextRange range;
15 final TextStyle style;
16
17 @override
18 int compareTo(Annotation other) => range.start.compareTo(other.range.start);
19 }
20
21 class TextAnnotation implements Comparable<TextAnnotation> {
22 TextAnnotation({required this.text, required this.style});
23
24 final TextStyle style;
25 final String text;
26
27 @override
28 int compareTo(TextAnnotation other) => text.compareTo(other.text);
29 }
30
31 class ValidatableAnnotatedEditableText extends EditableText {
32 ValidatableAnnotatedEditableText({
33 Key? key,
34 required FocusNode focusNode,
35 required TextEditingController controller,
36 // required List<String> wordList,
37 required Color cursorColor,
38 required Color backgroundCursorColor,
39 required this.validStyle,
40 required this.invalidStyle,
41 required this.words,
42 this.normalizeSeed = false,
43 TextStyle textStyle = const TextStyle(
44 color: Colors.black,
45 backgroundColor: Colors.transparent,
46 fontWeight: FontWeight.normal,
47 fontSize: 16,
48 ),
49 TextSelectionControls? selectionControls,
50 Color? selectionColor,
51 ValueChanged<String>? onChanged,
52 ValueChanged<String>? onSubmitted,
53 }) : super(
54 maxLines: null,
55 key: key,
56 focusNode: focusNode,
57 controller: controller,
58 cursorColor: cursorColor,
59 style: validStyle,
60 keyboardType: TextInputType.visiblePassword,
61 autocorrect: false,
62 autofocus: false,
63 selectionColor: selectionColor,
64 selectionControls: selectionControls,
65 backgroundCursorColor: backgroundCursorColor,
66 onChanged: onChanged,
67 onSubmitted: onSubmitted,
68 toolbarOptions: const ToolbarOptions(
69 copy: true,
70 cut: true,
71 paste: true,
72 selectAll: true,
73 ),
74 enableSuggestions: false,
75 enableInteractiveSelection: true,
76 showSelectionHandles: true,
77 showCursor: true,
78 );
79
80 final bool normalizeSeed;
81 final List<String> words;
82 final TextStyle validStyle;
83 final TextStyle invalidStyle;
84
85 @override
86 ValidatableAnnotatedEditableTextState createState() => ValidatableAnnotatedEditableTextState();
87 }
88
89 class ValidatableAnnotatedEditableTextState extends EditableTextState {
90 @override
91 ValidatableAnnotatedEditableText get widget => super.widget as ValidatableAnnotatedEditableText;
92
93 List<Annotation> getRanges() {
94 final result = <Annotation>[];
95 // Replace Ideographic Space (U+3000) with a normal space
96 final text = textEditingValue.text.replaceAll("\u3000", " ");
97 final source = text
98 .split(' ')
99 .map((word) {
100 final ranges = range(word, text);
101 final isValid = validate(word);
102
103 return ranges.map(
104 (range) => Annotation(
105 style: isValid ? widget.validStyle : widget.invalidStyle,
106 range: range,
107 ),
108 );
109 })
110 .expand((e) => e)
111 .toList();
112 source.sort();
113 Annotation? prev;
114
115 for (var item in source) {
116 Annotation? annotation;
117
118 if (prev == null) {
119 annotation = Annotation(
120 range: TextRange(start: 0, end: item.range.start),
121 style: Theme.of(context).textTheme.bodyMedium!.copyWith(
122 color: Theme.of(context).colorScheme.onSurfaceVariant,
123 backgroundColor: Colors.transparent,
124 ),
125 );
126 } else if (prev.range.end < item.range.start) {
127 annotation = Annotation(
128 range: TextRange(start: prev.range.end, end: item.range.start),
129 style: Theme.of(context).textTheme.bodyMedium!.copyWith(
130 color: Theme.of(context).colorScheme.onError,
131 backgroundColor: Colors.transparent,
132 ),
133 );
134 }
135
136 if (annotation != null) {
137 result.add(annotation);
138 result.add(item);
139 prev = item;
140 }
141 }
142
143 if (result.length > 0 && result.last.range.end < text.length) {
144 result.add(
145 Annotation(
146 range: TextRange(start: result.last.range.end, end: text.length),
147 style: Theme.of(context).textTheme.bodyMedium!.copyWith(
148 color: Theme.of(context).colorScheme.onSurfaceVariant,
149 backgroundColor: Colors.transparent,
150 ),
151 ),
152 );
153 }
154
155 return result;
156 }
157
158 bool validate(String source) =>
159 widget.words.indexOf(widget.normalizeSeed ? normalizeText(source) : source) >= 0;
160
161 List<TextRange> range(String pattern, String source) {
162 final result = <TextRange>[];
163
164 if (pattern.isEmpty || source.isEmpty) {
165 return result;
166 }
167
168 for (int index = source.indexOf(pattern);
169 index >= 0;
170 index = source.indexOf(pattern, index + 1)) {
171 final start = index;
172 final end = start + pattern.length;
173 result.add(TextRange(start: start, end: end));
174 }
175
176 return result;
177 }
178
179 @override
180 TextSpan buildTextSpan() {
181 final text = textEditingValue.text;
182 final ranges = getRanges().toSet();
183
184 if (ranges.isNotEmpty) {
185 return TextSpan(
186 style: widget.style,
187 children: ranges
188 .map(
189 (item) => TextSpan(
190 style: item.style,
191 text: item.range.textInside(text),
192 ),
193 )
194 .toList(),
195 );
196 }
197
198 return TextSpan(style: widget.style, text: text);
199 }
200 }