From db5dd54bf598be5965f7bdf71de236391d0f09bd Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Thu, 24 Sep 2026 20:58:19 +0200 Subject: [PATCH 1/5] Parse InteractiveViewer.alignment before passing it to Flutter InteractiveViewer handed the raw property value, a {x, y} map, to Flutter's InteractiveViewer(alignment:). On native and wasm clients the implicit cast in Control.get threw a TypeError while building, so the viewer was replaced by Flutter's error box. JS web builds skip that check, so the map reached Transform and the viewer painted nothing, raising a NoSuchMethodError on every frame or pointer event over it. Read it with getAlignment, as before the 1.0 rewrite. The docstring described alignment as the position of the content. In Flutter it is the origin of the zoom transform: it never moves the content, and gestures and the pan() limits assume a top-left origin. Document that, and point to Container.alignment for positioning. --- CHANGELOG.md | 6 +++ .../lib/src/controls/interactive_viewer.dart | 3 +- .../test/widgets/interactive_viewer_test.dart | 51 +++++++++++++++++++ .../flet/controls/core/interactive_viewer.py | 14 ++++- 4 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 packages/flet/test/widgets/interactive_viewer_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index c4ee25dc62..bbf184bc8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 1.0.2 + +### Bug fixes + +* Fix `InteractiveViewer` failing to render when `alignment` is set ([#6744](https://github.com/flet-dev/flet/issues/6744)) by @ndonkoHenri. + ## 1.0.1 ### Improvements diff --git a/packages/flet/lib/src/controls/interactive_viewer.dart b/packages/flet/lib/src/controls/interactive_viewer.dart index 732796177f..206705041c 100644 --- a/packages/flet/lib/src/controls/interactive_viewer.dart +++ b/packages/flet/lib/src/controls/interactive_viewer.dart @@ -7,6 +7,7 @@ import 'package:vector_math/vector_math_64.dart' show Matrix4, Quad, Vector3; import '../extensions/control.dart'; import '../models/control.dart'; +import '../utils/alignment.dart'; import '../utils/edge_insets.dart'; import '../utils/events.dart'; import '../utils/misc.dart'; @@ -158,7 +159,7 @@ class _InteractiveViewerControlState extends State scaleFactor: widget.control.getDouble("scale_factor", 200)!, clipBehavior: widget.control.getClipBehavior("clip_behavior", Clip.hardEdge)!, - alignment: widget.control.get("alignment"), + alignment: widget.control.getAlignment("alignment"), boundaryMargin: widget.control.getMargin("boundary_margin", EdgeInsets.zero)!, onInteractionStart: !widget.control.disabled diff --git a/packages/flet/test/widgets/interactive_viewer_test.dart b/packages/flet/test/widgets/interactive_viewer_test.dart new file mode 100644 index 0000000000..4569fcddb4 --- /dev/null +++ b/packages/flet/test/widgets/interactive_viewer_test.dart @@ -0,0 +1,51 @@ +import 'package:flet/src/controls/interactive_viewer.dart'; +import 'package:flet/src/flet_backend.dart'; +import 'package:flet/src/flet_core_extension.dart'; +import 'package:flet/src/models/control.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:provider/provider.dart'; + +/// Pumps an InteractiveViewer built from the same message shape the client +/// receives from Python. +Future _pumpViewer(WidgetTester tester, {Object? alignment}) async { + final backend = FletBackend( + pageUri: Uri.parse('http://localhost'), + assetsDir: '', + extensions: [FletCoreExtension()], + multiView: false); + final viewer = Control.fromMap({ + '_c': 'InteractiveViewer', + '_i': 1, + if (alignment != null) 'alignment': alignment, + 'content': {'_c': 'Text', '_i': 2, 'value': 'content'}, + }, backend); + await tester.pumpWidget(ChangeNotifierProvider.value( + value: backend, + child: MaterialApp( + home: Scaffold(body: InteractiveViewerControl(control: viewer))))); +} + +/// The alignment the control passed to Flutter's [InteractiveViewer]. +Alignment? _passedAlignment(WidgetTester tester) => + tester.widget(find.byType(InteractiveViewer)).alignment; + +void main() { + testWidgets('InteractiveViewer renders its content when alignment is set', + (tester) async { + await _pumpViewer(tester, alignment: {'x': 0, 'y': 0}); + + expect(tester.takeException(), isNull); + expect(find.text('content'), findsOneWidget); + expect(_passedAlignment(tester), Alignment.center); + }); + + testWidgets('InteractiveViewer passes no alignment when none is set', + (tester) async { + await _pumpViewer(tester); + + expect(tester.takeException(), isNull); + expect(find.text('content'), findsOneWidget); + expect(_passedAlignment(tester), isNull); + }); +} diff --git a/sdk/python/packages/flet/src/flet/controls/core/interactive_viewer.py b/sdk/python/packages/flet/src/flet/controls/core/interactive_viewer.py index 1610cf601c..7864ba7535 100644 --- a/sdk/python/packages/flet/src/flet/controls/core/interactive_viewer.py +++ b/sdk/python/packages/flet/src/flet/controls/core/interactive_viewer.py @@ -135,7 +135,19 @@ class InteractiveViewer(LayoutControl): alignment: Optional[Alignment] = None """ - The alignment of the :attr:`content` within this viewer. + The origin of the transformation applied to the :attr:`content` when it is + zoomed or panned, relative to the content's size. + + :meth:`zoom` scales the content around this point. It does not change where the + content is placed in this viewer; to position the content, wrap it in a control + that aligns its child, such as a :class:`~flet.Container` with + :attr:`~flet.Container.alignment` set. + + Note: + Gestures and the limits of :meth:`pan` treat the top-left corner as the + origin. With any other value, the content drifts away from the pointer while + zooming with a gesture, and once zoomed in, panning stops short of one edge + of the content and goes past the opposite one, leaving empty space. """ boundary_margin: MarginValue = field(default_factory=lambda: Margin.all(0)) From e4762d8a9e24a1ebba7eacdb29eb85cb108bde6c Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Thu, 24 Sep 2026 21:23:48 +0200 Subject: [PATCH 2/5] Add PR link to changelog entry --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bbf184bc8e..8ddef63a4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ### Bug fixes -* Fix `InteractiveViewer` failing to render when `alignment` is set ([#6744](https://github.com/flet-dev/flet/issues/6744)) by @ndonkoHenri. +* Fix `InteractiveViewer` failing to render when `alignment` is set ([#6744](https://github.com/flet-dev/flet/issues/6744), [#6887](https://github.com/flet-dev/flet/pull/6887)) by @ndonkoHenri. ## 1.0.1 From 69bc16ad9867d63202de8d46504f54919042ea21 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Thu, 24 Sep 2026 21:26:17 +0200 Subject: [PATCH 3/5] Remove the InteractiveViewer widget test --- .../test/widgets/interactive_viewer_test.dart | 51 ------------------- 1 file changed, 51 deletions(-) delete mode 100644 packages/flet/test/widgets/interactive_viewer_test.dart diff --git a/packages/flet/test/widgets/interactive_viewer_test.dart b/packages/flet/test/widgets/interactive_viewer_test.dart deleted file mode 100644 index 4569fcddb4..0000000000 --- a/packages/flet/test/widgets/interactive_viewer_test.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:flet/src/controls/interactive_viewer.dart'; -import 'package:flet/src/flet_backend.dart'; -import 'package:flet/src/flet_core_extension.dart'; -import 'package:flet/src/models/control.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:provider/provider.dart'; - -/// Pumps an InteractiveViewer built from the same message shape the client -/// receives from Python. -Future _pumpViewer(WidgetTester tester, {Object? alignment}) async { - final backend = FletBackend( - pageUri: Uri.parse('http://localhost'), - assetsDir: '', - extensions: [FletCoreExtension()], - multiView: false); - final viewer = Control.fromMap({ - '_c': 'InteractiveViewer', - '_i': 1, - if (alignment != null) 'alignment': alignment, - 'content': {'_c': 'Text', '_i': 2, 'value': 'content'}, - }, backend); - await tester.pumpWidget(ChangeNotifierProvider.value( - value: backend, - child: MaterialApp( - home: Scaffold(body: InteractiveViewerControl(control: viewer))))); -} - -/// The alignment the control passed to Flutter's [InteractiveViewer]. -Alignment? _passedAlignment(WidgetTester tester) => - tester.widget(find.byType(InteractiveViewer)).alignment; - -void main() { - testWidgets('InteractiveViewer renders its content when alignment is set', - (tester) async { - await _pumpViewer(tester, alignment: {'x': 0, 'y': 0}); - - expect(tester.takeException(), isNull); - expect(find.text('content'), findsOneWidget); - expect(_passedAlignment(tester), Alignment.center); - }); - - testWidgets('InteractiveViewer passes no alignment when none is set', - (tester) async { - await _pumpViewer(tester); - - expect(tester.takeException(), isNull); - expect(find.text('content'), findsOneWidget); - expect(_passedAlignment(tester), isNull); - }); -} From 4097e8bd71fdce246369b27994295ad87b593f22 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Fri, 25 Sep 2026 13:14:59 +0200 Subject: [PATCH 4/5] Explain the Audio example's local asset in the docs The docs page shows only main.py, so code copied from it fails without the example's assets/viper.mp3. The example intro now points at that file and at using a URL for src instead. The project description no longer calls the track remote. --- .../extensions/audio/audio_playback_controls/pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sdk/python/examples/extensions/audio/audio_playback_controls/pyproject.toml b/sdk/python/examples/extensions/audio/audio_playback_controls/pyproject.toml index fefb87a1b9..1042b26c78 100644 --- a/sdk/python/examples/extensions/audio/audio_playback_controls/pyproject.toml +++ b/sdk/python/examples/extensions/audio/audio_playback_controls/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "audio-playback-controls" version = "1.0.0" -description = "Controls audio playback, seek position, volume, and balance for a remote track." +description = "Controls audio playback, seek position, volume, and balance for a local track." requires-python = ">=3.10" keywords = ["services", "audio", "playback", "seek", "volume", "balance", "async"] authors = [{ name = "Flet team", email = "hello@flet.dev" }] @@ -15,6 +15,7 @@ categories = ["Media/Audio"] [tool.flet.metadata] title = "Audio playback controls" +docs_intro = "Plays `viper.mp3` from the example's [`assets`](https://github.com/flet-dev/flet/tree/main/sdk/python/examples/extensions/audio/audio_playback_controls/assets) folder. To run the code on its own, put that file in an `assets` folder next to your script, or set [`src`][flet_audio.Audio.src] to the URL of an audio file." controls = ["SafeArea", "Column", "Row", "Button", "Audio"] layout_pattern = "inline-actions" complexity = "basic" From fbea6e61e318cb2bbb3cd7483241e04488e6703d Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Fri, 25 Sep 2026 13:15:00 +0200 Subject: [PATCH 5/5] Warn when the desktop client comes from a previous flet build A client found in build/ is launched ahead of the standard one. On Windows and Linux this was never logged, and on macOS only with -v, so a client built before an extension was added went unnoticed: calls to that extension just timed out. All three platforms now log a warning with the client's path, which is shown without -v. --- CHANGELOG.md | 4 +++ .../flet-desktop/src/flet_desktop/__init__.py | 25 ++++++++++++++++++- website/docs/getting-started/running-app.md | 8 +++--- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ddef63a4e..7539290f07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## 1.0.2 +### Improvements + +* `flet run` and `ft.run()` now print a warning with the desktop client's path when they use one from a previous `flet build` in `build/`, so an outdated client that lacks an extension is easy to spot ([#6821](https://github.com/flet-dev/flet/issues/6821)) by @ndonkoHenri. + ### Bug fixes * Fix `InteractiveViewer` failing to render when `alignment` is set ([#6744](https://github.com/flet-dev/flet/issues/6744), [#6887](https://github.com/flet-dev/flet/pull/6887)) by @ndonkoHenri. diff --git a/sdk/python/packages/flet-desktop/src/flet_desktop/__init__.py b/sdk/python/packages/flet-desktop/src/flet_desktop/__init__.py index 84d31fe68f..3dc13d1b24 100644 --- a/sdk/python/packages/flet-desktop/src/flet_desktop/__init__.py +++ b/sdk/python/packages/flet-desktop/src/flet_desktop/__init__.py @@ -421,6 +421,25 @@ def find_macos_app_bundle(directory) -> Path | None: return None +def __log_build_client(build_dir: str, client_path: str | Path) -> None: + """ + Reports that a client from a previous `flet build` is used instead of the + standard one. + + Logged as a warning, so it is shown even without `flet run -v`: a client + built before an extension was added lacks that extension, and the app's + calls to it then time out without pointing at the client. + + Args: + build_dir: The `build/` directory the client was found in. + client_path: The client executable or `.app` bundle that is launched. + """ + logger.warning( + f"Using the Flet client from a previous `flet build`: {client_path}. " + f"Move, rename or delete {build_dir} to use the standard client." + ) + + def __linux_identity_args(args: list) -> tuple: """ Give the client window a per-app identity on Linux, without touching it. @@ -604,6 +623,8 @@ def __locate_and_unpack_flet_view(page_url, assets_dir, hidden): for f in os.listdir(build_windows): if f.endswith(".exe"): flet_path = os.path.join(build_windows, f) + if flet_path: + __log_build_client(build_windows, flet_path) # 2. Check FLET_VIEW_PATH (developer mode) if not flet_path: @@ -631,7 +652,7 @@ def __locate_and_unpack_flet_view(page_url, assets_dir, hidden): build_macos = os.path.join(os.getcwd(), "build", "macos") app_path = find_macos_app_bundle(build_macos) if app_path: - logger.info(f"Flet.app found in {build_macos}") + __log_build_client(build_macos, app_path) # 2. Check FLET_VIEW_PATH (developer mode) if not app_path: @@ -668,6 +689,8 @@ def __locate_and_unpack_flet_view(page_url, assets_dir, hidden): ef = os.path.join(build_linux, f) if os.path.isfile(ef) and stat.S_IXUSR & os.stat(ef)[stat.ST_MODE]: app_path = ef + if app_path: + __log_build_client(build_linux, app_path) # 2. Check FLET_VIEW_PATH (developer mode) if not app_path: diff --git a/website/docs/getting-started/running-app.md b/website/docs/getting-started/running-app.md index e3880d2463..875e127d6b 100644 --- a/website/docs/getting-started/running-app.md +++ b/website/docs/getting-started/running-app.md @@ -86,9 +86,11 @@ require rebuilding the client. See [Creating an Extension](../extend/user-extensions.md#change-python-files) for an example of this workflow. -To use the standard client again, move or rename the corresponding -`build/` directory and unset `FLET_VIEW_PATH` if it is set. Setting -`FLET_VIEW_PATH` alone does not override a client found in `build/`. +When Flet uses a client from `build/`, it prints a warning with the +client's path. To use the standard client again, move, rename or delete the +corresponding `build/` directory and unset `FLET_VIEW_PATH` if it is +set. Setting `FLET_VIEW_PATH` alone does not override a client found in +`build/`. :::note Opening the built executable directly runs the packaged app, including the Python