Skip to content

Commit ec0e821

Browse files
committed
Fix auto_scroll end-following past code blocks / nested scrollables
Two issues broke auto_scroll's end-following when a tall child (e.g. a Markdown code block) was inserted: - The ScrollMetricsNotification listener reacted to *nested* scrollables (a code block renders inside a horizontal scroller), tracking the wrong extent. Only react to this scrollable's own metrics (notification.depth == 0). - _onScroll unpinned purely on proximity (pixels >= max - threshold). A tall insert jumps maxScrollExtent past the threshold in one frame while pixels stays put, flipping to unpinned so it stopped following. Now it only unpins when the user scrolls *up* (pixels decrease) and re-pins at the end; content growing beneath a stationary position keeps it pinned.
1 parent 4645958 commit ec0e821

1 file changed

Lines changed: 17 additions & 3 deletions

File tree

packages/flet/lib/src/controls/scrollable_control.dart

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class _ScrollableControlState extends State<ScrollableControl>
4343
static const double _autoScrollThreshold = 40.0;
4444
bool _pinnedToEnd = true;
4545
double _lastMaxScrollExtent = 0;
46+
double _lastPixels = 0;
4647
bool _autoScrollScheduled = false;
4748
// When set (via the `auto_scroll_animation` property) auto-scroll animates
4849
// to the end with this duration/curve; otherwise it jumps instantly.
@@ -64,9 +65,17 @@ class _ScrollableControlState extends State<ScrollableControl>
6465
void _onScroll() {
6566
if (!_controller.hasClients) return;
6667
final pos = _controller.position;
67-
// "At the end" if within a small threshold of the max extent — so a
68-
// partially-visible last line still counts as pinned.
69-
_pinnedToEnd = pos.pixels >= pos.maxScrollExtent - _autoScrollThreshold;
68+
// Unpin only when the user scrolls *up* (pixels decrease). Content growing
69+
// beneath a stationary position (e.g. a tall Markdown code block inserted
70+
// at the end) leaves pixels unchanged, so it must NOT unpin — otherwise a
71+
// big extent jump would strand the view above the new content. Re-pin once
72+
// the position returns to within a small threshold of the end.
73+
if (pos.pixels < _lastPixels - 0.5) {
74+
_pinnedToEnd = false;
75+
} else if (pos.pixels >= pos.maxScrollExtent - _autoScrollThreshold) {
76+
_pinnedToEnd = true;
77+
}
78+
_lastPixels = pos.pixels;
7079
}
7180

7281
void _scheduleScrollToEnd({bool force = false}) {
@@ -95,6 +104,11 @@ class _ScrollableControlState extends State<ScrollableControl>
95104
Widget _wrapAutoScroll(Widget child) {
96105
return NotificationListener<ScrollMetricsNotification>(
97106
onNotification: (notification) {
107+
// Only react to this scrollable's own metrics. Nested scrollables
108+
// (e.g. a Markdown code block's horizontal scroller) bubble their
109+
// notifications through with depth > 0; responding to those would
110+
// track the wrong extent and break end-following.
111+
if (notification.depth != 0) return false;
98112
final max = notification.metrics.maxScrollExtent;
99113
if (max > _lastMaxScrollExtent + 0.5 && _pinnedToEnd) {
100114
_scheduleScrollToEnd();

0 commit comments

Comments
 (0)