Fix cursor alignment when a scrollbar axis stops overflowing

This commit is contained in:
fufesou
2026-09-14 11:18:03 +08:00
parent 74c818bef9
commit 6a62b7bbd9
2 changed files with 23 additions and 10 deletions

View File

@@ -1220,9 +1220,10 @@ class _ImagePaintState extends State<ImagePaint> {
// Layout can change scroll metrics without notification. Compare against this // Layout can change scroll metrics without notification. Compare against this
// build's fractions: a delayed refresh may already have changed the model. // build's fractions: a delayed refresh may already have changed the model.
void _syncScrollAfterLayout(CanvasModel canvas) { void _syncScrollAfterLayout(CanvasModel canvas) {
// Custom scrollbars also paint from scroll fractions; preserve Original's path. // Both supported scrollbar view styles also paint from scroll fractions.
if (canvas.scrollStyle != ScrollStyle.scrolledge && if (canvas.scrollStyle != ScrollStyle.scrolledge &&
canvas.viewStyle.style != kRemoteViewStyleCustom) return; canvas.viewStyle.style != kRemoteViewStyleCustom &&
canvas.viewStyle.style != kRemoteViewStyleOriginal) return;
final renderedScroll = (canvas.scrollX, canvas.scrollY); final renderedScroll = (canvas.scrollX, canvas.scrollY);
SchedulerBinding.instance.addPostFrameCallback((_) { SchedulerBinding.instance.addPostFrameCallback((_) {
if (!mounted) return; if (!mounted) return;
@@ -1434,11 +1435,14 @@ class CursorPaint extends StatelessWidget {
// Scrollbar and edge scrolling share a layout that ignores canvas pan offsets. // Scrollbar and edge scrolling share a layout that ignores canvas pan offsets.
final imageWidth = rect.width * c.scale; final imageWidth = rect.width * c.scale;
final imageHeight = rect.height * c.scale; final imageHeight = rect.height * c.scale;
// Match the integer centering in _buildCrossScrollbarFromLayout. // Match the video's integer centering on each fitting axis, even if a
cx = (c.size.width > imageWidth ? (c.size.width - imageWidth) ~/ 2 : 0) - // detached scroll controller left a stale fraction before layout sync.
imageWidth * c.scrollX; cx = c.size.width < imageWidth
cy = (c.size.height > imageHeight ? (c.size.height - imageHeight) ~/ 2 : 0) - ? -imageWidth * c.scrollX
imageHeight * c.scrollY; : ((c.size.width - imageWidth) ~/ 2).toDouble();
cy = c.size.height < imageHeight
? -imageHeight * c.scrollY
: ((c.size.height - imageHeight) ~/ 2).toDouble();
} }
final image = m.image ?? preDefaultCursor.image; final image = m.image ?? preDefaultCursor.image;

View File

@@ -16,6 +16,12 @@ import 'cursor_test_utils.dart';
const _viewport = Size(200, 160); const _viewport = Size(200, 160);
const _hotspot = Offset(4, 9); const _hotspot = Offset(4, 9);
const _scrollDeltas = [80.0, -20.0, 1000.0, 1000.0, -1000.0, -1000.0, 80.0, 0.0]; const _scrollDeltas = [80.0, -20.0, 1000.0, 1000.0, -1000.0, -1000.0, 80.0, 0.0];
const _fittingViewports = [
Size(500, 160), // Fit horizontally while vertical scrolling remains.
Size(200, 400), // Fit vertically while horizontal scrolling remains.
Size(400, 160), // Exact fit also detaches the controller.
Size(200, 320),
];
class _Peer extends Fake implements FfiModel { class _Peer extends Fake implements FfiModel {
_Peer(ui.Image frame) _Peer(ui.Image frame)
@@ -118,6 +124,7 @@ class _FFI extends Fake implements FFI {
void main() { void main() {
for (final style in [(kRemoteViewStyleOriginal, ScrollStyle.scrolledge), for (final style in [(kRemoteViewStyleOriginal, ScrollStyle.scrolledge),
(kRemoteViewStyleOriginal, ScrollStyle.scrollbar),
(kRemoteViewStyleCustom, ScrollStyle.scrolledge), (kRemoteViewStyleCustom, ScrollStyle.scrolledge),
(kRemoteViewStyleCustom, ScrollStyle.scrollbar)]) { (kRemoteViewStyleCustom, ScrollStyle.scrollbar)]) {
for (final texture in [false, true]) { for (final texture in [false, true]) {
@@ -203,17 +210,19 @@ Future<void> _checkRelayout(
cursor.dispose(); cursor.dispose();
}); });
await _mount(tester, ffi, 1); await _mount(tester, ffi, 1);
canvas.performEdgeScroll(Vector2(20, 20));
await tester.pump();
final videoWidget = texture ? find.byType(Texture) : _paintOf(video); final videoWidget = texture ? find.byType(Texture) : _paintOf(video);
_expectAlignment(tester, ffi, videoWidget);
for (final (viewport, dpr) in [ for (final (viewport, dpr) in [
for (final viewport in _fittingViewports)
...[(viewport, 1.0), (_viewport, 1.0)],
(const Size(160, 120), 2.0), (const Size(160, 120), 2.0),
(const Size(195, 155), 2.0), // Clamp both existing scroll positions. (const Size(195, 155), 2.0), // Clamp both existing scroll positions.
(const Size(240, 155), 2.0), // Detach the horizontal scroll controller. (const Size(240, 155), 2.0), // Detach the horizontal scroll controller.
(const Size(240, 200), 2.0), // No scrolling remains. (const Size(240, 200), 2.0), // No scrolling remains.
(_viewport, 1.0), (_viewport, 1.0),
]) { ]) {
canvas.performEdgeScroll(Vector2(20, 20));
await tester.pump();
_expectAlignment(tester, ffi, videoWidget);
tester.view.devicePixelRatio = dpr; tester.view.devicePixelRatio = dpr;
tester.view.physicalSize = viewport * dpr; tester.view.physicalSize = viewport * dpr;
canvas.relayout(viewport, dpr); canvas.relayout(viewport, dpr);