Skip to content

Instantly share code, notes, and snippets.

@justinmc
Created August 23, 2024 18:53
Show Gist options
  • Save justinmc/97bd3f3cec6f90e3ca3791f085ac3b2c to your computer and use it in GitHub Desktop.
Save justinmc/97bd3f3cec6f90e3ca3791f085ac3b2c to your computer and use it in GitHub Desktop.
Coloring a known formatted text structure based on flat ranges
import 'dart:math' as math;
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: const Padding(
padding: EdgeInsets.all(32.0),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
_RedRanger(
p1: 'Lorem ipsum dolor sit amet,',
bullets: <String>[
'consectetur adipiscing elit,',
'sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
],
p2: 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.',
redRanges: <TextRange>[
TextRange(start: 0, end: 1),
TextRange(start: 5, end: 10),
// The length of p1 is 27.
// Spanning p1 and the first bullet.
TextRange(start: 24, end: 30),
// Spanning both bullets
TextRange(start: 48, end: 58),
// The length of p1 and bullets is 121.
// Spanning the last bullet and p2.
TextRange(start: 115, end: 132),
TextRange(start: 142, end: 152),
],
),
],
),
),
),
);
}
}
class _RedRanger extends StatelessWidget {
const _RedRanger({
required this.p1,
required this.bullets,
required this.p2,
required this.redRanges,
});
final String p1;
final List<String> bullets;
final String p2;
final List<TextRange> redRanges;
static const TextStyle _redTextStyle = TextStyle(color: Colors.red);
static int _stringsLength(List<String> strings) {
return strings.fold(0, (int value, String bullet) {
return value + bullet.length;
});
}
static List<TextRange> _offsetTextRanges(List<TextRange> textRanges, int offset) {
return textRanges.map((final TextRange textRange) {
return TextRange(
start: math.max(0, textRange.start - offset),
end: textRange.end - offset,
);
}).toList();
}
// Returns the TextRanges that start after `start` and end after `end`, offset
// to be related to `start`.
static List<TextRange> _getTextRangesAt(List<TextRange> textRanges, int start, [int? end]) {
final List<TextRange> textRangesInRange = textRanges.where((TextRange textRange) {
if (textRange.end <= start) {
return false;
}
if (end != null && textRange.start >= end) {
return false;
}
return true;
}).toList();
return _offsetTextRanges(textRangesInRange, start);
}
// Expects that the ranges are relative to the start of `text`.
static List<TextSpan> _getRedParagraph(String text, List<TextRange> redRanges) {
if (redRanges.isEmpty) {
return <TextSpan>[TextSpan(text: text)];
}
final List<TextSpan> redParagraph = <TextSpan>[];
int i = 0;
for (final TextRange redRange in redRanges) {
assert(redRange.start >= i, '${redRange.start} >/= $i');
assert(redRange.start <= text.length);
if (redRange.start > i ) {
redParagraph.add(TextSpan(text: text.substring(i, redRange.start)));
i = redRange.start;
}
if (i < redRange.end) {
final int redEndInParagraph = math.min(redRange.end, text.length);
redParagraph.add(TextSpan(
text: text.substring(i, redEndInParagraph),
style: _redTextStyle,
));
// TODO(justinmc): I think it doesn't support overlapping ranges.
i = redEndInParagraph;
}
}
if (i < text.length) {
redParagraph.add(TextSpan(text: text.substring(i)));
}
return redParagraph;
}
@override
Widget build(BuildContext context) {
final List<TextRange> p1RedRanges = _getTextRangesAt(redRanges, 0, p1.length);
final int bulletsLength = _stringsLength(bullets);
final List<TextRange> p2RedRanges = _getTextRangesAt(
redRanges,
p1.length + bulletsLength,
p1.length + bulletsLength + p2.length,
);
return SelectionArea(
child: Column(
children: <Widget>[
Text.rich(
TextSpan(
children: _getRedParagraph(p1, p1RedRanges),
),
),
for (final String bullet in bullets)
Row(
children: <Widget>[
Container(
width: 8.0,
height: 8.0,
color: Colors.black,
),
Text.rich(
TextSpan(
children: _getRedParagraph(
bullet,
_getTextRangesAt(
redRanges,
p1.length + _stringsLength(bullets.sublist(0, bullets.indexOf(bullet))),
p1.length + _stringsLength(bullets.sublist(0, bullets.indexOf(bullet) + 1)),
),
),
),
),
],
),
Text.rich(
TextSpan(
children: _getRedParagraph(p2, p2RedRanges),
),
),
],
),
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment