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 48ba440737..7e8fea7503 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,7 +32,9 @@ * 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), [#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. * Fix absolute-path `src` (e.g. `Image(src="/images/foo.svg")`) breaking on web when the app is mounted at a non-root URL, pass `data:`/`blob:` URIs through the asset resolver unchanged, preserve origin-relative semantics when `assets_dir` is unset, and add a `window.flet.assetsDir` JS-interop bridge so embedding hosts can supply `assets_dir` to the top-level `FletApp` ([#6470](https://github.com/flet-dev/flet/pull/6470)) by @FeodorFitsner. 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); } diff --git a/packages/flet/lib/src/controls/scrollable_control.dart b/packages/flet/lib/src/controls/scrollable_control.dart index 79d4a7a07e..188a42122d 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,34 +113,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; - - Widget scrollViewChild = widget.child; - if (minWidth > 0 || minHeight > 0) { - scrollViewChild = ConstrainedBox( - constraints: - BoxConstraints(minWidth: minWidth, minHeight: minHeight), - child: scrollViewChild, - ); - } - - return SingleChildScrollView( - controller: _controller, + // 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: _InnerConstraintsEnforcer( + holder: _outerConstraints, scrollDirection: widget.scrollDirection, - child: scrollViewChild, - ); - }); + child: widget.child, + ), + ); } - return Scrollbar( + Widget result = Scrollbar( thumbVisibility: scrollConfiguration.thumbVisibility, trackVisibility: scrollConfiguration.trackVisibility, thickness: scrollConfiguration.thickness, @@ -150,5 +147,122 @@ 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. 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 { + 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() { + 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(); + } +} + +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 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) { + 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; } } 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"] 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. 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 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 4baf178251..91e6047876 100644 Binary files a/sdk/python/packages/flet/integration_tests/controls/material/golden/macos/navigation_bar/basic.png and b/sdk/python/packages/flet/integration_tests/controls/material/golden/macos/navigation_bar/basic.png differ diff --git a/sdk/python/packages/flet/integration_tests/controls/material/test_navigation_bar.py b/sdk/python/packages/flet/integration_tests/controls/material/test_navigation_bar.py index a8fae9e85b..6d1a072ad2 100644 --- a/sdk/python/packages/flet/integration_tests/controls/material/test_navigation_bar.py +++ b/sdk/python/packages/flet/integration_tests/controls/material/test_navigation_bar.py @@ -16,15 +16,16 @@ async def test_basic(flet_app: ftt.FletTestApp, request): await flet_app.assert_control_screenshot( request.node.name, ft.NavigationBar( + selected_index=2, destinations=[ ft.NavigationBarDestination(icon=ft.Icons.EXPLORE, label="Explore"), ft.NavigationBarDestination(icon=ft.Icons.COMMUTE, label="Commute"), ft.NavigationBarDestination( icon=ft.Icons.BOOKMARK_BORDER, - selected_icon=ft.Icons.BOOKMARK, + selected_icon=ft.Icon(ft.Icons.BOOKMARK), label="Favorites", ), - ] + ], ), ) 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(