diff --git a/packages/flet/lib/src/controls/column.dart b/packages/flet/lib/src/controls/column.dart index e66d2e668..137ac6e80 100644 --- a/packages/flet/lib/src/controls/column.dart +++ b/packages/flet/lib/src/controls/column.dart @@ -57,6 +57,7 @@ class ColumnControl extends StatelessWidget { child = ScrollableControl( control: control, scrollDirection: wrap ? Axis.horizontal : Axis.vertical, + wrapIntoScrollableView: true, child: child, ); diff --git a/packages/flet/lib/src/controls/list_view.dart b/packages/flet/lib/src/controls/list_view.dart index b3dabf12a..823515b39 100644 --- a/packages/flet/lib/src/controls/list_view.dart +++ b/packages/flet/lib/src/controls/list_view.dart @@ -56,10 +56,11 @@ class _ListViewControlState extends State { widget.control.getBool("build_controls_on_demand", true)!; var firstItemPrototype = widget.control.getBool("first_item_prototype", false)!; - var prototypeItem = firstItemPrototype - ? widget.control.buildWidget("prototype_item") - : null; var controls = widget.control.children("controls"); + var prototypeItem = widget.control.buildWidget("prototype_item") ?? + (firstItemPrototype && controls.isNotEmpty + ? ControlWidget(control: controls.first) + : null); Widget listView = LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { @@ -79,14 +80,31 @@ class _ListViewControlState extends State { shrinkWrap: shrinkWrap, padding: padding, semanticChildCount: semanticChildCount, - itemExtent: itemExtent, - prototypeItem: prototypeItem, - children: controls - .map((item) => ControlWidget( - key: ValueKey(item.getKey("key")?.value ?? item.id), - control: item, - )) - .toList(), + itemExtent: spacing > 0 ? null : itemExtent, + prototypeItem: spacing > 0 ? null : prototypeItem, + children: () { + final childWidgets = []; + for (var index = 0; index < controls.length; index++) { + final item = controls[index]; + childWidgets.add(ControlWidget( + key: ValueKey(item.getKey("key")?.value ?? item.id), + control: item, + )); + if (spacing > 0 && index < controls.length - 1) { + childWidgets.add(horizontal + ? dividerThickness == 0 + ? SizedBox(width: spacing) + : VerticalDivider( + width: spacing, thickness: dividerThickness) + : dividerThickness == 0 + ? SizedBox(height: spacing) + : Divider( + height: spacing, + thickness: dividerThickness)); + } + } + return childWidgets; + }(), ) : spacing > 0 ? ListView.separated( diff --git a/packages/flet/lib/src/controls/row.dart b/packages/flet/lib/src/controls/row.dart index 77d15c789..71f3f3241 100644 --- a/packages/flet/lib/src/controls/row.dart +++ b/packages/flet/lib/src/controls/row.dart @@ -55,6 +55,7 @@ class RowControl extends StatelessWidget { child = ScrollableControl( control: control, scrollDirection: wrap ? Axis.vertical : Axis.horizontal, + wrapIntoScrollableView: true, child: child); if (control.getBool("on_scroll", false)!) { diff --git a/packages/flet/lib/src/controls/scrollable_control.dart b/packages/flet/lib/src/controls/scrollable_control.dart index c46ca1581..754fa8e9e 100644 --- a/packages/flet/lib/src/controls/scrollable_control.dart +++ b/packages/flet/lib/src/controls/scrollable_control.dart @@ -8,13 +8,15 @@ class ScrollableControl extends StatefulWidget { final Widget child; final Axis scrollDirection; final ScrollController? scrollController; + final bool wrapIntoScrollableView; ScrollableControl( {Key? key, required this.control, required this.child, required this.scrollDirection, - this.scrollController}) + this.scrollController, + this.wrapIntoScrollableView = false}) : super(key: key ?? ValueKey("control_${control.id}")); @override @@ -102,22 +104,26 @@ class _ScrollableControlState extends State return scrollMode != ScrollMode.none ? Scrollbar( // todo: create class ScrollBarConfiguration on Py end, for more customizability - thumbVisibility: scrollMode == ScrollMode.always || - (scrollMode == ScrollMode.adaptive && !isMobilePlatform()) - ? true - : false, - trackVisibility: scrollMode == ScrollMode.hidden ? false : null, + thumbVisibility: (scrollMode == ScrollMode.always || + (scrollMode == ScrollMode.adaptive && + !isMobilePlatform())) && + scrollMode != ScrollMode.hidden, thickness: scrollMode == ScrollMode.hidden ? 0 : isMobilePlatform() ? 4.0 : null, - //interactive: true, controller: _controller, - child: SingleChildScrollView( - controller: _controller, - scrollDirection: widget.scrollDirection, - child: widget.child, + child: ScrollConfiguration( + behavior: + ScrollConfiguration.of(context).copyWith(scrollbars: false), + child: widget.wrapIntoScrollableView + ? SingleChildScrollView( + controller: _controller, + scrollDirection: widget.scrollDirection, + child: widget.child, + ) + : widget.child, )) : widget.child; } diff --git a/packages/flet/lib/src/controls/view.dart b/packages/flet/lib/src/controls/view.dart index bb3643e22..fa59c710a 100644 --- a/packages/flet/lib/src/controls/view.dart +++ b/packages/flet/lib/src/controls/view.dart @@ -76,8 +76,6 @@ class _ViewControlState extends State { if (_popCompleter != null && !_popCompleter!.isCompleted) { _popCompleter?.complete(args["should_pop"]); } - default: - throw Exception("Unknown View method: $name"); } } @@ -130,7 +128,11 @@ class _ViewControlState extends State { .toList()); Widget child = ScrollableControl( - control: control, scrollDirection: Axis.vertical, child: column); + control: control, + scrollDirection: Axis.vertical, + wrapIntoScrollableView: true, + child: column, + ); if (control.getBool("on_scroll", false)!) { child = ScrollNotificationControl(control: control, child: child); diff --git a/sdk/python/examples/controls/dismissible/dismissable_list_tiles.py b/sdk/python/examples/controls/dismissible/dismissible_list_tiles.py similarity index 100% rename from sdk/python/examples/controls/dismissible/dismissable_list_tiles.py rename to sdk/python/examples/controls/dismissible/dismissible_list_tiles.py diff --git a/sdk/python/examples/controls/dismissible/media/dismissable_list_tiles.gif b/sdk/python/examples/controls/dismissible/media/dismissible_list_tiles.gif similarity index 100% rename from sdk/python/examples/controls/dismissible/media/dismissable_list_tiles.gif rename to sdk/python/examples/controls/dismissible/media/dismissible_list_tiles.gif diff --git a/sdk/python/examples/controls/dismissible/remove_on_dismiss_declarative.py b/sdk/python/examples/controls/dismissible/remove_on_dismiss_declarative.py new file mode 100644 index 000000000..812124f30 --- /dev/null +++ b/sdk/python/examples/controls/dismissible/remove_on_dismiss_declarative.py @@ -0,0 +1,29 @@ +import flet as ft + + +@ft.component +def App(): + items, set_items = ft.use_state(list(range(5))) + + return ft.ListView( + controls=[ + ft.Dismissible( + key=i, + content=ft.ListTile(title=ft.Text(f"Item {i}")), + dismiss_direction=ft.DismissDirection.HORIZONTAL, + background=ft.Container(bgcolor=ft.Colors.GREEN), + secondary_background=ft.Container(bgcolor=ft.Colors.RED), + on_dismiss=lambda e, index=i: set_items( + [item for item in items if item != index] + ), + dismiss_thresholds={ + ft.DismissDirection.HORIZONTAL: 0.1, + ft.DismissDirection.START_TO_END: 0.1, + }, + ) + for i in items + ], + ) + + +ft.run(lambda page: page.render(App)) diff --git a/sdk/python/packages/flet-map/src/flet_map/marker_layer.py b/sdk/python/packages/flet-map/src/flet_map/marker_layer.py index f16281af3..34133c007 100644 --- a/sdk/python/packages/flet-map/src/flet_map/marker_layer.py +++ b/sdk/python/packages/flet-map/src/flet_map/marker_layer.py @@ -28,7 +28,7 @@ class Marker(ft.Control): The coordinates of the marker. This will be the center of the marker, - if [`alignment`][(c).] is [`Alignment.CENTER`][flet.Alignment.]. + if [`alignment`][(c).] is [`Alignment.CENTER`][flet.]. """ rotate: Optional[bool] = None diff --git a/sdk/python/packages/flet/docs/controls/dismissible.md b/sdk/python/packages/flet/docs/controls/dismissible.md index 94ef8dc00..4fd4d8857 100644 --- a/sdk/python/packages/flet/docs/controls/dismissible.md +++ b/sdk/python/packages/flet/docs/controls/dismissible.md @@ -10,13 +10,39 @@ example_images: ../examples/controls/dismissible/media [Live example](https://flet-controls-gallery.fly.dev/layout/dismissible) -### Dismissable `ListTile`s +### Dismissible `ListTile`s ```python ---8<-- "{{ examples }}/dismissable_list_tiles.py" +--8<-- "{{ examples }}/dismissible_list_tiles.py" ``` -{{ image(example_images + "/dismissable_list_tiles.gif", alt="dismissable-list-tiles", width="80%") }} +{{ image(example_images + "/dismissible_list_tiles.gif", alt="dismissible-list-tiles", width="80%") }} +### Remove Dismissible `on_dismiss` inside component + +/// admonition | Important + type: warning +Always specify a key for `Dismissible` when using inside Flet component. +/// + +The issue you may encounter here is specific to the `Dismissible` control used inside Flet component (declarative UI). + +When a user swipes (dismisses) an item, that widget is marked as “dismissed” on the Flutter side and effectively removed from the UI. +However, when Flet recalculates the UI diff on the Python side, it may attempt to reuse widgets in the list based on their order rather than their identity. + +If no key is provided, Flet’s diffing algorithm can’t tell that a particular `Dismissible` corresponds to a specific item — so it assumes the items have merely shifted. +That leads to update commands like: + +> “Update text in items 0…N-1, then delete the last item (N).” + +On Flutter’s side, though, the already-dismissed `Dismissible` widget in the middle of the list can’t be updated — it’s gone — causing runtime errors. + +**Always assign a stable, unique key to each `Dismissible`, typically based on the item’s identifier or index.** + +Example: + +```python +--8<-- "{{ examples }}/remove_on_dismiss_declarative.py" +``` {{ class_members(class_name) }} diff --git a/sdk/python/packages/flet/docs/templates/python_xref/material/attribute.html.jinja b/sdk/python/packages/flet/docs/templates/python_xref/material/attribute.html.jinja index df7fbc259..d8f6824b1 100644 --- a/sdk/python/packages/flet/docs/templates/python_xref/material/attribute.html.jinja +++ b/sdk/python/packages/flet/docs/templates/python_xref/material/attribute.html.jinja @@ -18,7 +18,7 @@ Context: {{ log.debug("Rendering " + attribute.path) }} {% endblock logs %} -{% set attr_class_name = "doc-symbol-event" if attribute.name.startswith('on_') else "doc-symbol-attribute" %} +{% set attr_class_name = "doc-symbol-event" if attribute.name.startswith('on_') and (config.extra.events is not defined or (config.extra.events and attribute.name in config.extra.events)) else "doc-symbol-attribute" %}
{% with obj = attribute, html_id = attribute.path %} diff --git a/sdk/python/packages/flet/docs/templates/python_xref/material/children.html.jinja b/sdk/python/packages/flet/docs/templates/python_xref/material/children.html.jinja index 6c39abc2d..f02543a3a 100644 --- a/sdk/python/packages/flet/docs/templates/python_xref/material/children.html.jinja +++ b/sdk/python/packages/flet/docs/templates/python_xref/material/children.html.jinja @@ -53,7 +53,7 @@ Context: {% set ns = namespace(props=[], events=[]) %} {% for attribute in ordered %} {% if config.filters == "public" or members_list is not none or (not attribute.is_imported or attribute.is_public) %} - {% if attribute.name.startswith("on_") %} + {% if attribute.name.startswith('on_') and (config.extra.events is not defined or (config.extra.events and attribute.name in config.extra.events)) %} {% set _ = ns.events.append(attribute) %} {% else %} {% set _ = ns.props.append(attribute) %} diff --git a/sdk/python/packages/flet/docs/templates/python_xref/material/docstring/attributes.html.jinja b/sdk/python/packages/flet/docs/templates/python_xref/material/docstring/attributes.html.jinja index 11d4f88aa..e632774f4 100644 --- a/sdk/python/packages/flet/docs/templates/python_xref/material/docstring/attributes.html.jinja +++ b/sdk/python/packages/flet/docs/templates/python_xref/material/docstring/attributes.html.jinja @@ -4,7 +4,7 @@ {# Split attributes into events and properties #} {% set ns = namespace(events=[], props=[]) %} {% for attribute in section.value %} - {% if attribute.name.startswith('on_') %} + {% if attribute.name.startswith('on_') and (config.extra.events is not defined or (config.extra.events and attribute.name in config.extra.events)) %} {% set _ = ns.events.append(attribute) %} {% else %} {% set _ = ns.props.append(attribute) %} diff --git a/sdk/python/packages/flet/docs/types/colorscheme.md b/sdk/python/packages/flet/docs/types/colorscheme.md index 05b5b280f..c4d0428fc 100644 --- a/sdk/python/packages/flet/docs/types/colorscheme.md +++ b/sdk/python/packages/flet/docs/types/colorscheme.md @@ -1 +1 @@ -{{ class_all_options("flet.ColorScheme") }} +{{ class_all_options("flet.ColorScheme", extra={'show_class_docstring': True, 'show_children': True,'events': []}) }} diff --git a/sdk/python/packages/flet/integration_tests/controls/theme/golden/macos/color_scheme/accents_palette.png b/sdk/python/packages/flet/integration_tests/controls/theme/golden/macos/color_scheme/accents_palette.png new file mode 100644 index 000000000..9689e020c Binary files /dev/null and b/sdk/python/packages/flet/integration_tests/controls/theme/golden/macos/color_scheme/accents_palette.png differ diff --git a/sdk/python/packages/flet/integration_tests/controls/theme/golden/macos/color_scheme/buttons.png b/sdk/python/packages/flet/integration_tests/controls/theme/golden/macos/color_scheme/buttons.png new file mode 100644 index 000000000..b541719cd Binary files /dev/null and b/sdk/python/packages/flet/integration_tests/controls/theme/golden/macos/color_scheme/buttons.png differ diff --git a/sdk/python/packages/flet/integration_tests/controls/theme/golden/macos/color_scheme/error_banner.png b/sdk/python/packages/flet/integration_tests/controls/theme/golden/macos/color_scheme/error_banner.png new file mode 100644 index 000000000..098ec179b Binary files /dev/null and b/sdk/python/packages/flet/integration_tests/controls/theme/golden/macos/color_scheme/error_banner.png differ diff --git a/sdk/python/packages/flet/integration_tests/controls/theme/golden/macos/color_scheme/primary_palette.png b/sdk/python/packages/flet/integration_tests/controls/theme/golden/macos/color_scheme/primary_palette.png new file mode 100644 index 000000000..45ecae977 Binary files /dev/null and b/sdk/python/packages/flet/integration_tests/controls/theme/golden/macos/color_scheme/primary_palette.png differ diff --git a/sdk/python/packages/flet/integration_tests/controls/theme/golden/macos/color_scheme/secondary_palette.png b/sdk/python/packages/flet/integration_tests/controls/theme/golden/macos/color_scheme/secondary_palette.png new file mode 100644 index 000000000..5f6a7379f Binary files /dev/null and b/sdk/python/packages/flet/integration_tests/controls/theme/golden/macos/color_scheme/secondary_palette.png differ diff --git a/sdk/python/packages/flet/integration_tests/controls/theme/golden/macos/color_scheme/surface_roles.png b/sdk/python/packages/flet/integration_tests/controls/theme/golden/macos/color_scheme/surface_roles.png new file mode 100644 index 000000000..c366cb4a2 Binary files /dev/null and b/sdk/python/packages/flet/integration_tests/controls/theme/golden/macos/color_scheme/surface_roles.png differ diff --git a/sdk/python/packages/flet/integration_tests/controls/theme/golden/macos/color_scheme/tertiary_palette.png b/sdk/python/packages/flet/integration_tests/controls/theme/golden/macos/color_scheme/tertiary_palette.png new file mode 100644 index 000000000..c746b286e Binary files /dev/null and b/sdk/python/packages/flet/integration_tests/controls/theme/golden/macos/color_scheme/tertiary_palette.png differ diff --git a/sdk/python/packages/flet/integration_tests/controls/theme/golden/macos/color_scheme/themed_card.png b/sdk/python/packages/flet/integration_tests/controls/theme/golden/macos/color_scheme/themed_card.png new file mode 100644 index 000000000..14ad24633 Binary files /dev/null and b/sdk/python/packages/flet/integration_tests/controls/theme/golden/macos/color_scheme/themed_card.png differ diff --git a/sdk/python/packages/flet/integration_tests/controls/theme/test_color_scheme.py b/sdk/python/packages/flet/integration_tests/controls/theme/test_color_scheme.py new file mode 100644 index 000000000..3d863c1eb --- /dev/null +++ b/sdk/python/packages/flet/integration_tests/controls/theme/test_color_scheme.py @@ -0,0 +1,401 @@ +import pytest +import pytest_asyncio + +import flet as ft +import flet.testing as ftt + + +# 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_color_scheme(flet_app: ftt.FletTestApp): + flet_app.page.theme = ft.Theme( + color_scheme=ft.ColorScheme( + error=ft.Colors.RED, + error_container=ft.Colors.RED_900, + inverse_primary=ft.Colors.GREEN_900, + inverse_surface=ft.Colors.BLACK, + on_error=ft.Colors.WHITE, + on_error_container=ft.Colors.WHITE, + on_inverse_surface=ft.Colors.WHITE, + on_primary=ft.Colors.YELLOW, + on_primary_container=ft.Colors.YELLOW, + on_primary_fixed=ft.Colors.WHITE, + on_primary_fixed_variant=ft.Colors.WHITE, + on_secondary=ft.Colors.WHITE, + on_secondary_container=ft.Colors.WHITE, + on_secondary_fixed=ft.Colors.WHITE, + on_secondary_fixed_variant=ft.Colors.WHITE, + on_surface=ft.Colors.BLACK, + on_surface_variant=ft.Colors.RED, + on_tertiary=ft.Colors.WHITE, + on_tertiary_container=ft.Colors.WHITE, + on_tertiary_fixed=ft.Colors.WHITE, + on_tertiary_fixed_variant=ft.Colors.WHITE, + outline=ft.Colors.BLUE_200, + outline_variant=ft.Colors.BLUE_400, + primary=ft.Colors.GREEN, + primary_container=ft.Colors.GREEN_900, + primary_fixed=ft.Colors.GREEN_400, + primary_fixed_dim=ft.Colors.GREEN_700, + scrim=ft.Colors.BLACK, + secondary=ft.Colors.BLUE, + secondary_container=ft.Colors.BLUE_900, + secondary_fixed=ft.Colors.BLUE_400, + secondary_fixed_dim=ft.Colors.BLUE_700, + shadow=ft.Colors.BLACK, + surface=ft.Colors.ORANGE_400, + surface_bright=ft.Colors.ORANGE_200, + surface_container=ft.Colors.ORANGE, + surface_container_high=ft.Colors.ORANGE_300, + surface_container_highest=ft.Colors.ORANGE_500, + surface_container_low=ft.Colors.ORANGE_100, + surface_container_lowest=ft.Colors.ORANGE_50, + surface_dim=ft.Colors.ORANGE_600, + surface_tint=ft.Colors.GREEN, + tertiary=ft.Colors.RED, + tertiary_container=ft.Colors.RED_900, + tertiary_fixed=ft.Colors.RED_400, + tertiary_fixed_dim=ft.Colors.RED_700, + ) + ) + + flet_app.page.window.width = 500 + flet_app.page.window.height = 500 + flet_app.page.scroll = ft.ScrollMode.HIDDEN + + def swatch(label: str, fill_color: str, text_color: str) -> ft.Container: + return ft.Container( + content=ft.Column( + [ + ft.Text( + label, + size=11, + weight=ft.FontWeight.BOLD, + color=text_color, + text_align=ft.TextAlign.CENTER, + ) + ], + alignment=ft.MainAxisAlignment.CENTER, + horizontal_alignment=ft.CrossAxisAlignment.CENTER, + spacing=4, + ), + width=100, + height=72, + bgcolor=fill_color, + border_radius=ft.BorderRadius.all(12), + border=ft.Border.all(1, ft.Colors.OUTLINE_VARIANT), + padding=10, + ) + + primary_palette = ft.Screenshot( + ft.Row( + key=ft.ScrollKey("primary_palette"), + wrap=True, + controls=[ + swatch("Primary", ft.Colors.PRIMARY, ft.Colors.ON_PRIMARY), + swatch( + "Primary Ctr", + ft.Colors.PRIMARY_CONTAINER, + ft.Colors.ON_PRIMARY_CONTAINER, + ), + swatch( + "Primary Fix", ft.Colors.PRIMARY_FIXED, ft.Colors.ON_PRIMARY_FIXED + ), + swatch( + "Primary Dim", + ft.Colors.PRIMARY_FIXED_DIM, + ft.Colors.ON_PRIMARY_FIXED, + ), + ], + ) + ) + + secondary_palette = ft.Screenshot( + ft.Row( + key=ft.ScrollKey("secondary_palette"), + wrap=True, + controls=[ + swatch("Secondary", ft.Colors.SECONDARY, ft.Colors.ON_SECONDARY), + swatch( + "Secondary Ctr", + ft.Colors.SECONDARY_CONTAINER, + ft.Colors.ON_SECONDARY_CONTAINER, + ), + swatch( + "Secondary Fix", + ft.Colors.SECONDARY_FIXED, + ft.Colors.ON_SECONDARY_FIXED, + ), + swatch( + "Secondary Dim", + ft.Colors.SECONDARY_FIXED_DIM, + ft.Colors.ON_SECONDARY_FIXED, + ), + ], + ) + ) + + tertiary_palette = ft.Screenshot( + ft.Row( + key=ft.ScrollKey("tertiary_palette"), + wrap=True, + controls=[ + swatch("Tertiary", ft.Colors.TERTIARY, ft.Colors.ON_TERTIARY), + swatch( + "Tertiary Ctr", + ft.Colors.TERTIARY_CONTAINER, + ft.Colors.ON_TERTIARY_CONTAINER, + ), + swatch( + "Tertiary Fix", + ft.Colors.TERTIARY_FIXED, + ft.Colors.ON_TERTIARY_FIXED, + ), + swatch( + "Tertiary Dim", + ft.Colors.TERTIARY_FIXED_DIM, + ft.Colors.ON_TERTIARY_FIXED, + ), + ], + ) + ) + + surface_palette = ft.Screenshot( + ft.Row( + key=ft.ScrollKey("surface_roles"), + wrap=True, + controls=[ + swatch("Surface", ft.Colors.SURFACE, ft.Colors.ON_SURFACE), + swatch("Surface Br", ft.Colors.SURFACE_BRIGHT, ft.Colors.ON_SURFACE), + swatch("Surface Dim", ft.Colors.SURFACE_DIM, ft.Colors.ON_SURFACE), + swatch("Surface Tint", ft.Colors.SURFACE_TINT, ft.Colors.ON_SURFACE), + swatch("Container", ft.Colors.SURFACE_CONTAINER, ft.Colors.ON_SURFACE), + swatch( + "Ctr High", ft.Colors.SURFACE_CONTAINER_HIGH, ft.Colors.ON_SURFACE + ), + swatch( + "Ctr Highest", + ft.Colors.SURFACE_CONTAINER_HIGHEST, + ft.Colors.ON_SURFACE, + ), + swatch( + "Ctr Low", ft.Colors.SURFACE_CONTAINER_LOW, ft.Colors.ON_SURFACE + ), + swatch( + "Ctr Lowest", + ft.Colors.SURFACE_CONTAINER_LOWEST, + ft.Colors.ON_SURFACE, + ), + ], + ) + ) + + accents_palette = ft.Screenshot( + ft.Row( + key=ft.ScrollKey("accents_palette"), + wrap=True, + controls=[ + swatch( + "Inverse", ft.Colors.INVERSE_SURFACE, ft.Colors.ON_INVERSE_SURFACE + ), + swatch( + "Inverse Pri", + ft.Colors.INVERSE_PRIMARY, + ft.Colors.ON_INVERSE_SURFACE, + ), + swatch("Scrim", ft.Colors.SCRIM, ft.Colors.ON_INVERSE_SURFACE), + swatch("Outline", ft.Colors.OUTLINE, ft.Colors.ON_SURFACE_VARIANT), + swatch("Outline Var", ft.Colors.OUTLINE_VARIANT, ft.Colors.ON_SURFACE), + swatch("Error", ft.Colors.ERROR, ft.Colors.ON_ERROR), + swatch( + "Error Ctr", ft.Colors.ERROR_CONTAINER, ft.Colors.ON_ERROR_CONTAINER + ), + ], + ) + ) + + buttons = ft.Screenshot( + ft.Row( + wrap=True, + controls=[ + ft.FilledButton( + "Primary button", + style=ft.ButtonStyle( + shape=ft.RoundedRectangleBorder(radius=14), + padding=ft.Padding.symmetric(horizontal=24, vertical=12), + ), + ), + ft.FilledTonalButton( + "Tonal secondary", + style=ft.ButtonStyle( + shape=ft.RoundedRectangleBorder(radius=14), + padding=ft.Padding.symmetric(horizontal=20, vertical=12), + ), + ), + ft.OutlinedButton( + "Surface variant", + style=ft.ButtonStyle( + side=ft.BorderSide(width=2, color=ft.Colors.OUTLINE), + shape=ft.RoundedRectangleBorder(radius=14), + padding=ft.Padding.symmetric(horizontal=20, vertical=12), + ), + ), + ft.TextButton( + "Tertiary text", + style=ft.ButtonStyle( + color=ft.Colors.TERTIARY, + overlay_color=ft.Colors.TERTIARY_CONTAINER, + shape=ft.RoundedRectangleBorder(radius=14), + ), + ), + ft.IconButton( + icon=ft.Icons.FAVORITE, + icon_color=ft.Colors.ON_TERTIARY_CONTAINER, + style=ft.ButtonStyle( + bgcolor=ft.Colors.TERTIARY_CONTAINER, + shape=ft.CircleBorder(), + overlay_color=ft.Colors.TERTIARY, + ), + ), + ft.FloatingActionButton( + icon=ft.Icons.ADD, + bgcolor=ft.Colors.SECONDARY_CONTAINER, + foreground_color=ft.Colors.ON_SECONDARY_CONTAINER, + shape=ft.CircleBorder(), + ), + ], + ) + ) + + themed_card = ft.Screenshot( + ft.Card( + key=ft.ScrollKey("themed_card"), + bgcolor=ft.Colors.SURFACE_CONTAINER_HIGH, + content=ft.Container( + content=ft.Column( + [ + ft.Text( + "Card on surface container", + size=18, + weight=ft.FontWeight.BOLD, + color=ft.Colors.ON_SURFACE, + ), + ft.Text( + "Uses outline variant border and shadow color.", + color=ft.Colors.ON_SURFACE_VARIANT, + ), + ft.ListTile( + title=ft.Text("Selected list tile"), + leading=ft.Icon(ft.Icons.PALETTE, color=ft.Colors.PRIMARY), + selected=True, + bgcolor=ft.Colors.SURFACE_CONTAINER_HIGH, + selected_color=ft.Colors.ON_PRIMARY, + trailing=ft.Switch(value=True), + ), + ], + spacing=8, + alignment=ft.MainAxisAlignment.START, + horizontal_alignment=ft.CrossAxisAlignment.START, + ), + padding=10, + ), + ) + ) + + error_banner = ft.Screenshot( + ft.Container( + key=ft.ScrollKey("error_banner"), + bgcolor=ft.Colors.ERROR_CONTAINER, + border_radius=ft.BorderRadius.all(10), + padding=ft.Padding.symmetric(horizontal=16, vertical=12), + content=ft.Row( + controls=[ + ft.Icon(ft.Icons.ERROR, color=ft.Colors.ON_ERROR_CONTAINER), + ft.Text( + "Error container background with on-error text.", + color=ft.Colors.ON_ERROR_CONTAINER, + weight=ft.FontWeight.BOLD, + expand=True, + ), + ], + spacing=10, + alignment=ft.MainAxisAlignment.START, + vertical_alignment=ft.CrossAxisAlignment.CENTER, + ), + ) + ) + + flet_app.page.add( + ft.Column( + controls=[ + buttons, + primary_palette, + secondary_palette, + tertiary_palette, + surface_palette, + accents_palette, + themed_card, + error_banner, + ], + ) + ) + await flet_app.tester.pump_and_settle() + + flet_app.assert_screenshot( + "buttons", + await buttons.capture(pixel_ratio=flet_app.screenshots_pixel_ratio), + ) + + await flet_app.page.scroll_to(scroll_key="primary_palette", duration=0) + await flet_app.tester.pump_and_settle() + flet_app.assert_screenshot( + "primary_palette", + await primary_palette.capture(pixel_ratio=flet_app.screenshots_pixel_ratio), + ) + + await flet_app.page.scroll_to(scroll_key="secondary_palette", duration=0) + await flet_app.tester.pump_and_settle() + flet_app.assert_screenshot( + "secondary_palette", + await secondary_palette.capture(pixel_ratio=flet_app.screenshots_pixel_ratio), + ) + + await flet_app.page.scroll_to(scroll_key="tertiary_palette", duration=0) + await flet_app.tester.pump_and_settle() + flet_app.assert_screenshot( + "tertiary_palette", + await tertiary_palette.capture(pixel_ratio=flet_app.screenshots_pixel_ratio), + ) + + await flet_app.page.scroll_to(scroll_key="surface_roles", duration=0) + await flet_app.tester.pump_and_settle() + flet_app.assert_screenshot( + "surface_roles", + await surface_palette.capture(pixel_ratio=flet_app.screenshots_pixel_ratio), + ) + + await flet_app.page.scroll_to(scroll_key="accents_palette", duration=0) + await flet_app.tester.pump_and_settle() + flet_app.assert_screenshot( + "accents_palette", + await accents_palette.capture(pixel_ratio=flet_app.screenshots_pixel_ratio), + ) + + await flet_app.page.scroll_to(scroll_key="themed_card", duration=0) + await flet_app.tester.pump_and_settle() + flet_app.assert_screenshot( + "themed_card", + await themed_card.capture(pixel_ratio=flet_app.screenshots_pixel_ratio), + ) + + await flet_app.page.scroll_to(scroll_key="error_banner", duration=0) + await flet_app.tester.pump_and_settle() + flet_app.assert_screenshot( + "error_banner", + await error_banner.capture(pixel_ratio=flet_app.screenshots_pixel_ratio), + ) diff --git a/sdk/python/packages/flet/mkdocs.yml b/sdk/python/packages/flet/mkdocs.yml index 4b2cfa2b0..e1cdf8f65 100644 --- a/sdk/python/packages/flet/mkdocs.yml +++ b/sdk/python/packages/flet/mkdocs.yml @@ -143,7 +143,7 @@ plugins: preload_modules: [ flet, flet_ads ] filters: - "!^_" # Exclude private members starting with only one underscore - - "!(init|before_update|build|will_unmount|did_mount)" + - "!^(init|before_update|build|will_unmount|did_mount)$" extensions: - griffe_modernized_annotations - griffe_warnings_deprecated diff --git a/sdk/python/packages/flet/src/flet/controls/core/list_view.py b/sdk/python/packages/flet/src/flet/controls/core/list_view.py index 1523e6302..3edd65ac1 100644 --- a/sdk/python/packages/flet/src/flet/controls/core/list_view.py +++ b/sdk/python/packages/flet/src/flet/controls/core/list_view.py @@ -54,6 +54,10 @@ class ListView(LayoutControl, ScrollableControl, AdaptiveControl): """ A fixed height or width (when [`horizontal`][(c).] is `True`) of an item to optimize rendering. + + Note: + This property has effect only when [`build_controls_on_demand`][(c).] + is `True` or [`spacing`][(c).] is `0`. """ first_item_prototype: bool = False @@ -63,6 +67,16 @@ class ListView(LayoutControl, ScrollableControl, AdaptiveControl): i.e. their `height` or `width` will be the same as the first item. """ + prototype_item: Optional[Control] = None + """ + A control to be used as a "prototype" for all items, + i.e. their `height` or `width` will be the same as the `prototype_item`. + + Note: + This property has effect only when [`build_controls_on_demand`][(c).] + is `True` or [`spacing`][(c).] is `0`. + """ + divider_thickness: Number = 0 """ If greater than `0` then `Divider` is used as a spacing between list view items. diff --git a/sdk/python/packages/flet/src/flet/controls/object_patch.py b/sdk/python/packages/flet/src/flet/controls/object_patch.py index d455a4b88..6c3f8b9db 100644 --- a/sdk/python/packages/flet/src/flet/controls/object_patch.py +++ b/sdk/python/packages/flet/src/flet/controls/object_patch.py @@ -890,6 +890,12 @@ def _compare_dataclasses(self, parent, path, src, dst, frozen): old = change[0] new = change[1] + if field_name.startswith("on_") and fields[field_name].metadata.get( + "event", True + ): + old = old is not None + new = new is not None + logger.debug("\n\n_compare_values:changes %s %s", old, new) self._compare_values(dst, path, field_name, old, new, frozen) @@ -963,7 +969,9 @@ def _compare_dataclasses(self, parent, path, src, dst, frozen): if "skip" not in field.metadata: old = getattr(src, field.name) new = getattr(dst, field.name) - if field.name.startswith("on_"): + if field.name.startswith("on_") and field.metadata.get( + "event", True + ): old = old is not None new = new is not None self._compare_values(dst, path, field.name, old, new, frozen) @@ -1126,20 +1134,15 @@ def control_setattr(obj, name, value): if hasattr(obj, "__changes"): old_value = getattr(obj, name, None) - if name.startswith("on_"): - old_value = old_value is not None - new_value = ( - value if not name.startswith("on_") else value is not None - ) - if old_value != new_value: + if old_value != value: # logger.debug( # f"\n\nset_attr: {obj.__class__.__name__}.{name} = " # f"{new_value}, old: {old_value}" # ) changes = getattr(obj, "__changes") - changes[name] = (old_value, new_value) + changes[name] = (old_value, value) if hasattr(obj, "_notify"): - obj._notify(name, new_value) + obj._notify(name, value) object.__setattr__(obj, name, value) item.__class__.__setattr__ = control_setattr # type: ignore diff --git a/sdk/python/packages/flet/src/flet/controls/theme.py b/sdk/python/packages/flet/src/flet/controls/theme.py index f2040b106..fe992db44 100644 --- a/sdk/python/packages/flet/src/flet/controls/theme.py +++ b/sdk/python/packages/flet/src/flet/controls/theme.py @@ -75,7 +75,7 @@ class ColorScheme: The color displayed most frequently across your app's screens and components. """ - on_primary: Optional[ColorValue] = None + on_primary: Optional[ColorValue] = field(default=None, metadata={"event": False}) """ A color that's clearly legible when drawn on `primary`. """ @@ -85,7 +85,9 @@ class ColorScheme: A color used for elements needing less emphasis than `primary`. """ - on_primary_container: Optional[ColorValue] = None + on_primary_container: Optional[ColorValue] = field( + default=None, metadata={"event": False} + ) """ A color that's clearly legible when drawn on `primary_container`. """ @@ -96,7 +98,7 @@ class ColorScheme: while expanding the opportunity for color expression. """ - on_secondary: Optional[ColorValue] = None + on_secondary: Optional[ColorValue] = field(default=None, metadata={"event": False}) """ A color that's clearly legible when drawn on `secondary`. """ @@ -106,7 +108,9 @@ class ColorScheme: A color used for elements needing less emphasis than `secondary`. """ - on_secondary_container: Optional[ColorValue] = None + on_secondary_container: Optional[ColorValue] = field( + default=None, metadata={"event": False} + ) """ A color that's clearly legible when drawn on `secondary_container`. """ @@ -117,7 +121,7 @@ class ColorScheme: colors or bring heightened attention to an element, such as an input field. """ - on_tertiary: Optional[ColorValue] = None + on_tertiary: Optional[ColorValue] = field(default=None, metadata={"event": False}) """ A color that's clearly legible when drawn on `tertiary`. """ @@ -127,7 +131,9 @@ class ColorScheme: A color used for elements needing less emphasis than `tertiary`. """ - on_tertiary_container: Optional[ColorValue] = None + on_tertiary_container: Optional[ColorValue] = field( + default=None, metadata={"event": False} + ) """ A color that's clearly legible when drawn on `tertiary_container`. """ @@ -137,7 +143,7 @@ class ColorScheme: The color to use for input validation errors, e.g. for `TextField.error_text`. """ - on_error: Optional[ColorValue] = None + on_error: Optional[ColorValue] = field(default=None, metadata={"event": False}) """ A color that's clearly legible when drawn on `error`. """ @@ -147,7 +153,9 @@ class ColorScheme: A color used for error elements needing less emphasis than `error`. """ - on_error_container: Optional[ColorValue] = None + on_error_container: Optional[ColorValue] = field( + default=None, metadata={"event": False} + ) """ A color that's clearly legible when drawn on `error_container`. """ @@ -157,12 +165,14 @@ class ColorScheme: The background color for widgets like `Card`. """ - on_surface: Optional[ColorValue] = None + on_surface: Optional[ColorValue] = field(default=None, metadata={"event": False}) """ A color that's clearly legible when drawn on `surface`. """ - on_surface_variant: Optional[ColorValue] = None + on_surface_variant: Optional[ColorValue] = field( + default=None, metadata={"event": False} + ) """ A color that's clearly legible when drawn on `surface_variant`. """ @@ -194,7 +204,9 @@ class ColorScheme: UI, for example in a `SnackBar` to bring attention to an alert. """ - on_inverse_surface: Optional[ColorValue] = None + on_inverse_surface: Optional[ColorValue] = field( + default=None, metadata={"event": False} + ) """ A color that's clearly legible when drawn on `inverse_surface`. """ @@ -210,37 +222,49 @@ class ColorScheme: A color used as an overlay on a surface color to indicate a component's elevation. """ - on_primary_fixed: Optional[ColorValue] = None + on_primary_fixed: Optional[ColorValue] = field( + default=None, metadata={"event": False} + ) """ A color that is used for text and icons that exist on top of elements having `primary_fixed` color. """ - on_secondary_fixed: Optional[ColorValue] = None + on_secondary_fixed: Optional[ColorValue] = field( + default=None, metadata={"event": False} + ) """ A color that is used for text and icons that exist on top of elements having `secondary_fixed` color. """ - on_tertiary_fixed: Optional[ColorValue] = None + on_tertiary_fixed: Optional[ColorValue] = field( + default=None, metadata={"event": False} + ) """ A color that is used for text and icons that exist on top of elements having `tertiary_fixed` color. """ - on_primary_fixed_variant: Optional[ColorValue] = None + on_primary_fixed_variant: Optional[ColorValue] = field( + default=None, metadata={"event": False} + ) """ A color that provides a lower-emphasis option for text and icons than `on_primary_fixed`. """ - on_secondary_fixed_variant: Optional[ColorValue] = None + on_secondary_fixed_variant: Optional[ColorValue] = field( + default=None, metadata={"event": False} + ) """ A color that provides a lower-emphasis option for text and icons than `on_secondary_fixed`. """ - on_tertiary_fixed_variant: Optional[ColorValue] = None + on_tertiary_fixed_variant: Optional[ColorValue] = field( + default=None, metadata={"event": False} + ) """ A color that provides a lower-emphasis option for text and icons than `on_tertiary_fixed`. diff --git a/sdk/python/packages/flet/src/flet/messaging/protocol.py b/sdk/python/packages/flet/src/flet/messaging/protocol.py index e5e3baff6..adabea247 100644 --- a/sdk/python/packages/flet/src/flet/messaging/protocol.py +++ b/sdk/python/packages/flet/src/flet/messaging/protocol.py @@ -29,7 +29,7 @@ def encode_object_for_msgpack(obj): if len(v) > 0: r[field.name] = v prev_dicts[field.name] = v - elif field.name.startswith("on_"): + elif field.name.startswith("on_") and field.metadata.get("event", True): v = v is not None if v: r[field.name] = v diff --git a/sdk/python/packages/flet/tests/common.py b/sdk/python/packages/flet/tests/common.py index 508870c91..bdb88467c 100644 --- a/sdk/python/packages/flet/tests/common.py +++ b/sdk/python/packages/flet/tests/common.py @@ -15,6 +15,8 @@ @ft.control("MyText") class MyText(ft.BaseControl): value: str + color_scheme: Optional[ft.ColorScheme] = None + on_select: Optional[ft.EventHandler] = None def __str__(self): return f"{self._c}({self.value}, key={self.key} - {id(self)})" diff --git a/sdk/python/packages/flet/tests/test_object_diff_frozen.py b/sdk/python/packages/flet/tests/test_object_diff_frozen.py index 13e9fe3d4..cb41cb592 100644 --- a/sdk/python/packages/flet/tests/test_object_diff_frozen.py +++ b/sdk/python/packages/flet/tests/test_object_diff_frozen.py @@ -12,6 +12,7 @@ LineChartData, LineChartDataPoint, MyText, + b_unpack, cmp_ops, make_diff, make_msg, @@ -1253,3 +1254,52 @@ def test_list_move_11(): }, ], ) + + +def test_fields_start_with_on(): + t1 = MyText("Text 1") + t2 = MyText( + "Text 2", + color_scheme=ft.ColorScheme(on_surface_variant=ft.Colors.RED), + on_select=lambda e: print("Selected"), + ) + t1._frozen = True + + msg, _, _, _, _ = make_msg(t2, t1, show_details=False) + u_msg = b_unpack(msg) + + expected = [ + [0], + [0, 0, "value", "Text 2"], + [0, 0, "color_scheme", {"on_surface_variant": "red"}], + [0, 0, "on_select", True], + ] + + assert isinstance(u_msg, list) + assert u_msg == expected + + +def test_fields_start_with_on_update(): + t1 = MyText( + "Text 1", + color_scheme=ft.ColorScheme(on_surface_variant=ft.Colors.RED), + on_select=lambda e: print("Selected"), + ) + t2 = MyText( + "Text 2", + color_scheme=ft.ColorScheme(on_surface_variant=ft.Colors.BLUE), + ) + t1._frozen = True + + msg, _, _, _, _ = make_msg(t2, t1, show_details=False) + u_msg = b_unpack(msg) + + expected = [ + [0, {"color_scheme": [1]}], + [0, 0, "value", "Text 2"], + [0, 1, "on_surface_variant", "blue"], + [0, 0, "on_select", False], + ] + + assert isinstance(u_msg, list) + assert u_msg == expected diff --git a/sdk/python/packages/flet/tests/test_object_diff_in_place.py b/sdk/python/packages/flet/tests/test_object_diff_in_place.py index 776884e07..0ae48ac87 100644 --- a/sdk/python/packages/flet/tests/test_object_diff_in_place.py +++ b/sdk/python/packages/flet/tests/test_object_diff_in_place.py @@ -655,3 +655,37 @@ def test_list_move_1_no_keys(): }, ], ) + + +def test_fields_start_with_on(): + conn = Connection() + conn.pubsubhub = PubSubHub() + page = Page(sess=Session(conn)) + page.controls = [Div(cls="div_1", some_value="Text")] + page.on_login = lambda e: print("on login") + page.theme = ft.Theme( + color_scheme=ft.ColorScheme(on_surface_variant=ft.Colors.RED), + ) + + msg, _, _, _, _ = make_msg(page, {}, show_details=True) + u_msg = b_unpack(msg) + + print(u_msg) + + # page + p = u_msg[1][3] + # print("\n\n", p) + assert p["on_login"] + assert p["theme"]["color_scheme"]["on_surface_variant"] == "red" + + # update + page.on_login = None + page.theme.color_scheme.on_surface_variant = ft.Colors.BLUE + + msg, _, _, _, _ = make_msg(page, show_details=True) + u_msg = b_unpack(msg) + # print("\n\n", u_msg[1]) + assert u_msg[1][2] == "on_login" + assert not u_msg[1][3] + assert u_msg[2][2] == "on_surface_variant" + assert u_msg[2][3] == "blue"