diff --git a/.github/workflows/macos-integration-tests.yml b/.github/workflows/macos-integration-tests.yml index c7e3602904..14b2a9ea88 100644 --- a/.github/workflows/macos-integration-tests.yml +++ b/.github/workflows/macos-integration-tests.yml @@ -50,7 +50,7 @@ jobs: - controls/material - controls/services - controls/theme - - controls/types + # - controls/types - extensions name: ${{ matrix.suite }} Integration Tests diff --git a/packages/flet/lib/flet.dart b/packages/flet/lib/flet.dart index 8e551fd7ef..437ab43026 100644 --- a/packages/flet/lib/flet.dart +++ b/packages/flet/lib/flet.dart @@ -60,6 +60,7 @@ export 'src/utils/platform.dart'; export 'src/utils/platform_utils_web.dart' if (dart.library.io) "src/utils/platform_utils_non_web.dart"; export 'src/utils/responsive.dart'; +export 'src/utils/scrollbar.dart'; export 'src/utils/strings.dart'; export 'src/utils/text.dart'; export 'src/utils/textfield.dart'; diff --git a/packages/flet/lib/src/controls/scrollable_control.dart b/packages/flet/lib/src/controls/scrollable_control.dart index aa935deea3..395993a5a0 100644 --- a/packages/flet/lib/src/controls/scrollable_control.dart +++ b/packages/flet/lib/src/controls/scrollable_control.dart @@ -3,9 +3,8 @@ import 'package:flutter/material.dart'; import '../models/control.dart'; import '../utils/animations.dart'; import '../utils/keys.dart'; -import '../utils/misc.dart'; import '../utils/numbers.dart'; -import '../utils/platform.dart'; +import '../utils/scrollbar.dart'; import '../utils/time.dart'; import '../widgets/flet_store_mixin.dart'; @@ -95,10 +94,11 @@ class _ScrollableControlState extends State @override Widget build(BuildContext context) { debugPrint("ScrollableControl build: ${widget.control.id}"); - ScrollMode scrollMode = - widget.control.getScrollMode("scroll", ScrollMode.none)!; + final scrollConfiguration = + widget.control.getScrollbarConfiguration("scroll"); - if (widget.control.getBool("auto_scroll", false)!) { + if (widget.control.getBool("auto_scroll", false)! && + scrollConfiguration != null) { WidgetsBinding.instance.addPostFrameCallback((_) { _controller.animateTo( _controller.position.maxScrollExtent, @@ -107,30 +107,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())) && - scrollMode != ScrollMode.hidden, - thickness: scrollMode == ScrollMode.hidden - ? 0 - : isMobilePlatform() - ? 4.0 - : null, - controller: _controller, - 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; + + if (scrollConfiguration == null) return widget.child; + + return Scrollbar( + thumbVisibility: scrollConfiguration.thumbVisibility, + trackVisibility: scrollConfiguration.trackVisibility, + thickness: scrollConfiguration.thickness, + radius: scrollConfiguration.radius, + interactive: scrollConfiguration.interactive, + scrollbarOrientation: scrollConfiguration.orientation, + controller: _controller, + child: ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false), + child: widget.wrapIntoScrollableView + ? SingleChildScrollView( + controller: _controller, + scrollDirection: widget.scrollDirection, + child: widget.child, + ) + : widget.child, + )); } } diff --git a/packages/flet/lib/src/utils/misc.dart b/packages/flet/lib/src/utils/misc.dart index e02242ab32..f6ce430ffa 100644 --- a/packages/flet/lib/src/utils/misc.dart +++ b/packages/flet/lib/src/utils/misc.dart @@ -66,13 +66,6 @@ CardVariant? parseCardVariant(String? value, [CardVariant? defaultValue]) { return parseEnum(CardVariant.values, value, defaultValue); } -enum ScrollMode { none, auto, adaptive, always, hidden } - -ScrollMode? parseScrollMode(String? value, - [ScrollMode? defaultValue = ScrollMode.none]) { - return parseEnum(ScrollMode.values, value, defaultValue); -} - enum LabelPosition { right, left } LabelPosition? parseLabelPosition(String? value, @@ -181,11 +174,6 @@ extension MiscParsers on Control { return parseCardVariant(get(propertyName), defaultValue); } - ScrollMode? getScrollMode(String propertyName, - [ScrollMode? defaultValue = ScrollMode.none]) { - return parseScrollMode(get(propertyName), defaultValue); - } - LabelPosition? getLabelPosition(String propertyName, [LabelPosition? defaultValue]) { return parseLabelPosition(get(propertyName), defaultValue); diff --git a/packages/flet/lib/src/utils/scrollbar.dart b/packages/flet/lib/src/utils/scrollbar.dart new file mode 100644 index 0000000000..48a8a83bf9 --- /dev/null +++ b/packages/flet/lib/src/utils/scrollbar.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; + +import '../models/control.dart'; +import 'borders.dart'; +import 'enums.dart'; +import 'numbers.dart'; +import 'platform.dart'; + +enum ScrollMode { auto, adaptive, always, hidden } + +ScrollMode? parseScrollMode(String? value, [ScrollMode? defaultValue]) { + return parseEnum(ScrollMode.values, value, defaultValue); +} + +class ScrollbarConfiguration { + final bool? thumbVisibility; + final bool? trackVisibility; + final double? thickness; + final Radius? radius; + final bool? interactive; + final ScrollbarOrientation? orientation; + + const ScrollbarConfiguration({ + this.thumbVisibility, + this.trackVisibility, + this.thickness, + this.radius, + this.interactive, + this.orientation, + }); + + factory ScrollbarConfiguration.fromScrollMode(ScrollMode mode) { + final defaultThickness = isMobilePlatform() ? 4.0 : null; + + switch (mode) { + case ScrollMode.auto: + return ScrollbarConfiguration(thickness: defaultThickness); + case ScrollMode.adaptive: + return ScrollbarConfiguration( + thumbVisibility: !isMobilePlatform(), thickness: defaultThickness); + case ScrollMode.always: + return ScrollbarConfiguration( + thumbVisibility: true, thickness: defaultThickness); + case ScrollMode.hidden: + return const ScrollbarConfiguration(thickness: 0); + } + } +} + +ScrollbarOrientation? parseScrollbarOrientation(String? value, + [ScrollbarOrientation? defaultValue]) { + return parseEnum(ScrollbarOrientation.values, value, defaultValue); +} + +ScrollbarConfiguration? parseScrollbarConfiguration(dynamic value, + [ScrollbarConfiguration? defaultValue]) { + if (value == null) return defaultValue; + if (value is! Map) { + final mode = parseScrollMode(value); + return mode == null + ? defaultValue + : ScrollbarConfiguration.fromScrollMode(mode); + } + + final baseConfiguration = ScrollbarConfiguration.fromScrollMode( + parseScrollMode(value["mode"], ScrollMode.auto)!); + + return ScrollbarConfiguration( + thumbVisibility: + parseBool(value["thumb_visibility"], baseConfiguration.thumbVisibility), + trackVisibility: + parseBool(value["track_visibility"], baseConfiguration.trackVisibility), + thickness: parseDouble(value["thickness"], baseConfiguration.thickness), + radius: parseRadius(value["radius"], baseConfiguration.radius), + interactive: parseBool(value["interactive"], baseConfiguration.interactive), + orientation: parseScrollbarOrientation( + value["orientation"], baseConfiguration.orientation), + ); +} + +extension ScrollbarParsers on Control { + ScrollMode? getScrollMode(String propertyName, [ScrollMode? defaultValue]) { + return parseScrollMode(get(propertyName), defaultValue); + } + + ScrollbarConfiguration? getScrollbarConfiguration(String propertyName, + [ScrollbarConfiguration? defaultValue]) { + return parseScrollbarConfiguration(get(propertyName), defaultValue); + } +} diff --git a/sdk/python/examples/controls/types/scroll_bar/showcase.py b/sdk/python/examples/controls/types/scroll_bar/showcase.py new file mode 100644 index 0000000000..420047cad1 --- /dev/null +++ b/sdk/python/examples/controls/types/scroll_bar/showcase.py @@ -0,0 +1,189 @@ +from typing import Optional + +import flet as ft + + +def main(page: ft.Page): + page.horizontal_alignment = ft.CrossAxisAlignment.CENTER + page.appbar = ft.AppBar(title="Scrollbar Dataclass Showcase") + + def get_scrollbar() -> ft.Scrollbar: + def str_as_bool(value: Optional[str]) -> Optional[bool]: + return True if value == "true" else False if value == "false" else None + + return ft.Scrollbar( + thumb_visibility=str_as_bool(thumb_visibility.value), + track_visibility=str_as_bool(track_visibility.value), + interactive=str_as_bool(interactive.value), + thickness=thickness_value.value if use_thickness.value else None, + radius=radius_value.value if use_radius.value else None, + orientation=None + if orientation.value == "none" + else ft.ScrollbarOrientation(orientation.value), + ) + + def get_preview_content(scrollbar: ft.Scrollbar) -> tuple[str, ft.Control]: + selected_orientation = scrollbar.orientation + is_horizontal = selected_orientation in ( + ft.ScrollbarOrientation.TOP, + ft.ScrollbarOrientation.BOTTOM, + ) + if is_horizontal: + return ( + "Horizontal preview (TOP/BOTTOM orientation)", + ft.Row( + spacing=8, + scroll=scrollbar, + controls=[ + ft.Container( + width=110, + height=80, + border=ft.Border.all(1, ft.Colors.OUTLINE_VARIANT), + border_radius=8, + bgcolor=ft.Colors.SURFACE_CONTAINER_HIGHEST, + alignment=ft.Alignment.CENTER, + content=ft.Text(f"Tile {i + 1}"), + ) + for i in range(18) + ], + ), + ) + + return ( + "Vertical preview (None/LEFT/RIGHT orientation)", + ft.Column( + spacing=4, + scroll=scrollbar, + controls=[ft.Text(f"Item {i + 1}") for i in range(40)], + ), + ) + + def update_preview(): + thickness_value.disabled = not use_thickness.value + radius_value.disabled = not use_radius.value + + scrollbar = get_scrollbar() + title, content = get_preview_content(scrollbar) + preview_title.value = title + preview_viewport.content = content + page.update() + + page.add( + ft.Text( + "Interactive playground for the Scrollbar dataclass. Change each property " + "and inspect the live result." + ), + ft.Row( + wrap=True, + spacing=12, + run_spacing=12, + alignment=ft.MainAxisAlignment.CENTER, + controls=[ + ft.Container( + width=360, + padding=12, + border=ft.Border.all(1, ft.Colors.OUTLINE_VARIANT), + border_radius=10, + bgcolor=ft.Colors.SURFACE_CONTAINER_LOW, + content=ft.Column( + spacing=10, + controls=[ + ft.Text("Configuration", weight=ft.FontWeight.BOLD), + thumb_visibility := ft.Dropdown( + label="thumb_visibility", + value="none", + on_select=update_preview, + options=[ + ft.DropdownOption("none", "None (theme default)"), + ft.DropdownOption("true", "True"), + ft.DropdownOption("false", "False"), + ], + ), + track_visibility := ft.Dropdown( + label="track_visibility", + value="none", + on_select=update_preview, + options=[ + ft.DropdownOption("none", "None (theme default)"), + ft.DropdownOption("true", "True"), + ft.DropdownOption("false", "False"), + ], + ), + interactive := ft.Dropdown( + label="interactive", + value="none", + on_select=update_preview, + options=[ + ft.DropdownOption( + "none", "None (platform default)" + ), + ft.DropdownOption("true", "True"), + ft.DropdownOption("false", "False"), + ], + ), + orientation := ft.Dropdown( + label="orientation", + value="none", + options=[ft.DropdownOption("none", "None (auto side)")] + + [ + ft.DropdownOption(o.value, o.name) + for o in ft.ScrollbarOrientation + ], + ), + use_thickness := ft.Checkbox( + label="Set thickness", + value=False, + on_change=update_preview, + ), + thickness_value := ft.Slider( + min=0, + max=20, + divisions=20, + value=8, + label="{value}", + on_change=update_preview, + ), + use_radius := ft.Checkbox( + label="Set radius", + value=False, + on_change=update_preview, + ), + radius_value := ft.Slider( + min=0, + max=20, + divisions=20, + value=8, + label="{value}", + on_change=update_preview, + ), + ], + ), + ), + ft.Container( + width=420, + padding=12, + border=ft.Border.all(1, ft.Colors.OUTLINE_VARIANT), + border_radius=10, + bgcolor=ft.Colors.SURFACE_CONTAINER_LOW, + content=ft.Column( + spacing=10, + controls=[ + ft.Text("Live preview", weight=ft.FontWeight.BOLD), + preview_title := ft.Text(weight=ft.FontWeight.BOLD), + preview_viewport := ft.Container( + height=260, + border=ft.Border.all(1, ft.Colors.OUTLINE), + border_radius=8, + padding=8, + ), + ], + ), + ), + ], + ), + ) + + update_preview() + + +ft.run(main) diff --git a/sdk/python/examples/controls/types/scroll_bar_orientation/showcase.py b/sdk/python/examples/controls/types/scroll_bar_orientation/showcase.py new file mode 100644 index 0000000000..ea8c463407 --- /dev/null +++ b/sdk/python/examples/controls/types/scroll_bar_orientation/showcase.py @@ -0,0 +1,91 @@ +import flet as ft + + +def showcase_card(orientation: ft.ScrollbarOrientation) -> ft.Container: + is_vertical = orientation in ( + ft.ScrollbarOrientation.LEFT, + ft.ScrollbarOrientation.RIGHT, + ) + scrollbar = ft.Scrollbar( + orientation=orientation, + thumb_visibility=True, + track_visibility=True, + thickness=10, + radius=8, + ) + + if is_vertical: + viewport = ft.Container( + height=220, + border=ft.Border.all(1, ft.Colors.OUTLINE), + border_radius=8, + padding=8, + content=ft.Column( + spacing=4, + scroll=scrollbar, + controls=[ft.Text(f"Item {i + 1}") for i in range(35)], + ), + ) + else: + viewport = ft.Container( + height=130, + border=ft.Border.all(1, ft.Colors.OUTLINE), + border_radius=8, + padding=8, + content=ft.Row( + spacing=8, + scroll=scrollbar, + controls=[ + ft.Container( + width=84, + height=72, + border=ft.Border.all(1, ft.Colors.OUTLINE_VARIANT), + border_radius=8, + bgcolor=ft.Colors.SURFACE_CONTAINER_HIGHEST, + alignment=ft.Alignment.CENTER, + content=ft.Text(f"{i + 1}"), + ) + for i in range(20) + ], + ), + ) + + return ft.Container( + width=330, + padding=12, + border=ft.Border.all(1, ft.Colors.OUTLINE_VARIANT), + border_radius=10, + bgcolor=ft.Colors.SURFACE_CONTAINER_LOW, + content=ft.Column( + spacing=8, + controls=[ + ft.Text(orientation.name, weight=ft.FontWeight.BOLD), + viewport, + ], + ), + ) + + +def main(page: ft.Page): + page.horizontal_alignment = ft.CrossAxisAlignment.CENTER + + page.appbar = ft.AppBar(title="ScrollbarOrientation Showcase") + page.add( + ft.Text( + "LEFT/RIGHT apply to vertical scrollables, TOP/BOTTOM apply to " + "horizontal scrollables.", + ), + ft.Row( + wrap=True, + spacing=12, + run_spacing=12, + scroll=ft.ScrollMode.AUTO, + alignment=ft.MainAxisAlignment.CENTER, + controls=[ + showcase_card(orientation) for orientation in ft.ScrollbarOrientation + ], + ), + ) + + +ft.run(main) diff --git a/sdk/python/packages/flet/docs/types/scrollbar.md b/sdk/python/packages/flet/docs/types/scrollbar.md new file mode 100644 index 0000000000..283521427a --- /dev/null +++ b/sdk/python/packages/flet/docs/types/scrollbar.md @@ -0,0 +1,16 @@ +--- +class_name: flet.Scrollbar +examples: ../../examples/controls/types/scroll_bar +--- + +{{ class_summary(class_name) }} + +## Examples + +### Showcase + +```python +--8<-- "{{ examples }}/showcase.py" +``` + +{{ class_members(class_name) }} diff --git a/sdk/python/packages/flet/docs/types/scrollbarorientation.md b/sdk/python/packages/flet/docs/types/scrollbarorientation.md new file mode 100644 index 0000000000..13960d5fec --- /dev/null +++ b/sdk/python/packages/flet/docs/types/scrollbarorientation.md @@ -0,0 +1,16 @@ +--- +class_name: flet.ScrollbarOrientation +examples: ../../examples/controls/types/scroll_bar_orientation +--- + +{{ class_summary(class_name) }} + +## Examples + +### Showcase + +```python +--8<-- "{{ examples }}/showcase.py" +``` + +{{ class_members(class_name, separate_signature=False) }} diff --git a/sdk/python/packages/flet/integration_tests/controls/types/golden/macos/align/align_inside_container.png b/sdk/python/packages/flet/integration_tests/controls/types/golden/macos/align/align_inside_container.png deleted file mode 100644 index 9e5205da18..0000000000 Binary files a/sdk/python/packages/flet/integration_tests/controls/types/golden/macos/align/align_inside_container.png and /dev/null differ diff --git a/sdk/python/packages/flet/integration_tests/controls/types/golden/macos/align/align_inside_stack.png b/sdk/python/packages/flet/integration_tests/controls/types/golden/macos/align/align_inside_stack.png deleted file mode 100644 index 892b24e802..0000000000 Binary files a/sdk/python/packages/flet/integration_tests/controls/types/golden/macos/align/align_inside_stack.png and /dev/null differ diff --git a/sdk/python/packages/flet/integration_tests/controls/types/golden/macos/margin/margin_around.png b/sdk/python/packages/flet/integration_tests/controls/types/golden/macos/margin/margin_around.png deleted file mode 100644 index f9619b83a8..0000000000 Binary files a/sdk/python/packages/flet/integration_tests/controls/types/golden/macos/margin/margin_around.png and /dev/null differ diff --git a/sdk/python/packages/flet/integration_tests/controls/types/golden/macos/margin/margin_bottom_right.png b/sdk/python/packages/flet/integration_tests/controls/types/golden/macos/margin/margin_bottom_right.png deleted file mode 100644 index b660d2bf66..0000000000 Binary files a/sdk/python/packages/flet/integration_tests/controls/types/golden/macos/margin/margin_bottom_right.png and /dev/null differ diff --git a/sdk/python/packages/flet/mkdocs.yml b/sdk/python/packages/flet/mkdocs.yml index ece12d4578..41e958ec23 100644 --- a/sdk/python/packages/flet/mkdocs.yml +++ b/sdk/python/packages/flet/mkdocs.yml @@ -875,6 +875,8 @@ nav: - PointMode: types/pointmode.md - PopupMenuPosition: types/popupmenuposition.md - RouteUrlStrategy: types/routeurlstrategy.md + - Scrollbar: types/scrollbar.md + - ScrollbarOrientation: types/scrollbarorientation.md - ScrollDirection: types/scrolldirection.md - ScrollMode: types/scrollmode.md - ScrollType: types/scrolltype.md diff --git a/sdk/python/packages/flet/src/flet/__init__.py b/sdk/python/packages/flet/src/flet/__init__.py index 9304f6a8b7..ca8e65399e 100644 --- a/sdk/python/packages/flet/src/flet/__init__.py +++ b/sdk/python/packages/flet/src/flet/__init__.py @@ -419,6 +419,8 @@ from flet.controls.scrollable_control import ( OnScrollEvent, ScrollableControl, + Scrollbar, + ScrollbarOrientation, ScrollDirection, ScrollType, ) @@ -962,6 +964,8 @@ "ScrollMode", "ScrollType", "ScrollableControl", + "Scrollbar", + "ScrollbarOrientation", "ScrollbarTheme", "SearchBar", "SearchBarTheme", diff --git a/sdk/python/packages/flet/src/flet/controls/base_page.py b/sdk/python/packages/flet/src/flet/controls/base_page.py index fbebc84b3b..e76b550ce3 100644 --- a/sdk/python/packages/flet/src/flet/controls/base_page.py +++ b/sdk/python/packages/flet/src/flet/controls/base_page.py @@ -27,6 +27,7 @@ from flet.controls.material.navigation_bar import NavigationBar from flet.controls.material.navigation_drawer import NavigationDrawer from flet.controls.padding import Padding, PaddingValue +from flet.controls.scrollable_control import Scrollbar from flet.controls.services.service import Service from flet.controls.transform import OffsetValue from flet.controls.types import ( @@ -702,7 +703,7 @@ def bgcolor(self, value: Optional[ColorValue]): # scroll @property - def scroll(self) -> Optional[ScrollMode]: + def scroll(self) -> Optional[Union[ScrollMode, Scrollbar]]: """ Scroll behavior mode for root view content. """ @@ -710,7 +711,7 @@ def scroll(self) -> Optional[ScrollMode]: return self.__root_view().scroll @scroll.setter - def scroll(self, value: Optional[ScrollMode]): + def scroll(self, value: Optional[Union[ScrollMode, Scrollbar]]): self.__root_view().scroll = value # auto_scroll diff --git a/sdk/python/packages/flet/src/flet/controls/scrollable_control.py b/sdk/python/packages/flet/src/flet/controls/scrollable_control.py index 65d72350b1..f3177fba10 100644 --- a/sdk/python/packages/flet/src/flet/controls/scrollable_control.py +++ b/sdk/python/packages/flet/src/flet/controls/scrollable_control.py @@ -13,7 +13,14 @@ ScrollMode, ) -__all__ = ["OnScrollEvent", "ScrollDirection", "ScrollType", "ScrollableControl"] +__all__ = [ + "OnScrollEvent", + "ScrollDirection", + "ScrollType", + "ScrollableControl", + "Scrollbar", + "ScrollbarOrientation", +] class ScrollType(Enum): @@ -78,6 +85,111 @@ class ScrollDirection(Enum): """ +class ScrollbarOrientation(Enum): + """ + Defines the edge/side of the viewport where the [`Scrollbar`][flet.] is shown. + """ + + LEFT = "left" + """ + Places the scrollbar on the left/leading edge of a vertical scrollable. + """ + + RIGHT = "right" + """ + Places the scrollbar on the right/trailing edge of a vertical scrollable. + """ + + TOP = "top" + """ + Places the scrollbar above a horizontal scrollable. + """ + + BOTTOM = "bottom" + """ + Places the scrollbar below a horizontal scrollable. + """ + + +@dataclass +class Scrollbar: + """ + Configures the scrollbar that scrollable controls render for their content. + """ + + thumb_visibility: Optional[bool] = None + """ + Whether this scrollbar's thumb should be always be visible, even when not being + scrolled. When `False`, the scrollbar will be shown during scrolling and + will fade out otherwise. + + If `None`, then [`ScrollbarTheme.thumb_visibility`][flet.] is used. + If that is also `None`, defaults to `False`. + """ + + track_visibility: Optional[bool] = None + """ + Indicates whether the scrollbar track should be visible, + so long as the [thumb][(c).thumb_visibility] is visible. + + If `None`, then [`ScrollbarTheme.track_visibility`][flet.] is used. + If that is also `None`, defaults to `False`. + """ + + thickness: Optional[Number] = None + """ + Controls the cross-axis size of the scrollbar in logical pixels. + The thickness of the scrollbar in the cross axis of the scrollable. + + If `None`, the default value is platform dependent: + `4.0` pixels on Android + ([`Page.platform`][flet.] == [`PagePlatform.ANDROID`][flet.]); + `3.0` pixels on iOS ([`Page.platform`][flet.] == [`PagePlatform.IOS`][flet.]); + `8.0` pixels on the remaining platforms. + """ + + radius: Optional[Number] = None + """ + Circular radius of the scrollbar thumb's rounded rectangle corners in logical + pixels. If `None`, platform defaults are used. + + The radius of the scrollbar thumb's rounded rectangle corners. + + If `None`, the default value is platform dependent: + no radius is applied on Android + ([`Page.platform`][flet.] == [`PagePlatform.ANDROID`][flet.]); + `1.5` pixels on iOS ([`Page.platform`][flet.] == [`PagePlatform.IOS`][flet.]); + `8.0` pixels on the remaining platforms. + """ + + interactive: Optional[bool] = None + """ + Whether this scroll bar should be interactive and respond to dragging on the + thumb, or tapping in the track area. + + When `False`, the scrollbar will not respond to gesture or hover events, and will + allow to click through it. + + If `None`, defaults to `True`, unless on Android, where it defaults to `False`. + """ + + orientation: Optional[ScrollbarOrientation] = None + """ + Specifies where the scrollbar should appear relative to the scrollable. + + If `None`, for a vertical scroll, defaults to [`ScrollbarOrientation.RIGHT`][flet.] + for left-to-right text direction and [`ScrollbarOrientation.LEFT`][flet.] + for right-to-left text direction, while for a horizontal scroll, it defaults to + [`ScrollbarOrientation.BOTTOM`][flet.]. + + Note: + [`ScrollbarOrientation.TOP`][flet.] and [`ScrollbarOrientation.BOTTOM`][flet.] + can only be used with a vertical scroll; [`ScrollbarOrientation.LEFT`][flet.] + and [`ScrollbarOrientation.RIGHT`][flet.] can only be used with a horizontal + scroll. + """ + + @dataclass class OnScrollEvent(Event["ScrollableControl"]): """ @@ -89,6 +201,7 @@ class OnScrollEvent(Event["ScrollableControl"]): Logical type of the scroll notification. Determines which optional fields are populated: + - [`ScrollType.UPDATE`][flet.]: [`scroll_delta`][(c).] - [`ScrollType.USER`][flet.]: [`direction`][(c).direction] - [`ScrollType.OVERSCROLL`][flet.]: [`overscroll`][(c).] and [`velocity`][(c).] @@ -222,9 +335,12 @@ class ScrollableControl(Control): - imperatively changing position with [`scroll_to()`][(c).scroll_to]. """ - scroll: Optional[ScrollMode] = None + scroll: Optional[Union[ScrollMode, Scrollbar]] = None """ - Enables a vertical scrolling for the Column to prevent its content overflow. + Defines the scroll bar configuration of this control. + + Can be a [`Scrollbar`][flet.] instance for full control over the appearance of the + scrollbar, or a [`ScrollMode`][flet.] value, for ready-made scrollbar behaviors. """ auto_scroll: bool = False diff --git a/sdk/python/packages/flet/src/flet/controls/types.py b/sdk/python/packages/flet/src/flet/controls/types.py index 8dc7abadf4..8c586ffd69 100644 --- a/sdk/python/packages/flet/src/flet/controls/types.py +++ b/sdk/python/packages/flet/src/flet/controls/types.py @@ -437,28 +437,65 @@ class TextAlign(Enum): class ScrollMode(Enum): """ - Weather scrolling is enabled and visibility of scroll bar options. + Defines scrolling behavior and scroll bar visibility for scrollable controls. + + When assigned to [`ScrollableControl.scroll`][flet.], for example, each value + internally maps to a specific [`Scrollbar`][flet.] configuration. """ AUTO = "auto" """ Scrolling is enabled and scroll bar is only shown when scrolling occurs. + + [`Scrollbar`][flet.] equivalent: + + ```python + ft.Scrollbar( + thickness=4.0 if page.platform.is_mobile() and not page.web else None, + ) + ``` """ ADAPTIVE = "adaptive" """ Scrolling is enabled and scroll bar is always shown when running app as web or \ desktop. + + [`Scrollbar`][flet.] equivalent: + + ```python + ft.Scrollbar( + thumb_visibility=page.web or not page.platform.is_mobile(), + thickness=4.0 if page.platform.is_mobile() and not page.web else None, + ) + ``` """ ALWAYS = "always" """ Scrolling is enabled and scroll bar is always shown. + + [`Scrollbar`][flet.] equivalent: + + ```python + ft.Scrollbar( + thumb_visibility=True, + thickness=4.0 if page.platform.is_mobile() and not page.web else None, + ) + ``` """ HIDDEN = "hidden" """ Scrolling is enabled, but scroll bar is always hidden. + + [`Scrollbar`][flet.] equivalent: + + ```python + ft.Scrollbar( + thickness=0, + ) + ``` """