kashida 0.0.2
kashida: ^0.0.2 copied to clipboard
Finding kashida (tatweel) insertion points and priorities, driven by a small pattern language.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:kashida/kashida.dart';
import 'package:kashida_example/demo_fonts.dart';
import 'package:kashida_example/justify.dart';
void main() {
runApp(const KashidaExampleApp());
}
class KashidaExampleApp extends StatelessWidget {
const KashidaExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Kashida example',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF1B5E4B)),
useMaterial3: true,
),
home: const KashidaDemoPage(),
);
}
}
class KashidaDemoPage extends StatefulWidget {
const KashidaDemoPage({super.key});
@override
State<KashidaDemoPage> createState() => _KashidaDemoPageState();
}
class _KashidaDemoPageState extends State<KashidaDemoPage> {
final _controller = TextEditingController(text: sampleText);
String _patternName = 'arabic-naskh';
String _fontId = demoFonts.first.id;
double _fontSize = 24;
double _paragraphWidth = 600;
double _minKashidas = 2;
double _maxKashidas = 4;
bool _justify = true;
bool _applyKashida = true;
@override
void dispose() {
_controller.dispose();
super.dispose();
}
DemoFont get _font => demoFontById(_fontId);
TextStyle get _textStyle => _font.styleAt(_fontSize);
@override
Widget build(BuildContext context) {
final set = requiredBuiltinPatternSet(_patternName);
final text = _controller.text;
final points = findKashidaPoints(text, set).points;
return Scaffold(
appBar: AppBar(title: const Text('Kashida example')),
body: LayoutBuilder(
builder: (context, constraints) {
const listPadding = 16.0;
const boxPadding = 12.0;
final availableWidth =
constraints.maxWidth - listPadding * 2 - boxPadding * 2;
final paragraphWidth = clampParagraphWidth(
_paragraphWidth,
availableWidth,
);
final widthCapped = paragraphWidth < _paragraphWidth;
return ListView(
padding: const EdgeInsets.all(listPadding),
children: [
TextField(
controller: _controller,
minLines: 3,
maxLines: 8,
textDirection: TextDirection.rtl,
style: _textStyle,
onChanged: (_) => setState(() {}),
decoration: const InputDecoration(
labelText: 'Text',
border: OutlineInputBorder(),
alignLabelWithHint: true,
),
),
const SizedBox(height: 16),
Wrap(
spacing: 12,
runSpacing: 12,
children: [
DropdownMenu<String>(
initialSelection: _fontId,
label: const Text('Font'),
dropdownMenuEntries: [
for (final font in demoFonts)
DropdownMenuEntry(value: font.id, label: font.label),
],
onSelected: (value) {
if (value == null) {
return;
}
setState(() => _fontId = value);
},
),
DropdownMenu<String>(
initialSelection: _patternName,
label: const Text('Pattern set'),
helperText: 'Suggested: ${_font.suggestedPattern}',
dropdownMenuEntries: [
for (final name in builtinPatternSetNames())
DropdownMenuEntry(value: name, label: name),
],
onSelected: (value) {
if (value == null) {
return;
}
setState(() => _patternName = value);
},
),
FilterChip(
label: const Text('Justify'),
selected: _justify,
onSelected: (value) => setState(() => _justify = value),
),
FilterChip(
label: const Text('Apply kashida'),
selected: _applyKashida,
onSelected: _justify
? (value) => setState(() => _applyKashida = value)
: null,
),
],
),
const SizedBox(height: 8),
Text('Size: ${_fontSize.round()}px'),
Slider(
min: 16,
max: 48,
divisions: 32,
value: _fontSize,
onChanged: (value) => setState(() => _fontSize = value),
),
Text(
widthCapped
? 'Paragraph width: ${paragraphWidth.round()}px '
'(capped from ${_paragraphWidth.round()}px)'
: 'Paragraph width: ${_paragraphWidth.round()}px',
),
Slider(
min: 280,
max: 800,
divisions: 52,
value: _paragraphWidth,
onChanged: (value) => setState(() => _paragraphWidth = value),
),
Text('Min kashidas per point: ${_minKashidas.round()}'),
Slider(
min: 1,
max: 6,
divisions: 5,
value: _minKashidas,
onChanged: (value) => setState(() => _minKashidas = value),
),
Text('Max kashidas per point: ${_maxKashidas.round()}'),
Slider(
min: 1,
max: 8,
divisions: 7,
value: _maxKashidas,
onChanged: (value) {
setState(() {
_maxKashidas = value;
if (_minKashidas > _maxKashidas) {
_minKashidas = _maxKashidas;
}
});
},
),
const SizedBox(height: 8),
_Section(
title: _justify
? 'Justified (last line of each paragraph left short)'
: 'Unjustified',
child: Align(
alignment: Alignment.topRight,
child: DecoratedBox(
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.outlineVariant,
),
borderRadius: BorderRadius.circular(8),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: SizedBox(
width: paragraphWidth,
child: KashidaText(
text,
patternSet: set,
width: paragraphWidth,
style: _textStyle,
textScaler: MediaQuery.textScalerOf(context),
minKashidas: _minKashidas.round(),
maxKashidas: _maxKashidas.round(),
justified: _justify,
applyKashida: _applyKashida,
),
),
),
),
),
),
const SizedBox(height: 16),
_Section(
title: 'Kashida points (${points.length})',
child: points.isEmpty
? const Text('No insertion points in this text.')
: Wrap(
spacing: 8,
runSpacing: 8,
children: [
for (final point in points)
Chip(
label: Text(
'after ${point.index} · priority ${point.priority}',
),
),
],
),
),
],
);
},
),
);
}
}
class _Section extends StatelessWidget {
const _Section({required this.title, required this.child});
final String title;
final Widget child;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(title, style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8),
child,
],
);
}
}