From e46d1ce35a75702ebc088d2eafb23c48da524d01 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Wed, 28 Oct 2026 16:47:49 +0100 Subject: [PATCH 01/10] Fix BottomAppBar notch position when the bar has a margin (#6986) --- CHANGELOG.md | 1 + .../flet/lib/src/controls/bottom_app_bar.dart | 31 ++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9cf707de8..dbca8f40ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,7 @@ ### Bug fixes * Fix `use_state()` type inference for callable initializers in mypy and ty ([#6960](https://github.com/flet-dev/flet/issues/6960), [#7014](https://github.com/flet-dev/flet/pull/7014)) by @mmporong. +* Fix `BottomAppBar` cutting its notch away from the floating action button when the bar has a left or top `margin` ([#6986](https://github.com/flet-dev/flet/issues/6986)) by @ndonkoHenri. * Fix `flet build` apps showing a blank page when the system suspended them while Python was starting, such as when iOS launches an app in the background for a notification action ([#6999](https://github.com/flet-dev/flet/pull/6999)) by @ndonkoHenri. * Fix `flet build` naming a Linux app's icons `None.png` and leaving the iOS frameworks it builds from the app's packages under shared identifiers when no `bundle_id` is set ([#6999](https://github.com/flet-dev/flet/pull/6999)) by @ndonkoHenri. * Fix `flet build` writing numbers in `[tool.flet.macos.entitlement]`, `[tool.flet.ios.info]` and `[tool.flet.macos.info]` as strings, producing invalid plists when keys or values contain `&` or `<`, and failing when a `pyproject.toml` value contains `{{` ([#6999](https://github.com/flet-dev/flet/pull/6999)) by @ndonkoHenri. diff --git a/packages/flet/lib/src/controls/bottom_app_bar.dart b/packages/flet/lib/src/controls/bottom_app_bar.dart index 95fe332269..b1ba6f3bae 100644 --- a/packages/flet/lib/src/controls/bottom_app_bar.dart +++ b/packages/flet/lib/src/controls/bottom_app_bar.dart @@ -31,7 +31,8 @@ class BottomAppBarControl extends StatelessWidget { padding: control.getPadding("padding"), height: control.getDouble("height"), elevation: control.getDouble("elevation"), - shape: control.getNotchedShape("shape", theme), + shape: _shiftNotch( + control.getNotchedShape("shape", theme), control.getMargin("margin")), shadowColor: control.getColor("shadow_color", context), color: control.getColor("bgcolor", context), notchMargin: control.getDouble("notch_margin", 4.0)!, @@ -50,3 +51,31 @@ class BottomAppBarControl extends StatelessWidget { return LayoutControl(control: control, child: bottomAppBar); } } + +/// Moves the notch of [shape] back under the floating action button when the +/// bar has a [margin]. +/// +/// The margin is applied around the bar, but `Scaffold` reports the button's +/// position relative to the whole bottom slot, so the notch would otherwise be +/// cut `margin.left` too far right and `margin.top` too far down. +NotchedShape? _shiftNotch(NotchedShape? shape, EdgeInsets? margin) { + if (shape == null || + margin == null || + (margin.left == 0 && margin.top == 0)) { + return shape; + } + return _OffsetNotchedShape(shape, Offset(-margin.left, -margin.top)); +} + +/// A [NotchedShape] that shifts the guest rectangle by [offset] before +/// cutting [inner]'s notch. +class _OffsetNotchedShape extends NotchedShape { + const _OffsetNotchedShape(this.inner, this.offset); + + final NotchedShape inner; + final Offset offset; + + @override + Path getOuterPath(Rect host, Rect? guest) => + inner.getOuterPath(host, guest?.shift(offset)); +} From 061b0f6400bfd32238ed299c147c0a6654b6208a Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Thu, 29 Oct 2026 10:04:41 +0100 Subject: [PATCH 02/10] Test the BottomAppBar notch position with a margin A page-level golden test with a centre-docked FloatingActionButton and a BottomAppBar with left, top and right margins. The small window keeps the bar large in the screenshot: without the fix the notch is cut 40 px to the right and 16 px too deep, and similarity drops to 98.6%, below the 99% threshold. --- .../bottom_app_bar/notch_with_margin.png | Bin 0 -> 10112 bytes .../controls/material/test_bottom_app_bar.py | 32 ++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 sdk/python/packages/flet/integration_tests/controls/material/golden/macos/bottom_app_bar/notch_with_margin.png diff --git a/sdk/python/packages/flet/integration_tests/controls/material/golden/macos/bottom_app_bar/notch_with_margin.png b/sdk/python/packages/flet/integration_tests/controls/material/golden/macos/bottom_app_bar/notch_with_margin.png new file mode 100644 index 0000000000000000000000000000000000000000..8b6854f3ce2be7c9ab0119ea58cde5f844c940d6 GIT binary patch literal 10112 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 Date: Thu, 29 Oct 2026 10:05:21 +0100 Subject: [PATCH 03/10] Link the PR in the changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dbca8f40ac..a10710b267 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,7 +60,7 @@ ### Bug fixes * Fix `use_state()` type inference for callable initializers in mypy and ty ([#6960](https://github.com/flet-dev/flet/issues/6960), [#7014](https://github.com/flet-dev/flet/pull/7014)) by @mmporong. -* Fix `BottomAppBar` cutting its notch away from the floating action button when the bar has a left or top `margin` ([#6986](https://github.com/flet-dev/flet/issues/6986)) by @ndonkoHenri. +* Fix `BottomAppBar` cutting its notch away from the floating action button when the bar has a left or top `margin` ([#6986](https://github.com/flet-dev/flet/issues/6986), [#7024](https://github.com/flet-dev/flet/pull/7024)) by @ndonkoHenri. * Fix `flet build` apps showing a blank page when the system suspended them while Python was starting, such as when iOS launches an app in the background for a notification action ([#6999](https://github.com/flet-dev/flet/pull/6999)) by @ndonkoHenri. * Fix `flet build` naming a Linux app's icons `None.png` and leaving the iOS frameworks it builds from the app's packages under shared identifiers when no `bundle_id` is set ([#6999](https://github.com/flet-dev/flet/pull/6999)) by @ndonkoHenri. * Fix `flet build` writing numbers in `[tool.flet.macos.entitlement]`, `[tool.flet.ios.info]` and `[tool.flet.macos.info]` as strings, producing invalid plists when keys or values contain `&` or `<`, and failing when a `pyproject.toml` value contains `{{` ([#6999](https://github.com/flet-dev/flet/pull/6999)) by @ndonkoHenri. From 59c0ce96fb236b9398763caa782e6a113fb60933 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Fri, 30 Oct 2026 10:05:53 +0100 Subject: [PATCH 04/10] Keep the BottomAppBar notch right with animated margins, in the body and from the theme An animated margin moves the bar without resizing it, so BottomAppBar never re-cut its notch: it kept the cut from the start of the animation and slid with the bar, ending up to 80 px off for a left/right swap. The control now mirrors `animate_margin` with a TweenAnimationBuilder that rebuilds the shape on every frame of that animation. Outside the scaffold's bottom slot, BottomAppBar measures the bar's own top, which already includes the top margin, so the shape only shifts horizontally there. A notch shape from BottomAppBarTheme now gets the same correction as one set on the control. --- .../flet/lib/src/controls/bottom_app_bar.dart | 70 ++++++++++++++----- 1 file changed, 51 insertions(+), 19 deletions(-) diff --git a/packages/flet/lib/src/controls/bottom_app_bar.dart b/packages/flet/lib/src/controls/bottom_app_bar.dart index b1ba6f3bae..d589d8d691 100644 --- a/packages/flet/lib/src/controls/bottom_app_bar.dart +++ b/packages/flet/lib/src/controls/bottom_app_bar.dart @@ -1,7 +1,9 @@ +import 'package:flutter/foundation.dart' show ValueListenable; import 'package:material_ui/material_ui.dart'; import '../extensions/control.dart'; import '../models/control.dart'; +import '../utils/animations.dart'; import '../utils/borders.dart'; import '../utils/colors.dart'; import '../utils/edge_insets.dart'; @@ -25,19 +27,39 @@ class BottomAppBarControl extends StatelessWidget { borderRadius != null && borderRadius != BorderRadius.zero ? Clip.antiAlias : Clip.none)!; + final shape = control.getNotchedShape( + "shape", theme, BottomAppBarTheme.of(context).shape); + final geometry = + Scaffold.maybeOf(context) != null ? Scaffold.geometryOf(context) : null; + final margin = control.getMargin("margin"); + final marginAnimation = control.getAnimation("animate_margin"); + final content = control.buildWidget("content"); - Widget bottomAppBar = BottomAppBar( - clipBehavior: clipBehavior, - padding: control.getPadding("padding"), - height: control.getDouble("height"), - elevation: control.getDouble("elevation"), - shape: _shiftNotch( - control.getNotchedShape("shape", theme), control.getMargin("margin")), - shadowColor: control.getColor("shadow_color", context), - color: control.getColor("bgcolor", context), - notchMargin: control.getDouble("notch_margin", 4.0)!, - child: control.buildWidget("content"), - ); + Widget buildBar(EdgeInsets? margin, Widget? child) => BottomAppBar( + clipBehavior: clipBehavior, + padding: control.getPadding("padding"), + height: control.getDouble("height"), + elevation: control.getDouble("elevation"), + shape: _shiftNotch(shape, margin, geometry), + shadowColor: control.getColor("shadow_color", context), + color: control.getColor("bgcolor", context), + notchMargin: control.getDouble("notch_margin", 4.0)!, + child: child, + ); + + // LayoutControl animates an animated margin around the bar, which moves + // the bar without resizing it, so BottomAppBar keeps its notch where it + // was first cut. Animating the same margin here rebuilds the notch for + // every frame of that animation. + Widget bottomAppBar = margin != null && marginAnimation != null + ? TweenAnimationBuilder( + tween: EdgeInsetsTween(begin: margin, end: margin), + duration: marginAnimation.duration, + curve: marginAnimation.curve, + builder: (context, margin, child) => buildBar(margin, child), + child: content, + ) + : buildBar(margin, content); if (borderRadius != null && borderRadius != BorderRadius.zero) { bottomAppBar = ClipRRect( @@ -58,24 +80,34 @@ class BottomAppBarControl extends StatelessWidget { /// The margin is applied around the bar, but `Scaffold` reports the button's /// position relative to the whole bottom slot, so the notch would otherwise be /// cut `margin.left` too far right and `margin.top` too far down. -NotchedShape? _shiftNotch(NotchedShape? shape, EdgeInsets? margin) { +NotchedShape? _shiftNotch(NotchedShape? shape, EdgeInsets? margin, + ValueListenable? geometry) { if (shape == null || margin == null || (margin.left == 0 && margin.top == 0)) { return shape; } - return _OffsetNotchedShape(shape, Offset(-margin.left, -margin.top)); + return _OffsetNotchedShape(shape, margin, geometry); } -/// A [NotchedShape] that shifts the guest rectangle by [offset] before +/// A [NotchedShape] that shifts the guest rectangle by [margin] before /// cutting [inner]'s notch. +/// +/// Outside the bottom slot, such as in the page body, BottomAppBar measures +/// the bar's own top, which already includes `margin.top`, so only the +/// horizontal shift applies there. class _OffsetNotchedShape extends NotchedShape { - const _OffsetNotchedShape(this.inner, this.offset); + const _OffsetNotchedShape(this.inner, this.margin, this.geometry); final NotchedShape inner; - final Offset offset; + final EdgeInsets margin; + final ValueListenable? geometry; @override - Path getOuterPath(Rect host, Rect? guest) => - inner.getOuterPath(host, guest?.shift(offset)); + Path getOuterPath(Rect host, Rect? guest) { + // Read while BottomAppBar cuts the notch, as it reads the same geometry. + final inBottomSlot = geometry?.value.bottomNavigationBarTop != null; + final offset = Offset(-margin.left, inBottomSlot ? -margin.top : 0); + return inner.getOuterPath(host, guest?.shift(offset)); + } } From 3d725a2cee47b1f66d9e02dc83e685710f36b5fa Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Fri, 30 Oct 2026 10:05:53 +0100 Subject: [PATCH 05/10] Read the bottom app bar theme from the keys Python sends The Python `Theme` sends `bottom_appbar_theme` with a `bgcolor` field, but parseTheme() read `bottom_app_bar_theme` and `color`, so a theme's BottomAppBarTheme was ignored entirely. --- packages/flet/lib/src/utils/theme.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/flet/lib/src/utils/theme.dart b/packages/flet/lib/src/utils/theme.dart index 0f54e88864..9c93632b19 100644 --- a/packages/flet/lib/src/utils/theme.dart +++ b/packages/flet/lib/src/utils/theme.dart @@ -290,7 +290,7 @@ ThemeData parseTheme( floatingActionButtonTheme: parseFloatingActionButtonTheme( value?["floating_action_button_theme"], theme), bottomAppBarTheme: - parseBottomAppBarTheme(value?["bottom_app_bar_theme"], theme), + parseBottomAppBarTheme(value?["bottom_appbar_theme"], theme), checkboxTheme: parseCheckboxTheme(value?["checkbox_theme"], theme), radioTheme: parseRadioTheme(value?["radio_theme"], theme), badgeTheme: parseBadgeTheme(value?["badge_theme"], theme), @@ -734,7 +734,7 @@ BottomAppBarThemeData? parseBottomAppBarTheme( if (value == null) return defaultValue; return theme.bottomAppBarTheme.copyWith( - color: parseColor(value["color"], theme), + color: parseColor(value["bgcolor"], theme), shadowColor: parseColor(value["shadow_color"], theme), elevation: parseDouble(value["elevation"]), height: parseDouble(value["height"]), From 5a2c9ea78e36278204fde5041a4118c550d10ec1 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Fri, 30 Oct 2026 10:05:54 +0100 Subject: [PATCH 06/10] Test the notch with a theme shape and run BottomAppBar tests per app The page-level notch tests set the page's bottom bar and FAB, so every test in the file now gets its own app, as in test_floating_action_button.py. test_theme_notch_with_margin takes the notch shape and color only from `page.theme`; it fails without the theme key fix (94.6% similarity). --- .../bottom_app_bar/notch_with_margin.png | Bin 10112 -> 11840 bytes .../theme_notch_with_margin.png | Bin 0 -> 10306 bytes .../controls/material/test_bottom_app_bar.py | 75 +++++++++++++----- 3 files changed, 55 insertions(+), 20 deletions(-) create mode 100644 sdk/python/packages/flet/integration_tests/controls/material/golden/macos/bottom_app_bar/theme_notch_with_margin.png diff --git a/sdk/python/packages/flet/integration_tests/controls/material/golden/macos/bottom_app_bar/notch_with_margin.png b/sdk/python/packages/flet/integration_tests/controls/material/golden/macos/bottom_app_bar/notch_with_margin.png index 8b6854f3ce2be7c9ab0119ea58cde5f844c940d6..ab39dc70ce5f48e51ccf6b6223ee7c800072a825 100644 GIT binary patch literal 11840 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 U&9OnnLBLPvj)8WKrhV*x0YKA!rvLx| literal 10112 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 ft.Page: + # A small window keeps the bar large in the screenshot, so a misplaced + # notch drops similarity below the threshold. + page = flet_app.page page.enable_screenshots = True - flet_app_function.resize_page(240, 160) + flet_app.resize_page(240, 160) page.bgcolor = ft.Colors.BLUE_GREY_200 - page.bottom_appbar = ft.BottomAppBar( - bgcolor=ft.Colors.WHITE, - shape=ft.CircularRectangleNotchShape(), - border_radius=36, - margin=ft.Margin.only(left=40, top=16, right=40, bottom=24), - height=70, - ) page.floating_action_button = ft.FloatingActionButton( icon=ft.Icons.ADD, shape=ft.CircleBorder(), @@ -44,12 +55,36 @@ async def test_notch_with_margin(flet_app_function: ftt.FletTestApp): foreground_color=ft.Colors.WHITE, ) page.floating_action_button_location = ft.FloatingActionButtonLocation.CENTER_DOCKED - page.update() - await flet_app_function.tester.pump_and_settle() + return page - flet_app_function.assert_screenshot( - "notch_with_margin", - await page.take_screenshot( - pixel_ratio=flet_app_function.screenshots_pixel_ratio - ), + +@pytest.mark.asyncio(loop_scope="function") +async def test_notch_with_margin(flet_app: ftt.FletTestApp): + """The notch stays centered under a docked FAB when the bar has margins.""" + page = _page_with_docked_fab(flet_app) + page.bottom_appbar = ft.BottomAppBar( + bgcolor=ft.Colors.WHITE, + shape=ft.CircularRectangleNotchShape(), + border_radius=36, + margin=ft.Margin.only(left=40, top=16, right=40, bottom=24), + height=70, + ) + await _assert_page_screenshot(flet_app, "notch_with_margin") + + +@pytest.mark.asyncio(loop_scope="function") +async def test_theme_notch_with_margin(flet_app: ftt.FletTestApp): + """A notch shape and color from the page theme apply, centered with margins.""" + page = _page_with_docked_fab(flet_app) + page.theme = ft.Theme( + bottom_appbar_theme=ft.BottomAppBarTheme( + bgcolor=ft.Colors.ORANGE_300, + shape=ft.CircularRectangleNotchShape(), + ) + ) + page.bottom_appbar = ft.BottomAppBar( + border_radius=36, + margin=ft.Margin.only(left=40, top=16, right=40, bottom=24), + height=70, ) + await _assert_page_screenshot(flet_app, "theme_notch_with_margin") From 8f753680a228b429564970bac2959db35a000c61 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Fri, 30 Oct 2026 10:05:55 +0100 Subject: [PATCH 07/10] Update changelogs for the BottomAppBar notch and theme fixes --- CHANGELOG.md | 3 ++- packages/flet/CHANGELOG.md | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a10710b267..3d40512f60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,7 +60,8 @@ ### Bug fixes * Fix `use_state()` type inference for callable initializers in mypy and ty ([#6960](https://github.com/flet-dev/flet/issues/6960), [#7014](https://github.com/flet-dev/flet/pull/7014)) by @mmporong. -* Fix `BottomAppBar` cutting its notch away from the floating action button when the bar has a left or top `margin` ([#6986](https://github.com/flet-dev/flet/issues/6986), [#7024](https://github.com/flet-dev/flet/pull/7024)) by @ndonkoHenri. +* Fix `BottomAppBar` cutting its notch away from the floating action button when the bar has a left or top `margin`, including an animated one ([#6986](https://github.com/flet-dev/flet/issues/6986), [#7024](https://github.com/flet-dev/flet/pull/7024)) by @ndonkoHenri. +* Fix `Theme.bottom_appbar_theme` being ignored, so its `bgcolor`, `shape` and other settings never reached `BottomAppBar` ([#7024](https://github.com/flet-dev/flet/pull/7024)) by @ndonkoHenri. * Fix `flet build` apps showing a blank page when the system suspended them while Python was starting, such as when iOS launches an app in the background for a notification action ([#6999](https://github.com/flet-dev/flet/pull/6999)) by @ndonkoHenri. * Fix `flet build` naming a Linux app's icons `None.png` and leaving the iOS frameworks it builds from the app's packages under shared identifiers when no `bundle_id` is set ([#6999](https://github.com/flet-dev/flet/pull/6999)) by @ndonkoHenri. * Fix `flet build` writing numbers in `[tool.flet.macos.entitlement]`, `[tool.flet.ios.info]` and `[tool.flet.macos.info]` as strings, producing invalid plists when keys or values contain `&` or `<`, and failing when a `pyproject.toml` value contains `{{` ([#6999](https://github.com/flet-dev/flet/pull/6999)) by @ndonkoHenri. diff --git a/packages/flet/CHANGELOG.md b/packages/flet/CHANGELOG.md index 058ca5d23f..67589f573d 100644 --- a/packages/flet/CHANGELOG.md +++ b/packages/flet/CHANGELOG.md @@ -1,5 +1,6 @@ ## 1.1.0 +* `BottomAppBarControl` shifts its notch by the bar's margin, following an `animate_margin` animation frame by frame, also for a notch shape from `BottomAppBarTheme`. `parseTheme()` reads the bottom app bar theme from `bottom_appbar_theme` and its color from `bgcolor`, the keys the Python `Theme` sends by @ndonkoHenri. * Update `file_picker` to 13.x (federated; iOS picks media with `PHPickerViewController`, dropping DKImagePickerController, whose camera and photo-library references made App Store Connect demand purpose strings). `FilePickerService` uses `pickFile()`/`pickFiles()`, reads bytes and uploads through `PlatformFile.readAsBytes()`/`readAsByteStream()`, passes `lockParentWindow` through `WindowsOptions`/`LinuxOptions` and `cancel_upload_on_window_blur` through `file_picker_web`'s `FilePickerWebOptions` (imported only on web). `saveFile()` always writes the file, so desktop saves without `src_bytes` create it empty. Bump `device_info_plus` to ^13.3.0, `package_info_plus` to ^10.2.2 and `share_plus` to ^13.3.1 (`win32` 6, required by file_picker 13's Windows implementation) by @FeodorFitsner. * Apply `min_width`/`max_width`/`min_height`/`max_height` to every `LayoutControl` as a `ConstrainedBox` around the control (outside any fixed size). A control that implements one of these names itself lists it in `skip_properties` (`ListTile.min_height`, `NavigationRail.min_width`) by @FeodorFitsner. * Pass `material_tap_target_size` to `Switch` and `ThemeData` (`parseMaterialTapTargetSize()`) by @FeodorFitsner. From b13ea07d595bc6bf58750b5c8b8293cf006e61e6 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Fri, 30 Oct 2026 13:13:25 +0100 Subject: [PATCH 08/10] Keep the BottomAppBar notch animation running when border_radius changes and honour skipped margins --- packages/flet/CHANGELOG.md | 2 +- .../flet/lib/src/controls/bottom_app_bar.dart | 51 +++++++++++-------- 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/packages/flet/CHANGELOG.md b/packages/flet/CHANGELOG.md index 67589f573d..e84ecb5b87 100644 --- a/packages/flet/CHANGELOG.md +++ b/packages/flet/CHANGELOG.md @@ -1,6 +1,6 @@ ## 1.1.0 -* `BottomAppBarControl` shifts its notch by the bar's margin, following an `animate_margin` animation frame by frame, also for a notch shape from `BottomAppBarTheme`. `parseTheme()` reads the bottom app bar theme from `bottom_appbar_theme` and its color from `bgcolor`, the keys the Python `Theme` sends by @ndonkoHenri. +* `BottomAppBarControl` shifts its notch by the bar's margin, following an `animate_margin` animation frame by frame, also for a notch shape from `BottomAppBarTheme`. `parseTheme()` reads the bottom app bar theme from `bottom_appbar_theme` and its color from `bgcolor`, the keys the Python `Theme` sends ([#6986](https://github.com/flet-dev/flet/issues/6986), [#7024](https://github.com/flet-dev/flet/pull/7024)) by @ndonkoHenri. * Update `file_picker` to 13.x (federated; iOS picks media with `PHPickerViewController`, dropping DKImagePickerController, whose camera and photo-library references made App Store Connect demand purpose strings). `FilePickerService` uses `pickFile()`/`pickFiles()`, reads bytes and uploads through `PlatformFile.readAsBytes()`/`readAsByteStream()`, passes `lockParentWindow` through `WindowsOptions`/`LinuxOptions` and `cancel_upload_on_window_blur` through `file_picker_web`'s `FilePickerWebOptions` (imported only on web). `saveFile()` always writes the file, so desktop saves without `src_bytes` create it empty. Bump `device_info_plus` to ^13.3.0, `package_info_plus` to ^10.2.2 and `share_plus` to ^13.3.1 (`win32` 6, required by file_picker 13's Windows implementation) by @FeodorFitsner. * Apply `min_width`/`max_width`/`min_height`/`max_height` to every `LayoutControl` as a `ConstrainedBox` around the control (outside any fixed size). A control that implements one of these names itself lists it in `skip_properties` (`ListTile.min_height`, `NavigationRail.min_width`) by @FeodorFitsner. * Pass `material_tap_target_size` to `Switch` and `ThemeData` (`parseMaterialTapTargetSize()`) by @FeodorFitsner. diff --git a/packages/flet/lib/src/controls/bottom_app_bar.dart b/packages/flet/lib/src/controls/bottom_app_bar.dart index d589d8d691..0a266d11c4 100644 --- a/packages/flet/lib/src/controls/bottom_app_bar.dart +++ b/packages/flet/lib/src/controls/bottom_app_bar.dart @@ -31,27 +31,43 @@ class BottomAppBarControl extends StatelessWidget { "shape", theme, BottomAppBarTheme.of(context).shape); final geometry = Scaffold.maybeOf(context) != null ? Scaffold.geometryOf(context) : null; - final margin = control.getMargin("margin"); + final skipProps = control.internals?["skip_properties"] as List?; + final margin = skipProps?.contains("margin") == true + ? null + : control.getMargin("margin"); final marginAnimation = control.getAnimation("animate_margin"); final content = control.buildWidget("content"); - Widget buildBar(EdgeInsets? margin, Widget? child) => BottomAppBar( - clipBehavior: clipBehavior, - padding: control.getPadding("padding"), - height: control.getDouble("height"), - elevation: control.getDouble("elevation"), - shape: _shiftNotch(shape, margin, geometry), - shadowColor: control.getColor("shadow_color", context), - color: control.getColor("bgcolor", context), - notchMargin: control.getDouble("notch_margin", 4.0)!, - child: child, + Widget buildBar(EdgeInsets? margin, Widget? child) { + Widget bar = BottomAppBar( + clipBehavior: clipBehavior, + padding: control.getPadding("padding"), + height: control.getDouble("height"), + elevation: control.getDouble("elevation"), + shape: _shiftNotch(shape, margin, geometry), + shadowColor: control.getColor("shadow_color", context), + color: control.getColor("bgcolor", context), + notchMargin: control.getDouble("notch_margin", 4.0)!, + child: child, + ); + if (borderRadius != null && borderRadius != BorderRadius.zero) { + bar = ClipRRect( + borderRadius: borderRadius, + // can't use Clip.none here, so fallback to Clip.antiAlias in that case + clipBehavior: + clipBehavior == Clip.none ? Clip.antiAlias : clipBehavior, + child: bar, ); + } + return bar; + } // LayoutControl animates an animated margin around the bar, which moves // the bar without resizing it, so BottomAppBar keeps its notch where it // was first cut. Animating the same margin here rebuilds the notch for - // every frame of that animation. - Widget bottomAppBar = margin != null && marginAnimation != null + // every frame of that animation. The clip is built inside the builder, + // so adding or removing a border radius keeps the animation running. + final bottomAppBar = margin != null && marginAnimation != null ? TweenAnimationBuilder( tween: EdgeInsetsTween(begin: margin, end: margin), duration: marginAnimation.duration, @@ -61,15 +77,6 @@ class BottomAppBarControl extends StatelessWidget { ) : buildBar(margin, content); - if (borderRadius != null && borderRadius != BorderRadius.zero) { - bottomAppBar = ClipRRect( - borderRadius: borderRadius, - // can't use Clip.none here, so fallback to Clip.antiAlias in that case - clipBehavior: clipBehavior == Clip.none ? Clip.antiAlias : clipBehavior, - child: bottomAppBar, - ); - } - return LayoutControl(control: control, child: bottomAppBar); } } From dddb2a4bce69123fc2f845b55513d06003cbe195 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Fri, 30 Oct 2026 13:13:25 +0100 Subject: [PATCH 09/10] Test the BottomAppBar notch position with margins, animations, the page body and the theme --- .../test/widgets/bottom_app_bar_test.dart | 225 ++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 packages/flet/test/widgets/bottom_app_bar_test.dart diff --git a/packages/flet/test/widgets/bottom_app_bar_test.dart b/packages/flet/test/widgets/bottom_app_bar_test.dart new file mode 100644 index 0000000000..04ceb7fb13 --- /dev/null +++ b/packages/flet/test/widgets/bottom_app_bar_test.dart @@ -0,0 +1,225 @@ +import 'dart:typed_data'; +import 'dart:ui' as ui; + +import 'package:flet/src/controls/bottom_app_bar.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/rendering.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:material_ui/material_ui.dart'; +import 'package:provider/provider.dart'; + +const int _width = 800; +const int _height = 600; + +/// Pixels skipped at each end of the bar when looking for the notch, so +/// rounded corners don't count as part of it. +const int _cornerInset = 24; + +final FletBackend _backend = FletBackend( + pageUri: Uri.parse('http://localhost'), + assetsDir: '', + extensions: [FletCoreExtension()], + multiView: false); + +Control _bar(Map props) => Control( + id: 2, + type: 'BottomAppBar', + properties: { + 'bgcolor': '#ff0000', + 'elevation': 0, + 'shape': {'_type': 'circular'}, + ...props, + }, + backend: _backend); + +Map _margin({int left = 0, int top = 0, int right = 0}) => + {'left': left, 'top': top, 'right': right, 'bottom': 0}; + +final FloatingActionButton _fab = FloatingActionButton( + onPressed: () {}, + backgroundColor: const Color(0x00000000), + elevation: 0, + highlightElevation: 0, + focusElevation: 0, + hoverElevation: 0); + +/// Places the floating action button's top-left corner at [offset]. +class _FixedFabLocation extends FloatingActionButtonLocation { + const _FixedFabLocation(this.offset); + + final Offset offset; + + @override + Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) => offset; +} + +final GlobalKey _boundaryKey = GlobalKey(); + +Widget _app(Widget scaffold, {ThemeData? theme}) => RepaintBoundary( + key: _boundaryKey, + child: ChangeNotifierProvider.value( + value: _backend, child: MaterialApp(theme: theme, home: scaffold))); + +Widget _slotScaffold(Control bar) => Scaffold( + backgroundColor: const Color(0xFFFFFFFF), + floatingActionButton: _fab, + floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, + bottomNavigationBar: BottomAppBarControl(control: bar), + ); + +Future _setSurfaceSize(WidgetTester tester) async { + await tester.binding + .setSurfaceSize(Size(_width.toDouble(), _height.toDouble())); + addTearDown(() => tester.binding.setSurfaceSize(null)); +} + +bool _isBar(ByteData data, int x, int y) { + final i = (y * _width + x) * 4; + return data.getUint8(i) > 200 && data.getUint8(i + 1) < 60; +} + +/// Measures the notch: its center's horizontal distance from the floating +/// action button's center, and its depth below the bar's top at that center. +Future<(double, int)> _measureNotch(WidgetTester tester) async { + final barRect = tester.getRect(find.byType(BottomAppBar)); + final boundary = + _boundaryKey.currentContext!.findRenderObject() as RenderRepaintBoundary; + final data = (await tester.runAsync(() async { + final image = await boundary.toImage(); + return image.toByteData(format: ui.ImageByteFormat.rawRgba); + }))!; + final y = barRect.top.round() + 3; + final notch = [ + for (var x = barRect.left.round() + _cornerInset; + x < barRect.right.round() - _cornerInset; + x++) + if (!_isBar(data, x, y)) x, + ]; + final fabX = tester.getCenter(find.byType(FloatingActionButton)).dx.round(); + var depth = 0; + for (var yy = barRect.top.round(); yy < barRect.bottom.round(); yy++) { + if (_isBar(data, fabX, yy)) break; + depth++; + } + expect(notch, isNotEmpty, reason: 'the bar has no notch'); + return ((notch.first + notch.last + 1) / 2 - fabX, depth); +} + +/// Pumps the bar from [from] to [to] with a 2 second margin animation and +/// returns the largest notch offset seen during and after the animation. +Future _worstOffsetWhileAnimating(WidgetTester tester, + Map from, Map to) async { + await tester + .pumpWidget(_app(_slotScaffold(_bar({'animate_margin': 2000, ...from})))); + await tester.pumpAndSettle(); + await tester + .pumpWidget(_app(_slotScaffold(_bar({'animate_margin': 2000, ...to})))); + var worst = 0.0; + for (final ms in [0, 500, 500, 1000]) { + await tester.pump(Duration(milliseconds: ms)); + final (dx, _) = await _measureNotch(tester); + if (dx.abs() > worst) worst = dx.abs(); + } + await tester.pumpAndSettle(); + final (dx, _) = await _measureNotch(tester); + return dx.abs() > worst ? dx.abs() : worst; +} + +void main() { + group('BottomAppBar notch with a margin in the bottom slot', () { + for (final margin in [ + null, + _margin(left: 40, right: 40), + _margin(top: 16), + _margin(left: 80), + ]) { + testWidgets('stays under the button with margin $margin', (tester) async { + await _setSurfaceSize(tester); + await tester.pumpWidget(_app(_slotScaffold(_bar({ + if (margin != null) 'margin': margin, + })))); + await tester.pumpAndSettle(); + final (dx, depth) = await _measureNotch(tester); + // The docked button's center is on the slot's top edge, so a top + // margin makes the notch shallower by the same amount. + final top = margin?['top'] ?? 0; + expect(dx.abs(), lessThan(1.5)); + expect(depth, inInclusiveRange(31 - top, 33 - top)); + }); + } + }); + + testWidgets('BottomAppBar notch follows an animated margin', (tester) async { + await _setSurfaceSize(tester); + final worst = await _worstOffsetWhileAnimating( + tester, {'margin': _margin(right: 80)}, {'margin': _margin(left: 80)}); + expect(worst, lessThan(1.5)); + }); + + testWidgets( + 'BottomAppBar notch follows an animated margin when border_radius ' + 'changes at the same time', (tester) async { + await _setSurfaceSize(tester); + final worst = await _worstOffsetWhileAnimating( + tester, + {'margin': _margin(right: 80)}, + {'margin': _margin(left: 80), 'border_radius': 16}); + expect(worst, lessThan(1.5)); + }); + + testWidgets('BottomAppBar notch keeps its depth in the page body', + (tester) async { + await _setSurfaceSize(tester); + // A 16 px top margin, then an 80 px bar whose top is at y=520, where the + // button's center sits. + await tester.pumpWidget(_app(Scaffold( + backgroundColor: const Color(0xFFFFFFFF), + floatingActionButton: _fab, + floatingActionButtonLocation: const _FixedFabLocation(Offset(372, 492)), + body: Align( + alignment: Alignment.bottomCenter, + child: SizedBox( + width: _width.toDouble(), + child: BottomAppBarControl( + control: _bar({'height': 80, 'margin': _margin(top: 16)})), + ), + ), + ))); + await tester.pumpAndSettle(); + final (dx, depth) = await _measureNotch(tester); + expect(dx.abs(), lessThan(1.5)); + expect(depth, inInclusiveRange(31, 33)); + }); + + testWidgets('BottomAppBar notch shape from the theme follows the margin', + (tester) async { + await _setSurfaceSize(tester); + await tester.pumpWidget(_app( + _slotScaffold(_bar({ + 'shape': null, + 'margin': _margin(left: 40, right: 40), + })), + theme: ThemeData( + bottomAppBarTheme: const BottomAppBarThemeData( + shape: CircularNotchedRectangle())))); + await tester.pumpAndSettle(); + final (dx, _) = await _measureNotch(tester); + expect(dx.abs(), lessThan(1.5)); + }); + + testWidgets('BottomAppBar notch ignores a margin in skip_properties', + (tester) async { + await _setSurfaceSize(tester); + await tester.pumpWidget(_app(_slotScaffold(_bar({ + 'margin': _margin(left: 80), + '_internals': { + 'skip_properties': ['margin'], + }, + })))); + await tester.pumpAndSettle(); + final (dx, _) = await _measureNotch(tester); + expect(dx.abs(), lessThan(1.5)); + }); +} From d949e540d2c1755b571e1d1ec93e0b8adecfad33 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Fri, 30 Oct 2026 13:33:02 +0100 Subject: [PATCH 10/10] Correct the TextInputAction.NONE docstring: Android sends it as a newline, which unfocuses single-line fields --- .../packages/flet/src/flet/controls/material/textfield.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sdk/python/packages/flet/src/flet/controls/material/textfield.py b/sdk/python/packages/flet/src/flet/controls/material/textfield.py index 4718614d05..6f991e9e3a 100644 --- a/sdk/python/packages/flet/src/flet/controls/material/textfield.py +++ b/sdk/python/packages/flet/src/flet/controls/material/textfield.py @@ -240,7 +240,8 @@ class TextInputAction(Enum): """ No action specific to the field; the platform decides how the keyboard looks. - Keeps focus in the field, so the keyboard stays open. + Pressing the action button removes focus from the field, which closes the + keyboard. In a multiline field it inserts a new line instead. Note: Supported on Android only.