pythonnative 0.5.0__py3-none-any.whl → 0.6.0__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.
- pythonnative/__init__.py +55 -7
- pythonnative/cli/pn.py +138 -10
- pythonnative/components.py +344 -22
- pythonnative/element.py +14 -8
- pythonnative/hooks.py +287 -0
- pythonnative/hot_reload.py +143 -0
- pythonnative/native_modules/__init__.py +19 -0
- pythonnative/native_modules/camera.py +105 -0
- pythonnative/native_modules/file_system.py +131 -0
- pythonnative/native_modules/location.py +61 -0
- pythonnative/native_modules/notifications.py +151 -0
- pythonnative/native_views.py +552 -18
- pythonnative/page.py +1 -0
- pythonnative/reconciler.py +153 -20
- pythonnative/style.py +115 -0
- pythonnative/templates/android_template/app/build.gradle +2 -7
- pythonnative/templates/android_template/build.gradle +1 -1
- {pythonnative-0.5.0.dist-info → pythonnative-0.6.0.dist-info}/METADATA +2 -19
- {pythonnative-0.5.0.dist-info → pythonnative-0.6.0.dist-info}/RECORD +23 -18
- pythonnative/collection_view.py +0 -0
- pythonnative/material_bottom_navigation_view.py +0 -0
- pythonnative/material_toolbar.py +0 -0
- {pythonnative-0.5.0.dist-info → pythonnative-0.6.0.dist-info}/WHEEL +0 -0
- {pythonnative-0.5.0.dist-info → pythonnative-0.6.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.5.0.dist-info → pythonnative-0.6.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.5.0.dist-info → pythonnative-0.6.0.dist-info}/top_level.txt +0 -0
pythonnative/components.py
CHANGED
|
@@ -4,34 +4,56 @@ Each function returns an :class:`Element` describing a native UI widget.
|
|
|
4
4
|
These are pure data — no native views are created until the reconciler
|
|
5
5
|
mounts the element tree.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
- ``Button``
|
|
11
|
-
- ``Column`` / ``Row`` (was *StackView* vertical/horizontal)
|
|
12
|
-
- ``ScrollView``
|
|
13
|
-
- ``TextInput`` (was *TextField*)
|
|
14
|
-
- ``Image`` (was *ImageView*)
|
|
15
|
-
- ``Switch``
|
|
16
|
-
- ``ProgressBar`` (was *ProgressView*)
|
|
17
|
-
- ``ActivityIndicator`` (was *ActivityIndicatorView*)
|
|
18
|
-
- ``WebView``
|
|
19
|
-
- ``Spacer`` (new)
|
|
7
|
+
Layout properties (``width``, ``height``, ``flex``, ``margin``,
|
|
8
|
+
``min_width``, ``max_width``, ``min_height``, ``max_height``,
|
|
9
|
+
``align_self``) are supported by all components.
|
|
20
10
|
"""
|
|
21
11
|
|
|
22
|
-
from typing import Any, Callable, Dict, Optional, Union
|
|
12
|
+
from typing import Any, Callable, Dict, List, Optional, Union
|
|
23
13
|
|
|
24
14
|
from .element import Element
|
|
25
15
|
|
|
16
|
+
# ======================================================================
|
|
17
|
+
# Shared helpers
|
|
18
|
+
# ======================================================================
|
|
19
|
+
|
|
20
|
+
PaddingValue = Union[int, float, Dict[str, Union[int, float]]]
|
|
21
|
+
MarginValue = Union[int, float, Dict[str, Union[int, float]]]
|
|
22
|
+
|
|
26
23
|
|
|
27
24
|
def _filter_none(**kwargs: Any) -> Dict[str, Any]:
|
|
28
25
|
"""Return *kwargs* with ``None``-valued entries removed."""
|
|
29
26
|
return {k: v for k, v in kwargs.items() if v is not None}
|
|
30
27
|
|
|
31
28
|
|
|
32
|
-
|
|
29
|
+
def _layout_props(
|
|
30
|
+
width: Optional[float] = None,
|
|
31
|
+
height: Optional[float] = None,
|
|
32
|
+
flex: Optional[float] = None,
|
|
33
|
+
margin: Optional[MarginValue] = None,
|
|
34
|
+
min_width: Optional[float] = None,
|
|
35
|
+
max_width: Optional[float] = None,
|
|
36
|
+
min_height: Optional[float] = None,
|
|
37
|
+
max_height: Optional[float] = None,
|
|
38
|
+
align_self: Optional[str] = None,
|
|
39
|
+
) -> Dict[str, Any]:
|
|
40
|
+
"""Collect common layout props into a dict (excluding Nones)."""
|
|
41
|
+
return _filter_none(
|
|
42
|
+
width=width,
|
|
43
|
+
height=height,
|
|
44
|
+
flex=flex,
|
|
45
|
+
margin=margin,
|
|
46
|
+
min_width=min_width,
|
|
47
|
+
max_width=max_width,
|
|
48
|
+
min_height=min_height,
|
|
49
|
+
max_height=max_height,
|
|
50
|
+
align_self=align_self,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ======================================================================
|
|
33
55
|
# Leaf components
|
|
34
|
-
#
|
|
56
|
+
# ======================================================================
|
|
35
57
|
|
|
36
58
|
|
|
37
59
|
def Text(
|
|
@@ -43,6 +65,15 @@ def Text(
|
|
|
43
65
|
text_align: Optional[str] = None,
|
|
44
66
|
background_color: Optional[str] = None,
|
|
45
67
|
max_lines: Optional[int] = None,
|
|
68
|
+
width: Optional[float] = None,
|
|
69
|
+
height: Optional[float] = None,
|
|
70
|
+
flex: Optional[float] = None,
|
|
71
|
+
margin: Optional[MarginValue] = None,
|
|
72
|
+
min_width: Optional[float] = None,
|
|
73
|
+
max_width: Optional[float] = None,
|
|
74
|
+
min_height: Optional[float] = None,
|
|
75
|
+
max_height: Optional[float] = None,
|
|
76
|
+
align_self: Optional[str] = None,
|
|
46
77
|
key: Optional[str] = None,
|
|
47
78
|
) -> Element:
|
|
48
79
|
"""Display text."""
|
|
@@ -55,6 +86,19 @@ def Text(
|
|
|
55
86
|
background_color=background_color,
|
|
56
87
|
max_lines=max_lines,
|
|
57
88
|
)
|
|
89
|
+
props.update(
|
|
90
|
+
_layout_props(
|
|
91
|
+
width=width,
|
|
92
|
+
height=height,
|
|
93
|
+
flex=flex,
|
|
94
|
+
margin=margin,
|
|
95
|
+
min_width=min_width,
|
|
96
|
+
max_width=max_width,
|
|
97
|
+
min_height=min_height,
|
|
98
|
+
max_height=max_height,
|
|
99
|
+
align_self=align_self,
|
|
100
|
+
)
|
|
101
|
+
)
|
|
58
102
|
return Element("Text", props, [], key=key)
|
|
59
103
|
|
|
60
104
|
|
|
@@ -66,6 +110,15 @@ def Button(
|
|
|
66
110
|
background_color: Optional[str] = None,
|
|
67
111
|
font_size: Optional[float] = None,
|
|
68
112
|
enabled: bool = True,
|
|
113
|
+
width: Optional[float] = None,
|
|
114
|
+
height: Optional[float] = None,
|
|
115
|
+
flex: Optional[float] = None,
|
|
116
|
+
margin: Optional[MarginValue] = None,
|
|
117
|
+
min_width: Optional[float] = None,
|
|
118
|
+
max_width: Optional[float] = None,
|
|
119
|
+
min_height: Optional[float] = None,
|
|
120
|
+
max_height: Optional[float] = None,
|
|
121
|
+
align_self: Optional[str] = None,
|
|
69
122
|
key: Optional[str] = None,
|
|
70
123
|
) -> Element:
|
|
71
124
|
"""Create a tappable button."""
|
|
@@ -80,6 +133,19 @@ def Button(
|
|
|
80
133
|
props["font_size"] = font_size
|
|
81
134
|
if not enabled:
|
|
82
135
|
props["enabled"] = False
|
|
136
|
+
props.update(
|
|
137
|
+
_layout_props(
|
|
138
|
+
width=width,
|
|
139
|
+
height=height,
|
|
140
|
+
flex=flex,
|
|
141
|
+
margin=margin,
|
|
142
|
+
min_width=min_width,
|
|
143
|
+
max_width=max_width,
|
|
144
|
+
min_height=min_height,
|
|
145
|
+
max_height=max_height,
|
|
146
|
+
align_self=align_self,
|
|
147
|
+
)
|
|
148
|
+
)
|
|
83
149
|
return Element("Button", props, [], key=key)
|
|
84
150
|
|
|
85
151
|
|
|
@@ -92,6 +158,15 @@ def TextInput(
|
|
|
92
158
|
font_size: Optional[float] = None,
|
|
93
159
|
color: Optional[str] = None,
|
|
94
160
|
background_color: Optional[str] = None,
|
|
161
|
+
width: Optional[float] = None,
|
|
162
|
+
height: Optional[float] = None,
|
|
163
|
+
flex: Optional[float] = None,
|
|
164
|
+
margin: Optional[MarginValue] = None,
|
|
165
|
+
min_width: Optional[float] = None,
|
|
166
|
+
max_width: Optional[float] = None,
|
|
167
|
+
min_height: Optional[float] = None,
|
|
168
|
+
max_height: Optional[float] = None,
|
|
169
|
+
align_self: Optional[str] = None,
|
|
95
170
|
key: Optional[str] = None,
|
|
96
171
|
) -> Element:
|
|
97
172
|
"""Create a single-line text entry field."""
|
|
@@ -108,6 +183,19 @@ def TextInput(
|
|
|
108
183
|
props["color"] = color
|
|
109
184
|
if background_color is not None:
|
|
110
185
|
props["background_color"] = background_color
|
|
186
|
+
props.update(
|
|
187
|
+
_layout_props(
|
|
188
|
+
width=width,
|
|
189
|
+
height=height,
|
|
190
|
+
flex=flex,
|
|
191
|
+
margin=margin,
|
|
192
|
+
min_width=min_width,
|
|
193
|
+
max_width=max_width,
|
|
194
|
+
min_height=min_height,
|
|
195
|
+
max_height=max_height,
|
|
196
|
+
align_self=align_self,
|
|
197
|
+
)
|
|
198
|
+
)
|
|
111
199
|
return Element("TextInput", props, [], key=key)
|
|
112
200
|
|
|
113
201
|
|
|
@@ -118,6 +206,13 @@ def Image(
|
|
|
118
206
|
height: Optional[float] = None,
|
|
119
207
|
scale_type: Optional[str] = None,
|
|
120
208
|
background_color: Optional[str] = None,
|
|
209
|
+
flex: Optional[float] = None,
|
|
210
|
+
margin: Optional[MarginValue] = None,
|
|
211
|
+
min_width: Optional[float] = None,
|
|
212
|
+
max_width: Optional[float] = None,
|
|
213
|
+
min_height: Optional[float] = None,
|
|
214
|
+
max_height: Optional[float] = None,
|
|
215
|
+
align_self: Optional[str] = None,
|
|
121
216
|
key: Optional[str] = None,
|
|
122
217
|
) -> Element:
|
|
123
218
|
"""Display an image from a resource path or URL."""
|
|
@@ -128,6 +223,17 @@ def Image(
|
|
|
128
223
|
scale_type=scale_type,
|
|
129
224
|
background_color=background_color,
|
|
130
225
|
)
|
|
226
|
+
props.update(
|
|
227
|
+
_layout_props(
|
|
228
|
+
flex=flex,
|
|
229
|
+
margin=margin,
|
|
230
|
+
min_width=min_width,
|
|
231
|
+
max_width=max_width,
|
|
232
|
+
min_height=min_height,
|
|
233
|
+
max_height=max_height,
|
|
234
|
+
align_self=align_self,
|
|
235
|
+
)
|
|
236
|
+
)
|
|
131
237
|
return Element("Image", props, [], key=key)
|
|
132
238
|
|
|
133
239
|
|
|
@@ -135,12 +241,18 @@ def Switch(
|
|
|
135
241
|
*,
|
|
136
242
|
value: bool = False,
|
|
137
243
|
on_change: Optional[Callable[[bool], None]] = None,
|
|
244
|
+
width: Optional[float] = None,
|
|
245
|
+
height: Optional[float] = None,
|
|
246
|
+
flex: Optional[float] = None,
|
|
247
|
+
margin: Optional[MarginValue] = None,
|
|
248
|
+
align_self: Optional[str] = None,
|
|
138
249
|
key: Optional[str] = None,
|
|
139
250
|
) -> Element:
|
|
140
251
|
"""Create a toggle switch."""
|
|
141
252
|
props: Dict[str, Any] = {"value": value}
|
|
142
253
|
if on_change is not None:
|
|
143
254
|
props["on_change"] = on_change
|
|
255
|
+
props.update(_layout_props(width=width, height=height, flex=flex, margin=margin, align_self=align_self))
|
|
144
256
|
return Element("Switch", props, [], key=key)
|
|
145
257
|
|
|
146
258
|
|
|
@@ -148,49 +260,66 @@ def ProgressBar(
|
|
|
148
260
|
*,
|
|
149
261
|
value: float = 0.0,
|
|
150
262
|
background_color: Optional[str] = None,
|
|
263
|
+
width: Optional[float] = None,
|
|
264
|
+
height: Optional[float] = None,
|
|
265
|
+
flex: Optional[float] = None,
|
|
266
|
+
margin: Optional[MarginValue] = None,
|
|
267
|
+
align_self: Optional[str] = None,
|
|
151
268
|
key: Optional[str] = None,
|
|
152
269
|
) -> Element:
|
|
153
270
|
"""Show determinate progress (0.0 – 1.0)."""
|
|
154
271
|
props = _filter_none(value=value, background_color=background_color)
|
|
272
|
+
props.update(_layout_props(width=width, height=height, flex=flex, margin=margin, align_self=align_self))
|
|
155
273
|
return Element("ProgressBar", props, [], key=key)
|
|
156
274
|
|
|
157
275
|
|
|
158
276
|
def ActivityIndicator(
|
|
159
277
|
*,
|
|
160
278
|
animating: bool = True,
|
|
279
|
+
width: Optional[float] = None,
|
|
280
|
+
height: Optional[float] = None,
|
|
281
|
+
margin: Optional[MarginValue] = None,
|
|
282
|
+
align_self: Optional[str] = None,
|
|
161
283
|
key: Optional[str] = None,
|
|
162
284
|
) -> Element:
|
|
163
285
|
"""Show an indeterminate loading spinner."""
|
|
164
|
-
|
|
286
|
+
props: Dict[str, Any] = {"animating": animating}
|
|
287
|
+
props.update(_layout_props(width=width, height=height, margin=margin, align_self=align_self))
|
|
288
|
+
return Element("ActivityIndicator", props, [], key=key)
|
|
165
289
|
|
|
166
290
|
|
|
167
291
|
def WebView(
|
|
168
292
|
*,
|
|
169
293
|
url: str = "",
|
|
294
|
+
width: Optional[float] = None,
|
|
295
|
+
height: Optional[float] = None,
|
|
296
|
+
flex: Optional[float] = None,
|
|
297
|
+
margin: Optional[MarginValue] = None,
|
|
298
|
+
align_self: Optional[str] = None,
|
|
170
299
|
key: Optional[str] = None,
|
|
171
300
|
) -> Element:
|
|
172
301
|
"""Embed web content."""
|
|
173
302
|
props: Dict[str, Any] = {}
|
|
174
303
|
if url:
|
|
175
304
|
props["url"] = url
|
|
305
|
+
props.update(_layout_props(width=width, height=height, flex=flex, margin=margin, align_self=align_self))
|
|
176
306
|
return Element("WebView", props, [], key=key)
|
|
177
307
|
|
|
178
308
|
|
|
179
309
|
def Spacer(
|
|
180
310
|
*,
|
|
181
311
|
size: Optional[float] = None,
|
|
312
|
+
flex: Optional[float] = None,
|
|
182
313
|
key: Optional[str] = None,
|
|
183
314
|
) -> Element:
|
|
184
315
|
"""Insert empty space with an optional fixed size."""
|
|
185
|
-
props = _filter_none(size=size)
|
|
316
|
+
props = _filter_none(size=size, flex=flex)
|
|
186
317
|
return Element("Spacer", props, [], key=key)
|
|
187
318
|
|
|
188
319
|
|
|
189
|
-
#
|
|
320
|
+
# ======================================================================
|
|
190
321
|
# Container components
|
|
191
|
-
#
|
|
192
|
-
|
|
193
|
-
PaddingValue = Union[int, float, Dict[str, Union[int, float]]]
|
|
322
|
+
# ======================================================================
|
|
194
323
|
|
|
195
324
|
|
|
196
325
|
def Column(
|
|
@@ -199,6 +328,15 @@ def Column(
|
|
|
199
328
|
padding: Optional[PaddingValue] = None,
|
|
200
329
|
alignment: Optional[str] = None,
|
|
201
330
|
background_color: Optional[str] = None,
|
|
331
|
+
width: Optional[float] = None,
|
|
332
|
+
height: Optional[float] = None,
|
|
333
|
+
flex: Optional[float] = None,
|
|
334
|
+
margin: Optional[MarginValue] = None,
|
|
335
|
+
min_width: Optional[float] = None,
|
|
336
|
+
max_width: Optional[float] = None,
|
|
337
|
+
min_height: Optional[float] = None,
|
|
338
|
+
max_height: Optional[float] = None,
|
|
339
|
+
align_self: Optional[str] = None,
|
|
202
340
|
key: Optional[str] = None,
|
|
203
341
|
) -> Element:
|
|
204
342
|
"""Arrange children vertically."""
|
|
@@ -208,6 +346,19 @@ def Column(
|
|
|
208
346
|
alignment=alignment,
|
|
209
347
|
background_color=background_color,
|
|
210
348
|
)
|
|
349
|
+
props.update(
|
|
350
|
+
_layout_props(
|
|
351
|
+
width=width,
|
|
352
|
+
height=height,
|
|
353
|
+
flex=flex,
|
|
354
|
+
margin=margin,
|
|
355
|
+
min_width=min_width,
|
|
356
|
+
max_width=max_width,
|
|
357
|
+
min_height=min_height,
|
|
358
|
+
max_height=max_height,
|
|
359
|
+
align_self=align_self,
|
|
360
|
+
)
|
|
361
|
+
)
|
|
211
362
|
return Element("Column", props, list(children), key=key)
|
|
212
363
|
|
|
213
364
|
|
|
@@ -217,6 +368,15 @@ def Row(
|
|
|
217
368
|
padding: Optional[PaddingValue] = None,
|
|
218
369
|
alignment: Optional[str] = None,
|
|
219
370
|
background_color: Optional[str] = None,
|
|
371
|
+
width: Optional[float] = None,
|
|
372
|
+
height: Optional[float] = None,
|
|
373
|
+
flex: Optional[float] = None,
|
|
374
|
+
margin: Optional[MarginValue] = None,
|
|
375
|
+
min_width: Optional[float] = None,
|
|
376
|
+
max_width: Optional[float] = None,
|
|
377
|
+
min_height: Optional[float] = None,
|
|
378
|
+
max_height: Optional[float] = None,
|
|
379
|
+
align_self: Optional[str] = None,
|
|
220
380
|
key: Optional[str] = None,
|
|
221
381
|
) -> Element:
|
|
222
382
|
"""Arrange children horizontally."""
|
|
@@ -226,6 +386,19 @@ def Row(
|
|
|
226
386
|
alignment=alignment,
|
|
227
387
|
background_color=background_color,
|
|
228
388
|
)
|
|
389
|
+
props.update(
|
|
390
|
+
_layout_props(
|
|
391
|
+
width=width,
|
|
392
|
+
height=height,
|
|
393
|
+
flex=flex,
|
|
394
|
+
margin=margin,
|
|
395
|
+
min_width=min_width,
|
|
396
|
+
max_width=max_width,
|
|
397
|
+
min_height=min_height,
|
|
398
|
+
max_height=max_height,
|
|
399
|
+
align_self=align_self,
|
|
400
|
+
)
|
|
401
|
+
)
|
|
229
402
|
return Element("Row", props, list(children), key=key)
|
|
230
403
|
|
|
231
404
|
|
|
@@ -233,9 +406,158 @@ def ScrollView(
|
|
|
233
406
|
child: Optional[Element] = None,
|
|
234
407
|
*,
|
|
235
408
|
background_color: Optional[str] = None,
|
|
409
|
+
width: Optional[float] = None,
|
|
410
|
+
height: Optional[float] = None,
|
|
411
|
+
flex: Optional[float] = None,
|
|
412
|
+
margin: Optional[MarginValue] = None,
|
|
413
|
+
align_self: Optional[str] = None,
|
|
236
414
|
key: Optional[str] = None,
|
|
237
415
|
) -> Element:
|
|
238
416
|
"""Wrap a single child in a scrollable container."""
|
|
239
417
|
children = [child] if child is not None else []
|
|
240
418
|
props = _filter_none(background_color=background_color)
|
|
419
|
+
props.update(_layout_props(width=width, height=height, flex=flex, margin=margin, align_self=align_self))
|
|
241
420
|
return Element("ScrollView", props, children, key=key)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def View(
|
|
424
|
+
*children: Element,
|
|
425
|
+
background_color: Optional[str] = None,
|
|
426
|
+
padding: Optional[PaddingValue] = None,
|
|
427
|
+
width: Optional[float] = None,
|
|
428
|
+
height: Optional[float] = None,
|
|
429
|
+
flex: Optional[float] = None,
|
|
430
|
+
margin: Optional[MarginValue] = None,
|
|
431
|
+
min_width: Optional[float] = None,
|
|
432
|
+
max_width: Optional[float] = None,
|
|
433
|
+
min_height: Optional[float] = None,
|
|
434
|
+
max_height: Optional[float] = None,
|
|
435
|
+
align_self: Optional[str] = None,
|
|
436
|
+
key: Optional[str] = None,
|
|
437
|
+
) -> Element:
|
|
438
|
+
"""Generic container view (``UIView`` / ``android.view.View``)."""
|
|
439
|
+
props = _filter_none(
|
|
440
|
+
background_color=background_color,
|
|
441
|
+
padding=padding,
|
|
442
|
+
)
|
|
443
|
+
props.update(
|
|
444
|
+
_layout_props(
|
|
445
|
+
width=width,
|
|
446
|
+
height=height,
|
|
447
|
+
flex=flex,
|
|
448
|
+
margin=margin,
|
|
449
|
+
min_width=min_width,
|
|
450
|
+
max_width=max_width,
|
|
451
|
+
min_height=min_height,
|
|
452
|
+
max_height=max_height,
|
|
453
|
+
align_self=align_self,
|
|
454
|
+
)
|
|
455
|
+
)
|
|
456
|
+
return Element("View", props, list(children), key=key)
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def SafeAreaView(
|
|
460
|
+
*children: Element,
|
|
461
|
+
background_color: Optional[str] = None,
|
|
462
|
+
padding: Optional[PaddingValue] = None,
|
|
463
|
+
key: Optional[str] = None,
|
|
464
|
+
) -> Element:
|
|
465
|
+
"""Container that respects safe area insets (notch, status bar)."""
|
|
466
|
+
props = _filter_none(background_color=background_color, padding=padding)
|
|
467
|
+
return Element("SafeAreaView", props, list(children), key=key)
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def Modal(
|
|
471
|
+
*children: Element,
|
|
472
|
+
visible: bool = False,
|
|
473
|
+
on_dismiss: Optional[Callable[[], None]] = None,
|
|
474
|
+
title: Optional[str] = None,
|
|
475
|
+
background_color: Optional[str] = None,
|
|
476
|
+
key: Optional[str] = None,
|
|
477
|
+
) -> Element:
|
|
478
|
+
"""Overlay modal dialog.
|
|
479
|
+
|
|
480
|
+
The modal is shown when ``visible=True`` and hidden when ``False``.
|
|
481
|
+
"""
|
|
482
|
+
props: Dict[str, Any] = {"visible": visible}
|
|
483
|
+
if on_dismiss is not None:
|
|
484
|
+
props["on_dismiss"] = on_dismiss
|
|
485
|
+
if title is not None:
|
|
486
|
+
props["title"] = title
|
|
487
|
+
if background_color is not None:
|
|
488
|
+
props["background_color"] = background_color
|
|
489
|
+
return Element("Modal", props, list(children), key=key)
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def Slider(
|
|
493
|
+
*,
|
|
494
|
+
value: float = 0.0,
|
|
495
|
+
min_value: float = 0.0,
|
|
496
|
+
max_value: float = 1.0,
|
|
497
|
+
on_change: Optional[Callable[[float], None]] = None,
|
|
498
|
+
width: Optional[float] = None,
|
|
499
|
+
margin: Optional[MarginValue] = None,
|
|
500
|
+
align_self: Optional[str] = None,
|
|
501
|
+
key: Optional[str] = None,
|
|
502
|
+
) -> Element:
|
|
503
|
+
"""Continuous value slider."""
|
|
504
|
+
props: Dict[str, Any] = {
|
|
505
|
+
"value": value,
|
|
506
|
+
"min_value": min_value,
|
|
507
|
+
"max_value": max_value,
|
|
508
|
+
}
|
|
509
|
+
if on_change is not None:
|
|
510
|
+
props["on_change"] = on_change
|
|
511
|
+
props.update(_layout_props(width=width, margin=margin, align_self=align_self))
|
|
512
|
+
return Element("Slider", props, [], key=key)
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def Pressable(
|
|
516
|
+
child: Optional[Element] = None,
|
|
517
|
+
*,
|
|
518
|
+
on_press: Optional[Callable[[], None]] = None,
|
|
519
|
+
on_long_press: Optional[Callable[[], None]] = None,
|
|
520
|
+
key: Optional[str] = None,
|
|
521
|
+
) -> Element:
|
|
522
|
+
"""Wrapper that adds press handling to any child element."""
|
|
523
|
+
props: Dict[str, Any] = {}
|
|
524
|
+
if on_press is not None:
|
|
525
|
+
props["on_press"] = on_press
|
|
526
|
+
if on_long_press is not None:
|
|
527
|
+
props["on_long_press"] = on_long_press
|
|
528
|
+
children = [child] if child is not None else []
|
|
529
|
+
return Element("Pressable", props, children, key=key)
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def FlatList(
|
|
533
|
+
*,
|
|
534
|
+
data: Optional[List[Any]] = None,
|
|
535
|
+
render_item: Optional[Callable[[Any, int], Element]] = None,
|
|
536
|
+
key_extractor: Optional[Callable[[Any, int], str]] = None,
|
|
537
|
+
separator_height: float = 0,
|
|
538
|
+
background_color: Optional[str] = None,
|
|
539
|
+
width: Optional[float] = None,
|
|
540
|
+
height: Optional[float] = None,
|
|
541
|
+
flex: Optional[float] = None,
|
|
542
|
+
margin: Optional[MarginValue] = None,
|
|
543
|
+
align_self: Optional[str] = None,
|
|
544
|
+
key: Optional[str] = None,
|
|
545
|
+
) -> Element:
|
|
546
|
+
"""Scrollable list that renders items from *data* using *render_item*.
|
|
547
|
+
|
|
548
|
+
Each item is rendered by calling ``render_item(item, index)``. If
|
|
549
|
+
``key_extractor`` is provided, it is called as ``key_extractor(item, index)``
|
|
550
|
+
to produce a stable key for each child element. This enables the
|
|
551
|
+
reconciler to preserve widget state across data changes.
|
|
552
|
+
"""
|
|
553
|
+
items: List[Element] = []
|
|
554
|
+
for i, item in enumerate(data or []):
|
|
555
|
+
el = render_item(item, i) if render_item else Text(str(item))
|
|
556
|
+
if key_extractor is not None:
|
|
557
|
+
el = Element(el.type, el.props, el.children, key=key_extractor(item, i))
|
|
558
|
+
items.append(el)
|
|
559
|
+
|
|
560
|
+
inner = Column(*items, spacing=separator_height)
|
|
561
|
+
sv_props = _filter_none(background_color=background_color)
|
|
562
|
+
sv_props.update(_layout_props(width=width, height=height, flex=flex, margin=margin, align_self=align_self))
|
|
563
|
+
return Element("ScrollView", sv_props, [inner], key=key)
|
pythonnative/element.py
CHANGED
|
@@ -1,23 +1,28 @@
|
|
|
1
1
|
"""Lightweight element descriptors for the virtual view tree.
|
|
2
2
|
|
|
3
3
|
An Element is an immutable description of a UI node — analogous to a React
|
|
4
|
-
element. It captures a type name
|
|
5
|
-
of children without creating any native
|
|
6
|
-
consumes these trees to determine what
|
|
7
|
-
updated, or removed.
|
|
4
|
+
element. It captures a type (name string **or** component function), a props
|
|
5
|
+
dictionary, and an ordered list of children without creating any native
|
|
6
|
+
platform objects. The reconciler consumes these trees to determine what
|
|
7
|
+
native views must be created, updated, or removed.
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
-
from typing import Any, Dict, List, Optional
|
|
10
|
+
from typing import Any, Dict, List, Optional, Union
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class Element:
|
|
14
|
-
"""Immutable description of a single UI node.
|
|
14
|
+
"""Immutable description of a single UI node.
|
|
15
|
+
|
|
16
|
+
``type_name`` may be a *string* (e.g. ``"Text"``) for built-in native
|
|
17
|
+
elements or a *callable* for function components decorated with
|
|
18
|
+
:func:`~pythonnative.hooks.component`.
|
|
19
|
+
"""
|
|
15
20
|
|
|
16
21
|
__slots__ = ("type", "props", "children", "key")
|
|
17
22
|
|
|
18
23
|
def __init__(
|
|
19
24
|
self,
|
|
20
|
-
type_name: str,
|
|
25
|
+
type_name: Union[str, Any],
|
|
21
26
|
props: Dict[str, Any],
|
|
22
27
|
children: List["Element"],
|
|
23
28
|
key: Optional[str] = None,
|
|
@@ -28,7 +33,8 @@ class Element:
|
|
|
28
33
|
self.key = key
|
|
29
34
|
|
|
30
35
|
def __repr__(self) -> str:
|
|
31
|
-
|
|
36
|
+
t = self.type if isinstance(self.type, str) else getattr(self.type, "__name__", repr(self.type))
|
|
37
|
+
return f"Element({t!r}, props={set(self.props)}, children={len(self.children)})"
|
|
32
38
|
|
|
33
39
|
def __eq__(self, other: object) -> bool:
|
|
34
40
|
if not isinstance(other, Element):
|