pythonnative 0.7.0__py3-none-any.whl → 0.9.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.
@@ -1,1404 +0,0 @@
1
- """Platform-specific native view creation and update logic.
2
-
3
- This module replaces the old per-widget files. All platform-branching
4
- lives here, guarded behind lazy imports so the module can be imported
5
- on desktop for testing.
6
- """
7
-
8
- from typing import Any, Callable, Dict, Optional, Union
9
-
10
- from .utils import IS_ANDROID
11
-
12
- # ======================================================================
13
- # Abstract handler protocol
14
- # ======================================================================
15
-
16
-
17
- class ViewHandler:
18
- """Protocol for creating, updating, and managing children of a native view type."""
19
-
20
- def create(self, props: Dict[str, Any]) -> Any:
21
- raise NotImplementedError
22
-
23
- def update(self, native_view: Any, changed_props: Dict[str, Any]) -> None:
24
- raise NotImplementedError
25
-
26
- def add_child(self, parent: Any, child: Any) -> None:
27
- pass
28
-
29
- def remove_child(self, parent: Any, child: Any) -> None:
30
- pass
31
-
32
- def insert_child(self, parent: Any, child: Any, index: int) -> None:
33
- self.add_child(parent, child)
34
-
35
-
36
- # ======================================================================
37
- # Registry
38
- # ======================================================================
39
-
40
-
41
- class NativeViewRegistry:
42
- """Maps element type names to platform-specific :class:`ViewHandler` instances."""
43
-
44
- def __init__(self) -> None:
45
- self._handlers: Dict[str, ViewHandler] = {}
46
-
47
- def register(self, type_name: str, handler: ViewHandler) -> None:
48
- self._handlers[type_name] = handler
49
-
50
- def create_view(self, type_name: str, props: Dict[str, Any]) -> Any:
51
- handler = self._handlers.get(type_name)
52
- if handler is None:
53
- raise ValueError(f"Unknown element type: {type_name!r}")
54
- return handler.create(props)
55
-
56
- def update_view(self, native_view: Any, type_name: str, changed_props: Dict[str, Any]) -> None:
57
- handler = self._handlers.get(type_name)
58
- if handler is not None:
59
- handler.update(native_view, changed_props)
60
-
61
- def add_child(self, parent: Any, child: Any, parent_type: str) -> None:
62
- handler = self._handlers.get(parent_type)
63
- if handler is not None:
64
- handler.add_child(parent, child)
65
-
66
- def remove_child(self, parent: Any, child: Any, parent_type: str) -> None:
67
- handler = self._handlers.get(parent_type)
68
- if handler is not None:
69
- handler.remove_child(parent, child)
70
-
71
- def insert_child(self, parent: Any, child: Any, parent_type: str, index: int) -> None:
72
- handler = self._handlers.get(parent_type)
73
- if handler is not None:
74
- handler.insert_child(parent, child, index)
75
-
76
-
77
- # ======================================================================
78
- # Shared helpers
79
- # ======================================================================
80
-
81
-
82
- def parse_color_int(color: Union[str, int]) -> int:
83
- """Parse ``#RRGGBB`` / ``#AARRGGBB`` hex string or raw int to a *signed* ARGB int.
84
-
85
- Java's ``setBackgroundColor`` et al. expect a signed 32-bit int, so values
86
- with a high alpha byte (e.g. 0xFF…) must be converted to negative ints.
87
- """
88
- if isinstance(color, int):
89
- val = color
90
- else:
91
- c = color.strip().lstrip("#")
92
- if len(c) == 6:
93
- c = "FF" + c
94
- val = int(c, 16)
95
- if val > 0x7FFFFFFF:
96
- val -= 0x100000000
97
- return val
98
-
99
-
100
- def _resolve_padding(
101
- padding: Any,
102
- ) -> tuple:
103
- """Normalise various padding representations to ``(left, top, right, bottom)``."""
104
- if padding is None:
105
- return (0, 0, 0, 0)
106
- if isinstance(padding, (int, float)):
107
- v = int(padding)
108
- return (v, v, v, v)
109
- if isinstance(padding, dict):
110
- h = int(padding.get("horizontal", 0))
111
- v = int(padding.get("vertical", 0))
112
- left = int(padding.get("left", h))
113
- right = int(padding.get("right", h))
114
- top = int(padding.get("top", v))
115
- bottom = int(padding.get("bottom", v))
116
- a = int(padding.get("all", 0))
117
- if a:
118
- left = left or a
119
- right = right or a
120
- top = top or a
121
- bottom = bottom or a
122
- return (left, top, right, bottom)
123
- return (0, 0, 0, 0)
124
-
125
-
126
- _LAYOUT_KEYS = frozenset(
127
- {
128
- "width",
129
- "height",
130
- "flex",
131
- "margin",
132
- "min_width",
133
- "max_width",
134
- "min_height",
135
- "max_height",
136
- "align_self",
137
- }
138
- )
139
-
140
-
141
- # ======================================================================
142
- # Platform handler registration (lazy imports inside functions)
143
- # ======================================================================
144
-
145
-
146
- def _register_android_handlers(registry: NativeViewRegistry) -> None: # noqa: C901
147
- from java import dynamic_proxy, jclass
148
-
149
- from .utils import get_android_context
150
-
151
- def _ctx() -> Any:
152
- return get_android_context()
153
-
154
- def _density() -> float:
155
- return float(_ctx().getResources().getDisplayMetrics().density)
156
-
157
- def _dp(value: float) -> int:
158
- return int(value * _density())
159
-
160
- def _apply_layout(view: Any, props: Dict[str, Any]) -> None:
161
- """Apply common layout properties to an Android view."""
162
- lp = view.getLayoutParams()
163
- LayoutParams = jclass("android.widget.LinearLayout$LayoutParams")
164
- ViewGroupLP = jclass("android.view.ViewGroup$LayoutParams")
165
- needs_set = False
166
-
167
- if lp is None:
168
- lp = LayoutParams(ViewGroupLP.WRAP_CONTENT, ViewGroupLP.WRAP_CONTENT)
169
- needs_set = True
170
-
171
- if "width" in props and props["width"] is not None:
172
- lp.width = _dp(float(props["width"]))
173
- needs_set = True
174
- if "height" in props and props["height"] is not None:
175
- lp.height = _dp(float(props["height"]))
176
- needs_set = True
177
- if "flex" in props and props["flex"] is not None:
178
- try:
179
- lp.weight = float(props["flex"])
180
- needs_set = True
181
- except Exception:
182
- pass
183
- if "margin" in props and props["margin"] is not None:
184
- left, top, right, bottom = _resolve_padding(props["margin"])
185
- try:
186
- lp.setMargins(_dp(left), _dp(top), _dp(right), _dp(bottom))
187
- needs_set = True
188
- except Exception:
189
- pass
190
-
191
- if needs_set:
192
- view.setLayoutParams(lp)
193
-
194
- # ---- Text -----------------------------------------------------------
195
- class AndroidTextHandler(ViewHandler):
196
- def create(self, props: Dict[str, Any]) -> Any:
197
- tv = jclass("android.widget.TextView")(_ctx())
198
- self._apply(tv, props)
199
- _apply_layout(tv, props)
200
- return tv
201
-
202
- def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
203
- self._apply(native_view, changed)
204
- if changed.keys() & _LAYOUT_KEYS:
205
- _apply_layout(native_view, changed)
206
-
207
- def _apply(self, tv: Any, props: Dict[str, Any]) -> None:
208
- if "text" in props:
209
- tv.setText(str(props["text"]))
210
- if "font_size" in props and props["font_size"] is not None:
211
- tv.setTextSize(float(props["font_size"]))
212
- if "color" in props and props["color"] is not None:
213
- tv.setTextColor(parse_color_int(props["color"]))
214
- if "background_color" in props and props["background_color"] is not None:
215
- tv.setBackgroundColor(parse_color_int(props["background_color"]))
216
- if "bold" in props and props["bold"]:
217
- tv.setTypeface(tv.getTypeface(), 1) # Typeface.BOLD = 1
218
- if "max_lines" in props and props["max_lines"] is not None:
219
- tv.setMaxLines(int(props["max_lines"]))
220
- if "text_align" in props:
221
- Gravity = jclass("android.view.Gravity")
222
- mapping = {"left": Gravity.START, "center": Gravity.CENTER, "right": Gravity.END}
223
- tv.setGravity(mapping.get(props["text_align"], Gravity.START))
224
-
225
- # ---- Button ---------------------------------------------------------
226
- class AndroidButtonHandler(ViewHandler):
227
- def create(self, props: Dict[str, Any]) -> Any:
228
- btn = jclass("android.widget.Button")(_ctx())
229
- self._apply(btn, props)
230
- _apply_layout(btn, props)
231
- return btn
232
-
233
- def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
234
- self._apply(native_view, changed)
235
- if changed.keys() & _LAYOUT_KEYS:
236
- _apply_layout(native_view, changed)
237
-
238
- def _apply(self, btn: Any, props: Dict[str, Any]) -> None:
239
- if "title" in props:
240
- btn.setText(str(props["title"]))
241
- if "font_size" in props and props["font_size"] is not None:
242
- btn.setTextSize(float(props["font_size"]))
243
- if "color" in props and props["color"] is not None:
244
- btn.setTextColor(parse_color_int(props["color"]))
245
- if "background_color" in props and props["background_color"] is not None:
246
- btn.setBackgroundColor(parse_color_int(props["background_color"]))
247
- if "enabled" in props:
248
- btn.setEnabled(bool(props["enabled"]))
249
- if "on_click" in props:
250
- cb = props["on_click"]
251
- if cb is not None:
252
-
253
- class ClickProxy(dynamic_proxy(jclass("android.view.View").OnClickListener)):
254
- def __init__(self, callback: Callable[[], None]) -> None:
255
- super().__init__()
256
- self.callback = callback
257
-
258
- def onClick(self, view: Any) -> None:
259
- self.callback()
260
-
261
- btn.setOnClickListener(ClickProxy(cb))
262
- else:
263
- btn.setOnClickListener(None)
264
-
265
- # ---- Column (vertical LinearLayout) ---------------------------------
266
- class AndroidColumnHandler(ViewHandler):
267
- def create(self, props: Dict[str, Any]) -> Any:
268
- ll = jclass("android.widget.LinearLayout")(_ctx())
269
- ll.setOrientation(jclass("android.widget.LinearLayout").VERTICAL)
270
- self._apply(ll, props)
271
- _apply_layout(ll, props)
272
- return ll
273
-
274
- def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
275
- self._apply(native_view, changed)
276
- if changed.keys() & _LAYOUT_KEYS:
277
- _apply_layout(native_view, changed)
278
-
279
- def _apply(self, ll: Any, props: Dict[str, Any]) -> None:
280
- Gravity = jclass("android.view.Gravity")
281
- if "spacing" in props and props["spacing"]:
282
- px = _dp(float(props["spacing"]))
283
- GradientDrawable = jclass("android.graphics.drawable.GradientDrawable")
284
- d = GradientDrawable()
285
- d.setColor(0x00000000)
286
- d.setSize(1, px)
287
- ll.setShowDividers(jclass("android.widget.LinearLayout").SHOW_DIVIDER_MIDDLE)
288
- ll.setDividerDrawable(d)
289
- if "padding" in props:
290
- left, top, right, bottom = _resolve_padding(props["padding"])
291
- ll.setPadding(_dp(left), _dp(top), _dp(right), _dp(bottom))
292
- gravity = 0
293
- ai = props.get("align_items") or props.get("alignment")
294
- if ai:
295
- cross_map = {
296
- "stretch": Gravity.FILL_HORIZONTAL,
297
- "fill": Gravity.FILL_HORIZONTAL,
298
- "flex_start": Gravity.START,
299
- "leading": Gravity.START,
300
- "start": Gravity.START,
301
- "center": Gravity.CENTER_HORIZONTAL,
302
- "flex_end": Gravity.END,
303
- "trailing": Gravity.END,
304
- "end": Gravity.END,
305
- }
306
- gravity |= cross_map.get(ai, 0)
307
- jc = props.get("justify_content")
308
- if jc:
309
- main_map = {
310
- "flex_start": Gravity.TOP,
311
- "center": Gravity.CENTER_VERTICAL,
312
- "flex_end": Gravity.BOTTOM,
313
- }
314
- gravity |= main_map.get(jc, 0)
315
- if gravity:
316
- ll.setGravity(gravity)
317
- if "background_color" in props and props["background_color"] is not None:
318
- ll.setBackgroundColor(parse_color_int(props["background_color"]))
319
-
320
- def add_child(self, parent: Any, child: Any) -> None:
321
- parent.addView(child)
322
-
323
- def remove_child(self, parent: Any, child: Any) -> None:
324
- parent.removeView(child)
325
-
326
- def insert_child(self, parent: Any, child: Any, index: int) -> None:
327
- parent.addView(child, index)
328
-
329
- # ---- Row (horizontal LinearLayout) ----------------------------------
330
- class AndroidRowHandler(ViewHandler):
331
- def create(self, props: Dict[str, Any]) -> Any:
332
- ll = jclass("android.widget.LinearLayout")(_ctx())
333
- ll.setOrientation(jclass("android.widget.LinearLayout").HORIZONTAL)
334
- self._apply(ll, props)
335
- _apply_layout(ll, props)
336
- return ll
337
-
338
- def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
339
- self._apply(native_view, changed)
340
- if changed.keys() & _LAYOUT_KEYS:
341
- _apply_layout(native_view, changed)
342
-
343
- def _apply(self, ll: Any, props: Dict[str, Any]) -> None:
344
- Gravity = jclass("android.view.Gravity")
345
- if "spacing" in props and props["spacing"]:
346
- px = _dp(float(props["spacing"]))
347
- GradientDrawable = jclass("android.graphics.drawable.GradientDrawable")
348
- d = GradientDrawable()
349
- d.setColor(0x00000000)
350
- d.setSize(px, 1)
351
- ll.setShowDividers(jclass("android.widget.LinearLayout").SHOW_DIVIDER_MIDDLE)
352
- ll.setDividerDrawable(d)
353
- if "padding" in props:
354
- left, top, right, bottom = _resolve_padding(props["padding"])
355
- ll.setPadding(_dp(left), _dp(top), _dp(right), _dp(bottom))
356
- gravity = 0
357
- ai = props.get("align_items") or props.get("alignment")
358
- if ai:
359
- cross_map = {
360
- "stretch": Gravity.FILL_VERTICAL,
361
- "fill": Gravity.FILL_VERTICAL,
362
- "flex_start": Gravity.TOP,
363
- "top": Gravity.TOP,
364
- "center": Gravity.CENTER_VERTICAL,
365
- "flex_end": Gravity.BOTTOM,
366
- "bottom": Gravity.BOTTOM,
367
- }
368
- gravity |= cross_map.get(ai, 0)
369
- jc = props.get("justify_content")
370
- if jc:
371
- main_map = {
372
- "flex_start": Gravity.START,
373
- "center": Gravity.CENTER_HORIZONTAL,
374
- "flex_end": Gravity.END,
375
- }
376
- gravity |= main_map.get(jc, 0)
377
- if gravity:
378
- ll.setGravity(gravity)
379
- if "background_color" in props and props["background_color"] is not None:
380
- ll.setBackgroundColor(parse_color_int(props["background_color"]))
381
-
382
- def add_child(self, parent: Any, child: Any) -> None:
383
- parent.addView(child)
384
-
385
- def remove_child(self, parent: Any, child: Any) -> None:
386
- parent.removeView(child)
387
-
388
- def insert_child(self, parent: Any, child: Any, index: int) -> None:
389
- parent.addView(child, index)
390
-
391
- # ---- ScrollView -----------------------------------------------------
392
- class AndroidScrollViewHandler(ViewHandler):
393
- def create(self, props: Dict[str, Any]) -> Any:
394
- sv = jclass("android.widget.ScrollView")(_ctx())
395
- if "background_color" in props and props["background_color"] is not None:
396
- sv.setBackgroundColor(parse_color_int(props["background_color"]))
397
- _apply_layout(sv, props)
398
- return sv
399
-
400
- def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
401
- if "background_color" in changed and changed["background_color"] is not None:
402
- native_view.setBackgroundColor(parse_color_int(changed["background_color"]))
403
- if changed.keys() & _LAYOUT_KEYS:
404
- _apply_layout(native_view, changed)
405
-
406
- def add_child(self, parent: Any, child: Any) -> None:
407
- parent.addView(child)
408
-
409
- def remove_child(self, parent: Any, child: Any) -> None:
410
- parent.removeView(child)
411
-
412
- # ---- TextInput (EditText) with on_change ----------------------------
413
- class AndroidTextInputHandler(ViewHandler):
414
- def create(self, props: Dict[str, Any]) -> Any:
415
- et = jclass("android.widget.EditText")(_ctx())
416
- self._apply(et, props)
417
- _apply_layout(et, props)
418
- return et
419
-
420
- def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
421
- self._apply(native_view, changed)
422
- if changed.keys() & _LAYOUT_KEYS:
423
- _apply_layout(native_view, changed)
424
-
425
- def _apply(self, et: Any, props: Dict[str, Any]) -> None:
426
- if "value" in props:
427
- et.setText(str(props["value"]))
428
- if "placeholder" in props:
429
- et.setHint(str(props["placeholder"]))
430
- if "font_size" in props and props["font_size"] is not None:
431
- et.setTextSize(float(props["font_size"]))
432
- if "color" in props and props["color"] is not None:
433
- et.setTextColor(parse_color_int(props["color"]))
434
- if "background_color" in props and props["background_color"] is not None:
435
- et.setBackgroundColor(parse_color_int(props["background_color"]))
436
- if "secure" in props and props["secure"]:
437
- InputType = jclass("android.text.InputType")
438
- et.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD)
439
- if "on_change" in props:
440
- cb = props["on_change"]
441
- if cb is not None:
442
- TextWatcher = jclass("android.text.TextWatcher")
443
-
444
- class ChangeProxy(dynamic_proxy(TextWatcher)):
445
- def __init__(self, callback: Callable[[str], None]) -> None:
446
- super().__init__()
447
- self.callback = callback
448
-
449
- def afterTextChanged(self, s: Any) -> None:
450
- self.callback(str(s))
451
-
452
- def beforeTextChanged(self, s: Any, start: int, count: int, after: int) -> None:
453
- pass
454
-
455
- def onTextChanged(self, s: Any, start: int, before: int, count: int) -> None:
456
- pass
457
-
458
- et.addTextChangedListener(ChangeProxy(cb))
459
-
460
- # ---- Image (with URL loading) ---------------------------------------
461
- class AndroidImageHandler(ViewHandler):
462
- def create(self, props: Dict[str, Any]) -> Any:
463
- iv = jclass("android.widget.ImageView")(_ctx())
464
- self._apply(iv, props)
465
- _apply_layout(iv, props)
466
- return iv
467
-
468
- def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
469
- self._apply(native_view, changed)
470
- if changed.keys() & _LAYOUT_KEYS:
471
- _apply_layout(native_view, changed)
472
-
473
- def _apply(self, iv: Any, props: Dict[str, Any]) -> None:
474
- if "background_color" in props and props["background_color"] is not None:
475
- iv.setBackgroundColor(parse_color_int(props["background_color"]))
476
- if "source" in props and props["source"]:
477
- self._load_source(iv, props["source"])
478
- if "scale_type" in props and props["scale_type"]:
479
- ScaleType = jclass("android.widget.ImageView$ScaleType")
480
- mapping = {
481
- "cover": ScaleType.CENTER_CROP,
482
- "contain": ScaleType.FIT_CENTER,
483
- "stretch": ScaleType.FIT_XY,
484
- "center": ScaleType.CENTER,
485
- }
486
- st = mapping.get(props["scale_type"])
487
- if st:
488
- iv.setScaleType(st)
489
-
490
- def _load_source(self, iv: Any, source: str) -> None:
491
- try:
492
- if source.startswith(("http://", "https://")):
493
- Thread = jclass("java.lang.Thread")
494
- Runnable = jclass("java.lang.Runnable")
495
- URL = jclass("java.net.URL")
496
- BitmapFactory = jclass("android.graphics.BitmapFactory")
497
- Handler = jclass("android.os.Handler")
498
- Looper = jclass("android.os.Looper")
499
- handler = Handler(Looper.getMainLooper())
500
-
501
- class LoadTask(dynamic_proxy(Runnable)):
502
- def __init__(self, image_view: Any, url_str: str, main_handler: Any) -> None:
503
- super().__init__()
504
- self.image_view = image_view
505
- self.url_str = url_str
506
- self.main_handler = main_handler
507
-
508
- def run(self) -> None:
509
- try:
510
- url = URL(self.url_str)
511
- stream = url.openStream()
512
- bitmap = BitmapFactory.decodeStream(stream)
513
- stream.close()
514
-
515
- class SetImage(dynamic_proxy(Runnable)):
516
- def __init__(self, view: Any, bmp: Any) -> None:
517
- super().__init__()
518
- self.view = view
519
- self.bmp = bmp
520
-
521
- def run(self) -> None:
522
- self.view.setImageBitmap(self.bmp)
523
-
524
- self.main_handler.post(SetImage(self.image_view, bitmap))
525
- except Exception:
526
- pass
527
-
528
- Thread(LoadTask(iv, source, handler)).start()
529
- else:
530
- ctx = _ctx()
531
- res = ctx.getResources()
532
- pkg = ctx.getPackageName()
533
- res_name = source.rsplit(".", 1)[0] if "." in source else source
534
- res_id = res.getIdentifier(res_name, "drawable", pkg)
535
- if res_id != 0:
536
- iv.setImageResource(res_id)
537
- except Exception:
538
- pass
539
-
540
- # ---- Switch (with on_change) ----------------------------------------
541
- class AndroidSwitchHandler(ViewHandler):
542
- def create(self, props: Dict[str, Any]) -> Any:
543
- sw = jclass("android.widget.Switch")(_ctx())
544
- self._apply(sw, props)
545
- _apply_layout(sw, props)
546
- return sw
547
-
548
- def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
549
- self._apply(native_view, changed)
550
-
551
- def _apply(self, sw: Any, props: Dict[str, Any]) -> None:
552
- if "value" in props:
553
- sw.setChecked(bool(props["value"]))
554
- if "on_change" in props and props["on_change"] is not None:
555
- cb = props["on_change"]
556
-
557
- class CheckedProxy(dynamic_proxy(jclass("android.widget.CompoundButton").OnCheckedChangeListener)):
558
- def __init__(self, callback: Callable[[bool], None]) -> None:
559
- super().__init__()
560
- self.callback = callback
561
-
562
- def onCheckedChanged(self, button: Any, checked: bool) -> None:
563
- self.callback(checked)
564
-
565
- sw.setOnCheckedChangeListener(CheckedProxy(cb))
566
-
567
- # ---- ProgressBar ----------------------------------------------------
568
- class AndroidProgressBarHandler(ViewHandler):
569
- def create(self, props: Dict[str, Any]) -> Any:
570
- style = jclass("android.R$attr").progressBarStyleHorizontal
571
- pb = jclass("android.widget.ProgressBar")(_ctx(), None, 0, style)
572
- pb.setMax(1000)
573
- self._apply(pb, props)
574
- _apply_layout(pb, props)
575
- return pb
576
-
577
- def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
578
- self._apply(native_view, changed)
579
-
580
- def _apply(self, pb: Any, props: Dict[str, Any]) -> None:
581
- if "value" in props:
582
- pb.setProgress(int(float(props["value"]) * 1000))
583
-
584
- # ---- ActivityIndicator (circular ProgressBar) -----------------------
585
- class AndroidActivityIndicatorHandler(ViewHandler):
586
- def create(self, props: Dict[str, Any]) -> Any:
587
- pb = jclass("android.widget.ProgressBar")(_ctx())
588
- if not props.get("animating", True):
589
- pb.setVisibility(jclass("android.view.View").GONE)
590
- _apply_layout(pb, props)
591
- return pb
592
-
593
- def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
594
- View = jclass("android.view.View")
595
- if "animating" in changed:
596
- native_view.setVisibility(View.VISIBLE if changed["animating"] else View.GONE)
597
-
598
- # ---- WebView --------------------------------------------------------
599
- class AndroidWebViewHandler(ViewHandler):
600
- def create(self, props: Dict[str, Any]) -> Any:
601
- wv = jclass("android.webkit.WebView")(_ctx())
602
- if "url" in props and props["url"]:
603
- wv.loadUrl(str(props["url"]))
604
- _apply_layout(wv, props)
605
- return wv
606
-
607
- def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
608
- if "url" in changed and changed["url"]:
609
- native_view.loadUrl(str(changed["url"]))
610
-
611
- # ---- Spacer ---------------------------------------------------------
612
- class AndroidSpacerHandler(ViewHandler):
613
- def create(self, props: Dict[str, Any]) -> Any:
614
- v = jclass("android.view.View")(_ctx())
615
- if "size" in props and props["size"] is not None:
616
- px = _dp(float(props["size"]))
617
- lp = jclass("android.widget.LinearLayout$LayoutParams")(px, px)
618
- v.setLayoutParams(lp)
619
- if "flex" in props and props["flex"] is not None:
620
- lp = v.getLayoutParams()
621
- if lp is None:
622
- lp = jclass("android.widget.LinearLayout$LayoutParams")(0, 0)
623
- lp.weight = float(props["flex"])
624
- v.setLayoutParams(lp)
625
- return v
626
-
627
- def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
628
- if "size" in changed and changed["size"] is not None:
629
- px = _dp(float(changed["size"]))
630
- lp = jclass("android.widget.LinearLayout$LayoutParams")(px, px)
631
- native_view.setLayoutParams(lp)
632
-
633
- # ---- View (generic container FrameLayout) ---------------------------
634
- class AndroidViewHandler(ViewHandler):
635
- def create(self, props: Dict[str, Any]) -> Any:
636
- fl = jclass("android.widget.FrameLayout")(_ctx())
637
- if "background_color" in props and props["background_color"] is not None:
638
- fl.setBackgroundColor(parse_color_int(props["background_color"]))
639
- if "padding" in props:
640
- left, top, right, bottom = _resolve_padding(props["padding"])
641
- fl.setPadding(_dp(left), _dp(top), _dp(right), _dp(bottom))
642
- _apply_layout(fl, props)
643
- return fl
644
-
645
- def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
646
- if "background_color" in changed and changed["background_color"] is not None:
647
- native_view.setBackgroundColor(parse_color_int(changed["background_color"]))
648
- if "padding" in changed:
649
- left, top, right, bottom = _resolve_padding(changed["padding"])
650
- native_view.setPadding(_dp(left), _dp(top), _dp(right), _dp(bottom))
651
- if changed.keys() & _LAYOUT_KEYS:
652
- _apply_layout(native_view, changed)
653
-
654
- def add_child(self, parent: Any, child: Any) -> None:
655
- parent.addView(child)
656
-
657
- def remove_child(self, parent: Any, child: Any) -> None:
658
- parent.removeView(child)
659
-
660
- def insert_child(self, parent: Any, child: Any, index: int) -> None:
661
- parent.addView(child, index)
662
-
663
- # ---- SafeAreaView (FrameLayout with fitsSystemWindows) ---------------
664
- class AndroidSafeAreaViewHandler(ViewHandler):
665
- def create(self, props: Dict[str, Any]) -> Any:
666
- fl = jclass("android.widget.FrameLayout")(_ctx())
667
- fl.setFitsSystemWindows(True)
668
- if "background_color" in props and props["background_color"] is not None:
669
- fl.setBackgroundColor(parse_color_int(props["background_color"]))
670
- if "padding" in props:
671
- left, top, right, bottom = _resolve_padding(props["padding"])
672
- fl.setPadding(_dp(left), _dp(top), _dp(right), _dp(bottom))
673
- return fl
674
-
675
- def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
676
- if "background_color" in changed and changed["background_color"] is not None:
677
- native_view.setBackgroundColor(parse_color_int(changed["background_color"]))
678
-
679
- def add_child(self, parent: Any, child: Any) -> None:
680
- parent.addView(child)
681
-
682
- def remove_child(self, parent: Any, child: Any) -> None:
683
- parent.removeView(child)
684
-
685
- # ---- Modal (AlertDialog) -------------------------------------------
686
- class AndroidModalHandler(ViewHandler):
687
- def create(self, props: Dict[str, Any]) -> Any:
688
- placeholder = jclass("android.view.View")(_ctx())
689
- placeholder.setVisibility(jclass("android.view.View").GONE)
690
- return placeholder
691
-
692
- def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
693
- pass
694
-
695
- def add_child(self, parent: Any, child: Any) -> None:
696
- pass
697
-
698
- # ---- Slider (SeekBar) -----------------------------------------------
699
- class AndroidSliderHandler(ViewHandler):
700
- def create(self, props: Dict[str, Any]) -> Any:
701
- sb = jclass("android.widget.SeekBar")(_ctx())
702
- sb.setMax(1000)
703
- self._apply(sb, props)
704
- _apply_layout(sb, props)
705
- return sb
706
-
707
- def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
708
- self._apply(native_view, changed)
709
-
710
- def _apply(self, sb: Any, props: Dict[str, Any]) -> None:
711
- min_val = float(props.get("min_value", 0))
712
- max_val = float(props.get("max_value", 1))
713
- rng = max_val - min_val if max_val != min_val else 1
714
- if "value" in props:
715
- normalized = (float(props["value"]) - min_val) / rng
716
- sb.setProgress(int(normalized * 1000))
717
- if "on_change" in props and props["on_change"] is not None:
718
- cb = props["on_change"]
719
-
720
- class SeekProxy(dynamic_proxy(jclass("android.widget.SeekBar").OnSeekBarChangeListener)):
721
- def __init__(self, callback: Callable[[float], None], mn: float, rn: float) -> None:
722
- super().__init__()
723
- self.callback = callback
724
- self.mn = mn
725
- self.rn = rn
726
-
727
- def onProgressChanged(self, seekBar: Any, progress: int, fromUser: bool) -> None:
728
- if fromUser:
729
- self.callback(self.mn + (progress / 1000.0) * self.rn)
730
-
731
- def onStartTrackingTouch(self, seekBar: Any) -> None:
732
- pass
733
-
734
- def onStopTrackingTouch(self, seekBar: Any) -> None:
735
- pass
736
-
737
- sb.setOnSeekBarChangeListener(SeekProxy(cb, min_val, rng))
738
-
739
- # ---- Pressable (FrameLayout with click listener) --------------------
740
- class AndroidPressableHandler(ViewHandler):
741
- def create(self, props: Dict[str, Any]) -> Any:
742
- fl = jclass("android.widget.FrameLayout")(_ctx())
743
- fl.setClickable(True)
744
- self._apply(fl, props)
745
- return fl
746
-
747
- def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
748
- self._apply(native_view, changed)
749
-
750
- def _apply(self, fl: Any, props: Dict[str, Any]) -> None:
751
- if "on_press" in props and props["on_press"] is not None:
752
- cb = props["on_press"]
753
-
754
- class PressProxy(dynamic_proxy(jclass("android.view.View").OnClickListener)):
755
- def __init__(self, callback: Callable[[], None]) -> None:
756
- super().__init__()
757
- self.callback = callback
758
-
759
- def onClick(self, view: Any) -> None:
760
- self.callback()
761
-
762
- fl.setOnClickListener(PressProxy(cb))
763
- if "on_long_press" in props and props["on_long_press"] is not None:
764
- cb = props["on_long_press"]
765
-
766
- class LongPressProxy(dynamic_proxy(jclass("android.view.View").OnLongClickListener)):
767
- def __init__(self, callback: Callable[[], None]) -> None:
768
- super().__init__()
769
- self.callback = callback
770
-
771
- def onLongClick(self, view: Any) -> bool:
772
- self.callback()
773
- return True
774
-
775
- fl.setOnLongClickListener(LongPressProxy(cb))
776
-
777
- def add_child(self, parent: Any, child: Any) -> None:
778
- parent.addView(child)
779
-
780
- def remove_child(self, parent: Any, child: Any) -> None:
781
- parent.removeView(child)
782
-
783
- registry.register("Text", AndroidTextHandler())
784
- registry.register("Button", AndroidButtonHandler())
785
- registry.register("Column", AndroidColumnHandler())
786
- registry.register("Row", AndroidRowHandler())
787
- registry.register("ScrollView", AndroidScrollViewHandler())
788
- registry.register("TextInput", AndroidTextInputHandler())
789
- registry.register("Image", AndroidImageHandler())
790
- registry.register("Switch", AndroidSwitchHandler())
791
- registry.register("ProgressBar", AndroidProgressBarHandler())
792
- registry.register("ActivityIndicator", AndroidActivityIndicatorHandler())
793
- registry.register("WebView", AndroidWebViewHandler())
794
- registry.register("Spacer", AndroidSpacerHandler())
795
- registry.register("View", AndroidViewHandler())
796
- registry.register("SafeAreaView", AndroidSafeAreaViewHandler())
797
- registry.register("Modal", AndroidModalHandler())
798
- registry.register("Slider", AndroidSliderHandler())
799
- registry.register("Pressable", AndroidPressableHandler())
800
-
801
-
802
- def _register_ios_handlers(registry: NativeViewRegistry) -> None: # noqa: C901
803
- from rubicon.objc import SEL, ObjCClass, objc_method
804
-
805
- NSObject = ObjCClass("NSObject")
806
- UIColor = ObjCClass("UIColor")
807
- UIFont = ObjCClass("UIFont")
808
-
809
- def _uicolor(color: Any) -> Any:
810
- argb = parse_color_int(color)
811
- if argb < 0:
812
- argb += 0x100000000
813
- a = ((argb >> 24) & 0xFF) / 255.0
814
- r = ((argb >> 16) & 0xFF) / 255.0
815
- g = ((argb >> 8) & 0xFF) / 255.0
816
- b = (argb & 0xFF) / 255.0
817
- return UIColor.colorWithRed_green_blue_alpha_(r, g, b, a)
818
-
819
- def _apply_ios_layout(view: Any, props: Dict[str, Any]) -> None:
820
- """Apply common layout constraints to an iOS view."""
821
- if "width" in props and props["width"] is not None:
822
- try:
823
- for c in list(view.constraints or []):
824
- if c.firstAttribute == 7: # NSLayoutAttributeWidth
825
- c.setActive_(False)
826
- view.widthAnchor.constraintEqualToConstant_(float(props["width"])).setActive_(True)
827
- except Exception:
828
- pass
829
- if "height" in props and props["height"] is not None:
830
- try:
831
- for c in list(view.constraints or []):
832
- if c.firstAttribute == 8: # NSLayoutAttributeHeight
833
- c.setActive_(False)
834
- view.heightAnchor.constraintEqualToConstant_(float(props["height"])).setActive_(True)
835
- except Exception:
836
- pass
837
-
838
- # ---- Text -----------------------------------------------------------
839
- class IOSTextHandler(ViewHandler):
840
- def create(self, props: Dict[str, Any]) -> Any:
841
- label = ObjCClass("UILabel").alloc().init()
842
- self._apply(label, props)
843
- _apply_ios_layout(label, props)
844
- return label
845
-
846
- def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
847
- self._apply(native_view, changed)
848
- if changed.keys() & _LAYOUT_KEYS:
849
- _apply_ios_layout(native_view, changed)
850
-
851
- def _apply(self, label: Any, props: Dict[str, Any]) -> None:
852
- if "text" in props:
853
- label.setText_(str(props["text"]))
854
- if "font_size" in props and props["font_size"] is not None:
855
- if props.get("bold"):
856
- label.setFont_(UIFont.boldSystemFontOfSize_(float(props["font_size"])))
857
- else:
858
- label.setFont_(UIFont.systemFontOfSize_(float(props["font_size"])))
859
- elif "bold" in props and props["bold"]:
860
- size = label.font().pointSize() if label.font() else 17.0
861
- label.setFont_(UIFont.boldSystemFontOfSize_(size))
862
- if "color" in props and props["color"] is not None:
863
- label.setTextColor_(_uicolor(props["color"]))
864
- if "background_color" in props and props["background_color"] is not None:
865
- label.setBackgroundColor_(_uicolor(props["background_color"]))
866
- if "max_lines" in props and props["max_lines"] is not None:
867
- label.setNumberOfLines_(int(props["max_lines"]))
868
- if "text_align" in props:
869
- mapping = {"left": 0, "center": 1, "right": 2}
870
- label.setTextAlignment_(mapping.get(props["text_align"], 0))
871
-
872
- # ---- Button ---------------------------------------------------------
873
-
874
- _pn_btn_handler_map: dict = {}
875
-
876
- class _PNButtonTarget(NSObject): # type: ignore[valid-type]
877
- _callback: Optional[Callable[[], None]] = None
878
-
879
- @objc_method
880
- def onTap_(self, sender: object) -> None:
881
- if self._callback is not None:
882
- self._callback()
883
-
884
- _pn_retained_views: list = []
885
-
886
- class IOSButtonHandler(ViewHandler):
887
- def create(self, props: Dict[str, Any]) -> Any:
888
- btn = ObjCClass("UIButton").alloc().init()
889
- btn.retain()
890
- _pn_retained_views.append(btn)
891
- _ios_blue = UIColor.colorWithRed_green_blue_alpha_(0.0, 0.478, 1.0, 1.0)
892
- btn.setTitleColor_forState_(_ios_blue, 0)
893
- self._apply(btn, props)
894
- _apply_ios_layout(btn, props)
895
- return btn
896
-
897
- def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
898
- self._apply(native_view, changed)
899
- if changed.keys() & _LAYOUT_KEYS:
900
- _apply_ios_layout(native_view, changed)
901
-
902
- def _apply(self, btn: Any, props: Dict[str, Any]) -> None:
903
- if "title" in props:
904
- btn.setTitle_forState_(str(props["title"]), 0)
905
- if "font_size" in props and props["font_size"] is not None:
906
- btn.titleLabel().setFont_(UIFont.systemFontOfSize_(float(props["font_size"])))
907
- if "background_color" in props and props["background_color"] is not None:
908
- btn.setBackgroundColor_(_uicolor(props["background_color"]))
909
- if "color" not in props:
910
- _white = UIColor.colorWithRed_green_blue_alpha_(1.0, 1.0, 1.0, 1.0)
911
- btn.setTitleColor_forState_(_white, 0)
912
- if "color" in props and props["color"] is not None:
913
- btn.setTitleColor_forState_(_uicolor(props["color"]), 0)
914
- if "enabled" in props:
915
- btn.setEnabled_(bool(props["enabled"]))
916
- if "on_click" in props:
917
- existing = _pn_btn_handler_map.get(id(btn))
918
- if existing is not None:
919
- existing._callback = props["on_click"]
920
- else:
921
- handler = _PNButtonTarget.new()
922
- handler._callback = props["on_click"]
923
- _pn_btn_handler_map[id(btn)] = handler
924
- btn.addTarget_action_forControlEvents_(handler, SEL("onTap:"), 1 << 6)
925
-
926
- # ---- Column (vertical UIStackView) ----------------------------------
927
- class IOSColumnHandler(ViewHandler):
928
- def create(self, props: Dict[str, Any]) -> Any:
929
- sv = ObjCClass("UIStackView").alloc().initWithFrame_(((0, 0), (0, 0)))
930
- sv.setAxis_(1) # vertical
931
- self._apply(sv, props)
932
- _apply_ios_layout(sv, props)
933
- return sv
934
-
935
- def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
936
- self._apply(native_view, changed)
937
- if changed.keys() & _LAYOUT_KEYS:
938
- _apply_ios_layout(native_view, changed)
939
-
940
- def _apply(self, sv: Any, props: Dict[str, Any]) -> None:
941
- if "spacing" in props and props["spacing"]:
942
- sv.setSpacing_(float(props["spacing"]))
943
- ai = props.get("align_items") or props.get("alignment")
944
- if ai:
945
- alignment_map = {
946
- "stretch": 0,
947
- "fill": 0,
948
- "flex_start": 1,
949
- "leading": 1,
950
- "center": 3,
951
- "flex_end": 4,
952
- "trailing": 4,
953
- }
954
- sv.setAlignment_(alignment_map.get(ai, 0))
955
- jc = props.get("justify_content")
956
- if jc:
957
- distribution_map = {
958
- "flex_start": 0,
959
- "center": 0,
960
- "flex_end": 0,
961
- "space_between": 3,
962
- "space_around": 4,
963
- "space_evenly": 4,
964
- }
965
- sv.setDistribution_(distribution_map.get(jc, 0))
966
- if "background_color" in props and props["background_color"] is not None:
967
- sv.setBackgroundColor_(_uicolor(props["background_color"]))
968
- if "padding" in props:
969
- left, top, right, bottom = _resolve_padding(props["padding"])
970
- sv.setLayoutMarginsRelativeArrangement_(True)
971
- try:
972
- sv.setDirectionalLayoutMargins_((top, left, bottom, right))
973
- except Exception:
974
- sv.setLayoutMargins_((top, left, bottom, right))
975
-
976
- def add_child(self, parent: Any, child: Any) -> None:
977
- parent.addArrangedSubview_(child)
978
-
979
- def remove_child(self, parent: Any, child: Any) -> None:
980
- parent.removeArrangedSubview_(child)
981
- child.removeFromSuperview()
982
-
983
- def insert_child(self, parent: Any, child: Any, index: int) -> None:
984
- parent.insertArrangedSubview_atIndex_(child, index)
985
-
986
- # ---- Row (horizontal UIStackView) -----------------------------------
987
- class IOSRowHandler(ViewHandler):
988
- def create(self, props: Dict[str, Any]) -> Any:
989
- sv = ObjCClass("UIStackView").alloc().initWithFrame_(((0, 0), (0, 0)))
990
- sv.setAxis_(0) # horizontal
991
- self._apply(sv, props)
992
- _apply_ios_layout(sv, props)
993
- return sv
994
-
995
- def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
996
- self._apply(native_view, changed)
997
- if changed.keys() & _LAYOUT_KEYS:
998
- _apply_ios_layout(native_view, changed)
999
-
1000
- def _apply(self, sv: Any, props: Dict[str, Any]) -> None:
1001
- if "spacing" in props and props["spacing"]:
1002
- sv.setSpacing_(float(props["spacing"]))
1003
- ai = props.get("align_items") or props.get("alignment")
1004
- if ai:
1005
- alignment_map = {
1006
- "stretch": 0,
1007
- "fill": 0,
1008
- "flex_start": 1,
1009
- "top": 1,
1010
- "center": 3,
1011
- "flex_end": 4,
1012
- "bottom": 4,
1013
- }
1014
- sv.setAlignment_(alignment_map.get(ai, 0))
1015
- jc = props.get("justify_content")
1016
- if jc:
1017
- distribution_map = {
1018
- "flex_start": 0,
1019
- "center": 0,
1020
- "flex_end": 0,
1021
- "space_between": 3,
1022
- "space_around": 4,
1023
- "space_evenly": 4,
1024
- }
1025
- sv.setDistribution_(distribution_map.get(jc, 0))
1026
- if "background_color" in props and props["background_color"] is not None:
1027
- sv.setBackgroundColor_(_uicolor(props["background_color"]))
1028
-
1029
- def add_child(self, parent: Any, child: Any) -> None:
1030
- parent.addArrangedSubview_(child)
1031
-
1032
- def remove_child(self, parent: Any, child: Any) -> None:
1033
- parent.removeArrangedSubview_(child)
1034
- child.removeFromSuperview()
1035
-
1036
- def insert_child(self, parent: Any, child: Any, index: int) -> None:
1037
- parent.insertArrangedSubview_atIndex_(child, index)
1038
-
1039
- # ---- ScrollView -----------------------------------------------------
1040
- class IOSScrollViewHandler(ViewHandler):
1041
- def create(self, props: Dict[str, Any]) -> Any:
1042
- sv = ObjCClass("UIScrollView").alloc().init()
1043
- if "background_color" in props and props["background_color"] is not None:
1044
- sv.setBackgroundColor_(_uicolor(props["background_color"]))
1045
- _apply_ios_layout(sv, props)
1046
- return sv
1047
-
1048
- def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
1049
- if "background_color" in changed and changed["background_color"] is not None:
1050
- native_view.setBackgroundColor_(_uicolor(changed["background_color"]))
1051
-
1052
- def add_child(self, parent: Any, child: Any) -> None:
1053
- child.setTranslatesAutoresizingMaskIntoConstraints_(False)
1054
- parent.addSubview_(child)
1055
- content_guide = parent.contentLayoutGuide
1056
- frame_guide = parent.frameLayoutGuide
1057
- child.topAnchor.constraintEqualToAnchor_(content_guide.topAnchor).setActive_(True)
1058
- child.leadingAnchor.constraintEqualToAnchor_(content_guide.leadingAnchor).setActive_(True)
1059
- child.trailingAnchor.constraintEqualToAnchor_(content_guide.trailingAnchor).setActive_(True)
1060
- child.bottomAnchor.constraintEqualToAnchor_(content_guide.bottomAnchor).setActive_(True)
1061
- child.widthAnchor.constraintEqualToAnchor_(frame_guide.widthAnchor).setActive_(True)
1062
-
1063
- def remove_child(self, parent: Any, child: Any) -> None:
1064
- child.removeFromSuperview()
1065
-
1066
- # ---- TextInput (UITextField with on_change) -------------------------
1067
- _pn_tf_handler_map: dict = {}
1068
-
1069
- class _PNTextFieldTarget(NSObject): # type: ignore[valid-type]
1070
- _callback: Optional[Callable[[str], None]] = None
1071
-
1072
- @objc_method
1073
- def onEdit_(self, sender: object) -> None:
1074
- if self._callback is not None:
1075
- try:
1076
- text = str(sender.text) if sender and hasattr(sender, "text") else ""
1077
- self._callback(text)
1078
- except Exception:
1079
- pass
1080
-
1081
- class IOSTextInputHandler(ViewHandler):
1082
- def create(self, props: Dict[str, Any]) -> Any:
1083
- tf = ObjCClass("UITextField").alloc().init()
1084
- tf.setBorderStyle_(2) # RoundedRect
1085
- self._apply(tf, props)
1086
- _apply_ios_layout(tf, props)
1087
- return tf
1088
-
1089
- def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
1090
- self._apply(native_view, changed)
1091
- if changed.keys() & _LAYOUT_KEYS:
1092
- _apply_ios_layout(native_view, changed)
1093
-
1094
- def _apply(self, tf: Any, props: Dict[str, Any]) -> None:
1095
- if "value" in props:
1096
- tf.setText_(str(props["value"]))
1097
- if "placeholder" in props:
1098
- tf.setPlaceholder_(str(props["placeholder"]))
1099
- if "font_size" in props and props["font_size"] is not None:
1100
- tf.setFont_(UIFont.systemFontOfSize_(float(props["font_size"])))
1101
- if "color" in props and props["color"] is not None:
1102
- tf.setTextColor_(_uicolor(props["color"]))
1103
- if "background_color" in props and props["background_color"] is not None:
1104
- tf.setBackgroundColor_(_uicolor(props["background_color"]))
1105
- if "secure" in props and props["secure"]:
1106
- tf.setSecureTextEntry_(True)
1107
- if "on_change" in props:
1108
- existing = _pn_tf_handler_map.get(id(tf))
1109
- if existing is not None:
1110
- existing._callback = props["on_change"]
1111
- else:
1112
- handler = _PNTextFieldTarget.new()
1113
- handler._callback = props["on_change"]
1114
- _pn_tf_handler_map[id(tf)] = handler
1115
- tf.addTarget_action_forControlEvents_(handler, SEL("onEdit:"), 1 << 17)
1116
-
1117
- # ---- Image (with URL loading) ---------------------------------------
1118
- class IOSImageHandler(ViewHandler):
1119
- def create(self, props: Dict[str, Any]) -> Any:
1120
- iv = ObjCClass("UIImageView").alloc().init()
1121
- self._apply(iv, props)
1122
- _apply_ios_layout(iv, props)
1123
- return iv
1124
-
1125
- def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
1126
- self._apply(native_view, changed)
1127
- if changed.keys() & _LAYOUT_KEYS:
1128
- _apply_ios_layout(native_view, changed)
1129
-
1130
- def _apply(self, iv: Any, props: Dict[str, Any]) -> None:
1131
- if "background_color" in props and props["background_color"] is not None:
1132
- iv.setBackgroundColor_(_uicolor(props["background_color"]))
1133
- if "source" in props and props["source"]:
1134
- self._load_source(iv, props["source"])
1135
- if "scale_type" in props and props["scale_type"]:
1136
- mapping = {"cover": 2, "contain": 1, "stretch": 0, "center": 4}
1137
- iv.setContentMode_(mapping.get(props["scale_type"], 1))
1138
-
1139
- def _load_source(self, iv: Any, source: str) -> None:
1140
- try:
1141
- if source.startswith(("http://", "https://")):
1142
- NSURL = ObjCClass("NSURL")
1143
- NSData = ObjCClass("NSData")
1144
- UIImage = ObjCClass("UIImage")
1145
- url = NSURL.URLWithString_(source)
1146
- data = NSData.dataWithContentsOfURL_(url)
1147
- if data:
1148
- image = UIImage.imageWithData_(data)
1149
- if image:
1150
- iv.setImage_(image)
1151
- else:
1152
- UIImage = ObjCClass("UIImage")
1153
- image = UIImage.imageNamed_(source)
1154
- if image:
1155
- iv.setImage_(image)
1156
- except Exception:
1157
- pass
1158
-
1159
- # ---- Switch (with on_change) ----------------------------------------
1160
- _pn_switch_handler_map: dict = {}
1161
-
1162
- class _PNSwitchTarget(NSObject): # type: ignore[valid-type]
1163
- _callback: Optional[Callable[[bool], None]] = None
1164
-
1165
- @objc_method
1166
- def onToggle_(self, sender: object) -> None:
1167
- if self._callback is not None:
1168
- try:
1169
- self._callback(bool(sender.isOn()))
1170
- except Exception:
1171
- pass
1172
-
1173
- class IOSSwitchHandler(ViewHandler):
1174
- def create(self, props: Dict[str, Any]) -> Any:
1175
- sw = ObjCClass("UISwitch").alloc().init()
1176
- self._apply(sw, props)
1177
- return sw
1178
-
1179
- def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
1180
- self._apply(native_view, changed)
1181
-
1182
- def _apply(self, sw: Any, props: Dict[str, Any]) -> None:
1183
- if "value" in props:
1184
- sw.setOn_animated_(bool(props["value"]), False)
1185
- if "on_change" in props:
1186
- existing = _pn_switch_handler_map.get(id(sw))
1187
- if existing is not None:
1188
- existing._callback = props["on_change"]
1189
- else:
1190
- handler = _PNSwitchTarget.new()
1191
- handler._callback = props["on_change"]
1192
- _pn_switch_handler_map[id(sw)] = handler
1193
- sw.addTarget_action_forControlEvents_(handler, SEL("onToggle:"), 1 << 12)
1194
-
1195
- # ---- ProgressBar (UIProgressView) -----------------------------------
1196
- class IOSProgressBarHandler(ViewHandler):
1197
- def create(self, props: Dict[str, Any]) -> Any:
1198
- pv = ObjCClass("UIProgressView").alloc().init()
1199
- if "value" in props:
1200
- pv.setProgress_(float(props["value"]))
1201
- _apply_ios_layout(pv, props)
1202
- return pv
1203
-
1204
- def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
1205
- if "value" in changed:
1206
- native_view.setProgress_(float(changed["value"]))
1207
-
1208
- # ---- ActivityIndicator ----------------------------------------------
1209
- class IOSActivityIndicatorHandler(ViewHandler):
1210
- def create(self, props: Dict[str, Any]) -> Any:
1211
- ai = ObjCClass("UIActivityIndicatorView").alloc().init()
1212
- if props.get("animating", True):
1213
- ai.startAnimating()
1214
- return ai
1215
-
1216
- def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
1217
- if "animating" in changed:
1218
- if changed["animating"]:
1219
- native_view.startAnimating()
1220
- else:
1221
- native_view.stopAnimating()
1222
-
1223
- # ---- WebView (WKWebView) --------------------------------------------
1224
- class IOSWebViewHandler(ViewHandler):
1225
- def create(self, props: Dict[str, Any]) -> Any:
1226
- wv = ObjCClass("WKWebView").alloc().init()
1227
- if "url" in props and props["url"]:
1228
- NSURL = ObjCClass("NSURL")
1229
- NSURLRequest = ObjCClass("NSURLRequest")
1230
- url_obj = NSURL.URLWithString_(str(props["url"]))
1231
- wv.loadRequest_(NSURLRequest.requestWithURL_(url_obj))
1232
- _apply_ios_layout(wv, props)
1233
- return wv
1234
-
1235
- def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
1236
- if "url" in changed and changed["url"]:
1237
- NSURL = ObjCClass("NSURL")
1238
- NSURLRequest = ObjCClass("NSURLRequest")
1239
- url_obj = NSURL.URLWithString_(str(changed["url"]))
1240
- native_view.loadRequest_(NSURLRequest.requestWithURL_(url_obj))
1241
-
1242
- # ---- Spacer ---------------------------------------------------------
1243
- class IOSSpacerHandler(ViewHandler):
1244
- def create(self, props: Dict[str, Any]) -> Any:
1245
- v = ObjCClass("UIView").alloc().init()
1246
- if "size" in props and props["size"] is not None:
1247
- size = float(props["size"])
1248
- v.setFrame_(((0, 0), (size, size)))
1249
- return v
1250
-
1251
- def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
1252
- if "size" in changed and changed["size"] is not None:
1253
- size = float(changed["size"])
1254
- native_view.setFrame_(((0, 0), (size, size)))
1255
-
1256
- # ---- View (generic UIView) -----------------------------------------
1257
- class IOSViewHandler(ViewHandler):
1258
- def create(self, props: Dict[str, Any]) -> Any:
1259
- v = ObjCClass("UIView").alloc().init()
1260
- if "background_color" in props and props["background_color"] is not None:
1261
- v.setBackgroundColor_(_uicolor(props["background_color"]))
1262
- _apply_ios_layout(v, props)
1263
- return v
1264
-
1265
- def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
1266
- if "background_color" in changed and changed["background_color"] is not None:
1267
- native_view.setBackgroundColor_(_uicolor(changed["background_color"]))
1268
- if changed.keys() & _LAYOUT_KEYS:
1269
- _apply_ios_layout(native_view, changed)
1270
-
1271
- def add_child(self, parent: Any, child: Any) -> None:
1272
- parent.addSubview_(child)
1273
-
1274
- def remove_child(self, parent: Any, child: Any) -> None:
1275
- child.removeFromSuperview()
1276
-
1277
- # ---- SafeAreaView ---------------------------------------------------
1278
- class IOSSafeAreaViewHandler(ViewHandler):
1279
- def create(self, props: Dict[str, Any]) -> Any:
1280
- v = ObjCClass("UIView").alloc().init()
1281
- if "background_color" in props and props["background_color"] is not None:
1282
- v.setBackgroundColor_(_uicolor(props["background_color"]))
1283
- return v
1284
-
1285
- def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
1286
- if "background_color" in changed and changed["background_color"] is not None:
1287
- native_view.setBackgroundColor_(_uicolor(changed["background_color"]))
1288
-
1289
- def add_child(self, parent: Any, child: Any) -> None:
1290
- parent.addSubview_(child)
1291
-
1292
- def remove_child(self, parent: Any, child: Any) -> None:
1293
- child.removeFromSuperview()
1294
-
1295
- # ---- Modal ----------------------------------------------------------
1296
- class IOSModalHandler(ViewHandler):
1297
- def create(self, props: Dict[str, Any]) -> Any:
1298
- v = ObjCClass("UIView").alloc().init()
1299
- v.setHidden_(True)
1300
- return v
1301
-
1302
- def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
1303
- pass
1304
-
1305
- # ---- Slider (UISlider) ----------------------------------------------
1306
- _pn_slider_handler_map: dict = {}
1307
-
1308
- class _PNSliderTarget(NSObject): # type: ignore[valid-type]
1309
- _callback: Optional[Callable[[float], None]] = None
1310
-
1311
- @objc_method
1312
- def onSlide_(self, sender: object) -> None:
1313
- if self._callback is not None:
1314
- try:
1315
- self._callback(float(sender.value))
1316
- except Exception:
1317
- pass
1318
-
1319
- class IOSSliderHandler(ViewHandler):
1320
- def create(self, props: Dict[str, Any]) -> Any:
1321
- sl = ObjCClass("UISlider").alloc().init()
1322
- self._apply(sl, props)
1323
- _apply_ios_layout(sl, props)
1324
- return sl
1325
-
1326
- def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
1327
- self._apply(native_view, changed)
1328
-
1329
- def _apply(self, sl: Any, props: Dict[str, Any]) -> None:
1330
- if "min_value" in props:
1331
- sl.setMinimumValue_(float(props["min_value"]))
1332
- if "max_value" in props:
1333
- sl.setMaximumValue_(float(props["max_value"]))
1334
- if "value" in props:
1335
- sl.setValue_(float(props["value"]))
1336
- if "on_change" in props:
1337
- existing = _pn_slider_handler_map.get(id(sl))
1338
- if existing is not None:
1339
- existing._callback = props["on_change"]
1340
- else:
1341
- handler = _PNSliderTarget.new()
1342
- handler._callback = props["on_change"]
1343
- _pn_slider_handler_map[id(sl)] = handler
1344
- sl.addTarget_action_forControlEvents_(handler, SEL("onSlide:"), 1 << 12)
1345
-
1346
- # ---- Pressable (UIView with tap gesture) ----------------------------
1347
- class IOSPressableHandler(ViewHandler):
1348
- def create(self, props: Dict[str, Any]) -> Any:
1349
- v = ObjCClass("UIView").alloc().init()
1350
- v.setUserInteractionEnabled_(True)
1351
- return v
1352
-
1353
- def update(self, native_view: Any, changed: Dict[str, Any]) -> None:
1354
- pass
1355
-
1356
- def add_child(self, parent: Any, child: Any) -> None:
1357
- parent.addSubview_(child)
1358
-
1359
- def remove_child(self, parent: Any, child: Any) -> None:
1360
- child.removeFromSuperview()
1361
-
1362
- registry.register("Text", IOSTextHandler())
1363
- registry.register("Button", IOSButtonHandler())
1364
- registry.register("Column", IOSColumnHandler())
1365
- registry.register("Row", IOSRowHandler())
1366
- registry.register("ScrollView", IOSScrollViewHandler())
1367
- registry.register("TextInput", IOSTextInputHandler())
1368
- registry.register("Image", IOSImageHandler())
1369
- registry.register("Switch", IOSSwitchHandler())
1370
- registry.register("ProgressBar", IOSProgressBarHandler())
1371
- registry.register("ActivityIndicator", IOSActivityIndicatorHandler())
1372
- registry.register("WebView", IOSWebViewHandler())
1373
- registry.register("Spacer", IOSSpacerHandler())
1374
- registry.register("View", IOSViewHandler())
1375
- registry.register("SafeAreaView", IOSSafeAreaViewHandler())
1376
- registry.register("Modal", IOSModalHandler())
1377
- registry.register("Slider", IOSSliderHandler())
1378
- registry.register("Pressable", IOSPressableHandler())
1379
-
1380
-
1381
- # ======================================================================
1382
- # Factory
1383
- # ======================================================================
1384
-
1385
- _registry: Optional[NativeViewRegistry] = None
1386
-
1387
-
1388
- def get_registry() -> NativeViewRegistry:
1389
- """Return the singleton registry, lazily creating platform handlers."""
1390
- global _registry
1391
- if _registry is not None:
1392
- return _registry
1393
- _registry = NativeViewRegistry()
1394
- if IS_ANDROID:
1395
- _register_android_handlers(_registry)
1396
- else:
1397
- _register_ios_handlers(_registry)
1398
- return _registry
1399
-
1400
-
1401
- def set_registry(registry: NativeViewRegistry) -> None:
1402
- """Inject a custom or mock registry (primarily for testing)."""
1403
- global _registry
1404
- _registry = registry