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.
@@ -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
- Naming follows React Native conventions:
8
-
9
- - ``Text`` (was *Label*)
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
- return Element("ActivityIndicator", {"animating": animating}, [], key=key)
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, a props dictionary, and an ordered list
5
- of children without creating any native platform objects. The reconciler
6
- consumes these trees to determine what native views must be created,
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
- return f"Element({self.type!r}, props={set(self.props)}, children={len(self.children)})"
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):