Skip to content

Commit 186bba6

Browse files
committed
feat(components,hooks,native_views): add native UI primitives
1 parent 81fb760 commit 186bba6

42 files changed

Lines changed: 6990 additions & 475 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/api/alerts.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Alerts
2+
3+
The [`Alert`][pythonnative.Alert] class provides imperative access to
4+
the host platform's alert dialogs and action sheets. Alerts are *not*
5+
part of the element tree — they're fire-and-forget calls that present
6+
a native dialog and dispatch button callbacks.
7+
8+
::: pythonnative.alerts
9+
options:
10+
show_root_heading: false
11+
show_root_toc_entry: false
12+
members_order: source
13+
filters: ["!^_"]
14+
15+
## Patterns
16+
17+
- **Confirm before destructive actions**: pair a `"destructive"`
18+
button with a `"cancel"` button via
19+
[`Alert.confirm`][pythonnative.alerts.Alert.confirm].
20+
- **Action sheets**: pass `style="action_sheet"` to render an iOS-style
21+
bottom sheet; on Android this falls back to a regular dialog.
22+
- **Pickers**: the built-in [`Picker`][pythonnative.Picker] component
23+
is implemented on top of action sheets — use it for select/dropdown
24+
widgets.
25+
26+
## Testing
27+
28+
When running off-device (e.g., in unit tests), `Alert.show` records
29+
each call to `Alert._test_log` instead of presenting a dialog. Reset
30+
the log with `Alert._test_log.clear()` between cases.

docs/api/animated.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Animated
2+
3+
PythonNative's `Animated` API mirrors React Native's. Build performant
4+
animations declaratively by binding [`AnimatedValue`][pythonnative.AnimatedValue]
5+
instances to the `style` of an `Animated.View`, `Animated.Text`, or
6+
`Animated.Image`. The reconciler holds a `ref` to the underlying
7+
native view; the animation driver pushes value changes directly to
8+
the native handler's `set_animated_property` hook so per-frame updates
9+
bypass full reconciliation.
10+
11+
::: pythonnative.animated
12+
options:
13+
show_root_heading: false
14+
show_root_toc_entry: false
15+
members_order: source
16+
filters: ["!^_"]
17+
show_if_no_docstring: true
18+
19+
## See also
20+
21+
- The [Animations guide](../guides/animations.md) walks through
22+
fade-ins, springs, sequences, and gesture-driven animations.
23+
- [`use_ref`][pythonnative.use_ref] explains the `ref` semantics that
24+
back `Animated.View`.

docs/api/hooks.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,21 @@ slot across renders.
1212
members_order: source
1313
filters: ["!^_"]
1414

15+
## Platform-metric hooks
16+
17+
These hooks subscribe to values published by
18+
`pythonnative.platform_metrics` and re-render the component when they
19+
change. The page host is the only code that updates the underlying
20+
values; user code consumes them.
21+
22+
- [`use_window_dimensions`][pythonnative.use_window_dimensions] — viewport size.
23+
- [`use_safe_area_insets`][pythonnative.use_safe_area_insets] — top/bottom/left/right insets.
24+
- [`use_keyboard_height`][pythonnative.use_keyboard_height] — software keyboard height.
25+
26+
For most apps the dedicated
27+
[`KeyboardAvoidingView`][pythonnative.KeyboardAvoidingView] component
28+
is preferable to consuming `use_keyboard_height` directly.
29+
1530
## Next steps
1631

1732
- Compose hooks into a screen: [Components](components.md).
@@ -21,3 +36,5 @@ slot across renders.
2136
- Share state across the tree with
2237
[`create_context`][pythonnative.create_context] and
2338
[`Provider`][pythonnative.Provider].
39+
- Animate without re-rendering using [`use_ref`][pythonnative.use_ref]
40+
+ `Animated`; see the [Animations guide](../guides/animations.md).

docs/api/platform.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Platform
2+
3+
The [`Platform`][pythonnative.Platform] class exposes runtime
4+
information about the host platform and a `select` helper for writing
5+
platform-aware code without scattering `if IS_IOS` / `if IS_ANDROID`
6+
branches throughout the codebase.
7+
8+
::: pythonnative.platform
9+
options:
10+
show_root_heading: false
11+
show_root_toc_entry: false
12+
members_order: source
13+
filters: ["!^_"]
14+
15+
## Quick reference
16+
17+
| Attribute | Values |
18+
|---|---|
19+
| `Platform.OS` | `"ios"`, `"android"`, or `"test"` |
20+
| `Platform.Version` | Best-effort OS version string |
21+
| `Platform.is_ios` / `Platform.is_android` / `Platform.is_test` | Booleans |
22+
| `Platform.select(spec, default=None)` | Pick a value matching the current platform |
23+
24+
## See also
25+
26+
- [Window dimensions, safe area, keyboard hooks](hooks.md) for
27+
reactive metric access.

docs/api/pythonnative.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@ The reference is split per module so each page stays scannable:
1717

1818
| Area | Page | Key symbols |
1919
|---|---|---|
20-
| Element factories | [Components](components.md) | [`Text`][pythonnative.Text], [`Button`][pythonnative.Button], [`Column`][pythonnative.Column], [`Row`][pythonnative.Row], [`ScrollView`][pythonnative.ScrollView], [`FlatList`][pythonnative.FlatList], [`ErrorBoundary`][pythonnative.ErrorBoundary] |
21-
| Hooks | [Hooks](hooks.md) | [`use_state`][pythonnative.use_state], [`use_reducer`][pythonnative.use_reducer], [`use_effect`][pythonnative.use_effect], [`use_memo`][pythonnative.use_memo], [`use_ref`][pythonnative.use_ref], [`use_context`][pythonnative.use_context] |
20+
| Element factories | [Components](components.md) | [`Text`][pythonnative.Text], [`Button`][pythonnative.Button], [`Column`][pythonnative.Column], [`Row`][pythonnative.Row], [`ScrollView`][pythonnative.ScrollView], [`FlatList`][pythonnative.FlatList], [`SectionList`][pythonnative.SectionList], [`Modal`][pythonnative.Modal], [`Pressable`][pythonnative.Pressable], [`StatusBar`][pythonnative.StatusBar], [`KeyboardAvoidingView`][pythonnative.KeyboardAvoidingView], [`RefreshControl`][pythonnative.RefreshControl], [`Picker`][pythonnative.Picker], [`ErrorBoundary`][pythonnative.ErrorBoundary] |
21+
| Hooks | [Hooks](hooks.md) | [`use_state`][pythonnative.use_state], [`use_reducer`][pythonnative.use_reducer], [`use_effect`][pythonnative.use_effect], [`use_memo`][pythonnative.use_memo], [`use_ref`][pythonnative.use_ref], [`use_context`][pythonnative.use_context], [`use_window_dimensions`][pythonnative.use_window_dimensions], [`use_safe_area_insets`][pythonnative.use_safe_area_insets], [`use_keyboard_height`][pythonnative.use_keyboard_height] |
22+
| Animations | [Animated](animated.md) | `Animated`, [`AnimatedValue`][pythonnative.AnimatedValue] |
23+
| System dialogs | [Alerts](alerts.md) | [`Alert`][pythonnative.Alert] |
24+
| Platform | [Platform](platform.md) | [`Platform`][pythonnative.Platform] |
2225
| Navigation | [Navigation](navigation.md) | [`NavigationContainer`][pythonnative.NavigationContainer], [`create_stack_navigator`][pythonnative.create_stack_navigator], [`create_tab_navigator`][pythonnative.create_tab_navigator], [`create_drawer_navigator`][pythonnative.create_drawer_navigator], [`use_navigation`][pythonnative.use_navigation] |
2326
| Styling | [Style](style.md) | [`StyleSheet`][pythonnative.StyleSheet], [`ThemeContext`][pythonnative.style.ThemeContext] |
2427
| Element descriptor | [Element](element.md) | [`Element`][pythonnative.Element] |

docs/concepts/components.md

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,39 @@ pn.Column(
7777

7878
**Lists:**
7979

80-
- [`FlatList(data, render_item, key_extractor, separator_height)`][pythonnative.FlatList]:
81-
scrollable data list.
80+
- [`FlatList(data, render_item, key_extractor, item_height, ...)`][pythonnative.FlatList]:
81+
scrollable data list. Pass `item_height=` to enable native
82+
virtualization (`UITableView` / `RecyclerView`); rows are mounted
83+
lazily as they scroll into view.
84+
- [`SectionList(sections, render_item, render_section_header, item_height, ...)`][pythonnative.SectionList]:
85+
virtualized list with section headers.
86+
87+
**Platform UI:**
88+
89+
- [`StatusBar(style, background_color, hidden)`][pythonnative.StatusBar]:
90+
configure the device's status bar (light/dark icons, color, hidden).
91+
- [`KeyboardAvoidingView(*children, behavior)`][pythonnative.KeyboardAvoidingView]:
92+
shift content up when the software keyboard appears.
93+
- [`RefreshControl(refreshing, on_refresh)`][pythonnative.RefreshControl]:
94+
pull-to-refresh spec for `ScrollView` and `FlatList` (passed via
95+
the `refresh_control=` prop).
96+
- [`Picker(value, items, on_change, placeholder)`][pythonnative.Picker]:
97+
select / dropdown widget backed by an action sheet.
98+
99+
**Imperative APIs:**
100+
101+
- [`Alert.show(title, message, buttons, style)`][pythonnative.Alert]:
102+
present a native alert dialog or action sheet.
103+
- [`Alert.confirm(title, on_confirm, on_cancel)`][pythonnative.alerts.Alert.confirm]:
104+
two-button confirm/cancel.
105+
106+
**Animations:**
107+
108+
- `Animated.View` / `Animated.Text` / `Animated.Image`: components
109+
whose `style` accepts [`AnimatedValue`][pythonnative.AnimatedValue]
110+
instances. Drive animations with `Animated.timing`,
111+
`Animated.spring`, or `Animated.decay`. See the
112+
[Animations guide](../guides/animations.md).
82113

83114
### Flex layout model
84115

@@ -214,7 +245,9 @@ hook state.
214245
- [`use_callback(fn, deps)`][pythonnative.use_callback]: stable
215246
function references.
216247
- [`use_ref(initial)`][pythonnative.use_ref]: mutable ref that
217-
persists across renders.
248+
persists across renders. When passed via the `ref=` prop, the
249+
reconciler populates `ref["current"]` with the underlying native
250+
view.
218251
- [`use_context(context)`][pythonnative.use_context]: read from a
219252
context provider.
220253
- [`use_navigation()`][pythonnative.use_navigation]: navigation
@@ -223,6 +256,12 @@ hook state.
223256
current route params.
224257
- [`use_focus_effect(effect, deps)`][pythonnative.use_focus_effect]:
225258
like `use_effect` but only runs when the screen is focused.
259+
- [`use_window_dimensions()`][pythonnative.use_window_dimensions]:
260+
reactive viewport size.
261+
- [`use_safe_area_insets()`][pythonnative.use_safe_area_insets]:
262+
reactive safe-area insets.
263+
- [`use_keyboard_height()`][pythonnative.use_keyboard_height]:
264+
reactive software-keyboard height.
226265

227266
### Custom hooks
228267

@@ -257,15 +296,22 @@ def MyComponent():
257296

258297
## Platform detection
259298

260-
Use `utils.IS_ANDROID` / `utils.IS_IOS` when you need
261-
platform-specific logic:
299+
The recommended way to write platform-aware code is via
300+
[`Platform`][pythonnative.Platform]:
262301

263302
```python
264-
from pythonnative.utils import IS_ANDROID
303+
import pythonnative as pn
304+
305+
title = pn.Platform.select({"ios": "iOS App", "android": "Android App"})
265306

266-
title = "Android App" if IS_ANDROID else "iOS App"
307+
if pn.Platform.is_ios:
308+
margin = 16
267309
```
268310

311+
`pn.Platform.OS` is `"ios"`, `"android"`, or `"test"` (the latter
312+
when running off-device, e.g., in unit tests). The lower-level
313+
`utils.IS_ANDROID` / `utils.IS_IOS` constants are still available.
314+
269315
## Next steps
270316

271317
- Learn the renderer underneath: [Architecture](architecture.md).

docs/guides/animations.md

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# Animations
2+
3+
PythonNative ships an `Animated` API modelled on React Native's. It's
4+
designed for the common case where a small set
5+
of style properties (opacity, transform, color) need to interpolate
6+
smoothly over time without re-rendering the component tree on every
7+
frame.
8+
9+
## Mental model
10+
11+
1. Create an [`AnimatedValue`][pythonnative.AnimatedValue] using
12+
[`use_memo`][pythonnative.use_memo] (so it survives re-renders).
13+
2. Bind the value into the `style` of an `Animated.View`,
14+
`Animated.Text`, or `Animated.Image`.
15+
3. Drive the value with `Animated.timing`, `Animated.spring`, or
16+
`Animated.decay`. Each driver returns a handle with `.start()` /
17+
`.stop()`.
18+
19+
The animated component captures a `ref` to the underlying native view
20+
(via the same [`use_ref`][pythonnative.use_ref] mechanism users have
21+
access to). The animation driver then invokes the platform's native
22+
animation API (`UIView.animate` on iOS, `ViewPropertyAnimator` on
23+
Android) directly on that view, so per-frame updates skip the
24+
reconciler.
25+
26+
## Fade in on mount
27+
28+
```python
29+
import pythonnative as pn
30+
31+
32+
@pn.component
33+
def FadeInBox():
34+
opacity = pn.use_memo(lambda: pn.Animated.Value(0.0), [])
35+
36+
def _fade_in():
37+
pn.Animated.timing(opacity, to=1.0, duration=400).start()
38+
39+
pn.use_effect(_fade_in, [])
40+
41+
return pn.Animated.View(
42+
pn.Text("Hello!"),
43+
style={
44+
"opacity": opacity,
45+
"background_color": "#0EA5E9",
46+
"padding": 16,
47+
"border_radius": 12,
48+
},
49+
)
50+
```
51+
52+
`opacity` starts at `0.0` and the timing animation interpolates it to
53+
`1.0` over 400 ms.
54+
55+
## Spring animation on press
56+
57+
```python
58+
@pn.component
59+
def Bouncy():
60+
scale = pn.use_memo(lambda: pn.Animated.Value(1.0), [])
61+
62+
def _press():
63+
pn.Animated.spring(scale, to=1.2, stiffness=200, damping=8).start()
64+
65+
return pn.Pressable(
66+
pn.Animated.View(
67+
pn.Text("Tap me"),
68+
style={"scale": scale, "padding": 12, "background_color": "#10B981"},
69+
),
70+
on_press=_press,
71+
)
72+
```
73+
74+
Available transform shortcuts inside `style`: `scale`, `scale_x`,
75+
`scale_y`, `translate_x`, `translate_y`, `rotate`. Each accepts an
76+
`AnimatedValue` and the runtime maps them to the underlying native
77+
animation property.
78+
79+
## Sequencing and parallel composition
80+
81+
```python
82+
opacity = pn.Animated.Value(0.0)
83+
translate_y = pn.Animated.Value(20.0)
84+
85+
pn.Animated.parallel([
86+
pn.Animated.timing(opacity, to=1.0, duration=300),
87+
pn.Animated.spring(translate_y, to=0.0),
88+
]).start()
89+
```
90+
91+
Use `Animated.sequence` for one-after-another execution and
92+
`Animated.delay(ms)` to insert pauses inside a sequence.
93+
94+
## Easing
95+
96+
`Animated.timing` accepts an `easing` argument: `"linear"`,
97+
`"ease_in"`, `"ease_out"`, `"ease_in_out"`, or `"bounce"`.
98+
99+
## Stopping an animation
100+
101+
`start()` returns the handle you started with, and the handle exposes
102+
`.stop()`. A common pattern is to keep the handle in a `use_ref` so
103+
you can cancel a long-running animation when the user interrupts.
104+
105+
## When NOT to use `Animated`
106+
107+
- For simple state transitions where re-rendering the tree is fine,
108+
plain [`use_state`][pythonnative.use_state] is simpler.
109+
- For physics simulations or per-frame layout (drag-and-drop, charts),
110+
consider running your own loop with
111+
[`use_effect`][pythonnative.use_effect] and a setter.

0 commit comments

Comments
 (0)