saminsohag

theme extantions added

## 0.1.11
* `GptMarkdownTheme` and `GptMarkdownThemeData` classes added.
## 0.1.10
* components are now selectable.
... ...
... ... @@ -4,7 +4,9 @@ import 'package:desktop_drop/desktop_drop.dart';
import 'package:flutter/material.dart';
import 'package:gpt_markdown/gpt_markdown.dart';
import 'package:flutter_math_fork/flutter_math.dart';
import 'package:gpt_markdown/theme.dart';
import 'package:watcher/watcher.dart';
import 'selectable_adapter.dart';
void main() {
runApp(const MyApp());
... ... @@ -31,10 +33,14 @@ class _MyAppState extends State<MyApp> {
colorSchemeSeed: Colors.blue,
),
darkTheme: ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
colorSchemeSeed: Colors.blue,
),
useMaterial3: true,
brightness: Brightness.dark,
colorSchemeSeed: Colors.blue,
extensions: [
GptMarkdownThemeData(
highlightColor: Colors.red,
),
]),
home: MyHomePage(
title: 'GptMarkdown',
onPressed: () {
... ... @@ -147,241 +153,254 @@ Markdown and LaTeX can be powerful tools for formatting text and mathematical ex
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
actions: [
IconButton(
onPressed: () {
setState(() {
selectable = !selectable;
});
},
icon: Icon(
Icons.select_all_outlined,
color: selectable
? Theme.of(context).colorScheme.onSurfaceVariant
: Theme.of(context).colorScheme.onSurface.withOpacity(0.38),
),
),
IconButton(
onPressed: () {
setState(() {
_direction = TextDirection.values[(_direction.index + 1) % 2];
});
},
icon: const [Text("LTR"), Text("RTL")][_direction.index],
),
IconButton(
onPressed: widget.onPressed,
icon: const Icon(Icons.sunny),
),
IconButton(
onPressed: () => setState(() {
writingMod = !writingMod;
}),
icon:
Icon(writingMod ? Icons.arrow_drop_down : Icons.arrow_drop_up),
),
],
return GptMarkdownTheme(
gptThemeData: GptMarkdownTheme.of(context).copyWith(
highlightColor: Colors.purple,
),
body: DropTarget(
onDragDone: (details) {
var files = details.files;
if (files.length != 1) {
return;
}
var file = files[0];
String path = file.path;
this.file = File(path);
load();
},
child: Stack(
children: [
Column(
children: [
Expanded(
child: ListView(
children: [
AnimatedBuilder(
animation: _controller,
builder: (context, _) {
return Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
border: Border.all(
width: 1,
color: Theme.of(context).colorScheme.outline),
),
child: Theme(
data: Theme.of(context),
// .copyWith(
// textTheme: const TextTheme(
// // For H1.
// headlineLarge: TextStyle(fontSize: 55),
// // For H2.
// headlineMedium: TextStyle(fontSize: 45),
// // For H3.
// headlineSmall: TextStyle(fontSize: 35),
// // For H4.
// titleLarge: TextStyle(fontSize: 25),
// // For H5.
// titleMedium: TextStyle(fontSize: 15),
// // For H6.
// titleSmall: TextStyle(fontSize: 10),
// ),
// ),
child: Builder(
builder: (context) {
Widget child = TexMarkdown(
_controller.text,
textDirection: _direction,
onLinkTab: (url, title) {
debugPrint(url);
debugPrint(title);
},
textAlign: TextAlign.justify,
textScaler: const TextScaler.linear(1),
style: const TextStyle(
fontSize: 15,
),
latexWorkaround: (tex) {
List<String> stack = [];
tex = tex.splitMapJoin(
RegExp(r"\\text\{|\{|\}|\_"),
onMatch: (p) {
String input = p[0] ?? "";
if (input == r"\text{") {
stack.add(input);
}
if (stack.isNotEmpty) {
if (input == r"{") {
child: Scaffold(
appBar: AppBar(
title: Text(widget.title),
actions: [
IconButton(
onPressed: () {
setState(() {
selectable = !selectable;
});
},
icon: Icon(
Icons.select_all_outlined,
color: selectable
? Theme.of(context).colorScheme.onSurfaceVariant
: Theme.of(context).colorScheme.onSurface.withOpacity(0.38),
),
),
IconButton(
onPressed: () {
setState(() {
_direction = TextDirection.values[(_direction.index + 1) % 2];
});
},
icon: const [Text("LTR"), Text("RTL")][_direction.index],
),
IconButton(
onPressed: widget.onPressed,
icon: const Icon(Icons.sunny),
),
IconButton(
onPressed: () => setState(() {
writingMod = !writingMod;
}),
icon: Icon(
writingMod ? Icons.arrow_drop_down : Icons.arrow_drop_up),
),
],
),
body: DropTarget(
onDragDone: (details) {
var files = details.files;
if (files.length != 1) {
return;
}
var file = files[0];
String path = file.path;
this.file = File(path);
load();
},
child: Stack(
children: [
Column(
children: [
Expanded(
child: ListView(
children: [
AnimatedBuilder(
animation: _controller,
builder: (context, _) {
return Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
border: Border.all(
width: 1,
color:
Theme.of(context).colorScheme.outline),
),
child: Theme(
data: Theme.of(context),
// .copyWith(
// textTheme: const TextTheme(
// // For H1.
// headlineLarge: TextStyle(fontSize: 55),
// // For H2.
// headlineMedium: TextStyle(fontSize: 45),
// // For H3.
// headlineSmall: TextStyle(fontSize: 35),
// // For H4.
// titleLarge: TextStyle(fontSize: 25),
// // For H5.
// titleMedium: TextStyle(fontSize: 15),
// // For H6.
// titleSmall: TextStyle(fontSize: 10),
// ),
// ),
child: Builder(
builder: (context) {
Widget child = TexMarkdown(
_controller.text,
textDirection: _direction,
onLinkTab: (url, title) {
debugPrint(url);
debugPrint(title);
},
textAlign: TextAlign.justify,
textScaler: const TextScaler.linear(1),
style: const TextStyle(
fontSize: 15,
),
latexWorkaround: (tex) {
List<String> stack = [];
tex = tex.splitMapJoin(
RegExp(r"\\text\{|\{|\}|\_"),
onMatch: (p) {
String input = p[0] ?? "";
if (input == r"\text{") {
stack.add(input);
}
if (input == r"}") {
stack.removeLast();
if (stack.isNotEmpty) {
if (input == r"{") {
stack.add(input);
}
if (input == r"}") {
stack.removeLast();
}
if (input == r"_") {
return r"\_";
}
}
if (input == r"_") {
return r"\_";
}
}
return input;
},
);
return tex.replaceAllMapped(
RegExp(r"align\*"),
(match) => "aligned");
},
latexBuilder:
(contex, tex, textStyle, inline) {
if (tex.contains(r"\begin{tabular}")) {
// return table.
String tableString = "|${(RegExp(
r"^\\begin\{tabular\}\{.*?\}(.*?)\\end\{tabular\}$",
multiLine: true,
dotAll: true,
).firstMatch(tex)?[1] ?? "").trim()}|";
tableString = tableString
.replaceAll(r"\\", "|\n|")
.replaceAll(r"\hline", "")
.replaceAll(
RegExp(r"(?<!\\)&"), "|");
var tableStringList = tableString
.split("\n")
..insert(1, "|---|");
tableString =
tableStringList.join("\n");
return TexMarkdown(tableString);
}
var controller = ScrollController();
Widget child = Math.tex(
tex,
textStyle: textStyle,
);
if (!inline) {
child = Padding(
padding: const EdgeInsets.all(0.0),
child: Material(
color: Theme.of(context)
.colorScheme
.onInverseSurface,
child: Padding(
padding:
const EdgeInsets.all(8.0),
child: Scrollbar(
controller: controller,
child: SingleChildScrollView(
return input;
},
);
return tex.replaceAllMapped(
RegExp(r"align\*"),
(match) => "aligned");
},
latexBuilder:
(contex, tex, textStyle, inline) {
if (tex.contains(r"\begin{tabular}")) {
// return table.
String tableString = "|${(RegExp(
r"^\\begin\{tabular\}\{.*?\}(.*?)\\end\{tabular\}$",
multiLine: true,
dotAll: true,
).firstMatch(tex)?[1] ?? "").trim()}|";
tableString = tableString
.replaceAll(r"\\", "|\n|")
.replaceAll(r"\hline", "")
.replaceAll(
RegExp(r"(?<!\\)&"), "|");
var tableStringList = tableString
.split("\n")
..insert(1, "|---|");
tableString =
tableStringList.join("\n");
return TexMarkdown(tableString);
}
var controller = ScrollController();
Widget child = Math.tex(
tex,
textStyle: textStyle,
);
if (!inline) {
child = Padding(
padding: const EdgeInsets.all(0.0),
child: Material(
color: Theme.of(context)
.colorScheme
.onInverseSurface,
child: Padding(
padding:
const EdgeInsets.all(8.0),
child: Scrollbar(
controller: controller,
scrollDirection:
Axis.horizontal,
child: Math.tex(
tex,
textStyle: textStyle,
child: SingleChildScrollView(
controller: controller,
scrollDirection:
Axis.horizontal,
child: Math.tex(
tex,
textStyle: textStyle,
),
),
),
),
),
);
}
child = SelectableAdapter(
selectedText: tex,
child: Math.tex(tex),
);
child = InkWell(
onTap: () {
debugPrint("Hello world");
},
child: child,
);
return child;
},
sourceTagBuilder:
(buildContext, string, textStyle) {
var value = int.tryParse(string);
value ??= -1;
value += 1;
return SizedBox(
height: 20,
width: 20,
child: Container(
decoration: BoxDecoration(
color: Colors.red,
borderRadius:
BorderRadius.circular(10),
),
child:
Center(child: Text("$value")),
),
);
}
child = InkWell(
onTap: () {
debugPrint("Hello world");
},
},
);
if (selectable) {
child = SelectionArea(
child: child,
);
return child;
},
sourceTagBuilder:
(buildContext, string, textStyle) {
var value = int.tryParse(string);
value ??= -1;
value += 1;
return SizedBox(
height: 20,
width: 20,
child: Container(
decoration: BoxDecoration(
color: Colors.red,
borderRadius:
BorderRadius.circular(10),
),
child: Center(child: Text("$value")),
),
);
},
);
if (selectable) {
child = SelectionArea(child: child);
}
return child;
},
}
return child;
},
),
// child: const Text("Hello"),
),
// child: const Text("Hello"),
),
);
},
),
],
);
},
),
],
),
),
),
if (writingMod)
ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 200),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
decoration: const InputDecoration(
border: OutlineInputBorder(),
label: Text("Type here:")),
maxLines: null,
controller: _controller,
if (writingMod)
ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 200),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
decoration: const InputDecoration(
border: OutlineInputBorder(),
label: Text("Type here:")),
maxLines: null,
controller: _controller,
),
),
),
),
],
),
],
],
),
],
),
),
),
);
... ...
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class SelectableAdapter extends StatelessWidget {
const SelectableAdapter(
{super.key, required this.selectedText, required this.child});
final Widget child;
final String selectedText;
@override
Widget build(BuildContext context) {
final SelectionRegistrar? registrar = SelectionContainer.maybeOf(context);
if (registrar == null) {
return child;
}
return MouseRegion(
cursor: SystemMouseCursors.text,
child: _SelectableAdapter(
registrar: registrar,
selectedText: selectedText,
child: child,
),
);
}
}
class _SelectableAdapter extends SingleChildRenderObjectWidget {
const _SelectableAdapter({
required this.registrar,
required Widget child,
required this.selectedText,
}) : super(child: child);
final SelectionRegistrar registrar;
final String selectedText;
@override
_RenderSelectableAdapter createRenderObject(BuildContext context) {
return _RenderSelectableAdapter(
DefaultSelectionStyle.of(context).selectionColor!,
selectedText,
registrar,
);
}
@override
void updateRenderObject(
BuildContext context, _RenderSelectableAdapter renderObject) {
renderObject
..selectionColor = DefaultSelectionStyle.of(context).selectionColor!
..registrar = registrar;
}
}
class _RenderSelectableAdapter extends RenderProxyBox
with Selectable, SelectionRegistrant {
String selectionText;
_RenderSelectableAdapter(
Color selectionColor,
this.selectionText,
SelectionRegistrar registrar,
) : _selectionColor = selectionColor,
_geometry = ValueNotifier<SelectionGeometry>(_noSelection) {
this.registrar = registrar;
_geometry.addListener(markNeedsPaint);
}
static const SelectionGeometry _noSelection =
SelectionGeometry(status: SelectionStatus.none, hasContent: true);
final ValueNotifier<SelectionGeometry> _geometry;
Color get selectionColor => _selectionColor;
late Color _selectionColor;
set selectionColor(Color value) {
if (_selectionColor == value) {
return;
}
_selectionColor = value;
markNeedsPaint();
}
// ValueListenable APIs
@override
void addListener(VoidCallback listener) => _geometry.addListener(listener);
@override
void removeListener(VoidCallback listener) =>
_geometry.removeListener(listener);
@override
SelectionGeometry get value => _geometry.value;
// Selectable APIs.
@override
List<Rect> get boundingBoxes => <Rect>[paintBounds];
// Adjust this value to enlarge or shrink the selection highlight.
static const double _padding = 0.0;
Rect _getSelectionHighlightRect() {
return Rect.fromLTWH(0 - _padding, 0 - _padding, size.width + _padding * 2,
size.height + _padding * 2);
}
Offset? _start;
Offset? _end;
void _updateGeometry() {
if (_start == null || _end == null) {
_geometry.value = _noSelection;
return;
}
final Rect renderObjectRect = Rect.fromLTWH(0, 0, size.width, size.height);
final Rect selectionRect = Rect.fromPoints(_start!, _end!);
if (renderObjectRect.intersect(selectionRect).isEmpty) {
_geometry.value = _noSelection;
} else {
final Rect selectionRect = _getSelectionHighlightRect();
final SelectionPoint firstSelectionPoint = SelectionPoint(
localPosition: selectionRect.bottomLeft,
lineHeight: selectionRect.size.height,
handleType: TextSelectionHandleType.left,
);
final SelectionPoint secondSelectionPoint = SelectionPoint(
localPosition: selectionRect.bottomRight,
lineHeight: selectionRect.size.height,
handleType: TextSelectionHandleType.right,
);
final bool isReversed;
if (_start!.dy > _end!.dy) {
isReversed = true;
} else if (_start!.dy < _end!.dy) {
isReversed = false;
} else {
isReversed = _start!.dx > _end!.dx;
}
_geometry.value = SelectionGeometry(
status: SelectionStatus.uncollapsed,
hasContent: true,
startSelectionPoint:
isReversed ? secondSelectionPoint : firstSelectionPoint,
endSelectionPoint:
isReversed ? firstSelectionPoint : secondSelectionPoint,
selectionRects: <Rect>[selectionRect],
);
}
}
@override
SelectionResult dispatchSelectionEvent(SelectionEvent event) {
SelectionResult result = SelectionResult.none;
switch (event.type) {
case SelectionEventType.startEdgeUpdate:
case SelectionEventType.endEdgeUpdate:
final Rect renderObjectRect =
Rect.fromLTWH(0, 0, size.width, size.height);
// Normalize offset in case it is out side of the rect.
final Offset point =
globalToLocal((event as SelectionEdgeUpdateEvent).globalPosition);
final Offset adjustedPoint =
SelectionUtils.adjustDragOffset(renderObjectRect, point);
if (event.type == SelectionEventType.startEdgeUpdate) {
_start = adjustedPoint;
} else {
_end = adjustedPoint;
}
result = SelectionUtils.getResultBasedOnRect(renderObjectRect, point);
case SelectionEventType.clear:
_start = _end = null;
case SelectionEventType.selectAll:
case SelectionEventType.selectWord:
case SelectionEventType.selectParagraph:
_start = Offset.zero;
_end = Offset.infinite;
case SelectionEventType.granularlyExtendSelection:
result = SelectionResult.end;
final GranularlyExtendSelectionEvent extendSelectionEvent =
event as GranularlyExtendSelectionEvent;
// Initialize the offset it there is no ongoing selection.
if (_start == null || _end == null) {
if (extendSelectionEvent.forward) {
_start = _end = Offset.zero;
} else {
_start = _end = Offset.infinite;
}
}
// Move the corresponding selection edge.
final Offset newOffset =
extendSelectionEvent.forward ? Offset.infinite : Offset.zero;
if (extendSelectionEvent.isEnd) {
if (newOffset == _end) {
result = extendSelectionEvent.forward
? SelectionResult.next
: SelectionResult.previous;
}
_end = newOffset;
} else {
if (newOffset == _start) {
result = extendSelectionEvent.forward
? SelectionResult.next
: SelectionResult.previous;
}
_start = newOffset;
}
case SelectionEventType.directionallyExtendSelection:
result = SelectionResult.end;
final DirectionallyExtendSelectionEvent extendSelectionEvent =
event as DirectionallyExtendSelectionEvent;
// Convert to local coordinates.
final double horizontalBaseLine = globalToLocal(Offset(event.dx, 0)).dx;
final Offset newOffset;
final bool forward;
switch (extendSelectionEvent.direction) {
case SelectionExtendDirection.backward:
case SelectionExtendDirection.previousLine:
forward = false;
// Initialize the offset it there is no ongoing selection.
if (_start == null || _end == null) {
_start = _end = Offset.infinite;
}
// Move the corresponding selection edge.
if (extendSelectionEvent.direction ==
SelectionExtendDirection.previousLine ||
horizontalBaseLine < 0) {
newOffset = Offset.zero;
} else {
newOffset = Offset.infinite;
}
case SelectionExtendDirection.nextLine:
case SelectionExtendDirection.forward:
forward = true;
// Initialize the offset it there is no ongoing selection.
if (_start == null || _end == null) {
_start = _end = Offset.zero;
}
// Move the corresponding selection edge.
if (extendSelectionEvent.direction ==
SelectionExtendDirection.nextLine ||
horizontalBaseLine > size.width) {
newOffset = Offset.infinite;
} else {
newOffset = Offset.zero;
}
}
if (extendSelectionEvent.isEnd) {
if (newOffset == _end) {
result = forward ? SelectionResult.next : SelectionResult.previous;
}
_end = newOffset;
} else {
if (newOffset == _start) {
result = forward ? SelectionResult.next : SelectionResult.previous;
}
_start = newOffset;
}
}
_updateGeometry();
return result;
}
// This method is called when users want to copy selected content in this
// widget into clipboard.
@override
SelectedContent? getSelectedContent() {
return value.hasSelection
? SelectedContent(plainText: selectionText)
: null;
}
LayerLink? _startHandle;
LayerLink? _endHandle;
@override
void pushHandleLayers(LayerLink? startHandle, LayerLink? endHandle) {
if (_startHandle == startHandle && _endHandle == endHandle) {
return;
}
_startHandle = startHandle;
_endHandle = endHandle;
markNeedsPaint();
}
@override
void paint(PaintingContext context, Offset offset) {
super.paint(context, offset);
if (!_geometry.value.hasSelection) {
return;
}
// Draw the selection highlight.
final Paint selectionPaint = Paint()
..style = PaintingStyle.fill
..color = _selectionColor;
context.canvas
.drawRect(_getSelectionHighlightRect().shift(offset), selectionPaint);
// Push the layer links if any.
if (_startHandle != null) {
context.pushLayer(
LeaderLayer(
link: _startHandle!,
offset: offset + value.startSelectionPoint!.localPosition,
),
(PaintingContext context, Offset offset) {},
Offset.zero,
);
}
if (_endHandle != null) {
context.pushLayer(
LeaderLayer(
link: _endHandle!,
offset: offset + value.endSelectionPoint!.localPosition,
),
(PaintingContext context, Offset offset) {},
Offset.zero,
);
}
}
@override
void dispose() {
_geometry.dispose();
super.dispose();
}
}
... ...
... ... @@ -126,7 +126,7 @@ packages:
path: ".."
relative: true
source: path
version: "0.1.10"
version: "0.1.11"
http:
dependency: transitive
description:
... ... @@ -373,5 +373,5 @@ packages:
source: hosted
version: "6.5.0"
sdks:
dart: ">=3.3.0 <4.0.0"
dart: ">=3.5.0 <4.0.0"
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
version: 1.0.0+1
environment:
sdk: '>=2.18.6 <4.0.0'
sdk: '>=3.5.0 <4.0.0'
dependencies:
flutter:
sdk: flutter
... ...
... ... @@ -7,6 +7,7 @@ import 'package:gpt_markdown/custom_widgets/custom_error_image.dart';
import 'package:gpt_markdown/custom_widgets/custom_rb_cb.dart';
import 'package:gpt_markdown/custom_widgets/markdow_config.dart';
import 'package:gpt_markdown/custom_widgets/unordered_ordered_list.dart';
import 'package:gpt_markdown/theme.dart';
import 'md_widget.dart';
/// Markdown components
... ... @@ -413,14 +414,14 @@ class HighlightedText extends InlineMd {
style: config.style?.copyWith(
fontWeight: FontWeight.bold,
background: Paint()
..color = Theme.of(context).colorScheme.onInverseSurface
..color = GptMarkdownTheme.of(context).highlightColor
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round,
) ??
TextStyle(
fontWeight: FontWeight.bold,
background: Paint()
..color = Theme.of(context).colorScheme.surfaceContainerHighest
..color = GptMarkdownTheme.of(context).highlightColor
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round,
),
... ...
import 'package:flutter/material.dart';
/// Theme defined for `TexMarkdown` widget
class GptMarkdownThemeData extends ThemeExtension<GptMarkdownThemeData> {
GptMarkdownThemeData({
required this.highlightColor,
});
/// Define default attributes.
factory GptMarkdownThemeData.from(BuildContext context) {
return GptMarkdownThemeData(
highlightColor: Theme.of(context).colorScheme.onInverseSurface,
);
}
Color highlightColor;
@override
GptMarkdownThemeData copyWith({Color? highlightColor}) {
return GptMarkdownThemeData(
highlightColor: highlightColor ?? this.highlightColor,
);
}
@override
GptMarkdownThemeData lerp(GptMarkdownThemeData? other, double t) {
if (other == null) {
return this;
}
return GptMarkdownThemeData(
highlightColor:
Color.lerp(highlightColor, other.highlightColor, t) ?? highlightColor,
);
}
}
/// Wrap a `Widget` with `GptMarkdownTheme` to provide `GptMarkdownThemeData` in your intiar app.
class GptMarkdownTheme extends InheritedWidget {
const GptMarkdownTheme({
super.key,
required this.gptThemeData,
required super.child,
});
final GptMarkdownThemeData gptThemeData;
static GptMarkdownThemeData of(BuildContext context) {
final provider =
context.dependOnInheritedWidgetOfExactType<GptMarkdownTheme>();
if (provider != null) {
return provider.gptThemeData;
}
final themeData = Theme.of(context).extension<GptMarkdownThemeData>();
if (themeData != null) {
return themeData;
}
return GptMarkdownThemeData.from(context);
}
@override
bool updateShouldNotify(GptMarkdownTheme oldWidget) {
return gptThemeData != oldWidget.gptThemeData;
}
}
... ...
name: gpt_markdown
description: "The purpose of this package is to render the response of ChatGPT into a Flutter app."
version: 0.1.10
version: 0.1.11
homepage: https://github.com/saminsohag/flutter_packages/tree/main/gpt_markdown
environment:
... ...