FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

feat!: add native lists, dark mode, rich text, and image pipeline by owenthcarey · Pull Request #15 · pythonnative/pythonnative · GitHub

Repository navigation

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension .java  (1) .kt  (2) .md  (11) .py  (31) .toml  (1) .xml  (1) .yaml  (9) .yml  (2) All 8 file types selected
Only manifest files
Viewed files
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Unified
Split
Hide whitespace
Diff view
Unified
Split
Hide whitespace
4 changes: 3 additions & 1 deletion .github/workflows/e2e.yml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,9 @@ jobs:
run: |
curl -Ls "https://get.maestro.mobile.dev" | bash
echo "$HOME/.maestro/bin" >> $GITHUB_PATH
brew tap facebook/fb && brew install idb-companion
# Homebrew refuses formulae from untrusted third-party taps by
# default; trust the tap explicitly before installing.
brew tap facebook/fb && brew trust facebook/fb && brew install idb-companion

- name: Build and run E2E tests
run: ./scripts/run-e2e.sh ios ${{ matrix.suites }}
Expand Down
12 changes: 6 additions & 6 deletions CONTRIBUTING.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
### Contributing to PythonNative

Thanks for your interest in contributing. This repository contains the PythonNative library, CLI, templates, a demo app, and a Django site used for docs/demo hosting and E2E. Contributions should keep the code reliable, cross‑platform, and easy to use.
Thanks for your interest in contributing. This repository contains the PythonNative library, the `pn` CLI, Android/iOS project templates, example apps, and the documentation site. Contributions should keep the code reliable, cross-platform, and easy to use.

## Quick start

Expand All @@ -23,7 +23,7 @@ Common library and CLI entry points:
# CLI help
pn --help

# create a new sample app (template fetch is remote)
# create a new sample app from the bundled templates
pn init my_app

# run the Hello World example
Expand All @@ -35,9 +35,9 @@ cd examples/hello-world && pn run android
- `src/pythonnative/`: installable library and CLI
- `pythonnative/`: core cross‑platform UI components and utilities
- `cli/`: `pn` command
- `templates/`: Android/iOS project templates (bundled with the package)
- `tests/`: unit tests for the library, plus the Maestro E2E suite
- `e2e/`: the comprehensive E2E suite (see [E2E tests](#e2e-tests-maestro) below and `tests/e2e/AGENTS.md`)
- `templates/`: Android/iOS project templates and zips
- `examples/`: runnable example apps
- `hello-world/`: minimal marketing demo
- `e2e-suite/`: comprehensive feature catalog that drives the Maestro E2E suite
Expand All @@ -49,7 +49,7 @@ cd examples/hello-world && pn run android
- Style: Black; lint: Ruff; typing where useful. Keep APIs stable.
- Prefer explicit, descriptive names; keep platform abstractions clean.
- Add/extend tests under `tests/` for new behavior.
- Don't commit generated artifacts or large binaries; templates live under `templates/`.
- Don't commit generated artifacts or large binaries; templates live under `src/pythonnative/templates/`.
- Docstrings: Google style throughout. Ruff is configured with the Google
convention (`pydocstyle.convention = "google"`) and enforces the `D` rule
set on `src/pythonnative/`. See the
Expand Down Expand Up @@ -229,7 +229,7 @@ Co-authored-by: Name <email>
- Tests: added/updated; `pytest` passes.
- Lint/format: `ruff check .`, `black` pass.
- Docs: update `README.md` if behavior changes.
- Templates: update `templates/` if generator output changes.
- Templates: update `src/pythonnative/templates/` if generator output changes.
- No generated artifacts committed.

## Versioning and releases
Expand Down Expand Up @@ -329,7 +329,7 @@ When you add a new public symbol you must also:
## Security and provenance

- Avoid bundling secrets or credentials in templates or code.
- Prefer runtime configuration via environment variables for Django and CI.
- Prefer runtime configuration via environment variables in CI.

## License

Expand Down
42 changes: 42 additions & 0 deletions docs/api/appearance.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Appearance

System color-scheme tracking with an optional app-level override.
The screen host publishes the platform's light/dark setting into this
module; [`use_color_scheme`][pythonnative.use_color_scheme] and
[`use_theme`][pythonnative.use_theme] subscribe to it and re-render
components when the effective scheme changes.

```python
import pythonnative as pn


@pn.component
def SchemeAwareBadge():
scheme = pn.use_color_scheme() # "light" or "dark"
theme = pn.use_theme() # built-in theme for the scheme
return pn.Text(
f"Currently {scheme}",
style={"color": theme["text_color"]},
)
```

Force an appearance regardless of the system setting (for example
from an in-app toggle), or return to following the system:

```python
pn.appearance.set_color_scheme("dark") # force dark
pn.appearance.set_color_scheme(None) # follow the system again
```

::: pythonnative.appearance
options:
show_root_heading: false
show_root_toc_entry: false
members_order: source
filters: ["!^_"]

## Next steps

- Theming with [`use_theme`][pythonnative.use_theme] and
[`ThemeContext`][pythonnative.ThemeContext]: see
[Style](style.md) and the [Styling guide](../guides/styling.md).
57 changes: 51 additions & 6 deletions docs/api/component-properties.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,19 @@ All components accept these layout properties in their `style` dict:
- `key`: stable identity for reconciliation (passed as a kwarg, not inside
`style`).

### Accessibility and testing (kwargs)

Most visible components also accept these keyword arguments (see
[Platform & Accessibility](../guides/platform-accessibility.md)):

- `accessibility_label`, `accessibility_hint`, `accessibility_role`,
`accessible`
- `accessibility_state`: dict of `disabled` / `selected` / `checked` /
`busy` / `expanded` flags
- `accessibility_live_region`: `"polite"` or `"assertive"`
- `test_id`: stable identifier exposed to UI test frameworks
(`accessibilityIdentifier` on iOS, resource-id on Android)

## View

```python
Expand Down Expand Up @@ -99,6 +112,14 @@ pn.Text(text, style={"font_size": 18, "color": "#333", "bold": True, "text_align
- `text`: display string (positional)
- Style properties: `font_size`, `color`, `bold`, `text_align`, `background_color`, `max_lines`

Rich text: pass a mix of strings and nested `Text` elements as the
positional parts; they flatten into a single native attributed string
(each nested `Text` styles its own run and inherits the rest):

```python
pn.Text("Total: ", pn.Text("$42", style={"bold": True, "color": "#16A34A"}))
```

## Button

```python
Expand Down Expand Up @@ -190,11 +211,22 @@ pn.TextInput(value="", placeholder="Enter text", on_change=handler, secure=False
## Image

```python
pn.Image(source="https://example.com/photo.jpg", style={"width": 200, "height": 150, "scale_type": "cover"})
pn.Image(source="https://example.com/photo.jpg",
placeholder_color="#E2E8F0",
on_load=lambda: ..., on_error=lambda message: ...,
style={"width": 200, "height": 150, "scale_type": "cover"})
```

- `source`: image URL (`http://...` / `https://...`) or local resource name
- Style properties: `width`, `height`, `scale_type` (`"cover"`, `"contain"`, `"stretch"`, `"center"`), `background_color`
- `source`: image URL (`http://...` / `https://...`), inline `data:` URI,
absolute file path, or local resource name
- `placeholder_color`: background shown while a remote image loads
- `on_load`: callback `() -> None` when the image is displayed
- `on_error`: callback `(str) -> None` when loading or decoding fails
- Style properties: `width`, `height`, `scale_type` (`"cover"`, `"contain"`, `"stretch"`, `"center"`), `background_color`, `tint_color`

Remote sources load through the shared [image pipeline](images.md):
background download, memory + disk caching, request deduplication,
and decode downsampled to the view's bounds.

## Switch

Expand Down Expand Up @@ -272,6 +304,17 @@ pn.Pressable(child, on_press=handler, on_long_press=handler)

Wraps any child element with tap/long-press handling.

`style` may also be a callable receiving `{"pressed": bool}`, re-applied
as the press state changes:

```python
pn.Pressable(
child,
on_press=handler,
style=lambda state: {"opacity": 0.6 if state["pressed"] else 1.0},
)
```

## Modal

```python
Expand Down Expand Up @@ -433,10 +476,12 @@ pn.FlatList(data=items, render_item=render_fn, key_extractor=key_fn,
- `data`: list of items
- `render_item`: `(item, index) -> Element` function
- `key_extractor`: `(item, index) -> str` for stable keys
- `item_height`: fixed row height; enables native virtualization
- `item_height`: fixed row height; qualifies the list for the native
RecyclerView / UITableView virtualization path (see the
[Lists guide](../guides/lists.md))
- `separator_height`: spacing between items
- `horizontal`: lay rows out left-to-right (eager backend)
- `num_columns`: render as a grid of N columns (eager backend)
- `horizontal`: lay rows out left-to-right (Python-windowed engine)
- `num_columns`: render as a grid of N columns (Python-windowed engine)
- `list_header` / `list_footer`: elements rendered once above / below rows
- `list_empty`: element rendered when `data` is empty
- `on_end_reached`: callback `() -> None` near the end (virtualized)
Expand Down
33 changes: 33 additions & 0 deletions docs/api/images.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Images

The shared image pipeline behind the
[`Image`][pythonnative.Image] and
[`ImageBackground`][pythonnative.ImageBackground] components. Remote
sources are downloaded on a background thread with an in-memory LRU
cache, a disk cache, and request deduplication (many views asking for
the same URL trigger one download). Platform handlers decode the
cached file downsampled to the view's bounds, so oversized photos
don't pin full-resolution bitmaps in memory.

Most apps never call this module directly; the `Image` component's
`placeholder_color`, `on_load`, and `on_error` props cover the common
cases:

```python
import pythonnative as pn

pn.Image(
source="https://example.com/photo.jpg",
placeholder_color="#E2E8F0",
on_load=lambda: print("loaded"),
on_error=lambda message: print("failed:", message),
style={"width": 200, "height": 120},
)
```

::: pythonnative.images
options:
show_root_heading: false
show_root_toc_entry: false
members_order: source
filters: ["!^_"]
6 changes: 4 additions & 2 deletions docs/api/pythonnative.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,14 @@ The reference is split per module so each page stays scannable:
| Area | Page | Key symbols |
|---|---|---|
| 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], [`Fragment`][pythonnative.Fragment], [`ErrorBoundary`][pythonnative.ErrorBoundary] |
| 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], [`memo`][pythonnative.memo] |
| 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], [`use_color_scheme`][pythonnative.use_color_scheme], [`memo`][pythonnative.memo] |
| Animations | [Animated](animated.md) | `Animated`, [`AnimatedValue`][pythonnative.AnimatedValue], [`use_animated_value`][pythonnative.use_animated_value] |
| System dialogs | [Alerts](alerts.md) | [`Alert`][pythonnative.Alert] |
| Platform | [Platform](platform.md) | [`Platform`][pythonnative.Platform] |
| 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] |
| Styling | [Style](style.md) | [`StyleSheet`][pythonnative.StyleSheet], [`Style`][pythonnative.style.Style], [`StyleProp`][pythonnative.style.StyleProp], [`style`][pythonnative.style.style], [`ThemeContext`][pythonnative.style.ThemeContext] |
| Styling | [Style](style.md) | [`StyleSheet`][pythonnative.StyleSheet], [`Style`][pythonnative.style.Style], [`StyleProp`][pythonnative.style.StyleProp], [`style`][pythonnative.style.style], [`ThemeContext`][pythonnative.style.ThemeContext], [`use_theme`][pythonnative.use_theme] |
| Appearance | [Appearance](appearance.md) | [`use_color_scheme`][pythonnative.use_color_scheme], `appearance.set_color_scheme`, `appearance.get_color_scheme` |
| Images | [Images](images.md) | `images.fetch`, `images.clear_cache` |
| Element descriptor | [Element](element.md) | [`Element`][pythonnative.Element] |
| Screen host | [Screen](screen.md) | [`create_screen`][pythonnative.create_screen] |
| Reconciler | [Reconciler](reconciler.md) | [`Reconciler`][pythonnative.reconciler.Reconciler] |
Expand Down
5 changes: 2 additions & 3 deletions docs/concepts/native-views.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ the render loop.
| `CreateOp(tag, type_name, props)` | Create a native view for `tag`. Props are already *clean*: callables have been routed to the event registry. |
| `UpdateOp(tag, changed_props)` | Apply only the props that changed (removed props arrive as `None`). |
| `InsertOp(parent_tag, child_tag, index)` | Place the child at `index` (move-aware: an attached child is repositioned, not duplicated). |
| `RemoveOp(parent_tag, child_tag)` | Detach the child without destroying it. |
| `DestroyOp(tag)` | Release the native view and drop the tag record. |
| `DestroyOp(tag)` | Release the native view (detaching it from its parent) and drop the tag record. |
| `SetFrameOp(tag, x, y, w, h)` | Apply a layout frame. Only emitted for frames that actually changed. |

Tags matter because the diff phase is pure: it runs before any native
Expand Down Expand Up @@ -158,7 +157,7 @@ geometry on Android and iOS; there's no "container-only" vs

Children of a container element become subviews of the corresponding
native view. The reconciler determines insertion order (and reorders
on key change) and expresses it as `InsertOp` / `RemoveOp` pairs; the
on key change) and expresses it as `InsertOp` / `DestroyOp` ops; the
handler performs the actual native mutations:

- iOS containers use `insertSubview_atIndex_` on a plain `UIView`.
Expand Down
1 change: 0 additions & 1 deletion docs/concepts/reconciliation.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ tree:
- For container elements, match children by `key` first and by
position only if no key was provided. Reorder, mount, and unmount
as needed using [`InsertOp`][pythonnative.mutations.InsertOp] /
[`RemoveOp`][pythonnative.mutations.RemoveOp] /
[`DestroyOp`][pythonnative.mutations.DestroyOp].

That's it. There's no "fiber tree", no time slicing, and no priority
Expand Down
44 changes: 33 additions & 11 deletions docs/guides/lists.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,26 @@
[`SectionList`][pythonnative.SectionList] are the two list components
shipped with PythonNative. Both are **virtualized**: only the rows
inside (and just beyond) the viewport are mounted as native views.
Leading and trailing spacers stand in for everything off-screen, and
the window of mounted rows shifts as the user scrolls. The windowing
runs in Python on top of the platform's native scroll view, so the
same behavior (including `on_end_reached`, viewability callbacks, and
the imperative scroll controller) is identical on Android, iOS, and
the desktop preview.
Under the hood the list picks one of two engines:

- **Native virtualization.** On Android and iOS, when every row's
extent is known up front (a fixed `item_height`, or exact
`get_item_height` / `section_header_height` values) and no
windowed-only feature is requested, the list is backed by a real
`RecyclerView` (Android) or `UITableView` (iOS). The platform owns
row recycling; each visible row hosts its own PythonNative subtree
that is mounted on demand and reconciled in place when its cell is
recycled. This is the fastest path and the one long feeds should
aim for.
- **Python windowing.** Everywhere else (the desktop preview,
variable-height rows, grids, horizontal lists, headers/footers/empty
states, and pull-to-refresh), rows render into a native scroll view
with leading and trailing spacers standing in for off-screen
content, and the window of mounted rows shifts from scroll events.

The public surface is identical on both paths: `on_end_reached`,
`on_viewable_items_changed`, `on_scroll`, and the imperative scroll
controller behave the same regardless of which engine is active.

```python
import pythonnative as pn
Expand All @@ -34,12 +48,15 @@ viewport ever exists.

Three ways to tell the list how tall rows are, in order of preference:

- `item_height=44`: uniform rows. Offsets are exact and cheap.
- `item_height=44`: uniform rows. Offsets are exact and cheap, and
the list qualifies for the native virtualization path.
- `get_item_height=lambda item, i: ...`: exact per-row extents
without measurement.
without measurement. Also qualifies for native virtualization.
- Nothing at all: rows start at `estimated_item_height` (default 44)
and are corrected with their measured extent once they've been on
screen. Scroll positions stay stable as estimates converge.
screen. Scroll positions stay stable as estimates converge. This
always uses the Python-windowed engine (native recyclers need exact
heights before rows are rendered).

`separator_height=` adds a fixed gap below every row.

Expand Down Expand Up @@ -159,8 +176,13 @@ estimated and measured).
the window refresh in place rather than tearing down and rebuilding
their subtree as the window shifts.
- Provide real extents (`item_height` / `get_item_height`) when you
can; measured estimation works but does more bookkeeping per
scroll.
can. Exact extents unlock the native RecyclerView / UITableView
path on device; measured estimation works but stays on the
Python-windowed engine and does more bookkeeping per scroll.
- Features that force the windowed engine (`list_header`,
`list_footer`, `list_empty`, `refresh_control`, `num_columns`,
`horizontal`) are fine for short lists, but a very long feed is
smoothest with none of them and a fixed `item_height`.
- Keep row subtrees shallow. The reconciler is fast, but mounting a
hundred `Text`/`Image`/`Button` nodes per row is wasteful work
every time a row enters the window.
Expand Down
Loading
Loading

Back | FazBrowse Home | New Git URL