fix(learn): a widget dropped while arranging Home stays where it was dropped, and its handle drags at once
Arranging Home kept no order of its own: a drop showed the old order until the layout saved came back through the settings, once the dashboard's changes settled 300 ms later, so the widget snapped back and then jumped. _Arranging now holds the layout as arranged, shows each change at once and saves it behind, taking a layout from the settings only once its own changes are back (or, with none pending, as changed on another device). The handle was an IconButton with an empty onPressed inside the drag listener, whose tap and tooltip competed with the drag; it is a plain icon now, named for screen readers. Whats-New: Arranging Home's widgets, a widget stays where you drop it, and dragging starts at once. Whats-New-De: Beim Anordnen der Widgets auf der Startseite bleibt ein Widget, wo du es ablegst, und das Ziehen beginnt sofort. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
bda09a2959
commit
12bc31b982
2 changed files with 93 additions and 9 deletions
|
|
@ -328,15 +328,51 @@ class const _AllHidden({required final VoidCallback onArrange})
|
|||
/// with a handle beside it to drag it elsewhere and an eye on its corner to
|
||||
/// hide or show it; the widgets themselves don't react meanwhile. A widget
|
||||
/// picked up lifts a little, with a tap the hand can feel.
|
||||
///
|
||||
/// Each change shows at once and is saved behind it: the layout saved
|
||||
/// comes back only once the dashboard's changes settle, and a drop showed
|
||||
/// the old order until then (the developer's review of 4 Nov 2026).
|
||||
class const _Arranging({
|
||||
required final HomeLayout layout,
|
||||
required final Widget Function(HomeWidget widget) section,
|
||||
required final ValueChanged<HomeLayout> onChanged,
|
||||
}) extends StatelessWidget {
|
||||
}) extends StatefulWidget {
|
||||
@override
|
||||
State<_Arranging> createState() => _ArrangingState();
|
||||
}
|
||||
|
||||
class _ArrangingState() extends State<_Arranging> {
|
||||
/// The layout as arranged here, ahead of the one saved.
|
||||
late var _layout = widget.layout;
|
||||
|
||||
/// The layouts changed here and not yet back from the settings.
|
||||
final _saving = <HomeLayout>[];
|
||||
|
||||
@override
|
||||
void didUpdateWidget(_Arranging oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
final saved = widget.layout;
|
||||
if (_saving.contains(saved)) {
|
||||
// One of ours, back: those before it are too.
|
||||
_saving.removeRange(0, _saving.indexOf(saved) + 1);
|
||||
} else if (_saving.isEmpty && saved != _layout) {
|
||||
// Changed elsewhere, as on another device.
|
||||
_layout = saved;
|
||||
}
|
||||
}
|
||||
|
||||
void _change(HomeLayout layout) {
|
||||
setState(() => _layout = layout);
|
||||
_saving.add(layout);
|
||||
widget.onChanged(layout);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final theme = Theme.of(context);
|
||||
final layout = _layout;
|
||||
final section = widget.section;
|
||||
String label(HomeWidget widget) => switch (widget) {
|
||||
HomeWidget.pomodoro => l10n.pomodoroTitle,
|
||||
HomeWidget.goal => l10n.homeWidgetGoal,
|
||||
|
|
@ -375,7 +411,7 @@ class const _Arranging({
|
|||
final order = [...layout.order];
|
||||
final moved = order.removeAt(from);
|
||||
order.insert(to, moved);
|
||||
onChanged(HomeLayout(order: order, hidden: layout.hidden));
|
||||
_change(HomeLayout(order: order, hidden: layout.hidden));
|
||||
},
|
||||
children: [
|
||||
for (final (i, widget) in layout.order.indexed)
|
||||
|
|
@ -384,14 +420,21 @@ class const _Arranging({
|
|||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: Row(
|
||||
children: [
|
||||
// A plain handle: a button's tap and a tooltip's long press
|
||||
// would compete with the drag, which then started late.
|
||||
ReorderableDragStartListener(
|
||||
index: i,
|
||||
child: IconButton(
|
||||
tooltip: l10n.homeDragToReorder,
|
||||
onPressed: () {},
|
||||
icon: Icon(
|
||||
LucideIcons.gripVertical,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.grab,
|
||||
child: Semantics(
|
||||
label: l10n.homeDragToReorder,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Icon(
|
||||
LucideIcons.gripVertical,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -417,7 +460,7 @@ class const _Arranging({
|
|||
child: _EyeBadge(
|
||||
hidden: layout.hidden.contains(widget),
|
||||
label: label(widget),
|
||||
onTap: () => onChanged(
|
||||
onTap: () => _change(
|
||||
HomeLayout(
|
||||
order: layout.order,
|
||||
hidden: layout.hidden.contains(widget)
|
||||
|
|
|
|||
|
|
@ -367,4 +367,45 @@ void main() {
|
|||
await tester.pumpAndSettle();
|
||||
expect(find.text('Arrange widgets'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('a widget dropped elsewhere stays where it was dropped at '
|
||||
"once, rather than snapping back until it is saved (the developer's "
|
||||
'review of 4 Nov 2026)', (tester) async {
|
||||
await showHome(tester);
|
||||
await tester.longPress(find.text('Set a study goal'));
|
||||
await tester.pumpAndSettle();
|
||||
double top(String name) =>
|
||||
tester.getTopLeft(find.byTooltip('Hide $name')).dy;
|
||||
final handles = find.byIcon(LucideIcons.gripVertical);
|
||||
final first = tester.getTopLeft(handles.at(0)).dy;
|
||||
final second = tester.getTopLeft(handles.at(1)).dy;
|
||||
final names = [
|
||||
for (final name in [
|
||||
'Daily goal',
|
||||
'Pick up learning',
|
||||
'Pomodoro',
|
||||
'PDF reading deadlines',
|
||||
'Week overview',
|
||||
'Activity heatmap',
|
||||
])
|
||||
if (find.byTooltip('Hide $name').evaluate().isNotEmpty) name,
|
||||
]..sort((a, b) => top(a).compareTo(top(b)));
|
||||
final moved = names.first;
|
||||
|
||||
// The handle takes the drag at once, without a tap of its own first.
|
||||
final drag = await tester.startGesture(tester.getCenter(handles.at(0)));
|
||||
for (var step = 0; step < 6; step++) {
|
||||
await drag.moveBy(Offset(0, (second - first) * 1.5 / 6));
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
}
|
||||
await drag.up();
|
||||
// Dropped, well before the dashboard's changes settle (300 ms).
|
||||
for (var frame = 0; frame < 12; frame++) {
|
||||
await tester.pump(const Duration(milliseconds: 16));
|
||||
}
|
||||
expect(top(moved), greaterThan(top(names[1])), reason: 'moved down');
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
expect(top(moved), greaterThan(top(names[1])), reason: 'and stays');
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue