pythonnative 0.22.0__py3-none-any.whl → 0.23.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.
Files changed (53) hide show
  1. pythonnative/__init__.py +16 -2
  2. pythonnative/animated.py +6 -6
  3. pythonnative/appearance.py +124 -0
  4. pythonnative/cli/pn.py +1 -1
  5. pythonnative/components.py +718 -48
  6. pythonnative/events.py +5 -5
  7. pythonnative/gestures.py +3 -3
  8. pythonnative/hooks.py +44 -3
  9. pythonnative/hot_reload.py +4 -4
  10. pythonnative/images.py +196 -0
  11. pythonnative/layout.py +3 -3
  12. pythonnative/mutations.py +4 -13
  13. pythonnative/native_modules/camera.py +1 -1
  14. pythonnative/native_modules/haptics.py +2 -2
  15. pythonnative/native_modules/location.py +1 -1
  16. pythonnative/native_modules/permissions.py +2 -2
  17. pythonnative/native_modules/secure_store.py +1 -1
  18. pythonnative/native_views/__init__.py +1 -7
  19. pythonnative/native_views/android.py +592 -64
  20. pythonnative/native_views/base.py +3 -3
  21. pythonnative/native_views/desktop.py +85 -26
  22. pythonnative/native_views/ios.py +762 -74
  23. pythonnative/navigation.py +4 -4
  24. pythonnative/net.py +3 -3
  25. pythonnative/platform.py +1 -1
  26. pythonnative/platform_metrics.py +5 -5
  27. pythonnative/preview.py +34 -3
  28. pythonnative/project/builder.py +1 -1
  29. pythonnative/project/config.py +4 -12
  30. pythonnative/project/doctor.py +2 -2
  31. pythonnative/project/icons.py +1 -1
  32. pythonnative/project/ios.py +1 -1
  33. pythonnative/project/permissions.py +2 -2
  34. pythonnative/project/runtime_assets.py +3 -3
  35. pythonnative/reconciler.py +9 -9
  36. pythonnative/runtime.py +8 -8
  37. pythonnative/screen.py +90 -5
  38. pythonnative/sdk/_components.py +1 -1
  39. pythonnative/storage.py +3 -3
  40. pythonnative/style.py +66 -7
  41. pythonnative/templates/android_template/app/src/main/AndroidManifest.xml +2 -1
  42. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNAccessibilityDelegate.kt +47 -0
  43. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNBorderDrawable.kt +67 -0
  44. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNVirtualListView.java +172 -0
  45. pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +2 -2
  46. pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITests.swift +1 -1
  47. pythonnative/virtual_rows.py +137 -0
  48. {pythonnative-0.22.0.dist-info → pythonnative-0.23.0.dist-info}/METADATA +13 -13
  49. {pythonnative-0.22.0.dist-info → pythonnative-0.23.0.dist-info}/RECORD +53 -47
  50. {pythonnative-0.22.0.dist-info → pythonnative-0.23.0.dist-info}/WHEEL +1 -1
  51. {pythonnative-0.22.0.dist-info → pythonnative-0.23.0.dist-info}/entry_points.txt +0 -0
  52. {pythonnative-0.22.0.dist-info → pythonnative-0.23.0.dist-info}/licenses/LICENSE +0 -0
  53. {pythonnative-0.22.0.dist-info → pythonnative-0.23.0.dist-info}/top_level.txt +0 -0
pythonnative/storage.py CHANGED
@@ -321,7 +321,7 @@ class AsyncStorage:
321
321
  """Return the JSON-decoded value stored at ``key``, or ``None``.
322
322
 
323
323
  If the stored value isn't valid JSON, returns ``None`` rather
324
- than raising assume the entry was written by another
324
+ than raising; assume the entry was written by another
325
325
  process or an older version of the app.
326
326
  """
327
327
  raw = await AsyncStorage.get(key)
@@ -347,7 +347,7 @@ def use_persisted_state(
347
347
  Backed by [`AsyncStorage`][pythonnative.storage.AsyncStorage]:
348
348
  behaves like ``use_state`` but loads the prior value (if any) on
349
349
  mount and persists every subsequent update. Until the load
350
- completes the value is ``initial`` the same fallback React
350
+ completes the value is ``initial``, the same fallback React
351
351
  Native users get with ``AsyncStorage.getItem``.
352
352
 
353
353
  The setter accepts either a value or a ``current -> new``
@@ -362,7 +362,7 @@ def use_persisted_state(
362
362
  initial: Value used before the first load completes.
363
363
 
364
364
  Returns:
365
- ``(value, setter)`` same shape as
365
+ ``(value, setter)``, same shape as
366
366
  [`use_state`][pythonnative.use_state].
367
367
 
368
368
  Example:
pythonnative/style.py CHANGED
@@ -36,7 +36,7 @@ Example:
36
36
 
37
37
  from typing import Any, Dict, List, Literal, Optional, Tuple, TypedDict, Union
38
38
 
39
- from .hooks import Context, create_context
39
+ from .hooks import Context, create_context, use_color_scheme, use_context
40
40
 
41
41
  # ======================================================================
42
42
  # Atomic value types
@@ -208,7 +208,7 @@ class Style(TypedDict, total=False):
208
208
  [`Literal`][typing.Literal]. ``Style`` is a `total=False` TypedDict
209
209
  so any subset of keys is valid at construction time.
210
210
 
211
- Custom native components may accept additional, unlisted keys
211
+ Custom native components may accept additional, unlisted keys;
212
212
  they are ignored by the built-in handlers but flow through the
213
213
  reconciler unmodified, so third-party handlers can read them.
214
214
 
@@ -283,6 +283,14 @@ class Style(TypedDict, total=False):
283
283
  # --- Visual: borders ---
284
284
  border_width: float
285
285
  border_radius: float
286
+ border_top_width: float
287
+ border_right_width: float
288
+ border_bottom_width: float
289
+ border_left_width: float
290
+ border_top_color: Color
291
+ border_right_color: Color
292
+ border_bottom_color: Color
293
+ border_left_color: Color
286
294
 
287
295
  # --- Visual: typography ---
288
296
  font_size: float
@@ -481,6 +489,7 @@ DEFAULT_LIGHT_THEME: Dict[str, Any] = {
481
489
  "spacing_large": 16,
482
490
  "border_radius": 8,
483
491
  }
492
+ """Built-in light theme selected by [`use_theme`][pythonnative.use_theme]."""
484
493
 
485
494
  DEFAULT_DARK_THEME: Dict[str, Any] = {
486
495
  "primary_color": "#0A84FF",
@@ -500,17 +509,65 @@ DEFAULT_DARK_THEME: Dict[str, Any] = {
500
509
  "spacing_large": 16,
501
510
  "border_radius": 8,
502
511
  }
512
+ """Built-in dark theme selected by [`use_theme`][pythonnative.use_theme]."""
503
513
 
504
- ThemeContext: Context = create_context(DEFAULT_LIGHT_THEME)
505
- """Default theme context populated with `DEFAULT_LIGHT_THEME`.
514
+ _FOLLOW_SYSTEM_THEME = object()
515
+ """Sentinel default for `ThemeContext`: resolve from the color scheme."""
506
516
 
507
- Wrap a subtree in
517
+ ThemeContext: Context = create_context(_FOLLOW_SYSTEM_THEME)
518
+ """Theme context that follows the system color scheme by default.
519
+
520
+ Without a provider, [`use_theme`][pythonnative.use_theme] resolves to
521
+ [`DEFAULT_LIGHT_THEME`][pythonnative.style.DEFAULT_LIGHT_THEME] or
522
+ [`DEFAULT_DARK_THEME`][pythonnative.style.DEFAULT_DARK_THEME] based on
523
+ the current appearance. Wrap a subtree in
508
524
  [`Provider(ThemeContext, my_theme, ...)`][pythonnative.Provider] to
509
- override the theme for that subtree, then read it inside descendants
510
- via [`use_context(ThemeContext)`][pythonnative.use_context].
525
+ pin an explicit theme for that subtree, then read it inside
526
+ descendants via [`use_theme`][pythonnative.use_theme] (or
527
+ [`use_context(ThemeContext)`][pythonnative.use_context]).
511
528
  """
512
529
 
513
530
 
531
+ def default_theme(scheme: str) -> Dict[str, Any]:
532
+ """Return the built-in theme dict for ``scheme`` (``"light"`` / ``"dark"``)."""
533
+ return DEFAULT_DARK_THEME if scheme == "dark" else DEFAULT_LIGHT_THEME
534
+
535
+
536
+ def use_theme() -> Dict[str, Any]:
537
+ """Return the active theme, following the system appearance by default.
538
+
539
+ If an ancestor mounted a ``Provider(ThemeContext, ...)``, that
540
+ theme is returned as-is. Otherwise the built-in light or dark
541
+ theme is selected from the effective color scheme (via
542
+ [`use_color_scheme`][pythonnative.use_color_scheme], so the
543
+ component re-renders when the system appearance flips).
544
+
545
+ Returns:
546
+ The active theme dict.
547
+
548
+ Raises:
549
+ RuntimeError: If called outside a `@component` function.
550
+
551
+ Example:
552
+ ```python
553
+ import pythonnative as pn
554
+
555
+ @pn.component
556
+ def Card():
557
+ theme = pn.use_theme()
558
+ return pn.View(
559
+ pn.Text("Hello", style=pn.style(color=theme["text_color"])),
560
+ style=pn.style(background_color=theme["surface_color"]),
561
+ )
562
+ ```
563
+ """
564
+ scheme = use_color_scheme()
565
+ theme = use_context(ThemeContext)
566
+ if theme is _FOLLOW_SYSTEM_THEME:
567
+ return default_theme(scheme)
568
+ return theme
569
+
570
+
514
571
  __all__ = [
515
572
  "AlignItems",
516
573
  "AlignSelf",
@@ -543,6 +600,8 @@ __all__ = [
543
600
  "TransformScaleY",
544
601
  "TransformSpec",
545
602
  "TransformTranslate",
603
+ "default_theme",
546
604
  "resolve_style",
547
605
  "style",
606
+ "use_theme",
548
607
  ]
@@ -14,7 +14,8 @@
14
14
  tools:targetApi="31">
15
15
  <activity
16
16
  android:name=".MainActivity"
17
- android:exported="true">
17
+ android:exported="true"
18
+ android:windowSoftInputMode="adjustResize">
18
19
  <intent-filter>
19
20
  <action android:name="android.intent.action.MAIN" />
20
21
 
@@ -0,0 +1,47 @@
1
+ package com.pythonnative.android_template
2
+
3
+ import android.os.Build
4
+ import android.view.View
5
+ import android.view.accessibility.AccessibilityNodeInfo
6
+
7
+ /**
8
+ * Accessibility delegate that exposes PythonNative's `test_id` and
9
+ * `accessibility_state` props through the Android accessibility tree.
10
+ *
11
+ * `test_id` is surfaced as the node's view-id resource name, which
12
+ * UI Automator-based tools (Maestro, UiAutomator2, Appium) match as
13
+ * `resource-id`. State flags map onto the closest
14
+ * [AccessibilityNodeInfo] equivalents so TalkBack announces them.
15
+ *
16
+ * Instances are created and configured from Python (the Android view
17
+ * handlers); fields left null are not applied.
18
+ */
19
+ class PNAccessibilityDelegate : View.AccessibilityDelegate() {
20
+ var testId: String? = null
21
+ var stateDisabled: Boolean? = null
22
+ var stateSelected: Boolean? = null
23
+ var stateChecked: Boolean? = null
24
+ var stateBusy: Boolean? = null
25
+ var stateExpanded: Boolean? = null
26
+
27
+ override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfo) {
28
+ super.onInitializeAccessibilityNodeInfo(host, info)
29
+ testId?.let { info.viewIdResourceName = it }
30
+ stateDisabled?.let { info.isEnabled = !it }
31
+ stateSelected?.let { info.isSelected = it }
32
+ stateChecked?.let {
33
+ info.isCheckable = true
34
+ info.isChecked = it
35
+ }
36
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
37
+ val states = mutableListOf<String>()
38
+ if (stateBusy == true) states.add("busy")
39
+ when (stateExpanded) {
40
+ true -> states.add("expanded")
41
+ false -> states.add("collapsed")
42
+ null -> {}
43
+ }
44
+ if (states.isNotEmpty()) info.stateDescription = states.joinToString(", ")
45
+ }
46
+ }
47
+ }
@@ -0,0 +1,67 @@
1
+ package com.pythonnative.android_template
2
+
3
+ import android.graphics.Canvas
4
+ import android.graphics.ColorFilter
5
+ import android.graphics.Paint
6
+ import android.graphics.PixelFormat
7
+ import android.graphics.RectF
8
+ import android.graphics.drawable.Drawable
9
+
10
+ /**
11
+ * Background drawable that renders an optional rounded fill plus four
12
+ * independent border strips. Used for PythonNative's
13
+ * `border_<side>_width` / `border_<side>_color` style props, which
14
+ * `GradientDrawable`'s single uniform stroke cannot express.
15
+ *
16
+ * Widths are in pixels; sides are ordered left, top, right, bottom.
17
+ */
18
+ class PNBorderDrawable(
19
+ private val hasBackground: Boolean,
20
+ private val backgroundColor: Int,
21
+ private val cornerRadius: Float,
22
+ private val widths: FloatArray,
23
+ private val colors: IntArray,
24
+ ) : Drawable() {
25
+ private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
26
+
27
+ override fun draw(canvas: Canvas) {
28
+ val b = RectF(bounds)
29
+ if (hasBackground) {
30
+ paint.style = Paint.Style.FILL
31
+ paint.color = backgroundColor
32
+ if (cornerRadius > 0f) {
33
+ canvas.drawRoundRect(b, cornerRadius, cornerRadius, paint)
34
+ } else {
35
+ canvas.drawRect(b, paint)
36
+ }
37
+ }
38
+ paint.style = Paint.Style.FILL
39
+ if (widths[0] > 0f) {
40
+ paint.color = colors[0]
41
+ canvas.drawRect(b.left, b.top, b.left + widths[0], b.bottom, paint)
42
+ }
43
+ if (widths[1] > 0f) {
44
+ paint.color = colors[1]
45
+ canvas.drawRect(b.left, b.top, b.right, b.top + widths[1], paint)
46
+ }
47
+ if (widths[2] > 0f) {
48
+ paint.color = colors[2]
49
+ canvas.drawRect(b.right - widths[2], b.top, b.right, b.bottom, paint)
50
+ }
51
+ if (widths[3] > 0f) {
52
+ paint.color = colors[3]
53
+ canvas.drawRect(b.left, b.bottom - widths[3], b.right, b.bottom, paint)
54
+ }
55
+ }
56
+
57
+ override fun setAlpha(alpha: Int) {
58
+ paint.alpha = alpha
59
+ }
60
+
61
+ override fun setColorFilter(colorFilter: ColorFilter?) {
62
+ paint.colorFilter = colorFilter
63
+ }
64
+
65
+ @Deprecated("Deprecated in Java")
66
+ override fun getOpacity(): Int = PixelFormat.TRANSLUCENT
67
+ }
@@ -0,0 +1,172 @@
1
+ package com.pythonnative.android_template;
2
+
3
+ import android.content.Context;
4
+ import android.util.TypedValue;
5
+ import android.view.ViewGroup;
6
+ import android.widget.FrameLayout;
7
+
8
+ import androidx.annotation.NonNull;
9
+ import androidx.recyclerview.widget.LinearLayoutManager;
10
+ import androidx.recyclerview.widget.RecyclerView;
11
+
12
+ /**
13
+ * RecyclerView-backed virtualized list used by PythonNative's Android bridge.
14
+ *
15
+ * <p>Chaquopy can proxy Java interfaces from Python, but it can't subclass Java
16
+ * abstract classes such as RecyclerView.Adapter or RecyclerView.ViewHolder. This
17
+ * helper owns those abstract-class implementations in Java and delegates row
18
+ * content back to Python through the small Delegate interface below.</p>
19
+ *
20
+ * <p>Rows may have per-position heights (section lists interleave headers and
21
+ * items); the delegate reports each row's height in dp. Scroll changes are
22
+ * forwarded to the delegate in dp so the Python side can drive on_scroll,
23
+ * on_end_reached, and viewability callbacks with one code path per platform.</p>
24
+ */
25
+ public class PNVirtualListView extends RecyclerView {
26
+ public interface Delegate {
27
+ int getCount();
28
+
29
+ float getRowHeightDp(int position);
30
+
31
+ void mountRow(int position, FrameLayout container, float widthDp, float heightDp);
32
+
33
+ void onRowPress(int position);
34
+
35
+ void onRowRecycled(FrameLayout container);
36
+
37
+ void onScrolled(float offsetDp, float extentDp, float rangeDp);
38
+ }
39
+
40
+ private final float density;
41
+ private final RowAdapter rowAdapter;
42
+ private Delegate delegate;
43
+
44
+ public PNVirtualListView(@NonNull Context context, @NonNull Delegate delegate) {
45
+ super(context);
46
+ this.delegate = delegate;
47
+ this.density = context.getResources().getDisplayMetrics().density;
48
+ setLayoutManager(new LinearLayoutManager(context));
49
+ rowAdapter = new RowAdapter();
50
+ setAdapter(rowAdapter);
51
+ addOnScrollListener(new OnScrollListener() {
52
+ @Override
53
+ public void onScrolled(@NonNull RecyclerView rv, int dx, int dy) {
54
+ PNVirtualListView.this.delegate.onScrolled(
55
+ rv.computeVerticalScrollOffset() / PNVirtualListView.this.density,
56
+ rv.computeVerticalScrollExtent() / PNVirtualListView.this.density,
57
+ rv.computeVerticalScrollRange() / PNVirtualListView.this.density
58
+ );
59
+ }
60
+ });
61
+ }
62
+
63
+ public void setDelegate(@NonNull Delegate delegate) {
64
+ this.delegate = delegate;
65
+ rowAdapter.notifyDataSetChanged();
66
+ }
67
+
68
+ public void notifyDataChanged() {
69
+ rowAdapter.notifyDataSetChanged();
70
+ }
71
+
72
+ public void scrollToOffsetDp(float offsetDp, boolean animated) {
73
+ int targetPx = Math.round(offsetDp * density);
74
+ int currentPx = computeVerticalScrollOffset();
75
+ if (animated) {
76
+ smoothScrollBy(0, targetPx - currentPx);
77
+ } else {
78
+ scrollBy(0, targetPx - currentPx);
79
+ }
80
+ }
81
+
82
+ public void scrollToIndex(int position, boolean animated) {
83
+ int clamped = Math.max(0, Math.min(position, Math.max(0, delegate.getCount() - 1)));
84
+ if (animated) {
85
+ smoothScrollToPosition(clamped);
86
+ } else {
87
+ scrollToPosition(clamped);
88
+ }
89
+ }
90
+
91
+ private int rowHeightPx(int position) {
92
+ return Math.max(
93
+ 1,
94
+ Math.round(TypedValue.applyDimension(
95
+ TypedValue.COMPLEX_UNIT_DIP,
96
+ delegate.getRowHeightDp(position),
97
+ getResources().getDisplayMetrics()
98
+ ))
99
+ );
100
+ }
101
+
102
+ private float currentWidthDp(FrameLayout container) {
103
+ int widthPx = container.getWidth();
104
+ if (widthPx <= 0) {
105
+ widthPx = getWidth();
106
+ }
107
+ if (widthPx <= 0) {
108
+ widthPx = getResources().getDisplayMetrics().widthPixels;
109
+ }
110
+ return widthPx / density;
111
+ }
112
+
113
+ private class RowHolder extends RecyclerView.ViewHolder {
114
+ final FrameLayout container;
115
+
116
+ RowHolder(@NonNull FrameLayout container) {
117
+ super(container);
118
+ this.container = container;
119
+ }
120
+ }
121
+
122
+ private class RowAdapter extends RecyclerView.Adapter<RowHolder> {
123
+ @Override
124
+ public int getItemCount() {
125
+ return delegate.getCount();
126
+ }
127
+
128
+ @NonNull
129
+ @Override
130
+ public RowHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
131
+ FrameLayout container = new FrameLayout(parent.getContext());
132
+ container.setLayoutParams(
133
+ new RecyclerView.LayoutParams(
134
+ ViewGroup.LayoutParams.MATCH_PARENT,
135
+ ViewGroup.LayoutParams.WRAP_CONTENT
136
+ )
137
+ );
138
+ return new RowHolder(container);
139
+ }
140
+
141
+ @Override
142
+ public void onBindViewHolder(@NonNull RowHolder holder, int position) {
143
+ RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) holder.container.getLayoutParams();
144
+ int heightPx = rowHeightPx(position);
145
+ if (params.height != heightPx) {
146
+ params.height = heightPx;
147
+ holder.container.setLayoutParams(params);
148
+ }
149
+ holder.container.removeAllViews();
150
+ holder.container.setOnClickListener(v -> {
151
+ int pos = holder.getBindingAdapterPosition();
152
+ if (pos != RecyclerView.NO_POSITION) {
153
+ delegate.onRowPress(pos);
154
+ }
155
+ });
156
+ delegate.mountRow(
157
+ position,
158
+ holder.container,
159
+ currentWidthDp(holder.container),
160
+ delegate.getRowHeightDp(position)
161
+ );
162
+ }
163
+
164
+ @Override
165
+ public void onViewRecycled(@NonNull RowHolder holder) {
166
+ holder.container.removeAllViews();
167
+ holder.container.setOnClickListener(null);
168
+ delegate.onRowRecycled(holder.container);
169
+ super.onViewRecycled(holder);
170
+ }
171
+ }
172
+ }
@@ -253,8 +253,8 @@
253
253
  isa = XCRemoteSwiftPackageReference;
254
254
  repositoryURL = "https://github.com/pvieito/PythonKit";
255
255
  requirement = {
256
- branch = master;
257
- kind = branch;
256
+ kind = upToNextMinorVersion;
257
+ minimumVersion = 0.5.1;
258
258
  };
259
259
  };
260
260
  /* End XCRemoteSwiftPackageReference section */
@@ -15,7 +15,7 @@ final class ios_templateUITests: XCTestCase {
15
15
  // In UI tests it is usually best to stop immediately when a failure occurs.
16
16
  continueAfterFailure = false
17
17
 
18
- // In UI tests its important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
18
+ // In UI tests it's important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
19
19
  }
20
20
 
21
21
  override func tearDownWithError() throws {
@@ -0,0 +1,137 @@
1
+ """Row subtrees for natively virtualized lists.
2
+
3
+ Native virtualized lists (Android ``RecyclerView`` via
4
+ ``PNVirtualListView``, iOS ``UITableView``) own row lifecycle on the
5
+ platform side: the platform decides when a row enters the viewport,
6
+ hands PythonNative an empty native container, and recycles that
7
+ container when the row scrolls away.
8
+
9
+ Each visible row hosts a full PythonNative subtree driven by its own
10
+ nested [`Reconciler`][pythonnative.reconciler.Reconciler]. The
11
+ [`RowSubtree`][pythonnative.virtual_rows.RowSubtree] class wraps that
12
+ per-row reconciler: it mounts the row element into fresh native views,
13
+ re-reconciles in place when the platform rebinds the same container to
14
+ a different row index (recycling), and tears everything down when the
15
+ list is destroyed.
16
+
17
+ State inside rows works: hooks like ``use_state`` mark the row's own
18
+ reconciler dirty and a guarded flush re-renders just that row.
19
+ """
20
+
21
+ from typing import Any, Callable, Dict
22
+
23
+ from .element import Element
24
+
25
+ __all__ = ["RowSubtree", "RowHostPool"]
26
+
27
+
28
+ class RowSubtree:
29
+ """One list row's mounted PythonNative subtree.
30
+
31
+ Owns a nested reconciler whose viewport is the row's cell size.
32
+ The native root view produced by
33
+ [`mount`][pythonnative.virtual_rows.RowSubtree.mount] is what the
34
+ platform handler attaches to the recycled cell container.
35
+ """
36
+
37
+ def __init__(self) -> None:
38
+ from .native_views import get_registry
39
+ from .reconciler import Reconciler
40
+
41
+ self._reconciler = Reconciler(get_registry())
42
+ # Guarded synchronous flush so ``use_state`` inside a row
43
+ # re-renders the row. Re-entrant requests (a setter firing
44
+ # during the flush itself) are queued and drained after.
45
+ self._flushing = False
46
+ self._flush_queued = False
47
+ self._reconciler._screen_re_render = self._request_flush
48
+ self.native_root: Any = None
49
+
50
+ def _request_flush(self) -> None:
51
+ if self._flushing:
52
+ self._flush_queued = True
53
+ return
54
+ self._flushing = True
55
+ try:
56
+ for _ in range(8):
57
+ self._flush_queued = False
58
+ self.native_root = self._reconciler.flush_dirty()
59
+ if not self._flush_queued:
60
+ break
61
+ finally:
62
+ self._flushing = False
63
+
64
+ def mount(self, element: Element, width: float, height: float) -> Any:
65
+ """Mount ``element`` at the given cell size; returns the native root."""
66
+ self.native_root = self._reconciler.mount(element)
67
+ self._reconciler.set_viewport_size(float(width), float(height))
68
+ return self.native_root
69
+
70
+ def rebind(self, element: Element, width: float, height: float) -> Any:
71
+ """Reconcile the subtree to ``element`` (cell recycled to a new row).
72
+
73
+ Cheaper than unmount + mount when consecutive rows share a
74
+ shape, which is the overwhelmingly common case in lists.
75
+ Returns the (possibly replaced) native root.
76
+ """
77
+ self._reconciler.set_viewport_size(float(width), float(height))
78
+ self.native_root = self._reconciler.reconcile(element)
79
+ return self.native_root
80
+
81
+ def unmount(self) -> None:
82
+ """Destroy the subtree and release its native views."""
83
+ try:
84
+ self._reconciler.unmount()
85
+ except Exception:
86
+ pass
87
+ self.native_root = None
88
+
89
+
90
+ class RowHostPool:
91
+ """Per-list bookkeeping of row subtrees keyed by native container.
92
+
93
+ Platform handlers create one pool per virtualized list view. Keys
94
+ are platform-stable container identities (Java identity hash on
95
+ Android, ``contentView`` pointer on iOS).
96
+ """
97
+
98
+ def __init__(self) -> None:
99
+ self._subtrees: Dict[int, RowSubtree] = {}
100
+
101
+ def bind(
102
+ self,
103
+ container_key: int,
104
+ make_element: Callable[[], Element],
105
+ width: float,
106
+ height: float,
107
+ ) -> Any:
108
+ """Mount or rebind the subtree for ``container_key``.
109
+
110
+ Returns the subtree's native root. The caller must (re)attach
111
+ it to the container: platforms strip a recycled cell's
112
+ children before rebinding, so even an unchanged root needs
113
+ re-attachment.
114
+ """
115
+ element = make_element()
116
+ subtree = self._subtrees.get(container_key)
117
+ if subtree is None:
118
+ subtree = RowSubtree()
119
+ self._subtrees[container_key] = subtree
120
+ return subtree.mount(element, width, height)
121
+ return subtree.rebind(element, width, height)
122
+
123
+ def release(self, container_key: int) -> None:
124
+ """Unmount the subtree for one recycled container, if any."""
125
+ subtree = self._subtrees.pop(container_key, None)
126
+ if subtree is not None:
127
+ subtree.unmount()
128
+
129
+ def release_all(self) -> None:
130
+ """Unmount every subtree (list destroyed)."""
131
+ subtrees = list(self._subtrees.values())
132
+ self._subtrees.clear()
133
+ for subtree in subtrees:
134
+ subtree.unmount()
135
+
136
+ def __len__(self) -> int:
137
+ return len(self._subtrees)