pythonnative 0.23.0__py3-none-any.whl → 0.24.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 +29 -50
- pythonnative/animated.py +1 -1
- pythonnative/cli/pn.py +3 -3
- pythonnative/components.py +250 -513
- pythonnative/diagnostics.py +214 -0
- pythonnative/element.py +5 -2
- pythonnative/events.py +13 -8
- pythonnative/hooks.py +413 -49
- pythonnative/hot_reload.py +9 -1
- pythonnative/native_views/android.py +78 -1
- pythonnative/native_views/desktop.py +38 -1
- pythonnative/native_views/ios.py +241 -7
- pythonnative/navigation.py +41 -17
- pythonnative/preview.py +43 -7
- pythonnative/reconciler.py +863 -441
- pythonnative/screen.py +324 -27
- pythonnative/storage.py +3 -3
- pythonnative/style.py +83 -0
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/ScreenFragment.kt +22 -0
- {pythonnative-0.23.0.dist-info → pythonnative-0.24.0.dist-info}/METADATA +5 -4
- {pythonnative-0.23.0.dist-info → pythonnative-0.24.0.dist-info}/RECORD +25 -24
- {pythonnative-0.23.0.dist-info → pythonnative-0.24.0.dist-info}/WHEEL +0 -0
- {pythonnative-0.23.0.dist-info → pythonnative-0.24.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.23.0.dist-info → pythonnative-0.24.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.23.0.dist-info → pythonnative-0.24.0.dist-info}/top_level.txt +0 -0
pythonnative/screen.py
CHANGED
|
@@ -27,7 +27,7 @@ Example:
|
|
|
27
27
|
count, set_count = pn.use_state(0)
|
|
28
28
|
return pn.Column(
|
|
29
29
|
pn.Text(f"Count: {count}", style={"font_size": 24}),
|
|
30
|
-
pn.Button("Tap me",
|
|
30
|
+
pn.Button("Tap me", on_press=lambda: set_count(count + 1)),
|
|
31
31
|
style={"spacing": 12, "padding": 16},
|
|
32
32
|
)
|
|
33
33
|
```
|
|
@@ -48,8 +48,10 @@ import json
|
|
|
48
48
|
import os
|
|
49
49
|
import sys
|
|
50
50
|
import threading
|
|
51
|
-
|
|
51
|
+
import traceback
|
|
52
|
+
from typing import Any, Dict, Optional, Sequence, Tuple
|
|
52
53
|
|
|
54
|
+
from . import diagnostics
|
|
53
55
|
from .utils import IS_ANDROID, IS_DESKTOP, IS_IOS, set_android_context
|
|
54
56
|
|
|
55
57
|
_MAX_RENDER_PASSES = 25
|
|
@@ -186,6 +188,10 @@ def _init_host_common(host: Any, component_path: str, component_func: Any) -> No
|
|
|
186
188
|
host._hot_reload_manifest_path = None
|
|
187
189
|
host._hot_reload_last_version = None
|
|
188
190
|
host._layout_listener = None # retained on Android to prevent GC
|
|
191
|
+
# RedBox state: a dedicated reconciler that mounts the dev error
|
|
192
|
+
# overlay in place of the app tree (see ``_show_redbox``).
|
|
193
|
+
host._redbox_reconciler = None
|
|
194
|
+
host._redbox_root = None
|
|
189
195
|
# Focus state: drives ``use_focus_effect``. Starts focused because
|
|
190
196
|
# a host is only created when the screen is being presented; the
|
|
191
197
|
# platform lifecycle hooks (``on_resume`` / ``on_pause``) flip this
|
|
@@ -207,6 +213,54 @@ def _set_host_focused(host: Any, focused: bool) -> None:
|
|
|
207
213
|
pass
|
|
208
214
|
|
|
209
215
|
|
|
216
|
+
def _destroy_host(host: Any) -> None:
|
|
217
|
+
"""Tear down a screen host when the native screen is destroyed for good.
|
|
218
|
+
|
|
219
|
+
Called from each platform's ``on_destroy``. Unmounting the
|
|
220
|
+
reconciler runs every pending effect cleanup, destroys the native
|
|
221
|
+
views (releasing their event registrations), and clears back
|
|
222
|
+
handlers, so a popped screen doesn't leak its whole tree. Also
|
|
223
|
+
unregisters the host's RedBox reporter so runtime errors from other
|
|
224
|
+
screens don't route to a dead overlay.
|
|
225
|
+
"""
|
|
226
|
+
_clear_redbox(host, reattach=False)
|
|
227
|
+
diagnostics.set_error_reporter(host, None)
|
|
228
|
+
reconciler = host._reconciler
|
|
229
|
+
host._reconciler = None
|
|
230
|
+
if reconciler is not None:
|
|
231
|
+
try:
|
|
232
|
+
reconciler.unmount()
|
|
233
|
+
except Exception:
|
|
234
|
+
_log_pn("_destroy_host: reconciler.unmount() failed")
|
|
235
|
+
root = host._root_native_view
|
|
236
|
+
host._root_native_view = None
|
|
237
|
+
if root is not None:
|
|
238
|
+
try:
|
|
239
|
+
host._detach_root(root)
|
|
240
|
+
except Exception:
|
|
241
|
+
pass
|
|
242
|
+
host._nav_handle = None
|
|
243
|
+
host._focus_subscribers = []
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _host_back_pressed(host: Any) -> bool:
|
|
247
|
+
"""Offer the system back action to ``use_back_handler`` subscribers.
|
|
248
|
+
|
|
249
|
+
Called by the native templates before running the platform's
|
|
250
|
+
default back behavior. Returns ``True`` when a handler consumed
|
|
251
|
+
the event, in which case the platform must *not* pop the screen.
|
|
252
|
+
"""
|
|
253
|
+
reconciler = host._reconciler
|
|
254
|
+
if reconciler is None:
|
|
255
|
+
return False
|
|
256
|
+
try:
|
|
257
|
+
return bool(reconciler.dispatch_back_press())
|
|
258
|
+
except Exception as exc:
|
|
259
|
+
if not diagnostics.report_error(exc, phase="back handler"):
|
|
260
|
+
traceback.print_exc()
|
|
261
|
+
return False
|
|
262
|
+
|
|
263
|
+
|
|
210
264
|
def _push_viewport_size(host: Any, width: float, height: float) -> None:
|
|
211
265
|
"""Forward a viewport-size change to the reconciler.
|
|
212
266
|
|
|
@@ -223,6 +277,9 @@ def _push_viewport_size(host: Any, width: float, height: float) -> None:
|
|
|
223
277
|
if width <= 0 or height <= 0:
|
|
224
278
|
return
|
|
225
279
|
host._reconciler.set_viewport_size(float(width), float(height))
|
|
280
|
+
redbox = getattr(host, "_redbox_reconciler", None)
|
|
281
|
+
if redbox is not None:
|
|
282
|
+
redbox.set_viewport_size(float(width), float(height))
|
|
226
283
|
try:
|
|
227
284
|
from . import platform_metrics
|
|
228
285
|
|
|
@@ -269,6 +326,32 @@ def _flush_scheduled_renders(hosts: Sequence[Any]) -> None:
|
|
|
269
326
|
_re_render(host)
|
|
270
327
|
|
|
271
328
|
|
|
329
|
+
def _seed_initial_viewport(host: Any) -> None:
|
|
330
|
+
"""Give the reconciler a plausible viewport size *before* the first mount.
|
|
331
|
+
|
|
332
|
+
The authoritative size arrives right after attach (Android's
|
|
333
|
+
``OnLayoutChangeListener``, iOS ``viewDidLayoutSubviews``, the
|
|
334
|
+
desktop stage size), but by then the mount commit has already run:
|
|
335
|
+
without a viewport its layout pass is skipped, so mount-time
|
|
336
|
+
[`use_layout_effect`][pythonnative.use_layout_effect] callbacks
|
|
337
|
+
would observe no committed frames. Hosts opt in by providing
|
|
338
|
+
``_initial_viewport_size()``; the estimate only needs to be
|
|
339
|
+
plausible (screen-sized), not pixel-perfect.
|
|
340
|
+
"""
|
|
341
|
+
getter = getattr(host, "_initial_viewport_size", None)
|
|
342
|
+
if not callable(getter):
|
|
343
|
+
return
|
|
344
|
+
try:
|
|
345
|
+
size = getter()
|
|
346
|
+
except Exception:
|
|
347
|
+
return
|
|
348
|
+
if not size:
|
|
349
|
+
return
|
|
350
|
+
width, height = size
|
|
351
|
+
if width > 0 and height > 0:
|
|
352
|
+
host._reconciler.set_viewport_size(float(width), float(height))
|
|
353
|
+
|
|
354
|
+
|
|
272
355
|
def _on_create(host: Any) -> None:
|
|
273
356
|
from .hooks import NavigationHandle, Provider, _NavigationContext
|
|
274
357
|
|
|
@@ -285,23 +368,30 @@ def _on_create(host: Any) -> None:
|
|
|
285
368
|
# If we're already mounted, just re-attach the existing root view
|
|
286
369
|
# to the (newly created) native container; ``on_resume`` will
|
|
287
370
|
# fire the focus subscribers separately.
|
|
371
|
+
_register_redbox_reporter(host)
|
|
288
372
|
if host._reconciler is not None and host._root_native_view is not None:
|
|
289
373
|
host._attach_root(host._root_native_view)
|
|
290
374
|
return
|
|
291
375
|
|
|
292
376
|
host._nav_handle = NavigationHandle(host)
|
|
293
377
|
host._reconciler = _new_reconciler(host)
|
|
378
|
+
_seed_initial_viewport(host)
|
|
294
379
|
|
|
295
|
-
app_element = _render_app(host)
|
|
296
|
-
provider_element = Provider(_NavigationContext, host._nav_handle, app_element)
|
|
297
|
-
|
|
298
|
-
host._is_rendering = True
|
|
299
380
|
try:
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
381
|
+
app_element = _render_app(host)
|
|
382
|
+
provider_element = Provider(_NavigationContext, host._nav_handle, app_element)
|
|
383
|
+
|
|
384
|
+
host._is_rendering = True
|
|
385
|
+
try:
|
|
386
|
+
host._root_native_view = host._reconciler.mount(provider_element)
|
|
387
|
+
host._attach_root(host._root_native_view)
|
|
388
|
+
_drain_renders(host)
|
|
389
|
+
finally:
|
|
390
|
+
host._is_rendering = False
|
|
391
|
+
except Exception as exc:
|
|
392
|
+
if not diagnostics.is_dev():
|
|
393
|
+
raise
|
|
394
|
+
_show_redbox(host, exc, phase="mount")
|
|
305
395
|
|
|
306
396
|
|
|
307
397
|
def _request_render(host: Any) -> None:
|
|
@@ -333,13 +423,18 @@ def _re_render(host: Any) -> None:
|
|
|
333
423
|
hot reload.
|
|
334
424
|
"""
|
|
335
425
|
_log_pn("_re_render: starting local render pass")
|
|
336
|
-
host._is_rendering = True
|
|
337
426
|
try:
|
|
338
|
-
host.
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
427
|
+
host._is_rendering = True
|
|
428
|
+
try:
|
|
429
|
+
host._render_queued = False
|
|
430
|
+
_commit_dirty(host)
|
|
431
|
+
_drain_renders(host)
|
|
432
|
+
finally:
|
|
433
|
+
host._is_rendering = False
|
|
434
|
+
except Exception as exc:
|
|
435
|
+
if not diagnostics.is_dev():
|
|
436
|
+
raise
|
|
437
|
+
_show_redbox(host, exc, phase="render")
|
|
343
438
|
_log_pn("_re_render: done")
|
|
344
439
|
|
|
345
440
|
|
|
@@ -367,6 +462,130 @@ def _drain_renders(host: Any) -> None:
|
|
|
367
462
|
_commit_dirty(host)
|
|
368
463
|
|
|
369
464
|
|
|
465
|
+
# ======================================================================
|
|
466
|
+
# RedBox (dev-mode error overlay)
|
|
467
|
+
# ======================================================================
|
|
468
|
+
#
|
|
469
|
+
# In dev mode (``pn preview``, ``pn run`` with hot reload, ``PN_DEV=1``)
|
|
470
|
+
# an uncaught error from a mount, render, effect, or event handler
|
|
471
|
+
# replaces the screen with a full-screen traceback instead of crashing
|
|
472
|
+
# the process or dying silently in the log. The overlay is a normal
|
|
473
|
+
# PythonNative element tree mounted by a *dedicated* reconciler so a
|
|
474
|
+
# broken app tree can't take the error display down with it. Fixing the
|
|
475
|
+
# code and saving clears it via hot reload; the Dismiss button restores
|
|
476
|
+
# the previous native root (which is still mounted underneath).
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def _redbox_element(exc: BaseException, phase: str, on_dismiss: Any) -> Any:
|
|
480
|
+
"""Build the RedBox element tree for ``exc``."""
|
|
481
|
+
from .components import Button, Column, ScrollView, Text
|
|
482
|
+
|
|
483
|
+
trace = "".join(traceback.format_exception(type(exc), exc, exc.__traceback__))
|
|
484
|
+
message = str(exc) or "(no message)"
|
|
485
|
+
return Column(
|
|
486
|
+
Column(
|
|
487
|
+
Text(
|
|
488
|
+
f"{type(exc).__name__} in {phase}",
|
|
489
|
+
style={"color": "#FFD3DA", "font_size": 13, "bold": True},
|
|
490
|
+
),
|
|
491
|
+
Text(message, style={"color": "#FFFFFF", "font_size": 17, "bold": True}),
|
|
492
|
+
style={
|
|
493
|
+
"background_color": "#C4283C",
|
|
494
|
+
"padding": 16,
|
|
495
|
+
"padding_top": 56,
|
|
496
|
+
"spacing": 6,
|
|
497
|
+
},
|
|
498
|
+
),
|
|
499
|
+
ScrollView(
|
|
500
|
+
Text(trace, style={"color": "#FF9AA8", "font_size": 12}),
|
|
501
|
+
style={"flex": 1, "padding": 12},
|
|
502
|
+
),
|
|
503
|
+
Column(
|
|
504
|
+
Button("Dismiss", on_press=on_dismiss, style={"color": "#FFFFFF"}),
|
|
505
|
+
Text(
|
|
506
|
+
"Fix the error and save to reload.",
|
|
507
|
+
style={"color": "#8E8E93", "font_size": 12, "text_align": "center"},
|
|
508
|
+
),
|
|
509
|
+
style={"padding": 12, "padding_bottom": 32, "spacing": 4},
|
|
510
|
+
),
|
|
511
|
+
style={"flex": 1, "background_color": "#1C1C1E"},
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def _show_redbox(host: Any, exc: BaseException, phase: str = "render") -> None:
|
|
516
|
+
"""Mount the dev error overlay over the host's screen.
|
|
517
|
+
|
|
518
|
+
Safe to call repeatedly (a newer error replaces the current
|
|
519
|
+
overlay) and from any thread (the mount hops to the main thread).
|
|
520
|
+
Failures inside the RedBox itself fall back to printing both
|
|
521
|
+
tracebacks so the original error is never lost.
|
|
522
|
+
"""
|
|
523
|
+
_log_pn(f"_show_redbox: {type(exc).__name__} during {phase}")
|
|
524
|
+
try:
|
|
525
|
+
print(f"[PN] {phase} error:", file=sys.stderr)
|
|
526
|
+
traceback.print_exception(type(exc), exc, exc.__traceback__)
|
|
527
|
+
except Exception:
|
|
528
|
+
pass
|
|
529
|
+
|
|
530
|
+
def _mount() -> None:
|
|
531
|
+
try:
|
|
532
|
+
_clear_redbox(host, reattach=False)
|
|
533
|
+
from .native_views import get_registry
|
|
534
|
+
from .reconciler import Reconciler
|
|
535
|
+
|
|
536
|
+
redbox = Reconciler(get_registry())
|
|
537
|
+
redbox._screen_re_render = redbox.flush_dirty
|
|
538
|
+
element = _redbox_element(exc, phase, lambda: _clear_redbox(host))
|
|
539
|
+
root = redbox.mount(element)
|
|
540
|
+
width, height = (0.0, 0.0)
|
|
541
|
+
if host._reconciler is not None:
|
|
542
|
+
width, height = host._reconciler._viewport_size
|
|
543
|
+
if width <= 0 or height <= 0:
|
|
544
|
+
from . import platform_metrics
|
|
545
|
+
|
|
546
|
+
dims = platform_metrics.get_window_dimensions()
|
|
547
|
+
width, height = dims.width, dims.height
|
|
548
|
+
if width > 0 and height > 0:
|
|
549
|
+
redbox.set_viewport_size(width, height)
|
|
550
|
+
host._redbox_reconciler = redbox
|
|
551
|
+
host._redbox_root = root
|
|
552
|
+
if host._root_native_view is not None:
|
|
553
|
+
host._detach_root(host._root_native_view)
|
|
554
|
+
host._attach_root(root)
|
|
555
|
+
except Exception:
|
|
556
|
+
print("[PN] RedBox failed to mount:", file=sys.stderr)
|
|
557
|
+
traceback.print_exc()
|
|
558
|
+
|
|
559
|
+
from .runtime import call_on_main_thread
|
|
560
|
+
|
|
561
|
+
call_on_main_thread(_mount)
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def _clear_redbox(host: Any, reattach: bool = True) -> None:
|
|
565
|
+
"""Tear down the RedBox overlay and optionally restore the app root."""
|
|
566
|
+
redbox = getattr(host, "_redbox_reconciler", None)
|
|
567
|
+
if redbox is None:
|
|
568
|
+
return
|
|
569
|
+
host._redbox_reconciler = None
|
|
570
|
+
host._redbox_root = None
|
|
571
|
+
try:
|
|
572
|
+
redbox.unmount()
|
|
573
|
+
except Exception:
|
|
574
|
+
pass
|
|
575
|
+
if reattach and host._root_native_view is not None:
|
|
576
|
+
try:
|
|
577
|
+
host._attach_root(host._root_native_view)
|
|
578
|
+
except Exception:
|
|
579
|
+
pass
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def _register_redbox_reporter(host: Any) -> None:
|
|
583
|
+
"""Route runtime errors (events, async) for this host to the RedBox."""
|
|
584
|
+
if not diagnostics.is_dev():
|
|
585
|
+
return
|
|
586
|
+
diagnostics.set_error_reporter(host, lambda exc, phase: _show_redbox(host, exc, phase))
|
|
587
|
+
|
|
588
|
+
|
|
370
589
|
def _set_args(host: Any, args: Any) -> None:
|
|
371
590
|
if isinstance(args, str):
|
|
372
591
|
try:
|
|
@@ -380,6 +599,10 @@ def _set_args(host: Any, args: Any) -> None:
|
|
|
380
599
|
def _enable_hot_reload(host: Any, manifest_path: str) -> None:
|
|
381
600
|
host._hot_reload_manifest_path = manifest_path
|
|
382
601
|
host._hot_reload_last_version = None
|
|
602
|
+
# Hot reload only runs on debug builds, so treat it as the on-device
|
|
603
|
+
# dev-mode switch (validation warnings, hook-order checks, RedBox).
|
|
604
|
+
diagnostics.set_dev_mode(True)
|
|
605
|
+
_register_redbox_reporter(host)
|
|
383
606
|
|
|
384
607
|
|
|
385
608
|
def _hot_reload_tick(host: Any) -> bool:
|
|
@@ -473,8 +696,13 @@ def _reload_host(host: Any, changed_modules: Optional[Sequence[str]] = None) ->
|
|
|
473
696
|
|
|
474
697
|
try:
|
|
475
698
|
new_component = _import_component(host._component_path)
|
|
476
|
-
except Exception as
|
|
477
|
-
_log_pn(f"_reload_host: re-import failed: {
|
|
699
|
+
except Exception as exc:
|
|
700
|
+
_log_pn(f"_reload_host: re-import failed: {exc!r}; aborting reload")
|
|
701
|
+
if diagnostics.is_dev():
|
|
702
|
+
# A syntax error (or import-time crash) in the edited file:
|
|
703
|
+
# show it in the RedBox so the developer sees it on device
|
|
704
|
+
# instead of the app silently keeping the old code.
|
|
705
|
+
_show_redbox(host, exc, phase="hot reload import")
|
|
478
706
|
return
|
|
479
707
|
host._component = new_component
|
|
480
708
|
|
|
@@ -482,11 +710,22 @@ def _reload_host(host: Any, changed_modules: Optional[Sequence[str]] = None) ->
|
|
|
482
710
|
_log_pn(f"_reload_host: host=0x{id(host):x} reconciler=None; skipping refresh")
|
|
483
711
|
return
|
|
484
712
|
|
|
713
|
+
# A reload always replaces whatever error state was on screen.
|
|
714
|
+
# Re-attach the current root now: Fast Refresh patches that tree in
|
|
715
|
+
# place (attach must happen regardless of whether it succeeds), and
|
|
716
|
+
# a full remount swaps in its own fresh root anyway.
|
|
717
|
+
_clear_redbox(host)
|
|
718
|
+
|
|
485
719
|
if _try_fast_refresh(host, reloaded):
|
|
486
720
|
print(f"[hot-reload] Fast Refresh: {', '.join(requested) or ', '.join(reloaded)}", file=sys.stderr)
|
|
487
721
|
return
|
|
488
722
|
|
|
489
|
-
|
|
723
|
+
try:
|
|
724
|
+
_full_remount(host, reloaded)
|
|
725
|
+
except Exception as exc:
|
|
726
|
+
if not diagnostics.is_dev():
|
|
727
|
+
raise
|
|
728
|
+
_show_redbox(host, exc, phase="hot reload")
|
|
490
729
|
|
|
491
730
|
|
|
492
731
|
def _try_fast_refresh(host: Any, reloaded_modules: Sequence[str]) -> bool:
|
|
@@ -819,6 +1058,15 @@ if IS_ANDROID:
|
|
|
819
1058
|
set_android_context(native_instance)
|
|
820
1059
|
_init_host_common(self, component_path, component_func)
|
|
821
1060
|
|
|
1061
|
+
def _initial_viewport_size(self) -> Optional[Tuple[float, float]]:
|
|
1062
|
+
"""Estimate the viewport from display metrics (pre-attach)."""
|
|
1063
|
+
try:
|
|
1064
|
+
metrics = self.native_instance.getResources().getDisplayMetrics()
|
|
1065
|
+
density = float(metrics.density) or 1.0
|
|
1066
|
+
return (metrics.widthPixels / density, metrics.heightPixels / density)
|
|
1067
|
+
except Exception:
|
|
1068
|
+
return None
|
|
1069
|
+
|
|
822
1070
|
def on_create(self) -> None:
|
|
823
1071
|
_android_publish_color_scheme(self.native_instance)
|
|
824
1072
|
_on_create(self)
|
|
@@ -844,7 +1092,10 @@ if IS_ANDROID:
|
|
|
844
1092
|
pass
|
|
845
1093
|
|
|
846
1094
|
def on_destroy(self) -> None:
|
|
847
|
-
|
|
1095
|
+
_destroy_host(self)
|
|
1096
|
+
|
|
1097
|
+
def on_back_pressed(self) -> bool:
|
|
1098
|
+
return _host_back_pressed(self)
|
|
848
1099
|
|
|
849
1100
|
def enable_hot_reload(self, manifest_path: str, source_root: Optional[str] = None) -> None:
|
|
850
1101
|
_enable_hot_reload(self, manifest_path)
|
|
@@ -938,11 +1189,18 @@ if IS_ANDROID:
|
|
|
938
1189
|
_android_push_initial_viewport(self, container)
|
|
939
1190
|
|
|
940
1191
|
def _detach_root(self, native_view: Any) -> None:
|
|
1192
|
+
# Remove this host's specific root view from whatever parent
|
|
1193
|
+
# holds it. Never clear the shared fragment container: when a
|
|
1194
|
+
# fragment is popped, its ``onDestroy`` (and therefore this
|
|
1195
|
+
# detach) runs *after* the screen below has already re-attached
|
|
1196
|
+
# its own root to the container, so a ``removeAllViews()`` here
|
|
1197
|
+
# would blank the restored screen.
|
|
1198
|
+
if native_view is None:
|
|
1199
|
+
return
|
|
941
1200
|
try:
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
container.removeAllViews()
|
|
1201
|
+
parent = native_view.getParent()
|
|
1202
|
+
if parent is not None:
|
|
1203
|
+
parent.removeView(native_view)
|
|
946
1204
|
except Exception:
|
|
947
1205
|
pass
|
|
948
1206
|
|
|
@@ -1005,6 +1263,17 @@ elif IS_DESKTOP:
|
|
|
1005
1263
|
self.native_instance = native_instance
|
|
1006
1264
|
_init_host_common(self, component_path, component_func)
|
|
1007
1265
|
|
|
1266
|
+
def _initial_viewport_size(self) -> Optional[Tuple[float, float]]:
|
|
1267
|
+
"""Read the stage size from the DesktopApp controller (pre-attach)."""
|
|
1268
|
+
app = self.native_instance
|
|
1269
|
+
if app is None or not hasattr(app, "viewport_size"):
|
|
1270
|
+
return None
|
|
1271
|
+
try:
|
|
1272
|
+
width, height = app.viewport_size()
|
|
1273
|
+
return (float(width), float(height))
|
|
1274
|
+
except Exception:
|
|
1275
|
+
return None
|
|
1276
|
+
|
|
1008
1277
|
def on_create(self) -> None:
|
|
1009
1278
|
_on_create(self)
|
|
1010
1279
|
|
|
@@ -1024,7 +1293,10 @@ elif IS_DESKTOP:
|
|
|
1024
1293
|
pass
|
|
1025
1294
|
|
|
1026
1295
|
def on_destroy(self) -> None:
|
|
1027
|
-
|
|
1296
|
+
_destroy_host(self)
|
|
1297
|
+
|
|
1298
|
+
def on_back_pressed(self) -> bool:
|
|
1299
|
+
return _host_back_pressed(self)
|
|
1028
1300
|
|
|
1029
1301
|
def enable_hot_reload(self, manifest_path: str, source_root: Optional[str] = None) -> None:
|
|
1030
1302
|
_enable_hot_reload(self, manifest_path)
|
|
@@ -1352,6 +1624,24 @@ else:
|
|
|
1352
1624
|
if self.native_instance is not None:
|
|
1353
1625
|
_ios_register_screen(self.native_instance, self)
|
|
1354
1626
|
|
|
1627
|
+
def _initial_viewport_size(self) -> Optional[Tuple[float, float]]:
|
|
1628
|
+
"""Estimate the viewport from the VC's view bounds (pre-attach)."""
|
|
1629
|
+
try:
|
|
1630
|
+
if self.native_instance is not None:
|
|
1631
|
+
bounds = self.native_instance.view.bounds
|
|
1632
|
+
width = float(bounds.size.width)
|
|
1633
|
+
height = float(bounds.size.height)
|
|
1634
|
+
if width > 0 and height > 0:
|
|
1635
|
+
return (width, height)
|
|
1636
|
+
except Exception:
|
|
1637
|
+
pass
|
|
1638
|
+
try:
|
|
1639
|
+
UIScreen = ObjCClass("UIScreen")
|
|
1640
|
+
screen_bounds = UIScreen.mainScreen.bounds
|
|
1641
|
+
return (float(screen_bounds.size.width), float(screen_bounds.size.height))
|
|
1642
|
+
except Exception:
|
|
1643
|
+
return None
|
|
1644
|
+
|
|
1355
1645
|
def on_create(self) -> None:
|
|
1356
1646
|
_ios_publish_color_scheme()
|
|
1357
1647
|
_on_create(self)
|
|
@@ -1368,6 +1658,10 @@ else:
|
|
|
1368
1658
|
def on_destroy(self) -> None:
|
|
1369
1659
|
if self.native_instance is not None:
|
|
1370
1660
|
_ios_unregister_screen(self.native_instance)
|
|
1661
|
+
_destroy_host(self)
|
|
1662
|
+
|
|
1663
|
+
def on_back_pressed(self) -> bool:
|
|
1664
|
+
return _host_back_pressed(self)
|
|
1371
1665
|
|
|
1372
1666
|
def enable_hot_reload(self, manifest_path: str, source_root: Optional[str] = None) -> None:
|
|
1373
1667
|
_enable_hot_reload(self, manifest_path)
|
|
@@ -1631,7 +1925,10 @@ else:
|
|
|
1631
1925
|
pass
|
|
1632
1926
|
|
|
1633
1927
|
def on_destroy(self) -> None:
|
|
1634
|
-
|
|
1928
|
+
_destroy_host(self)
|
|
1929
|
+
|
|
1930
|
+
def on_back_pressed(self) -> bool:
|
|
1931
|
+
return _host_back_pressed(self)
|
|
1635
1932
|
|
|
1636
1933
|
def enable_hot_reload(self, manifest_path: str, source_root: Optional[str] = None) -> None:
|
|
1637
1934
|
_enable_hot_reload(self, manifest_path)
|
pythonnative/storage.py
CHANGED
|
@@ -375,7 +375,7 @@ def use_persisted_state(
|
|
|
375
375
|
theme, set_theme = pn.use_persisted_state("settings.theme", "light")
|
|
376
376
|
return pn.Button(
|
|
377
377
|
f"Theme: {theme}",
|
|
378
|
-
|
|
378
|
+
on_press=lambda: set_theme("dark" if theme == "light" else "light"),
|
|
379
379
|
)
|
|
380
380
|
```
|
|
381
381
|
"""
|
|
@@ -389,14 +389,14 @@ def use_persisted_state(
|
|
|
389
389
|
stored = await AsyncStorage.get_json(key)
|
|
390
390
|
if stored is not None:
|
|
391
391
|
set_state(stored)
|
|
392
|
-
loaded
|
|
392
|
+
loaded.current = True
|
|
393
393
|
|
|
394
394
|
use_async_effect(_load, [key])
|
|
395
395
|
|
|
396
396
|
def setter(value_or_updater: Any) -> None:
|
|
397
397
|
def _reducer(current: Any) -> Any:
|
|
398
398
|
new_value = value_or_updater(current) if callable(value_or_updater) else value_or_updater
|
|
399
|
-
if loaded
|
|
399
|
+
if loaded.current is True:
|
|
400
400
|
run_async(AsyncStorage.set_json(key, new_value))
|
|
401
401
|
return new_value
|
|
402
402
|
|
pythonnative/style.py
CHANGED
|
@@ -34,8 +34,10 @@ Example:
|
|
|
34
34
|
```
|
|
35
35
|
"""
|
|
36
36
|
|
|
37
|
+
import difflib
|
|
37
38
|
from typing import Any, Dict, List, Literal, Optional, Tuple, TypedDict, Union
|
|
38
39
|
|
|
40
|
+
from . import diagnostics
|
|
39
41
|
from .hooks import Context, create_context, use_color_scheme, use_context
|
|
40
42
|
|
|
41
43
|
# ======================================================================
|
|
@@ -74,6 +76,30 @@ EdgeValue = Union[Dimension, EdgeInsets]
|
|
|
74
76
|
[`EdgeInsets`][pythonnative.EdgeInsets] dict."""
|
|
75
77
|
|
|
76
78
|
|
|
79
|
+
class AccessibilityState(TypedDict, total=False):
|
|
80
|
+
"""Current widget state exposed to assistive technology.
|
|
81
|
+
|
|
82
|
+
Passed via the ``accessibility_state=`` prop on interactive
|
|
83
|
+
components. All keys are optional; omitted keys are treated as
|
|
84
|
+
unset (not ``False``).
|
|
85
|
+
|
|
86
|
+
Attributes:
|
|
87
|
+
disabled: The widget is visible but not interactive.
|
|
88
|
+
selected: The widget is currently selected (e.g. the active
|
|
89
|
+
tab).
|
|
90
|
+
checked: Toggle/checkbox state. ``"mixed"`` represents a
|
|
91
|
+
tri-state checkbox.
|
|
92
|
+
busy: The widget is loading or otherwise temporarily busy.
|
|
93
|
+
expanded: A disclosure/accordion is currently expanded.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
disabled: bool
|
|
97
|
+
selected: bool
|
|
98
|
+
checked: Union[bool, Literal["mixed"]]
|
|
99
|
+
busy: bool
|
|
100
|
+
expanded: bool
|
|
101
|
+
|
|
102
|
+
|
|
77
103
|
class ShadowOffset(TypedDict):
|
|
78
104
|
"""Shadow displacement in points."""
|
|
79
105
|
|
|
@@ -136,6 +162,17 @@ TransformSpec = Union[TransformEntry, List[TransformEntry]]
|
|
|
136
162
|
# ----------------------------------------------------------------------
|
|
137
163
|
|
|
138
164
|
FlexDirection = Literal["row", "column", "row_reverse", "column_reverse"]
|
|
165
|
+
FlexWrap = Literal["nowrap", "wrap", "wrap_reverse"]
|
|
166
|
+
AlignContent = Literal[
|
|
167
|
+
"flex_start",
|
|
168
|
+
"center",
|
|
169
|
+
"flex_end",
|
|
170
|
+
"stretch",
|
|
171
|
+
"space_between",
|
|
172
|
+
"space_around",
|
|
173
|
+
"space_evenly",
|
|
174
|
+
]
|
|
175
|
+
LayoutDirection = Literal["ltr", "rtl"]
|
|
139
176
|
JustifyContent = Literal[
|
|
140
177
|
"flex_start",
|
|
141
178
|
"center",
|
|
@@ -241,9 +278,12 @@ class Style(TypedDict, total=False):
|
|
|
241
278
|
flex_shrink: float
|
|
242
279
|
flex_basis: Dimension
|
|
243
280
|
flex_direction: FlexDirection
|
|
281
|
+
flex_wrap: FlexWrap
|
|
244
282
|
justify_content: JustifyContent
|
|
245
283
|
align_items: AlignItems
|
|
246
284
|
align_self: AlignSelf
|
|
285
|
+
align_content: AlignContent
|
|
286
|
+
direction: LayoutDirection
|
|
247
287
|
|
|
248
288
|
# --- Layout: position ---
|
|
249
289
|
position: Position
|
|
@@ -251,6 +291,8 @@ class Style(TypedDict, total=False):
|
|
|
251
291
|
right: Dimension
|
|
252
292
|
bottom: Dimension
|
|
253
293
|
left: Dimension
|
|
294
|
+
start: Dimension
|
|
295
|
+
end: Dimension
|
|
254
296
|
|
|
255
297
|
# --- Layout: spacing ---
|
|
256
298
|
padding: EdgeValue
|
|
@@ -258,6 +300,8 @@ class Style(TypedDict, total=False):
|
|
|
258
300
|
padding_bottom: Dimension
|
|
259
301
|
padding_left: Dimension
|
|
260
302
|
padding_right: Dimension
|
|
303
|
+
padding_start: Dimension
|
|
304
|
+
padding_end: Dimension
|
|
261
305
|
padding_horizontal: Dimension
|
|
262
306
|
padding_vertical: Dimension
|
|
263
307
|
margin: EdgeValue
|
|
@@ -265,10 +309,14 @@ class Style(TypedDict, total=False):
|
|
|
265
309
|
margin_bottom: Dimension
|
|
266
310
|
margin_left: Dimension
|
|
267
311
|
margin_right: Dimension
|
|
312
|
+
margin_start: Dimension
|
|
313
|
+
margin_end: Dimension
|
|
268
314
|
margin_horizontal: Dimension
|
|
269
315
|
margin_vertical: Dimension
|
|
270
316
|
spacing: float
|
|
271
317
|
gap: float
|
|
318
|
+
row_gap: float
|
|
319
|
+
column_gap: float
|
|
272
320
|
|
|
273
321
|
# --- Visual: clipping & overflow ---
|
|
274
322
|
overflow: Overflow
|
|
@@ -378,6 +426,40 @@ def resolve_style(value: StyleProp) -> Dict[str, Any]:
|
|
|
378
426
|
return result
|
|
379
427
|
|
|
380
428
|
|
|
429
|
+
_KNOWN_STYLE_KEYS = frozenset(Style.__annotations__)
|
|
430
|
+
"""Every key declared on the [`Style`][pythonnative.Style] TypedDict."""
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def validate_style_keys(style_dict: Dict[str, Any], owner: str = "") -> None:
|
|
434
|
+
"""Warn (once per key) about style keys no built-in handler reads.
|
|
435
|
+
|
|
436
|
+
Dev-mode only; production skips the scan entirely. Typos get a
|
|
437
|
+
"did you mean" suggestion via fuzzy matching against the declared
|
|
438
|
+
[`Style`][pythonnative.Style] keys. Unknown keys still flow through
|
|
439
|
+
to handlers untouched, so custom components that read extra keys
|
|
440
|
+
keep working (at the cost of one dev warning).
|
|
441
|
+
|
|
442
|
+
Args:
|
|
443
|
+
style_dict: The flattened style dict about to be merged into an
|
|
444
|
+
element's props.
|
|
445
|
+
owner: Element type name for the warning message (e.g.
|
|
446
|
+
``"Text"``).
|
|
447
|
+
"""
|
|
448
|
+
if not diagnostics.is_dev() or not style_dict:
|
|
449
|
+
return
|
|
450
|
+
for key in style_dict:
|
|
451
|
+
if key in _KNOWN_STYLE_KEYS:
|
|
452
|
+
continue
|
|
453
|
+
matches = difflib.get_close_matches(str(key), _KNOWN_STYLE_KEYS, n=1)
|
|
454
|
+
hint = f" Did you mean {matches[0]!r}?" if matches else ""
|
|
455
|
+
diagnostics.warn_once(
|
|
456
|
+
f"Unknown style key {key!r}"
|
|
457
|
+
+ (f" on {owner}" if owner else "")
|
|
458
|
+
+ f".{hint} Built-in components ignore keys they don't recognize.",
|
|
459
|
+
key=f"style:{owner}:{key}",
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
|
|
381
463
|
# ======================================================================
|
|
382
464
|
# StyleSheet
|
|
383
465
|
# ======================================================================
|
|
@@ -604,4 +686,5 @@ __all__ = [
|
|
|
604
686
|
"resolve_style",
|
|
605
687
|
"style",
|
|
606
688
|
"use_theme",
|
|
689
|
+
"validate_style_keys",
|
|
607
690
|
]
|