Created
August 23, 2024 18:53
-
-
Save justinmc/97bd3f3cec6f90e3ca3791f085ac3b2c to your computer and use it in GitHub Desktop.
Coloring a known formatted text structure based on flat ranges
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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