fix(app): study faces centred in the card
The read-only view passed its alignment to the text alone, which the editor lays out only as wide as itself, so a centred face stayed at the start. It now aligns top-level paragraphs and headings that have no alignment of their own; lists, quotes and the rest keep the start, and an alignment chosen in the format bar wins. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
43b2d86fc0
commit
7c00abcc05
4 changed files with 135 additions and 22 deletions
|
|
@ -88,31 +88,21 @@ class const EditorBlocksScope({
|
|||
/// English.
|
||||
Map<String, BlockComponentBuilder> buildersWithUnknown(
|
||||
EditorBlocks blocks,
|
||||
AppLocalizations l10n, {
|
||||
TextAlign? textAlign,
|
||||
}) => {
|
||||
AppLocalizations l10n,
|
||||
) => {
|
||||
for (final MapEntry(key: type, value: builder) in blocks.builders.entries)
|
||||
type: _withPlaceholder(builder, l10n, textAlign),
|
||||
type: _withPlaceholder(builder, l10n),
|
||||
errorBlockComponentBuilderKey: _UnknownBlockBuilder(),
|
||||
};
|
||||
|
||||
/// [builder] alike, but saying what its block is in [l10n]'s language while
|
||||
/// empty, for the editor's own text blocks, and aligning their text as
|
||||
/// [textAlign] says where the text doesn't say otherwise; others as they
|
||||
/// are.
|
||||
/// empty, for the editor's own text blocks; others as they are.
|
||||
BlockComponentBuilder _withPlaceholder(
|
||||
BlockComponentBuilder builder,
|
||||
AppLocalizations l10n,
|
||||
TextAlign? textAlign,
|
||||
) {
|
||||
BlockComponentConfiguration saying(String Function(Node node) text) =>
|
||||
builder.configuration.copyWith(
|
||||
placeholderText: text,
|
||||
textAlign: switch (textAlign) {
|
||||
final align? => (_) => align,
|
||||
null => null,
|
||||
},
|
||||
);
|
||||
builder.configuration.copyWith(placeholderText: text);
|
||||
return switch (builder) {
|
||||
final ParagraphBlockComponentBuilder paragraph =>
|
||||
ParagraphBlockComponentBuilder(
|
||||
|
|
|
|||
|
|
@ -24,7 +24,9 @@ class const RichContentView({
|
|||
/// The text's style; the theme's `bodyLarge` without.
|
||||
final TextStyle? textStyle,
|
||||
|
||||
/// How the text aligns where it doesn't say itself; at the start without.
|
||||
/// How paragraphs and headings align where the editor gave them no
|
||||
/// alignment of their own; at the start without. Lists, quotes and the
|
||||
/// other blocks keep the start: centred, a list's markers drift apart.
|
||||
final TextAlign? textAlign,
|
||||
super.key,
|
||||
}) extends StatefulWidget {
|
||||
|
|
@ -33,16 +35,17 @@ class const RichContentView({
|
|||
}
|
||||
|
||||
class _RichContentViewState() extends State<RichContentView> {
|
||||
late EditorState _editorState = _readOnly(widget.content);
|
||||
late EditorState _editorState = _readOnly(widget.content, widget.textAlign);
|
||||
|
||||
@override
|
||||
void didUpdateWidget(RichContentView oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.content != oldWidget.content) {
|
||||
if (widget.content != oldWidget.content ||
|
||||
widget.textAlign != oldWidget.textAlign) {
|
||||
// The old editor is still attached until this frame is built.
|
||||
final previous = _editorState;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => previous.dispose());
|
||||
_editorState = _readOnly(widget.content);
|
||||
_editorState = _readOnly(widget.content, widget.textAlign);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -68,7 +71,6 @@ class _RichContentViewState() extends State<RichContentView> {
|
|||
blockComponentBuilders: buildersWithUnknown(
|
||||
blocksOf(context),
|
||||
AppLocalizations.of(context),
|
||||
textAlign: widget.textAlign,
|
||||
),
|
||||
editable: false,
|
||||
disableSelectionService: true,
|
||||
|
|
@ -78,5 +80,25 @@ class _RichContentViewState() extends State<RichContentView> {
|
|||
);
|
||||
}
|
||||
|
||||
EditorState _readOnly(RichContent content) =>
|
||||
EditorState(document: content.toDocument())..editable = false;
|
||||
/// [content] to show, its top-level paragraphs and headings aligned as
|
||||
/// [textAlign] says where they don't say themselves. The editor lays a text
|
||||
/// block's text out only as wide as it is, so aligning the text alone moves
|
||||
/// nothing: the block's own `align` places it. The document is never saved.
|
||||
EditorState _readOnly(RichContent content, TextAlign? textAlign) {
|
||||
final document = content.toDocument();
|
||||
final align = switch (textAlign) {
|
||||
TextAlign.center => 'center',
|
||||
TextAlign.right => 'right',
|
||||
_ => null,
|
||||
};
|
||||
if (align != null) {
|
||||
for (final node in document.root.children) {
|
||||
if ((node.type == ParagraphBlockKeys.type ||
|
||||
node.type == HeadingBlockKeys.type) &&
|
||||
node.attributes[blockComponentAlign] == null) {
|
||||
node.updateAttributes({blockComponentAlign: align});
|
||||
}
|
||||
}
|
||||
}
|
||||
return EditorState(document: document)..editable = false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,4 +74,83 @@ void main() {
|
|||
expect(tapped, 1);
|
||||
expect(tester.testTextInput.hasAnyClients, isFalse);
|
||||
}, variant: TargetPlatformVariant.all());
|
||||
|
||||
group('asked to centre', () {
|
||||
Map<String, Object?> block(
|
||||
String type,
|
||||
String text, [
|
||||
Map<String, Object?> data = const {},
|
||||
]) => {
|
||||
'type': type,
|
||||
'data': {
|
||||
...data,
|
||||
'delta': [
|
||||
{'insert': text},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
Future<void> showCentred(
|
||||
WidgetTester tester,
|
||||
List<Map<String, Object?>> blocks,
|
||||
) async {
|
||||
tester.view.physicalSize = const Size(400, 800);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
home: Scaffold(
|
||||
body: ListView(
|
||||
children: [
|
||||
RichContentView(
|
||||
content: RichContent({
|
||||
'document': {'type': 'page', 'children': blocks},
|
||||
}),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
}
|
||||
|
||||
double centreOf(WidgetTester tester, String text) =>
|
||||
tester.getCenter(find.text(text, findRichText: true)).dx;
|
||||
double leftOf(WidgetTester tester, String text) =>
|
||||
tester.getTopLeft(find.text(text, findRichText: true)).dx;
|
||||
|
||||
testWidgets('centres paragraphs and headings', (tester) async {
|
||||
await showCentred(tester, [
|
||||
block('heading', 'Greetings', {'level': 2}),
|
||||
block('paragraph', 'hola'),
|
||||
]);
|
||||
|
||||
expect(centreOf(tester, 'Greetings'), moreOrLessEquals(200, epsilon: 1));
|
||||
expect(centreOf(tester, 'hola'), moreOrLessEquals(200, epsilon: 1));
|
||||
});
|
||||
|
||||
testWidgets('leaves a list at the start: centred, its markers would '
|
||||
'drift apart', (tester) async {
|
||||
await showCentred(tester, [
|
||||
block('bulleted_list', 'hola'),
|
||||
block('bulleted_list', 'buenos días'),
|
||||
]);
|
||||
|
||||
expect(leftOf(tester, 'hola'), lessThan(60));
|
||||
expect(leftOf(tester, 'buenos días'), leftOf(tester, 'hola'));
|
||||
});
|
||||
|
||||
testWidgets('keeps the alignment a paragraph was given in the editor', (
|
||||
tester,
|
||||
) async {
|
||||
await showCentred(tester, [
|
||||
block('paragraph', 'hola', {'align': 'left'}),
|
||||
]);
|
||||
|
||||
expect(leftOf(tester, 'hola'), lessThan(40));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -138,6 +138,28 @@ void main() {
|
|||
expect(find.textContaining('cat', findRichText: true), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('the faces are centred in the card, and a tap on their text '
|
||||
'shows and hides the answer too', (tester) async {
|
||||
await cards.create(
|
||||
'japanese',
|
||||
CardContent(front: paragraph('猫'), back: paragraph('cat')),
|
||||
);
|
||||
await study(tester, width: 400);
|
||||
final card = tester.getRect(find.byType(StudyCardSurface));
|
||||
double centreOf(String text) =>
|
||||
tester.getCenter(find.text(text, findRichText: true)).dx;
|
||||
|
||||
expect(centreOf('猫'), moreOrLessEquals(card.center.dx, epsilon: 1));
|
||||
|
||||
await tester.tap(find.text('猫', findRichText: true));
|
||||
await tester.pumpAndSettle();
|
||||
expect(centreOf('cat'), moreOrLessEquals(card.center.dx, epsilon: 1));
|
||||
|
||||
await tester.tap(find.text('cat', findRichText: true));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.textContaining('cat', findRichText: true), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('on a phone, a card swiped right is rated Good, one let go '
|
||||
'early springs back', (tester) async {
|
||||
await cards.create(
|
||||
|
|
|
|||
Loading…
Reference in a new issue