diff --git a/CHANGELOG.md b/CHANGELOG.md index 022874184b..6cfafc0313 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +## 1.0.0 + +### Breaking changes + +* **`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/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/packages/flet/lib/src/controls/cupertino_textfield.dart b/packages/flet/lib/src/controls/cupertino_textfield.dart index 66a3c4e564..602c3906ca 100644 --- a/packages/flet/lib/src/controls/cupertino_textfield.dart +++ b/packages/flet/lib/src/controls/cupertino_textfield.dart @@ -217,30 +217,8 @@ class _CupertinoTextFieldControlState extends State { _selection = selection; } - var borderRadius = widget.control.getBorderRadius("border_radius"); - - 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; - } - } + var boxBorder = parseFormFieldBoxBorder(widget.control, Theme.of(context), + focused: _focused); var canRevealPassword = widget.control.getBool("can_reveal_password", false)!; @@ -294,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/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..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("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 26aa00aa4e..1fabcc1b64 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,246 @@ 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)); + +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; + } +} + +/// 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); +} -FormFieldInputBorder? parseFormFieldInputBorder(String? value, - [FormFieldInputBorder? defaultValue]) { - return parseEnum(FormFieldInputBorder.values, value, 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)); + } + + // 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; +} + +/// A [BoxDecoration]'s border values, translated from an [InputBorder]. +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"]; + // 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 + // Material side. + 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, + // 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) ?? + legacy.radius); + } } TextInputType? parseTextInputType(String? value, @@ -47,19 +277,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 +337,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 +412,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/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/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/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/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/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", ), 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/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/buttons.py b/sdk/python/packages/flet/src/flet/controls/buttons.py index 1dfbd5d69b..f5cd6f138c 100644 --- a/sdk/python/packages/flet/src/flet/controls/buttons.py +++ b/sdk/python/packages/flet/src/flet/controls/buttons.py @@ -89,7 +89,7 @@ class RoundedRectangleBorder(OutlinedBorder): A border with rounded rectangle corners. """ - radius: Optional[BorderRadiusValue] = None + radius: BorderRadiusValue = 0 """ The radius for each corner. """ 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/cupertino/cupertino_textfield.py b/sdk/python/packages/flet/src/flet/controls/cupertino/cupertino_textfield.py index f647a6fcfd..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 @@ -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 from flet.controls.material.textfield import TextField from flet.controls.padding import Padding, PaddingValue from flet.controls.text_style import TextStyle @@ -50,6 +52,7 @@ class CupertinoTextField(TextField): """ An iOS-style text field. + Example: ```python ft.CupertinoTextField(placeholder_text="Search") ``` @@ -134,3 +137,20 @@ class CupertinoTextField(TextField): or the clear button when :attr:`clear_button_visibility_mode` is not :attr:`flet.OverlayVisibilityMode.NEVER`. """ + + border: Optional[ControlStateValue[InputBorder]] = field(default=None, 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. + """ 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/dropdown.py b/sdk/python/packages/flet/src/flet/controls/material/dropdown.py index f84e23327c..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,5 +1,5 @@ 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 @@ -284,49 +284,105 @@ class Dropdown(LayoutControl): The :attr:`label`'s text style. """ - border: Optional[InputBorder] = None + border: Optional[ControlStateValue[InputBorder]] = None """ - Border around input. + The border drawn around the decorated input area. - Defaults to `InputBorder.OUTLINE`. + 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. """ - color: Optional[ColorValue] = None + 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 """ - Text color. + Rounds the corners of an outlined border. """ - border_width: Number = 1 + 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. - - Tip: - Set to `0` to completely remove the border. """ - border_color: Optional[ColorValue] = None + 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 """ - Border color. - - Tip: - Set to :attr:`flet.Colors.TRANSPARENT` to hide the border. + The border color. """ - border_radius: Optional[BorderRadiusValue] = None + 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 """ - 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. + Border width in focused state. """ - focused_border_width: Optional[Number] = None + 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 width in focused state. + Border color 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..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,7 +1,8 @@ -from typing import Optional +from typing import Annotated, Optional 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,32 @@ 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`. + """ + + 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/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 d4a5271476..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,9 +1,11 @@ -from enum import Enum -from typing import Optional, Union +from dataclasses import field +from typing import Annotated, 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 @@ -15,30 +17,156 @@ StrOrControl, VerticalAlignment, ) +from flet.utils.deprecated import deprecated_warning +from flet.utils.validation import V +__all__ = [ + "FormFieldControl", + "InputBorder", + "OutlineInputBorder", + "UnderlineInputBorder", +] -class InputBorder(Enum): + +class _InputBorderMeta(type): """ - Border styles supported by :class:`~flet.FormFieldControl`. + Serves the legacy `InputBorder.OUTLINE` / `UNDERLINE` / `NONE` members. - These values select the border style drawn around the decorated input area. + 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. """ - NONE = "none" + _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(metaclass=_InputBorderMeta): """ - Draws no border around the decoration's container. + Base class for borders drawn around the decorated input area of + :class:`~flet.FormFieldControl`s. Not intended to be used directly. + + See subclasses/implementations: + + - :class:`~flet.OutlineInputBorder` + - :class:`~flet.UnderlineInputBorder` + - :meth:`flet.InputBorder.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 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. + """ + + 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. + """ + + 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,49 +218,113 @@ class FormFieldControl(LayoutControl): The icon to show before the input field and outside of the decoration's container. """ - border: InputBorder = InputBorder.OUTLINE + border: Optional[ControlStateValue[InputBorder]] = None """ - 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. """ - color: Optional[ColorValue] = None + 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 """ - Text color. + Rounds the corners of an outlined border. """ - bgcolor: Optional[ColorValue] = None + 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 """ - TextField background color. - - Note: - Will not be visible if :attr:`filled` is `False`. + The width of the border in virtual pixels. """ - border_radius: Optional[BorderRadiusValue] = None + 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 """ - 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. + The border color. """ - border_width: Optional[Number] = None + 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. """ - The width of the border in virtual pixels. - Defaults to `1`. + 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. + """ - Tip: - Set to `0` to completely remove the border. + color: Optional[ColorValue] = None + """ + Text color. """ - border_color: Optional[ColorValue] = None + bgcolor: Optional[ColorValue] = None """ - The border color. + TextField background color. - Tip: - Set to :attr:`flet.Colors.TRANSPARENT` to invisible/hide the border. + Note: + Will not be visible if :attr:`filled` is `False`. """ focused_color: Optional[ColorValue] = None @@ -148,16 +340,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 +373,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 @@ -366,7 +549,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( 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: 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..63d5889d6a 100644 --- a/website/docs/updates/breaking-changes/index.md +++ b/website/docs/updates/breaking-changes/index.md @@ -22,6 +22,16 @@ 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 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 #### 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..8b8ff3e42a --- /dev/null +++ b/website/docs/updates/breaking-changes/v1-0-0/inputborder-class-hierarchy.md @@ -0,0 +1,246 @@ +--- +title: "InputBorder is now a class hierarchy instead of an enum" +--- + +# `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 +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 + +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: +`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, the `DEFAULT`, `FOCUSED` and `DISABLED` + entries apply; `ERROR` is ignored, as this control does not render an error + state. + +## Timeline + +- Deprecated in: `1.0.0` +- Removal in: `1.3.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) +- Issues and PRs: [#6773](https://github.com/flet-dev/flet/pull/6773) +- 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..cf28a1d5a9 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 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 @@ -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: