diff --git a/.codex/skills/flet-validation/SKILL.md b/.codex/skills/flet-validation/SKILL.md index c8b26bfdf2..1ee4190174 100644 --- a/.codex/skills/flet-validation/SKILL.md +++ b/.codex/skills/flet-validation/SKILL.md @@ -141,6 +141,8 @@ When a property has validation, document it in that property’s docstring (goog `sdk/python/packages/flet/src/flet/utils/validation.py`. - Each `V.*` helper includes `Property docstring Raises wording`. - Keep property `Raises` entries as negations of the annotation rule. + - For strict inequalities, say `"strictly"`. + For example: `V.gt(x)` -> `ValueError: If it is not strictly greater than \`x\`.` - For sign-neutral divisibility helpers (`factor_of`, `multiple_of`), add explicit sign rules (`V.gt(0)` or `V.lt(0)`) when direction matters, and include separate `Raises` entries for those sign rules. diff --git a/CHANGELOG.md b/CHANGELOG.md index ef7c19a85d..33154db03f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ ### Bug fixes * Fix `flet build` and `flet publish` dependency parsing for `project.dependencies` and Poetry constraints with `<`/`<=`, and add coverage for normalized requirement handling ([#6332](https://github.com/flet-dev/flet/issues/6332), [#6340](https://github.com/flet-dev/flet/pull/6340)) by @td3447. +* Handle unbounded width in `ResponsiveRow` with an explicit error, treat child controls with `col=0` as hidden, and clarify `Container` expansion behavior when `alignment` is set ([#1951](https://github.com/flet-dev/flet/issues/1951), [#3805](https://github.com/flet-dev/flet/issues/3805), [#5209](https://github.com/flet-dev/flet/issues/5209), [#6354](https://github.com/flet-dev/flet/pull/6354)) by @ndonkoHenri. ### Other changes diff --git a/client/pubspec.lock b/client/pubspec.lock index 7049848f75..f865fa37eb 100644 --- a/client/pubspec.lock +++ b/client/pubspec.lock @@ -911,10 +911,10 @@ packages: dependency: transitive description: name: matcher - sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 url: "https://pub.dev" source: hosted - version: "0.12.18" + version: "0.12.19" material_color_utilities: dependency: transitive description: @@ -1628,10 +1628,10 @@ packages: dependency: transitive description: name: test_api - sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" + sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" url: "https://pub.dev" source: hosted - version: "0.7.9" + version: "0.7.10" torch_light: dependency: transitive description: diff --git a/packages/flet/CHANGELOG.md b/packages/flet/CHANGELOG.md index 9e1c06f85c..66dc292520 100644 --- a/packages/flet/CHANGELOG.md +++ b/packages/flet/CHANGELOG.md @@ -6,6 +6,8 @@ ### Bug fixes +* Handle unbounded width in `ResponsiveRow` with an explicit error and treat child controls with `col=0` as hidden at the current breakpoint ([#1951](https://github.com/flet-dev/flet/issues/1951), [#3805](https://github.com/flet-dev/flet/issues/3805), [#6354](https://github.com/flet-dev/flet/pull/6354)) by @ndonkoHenri. + ### Other changes ## 0.84.0 diff --git a/packages/flet/lib/src/controls/responsive_row.dart b/packages/flet/lib/src/controls/responsive_row.dart index e4fd0f15cb..2c224c6e6b 100644 --- a/packages/flet/lib/src/controls/responsive_row.dart +++ b/packages/flet/lib/src/controls/responsive_row.dart @@ -25,7 +25,17 @@ class ResponsiveRowControl extends StatelessWidget with FletStoreMixin { return withPageSize((context, view) { var result = LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { - // breakpoints + if (!constraints.hasBoundedWidth) { + return const ErrorControl( + "Error displaying ResponsiveRow: width is unbounded.", + description: + "Set a fixed width, a non-zero expand, or place it inside a control with bounded width.", + ); + } + + // Resolve the active breakpoint map once per layout pass. Responsive + // properties such as `columns`, `spacing`, and child `col` values are + // all interpreted against this map. final rawBreakpoints = control.get("breakpoints", view.breakpoints)!; final breakpoints = {}; @@ -45,13 +55,28 @@ class ResponsiveRowControl extends StatelessWidget with FletStoreMixin { for (var ctrl in control.children("controls")) { final col = ctrl.getResponsiveNumber("col", 12)!; var bpCol = getBreakpointNumber(col, view.size.width, breakpoints); + + // `col=0` means "do not occupy any columns" for the current + // breakpoint, so the child should not participate in layout. + if (bpCol <= 0) { + continue; + } + totalCols += bpCol; - // calculate child width + // Convert virtual columns into a fixed pixel width for this child. + // We first remove the total horizontal gaps from the available width, + // then divide the remaining width across the configured columns. var colWidth = (constraints.maxWidth - bpSpacing * (bpColumns - 1)) / bpColumns; var childWidth = colWidth * bpCol + bpSpacing * (bpCol - 1); + // Guard against tiny/invalid available widths so Flutter never sees + // negative box constraints. + if (childWidth < 0) { + childWidth = 0; + } + controls.add(ConstrainedBox( constraints: BoxConstraints(minWidth: childWidth, maxWidth: childWidth), @@ -62,6 +87,8 @@ class ResponsiveRowControl extends StatelessWidget with FletStoreMixin { var wrap = (totalCols > bpColumns); try { + // Keep a single row when everything fits; otherwise switch to Wrap so + // children can continue on the next line. return wrap ? Wrap( direction: Axis.horizontal, diff --git a/sdk/python/packages/flet/integration_tests/controls/core/golden/macos/page_view/unbounded_height.png b/sdk/python/packages/flet/integration_tests/controls/core/golden/macos/page_view/unbounded_height.png new file mode 100644 index 0000000000..a592ce77b5 Binary files /dev/null and b/sdk/python/packages/flet/integration_tests/controls/core/golden/macos/page_view/unbounded_height.png differ diff --git a/sdk/python/packages/flet/integration_tests/controls/core/golden/macos/pagelet/unbounded_height.png b/sdk/python/packages/flet/integration_tests/controls/core/golden/macos/pagelet/unbounded_height.png new file mode 100644 index 0000000000..124d2a6572 Binary files /dev/null and b/sdk/python/packages/flet/integration_tests/controls/core/golden/macos/pagelet/unbounded_height.png differ diff --git a/sdk/python/packages/flet/integration_tests/controls/core/golden/macos/responsive_row/unbounded_width.png b/sdk/python/packages/flet/integration_tests/controls/core/golden/macos/responsive_row/unbounded_width.png new file mode 100644 index 0000000000..5f8fc7f374 Binary files /dev/null and b/sdk/python/packages/flet/integration_tests/controls/core/golden/macos/responsive_row/unbounded_width.png differ diff --git a/sdk/python/packages/flet/integration_tests/controls/core/golden/macos/responsive_row/zero_col_controls_are_hidden_at_breakpoint.png b/sdk/python/packages/flet/integration_tests/controls/core/golden/macos/responsive_row/zero_col_controls_are_hidden_at_breakpoint.png new file mode 100644 index 0000000000..4c7f526994 Binary files /dev/null and b/sdk/python/packages/flet/integration_tests/controls/core/golden/macos/responsive_row/zero_col_controls_are_hidden_at_breakpoint.png differ diff --git a/sdk/python/packages/flet/integration_tests/controls/core/test_page_view.py b/sdk/python/packages/flet/integration_tests/controls/core/test_page_view.py index c4faf37e7b..f1a6e5aebb 100644 --- a/sdk/python/packages/flet/integration_tests/controls/core/test_page_view.py +++ b/sdk/python/packages/flet/integration_tests/controls/core/test_page_view.py @@ -1,10 +1,17 @@ import pytest +import pytest_asyncio import flet as ft import flet.testing as ftt -@pytest.mark.asyncio(loop_scope="module") +# Create a new flet_app instance for each test method +@pytest_asyncio.fixture(scope="function", autouse=True) +def flet_app(flet_app_function): + return flet_app_function + + +@pytest.mark.asyncio(loop_scope="function") async def test_basic(flet_app: ftt.FletTestApp, request): await flet_app.assert_control_screenshot( request.node.name, @@ -30,3 +37,29 @@ async def test_basic(flet_app: ftt.FletTestApp, request): ], ), ) + + +@pytest.mark.asyncio(loop_scope="function") +async def test_unbounded_height(flet_app: ftt.FletTestApp, request): + flet_app.page.theme_mode = ft.ThemeMode.LIGHT + await flet_app.assert_control_screenshot( + request.node.name, + ft.Column( + controls=[ + ft.PageView( + controls=[ + ft.Container( + bgcolor=ft.Colors.PURPLE, + alignment=ft.Alignment.CENTER, + content=ft.Text("One", color=ft.Colors.WHITE), + ), + ft.Container( + bgcolor=ft.Colors.TEAL, + alignment=ft.Alignment.CENTER, + content=ft.Text("Two", color=ft.Colors.WHITE), + ), + ], + ) + ] + ), + ) diff --git a/sdk/python/packages/flet/integration_tests/controls/core/test_pagelet.py b/sdk/python/packages/flet/integration_tests/controls/core/test_pagelet.py index 4e39750877..c825a7ea33 100644 --- a/sdk/python/packages/flet/integration_tests/controls/core/test_pagelet.py +++ b/sdk/python/packages/flet/integration_tests/controls/core/test_pagelet.py @@ -111,3 +111,19 @@ async def test_cupertino_adaptive(flet_app: ftt.FletTestApp, request): ), ), ) + + +@pytest.mark.asyncio(loop_scope="function") +async def test_unbounded_height(flet_app: ftt.FletTestApp, request): + flet_app.page.theme_mode = ft.ThemeMode.LIGHT + await flet_app.assert_control_screenshot( + request.node.name, + ft.Column( + controls=[ + ft.Pagelet( + appbar=ft.AppBar(title="Pagelet AppBar"), + content=ft.Text("Pagelet Content"), + ) + ] + ), + ) diff --git a/sdk/python/packages/flet/integration_tests/controls/core/test_responsive_row.py b/sdk/python/packages/flet/integration_tests/controls/core/test_responsive_row.py index c97c4b2cf3..a4272278cf 100644 --- a/sdk/python/packages/flet/integration_tests/controls/core/test_responsive_row.py +++ b/sdk/python/packages/flet/integration_tests/controls/core/test_responsive_row.py @@ -1,10 +1,17 @@ import pytest +import pytest_asyncio import flet as ft import flet.testing as ftt -@pytest.mark.asyncio(loop_scope="module") +# Create a new flet_app instance for each test method +@pytest_asyncio.fixture(scope="function", autouse=True) +def flet_app(flet_app_function): + return flet_app_function + + +@pytest.mark.asyncio(loop_scope="function") async def test_responsive_row_basic(flet_app: ftt.FletTestApp, request): await flet_app.assert_control_screenshot( request.node.name, @@ -16,3 +23,51 @@ async def test_responsive_row_basic(flet_app: ftt.FletTestApp, request): ], ), ) + + +@pytest.mark.asyncio(loop_scope="function") +async def test_unbounded_width(flet_app: ftt.FletTestApp, request): + flet_app.page.theme_mode = ft.ThemeMode.LIGHT + await flet_app.assert_control_screenshot( + request.node.name, + ft.Row( + controls=[ + ft.ResponsiveRow( + controls=[ + ft.Text("Item 1"), + ft.Text("Item 2"), + ] + ) + ] + ), + ) + + +@pytest.mark.asyncio(loop_scope="function") +async def test_zero_col_controls_are_hidden_at_breakpoint( + flet_app: ftt.FletTestApp, request +): + flet_app.resize_page(360, 240) + flet_app.page.theme_mode = ft.ThemeMode.LIGHT + await flet_app.assert_control_screenshot( + request.node.name, + ft.ResponsiveRow( + controls=[ + ft.Container( + col={"xs": 0, "xl": 2}, + bgcolor=ft.Colors.GREEN, + content=ft.Text("Left"), + ), + ft.Container( + col={"xs": 12, "xl": 8}, + bgcolor=ft.Colors.RED, + content=ft.Text("Center"), + ), + ft.Container( + col={"xs": 0, "xl": 2}, + bgcolor=ft.Colors.BLUE, + content=ft.Text("Right"), + ), + ], + ), + ) diff --git a/sdk/python/packages/flet/integration_tests/controls/material/golden/macos/navigation_rail/unbounded_height.png b/sdk/python/packages/flet/integration_tests/controls/material/golden/macos/navigation_rail/unbounded_height.png new file mode 100644 index 0000000000..38ea12f713 Binary files /dev/null and b/sdk/python/packages/flet/integration_tests/controls/material/golden/macos/navigation_rail/unbounded_height.png differ diff --git a/sdk/python/packages/flet/integration_tests/controls/material/golden/macos/tabs/unbounded_tabbarview_height.png b/sdk/python/packages/flet/integration_tests/controls/material/golden/macos/tabs/unbounded_tabbarview_height.png new file mode 100644 index 0000000000..1b69a0efda Binary files /dev/null and b/sdk/python/packages/flet/integration_tests/controls/material/golden/macos/tabs/unbounded_tabbarview_height.png differ diff --git a/sdk/python/packages/flet/integration_tests/controls/material/test_navigation_rail.py b/sdk/python/packages/flet/integration_tests/controls/material/test_navigation_rail.py index 49ea3d6252..4aa08c52ea 100644 --- a/sdk/python/packages/flet/integration_tests/controls/material/test_navigation_rail.py +++ b/sdk/python/packages/flet/integration_tests/controls/material/test_navigation_rail.py @@ -82,3 +82,32 @@ async def test_no_selected_icon(flet_app: ftt.FletTestApp, request): ), ), ) + + +@pytest.mark.asyncio(loop_scope="function") +async def test_unbounded_height(flet_app: ftt.FletTestApp, request): + flet_app.page.theme_mode = ft.ThemeMode.LIGHT + await flet_app.assert_control_screenshot( + request.node.name, + ft.Column( + controls=[ + ft.NavigationRail( + selected_index=0, + label_type=ft.NavigationRailLabelType.ALL, + min_width=100, + destinations=[ + ft.NavigationRailDestination( + icon=ft.Icons.FAVORITE_BORDER, + selected_icon=ft.Icons.FAVORITE, + label="First", + ), + ft.NavigationRailDestination( + icon=ft.Icons.SETTINGS_OUTLINED, + selected_icon=ft.Icon(ft.Icons.SETTINGS), + label=ft.Text("Settings"), + ), + ], + ) + ] + ), + ) diff --git a/sdk/python/packages/flet/integration_tests/controls/material/test_tabs.py b/sdk/python/packages/flet/integration_tests/controls/material/test_tabs.py index d8b8e011d6..c357d0446b 100644 --- a/sdk/python/packages/flet/integration_tests/controls/material/test_tabs.py +++ b/sdk/python/packages/flet/integration_tests/controls/material/test_tabs.py @@ -271,3 +271,35 @@ async def test_disabled_tabs(flet_app: ftt.FletTestApp): await flet_app.tester.pump_and_settle() assert tabs.selected_index == 1 assert clicked_indexes == [1] + + +@pytest.mark.asyncio(loop_scope="function") +async def test_unbounded_tabbarview_height(flet_app: ftt.FletTestApp, request): + flet_app.page.theme_mode = ft.ThemeMode.LIGHT + await flet_app.assert_control_screenshot( + name=request.node.name, + control=ft.Column( + controls=[ + ft.Tabs( + length=1, + content=ft.Column( + controls=[ + ft.TabBar( + tabs=[ + ft.Tab(label="Tab 1"), + ] + ), + ft.TabBarView( + controls=[ + ft.Container( + content=ft.Text("Tab 1 content"), + alignment=ft.Alignment.CENTER, + ) + ], + ), + ], + ), + ) + ] + ), + ) diff --git a/sdk/python/packages/flet/src/flet/controls/control.py b/sdk/python/packages/flet/src/flet/controls/control.py index 4ff916b888..66f56eb413 100644 --- a/sdk/python/packages/flet/src/flet/controls/control.py +++ b/sdk/python/packages/flet/src/flet/controls/control.py @@ -64,7 +64,10 @@ class Control(BaseControl): Can be a number or a dictionary configured to have a different value for specific breakpoints, for example `col={"sm": 6}`. - This control spans the 12 virtual columns by default: + A value of `0` hides the control for that breakpoint, so it does not occupy any + columns in the parent :class:`~flet.ResponsiveRow`. + + This control spans the 12 virtual columns by default. | Breakpoint | Dimension | |---|---| diff --git a/sdk/python/packages/flet/src/flet/controls/core/responsive_row.py b/sdk/python/packages/flet/src/flet/controls/core/responsive_row.py index cca858e4d2..0d0a07e0ba 100644 --- a/sdk/python/packages/flet/src/flet/controls/core/responsive_row.py +++ b/sdk/python/packages/flet/src/flet/controls/core/responsive_row.py @@ -12,6 +12,7 @@ ResponsiveNumber, ResponsiveRowBreakpoint, ) +from flet.utils.validation import V, ValidationRules __all__ = ["ResponsiveNumber", "ResponsiveRow", "ResponsiveRowBreakpoint"] @@ -44,7 +45,6 @@ class ResponsiveRow(LayoutControl, AdaptiveControl): ], ) ``` - """ controls: list[Control] = field(default_factory=list) @@ -52,9 +52,24 @@ class ResponsiveRow(LayoutControl, AdaptiveControl): A list of Controls to display. """ + __validation_rules__: ValidationRules = ( + V.ensure( + lambda ctrl: ( + ctrl.columns > 0 + if isinstance(ctrl.columns, (int, float)) + else all(v > 0 for v in ctrl.columns.values()) + ), + message="columns must be greater than 0 for all breakpoints", + ), + ) + columns: ResponsiveNumber = 12 """ The number of virtual columns to layout children. + + Raises: + ValueError: If it is not strictly greater than `0`. + ValueError: If any breakpoint-specific value is not strictly greater than `0`. """ alignment: MainAxisAlignment = MainAxisAlignment.START diff --git a/sdk/python/packages/flet/src/flet/controls/material/container.py b/sdk/python/packages/flet/src/flet/controls/material/container.py index 6163e38c06..8ebb3a02de 100644 --- a/sdk/python/packages/flet/src/flet/controls/material/container.py +++ b/sdk/python/packages/flet/src/flet/controls/material/container.py @@ -54,7 +54,15 @@ class Container(LayoutControl, AdaptiveControl): alignment: Optional[Alignment] = None """ - Defines the alignment of the :attr:`content` inside the container. + Defines the alignment of the :attr:`content` inside this container. + + Note: + If `alignment` is non-`None`, this container may expand to fill the + available space from its parent (before positioning its :attr:`content` + within itself according to the given `alignment`) instead of shrinking to its + :attr:`content`. If you need this container to keep a fixed size, give it + container an explicit `width` and/or `height` values, or constrain it via + its parent. """ bgcolor: Optional[ColorValue] = None diff --git a/website/docs/cookbook/logging.md b/website/docs/cookbook/logging.md index b3d63d05c8..a6f88f4553 100644 --- a/website/docs/cookbook/logging.md +++ b/website/docs/cookbook/logging.md @@ -7,23 +7,68 @@ You may need to enable detailed logging to troubleshoot Flet library or when sub ## Python -Flet Python modules expose named loggers: `flet_core` and `flet`. +Flet Python uses the following named loggers: -To enable detailed/verbose Flet logging in your program add this code before calling `ft.run()`: +- `flet` - general framework and transport logging. +- `flet_object_patch` - detailed control tree diff/patch logging. +- `flet_components` - declarative component lifecycle logging. + +For normal use, configure logging before calling `ft.run()`: ```python import logging + +logging.basicConfig(level=logging.INFO) +``` + +This gives you the usual `flet` logs without too much noise. + +To see more detail from the main `flet` logger, either raise the root logging level: + +```python +import logging + logging.basicConfig(level=logging.DEBUG) ``` -This will enable loggers across all Flet modules (`flet_core` and `flet`). +or set the `flet` logger explicitly: + +```python +import logging + +logging.basicConfig(level=logging.INFO) +logging.getLogger("flet").setLevel(logging.DEBUG) +``` + +`flet_object_patch` and `flet_components` set their own level to `INFO`, so +they do not inherit the root `DEBUG` level unless you enable them explicitly. -To reduce verbosity you may suppress logging messages from `flet_core` module, but adding: +To enable the most verbose diagnostics, set the corresponding loggers to `DEBUG`: ```python -logging.getLogger("flet_core").setLevel(logging.INFO) +import logging + +logging.basicConfig(level=logging.DEBUG) +logging.getLogger("flet_object_patch").setLevel(logging.DEBUG) +logging.getLogger("flet_components").setLevel(logging.DEBUG) ``` -Debug logging is usually needed for troubleshooting purposes, when submitting a new Flet issue. +Use this level mostly for troubleshooting and issue reports. + +## Built-in Web Server + +When running a web app, Flet starts its built-in web transport on top of +FastAPI and Uvicorn. + +The effective level of the `flet` Python logger is passed to the built-in web +server, so configure `flet` logging before starting the app: + +```python +import logging + +logging.basicConfig(level=logging.INFO) +logging.getLogger("flet").setLevel(logging.DEBUG) +``` -In the most cases you should be fine with `INFO` logging level. +If you host a Flet ASGI app with your own server process, such as `uvicorn` or +`gunicorn`, configure that server's logging separately using its own options. diff --git a/website/docs/publish/macos.md b/website/docs/publish/macos.md index c1e0cdd552..f50483b8e0 100644 --- a/website/docs/publish/macos.md +++ b/website/docs/publish/macos.md @@ -233,7 +233,7 @@ Its value is determined in the following order of precedence: 3. Values injected by [cross-platform permission bundles](index.md#permissions), if any. 4. Defaults: - ```toml + ```toml [tool.flet.macos.entitlement] "com.apple.security.app-sandbox" = false "com.apple.security.cs.allow-jit" = true