saminsohag

theme extantions added

  1 +## 0.1.11
  2 +
  3 +* `GptMarkdownTheme` and `GptMarkdownThemeData` classes added.
  4 +
1 ## 0.1.10 5 ## 0.1.10
2 6
3 * components are now selectable. 7 * components are now selectable.
@@ -4,7 +4,9 @@ import 'package:desktop_drop/desktop_drop.dart'; @@ -4,7 +4,9 @@ import 'package:desktop_drop/desktop_drop.dart';
4 import 'package:flutter/material.dart'; 4 import 'package:flutter/material.dart';
5 import 'package:gpt_markdown/gpt_markdown.dart'; 5 import 'package:gpt_markdown/gpt_markdown.dart';
6 import 'package:flutter_math_fork/flutter_math.dart'; 6 import 'package:flutter_math_fork/flutter_math.dart';
  7 +import 'package:gpt_markdown/theme.dart';
7 import 'package:watcher/watcher.dart'; 8 import 'package:watcher/watcher.dart';
  9 +import 'selectable_adapter.dart';
8 10
9 void main() { 11 void main() {
10 runApp(const MyApp()); 12 runApp(const MyApp());
@@ -31,10 +33,14 @@ class _MyAppState extends State<MyApp> { @@ -31,10 +33,14 @@ class _MyAppState extends State<MyApp> {
31 colorSchemeSeed: Colors.blue, 33 colorSchemeSeed: Colors.blue,
32 ), 34 ),
33 darkTheme: ThemeData( 35 darkTheme: ThemeData(
34 - useMaterial3: true,  
35 - brightness: Brightness.dark,  
36 - colorSchemeSeed: Colors.blue,  
37 - ), 36 + useMaterial3: true,
  37 + brightness: Brightness.dark,
  38 + colorSchemeSeed: Colors.blue,
  39 + extensions: [
  40 + GptMarkdownThemeData(
  41 + highlightColor: Colors.red,
  42 + ),
  43 + ]),
38 home: MyHomePage( 44 home: MyHomePage(
39 title: 'GptMarkdown', 45 title: 'GptMarkdown',
40 onPressed: () { 46 onPressed: () {
@@ -147,241 +153,254 @@ Markdown and LaTeX can be powerful tools for formatting text and mathematical ex @@ -147,241 +153,254 @@ Markdown and LaTeX can be powerful tools for formatting text and mathematical ex
147 153
148 @override 154 @override
149 Widget build(BuildContext context) { 155 Widget build(BuildContext context) {
150 - return Scaffold(  
151 - appBar: AppBar(  
152 - title: Text(widget.title),  
153 - actions: [  
154 - IconButton(  
155 - onPressed: () {  
156 - setState(() {  
157 - selectable = !selectable;  
158 - });  
159 - },  
160 - icon: Icon(  
161 - Icons.select_all_outlined,  
162 - color: selectable  
163 - ? Theme.of(context).colorScheme.onSurfaceVariant  
164 - : Theme.of(context).colorScheme.onSurface.withOpacity(0.38),  
165 - ),  
166 - ),  
167 - IconButton(  
168 - onPressed: () {  
169 - setState(() {  
170 - _direction = TextDirection.values[(_direction.index + 1) % 2];  
171 - });  
172 - },  
173 - icon: const [Text("LTR"), Text("RTL")][_direction.index],  
174 - ),  
175 - IconButton(  
176 - onPressed: widget.onPressed,  
177 - icon: const Icon(Icons.sunny),  
178 - ),  
179 - IconButton(  
180 - onPressed: () => setState(() {  
181 - writingMod = !writingMod;  
182 - }),  
183 - icon:  
184 - Icon(writingMod ? Icons.arrow_drop_down : Icons.arrow_drop_up),  
185 - ),  
186 - ], 156 + return GptMarkdownTheme(
  157 + gptThemeData: GptMarkdownTheme.of(context).copyWith(
  158 + highlightColor: Colors.purple,
187 ), 159 ),
188 - body: DropTarget(  
189 - onDragDone: (details) {  
190 - var files = details.files;  
191 - if (files.length != 1) {  
192 - return;  
193 - }  
194 - var file = files[0];  
195 - String path = file.path;  
196 - this.file = File(path);  
197 - load();  
198 - },  
199 - child: Stack(  
200 - children: [  
201 - Column(  
202 - children: [  
203 - Expanded(  
204 - child: ListView(  
205 - children: [  
206 - AnimatedBuilder(  
207 - animation: _controller,  
208 - builder: (context, _) {  
209 - return Container(  
210 - padding: const EdgeInsets.all(8),  
211 - decoration: BoxDecoration(  
212 - border: Border.all(  
213 - width: 1,  
214 - color: Theme.of(context).colorScheme.outline),  
215 - ),  
216 - child: Theme(  
217 - data: Theme.of(context),  
218 - // .copyWith(  
219 - // textTheme: const TextTheme(  
220 - // // For H1.  
221 - // headlineLarge: TextStyle(fontSize: 55),  
222 - // // For H2.  
223 - // headlineMedium: TextStyle(fontSize: 45),  
224 - // // For H3.  
225 - // headlineSmall: TextStyle(fontSize: 35),  
226 - // // For H4.  
227 - // titleLarge: TextStyle(fontSize: 25),  
228 - // // For H5.  
229 - // titleMedium: TextStyle(fontSize: 15),  
230 - // // For H6.  
231 - // titleSmall: TextStyle(fontSize: 10),  
232 - // ),  
233 - // ),  
234 - child: Builder(  
235 - builder: (context) {  
236 - Widget child = TexMarkdown(  
237 - _controller.text,  
238 - textDirection: _direction,  
239 - onLinkTab: (url, title) {  
240 - debugPrint(url);  
241 - debugPrint(title);  
242 - },  
243 - textAlign: TextAlign.justify,  
244 - textScaler: const TextScaler.linear(1),  
245 - style: const TextStyle(  
246 - fontSize: 15,  
247 - ),  
248 - latexWorkaround: (tex) {  
249 - List<String> stack = [];  
250 - tex = tex.splitMapJoin(  
251 - RegExp(r"\\text\{|\{|\}|\_"),  
252 - onMatch: (p) {  
253 - String input = p[0] ?? "";  
254 - if (input == r"\text{") {  
255 - stack.add(input);  
256 - }  
257 - if (stack.isNotEmpty) {  
258 - if (input == r"{") { 160 + child: Scaffold(
  161 + appBar: AppBar(
  162 + title: Text(widget.title),
  163 + actions: [
  164 + IconButton(
  165 + onPressed: () {
  166 + setState(() {
  167 + selectable = !selectable;
  168 + });
  169 + },
  170 + icon: Icon(
  171 + Icons.select_all_outlined,
  172 + color: selectable
  173 + ? Theme.of(context).colorScheme.onSurfaceVariant
  174 + : Theme.of(context).colorScheme.onSurface.withOpacity(0.38),
  175 + ),
  176 + ),
  177 + IconButton(
  178 + onPressed: () {
  179 + setState(() {
  180 + _direction = TextDirection.values[(_direction.index + 1) % 2];
  181 + });
  182 + },
  183 + icon: const [Text("LTR"), Text("RTL")][_direction.index],
  184 + ),
  185 + IconButton(
  186 + onPressed: widget.onPressed,
  187 + icon: const Icon(Icons.sunny),
  188 + ),
  189 + IconButton(
  190 + onPressed: () => setState(() {
  191 + writingMod = !writingMod;
  192 + }),
  193 + icon: Icon(
  194 + writingMod ? Icons.arrow_drop_down : Icons.arrow_drop_up),
  195 + ),
  196 + ],
  197 + ),
  198 + body: DropTarget(
  199 + onDragDone: (details) {
  200 + var files = details.files;
  201 + if (files.length != 1) {
  202 + return;
  203 + }
  204 + var file = files[0];
  205 + String path = file.path;
  206 + this.file = File(path);
  207 + load();
  208 + },
  209 + child: Stack(
  210 + children: [
  211 + Column(
  212 + children: [
  213 + Expanded(
  214 + child: ListView(
  215 + children: [
  216 + AnimatedBuilder(
  217 + animation: _controller,
  218 + builder: (context, _) {
  219 + return Container(
  220 + padding: const EdgeInsets.all(8),
  221 + decoration: BoxDecoration(
  222 + border: Border.all(
  223 + width: 1,
  224 + color:
  225 + Theme.of(context).colorScheme.outline),
  226 + ),
  227 + child: Theme(
  228 + data: Theme.of(context),
  229 + // .copyWith(
  230 + // textTheme: const TextTheme(
  231 + // // For H1.
  232 + // headlineLarge: TextStyle(fontSize: 55),
  233 + // // For H2.
  234 + // headlineMedium: TextStyle(fontSize: 45),
  235 + // // For H3.
  236 + // headlineSmall: TextStyle(fontSize: 35),
  237 + // // For H4.
  238 + // titleLarge: TextStyle(fontSize: 25),
  239 + // // For H5.
  240 + // titleMedium: TextStyle(fontSize: 15),
  241 + // // For H6.
  242 + // titleSmall: TextStyle(fontSize: 10),
  243 + // ),
  244 + // ),
  245 + child: Builder(
  246 + builder: (context) {
  247 + Widget child = TexMarkdown(
  248 + _controller.text,
  249 + textDirection: _direction,
  250 + onLinkTab: (url, title) {
  251 + debugPrint(url);
  252 + debugPrint(title);
  253 + },
  254 + textAlign: TextAlign.justify,
  255 + textScaler: const TextScaler.linear(1),
  256 + style: const TextStyle(
  257 + fontSize: 15,
  258 + ),
  259 + latexWorkaround: (tex) {
  260 + List<String> stack = [];
  261 + tex = tex.splitMapJoin(
  262 + RegExp(r"\\text\{|\{|\}|\_"),
  263 + onMatch: (p) {
  264 + String input = p[0] ?? "";
  265 + if (input == r"\text{") {
259 stack.add(input); 266 stack.add(input);
260 } 267 }
261 - if (input == r"}") {  
262 - stack.removeLast(); 268 + if (stack.isNotEmpty) {
  269 + if (input == r"{") {
  270 + stack.add(input);
  271 + }
  272 + if (input == r"}") {
  273 + stack.removeLast();
  274 + }
  275 + if (input == r"_") {
  276 + return r"\_";
  277 + }
263 } 278 }
264 - if (input == r"_") {  
265 - return r"\_";  
266 - }  
267 - }  
268 - return input;  
269 - },  
270 - );  
271 - return tex.replaceAllMapped(  
272 - RegExp(r"align\*"),  
273 - (match) => "aligned");  
274 - },  
275 - latexBuilder:  
276 - (contex, tex, textStyle, inline) {  
277 - if (tex.contains(r"\begin{tabular}")) {  
278 - // return table.  
279 - String tableString = "|${(RegExp(  
280 - r"^\\begin\{tabular\}\{.*?\}(.*?)\\end\{tabular\}$",  
281 - multiLine: true,  
282 - dotAll: true,  
283 - ).firstMatch(tex)?[1] ?? "").trim()}|";  
284 - tableString = tableString  
285 - .replaceAll(r"\\", "|\n|")  
286 - .replaceAll(r"\hline", "")  
287 - .replaceAll(  
288 - RegExp(r"(?<!\\)&"), "|");  
289 - var tableStringList = tableString  
290 - .split("\n")  
291 - ..insert(1, "|---|");  
292 - tableString =  
293 - tableStringList.join("\n");  
294 - return TexMarkdown(tableString);  
295 - }  
296 - var controller = ScrollController();  
297 - Widget child = Math.tex(  
298 - tex,  
299 - textStyle: textStyle,  
300 - );  
301 - if (!inline) {  
302 - child = Padding(  
303 - padding: const EdgeInsets.all(0.0),  
304 - child: Material(  
305 - color: Theme.of(context)  
306 - .colorScheme  
307 - .onInverseSurface,  
308 - child: Padding(  
309 - padding:  
310 - const EdgeInsets.all(8.0),  
311 - child: Scrollbar(  
312 - controller: controller,  
313 - child: SingleChildScrollView( 279 + return input;
  280 + },
  281 + );
  282 + return tex.replaceAllMapped(
  283 + RegExp(r"align\*"),
  284 + (match) => "aligned");
  285 + },
  286 + latexBuilder:
  287 + (contex, tex, textStyle, inline) {
  288 + if (tex.contains(r"\begin{tabular}")) {
  289 + // return table.
  290 + String tableString = "|${(RegExp(
  291 + r"^\\begin\{tabular\}\{.*?\}(.*?)\\end\{tabular\}$",
  292 + multiLine: true,
  293 + dotAll: true,
  294 + ).firstMatch(tex)?[1] ?? "").trim()}|";
  295 + tableString = tableString
  296 + .replaceAll(r"\\", "|\n|")
  297 + .replaceAll(r"\hline", "")
  298 + .replaceAll(
  299 + RegExp(r"(?<!\\)&"), "|");
  300 + var tableStringList = tableString
  301 + .split("\n")
  302 + ..insert(1, "|---|");
  303 + tableString =
  304 + tableStringList.join("\n");
  305 + return TexMarkdown(tableString);
  306 + }
  307 + var controller = ScrollController();
  308 + Widget child = Math.tex(
  309 + tex,
  310 + textStyle: textStyle,
  311 + );
  312 + if (!inline) {
  313 + child = Padding(
  314 + padding: const EdgeInsets.all(0.0),
  315 + child: Material(
  316 + color: Theme.of(context)
  317 + .colorScheme
  318 + .onInverseSurface,
  319 + child: Padding(
  320 + padding:
  321 + const EdgeInsets.all(8.0),
  322 + child: Scrollbar(
314 controller: controller, 323 controller: controller,
315 - scrollDirection:  
316 - Axis.horizontal,  
317 - child: Math.tex(  
318 - tex,  
319 - textStyle: textStyle, 324 + child: SingleChildScrollView(
  325 + controller: controller,
  326 + scrollDirection:
  327 + Axis.horizontal,
  328 + child: Math.tex(
  329 + tex,
  330 + textStyle: textStyle,
  331 + ),
320 ), 332 ),
321 ), 333 ),
322 ), 334 ),
323 ), 335 ),
  336 + );
  337 + }
  338 + child = SelectableAdapter(
  339 + selectedText: tex,
  340 + child: Math.tex(tex),
  341 + );
  342 + child = InkWell(
  343 + onTap: () {
  344 + debugPrint("Hello world");
  345 + },
  346 + child: child,
  347 + );
  348 + return child;
  349 + },
  350 + sourceTagBuilder:
  351 + (buildContext, string, textStyle) {
  352 + var value = int.tryParse(string);
  353 + value ??= -1;
  354 + value += 1;
  355 + return SizedBox(
  356 + height: 20,
  357 + width: 20,
  358 + child: Container(
  359 + decoration: BoxDecoration(
  360 + color: Colors.red,
  361 + borderRadius:
  362 + BorderRadius.circular(10),
  363 + ),
  364 + child:
  365 + Center(child: Text("$value")),
324 ), 366 ),
325 ); 367 );
326 - }  
327 - child = InkWell(  
328 - onTap: () {  
329 - debugPrint("Hello world");  
330 - }, 368 + },
  369 + );
  370 + if (selectable) {
  371 + child = SelectionArea(
331 child: child, 372 child: child,
332 ); 373 );
333 - return child;  
334 - },  
335 - sourceTagBuilder:  
336 - (buildContext, string, textStyle) {  
337 - var value = int.tryParse(string);  
338 - value ??= -1;  
339 - value += 1;  
340 - return SizedBox(  
341 - height: 20,  
342 - width: 20,  
343 - child: Container(  
344 - decoration: BoxDecoration(  
345 - color: Colors.red,  
346 - borderRadius:  
347 - BorderRadius.circular(10),  
348 - ),  
349 - child: Center(child: Text("$value")),  
350 - ),  
351 - );  
352 - },  
353 - );  
354 - if (selectable) {  
355 - child = SelectionArea(child: child);  
356 - }  
357 - return child;  
358 - }, 374 + }
  375 + return child;
  376 + },
  377 + ),
  378 + // child: const Text("Hello"),
359 ), 379 ),
360 - // child: const Text("Hello"),  
361 - ),  
362 - );  
363 - },  
364 - ),  
365 - ], 380 + );
  381 + },
  382 + ),
  383 + ],
  384 + ),
366 ), 385 ),
367 - ),  
368 - if (writingMod)  
369 - ConstrainedBox(  
370 - constraints: const BoxConstraints(maxHeight: 200),  
371 - child: Padding(  
372 - padding: const EdgeInsets.all(8.0),  
373 - child: TextField(  
374 - decoration: const InputDecoration(  
375 - border: OutlineInputBorder(),  
376 - label: Text("Type here:")),  
377 - maxLines: null,  
378 - controller: _controller, 386 + if (writingMod)
  387 + ConstrainedBox(
  388 + constraints: const BoxConstraints(maxHeight: 200),
  389 + child: Padding(
  390 + padding: const EdgeInsets.all(8.0),
  391 + child: TextField(
  392 + decoration: const InputDecoration(
  393 + border: OutlineInputBorder(),
  394 + label: Text("Type here:")),
  395 + maxLines: null,
  396 + controller: _controller,
  397 + ),
379 ), 398 ),
380 ), 399 ),
381 - ),  
382 - ],  
383 - ),  
384 - ], 400 + ],
  401 + ),
  402 + ],
  403 + ),
385 ), 404 ),
386 ), 405 ),
387 ); 406 );
  1 +import 'package:flutter/material.dart';
  2 +import 'package:flutter/rendering.dart';
  3 +
  4 +class SelectableAdapter extends StatelessWidget {
  5 + const SelectableAdapter(
  6 + {super.key, required this.selectedText, required this.child});
  7 +
  8 + final Widget child;
  9 + final String selectedText;
  10 +
  11 + @override
  12 + Widget build(BuildContext context) {
  13 + final SelectionRegistrar? registrar = SelectionContainer.maybeOf(context);
  14 + if (registrar == null) {
  15 + return child;
  16 + }
  17 + return MouseRegion(
  18 + cursor: SystemMouseCursors.text,
  19 + child: _SelectableAdapter(
  20 + registrar: registrar,
  21 + selectedText: selectedText,
  22 + child: child,
  23 + ),
  24 + );
  25 + }
  26 +}
  27 +
  28 +class _SelectableAdapter extends SingleChildRenderObjectWidget {
  29 + const _SelectableAdapter({
  30 + required this.registrar,
  31 + required Widget child,
  32 + required this.selectedText,
  33 + }) : super(child: child);
  34 +
  35 + final SelectionRegistrar registrar;
  36 + final String selectedText;
  37 +
  38 + @override
  39 + _RenderSelectableAdapter createRenderObject(BuildContext context) {
  40 + return _RenderSelectableAdapter(
  41 + DefaultSelectionStyle.of(context).selectionColor!,
  42 + selectedText,
  43 + registrar,
  44 + );
  45 + }
  46 +
  47 + @override
  48 + void updateRenderObject(
  49 + BuildContext context, _RenderSelectableAdapter renderObject) {
  50 + renderObject
  51 + ..selectionColor = DefaultSelectionStyle.of(context).selectionColor!
  52 + ..registrar = registrar;
  53 + }
  54 +}
  55 +
  56 +class _RenderSelectableAdapter extends RenderProxyBox
  57 + with Selectable, SelectionRegistrant {
  58 + String selectionText;
  59 + _RenderSelectableAdapter(
  60 + Color selectionColor,
  61 + this.selectionText,
  62 + SelectionRegistrar registrar,
  63 + ) : _selectionColor = selectionColor,
  64 + _geometry = ValueNotifier<SelectionGeometry>(_noSelection) {
  65 + this.registrar = registrar;
  66 + _geometry.addListener(markNeedsPaint);
  67 + }
  68 +
  69 + static const SelectionGeometry _noSelection =
  70 + SelectionGeometry(status: SelectionStatus.none, hasContent: true);
  71 + final ValueNotifier<SelectionGeometry> _geometry;
  72 +
  73 + Color get selectionColor => _selectionColor;
  74 + late Color _selectionColor;
  75 + set selectionColor(Color value) {
  76 + if (_selectionColor == value) {
  77 + return;
  78 + }
  79 + _selectionColor = value;
  80 + markNeedsPaint();
  81 + }
  82 +
  83 + // ValueListenable APIs
  84 +
  85 + @override
  86 + void addListener(VoidCallback listener) => _geometry.addListener(listener);
  87 +
  88 + @override
  89 + void removeListener(VoidCallback listener) =>
  90 + _geometry.removeListener(listener);
  91 +
  92 + @override
  93 + SelectionGeometry get value => _geometry.value;
  94 +
  95 + // Selectable APIs.
  96 +
  97 + @override
  98 + List<Rect> get boundingBoxes => <Rect>[paintBounds];
  99 +
  100 + // Adjust this value to enlarge or shrink the selection highlight.
  101 + static const double _padding = 0.0;
  102 + Rect _getSelectionHighlightRect() {
  103 + return Rect.fromLTWH(0 - _padding, 0 - _padding, size.width + _padding * 2,
  104 + size.height + _padding * 2);
  105 + }
  106 +
  107 + Offset? _start;
  108 + Offset? _end;
  109 + void _updateGeometry() {
  110 + if (_start == null || _end == null) {
  111 + _geometry.value = _noSelection;
  112 + return;
  113 + }
  114 + final Rect renderObjectRect = Rect.fromLTWH(0, 0, size.width, size.height);
  115 + final Rect selectionRect = Rect.fromPoints(_start!, _end!);
  116 + if (renderObjectRect.intersect(selectionRect).isEmpty) {
  117 + _geometry.value = _noSelection;
  118 + } else {
  119 + final Rect selectionRect = _getSelectionHighlightRect();
  120 + final SelectionPoint firstSelectionPoint = SelectionPoint(
  121 + localPosition: selectionRect.bottomLeft,
  122 + lineHeight: selectionRect.size.height,
  123 + handleType: TextSelectionHandleType.left,
  124 + );
  125 + final SelectionPoint secondSelectionPoint = SelectionPoint(
  126 + localPosition: selectionRect.bottomRight,
  127 + lineHeight: selectionRect.size.height,
  128 + handleType: TextSelectionHandleType.right,
  129 + );
  130 + final bool isReversed;
  131 + if (_start!.dy > _end!.dy) {
  132 + isReversed = true;
  133 + } else if (_start!.dy < _end!.dy) {
  134 + isReversed = false;
  135 + } else {
  136 + isReversed = _start!.dx > _end!.dx;
  137 + }
  138 + _geometry.value = SelectionGeometry(
  139 + status: SelectionStatus.uncollapsed,
  140 + hasContent: true,
  141 + startSelectionPoint:
  142 + isReversed ? secondSelectionPoint : firstSelectionPoint,
  143 + endSelectionPoint:
  144 + isReversed ? firstSelectionPoint : secondSelectionPoint,
  145 + selectionRects: <Rect>[selectionRect],
  146 + );
  147 + }
  148 + }
  149 +
  150 + @override
  151 + SelectionResult dispatchSelectionEvent(SelectionEvent event) {
  152 + SelectionResult result = SelectionResult.none;
  153 + switch (event.type) {
  154 + case SelectionEventType.startEdgeUpdate:
  155 + case SelectionEventType.endEdgeUpdate:
  156 + final Rect renderObjectRect =
  157 + Rect.fromLTWH(0, 0, size.width, size.height);
  158 + // Normalize offset in case it is out side of the rect.
  159 + final Offset point =
  160 + globalToLocal((event as SelectionEdgeUpdateEvent).globalPosition);
  161 + final Offset adjustedPoint =
  162 + SelectionUtils.adjustDragOffset(renderObjectRect, point);
  163 + if (event.type == SelectionEventType.startEdgeUpdate) {
  164 + _start = adjustedPoint;
  165 + } else {
  166 + _end = adjustedPoint;
  167 + }
  168 + result = SelectionUtils.getResultBasedOnRect(renderObjectRect, point);
  169 + case SelectionEventType.clear:
  170 + _start = _end = null;
  171 + case SelectionEventType.selectAll:
  172 + case SelectionEventType.selectWord:
  173 + case SelectionEventType.selectParagraph:
  174 + _start = Offset.zero;
  175 + _end = Offset.infinite;
  176 + case SelectionEventType.granularlyExtendSelection:
  177 + result = SelectionResult.end;
  178 + final GranularlyExtendSelectionEvent extendSelectionEvent =
  179 + event as GranularlyExtendSelectionEvent;
  180 + // Initialize the offset it there is no ongoing selection.
  181 + if (_start == null || _end == null) {
  182 + if (extendSelectionEvent.forward) {
  183 + _start = _end = Offset.zero;
  184 + } else {
  185 + _start = _end = Offset.infinite;
  186 + }
  187 + }
  188 + // Move the corresponding selection edge.
  189 + final Offset newOffset =
  190 + extendSelectionEvent.forward ? Offset.infinite : Offset.zero;
  191 + if (extendSelectionEvent.isEnd) {
  192 + if (newOffset == _end) {
  193 + result = extendSelectionEvent.forward
  194 + ? SelectionResult.next
  195 + : SelectionResult.previous;
  196 + }
  197 + _end = newOffset;
  198 + } else {
  199 + if (newOffset == _start) {
  200 + result = extendSelectionEvent.forward
  201 + ? SelectionResult.next
  202 + : SelectionResult.previous;
  203 + }
  204 + _start = newOffset;
  205 + }
  206 + case SelectionEventType.directionallyExtendSelection:
  207 + result = SelectionResult.end;
  208 + final DirectionallyExtendSelectionEvent extendSelectionEvent =
  209 + event as DirectionallyExtendSelectionEvent;
  210 + // Convert to local coordinates.
  211 + final double horizontalBaseLine = globalToLocal(Offset(event.dx, 0)).dx;
  212 + final Offset newOffset;
  213 + final bool forward;
  214 + switch (extendSelectionEvent.direction) {
  215 + case SelectionExtendDirection.backward:
  216 + case SelectionExtendDirection.previousLine:
  217 + forward = false;
  218 + // Initialize the offset it there is no ongoing selection.
  219 + if (_start == null || _end == null) {
  220 + _start = _end = Offset.infinite;
  221 + }
  222 + // Move the corresponding selection edge.
  223 + if (extendSelectionEvent.direction ==
  224 + SelectionExtendDirection.previousLine ||
  225 + horizontalBaseLine < 0) {
  226 + newOffset = Offset.zero;
  227 + } else {
  228 + newOffset = Offset.infinite;
  229 + }
  230 + case SelectionExtendDirection.nextLine:
  231 + case SelectionExtendDirection.forward:
  232 + forward = true;
  233 + // Initialize the offset it there is no ongoing selection.
  234 + if (_start == null || _end == null) {
  235 + _start = _end = Offset.zero;
  236 + }
  237 + // Move the corresponding selection edge.
  238 + if (extendSelectionEvent.direction ==
  239 + SelectionExtendDirection.nextLine ||
  240 + horizontalBaseLine > size.width) {
  241 + newOffset = Offset.infinite;
  242 + } else {
  243 + newOffset = Offset.zero;
  244 + }
  245 + }
  246 + if (extendSelectionEvent.isEnd) {
  247 + if (newOffset == _end) {
  248 + result = forward ? SelectionResult.next : SelectionResult.previous;
  249 + }
  250 + _end = newOffset;
  251 + } else {
  252 + if (newOffset == _start) {
  253 + result = forward ? SelectionResult.next : SelectionResult.previous;
  254 + }
  255 + _start = newOffset;
  256 + }
  257 + }
  258 + _updateGeometry();
  259 + return result;
  260 + }
  261 +
  262 + // This method is called when users want to copy selected content in this
  263 + // widget into clipboard.
  264 + @override
  265 + SelectedContent? getSelectedContent() {
  266 + return value.hasSelection
  267 + ? SelectedContent(plainText: selectionText)
  268 + : null;
  269 + }
  270 +
  271 + LayerLink? _startHandle;
  272 + LayerLink? _endHandle;
  273 +
  274 + @override
  275 + void pushHandleLayers(LayerLink? startHandle, LayerLink? endHandle) {
  276 + if (_startHandle == startHandle && _endHandle == endHandle) {
  277 + return;
  278 + }
  279 + _startHandle = startHandle;
  280 + _endHandle = endHandle;
  281 + markNeedsPaint();
  282 + }
  283 +
  284 + @override
  285 + void paint(PaintingContext context, Offset offset) {
  286 + super.paint(context, offset);
  287 + if (!_geometry.value.hasSelection) {
  288 + return;
  289 + }
  290 + // Draw the selection highlight.
  291 + final Paint selectionPaint = Paint()
  292 + ..style = PaintingStyle.fill
  293 + ..color = _selectionColor;
  294 + context.canvas
  295 + .drawRect(_getSelectionHighlightRect().shift(offset), selectionPaint);
  296 +
  297 + // Push the layer links if any.
  298 + if (_startHandle != null) {
  299 + context.pushLayer(
  300 + LeaderLayer(
  301 + link: _startHandle!,
  302 + offset: offset + value.startSelectionPoint!.localPosition,
  303 + ),
  304 + (PaintingContext context, Offset offset) {},
  305 + Offset.zero,
  306 + );
  307 + }
  308 + if (_endHandle != null) {
  309 + context.pushLayer(
  310 + LeaderLayer(
  311 + link: _endHandle!,
  312 + offset: offset + value.endSelectionPoint!.localPosition,
  313 + ),
  314 + (PaintingContext context, Offset offset) {},
  315 + Offset.zero,
  316 + );
  317 + }
  318 + }
  319 +
  320 + @override
  321 + void dispose() {
  322 + _geometry.dispose();
  323 + super.dispose();
  324 + }
  325 +}
@@ -126,7 +126,7 @@ packages: @@ -126,7 +126,7 @@ packages:
126 path: ".." 126 path: ".."
127 relative: true 127 relative: true
128 source: path 128 source: path
129 - version: "0.1.10" 129 + version: "0.1.11"
130 http: 130 http:
131 dependency: transitive 131 dependency: transitive
132 description: 132 description:
@@ -373,5 +373,5 @@ packages: @@ -373,5 +373,5 @@ packages:
373 source: hosted 373 source: hosted
374 version: "6.5.0" 374 version: "6.5.0"
375 sdks: 375 sdks:
376 - dart: ">=3.3.0 <4.0.0" 376 + dart: ">=3.5.0 <4.0.0"
377 flutter: ">=3.18.0-18.0.pre.54" 377 flutter: ">=3.18.0-18.0.pre.54"
@@ -5,7 +5,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev @@ -5,7 +5,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
5 version: 1.0.0+1 5 version: 1.0.0+1
6 6
7 environment: 7 environment:
8 - sdk: '>=2.18.6 <4.0.0' 8 + sdk: '>=3.5.0 <4.0.0'
9 dependencies: 9 dependencies:
10 flutter: 10 flutter:
11 sdk: flutter 11 sdk: flutter
@@ -7,6 +7,7 @@ import 'package:gpt_markdown/custom_widgets/custom_error_image.dart'; @@ -7,6 +7,7 @@ import 'package:gpt_markdown/custom_widgets/custom_error_image.dart';
7 import 'package:gpt_markdown/custom_widgets/custom_rb_cb.dart'; 7 import 'package:gpt_markdown/custom_widgets/custom_rb_cb.dart';
8 import 'package:gpt_markdown/custom_widgets/markdow_config.dart'; 8 import 'package:gpt_markdown/custom_widgets/markdow_config.dart';
9 import 'package:gpt_markdown/custom_widgets/unordered_ordered_list.dart'; 9 import 'package:gpt_markdown/custom_widgets/unordered_ordered_list.dart';
  10 +import 'package:gpt_markdown/theme.dart';
10 import 'md_widget.dart'; 11 import 'md_widget.dart';
11 12
12 /// Markdown components 13 /// Markdown components
@@ -413,14 +414,14 @@ class HighlightedText extends InlineMd { @@ -413,14 +414,14 @@ class HighlightedText extends InlineMd {
413 style: config.style?.copyWith( 414 style: config.style?.copyWith(
414 fontWeight: FontWeight.bold, 415 fontWeight: FontWeight.bold,
415 background: Paint() 416 background: Paint()
416 - ..color = Theme.of(context).colorScheme.onInverseSurface 417 + ..color = GptMarkdownTheme.of(context).highlightColor
417 ..strokeCap = StrokeCap.round 418 ..strokeCap = StrokeCap.round
418 ..strokeJoin = StrokeJoin.round, 419 ..strokeJoin = StrokeJoin.round,
419 ) ?? 420 ) ??
420 TextStyle( 421 TextStyle(
421 fontWeight: FontWeight.bold, 422 fontWeight: FontWeight.bold,
422 background: Paint() 423 background: Paint()
423 - ..color = Theme.of(context).colorScheme.surfaceContainerHighest 424 + ..color = GptMarkdownTheme.of(context).highlightColor
424 ..strokeCap = StrokeCap.round 425 ..strokeCap = StrokeCap.round
425 ..strokeJoin = StrokeJoin.round, 426 ..strokeJoin = StrokeJoin.round,
426 ), 427 ),
  1 +import 'package:flutter/material.dart';
  2 +
  3 +/// Theme defined for `TexMarkdown` widget
  4 +class GptMarkdownThemeData extends ThemeExtension<GptMarkdownThemeData> {
  5 + GptMarkdownThemeData({
  6 + required this.highlightColor,
  7 + });
  8 +
  9 + /// Define default attributes.
  10 + factory GptMarkdownThemeData.from(BuildContext context) {
  11 + return GptMarkdownThemeData(
  12 + highlightColor: Theme.of(context).colorScheme.onInverseSurface,
  13 + );
  14 + }
  15 +
  16 + Color highlightColor;
  17 +
  18 + @override
  19 + GptMarkdownThemeData copyWith({Color? highlightColor}) {
  20 + return GptMarkdownThemeData(
  21 + highlightColor: highlightColor ?? this.highlightColor,
  22 + );
  23 + }
  24 +
  25 + @override
  26 + GptMarkdownThemeData lerp(GptMarkdownThemeData? other, double t) {
  27 + if (other == null) {
  28 + return this;
  29 + }
  30 + return GptMarkdownThemeData(
  31 + highlightColor:
  32 + Color.lerp(highlightColor, other.highlightColor, t) ?? highlightColor,
  33 + );
  34 + }
  35 +}
  36 +
  37 +/// Wrap a `Widget` with `GptMarkdownTheme` to provide `GptMarkdownThemeData` in your intiar app.
  38 +class GptMarkdownTheme extends InheritedWidget {
  39 + const GptMarkdownTheme({
  40 + super.key,
  41 + required this.gptThemeData,
  42 + required super.child,
  43 + });
  44 + final GptMarkdownThemeData gptThemeData;
  45 +
  46 + static GptMarkdownThemeData of(BuildContext context) {
  47 + final provider =
  48 + context.dependOnInheritedWidgetOfExactType<GptMarkdownTheme>();
  49 + if (provider != null) {
  50 + return provider.gptThemeData;
  51 + }
  52 + final themeData = Theme.of(context).extension<GptMarkdownThemeData>();
  53 + if (themeData != null) {
  54 + return themeData;
  55 + }
  56 + return GptMarkdownThemeData.from(context);
  57 + }
  58 +
  59 + @override
  60 + bool updateShouldNotify(GptMarkdownTheme oldWidget) {
  61 + return gptThemeData != oldWidget.gptThemeData;
  62 + }
  63 +}
1 name: gpt_markdown 1 name: gpt_markdown
2 description: "The purpose of this package is to render the response of ChatGPT into a Flutter app." 2 description: "The purpose of this package is to render the response of ChatGPT into a Flutter app."
3 -version: 0.1.10 3 +version: 0.1.11
4 homepage: https://github.com/saminsohag/flutter_packages/tree/main/gpt_markdown 4 homepage: https://github.com/saminsohag/flutter_packages/tree/main/gpt_markdown
5 5
6 environment: 6 environment: