fix(learn): a passage's gaps flow inside its text
A passage was a Wrap of a Text per part between gaps. A Wrap places whole children, so a part longer than a line took the width to itself, and every gap and the text after it started a line of its own. One Text.rich with each gap a WidgetSpan wraps the text as one paragraph, the gaps standing in it. Whats-New: Fill-in-the-blank passages read as running text, their gaps inside the lines. Whats-New-De: Lückentexte lesen sich als fortlaufender Text, die Lücken stehen in den Zeilen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
3ee29de391
commit
b45cb0f3cb
2 changed files with 71 additions and 18 deletions
|
|
@ -339,26 +339,38 @@ class const FillInTheBlankView({
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Wrap(
|
// One paragraph with the gaps standing in its lines: a Wrap of a
|
||||||
crossAxisAlignment: WrapCrossAlignment.center,
|
// Text per part gave a long part the width to itself, and put each
|
||||||
runSpacing: 8,
|
// gap, and the text after it, on a line of its own.
|
||||||
children: [
|
Text.rich(
|
||||||
for (var gap = 0; gap < exercise.gaps; gap++) ...[
|
TextSpan(
|
||||||
Text(parts[gap], style: theme.textTheme.bodyLarge),
|
style: theme.textTheme.bodyLarge,
|
||||||
DragTarget<String>(
|
children: [
|
||||||
onAcceptWithDetails: (details) => fill(gap, details.data),
|
for (var gap = 0; gap < exercise.gaps; gap++) ...[
|
||||||
builder: (context, candidates, _) => _Gap(
|
TextSpan(text: parts[gap]),
|
||||||
word: words[gap],
|
WidgetSpan(
|
||||||
highlighted: candidates.isNotEmpty,
|
alignment: PlaceholderAlignment.middle,
|
||||||
mark: markOf(grade, gap, picked: words[gap] != null),
|
child: Padding(
|
||||||
onTap: words[gap] == null
|
// Room above and below, so a gap on one line doesn't
|
||||||
? null
|
// touch a gap on the next.
|
||||||
: () => onChanged([...words]..[gap] = null),
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
|
child: DragTarget<String>(
|
||||||
|
onAcceptWithDetails: (details) => fill(gap, details.data),
|
||||||
|
builder: (context, candidates, _) => _Gap(
|
||||||
|
word: words[gap],
|
||||||
|
highlighted: candidates.isNotEmpty,
|
||||||
|
mark: markOf(grade, gap, picked: words[gap] != null),
|
||||||
|
onTap: words[gap] == null
|
||||||
|
? null
|
||||||
|
: () => onChanged([...words]..[gap] = null),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
|
TextSpan(text: parts.last),
|
||||||
],
|
],
|
||||||
Text(parts.last, style: theme.textTheme.bodyLarge),
|
),
|
||||||
],
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Wrap(
|
Wrap(
|
||||||
|
|
|
||||||
|
|
@ -948,6 +948,47 @@ void main() {
|
||||||
expect(find.text('Correct!'), findsOneWidget);
|
expect(find.text('Correct!'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('a passage is one paragraph, its gaps standing in its lines; '
|
||||||
|
'a filled gap tapped gives its word back', (tester) async {
|
||||||
|
final paths = repository();
|
||||||
|
final id = await paths.createPath(
|
||||||
|
'japanese',
|
||||||
|
LearningPathContent.titled('Particles'),
|
||||||
|
);
|
||||||
|
await paths.createStep(
|
||||||
|
id,
|
||||||
|
StepKind.fillInTheBlank,
|
||||||
|
step(
|
||||||
|
StepKind.fillInTheBlank,
|
||||||
|
'Fill in',
|
||||||
|
const FillInTheBlank(passage: '私___学生です。', blanks: ['は']),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await showApp(tester, RoutePaths.path('japanese', id));
|
||||||
|
final passage = find.byWidgetPredicate(
|
||||||
|
(widget) =>
|
||||||
|
widget is RichText && widget.text.toPlainText().startsWith('私'),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(passage, findsOneWidget);
|
||||||
|
expect(
|
||||||
|
(tester.widget<RichText>(passage).text as TextSpan).toPlainText(),
|
||||||
|
contains('学生です。'),
|
||||||
|
reason: 'the text after the gap is in the same paragraph',
|
||||||
|
);
|
||||||
|
|
||||||
|
final inPassage = find.descendant(of: passage, matching: find.text('は'));
|
||||||
|
await tester.tap(find.widgetWithText(ActionChip, 'は'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(inPassage, findsOneWidget);
|
||||||
|
expect(find.text('は'), findsOneWidget, reason: 'gone from the words');
|
||||||
|
|
||||||
|
await tester.tap(inPassage);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(inPassage, findsNothing);
|
||||||
|
expect(find.text('は'), findsOneWidget, reason: 'back among the words');
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('sorts items by tapping one and then its category', (
|
testWidgets('sorts items by tapping one and then its category', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue