fix(todo): a task row's check stands by its title's first line

The round check sat in the middle of the row, so on a row with a
description, a date or a long title it stood below the title. As
Todoist's it now stands by the title's first line. ListTile centres its
leading or tops it at its padding, never at the title's line, so the
check and the texts share the tile's title, the texts lowered by half
of what the check's 48 dp tap target is taller than a line (computed
from the text scale, the tap target kept).

Whats-New: A task's circle stands beside the first line of its title.
Whats-New-De: Der Kreis einer Aufgabe steht neben der ersten Zeile ihres Titels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-11-04 20:41:24 +01:00
commit a71d80fc2d
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -1,5 +1,6 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart' show kLongPressTimeout;
import 'package:flutter/rendering.dart' show RenderParagraph;
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_app/halkyon_app.dart';
@ -1878,6 +1879,54 @@ void main() {
expect(count, findsNothing);
});
testWidgets("a row's check stands by its title's first line, not in the "
"middle of the row, as Todoist's (the developer's review, 4 Nov "
'2026)', (tester) async {
await start(tester, usedBefore: true);
for (final title in [
'Short',
'A title long enough to run onto a second line and even onto a third',
]) {
await seed(
tester,
TaskContent(
title: title,
description: 'With a description',
projectId: inbox(),
date: TaskDate(Day.of(DateTime.now()).plus(1)),
),
);
}
await goTab(tester, 'Inbox');
for (final title in ['Short', 'A title long']) {
final tile = find.ancestor(
of: find.textContaining(title),
matching: find.byType(ListTile),
);
final check = tester.getRect(
find.descendant(of: tile, matching: find.byIcon(LucideIcons.circle)),
);
final text = find.textContaining(title);
final paragraph = tester.renderObject<RenderParagraph>(text);
final firstLine = paragraph
.getBoxesForSelection(
const TextSelection(baseOffset: 0, extentOffset: 1),
)
.first;
final lineCentre =
tester.getTopLeft(text).dy + (firstLine.top + firstLine.bottom) / 2;
expect(check.center.dy, moreOrLessEquals(lineCentre, epsilon: 1));
expect(
tester.getSize(
find.descendant(of: tile, matching: find.byType(IconButton)),
),
const Size.square(48),
reason: 'its tap target kept',
);
}
});
testWidgets('Today puts what is overdue first; Reschedule moves it to '
'the day picked', (tester) async {
final today = Day.of(DateTime.now());