pythonnative 0.4.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 +94 -66
- pythonnative/cli/pn.py +153 -24
- pythonnative/components.py +563 -0
- pythonnative/element.py +53 -0
- 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 +1334 -0
- pythonnative/page.py +320 -247
- pythonnative/reconciler.py +262 -0
- pythonnative/style.py +115 -0
- pythonnative/templates/android_template/app/build.gradle +2 -7
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PageFragment.kt +2 -1
- pythonnative/templates/android_template/build.gradle +1 -1
- pythonnative/utils.py +21 -29
- {pythonnative-0.4.0.dist-info → pythonnative-0.6.0.dist-info}/METADATA +20 -19
- {pythonnative-0.4.0.dist-info → pythonnative-0.6.0.dist-info}/RECORD +25 -40
- pythonnative/activity_indicator_view.py +0 -71
- pythonnative/button.py +0 -113
- pythonnative/collection_view.py +0 -0
- pythonnative/date_picker.py +0 -76
- pythonnative/image_view.py +0 -78
- pythonnative/label.py +0 -133
- pythonnative/list_view.py +0 -76
- pythonnative/material_activity_indicator_view.py +0 -71
- pythonnative/material_bottom_navigation_view.py +0 -0
- pythonnative/material_button.py +0 -69
- pythonnative/material_date_picker.py +0 -87
- pythonnative/material_progress_view.py +0 -70
- pythonnative/material_search_bar.py +0 -69
- pythonnative/material_switch.py +0 -69
- pythonnative/material_time_picker.py +0 -76
- pythonnative/material_toolbar.py +0 -0
- pythonnative/picker_view.py +0 -69
- pythonnative/progress_view.py +0 -70
- pythonnative/scroll_view.py +0 -101
- pythonnative/search_bar.py +0 -69
- pythonnative/stack_view.py +0 -199
- pythonnative/switch.py +0 -68
- pythonnative/text_field.py +0 -132
- pythonnative/text_view.py +0 -135
- pythonnative/time_picker.py +0 -77
- pythonnative/view.py +0 -173
- pythonnative/web_view.py +0 -60
- {pythonnative-0.4.0.dist-info → pythonnative-0.6.0.dist-info}/WHEEL +0 -0
- {pythonnative-0.4.0.dist-info → pythonnative-0.6.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.4.0.dist-info → pythonnative-0.6.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.4.0.dist-info → pythonnative-0.6.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
"""Built-in element-creating functions for declarative UI composition.
|
|
2
|
+
|
|
3
|
+
Each function returns an :class:`Element` describing a native UI widget.
|
|
4
|
+
These are pure data — no native views are created until the reconciler
|
|
5
|
+
mounts the element tree.
|
|
6
|
+
|
|
7
|
+
Layout properties (``width``, ``height``, ``flex``, ``margin``,
|
|
8
|
+
``min_width``, ``max_width``, ``min_height``, ``max_height``,
|
|
9
|
+
``align_self``) are supported by all components.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from typing import Any, Callable, Dict, List, Optional, Union
|
|
13
|
+
|
|
14
|
+
from .element import Element
|
|
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
|
+
|
|
23
|
+
|
|
24
|
+
def _filter_none(**kwargs: Any) -> Dict[str, Any]:
|
|
25
|
+
"""Return *kwargs* with ``None``-valued entries removed."""
|
|
26
|
+
return {k: v for k, v in kwargs.items() if v is not None}
|
|
27
|
+
|
|
28
|
+
|
|
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
|
+
# ======================================================================
|
|
55
|
+
# Leaf components
|
|
56
|
+
# ======================================================================
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def Text(
|
|
60
|
+
text: str = "",
|
|
61
|
+
*,
|
|
62
|
+
font_size: Optional[float] = None,
|
|
63
|
+
color: Optional[str] = None,
|
|
64
|
+
bold: bool = False,
|
|
65
|
+
text_align: Optional[str] = None,
|
|
66
|
+
background_color: Optional[str] = None,
|
|
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,
|
|
77
|
+
key: Optional[str] = None,
|
|
78
|
+
) -> Element:
|
|
79
|
+
"""Display text."""
|
|
80
|
+
props = _filter_none(
|
|
81
|
+
text=text,
|
|
82
|
+
font_size=font_size,
|
|
83
|
+
color=color,
|
|
84
|
+
bold=bold or None,
|
|
85
|
+
text_align=text_align,
|
|
86
|
+
background_color=background_color,
|
|
87
|
+
max_lines=max_lines,
|
|
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
|
+
)
|
|
102
|
+
return Element("Text", props, [], key=key)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def Button(
|
|
106
|
+
title: str = "",
|
|
107
|
+
*,
|
|
108
|
+
on_click: Optional[Callable[[], None]] = None,
|
|
109
|
+
color: Optional[str] = None,
|
|
110
|
+
background_color: Optional[str] = None,
|
|
111
|
+
font_size: Optional[float] = None,
|
|
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,
|
|
122
|
+
key: Optional[str] = None,
|
|
123
|
+
) -> Element:
|
|
124
|
+
"""Create a tappable button."""
|
|
125
|
+
props: Dict[str, Any] = {"title": title}
|
|
126
|
+
if on_click is not None:
|
|
127
|
+
props["on_click"] = on_click
|
|
128
|
+
if color is not None:
|
|
129
|
+
props["color"] = color
|
|
130
|
+
if background_color is not None:
|
|
131
|
+
props["background_color"] = background_color
|
|
132
|
+
if font_size is not None:
|
|
133
|
+
props["font_size"] = font_size
|
|
134
|
+
if not enabled:
|
|
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
|
+
)
|
|
149
|
+
return Element("Button", props, [], key=key)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def TextInput(
|
|
153
|
+
*,
|
|
154
|
+
value: str = "",
|
|
155
|
+
placeholder: str = "",
|
|
156
|
+
on_change: Optional[Callable[[str], None]] = None,
|
|
157
|
+
secure: bool = False,
|
|
158
|
+
font_size: Optional[float] = None,
|
|
159
|
+
color: Optional[str] = None,
|
|
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,
|
|
170
|
+
key: Optional[str] = None,
|
|
171
|
+
) -> Element:
|
|
172
|
+
"""Create a single-line text entry field."""
|
|
173
|
+
props: Dict[str, Any] = {"value": value}
|
|
174
|
+
if placeholder:
|
|
175
|
+
props["placeholder"] = placeholder
|
|
176
|
+
if on_change is not None:
|
|
177
|
+
props["on_change"] = on_change
|
|
178
|
+
if secure:
|
|
179
|
+
props["secure"] = True
|
|
180
|
+
if font_size is not None:
|
|
181
|
+
props["font_size"] = font_size
|
|
182
|
+
if color is not None:
|
|
183
|
+
props["color"] = color
|
|
184
|
+
if background_color is not None:
|
|
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
|
+
)
|
|
199
|
+
return Element("TextInput", props, [], key=key)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def Image(
|
|
203
|
+
source: str = "",
|
|
204
|
+
*,
|
|
205
|
+
width: Optional[float] = None,
|
|
206
|
+
height: Optional[float] = None,
|
|
207
|
+
scale_type: Optional[str] = None,
|
|
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,
|
|
216
|
+
key: Optional[str] = None,
|
|
217
|
+
) -> Element:
|
|
218
|
+
"""Display an image from a resource path or URL."""
|
|
219
|
+
props = _filter_none(
|
|
220
|
+
source=source or None,
|
|
221
|
+
width=width,
|
|
222
|
+
height=height,
|
|
223
|
+
scale_type=scale_type,
|
|
224
|
+
background_color=background_color,
|
|
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
|
+
)
|
|
237
|
+
return Element("Image", props, [], key=key)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def Switch(
|
|
241
|
+
*,
|
|
242
|
+
value: bool = False,
|
|
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,
|
|
249
|
+
key: Optional[str] = None,
|
|
250
|
+
) -> Element:
|
|
251
|
+
"""Create a toggle switch."""
|
|
252
|
+
props: Dict[str, Any] = {"value": value}
|
|
253
|
+
if on_change is not None:
|
|
254
|
+
props["on_change"] = on_change
|
|
255
|
+
props.update(_layout_props(width=width, height=height, flex=flex, margin=margin, align_self=align_self))
|
|
256
|
+
return Element("Switch", props, [], key=key)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def ProgressBar(
|
|
260
|
+
*,
|
|
261
|
+
value: float = 0.0,
|
|
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,
|
|
268
|
+
key: Optional[str] = None,
|
|
269
|
+
) -> Element:
|
|
270
|
+
"""Show determinate progress (0.0 – 1.0)."""
|
|
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))
|
|
273
|
+
return Element("ProgressBar", props, [], key=key)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def ActivityIndicator(
|
|
277
|
+
*,
|
|
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,
|
|
283
|
+
key: Optional[str] = None,
|
|
284
|
+
) -> Element:
|
|
285
|
+
"""Show an indeterminate loading spinner."""
|
|
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)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def WebView(
|
|
292
|
+
*,
|
|
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,
|
|
299
|
+
key: Optional[str] = None,
|
|
300
|
+
) -> Element:
|
|
301
|
+
"""Embed web content."""
|
|
302
|
+
props: Dict[str, Any] = {}
|
|
303
|
+
if url:
|
|
304
|
+
props["url"] = url
|
|
305
|
+
props.update(_layout_props(width=width, height=height, flex=flex, margin=margin, align_self=align_self))
|
|
306
|
+
return Element("WebView", props, [], key=key)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def Spacer(
|
|
310
|
+
*,
|
|
311
|
+
size: Optional[float] = None,
|
|
312
|
+
flex: Optional[float] = None,
|
|
313
|
+
key: Optional[str] = None,
|
|
314
|
+
) -> Element:
|
|
315
|
+
"""Insert empty space with an optional fixed size."""
|
|
316
|
+
props = _filter_none(size=size, flex=flex)
|
|
317
|
+
return Element("Spacer", props, [], key=key)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
# ======================================================================
|
|
321
|
+
# Container components
|
|
322
|
+
# ======================================================================
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def Column(
|
|
326
|
+
*children: Element,
|
|
327
|
+
spacing: float = 0,
|
|
328
|
+
padding: Optional[PaddingValue] = None,
|
|
329
|
+
alignment: Optional[str] = None,
|
|
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,
|
|
340
|
+
key: Optional[str] = None,
|
|
341
|
+
) -> Element:
|
|
342
|
+
"""Arrange children vertically."""
|
|
343
|
+
props = _filter_none(
|
|
344
|
+
spacing=spacing or None,
|
|
345
|
+
padding=padding,
|
|
346
|
+
alignment=alignment,
|
|
347
|
+
background_color=background_color,
|
|
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
|
+
)
|
|
362
|
+
return Element("Column", props, list(children), key=key)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def Row(
|
|
366
|
+
*children: Element,
|
|
367
|
+
spacing: float = 0,
|
|
368
|
+
padding: Optional[PaddingValue] = None,
|
|
369
|
+
alignment: Optional[str] = None,
|
|
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,
|
|
380
|
+
key: Optional[str] = None,
|
|
381
|
+
) -> Element:
|
|
382
|
+
"""Arrange children horizontally."""
|
|
383
|
+
props = _filter_none(
|
|
384
|
+
spacing=spacing or None,
|
|
385
|
+
padding=padding,
|
|
386
|
+
alignment=alignment,
|
|
387
|
+
background_color=background_color,
|
|
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
|
+
)
|
|
402
|
+
return Element("Row", props, list(children), key=key)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def ScrollView(
|
|
406
|
+
child: Optional[Element] = None,
|
|
407
|
+
*,
|
|
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,
|
|
414
|
+
key: Optional[str] = None,
|
|
415
|
+
) -> Element:
|
|
416
|
+
"""Wrap a single child in a scrollable container."""
|
|
417
|
+
children = [child] if child is not None else []
|
|
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))
|
|
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
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Lightweight element descriptors for the virtual view tree.
|
|
2
|
+
|
|
3
|
+
An Element is an immutable description of a UI node — analogous to a React
|
|
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
|
+
"""
|
|
9
|
+
|
|
10
|
+
from typing import Any, Dict, List, Optional, Union
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Element:
|
|
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
|
+
"""
|
|
20
|
+
|
|
21
|
+
__slots__ = ("type", "props", "children", "key")
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
type_name: Union[str, Any],
|
|
26
|
+
props: Dict[str, Any],
|
|
27
|
+
children: List["Element"],
|
|
28
|
+
key: Optional[str] = None,
|
|
29
|
+
) -> None:
|
|
30
|
+
self.type = type_name
|
|
31
|
+
self.props = props
|
|
32
|
+
self.children = children
|
|
33
|
+
self.key = key
|
|
34
|
+
|
|
35
|
+
def __repr__(self) -> str:
|
|
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)})"
|
|
38
|
+
|
|
39
|
+
def __eq__(self, other: object) -> bool:
|
|
40
|
+
if not isinstance(other, Element):
|
|
41
|
+
return NotImplemented
|
|
42
|
+
return (
|
|
43
|
+
self.type == other.type
|
|
44
|
+
and self.props == other.props
|
|
45
|
+
and self.children == other.children
|
|
46
|
+
and self.key == other.key
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def __ne__(self, other: object) -> bool:
|
|
50
|
+
result = self.__eq__(other)
|
|
51
|
+
if result is NotImplemented:
|
|
52
|
+
return result
|
|
53
|
+
return not result
|