pythonnative 0.26.0__py3-none-any.whl → 0.27.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 +1 -1
- pythonnative/animated.py +697 -62
- pythonnative/components.py +61 -7
- pythonnative/gestures.py +446 -34
- pythonnative/native_modules/net_info.py +73 -4
- pythonnative/native_modules/notifications.py +37 -9
- pythonnative/native_views/android.py +320 -16
- pythonnative/native_views/desktop.py +766 -47
- pythonnative/native_views/ios.py +522 -33
- pythonnative/reconciler.py +28 -0
- pythonnative/style.py +19 -0
- pythonnative/templates/android_template/app/build.gradle +4 -0
- pythonnative/templates/android_template/app/src/main/AndroidManifest.xml +4 -0
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +60 -0
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNFrameLayout.kt +50 -0
- pythonnative/templates/ios_template/ios_template/Info.plist +5 -0
- {pythonnative-0.26.0.dist-info → pythonnative-0.27.0.dist-info}/METADATA +3 -3
- {pythonnative-0.26.0.dist-info → pythonnative-0.27.0.dist-info}/RECORD +22 -21
- {pythonnative-0.26.0.dist-info → pythonnative-0.27.0.dist-info}/WHEEL +0 -0
- {pythonnative-0.26.0.dist-info → pythonnative-0.27.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.26.0.dist-info → pythonnative-0.27.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.26.0.dist-info → pythonnative-0.27.0.dist-info}/top_level.txt +0 -0
|
@@ -36,15 +36,45 @@ parent-relative frames render correctly without reparenting.
|
|
|
36
36
|
ScrollViews shift their children's placement by the current scroll
|
|
37
37
|
offset, which yields real wheel scrolling in the preview.
|
|
38
38
|
|
|
39
|
+
Interaction and stacking props
|
|
40
|
+
------------------------------
|
|
41
|
+
``z_index`` is honored among logical siblings: whenever a sibling set
|
|
42
|
+
contains an explicit ``z_index``, the parent's children are re-lifted
|
|
43
|
+
in ascending ``z_index`` order (missing values count as 0, ties keep
|
|
44
|
+
insertion order), so higher values render above their siblings.
|
|
45
|
+
``pointer_events`` is approximated by suspending a widget's Tk binding
|
|
46
|
+
tags: ``"none"`` mutes the view and its descendants, ``"box_none"``
|
|
47
|
+
mutes only the view itself, and ``"box_only"`` mutes only the
|
|
48
|
+
descendants. Suspension is reversible (the original tags are restored
|
|
49
|
+
when the prop returns to ``"auto"``), but it is coarser than the real
|
|
50
|
+
platforms: all bindings go quiet (keyboard included), and muted
|
|
51
|
+
widgets drop events rather than passing them through to whatever sits
|
|
52
|
+
underneath. ``hit_slop`` can't grow the initial press target (Tk
|
|
53
|
+
hit-tests presses strictly by widget bounds), but it does expand press
|
|
54
|
+
*tracking*: once a press starts, Tk's implicit grab keeps streaming
|
|
55
|
+
motion and release events to the pressed widget, and ``Pressable``
|
|
56
|
+
treats positions within the slop insets of its frame as still inside
|
|
57
|
+
when deciding whether a release fires ``on_press`` or a pending long
|
|
58
|
+
press survives pointer drift.
|
|
59
|
+
|
|
39
60
|
Scope
|
|
40
61
|
-----
|
|
41
62
|
This is a **preview** backend, not a production desktop target. It
|
|
42
63
|
favors fidelity of layout and behavior over pixel-perfect chrome:
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
``
|
|
64
|
+
shadows, per-widget opacity, and overflow clipping are approximated or
|
|
65
|
+
omitted (Tkinter can't express them cheaply). Rounded corners
|
|
66
|
+
(``border_radius`` plus the per-corner ``border_*_radius`` keys, which
|
|
67
|
+
fall back to it) are approximated on Frame-based views by painting the
|
|
68
|
+
background and a uniform border onto a covering ``Canvas``; the corner
|
|
69
|
+
cutouts are filled with the parent's solid background rather than
|
|
70
|
+
being truly transparent, and content leaves (Text, Button, Image)
|
|
71
|
+
ignore radius entirely. Native animation is declined
|
|
72
|
+
(``start_animation`` returns ``False``), so the Python ticker drives
|
|
73
|
+
previews of animations through ``set_animated_property``: translation
|
|
74
|
+
maps onto placement, animated ``background_color`` and ``color``
|
|
75
|
+
(including interpolated ``"#AARRGGBB"`` strings) map onto ``configure``
|
|
76
|
+
(or the rounded-corner canvas when one is active), and rotate, scale,
|
|
77
|
+
and opacity frames are silently ignored.
|
|
48
78
|
|
|
49
79
|
This module imports ``tkinter`` at import time, so it is only imported
|
|
50
80
|
when ``PN_PLATFORM=desktop``. Off-device unit tests inject a mock
|
|
@@ -54,6 +84,7 @@ and never trigger this path.
|
|
|
54
84
|
|
|
55
85
|
from __future__ import annotations
|
|
56
86
|
|
|
87
|
+
import bisect
|
|
57
88
|
import math
|
|
58
89
|
import re
|
|
59
90
|
import time
|
|
@@ -327,6 +358,16 @@ def _place(widget: Any) -> None:
|
|
|
327
358
|
widget.lift()
|
|
328
359
|
except Exception:
|
|
329
360
|
diagnostics.swallowed("desktop._place")
|
|
361
|
+
return
|
|
362
|
+
# The unconditional lift above would put a low ``z_index`` sibling
|
|
363
|
+
# on top, so restack whenever this sibling set uses ``z_index``.
|
|
364
|
+
if parent is not None and getattr(parent, "_pn_has_z", False):
|
|
365
|
+
_restack(parent)
|
|
366
|
+
# Frames are the only reliable resize signal for unmapped widgets,
|
|
367
|
+
# so keep the rounded background in sync here (cheap when the
|
|
368
|
+
# paint signature is unchanged).
|
|
369
|
+
if getattr(widget, "_pn_radius_canvas", None) is not None:
|
|
370
|
+
_redraw_rounded_background(widget)
|
|
330
371
|
|
|
331
372
|
|
|
332
373
|
def _register_child(parent: Any, child: Any, index: int) -> None:
|
|
@@ -347,6 +388,369 @@ def _unregister_child(parent: Any, child: Any) -> None:
|
|
|
347
388
|
parent._pn_children = children
|
|
348
389
|
|
|
349
390
|
|
|
391
|
+
def _z_index(widget: Any) -> float:
|
|
392
|
+
"""Return the widget's ``z_index`` (missing or unparseable is 0)."""
|
|
393
|
+
merged = getattr(widget, "_pn_props", None) or {}
|
|
394
|
+
z = merged.get("z_index")
|
|
395
|
+
if z is None:
|
|
396
|
+
return 0.0
|
|
397
|
+
try:
|
|
398
|
+
return float(z)
|
|
399
|
+
except (TypeError, ValueError):
|
|
400
|
+
return 0.0
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _lift_subtree(widget: Any) -> None:
|
|
404
|
+
"""Lift ``widget``, then its logical descendants in ``z_index`` order.
|
|
405
|
+
|
|
406
|
+
Every desktop widget is a Tk child of the shared stage, so
|
|
407
|
+
stacking is one global order: lifting a container doesn't carry
|
|
408
|
+
its subtree along. Pre-order lifting keeps each subtree above its
|
|
409
|
+
root and applies nested ``z_index`` ordering along the way.
|
|
410
|
+
"""
|
|
411
|
+
try:
|
|
412
|
+
widget.lift()
|
|
413
|
+
except Exception:
|
|
414
|
+
diagnostics.swallowed("desktop._lift_subtree")
|
|
415
|
+
for child in sorted(getattr(widget, "_pn_children", None) or [], key=_z_index):
|
|
416
|
+
_lift_subtree(child)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _restack(parent: Any) -> None:
|
|
420
|
+
"""Re-lift ``parent``'s children (and their subtrees) by ``z_index``.
|
|
421
|
+
|
|
422
|
+
``sorted`` is stable, so siblings with equal ``z_index`` keep their
|
|
423
|
+
insertion order. Called whenever a sibling set contains an explicit
|
|
424
|
+
``z_index`` (see ``_place`` and ``_apply_common``).
|
|
425
|
+
"""
|
|
426
|
+
for child in sorted(getattr(parent, "_pn_children", None) or [], key=_z_index):
|
|
427
|
+
_lift_subtree(child)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
# ----------------------------------------------------------------------
|
|
431
|
+
# ``pointer_events`` (best-effort via Tk binding tags)
|
|
432
|
+
# ----------------------------------------------------------------------
|
|
433
|
+
#
|
|
434
|
+
# A muted widget gets a single dummy binding tag with no bindings, so
|
|
435
|
+
# no event scripts (its own, its class's, or the global "all" tag's)
|
|
436
|
+
# run while the mode is active; the original tags are saved and
|
|
437
|
+
# restored when the prop returns to "auto". This is coarser than the
|
|
438
|
+
# real platforms: keyboard bindings go quiet too, and Tk still routes
|
|
439
|
+
# the event to the widget under the cursor, so muted widgets drop
|
|
440
|
+
# events instead of letting them fall through to lower widgets.
|
|
441
|
+
|
|
442
|
+
_PE_MUTED_TAG = "pn_pointer_events_off"
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _pointer_mode(widget: Any) -> str:
|
|
446
|
+
"""Return the widget's own ``pointer_events`` mode (default ``"auto"``)."""
|
|
447
|
+
merged = getattr(widget, "_pn_props", None) or {}
|
|
448
|
+
mode = merged.get("pointer_events")
|
|
449
|
+
return mode if mode in ("none", "box_none", "box_only") else "auto"
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def _pointer_muted(widget: Any) -> bool:
|
|
453
|
+
"""Whether the widget should ignore pointer events right now.
|
|
454
|
+
|
|
455
|
+
True when its own mode mutes the view (``"none"`` / ``"box_none"``)
|
|
456
|
+
or when any logical ancestor mutes its subtree (``"none"`` /
|
|
457
|
+
``"box_only"``).
|
|
458
|
+
"""
|
|
459
|
+
if _pointer_mode(widget) in ("none", "box_none"):
|
|
460
|
+
return True
|
|
461
|
+
node = getattr(widget, "_pn_parent", None)
|
|
462
|
+
while node is not None:
|
|
463
|
+
if _pointer_mode(node) in ("none", "box_only"):
|
|
464
|
+
return True
|
|
465
|
+
node = getattr(node, "_pn_parent", None)
|
|
466
|
+
return False
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def _set_pointer_muted(widget: Any, muted: bool) -> None:
|
|
470
|
+
"""Suspend or restore the widget's Tk binding tags (idempotent)."""
|
|
471
|
+
try:
|
|
472
|
+
current = tuple(widget.bindtags())
|
|
473
|
+
if muted:
|
|
474
|
+
if current != (_PE_MUTED_TAG,):
|
|
475
|
+
widget._pn_saved_bindtags = current
|
|
476
|
+
widget.bindtags((_PE_MUTED_TAG,))
|
|
477
|
+
elif current == (_PE_MUTED_TAG,):
|
|
478
|
+
saved = getattr(widget, "_pn_saved_bindtags", None)
|
|
479
|
+
if saved:
|
|
480
|
+
widget.bindtags(saved)
|
|
481
|
+
except Exception:
|
|
482
|
+
diagnostics.swallowed("desktop._set_pointer_muted")
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def _refresh_pointer_events(widget: Any) -> None:
|
|
486
|
+
"""Recompute pointer muting for ``widget`` and its logical subtree."""
|
|
487
|
+
muted = _pointer_muted(widget)
|
|
488
|
+
_set_pointer_muted(widget, muted)
|
|
489
|
+
canvas = getattr(widget, "_pn_radius_canvas", None)
|
|
490
|
+
if canvas is not None:
|
|
491
|
+
# The rounded-corner canvas covers the widget and forwards
|
|
492
|
+
# events to it (see ``_set_rounded_background``), so it mutes
|
|
493
|
+
# exactly when its host does.
|
|
494
|
+
_set_pointer_muted(canvas, muted)
|
|
495
|
+
for child in getattr(widget, "_pn_children", None) or []:
|
|
496
|
+
_refresh_pointer_events(child)
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
# ----------------------------------------------------------------------
|
|
500
|
+
# ``hit_slop`` (press-tracking expansion)
|
|
501
|
+
# ----------------------------------------------------------------------
|
|
502
|
+
#
|
|
503
|
+
# Tk delivers the initial press only to the widget under the cursor,
|
|
504
|
+
# so the slop insets can't grow the press target the way they do on
|
|
505
|
+
# device. They do expand the *tracking* rect: Tk's implicit grab keeps
|
|
506
|
+
# streaming motion and release events to the pressed widget even past
|
|
507
|
+
# its edges, and ``_within_hit_rect`` treats positions within the slop
|
|
508
|
+
# insets of the widget's frame as inside.
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def _parse_hit_slop(value: Any) -> Tuple[float, float, float, float]:
|
|
512
|
+
"""Normalize a ``hit_slop`` prop into ``(top, left, bottom, right)`` insets.
|
|
513
|
+
|
|
514
|
+
Accepts a uniform number or a dict with any of ``top`` / ``left``
|
|
515
|
+
/ ``bottom`` / ``right``; missing or unparseable entries are 0.
|
|
516
|
+
"""
|
|
517
|
+
if isinstance(value, dict):
|
|
518
|
+
return (
|
|
519
|
+
max(0.0, _finite(value.get("top"))),
|
|
520
|
+
max(0.0, _finite(value.get("left"))),
|
|
521
|
+
max(0.0, _finite(value.get("bottom"))),
|
|
522
|
+
max(0.0, _finite(value.get("right"))),
|
|
523
|
+
)
|
|
524
|
+
uniform = max(0.0, _finite(value))
|
|
525
|
+
return (uniform, uniform, uniform, uniform)
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def _within_hit_rect(widget: Any, x: float, y: float) -> bool:
|
|
529
|
+
"""Whether a widget-relative point is inside the frame plus ``hit_slop``.
|
|
530
|
+
|
|
531
|
+
Falls back to the live Tk size when the layout frame isn't known
|
|
532
|
+
yet, and to "inside" when neither is available (never drop a press
|
|
533
|
+
for lack of geometry).
|
|
534
|
+
"""
|
|
535
|
+
frame = getattr(widget, "_pn_frame", None)
|
|
536
|
+
if frame is not None:
|
|
537
|
+
w, h = frame[2], frame[3]
|
|
538
|
+
else:
|
|
539
|
+
try:
|
|
540
|
+
w, h = float(widget.winfo_width()), float(widget.winfo_height())
|
|
541
|
+
except Exception:
|
|
542
|
+
return True
|
|
543
|
+
top, left, bottom, right = getattr(widget, "_pn_hit_slop", (0.0, 0.0, 0.0, 0.0))
|
|
544
|
+
return (-left <= x <= w + right) and (-top <= y <= h + bottom)
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
# ----------------------------------------------------------------------
|
|
548
|
+
# Rounded corners (Canvas approximation)
|
|
549
|
+
# ----------------------------------------------------------------------
|
|
550
|
+
#
|
|
551
|
+
# Tk widgets are rectangles and can't be clipped, so Frame-based views
|
|
552
|
+
# with a border radius get a full-size Canvas child that paints the
|
|
553
|
+
# background (and a uniform border outline) as a rounded polygon. The
|
|
554
|
+
# corner cutouts can't be transparent; they're filled with the logical
|
|
555
|
+
# parent's background color, which reads correctly whenever the parent
|
|
556
|
+
# paints a solid color behind the view. Content leaves (Text, Button,
|
|
557
|
+
# Image) skip the approximation entirely: a covering canvas would hide
|
|
558
|
+
# what they draw.
|
|
559
|
+
|
|
560
|
+
_RADIUS_ARC_STEPS = 8
|
|
561
|
+
|
|
562
|
+
_SIDE_BORDER_WIDTH_KEYS = (
|
|
563
|
+
"border_left_width",
|
|
564
|
+
"border_top_width",
|
|
565
|
+
"border_right_width",
|
|
566
|
+
"border_bottom_width",
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def _corner_radii(props: Dict[str, Any]) -> Tuple[float, float, float, float]:
|
|
571
|
+
"""Resolve ``(top-left, top-right, bottom-right, bottom-left)`` radii.
|
|
572
|
+
|
|
573
|
+
Per-corner keys fall back to ``border_radius``; missing values are
|
|
574
|
+
0. Negative or unparseable values clamp to 0.
|
|
575
|
+
"""
|
|
576
|
+
base = max(0.0, _finite(props.get("border_radius")))
|
|
577
|
+
|
|
578
|
+
def _one(key: str) -> float:
|
|
579
|
+
value = props.get(key)
|
|
580
|
+
if value is None:
|
|
581
|
+
return base
|
|
582
|
+
return max(0.0, _finite(value, base))
|
|
583
|
+
|
|
584
|
+
return (
|
|
585
|
+
_one("border_top_left_radius"),
|
|
586
|
+
_one("border_top_right_radius"),
|
|
587
|
+
_one("border_bottom_right_radius"),
|
|
588
|
+
_one("border_bottom_left_radius"),
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def _resolve_border(props: Dict[str, Any]) -> Tuple[float, str]:
|
|
593
|
+
"""Resolve the uniform ``(width, color)`` border approximation.
|
|
594
|
+
|
|
595
|
+
Tk borders are uniform, so per-side borders collapse to the widest
|
|
596
|
+
side's width and the first nonzero side's color.
|
|
597
|
+
"""
|
|
598
|
+
width = props.get("border_width")
|
|
599
|
+
color = _tk_color(props.get("border_color")) or "#3c3c43"
|
|
600
|
+
side_widths = [props.get(k) for k in _SIDE_BORDER_WIDTH_KEYS]
|
|
601
|
+
if any(w is not None for w in side_widths):
|
|
602
|
+
width = max(float(w) for w in side_widths if w is not None)
|
|
603
|
+
for side, w in zip(("left", "top", "right", "bottom"), side_widths):
|
|
604
|
+
if w is not None and float(w) > 0:
|
|
605
|
+
side_color = _tk_color(props.get(f"border_{side}_color"))
|
|
606
|
+
if side_color is not None:
|
|
607
|
+
color = side_color
|
|
608
|
+
break
|
|
609
|
+
return (max(0.0, _finite(width)), color)
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
def _underlay_color(widget: Any) -> str:
|
|
613
|
+
"""Best-effort color of whatever paints behind ``widget``.
|
|
614
|
+
|
|
615
|
+
Used to fill the corner cutouts of a rounded view: the logical
|
|
616
|
+
parent's rounded fill when it has one, else its plain Tk
|
|
617
|
+
background. A solid guess is the documented limitation; gradients
|
|
618
|
+
or images behind a rounded view can't be matched.
|
|
619
|
+
"""
|
|
620
|
+
parent = getattr(widget, "_pn_parent", None)
|
|
621
|
+
target = parent if parent is not None else get_root_container()
|
|
622
|
+
if target is not None:
|
|
623
|
+
state = getattr(target, "_pn_radius_state", None)
|
|
624
|
+
if state is not None and state.get("fill"):
|
|
625
|
+
return str(state["fill"])
|
|
626
|
+
try:
|
|
627
|
+
return str(target.cget("background"))
|
|
628
|
+
except Exception:
|
|
629
|
+
pass
|
|
630
|
+
return "#ffffff"
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def _rounded_rect_points(w: float, h: float, radii: Tuple[float, float, float, float]) -> List[float]:
|
|
634
|
+
"""Vertices of a rounded rectangle, corner arcs sampled clockwise.
|
|
635
|
+
|
|
636
|
+
Radii are clamped proportionally so adjacent corners never
|
|
637
|
+
overlap. A zero radius degenerates to the plain corner point.
|
|
638
|
+
"""
|
|
639
|
+
tl, tr, br, bl = radii
|
|
640
|
+
scale = min(
|
|
641
|
+
1.0,
|
|
642
|
+
w / max(1e-6, tl + tr),
|
|
643
|
+
w / max(1e-6, bl + br),
|
|
644
|
+
h / max(1e-6, tl + bl),
|
|
645
|
+
h / max(1e-6, tr + br),
|
|
646
|
+
)
|
|
647
|
+
tl, tr, br, bl = (r * scale for r in (tl, tr, br, bl))
|
|
648
|
+
points: List[float] = []
|
|
649
|
+
|
|
650
|
+
def _arc(cx: float, cy: float, r: float, start_deg: float) -> None:
|
|
651
|
+
if r <= 0:
|
|
652
|
+
points.extend((cx, cy))
|
|
653
|
+
return
|
|
654
|
+
for step in range(_RADIUS_ARC_STEPS + 1):
|
|
655
|
+
a = math.radians(start_deg - 90.0 * step / _RADIUS_ARC_STEPS)
|
|
656
|
+
points.extend((cx + r * math.cos(a), cy - r * math.sin(a)))
|
|
657
|
+
|
|
658
|
+
_arc(tl, tl, tl, 180.0)
|
|
659
|
+
_arc(w - tr, tr, tr, 90.0)
|
|
660
|
+
_arc(w - br, h - br, br, 0.0)
|
|
661
|
+
_arc(bl, h - bl, bl, -90.0)
|
|
662
|
+
return points
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
def _redraw_rounded_background(widget: Any) -> None:
|
|
666
|
+
"""Repaint the rounded background canvas if its inputs changed.
|
|
667
|
+
|
|
668
|
+
Sizes prefer the layout frame (kept current by ``_place``) over
|
|
669
|
+
``winfo_*`` so the polygon is right even before Tk maps the
|
|
670
|
+
window. Redraws are skipped when the (size, colors, radii,
|
|
671
|
+
underlay) signature matches the last paint.
|
|
672
|
+
"""
|
|
673
|
+
canvas = getattr(widget, "_pn_radius_canvas", None)
|
|
674
|
+
state = getattr(widget, "_pn_radius_state", None)
|
|
675
|
+
if canvas is None or state is None:
|
|
676
|
+
return
|
|
677
|
+
frame = getattr(widget, "_pn_frame", None)
|
|
678
|
+
if frame is not None and frame[2] > 0 and frame[3] > 0:
|
|
679
|
+
w, h = frame[2], frame[3]
|
|
680
|
+
else:
|
|
681
|
+
try:
|
|
682
|
+
w, h = float(canvas.winfo_width()), float(canvas.winfo_height())
|
|
683
|
+
except Exception:
|
|
684
|
+
return
|
|
685
|
+
if w <= 1 or h <= 1:
|
|
686
|
+
return
|
|
687
|
+
under = _underlay_color(widget)
|
|
688
|
+
signature = (w, h, state["radii"], state["fill"], state["outline"], state["outline_width"], under)
|
|
689
|
+
if state.get("drawn") == signature:
|
|
690
|
+
return
|
|
691
|
+
state["drawn"] = signature
|
|
692
|
+
try:
|
|
693
|
+
canvas.configure(background=under)
|
|
694
|
+
canvas.delete("all")
|
|
695
|
+
if state["fill"] or state["outline"]:
|
|
696
|
+
canvas.create_polygon(
|
|
697
|
+
_rounded_rect_points(w, h, state["radii"]),
|
|
698
|
+
fill=state["fill"] or "",
|
|
699
|
+
outline=state["outline"],
|
|
700
|
+
width=max(1.0, state["outline_width"]),
|
|
701
|
+
)
|
|
702
|
+
except Exception:
|
|
703
|
+
diagnostics.swallowed("desktop._redraw_rounded_background")
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
def _set_rounded_background(widget: Any, radii: Tuple[float, float, float, float], props: Dict[str, Any]) -> None:
|
|
707
|
+
"""Install (or refresh) the rounded background canvas on ``widget``."""
|
|
708
|
+
border_width, border_color = _resolve_border(props)
|
|
709
|
+
widget._pn_radius_state = {
|
|
710
|
+
"radii": radii,
|
|
711
|
+
"fill": _tk_color(props.get("background_color")),
|
|
712
|
+
"outline": border_color if border_width else "",
|
|
713
|
+
"outline_width": border_width,
|
|
714
|
+
}
|
|
715
|
+
canvas = getattr(widget, "_pn_radius_canvas", None)
|
|
716
|
+
if canvas is None:
|
|
717
|
+
try:
|
|
718
|
+
canvas = tk.Canvas(widget, highlightthickness=0, bd=0)
|
|
719
|
+
canvas.place(x=0, y=0, relwidth=1.0, relheight=1.0)
|
|
720
|
+
# Below any direct Tk children the handler owns (TabBar /
|
|
721
|
+
# SegmentedControl buttons); PythonNative children live on
|
|
722
|
+
# the stage and are lifted above the whole frame anyway.
|
|
723
|
+
# ``Canvas.lower`` is the canvas-item method, so reach for
|
|
724
|
+
# the widget-stacking ``Misc.lower`` explicitly.
|
|
725
|
+
tk.Misc.lower(canvas)
|
|
726
|
+
# Clicks over the frame now land on the covering canvas,
|
|
727
|
+
# so append the frame's bindtag: its gesture and press
|
|
728
|
+
# bindings keep firing, with matching coordinates (the
|
|
729
|
+
# canvas fills the frame exactly).
|
|
730
|
+
canvas.bindtags(tuple(canvas.bindtags()) + (str(widget),))
|
|
731
|
+
canvas.bind("<Configure>", lambda _e: _redraw_rounded_background(widget), add="+")
|
|
732
|
+
except Exception:
|
|
733
|
+
diagnostics.swallowed("desktop._set_rounded_background")
|
|
734
|
+
return
|
|
735
|
+
canvas._pn_parent = widget # wheel hit-testing walks through
|
|
736
|
+
widget._pn_radius_canvas = canvas
|
|
737
|
+
_set_pointer_muted(canvas, _pointer_muted(widget))
|
|
738
|
+
_redraw_rounded_background(widget)
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
def _clear_rounded_background(widget: Any) -> None:
|
|
742
|
+
"""Drop the rounded background canvas when radii return to zero."""
|
|
743
|
+
canvas = getattr(widget, "_pn_radius_canvas", None)
|
|
744
|
+
if canvas is None:
|
|
745
|
+
return
|
|
746
|
+
widget._pn_radius_canvas = None
|
|
747
|
+
widget._pn_radius_state = None
|
|
748
|
+
try:
|
|
749
|
+
canvas.destroy()
|
|
750
|
+
except Exception:
|
|
751
|
+
diagnostics.swallowed("desktop._clear_rounded_background")
|
|
752
|
+
|
|
753
|
+
|
|
350
754
|
def _set_translate_from_transform(widget: Any, spec: Any) -> None:
|
|
351
755
|
"""Extract a translate offset from a ``transform`` prop for placement.
|
|
352
756
|
|
|
@@ -370,44 +774,40 @@ def _set_translate_from_transform(widget: Any, spec: Any) -> None:
|
|
|
370
774
|
|
|
371
775
|
def _apply_common(widget: Any, props: Dict[str, Any]) -> None:
|
|
372
776
|
"""Apply visual props shared across most handlers (bg, border, transform)."""
|
|
373
|
-
if
|
|
777
|
+
radii = _corner_radii(props) if isinstance(widget, tk.Frame) else (0.0, 0.0, 0.0, 0.0)
|
|
778
|
+
rounded = any(r > 0 for r in radii)
|
|
779
|
+
if "background_color" in props and not rounded:
|
|
374
780
|
color = _tk_color(props["background_color"])
|
|
375
781
|
if color is not None:
|
|
376
782
|
try:
|
|
377
783
|
widget.configure(background=color)
|
|
378
784
|
except Exception:
|
|
379
785
|
diagnostics.swallowed("desktop._apply_common")
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
"border_right_width",
|
|
384
|
-
"border_bottom_width",
|
|
385
|
-
)
|
|
386
|
-
if any(k in props for k in ("border_width", "border_color", *side_width_keys)):
|
|
786
|
+
if rounded:
|
|
787
|
+
# Background and border move onto the rounded canvas; the
|
|
788
|
+
# square highlight border would poke out of the corners.
|
|
387
789
|
try:
|
|
388
|
-
|
|
389
|
-
color = _tk_color(props.get("border_color")) or "#3c3c43"
|
|
390
|
-
# Tk highlight borders are uniform, so per-side borders are
|
|
391
|
-
# approximated with the widest side's width and its color.
|
|
392
|
-
side_widths = [props.get(k) for k in side_width_keys]
|
|
393
|
-
if any(w is not None for w in side_widths):
|
|
394
|
-
width = max(float(w) for w in side_widths if w is not None)
|
|
395
|
-
for side, w in zip(("left", "top", "right", "bottom"), side_widths):
|
|
396
|
-
if w is not None and float(w) > 0:
|
|
397
|
-
side_color = _tk_color(props.get(f"border_{side}_color"))
|
|
398
|
-
if side_color is not None:
|
|
399
|
-
color = side_color
|
|
400
|
-
break
|
|
401
|
-
if width:
|
|
402
|
-
widget.configure(
|
|
403
|
-
highlightthickness=int(round(_finite(width))),
|
|
404
|
-
highlightbackground=color,
|
|
405
|
-
highlightcolor=color,
|
|
406
|
-
)
|
|
407
|
-
else:
|
|
408
|
-
widget.configure(highlightthickness=0)
|
|
790
|
+
widget.configure(highlightthickness=0)
|
|
409
791
|
except Exception:
|
|
410
792
|
diagnostics.swallowed("desktop._apply_common")
|
|
793
|
+
_set_rounded_background(widget, radii, props)
|
|
794
|
+
else:
|
|
795
|
+
_clear_rounded_background(widget)
|
|
796
|
+
if any(k in props for k in ("border_width", "border_color", *_SIDE_BORDER_WIDTH_KEYS)):
|
|
797
|
+
try:
|
|
798
|
+
width, color = _resolve_border(props)
|
|
799
|
+
if width:
|
|
800
|
+
widget.configure(
|
|
801
|
+
highlightthickness=int(round(width)),
|
|
802
|
+
highlightbackground=color,
|
|
803
|
+
highlightcolor=color,
|
|
804
|
+
)
|
|
805
|
+
else:
|
|
806
|
+
widget.configure(highlightthickness=0)
|
|
807
|
+
except Exception:
|
|
808
|
+
diagnostics.swallowed("desktop._apply_common")
|
|
809
|
+
if "hit_slop" in props:
|
|
810
|
+
widget._pn_hit_slop = _parse_hit_slop(props.get("hit_slop"))
|
|
411
811
|
if "test_id" in props and props["test_id"] is not None:
|
|
412
812
|
# Stamped for introspection so preview-level tooling and tests
|
|
413
813
|
# can locate widgets the same way Maestro does on device.
|
|
@@ -415,6 +815,13 @@ def _apply_common(widget: Any, props: Dict[str, Any]) -> None:
|
|
|
415
815
|
if "transform" in props:
|
|
416
816
|
_set_translate_from_transform(widget, props["transform"])
|
|
417
817
|
_place(widget)
|
|
818
|
+
if "z_index" in props:
|
|
819
|
+
parent = getattr(widget, "_pn_parent", None)
|
|
820
|
+
if parent is not None:
|
|
821
|
+
parent._pn_has_z = True
|
|
822
|
+
_restack(parent)
|
|
823
|
+
if "pointer_events" in props:
|
|
824
|
+
_refresh_pointer_events(widget)
|
|
418
825
|
|
|
419
826
|
|
|
420
827
|
# ======================================================================
|
|
@@ -538,7 +945,15 @@ def _install_wheel_bindings(container: Any) -> None:
|
|
|
538
945
|
|
|
539
946
|
|
|
540
947
|
def _content_extent(widget: Any) -> Tuple[float, float]:
|
|
541
|
-
"""Max (right, bottom) edge over the scroll container's children.
|
|
948
|
+
"""Max (right, bottom) edge over the scroll container's children.
|
|
949
|
+
|
|
950
|
+
Containers that window their children (``VirtualList``) publish
|
|
951
|
+
the full logical extent via ``_pn_content_extent`` so clamping
|
|
952
|
+
covers rows that aren't currently mounted.
|
|
953
|
+
"""
|
|
954
|
+
override = getattr(widget, "_pn_content_extent", None)
|
|
955
|
+
if override is not None:
|
|
956
|
+
return override
|
|
542
957
|
max_x = 0.0
|
|
543
958
|
max_y = 0.0
|
|
544
959
|
for child in getattr(widget, "_pn_children", []) or []:
|
|
@@ -565,7 +980,12 @@ def _scroll_to(widget: Any, x: float, y: float, fire_event: bool = True) -> None
|
|
|
565
980
|
widget._pn_scroll_offset = new_offset
|
|
566
981
|
for child in getattr(widget, "_pn_children", []) or []:
|
|
567
982
|
_place(child)
|
|
568
|
-
|
|
983
|
+
hook = getattr(widget, "_pn_scroll_hook", None)
|
|
984
|
+
if hook is not None:
|
|
985
|
+
# ``VirtualList`` re-windows its rows and reports the richer
|
|
986
|
+
# native-list scroll payload instead of the default event.
|
|
987
|
+
hook(new_offset, fire_event)
|
|
988
|
+
elif fire_event:
|
|
569
989
|
_fire(widget, "on_scroll", {"x": new_offset[0], "y": new_offset[1]})
|
|
570
990
|
|
|
571
991
|
|
|
@@ -622,6 +1042,15 @@ class DesktopViewHandler(ViewHandler):
|
|
|
622
1042
|
child._pn_parent = parent
|
|
623
1043
|
_register_child(parent, child, index)
|
|
624
1044
|
_place(child)
|
|
1045
|
+
# ``z_index`` applies before insertion (create-time props run
|
|
1046
|
+
# first), so stacking and pointer muting settle here, once the
|
|
1047
|
+
# parent chain is known.
|
|
1048
|
+
merged = getattr(child, "_pn_props", None) or {}
|
|
1049
|
+
if merged.get("z_index") is not None:
|
|
1050
|
+
parent._pn_has_z = True
|
|
1051
|
+
if getattr(parent, "_pn_has_z", False):
|
|
1052
|
+
_restack(parent)
|
|
1053
|
+
_refresh_pointer_events(child)
|
|
625
1054
|
|
|
626
1055
|
def remove_child(self, parent: Any, child: Any) -> None:
|
|
627
1056
|
_unregister_child(parent, child)
|
|
@@ -653,9 +1082,14 @@ class DesktopViewHandler(ViewHandler):
|
|
|
653
1082
|
def set_animated_property(self, native_view: Any, prop_name: str, value: Any) -> None:
|
|
654
1083
|
"""Apply one frame of a Python-driven animation.
|
|
655
1084
|
|
|
656
|
-
Translation maps onto placement;
|
|
657
|
-
``
|
|
658
|
-
|
|
1085
|
+
Translation maps onto placement; ``background_color`` and
|
|
1086
|
+
``color`` map onto ``configure`` and accept every color form
|
|
1087
|
+
``_tk_color`` understands, including the ``"#AARRGGBB"``
|
|
1088
|
+
strings that color interpolations emit (alpha is dropped).
|
|
1089
|
+
Opacity, scale, and rotation (numeric degrees) have no cheap
|
|
1090
|
+
Tk analogue, and other animated style keys have no desktop
|
|
1091
|
+
mapping; those frames are silently skipped (a documented
|
|
1092
|
+
preview limitation).
|
|
659
1093
|
"""
|
|
660
1094
|
if native_view is None:
|
|
661
1095
|
return
|
|
@@ -670,10 +1104,24 @@ class DesktopViewHandler(ViewHandler):
|
|
|
670
1104
|
elif prop_name == "background_color":
|
|
671
1105
|
color = _tk_color(value)
|
|
672
1106
|
if color is not None:
|
|
1107
|
+
state = getattr(native_view, "_pn_radius_state", None)
|
|
1108
|
+
if state is not None:
|
|
1109
|
+
# A rounded view paints its background on the
|
|
1110
|
+
# radius canvas, not the frame itself.
|
|
1111
|
+
state["fill"] = color
|
|
1112
|
+
_redraw_rounded_background(native_view)
|
|
1113
|
+
return
|
|
673
1114
|
try:
|
|
674
1115
|
native_view.configure(background=color)
|
|
675
1116
|
except Exception:
|
|
676
1117
|
diagnostics.swallowed("desktop.DesktopViewHandler.set_animated_property")
|
|
1118
|
+
elif prop_name == "color":
|
|
1119
|
+
color = _tk_color(value)
|
|
1120
|
+
if color is not None:
|
|
1121
|
+
try:
|
|
1122
|
+
native_view.configure(foreground=color)
|
|
1123
|
+
except Exception:
|
|
1124
|
+
diagnostics.swallowed("desktop.DesktopViewHandler.set_animated_property")
|
|
677
1125
|
|
|
678
1126
|
|
|
679
1127
|
# ======================================================================
|
|
@@ -743,6 +1191,257 @@ class ScrollViewHandler(FlexContainerHandler):
|
|
|
743
1191
|
_scroll_to(native_view, sx, sy, fire_event=False)
|
|
744
1192
|
|
|
745
1193
|
|
|
1194
|
+
# ======================================================================
|
|
1195
|
+
# VirtualList (natively virtualized list preview)
|
|
1196
|
+
# ======================================================================
|
|
1197
|
+
|
|
1198
|
+
|
|
1199
|
+
class VirtualListHandler(ScrollViewHandler):
|
|
1200
|
+
"""Preview twin of the native ``VirtualList`` handlers.
|
|
1201
|
+
|
|
1202
|
+
Android backs ``VirtualList`` with a ``RecyclerView`` and iOS with
|
|
1203
|
+
a ``UITableView``; the desktop preview reuses the ScrollView
|
|
1204
|
+
machinery and does its own row windowing so trees that target the
|
|
1205
|
+
native-list contract behave the same way here. Rows within one
|
|
1206
|
+
viewport of the scroll window host a live subtree driven by a
|
|
1207
|
+
nested reconciler (see ``pythonnative.virtual_rows``); rows that
|
|
1208
|
+
leave the window are unmounted.
|
|
1209
|
+
|
|
1210
|
+
Expects props:
|
|
1211
|
+
|
|
1212
|
+
- ``count``: total number of rows.
|
|
1213
|
+
- ``row_height``: uniform row extent in points, or
|
|
1214
|
+
- ``row_heights``: per-row extents.
|
|
1215
|
+
- ``render_row``: ``render_row(index) -> Element`` producing one
|
|
1216
|
+
row's subtree. Called lazily as rows enter the window.
|
|
1217
|
+
- ``shows_scroll_indicator``: accepted and ignored (the preview
|
|
1218
|
+
draws no scroll bar).
|
|
1219
|
+
|
|
1220
|
+
Events (dispatched by tag): ``on_scroll`` with
|
|
1221
|
+
``{"x", "y", "extent", "range"}`` in points, matching the device
|
|
1222
|
+
handlers. ``on_row_press`` is not synthesized: preview rows are
|
|
1223
|
+
live widgets, so presses land on the row's own Pressable / Button
|
|
1224
|
+
children.
|
|
1225
|
+
|
|
1226
|
+
Commands: ``scroll_to_offset`` / ``scroll_to_index`` /
|
|
1227
|
+
``scroll_to_end`` / ``get_scroll_offset``. The ``animated`` flag
|
|
1228
|
+
is accepted and ignored (the preview jumps, like ScrollView).
|
|
1229
|
+
"""
|
|
1230
|
+
|
|
1231
|
+
def build(self, props: Dict[str, Any]) -> Any:
|
|
1232
|
+
from ..virtual_rows import RowHostPool
|
|
1233
|
+
|
|
1234
|
+
frame = super().build(props)
|
|
1235
|
+
frame._pn_vl = {
|
|
1236
|
+
"count": 0,
|
|
1237
|
+
"row_height": 44.0,
|
|
1238
|
+
"row_heights": None,
|
|
1239
|
+
"render_row": None,
|
|
1240
|
+
"starts": [0.0],
|
|
1241
|
+
"pool": RowHostPool(),
|
|
1242
|
+
"cells": {},
|
|
1243
|
+
"cell_width": 0.0,
|
|
1244
|
+
}
|
|
1245
|
+
frame._pn_content_extent = (0.0, 0.0)
|
|
1246
|
+
|
|
1247
|
+
def _hook(offset: Tuple[float, float], fire_event: bool) -> None:
|
|
1248
|
+
self._rewindow(frame)
|
|
1249
|
+
if fire_event:
|
|
1250
|
+
viewport = getattr(frame, "_pn_frame", None)
|
|
1251
|
+
_fire(
|
|
1252
|
+
frame,
|
|
1253
|
+
"on_scroll",
|
|
1254
|
+
{
|
|
1255
|
+
"x": 0.0,
|
|
1256
|
+
"y": offset[1],
|
|
1257
|
+
"extent": viewport[3] if viewport else 0.0,
|
|
1258
|
+
"range": frame._pn_vl["starts"][-1],
|
|
1259
|
+
},
|
|
1260
|
+
)
|
|
1261
|
+
|
|
1262
|
+
frame._pn_scroll_hook = _hook
|
|
1263
|
+
return frame
|
|
1264
|
+
|
|
1265
|
+
def apply(self, frame: Any, props: Dict[str, Any]) -> None:
|
|
1266
|
+
super().apply(frame, props)
|
|
1267
|
+
layout_changed, content_changed = self._read_data_props(frame, props)
|
|
1268
|
+
if layout_changed:
|
|
1269
|
+
# Geometry changed under the mounted window; rebuild it.
|
|
1270
|
+
self._release_rows(frame)
|
|
1271
|
+
self._rewindow(frame)
|
|
1272
|
+
elif content_changed:
|
|
1273
|
+
# Only ``render_row`` changed (a fresh closure every
|
|
1274
|
+
# render); reconcile the live rows in place, mirroring
|
|
1275
|
+
# the device handlers' reload-on-render behavior.
|
|
1276
|
+
self._rebind_rows(frame)
|
|
1277
|
+
|
|
1278
|
+
def set_frame(self, native_view: Any, x: float, y: float, width: float, height: float) -> None:
|
|
1279
|
+
super().set_frame(native_view, x, y, width, height)
|
|
1280
|
+
self._rewindow(native_view)
|
|
1281
|
+
|
|
1282
|
+
def measure_intrinsic(self, native_view: Any, max_width: float, max_height: float) -> Tuple[float, float]:
|
|
1283
|
+
# Fill the available space, like a ScrollView clamped to its
|
|
1284
|
+
# parent; collapse to 0 on unbounded axes (nested lists don't
|
|
1285
|
+
# scroll, matching the device handlers).
|
|
1286
|
+
w = max_width if math.isfinite(max_width) else 0.0
|
|
1287
|
+
h = max_height if math.isfinite(max_height) else 0.0
|
|
1288
|
+
return (max(0.0, w), max(0.0, h))
|
|
1289
|
+
|
|
1290
|
+
def command(self, native_view: Any, name: str, args: Dict[str, Any]) -> Any:
|
|
1291
|
+
info = getattr(native_view, "_pn_vl", None)
|
|
1292
|
+
if info is None:
|
|
1293
|
+
return None
|
|
1294
|
+
if name == "scroll_to_offset":
|
|
1295
|
+
_scroll_to(native_view, 0.0, _finite(args.get("y", 0.0)))
|
|
1296
|
+
return True
|
|
1297
|
+
if name == "scroll_to_index":
|
|
1298
|
+
count = info["count"]
|
|
1299
|
+
index = max(0, min(int(_finite(args.get("index", 0))), max(0, count - 1)))
|
|
1300
|
+
_scroll_to(native_view, 0.0, info["starts"][index] if count else 0.0)
|
|
1301
|
+
return True
|
|
1302
|
+
if name == "scroll_to_end":
|
|
1303
|
+
_scroll_to(native_view, 0.0, info["starts"][-1])
|
|
1304
|
+
return True
|
|
1305
|
+
if name == "get_scroll_offset":
|
|
1306
|
+
sx, sy = getattr(native_view, "_pn_scroll_offset", (0.0, 0.0))
|
|
1307
|
+
return {"x": sx, "y": sy}
|
|
1308
|
+
return None
|
|
1309
|
+
|
|
1310
|
+
def destroy(self, native_view: Any) -> None:
|
|
1311
|
+
info = getattr(native_view, "_pn_vl", None)
|
|
1312
|
+
if info is not None:
|
|
1313
|
+
self._release_rows(native_view)
|
|
1314
|
+
try:
|
|
1315
|
+
info["pool"].release_all()
|
|
1316
|
+
except Exception:
|
|
1317
|
+
diagnostics.swallowed("desktop.VirtualListHandler.destroy")
|
|
1318
|
+
super().destroy(native_view)
|
|
1319
|
+
|
|
1320
|
+
# -- data + windowing ----------------------------------------------
|
|
1321
|
+
|
|
1322
|
+
def _read_data_props(self, frame: Any, props: Dict[str, Any]) -> Tuple[bool, bool]:
|
|
1323
|
+
"""Fold changed data props into ``_pn_vl``.
|
|
1324
|
+
|
|
1325
|
+
Returns ``(layout_changed, content_changed)``: the first is
|
|
1326
|
+
true when row geometry changed (count or extents) and the
|
|
1327
|
+
window must be rebuilt; the second when ``render_row`` changed
|
|
1328
|
+
and live rows only need a rebind.
|
|
1329
|
+
"""
|
|
1330
|
+
info = frame._pn_vl
|
|
1331
|
+
layout_changed = False
|
|
1332
|
+
if "count" in props:
|
|
1333
|
+
info["count"] = int(props.get("count") or 0)
|
|
1334
|
+
layout_changed = True
|
|
1335
|
+
if "row_height" in props and props.get("row_height") is not None:
|
|
1336
|
+
info["row_height"] = max(0.0, _finite(props["row_height"], 44.0))
|
|
1337
|
+
layout_changed = True
|
|
1338
|
+
if "row_heights" in props:
|
|
1339
|
+
heights = props.get("row_heights")
|
|
1340
|
+
info["row_heights"] = [max(0.0, _finite(h)) for h in heights] if heights else None
|
|
1341
|
+
layout_changed = True
|
|
1342
|
+
content_changed = False
|
|
1343
|
+
if "render_row" in props:
|
|
1344
|
+
info["render_row"] = props.get("render_row")
|
|
1345
|
+
content_changed = True
|
|
1346
|
+
if layout_changed:
|
|
1347
|
+
n = info["count"]
|
|
1348
|
+
heights = info["row_heights"]
|
|
1349
|
+
starts = [0.0] * (n + 1)
|
|
1350
|
+
acc = 0.0
|
|
1351
|
+
for i in range(n):
|
|
1352
|
+
starts[i] = acc
|
|
1353
|
+
extent = heights[i] if heights is not None and i < len(heights) else info["row_height"]
|
|
1354
|
+
acc += max(0.0, extent)
|
|
1355
|
+
starts[n] = acc
|
|
1356
|
+
info["starts"] = starts
|
|
1357
|
+
# Publish the full content extent so ``_scroll_to`` clamps
|
|
1358
|
+
# against every row, not just the mounted window.
|
|
1359
|
+
frame._pn_content_extent = (0.0, acc)
|
|
1360
|
+
return (layout_changed, content_changed)
|
|
1361
|
+
|
|
1362
|
+
def _rewindow(self, frame: Any) -> None:
|
|
1363
|
+
"""Mount rows within one viewport of the window, unmount the rest."""
|
|
1364
|
+
info = getattr(frame, "_pn_vl", None)
|
|
1365
|
+
if info is None or info["render_row"] is None:
|
|
1366
|
+
return
|
|
1367
|
+
viewport = getattr(frame, "_pn_frame", None)
|
|
1368
|
+
if viewport is None:
|
|
1369
|
+
return
|
|
1370
|
+
n = info["count"]
|
|
1371
|
+
width = max(0.0, viewport[2])
|
|
1372
|
+
height = max(0.0, viewport[3])
|
|
1373
|
+
if n <= 0 or width <= 0 or height <= 0:
|
|
1374
|
+
self._release_rows(frame)
|
|
1375
|
+
return
|
|
1376
|
+
if width != info["cell_width"]:
|
|
1377
|
+
# Mounted rows were laid out against a different width.
|
|
1378
|
+
self._release_rows(frame)
|
|
1379
|
+
info["cell_width"] = width
|
|
1380
|
+
starts = info["starts"]
|
|
1381
|
+
offset = getattr(frame, "_pn_scroll_offset", (0.0, 0.0))[1]
|
|
1382
|
+
lo = max(0.0, offset - height)
|
|
1383
|
+
hi = offset + 2.0 * height
|
|
1384
|
+
first = max(0, bisect.bisect_right(starts, lo, 0, n) - 1)
|
|
1385
|
+
last = min(n - 1, bisect.bisect_left(starts, hi, 0, n))
|
|
1386
|
+
cells: Dict[int, Any] = info["cells"]
|
|
1387
|
+
for index in [i for i in cells if i < first or i > last]:
|
|
1388
|
+
self._release_row(frame, index)
|
|
1389
|
+
for index in range(first, last + 1):
|
|
1390
|
+
if index in cells:
|
|
1391
|
+
continue
|
|
1392
|
+
cell = tk.Frame(_master(), highlightthickness=0, bd=0)
|
|
1393
|
+
cell._pn_parent = frame
|
|
1394
|
+
cells[index] = cell
|
|
1395
|
+
_register_child(frame, cell, len(getattr(frame, "_pn_children", None) or []))
|
|
1396
|
+
cell._pn_frame = (0.0, starts[index], width, starts[index + 1] - starts[index])
|
|
1397
|
+
_place(cell)
|
|
1398
|
+
self._attach_row(frame, cell, index)
|
|
1399
|
+
|
|
1400
|
+
def _attach_row(self, frame: Any, cell: Any, index: int) -> None:
|
|
1401
|
+
"""Mount (or rebind) row ``index`` into ``cell`` and place its root."""
|
|
1402
|
+
info = frame._pn_vl
|
|
1403
|
+
render_row = info["render_row"]
|
|
1404
|
+
cell_frame = getattr(cell, "_pn_frame", (0.0, 0.0, 0.0, 0.0))
|
|
1405
|
+
try:
|
|
1406
|
+
root = info["pool"].bind(index, lambda: render_row(index), cell_frame[2], cell_frame[3])
|
|
1407
|
+
except Exception:
|
|
1408
|
+
diagnostics.swallowed("desktop.VirtualListHandler._attach_row")
|
|
1409
|
+
return
|
|
1410
|
+
if root is None:
|
|
1411
|
+
return
|
|
1412
|
+
# A rebind can replace the subtree's root, so reset the cell's
|
|
1413
|
+
# child list instead of accumulating stale widgets.
|
|
1414
|
+
root._pn_parent = cell
|
|
1415
|
+
cell._pn_children = [root]
|
|
1416
|
+
_place(root)
|
|
1417
|
+
|
|
1418
|
+
def _rebind_rows(self, frame: Any) -> None:
|
|
1419
|
+
info = frame._pn_vl
|
|
1420
|
+
for index, cell in list(info["cells"].items()):
|
|
1421
|
+
self._attach_row(frame, cell, index)
|
|
1422
|
+
|
|
1423
|
+
def _release_row(self, frame: Any, index: int) -> None:
|
|
1424
|
+
info = frame._pn_vl
|
|
1425
|
+
cell = info["cells"].pop(index, None)
|
|
1426
|
+
try:
|
|
1427
|
+
info["pool"].release(index)
|
|
1428
|
+
except Exception:
|
|
1429
|
+
diagnostics.swallowed("desktop.VirtualListHandler._release_row")
|
|
1430
|
+
if cell is not None:
|
|
1431
|
+
_unregister_child(frame, cell)
|
|
1432
|
+
try:
|
|
1433
|
+
cell.destroy()
|
|
1434
|
+
except Exception:
|
|
1435
|
+
diagnostics.swallowed("desktop.VirtualListHandler._release_row")
|
|
1436
|
+
|
|
1437
|
+
def _release_rows(self, frame: Any) -> None:
|
|
1438
|
+
info = getattr(frame, "_pn_vl", None)
|
|
1439
|
+
if info is None:
|
|
1440
|
+
return
|
|
1441
|
+
for index in list(info["cells"]):
|
|
1442
|
+
self._release_row(frame, index)
|
|
1443
|
+
|
|
1444
|
+
|
|
746
1445
|
# ======================================================================
|
|
747
1446
|
# Text
|
|
748
1447
|
# ======================================================================
|
|
@@ -1239,7 +1938,16 @@ class WebViewHandler(DesktopViewHandler):
|
|
|
1239
1938
|
|
|
1240
1939
|
|
|
1241
1940
|
class PressableHandler(DesktopViewHandler):
|
|
1242
|
-
"""A frame that forwards press / long-press / gestures.
|
|
1941
|
+
"""A frame that forwards press / long-press / gestures.
|
|
1942
|
+
|
|
1943
|
+
``hit_slop`` support is partial (see ``_within_hit_rect``): the
|
|
1944
|
+
slop insets can't grow the initial press target, but they expand
|
|
1945
|
+
the tracking rect, so a release (or pointer drift during a pending
|
|
1946
|
+
long press) within the slop of the frame's edges still counts as
|
|
1947
|
+
inside. A release outside the expanded rect fires ``on_press_out``
|
|
1948
|
+
without ``on_press``, matching the device backends' cancel-on-exit
|
|
1949
|
+
behavior.
|
|
1950
|
+
"""
|
|
1243
1951
|
|
|
1244
1952
|
def build(self, props: Dict[str, Any]) -> Any:
|
|
1245
1953
|
frame = tk.Frame(_master(), highlightthickness=0, bd=0, cursor="hand2")
|
|
@@ -1252,7 +1960,8 @@ class PressableHandler(DesktopViewHandler):
|
|
|
1252
1960
|
frame._pn_long_fired = False
|
|
1253
1961
|
self._cancel_long(frame)
|
|
1254
1962
|
_fire(frame, "on_press_out")
|
|
1255
|
-
|
|
1963
|
+
inside = event is None or _within_hit_rect(frame, float(event.x), float(event.y))
|
|
1964
|
+
if not fired_long and inside:
|
|
1256
1965
|
_fire(frame, "on_press")
|
|
1257
1966
|
|
|
1258
1967
|
def _on_press_down(_event: Any = None) -> None:
|
|
@@ -1265,13 +1974,19 @@ class PressableHandler(DesktopViewHandler):
|
|
|
1265
1974
|
frame._pn_long_fired = True
|
|
1266
1975
|
_fire(frame, "on_long_press")
|
|
1267
1976
|
|
|
1268
|
-
def
|
|
1269
|
-
|
|
1977
|
+
def _on_drift(event: Any = None) -> None:
|
|
1978
|
+
# A pending long press survives pointer drift inside the
|
|
1979
|
+
# hit rect (frame plus hit_slop). <Leave> fires once at
|
|
1980
|
+
# the edge crossing, so <B1-Motion> covers travel beyond
|
|
1981
|
+
# it; Tk's implicit grab keeps reporting positions.
|
|
1982
|
+
if event is None or not _within_hit_rect(frame, float(event.x), float(event.y)):
|
|
1983
|
+
self._cancel_long(frame)
|
|
1270
1984
|
|
|
1271
1985
|
try:
|
|
1272
1986
|
frame.bind("<ButtonRelease-1>", _on_release, add="+")
|
|
1273
1987
|
frame.bind("<ButtonPress-1>", _on_press_down, add="+")
|
|
1274
|
-
frame.bind("<
|
|
1988
|
+
frame.bind("<B1-Motion>", _on_drift, add="+")
|
|
1989
|
+
frame.bind("<Leave>", _on_drift, add="+")
|
|
1275
1990
|
except Exception:
|
|
1276
1991
|
diagnostics.swallowed("desktop.PressableHandler._bind")
|
|
1277
1992
|
|
|
@@ -1597,9 +2312,12 @@ def register_handlers(registry: Any) -> None:
|
|
|
1597
2312
|
"""Register every built-in desktop handler on ``registry``.
|
|
1598
2313
|
|
|
1599
2314
|
Mirrors ``register_handlers`` in the iOS / Android backends so the
|
|
1600
|
-
desktop registry services the same element types
|
|
1601
|
-
|
|
1602
|
-
|
|
2315
|
+
desktop registry services the same element types, including
|
|
2316
|
+
``VirtualList``. ``FlatList`` and ``SectionList`` still pick the
|
|
2317
|
+
Python-windowed engine on desktop (``_native_lists_supported`` is
|
|
2318
|
+
false here), but trees that emit ``VirtualList`` directly, and
|
|
2319
|
+
tests that exercise the native-list routing, get the same command
|
|
2320
|
+
and prop contract in the preview.
|
|
1603
2321
|
"""
|
|
1604
2322
|
flex = FlexContainerHandler()
|
|
1605
2323
|
registry.register("View", flex)
|
|
@@ -1627,3 +2345,4 @@ def register_handlers(registry: Any) -> None:
|
|
|
1627
2345
|
registry.register("Checkbox", CheckboxHandler())
|
|
1628
2346
|
registry.register("SegmentedControl", SegmentedControlHandler())
|
|
1629
2347
|
registry.register("DatePicker", DatePickerHandler())
|
|
2348
|
+
registry.register("VirtualList", VirtualListHandler())
|