From 9076135de0509dd16346a58ca5b6d96f4f118e23 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Sun, 8 Mar 2026 20:53:04 +0100 Subject: [PATCH 01/11] initial commit --- .../lib/src/controls/scrollable_control.dart | 104 +++++++++++++++--- sdk/python/packages/flet/src/flet/__init__.py | 4 + .../flet/src/flet/controls/base_page.py | 5 +- .../src/flet/controls/scrollable_control.py | 88 ++++++++++++++- 4 files changed, 183 insertions(+), 18 deletions(-) diff --git a/packages/flet/lib/src/controls/scrollable_control.dart b/packages/flet/lib/src/controls/scrollable_control.dart index aa935deea3..cf228f7a32 100644 --- a/packages/flet/lib/src/controls/scrollable_control.dart +++ b/packages/flet/lib/src/controls/scrollable_control.dart @@ -9,6 +9,88 @@ import '../utils/platform.dart'; import '../utils/time.dart'; import '../widgets/flet_store_mixin.dart'; +class _ScrollbarConfiguration { + final ScrollMode mode; + final bool? thumbVisibility; + final bool? trackVisibility; + final double? thickness; + final Radius? radius; + final bool? interactive; + final ScrollbarOrientation? orientation; + + const _ScrollbarConfiguration({ + required this.mode, + this.thumbVisibility, + this.trackVisibility, + this.thickness, + this.radius, + this.interactive, + this.orientation, + }); + + factory _ScrollbarConfiguration.fromValue(dynamic value) { + if (value is Map) { + final modeValue = value["mode"] ?? value["scroll_mode"]; + final parsedRadius = parseDouble(value["radius"]); + return _ScrollbarConfiguration( + mode: parseScrollMode( + modeValue is String ? modeValue : null, ScrollMode.auto) ?? + ScrollMode.auto, + thumbVisibility: parseBool(value["thumb_visibility"]), + trackVisibility: parseBool(value["track_visibility"]), + thickness: parseDouble(value["thickness"]), + radius: parsedRadius != null ? Radius.circular(parsedRadius) : null, + interactive: parseBool(value["interactive"]), + orientation: _parseScrollbarOrientation(value["orientation"]), + ); + } + + return _ScrollbarConfiguration( + mode: parseScrollMode(value is String ? value : null, ScrollMode.none) ?? + ScrollMode.none, + ); + } + + bool get enabled => mode != ScrollMode.none; + + bool get effectiveThumbVisibility { + final defaultValue = (mode == ScrollMode.always || + (mode == ScrollMode.adaptive && !isMobilePlatform())) && + mode != ScrollMode.hidden; + return thumbVisibility ?? defaultValue; + } + + double? get effectiveThickness { + if (thickness != null) { + return thickness; + } + return mode == ScrollMode.hidden + ? 0 + : isMobilePlatform() + ? 4.0 + : null; + } +} + +ScrollbarOrientation? _parseScrollbarOrientation(dynamic value, + [ScrollbarOrientation? defaultValue]) { + if (value is! String) { + return defaultValue; + } + switch (value.toLowerCase()) { + case "left": + return ScrollbarOrientation.left; + case "right": + return ScrollbarOrientation.right; + case "top": + return ScrollbarOrientation.top; + case "bottom": + return ScrollbarOrientation.bottom; + default: + return defaultValue; + } +} + class ScrollableControl extends StatefulWidget { final Control control; final Widget child; @@ -95,8 +177,8 @@ 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 = + _ScrollbarConfiguration.fromValue(widget.control.get("scroll")); if (widget.control.getBool("auto_scroll", false)!) { WidgetsBinding.instance.addPostFrameCallback((_) { @@ -107,18 +189,14 @@ class _ScrollableControlState extends State ); }); } - return scrollMode != ScrollMode.none + return scrollConfiguration.enabled ? 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, + thumbVisibility: scrollConfiguration.effectiveThumbVisibility, + trackVisibility: scrollConfiguration.trackVisibility, + thickness: scrollConfiguration.effectiveThickness, + radius: scrollConfiguration.radius, + interactive: scrollConfiguration.interactive, + scrollbarOrientation: scrollConfiguration.orientation, controller: _controller, child: ScrollConfiguration( behavior: 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..28f6b73be3 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,77 @@ class ScrollDirection(Enum): """ +class ScrollbarOrientation(Enum): + """ + Side of the viewport where the scrollbar is shown. + """ + + LEFT = "left" + """ + Place the scrollbar at the left edge. + """ + + RIGHT = "right" + """ + Place the scrollbar at the right edge. + """ + + TOP = "top" + """ + Place the scrollbar at the top edge. + """ + + BOTTOM = "bottom" + """ + Place the scrollbar at the bottom edge. + """ + + +@dataclass +class Scrollbar: + """ + Per-control scrollbar configuration for [`ScrollableControl.scroll`][flet.]. + """ + + mode: ScrollMode = ScrollMode.AUTO + """ + Scroll behavior mode baseline. + + This keeps parity with legacy [`ScrollMode`][flet.] values when `scroll` + was enum-only. + """ + + thumb_visibility: Optional[bool] = None + """ + Overrides thumb visibility when provided. + """ + + track_visibility: Optional[bool] = None + """ + Overrides track visibility when provided. + """ + + thickness: Optional[Number] = None + """ + Overrides scrollbar thickness in logical pixels when provided. + """ + + radius: Optional[Number] = None + """ + Overrides scrollbar thumb corner radius when provided. + """ + + interactive: Optional[bool] = None + """ + Overrides whether the scrollbar is interactive when provided. + """ + + orientation: Optional[ScrollbarOrientation] = None + """ + Overrides the side where scrollbar is displayed. + """ + + @dataclass class OnScrollEvent(Event["ScrollableControl"]): """ @@ -222,9 +300,13 @@ 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. + Configures scrolling for this control. + + Accepts either: + - [`ScrollMode`][flet.] for legacy behavior, + - [`Scrollbar`][flet.] for per-control scrollbar customization. """ auto_scroll: bool = False From 80a87e189fd76badcb7d0237a2315342d08305c8 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Sun, 8 Mar 2026 21:02:31 +0100 Subject: [PATCH 02/11] Refactor ScrollbarConfiguration to improve reusability and simplify ScrollableControl integration --- .../lib/src/controls/scrollable_control.dart | 101 +++--------------- packages/flet/lib/src/utils/misc.dart | 54 ++++++++++ 2 files changed, 69 insertions(+), 86 deletions(-) diff --git a/packages/flet/lib/src/controls/scrollable_control.dart b/packages/flet/lib/src/controls/scrollable_control.dart index cf228f7a32..7546b51d6c 100644 --- a/packages/flet/lib/src/controls/scrollable_control.dart +++ b/packages/flet/lib/src/controls/scrollable_control.dart @@ -9,88 +9,6 @@ import '../utils/platform.dart'; import '../utils/time.dart'; import '../widgets/flet_store_mixin.dart'; -class _ScrollbarConfiguration { - final ScrollMode mode; - final bool? thumbVisibility; - final bool? trackVisibility; - final double? thickness; - final Radius? radius; - final bool? interactive; - final ScrollbarOrientation? orientation; - - const _ScrollbarConfiguration({ - required this.mode, - this.thumbVisibility, - this.trackVisibility, - this.thickness, - this.radius, - this.interactive, - this.orientation, - }); - - factory _ScrollbarConfiguration.fromValue(dynamic value) { - if (value is Map) { - final modeValue = value["mode"] ?? value["scroll_mode"]; - final parsedRadius = parseDouble(value["radius"]); - return _ScrollbarConfiguration( - mode: parseScrollMode( - modeValue is String ? modeValue : null, ScrollMode.auto) ?? - ScrollMode.auto, - thumbVisibility: parseBool(value["thumb_visibility"]), - trackVisibility: parseBool(value["track_visibility"]), - thickness: parseDouble(value["thickness"]), - radius: parsedRadius != null ? Radius.circular(parsedRadius) : null, - interactive: parseBool(value["interactive"]), - orientation: _parseScrollbarOrientation(value["orientation"]), - ); - } - - return _ScrollbarConfiguration( - mode: parseScrollMode(value is String ? value : null, ScrollMode.none) ?? - ScrollMode.none, - ); - } - - bool get enabled => mode != ScrollMode.none; - - bool get effectiveThumbVisibility { - final defaultValue = (mode == ScrollMode.always || - (mode == ScrollMode.adaptive && !isMobilePlatform())) && - mode != ScrollMode.hidden; - return thumbVisibility ?? defaultValue; - } - - double? get effectiveThickness { - if (thickness != null) { - return thickness; - } - return mode == ScrollMode.hidden - ? 0 - : isMobilePlatform() - ? 4.0 - : null; - } -} - -ScrollbarOrientation? _parseScrollbarOrientation(dynamic value, - [ScrollbarOrientation? defaultValue]) { - if (value is! String) { - return defaultValue; - } - switch (value.toLowerCase()) { - case "left": - return ScrollbarOrientation.left; - case "right": - return ScrollbarOrientation.right; - case "top": - return ScrollbarOrientation.top; - case "bottom": - return ScrollbarOrientation.bottom; - default: - return defaultValue; - } -} - class ScrollableControl extends StatefulWidget { final Control control; final Widget child; @@ -177,8 +95,19 @@ class _ScrollableControlState extends State @override Widget build(BuildContext context) { debugPrint("ScrollableControl build: ${widget.control.id}"); - final scrollConfiguration = - _ScrollbarConfiguration.fromValue(widget.control.get("scroll")); + final scrollConfiguration = widget.control.getScrollbarConfiguration( + "scroll", const ScrollbarConfiguration(mode: ScrollMode.none))!; + final scrollMode = scrollConfiguration.mode; + final thumbVisibility = scrollConfiguration.thumbVisibility ?? + ((scrollMode == ScrollMode.always || + (scrollMode == ScrollMode.adaptive && !isMobilePlatform())) && + scrollMode != ScrollMode.hidden); + final thickness = scrollConfiguration.thickness ?? + (scrollMode == ScrollMode.hidden + ? 0 + : isMobilePlatform() + ? 4.0 + : null); if (widget.control.getBool("auto_scroll", false)!) { WidgetsBinding.instance.addPostFrameCallback((_) { @@ -191,9 +120,9 @@ class _ScrollableControlState extends State } return scrollConfiguration.enabled ? Scrollbar( - thumbVisibility: scrollConfiguration.effectiveThumbVisibility, + thumbVisibility: thumbVisibility, trackVisibility: scrollConfiguration.trackVisibility, - thickness: scrollConfiguration.effectiveThickness, + thickness: thickness, radius: scrollConfiguration.radius, interactive: scrollConfiguration.interactive, scrollbarOrientation: scrollConfiguration.orientation, diff --git a/packages/flet/lib/src/utils/misc.dart b/packages/flet/lib/src/utils/misc.dart index e02242ab32..a16ecfd1e3 100644 --- a/packages/flet/lib/src/utils/misc.dart +++ b/packages/flet/lib/src/utils/misc.dart @@ -73,6 +73,55 @@ ScrollMode? parseScrollMode(String? value, return parseEnum(ScrollMode.values, value, defaultValue); } +class ScrollbarConfiguration { + final ScrollMode mode; + final bool? thumbVisibility; + final bool? trackVisibility; + final double? thickness; + final Radius? radius; + final bool? interactive; + final ScrollbarOrientation? orientation; + + const ScrollbarConfiguration({ + required this.mode, + this.thumbVisibility, + this.trackVisibility, + this.thickness, + this.radius, + this.interactive, + this.orientation, + }); + + bool get enabled => mode != ScrollMode.none; +} + +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) { + return ScrollbarConfiguration( + mode: parseScrollMode(value, ScrollMode.none)!, + ); + } + + final parsedRadius = parseDouble(value["radius"]); + return ScrollbarConfiguration( + mode: parseScrollMode( + value["mode"] ?? value["scroll_mode"], ScrollMode.auto)!, + thumbVisibility: parseBool(value["thumb_visibility"]), + trackVisibility: parseBool(value["track_visibility"]), + thickness: parseDouble(value["thickness"]), + radius: parsedRadius != null ? Radius.circular(parsedRadius) : null, + interactive: parseBool(value["interactive"]), + orientation: parseScrollbarOrientation(value["orientation"]), + ); +} + enum LabelPosition { right, left } LabelPosition? parseLabelPosition(String? value, @@ -186,6 +235,11 @@ extension MiscParsers on Control { return parseScrollMode(get(propertyName), defaultValue); } + ScrollbarConfiguration? getScrollbarConfiguration(String propertyName, + [ScrollbarConfiguration? defaultValue]) { + return parseScrollbarConfiguration(get(propertyName), defaultValue); + } + LabelPosition? getLabelPosition(String propertyName, [LabelPosition? defaultValue]) { return parseLabelPosition(get(propertyName), defaultValue); From 47ae7858d6dde1514b749b82ed462d82837dc437 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Sun, 8 Mar 2026 21:03:16 +0100 Subject: [PATCH 03/11] example --- .../controls/types/scroll_bar/showcase.py | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 sdk/python/examples/controls/types/scroll_bar/showcase.py 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..afa926746e --- /dev/null +++ b/sdk/python/examples/controls/types/scroll_bar/showcase.py @@ -0,0 +1,72 @@ +from typing import Union + +import flet as ft + + +def showcase_card( + title: str, scroll: Union[ft.ScrollMode, ft.Scrollbar] +) -> ft.Container: + return ft.Container( + width=320, + 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(title, weight=ft.FontWeight.BOLD), + ft.Container( + height=210, + border=ft.Border.all(1, ft.Colors.OUTLINE), + border_radius=8, + padding=8, + content=ft.Column( + spacing=4, + scroll=scroll, + controls=[ft.Text(f"Item {i + 1}") for i in range(35)], + ), + ), + ], + ), + ) + + +def main(page: ft.Page): + page.horizontal_alignment = ft.CrossAxisAlignment.CENTER + page.appbar = ft.AppBar(title="ScrollBar Showcase") + + page.add( + ft.Text("Legacy ScrollMode and new Scrollbar object can be mixed."), + ft.Row( + wrap=True, + spacing=12, + run_spacing=12, + scroll=ft.ScrollMode.AUTO, + controls=[ + showcase_card("Legacy: ScrollMode.AUTO", ft.ScrollMode.AUTO), + showcase_card( + "Custom: ALWAYS + thick + track", + ft.Scrollbar( + mode=ft.ScrollMode.ALWAYS, + thickness=12, + radius=8, + thumb_visibility=True, + track_visibility=True, + ), + ), + showcase_card( + "Custom: ADAPTIVE + non-interactive", + ft.Scrollbar( + mode=ft.ScrollMode.ADAPTIVE, + orientation=ft.ScrollbarOrientation.LEFT, + interactive=False, + thickness=6, + ), + ), + ], + ), + ) + + +ft.run(main) From 97100b98421fd8a2a4fea34527776a106f60652c Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Sun, 8 Mar 2026 21:12:25 +0100 Subject: [PATCH 04/11] Extract ScrollbarConfiguration into a dedicated utility file for improved reusability and modularity. --- packages/flet/lib/flet.dart | 1 + .../lib/src/controls/scrollable_control.dart | 2 +- packages/flet/lib/src/utils/misc.dart | 62 +---------------- packages/flet/lib/src/utils/scrollbar.dart | 68 +++++++++++++++++++ .../src/flet/controls/scrollable_control.py | 2 + 5 files changed, 73 insertions(+), 62 deletions(-) create mode 100644 packages/flet/lib/src/utils/scrollbar.dart 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 7546b51d6c..6d383f14c5 100644 --- a/packages/flet/lib/src/controls/scrollable_control.dart +++ b/packages/flet/lib/src/controls/scrollable_control.dart @@ -3,9 +3,9 @@ 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'; diff --git a/packages/flet/lib/src/utils/misc.dart b/packages/flet/lib/src/utils/misc.dart index a16ecfd1e3..ed9567a4a1 100644 --- a/packages/flet/lib/src/utils/misc.dart +++ b/packages/flet/lib/src/utils/misc.dart @@ -7,6 +7,7 @@ import '../models/control.dart'; import 'borders.dart'; import 'enums.dart'; import 'numbers.dart'; +import 'scrollbar.dart'; Clip? parseClip(String? value, [Clip? defaultValue]) { return parseEnum(Clip.values, value, defaultValue); @@ -66,62 +67,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); -} - -class ScrollbarConfiguration { - final ScrollMode mode; - final bool? thumbVisibility; - final bool? trackVisibility; - final double? thickness; - final Radius? radius; - final bool? interactive; - final ScrollbarOrientation? orientation; - - const ScrollbarConfiguration({ - required this.mode, - this.thumbVisibility, - this.trackVisibility, - this.thickness, - this.radius, - this.interactive, - this.orientation, - }); - - bool get enabled => mode != ScrollMode.none; -} - -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) { - return ScrollbarConfiguration( - mode: parseScrollMode(value, ScrollMode.none)!, - ); - } - - final parsedRadius = parseDouble(value["radius"]); - return ScrollbarConfiguration( - mode: parseScrollMode( - value["mode"] ?? value["scroll_mode"], ScrollMode.auto)!, - thumbVisibility: parseBool(value["thumb_visibility"]), - trackVisibility: parseBool(value["track_visibility"]), - thickness: parseDouble(value["thickness"]), - radius: parsedRadius != null ? Radius.circular(parsedRadius) : null, - interactive: parseBool(value["interactive"]), - orientation: parseScrollbarOrientation(value["orientation"]), - ); -} - enum LabelPosition { right, left } LabelPosition? parseLabelPosition(String? value, @@ -235,11 +180,6 @@ extension MiscParsers on Control { return parseScrollMode(get(propertyName), defaultValue); } - ScrollbarConfiguration? getScrollbarConfiguration(String propertyName, - [ScrollbarConfiguration? defaultValue]) { - return parseScrollbarConfiguration(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..2cff74781a --- /dev/null +++ b/packages/flet/lib/src/utils/scrollbar.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; + +import '../models/control.dart'; +import 'enums.dart'; +import 'numbers.dart'; + +enum ScrollMode { none, auto, adaptive, always, hidden } + +ScrollMode? parseScrollMode(String? value, + [ScrollMode? defaultValue = ScrollMode.none]) { + return parseEnum(ScrollMode.values, value, defaultValue); +} + +class ScrollbarConfiguration { + final ScrollMode mode; + final bool? thumbVisibility; + final bool? trackVisibility; + final double? thickness; + final Radius? radius; + final bool? interactive; + final ScrollbarOrientation? orientation; + + const ScrollbarConfiguration({ + required this.mode, + this.thumbVisibility, + this.trackVisibility, + this.thickness, + this.radius, + this.interactive, + this.orientation, + }); + + bool get enabled => mode != ScrollMode.none; +} + +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) { + return ScrollbarConfiguration( + mode: parseScrollMode(value, ScrollMode.none)!, + ); + } + + final parsedRadius = parseDouble(value["radius"]); + return ScrollbarConfiguration( + mode: parseScrollMode( + value["mode"] ?? value["scroll_mode"], ScrollMode.auto)!, + thumbVisibility: parseBool(value["thumb_visibility"]), + trackVisibility: parseBool(value["track_visibility"]), + thickness: parseDouble(value["thickness"]), + radius: parsedRadius != null ? Radius.circular(parsedRadius) : null, + interactive: parseBool(value["interactive"]), + orientation: parseScrollbarOrientation(value["orientation"]), + ); +} + +extension ScrollbarParsers on Control { + ScrollbarConfiguration? getScrollbarConfiguration(String propertyName, + [ScrollbarConfiguration? defaultValue]) { + return parseScrollbarConfiguration(get(propertyName), defaultValue); + } +} 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 28f6b73be3..d961418bde 100644 --- a/sdk/python/packages/flet/src/flet/controls/scrollable_control.py +++ b/sdk/python/packages/flet/src/flet/controls/scrollable_control.py @@ -167,6 +167,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).] @@ -305,6 +306,7 @@ class ScrollableControl(Control): Configures scrolling for this control. Accepts either: + - [`ScrollMode`][flet.] for legacy behavior, - [`Scrollbar`][flet.] for per-control scrollbar customization. """ From ad5bd3553858662cfa2762980044e2e74fdff694 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Sun, 8 Mar 2026 21:24:41 +0100 Subject: [PATCH 05/11] improvements --- packages/flet/lib/src/utils/misc.dart | 6 ------ packages/flet/lib/src/utils/scrollbar.dart | 20 ++++++++++---------- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/packages/flet/lib/src/utils/misc.dart b/packages/flet/lib/src/utils/misc.dart index ed9567a4a1..f6ce430ffa 100644 --- a/packages/flet/lib/src/utils/misc.dart +++ b/packages/flet/lib/src/utils/misc.dart @@ -7,7 +7,6 @@ import '../models/control.dart'; import 'borders.dart'; import 'enums.dart'; import 'numbers.dart'; -import 'scrollbar.dart'; Clip? parseClip(String? value, [Clip? defaultValue]) { return parseEnum(Clip.values, value, defaultValue); @@ -175,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 index 2cff74781a..e3b9cb3389 100644 --- a/packages/flet/lib/src/utils/scrollbar.dart +++ b/packages/flet/lib/src/utils/scrollbar.dart @@ -1,13 +1,13 @@ import 'package:flutter/material.dart'; import '../models/control.dart'; +import 'borders.dart'; import 'enums.dart'; import 'numbers.dart'; -enum ScrollMode { none, auto, adaptive, always, hidden } +enum ScrollMode { auto, adaptive, always, hidden } -ScrollMode? parseScrollMode(String? value, - [ScrollMode? defaultValue = ScrollMode.none]) { +ScrollMode? parseScrollMode(String? value, [ScrollMode? defaultValue]) { return parseEnum(ScrollMode.values, value, defaultValue); } @@ -29,8 +29,6 @@ class ScrollbarConfiguration { this.interactive, this.orientation, }); - - bool get enabled => mode != ScrollMode.none; } ScrollbarOrientation? parseScrollbarOrientation(String? value, @@ -42,25 +40,27 @@ ScrollbarConfiguration? parseScrollbarConfiguration(dynamic value, [ScrollbarConfiguration? defaultValue]) { if (value == null) return defaultValue; if (value is! Map) { - return ScrollbarConfiguration( - mode: parseScrollMode(value, ScrollMode.none)!, - ); + final mode = parseScrollMode(value); + return mode == null ? defaultValue : ScrollbarConfiguration(mode: mode); } - final parsedRadius = parseDouble(value["radius"]); return ScrollbarConfiguration( mode: parseScrollMode( value["mode"] ?? value["scroll_mode"], ScrollMode.auto)!, thumbVisibility: parseBool(value["thumb_visibility"]), trackVisibility: parseBool(value["track_visibility"]), thickness: parseDouble(value["thickness"]), - radius: parsedRadius != null ? Radius.circular(parsedRadius) : null, + radius: parseRadius(value["radius"]), interactive: parseBool(value["interactive"]), orientation: parseScrollbarOrientation(value["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); From 512aa47b2c4b1291823966bfd045480b76984cfe Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Sun, 8 Mar 2026 21:28:10 +0100 Subject: [PATCH 06/11] Refactor ScrollableControl to enhance scrollbar configuration handling and improve auto-scroll functionality --- .../lib/src/controls/scrollable_control.dart | 69 ++++++++++--------- 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/packages/flet/lib/src/controls/scrollable_control.dart b/packages/flet/lib/src/controls/scrollable_control.dart index 6d383f14c5..8107698b63 100644 --- a/packages/flet/lib/src/controls/scrollable_control.dart +++ b/packages/flet/lib/src/controls/scrollable_control.dart @@ -95,13 +95,26 @@ class _ScrollableControlState extends State @override Widget build(BuildContext context) { debugPrint("ScrollableControl build: ${widget.control.id}"); - final scrollConfiguration = widget.control.getScrollbarConfiguration( - "scroll", const ScrollbarConfiguration(mode: ScrollMode.none))!; + final scrollConfiguration = + widget.control.getScrollbarConfiguration("scroll"); + + if (widget.control.getBool("auto_scroll", false)! && + scrollConfiguration != null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _controller.animateTo( + _controller.position.maxScrollExtent, + duration: const Duration(seconds: 1), + curve: Curves.ease, + ); + }); + } + + if (scrollConfiguration == null) return widget.child; + final scrollMode = scrollConfiguration.mode; final thumbVisibility = scrollConfiguration.thumbVisibility ?? ((scrollMode == ScrollMode.always || - (scrollMode == ScrollMode.adaptive && !isMobilePlatform())) && - scrollMode != ScrollMode.hidden); + (scrollMode == ScrollMode.adaptive && !isMobilePlatform()))); final thickness = scrollConfiguration.thickness ?? (scrollMode == ScrollMode.hidden ? 0 @@ -109,35 +122,23 @@ class _ScrollableControlState extends State ? 4.0 : null); - if (widget.control.getBool("auto_scroll", false)!) { - WidgetsBinding.instance.addPostFrameCallback((_) { - _controller.animateTo( - _controller.position.maxScrollExtent, - duration: const Duration(seconds: 1), - curve: Curves.ease, - ); - }); - } - return scrollConfiguration.enabled - ? Scrollbar( - thumbVisibility: thumbVisibility, - trackVisibility: scrollConfiguration.trackVisibility, - thickness: 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, - )) - : widget.child; + return Scrollbar( + thumbVisibility: thumbVisibility, + trackVisibility: scrollConfiguration.trackVisibility, + thickness: 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, + )); } } From 8879ede0997054b8080fe1ae2996787d4490c2b9 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Sun, 8 Mar 2026 21:45:22 +0100 Subject: [PATCH 07/11] Add documentation for Scrollbar and ScrollbarOrientation classes --- .../packages/flet/docs/types/scrollbar.md | 16 ++++ .../flet/docs/types/scrollbarorientation.md | 16 ++++ sdk/python/packages/flet/mkdocs.yml | 2 + .../src/flet/controls/scrollable_control.py | 74 ++++++++++++++----- 4 files changed, 91 insertions(+), 17 deletions(-) create mode 100644 sdk/python/packages/flet/docs/types/scrollbar.md create mode 100644 sdk/python/packages/flet/docs/types/scrollbarorientation.md 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..15969a0dfd --- /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 +--- + +{{ 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/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/controls/scrollable_control.py b/sdk/python/packages/flet/src/flet/controls/scrollable_control.py index d961418bde..ef060a4e45 100644 --- a/sdk/python/packages/flet/src/flet/controls/scrollable_control.py +++ b/sdk/python/packages/flet/src/flet/controls/scrollable_control.py @@ -87,34 +87,34 @@ class ScrollDirection(Enum): class ScrollbarOrientation(Enum): """ - Side of the viewport where the scrollbar is shown. + Defines the edge/side of the viewport where the scrollbar is shown. """ LEFT = "left" """ - Place the scrollbar at the left edge. + Places the scrollbar on the left/leading edge of a vertical scrollable. """ RIGHT = "right" """ - Place the scrollbar at the right edge. + Places the scrollbar on the right/trailing edge of a vertical scrollable. """ TOP = "top" """ - Place the scrollbar at the top edge. + Places the scrollbar above a horizontal scrollable. """ BOTTOM = "bottom" """ - Place the scrollbar at the bottom edge. + Places the scrollbar below a horizontal scrollable. """ @dataclass class Scrollbar: """ - Per-control scrollbar configuration for [`ScrollableControl.scroll`][flet.]. + Configures the scrollbar that scrollable controls render for their content. """ mode: ScrollMode = ScrollMode.AUTO @@ -127,32 +127,74 @@ class Scrollbar: thumb_visibility: Optional[bool] = None """ - Overrides thumb visibility when provided. + 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 """ - Overrides track visibility when provided. + 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 """ - Overrides scrollbar thickness in logical pixels when provided. + 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 """ - Overrides scrollbar thumb corner radius when provided. + 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 """ - Overrides whether the scrollbar is interactive when provided. + 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 """ - Overrides the side where scrollbar is displayed. + 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. """ @@ -303,12 +345,10 @@ class ScrollableControl(Control): scroll: Optional[Union[ScrollMode, Scrollbar]] = None """ - Configures scrolling for this control. - - Accepts either: + Defines the scroll bar configuration of this control. - - [`ScrollMode`][flet.] for legacy behavior, - - [`Scrollbar`][flet.] for per-control scrollbar customization. + 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 From 852b2eece9a25d936ebc072389073403c92fcfe8 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Sun, 8 Mar 2026 23:43:31 +0100 Subject: [PATCH 08/11] Refactor ScrollableControl and ScrollbarConfiguration to remove `ScrollMode` property, simplify configuration handling, and improve code clarity --- .../lib/src/controls/scrollable_control.dart | 16 +------ packages/flet/lib/src/utils/scrollbar.dart | 44 ++++++++++++++----- .../src/flet/controls/scrollable_control.py | 10 +---- .../packages/flet/src/flet/controls/types.py | 39 +++++++++++++++- 4 files changed, 74 insertions(+), 35 deletions(-) diff --git a/packages/flet/lib/src/controls/scrollable_control.dart b/packages/flet/lib/src/controls/scrollable_control.dart index 8107698b63..395993a5a0 100644 --- a/packages/flet/lib/src/controls/scrollable_control.dart +++ b/packages/flet/lib/src/controls/scrollable_control.dart @@ -4,7 +4,6 @@ import '../models/control.dart'; import '../utils/animations.dart'; import '../utils/keys.dart'; import '../utils/numbers.dart'; -import '../utils/platform.dart'; import '../utils/scrollbar.dart'; import '../utils/time.dart'; import '../widgets/flet_store_mixin.dart'; @@ -111,21 +110,10 @@ class _ScrollableControlState extends State if (scrollConfiguration == null) return widget.child; - final scrollMode = scrollConfiguration.mode; - final thumbVisibility = scrollConfiguration.thumbVisibility ?? - ((scrollMode == ScrollMode.always || - (scrollMode == ScrollMode.adaptive && !isMobilePlatform()))); - final thickness = scrollConfiguration.thickness ?? - (scrollMode == ScrollMode.hidden - ? 0 - : isMobilePlatform() - ? 4.0 - : null); - return Scrollbar( - thumbVisibility: thumbVisibility, + thumbVisibility: scrollConfiguration.thumbVisibility, trackVisibility: scrollConfiguration.trackVisibility, - thickness: thickness, + thickness: scrollConfiguration.thickness, radius: scrollConfiguration.radius, interactive: scrollConfiguration.interactive, scrollbarOrientation: scrollConfiguration.orientation, diff --git a/packages/flet/lib/src/utils/scrollbar.dart b/packages/flet/lib/src/utils/scrollbar.dart index e3b9cb3389..48a8a83bf9 100644 --- a/packages/flet/lib/src/utils/scrollbar.dart +++ b/packages/flet/lib/src/utils/scrollbar.dart @@ -4,6 +4,7 @@ import '../models/control.dart'; import 'borders.dart'; import 'enums.dart'; import 'numbers.dart'; +import 'platform.dart'; enum ScrollMode { auto, adaptive, always, hidden } @@ -12,7 +13,6 @@ ScrollMode? parseScrollMode(String? value, [ScrollMode? defaultValue]) { } class ScrollbarConfiguration { - final ScrollMode mode; final bool? thumbVisibility; final bool? trackVisibility; final double? thickness; @@ -21,7 +21,6 @@ class ScrollbarConfiguration { final ScrollbarOrientation? orientation; const ScrollbarConfiguration({ - required this.mode, this.thumbVisibility, this.trackVisibility, this.thickness, @@ -29,6 +28,23 @@ class ScrollbarConfiguration { 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, @@ -41,18 +57,24 @@ ScrollbarConfiguration? parseScrollbarConfiguration(dynamic value, if (value == null) return defaultValue; if (value is! Map) { final mode = parseScrollMode(value); - return mode == null ? defaultValue : ScrollbarConfiguration(mode: mode); + return mode == null + ? defaultValue + : ScrollbarConfiguration.fromScrollMode(mode); } + final baseConfiguration = ScrollbarConfiguration.fromScrollMode( + parseScrollMode(value["mode"], ScrollMode.auto)!); + return ScrollbarConfiguration( - mode: parseScrollMode( - value["mode"] ?? value["scroll_mode"], ScrollMode.auto)!, - thumbVisibility: parseBool(value["thumb_visibility"]), - trackVisibility: parseBool(value["track_visibility"]), - thickness: parseDouble(value["thickness"]), - radius: parseRadius(value["radius"]), - interactive: parseBool(value["interactive"]), - orientation: parseScrollbarOrientation(value["orientation"]), + 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), ); } 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 ef060a4e45..f3177fba10 100644 --- a/sdk/python/packages/flet/src/flet/controls/scrollable_control.py +++ b/sdk/python/packages/flet/src/flet/controls/scrollable_control.py @@ -87,7 +87,7 @@ class ScrollDirection(Enum): class ScrollbarOrientation(Enum): """ - Defines the edge/side of the viewport where the scrollbar is shown. + Defines the edge/side of the viewport where the [`Scrollbar`][flet.] is shown. """ LEFT = "left" @@ -117,14 +117,6 @@ class Scrollbar: Configures the scrollbar that scrollable controls render for their content. """ - mode: ScrollMode = ScrollMode.AUTO - """ - Scroll behavior mode baseline. - - This keeps parity with legacy [`ScrollMode`][flet.] values when `scroll` - was enum-only. - """ - thumb_visibility: Optional[bool] = None """ Whether this scrollbar's thumb should be always be visible, even when not being 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, + ) + ``` """ From 0c409c8db4ad6f3f6ac41739a34b4967ce632b75 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Sun, 8 Mar 2026 23:51:22 +0100 Subject: [PATCH 09/11] Add showcase for ScrollbarOrientation with example usage and documentation update --- client/pubspec.lock | 8 +- .../controls/types/scroll_bar/showcase.py | 276 +++++++++++++++--- .../types/scroll_bar_orientation/showcase.py | 91 ++++++ .../flet/docs/types/scrollbarorientation.md | 2 +- 4 files changed, 325 insertions(+), 52 deletions(-) create mode 100644 sdk/python/examples/controls/types/scroll_bar_orientation/showcase.py diff --git a/client/pubspec.lock b/client/pubspec.lock index 58ce9014c8..0e38c0da5e 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/sdk/python/examples/controls/types/scroll_bar/showcase.py b/sdk/python/examples/controls/types/scroll_bar/showcase.py index afa926746e..849d8b25ac 100644 --- a/sdk/python/examples/controls/types/scroll_bar/showcase.py +++ b/sdk/python/examples/controls/types/scroll_bar/showcase.py @@ -1,72 +1,254 @@ -from typing import Union +from typing import Optional import flet as ft -def showcase_card( - title: str, scroll: Union[ft.ScrollMode, ft.Scrollbar] -) -> ft.Container: - return ft.Container( - width=320, - 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(title, weight=ft.FontWeight.BOLD), - ft.Container( - height=210, - border=ft.Border.all(1, ft.Colors.OUTLINE), - border_radius=8, - padding=8, - content=ft.Column( - spacing=4, - scroll=scroll, - controls=[ft.Text(f"Item {i + 1}") for i in range(35)], - ), - ), - ], - ), - ) +def parse_optional_bool(value: Optional[str]): + if value == "true": + return True + if value == "false": + return False + return None def main(page: ft.Page): page.horizontal_alignment = ft.CrossAxisAlignment.CENTER - page.appbar = ft.AppBar(title="ScrollBar Showcase") + page.appbar = ft.AppBar(title="Scrollbar Dataclass Showcase") + + thumb_visibility = ft.Dropdown( + label="thumb_visibility", + value="none", + options=[ + ft.dropdown.Option("none", "None (theme/default)"), + ft.dropdown.Option("true", "True"), + ft.dropdown.Option("false", "False"), + ], + ) + track_visibility = ft.Dropdown( + label="track_visibility", + value="none", + options=[ + ft.dropdown.Option("none", "None (theme/default)"), + ft.dropdown.Option("true", "True"), + ft.dropdown.Option("false", "False"), + ], + ) + interactive = ft.Dropdown( + label="interactive", + value="none", + options=[ + ft.dropdown.Option("none", "None (platform default)"), + ft.dropdown.Option("true", "True"), + ft.dropdown.Option("false", "False"), + ], + ) + orientation = ft.Dropdown( + label="orientation", + value="none", + options=[ft.dropdown.Option("none", "None (auto side)")] + + [ft.dropdown.Option(o.value, o.name) for o in ft.ScrollbarOrientation], + ) + use_thickness = ft.Checkbox(label="Set thickness", value=False) + thickness_value = ft.Slider(min=0, max=20, divisions=20, value=8, label="{value}") + use_radius = ft.Checkbox(label="Set radius", value=False) + radius_value = ft.Slider(min=0, max=20, divisions=20, value=8, label="{value}") + + code_preview = ft.TextField( + label="Generated Scrollbar()", + read_only=True, + multiline=True, + min_lines=4, + max_lines=8, + ) + current_mode_hint = ft.Text(size=12, color=ft.Colors.ON_SURFACE_VARIANT) + 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, + ) + + def parse_orientation(): + if orientation.value == "none": + return None + return ft.ScrollbarOrientation(orientation.value) + + def build_scrollbar() -> ft.Scrollbar: + thickness = float(thickness_value.value) if use_thickness.value else None + radius = float(radius_value.value) if use_radius.value else None + return ft.Scrollbar( + thumb_visibility=parse_optional_bool(thumb_visibility.value), + track_visibility=parse_optional_bool(track_visibility.value), + interactive=parse_optional_bool(interactive.value), + thickness=thickness, + radius=radius, + orientation=parse_orientation(), + ) + + def build_scrollbar_code(scrollbar: ft.Scrollbar) -> str: + args: list[str] = [] + if scrollbar.thumb_visibility is not None: + args.append(f"thumb_visibility={scrollbar.thumb_visibility}") + if scrollbar.track_visibility is not None: + args.append(f"track_visibility={scrollbar.track_visibility}") + if scrollbar.interactive is not None: + args.append(f"interactive={scrollbar.interactive}") + if scrollbar.thickness is not None: + thickness = ( + int(scrollbar.thickness) + if float(scrollbar.thickness).is_integer() + else scrollbar.thickness + ) + args.append(f"thickness={thickness}") + if scrollbar.radius is not None: + radius = ( + int(scrollbar.radius) + if float(scrollbar.radius).is_integer() + else scrollbar.radius + ) + args.append(f"radius={radius}") + if scrollbar.orientation is not None: + args.append( + f"orientation=ft.ScrollbarOrientation.{scrollbar.orientation.name}" + ) + + if not args: + return "ft.Scrollbar()" + + return "ft.Scrollbar(\n" + "".join([f" {arg},\n" for arg in args]) + ")" + + def build_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 mode_hint(scrollbar: ft.Scrollbar) -> str: + if scrollbar.thickness == 0: + return "Equivalent legacy mode: ScrollMode.HIDDEN" + + thumb = scrollbar.thumb_visibility + thickness = scrollbar.thickness + mobile_default_thickness = ( + 4.0 if page.platform.is_mobile() and not page.web else None + ) + + if thumb is True and thickness == mobile_default_thickness: + return "Equivalent legacy mode: ScrollMode.ALWAYS" + if ( + thumb == (not (page.platform.is_mobile() and not page.web)) + and thickness == mobile_default_thickness + ): + return "Equivalent legacy mode: ScrollMode.ADAPTIVE" + if thumb is None and thickness == mobile_default_thickness: + return "Equivalent legacy mode: ScrollMode.AUTO" + return "Custom configuration (no exact ScrollMode equivalent)" + + def update_preview(_=None): + thickness_value.disabled = not use_thickness.value + radius_value.disabled = not use_radius.value + + scrollbar = build_scrollbar() + title, content = build_preview_content(scrollbar) + preview_title.value = title + preview_viewport.content = content + code_preview.value = build_scrollbar_code(scrollbar) + current_mode_hint.value = mode_hint(scrollbar) + page.update() + + dropdowns = [thumb_visibility, track_visibility, interactive, orientation] + for c in dropdowns: + c.on_select = update_preview + + controls_with_on_change = [use_thickness, thickness_value, use_radius, radius_value] + for c in controls_with_on_change: + c.on_change = update_preview page.add( - ft.Text("Legacy ScrollMode and new Scrollbar object can be mixed."), + 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, - scroll=ft.ScrollMode.AUTO, + alignment=ft.MainAxisAlignment.CENTER, controls=[ - showcase_card("Legacy: ScrollMode.AUTO", ft.ScrollMode.AUTO), - showcase_card( - "Custom: ALWAYS + thick + track", - ft.Scrollbar( - mode=ft.ScrollMode.ALWAYS, - thickness=12, - radius=8, - thumb_visibility=True, - track_visibility=True, + 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, + track_visibility, + interactive, + orientation, + use_thickness, + thickness_value, + use_radius, + radius_value, + ], ), ), - showcase_card( - "Custom: ADAPTIVE + non-interactive", - ft.Scrollbar( - mode=ft.ScrollMode.ADAPTIVE, - orientation=ft.ScrollbarOrientation.LEFT, - interactive=False, - thickness=6, + 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, + preview_viewport, + current_mode_hint, + code_preview, + ], ), ), ], ), ) + 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/scrollbarorientation.md b/sdk/python/packages/flet/docs/types/scrollbarorientation.md index 15969a0dfd..13960d5fec 100644 --- a/sdk/python/packages/flet/docs/types/scrollbarorientation.md +++ b/sdk/python/packages/flet/docs/types/scrollbarorientation.md @@ -1,6 +1,6 @@ --- class_name: flet.ScrollbarOrientation -examples: ../../examples/controls/types/scroll_bar +examples: ../../examples/controls/types/scroll_bar_orientation --- {{ class_summary(class_name) }} From 504e4ef721479518f9da3eecb700121726218089 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Tue, 10 Mar 2026 22:54:00 +0100 Subject: [PATCH 10/11] improve example --- .../controls/types/scroll_bar/showcase.py | 243 +++++++----------- 1 file changed, 89 insertions(+), 154 deletions(-) diff --git a/sdk/python/examples/controls/types/scroll_bar/showcase.py b/sdk/python/examples/controls/types/scroll_bar/showcase.py index 849d8b25ac..420047cad1 100644 --- a/sdk/python/examples/controls/types/scroll_bar/showcase.py +++ b/sdk/python/examples/controls/types/scroll_bar/showcase.py @@ -3,122 +3,26 @@ import flet as ft -def parse_optional_bool(value: Optional[str]): - if value == "true": - return True - if value == "false": - return False - return None - - def main(page: ft.Page): page.horizontal_alignment = ft.CrossAxisAlignment.CENTER page.appbar = ft.AppBar(title="Scrollbar Dataclass Showcase") - thumb_visibility = ft.Dropdown( - label="thumb_visibility", - value="none", - options=[ - ft.dropdown.Option("none", "None (theme/default)"), - ft.dropdown.Option("true", "True"), - ft.dropdown.Option("false", "False"), - ], - ) - track_visibility = ft.Dropdown( - label="track_visibility", - value="none", - options=[ - ft.dropdown.Option("none", "None (theme/default)"), - ft.dropdown.Option("true", "True"), - ft.dropdown.Option("false", "False"), - ], - ) - interactive = ft.Dropdown( - label="interactive", - value="none", - options=[ - ft.dropdown.Option("none", "None (platform default)"), - ft.dropdown.Option("true", "True"), - ft.dropdown.Option("false", "False"), - ], - ) - orientation = ft.Dropdown( - label="orientation", - value="none", - options=[ft.dropdown.Option("none", "None (auto side)")] - + [ft.dropdown.Option(o.value, o.name) for o in ft.ScrollbarOrientation], - ) - use_thickness = ft.Checkbox(label="Set thickness", value=False) - thickness_value = ft.Slider(min=0, max=20, divisions=20, value=8, label="{value}") - use_radius = ft.Checkbox(label="Set radius", value=False) - radius_value = ft.Slider(min=0, max=20, divisions=20, value=8, label="{value}") + 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 - code_preview = ft.TextField( - label="Generated Scrollbar()", - read_only=True, - multiline=True, - min_lines=4, - max_lines=8, - ) - current_mode_hint = ft.Text(size=12, color=ft.Colors.ON_SURFACE_VARIANT) - 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, - ) - - def parse_orientation(): - if orientation.value == "none": - return None - return ft.ScrollbarOrientation(orientation.value) - - def build_scrollbar() -> ft.Scrollbar: - thickness = float(thickness_value.value) if use_thickness.value else None - radius = float(radius_value.value) if use_radius.value else None return ft.Scrollbar( - thumb_visibility=parse_optional_bool(thumb_visibility.value), - track_visibility=parse_optional_bool(track_visibility.value), - interactive=parse_optional_bool(interactive.value), - thickness=thickness, - radius=radius, - orientation=parse_orientation(), + 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 build_scrollbar_code(scrollbar: ft.Scrollbar) -> str: - args: list[str] = [] - if scrollbar.thumb_visibility is not None: - args.append(f"thumb_visibility={scrollbar.thumb_visibility}") - if scrollbar.track_visibility is not None: - args.append(f"track_visibility={scrollbar.track_visibility}") - if scrollbar.interactive is not None: - args.append(f"interactive={scrollbar.interactive}") - if scrollbar.thickness is not None: - thickness = ( - int(scrollbar.thickness) - if float(scrollbar.thickness).is_integer() - else scrollbar.thickness - ) - args.append(f"thickness={thickness}") - if scrollbar.radius is not None: - radius = ( - int(scrollbar.radius) - if float(scrollbar.radius).is_integer() - else scrollbar.radius - ) - args.append(f"radius={radius}") - if scrollbar.orientation is not None: - args.append( - f"orientation=ft.ScrollbarOrientation.{scrollbar.orientation.name}" - ) - - if not args: - return "ft.Scrollbar()" - - return "ft.Scrollbar(\n" + "".join([f" {arg},\n" for arg in args]) + ")" - - def build_preview_content(scrollbar: ft.Scrollbar) -> tuple[str, ft.Control]: + def get_preview_content(scrollbar: ft.Scrollbar) -> tuple[str, ft.Control]: selected_orientation = scrollbar.orientation is_horizontal = selected_orientation in ( ft.ScrollbarOrientation.TOP, @@ -154,47 +58,16 @@ def build_preview_content(scrollbar: ft.Scrollbar) -> tuple[str, ft.Control]: ), ) - def mode_hint(scrollbar: ft.Scrollbar) -> str: - if scrollbar.thickness == 0: - return "Equivalent legacy mode: ScrollMode.HIDDEN" - - thumb = scrollbar.thumb_visibility - thickness = scrollbar.thickness - mobile_default_thickness = ( - 4.0 if page.platform.is_mobile() and not page.web else None - ) - - if thumb is True and thickness == mobile_default_thickness: - return "Equivalent legacy mode: ScrollMode.ALWAYS" - if ( - thumb == (not (page.platform.is_mobile() and not page.web)) - and thickness == mobile_default_thickness - ): - return "Equivalent legacy mode: ScrollMode.ADAPTIVE" - if thumb is None and thickness == mobile_default_thickness: - return "Equivalent legacy mode: ScrollMode.AUTO" - return "Custom configuration (no exact ScrollMode equivalent)" - - def update_preview(_=None): + def update_preview(): thickness_value.disabled = not use_thickness.value radius_value.disabled = not use_radius.value - scrollbar = build_scrollbar() - title, content = build_preview_content(scrollbar) + scrollbar = get_scrollbar() + title, content = get_preview_content(scrollbar) preview_title.value = title preview_viewport.content = content - code_preview.value = build_scrollbar_code(scrollbar) - current_mode_hint.value = mode_hint(scrollbar) page.update() - dropdowns = [thumb_visibility, track_visibility, interactive, orientation] - for c in dropdowns: - c.on_select = update_preview - - controls_with_on_change = [use_thickness, thickness_value, use_radius, radius_value] - for c in controls_with_on_change: - c.on_change = update_preview - page.add( ft.Text( "Interactive playground for the Scrollbar dataclass. Change each property " @@ -216,14 +89,73 @@ def update_preview(_=None): spacing=10, controls=[ ft.Text("Configuration", weight=ft.FontWeight.BOLD), - thumb_visibility, - track_visibility, - interactive, - orientation, - use_thickness, - thickness_value, - use_radius, - radius_value, + 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, + ), ], ), ), @@ -237,10 +169,13 @@ def update_preview(_=None): spacing=10, controls=[ ft.Text("Live preview", weight=ft.FontWeight.BOLD), - preview_title, - preview_viewport, - current_mode_hint, - code_preview, + 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, + ), ], ), ), From fe78563fce7829550879d08c53cb0665a6b996db Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Tue, 10 Mar 2026 23:13:43 +0100 Subject: [PATCH 11/11] delete test leftovers --- .github/workflows/macos-integration-tests.yml | 2 +- .../macos/align/align_inside_container.png | Bin 3733 -> 0 bytes .../golden/macos/align/align_inside_stack.png | Bin 9313 -> 0 bytes .../types/golden/macos/margin/margin_around.png | Bin 7373 -> 0 bytes .../golden/macos/margin/margin_bottom_right.png | Bin 7072 -> 0 bytes 5 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 sdk/python/packages/flet/integration_tests/controls/types/golden/macos/align/align_inside_container.png delete mode 100644 sdk/python/packages/flet/integration_tests/controls/types/golden/macos/align/align_inside_stack.png delete mode 100644 sdk/python/packages/flet/integration_tests/controls/types/golden/macos/margin/margin_around.png delete mode 100644 sdk/python/packages/flet/integration_tests/controls/types/golden/macos/margin/margin_bottom_right.png 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/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 9e5205da18f1bcd4c3af099cbaab0cb370bb847e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3733 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 Tvo47HNPxVw3b0c0%lH2QwBkyy 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 b660d2bf6665740e8cc15c8d612dbb80a3667635..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7072 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