From 5e029b5346555aac0bcf5067e4474ce2b39bd6ac Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Sun, 14 Dec 2025 15:18:28 +0100 Subject: [PATCH 01/12] Refactor theme handling and enhance tab construction for better customization and clarity. --- .../flet/lib/src/controls/control_widget.dart | 4 +- packages/flet/lib/src/controls/tabs.dart | 112 ++++++++++++++++-- 2 files changed, 102 insertions(+), 14 deletions(-) diff --git a/packages/flet/lib/src/controls/control_widget.dart b/packages/flet/lib/src/controls/control_widget.dart index 8b1e331e2b..8cb5264eeb 100644 --- a/packages/flet/lib/src/controls/control_widget.dart +++ b/packages/flet/lib/src/controls/control_widget.dart @@ -77,9 +77,7 @@ class ControlWidget extends StatelessWidget { (backend) => backend.platformBrightness, ); return buildTheme(brightness); - } - - if (themeMode == ThemeMode.light) { + } else if (themeMode == ThemeMode.light) { return buildTheme(Brightness.light); } else if (themeMode == ThemeMode.dark) { return buildTheme(Brightness.dark); diff --git a/packages/flet/lib/src/controls/tabs.dart b/packages/flet/lib/src/controls/tabs.dart index 31c9191fa3..95bc2ba4ee 100644 --- a/packages/flet/lib/src/controls/tabs.dart +++ b/packages/flet/lib/src/controls/tabs.dart @@ -1,21 +1,27 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import '../extensions/control.dart'; +import '../flet_backend.dart'; import '../models/control.dart'; import '../utils/alignment.dart'; import '../utils/animations.dart'; import '../utils/borders.dart'; import '../utils/colors.dart'; import '../utils/edge_insets.dart'; +import '../utils/keys.dart'; import '../utils/layout.dart'; import '../utils/misc.dart'; import '../utils/mouse.dart'; import '../utils/numbers.dart'; import '../utils/tabs.dart'; import '../utils/text.dart'; +import '../utils/theme.dart'; import '../utils/time.dart'; +import '../widgets/control_inherited_notifier.dart'; import '../widgets/error.dart'; import 'base_controls.dart'; +import 'control_widget.dart'; /// Default duration for tab animation if none is provided. const Duration kDefaultTabAnimationDuration = Duration(milliseconds: 100); @@ -202,23 +208,95 @@ class TabBarViewControl extends StatelessWidget { } } -class TabControl extends StatelessWidget { +class TabControl extends Tab { final Control control; - const TabControl({super.key, required this.control}); + TabControl({super.key, required this.control}) + : super( + // These are primarily used by Flutter's TabBar heuristics (e.g. to + // decide text-vs-text+icon height) because TabBar checks `tab is Tab`. + // The actual tab UI is built from `control` in `build()`. + text: _tabTextHint(control), + icon: _tabIconHint(control), + ); + + static bool _hasLabel(Control control) => control.get("label") != null; + + static bool _hasIcon(Control control) => control.get("icon") != null; + + static String? _tabTextHint(Control control) { + if (!_hasLabel(control)) return null; + final label = control.get("label"); + return label is String ? label : ""; + } + + static Widget? _tabIconHint(Control control) { + if (!_hasIcon(control)) return null; + // Non-null placeholder so TabBar can detect icon presence. + return const SizedBox.shrink(); + } + + static Key? _keyFromControl(Control control) { + final controlKey = control.getKey("key"); + if (controlKey is ControlValueKey) { + return ValueKey(controlKey.value); + } + return null; + } @override Widget build(BuildContext context) { debugPrint("TabControl build: ${control.id}"); - return BaseControl( - control: control, - child: Tab( - icon: control.buildIconOrWidget("icon"), - height: control.getDouble("height"), - iconMargin: control.getMargin("icon_margin"), - child: control.buildTextOrWidget("label"), - )); + Widget tab = ControlInheritedNotifier( + notifier: control, + child: Builder(builder: (context) { + ControlInheritedNotifier.of(context); + + return BaseControl( + control: control, + child: Tab( + key: _keyFromControl(control), + icon: control.buildIconOrWidget("icon"), + height: control.getDouble("height"), + iconMargin: control.getMargin("icon_margin"), + child: control.buildTextOrWidget("label"), + ), + ); + }), + ); + + final hasNoThemes = + control.get("theme") == null && control.get("dark_theme") == null; + final themeMode = control.getThemeMode("theme_mode"); + if (hasNoThemes && themeMode == null) { + return tab; + } + + final ThemeData? parentTheme = + (themeMode == null) ? Theme.of(context) : null; + + Widget buildTheme(Brightness? brightness) { + final themeData = control.getTheme( + brightness == Brightness.dark ? "dark_theme" : "theme", + context, + brightness, + parentTheme: parentTheme); + return Theme(data: themeData, child: tab); + } + + if (themeMode == ThemeMode.system) { + final brightness = context.select( + (backend) => backend.platformBrightness, + ); + return buildTheme(brightness); + } else if (themeMode == ThemeMode.light) { + return buildTheme(Brightness.light); + } else if (themeMode == ThemeMode.dark) { + return buildTheme(Brightness.dark); + } else { + return buildTheme(parentTheme?.brightness); + } } } @@ -268,7 +346,19 @@ class _TabBarControlState extends State { .getTextStyle("unselected_label_text_style", Theme.of(context)); var splashBorderRadius = widget.control.getBorderRadius("splash_border_radius"); - var tabs = widget.control.buildWidgets("tabs"); + final tabs = widget.control.children("tabs").map((tab) { + // Ensure parent gets rebuilt when a tab becomes visible/invisible. + tab.notifyParent = true; + + if (tab.type == "Tab") { + return TabControl(control: tab); + } + + // TabBar applies consistent sizing/ink behavior only when `tab is Tab`. + // Wrapping arbitrary controls into a `Tab` keeps hover/splash sizes aligned + // with the tab bar. + return Tab(child: ControlWidget(control: tab)); + }).toList(); void onTap(int index) { widget.control.triggerEvent("click", index); From 33acb06a0164ced2d3e16800ff0d995786066e6d Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Sun, 14 Dec 2025 15:36:08 +0100 Subject: [PATCH 02/12] Refactor control widget theming and notifier logic by introducing `control_wrappers`. --- .../flet/lib/src/controls/control_widget.dart | 62 ++++------------- packages/flet/lib/src/controls/tabs.dart | 66 ++++--------------- .../lib/src/widgets/control_wrappers.dart | 64 ++++++++++++++++++ 3 files changed, 90 insertions(+), 102 deletions(-) create mode 100644 packages/flet/lib/src/widgets/control_wrappers.dart diff --git a/packages/flet/lib/src/controls/control_widget.dart b/packages/flet/lib/src/controls/control_widget.dart index 8cb5264eeb..b677c8953c 100644 --- a/packages/flet/lib/src/controls/control_widget.dart +++ b/packages/flet/lib/src/controls/control_widget.dart @@ -1,13 +1,10 @@ import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; import '../extensions/control.dart'; import '../flet_backend.dart'; import '../models/control.dart'; import '../utils/keys.dart'; -import '../utils/numbers.dart'; -import '../utils/theme.dart'; -import '../widgets/control_inherited_notifier.dart'; +import '../widgets/control_wrappers.dart'; import '../widgets/error.dart'; class ControlWidget extends StatelessWidget { @@ -35,54 +32,19 @@ class ControlWidget extends StatelessWidget { } widget = ErrorControl("Unknown control: ${control.type}"); } else { - widget = ControlInheritedNotifier( - notifier: control, - child: Builder(builder: (context) { - ControlInheritedNotifier.of(context); - - Widget? cw; - for (var extension in FletBackend.of(context).extensions) { - cw = extension.createWidget(key, control); - if (cw != null) return cw; - } - - return ErrorControl("Unknown control: ${control.type}"); - }), - ); + widget = withControlInheritedNotifier(control, (context) { + Widget? cw; + for (var extension in FletBackend.of(context).extensions) { + cw = extension.createWidget(key, control); + if (cw != null) return cw; + } + + return ErrorControl("Unknown control: ${control.type}"); + }); } - // Return original widget if no theme is defined final isRootControl = control == FletBackend.of(context).page; - final hasNoThemes = control.getString("theme") == null && - control.getString("dark_theme") == null; - final themeMode = control.getThemeMode("theme_mode"); - - if (isRootControl || (hasNoThemes && themeMode == null)) { - return widget; - } - - // Wrap in Theme widget - final ThemeData? parentTheme = - (themeMode == null) ? Theme.of(context) : null; - - Widget buildTheme(Brightness? brightness) { - final themeProp = brightness == Brightness.dark ? "dark_theme" : "theme"; - final themeData = parseTheme(control.get(themeProp), context, brightness, - parentTheme: parentTheme); - return Theme(data: themeData, child: widget!); - } - - if (themeMode == ThemeMode.system) { - final brightness = context.select( - (backend) => backend.platformBrightness, - ); - return buildTheme(brightness); - } else if (themeMode == ThemeMode.light) { - return buildTheme(Brightness.light); - } else if (themeMode == ThemeMode.dark) { - return buildTheme(Brightness.dark); - } else { - return buildTheme(parentTheme?.brightness); - } + return wrapWithControlTheme(control, context, widget, + isRootControl: isRootControl); } } diff --git a/packages/flet/lib/src/controls/tabs.dart b/packages/flet/lib/src/controls/tabs.dart index 95bc2ba4ee..424eba8a77 100644 --- a/packages/flet/lib/src/controls/tabs.dart +++ b/packages/flet/lib/src/controls/tabs.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; import '../extensions/control.dart'; -import '../flet_backend.dart'; import '../models/control.dart'; import '../utils/alignment.dart'; import '../utils/animations.dart'; @@ -16,9 +14,8 @@ import '../utils/mouse.dart'; import '../utils/numbers.dart'; import '../utils/tabs.dart'; import '../utils/text.dart'; -import '../utils/theme.dart'; import '../utils/time.dart'; -import '../widgets/control_inherited_notifier.dart'; +import '../widgets/control_wrappers.dart'; import '../widgets/error.dart'; import 'base_controls.dart'; import 'control_widget.dart'; @@ -248,55 +245,20 @@ class TabControl extends Tab { Widget build(BuildContext context) { debugPrint("TabControl build: ${control.id}"); - Widget tab = ControlInheritedNotifier( - notifier: control, - child: Builder(builder: (context) { - ControlInheritedNotifier.of(context); - - return BaseControl( - control: control, - child: Tab( - key: _keyFromControl(control), - icon: control.buildIconOrWidget("icon"), - height: control.getDouble("height"), - iconMargin: control.getMargin("icon_margin"), - child: control.buildTextOrWidget("label"), - ), - ); - }), - ); - - final hasNoThemes = - control.get("theme") == null && control.get("dark_theme") == null; - final themeMode = control.getThemeMode("theme_mode"); - if (hasNoThemes && themeMode == null) { - return tab; - } - - final ThemeData? parentTheme = - (themeMode == null) ? Theme.of(context) : null; - - Widget buildTheme(Brightness? brightness) { - final themeData = control.getTheme( - brightness == Brightness.dark ? "dark_theme" : "theme", - context, - brightness, - parentTheme: parentTheme); - return Theme(data: themeData, child: tab); - } - - if (themeMode == ThemeMode.system) { - final brightness = context.select( - (backend) => backend.platformBrightness, + final tabWidget = withControlInheritedNotifier(control, (context) { + return BaseControl( + control: control, + child: Tab( + key: _keyFromControl(control), + icon: control.buildIconOrWidget("icon"), + height: control.getDouble("height"), + iconMargin: control.getMargin("icon_margin"), + child: control.buildTextOrWidget("label"), + ), ); - return buildTheme(brightness); - } else if (themeMode == ThemeMode.light) { - return buildTheme(Brightness.light); - } else if (themeMode == ThemeMode.dark) { - return buildTheme(Brightness.dark); - } else { - return buildTheme(parentTheme?.brightness); - } + }); + + return wrapWithControlTheme(control, context, tabWidget); } } diff --git a/packages/flet/lib/src/widgets/control_wrappers.dart b/packages/flet/lib/src/widgets/control_wrappers.dart new file mode 100644 index 0000000000..a536e21608 --- /dev/null +++ b/packages/flet/lib/src/widgets/control_wrappers.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../extensions/control.dart'; +import '../flet_backend.dart'; +import '../models/control.dart'; +import '../utils/theme.dart'; +import 'control_inherited_notifier.dart'; + +Widget withControlInheritedNotifier(Control control, WidgetBuilder builder) { + if (control.internals?["skip_inherited_notifier"] == true) { + return Builder(builder: builder); + } + + return ControlInheritedNotifier( + notifier: control, + child: Builder(builder: (context) { + ControlInheritedNotifier.of(context); + return builder(context); + }), + ); +} + +Widget wrapWithControlTheme( + Control control, + BuildContext context, + Widget child, { + bool isRootControl = false, + bool respectSkipInheritedNotifier = true, +}) { + if (isRootControl) return child; + if (respectSkipInheritedNotifier && + control.internals?["skip_inherited_notifier"] == true) { + return child; + } + + final hasNoThemes = + control.get("theme") == null && control.get("dark_theme") == null; + final themeMode = control.getThemeMode("theme_mode"); + if (hasNoThemes && themeMode == null) { + return child; + } + + final ThemeData? parentTheme = (themeMode == null) ? Theme.of(context) : null; + + Widget buildTheme(Brightness? brightness) { + final themeProp = brightness == Brightness.dark ? "dark_theme" : "theme"; + final themeData = parseTheme(control.get(themeProp), context, brightness, + parentTheme: parentTheme); + return Theme(data: themeData, child: child); + } + + if (themeMode == ThemeMode.system) { + final brightness = context.select( + (backend) => backend.platformBrightness); + return buildTheme(brightness); + } else if (themeMode == ThemeMode.light) { + return buildTheme(Brightness.light); + } else if (themeMode == ThemeMode.dark) { + return buildTheme(Brightness.dark); + } else { + return buildTheme(parentTheme?.brightness); + } +} From f3d2238d9893dfe7e0d82d79df8ed6e1bab8795c Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Sun, 14 Dec 2025 15:42:32 +0100 Subject: [PATCH 03/12] Refactor theme handling logic and simplify root control skipping logic. --- .../flet/lib/src/controls/control_widget.dart | 4 +- .../lib/src/widgets/control_wrappers.dart | 43 ++++++++++++------- 2 files changed, 28 insertions(+), 19 deletions(-) diff --git a/packages/flet/lib/src/controls/control_widget.dart b/packages/flet/lib/src/controls/control_widget.dart index b677c8953c..a91f0175fb 100644 --- a/packages/flet/lib/src/controls/control_widget.dart +++ b/packages/flet/lib/src/controls/control_widget.dart @@ -43,8 +43,6 @@ class ControlWidget extends StatelessWidget { }); } - final isRootControl = control == FletBackend.of(context).page; - return wrapWithControlTheme(control, context, widget, - isRootControl: isRootControl); + return wrapWithControlTheme(control, context, widget); } } diff --git a/packages/flet/lib/src/widgets/control_wrappers.dart b/packages/flet/lib/src/widgets/control_wrappers.dart index a536e21608..04f502f437 100644 --- a/packages/flet/lib/src/widgets/control_wrappers.dart +++ b/packages/flet/lib/src/widgets/control_wrappers.dart @@ -25,10 +25,11 @@ Widget wrapWithControlTheme( Control control, BuildContext context, Widget child, { - bool isRootControl = false, bool respectSkipInheritedNotifier = true, }) { - if (isRootControl) return child; + // skip root/page control + if (control == FletBackend.of(context).page) return child; + if (respectSkipInheritedNotifier && control.internals?["skip_inherited_notifier"] == true) { return child; @@ -43,22 +44,32 @@ Widget wrapWithControlTheme( final ThemeData? parentTheme = (themeMode == null) ? Theme.of(context) : null; + /// Converts ThemeMode to Brightness + Brightness? themeModeToBrightness(ThemeMode? mode) { + switch (mode) { + case ThemeMode.light: + return Brightness.light; + case ThemeMode.dark: + return Brightness.dark; + case ThemeMode.system: + return context.select( + (backend) => backend.platformBrightness, + ); + case null: + return parentTheme?.brightness; + } + } + Widget buildTheme(Brightness? brightness) { - final themeProp = brightness == Brightness.dark ? "dark_theme" : "theme"; - final themeData = parseTheme(control.get(themeProp), context, brightness, - parentTheme: parentTheme); + final themeData = control.getTheme( + brightness == Brightness.dark ? "dark_theme" : "theme", + context, + brightness, + parentTheme: parentTheme, + ); return Theme(data: themeData, child: child); } - if (themeMode == ThemeMode.system) { - final brightness = context.select( - (backend) => backend.platformBrightness); - return buildTheme(brightness); - } else if (themeMode == ThemeMode.light) { - return buildTheme(Brightness.light); - } else if (themeMode == ThemeMode.dark) { - return buildTheme(Brightness.dark); - } else { - return buildTheme(parentTheme?.brightness); - } + final brightness = themeModeToBrightness(themeMode); + return buildTheme(brightness); } From fcf1360fca8eeffe99ae1e7cb6a33b9ee92375d0 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Sun, 14 Dec 2025 15:50:57 +0100 Subject: [PATCH 04/12] Refactor `wrapWithControlTheme` by removing `respectSkipInheritedNotifier` argument for streamlined semantics. --- packages/flet/lib/src/widgets/control_wrappers.dart | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/flet/lib/src/widgets/control_wrappers.dart b/packages/flet/lib/src/widgets/control_wrappers.dart index 04f502f437..24e67afaab 100644 --- a/packages/flet/lib/src/widgets/control_wrappers.dart +++ b/packages/flet/lib/src/widgets/control_wrappers.dart @@ -22,16 +22,13 @@ Widget withControlInheritedNotifier(Control control, WidgetBuilder builder) { } Widget wrapWithControlTheme( - Control control, - BuildContext context, - Widget child, { - bool respectSkipInheritedNotifier = true, -}) { + Control control, BuildContext context, Widget child) { // skip root/page control if (control == FletBackend.of(context).page) return child; - if (respectSkipInheritedNotifier && - control.internals?["skip_inherited_notifier"] == true) { + // Preserve ControlWidget semantics: if a control opts out of + // ControlInheritedNotifier, it also skips per-control theme wrapping. + if (control.internals?["skip_inherited_notifier"] == true) { return child; } From e5e77250e216e7fcae5c19f0a073951cb14ba612 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Sun, 14 Dec 2025 16:13:38 +0100 Subject: [PATCH 05/12] Refactor control widget rendering to streamline theme and notifier wrapping. --- .../flet/lib/src/controls/control_widget.dart | 21 ++++--------------- packages/flet/lib/src/controls/tabs.dart | 4 +--- .../lib/src/widgets/control_wrappers.dart | 17 +++++++++++++-- 3 files changed, 20 insertions(+), 22 deletions(-) diff --git a/packages/flet/lib/src/controls/control_widget.dart b/packages/flet/lib/src/controls/control_widget.dart index a91f0175fb..0f2c4f6fb2 100644 --- a/packages/flet/lib/src/controls/control_widget.dart +++ b/packages/flet/lib/src/controls/control_widget.dart @@ -24,25 +24,12 @@ class ControlWidget extends StatelessWidget { key = ValueKey(controlKey.value); } - Widget? widget; - if (control.internals?["skip_inherited_notifier"] == true) { + return wrapWithControlInheritedNotifierAndTheme(control, context, (context) { for (var extension in FletBackend.of(context).extensions) { - widget = extension.createWidget(key, control); + final widget = extension.createWidget(key, control); if (widget != null) return widget; } - widget = ErrorControl("Unknown control: ${control.type}"); - } else { - widget = withControlInheritedNotifier(control, (context) { - Widget? cw; - for (var extension in FletBackend.of(context).extensions) { - cw = extension.createWidget(key, control); - if (cw != null) return cw; - } - - return ErrorControl("Unknown control: ${control.type}"); - }); - } - - return wrapWithControlTheme(control, context, widget); + return ErrorControl("Unknown control: ${control.type}"); + }); } } diff --git a/packages/flet/lib/src/controls/tabs.dart b/packages/flet/lib/src/controls/tabs.dart index 424eba8a77..ca6256794f 100644 --- a/packages/flet/lib/src/controls/tabs.dart +++ b/packages/flet/lib/src/controls/tabs.dart @@ -245,7 +245,7 @@ class TabControl extends Tab { Widget build(BuildContext context) { debugPrint("TabControl build: ${control.id}"); - final tabWidget = withControlInheritedNotifier(control, (context) { + return wrapWithControlInheritedNotifierAndTheme(control, context, (context) { return BaseControl( control: control, child: Tab( @@ -257,8 +257,6 @@ class TabControl extends Tab { ), ); }); - - return wrapWithControlTheme(control, context, tabWidget); } } diff --git a/packages/flet/lib/src/widgets/control_wrappers.dart b/packages/flet/lib/src/widgets/control_wrappers.dart index 24e67afaab..bfc2a3943b 100644 --- a/packages/flet/lib/src/widgets/control_wrappers.dart +++ b/packages/flet/lib/src/widgets/control_wrappers.dart @@ -7,9 +7,13 @@ import '../models/control.dart'; import '../utils/theme.dart'; import 'control_inherited_notifier.dart'; -Widget withControlInheritedNotifier(Control control, WidgetBuilder builder) { +Widget withControlInheritedNotifier( + Control control, + BuildContext context, + WidgetBuilder builder, +) { if (control.internals?["skip_inherited_notifier"] == true) { - return Builder(builder: builder); + return builder(context); } return ControlInheritedNotifier( @@ -21,6 +25,15 @@ Widget withControlInheritedNotifier(Control control, WidgetBuilder builder) { ); } +Widget wrapWithControlInheritedNotifierAndTheme( + Control control, + BuildContext context, + WidgetBuilder builder, +) { + final child = withControlInheritedNotifier(control, context, builder); + return wrapWithControlTheme(control, context, child); +} + Widget wrapWithControlTheme( Control control, BuildContext context, Widget child) { // skip root/page control From fdc208a28f6809c1b3f42a72cb6a90b95f2a6ded Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Sun, 14 Dec 2025 16:37:18 +0100 Subject: [PATCH 06/12] Refactor `wrapWithControlInheritedNotifierAndTheme` to remove redundant `context` parameter. --- .../flet/lib/src/controls/control_widget.dart | 2 +- packages/flet/lib/src/controls/tabs.dart | 2 +- .../flet/lib/src/widgets/control_wrappers.dart | 18 ++++++------------ 3 files changed, 8 insertions(+), 14 deletions(-) diff --git a/packages/flet/lib/src/controls/control_widget.dart b/packages/flet/lib/src/controls/control_widget.dart index 0f2c4f6fb2..68ffd0ce27 100644 --- a/packages/flet/lib/src/controls/control_widget.dart +++ b/packages/flet/lib/src/controls/control_widget.dart @@ -24,7 +24,7 @@ class ControlWidget extends StatelessWidget { key = ValueKey(controlKey.value); } - return wrapWithControlInheritedNotifierAndTheme(control, context, (context) { + return wrapWithControlInheritedNotifierAndTheme(control, (context) { for (var extension in FletBackend.of(context).extensions) { final widget = extension.createWidget(key, control); if (widget != null) return widget; diff --git a/packages/flet/lib/src/controls/tabs.dart b/packages/flet/lib/src/controls/tabs.dart index ca6256794f..7970104a7f 100644 --- a/packages/flet/lib/src/controls/tabs.dart +++ b/packages/flet/lib/src/controls/tabs.dart @@ -245,7 +245,7 @@ class TabControl extends Tab { Widget build(BuildContext context) { debugPrint("TabControl build: ${control.id}"); - return wrapWithControlInheritedNotifierAndTheme(control, context, (context) { + return wrapWithControlInheritedNotifierAndTheme(control, (context) { return BaseControl( control: control, child: Tab( diff --git a/packages/flet/lib/src/widgets/control_wrappers.dart b/packages/flet/lib/src/widgets/control_wrappers.dart index bfc2a3943b..380174dedf 100644 --- a/packages/flet/lib/src/widgets/control_wrappers.dart +++ b/packages/flet/lib/src/widgets/control_wrappers.dart @@ -7,13 +7,9 @@ import '../models/control.dart'; import '../utils/theme.dart'; import 'control_inherited_notifier.dart'; -Widget withControlInheritedNotifier( - Control control, - BuildContext context, - WidgetBuilder builder, -) { +Widget withControlInheritedNotifier(Control control, WidgetBuilder builder) { if (control.internals?["skip_inherited_notifier"] == true) { - return builder(context); + return Builder(builder: builder); } return ControlInheritedNotifier( @@ -26,12 +22,10 @@ Widget withControlInheritedNotifier( } Widget wrapWithControlInheritedNotifierAndTheme( - Control control, - BuildContext context, - WidgetBuilder builder, -) { - final child = withControlInheritedNotifier(control, context, builder); - return wrapWithControlTheme(control, context, child); + Control control, WidgetBuilder builder) { + return Builder( + builder: (context) => wrapWithControlTheme( + control, context, withControlInheritedNotifier(control, builder))); } Widget wrapWithControlTheme( From 697399bfd324cc7235c2236fef54e14d2c7fc021 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Sun, 14 Dec 2025 16:43:06 +0100 Subject: [PATCH 07/12] Moved `wrapWithControlTheme` and `withControlInheritedNotifier` from `control_wrappers` to `control_inherited_notifier`, and updated imports accordingly. --- .../flet/lib/src/controls/control_widget.dart | 2 +- packages/flet/lib/src/controls/tabs.dart | 2 +- .../widgets/control_inherited_notifier.dart | 75 +++++++++++++++++- .../lib/src/widgets/control_wrappers.dart | 79 ------------------- 4 files changed, 76 insertions(+), 82 deletions(-) delete mode 100644 packages/flet/lib/src/widgets/control_wrappers.dart diff --git a/packages/flet/lib/src/controls/control_widget.dart b/packages/flet/lib/src/controls/control_widget.dart index 68ffd0ce27..9c982104de 100644 --- a/packages/flet/lib/src/controls/control_widget.dart +++ b/packages/flet/lib/src/controls/control_widget.dart @@ -4,7 +4,7 @@ import '../extensions/control.dart'; import '../flet_backend.dart'; import '../models/control.dart'; import '../utils/keys.dart'; -import '../widgets/control_wrappers.dart'; +import '../widgets/control_inherited_notifier.dart'; import '../widgets/error.dart'; class ControlWidget extends StatelessWidget { diff --git a/packages/flet/lib/src/controls/tabs.dart b/packages/flet/lib/src/controls/tabs.dart index 7970104a7f..7b70178220 100644 --- a/packages/flet/lib/src/controls/tabs.dart +++ b/packages/flet/lib/src/controls/tabs.dart @@ -15,7 +15,7 @@ import '../utils/numbers.dart'; import '../utils/tabs.dart'; import '../utils/text.dart'; import '../utils/time.dart'; -import '../widgets/control_wrappers.dart'; +import '../widgets/control_inherited_notifier.dart'; import '../widgets/error.dart'; import 'base_controls.dart'; import 'control_widget.dart'; diff --git a/packages/flet/lib/src/widgets/control_inherited_notifier.dart b/packages/flet/lib/src/widgets/control_inherited_notifier.dart index 8357f2b95d..27254ffed6 100644 --- a/packages/flet/lib/src/widgets/control_inherited_notifier.dart +++ b/packages/flet/lib/src/widgets/control_inherited_notifier.dart @@ -1,6 +1,10 @@ -import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../extensions/control.dart'; +import '../flet_backend.dart'; import '../models/control.dart'; +import '../utils/theme.dart'; /// InheritedNotifier for Control. class ControlInheritedNotifier extends InheritedNotifier { @@ -21,3 +25,72 @@ class ControlInheritedNotifier extends InheritedNotifier { return notifier != oldWidget.notifier; } } + +Widget withControlInheritedNotifier(Control control, WidgetBuilder builder) { + if (control.internals?["skip_inherited_notifier"] == true) { + return Builder(builder: builder); + } + + return ControlInheritedNotifier( + notifier: control, + child: Builder(builder: (context) { + ControlInheritedNotifier.of(context); + return builder(context); + }), + ); +} + +Widget wrapWithControlInheritedNotifierAndTheme( + Control control, + WidgetBuilder builder, +) { + return Builder(builder: (context) { + final child = withControlInheritedNotifier(control, builder); + return wrapWithControlTheme(control, context, child); + }); +} + +Widget wrapWithControlTheme( + Control control, BuildContext context, Widget child) { + // skip root/page control + if (control == FletBackend.of(context).page) return child; + + // if a control opts out of ControlInheritedNotifier, + // it also skips per-control theme wrapping. + if (control.internals?["skip_inherited_notifier"] == true) return child; + + final hasNoThemes = + control.get("theme") == null && control.get("dark_theme") == null; + final themeMode = control.getThemeMode("theme_mode"); + if (hasNoThemes && themeMode == null) return child; + + final ThemeData? parentTheme = (themeMode == null) ? Theme.of(context) : null; + + /// Converts ThemeMode to Brightness + Brightness? themeModeToBrightness(ThemeMode? mode) { + switch (mode) { + case ThemeMode.light: + return Brightness.light; + case ThemeMode.dark: + return Brightness.dark; + case ThemeMode.system: + return context.select( + (backend) => backend.platformBrightness, + ); + case null: + return parentTheme?.brightness; + } + } + + Widget buildTheme(Brightness? brightness) { + final themeData = control.getTheme( + brightness == Brightness.dark ? "dark_theme" : "theme", + context, + brightness, + parentTheme: parentTheme, + ); + return Theme(data: themeData, child: child); + } + + return buildTheme(themeModeToBrightness(themeMode)); +} diff --git a/packages/flet/lib/src/widgets/control_wrappers.dart b/packages/flet/lib/src/widgets/control_wrappers.dart deleted file mode 100644 index 380174dedf..0000000000 --- a/packages/flet/lib/src/widgets/control_wrappers.dart +++ /dev/null @@ -1,79 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -import '../extensions/control.dart'; -import '../flet_backend.dart'; -import '../models/control.dart'; -import '../utils/theme.dart'; -import 'control_inherited_notifier.dart'; - -Widget withControlInheritedNotifier(Control control, WidgetBuilder builder) { - if (control.internals?["skip_inherited_notifier"] == true) { - return Builder(builder: builder); - } - - return ControlInheritedNotifier( - notifier: control, - child: Builder(builder: (context) { - ControlInheritedNotifier.of(context); - return builder(context); - }), - ); -} - -Widget wrapWithControlInheritedNotifierAndTheme( - Control control, WidgetBuilder builder) { - return Builder( - builder: (context) => wrapWithControlTheme( - control, context, withControlInheritedNotifier(control, builder))); -} - -Widget wrapWithControlTheme( - Control control, BuildContext context, Widget child) { - // skip root/page control - if (control == FletBackend.of(context).page) return child; - - // Preserve ControlWidget semantics: if a control opts out of - // ControlInheritedNotifier, it also skips per-control theme wrapping. - if (control.internals?["skip_inherited_notifier"] == true) { - return child; - } - - final hasNoThemes = - control.get("theme") == null && control.get("dark_theme") == null; - final themeMode = control.getThemeMode("theme_mode"); - if (hasNoThemes && themeMode == null) { - return child; - } - - final ThemeData? parentTheme = (themeMode == null) ? Theme.of(context) : null; - - /// Converts ThemeMode to Brightness - Brightness? themeModeToBrightness(ThemeMode? mode) { - switch (mode) { - case ThemeMode.light: - return Brightness.light; - case ThemeMode.dark: - return Brightness.dark; - case ThemeMode.system: - return context.select( - (backend) => backend.platformBrightness, - ); - case null: - return parentTheme?.brightness; - } - } - - Widget buildTheme(Brightness? brightness) { - final themeData = control.getTheme( - brightness == Brightness.dark ? "dark_theme" : "theme", - context, - brightness, - parentTheme: parentTheme, - ); - return Theme(data: themeData, child: child); - } - - final brightness = themeModeToBrightness(themeMode); - return buildTheme(brightness); -} From 3aba49eb714032ca45617cf531cebf2772973b96 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Sun, 14 Dec 2025 16:58:53 +0100 Subject: [PATCH 08/12] Enhance documentation for `ControlWidget` and `ControlInheritedNotifier`, add comments explaining key functionalities and behaviors. --- .../flet/lib/src/controls/control_widget.dart | 10 ++++++- .../widgets/control_inherited_notifier.dart | 30 +++++++++++++++---- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/packages/flet/lib/src/controls/control_widget.dart b/packages/flet/lib/src/controls/control_widget.dart index 9c982104de..d6dd104ad4 100644 --- a/packages/flet/lib/src/controls/control_widget.dart +++ b/packages/flet/lib/src/controls/control_widget.dart @@ -1,12 +1,19 @@ import 'package:flutter/material.dart'; -import '../extensions/control.dart'; import '../flet_backend.dart'; import '../models/control.dart'; import '../utils/keys.dart'; import '../widgets/control_inherited_notifier.dart'; import '../widgets/error.dart'; +/// Builds the Flutter [Widget] for a [Control]. +/// +/// Responsibilities: +/// - Resolves a stable Flutter [Key] from the control's `key` property (including +/// registering a scroll [GlobalKey] with the backend). +/// - Delegates widget creation to registered extensions. +/// - Wraps the result in: +/// [ControlInheritedNotifier] (reactivity) and per-control theme overrides. class ControlWidget extends StatelessWidget { final Control control; @@ -17,6 +24,7 @@ class ControlWidget extends StatelessWidget { ControlKey? controlKey = control.getKey("key"); Key? key; if (controlKey is ControlScrollKey) { + // A scroll key needs to be a GlobalKey so the backend can access state. key = GlobalKey(); FletBackend.of(context).globalKeys[controlKey.toString()] = key as GlobalKey; diff --git a/packages/flet/lib/src/widgets/control_inherited_notifier.dart b/packages/flet/lib/src/widgets/control_inherited_notifier.dart index 27254ffed6..991a203623 100644 --- a/packages/flet/lib/src/widgets/control_inherited_notifier.dart +++ b/packages/flet/lib/src/widgets/control_inherited_notifier.dart @@ -6,7 +6,10 @@ import '../flet_backend.dart'; import '../models/control.dart'; import '../utils/theme.dart'; -/// InheritedNotifier for Control. +/// InheritedNotifier for a [Control]. +/// +/// Used to rebuild a control subtree when the +/// corresponding [Control] (a [ChangeNotifier]) changes. class ControlInheritedNotifier extends InheritedNotifier { const ControlInheritedNotifier({ super.key, @@ -14,6 +17,8 @@ class ControlInheritedNotifier extends InheritedNotifier { required super.child, }) : super(); + /// Establishes a dependency on the nearest [ControlInheritedNotifier] and + /// returns its [Control]. static Control? of(BuildContext context) { return context .dependOnInheritedWidgetOfExactType() @@ -26,6 +31,10 @@ class ControlInheritedNotifier extends InheritedNotifier { } } +/// Wraps [builder] with [ControlInheritedNotifier], unless the control opts out. +/// +/// If `"skip_inherited_notifier"` internal is `true`, this returns +/// [builder] unwrapped to preserve historical semantics. Widget withControlInheritedNotifier(Control control, WidgetBuilder builder) { if (control.internals?["skip_inherited_notifier"] == true) { return Builder(builder: builder); @@ -40,6 +49,9 @@ Widget withControlInheritedNotifier(Control control, WidgetBuilder builder) { ); } +/// Convenience wrapper that applies both: +/// - [withControlInheritedNotifier] +/// - [wrapWithControlTheme] Widget wrapWithControlInheritedNotifierAndTheme( Control control, WidgetBuilder builder, @@ -50,13 +62,21 @@ Widget wrapWithControlInheritedNotifierAndTheme( }); } +/// Applies per-control theming (`theme`, `dark_theme`, `theme_mode`) to `child`. +/// +/// Returns `child` unchanged when: +/// - `control` is the page/root control +/// - `"skip_inherited_notifier"` internal is `true` +/// - no `theme`/`dark_theme` is set and `theme_mode` is `null` +/// +/// Parameters: +/// - `control`: the control whose per-control theme (if any) will be applied. +/// - `context`: used to access `FletBackend` and the ambient `Theme`. +/// - `child`: the widget subtree to wrap with the per-control `Theme`. Widget wrapWithControlTheme( Control control, BuildContext context, Widget child) { - // skip root/page control if (control == FletBackend.of(context).page) return child; - // if a control opts out of ControlInheritedNotifier, - // it also skips per-control theme wrapping. if (control.internals?["skip_inherited_notifier"] == true) return child; final hasNoThemes = @@ -66,7 +86,7 @@ Widget wrapWithControlTheme( final ThemeData? parentTheme = (themeMode == null) ? Theme.of(context) : null; - /// Converts ThemeMode to Brightness + /// Converts [ThemeMode] to [Brightness] used by [Control.getTheme]. Brightness? themeModeToBrightness(ThemeMode? mode) { switch (mode) { case ThemeMode.light: From a417cf3c70f7a7942ec531b4ea1657c376cd2c53 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Sun, 14 Dec 2025 17:18:34 +0100 Subject: [PATCH 09/12] Refactor: Introduce `buildInControlContext` and rename wrapper methods for clarity. --- .../flet/lib/src/controls/control_widget.dart | 2 +- packages/flet/lib/src/controls/tabs.dart | 2 +- .../widgets/control_inherited_notifier.dart | 23 ++++++++++++------- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/packages/flet/lib/src/controls/control_widget.dart b/packages/flet/lib/src/controls/control_widget.dart index d6dd104ad4..8ec4467efd 100644 --- a/packages/flet/lib/src/controls/control_widget.dart +++ b/packages/flet/lib/src/controls/control_widget.dart @@ -32,7 +32,7 @@ class ControlWidget extends StatelessWidget { key = ValueKey(controlKey.value); } - return wrapWithControlInheritedNotifierAndTheme(control, (context) { + return control.buildInControlContext((context) { for (var extension in FletBackend.of(context).extensions) { final widget = extension.createWidget(key, control); if (widget != null) return widget; diff --git a/packages/flet/lib/src/controls/tabs.dart b/packages/flet/lib/src/controls/tabs.dart index 7b70178220..27d8fe127f 100644 --- a/packages/flet/lib/src/controls/tabs.dart +++ b/packages/flet/lib/src/controls/tabs.dart @@ -245,7 +245,7 @@ class TabControl extends Tab { Widget build(BuildContext context) { debugPrint("TabControl build: ${control.id}"); - return wrapWithControlInheritedNotifierAndTheme(control, (context) { + return control.buildInControlContext((context) { return BaseControl( control: control, child: Tab( diff --git a/packages/flet/lib/src/widgets/control_inherited_notifier.dart b/packages/flet/lib/src/widgets/control_inherited_notifier.dart index 991a203623..161a2b792b 100644 --- a/packages/flet/lib/src/widgets/control_inherited_notifier.dart +++ b/packages/flet/lib/src/widgets/control_inherited_notifier.dart @@ -35,7 +35,7 @@ class ControlInheritedNotifier extends InheritedNotifier { /// /// If `"skip_inherited_notifier"` internal is `true`, this returns /// [builder] unwrapped to preserve historical semantics. -Widget withControlInheritedNotifier(Control control, WidgetBuilder builder) { +Widget withControlNotifier(Control control, WidgetBuilder builder) { if (control.internals?["skip_inherited_notifier"] == true) { return Builder(builder: builder); } @@ -50,15 +50,15 @@ Widget withControlInheritedNotifier(Control control, WidgetBuilder builder) { } /// Convenience wrapper that applies both: -/// - [withControlInheritedNotifier] -/// - [wrapWithControlTheme] -Widget wrapWithControlInheritedNotifierAndTheme( +/// - [withControlNotifier] +/// - [withControlTheme] +Widget withControlContext( Control control, WidgetBuilder builder, ) { return Builder(builder: (context) { - final child = withControlInheritedNotifier(control, builder); - return wrapWithControlTheme(control, context, child); + final child = withControlNotifier(control, builder); + return withControlTheme(control, context, child); }); } @@ -73,8 +73,7 @@ Widget wrapWithControlInheritedNotifierAndTheme( /// - `control`: the control whose per-control theme (if any) will be applied. /// - `context`: used to access `FletBackend` and the ambient `Theme`. /// - `child`: the widget subtree to wrap with the per-control `Theme`. -Widget wrapWithControlTheme( - Control control, BuildContext context, Widget child) { +Widget withControlTheme(Control control, BuildContext context, Widget child) { if (control == FletBackend.of(context).page) return child; if (control.internals?["skip_inherited_notifier"] == true) return child; @@ -114,3 +113,11 @@ Widget wrapWithControlTheme( return buildTheme(themeModeToBrightness(themeMode)); } + +extension ControlContextBuilder on Control { + /// Builds a widget under this control's standard "control context": + /// [ControlInheritedNotifier] + per-control theme wrapping. + Widget buildInControlContext(WidgetBuilder builder) { + return withControlContext(this, builder); + } +} From f7f4a9eb64cb9b1d097f9b1f8509139c97f1b131 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Sun, 14 Dec 2025 17:36:31 +0100 Subject: [PATCH 10/12] Enhance comments in TabControl constructor to clarify hint usage for Flutter's TabBar heuristics --- .../flet/lib/src/controls/control_widget.dart | 2 +- packages/flet/lib/src/controls/tabs.dart | 23 ++++++++----------- .../widgets/control_inherited_notifier.dart | 4 ++++ 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/packages/flet/lib/src/controls/control_widget.dart b/packages/flet/lib/src/controls/control_widget.dart index 8ec4467efd..ae4db35a96 100644 --- a/packages/flet/lib/src/controls/control_widget.dart +++ b/packages/flet/lib/src/controls/control_widget.dart @@ -12,7 +12,7 @@ import '../widgets/error.dart'; /// - Resolves a stable Flutter [Key] from the control's `key` property (including /// registering a scroll [GlobalKey] with the backend). /// - Delegates widget creation to registered extensions. -/// - Wraps the result in: +/// - Builds the result in this control's standard "control context": /// [ControlInheritedNotifier] (reactivity) and per-control theme overrides. class ControlWidget extends StatelessWidget { final Control control; diff --git a/packages/flet/lib/src/controls/tabs.dart b/packages/flet/lib/src/controls/tabs.dart index 27d8fe127f..dcbccae195 100644 --- a/packages/flet/lib/src/controls/tabs.dart +++ b/packages/flet/lib/src/controls/tabs.dart @@ -210,27 +210,24 @@ class TabControl extends Tab { TabControl({super.key, required this.control}) : super( - // These are primarily used by Flutter's TabBar heuristics (e.g. to - // decide text-vs-text+icon height) because TabBar checks `tab is Tab`. - // The actual tab UI is built from `control` in `build()`. + // These values are *hints* for Flutter's TabBar heuristics. + // + // TabBar applies different sizing/ink behavior when items in `tabs:` + // are actual `Tab` instances (it literally checks `tab is Tab`). + // In Flet, the real content is built from `control` in `build()`, + // but providing `text`/`icon` here lets TabBar pick the correct + // default height (text vs text+icon) and ensures consistent hover/ + // splash overlay sizing (see issue #5599). text: _tabTextHint(control), icon: _tabIconHint(control), ); - static bool _hasLabel(Control control) => control.get("label") != null; - - static bool _hasIcon(Control control) => control.get("icon") != null; - static String? _tabTextHint(Control control) { - if (!_hasLabel(control)) return null; - final label = control.get("label"); - return label is String ? label : ""; + return control.get("label") != null ? "" : null; } static Widget? _tabIconHint(Control control) { - if (!_hasIcon(control)) return null; - // Non-null placeholder so TabBar can detect icon presence. - return const SizedBox.shrink(); + return control.get("icon") != null ? const SizedBox.shrink() : null; } static Key? _keyFromControl(Control control) { diff --git a/packages/flet/lib/src/widgets/control_inherited_notifier.dart b/packages/flet/lib/src/widgets/control_inherited_notifier.dart index 161a2b792b..ef17de32c6 100644 --- a/packages/flet/lib/src/widgets/control_inherited_notifier.dart +++ b/packages/flet/lib/src/widgets/control_inherited_notifier.dart @@ -117,6 +117,10 @@ Widget withControlTheme(Control control, BuildContext context, Widget child) { extension ControlContextBuilder on Control { /// Builds a widget under this control's standard "control context": /// [ControlInheritedNotifier] + per-control theme wrapping. + /// + /// This is primarily used by [ControlWidget] and any "special" controls that + /// must subclass a Flutter widget (e.g. a control that must be a `Tab`) but + /// still need the same wrapper behavior as a normal `ControlWidget`. Widget buildInControlContext(WidgetBuilder builder) { return withControlContext(this, builder); } From 1a4ea30cbee74119044aa2676c91d7031f1fbdca Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Sun, 14 Dec 2025 17:52:42 +0100 Subject: [PATCH 11/12] Rename `withControlNotifier` to `withControlInheritedNotifier` for clarity and update references in `withControlContext`. --- .../flet/lib/src/widgets/control_inherited_notifier.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/flet/lib/src/widgets/control_inherited_notifier.dart b/packages/flet/lib/src/widgets/control_inherited_notifier.dart index ef17de32c6..f374d5097d 100644 --- a/packages/flet/lib/src/widgets/control_inherited_notifier.dart +++ b/packages/flet/lib/src/widgets/control_inherited_notifier.dart @@ -35,7 +35,7 @@ class ControlInheritedNotifier extends InheritedNotifier { /// /// If `"skip_inherited_notifier"` internal is `true`, this returns /// [builder] unwrapped to preserve historical semantics. -Widget withControlNotifier(Control control, WidgetBuilder builder) { +Widget withControlInheritedNotifier(Control control, WidgetBuilder builder) { if (control.internals?["skip_inherited_notifier"] == true) { return Builder(builder: builder); } @@ -50,14 +50,14 @@ Widget withControlNotifier(Control control, WidgetBuilder builder) { } /// Convenience wrapper that applies both: -/// - [withControlNotifier] +/// - [withControlInheritedNotifier] /// - [withControlTheme] Widget withControlContext( Control control, WidgetBuilder builder, ) { return Builder(builder: (context) { - final child = withControlNotifier(control, builder); + final child = withControlInheritedNotifier(control, builder); return withControlTheme(control, context, child); }); } From 78d2be1ca730cd90b74472f8c321900db6366091 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Sun, 14 Dec 2025 19:03:29 +0100 Subject: [PATCH 12/12] Refactor tab hint logic --- packages/flet/lib/src/controls/tabs.dart | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/packages/flet/lib/src/controls/tabs.dart b/packages/flet/lib/src/controls/tabs.dart index dcbccae195..854d1d7b7f 100644 --- a/packages/flet/lib/src/controls/tabs.dart +++ b/packages/flet/lib/src/controls/tabs.dart @@ -218,18 +218,10 @@ class TabControl extends Tab { // but providing `text`/`icon` here lets TabBar pick the correct // default height (text vs text+icon) and ensures consistent hover/ // splash overlay sizing (see issue #5599). - text: _tabTextHint(control), - icon: _tabIconHint(control), + text: control.buildTextOrWidget("label") != null ? "" : null, + icon: control.buildIconOrWidget("icon"), ); - static String? _tabTextHint(Control control) { - return control.get("label") != null ? "" : null; - } - - static Widget? _tabIconHint(Control control) { - return control.get("icon") != null ? const SizedBox.shrink() : null; - } - static Key? _keyFromControl(Control control) { final controlKey = control.getKey("key"); if (controlKey is ControlValueKey) {