From 27c4c56c8a167d38c8bb71fb5a18fde5fd953e86 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Tue, 5 May 2026 20:37:15 +0200 Subject: [PATCH 01/21] initial commit --- .../src/controls/navigation_bar_destination.dart | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/flet/lib/src/controls/navigation_bar_destination.dart b/packages/flet/lib/src/controls/navigation_bar_destination.dart index 53ad0cc301..ab227eec87 100644 --- a/packages/flet/lib/src/controls/navigation_bar_destination.dart +++ b/packages/flet/lib/src/controls/navigation_bar_destination.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import '../extensions/control.dart'; import '../models/control.dart'; -import '../utils/icons.dart'; import '../utils/numbers.dart'; import 'base_controls.dart'; @@ -15,14 +14,13 @@ class NavigationBarDestinationControl extends StatelessWidget { Widget build(BuildContext context) { debugPrint("NavigationBarDestination build: ${control.id}"); - var selectedIcon = control.getIconData("selected_icon"); var child = NavigationDestination( - enabled: !control.disabled, - tooltip: !control.disabled ? control.getString("tooltip") : null, - icon: control.buildIconOrWidget("icon")!, - selectedIcon: control.buildWidget("selected_icon") ?? - (selectedIcon != null ? Icon(selectedIcon) : null), - label: control.getString("label", "")!); + enabled: !control.disabled, + tooltip: !control.disabled ? control.getString("tooltip") : null, + icon: control.buildIconOrWidget("icon")!, + selectedIcon: control.buildIconOrWidget("selected_icon"), + label: control.getString("label", "")!, + ); return BaseControl(control: control, child: child); } From d9f276b13a620d43fc619cf2b2b6246e2a8eef2c Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Tue, 5 May 2026 20:39:28 +0200 Subject: [PATCH 02/21] update test --- .../golden/macos/navigation_bar/basic.png | Bin 13255 -> 12887 bytes .../controls/material/test_navigation_bar.py | 5 +++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/sdk/python/packages/flet/integration_tests/controls/material/golden/macos/navigation_bar/basic.png b/sdk/python/packages/flet/integration_tests/controls/material/golden/macos/navigation_bar/basic.png index 4baf1782514a62da059c47c37fd1d617b30789e4..91e6047876437102490831f8ef2a4ccac128a090 100644 GIT binary patch literal 12887 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 literal 13255 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 XiQ|&AEee-^_7W Date: Tue, 5 May 2026 20:40:19 +0200 Subject: [PATCH 03/21] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa0a30246a..cb1d36df57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ * Fix `Page.on_resize` and `Page.on_media_change` not firing after mobile orientation changes ([#6457](https://github.com/flet-dev/flet/issues/6457), [#6423](https://github.com/flet-dev/flet/pull/6423)) by @ndonkoHenri. * Fix `flet pack` desktop packaging so Windows and Linux bundles include the expected client archive, and Windows taskbar pins point to the packed app instead of the cached `flet.exe` ([#5151](https://github.com/flet-dev/flet/issues/5151), [#6403](https://github.com/flet-dev/flet/pull/6403)) by @ndonkoHenri. * Fix environment variable priority in `flet build` template: inherit from `Platform.environment` and use `putIfAbsent` for FLET_* variables so pre-set system env vars are not overwritten ([#6394](https://github.com/flet-dev/flet/pull/6394)) by @Bahtya. +* Fix `NavigationBarDestination.selected_icon` rendering wrongly when provided as an `Icon` control ([#6460](https://github.com/flet-dev/flet/issues/6460)) by @ndonkoHenri. * Fix 3- and 4-digit hex color shorthand (e.g. `#c00`, `#fc00`) rendering as invisible by expanding them to their full 6/8-digit forms ([#6419](https://github.com/flet-dev/flet/issues/6419), [#6421](https://github.com/flet-dev/flet/pull/6421)) by @ndonkoHenri. * Fix `LineChart` (and other charts) silently dropping custom `ChartAxisLabel` entries whose `value` matched a tick only after floating-point rounding (e.g. `0.1`, `0.2`, `0.3`) by switching label lookup to a tolerance-based comparison scaled to the axis interval ([#6445](https://github.com/flet-dev/flet/issues/6445), [#6459](https://github.com/flet-dev/flet/pull/6459)) by @KangZhaoKui. From f1929bed6af87801786e373378ad27676e4eff78 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Tue, 5 May 2026 20:46:37 +0200 Subject: [PATCH 04/21] update changelog with PR number --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb1d36df57..f1a93b64a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,7 +29,7 @@ * Fix `Page.on_resize` and `Page.on_media_change` not firing after mobile orientation changes ([#6457](https://github.com/flet-dev/flet/issues/6457), [#6423](https://github.com/flet-dev/flet/pull/6423)) by @ndonkoHenri. * Fix `flet pack` desktop packaging so Windows and Linux bundles include the expected client archive, and Windows taskbar pins point to the packed app instead of the cached `flet.exe` ([#5151](https://github.com/flet-dev/flet/issues/5151), [#6403](https://github.com/flet-dev/flet/pull/6403)) by @ndonkoHenri. * Fix environment variable priority in `flet build` template: inherit from `Platform.environment` and use `putIfAbsent` for FLET_* variables so pre-set system env vars are not overwritten ([#6394](https://github.com/flet-dev/flet/pull/6394)) by @Bahtya. -* Fix `NavigationBarDestination.selected_icon` rendering wrongly when provided as an `Icon` control ([#6460](https://github.com/flet-dev/flet/issues/6460)) by @ndonkoHenri. +* Fix `NavigationBarDestination.selected_icon` rendering wrongly when provided as an `Icon` control ([#6460](https://github.com/flet-dev/flet/issues/6460), [#6468](https://github.com/flet-dev/flet/pull/6468)) by @ndonkoHenri. * Fix 3- and 4-digit hex color shorthand (e.g. `#c00`, `#fc00`) rendering as invisible by expanding them to their full 6/8-digit forms ([#6419](https://github.com/flet-dev/flet/issues/6419), [#6421](https://github.com/flet-dev/flet/pull/6421)) by @ndonkoHenri. * Fix `LineChart` (and other charts) silently dropping custom `ChartAxisLabel` entries whose `value` matched a tick only after floating-point rounding (e.g. `0.1`, `0.2`, `0.3`) by switching label lookup to a tolerance-based comparison scaled to the axis interval ([#6445](https://github.com/flet-dev/flet/issues/6445), [#6459](https://github.com/flet-dev/flet/pull/6459)) by @KangZhaoKui. From 409fab6ee19b2e80872b79b4e966221710fb8b7e Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Tue, 5 May 2026 21:24:19 +0200 Subject: [PATCH 05/21] fix #6443: improve `LineChart` event spot serialization --- sdk/python/packages/flet-charts/src/flet_charts/line_chart.py | 4 ++++ .../src/flutter/flet_charts/lib/src/utils/line_chart.dart | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/sdk/python/packages/flet-charts/src/flet_charts/line_chart.py b/sdk/python/packages/flet-charts/src/flet_charts/line_chart.py index 184a362bd4..f49cb9289a 100644 --- a/sdk/python/packages/flet-charts/src/flet_charts/line_chart.py +++ b/sdk/python/packages/flet-charts/src/flet_charts/line_chart.py @@ -59,6 +59,10 @@ class LineChartEvent(ft.Event["LineChart"]): spots: list[LineChartEventSpot] """ Spots on which the event occurred. + + Note: + This list is empty when the event does not target a concrete point, for + example when the pointer hovers over or taps empty chart space. """ diff --git a/sdk/python/packages/flet-charts/src/flutter/flet_charts/lib/src/utils/line_chart.dart b/sdk/python/packages/flet-charts/src/flutter/flet_charts/lib/src/utils/line_chart.dart index e5ad8afcaf..437b0f7aee 100644 --- a/sdk/python/packages/flet-charts/src/flutter/flet_charts/lib/src/utils/line_chart.dart +++ b/sdk/python/packages/flet-charts/src/flutter/flet_charts/lib/src/utils/line_chart.dart @@ -25,7 +25,7 @@ class LineChartEventData extends Equatable { Map toMap() => { 'type': eventType, - 'spots': barSpots, + 'spots': barSpots.map((spot) => spot.toMap()).toList(), }; @override From 5e187807ebc784a204a72e2d4e5fcd2625605677 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Tue, 5 May 2026 21:51:05 +0200 Subject: [PATCH 06/21] update changelogs and add dev-testing-skill --- .agents/skills/test-flet-apps-dev/SKILL.md | 152 +++++++++++++++++++ CHANGELOG.md | 1 + sdk/python/packages/flet-charts/CHANGELOG.md | 7 + 3 files changed, 160 insertions(+) create mode 100644 .agents/skills/test-flet-apps-dev/SKILL.md diff --git a/.agents/skills/test-flet-apps-dev/SKILL.md b/.agents/skills/test-flet-apps-dev/SKILL.md new file mode 100644 index 0000000000..837a7fe852 --- /dev/null +++ b/.agents/skills/test-flet-apps-dev/SKILL.md @@ -0,0 +1,152 @@ +--- +name: test-flet-apps-dev +description: Use when testing or debugging Flet apps in maintainer/contributor development mode with local Python package sources and the local Flutter client, including web, desktop, browser, and computer-use verification workflows. +--- + +# Test Flet Apps In Dev Mode + +Use this skill when validating a Flet app, example, feature, or bug fix against +this repo's local Python packages and/or local Flutter client during maintainer +or contributor work. + +## Core model + +Flet dev-mode testing usually has one or two processes: + +1. A Python Flet app/server, run from `sdk/python`. +2. The local Flutter client, run from `client`, when Dart/client or extension code + must be tested. + +If only Python code changed, `uv run flet run ...` is often enough. If Dart, +Flutter, extension, or transport code changed, run the local Flutter client so +the changed Dart code is actually used. + +The local Flutter client debug build uses a fixed app-server URL from +`client/lib/main.dart`: + +```dart +if (kDebugMode) { + pageUrl = "http://localhost:8550"; +} +``` + +Therefore, when using the local Flutter client without extra URL arguments, +start the Python app on port `8550`. + +## Start the Python app + +Run from `{repo}/sdk/python`: + +```bash +uv run flet run -w -p 8550 examples/controls/core/interactive_viewer/handling_events/main.py +``` + +Adjust the sample path as needed. Use port `8550` when the local Flutter client +will connect with its default debug URL. Keep this process running and watch its +stdout for Python callback output, tracebacks, and event payloads. + +For web-only checks with the packaged web client, open: + +```text +http://127.0.0.1:8550 +``` + +## Run the local Flutter client + +Run these from `{repo}/client`. + +### Desktop + +Use when validating native desktop behavior on the current host OS: + +```bash +fvm flutter run -d macos # macOS +fvm flutter run -d windows # Windows +fvm flutter run -d linux # Linux +``` + +The debug client defaults to `http://localhost:8550` when no app URL argument is +provided. Use the platform target that exists on the current machine. Use +Computer Use or the relevant platform automation to navigate and interact with +the app window. + +### Flutter web in Chrome + +Use when Dart web behavior must be validated in Flutter's default web debug +browser: + +```bash +fvm flutter run -d chrome +``` + +This opens a fresh browser connected to the Python app server on port `8550`. + +### Flutter web in another Chromium browser + +If the requested browser is not listed by `flutter devices`, prefer the web +server target and open the served URL in that browser: + +```bash +fvm flutter run -d web-server --web-hostname 127.0.0.1 --web-port 8660 +open -a "Brave Browser" http://127.0.0.1:8660 +``` + +Using `CHROME_EXECUTABLE` can work, but Flutter may fail to attach its debug +websocket in non-default Chromium browsers. Fall back to `web-server` if that +happens. + +## Browser and UI interaction + +- For local browser targets (`localhost`, `127.0.0.1`, `file://`), prefer the + in-app browser or the Browser Use plugin when explicitly requested. +- Use Computer Use for native desktop apps and external browsers when browser + MCP is not the requested tool or cannot control that browser. +- For chart/canvas-heavy UI, click/hover coordinates may be necessary because + accessibility trees often expose only the HTML canvas container. + +## Reading evidence + +Always inspect both sides: + +- Python app stdout: event payloads, user `print()` calls, Python tracebacks. +- Flutter run stdout: client-side event payloads, WebSocket messages, Flutter + exceptions, hot reload/restart status. +- Browser/app state: the actual rendered UI and any visible error banner. + +For client/server protocol bugs, compare the raw outgoing Dart event in Flutter +logs with the decoded Python event object in Python logs. + +## Hot reload and restart + +For Flutter client sessions: + +- Press `r` for hot reload after many Dart-only edits. +- Press `R` for hot restart if state, initialization, or extension registration + may be stale. +- Quit with `q` before final response unless the user explicitly wants the app + left running. +- Press `h` for help on other Flutter run key commands. + +For Python app sessions, restart `uv run flet run ...` after Python source or +sample changes if the running process does not pick them up. + +## Troubleshooting + +- If a sandboxed Flutter command fails trying to write FVM or Flutter cache files + such as `engine.stamp`, rerun the same command with escalation. +- If `flutter devices` does not list Brave/Edge/etc., use `flutter run -d web-server` + and open the URL in the target browser. +- If the UI shows a generic Flet error banner, check Python stdout first; the + root cause is often a handler exception. +- If an event handler indexes a list payload, confirm the empty-list case before + treating it as a framework bug. +- If the local Flutter client cannot connect, confirm the Python app is running + on port `8550` or pass an explicit app URL when the client path supports it. + +## Finish checklist + +- Stop long-running app/test sessions unless asked to leave them running. +- State exactly which surfaces were tested: packaged web, local Flutter web, + desktop target, target browser, or sample-only. +- Include the key observed payload/error before and after the fix. +- Separate framework bugs from sample-code guard issues. diff --git a/CHANGELOG.md b/CHANGELOG.md index f1a93b64a0..e797772359 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ * Fix environment variable priority in `flet build` template: inherit from `Platform.environment` and use `putIfAbsent` for FLET_* variables so pre-set system env vars are not overwritten ([#6394](https://github.com/flet-dev/flet/pull/6394)) by @Bahtya. * Fix `NavigationBarDestination.selected_icon` rendering wrongly when provided as an `Icon` control ([#6460](https://github.com/flet-dev/flet/issues/6460), [#6468](https://github.com/flet-dev/flet/pull/6468)) by @ndonkoHenri. * Fix 3- and 4-digit hex color shorthand (e.g. `#c00`, `#fc00`) rendering as invisible by expanding them to their full 6/8-digit forms ([#6419](https://github.com/flet-dev/flet/issues/6419), [#6421](https://github.com/flet-dev/flet/pull/6421)) by @ndonkoHenri. +* Fix `LineChartEvent.spots` returning undecoded MessagePack extension values instead of `LineChartEventSpot` objects ([#6443](https://github.com/flet-dev/flet/issues/6443), [#6468](https://github.com/flet-dev/flet/pull/6468)) by @ndonkoHenri. * Fix `LineChart` (and other charts) silently dropping custom `ChartAxisLabel` entries whose `value` matched a tick only after floating-point rounding (e.g. `0.1`, `0.2`, `0.3`) by switching label lookup to a tolerance-based comparison scaled to the axis interval ([#6445](https://github.com/flet-dev/flet/issues/6445), [#6459](https://github.com/flet-dev/flet/pull/6459)) by @KangZhaoKui. ### Documentation diff --git a/sdk/python/packages/flet-charts/CHANGELOG.md b/sdk/python/packages/flet-charts/CHANGELOG.md index 75d29e7de5..c0de249a67 100644 --- a/sdk/python/packages/flet-charts/CHANGELOG.md +++ b/sdk/python/packages/flet-charts/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## 0.85.0 + +### Fixed + +- Fixed `LineChartEvent.spots` returning undecoded MessagePack extension values instead of `LineChartEventSpot` objects ([#6443](https://github.com/flet-dev/flet/issues/6443), [#6468](https://github.com/flet-dev/flet/pull/6468)) by @ndonkoHenri. +- Fixed `LineChart` (and other charts) silently dropping custom `ChartAxisLabel` entries whose `value` matched a tick only after floating-point rounding (e.g. `0.1`, `0.2`, `0.3`) by switching label lookup to a tolerance-based comparison scaled to the axis interval ([#6445](https://github.com/flet-dev/flet/issues/6445), [#6459](https://github.com/flet-dev/flet/pull/6459)) by @KangZhaoKui. + ## 0.80.0 Initial release. From 0bbec85201f072d5480a0cd74ea4d19679472fa5 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Wed, 6 May 2026 00:47:47 +0200 Subject: [PATCH 07/21] fix failing CI --- .../flet/integration_tests/examples/controls/core/test_row.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sdk/python/packages/flet/integration_tests/examples/controls/core/test_row.py b/sdk/python/packages/flet/integration_tests/examples/controls/core/test_row.py index 074e132daf..482ef87e68 100644 --- a/sdk/python/packages/flet/integration_tests/examples/controls/core/test_row.py +++ b/sdk/python/packages/flet/integration_tests/examples/controls/core/test_row.py @@ -14,6 +14,7 @@ async def test_image_for_docs(flet_app_function: ftt.FletTestApp, request): await flet_app_function.assert_control_screenshot( request.node.name, ft.Row( + width=445, scroll=ft.ScrollMode.AUTO, controls=[ ft.Card( From 1edca06be02ffbec37b706514cf28d640d740b2c Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Tue, 5 May 2026 20:31:14 -0700 Subject: [PATCH 08/21] fix flaky screenshot capture: await endOfFrame before toImage() Avoids the `!debugNeedsPaint` assertion in RenderRepaintBoundary.toImage when the boundary is still dirty at capture time on slower runners. Also temporarily narrows macos-integration-tests workflow to examples/controls/core::test_row.py to validate the fix in CI. --- .github/workflows/macos-integration-tests.yml | 15 +++------------ packages/flet/lib/src/controls/screenshot.dart | 3 +++ 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/.github/workflows/macos-integration-tests.yml b/.github/workflows/macos-integration-tests.yml index e44ab80d04..f9457bec58 100644 --- a/.github/workflows/macos-integration-tests.yml +++ b/.github/workflows/macos-integration-tests.yml @@ -38,19 +38,9 @@ jobs: strategy: fail-fast: false matrix: + # TEMP: narrowed to validate screenshot race fix; revert before merge. suite: - - apps - - examples/apps - examples/controls/core - - examples/controls/cupertino - - examples/controls/material - - examples/extensions - - controls/core - - controls/cupertino - - controls/material - - controls/services - - controls/theme - - extensions name: ${{ matrix.suite }} Integration Tests steps: @@ -73,7 +63,8 @@ jobs: - name: Run integration tests (${{ matrix.suite }}) working-directory: sdk/python run: | - uv run pytest -s -o log_cli=true -o log_cli_level=INFO packages/flet/integration_tests/${{ matrix.suite }} + # TEMP: scoped to test_row.py to validate screenshot race fix; revert before merge. + uv run pytest -s -o log_cli=true -o log_cli_level=INFO packages/flet/integration_tests/${{ matrix.suite }}/test_row.py - name: Prepare failure screenshots if: failure() diff --git a/packages/flet/lib/src/controls/screenshot.dart b/packages/flet/lib/src/controls/screenshot.dart index d3ddbf2270..8fbf5ae677 100644 --- a/packages/flet/lib/src/controls/screenshot.dart +++ b/packages/flet/lib/src/controls/screenshot.dart @@ -29,6 +29,9 @@ class _InteractiveViewerControlState extends State { debugPrint("Screenshot.$name($args)"); switch (name) { case "capture": + // Ensure the RepaintBoundary has finished painting before toImage() + // is called, otherwise Flutter trips the `!debugNeedsPaint` assertion. + await WidgetsBinding.instance.endOfFrame; return await _screenshotController.capture( pixelRatio: args["pixel_ratio"], delay: parseDuration( From 8a11c5f38569e8688d5d59d2f4e6cfbe904f4e86 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Tue, 5 May 2026 20:54:53 -0700 Subject: [PATCH 09/21] test(row): pin viewport in test_alignment / test_vertical_alignment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The macOS runner upgrade (macos-14 → -15 → -26) increased the default viewport size, making the Row alignment screenshots flake with `!debugNeedsPaint` because the screenshot package's 20ms post-delay was no longer enough to flush a paint of the larger surface. Calling resize_page(800, 1000) restores deterministic paint workload. Reverts the earlier endOfFrame attempt, which deadlocks under the flutter_test fake scheduler. --- packages/flet/lib/src/controls/screenshot.dart | 3 --- .../integration_tests/examples/controls/core/test_row.py | 5 +++++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/flet/lib/src/controls/screenshot.dart b/packages/flet/lib/src/controls/screenshot.dart index 8fbf5ae677..d3ddbf2270 100644 --- a/packages/flet/lib/src/controls/screenshot.dart +++ b/packages/flet/lib/src/controls/screenshot.dart @@ -29,9 +29,6 @@ class _InteractiveViewerControlState extends State { debugPrint("Screenshot.$name($args)"); switch (name) { case "capture": - // Ensure the RepaintBoundary has finished painting before toImage() - // is called, otherwise Flutter trips the `!debugNeedsPaint` assertion. - await WidgetsBinding.instance.endOfFrame; return await _screenshotController.capture( pixelRatio: args["pixel_ratio"], delay: parseDuration( diff --git a/sdk/python/packages/flet/integration_tests/examples/controls/core/test_row.py b/sdk/python/packages/flet/integration_tests/examples/controls/core/test_row.py index 482ef87e68..7f80ed24df 100644 --- a/sdk/python/packages/flet/integration_tests/examples/controls/core/test_row.py +++ b/sdk/python/packages/flet/integration_tests/examples/controls/core/test_row.py @@ -39,6 +39,9 @@ async def test_image_for_docs(flet_app_function: ftt.FletTestApp, request): ) @pytest.mark.asyncio(loop_scope="function") async def test_alignment(flet_app_function: ftt.FletTestApp): + # Pin viewport so paint workload is independent of the runner's display size. + flet_app_function.resize_page(800, 1000) + flet_app_function.page.update() flet_app_function.assert_screenshot( "alignment", await flet_app_function.take_page_controls_screenshot(), @@ -52,6 +55,8 @@ async def test_alignment(flet_app_function: ftt.FletTestApp): ) @pytest.mark.asyncio(loop_scope="function") async def test_vertical_alignment(flet_app_function: ftt.FletTestApp): + flet_app_function.resize_page(800, 1000) + flet_app_function.page.update() flet_app_function.assert_screenshot( "vertical_alignment", await flet_app_function.take_page_controls_screenshot(), From f6324881b1952625a5426d45f3ff62edc86b5a0e Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Wed, 6 May 2026 12:47:07 +0200 Subject: [PATCH 10/21] fix #6429: update tile layer URL to OpenStreetMap --- .../examples/extensions/map/basic/main.py | 4 ++- .../extensions/map/basic/pyproject.toml | 2 +- .../extensions/map/camera_controls/main.py | 3 ++- .../extensions/map/idle_camera/main.py | 3 ++- .../extensions/map/interaction_flags/main.py | 17 +++++------- .../map/interaction_flags/pyproject.toml | 4 +-- .../extensions/map/multi_layers/main.py | 26 +++++-------------- .../map/multi_layers/pyproject.toml | 4 +-- .../extensions/map/overlay_images/main.py | 4 ++- .../map/overlay_images/pyproject.toml | 2 +- 10 files changed, 29 insertions(+), 40 deletions(-) diff --git a/sdk/python/examples/extensions/map/basic/main.py b/sdk/python/examples/extensions/map/basic/main.py index 45a4d8870c..17ef94dbe2 100644 --- a/sdk/python/examples/extensions/map/basic/main.py +++ b/sdk/python/examples/extensions/map/basic/main.py @@ -10,9 +10,11 @@ def main(page: ft.Page): expand=True, layers=[ ftm.TileLayer( - url_template="https://tile.memomaps.de/tilegen/{z}/{x}/{y}.png", + url_template="https://tile.openstreetmap.org/{z}/{x}/{y}.png", + user_agent_package_name="flet-map-examples/1.0", on_image_error=lambda e: print(f"TileLayer Error: {e.data}"), ), + ftm.SimpleAttribution(text="OpenStreetMap contributors"), ], ), ) diff --git a/sdk/python/examples/extensions/map/basic/pyproject.toml b/sdk/python/examples/extensions/map/basic/pyproject.toml index 9192cfc420..845cc9d39a 100644 --- a/sdk/python/examples/extensions/map/basic/pyproject.toml +++ b/sdk/python/examples/extensions/map/basic/pyproject.toml @@ -15,7 +15,7 @@ categories = ["Extensions/Map"] [tool.flet.metadata] title = "Basic" -controls = ["SafeArea", "Map", "TileLayer"] +controls = ["SafeArea", "Map", "TileLayer", "SimpleAttribution"] layout_pattern = "single-panel" complexity = "basic" features = ["map tile rendering", "tile load error callback"] diff --git a/sdk/python/examples/extensions/map/camera_controls/main.py b/sdk/python/examples/extensions/map/camera_controls/main.py index f3f74e9d37..da1886e17f 100644 --- a/sdk/python/examples/extensions/map/camera_controls/main.py +++ b/sdk/python/examples/extensions/map/camera_controls/main.py @@ -76,7 +76,8 @@ async def set_world_zoom(e: ft.Event[ft.Button]): initial_zoom=5, layers=[ ftm.TileLayer( - url_template="https://tile.memomaps.de/tilegen/{z}/{x}/{y}.png" + url_template="https://tile.openstreetmap.org/{z}/{x}/{y}.png", + user_agent_package_name="flet-map-examples/1.0", ), ftm.SimpleAttribution( text="OpenStreetMap contributors", diff --git a/sdk/python/examples/extensions/map/idle_camera/main.py b/sdk/python/examples/extensions/map/idle_camera/main.py index 7a14addd86..d2c383beb6 100644 --- a/sdk/python/examples/extensions/map/idle_camera/main.py +++ b/sdk/python/examples/extensions/map/idle_camera/main.py @@ -63,7 +63,8 @@ async def handle_map_event(e: ftm.MapEvent): ), layers=[ ftm.TileLayer( - url_template="https://tile.memomaps.de/tilegen/{z}/{x}/{y}.png" + url_template="https://tile.openstreetmap.org/{z}/{x}/{y}.png", + user_agent_package_name="flet-map-examples/1.0", ), ftm.SimpleAttribution( text="OpenStreetMap contributors", diff --git a/sdk/python/examples/extensions/map/interaction_flags/main.py b/sdk/python/examples/extensions/map/interaction_flags/main.py index 77a2b07dd5..580b8914c5 100644 --- a/sdk/python/examples/extensions/map/interaction_flags/main.py +++ b/sdk/python/examples/extensions/map/interaction_flags/main.py @@ -56,18 +56,15 @@ def handle_map_event(e: ftm.MapEvent): ), layers=[ ftm.TileLayer( - url_template="https://tile.memomaps.de/tilegen/{z}/{x}/{y}.png", + url_template="https://tile.openstreetmap.org/{z}/{x}/{y}.png", + user_agent_package_name="flet-map-examples/1.0", on_image_error=lambda e: print(f"TileLayer Error: {e.data}"), ), - ftm.RichAttribution( - attributions=[ - ftm.TextSourceAttribution( - text="OpenStreetMap contributors", - on_click=lambda e: e.page.launch_url( - "https://www.openstreetmap.org/copyright" - ), - ) - ] + ftm.SimpleAttribution( + text="OpenStreetMap contributors", + on_click=lambda e: e.page.launch_url( + "https://www.openstreetmap.org/copyright" + ), ), ], ) diff --git a/sdk/python/examples/extensions/map/interaction_flags/pyproject.toml b/sdk/python/examples/extensions/map/interaction_flags/pyproject.toml index da37819e2c..c45e5a1d57 100644 --- a/sdk/python/examples/extensions/map/interaction_flags/pyproject.toml +++ b/sdk/python/examples/extensions/map/interaction_flags/pyproject.toml @@ -15,10 +15,10 @@ categories = ["Extensions/Map"] [tool.flet.metadata] title = "Interaction Flags" -controls = ["SafeArea", "Column", "ResponsiveRow", "Container", "Checkbox", "Map", "TileLayer", "RichAttribution", "TextSourceAttribution", "Text", "AppBar"] +controls = ["SafeArea", "Column", "ResponsiveRow", "Container", "Checkbox", "Map", "TileLayer", "SimpleAttribution", "Text", "AppBar"] layout_pattern = "single-panel" complexity = "basic" -features = ["runtime interaction toggles", "map event logging", "custom attributions"] +features = ["runtime interaction toggles", "map event logging", "tile attribution"] [tool.flet] org = "dev.flet" diff --git a/sdk/python/examples/extensions/map/multi_layers/main.py b/sdk/python/examples/extensions/map/multi_layers/main.py index 693cb9716f..e79263637b 100644 --- a/sdk/python/examples/extensions/map/multi_layers/main.py +++ b/sdk/python/examples/extensions/map/multi_layers/main.py @@ -50,29 +50,15 @@ def handle_tap(e: ftm.MapTapEvent): on_event=print, layers=[ ftm.TileLayer( - url_template="https://tile.memomaps.de/tilegen/{z}/{x}/{y}.png", + url_template="https://tile.openstreetmap.org/{z}/{x}/{y}.png", + user_agent_package_name="flet-map-examples/1.0", on_image_error=lambda e: print("TileLayer Error"), ), - ftm.RichAttribution( - attributions=[ - ftm.TextSourceAttribution( - text="OpenStreetMap Contributors", - on_click=lambda e: e.page.launch_url( - "https://www.openstreetmap.org/copyright" - ), - ), - ftm.TextSourceAttribution( - text="Flet", - on_click=lambda e: e.page.launch_url( - "https://flet.dev" - ), - ), - ] - ), ftm.SimpleAttribution( - text="Flet", - alignment=ft.Alignment.TOP_RIGHT, - on_click=lambda e: print("Clicked SimpleAttribution"), + text="OpenStreetMap contributors", + on_click=lambda e: e.page.launch_url( + "https://www.openstreetmap.org/copyright" + ), ), marker_layer := ftm.MarkerLayer( markers=[ diff --git a/sdk/python/examples/extensions/map/multi_layers/pyproject.toml b/sdk/python/examples/extensions/map/multi_layers/pyproject.toml index 64ec87c460..be5257ffb9 100644 --- a/sdk/python/examples/extensions/map/multi_layers/pyproject.toml +++ b/sdk/python/examples/extensions/map/multi_layers/pyproject.toml @@ -15,10 +15,10 @@ categories = ["Extensions/Map"] [tool.flet.metadata] title = "Multiple Layers" -controls = ["SafeArea", "Column", "Text", "Map", "TileLayer", "RichAttribution", "SimpleAttribution", "TextSourceAttribution", "MarkerLayer", "Marker", "CircleLayer", "CircleMarker", "PolygonLayer", "PolygonMarker", "PolylineLayer", "PolylineMarker", "Icon", "AppBar"] +controls = ["SafeArea", "Column", "Text", "Map", "TileLayer", "SimpleAttribution", "MarkerLayer", "Marker", "CircleLayer", "CircleMarker", "PolygonLayer", "PolygonMarker", "PolylineLayer", "PolylineMarker", "Icon", "AppBar"] layout_pattern = "single-panel" complexity = "basic" -features = ["multi-layer map composition", "tap and secondary-tap overlay creation", "interactive attributions"] +features = ["multi-layer map composition", "tap and secondary-tap overlay creation", "tile attribution"] [tool.flet] org = "dev.flet" diff --git a/sdk/python/examples/extensions/map/overlay_images/main.py b/sdk/python/examples/extensions/map/overlay_images/main.py index 7118545981..3c264b8300 100644 --- a/sdk/python/examples/extensions/map/overlay_images/main.py +++ b/sdk/python/examples/extensions/map/overlay_images/main.py @@ -14,7 +14,8 @@ def main(page: ft.Page): initial_zoom=6, layers=[ ftm.TileLayer( - url_template="https://tile.memomaps.de/tilegen/{z}/{x}/{y}.png" + url_template="https://tile.openstreetmap.org/{z}/{x}/{y}.png", + user_agent_package_name="flet-map-examples/1.0", ), ftm.OverlayImageLayer( overlay_images=[ @@ -41,6 +42,7 @@ def main(page: ft.Page): ), ] ), + ftm.SimpleAttribution(text="OpenStreetMap contributors"), ], ), ) diff --git a/sdk/python/examples/extensions/map/overlay_images/pyproject.toml b/sdk/python/examples/extensions/map/overlay_images/pyproject.toml index 547ea55171..ee24030e30 100644 --- a/sdk/python/examples/extensions/map/overlay_images/pyproject.toml +++ b/sdk/python/examples/extensions/map/overlay_images/pyproject.toml @@ -15,7 +15,7 @@ categories = ["Extensions/Map"] [tool.flet.metadata] title = "Overlay Images" -controls = ["SafeArea", "Map", "TileLayer", "OverlayImageLayer", "OverlayImage", "RotatedOverlayImage"] +controls = ["SafeArea", "Map", "TileLayer", "OverlayImageLayer", "OverlayImage", "RotatedOverlayImage", "SimpleAttribution"] layout_pattern = "single-panel" complexity = "basic" features = ["rectangular image overlay", "rotated image overlay"] From a62612fcef993aa8acbf1ca7ec8556ffa099976b Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Wed, 6 May 2026 07:22:05 -0700 Subject: [PATCH 11/21] test: bump screenshot capture delay to 200ms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The screenshot package waits 20ms before calling RenderRepaintBoundary.toImage(). On macos-26 runners that's too short — the capture invocation itself can schedule a frame that's still painting when toImage() runs, tripping the `!debugNeedsPaint` assertion. Pass an explicit 200ms delay from the testing harness for both take_page_controls_screenshot and assert_control_screenshot. --- .../packages/flet/src/flet/testing/flet_test_app.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/sdk/python/packages/flet/src/flet/testing/flet_test_app.py b/sdk/python/packages/flet/src/flet/testing/flet_test_app.py index 33cf45e15a..1d7713dd52 100644 --- a/sdk/python/packages/flet/src/flet/testing/flet_test_app.py +++ b/sdk/python/packages/flet/src/flet/testing/flet_test_app.py @@ -351,8 +351,12 @@ async def take_page_controls_screenshot( scr = await self.wrap_page_controls_in_screenshot( pump_times=pump_times, pump_duration=pump_duration ) + # 200ms headroom — the screenshot package's 20ms default isn't enough + # on busy runners to ride out a frame scheduled by the capture + # invocation itself, which trips `!debugNeedsPaint` in toImage(). return await scr.capture( - pixel_ratio=pixel_ratio or self.screenshots_pixel_ratio + pixel_ratio=pixel_ratio or self.screenshots_pixel_ratio, + delay=ft.Duration(milliseconds=200), ) async def assert_control_screenshot( @@ -385,7 +389,10 @@ async def assert_control_screenshot( await self.tester.pump(duration=pump_duration) self.assert_screenshot( name, - await screenshot.capture(pixel_ratio=self.screenshots_pixel_ratio), + await screenshot.capture( + pixel_ratio=self.screenshots_pixel_ratio, + delay=ft.Duration(milliseconds=200), + ), similarity_threshold=similarity_threshold, ) From 2c9ef314dbf20b81db4c577db8cd31aa9be1f9a7 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Wed, 6 May 2026 07:23:36 -0700 Subject: [PATCH 12/21] ci: bump test log level to DEBUG (temp) To capture more diagnostic output for the test_alignment screenshot race investigation. Revert before merge. --- .github/workflows/macos-integration-tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/macos-integration-tests.yml b/.github/workflows/macos-integration-tests.yml index f9457bec58..caa214742a 100644 --- a/.github/workflows/macos-integration-tests.yml +++ b/.github/workflows/macos-integration-tests.yml @@ -63,8 +63,8 @@ jobs: - name: Run integration tests (${{ matrix.suite }}) working-directory: sdk/python run: | - # TEMP: scoped to test_row.py to validate screenshot race fix; revert before merge. - uv run pytest -s -o log_cli=true -o log_cli_level=INFO packages/flet/integration_tests/${{ matrix.suite }}/test_row.py + # TEMP: scoped to test_row.py + DEBUG log level to validate screenshot race fix; revert before merge. + uv run pytest -s -o log_cli=true -o log_cli_level=DEBUG packages/flet/integration_tests/${{ matrix.suite }}/test_row.py - name: Prepare failure screenshots if: failure() From 0e3228ee7536ff0c3b9f5bd62bf6370a2f08235a Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Wed, 6 May 2026 07:40:37 -0700 Subject: [PATCH 13/21] test(row): pump_and_settle after resize_page The OS window resize triggered by resize_page propagates asynchronously. Without an explicit settle, it races into the capture's post-delay and re-dirties the RepaintBoundary right before toImage() runs, tripping `!debugNeedsPaint`. CI debug log showed the window UPDATE_CONTROL_PROPS arriving 1ms after the capture invocation was sent. --- .../flet/integration_tests/examples/controls/core/test_row.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sdk/python/packages/flet/integration_tests/examples/controls/core/test_row.py b/sdk/python/packages/flet/integration_tests/examples/controls/core/test_row.py index 7f80ed24df..f8e92d92ec 100644 --- a/sdk/python/packages/flet/integration_tests/examples/controls/core/test_row.py +++ b/sdk/python/packages/flet/integration_tests/examples/controls/core/test_row.py @@ -42,6 +42,9 @@ async def test_alignment(flet_app_function: ftt.FletTestApp): # Pin viewport so paint workload is independent of the runner's display size. flet_app_function.resize_page(800, 1000) flet_app_function.page.update() + # The OS window resize is async — wait for it to settle before capture, + # otherwise it races into the capture's post-delay and dirties the boundary. + await flet_app_function.tester.pump_and_settle() flet_app_function.assert_screenshot( "alignment", await flet_app_function.take_page_controls_screenshot(), @@ -57,6 +60,7 @@ async def test_alignment(flet_app_function: ftt.FletTestApp): async def test_vertical_alignment(flet_app_function: ftt.FletTestApp): flet_app_function.resize_page(800, 1000) flet_app_function.page.update() + await flet_app_function.tester.pump_and_settle() flet_app_function.assert_screenshot( "vertical_alignment", await flet_app_function.take_page_controls_screenshot(), From 1285f5504cd36568abef4c7944700682714e4d95 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Wed, 6 May 2026 07:52:24 -0700 Subject: [PATCH 14/21] test(row): revert resize_page additions The resize_page workaround introduced its own race: macOS window resize is OS-async beyond Flutter's pump cycle, the requested height is clamped to the runner's display, and the late-arriving resize event re-dirties the boundary anyway. Revert and rely on the 200ms capture delay only. --- .../integration_tests/examples/controls/core/test_row.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/sdk/python/packages/flet/integration_tests/examples/controls/core/test_row.py b/sdk/python/packages/flet/integration_tests/examples/controls/core/test_row.py index f8e92d92ec..482ef87e68 100644 --- a/sdk/python/packages/flet/integration_tests/examples/controls/core/test_row.py +++ b/sdk/python/packages/flet/integration_tests/examples/controls/core/test_row.py @@ -39,12 +39,6 @@ async def test_image_for_docs(flet_app_function: ftt.FletTestApp, request): ) @pytest.mark.asyncio(loop_scope="function") async def test_alignment(flet_app_function: ftt.FletTestApp): - # Pin viewport so paint workload is independent of the runner's display size. - flet_app_function.resize_page(800, 1000) - flet_app_function.page.update() - # The OS window resize is async — wait for it to settle before capture, - # otherwise it races into the capture's post-delay and dirties the boundary. - await flet_app_function.tester.pump_and_settle() flet_app_function.assert_screenshot( "alignment", await flet_app_function.take_page_controls_screenshot(), @@ -58,9 +52,6 @@ async def test_alignment(flet_app_function: ftt.FletTestApp): ) @pytest.mark.asyncio(loop_scope="function") async def test_vertical_alignment(flet_app_function: ftt.FletTestApp): - flet_app_function.resize_page(800, 1000) - flet_app_function.page.update() - await flet_app_function.tester.pump_and_settle() flet_app_function.assert_screenshot( "vertical_alignment", await flet_app_function.take_page_controls_screenshot(), From 3213c10a98e4fb7b27008f1b1bb5bb9cffd0f350 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Wed, 6 May 2026 10:05:29 -0700 Subject: [PATCH 15/21] test(row): pump 2s before test_alignment capture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use pump_times to outlast any scrollbar-fade or SafeArea inset adjustment that might keep the boundary dirty for ~1.5s. Diagnostic — narrowing down the !debugNeedsPaint cause. --- .../integration_tests/examples/controls/core/test_row.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sdk/python/packages/flet/integration_tests/examples/controls/core/test_row.py b/sdk/python/packages/flet/integration_tests/examples/controls/core/test_row.py index 482ef87e68..1a2f8d2553 100644 --- a/sdk/python/packages/flet/integration_tests/examples/controls/core/test_row.py +++ b/sdk/python/packages/flet/integration_tests/examples/controls/core/test_row.py @@ -41,7 +41,10 @@ async def test_image_for_docs(flet_app_function: ftt.FletTestApp, request): async def test_alignment(flet_app_function: ftt.FletTestApp): flet_app_function.assert_screenshot( "alignment", - await flet_app_function.take_page_controls_screenshot(), + await flet_app_function.take_page_controls_screenshot( + pump_times=10, + pump_duration=ft.Duration(milliseconds=200), + ), ) From 08e15f5c3b9a6f524f9d43649a61a6181bfb2a35 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Wed, 6 May 2026 10:24:31 -0700 Subject: [PATCH 16/21] example(row/alignment): drop redundant inner Column scroll The page is already scrollable; the inner Column.scroll=AUTO produces a double-scroll layout that combined with intrinsic_width wrapping in the test harness keeps the RepaintBoundary perpetually dirty, tripping !debugNeedsPaint when the screenshot is captured. --- sdk/python/examples/controls/core/row/alignment/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/python/examples/controls/core/row/alignment/main.py b/sdk/python/examples/controls/core/row/alignment/main.py index 270670e0e3..8612682290 100644 --- a/sdk/python/examples/controls/core/row/alignment/main.py +++ b/sdk/python/examples/controls/core/row/alignment/main.py @@ -36,7 +36,7 @@ def main(page: ft.Page): page.add( ft.SafeArea( content=ft.Column( - scroll=ft.ScrollMode.AUTO, + # scroll=ft.ScrollMode.AUTO, controls=[ RowWithAlignment(alignment=ft.MainAxisAlignment.START), RowWithAlignment(alignment=ft.MainAxisAlignment.CENTER), From 0d61e6bd765f2c82851808bf64c12954232b6fc6 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Wed, 6 May 2026 10:34:47 -0700 Subject: [PATCH 17/21] revert diagnostic-only changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drops the 200ms capture delay, pump_times=10 in test_alignment, and DEBUG-level pytest logging — none were the actual fix; the inner double-scroll in the alignment example was. Workflow stays narrowed to test_row.py for one more validation run before reverting that too. --- .github/workflows/macos-integration-tests.yml | 4 ++-- .../examples/controls/core/test_row.py | 5 +---- .../packages/flet/src/flet/testing/flet_test_app.py | 11 ++--------- 3 files changed, 5 insertions(+), 15 deletions(-) diff --git a/.github/workflows/macos-integration-tests.yml b/.github/workflows/macos-integration-tests.yml index caa214742a..3999d16850 100644 --- a/.github/workflows/macos-integration-tests.yml +++ b/.github/workflows/macos-integration-tests.yml @@ -63,8 +63,8 @@ jobs: - name: Run integration tests (${{ matrix.suite }}) working-directory: sdk/python run: | - # TEMP: scoped to test_row.py + DEBUG log level to validate screenshot race fix; revert before merge. - uv run pytest -s -o log_cli=true -o log_cli_level=DEBUG packages/flet/integration_tests/${{ matrix.suite }}/test_row.py + # TEMP: scoped to test_row.py to validate screenshot fix; revert before merge. + uv run pytest -s -o log_cli=true -o log_cli_level=INFO packages/flet/integration_tests/${{ matrix.suite }}/test_row.py - name: Prepare failure screenshots if: failure() diff --git a/sdk/python/packages/flet/integration_tests/examples/controls/core/test_row.py b/sdk/python/packages/flet/integration_tests/examples/controls/core/test_row.py index 1a2f8d2553..482ef87e68 100644 --- a/sdk/python/packages/flet/integration_tests/examples/controls/core/test_row.py +++ b/sdk/python/packages/flet/integration_tests/examples/controls/core/test_row.py @@ -41,10 +41,7 @@ async def test_image_for_docs(flet_app_function: ftt.FletTestApp, request): async def test_alignment(flet_app_function: ftt.FletTestApp): flet_app_function.assert_screenshot( "alignment", - await flet_app_function.take_page_controls_screenshot( - pump_times=10, - pump_duration=ft.Duration(milliseconds=200), - ), + await flet_app_function.take_page_controls_screenshot(), ) diff --git a/sdk/python/packages/flet/src/flet/testing/flet_test_app.py b/sdk/python/packages/flet/src/flet/testing/flet_test_app.py index 1d7713dd52..33cf45e15a 100644 --- a/sdk/python/packages/flet/src/flet/testing/flet_test_app.py +++ b/sdk/python/packages/flet/src/flet/testing/flet_test_app.py @@ -351,12 +351,8 @@ async def take_page_controls_screenshot( scr = await self.wrap_page_controls_in_screenshot( pump_times=pump_times, pump_duration=pump_duration ) - # 200ms headroom — the screenshot package's 20ms default isn't enough - # on busy runners to ride out a frame scheduled by the capture - # invocation itself, which trips `!debugNeedsPaint` in toImage(). return await scr.capture( - pixel_ratio=pixel_ratio or self.screenshots_pixel_ratio, - delay=ft.Duration(milliseconds=200), + pixel_ratio=pixel_ratio or self.screenshots_pixel_ratio ) async def assert_control_screenshot( @@ -389,10 +385,7 @@ async def assert_control_screenshot( await self.tester.pump(duration=pump_duration) self.assert_screenshot( name, - await screenshot.capture( - pixel_ratio=self.screenshots_pixel_ratio, - delay=ft.Duration(milliseconds=200), - ), + await screenshot.capture(pixel_ratio=self.screenshots_pixel_ratio), similarity_threshold=similarity_threshold, ) From 5af64758eedc297a613a653b81762d2ff3729027 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Wed, 6 May 2026 10:49:14 -0700 Subject: [PATCH 18/21] fix(scrollable_control): use MediaQuery.sizeOf instead of LayoutBuilder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #6450 wrapped the scrollable child in a LayoutBuilder to read viewport size for the min-height constraint. LayoutBuilder reports 0 for intrinsic dimensions, which collapses any ancestor IntrinsicWidth / IntrinsicHeight and leaves the layout in an unstable state — the test harness's intrinsic_width=True wrapper then perpetually marks the RepaintBoundary dirty, tripping !debugNeedsPaint when toImage() runs. MediaQuery.sizeOf exposes the same viewport size through a regular InheritedWidget, transparent to intrinsic queries, while SingleChildScrollView already forwards cross-axis intrinsic queries to its child. Net effect: vertical alignment in scrollable Page/View still works (the original #6450 intent), and IntrinsicWidth/Height ancestors are no longer broken. Restores scroll=AUTO on the alignment example to validate the fix. --- .../lib/src/controls/scrollable_control.dart | 42 +++++++++---------- .../controls/core/row/alignment/main.py | 2 +- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/packages/flet/lib/src/controls/scrollable_control.dart b/packages/flet/lib/src/controls/scrollable_control.dart index 79d4a7a07e..cb8d877582 100644 --- a/packages/flet/lib/src/controls/scrollable_control.dart +++ b/packages/flet/lib/src/controls/scrollable_control.dart @@ -111,31 +111,29 @@ class _ScrollableControlState extends State Widget child = widget.child; if (widget.wrapIntoScrollableView) { - child = LayoutBuilder(builder: (context, constraints) { - final minWidth = widget.scrollDirection == Axis.horizontal && - constraints.hasBoundedWidth - ? constraints.maxWidth - : 0.0; - final minHeight = widget.scrollDirection == Axis.vertical && - constraints.hasBoundedHeight - ? constraints.maxHeight - : 0.0; + // Use MediaQuery instead of LayoutBuilder so ancestor IntrinsicWidth / + // IntrinsicHeight widgets keep working — LayoutBuilder reports 0 for + // intrinsic dimensions, which collapses any intrinsic-sized ancestor. + final size = MediaQuery.sizeOf(context); + final minWidth = + widget.scrollDirection == Axis.horizontal ? size.width : 0.0; + final minHeight = + widget.scrollDirection == Axis.vertical ? size.height : 0.0; - Widget scrollViewChild = widget.child; - if (minWidth > 0 || minHeight > 0) { - scrollViewChild = ConstrainedBox( - constraints: - BoxConstraints(minWidth: minWidth, minHeight: minHeight), - child: scrollViewChild, - ); - } - - return SingleChildScrollView( - controller: _controller, - scrollDirection: widget.scrollDirection, + Widget scrollViewChild = widget.child; + if (minWidth > 0 || minHeight > 0) { + scrollViewChild = ConstrainedBox( + constraints: + BoxConstraints(minWidth: minWidth, minHeight: minHeight), child: scrollViewChild, ); - }); + } + + child = SingleChildScrollView( + controller: _controller, + scrollDirection: widget.scrollDirection, + child: scrollViewChild, + ); } return Scrollbar( diff --git a/sdk/python/examples/controls/core/row/alignment/main.py b/sdk/python/examples/controls/core/row/alignment/main.py index 8612682290..270670e0e3 100644 --- a/sdk/python/examples/controls/core/row/alignment/main.py +++ b/sdk/python/examples/controls/core/row/alignment/main.py @@ -36,7 +36,7 @@ def main(page: ft.Page): page.add( ft.SafeArea( content=ft.Column( - # scroll=ft.ScrollMode.AUTO, + scroll=ft.ScrollMode.AUTO, controls=[ RowWithAlignment(alignment=ft.MainAxisAlignment.START), RowWithAlignment(alignment=ft.MainAxisAlignment.CENTER), From 2a63caff4610456c0ed004644428b2b092a4c79f Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Wed, 6 May 2026 11:21:10 -0700 Subject: [PATCH 19/21] fix(scrollable_control): replace LayoutBuilder with paired RenderProxyBoxes PR #6450 used a LayoutBuilder to read the parent's constraints and apply the viewport's max-extent as a min-height on the scroll-view child, so vertical alignment works in scrollable Page/View when content is shorter than the viewport. LayoutBuilder, however, reports 0 for intrinsic dimensions, which collapses any ancestor IntrinsicWidth / IntrinsicHeight and leaves the layout perpetually dirty (test_alignment was tripping !debugNeedsPaint in toImage()). Replace the LayoutBuilder with two cooperating RenderProxyBoxes that forward intrinsic queries to their child: * _OuterConstraintsReader sits outside Scrollbar/SingleChildScrollView and captures its incoming constraints during performLayout. * _InnerConstraintsEnforcer sits inside SingleChildScrollView and reads the captured value back, applying it as a min on the scroll-view child. Same render output as the original LayoutBuilder (including correct no-op behavior for nested-in-unbounded-scroll cases), but transparent to ancestor IntrinsicWidth/Height. --- .../lib/src/controls/scrollable_control.dart | 130 +++++++++++++++--- 1 file changed, 110 insertions(+), 20 deletions(-) diff --git a/packages/flet/lib/src/controls/scrollable_control.dart b/packages/flet/lib/src/controls/scrollable_control.dart index cb8d877582..6cb67b0df5 100644 --- a/packages/flet/lib/src/controls/scrollable_control.dart +++ b/packages/flet/lib/src/controls/scrollable_control.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; import '../models/control.dart'; import '../utils/animations.dart'; @@ -32,6 +33,7 @@ class _ScrollableControlState extends State with FletStoreMixin { late final ScrollController _controller; late bool _ownController = false; + final _ConstraintsHolder _outerConstraints = _ConstraintsHolder(); @override void initState() { @@ -111,32 +113,29 @@ class _ScrollableControlState extends State Widget child = widget.child; if (widget.wrapIntoScrollableView) { - // Use MediaQuery instead of LayoutBuilder so ancestor IntrinsicWidth / - // IntrinsicHeight widgets keep working — LayoutBuilder reports 0 for - // intrinsic dimensions, which collapses any intrinsic-sized ancestor. - final size = MediaQuery.sizeOf(context); - final minWidth = - widget.scrollDirection == Axis.horizontal ? size.width : 0.0; - final minHeight = - widget.scrollDirection == Axis.vertical ? size.height : 0.0; - - Widget scrollViewChild = widget.child; - if (minWidth > 0 || minHeight > 0) { - scrollViewChild = ConstrainedBox( - constraints: - BoxConstraints(minWidth: minWidth, minHeight: minHeight), - child: scrollViewChild, - ); - } - + // The pre-#6450 path used a plain SingleChildScrollView. PR #6450 added + // a LayoutBuilder + ConstrainedBox(minHeight: parentMaxHeight) wrapper + // so vertical alignment works in scrollable Page/View when content is + // shorter than the viewport. LayoutBuilder, however, reports 0 for + // intrinsic dimensions, which collapses any ancestor IntrinsicWidth / + // IntrinsicHeight and leaves the layout perpetually dirty. + // + // Replicate the behavior with two cooperating RenderProxyBoxes that + // forward intrinsic queries to their child. The outer reader captures + // the parent's constraints during performLayout; the inner enforcer + // reads them back and applies them as a min on the scroll-view child. child = SingleChildScrollView( controller: _controller, scrollDirection: widget.scrollDirection, - child: scrollViewChild, + child: _InnerConstraintsEnforcer( + holder: _outerConstraints, + scrollDirection: widget.scrollDirection, + child: widget.child, + ), ); } - return Scrollbar( + Widget result = Scrollbar( thumbVisibility: scrollConfiguration.thumbVisibility, trackVisibility: scrollConfiguration.trackVisibility, thickness: scrollConfiguration.thickness, @@ -148,5 +147,96 @@ class _ScrollableControlState extends State behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false), child: child, )); + + if (widget.wrapIntoScrollableView) { + result = _OuterConstraintsReader( + holder: _outerConstraints, + child: result, + ); + } + + return result; + } +} + +/// Carries box constraints from [_OuterConstraintsReader] (outside the scroll +/// view) to [_InnerConstraintsEnforcer] (inside it) within a single layout +/// pass. +class _ConstraintsHolder { + BoxConstraints? value; +} + +class _OuterConstraintsReader extends SingleChildRenderObjectWidget { + const _OuterConstraintsReader({required this.holder, super.child}); + final _ConstraintsHolder holder; + + @override + RenderObject createRenderObject(BuildContext context) => + _RenderOuterConstraintsReader(holder); + + @override + void updateRenderObject( + BuildContext context, _RenderOuterConstraintsReader renderObject) { + renderObject.holder = holder; + } +} + +class _RenderOuterConstraintsReader extends RenderProxyBox { + _RenderOuterConstraintsReader(this.holder); + _ConstraintsHolder holder; + + @override + void performLayout() { + holder.value = constraints; + super.performLayout(); + } +} + +class _InnerConstraintsEnforcer extends SingleChildRenderObjectWidget { + const _InnerConstraintsEnforcer({ + required this.holder, + required this.scrollDirection, + super.child, + }); + final _ConstraintsHolder holder; + final Axis scrollDirection; + + @override + RenderObject createRenderObject(BuildContext context) => + _RenderInnerConstraintsEnforcer(holder, scrollDirection); + + @override + void updateRenderObject( + BuildContext context, _RenderInnerConstraintsEnforcer renderObject) { + renderObject + ..holder = holder + ..scrollDirection = scrollDirection; + } +} + +class _RenderInnerConstraintsEnforcer extends RenderProxyBox { + _RenderInnerConstraintsEnforcer(this.holder, this.scrollDirection); + _ConstraintsHolder holder; + Axis scrollDirection; + + @override + void performLayout() { + if (child == null) { + size = computeSizeForNoChild(constraints); + return; + } + BoxConstraints childConstraints = constraints; + final outer = holder.value; + if (outer != null) { + if (scrollDirection == Axis.vertical && outer.hasBoundedHeight) { + childConstraints = + childConstraints.copyWith(minHeight: outer.maxHeight); + } else if (scrollDirection == Axis.horizontal && outer.hasBoundedWidth) { + childConstraints = + childConstraints.copyWith(minWidth: outer.maxWidth); + } + } + child!.layout(childConstraints, parentUsesSize: true); + size = child!.size; } } From f8cd952d3cafb103934330dbe78d754f556a3e6b Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Wed, 6 May 2026 11:40:41 -0700 Subject: [PATCH 20/21] fix(scrollable_control): mark inner enforcer dirty on outer constraint change MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The inner enforcer lives inside SingleChildScrollView, which provides unbounded constraints in the scroll axis to its child. On window resize those constraints don't change, so Flutter's layout pipeline skips the inner enforcer's performLayout — and it keeps applying the stale min captured on first layout. Result: vertical alignment looks correct initially but freezes at the original viewport size after a resize. Track the inner enforcer in the constraints holder; when the outer reader sees its incoming constraints change, mark the inner dirty via invokeLayoutCallback (which enables mutations during layout). The inner then re-runs performLayout later in the same pass with the updated holder value. --- .../lib/src/controls/scrollable_control.dart | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/packages/flet/lib/src/controls/scrollable_control.dart b/packages/flet/lib/src/controls/scrollable_control.dart index 6cb67b0df5..188a42122d 100644 --- a/packages/flet/lib/src/controls/scrollable_control.dart +++ b/packages/flet/lib/src/controls/scrollable_control.dart @@ -161,9 +161,14 @@ class _ScrollableControlState extends State /// Carries box constraints from [_OuterConstraintsReader] (outside the scroll /// view) to [_InnerConstraintsEnforcer] (inside it) within a single layout -/// pass. +/// pass. Holds a reference to the inner enforcer so the outer reader can +/// mark it dirty when its incoming constraints change — without that, the +/// inner enforcer would skip re-layout on window resize because the +/// constraints it sees from SingleChildScrollView (unbounded in scroll axis) +/// don't change. class _ConstraintsHolder { BoxConstraints? value; + RenderObject? listener; } class _OuterConstraintsReader extends SingleChildRenderObjectWidget { @@ -187,7 +192,16 @@ class _RenderOuterConstraintsReader extends RenderProxyBox { @override void performLayout() { + final changed = holder.value != constraints; holder.value = constraints; + if (changed && holder.listener != null) { + // Force the inner enforcer to re-run performLayout in this layout pass. + // invokeLayoutCallback enables mutations during layout — without it, + // markNeedsLayout asserts. + invokeLayoutCallback((_) { + holder.listener?.markNeedsLayout(); + }); + } super.performLayout(); } } @@ -219,6 +233,18 @@ class _RenderInnerConstraintsEnforcer extends RenderProxyBox { _ConstraintsHolder holder; Axis scrollDirection; + @override + void attach(PipelineOwner owner) { + super.attach(owner); + holder.listener = this; + } + + @override + void detach() { + if (holder.listener == this) holder.listener = null; + super.detach(); + } + @override void performLayout() { if (child == null) { From dfcf01421dac09fad8a23adbef15c022eaf3cb7a Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Wed, 6 May 2026 11:48:56 -0700 Subject: [PATCH 21/21] ci: restore full integration test matrix Revert the temporary narrowing that scoped runs to test_row.py while diagnosing the !debugNeedsPaint regression introduced by PR #6450. --- .github/workflows/macos-integration-tests.yml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/workflows/macos-integration-tests.yml b/.github/workflows/macos-integration-tests.yml index 3999d16850..e44ab80d04 100644 --- a/.github/workflows/macos-integration-tests.yml +++ b/.github/workflows/macos-integration-tests.yml @@ -38,9 +38,19 @@ jobs: strategy: fail-fast: false matrix: - # TEMP: narrowed to validate screenshot race fix; revert before merge. suite: + - apps + - examples/apps - examples/controls/core + - examples/controls/cupertino + - examples/controls/material + - examples/extensions + - controls/core + - controls/cupertino + - controls/material + - controls/services + - controls/theme + - extensions name: ${{ matrix.suite }} Integration Tests steps: @@ -63,8 +73,7 @@ jobs: - name: Run integration tests (${{ matrix.suite }}) working-directory: sdk/python run: | - # TEMP: scoped to test_row.py to validate screenshot fix; revert before merge. - uv run pytest -s -o log_cli=true -o log_cli_level=INFO packages/flet/integration_tests/${{ matrix.suite }}/test_row.py + uv run pytest -s -o log_cli=true -o log_cli_level=INFO packages/flet/integration_tests/${{ matrix.suite }} - name: Prepare failure screenshots if: failure()