flet 0.70.0.dev5774__py3-none-any.whl → 0.70.0.dev5835__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of flet might be problematic. Click here for more details.
- flet/__init__.py +32 -4
- flet/components/__init__.py +0 -0
- flet/components/component.py +346 -0
- flet/components/component_decorator.py +24 -0
- flet/components/component_owned.py +22 -0
- flet/components/hooks/__init__.py +0 -0
- flet/components/hooks/hook.py +12 -0
- flet/components/hooks/use_callback.py +28 -0
- flet/components/hooks/use_context.py +91 -0
- flet/components/hooks/use_effect.py +104 -0
- flet/components/hooks/use_memo.py +52 -0
- flet/components/hooks/use_state.py +58 -0
- flet/components/memo.py +34 -0
- flet/components/observable.py +269 -0
- flet/components/public_utils.py +10 -0
- flet/components/utils.py +85 -0
- flet/controls/base_control.py +34 -10
- flet/controls/base_page.py +44 -40
- flet/controls/context.py +22 -1
- flet/controls/control.py +12 -6
- flet/controls/control_event.py +19 -2
- flet/controls/core/animated_switcher.py +3 -2
- flet/controls/core/autofill_group.py +6 -2
- flet/controls/core/column.py +5 -0
- flet/controls/core/dismissible.py +12 -10
- flet/controls/core/drag_target.py +20 -10
- flet/controls/core/draggable.py +9 -9
- flet/controls/core/icon.py +16 -12
- flet/controls/core/interactive_viewer.py +24 -23
- flet/controls/core/pagelet.py +3 -2
- flet/controls/core/reorderable_draggable.py +3 -2
- flet/controls/core/row.py +5 -0
- flet/controls/core/safe_area.py +3 -2
- flet/controls/core/text_span.py +5 -3
- flet/controls/core/view.py +6 -6
- flet/controls/core/window_drag_area.py +3 -2
- flet/controls/cupertino/cupertino_action_sheet.py +10 -5
- flet/controls/cupertino/cupertino_action_sheet_action.py +3 -4
- flet/controls/cupertino/cupertino_activity_indicator.py +4 -3
- flet/controls/cupertino/cupertino_alert_dialog.py +6 -3
- flet/controls/cupertino/cupertino_button.py +6 -5
- flet/controls/cupertino/cupertino_context_menu.py +8 -4
- flet/controls/cupertino/cupertino_context_menu_action.py +3 -4
- flet/controls/cupertino/cupertino_date_picker.py +44 -28
- flet/controls/cupertino/cupertino_dialog_action.py +3 -4
- flet/controls/cupertino/cupertino_list_tile.py +3 -4
- flet/controls/cupertino/cupertino_navigation_bar.py +6 -5
- flet/controls/cupertino/cupertino_picker.py +14 -10
- flet/controls/cupertino/cupertino_segmented_button.py +6 -5
- flet/controls/cupertino/cupertino_slider.py +16 -12
- flet/controls/cupertino/cupertino_sliding_segmented_button.py +6 -5
- flet/controls/cupertino/cupertino_timer_picker.py +38 -31
- flet/controls/id_counter.py +24 -0
- flet/controls/material/alert_dialog.py +6 -5
- flet/controls/material/app_bar.py +17 -14
- flet/controls/material/banner.py +13 -11
- flet/controls/material/bottom_app_bar.py +5 -4
- flet/controls/material/bottom_sheet.py +5 -4
- flet/controls/material/button.py +12 -4
- flet/controls/material/chip.py +13 -12
- flet/controls/material/circle_avatar.py +17 -13
- flet/controls/material/datatable.py +48 -41
- flet/controls/material/divider.py +30 -14
- flet/controls/material/dropdown.py +5 -3
- flet/controls/material/expansion_tile.py +11 -22
- flet/controls/material/floating_action_button.py +32 -23
- flet/controls/material/icon_button.py +7 -3
- flet/controls/material/navigation_rail.py +14 -11
- flet/controls/material/outlined_button.py +7 -3
- flet/controls/material/progress_bar.py +18 -10
- flet/controls/material/radio_group.py +5 -1
- flet/controls/material/range_slider.py +13 -13
- flet/controls/material/segmented_button.py +21 -17
- flet/controls/material/selection_area.py +3 -2
- flet/controls/material/slider.py +16 -12
- flet/controls/material/snack_bar.py +18 -10
- flet/controls/material/switch.py +6 -5
- flet/controls/material/tabs.py +18 -14
- flet/controls/material/textfield.py +32 -15
- flet/controls/material/vertical_divider.py +20 -12
- flet/controls/object_patch.py +434 -197
- flet/controls/page.py +205 -85
- flet/controls/services/haptic_feedback.py +0 -3
- flet/controls/services/shake_detector.py +0 -3
- flet/messaging/flet_socket_server.py +13 -6
- flet/messaging/session.py +103 -10
- flet/{controls/session_storage.py → messaging/session_store.py} +2 -2
- flet/version.py +1 -1
- {flet-0.70.0.dev5774.dist-info → flet-0.70.0.dev5835.dist-info}/METADATA +5 -5
- {flet-0.70.0.dev5774.dist-info → flet-0.70.0.dev5835.dist-info}/RECORD +93 -80
- flet/controls/cache.py +0 -87
- flet/controls/control_id.py +0 -22
- flet/controls/core/state_view.py +0 -60
- {flet-0.70.0.dev5774.dist-info → flet-0.70.0.dev5835.dist-info}/WHEEL +0 -0
- {flet-0.70.0.dev5774.dist-info → flet-0.70.0.dev5835.dist-info}/entry_points.txt +0 -0
- {flet-0.70.0.dev5774.dist-info → flet-0.70.0.dev5835.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from collections.abc import Awaitable, Callable, Sequence
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from flet.components.hooks.hook import Hook
|
|
7
|
+
from flet.components.utils import current_component
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class EffectHook(Hook):
|
|
12
|
+
setup: Callable[[], Any | Awaitable[Any]]
|
|
13
|
+
cleanup: Callable[[], Any | Awaitable[Any]] | None = None
|
|
14
|
+
deps: list[Any] | None = None
|
|
15
|
+
prev_deps: list[Any] | None = None
|
|
16
|
+
|
|
17
|
+
# runtime
|
|
18
|
+
_setup_task: asyncio.Task | None = None # last scheduled setup task
|
|
19
|
+
_cleanup_task: asyncio.Task | None = None # last scheduled cleanup task
|
|
20
|
+
|
|
21
|
+
def cancel(self):
|
|
22
|
+
if self._setup_task and not self._setup_task.done():
|
|
23
|
+
self._setup_task.cancel()
|
|
24
|
+
if self._cleanup_task and not self._cleanup_task.done():
|
|
25
|
+
self._cleanup_task.cancel()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def use_effect(
|
|
29
|
+
setup: Callable[[], Any | Awaitable[Any]],
|
|
30
|
+
dependencies: Sequence[Any] | None = None,
|
|
31
|
+
cleanup: Callable[[], Any | Awaitable[Any]] | None = None,
|
|
32
|
+
):
|
|
33
|
+
"""
|
|
34
|
+
Perform side effects in function components.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
setup: A function that performs the side effect. It may optionally return
|
|
38
|
+
a cleanup function.
|
|
39
|
+
dependencies: If present, the effect is only re-run when one of the dependencies
|
|
40
|
+
has changed. If absent, the effect is only run on initial render.
|
|
41
|
+
cleanup: An optional function that cleans up after the effect. It is run
|
|
42
|
+
before the effect is re-run, and when the component unmounts.
|
|
43
|
+
"""
|
|
44
|
+
component = current_component()
|
|
45
|
+
deps = list(dependencies) if dependencies is not None else None
|
|
46
|
+
|
|
47
|
+
# get or create effect hook
|
|
48
|
+
hook = component.use_hook(
|
|
49
|
+
lambda: EffectHook(
|
|
50
|
+
component,
|
|
51
|
+
setup=setup,
|
|
52
|
+
deps=deps,
|
|
53
|
+
cleanup=cleanup,
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# update effect hook
|
|
58
|
+
hook.setup = setup
|
|
59
|
+
hook.prev_deps = hook.deps
|
|
60
|
+
hook.deps = deps
|
|
61
|
+
hook.cleanup = cleanup
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def on_mounted(fn: Callable[[], Any | Awaitable[Any]]) -> None:
|
|
65
|
+
"""
|
|
66
|
+
Run exactly once after the component mounts.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
fn: A function to run after the component mounts.
|
|
70
|
+
"""
|
|
71
|
+
use_effect(fn, dependencies=[])
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def on_unmounted(fn: Callable[[], Any | Awaitable[Any]]) -> None:
|
|
75
|
+
"""
|
|
76
|
+
Run exactly once when the component unmounts.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
fn: A function to run when the component unmounts.
|
|
80
|
+
"""
|
|
81
|
+
# No-op setup; only need cleanup to fire on unmount
|
|
82
|
+
use_effect(lambda: None, dependencies=[], cleanup=fn)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def on_updated(
|
|
86
|
+
fn: Callable[[], Any | Awaitable[Any]], dependencies: Sequence[Any] | None = None
|
|
87
|
+
) -> None:
|
|
88
|
+
"""
|
|
89
|
+
Run after each post-mount render (or when dependencies change).
|
|
90
|
+
With dependencies=None this fires every update; with dependencies=[...] only
|
|
91
|
+
on changes.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
fn: A function to run after each post-mount render (or when dependencies
|
|
95
|
+
change).
|
|
96
|
+
dependencies: If present, fn is only run when one of the dependencies has
|
|
97
|
+
changed. If absent, fn is run after every render.
|
|
98
|
+
"""
|
|
99
|
+
use_effect(fn, dependencies=dependencies)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
on_mounted = on_mounted # alias
|
|
103
|
+
on_unmounted = on_unmounted # alias
|
|
104
|
+
on_updated = on_updated # alias
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Any, Callable, Generic, TypeVar
|
|
4
|
+
|
|
5
|
+
from flet.components.hooks.hook import Hook
|
|
6
|
+
from flet.components.utils import current_component, shallow_compare_args
|
|
7
|
+
|
|
8
|
+
MemoValueT = TypeVar("MemoValueT")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class MemoHook(Hook, Generic[MemoValueT]):
|
|
13
|
+
value: MemoValueT | None = None
|
|
14
|
+
prev_deps: list[Any] | None = None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def use_memo(
|
|
18
|
+
calculate_value: Callable[[], MemoValueT], dependencies: Sequence[Any] | None = None
|
|
19
|
+
) -> MemoValueT:
|
|
20
|
+
"""
|
|
21
|
+
Memoize a computed value between renders.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
calculate_value: A function that computes the value to be memoized.
|
|
25
|
+
dependencies: If present, the value is only recomputed when one of
|
|
26
|
+
the dependencies has changed. If absent, the value is only computed
|
|
27
|
+
on initial render.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
A memoized value whose identity is stable between renders.
|
|
31
|
+
"""
|
|
32
|
+
component = current_component()
|
|
33
|
+
|
|
34
|
+
def _create() -> MemoHook[MemoValueT]:
|
|
35
|
+
h = MemoHook[MemoValueT](component)
|
|
36
|
+
# If deps is None we recompute every render, so no need to preset prev_deps.
|
|
37
|
+
if dependencies is None:
|
|
38
|
+
h.value = calculate_value()
|
|
39
|
+
return h
|
|
40
|
+
|
|
41
|
+
hook = component.use_hook(_create) # type: MemoHook[MemoValueT]
|
|
42
|
+
|
|
43
|
+
if dependencies is None:
|
|
44
|
+
# Always recompute
|
|
45
|
+
hook.value = calculate_value()
|
|
46
|
+
return hook.value # type: ignore[return-value]
|
|
47
|
+
|
|
48
|
+
if hook.prev_deps is None or not shallow_compare_args(hook.prev_deps, dependencies):
|
|
49
|
+
hook.value = calculate_value()
|
|
50
|
+
hook.prev_deps = list(dependencies)
|
|
51
|
+
|
|
52
|
+
return hook.value # type: ignore[return-value]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Any, TypeVar
|
|
4
|
+
|
|
5
|
+
from flet.components.hooks.hook import Hook
|
|
6
|
+
from flet.components.observable import Observable, ObservableSubscription
|
|
7
|
+
from flet.components.utils import current_component
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class StateHook(Hook):
|
|
12
|
+
value: Any
|
|
13
|
+
subscription: ObservableSubscription | None = None
|
|
14
|
+
version: int = 0
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
StateT = TypeVar("StateT")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def use_state(
|
|
21
|
+
initial: StateT | Callable[[], StateT],
|
|
22
|
+
) -> tuple[StateT, Callable[[StateT], None]]:
|
|
23
|
+
"""
|
|
24
|
+
Add state to function components.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
initial: Initial state value or a function that returns the initial state value.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
A tuple of the current state value and a function to update it.
|
|
31
|
+
"""
|
|
32
|
+
component = current_component()
|
|
33
|
+
hook = component.use_hook(
|
|
34
|
+
lambda: StateHook(
|
|
35
|
+
component,
|
|
36
|
+
initial() if callable(initial) else initial,
|
|
37
|
+
)
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def update_subscription(hook: StateHook):
|
|
41
|
+
if hook.subscription:
|
|
42
|
+
component._detach_observable_subscription(hook.subscription)
|
|
43
|
+
hook.subscription = None
|
|
44
|
+
if isinstance(hook.value, Observable):
|
|
45
|
+
hook.subscription = component._attach_observable_subscription(hook.value)
|
|
46
|
+
|
|
47
|
+
update_subscription(hook)
|
|
48
|
+
|
|
49
|
+
def set_state(new_value: Any):
|
|
50
|
+
# shallow equality; swap to "is" or custom comparator if needed
|
|
51
|
+
if new_value != hook.value:
|
|
52
|
+
hook.value = new_value
|
|
53
|
+
update_subscription(hook)
|
|
54
|
+
hook.version += 1
|
|
55
|
+
if hook.component:
|
|
56
|
+
hook.component._schedule_update()
|
|
57
|
+
|
|
58
|
+
return hook.value, set_state
|
flet/components/memo.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from flet.components.utils import current_renderer
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def memo(fn):
|
|
5
|
+
"""
|
|
6
|
+
Lets you skip re-rendering a component when its props are unchanged.
|
|
7
|
+
|
|
8
|
+
Example:
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
import flet as ft
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@ft.component
|
|
15
|
+
def MyComponent(x, y):
|
|
16
|
+
return ft.Text(f"x={x}, y={y}")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
MemoizedMyComponent = ft.memo(MyComponent)
|
|
20
|
+
|
|
21
|
+
flet.run(
|
|
22
|
+
lambda page: page.render(
|
|
23
|
+
lambda: MemoizedMyComponent(x=1, y=2),
|
|
24
|
+
),
|
|
25
|
+
)
|
|
26
|
+
```
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def memo_wrapper(*args, **kwargs):
|
|
30
|
+
r = current_renderer()
|
|
31
|
+
r.set_memo()
|
|
32
|
+
return fn(*args, **kwargs)
|
|
33
|
+
|
|
34
|
+
return memo_wrapper
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import weakref
|
|
5
|
+
from dataclasses import InitVar, dataclass
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Callable, Optional
|
|
7
|
+
|
|
8
|
+
from flet.components.utils import value_equal
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from flet.components.component import Component
|
|
12
|
+
|
|
13
|
+
from flet.components.component_owned import ComponentOwned
|
|
14
|
+
|
|
15
|
+
Listener = Callable[[Any, Optional[str]], None] # (sender, field|None)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def observable(cls):
|
|
19
|
+
"""
|
|
20
|
+
Makes a class observable by mixing in [`Observable`][flet.Observable].
|
|
21
|
+
Can be applied to any class including [`dataclass`][dataclasses.dataclass].
|
|
22
|
+
For dataclasses, decorator can be placed either above or below
|
|
23
|
+
the `@dataclass` decorator.
|
|
24
|
+
|
|
25
|
+
Example:
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from dataclasses import dataclass
|
|
29
|
+
import flet as ft
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@ft.observable
|
|
33
|
+
@dataclass
|
|
34
|
+
class MyDataClass:
|
|
35
|
+
x: int
|
|
36
|
+
y: int
|
|
37
|
+
```
|
|
38
|
+
"""
|
|
39
|
+
if Observable in cls.__mro__:
|
|
40
|
+
return cls
|
|
41
|
+
# Build a new class whose MRO is (Observable, cls)
|
|
42
|
+
ns = dict(cls.__dict__)
|
|
43
|
+
# Defensive: avoid carrying these special slots over
|
|
44
|
+
ns.pop("__dict__", None)
|
|
45
|
+
ns.pop("__weakref__", None)
|
|
46
|
+
|
|
47
|
+
Mixed = type(cls.__name__, (Observable, cls), ns)
|
|
48
|
+
Mixed.__module__ = cls.__module__
|
|
49
|
+
Mixed.__qualname__ = cls.__qualname__
|
|
50
|
+
return Mixed
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class ObservableSubscription(ComponentOwned):
|
|
55
|
+
observable: InitVar[Observable]
|
|
56
|
+
|
|
57
|
+
def __post_init__(self, owner: Component, observable: Observable) -> None:
|
|
58
|
+
super().__post_init__(owner)
|
|
59
|
+
self.__disposer = observable.subscribe(self.__on_change)
|
|
60
|
+
|
|
61
|
+
def dispose(self):
|
|
62
|
+
if callable(self.__disposer):
|
|
63
|
+
self.__disposer()
|
|
64
|
+
self.__disposer = None
|
|
65
|
+
|
|
66
|
+
def __on_change(self, _sender, _field):
|
|
67
|
+
if self.component:
|
|
68
|
+
self.component._schedule_update()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class Observable:
|
|
72
|
+
"""
|
|
73
|
+
Mixin: notifies when fields change; auto-wraps lists/dicts to be observable.
|
|
74
|
+
|
|
75
|
+
Example:
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
import flet as ft
|
|
79
|
+
from dataclasses import dataclass
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@ft.observable
|
|
83
|
+
@dataclass
|
|
84
|
+
class MyDataClass:
|
|
85
|
+
x: int
|
|
86
|
+
y: int
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
obj = MyDataClass(1, 2)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def listener(sender, field):
|
|
93
|
+
print(f"Changed: {field} in {sender}")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
obj.subscribe(listener)
|
|
97
|
+
obj.x = 3
|
|
98
|
+
obj.y = 4
|
|
99
|
+
```
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
__version__ = 0 # optional version counter
|
|
103
|
+
|
|
104
|
+
# listeners store (lazy)
|
|
105
|
+
@property
|
|
106
|
+
def __listeners(self):
|
|
107
|
+
storage_name = "_Observable__listeners_storage"
|
|
108
|
+
try:
|
|
109
|
+
return object.__getattribute__(self, storage_name)
|
|
110
|
+
except AttributeError:
|
|
111
|
+
ws = weakref.WeakSet()
|
|
112
|
+
object.__setattr__(self, storage_name, ws)
|
|
113
|
+
return ws
|
|
114
|
+
|
|
115
|
+
# subscribe / notify
|
|
116
|
+
def subscribe(self, fn: Listener) -> Callable[[], None]:
|
|
117
|
+
self.__listeners.add(fn)
|
|
118
|
+
|
|
119
|
+
def dispose():
|
|
120
|
+
with contextlib.suppress(KeyError):
|
|
121
|
+
self.__listeners.remove(fn)
|
|
122
|
+
|
|
123
|
+
return dispose
|
|
124
|
+
|
|
125
|
+
def _notify(self, field: str | None):
|
|
126
|
+
self.__version__ += 1
|
|
127
|
+
for fn in list(self.__listeners):
|
|
128
|
+
fn(self, field)
|
|
129
|
+
|
|
130
|
+
# collection wrapping
|
|
131
|
+
def _wrap_if_collection(self, name: str, value: Any) -> Any:
|
|
132
|
+
if isinstance(value, list) and not isinstance(value, ObservableList):
|
|
133
|
+
return ObservableList(self, name, value)
|
|
134
|
+
if isinstance(value, dict) and not isinstance(value, ObservableDict):
|
|
135
|
+
return ObservableDict(self, name, value)
|
|
136
|
+
return value
|
|
137
|
+
|
|
138
|
+
# attribute interception
|
|
139
|
+
def __setattr__(self, name: str, value: Any):
|
|
140
|
+
if name.startswith("_"): # private/internal, don't notify
|
|
141
|
+
object.__setattr__(self, name, value)
|
|
142
|
+
return
|
|
143
|
+
value = self._wrap_if_collection(name, value)
|
|
144
|
+
had = hasattr(self, name)
|
|
145
|
+
old = object.__getattribute__(self, name) if had else None
|
|
146
|
+
if not value_equal(old, value):
|
|
147
|
+
object.__setattr__(self, name, value)
|
|
148
|
+
self._notify(name)
|
|
149
|
+
|
|
150
|
+
def __delattr__(self, name: str):
|
|
151
|
+
existed = hasattr(self, name)
|
|
152
|
+
object.__delattr__(self, name)
|
|
153
|
+
if existed:
|
|
154
|
+
self._notify(name)
|
|
155
|
+
|
|
156
|
+
def __repr__(self):
|
|
157
|
+
return f"{super().__repr__()} (version={self.__version__})"
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# Observable collections -----------------------------------------
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class ObservableList(list):
|
|
164
|
+
__slots__ = ("_owner_ref", "_field")
|
|
165
|
+
|
|
166
|
+
def __init__(self, owner: Observable, field: str, iterable=()):
|
|
167
|
+
super().__init__(iterable)
|
|
168
|
+
self._owner_ref = weakref.ref(owner)
|
|
169
|
+
self._field = field
|
|
170
|
+
|
|
171
|
+
def _touch(self):
|
|
172
|
+
owner = self._owner_ref()
|
|
173
|
+
if owner:
|
|
174
|
+
owner._notify(self._field)
|
|
175
|
+
|
|
176
|
+
def _wrap(self, v):
|
|
177
|
+
owner = self._owner_ref()
|
|
178
|
+
return owner._wrap_if_collection(self._field, v) if owner else v
|
|
179
|
+
|
|
180
|
+
def append(self, x):
|
|
181
|
+
super().append(self._wrap(x))
|
|
182
|
+
self._touch()
|
|
183
|
+
|
|
184
|
+
def extend(self, it):
|
|
185
|
+
super().extend(self._wrap(v) for v in it)
|
|
186
|
+
self._touch()
|
|
187
|
+
|
|
188
|
+
def insert(self, i, x):
|
|
189
|
+
super().insert(i, self._wrap(x))
|
|
190
|
+
self._touch()
|
|
191
|
+
|
|
192
|
+
def remove(self, x):
|
|
193
|
+
super().remove(x)
|
|
194
|
+
self._touch()
|
|
195
|
+
|
|
196
|
+
def clear(self):
|
|
197
|
+
super().clear()
|
|
198
|
+
self._touch()
|
|
199
|
+
|
|
200
|
+
def sort(self, *a, **k):
|
|
201
|
+
super().sort(*a, **k)
|
|
202
|
+
self._touch()
|
|
203
|
+
|
|
204
|
+
def reverse(self):
|
|
205
|
+
super().reverse()
|
|
206
|
+
self._touch()
|
|
207
|
+
|
|
208
|
+
def pop(self, i=-1):
|
|
209
|
+
v = super().pop(i)
|
|
210
|
+
self._touch()
|
|
211
|
+
return v
|
|
212
|
+
|
|
213
|
+
def __setitem__(self, i, v):
|
|
214
|
+
super().__setitem__(i, self._wrap(v))
|
|
215
|
+
self._touch()
|
|
216
|
+
|
|
217
|
+
def __delitem__(self, i):
|
|
218
|
+
super().__delitem__(i)
|
|
219
|
+
self._touch()
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class ObservableDict(dict):
|
|
223
|
+
__slots__ = ("_owner_ref", "_field")
|
|
224
|
+
|
|
225
|
+
def __init__(self, owner: Observable, field: str, mapping=()):
|
|
226
|
+
super().__init__(mapping)
|
|
227
|
+
self._owner_ref = weakref.ref(owner)
|
|
228
|
+
self._field = field
|
|
229
|
+
|
|
230
|
+
def _touch(self):
|
|
231
|
+
owner = self._owner_ref()
|
|
232
|
+
if owner:
|
|
233
|
+
owner._notify(self._field)
|
|
234
|
+
|
|
235
|
+
def _wrap(self, v):
|
|
236
|
+
owner = self._owner_ref()
|
|
237
|
+
return owner._wrap_if_collection(self._field, v) if owner else v
|
|
238
|
+
|
|
239
|
+
def __setitem__(self, k, v):
|
|
240
|
+
super().__setitem__(k, self._wrap(v))
|
|
241
|
+
self._touch()
|
|
242
|
+
|
|
243
|
+
def __delitem__(self, k):
|
|
244
|
+
super().__delitem__(k)
|
|
245
|
+
self._touch()
|
|
246
|
+
|
|
247
|
+
def clear(self):
|
|
248
|
+
super().clear()
|
|
249
|
+
self._touch()
|
|
250
|
+
|
|
251
|
+
def update(self, *a, **k):
|
|
252
|
+
super().update(*a, **{kk: self._wrap(vv) for kk, vv in k.items()})
|
|
253
|
+
self._touch()
|
|
254
|
+
|
|
255
|
+
def pop(self, k, *d):
|
|
256
|
+
v = super().pop(k, *d)
|
|
257
|
+
self._touch()
|
|
258
|
+
return v
|
|
259
|
+
|
|
260
|
+
def popitem(self):
|
|
261
|
+
kv = super().popitem()
|
|
262
|
+
self._touch()
|
|
263
|
+
return kv
|
|
264
|
+
|
|
265
|
+
def setdefault(self, k, d=None):
|
|
266
|
+
if k not in self:
|
|
267
|
+
super().__setitem__(k, self._wrap(d))
|
|
268
|
+
self._touch()
|
|
269
|
+
return dict.__getitem__(self, k)
|
flet/components/utils.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import contextvars
|
|
2
|
+
import math
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from flet.components.component import Component, Renderer
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
_CURRENT_RENDERER: "contextvars.ContextVar[Renderer | None]" = contextvars.ContextVar(
|
|
11
|
+
"CURRENT_RENDERER", default=None
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def current_renderer() -> "Renderer":
|
|
16
|
+
r = _CURRENT_RENDERER.get()
|
|
17
|
+
if r is None:
|
|
18
|
+
raise RuntimeError(
|
|
19
|
+
"No current renderer is set. Call via Renderer.render(...) "
|
|
20
|
+
"or Renderer.with_context(...)."
|
|
21
|
+
)
|
|
22
|
+
return r
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def current_component() -> "Component":
|
|
26
|
+
r = current_renderer()
|
|
27
|
+
if not r._render_stack:
|
|
28
|
+
raise RuntimeError("Hooks must be called inside a component render.")
|
|
29
|
+
return r._render_stack[-1]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def value_equal(a, b) -> bool:
|
|
33
|
+
# Fast path
|
|
34
|
+
if a is b:
|
|
35
|
+
return True
|
|
36
|
+
# Handle normal equality
|
|
37
|
+
try:
|
|
38
|
+
if a == b:
|
|
39
|
+
return True
|
|
40
|
+
except Exception:
|
|
41
|
+
pass
|
|
42
|
+
# Treat NaN == NaN as equal (like JS Object.is)
|
|
43
|
+
return (
|
|
44
|
+
isinstance(a, float)
|
|
45
|
+
and isinstance(b, float)
|
|
46
|
+
and math.isnan(a)
|
|
47
|
+
and math.isnan(b)
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def shallow_compare_args(
|
|
52
|
+
prev_args: Sequence[Any],
|
|
53
|
+
args: Sequence[Any],
|
|
54
|
+
) -> bool:
|
|
55
|
+
if prev_args is args:
|
|
56
|
+
return True
|
|
57
|
+
if len(prev_args) != len(args):
|
|
58
|
+
return False
|
|
59
|
+
return all(not (a is not b and a != b) for a, b in zip(prev_args, args))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def shallow_compare_kwargs(
|
|
63
|
+
prev_kwargs: dict[str, Any],
|
|
64
|
+
kwargs: dict[str, Any],
|
|
65
|
+
) -> bool:
|
|
66
|
+
if prev_kwargs is kwargs:
|
|
67
|
+
return True
|
|
68
|
+
if prev_kwargs.keys() != kwargs.keys():
|
|
69
|
+
return False
|
|
70
|
+
for k in prev_kwargs:
|
|
71
|
+
a, b = prev_kwargs[k], kwargs[k]
|
|
72
|
+
if a is not b and a != b:
|
|
73
|
+
return False
|
|
74
|
+
return True
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def shallow_compare_args_and_kwargs(
|
|
78
|
+
prev_args: tuple[Any, ...],
|
|
79
|
+
prev_kwargs: dict[str, Any],
|
|
80
|
+
args: tuple[Any, ...],
|
|
81
|
+
kwargs: dict[str, Any],
|
|
82
|
+
) -> bool:
|
|
83
|
+
return shallow_compare_args(prev_args, args) and shallow_compare_kwargs(
|
|
84
|
+
prev_kwargs, kwargs
|
|
85
|
+
)
|