Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 17 additions & 16 deletions packages/flet/lib/src/controls/button.dart
Original file line number Diff line number Diff line change
Expand Up @@ -98,20 +98,22 @@ class _ButtonControlState extends State<ButtonControl> with FletStoreMixin {
var theme = Theme.of(context);

var style = parseButtonStyle(
widget.control.internals?["style"], Theme.of(context),
defaultForegroundColor: widget.control
.getColor("color", context, theme.colorScheme.primary)!,
defaultBackgroundColor: widget.control
.getColor("bgcolor", context, theme.colorScheme.surface)!,
defaultOverlayColor: theme.colorScheme.primary.withOpacity(0.08),
defaultShadowColor: theme.colorScheme.shadow,
defaultSurfaceTintColor: theme.colorScheme.surfaceTint,
defaultElevation: widget.control.getDouble("elevation", 1)!,
defaultPadding: const EdgeInsets.symmetric(horizontal: 8),
defaultBorderSide: BorderSide.none,
defaultShape: theme.useMaterial3
? const StadiumBorder()
: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)));
widget.control.internals?["style"],
theme,
defaultForegroundColor:
widget.control.getColor("color", context, theme.colorScheme.primary)!,
defaultBackgroundColor: widget.control
.getColor("bgcolor", context, theme.colorScheme.surface)!,
defaultOverlayColor: theme.colorScheme.primary.withOpacity(0.08),
defaultShadowColor: theme.colorScheme.shadow,
defaultSurfaceTintColor: theme.colorScheme.surfaceTint,
defaultElevation: widget.control.getDouble("elevation", 1)!,
defaultPadding: const EdgeInsets.symmetric(horizontal: 8),
defaultBorderSide: BorderSide.none,
defaultShape: theme.useMaterial3
? const StadiumBorder()
: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
);

Widget error = const ErrorControl("Error displaying Button",
description: "\"icon\" must be specified together with \"content\"");
Expand Down Expand Up @@ -203,8 +205,7 @@ class _ButtonControlState extends State<ButtonControl> with FletStoreMixin {
onLongPress: onLongPressHandler,
onHover: onHoverHandler,
clipBehavior: clipBehavior,
child:
widget.control.buildTextOrWidget("content") ?? const Text(""));
child: content ?? const Text(""));
} else if (isOutlinedButton) {
button = OutlinedButton(
autofocus: autofocus,
Expand Down
22 changes: 10 additions & 12 deletions packages/flet/lib/src/controls/row.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,7 @@ class RowControl extends StatelessWidget {
debugPrint("Row build: ${control.id}");

var spacing = control.getDouble("spacing", 10)!;
var mainAlignment = parseMainAxisAlignment(
control.getString("alignment"), MainAxisAlignment.start)!;
var tight = control.getBool("tight", false)!;
var wrap = control.getBool("wrap", false)!;
var intrinsicHeight = control.getBool("intrinsic_height", false)!;
var verticalAlignment = control.getString("vertical_alignment");
var controls = control.buildWidgets("controls");

Widget child = wrap
Expand All @@ -35,20 +30,23 @@ class RowControl extends StatelessWidget {
control.getString("alignment"), WrapAlignment.start)!,
runAlignment: parseWrapAlignment(
control.getString("run_alignment"), WrapAlignment.start)!,
crossAxisAlignment: parseWrapCrossAlignment(
verticalAlignment, WrapCrossAlignment.center)!,
crossAxisAlignment: control.getWrapCrossAlignment(
"vertical_alignment", WrapCrossAlignment.center)!,
children: controls,
)
: Row(
spacing: spacing,
mainAxisAlignment: mainAlignment,
mainAxisSize: tight ? MainAxisSize.min : MainAxisSize.max,
crossAxisAlignment: parseCrossAxisAlignment(
verticalAlignment, CrossAxisAlignment.center)!,
mainAxisSize: control.getBool("tight", false)!
? MainAxisSize.min
: MainAxisSize.max,
mainAxisAlignment: control.getMainAxisAlignment(
"alignment", MainAxisAlignment.start)!,
crossAxisAlignment: control.getCrossAxisAlignment(
"vertical_alignment", CrossAxisAlignment.center)!,
children: controls,
);

if (intrinsicHeight) {
if (control.getBool("intrinsic_height", false)!) {
child = IntrinsicHeight(child: child);
}

Expand Down
18 changes: 17 additions & 1 deletion packages/flet/lib/src/utils/buttons.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,22 @@ ButtonStyle? parseButtonStyle(dynamic value, ThemeData theme,
TextStyle? defaultTextStyle,
ButtonStyle? defaultValue}) {
if (value == null) return defaultValue;

WidgetStateProperty<TextStyle?>? parseButtonTextStyle(
dynamic value, ThemeData theme,
{TextStyle? defaultTextStyle}) {
final ts = parseWidgetStateTextStyle(value, theme,
defaultTextStyle: defaultTextStyle);
if (ts == null) return null;

// Match Material button defaults to avoid TextStyle.lerp assertions when
// animating between states.
return WidgetStateProperty.resolveWith((Set<WidgetState> states) {
final resolved = ts.resolve(states);
return resolved?.copyWith(inherit: false);
});
}

return ButtonStyle(
foregroundColor: parseWidgetStateColor(value["color"], theme,
defaultColor: defaultForegroundColor),
Expand All @@ -54,7 +70,7 @@ ButtonStyle? parseButtonStyle(dynamic value, ThemeData theme,
defaultColor: defaultForegroundColor),
alignment: parseAlignment(value["alignment"]),
enableFeedback: parseBool(value["enable_feedback"]),
textStyle: parseWidgetStateTextStyle(value["text_style"], theme,
textStyle: parseButtonTextStyle(value["text_style"], theme,
defaultTextStyle: defaultTextStyle),
iconSize: parseWidgetStateDouble(value["icon_size"]),
visualDensity: parseVisualDensity(value["visual_density"]),
Expand Down
31 changes: 20 additions & 11 deletions packages/flet/lib/src/utils/text.dart
Original file line number Diff line number Diff line change
Expand Up @@ -140,20 +140,19 @@ TextAffinity? parseTextAffinity(String? value, [TextAffinity? defaultValue]) {
defaultValue;
}

TextStyle? parseTextStyle(dynamic value, ThemeData theme,
[TextStyle? defaultValue]) {
if (value == null) return defaultValue;
var fontWeight = value["weight"];

List<FontVariation>? variations;
if (fontWeight != null && fontWeight.startsWith("w")) {
variations = [
FontVariation('wght', parseDouble(fontWeight.substring(1), 0)!)
];
List<FontVariation>? parseFontVariations(dynamic fontWeight,
[List<FontVariation>? defaultValue]) {
if (fontWeight != null &&
fontWeight is String &&
fontWeight.startsWith("w")) {
return [FontVariation('wght', parseDouble(fontWeight.substring(1), 0)!)];
}
return defaultValue;
}

List<TextDecoration> parseTextDecorations(dynamic decorationValue) {
List<TextDecoration> decorations = [];
var decor = parseInt(value["decoration"], 0)!;
var decor = parseInt(decorationValue, 0)!;
if (decor & 0x1 > 0) {
decorations.add(TextDecoration.underline);
}
Expand All @@ -163,6 +162,16 @@ TextStyle? parseTextStyle(dynamic value, ThemeData theme,
if (decor & 0x4 > 0) {
decorations.add(TextDecoration.lineThrough);
}
return decorations;
}

TextStyle? parseTextStyle(dynamic value, ThemeData theme,
[TextStyle? defaultValue]) {
if (value == null) return defaultValue;

var fontWeight = value["weight"];
List<FontVariation>? variations = parseFontVariations(fontWeight);
List<TextDecoration> decorations = parseTextDecorations(value["decoration"]);

return TextStyle(
fontSize: parseDouble(value["size"]),
Expand Down
135 changes: 135 additions & 0 deletions sdk/python/examples/apps/timer/declarative.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import asyncio
import time
from dataclasses import dataclass

import flet as ft


def format_hhmmss(seconds: int) -> str:
"""Format elapsed seconds as HH:MM:SS."""
h = seconds // 3600
m = (seconds % 3600) // 60
s = seconds % 60
return f"{h:02}:{m:02}:{s:02}"


@ft.observable
@dataclass
class TimerState:
"""
Declarative timer state.
This class is the single source of truth for the UI.
Any mutation of its public fields automatically triggers a re-render.
"""

running: bool = False
paused: bool = False
elapsed: int = 0 # seconds shown in UI

# Internals
_base_elapsed: int = 0 # accumulated time before the current run
_started_at: float = 0.0 # wall-clock start time
_task: asyncio.Task | None = None # background ticker task

async def _ticker(self):
"""
Background task that updates elapsed time once per second
while the timer is running.
"""
while self.running:
if not self.paused:
self.elapsed = self._base_elapsed + int(time.time() - self._started_at)
await asyncio.sleep(1)

# Task cleanup when stopped
self._task = None

def toggle(self):
"""
Toggle button handler:
- stopped → start
- running → pause
- paused → resume
"""
# stopped → start
if not self.running:
self.running = True
self.paused = False
self._base_elapsed = self.elapsed
self._started_at = time.time()

# Ensure only one ticker task runs
if self._task is None or self._task.done():
self._task = asyncio.create_task(self._ticker())
return

# running → pause
if not self.paused:
self._base_elapsed += int(time.time() - self._started_at)
self.elapsed = self._base_elapsed
self.paused = True
return

# paused → resume
self.paused = False
self._started_at = time.time()

def stop(self):
"""Stop the timer and reset all state."""
self.running = False
self.paused = False
self.elapsed = 0
self._base_elapsed = 0
self._started_at = 0.0


@ft.component
def App():
state, _ = ft.use_state(TimerState())

# Button appearance derived entirely from state
label = "Pause" if state.running and not state.paused else "Start"
icon = ft.Icons.PAUSE if state.running and not state.paused else ft.Icons.PLAY_ARROW

return ft.SafeArea(
ft.Column(
spacing=20,
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
alignment=ft.MainAxisAlignment.CENTER,
controls=[
ft.Text(
format_hhmmss(state.elapsed),
size=30,
weight=ft.FontWeight.BOLD,
),
ft.Row(
alignment=ft.MainAxisAlignment.CENTER,
controls=[
ft.FilledButton(
label,
icon=icon,
on_click=state.toggle,
),
ft.TextButton(
"Stop",
icon=ft.Icons.STOP,
on_click=state.stop,
disabled=not state.running and state.elapsed == 0,
),
],
),
],
)
)


def main(page: ft.Page):
"""Application entry point."""
page.vertical_alignment = ft.MainAxisAlignment.CENTER
page.horizontal_alignment = ft.CrossAxisAlignment.CENTER

page.render(App)


ft.run(main)
Loading
Loading