diff --git a/package/lib/src/controls/container.dart b/package/lib/src/controls/container.dart index 89619345a3..de7a54c2d3 100644 --- a/package/lib/src/controls/container.dart +++ b/package/lib/src/controls/container.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:typed_data'; import 'package:collection/collection.dart'; +import 'package:flet/src/utils/shadows.dart'; import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; @@ -64,6 +65,7 @@ class ContainerControl extends StatelessWidget { : null; var animation = parseAnimation(control, "animate"); + var blur = parseBlur(control, "blur"); final server = FletAppServices.of(context).server; @@ -107,6 +109,8 @@ class ContainerControl extends StatelessWidget { control.attrString("shape", "")!.toLowerCase(), orElse: () => BoxShape.rectangle); + var borderRadius = parseBorderRadius(control, "borderRadius"); + var boxDecor = BoxDecoration( color: bgColor, gradient: gradient, @@ -114,8 +118,11 @@ class ContainerControl extends StatelessWidget { backgroundBlendMode: bgColor != null || gradient != null ? blendMode : null, border: parseBorder(Theme.of(context), control, "border"), - borderRadius: parseBorderRadius(control, "borderRadius"), - shape: shape); + borderRadius: borderRadius, + shape: shape, + boxShadow: parseBoxShadow(Theme.of(context), control, "shadow")); + + Widget? result; if ((onClick || onLongPress || onHover) && ink && !disabled) { var ink = Ink( @@ -165,36 +172,33 @@ class ContainerControl extends StatelessWidget { child: child, ), )); - return constrainedControl( - context, - animation == null - ? Container( - width: control.attrDouble("width"), - height: control.attrDouble("height"), - margin: parseEdgeInsets(control, "margin"), - clipBehavior: clipBehavior, - child: ink, - ) - : AnimatedContainer( - duration: animation.duration, - curve: animation.curve, - width: control.attrDouble("width"), - height: control.attrDouble("height"), - margin: parseEdgeInsets(control, "margin"), - clipBehavior: clipBehavior, - onEnd: control.attrBool("onAnimationEnd", false)! - ? () { - server.sendPageEvent( - eventTarget: control.id, - eventName: "animation_end", - eventData: "container"); - } - : null, - child: ink), - parent, - control); + + result = animation == null + ? Container( + width: control.attrDouble("width"), + height: control.attrDouble("height"), + margin: parseEdgeInsets(control, "margin"), + clipBehavior: clipBehavior, + child: ink, + ) + : AnimatedContainer( + duration: animation.duration, + curve: animation.curve, + width: control.attrDouble("width"), + height: control.attrDouble("height"), + margin: parseEdgeInsets(control, "margin"), + clipBehavior: clipBehavior, + onEnd: control.attrBool("onAnimationEnd", false)! + ? () { + server.sendPageEvent( + eventTarget: control.id, + eventName: "animation_end", + eventData: "container"); + } + : null, + child: ink); } else { - Widget container = animation == null + result = animation == null ? Container( width: control.attrDouble("width"), height: control.attrDouble("height"), @@ -225,7 +229,7 @@ class ContainerControl extends StatelessWidget { child: child); if ((onClick || onLongPress || onHover) && !disabled) { - container = MouseRegion( + result = MouseRegion( cursor: SystemMouseCursors.click, onEnter: onHover ? (value) { @@ -271,12 +275,21 @@ class ContainerControl extends StatelessWidget { eventData: ""); } : null, - child: container, + child: result, ), ); } - return constrainedControl(context, container, parent, control); } + + if (blur != null) { + result = borderRadius != null + ? ClipRRect( + borderRadius: borderRadius, + child: BackdropFilter(filter: blur, child: result)) + : ClipRect(child: BackdropFilter(filter: blur, child: result)); + } + + return constrainedControl(context, result, parent, control); }); } } diff --git a/package/lib/src/utils/colors.dart b/package/lib/src/utils/colors.dart index 2f67e511db..80d88a15b3 100644 --- a/package/lib/src/utils/colors.dart +++ b/package/lib/src/utils/colors.dart @@ -100,6 +100,7 @@ Map _materialColors = { "deeporange": Colors.deepOrange, "brown": Colors.brown, "bluegrey": Colors.blueGrey, + "grey": Colors.grey }; Map _materialAccentColors = { diff --git a/package/lib/src/utils/images.dart b/package/lib/src/utils/images.dart index 54f2adcd54..f2b274d455 100644 --- a/package/lib/src/utils/images.dart +++ b/package/lib/src/utils/images.dart @@ -1,7 +1,12 @@ +import 'dart:convert'; +import 'dart:ui'; + import 'package:collection/collection.dart'; +import 'package:flet/src/utils/numbers.dart'; import 'package:flutter/material.dart'; import '../models/control.dart'; +import 'gradient.dart'; export 'images_io.dart' if (dart.library.js) "images_web.dart"; @@ -17,3 +22,31 @@ BoxFit? parseBoxFit(Control control, String propName) { return BoxFit.values.firstWhereOrNull((e) => e.name.toLowerCase() == control.attrString(propName, "")!.toLowerCase()); } + +ImageFilter? parseBlur(Control control, String propName) { + var v = control.attrString(propName, null); + if (v == null) { + return null; + } + + final j1 = json.decode(v); + return blurImageFilterFromJSON(j1); +} + +ImageFilter blurImageFilterFromJSON(dynamic json) { + double sigmaX = 0.0; + double sigmaY = 0.0; + TileMode tileMode = TileMode.clamp; + if (json is int || json is double) { + sigmaX = sigmaY = parseDouble(json); + } else if (json is List && json.length > 1) { + sigmaX = parseDouble(json[0]); + sigmaY = parseDouble(json[1]); + } else { + sigmaX = parseDouble(json["sigma_x"]); + sigmaY = parseDouble(json["sigma_y"]); + tileMode = parseTileMode(json["tile_mode"]); + } + + return ImageFilter.blur(sigmaX: sigmaX, sigmaY: sigmaY, tileMode: tileMode); +} diff --git a/package/lib/src/utils/shadows.dart b/package/lib/src/utils/shadows.dart new file mode 100644 index 0000000000..4d22df509d --- /dev/null +++ b/package/lib/src/utils/shadows.dart @@ -0,0 +1,45 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; + +import '../models/control.dart'; +import '../utils/numbers.dart'; +import '../utils/transforms.dart'; +import 'colors.dart'; + +List parseBoxShadow( + ThemeData theme, Control control, String propName) { + var v = control.attrString(propName, null); + if (v == null) { + return []; + } + + final j1 = json.decode(v); + return boxShadowsFromJSON(theme, j1); +} + +List boxShadowsFromJSON(ThemeData theme, dynamic json) { + if (json is List) { + return json.map((e) => boxShadowFromJSON(theme, e)).toList(); + } else { + return [boxShadowFromJSON(theme, json)]; + } +} + +BoxShadow boxShadowFromJSON(ThemeData theme, dynamic json) { + var offset = json["offset"] != null ? offsetFromJSON(json["offset"]) : null; + return BoxShadow( + color: json["color"] != null + ? HexColor.fromString(theme, json["color"]) ?? const Color(0xFF000000) + : const Color(0xFF000000), + offset: offset != null ? Offset(offset.x, offset.y) : Offset.zero, + blurStyle: json["blur_style"] != null + ? BlurStyle.values + .firstWhere((e) => e.name.toLowerCase() == json["blur_style"]) + : BlurStyle.normal, + blurRadius: + json["blur_radius"] != null ? parseDouble(json["blur_radius"]) : 0.0, + spreadRadius: json["spread_radius"] != null + ? parseDouble(json["spread_radius"]) + : 0.0); +} diff --git a/sdk/python/packages/flet-core/src/flet_core/__init__.py b/sdk/python/packages/flet-core/src/flet_core/__init__.py index 0d3d6215e8..5193e97db0 100644 --- a/sdk/python/packages/flet-core/src/flet_core/__init__.py +++ b/sdk/python/packages/flet-core/src/flet_core/__init__.py @@ -35,7 +35,14 @@ from flet_core.checkbox import Checkbox from flet_core.circle_avatar import CircleAvatar from flet_core.column import Column -from flet_core.container import Container, ContainerTapEvent +from flet_core.container import ( + Blur, + BlurTileMode, + BoxShadow, + Container, + ContainerTapEvent, + ShadowBlurStyle, +) from flet_core.control import Control, OptionalNumber from flet_core.control_event import ControlEvent from flet_core.datatable import ( diff --git a/sdk/python/packages/flet-core/src/flet_core/colors.py b/sdk/python/packages/flet-core/src/flet_core/colors.py index 043f08024c..a5150ffe9f 100644 --- a/sdk/python/packages/flet-core/src/flet_core/colors.py +++ b/sdk/python/packages/flet-core/src/flet_core/colors.py @@ -356,3 +356,14 @@ DEEP_ORANGE_ACCENT_200 = "deeporangeaccent200" DEEP_ORANGE_ACCENT_400 = "deeporangeaccent400" DEEP_ORANGE_ACCENT_700 = "deeporangeaccent700" +GREY = "grey" +GREY_50 = "grey50" +GREY_100 = "grey100" +GREY_200 = "grey200" +GREY_300 = "grey300" +GREY_400 = "grey400" +GREY_500 = "grey500" +GREY_600 = "grey600" +GREY_700 = "grey700" +GREY_800 = "grey800" +GREY_900 = "grey900" diff --git a/sdk/python/packages/flet-core/src/flet_core/container.py b/sdk/python/packages/flet-core/src/flet_core/container.py index 423dc66989..67bdb9490b 100644 --- a/sdk/python/packages/flet-core/src/flet_core/container.py +++ b/sdk/python/packages/flet-core/src/flet_core/container.py @@ -1,5 +1,8 @@ +import dataclasses import json -from typing import Any, Optional, Union +from dataclasses import field +from enum import Enum +from typing import Any, List, Optional, Tuple, Union from flet_core.alignment import Alignment from flet_core.border import Border @@ -35,6 +38,36 @@ from typing_extensions import Literal +class BlurTileMode(Enum): + CLAMP = "clamp" + DECAL = "decal" + MIRROR = "mirror" + REPEATED = "repeated" + + +class ShadowBlurStyle(Enum): + NORMAL = "normal" + SOLID = "solid" + OUTER = "outer" + INNER = "inner" + + +@dataclasses.dataclass +class Blur: + sigma_x: float + sigma_y: float + tile_mode: BlurTileMode = field(default=BlurTileMode.CLAMP) + + +@dataclasses.dataclass +class BoxShadow: + spread_radius: Optional[float] = field(default=None) + blur_radius: Optional[float] = field(default=None) + color: Optional[str] = field(default=None) + offset: OffsetValue = field(default=None) + tile_mode: ShadowBlurStyle = field(default=ShadowBlurStyle.NORMAL) + + class Container(ConstrainedControl): """ Container allows to decorate a control with background color and border and position it with padding, margin and alignment. @@ -110,6 +143,10 @@ def __init__( clip_behavior: Optional[ClipBehavior] = None, ink: Optional[bool] = None, animate: AnimationValue = None, + blur: Union[ + None, float, int, Tuple[Union[float, int], Union[float, int]], Blur + ] = None, + shadow: Union[None, BoxShadow, List[BoxShadow]] = None, on_click=None, on_long_press=None, on_hover=None, @@ -168,6 +205,8 @@ def convert_container_tap_event_data(e): self.clip_behavior = clip_behavior self.ink = ink self.animate = animate + self.blur = blur + self.shadow = shadow self.on_click = on_click self.on_long_press = on_long_press self.on_hover = on_hover @@ -184,6 +223,8 @@ def _before_build_command(self): self._set_attr_json("alignment", self.__alignment) self._set_attr_json("gradient", self.__gradient) self._set_attr_json("animate", self.__animate) + self._set_attr_json("blur", self.__blur) + self._set_attr_json("shadow", self.__shadow if self.__shadow else None) def _get_children(self): children = [] @@ -258,6 +299,29 @@ def blend_mode(self, value: BlendMode): def __set_blend_mode(self, value: BlendModeString): self._set_attr("blendMode", value) + # blur + @property + def blur(self): + return self.__blur + + @blur.setter + def blur( + self, + value: Union[ + None, float, int, Tuple[Union[float, int], Union[float, int]], Blur + ], + ): + self.__blur = value + + # shadow + @property + def shadow(self): + return self.__shadow + + @shadow.setter + def shadow(self, value: Union[None, BoxShadow, List[BoxShadow]]): + self.__shadow = value if value is not None else [] + # border @property def border(self) -> Optional[Border]: