From a1d77feb435d8d91778f6b9fce13c6ab640c452c Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Sat, 15 Aug 2026 17:36:04 +0200 Subject: [PATCH 01/13] update examples --- .../examples/apps/7guis/counter/main.py | 2 +- .../text_capitalization/showcase/main.py | 2 +- .../material/dropdown/icon_selection/main.py | 2 +- .../controls/material/dropdown/styled/main.py | 79 ++++++++++++++----- .../material/text_field/styled/main.py | 13 ++- .../underlined_and_borderless/main.py | 8 +- 6 files changed, 75 insertions(+), 31 deletions(-) diff --git a/sdk/python/examples/apps/7guis/counter/main.py b/sdk/python/examples/apps/7guis/counter/main.py index 3936f376ef..b7d4aaf5bc 100644 --- a/sdk/python/examples/apps/7guis/counter/main.py +++ b/sdk/python/examples/apps/7guis/counter/main.py @@ -43,7 +43,7 @@ def increment(e: ft.Event[ft.Button]): width=120, text_align=ft.TextAlign.RIGHT, bgcolor=ft.Colors.SURFACE, - border_radius=14, + border=ft.OutlineInputBorder(border_radius=14), ), ft.FilledButton( "Increment", diff --git a/sdk/python/examples/controls/core/types/text_capitalization/showcase/main.py b/sdk/python/examples/controls/core/types/text_capitalization/showcase/main.py index c0679c974a..c1dd378fd3 100644 --- a/sdk/python/examples/controls/core/types/text_capitalization/showcase/main.py +++ b/sdk/python/examples/controls/core/types/text_capitalization/showcase/main.py @@ -6,7 +6,7 @@ def showcase_card(cap: ft.TextCapitalization) -> ft.Container: width=260, label="Type here", capitalization=cap, - border=ft.InputBorder.OUTLINE, + border=ft.OutlineInputBorder(), ) return ft.Container( diff --git a/sdk/python/examples/controls/material/dropdown/icon_selection/main.py b/sdk/python/examples/controls/material/dropdown/icon_selection/main.py index 797320a766..2e8ccccefd 100644 --- a/sdk/python/examples/controls/material/dropdown/icon_selection/main.py +++ b/sdk/python/examples/controls/material/dropdown/icon_selection/main.py @@ -18,7 +18,7 @@ def get_options() -> list[ft.DropdownOption]: ft.SafeArea( content=ft.Dropdown( key="icon_dropdown", - border=ft.InputBorder.UNDERLINE, + border=ft.UnderlineInputBorder(), enable_filter=True, editable=True, leading_icon=ft.Icons.SEARCH, diff --git a/sdk/python/examples/controls/material/dropdown/styled/main.py b/sdk/python/examples/controls/material/dropdown/styled/main.py index 7c8014e49c..06843687e8 100644 --- a/sdk/python/examples/controls/material/dropdown/styled/main.py +++ b/sdk/python/examples/controls/material/dropdown/styled/main.py @@ -13,10 +13,18 @@ def main(page: ft.Page): color=ft.Colors.PURPLE_200, bgcolor=ft.Colors.BLUE_200, filled=True, - border_radius=30, - border_color=ft.Colors.GREEN_800, - focused_border_color=ft.Colors.GREEN_ACCENT_400, - focused_border_width=5, + border={ + ft.ControlState.DEFAULT: ft.OutlineInputBorder( + border_radius=30, + side=ft.BorderSide(color=ft.Colors.GREEN_800), + ), + ft.ControlState.FOCUSED: ft.OutlineInputBorder( + border_radius=30, + side=ft.BorderSide( + width=5, color=ft.Colors.GREEN_ACCENT_400 + ), + ), + }, options=[ ft.DropdownOption("a", "Style 1A"), ft.DropdownOption("b", "Style 1B"), @@ -25,14 +33,20 @@ def main(page: ft.Page): ), ft.Dropdown( key="styled_dropdown_2", - border_radius=30, + border={ + ft.ControlState.DEFAULT: ft.OutlineInputBorder( + border_radius=30, + side=ft.BorderSide(color=ft.Colors.TRANSPARENT), + ), + ft.ControlState.FOCUSED: ft.OutlineInputBorder( + border_radius=30, + side=ft.BorderSide(width=20, color=ft.Colors.PINK_300), + ), + }, filled=True, fill_color=ft.Colors.RED_400, - border_color=ft.Colors.TRANSPARENT, bgcolor=ft.Colors.RED_200, color=ft.Colors.CYAN_400, - focused_border_color=ft.Colors.PINK_300, - focused_border_width=20, options=[ ft.DropdownOption("a", "Style 2A"), ft.DropdownOption("b", "Style 2B"), @@ -41,12 +55,21 @@ def main(page: ft.Page): ), ft.Dropdown( key="styled_dropdown_3", - border_color=ft.Colors.PINK_ACCENT, - focused_border_color=ft.Colors.GREEN_ACCENT_400, - focused_border_width=25, - border_radius=30, + border={ + ft.ControlState.DEFAULT: ft.OutlineInputBorder( + border_radius=30, + side=ft.BorderSide( + width=5, color=ft.Colors.PINK_ACCENT + ), + ), + ft.ControlState.FOCUSED: ft.OutlineInputBorder( + border_radius=30, + side=ft.BorderSide( + width=25, color=ft.Colors.GREEN_ACCENT_400 + ), + ), + }, width=150, - border_width=5, options=[ ft.DropdownOption("a", "Style 3A"), ft.DropdownOption("b", "Style 3B"), @@ -59,12 +82,20 @@ def main(page: ft.Page): key="styled_dropdown_4", text_size=30, color=ft.Colors.ORANGE_ACCENT, - border_radius=20, + border={ + ft.ControlState.DEFAULT: ft.OutlineInputBorder( + border_radius=20, + side=ft.BorderSide.none(), + ), + ft.ControlState.FOCUSED: ft.OutlineInputBorder( + border_radius=20, + side=ft.BorderSide( + width=10, color=ft.Colors.GREEN_100 + ), + ), + }, filled=True, - border_width=0, autofocus=True, - focused_border_color=ft.Colors.GREEN_100, - focused_border_width=10, width=200, height=50, options=[ @@ -77,11 +108,17 @@ def main(page: ft.Page): ft.Dropdown( key="styled_dropdown_5", text_size=30, - border_radius=20, + border={ + ft.ControlState.DEFAULT: ft.OutlineInputBorder( + border_radius=20, + side=ft.BorderSide.none(), + ), + ft.ControlState.FOCUSED: ft.OutlineInputBorder( + border_radius=20, + side=ft.BorderSide(width=10, color=ft.Colors.GREEN_100), + ), + }, filled=True, - border_width=0, - focused_border_color=ft.Colors.GREEN_100, - focused_border_width=10, content_padding=20, width=200, options=[ diff --git a/sdk/python/examples/controls/material/text_field/styled/main.py b/sdk/python/examples/controls/material/text_field/styled/main.py index e45fa80469..a831786455 100644 --- a/sdk/python/examples/controls/material/text_field/styled/main.py +++ b/sdk/python/examples/controls/material/text_field/styled/main.py @@ -14,9 +14,16 @@ async def main(page: ft.Page): filled=True, focused_color=ft.Colors.GREEN, focused_bgcolor=ft.Colors.CYAN_200, - border_radius=30, - border_color=ft.Colors.GREEN_800, - focused_border_color=ft.Colors.GREEN_ACCENT_400, + border={ + ft.ControlState.DEFAULT: ft.OutlineInputBorder( + border_radius=30, + side=ft.BorderSide(color=ft.Colors.GREEN_800), + ), + ft.ControlState.FOCUSED: ft.OutlineInputBorder( + border_radius=30, + side=ft.BorderSide(width=2, color=ft.Colors.GREEN_ACCENT_400), + ), + }, max_length=20, capitalization=ft.TextCapitalization.CHARACTERS, ) diff --git a/sdk/python/examples/controls/material/text_field/underlined_and_borderless/main.py b/sdk/python/examples/controls/material/text_field/underlined_and_borderless/main.py index 48bfe8f685..12468e7ec8 100644 --- a/sdk/python/examples/controls/material/text_field/underlined_and_borderless/main.py +++ b/sdk/python/examples/controls/material/text_field/underlined_and_borderless/main.py @@ -9,26 +9,26 @@ def main(page: ft.Page): ft.TextField( key="underlined_field", label="Underlined", - border=ft.InputBorder.UNDERLINE, + border=ft.UnderlineInputBorder(), hint_text="Enter text here", ), ft.TextField( key="underlined_filled_field", label="Underlined filled", - border=ft.InputBorder.UNDERLINE, + border=ft.UnderlineInputBorder(), filled=True, hint_text="Enter text here", ), ft.TextField( key="borderless_field", label="Borderless", - border=ft.InputBorder.NONE, + border=ft.InputBorder.none(), hint_text="Enter text here", ), ft.TextField( key="borderless_filled_field", label="Borderless filled", - border=ft.InputBorder.NONE, + border=ft.InputBorder.none(), filled=True, hint_text="Enter text here", ), From 23383eb19f459d24ac19f65e5feec698d00cbdd2 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Sun, 16 Aug 2026 02:01:40 +0200 Subject: [PATCH 02/13] feat!: replace `InputBorder` enum with a class hierarchy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `InputBorder` was an enum (`OUTLINE`/`UNDERLINE`/`NONE`) paired with five loose properties on every form field: `border_radius`, `border_width`, `border_color`, `focused_border_width` and `focused_border_color`. That shape could not express Flutter's API — `gap_padding` was unavailable, `UnderlineInputBorder`'s corner radius was silently ignored, and the error and disabled borders could not be styled at all — and every new Flutter border property would have required another top-level property on each control. `InputBorder` is now a base class with `OutlineInputBorder`, `UnderlineInputBorder` and `InputBorder.none()`, mirroring Flutter's classes and their defaults. `FormFieldControl.border` and `Dropdown.border` accept either a single border or a `ControlState` dictionary, so the focused, error and disabled borders become stylable. The five loose properties are removed. Behavior changes that follow from the new shape: * A border with no explicit `side` defers to the Material theme per state instead of always painting black, which fixes dark mode and custom themes. * `DropdownM2` gains `menu_border_radius` for the open menu, which the shared `border_radius` used to shape alongside the field. * `CupertinoTextField` translates the value to its box decoration: `InputBorder.none()` now actually removes the border where the enum value was ignored, and an outline without a `side` keeps the native iOS hairline. The M3 `Dropdown` no longer duplicates the border-building logic: it shares `parseFormFieldBorders` with `buildInputDecoration`, which also populates the `errorBorder`, `focusedErrorBorder` and `disabledBorder` slots. --- .../lib/src/controls/cupertino_textfield.dart | 58 ++++-- packages/flet/lib/src/controls/dropdown.dart | 67 +------ .../flet/lib/src/controls/dropdownm2.dart | 2 +- packages/flet/lib/src/utils/form_field.dart | 168 +++++++++------- sdk/python/packages/flet/src/flet/__init__.py | 6 + .../controls/cupertino/cupertino_textfield.py | 6 + .../src/flet/controls/material/dropdown.py | 56 ++---- .../src/flet/controls/material/dropdownm2.py | 12 ++ .../controls/material/form_field_control.py | 185 +++++++++++++----- 9 files changed, 316 insertions(+), 244 deletions(-) diff --git a/packages/flet/lib/src/controls/cupertino_textfield.dart b/packages/flet/lib/src/controls/cupertino_textfield.dart index 66a3c4e564..6f29f7f751 100644 --- a/packages/flet/lib/src/controls/cupertino_textfield.dart +++ b/packages/flet/lib/src/controls/cupertino_textfield.dart @@ -217,29 +217,45 @@ class _CupertinoTextFieldControlState extends State { _selection = selection; } - var borderRadius = widget.control.getBorderRadius("border_radius"); + // CupertinoTextField takes a BoxDecoration, not an InputDecoration, so the + // InputBorder value is translated to a BoxBorder. Only the default-state + // entry of a state map is used. + var borderValue = widget.control.get("border"); + if (borderValue is Map && !borderValue.containsKey("_type")) { + borderValue = borderValue["default"]; + } + var explicitSide = borderValue is Map ? borderValue["side"] : null; + var borderSide = parseBorderSide(explicitSide, Theme.of(context), + defaultValue: const BorderSide())!; BoxBorder? border; - var borderWidth = widget.control.getDouble("border_width", 1.0)!; - var borderColor = widget.control.getColor("border_color", context) ?? - const Color(0xFF000000); - - try { - border = widget.control.getBorder("border", Theme.of(context)); - // adaptive TextField is being created - } catch (e) { - FormFieldInputBorder inputBorder = parseFormFieldInputBorder( - widget.control.getString("border"), - FormFieldInputBorder.outline, - )!; - - if (inputBorder == FormFieldInputBorder.outline) { - border = Border.all(color: borderColor, width: borderWidth); - } else if (inputBorder == FormFieldInputBorder.underline) { - border = - Border(bottom: BorderSide(color: borderColor, width: borderWidth)); - borderRadius = BorderRadius.zero; - } + BorderRadius? borderRadius; + switch (borderValue is Map ? borderValue["_type"] : "outline") { + case "underline": + border = Border(bottom: borderSide); + // The underline's default top-corner radius is treated as + // unconfigured, and a non-zero radius cannot be painted together + // with a hairline solid side. + var radius = parseBorderRadius(borderValue["border_radius"]); + borderRadius = radius == null || + radius == kUnderlineInputBorderDefaultRadius || + (borderSide.width == 0.0 && + borderSide.style == BorderStyle.solid) + ? BorderRadius.zero + : radius; + case "none": + // A nothing-painting border: copyWith() below cannot clear the default + // decoration's border, so replace it instead. + border = const Border.fromBorderSide(BorderSide.none); + default: + // Outline: without an explicit side, keep CupertinoTextField's native + // hairline border; an explicit radius still applies to it. + if (explicitSide != null) { + border = Border.fromBorderSide(borderSide); + } + borderRadius = borderValue is Map + ? parseBorderRadius(borderValue["border_radius"]) + : null; } var canRevealPassword = diff --git a/packages/flet/lib/src/controls/dropdown.dart b/packages/flet/lib/src/controls/dropdown.dart index 71e9d48e5e..9c07d7fbea 100644 --- a/packages/flet/lib/src/controls/dropdown.dart +++ b/packages/flet/lib/src/controls/dropdown.dart @@ -5,7 +5,6 @@ import 'package:flutter/services.dart'; import '../extensions/control.dart'; import '../models/control.dart'; -import '../utils/borders.dart'; import '../utils/buttons.dart'; import '../utils/colors.dart'; import '../utils/edge_insets.dart'; @@ -104,66 +103,11 @@ class _DropdownControlState extends State { var textAlign = widget.control.getTextAlign("text_align", TextAlign.start)!; var fillColor = widget.control.getColor("fill_color", context); - var borderColor = widget.control.getColor("border_color", context); - - var borderRadius = widget.control.getBorderRadius("border_radius"); - var focusedBorderColor = - widget.control.getColor("focused_border_color", context); - var borderWidth = widget.control.getDouble("border_width"); - var focusedBorderWidth = widget.control.getDouble("focused_border_width"); var menuWidth = widget.control.getDouble("menu_width"); var bgColor = widget.control.getWidgetStateColor("bgcolor", theme); var elevation = widget.control.getWidgetStateDouble("elevation"); - var inputBorder = widget.control - .getFormFieldInputBorder("border", FormFieldInputBorder.outline)!; - - InputBorder? border; - - if (inputBorder == FormFieldInputBorder.underline) { - border = UnderlineInputBorder( - borderSide: BorderSide( - color: borderColor ?? const Color(0xFF000000), - width: borderWidth ?? 1.0)); - } else if (inputBorder == FormFieldInputBorder.none) { - border = InputBorder.none; - } else if (inputBorder == FormFieldInputBorder.outline || - borderRadius != null || - borderColor != null || - borderWidth != null) { - border = OutlineInputBorder( - borderSide: BorderSide( - color: borderColor ?? const Color(0xFF000000), - width: borderWidth ?? 1.0)); - if (borderRadius != null) { - border = - (border as OutlineInputBorder).copyWith(borderRadius: borderRadius); - } - if (borderColor != null || borderWidth != null) { - border = (border as OutlineInputBorder).copyWith( - borderSide: borderWidth == 0 - ? BorderSide.none - : BorderSide( - color: borderColor ?? - theme.colorScheme.onSurface.withValues(alpha: 0.38), - width: borderWidth ?? 1.0)); - } - } - - InputBorder? focusedBorder; - if (borderColor != null || - borderWidth != null || - focusedBorderColor != null || - focusedBorderWidth != null) { - focusedBorder = border?.copyWith( - borderSide: borderWidth == 0 - ? BorderSide.none - : BorderSide( - color: focusedBorderColor ?? - borderColor ?? - theme.colorScheme.primary, - width: focusedBorderWidth ?? borderWidth ?? 2.0)); - } + var borders = parseFormFieldBorders(widget.control, theme); InputDecorationTheme inputDecorationTheme = InputDecorationTheme( filled: widget.control.getBool("filled", false)!, @@ -171,9 +115,12 @@ class _DropdownControlState extends State { hintStyle: widget.control.getTextStyle("hint_style", theme), errorStyle: widget.control.getTextStyle("error_style", theme), helperStyle: widget.control.getTextStyle("helper_style", theme), - border: border, - enabledBorder: border, - focusedBorder: focusedBorder, + border: borders.border, + enabledBorder: borders.enabledBorder, + focusedBorder: borders.focusedBorder, + errorBorder: borders.errorBorder, + focusedErrorBorder: borders.focusedErrorBorder, + disabledBorder: borders.disabledBorder, isDense: widget.control.getBool("dense", false)!, contentPadding: widget.control.getPadding("content_padding"), ); diff --git a/packages/flet/lib/src/controls/dropdownm2.dart b/packages/flet/lib/src/controls/dropdownm2.dart index b1437a9b3a..1418e53442 100644 --- a/packages/flet/lib/src/controls/dropdownm2.dart +++ b/packages/flet/lib/src/controls/dropdownm2.dart @@ -138,7 +138,7 @@ class _DropdownM2ControlState extends State { iconDisabledColor: widget.control.getColor("select_icon_disabled_color", context), iconSize: widget.control.getDouble("select_icon_size", 24.0)!, - borderRadius: widget.control.getBorderRadius("border_radius"), + borderRadius: widget.control.getBorderRadius("menu_border_radius"), alignment: widget.control.getAlignment("alignment") ?? AlignmentDirectional.centerStart, isExpanded: widget.control.getBool("options_fill_horizontally", true)!, diff --git a/packages/flet/lib/src/utils/form_field.dart b/packages/flet/lib/src/utils/form_field.dart index 26aa00aa4e..6b24ef954a 100644 --- a/packages/flet/lib/src/utils/form_field.dart +++ b/packages/flet/lib/src/utils/form_field.dart @@ -1,6 +1,5 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'enums.dart'; import '../extensions/control.dart'; import '../models/control.dart'; @@ -8,15 +7,101 @@ import '../utils/colors.dart'; import 'borders.dart'; import 'box.dart'; import 'edge_insets.dart'; +import 'enums.dart'; import 'numbers.dart'; import 'text.dart'; import 'time.dart'; -enum FormFieldInputBorder { outline, underline, none } +/// [UnderlineInputBorder]'s default corner radius, mirroring both Flutter's +/// and the Python-side default. +const BorderRadius kUnderlineInputBorderDefaultRadius = BorderRadius.only( + topLeft: Radius.circular(4.0), topRight: Radius.circular(4.0)); -FormFieldInputBorder? parseFormFieldInputBorder(String? value, - [FormFieldInputBorder? defaultValue]) { - return parseEnum(FormFieldInputBorder.values, value, defaultValue); +InputBorder? parseInputBorder(dynamic value, ThemeData? theme, + {BorderSide? defaultSide, InputBorder? defaultValue}) { + if (value is! Map) return defaultValue; + var side = parseBorderSide(value["side"], theme, defaultValue: defaultSide); + switch (value["_type"]) { + case "underline": + return UnderlineInputBorder( + borderSide: side ?? const BorderSide(), + borderRadius: parseBorderRadius( + value["border_radius"], kUnderlineInputBorderDefaultRadius)!); + case "outline": + return OutlineInputBorder( + borderSide: side ?? const BorderSide(), + borderRadius: parseBorderRadius(value["border_radius"], + const BorderRadius.all(Radius.circular(4.0)))!, + gapPadding: parseDouble(value["gap_padding"], 4.0)!); + case "none": + return InputBorder.none; + default: + return defaultValue; + } +} + +/// Per-state input borders parsed from a control's "border" property, mapped +/// onto the [InputDecoration]/[InputDecorationTheme] border slots. +class FormFieldBorders { + InputBorder? border; + InputBorder? enabledBorder; + InputBorder? focusedBorder; + InputBorder? errorBorder; + InputBorder? focusedErrorBorder; + InputBorder? disabledBorder; +} + +/// Parses the "border" property of [control] — either a single border or a +/// map of control states ("default", "focused", "error", "disabled") to +/// borders — into [FormFieldBorders]. +/// +/// The default/single border defines the shape for all states. When its side +/// is unset, only [FormFieldBorders.border] is populated with it, so the +/// Material theme keeps resolving the border side per state; an explicit side +/// additionally populates [FormFieldBorders.enabledBorder]. A state entry +/// without a side falls back to the default entry's side, then to a themed +/// side for that state. +FormFieldBorders parseFormFieldBorders(Control control, ThemeData theme) { + var borders = FormFieldBorders(); + var value = control.get("border"); + + Map? stateMap; + dynamic defaultEntry = value; + if (value is Map && !value.containsKey("_type")) { + stateMap = value; + defaultEntry = value["default"]; + } + + var defaultSide = + defaultEntry is Map ? parseBorderSide(defaultEntry["side"], theme) : null; + var defaultBorder = parseInputBorder(defaultEntry, theme, + defaultValue: const OutlineInputBorder())!; + borders.border = defaultBorder; + if (defaultSide != null || defaultBorder == InputBorder.none) { + borders.enabledBorder = defaultBorder; + } + + if (stateMap != null) { + InputBorder? stateBorder(String stateName, BorderSide themedSide) { + return parseInputBorder(stateMap![stateName], theme, + defaultSide: defaultSide ?? themedSide); + } + + borders.focusedBorder = stateBorder( + "focused", BorderSide(color: theme.colorScheme.primary, width: 2.0)); + borders.errorBorder = stateBorder( + "error", BorderSide(color: theme.colorScheme.error, width: 1.0)); + // The "error" entry also covers the focused-error state, at Material's + // focused weight when no side is configured. + borders.focusedErrorBorder = stateBorder( + "error", BorderSide(color: theme.colorScheme.error, width: 2.0)); + borders.disabledBorder = stateBorder( + "disabled", + BorderSide( + color: theme.colorScheme.onSurface.withValues(alpha: 0.12), + width: 1.0)); + } + return borders; } TextInputType? parseTextInputType(String? value, @@ -47,19 +132,11 @@ InputDecoration buildInputDecoration( int? maxLength, bool focused = false, }) { - FormFieldInputBorder inputBorder = parseFormFieldInputBorder( - control.getString("border"), - FormFieldInputBorder.outline, - )!; + var borders = parseFormFieldBorders(control, Theme.of(context)); var bgcolor = control.getColor("bgcolor", context); var focusedBgcolor = control.getColor("focused_bgcolor", context); var fillColor = control.getColor("fill_color", context); var hoverColor = control.getColor("hover_color", context); - var borderColor = control.getColor("border_color", context); - var borderRadius = control.getBorderRadius("border_radius"); - var focusedBorderColor = control.getColor("focused_border_color", context); - var borderWidth = control.getDouble("border_width"); - var focusedBorderWidth = control.getDouble("focused_border_width"); //counter String? counterText; @@ -115,64 +192,18 @@ InputDecoration buildInputDecoration( suffixText = control.getString("suffix"); } - InputBorder? border; - if (inputBorder == FormFieldInputBorder.underline) { - border = UnderlineInputBorder( - borderSide: BorderSide( - color: borderColor ?? const Color(0xFF000000), - width: borderWidth ?? 1.0)); - } else if (inputBorder == FormFieldInputBorder.none) { - border = InputBorder.none; - } else if (inputBorder == FormFieldInputBorder.outline || - borderRadius != null || - borderColor != null || - borderWidth != null) { - border = OutlineInputBorder( - borderSide: BorderSide( - color: borderColor ?? const Color(0xFF000000), - width: borderWidth ?? 1.0)); - if (borderRadius != null) { - border = - (border as OutlineInputBorder).copyWith(borderRadius: borderRadius); - } - if (borderColor != null || borderWidth != null) { - border = (border as OutlineInputBorder).copyWith( - borderSide: borderWidth == 0 - ? BorderSide.none - : BorderSide( - color: borderColor ?? - Theme.of(context) - .colorScheme - .onSurface - .withAlpha((255.0 * 0.38).round()), - width: borderWidth ?? 1.0)); - } - } - - InputBorder? focusedBorder; - if (borderColor != null || - borderWidth != null || - focusedBorderColor != null || - focusedBorderWidth != null) { - focusedBorder = border?.copyWith( - borderSide: borderWidth == 0 - ? BorderSide.none - : BorderSide( - color: focusedBorderColor ?? - borderColor ?? - Theme.of(context).colorScheme.primary, - width: focusedBorderWidth ?? borderWidth ?? 2.0)); - } - return InputDecoration( enabled: !control.disabled, contentPadding: control.getEdgeInsets("content_padding"), isDense: control.getBool("dense"), label: control.buildTextOrWidget("label"), labelStyle: control.getTextStyle("label_style", Theme.of(context)), - border: border, - enabledBorder: border, - focusedBorder: focusedBorder, + border: borders.border, + enabledBorder: borders.enabledBorder, + focusedBorder: borders.focusedBorder, + errorBorder: borders.errorBorder, + focusedErrorBorder: borders.focusedErrorBorder, + disabledBorder: borders.disabledBorder, hoverColor: hoverColor, icon: control.buildIconOrWidget("icon"), filled: control.getBool("filled", false)!, @@ -236,11 +267,6 @@ StrutStyle? parseStrutStyle(dynamic value, [StrutStyle? defaultValue]) { } extension FormFieldParsers on Control { - FormFieldInputBorder? getFormFieldInputBorder(String propertyName, - [FormFieldInputBorder? defaultValue]) { - return parseFormFieldInputBorder(get(propertyName), defaultValue); - } - TextInputType? getTextInputType(String propertyName, [TextInputType? defaultValue]) { return parseTextInputType(get(propertyName), defaultValue); diff --git a/sdk/python/packages/flet/src/flet/__init__.py b/sdk/python/packages/flet/src/flet/__init__.py index c534e26a23..11541090e2 100644 --- a/sdk/python/packages/flet/src/flet/__init__.py +++ b/sdk/python/packages/flet/src/flet/__init__.py @@ -412,6 +412,8 @@ from flet.controls.material.form_field_control import ( FormFieldControl, InputBorder, + OutlineInputBorder, + UnderlineInputBorder, ) from flet.controls.material.icon_button import ( FilledIconButton, @@ -1031,6 +1033,7 @@ "OnReorderEvent", "OnScrollEvent", "Orientation", + "OutlineInputBorder", "OutlinedBorder", "OutlinedButton", "OutlinedButtonTheme", @@ -1195,6 +1198,7 @@ "TooltipValue", "Transform", "TransparentPointer", + "UnderlineInputBorder", "UnderlineTabIndicator", "Url", "UrlLauncher", @@ -1565,6 +1569,7 @@ "OnReorderEvent": "flet.controls.material.reorderable_list_view", "OnScrollEvent": "flet.controls.scrollable_control", "Orientation": "flet.controls.types", + "OutlineInputBorder": "flet.controls.material.form_field_control", "OutlinedBorder": "flet.controls.buttons", "OutlinedButton": "flet.controls.material.outlined_button", "OutlinedButtonTheme": "flet.controls.theme", @@ -1729,6 +1734,7 @@ "TooltipValue": "flet.controls.material.tooltip", "Transform": "flet.controls.transform", "TransparentPointer": "flet.controls.core.transparent_pointer", + "UnderlineInputBorder": "flet.controls.material.form_field_control", "UnderlineTabIndicator": "flet.controls.material.tabs", "Url": "flet.controls.types", "UrlLauncher": "flet.controls.services.url_launcher", diff --git a/sdk/python/packages/flet/src/flet/controls/cupertino/cupertino_textfield.py b/sdk/python/packages/flet/src/flet/controls/cupertino/cupertino_textfield.py index f647a6fcfd..f2976dc5e7 100644 --- a/sdk/python/packages/flet/src/flet/controls/cupertino/cupertino_textfield.py +++ b/sdk/python/packages/flet/src/flet/controls/cupertino/cupertino_textfield.py @@ -53,6 +53,12 @@ class CupertinoTextField(TextField): ```python ft.CupertinoTextField(placeholder_text="Search") ``` + + Note: + This control draws its :attr:`~flet.FormFieldControl.border` as a + static box decoration. In the :class:`~flet.ControlState` dictionary + form, only the `DEFAULT` entry is used — there are no focused, error, + or disabled border variants. """ placeholder_text: Optional[str] = None diff --git a/sdk/python/packages/flet/src/flet/controls/material/dropdown.py b/sdk/python/packages/flet/src/flet/controls/material/dropdown.py index f84e23327c..259ee899a3 100644 --- a/sdk/python/packages/flet/src/flet/controls/material/dropdown.py +++ b/sdk/python/packages/flet/src/flet/controls/material/dropdown.py @@ -2,13 +2,12 @@ from typing import Optional from flet.controls.base_control import control -from flet.controls.border_radius import BorderRadiusValue from flet.controls.buttons import ButtonStyle from flet.controls.control import Control from flet.controls.control_event import ControlEventHandler from flet.controls.control_state import ControlStateValue from flet.controls.layout_control import LayoutControl -from flet.controls.material.form_field_control import InputBorder +from flet.controls.material.form_field_control import InputBorder, OutlineInputBorder from flet.controls.material.menu_bar import MenuStyle from flet.controls.material.textfield import InputFilter, TextCapitalization from flet.controls.padding import PaddingValue @@ -284,49 +283,32 @@ class Dropdown(LayoutControl): The :attr:`label`'s text style. """ - border: Optional[InputBorder] = None + border: ControlStateValue[InputBorder] = field(default_factory=OutlineInputBorder) """ - Border around input. + The border drawn around the decorated input area. - Defaults to `InputBorder.OUTLINE`. - """ - - color: Optional[ColorValue] = None - """ - Text color. - """ - - border_width: Number = 1 - """ - The width of the border in virtual pixels. - - Tip: - Set to `0` to completely remove the border. - """ + Accepts a single :class:`~flet.InputBorder` or a dictionary mapping + :class:`~flet.ControlState`s to :class:`~flet.InputBorder`s. Supported + state keys are :attr:`flet.ControlState.DEFAULT`, + :attr:`flet.ControlState.FOCUSED`, :attr:`flet.ControlState.ERROR`, + and :attr:`flet.ControlState.DISABLED`. - border_color: Optional[ColorValue] = None - """ - Border color. + A single border defines the shape for all states. If its `side` is unset, + the Material theme resolves the border color and weight per state (for + example, the focused border uses the theme's primary color); an explicit + `side` applies to the enabled state while the other states remain + theme-resolved. - Tip: - Set to :attr:`flet.Colors.TRANSPARENT` to hide the border. - """ + In the dictionary form, the `DEFAULT` entry behaves like the single form, + and each state entry without an explicit `side` falls back to the `DEFAULT` + entry's `side`, if set. - border_radius: Optional[BorderRadiusValue] = None - """ - The border radius applied to the corners of the dropdown input field. - Accepts a value in virtual pixels or a `BorderRadiusValue` object. - If set to `None`, the default border radius defined by the theme or system is used. + Defaults to `OutlineInputBorder()`. """ - focused_border_width: Optional[Number] = None - """ - Border width in focused state. - """ - - focused_border_color: Optional[ColorValue] = None + color: Optional[ColorValue] = None """ - Border color in focused state. + Text color. """ content_padding: Optional[PaddingValue] = None diff --git a/sdk/python/packages/flet/src/flet/controls/material/dropdownm2.py b/sdk/python/packages/flet/src/flet/controls/material/dropdownm2.py index 52da53640f..0e35d0f5ea 100644 --- a/sdk/python/packages/flet/src/flet/controls/material/dropdownm2.py +++ b/sdk/python/packages/flet/src/flet/controls/material/dropdownm2.py @@ -2,6 +2,7 @@ from flet.controls.alignment import Alignment from flet.controls.base_control import control +from flet.controls.border_radius import BorderRadiusValue from flet.controls.control import Control from flet.controls.control_event import ControlEventHandler from flet.controls.material.form_field_control import FormFieldControl @@ -142,6 +143,17 @@ class DropdownM2(FormFieldControl): The dropdown's elevation. """ + menu_border_radius: Optional[BorderRadiusValue] = None + """ + The radii of the open dropdown menu's rounded rectangle shape. + + If `None` (the default), the menu uses its default shape. + + Note: + This shapes the menu only; the input field's border is configured + through :attr:`border`. + """ + item_height: Optional[Number] = None """ The height of the items/options in the dropdown menu. diff --git a/sdk/python/packages/flet/src/flet/controls/material/form_field_control.py b/sdk/python/packages/flet/src/flet/controls/material/form_field_control.py index d4a5271476..f7f72fa158 100644 --- a/sdk/python/packages/flet/src/flet/controls/material/form_field_control.py +++ b/sdk/python/packages/flet/src/flet/controls/material/form_field_control.py @@ -1,9 +1,11 @@ -from enum import Enum +from dataclasses import field from typing import Optional, Union -from flet.controls.base_control import control -from flet.controls.border_radius import BorderRadiusValue +from flet.controls.base_control import control, value +from flet.controls.border import BorderSide +from flet.controls.border_radius import BorderRadius, BorderRadiusValue from flet.controls.box import BoxConstraints +from flet.controls.control_state import ControlStateValue from flet.controls.duration import DurationValue from flet.controls.layout_control import LayoutControl from flet.controls.padding import PaddingValue @@ -16,29 +18,122 @@ VerticalAlignment, ) +__all__ = [ + "FormFieldControl", + "InputBorder", + "OutlineInputBorder", + "UnderlineInputBorder", +] -class InputBorder(Enum): + +@value +class InputBorder: """ - Border styles supported by :class:`~flet.FormFieldControl`. + Base class for borders drawn around the decorated input area of + :class:`~flet.FormFieldControl`s. Not intended to be used directly. + + See subclasses/implementations: - These values select the border style drawn around the decorated input area. + - :class:`~flet.OutlineInputBorder` + - :class:`~flet.UnderlineInputBorder` + - :meth:`flet.InputBorder.none` """ - NONE = "none" + _type: Optional[str] = field(init=False, repr=False, compare=False, default=None) + + def __post_init__(self): + raise TypeError( + "InputBorder is not intended to be instantiated directly; use " + "OutlineInputBorder, UnderlineInputBorder, or InputBorder.none()." + ) + + @staticmethod + def none() -> "InputBorder": + """ + A border that draws nothing around the decoration's container, + mirroring Flutter's `InputBorder.none`. + """ + return _NoInputBorder() + + +@value +class UnderlineInputBorder(InputBorder): """ - Draws no border around the decoration's container. + Draws a horizontal line along the bottom edge of the decoration's container. """ - OUTLINE = "outline" + side: Optional[BorderSide] = None """ - Draws a border around all sides of the decoration's container. + The color and weight of the underline. + + If `None` (the default), the color and weight are resolved by the Material + theme for each interactive state — for example, the focused underline uses + the theme's primary color. An explicit `side` applies to the enabled state; + provide per-state borders on :attr:`flet.FormFieldControl.border` for full control. """ - UNDERLINE = "underline" + border_radius: BorderRadiusValue = field( + default_factory=lambda: BorderRadius.only(top_left=4, top_right=4) + ) """ - Draws a horizontal line along the bottom edge of the decoration's container. + The radii of the container's corners. + + Only the top corners are rounded by default. The radius shapes/clips the + fill of the decoration's container (visible when + :attr:`flet.FormFieldControl.filled` is `True`); the drawn border remains the + bottom line. + """ + + def __post_init__(self): + self._type = "underline" + + +@value +class OutlineInputBorder(InputBorder): + """ + Draws a rounded rectangle around all sides of the decoration's container. + """ + + side: Optional[BorderSide] = None + """ + The color and weight of the border line. + + If `None` (the default), the color and weight are resolved by the Material + theme for each interactive state — for example, the focused border uses the + theme's primary color. An explicit `side` applies to the enabled state; + provide per-state borders on :attr:`flet.FormFieldControl.border` for full control. + """ + + border_radius: BorderRadiusValue = 4 + """ + The radii of the border's rounded rectangle corners. + + Defaults to `4` — all corners rounded. + """ + + gap_padding: Number = 4.0 + """ + Horizontal padding on either side of the border's gap cut out for the + floating :attr:`flet.FormFieldControl.label`. + + Must be non-negative. Defaults to `4.0`. + """ + + def __post_init__(self): + self._type = "outline" + + +@value +class _NoInputBorder(InputBorder): + """ + Draws no border around the decoration's container. + + Use :meth:`flet.InputBorder.none` to obtain an instance. """ + def __post_init__(self): + self._type = "none" + @control(kw_only=True) class FormFieldControl(LayoutControl): @@ -90,9 +185,27 @@ class FormFieldControl(LayoutControl): The icon to show before the input field and outside of the decoration's container. """ - border: InputBorder = InputBorder.OUTLINE + border: ControlStateValue[InputBorder] = field(default_factory=OutlineInputBorder) """ - Border around input. + The border drawn around the decorated input area. + + Accepts a single :class:`~flet.InputBorder` or a dictionary mapping + :class:`~flet.ControlState`s to :class:`~flet.InputBorder`s. Supported + state keys are :attr:`flet.ControlState.DEFAULT`, + :attr:`flet.ControlState.FOCUSED`, :attr:`flet.ControlState.ERROR`, + and :attr:`flet.ControlState.DISABLED`. + + A single border defines the shape for all states. If its `side` is unset, + the Material theme resolves the border color and weight per state (for + example, the focused border uses the theme's primary color); an explicit + `side` applies to the enabled state while the other states remain + theme-resolved. + + In the dictionary form, the `DEFAULT` entry behaves like the single form, + and each state entry without an explicit `side` falls back to the `DEFAULT` + entry's `side`, if set. + + Defaults to `OutlineInputBorder()`. """ color: Optional[ColorValue] = None @@ -108,33 +221,6 @@ class FormFieldControl(LayoutControl): Will not be visible if :attr:`filled` is `False`. """ - border_radius: Optional[BorderRadiusValue] = None - """ - Rounds the corners of the outlined decoration border. - - Note: - This is applied when :attr:`border` uses an outlined - border. Underline and borderless variants do not visibly use this radius. - """ - - border_width: Optional[Number] = None - """ - The width of the border in virtual pixels. - - Defaults to `1`. - - Tip: - Set to `0` to completely remove the border. - """ - - border_color: Optional[ColorValue] = None - """ - The border color. - - Tip: - Set to :attr:`flet.Colors.TRANSPARENT` to invisible/hide the border. - """ - focused_color: Optional[ColorValue] = None """ The text's color when focused. @@ -148,16 +234,6 @@ class FormFieldControl(LayoutControl): Will not be visible if :attr:`filled` is `False`. """ - focused_border_width: Optional[Number] = None - """ - Border width in focused state. - """ - - focused_border_color: Optional[ColorValue] = None - """ - Border color in focused state. - """ - content_padding: Optional[PaddingValue] = None """ The padding for the input decoration's container. @@ -191,9 +267,10 @@ class FormFieldControl(LayoutControl): Note: Text fields usually indicate focus by changing the focused border instead of - the fill color. In Flet, prefer :attr:`focused_bgcolor` and - :attr:`focused_border_color` when you need explicit - focused-state styling. + the fill color. In Flet, prefer :attr:`focused_bgcolor` and a focused + :attr:`border` state (e.g. + `border={ft.ControlState.FOCUSED: ft.OutlineInputBorder(...)}`) + when you need explicit focused-state styling. """ align_label_with_hint: Optional[bool] = None From e8c7e91ae1d5a86df0299f7f5468478d1b6511c2 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Sun, 16 Aug 2026 02:01:56 +0200 Subject: [PATCH 03/13] docs: InputBorder migration guide, type pages and examples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the 1.0.0 breaking-change guide for the `InputBorder` class hierarchy, covering the border styles, corner radius, per-state borders, the `DropdownM2` menu radius split, and code that reads or compares borders rather than setting them — `InputBorder` is no longer an enum, so comparisons against its members raise. Splits the former single `InputBorder` type page into a union index plus `OutlineInputBorder` and `UnderlineInputBorder` pages, following the `OutlinedBorder` layout. `NoInputBorder` is deliberately absent: the class is private and reached through `InputBorder.none()`. New example apps: `types/input_border/showcase` for the three border styles, `types/input_border/styling` for custom sides, radii and per-state borders, and `material/dropdownm2/styling` showing the field border and the menu radius side by side. `DropdownM2` had no examples before. Release notes gain a 1.0.x section and the missing 0.86.x patch entries. --- .../core/types/input_border/showcase/main.py | 38 +++ .../input_border/showcase/pyproject.toml | 26 ++ .../core/types/input_border/styling/main.py | 58 +++++ .../types/input_border/styling/pyproject.toml | 26 ++ .../material/dropdownm2/styling/main.py | 43 ++++ .../dropdownm2/styling/pyproject.toml | 26 ++ website/docs/controls/dropdownm2.md | 9 +- website/docs/types/inputborder.md | 7 - website/docs/types/inputborder/index.md | 18 ++ website/docs/types/outlineinputborder.md | 7 + website/docs/types/underlineinputborder.md | 7 + .../docs/updates/breaking-changes/index.md | 6 + .../v1-0-0/inputborder-class-hierarchy.md | 242 ++++++++++++++++++ website/docs/updates/release-notes.md | 9 + website/sidebars.yml | 7 +- 15 files changed, 520 insertions(+), 9 deletions(-) create mode 100644 sdk/python/examples/controls/core/types/input_border/showcase/main.py create mode 100644 sdk/python/examples/controls/core/types/input_border/showcase/pyproject.toml create mode 100644 sdk/python/examples/controls/core/types/input_border/styling/main.py create mode 100644 sdk/python/examples/controls/core/types/input_border/styling/pyproject.toml create mode 100644 sdk/python/examples/controls/material/dropdownm2/styling/main.py create mode 100644 sdk/python/examples/controls/material/dropdownm2/styling/pyproject.toml delete mode 100644 website/docs/types/inputborder.md create mode 100644 website/docs/types/inputborder/index.md create mode 100644 website/docs/types/outlineinputborder.md create mode 100644 website/docs/types/underlineinputborder.md create mode 100644 website/docs/updates/breaking-changes/v1-0-0/inputborder-class-hierarchy.md diff --git a/sdk/python/examples/controls/core/types/input_border/showcase/main.py b/sdk/python/examples/controls/core/types/input_border/showcase/main.py new file mode 100644 index 0000000000..3f75b8a6ac --- /dev/null +++ b/sdk/python/examples/controls/core/types/input_border/showcase/main.py @@ -0,0 +1,38 @@ +import flet as ft + + +def main(page: ft.Page): + page.add( + ft.SafeArea( + content=ft.Column( + controls=[ + ft.TextField( + label="Outline", + border=ft.OutlineInputBorder(), + hint_text="The default border", + ), + ft.TextField( + label="Underline", + border=ft.UnderlineInputBorder(), + hint_text="A line along the bottom edge", + ), + ft.TextField( + label="Underline filled", + border=ft.UnderlineInputBorder(), + filled=True, + hint_text="The radius rounds the fill's top corners", + ), + ft.TextField( + label="None", + border=ft.InputBorder.none(), + filled=True, + hint_text="Draws no border at all", + ), + ], + ), + ), + ) + + +if __name__ == "__main__": + ft.run(main) diff --git a/sdk/python/examples/controls/core/types/input_border/showcase/pyproject.toml b/sdk/python/examples/controls/core/types/input_border/showcase/pyproject.toml new file mode 100644 index 0000000000..9f534f8e31 --- /dev/null +++ b/sdk/python/examples/controls/core/types/input_border/showcase/pyproject.toml @@ -0,0 +1,26 @@ +[project] +name = "types-input-border-showcase" +version = "1.0.0" +description = "Compares the InputBorder styles by applying them to TextFields." +requires-python = ">=3.10" +keywords = ["input border", "types", "text field", "showcase"] +authors = [{ name = "Flet team", email = "hello@flet.dev" }] +dependencies = ["flet"] + +[dependency-groups] +dev = ["flet-cli", "flet-desktop", "flet-web"] + +[tool.flet.gallery] +categories = ["Input/TextField"] + +[tool.flet.metadata] +title = "Input border showcase" +controls = ["SafeArea", "Column", "TextField"] +layout_pattern = "gallery" +complexity = "basic" +features = ["outline border", "underline border", "borderless"] + +[tool.flet] +org = "dev.flet" +company = "Flet" +copyright = "Copyright (C) 2023-2026 by Flet" diff --git a/sdk/python/examples/controls/core/types/input_border/styling/main.py b/sdk/python/examples/controls/core/types/input_border/styling/main.py new file mode 100644 index 0000000000..89b9b7c9ff --- /dev/null +++ b/sdk/python/examples/controls/core/types/input_border/styling/main.py @@ -0,0 +1,58 @@ +import flet as ft + + +def main(page: ft.Page): + page.add( + ft.SafeArea( + content=ft.Column( + controls=[ + ft.TextField( + label="Custom outline", + border=ft.OutlineInputBorder( + border_radius=12, + gap_padding=8, + side=ft.BorderSide(width=2, color=ft.Colors.TEAL), + ), + ), + ft.TextField( + label="Custom underline", + border=ft.UnderlineInputBorder( + border_radius=ft.BorderRadius.only( + top_left=12, top_right=12 + ), + side=ft.BorderSide(width=3, color=ft.Colors.DEEP_ORANGE), + ), + filled=True, + ), + ft.TextField( + label="Per-state borders", + hint_text="Focus me", + border={ + ft.ControlState.DEFAULT: ft.OutlineInputBorder( + border_radius=20, + side=ft.BorderSide(color=ft.Colors.BLUE_GREY_400), + ), + ft.ControlState.FOCUSED: ft.OutlineInputBorder( + border_radius=20, + side=ft.BorderSide(width=3, color=ft.Colors.INDIGO), + ), + }, + ), + ft.TextField( + label="Error border", + error="This value is required", + border={ + ft.ControlState.DEFAULT: ft.OutlineInputBorder(), + ft.ControlState.ERROR: ft.UnderlineInputBorder( + side=ft.BorderSide(width=3, color=ft.Colors.PINK), + ), + }, + ), + ], + ), + ), + ) + + +if __name__ == "__main__": + ft.run(main) diff --git a/sdk/python/examples/controls/core/types/input_border/styling/pyproject.toml b/sdk/python/examples/controls/core/types/input_border/styling/pyproject.toml new file mode 100644 index 0000000000..5945184db6 --- /dev/null +++ b/sdk/python/examples/controls/core/types/input_border/styling/pyproject.toml @@ -0,0 +1,26 @@ +[project] +name = "types-input-border-styling" +version = "1.0.0" +description = "Styles input borders with custom sides, corner radii, and per-state borders." +requires-python = ">=3.10" +keywords = ["input border", "types", "text field", "border side", "control state"] +authors = [{ name = "Flet team", email = "hello@flet.dev" }] +dependencies = ["flet"] + +[dependency-groups] +dev = ["flet-cli", "flet-desktop", "flet-web"] + +[tool.flet.gallery] +categories = ["Input/TextField"] + +[tool.flet.metadata] +title = "Input border styling" +controls = ["SafeArea", "Column", "TextField"] +layout_pattern = "gallery" +complexity = "basic" +features = ["custom border side", "corner radius", "per-state borders"] + +[tool.flet] +org = "dev.flet" +company = "Flet" +copyright = "Copyright (C) 2023-2026 by Flet" diff --git a/sdk/python/examples/controls/material/dropdownm2/styling/main.py b/sdk/python/examples/controls/material/dropdownm2/styling/main.py new file mode 100644 index 0000000000..6f1fdd5878 --- /dev/null +++ b/sdk/python/examples/controls/material/dropdownm2/styling/main.py @@ -0,0 +1,43 @@ +import flet as ft + + +def main(page: ft.Page): + page.add( + ft.SafeArea( + content=ft.Column( + controls=[ + ft.DropdownM2( + label="Matching field and menu", + value="a", + border=ft.OutlineInputBorder( + border_radius=20, + side=ft.BorderSide(width=2, color=ft.Colors.TEAL), + ), + menu_border_radius=20, + options=[ + ft.dropdownm2.Option("a", "Alice"), + ft.dropdownm2.Option("b", "Bob"), + ft.dropdownm2.Option("c", "Carol"), + ], + ), + ft.DropdownM2( + label="Underlined field, rounded menu", + value="a", + border=ft.UnderlineInputBorder(), + menu_border_radius=ft.BorderRadius.only( + bottom_left=16, bottom_right=16 + ), + options=[ + ft.dropdownm2.Option("a", "Alice"), + ft.dropdownm2.Option("b", "Bob"), + ft.dropdownm2.Option("c", "Carol"), + ], + ), + ], + ), + ), + ) + + +if __name__ == "__main__": + ft.run(main) diff --git a/sdk/python/examples/controls/material/dropdownm2/styling/pyproject.toml b/sdk/python/examples/controls/material/dropdownm2/styling/pyproject.toml new file mode 100644 index 0000000000..d3801e9fca --- /dev/null +++ b/sdk/python/examples/controls/material/dropdownm2/styling/pyproject.toml @@ -0,0 +1,26 @@ +[project] +name = "dropdownm2-styling" +version = "1.0.0" +description = "Styles the DropdownM2 input field border and the open menu shape independently." +requires-python = ">=3.10" +keywords = ["dropdownm2", "input border", "menu border radius", "styling"] +authors = [{ name = "Flet team", email = "hello@flet.dev" }] +dependencies = ["flet"] + +[dependency-groups] +dev = ["flet-cli", "flet-desktop", "flet-web"] + +[tool.flet.gallery] +categories = ["Input/Dropdown"] + +[tool.flet.metadata] +title = "DropdownM2 styling" +controls = ["SafeArea", "Column", "DropdownM2"] +layout_pattern = "gallery" +complexity = "basic" +features = ["input border", "menu border radius"] + +[tool.flet] +org = "dev.flet" +company = "Flet" +copyright = "Copyright (C) 2023-2026 by Flet" diff --git a/website/docs/controls/dropdownm2.md b/website/docs/controls/dropdownm2.md index e55db7ff94..0939d019b2 100644 --- a/website/docs/controls/dropdownm2.md +++ b/website/docs/controls/dropdownm2.md @@ -1,8 +1,15 @@ --- class_name: "flet.DropdownM2" +examples: "controls/material/dropdownm2" title: "DropdownM2" --- -import {ClassAll} from '@site/src/components/crocodocs'; +import {ClassAll, CodeExample} from '@site/src/components/crocodocs'; + +## Examples + +### Field border and menu shape + + diff --git a/website/docs/types/inputborder.md b/website/docs/types/inputborder.md deleted file mode 100644 index 0eb2a23c80..0000000000 --- a/website/docs/types/inputborder.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: "InputBorder" ---- - -import {ClassAll} from '@site/src/components/crocodocs'; - - diff --git a/website/docs/types/inputborder/index.md b/website/docs/types/inputborder/index.md new file mode 100644 index 0000000000..672fdf2fcf --- /dev/null +++ b/website/docs/types/inputborder/index.md @@ -0,0 +1,18 @@ +--- +examples: "controls/core/types/input_border" +title: "InputBorder" +--- + +import {ClassAll, CodeExample} from '@site/src/components/crocodocs'; + + + +## Examples + +### Border styles + + + +### Styling and per-state borders + + diff --git a/website/docs/types/outlineinputborder.md b/website/docs/types/outlineinputborder.md new file mode 100644 index 0000000000..33f15bf974 --- /dev/null +++ b/website/docs/types/outlineinputborder.md @@ -0,0 +1,7 @@ +--- +title: "OutlineInputBorder" +--- + +import {ClassAll} from '@site/src/components/crocodocs'; + + diff --git a/website/docs/types/underlineinputborder.md b/website/docs/types/underlineinputborder.md new file mode 100644 index 0000000000..cd8c0ae2fd --- /dev/null +++ b/website/docs/types/underlineinputborder.md @@ -0,0 +1,7 @@ +--- +title: "UnderlineInputBorder" +--- + +import {ClassAll} from '@site/src/components/crocodocs'; + + diff --git a/website/docs/updates/breaking-changes/index.md b/website/docs/updates/breaking-changes/index.md index 007e668502..c25ebbef53 100644 --- a/website/docs/updates/breaking-changes/index.md +++ b/website/docs/updates/breaking-changes/index.md @@ -22,6 +22,12 @@ This page lists the guides created for each release. The following guides are available. They're sorted by release, with the most recent release first. Each guide explains the change, the reason for it, and how to migrate your code. +### Released in Flet 1.0.0 + +#### Breaking changes + +- [`InputBorder` is now a class hierarchy; loose border props removed](/docs/updates/breaking-changes/v1-0-0/inputborder-class-hierarchy) + ### Released in Flet 0.86.0 #### Breaking changes diff --git a/website/docs/updates/breaking-changes/v1-0-0/inputborder-class-hierarchy.md b/website/docs/updates/breaking-changes/v1-0-0/inputborder-class-hierarchy.md new file mode 100644 index 0000000000..3a2f8f2ca3 --- /dev/null +++ b/website/docs/updates/breaking-changes/v1-0-0/inputborder-class-hierarchy.md @@ -0,0 +1,242 @@ +--- +title: "InputBorder is now a class hierarchy; loose border props removed" +--- + +# `InputBorder` is now a class hierarchy; loose border props removed + +:::note +This guide is accurate as of Flet 1.0.0. Later releases might add new APIs or +additional migration paths. + +The [breaking changes and deprecations index](../index.md) lists the guides created for each release. +::: + +## Summary + +Flet 1.0.0 replaced the `InputBorder` **enum** with a hierarchy of classes that +mirrors Flutter's +[`InputBorder`](https://api.flutter.dev/flutter/material/InputBorder-class.html): + +- [`OutlineInputBorder`][flet.OutlineInputBorder] — a rounded rectangle + around all sides (`side`, `border_radius`, `gap_padding`) +- [`UnderlineInputBorder`][flet.UnderlineInputBorder] — a line along the + bottom edge (`side`, `border_radius`) +- [`ft.InputBorder.none()`][flet.InputBorder.none] — draws nothing + +At the same time, the loose border properties on `TextField`, `Dropdown`, +`DropdownM2`, and `CupertinoTextField` were **removed**: `border_radius`, +`border_width`, `border_color`, `focused_border_width`, and +`focused_border_color`. Everything they expressed (and more) now lives on the +`border` property, which accepts a single `InputBorder` or a dictionary mapping +[`ControlState`][flet.ControlState]s to `InputBorder`s. + +## Background + +The enum plus five loose properties could not represent Flutter's actual API: +`gap_padding` wasn't exposed, `UnderlineInputBorder`'s corner radius was +ignored, and error/disabled borders couldn't be styled at all. Each new +Flutter border property would have required another top-level control property. +The class hierarchy scales with Flutter instead: new border types, such as the +`ShapedInputBorder` added in Flutter 3.44, become new classes rather than more +properties on every form field. + +## Migration guide + +### Border style + +Code before migration: + +```python +ft.TextField(border=ft.InputBorder.OUTLINE) +ft.TextField(border=ft.InputBorder.UNDERLINE) +ft.TextField(border=ft.InputBorder.NONE) +``` + +Code after migration: + +```python +ft.TextField(border=ft.OutlineInputBorder()) # the default; can be omitted +ft.TextField(border=ft.UnderlineInputBorder()) +ft.TextField(border=ft.InputBorder.none()) +``` + +### Corner radius, color, and width + +Code before migration: + +```python +ft.TextField( + border_radius=30, + border_color=ft.Colors.GREEN_800, + border_width=2, +) +``` + +Code after migration: + +```python +ft.TextField( + border=ft.OutlineInputBorder( + border_radius=30, + side=ft.BorderSide(width=2, color=ft.Colors.GREEN_800), + ), +) +``` + +To fully remove the border (previously `border_width=0` or +`border_color=ft.Colors.TRANSPARENT`), use `border=ft.InputBorder.none()`, or +`side=ft.BorderSide.none()` to keep the outline's shape for the fill. + +Unlike the old `border_color`, an explicit `side` on a single border applies +to the **enabled** state only — the focused border stays theme-colored +(primary, 2px). To tint the focused border too, as the old code did, use the +per-state form below. + +### Focused (and other per-state) borders + +Code before migration: + +```python +ft.TextField( + border_radius=30, + border_color=ft.Colors.GREEN_800, + focused_border_color=ft.Colors.GREEN_ACCENT_400, + focused_border_width=5, +) +``` + +Code after migration: + +```python +ft.TextField( + border={ + ft.ControlState.DEFAULT: ft.OutlineInputBorder( + border_radius=30, + side=ft.BorderSide(color=ft.Colors.GREEN_800), + ), + ft.ControlState.FOCUSED: ft.OutlineInputBorder( + border_radius=30, + side=ft.BorderSide(width=5, color=ft.Colors.GREEN_ACCENT_400), + ), + }, +) +``` + +Supported state keys are `DEFAULT`, `FOCUSED`, `ERROR`, and `DISABLED` — the +error and disabled borders were not stylable before. A state entry without an +explicit `side` falls back to the `DEFAULT` entry's `side`, matching the old +`focused_border_color or border_color` **color** behavior. + +The `ERROR` entry covers both error states: the field showing an error while +unfocused and while focused. When it carries no explicit `side`, the focused +variant is drawn at the thicker Material focus weight. + +Note that an explicit `side` renders at exactly the width it specifies — +`ft.BorderSide` defaults to width `1`. The old implicit focused width of `2` +(applied when neither `focused_border_width` nor `border_width` was set) is +gone: pass `side=ft.BorderSide(width=2, ...)` in the `FOCUSED` entry to keep +the previous focus emphasis. + +### DropdownM2 menu corners + +`DropdownM2.border_radius` used to round both the input field and the open +menu. Those are now separate: `border` shapes the field, and the new +`menu_border_radius` shapes the menu. Set both to keep the old look. + +Code before migration: + +```python +ft.DropdownM2(border_radius=20) +``` + +Code after migration: + +```python +ft.DropdownM2( + border=ft.OutlineInputBorder(border_radius=20), + menu_border_radius=20, +) +``` + +### Reading and comparing borders + +`ft.InputBorder` is no longer an enum, so code that inspects a border rather +than setting one needs updating. There are no enum members to compare against +or iterate. + +Code before migration: + +```python +if field.border == ft.InputBorder.NONE: + ... +for style in ft.InputBorder: + ... +``` + +Code after migration: + +```python +if field.border == ft.InputBorder.none(): + ... +for style in ( + ft.OutlineInputBorder(), + ft.UnderlineInputBorder(), + ft.InputBorder.none(), +): + ... +``` + +Borders are compared by value, so `ft.OutlineInputBorder(border_radius=4)` +equals `ft.OutlineInputBorder()` — `4` is the default. To test only the kind of +border, use `isinstance(field.border, ft.OutlineInputBorder)`. Note that +`field.border` may also hold a `ControlState` dictionary rather than a single +border. + +### Behavior changes to be aware of + +- **Theme-driven border colors by default** (Material controls: `TextField`, + `Dropdown`, `DropdownM2`). Previously the enabled border was always drawn + black unless `border_color` was set — including in dark mode. Now a border + without an explicit `side` lets the Material theme resolve the color and + width per state: focused uses the primary color, error the error color, and + the enabled color comes from the theme's outline (or, for `filled` fields, + its active-indicator color). This is the Flutter default and works correctly + with dark mode and custom themes. +- **An explicit `side` on a single border styles the enabled state only.** + Previously `border_color` also tinted the focused border. Now the focused, + error, and disabled states stay theme-resolved (focused: primary color, + 2px) unless you use the `ControlState` dictionary form — see the per-state + example above for restoring the old focused look. +- **Underline borders now honor a corner radius.** The old `border_radius` + prop was applied only to outlined borders and silently ignored when + `border` was `InputBorder.UNDERLINE`. `UnderlineInputBorder.border_radius` + is now passed through to Flutter, where it rounds the corners of the + decoration's container — visible when the field is `filled`, because the + fill is clipped to that radius. The border itself is still drawn as a + single line along the bottom edge. It defaults to `4` on the top corners. +- **`CupertinoTextField`** draws a box decoration rather than a Material input + decoration, so it translates the border differently. Leaving `border` unset + keeps the native iOS hairline, exactly as before. Two explicit values change + appearance: `ft.OutlineInputBorder()` without a `side` now also keeps that + native hairline, where `InputBorder.OUTLINE` used to paint a solid black + 1px box (pass a `side` to draw your own); and `ft.InputBorder.none()` now + actually removes the border, where `InputBorder.NONE` was silently ignored. + An outline with an explicit `side` draws on all sides, and an underline + draws the bottom side only, now honoring a non-default `border_radius` for + the fill. A `border_radius` equal to the outline default (`4`) is + indistinguishable from unset and keeps the native radius of `5`. In the + `ControlState` dictionary form, only the `DEFAULT` entry is used — the box + decoration has no focused, error, or disabled variants. + +## Timeline + +- Changed in: `1.0.0` + +## References + +- API documentation: [`InputBorder`][flet.InputBorder], + [`OutlineInputBorder`][flet.OutlineInputBorder], + [`UnderlineInputBorder`][flet.UnderlineInputBorder], + [`ControlState`][flet.ControlState] +- [Flutter `InputBorder` API](https://api.flutter.dev/flutter/material/InputBorder-class.html) +- Release notes: [Flet 1.0.0](../../release-notes.md#10x) diff --git a/website/docs/updates/release-notes.md b/website/docs/updates/release-notes.md index a39a075677..13404000cf 100644 --- a/website/docs/updates/release-notes.md +++ b/website/docs/updates/release-notes.md @@ -8,8 +8,17 @@ This page links release announcements, changelogs, and migration notes for Flet ## Stable releases +### 1.0.x + +- 1.0.0: [Changelog](https://github.com/flet-dev/flet/blob/main/CHANGELOG.md#100), [Breaking changes and deprecations](breaking-changes/index.md#released-in-flet-100) + ### 0.86.x +- 0.86.5: [Changelog](https://github.com/flet-dev/flet/blob/main/CHANGELOG.md#0865) +- 0.86.4: [Changelog](https://github.com/flet-dev/flet/blob/main/CHANGELOG.md#0864) +- 0.86.3: [Changelog](https://github.com/flet-dev/flet/blob/main/CHANGELOG.md#0863) +- 0.86.2: [Changelog](https://github.com/flet-dev/flet/blob/main/CHANGELOG.md#0862) +- 0.86.1: [Changelog](https://github.com/flet-dev/flet/blob/main/CHANGELOG.md#0861) - 0.86.0: [Announcement](/blog/flet-v-0-86-release-announcement), [Changelog](https://github.com/flet-dev/flet/blob/main/CHANGELOG.md#0860), [Breaking changes and deprecations](breaking-changes/index.md#released-in-flet-0860) ### 0.85.x diff --git a/website/sidebars.yml b/website/sidebars.yml index 90c24e9b9f..8825e396c1 100644 --- a/website/sidebars.yml +++ b/website/sidebars.yml @@ -72,6 +72,8 @@ docs: Release notes: updates/release-notes.md Breaking changes and deprecations: _index: updates/breaking-changes/index.md + v1.0.0: + InputBorder is now a class hierarchy; loose border props removed: updates/breaking-changes/v1-0-0/inputborder-class-hierarchy.md v0.86.0: App files ship unpacked in a read-only bundle; storage dirs reworked: updates/breaking-changes/v0-86-0/app-files-unpacked-read-only-bundle.md "Android: site-packages ship zipped; some packages need extract_packages": updates/breaking-changes/v0-86-0/android-extract-packages.md @@ -523,7 +525,10 @@ docs: LinearGradient: types/lineargradient.md RadialGradient: types/radialgradient.md SweepGradient: types/sweepgradient.md - - types/inputborder.md + - InputBorder: + _index: types/inputborder/index.md + OutlineInputBorder: types/outlineinputborder.md + UnderlineInputBorder: types/underlineinputborder.md - types/inputfilter.md - types/iosutsname.md - Key: From 1f22cddc4f996ac51a9b9aa4e7e4f359cdf7dcfc Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Sun, 16 Aug 2026 02:02:06 +0200 Subject: [PATCH 04/13] docs(protocol): record why structural dataclasses are always emitted The msgpack encoder emits a nested dataclass unconditionally, including when it equals its field's `default_factory` product, while the list, dict and scalar branches beside it prune values that match their defaults. That asymmetry is load-bearing rather than accidental. The encoder also writes the `__prev_*` snapshots that are the differ's only model of client state, and in-place mutations of a nested value produce nested-path patch ops, which require the client to already hold the parent key. Pruning here without a differ that emits whole-value replaces for pruned fields leaves the client applying a patch into a key it never received. Record the constraint at the branch so it is not removed as a stray inconsistency. --- sdk/python/packages/flet/src/flet/messaging/protocol.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/sdk/python/packages/flet/src/flet/messaging/protocol.py b/sdk/python/packages/flet/src/flet/messaging/protocol.py index 9ddde6e431..e8a45c36f9 100644 --- a/sdk/python/packages/flet/src/flet/messaging/protocol.py +++ b/sdk/python/packages/flet/src/flet/messaging/protocol.py @@ -126,6 +126,12 @@ def encode_object_for_msgpack(obj): r[fname] = v prev_dicts[fname] = v elif is_dataclass(v): + # Emitted unconditionally, even when equal to the + # field's default_factory product: the differ patches + # nested fields in place with nested-path ops, which + # requires the client to already hold the parent key. + # Pruning here would only be safe together with a differ + # that emits whole-value replaces for pruned fields. r[fname] = v prev_classes[fname] = v elif v is not None: From 592b0c1c244a1b83220e753ae86eb19bfe76b068 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Sun, 16 Aug 2026 02:17:51 +0200 Subject: [PATCH 05/13] feat(cupertino): honor focused and disabled border states `CupertinoTextField` decorates with a `BoxDecoration`, which holds a single static border, so a `ControlState` dictionary passed to `border` collapsed to its `DEFAULT` entry and the remaining states were silently dropped. The control already rebuilds on focus change and knows whether it is disabled, so the applicable entry can be resolved at build time: `DISABLED` takes precedence, then `FOCUSED`, then `DEFAULT`. As on the Material side, a state entry without a `side` inherits the default entry's side. `ERROR` remains unsupported because the control does not render an error state at all. The translation also moves out of `build()` into `parseFormFieldBoxBorder` in `utils/form_field.dart`, beside the Material `parseFormFieldBorders`. Both consume the same wire shape, so keeping them adjacent makes it harder for one to drift when a border type is added or its defaults change. --- .../lib/src/controls/cupertino_textfield.dart | 46 ++----------- packages/flet/lib/src/utils/form_field.dart | 65 +++++++++++++++++++ .../controls/cupertino/cupertino_textfield.py | 10 +-- .../v1-0-0/inputborder-class-hierarchy.md | 5 +- 4 files changed, 78 insertions(+), 48 deletions(-) diff --git a/packages/flet/lib/src/controls/cupertino_textfield.dart b/packages/flet/lib/src/controls/cupertino_textfield.dart index 6f29f7f751..602c3906ca 100644 --- a/packages/flet/lib/src/controls/cupertino_textfield.dart +++ b/packages/flet/lib/src/controls/cupertino_textfield.dart @@ -217,46 +217,8 @@ class _CupertinoTextFieldControlState extends State { _selection = selection; } - // CupertinoTextField takes a BoxDecoration, not an InputDecoration, so the - // InputBorder value is translated to a BoxBorder. Only the default-state - // entry of a state map is used. - var borderValue = widget.control.get("border"); - if (borderValue is Map && !borderValue.containsKey("_type")) { - borderValue = borderValue["default"]; - } - var explicitSide = borderValue is Map ? borderValue["side"] : null; - var borderSide = parseBorderSide(explicitSide, Theme.of(context), - defaultValue: const BorderSide())!; - - BoxBorder? border; - BorderRadius? borderRadius; - switch (borderValue is Map ? borderValue["_type"] : "outline") { - case "underline": - border = Border(bottom: borderSide); - // The underline's default top-corner radius is treated as - // unconfigured, and a non-zero radius cannot be painted together - // with a hairline solid side. - var radius = parseBorderRadius(borderValue["border_radius"]); - borderRadius = radius == null || - radius == kUnderlineInputBorderDefaultRadius || - (borderSide.width == 0.0 && - borderSide.style == BorderStyle.solid) - ? BorderRadius.zero - : radius; - case "none": - // A nothing-painting border: copyWith() below cannot clear the default - // decoration's border, so replace it instead. - border = const Border.fromBorderSide(BorderSide.none); - default: - // Outline: without an explicit side, keep CupertinoTextField's native - // hairline border; an explicit radius still applies to it. - if (explicitSide != null) { - border = Border.fromBorderSide(borderSide); - } - borderRadius = borderValue is Map - ? parseBorderRadius(borderValue["border_radius"]) - : null; - } + var boxBorder = parseFormFieldBoxBorder(widget.control, Theme.of(context), + focused: _focused); var canRevealPassword = widget.control.getBool("can_reveal_password", false)!; @@ -310,8 +272,8 @@ class _CupertinoTextFieldControlState extends State { image: widget.control.getDecorationImage("image", context), backgroundBlendMode: bgcolor != null || gradient != null ? blendMode : null, - border: border, - borderRadius: borderRadius, + border: boxBorder.border, + borderRadius: boxBorder.borderRadius, boxShadow: widget.control.getBoxShadows("shadows", Theme.of(context))), cursorHeight: widget.control.getDouble("cursor_height"), diff --git a/packages/flet/lib/src/utils/form_field.dart b/packages/flet/lib/src/utils/form_field.dart index 6b24ef954a..a5462fba4b 100644 --- a/packages/flet/lib/src/utils/form_field.dart +++ b/packages/flet/lib/src/utils/form_field.dart @@ -104,6 +104,71 @@ FormFieldBorders parseFormFieldBorders(Control control, ThemeData theme) { return borders; } +/// A [BoxDecoration]'s border values, translated from an input border. +class FormFieldBoxBorder { + final BoxBorder? border; + final BorderRadius? borderRadius; + + const FormFieldBoxBorder({this.border, this.borderRadius}); +} + +/// Translates the "border" property of [control] into [BoxDecoration] values, +/// for controls decorated with a [BoxDecoration] rather than an +/// [InputDecoration]. +/// +/// A [BoxDecoration] holds one static border, so the entry matching the +/// control's current state is resolved here instead of by the framework; +/// "error" has no counterpart on this side and is ignored. A null +/// [FormFieldBoxBorder.border] means "keep the widget's own default border", +/// since [BoxDecoration.copyWith] leaves null arguments unchanged. +FormFieldBoxBorder parseFormFieldBoxBorder(Control control, ThemeData theme, + {bool focused = false}) { + var value = control.get("border"); + + dynamic defaultEntry = value; + dynamic entry = value; + if (value is Map && !value.containsKey("_type")) { + defaultEntry = value["default"]; + entry = (control.disabled ? value["disabled"] : null) ?? + (focused ? value["focused"] : null) ?? + defaultEntry; + } + + // A state entry without a side inherits the default entry's side, as on the + // Material side. + var explicitSide = (entry is Map ? entry["side"] : null) ?? + (defaultEntry is Map ? defaultEntry["side"] : null); + var borderSide = + parseBorderSide(explicitSide, theme, defaultValue: const BorderSide())!; + + switch (entry is Map ? entry["_type"] : "outline") { + case "underline": + // The underline's default top-corner radius is treated as unconfigured, + // and a non-zero radius cannot be painted with a hairline solid side. + var radius = parseBorderRadius(entry["border_radius"]); + return FormFieldBoxBorder( + border: Border(bottom: borderSide), + borderRadius: radius == null || + radius == kUnderlineInputBorderDefaultRadius || + (borderSide.width == 0.0 && + borderSide.style == BorderStyle.solid) + ? BorderRadius.zero + : radius); + case "none": + // A nothing-painting border: copyWith cannot clear an existing border, + // so it must be replaced instead. + return const FormFieldBoxBorder( + border: Border.fromBorderSide(BorderSide.none)); + default: + // Outline: without an explicit side, keep the widget's native border. + return FormFieldBoxBorder( + border: + explicitSide != null ? Border.fromBorderSide(borderSide) : null, + borderRadius: + entry is Map ? parseBorderRadius(entry["border_radius"]) : null); + } +} + TextInputType? parseTextInputType(String? value, [TextInputType? defaultValue]) { const typeMap = { diff --git a/sdk/python/packages/flet/src/flet/controls/cupertino/cupertino_textfield.py b/sdk/python/packages/flet/src/flet/controls/cupertino/cupertino_textfield.py index f2976dc5e7..00d66922a3 100644 --- a/sdk/python/packages/flet/src/flet/controls/cupertino/cupertino_textfield.py +++ b/sdk/python/packages/flet/src/flet/controls/cupertino/cupertino_textfield.py @@ -55,10 +55,12 @@ class CupertinoTextField(TextField): ``` Note: - This control draws its :attr:`~flet.FormFieldControl.border` as a - static box decoration. In the :class:`~flet.ControlState` dictionary - form, only the `DEFAULT` entry is used — there are no focused, error, - or disabled border variants. + This control draws its :attr:`~flet.FormFieldControl.border` as a box + decoration. In the :class:`~flet.ControlState` dictionary form, the + :attr:`flet.ControlState.DEFAULT`, :attr:`flet.ControlState.FOCUSED` + and :attr:`flet.ControlState.DISABLED` entries apply; + :attr:`flet.ControlState.ERROR` is ignored, as this control does not + render an error state. """ placeholder_text: Optional[str] = None diff --git a/website/docs/updates/breaking-changes/v1-0-0/inputborder-class-hierarchy.md b/website/docs/updates/breaking-changes/v1-0-0/inputborder-class-hierarchy.md index 3a2f8f2ca3..557aee9cdd 100644 --- a/website/docs/updates/breaking-changes/v1-0-0/inputborder-class-hierarchy.md +++ b/website/docs/updates/breaking-changes/v1-0-0/inputborder-class-hierarchy.md @@ -225,8 +225,9 @@ border. draws the bottom side only, now honoring a non-default `border_radius` for the fill. A `border_radius` equal to the outline default (`4`) is indistinguishable from unset and keeps the native radius of `5`. In the - `ControlState` dictionary form, only the `DEFAULT` entry is used — the box - decoration has no focused, error, or disabled variants. + `ControlState` dictionary form, the `DEFAULT`, `FOCUSED` and `DISABLED` + entries apply; `ERROR` is ignored, as this control does not render an error + state. ## Timeline From 87765db149f8d75b242ba58d34504707c658e4ff Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Sun, 16 Aug 2026 18:19:26 +0200 Subject: [PATCH 06/13] docs(cupertino): document `border` on CupertinoTextField Inherited properties are documented on the class that declares them, so `border` was described only on `FormFieldControl`, in terms of a Material input decoration: theme-resolved sides and a slot per interactive state. `CupertinoTextField` renders a box decoration instead, where an outline without a side keeps the platform border, an underline draws one edge, and there is no error state to style. Redeclare the property so the control documents its own behaviour, and drop the class-level note it replaces. The field is redeclared `kw_only=True`: `FormFieldControl` is a keyword-only dataclass while this control is not, so without it the property would become the first positional parameter and `CupertinoTextField("hello")` would set the border rather than the text. --- .../controls/cupertino/cupertino_textfield.py | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/sdk/python/packages/flet/src/flet/controls/cupertino/cupertino_textfield.py b/sdk/python/packages/flet/src/flet/controls/cupertino/cupertino_textfield.py index 00d66922a3..434f548fa2 100644 --- a/sdk/python/packages/flet/src/flet/controls/cupertino/cupertino_textfield.py +++ b/sdk/python/packages/flet/src/flet/controls/cupertino/cupertino_textfield.py @@ -4,7 +4,9 @@ from flet.controls.base_control import control from flet.controls.box import BoxShadowValue, DecorationImage +from flet.controls.control_state import ControlStateValue from flet.controls.gradients import Gradient +from flet.controls.material.form_field_control import InputBorder, OutlineInputBorder from flet.controls.material.textfield import TextField from flet.controls.padding import Padding, PaddingValue from flet.controls.text_style import TextStyle @@ -50,17 +52,10 @@ class CupertinoTextField(TextField): """ An iOS-style text field. + Example: ```python ft.CupertinoTextField(placeholder_text="Search") ``` - - Note: - This control draws its :attr:`~flet.FormFieldControl.border` as a box - decoration. In the :class:`~flet.ControlState` dictionary form, the - :attr:`flet.ControlState.DEFAULT`, :attr:`flet.ControlState.FOCUSED` - and :attr:`flet.ControlState.DISABLED` entries apply; - :attr:`flet.ControlState.ERROR` is ignored, as this control does not - render an error state. """ placeholder_text: Optional[str] = None @@ -142,3 +137,22 @@ class CupertinoTextField(TextField): or the clear button when :attr:`clear_button_visibility_mode` is not :attr:`flet.OverlayVisibilityMode.NEVER`. """ + + border: ControlStateValue[InputBorder] = field( + default_factory=OutlineInputBorder, kw_only=True + ) + """ + The border drawn around this text field. + + Accepts a single :class:`~flet.InputBorder` or a dictionary mapping + :class:`~flet.ControlState`s to :class:`~flet.InputBorder`s. The + :attr:`flet.ControlState.DEFAULT`, :attr:`flet.ControlState.FOCUSED` and + :attr:`flet.ControlState.DISABLED` entries apply; + :attr:`flet.ControlState.ERROR` is ignored, as this control does not + render an error state. + + An :class:`~flet.OutlineInputBorder` without a `side` keeps the native iOS + border; give it a `side` to draw your own on all four edges. An + :class:`~flet.UnderlineInputBorder` draws the bottom edge only, and + :meth:`flet.InputBorder.none` removes the border entirely. + """ From b44c713af69e769d9f47024f2fecdf16e415821f Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Sun, 16 Aug 2026 19:22:28 +0200 Subject: [PATCH 07/13] refactor: give `RoundedRectangleBorder.radius` a concrete default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flutter defaults `RoundedRectangleBorder.borderRadius` to `BorderRadius.zero`, a static constructor constant, but the property was declared `Optional` with a `None` default — which reads as "no radius configured" when the shape always applies zero. Declaring it `BorderRadiusValue = 0` makes the signature state what the widget does. Rendering is unchanged: the Dart parser already falls back to `BorderRadius.zero` for an absent key, and the encoder prunes a value equal to its declared default, so the unset case and an explicit `radius=0` both put nothing on the wire. `BeveledRectangleBorder` and `ContinuousRectangleBorder` inherit the field. The `copy()` signatures keep `Optional`/`None`, where `None` means "keep the current value" rather than "no radius". Passing `radius=None` explicitly still works at runtime but is now a type error. --- sdk/python/packages/flet/src/flet/controls/buttons.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sdk/python/packages/flet/src/flet/controls/buttons.py b/sdk/python/packages/flet/src/flet/controls/buttons.py index 1dfbd5d69b..3db47d4e8d 100644 --- a/sdk/python/packages/flet/src/flet/controls/buttons.py +++ b/sdk/python/packages/flet/src/flet/controls/buttons.py @@ -89,9 +89,11 @@ class RoundedRectangleBorder(OutlinedBorder): A border with rounded rectangle corners. """ - radius: Optional[BorderRadiusValue] = None + radius: BorderRadiusValue = 0 """ The radius for each corner. + + Defaults to `0` — square corners. """ def __post_init__(self): From f80e4c79e2b6e55cd2a508f8b4f2aba691eabf44 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Sun, 16 Aug 2026 19:22:29 +0200 Subject: [PATCH 08/13] docs: drop redundant default mention from `border` docstrings The rendered signature already shows `field(default_factory=OutlineInputBorder)`, so the trailing "Defaults to ..." line restated it. Removed from `FormFieldControl.border` and `Dropdown.border`, matching how `CupertinoTextField.border` documents the same property. --- sdk/python/packages/flet/src/flet/controls/material/dropdown.py | 2 -- .../flet/src/flet/controls/material/form_field_control.py | 2 -- 2 files changed, 4 deletions(-) diff --git a/sdk/python/packages/flet/src/flet/controls/material/dropdown.py b/sdk/python/packages/flet/src/flet/controls/material/dropdown.py index 259ee899a3..8980bf38f3 100644 --- a/sdk/python/packages/flet/src/flet/controls/material/dropdown.py +++ b/sdk/python/packages/flet/src/flet/controls/material/dropdown.py @@ -302,8 +302,6 @@ class Dropdown(LayoutControl): In the dictionary form, the `DEFAULT` entry behaves like the single form, and each state entry without an explicit `side` falls back to the `DEFAULT` entry's `side`, if set. - - Defaults to `OutlineInputBorder()`. """ color: Optional[ColorValue] = None diff --git a/sdk/python/packages/flet/src/flet/controls/material/form_field_control.py b/sdk/python/packages/flet/src/flet/controls/material/form_field_control.py index f7f72fa158..66c5fd0f29 100644 --- a/sdk/python/packages/flet/src/flet/controls/material/form_field_control.py +++ b/sdk/python/packages/flet/src/flet/controls/material/form_field_control.py @@ -204,8 +204,6 @@ class FormFieldControl(LayoutControl): In the dictionary form, the `DEFAULT` entry behaves like the single form, and each state entry without an explicit `side` falls back to the `DEFAULT` entry's `side`, if set. - - Defaults to `OutlineInputBorder()`. """ color: Optional[ColorValue] = None From d259096c443878cf6d327a9b9e9506ded5787f01 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Sun, 16 Aug 2026 19:27:22 +0200 Subject: [PATCH 09/13] refactor: mirror Flutter's static defaults instead of `Optional = None` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Properties whose real default is a fixed constant were declared `Optional[X] = None`, so the signature said "unset" while the widget always applied a value. Several docstrings had to spell the truth out in prose — "Defaults to opaque black", "If not set, the effective default is `4.0`" — which is the tell that the signature was wrong. Declare those defaults concretely so the signature states what the control does. `Optional = None` stays wherever a widget resolves the value at runtime from the theme, the platform or its own state, because there `None` is honest: `Paint.gradient` and `CupertinoAppBar.brightness` are untouched, for example. Paint: `color`, `blend_mode`, `anti_alias`, `stroke_cap`, `stroke_join`, `stroke_miter_limit`, `stroke_width`, `style`. Controls: `Button.autofocus`, `FormFieldControl.fit_parent_size`, `Semantics.container`, `BasePage.show_semantics_debugger`, `Text.no_wrap`, `GridView.clip_behavior`, `ExpansionPanelList.spacing`, the three `CupertinoAppBar.automatic*` flags, `canvas.Path.Rect.border_radius` and `canvas.Text.max_width`. Rendering is unchanged throughout: every Dart parser already falls back to the same constant when the key is absent, and the encoder prunes a value equal to its declared default, so the unset case and an explicitly-passed default now encode identically while non-default values still transmit. Prose that only restated a default is dropped, since the signature carries it. Passing `None` explicitly still works at runtime but is now a type error. --- .../flet/src/flet/controls/base_page.py | 2 +- .../src/flet/controls/core/canvas/path.py | 2 +- .../src/flet/controls/core/canvas/text.py | 4 +- .../flet/src/flet/controls/core/grid_view.py | 2 +- .../flet/src/flet/controls/core/semantics.py | 2 +- .../flet/src/flet/controls/core/text.py | 2 +- .../controls/cupertino/cupertino_app_bar.py | 6 +-- .../flet/src/flet/controls/material/button.py | 2 +- .../flet/controls/material/expansion_panel.py | 2 +- .../controls/material/form_field_control.py | 2 +- .../flet/src/flet/controls/painting.py | 37 +++++++------------ 11 files changed, 26 insertions(+), 37 deletions(-) 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 807bfe4c18..de89c80c39 100644 --- a/sdk/python/packages/flet/src/flet/controls/base_page.py +++ b/sdk/python/packages/flet/src/flet/controls/base_page.py @@ -199,7 +199,7 @@ class BasePage(AdaptiveControl): Configures supported locales and the current locale. """ - show_semantics_debugger: Optional[bool] = None + show_semantics_debugger: bool = False """ Whether to turn on an overlay that shows the accessibility information reported by \ the framework. diff --git a/sdk/python/packages/flet/src/flet/controls/core/canvas/path.py b/sdk/python/packages/flet/src/flet/controls/core/canvas/path.py index 23188d9704..7b4778e883 100644 --- a/sdk/python/packages/flet/src/flet/controls/core/canvas/path.py +++ b/sdk/python/packages/flet/src/flet/controls/core/canvas/path.py @@ -272,7 +272,7 @@ class Rect(PathElement): Height of the rectangle. """ - border_radius: Optional[BorderRadiusValue] = None + border_radius: BorderRadiusValue = 0 """ Optional border radius to round rectangle corners. """ diff --git a/sdk/python/packages/flet/src/flet/controls/core/canvas/text.py b/sdk/python/packages/flet/src/flet/controls/core/canvas/text.py index 1c32d636d4..f15fa65f3b 100644 --- a/sdk/python/packages/flet/src/flet/controls/core/canvas/text.py +++ b/sdk/python/packages/flet/src/flet/controls/core/canvas/text.py @@ -61,11 +61,11 @@ class Text(Shape): that overflows the width constraints are dropped. """ - max_width: Optional[Number] = None + max_width: Number = float("inf") """ The maximum width of the painted text. - Defaults to `None` - infinity. + An infinite width leaves the text unconstrained. """ ellipsis: Optional[str] = None diff --git a/sdk/python/packages/flet/src/flet/controls/core/grid_view.py b/sdk/python/packages/flet/src/flet/controls/core/grid_view.py index ddfac29574..36edff12af 100644 --- a/sdk/python/packages/flet/src/flet/controls/core/grid_view.py +++ b/sdk/python/packages/flet/src/flet/controls/core/grid_view.py @@ -96,7 +96,7 @@ class GridView(LayoutControl, ScrollableControl, AdaptiveControl): The amount of space by which to inset the children. """ - clip_behavior: Optional[ClipBehavior] = None + clip_behavior: ClipBehavior = ClipBehavior.HARD_EDGE """ The content will be clipped (or not) according to this option. """ diff --git a/sdk/python/packages/flet/src/flet/controls/core/semantics.py b/sdk/python/packages/flet/src/flet/controls/core/semantics.py index e2d18f2ebf..e31dc35b86 100644 --- a/sdk/python/packages/flet/src/flet/controls/core/semantics.py +++ b/sdk/python/packages/flet/src/flet/controls/core/semantics.py @@ -170,7 +170,7 @@ class Semantics(Control): A hint describing what happens when the user activates :attr:`on_long_press`. """ - container: Optional[bool] = None + container: bool = False """ Whether this semantics node should introduce its own semantic container. diff --git a/sdk/python/packages/flet/src/flet/controls/core/text.py b/sdk/python/packages/flet/src/flet/controls/core/text.py index 798e4dbf85..b7247ca294 100644 --- a/sdk/python/packages/flet/src/flet/controls/core/text.py +++ b/sdk/python/packages/flet/src/flet/controls/core/text.py @@ -313,7 +313,7 @@ class Text(LayoutControl): Defaults to `False`. """ - no_wrap: Optional[bool] = None + no_wrap: bool = False """ If `False` (default) the text should break at soft line breaks. diff --git a/sdk/python/packages/flet/src/flet/controls/cupertino/cupertino_app_bar.py b/sdk/python/packages/flet/src/flet/controls/cupertino/cupertino_app_bar.py index 3bf8ac8007..ece0eea6c0 100644 --- a/sdk/python/packages/flet/src/flet/controls/cupertino/cupertino_app_bar.py +++ b/sdk/python/packages/flet/src/flet/controls/cupertino/cupertino_app_bar.py @@ -53,7 +53,7 @@ class CupertinoAppBar(Control): Default color is defined by current theme. """ - automatically_imply_leading: Optional[bool] = None + automatically_imply_leading: bool = True """ Whether we should try to imply the :attr:`leading` control if `None`. @@ -64,7 +64,7 @@ class CupertinoAppBar(Control): - If a :attr:`leading` control is provided, this parameter has no effect. """ - automatically_imply_title: Optional[bool] = None + automatically_imply_title: bool = True """ Whether we should try to imply the `title` control if `None`. @@ -135,7 +135,7 @@ class CupertinoAppBar(Control): the :attr:`bgcolor`. """ - automatic_background_visibility: Optional[bool] = None + automatic_background_visibility: bool = True """ Whether the navigation bar should appear transparent when content is scrolled \ under it. diff --git a/sdk/python/packages/flet/src/flet/controls/material/button.py b/sdk/python/packages/flet/src/flet/controls/material/button.py index 55bca37e00..077fcf8969 100644 --- a/sdk/python/packages/flet/src/flet/controls/material/button.py +++ b/sdk/python/packages/flet/src/flet/controls/material/button.py @@ -86,7 +86,7 @@ class Button(LayoutControl, AdaptiveControl): The button's style. """ - autofocus: Optional[bool] = None + autofocus: bool = False """ Whether this button should be focused initially. """ diff --git a/sdk/python/packages/flet/src/flet/controls/material/expansion_panel.py b/sdk/python/packages/flet/src/flet/controls/material/expansion_panel.py index 49f5abe80f..983262eaa1 100644 --- a/sdk/python/packages/flet/src/flet/controls/material/expansion_panel.py +++ b/sdk/python/packages/flet/src/flet/controls/material/expansion_panel.py @@ -194,7 +194,7 @@ class ExpansionPanelList(LayoutControl, ScrollableControl): :attr:`flet.Colors.WHITE_60` in dark theme mode. """ - spacing: Optional[Number] = None + spacing: Number = 16.0 """ The size of the gap between the :attr:`controls`s when expanded. """ diff --git a/sdk/python/packages/flet/src/flet/controls/material/form_field_control.py b/sdk/python/packages/flet/src/flet/controls/material/form_field_control.py index 66c5fd0f29..1708e9ba89 100644 --- a/sdk/python/packages/flet/src/flet/controls/material/form_field_control.py +++ b/sdk/python/packages/flet/src/flet/controls/material/form_field_control.py @@ -441,7 +441,7 @@ class FormFieldControl(LayoutControl): :attr:`counter`, :attr:`icon`, :attr:`prefix`, or :attr:`suffix`. """ - fit_parent_size: Optional[bool] = None + fit_parent_size: bool = False """ Whether the editable area should expand to fill the height of its parent. diff --git a/sdk/python/packages/flet/src/flet/controls/painting.py b/sdk/python/packages/flet/src/flet/controls/painting.py index eb88d8ea17..745ef130b1 100644 --- a/sdk/python/packages/flet/src/flet/controls/painting.py +++ b/sdk/python/packages/flet/src/flet/controls/painting.py @@ -5,6 +5,7 @@ from flet.controls.base_control import value from flet.controls.blur import BlurValue +from flet.controls.colors import Colors from flet.controls.gradients import GradientTileMode from flet.controls.transform import OffsetValue from flet.controls.types import ( @@ -317,18 +318,14 @@ class Paint: A description of the style to use when drawing a shape on the canvas. """ - color: Optional[ColorValue] = None + color: ColorValue = Colors.BLACK """ The color to use when stroking or filling a shape. - - Defaults to opaque black. """ - blend_mode: Optional[BlendMode] = None + blend_mode: BlendMode = BlendMode.SRC_OVER """ A blend mode to apply when a shape is drawn or a layer is composited. - - Defaults to :attr:`flet.BlendMode.SRC_OVER`. """ blur_image: Optional[BlurValue] = None @@ -336,11 +333,9 @@ class Paint: Blur image when drawing it on a canvas. """ - anti_alias: Optional[bool] = None + anti_alias: bool = True """ Whether to apply anti-aliasing to lines and images drawn on the canvas. - - Defaults to `True`. """ gradient: Optional[PaintGradient] = None @@ -348,42 +343,38 @@ class Paint: Configures gradient paint. """ - stroke_cap: Optional[StrokeCap] = None + stroke_cap: StrokeCap = StrokeCap.BUTT """ The kind of finish to place on the ends of stroked lines. This applies when :attr:`style` is :attr:`flet.PaintingStyle.STROKE`. - If not set, the effective default is :attr:`flet.StrokeCap.BUTT`. """ - stroke_join: Optional[StrokeJoin] = None + stroke_join: StrokeJoin = StrokeJoin.MITER """ The kind of finish to place on joins between stroked segments. - This applies when :attr:`style` is - :attr:`flet.PaintingStyle.STROKE`. If not set, the effective default is - :attr:`flet.StrokeJoin.MITER`. + This applies when :attr:`style` is :attr:`flet.PaintingStyle.STROKE`. See also: :attr:`stroke_miter_limit` """ - stroke_miter_limit: Optional[Number] = None + stroke_miter_limit: Number = 4.0 """ The limit for drawing miter joins when :attr:`stroke_join` is :attr:`flet.StrokeJoin.MITER` and :attr:`style` is :attr:`flet.PaintingStyle.STROKE`. - If this limit is exceeded, a bevel join is used instead. If not set, the effective - default is `4.0`. + If this limit is exceeded, a bevel join is used instead. """ - stroke_width: Optional[Number] = None + stroke_width: Number = 0.0 """ How wide stroked edges should be, in logical pixels. - This applies when :attr:`style` is :attr:`flet.PaintingStyle.STROKE`. If not set, - the effective default is `0.0`, which corresponds to a hairline width. + This applies when :attr:`style` is :attr:`flet.PaintingStyle.STROKE`. + A width of `0.0` corresponds to a hairline. """ stroke_dash_pattern: Optional[list[Number]] = None @@ -398,11 +389,9 @@ class Paint: This applies only when :attr:`style` is :attr:`flet.PaintingStyle.STROKE`. """ - style: Optional[PaintingStyle] = None + style: PaintingStyle = PaintingStyle.FILL """ Whether to paint filled interiors or only stroked outlines. - - If not set, the effective default is :attr:`flet.PaintingStyle.FILL`. """ def copy( From a5c14f0e5a4c433ba0065586c8f9f0fc0309f8dd Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Sun, 16 Aug 2026 20:44:39 +0200 Subject: [PATCH 10/13] docs: drop remaining restated defaults from border docstrings The rendered signature already carries the default, so prose repeating it was redundant on `RoundedRectangleBorder.radius`, `OutlineInputBorder.border_radius` and `gap_padding`, matching the `border` properties. Also reference `InputBorder` from the Dart doc comment instead of naming it in prose. --- packages/flet/lib/src/utils/form_field.dart | 2 +- sdk/python/packages/flet/src/flet/controls/buttons.py | 2 -- .../flet/src/flet/controls/material/form_field_control.py | 4 +--- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/flet/lib/src/utils/form_field.dart b/packages/flet/lib/src/utils/form_field.dart index a5462fba4b..0095be5837 100644 --- a/packages/flet/lib/src/utils/form_field.dart +++ b/packages/flet/lib/src/utils/form_field.dart @@ -104,7 +104,7 @@ FormFieldBorders parseFormFieldBorders(Control control, ThemeData theme) { return borders; } -/// A [BoxDecoration]'s border values, translated from an input border. +/// A [BoxDecoration]'s border values, translated from an [InputBorder]. class FormFieldBoxBorder { final BoxBorder? border; final BorderRadius? borderRadius; diff --git a/sdk/python/packages/flet/src/flet/controls/buttons.py b/sdk/python/packages/flet/src/flet/controls/buttons.py index 3db47d4e8d..f5cd6f138c 100644 --- a/sdk/python/packages/flet/src/flet/controls/buttons.py +++ b/sdk/python/packages/flet/src/flet/controls/buttons.py @@ -92,8 +92,6 @@ class RoundedRectangleBorder(OutlinedBorder): radius: BorderRadiusValue = 0 """ The radius for each corner. - - Defaults to `0` — square corners. """ def __post_init__(self): diff --git a/sdk/python/packages/flet/src/flet/controls/material/form_field_control.py b/sdk/python/packages/flet/src/flet/controls/material/form_field_control.py index 1708e9ba89..476fba8b7c 100644 --- a/sdk/python/packages/flet/src/flet/controls/material/form_field_control.py +++ b/sdk/python/packages/flet/src/flet/controls/material/form_field_control.py @@ -107,8 +107,6 @@ class OutlineInputBorder(InputBorder): border_radius: BorderRadiusValue = 4 """ The radii of the border's rounded rectangle corners. - - Defaults to `4` — all corners rounded. """ gap_padding: Number = 4.0 @@ -116,7 +114,7 @@ class OutlineInputBorder(InputBorder): Horizontal padding on either side of the border's gap cut out for the floating :attr:`flet.FormFieldControl.label`. - Must be non-negative. Defaults to `4.0`. + Must be non-negative. """ def __post_init__(self): From d1d8b39b69b6c42c7026c48ecf41a111b1249236 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Sun, 16 Aug 2026 21:03:40 +0200 Subject: [PATCH 11/13] docs: add 1.0.0 changelog entries for the `InputBorder` hierarchy Root changelog covers the two user-facing breaking changes: the `InputBorder` class hierarchy with per-state borders and the removed loose properties, and the properties that now declare their constant Flutter default rather than `Optional = None`, so reading one returns the value the control applies. The Dart package changelog covers only what extension authors must know: the `FormFieldInputBorder` enum and its parse helpers are gone, replaced by `parseInputBorder()`, `parseFormFieldBorders()` and `parseFormFieldBoxBorder()`. Also link the pull request from the migration guide's references. --- CHANGELOG.md | 7 +++++++ packages/flet/CHANGELOG.md | 4 ++++ .../breaking-changes/v1-0-0/inputborder-class-hierarchy.md | 1 + 3 files changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 022874184b..d100004c70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 1.0.0 + +### Breaking changes + +* **`InputBorder` is now a class hierarchy instead of an enum.** `OutlineInputBorder`, `UnderlineInputBorder` and `InputBorder.none()` mirror Flutter's classes and their defaults, and `border` accepts either a single border or a `ControlState` dictionary, so the focused, error and disabled borders are stylable for the first time. The `border_radius`, `border_width`, `border_color`, `focused_border_width` and `focused_border_color` properties are removed from `TextField`, `Dropdown`, `DropdownM2` and `CupertinoTextField`, and `DropdownM2` gains `menu_border_radius` for the open menu that the shared `border_radius` used to shape. A border with no explicit `side` now takes its color and weight from the Material theme per state instead of always painting black, so dark mode and custom themes work; on `CupertinoTextField`, `InputBorder.none()` now actually removes the border and an outline without a `side` keeps the native iOS one. See the [InputBorder class hierarchy](/docs/updates/breaking-changes/v1-0-0/inputborder-class-hierarchy) guide ([#6773](https://github.com/flet-dev/flet/pull/6773)) by @ndonkoHenri. +* Properties whose Flutter default is a fixed constant now declare that constant rather than `Optional[...] = None`, so reading one returns the value the control actually applies instead of `None`: the eight `Paint` style properties, `RoundedRectangleBorder.radius`, `Button.autofocus`, `Text.no_wrap`, `GridView.clip_behavior`, `Semantics.container`, `ExpansionPanelList.spacing`, `TextField.fit_parent_size`, `Page.show_semantics_debugger`, the three `CupertinoAppBar.automatic*` flags, `Path.Rect.border_radius` and `canvas.Text.max_width`. Rendering is unchanged, and properties a widget resolves at runtime from the theme, the platform or its own state keep `None` ([#6773](https://github.com/flet-dev/flet/pull/6773)) by @ndonkoHenri. + ## 0.86.7 ### Bug fixes diff --git a/packages/flet/CHANGELOG.md b/packages/flet/CHANGELOG.md index 7b57fbbb5a..8af625a5f7 100644 --- a/packages/flet/CHANGELOG.md +++ b/packages/flet/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.0.0 + +* Replace the `FormFieldInputBorder` enum and its `parseFormFieldInputBorder()` / `Control.getFormFieldInputBorder()` helpers with `parseInputBorder()`, which builds an `InputBorder` from a serialized border object. `parseFormFieldBorders()` maps a control's `border` property — a single border or a map of control states — onto the `InputDecoration` border slots, including `errorBorder`, `focusedErrorBorder` and `disabledBorder`, and `parseFormFieldBoxBorder()` translates the same property for controls decorated with a `BoxDecoration` instead ([#6773](https://github.com/flet-dev/flet/pull/6773)) by @ndonkoHenri. + ## 0.86.7 * `FletJS.canvasKitBaseUrl` is now `String?`. `flutter_bootstrap.js` applies `flet.canvasKitBaseUrl` and `flet.fontFallbackBaseUrl` whenever they are set rather than only when `flet.noCdn` is true, and both default to `null` in CDN mode — so a host serving its own copy of the runtime can point them anywhere without also claiming a no-CDN build. The getter has no readers in this package; the annotation now matches the value it can carry. diff --git a/website/docs/updates/breaking-changes/v1-0-0/inputborder-class-hierarchy.md b/website/docs/updates/breaking-changes/v1-0-0/inputborder-class-hierarchy.md index 557aee9cdd..09480a0dcc 100644 --- a/website/docs/updates/breaking-changes/v1-0-0/inputborder-class-hierarchy.md +++ b/website/docs/updates/breaking-changes/v1-0-0/inputborder-class-hierarchy.md @@ -240,4 +240,5 @@ border. [`UnderlineInputBorder`][flet.UnderlineInputBorder], [`ControlState`][flet.ControlState] - [Flutter `InputBorder` API](https://api.flutter.dev/flutter/material/InputBorder-class.html) +- Issues and PRs: [#6773](https://github.com/flet-dev/flet/pull/6773) - Release notes: [Flet 1.0.0](../../release-notes.md#10x) From 77f332b931b660cfbac02784534c67b09d4d4202 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Mon, 17 Aug 2026 03:50:54 +0200 Subject: [PATCH 12/13] fix(cupertino): let a disabled border outrank a focused one `CupertinoTextField` decorates with a `BoxDecoration`, which holds a single static border, so `parseFormFieldBoxBorder` resolves the applicable `ControlState` entry itself rather than handing the framework a slot per state. That resolution treated `disabled` and `focused` as peers, so a disabled control whose border map omits `DISABLED` fell through to the `FOCUSED` entry instead of the default one. `InputDecorator` short-circuits on disabled and never consults focus, and the Material path inherits that by populating the border slots; make the Cupertino path agree. Reachable because `CupertinoTextField` clears `canRequestFocus` in `didUpdateWidget`, after this build has already chosen a border, so disabling a focused field painted the focused border until the focus listener caught up. --- packages/flet/lib/src/utils/form_field.dart | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/flet/lib/src/utils/form_field.dart b/packages/flet/lib/src/utils/form_field.dart index 0095be5837..96a046ffc8 100644 --- a/packages/flet/lib/src/utils/form_field.dart +++ b/packages/flet/lib/src/utils/form_field.dart @@ -129,9 +129,11 @@ FormFieldBoxBorder parseFormFieldBoxBorder(Control control, ThemeData theme, dynamic entry = value; if (value is Map && !value.containsKey("_type")) { defaultEntry = value["default"]; - entry = (control.disabled ? value["disabled"] : null) ?? - (focused ? value["focused"] : null) ?? - defaultEntry; + // Disabled wins outright, as in InputDecorator: a disabled control never + // shows the focused border, even while its focus node reports focus. + entry = control.disabled + ? (value["disabled"] ?? defaultEntry) + : ((focused ? value["focused"] : null) ?? defaultEntry); } // A state entry without a side inherits the default entry's side, as on the From 96aa0e2dc1735f635defc34e78c49ce34f477779 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Tue, 18 Aug 2026 21:10:54 +0200 Subject: [PATCH 13/13] feat: deprecate the old border API instead of removing it in 1.0.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The class hierarchy replaced the `InputBorder` enum and the five loose border properties outright, so every app styling a form field had to be edited before it would run. Keep the old spellings working for three minor releases instead. `InputBorder.OUTLINE`, `UNDERLINE` and `NONE` resolve again — a metaclass serves them, returning the equivalent class instance — so old code not only runs but produces new-API values. `border_radius`, `border_width`, `border_color`, `focused_border_width` and `focused_border_color` return to `FormFieldControl` and `Dropdown`, and `DropdownM2.border_radius` becomes an alias for `menu_border_radius`. All of them warn through `V.deprecated` and are scheduled for removal in `1.3.0`. Old and new values combine per the repo's non-copying model: Python sends both and Dart prefers the new one, so a `border` carrying a side or per-state entries ignores the deprecated properties, while a bare `border=InputBorder.UNDERLINE` still picks up a legacy `border_color`. That requires `border` to be `Optional` again — with a `default_factory` the client cannot tell a set value from a default, so the fallback could never fire; it becomes non-optional once the deprecations are removed. What stays breaking is what no shim can cover: `InputBorder` is no longer iterable, its members have no `.value` or `.name`, identity comparison no longer holds, and the rendering changes remain. The changelog now separates those from the deprecations, and the guide documents both. --- CHANGELOG.md | 8 +- .../flet/lib/src/controls/dropdownm2.dart | 4 +- packages/flet/lib/src/utils/form_field.dart | 84 ++++++++++++- .../controls/cupertino/cupertino_textfield.py | 6 +- .../src/flet/controls/material/dropdown.py | 82 ++++++++++++- .../src/flet/controls/material/dropdownm2.py | 17 ++- .../controls/material/form_field_control.py | 116 +++++++++++++++++- .../docs/updates/breaking-changes/index.md | 6 +- .../v1-0-0/inputborder-class-hierarchy.md | 18 +-- website/sidebars.yml | 2 +- 10 files changed, 317 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d100004c70..6cfafc0313 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,15 @@ ### Breaking changes -* **`InputBorder` is now a class hierarchy instead of an enum.** `OutlineInputBorder`, `UnderlineInputBorder` and `InputBorder.none()` mirror Flutter's classes and their defaults, and `border` accepts either a single border or a `ControlState` dictionary, so the focused, error and disabled borders are stylable for the first time. The `border_radius`, `border_width`, `border_color`, `focused_border_width` and `focused_border_color` properties are removed from `TextField`, `Dropdown`, `DropdownM2` and `CupertinoTextField`, and `DropdownM2` gains `menu_border_radius` for the open menu that the shared `border_radius` used to shape. A border with no explicit `side` now takes its color and weight from the Material theme per state instead of always painting black, so dark mode and custom themes work; on `CupertinoTextField`, `InputBorder.none()` now actually removes the border and an outline without a `side` keeps the native iOS one. See the [InputBorder class hierarchy](/docs/updates/breaking-changes/v1-0-0/inputborder-class-hierarchy) guide ([#6773](https://github.com/flet-dev/flet/pull/6773)) by @ndonkoHenri. +* **`InputBorder` is a class hierarchy instead of an enum**, so enum-shaped usage no longer works: `InputBorder` is not iterable, its members have no `.value` or `.name`, and `InputBorder.OUTLINE is InputBorder.OUTLINE` is now `False` because each access returns a new instance — compare with `==`. Assigning a border is unaffected; see the deprecations below. Rendering changes as well: a border with no explicit `side` now takes its color and weight from the Material theme per state instead of always painting black, so dark mode and custom themes work; an underline finally honors its `border_radius`; `DropdownM2`'s open menu is shaped by the new `menu_border_radius` rather than by the field's radius; and on `CupertinoTextField`, `InputBorder.none()` now actually removes the border while an outline without a `side` keeps the native iOS one. See the [InputBorder class hierarchy](/docs/updates/breaking-changes/v1-0-0/inputborder-class-hierarchy) guide ([#6773](https://github.com/flet-dev/flet/pull/6773)) by @ndonkoHenri. * Properties whose Flutter default is a fixed constant now declare that constant rather than `Optional[...] = None`, so reading one returns the value the control actually applies instead of `None`: the eight `Paint` style properties, `RoundedRectangleBorder.radius`, `Button.autofocus`, `Text.no_wrap`, `GridView.clip_behavior`, `Semantics.container`, `ExpansionPanelList.spacing`, `TextField.fit_parent_size`, `Page.show_semantics_debugger`, the three `CupertinoAppBar.automatic*` flags, `Path.Rect.border_radius` and `canvas.Text.max_width`. Rendering is unchanged, and properties a widget resolves at runtime from the theme, the platform or its own state keep `None` ([#6773](https://github.com/flet-dev/flet/pull/6773)) by @ndonkoHenri. +### Deprecations + +* `InputBorder.OUTLINE`, `InputBorder.UNDERLINE` and `InputBorder.NONE` are deprecated in favor of `OutlineInputBorder()`, `UnderlineInputBorder()` and `InputBorder.none()`. Each still resolves, returning the equivalent instance, and they are scheduled for removal in `1.3.0`. See the [InputBorder class hierarchy](/docs/updates/breaking-changes/v1-0-0/inputborder-class-hierarchy) guide ([#6773](https://github.com/flet-dev/flet/pull/6773)) by @ndonkoHenri. +* The `border_radius`, `border_width`, `border_color`, `focused_border_width` and `focused_border_color` properties of `TextField`, `Dropdown`, `DropdownM2` and `CupertinoTextField` are deprecated in favor of `border`, which accepts an `InputBorder` or a `ControlState` dictionary. They keep working and are scheduled for removal in `1.3.0`; where both are set, `border` wins. See the [InputBorder class hierarchy](/docs/updates/breaking-changes/v1-0-0/inputborder-class-hierarchy) guide ([#6773](https://github.com/flet-dev/flet/pull/6773)) by @ndonkoHenri. +* `DropdownM2.border_radius` is deprecated in favor of `menu_border_radius` for the open menu, or `border` for the input field. It is scheduled for removal in `1.3.0`. See the [InputBorder class hierarchy](/docs/updates/breaking-changes/v1-0-0/inputborder-class-hierarchy) guide ([#6773](https://github.com/flet-dev/flet/pull/6773)) by @ndonkoHenri. + ## 0.86.7 ### Bug fixes diff --git a/packages/flet/lib/src/controls/dropdownm2.dart b/packages/flet/lib/src/controls/dropdownm2.dart index 1418e53442..a80b7499ec 100644 --- a/packages/flet/lib/src/controls/dropdownm2.dart +++ b/packages/flet/lib/src/controls/dropdownm2.dart @@ -138,7 +138,9 @@ class _DropdownM2ControlState extends State { iconDisabledColor: widget.control.getColor("select_icon_disabled_color", context), iconSize: widget.control.getDouble("select_icon_size", 24.0)!, - borderRadius: widget.control.getBorderRadius("menu_border_radius"), + // border_radius is deprecated in 1.0.0 and removed in 1.3.0. + borderRadius: widget.control.getBorderRadius("menu_border_radius") ?? + widget.control.getBorderRadius("border_radius"), alignment: widget.control.getAlignment("alignment") ?? AlignmentDirectional.centerStart, isExpanded: widget.control.getBool("options_fill_horizontally", true)!, diff --git a/packages/flet/lib/src/utils/form_field.dart b/packages/flet/lib/src/utils/form_field.dart index 96a046ffc8..1fabcc1b64 100644 --- a/packages/flet/lib/src/utils/form_field.dart +++ b/packages/flet/lib/src/utils/form_field.dart @@ -40,6 +40,61 @@ InputBorder? parseInputBorder(dynamic value, ThemeData? theme, } } +/// The loose border properties deprecated in 1.0.0 and removed in 1.3.0: +/// `border_radius`, `border_width`, `border_color`, `focused_border_width` +/// and `focused_border_color`. +/// +/// They fill in only what the `border` property left unspecified, so a control +/// written against the new API is never affected by a stray legacy value. +class _LegacyBorderProps { + final BorderRadius? radius; + final double? width; + final Color? color; + final double? focusedWidth; + final Color? focusedColor; + + const _LegacyBorderProps(this.radius, this.width, this.color, + this.focusedWidth, this.focusedColor); + + factory _LegacyBorderProps.of(Control control, ThemeData theme) => + _LegacyBorderProps( + control.getBorderRadius("border_radius"), + control.getDouble("border_width"), + parseColor(control.getString("border_color"), theme), + control.getDouble("focused_border_width"), + parseColor(control.getString("focused_border_color"), theme), + ); + + bool get isEmpty => + radius == null && + width == null && + color == null && + focusedWidth == null && + focusedColor == null; + + /// Whether a border line was configured. A radius on its own is not one, so + /// it must not conjure a border where the widget had none. + bool get hasSide => + width != null || + color != null || + focusedWidth != null || + focusedColor != null; + + BorderSide get side => width == 0 + ? BorderSide.none + : BorderSide( + color: color ?? const Color(0xFF000000), width: width ?? 1.0); + + BorderSide focusedSideOr(bool focused, ThemeData theme) => + focused ? focusedSide(theme) : side; + + BorderSide focusedSide(ThemeData theme) => width == 0 + ? BorderSide.none + : BorderSide( + color: focusedColor ?? color ?? theme.colorScheme.primary, + width: focusedWidth ?? width ?? 2.0); +} + /// Per-state input borders parsed from a control's "border" property, mapped /// onto the [InputDecoration]/[InputDecorationTheme] border slots. class FormFieldBorders { @@ -101,6 +156,19 @@ FormFieldBorders parseFormFieldBorders(Control control, ThemeData theme) { color: theme.colorScheme.onSurface.withValues(alpha: 0.12), width: 1.0)); } + + // Deprecated fallback: only fills what the new API left unspecified. + var legacy = _LegacyBorderProps.of(control, theme); + if (!legacy.isEmpty && stateMap == null && defaultSide == null) { + var border = borders.border!; + if (legacy.radius != null && border is OutlineInputBorder) { + border = border.copyWith(borderRadius: legacy.radius); + } + borders.border = border.copyWith(borderSide: legacy.side); + borders.enabledBorder = borders.border; + borders.focusedBorder = + borders.border!.copyWith(borderSide: legacy.focusedSide(theme)); + } return borders; } @@ -138,11 +206,19 @@ FormFieldBoxBorder parseFormFieldBoxBorder(Control control, ThemeData theme, // A state entry without a side inherits the default entry's side, as on the // Material side. - var explicitSide = (entry is Map ? entry["side"] : null) ?? + dynamic explicitSide = (entry is Map ? entry["side"] : null) ?? (defaultEntry is Map ? defaultEntry["side"] : null); var borderSide = parseBorderSide(explicitSide, theme, defaultValue: const BorderSide())!; + // Deprecated fallback: only fills what the new API left unspecified. + var legacy = _LegacyBorderProps.of(control, theme); + if (explicitSide == null && legacy.hasSide) { + borderSide = + control.disabled ? legacy.side : legacy.focusedSideOr(focused, theme); + explicitSide = true; + } + switch (entry is Map ? entry["_type"] : "outline") { case "underline": // The underline's default top-corner radius is treated as unconfigured, @@ -166,8 +242,10 @@ FormFieldBoxBorder parseFormFieldBoxBorder(Control control, ThemeData theme, return FormFieldBoxBorder( border: explicitSide != null ? Border.fromBorderSide(borderSide) : null, - borderRadius: - entry is Map ? parseBorderRadius(entry["border_radius"]) : null); + borderRadius: (entry is Map + ? parseBorderRadius(entry["border_radius"]) + : null) ?? + legacy.radius); } } diff --git a/sdk/python/packages/flet/src/flet/controls/cupertino/cupertino_textfield.py b/sdk/python/packages/flet/src/flet/controls/cupertino/cupertino_textfield.py index 434f548fa2..2f4989912e 100644 --- a/sdk/python/packages/flet/src/flet/controls/cupertino/cupertino_textfield.py +++ b/sdk/python/packages/flet/src/flet/controls/cupertino/cupertino_textfield.py @@ -6,7 +6,7 @@ from flet.controls.box import BoxShadowValue, DecorationImage from flet.controls.control_state import ControlStateValue from flet.controls.gradients import Gradient -from flet.controls.material.form_field_control import InputBorder, OutlineInputBorder +from flet.controls.material.form_field_control import InputBorder from flet.controls.material.textfield import TextField from flet.controls.padding import Padding, PaddingValue from flet.controls.text_style import TextStyle @@ -138,9 +138,7 @@ class CupertinoTextField(TextField): is not :attr:`flet.OverlayVisibilityMode.NEVER`. """ - border: ControlStateValue[InputBorder] = field( - default_factory=OutlineInputBorder, kw_only=True - ) + border: Optional[ControlStateValue[InputBorder]] = field(default=None, kw_only=True) """ The border drawn around this text field. diff --git a/sdk/python/packages/flet/src/flet/controls/material/dropdown.py b/sdk/python/packages/flet/src/flet/controls/material/dropdown.py index 8980bf38f3..4da1d680ec 100644 --- a/sdk/python/packages/flet/src/flet/controls/material/dropdown.py +++ b/sdk/python/packages/flet/src/flet/controls/material/dropdown.py @@ -1,13 +1,14 @@ from dataclasses import field -from typing import Optional +from typing import Annotated, Optional from flet.controls.base_control import control +from flet.controls.border_radius import BorderRadiusValue from flet.controls.buttons import ButtonStyle from flet.controls.control import Control from flet.controls.control_event import ControlEventHandler from flet.controls.control_state import ControlStateValue from flet.controls.layout_control import LayoutControl -from flet.controls.material.form_field_control import InputBorder, OutlineInputBorder +from flet.controls.material.form_field_control import InputBorder from flet.controls.material.menu_bar import MenuStyle from flet.controls.material.textfield import InputFilter, TextCapitalization from flet.controls.padding import PaddingValue @@ -283,7 +284,7 @@ class Dropdown(LayoutControl): The :attr:`label`'s text style. """ - border: ControlStateValue[InputBorder] = field(default_factory=OutlineInputBorder) + border: Optional[ControlStateValue[InputBorder]] = None """ The border drawn around the decorated input area. @@ -304,6 +305,81 @@ class Dropdown(LayoutControl): entry's `side`, if set. """ + border_radius: Annotated[ + Optional[BorderRadiusValue], + V.deprecated( + "border", + version="1.0.0", + delete_version="1.3.0", + reason="Use border=OutlineInputBorder(border_radius=...) instead.", + docs_reason="Use :attr:`border` with an " + ":class:`~flet.OutlineInputBorder` instead.", + ), + ] = None + """ + Rounds the corners of an outlined border. + """ + + border_width: Annotated[ + Optional[Number], + V.deprecated( + "border", + version="1.0.0", + delete_version="1.3.0", + reason="Use border=OutlineInputBorder(side=BorderSide(width=...)) instead.", + docs_reason="Use :attr:`border` with a :class:`~flet.BorderSide` instead.", + ), + ] = None + """ + The width of the border in virtual pixels. + """ + + border_color: Annotated[ + Optional[ColorValue], + V.deprecated( + "border", + version="1.0.0", + delete_version="1.3.0", + reason="Use border=OutlineInputBorder(side=BorderSide(color=...)) instead.", + docs_reason="Use :attr:`border` with a :class:`~flet.BorderSide` instead.", + ), + ] = None + """ + The border color. + """ + + focused_border_width: Annotated[ + Optional[Number], + V.deprecated( + "border", + version="1.0.0", + delete_version="1.3.0", + reason="Use border={ControlState.FOCUSED: OutlineInputBorder(...)} " + "instead.", + docs_reason="Use :attr:`border` with a " + ":attr:`flet.ControlState.FOCUSED` entry instead.", + ), + ] = None + """ + Border width in focused state. + """ + + focused_border_color: Annotated[ + Optional[ColorValue], + V.deprecated( + "border", + version="1.0.0", + delete_version="1.3.0", + reason="Use border={ControlState.FOCUSED: OutlineInputBorder(...)} " + "instead.", + docs_reason="Use :attr:`border` with a " + ":attr:`flet.ControlState.FOCUSED` entry instead.", + ), + ] = None + """ + Border color in focused state. + """ + color: Optional[ColorValue] = None """ Text color. diff --git a/sdk/python/packages/flet/src/flet/controls/material/dropdownm2.py b/sdk/python/packages/flet/src/flet/controls/material/dropdownm2.py index 0e35d0f5ea..2f2a86f58a 100644 --- a/sdk/python/packages/flet/src/flet/controls/material/dropdownm2.py +++ b/sdk/python/packages/flet/src/flet/controls/material/dropdownm2.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Annotated, Optional from flet.controls.alignment import Alignment from flet.controls.base_control import control @@ -154,6 +154,21 @@ class DropdownM2(FormFieldControl): through :attr:`border`. """ + border_radius: Annotated[ + Optional[BorderRadiusValue], + V.deprecated( + "menu_border_radius", + version="1.0.0", + delete_version="1.3.0", + reason="Use menu_border_radius for the menu, or border for the field.", + docs_reason="Use :attr:`menu_border_radius` to shape the menu, or " + ":attr:`border` to shape the input field.", + ), + ] = None + """ + The radii of the open dropdown menu's rounded rectangle shape. + """ + item_height: Optional[Number] = None """ The height of the items/options in the dropdown menu. diff --git a/sdk/python/packages/flet/src/flet/controls/material/form_field_control.py b/sdk/python/packages/flet/src/flet/controls/material/form_field_control.py index 476fba8b7c..8c94d13936 100644 --- a/sdk/python/packages/flet/src/flet/controls/material/form_field_control.py +++ b/sdk/python/packages/flet/src/flet/controls/material/form_field_control.py @@ -1,5 +1,5 @@ from dataclasses import field -from typing import Optional, Union +from typing import Annotated, Optional, Union from flet.controls.base_control import control, value from flet.controls.border import BorderSide @@ -17,6 +17,8 @@ StrOrControl, VerticalAlignment, ) +from flet.utils.deprecated import deprecated_warning +from flet.utils.validation import V __all__ = [ "FormFieldControl", @@ -26,8 +28,41 @@ ] +class _InputBorderMeta(type): + """ + Serves the legacy `InputBorder.OUTLINE` / `UNDERLINE` / `NONE` members. + + They were enum members before 1.0. Resolving them here returns the + equivalent class instance so existing code keeps working, with a + deprecation warning, until they are removed in 1.3.0. + """ + + _LEGACY_MEMBERS = ("OUTLINE", "UNDERLINE", "NONE") + + def __getattr__(cls, name: str): + if name in _InputBorderMeta._LEGACY_MEMBERS: + replacement = { + "OUTLINE": "OutlineInputBorder()", + "UNDERLINE": "UnderlineInputBorder()", + "NONE": "InputBorder.none()", + }[name] + deprecated_warning( + name=f"InputBorder.{name}", + reason=f"Use {replacement} instead.", + version="1.0.0", + delete_version="1.3.0", + type="property", + ) + return { + "OUTLINE": OutlineInputBorder, + "UNDERLINE": UnderlineInputBorder, + "NONE": _NoInputBorder, + }[name]() + raise AttributeError(name) + + @value -class InputBorder: +class InputBorder(metaclass=_InputBorderMeta): """ Base class for borders drawn around the decorated input area of :class:`~flet.FormFieldControl`s. Not intended to be used directly. @@ -183,7 +218,7 @@ class FormFieldControl(LayoutControl): The icon to show before the input field and outside of the decoration's container. """ - border: ControlStateValue[InputBorder] = field(default_factory=OutlineInputBorder) + border: Optional[ControlStateValue[InputBorder]] = None """ The border drawn around the decorated input area. @@ -204,6 +239,81 @@ class FormFieldControl(LayoutControl): entry's `side`, if set. """ + border_radius: Annotated[ + Optional[BorderRadiusValue], + V.deprecated( + "border", + version="1.0.0", + delete_version="1.3.0", + reason="Use border=OutlineInputBorder(border_radius=...) instead.", + docs_reason="Use :attr:`border` with an " + ":class:`~flet.OutlineInputBorder` instead.", + ), + ] = None + """ + Rounds the corners of an outlined border. + """ + + border_width: Annotated[ + Optional[Number], + V.deprecated( + "border", + version="1.0.0", + delete_version="1.3.0", + reason="Use border=OutlineInputBorder(side=BorderSide(width=...)) instead.", + docs_reason="Use :attr:`border` with a :class:`~flet.BorderSide` instead.", + ), + ] = None + """ + The width of the border in virtual pixels. + """ + + border_color: Annotated[ + Optional[ColorValue], + V.deprecated( + "border", + version="1.0.0", + delete_version="1.3.0", + reason="Use border=OutlineInputBorder(side=BorderSide(color=...)) instead.", + docs_reason="Use :attr:`border` with a :class:`~flet.BorderSide` instead.", + ), + ] = None + """ + The border color. + """ + + focused_border_width: Annotated[ + Optional[Number], + V.deprecated( + "border", + version="1.0.0", + delete_version="1.3.0", + reason="Use border={ControlState.FOCUSED: OutlineInputBorder(...)} " + "instead.", + docs_reason="Use :attr:`border` with a " + ":attr:`flet.ControlState.FOCUSED` entry instead.", + ), + ] = None + """ + Border width in focused state. + """ + + focused_border_color: Annotated[ + Optional[ColorValue], + V.deprecated( + "border", + version="1.0.0", + delete_version="1.3.0", + reason="Use border={ControlState.FOCUSED: OutlineInputBorder(...)} " + "instead.", + docs_reason="Use :attr:`border` with a " + ":attr:`flet.ControlState.FOCUSED` entry instead.", + ), + ] = None + """ + Border color in focused state. + """ + color: Optional[ColorValue] = None """ Text color. diff --git a/website/docs/updates/breaking-changes/index.md b/website/docs/updates/breaking-changes/index.md index c25ebbef53..63d5889d6a 100644 --- a/website/docs/updates/breaking-changes/index.md +++ b/website/docs/updates/breaking-changes/index.md @@ -26,7 +26,11 @@ Each guide explains the change, the reason for it, and how to migrate your code. #### Breaking changes -- [`InputBorder` is now a class hierarchy; loose border props removed](/docs/updates/breaking-changes/v1-0-0/inputborder-class-hierarchy) +- [`InputBorder` is now a class hierarchy instead of an enum](/docs/updates/breaking-changes/v1-0-0/inputborder-class-hierarchy) + +#### Deprecations + +- [`InputBorder` enum members and the loose border properties deprecated](/docs/updates/breaking-changes/v1-0-0/inputborder-class-hierarchy) ### Released in Flet 0.86.0 diff --git a/website/docs/updates/breaking-changes/v1-0-0/inputborder-class-hierarchy.md b/website/docs/updates/breaking-changes/v1-0-0/inputborder-class-hierarchy.md index 09480a0dcc..8b8ff3e42a 100644 --- a/website/docs/updates/breaking-changes/v1-0-0/inputborder-class-hierarchy.md +++ b/website/docs/updates/breaking-changes/v1-0-0/inputborder-class-hierarchy.md @@ -1,8 +1,8 @@ --- -title: "InputBorder is now a class hierarchy; loose border props removed" +title: "InputBorder is now a class hierarchy instead of an enum" --- -# `InputBorder` is now a class hierarchy; loose border props removed +# `InputBorder` is now a class hierarchy instead of an enum :::note This guide is accurate as of Flet 1.0.0. Later releases might add new APIs or @@ -23,13 +23,14 @@ mirrors Flutter's bottom edge (`side`, `border_radius`) - [`ft.InputBorder.none()`][flet.InputBorder.none] — draws nothing -At the same time, the loose border properties on `TextField`, `Dropdown`, -`DropdownM2`, and `CupertinoTextField` were **removed**: `border_radius`, -`border_width`, `border_color`, `focused_border_width`, and -`focused_border_color`. Everything they expressed (and more) now lives on the -`border` property, which accepts a single `InputBorder` or a dictionary mapping +Everything the old API expressed — and more — now lives on the `border` +property, which accepts a single `InputBorder` or a dictionary mapping [`ControlState`][flet.ControlState]s to `InputBorder`s. +Where old and new are combined, the new API wins: a `border` that specifies a +side or per-state entries ignores the deprecated properties entirely, while a +bare `border=ft.InputBorder.UNDERLINE` still picks up a legacy `border_color`. + ## Background The enum plus five loose properties could not represent Flutter's actual API: @@ -231,7 +232,8 @@ border. ## Timeline -- Changed in: `1.0.0` +- Deprecated in: `1.0.0` +- Removal in: `1.3.0` ## References diff --git a/website/sidebars.yml b/website/sidebars.yml index 8825e396c1..cf28a1d5a9 100644 --- a/website/sidebars.yml +++ b/website/sidebars.yml @@ -73,7 +73,7 @@ docs: Breaking changes and deprecations: _index: updates/breaking-changes/index.md v1.0.0: - InputBorder is now a class hierarchy; loose border props removed: updates/breaking-changes/v1-0-0/inputborder-class-hierarchy.md + "InputBorder is now a class hierarchy instead of an enum": updates/breaking-changes/v1-0-0/inputborder-class-hierarchy.md v0.86.0: App files ship unpacked in a read-only bundle; storage dirs reworked: updates/breaking-changes/v0-86-0/app-files-unpacked-read-only-bundle.md "Android: site-packages ship zipped; some packages need extract_packages": updates/breaking-changes/v0-86-0/android-extract-packages.md