Skip to content

Commit f259b95

Browse files
Fix auto_scroll end-following past code blocks / nested scrollables (#6642)
* 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. * Fix embedded FletApp stdout/stderr dropped by the framed transport The Pyodide↔Dart postMessage transport frames every packet as [type:u8][payload] (0x00 = MsgPack Flet protocol frame, 0x01 = raw DataChannel frame), and PyodideConnection.send_message prepends the 0x00 byte. The python_output shim, however, posted the raw msgpack [7, {...}] with no type byte, so the Dart side read msgpack's leading 0x92 (array-of-2 marker) as an unknown packet type and silently dropped every stdout/stderr line — the host page's Console pane never saw embedded-app output. Prepend the 0x00 type byte in both worker templates (client + cookiecutter build template).
1 parent 4645958 commit f259b95

3 files changed

Lines changed: 33 additions & 5 deletions

File tree

client/web/python-worker.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,8 +170,15 @@ self.initPyodide = async function () {
170170
import msgpack as _msgpack
171171
172172
def _send_python_output(text, is_stderr):
173+
# Frame exactly like PyodideConnection.send_message: a 0x00 type
174+
# byte (0x00 = MsgPack Flet protocol frame, 0x01 = raw DataChannel
175+
# frame) in front of the packed [action, body]. Without the prefix
176+
# the Dart side reads msgpack's leading 0x92 as an unknown packet
177+
# type and silently drops the line. bytes([0]) avoids a literal
178+
# NUL escape inside this JS template string.
173179
flet_js.receive_callback(
174-
_msgpack.packb(
180+
bytes([0])
181+
+ _msgpack.packb(
175182
[7, {"text": text, "is_stderr": bool(is_stderr)}]
176183
)
177184
)

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();

sdk/python/templates/build/{{cookiecutter.out_dir}}/web/python-worker.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,8 +170,15 @@ self.initPyodide = async function () {
170170
import msgpack as _msgpack
171171
172172
def _send_python_output(text, is_stderr):
173+
# Frame exactly like PyodideConnection.send_message: a 0x00 type
174+
# byte (0x00 = MsgPack Flet protocol frame, 0x01 = raw DataChannel
175+
# frame) in front of the packed [action, body]. Without the prefix
176+
# the Dart side reads msgpack's leading 0x92 as an unknown packet
177+
# type and silently drops the line. bytes([0]) avoids a literal
178+
# NUL escape inside this JS template string.
173179
flet_js.receive_callback(
174-
_msgpack.packb(
180+
bytes([0])
181+
+ _msgpack.packb(
175182
[7, {"text": text, "is_stderr": bool(is_stderr)}]
176183
)
177184
)

0 commit comments

Comments
 (0)