diff --git a/.commitlintrc.yml b/.commitlintrc.yml new file mode 100644 index 0000000..455e892 --- /dev/null +++ b/.commitlintrc.yml @@ -0,0 +1,30 @@ +extends: + - '@commitlint/config-conventional' + +rules: + type-enum: + - 2 + - always + - - build + - chore + - ci + - docs + - feat + - fix + - perf + - refactor + - revert + - style + - test + scope-case: + - 0 + subject-case: + - 0 + header-max-length: + - 1 + - always + - 100 + body-max-line-length: + - 0 + footer-max-line-length: + - 0 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8e801b3..a177889 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.9', '3.10', '3.11'] + python-version: ['3.10', '3.11', '3.12'] steps: - name: Checkout @@ -43,6 +43,11 @@ jobs: run: | pip install . + - name: Build package + run: | + pip install build + python -m build + - name: Run tests run: | pytest -q diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 0000000..7935a34 --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,79 @@ +name: E2E + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +jobs: + e2e-android: + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Set up Java 17 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + + - name: Install PythonNative + run: pip install -e . + + - name: Install Maestro + run: | + curl -Ls "https://get.maestro.mobile.dev" | bash + echo "$HOME/.maestro/bin" >> $GITHUB_PATH + + - name: Build, install, and run E2E tests + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: 31 + arch: x86_64 + script: >- + cd examples/hello-world && + pn run android && + sleep 5 && + cd ../.. && + maestro test tests/e2e/android.yaml + + e2e-ios: + runs-on: macos-latest + timeout-minutes: 30 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install PythonNative + run: pip install -e . + + - name: Install Maestro and idb + run: | + curl -Ls "https://get.maestro.mobile.dev" | bash + echo "$HOME/.maestro/bin" >> $GITHUB_PATH + brew tap facebook/fb && brew install idb-companion + + - name: Build and launch iOS app + working-directory: examples/hello-world + run: pn run ios + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Run E2E tests + run: maestro --platform ios test tests/e2e/ios.yaml diff --git a/.github/workflows/pr-lint.yml b/.github/workflows/pr-lint.yml new file mode 100644 index 0000000..f616d1a --- /dev/null +++ b/.github/workflows/pr-lint.yml @@ -0,0 +1,46 @@ +name: PR Lint + +on: + pull_request: + types: [opened, edited, synchronize, reopened] + +permissions: + pull-requests: read + +jobs: + pr-title: + name: PR title (Conventional Commits) + runs-on: ubuntu-latest + steps: + - name: Validate PR title + uses: amannn/action-semantic-pull-request@v5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + types: | + build + chore + ci + docs + feat + fix + perf + refactor + revert + style + test + requireScope: false + subjectPattern: ^[a-z].+[^.]$ + subjectPatternError: | + Subject "{subject}" must start with a lowercase letter and must not + end with a period. + Example: "feat(cli): add init subcommand" + + commits: + name: Commit messages + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: wagoid/commitlint-github-action@v6 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 07450cc..20ac529 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,31 +1,47 @@ -name: Release to PyPI +name: Release on: push: - tags: - - 'v*.*.*' + branches: [main] + workflow_dispatch: jobs: - build-and-publish: + release: + name: Semantic Release runs-on: ubuntu-latest + concurrency: + group: release + cancel-in-progress: false permissions: - contents: read + contents: write id-token: write + steps: - name: Checkout uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Set up Python uses: actions/setup-python@v5 with: - python-version: '3.9' + python-version: '3.12' + + - name: Install build tools + run: python -m pip install -U pip build + + - name: Python Semantic Release + id: release + uses: python-semantic-release/python-semantic-release@v9 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} - - name: Build sdist and wheel - run: | - python -m pip install -U pip build - python -m build + - name: Build package + if: steps.release.outputs.released == 'true' + run: python -m build - - name: Publish package to PyPI + - name: Publish to PyPI + if: steps.release.outputs.released == 'true' uses: pypa/gh-action-pypi-publish@release/v1 with: skip-existing: true diff --git a/.gitignore b/.gitignore index c5de671..4f1c160 100644 --- a/.gitignore +++ b/.gitignore @@ -162,3 +162,6 @@ cython_debug/ # Metadata *_metadata.json + +# macOS +.DS_Store diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..1f6260b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,251 @@ +# CHANGELOG + + +## v0.8.0 (2026-04-08) + +### Features + +- **hooks,reconciler**: Defer effects; add batching and use_reducer + ([`bf6bb57`](https://github.com/pythonnative/pythonnative/commit/bf6bb57b6f97c140820a902ea0eff6bf6a7ffdbc)) + +- **native_views,components**: Add flexbox-inspired layout system + ([`094d997`](https://github.com/pythonnative/pythonnative/commit/094d99786f7153a7286eb7db9775db0bb90abf1d)) + +- **navigation**: Add declarative navigation system + ([`828bbb0`](https://github.com/pythonnative/pythonnative/commit/828bbb0c83fba640a7055edf1237500f27493fd3)) + +- **navigation**: Add native tab bars and nested navigator forwarding + ([`2b80032`](https://github.com/pythonnative/pythonnative/commit/2b8003218267dd39b968c630b89bd5e212ea7254)) + +### Refactoring + +- **native_views**: Split monolithic module into platform-specific package + ([`d0068fd`](https://github.com/pythonnative/pythonnative/commit/d0068fdbcceb4745b02d8043b03eade2b54dde66)) + + +## v0.7.0 (2026-04-03) + +### Features + +- Replace class-based Page with function components, style prop, and use_navigation hook + ([`8103710`](https://github.com/pythonnative/pythonnative/commit/8103710aed5feb564583bb161cf81771669645fe)) + + +## v0.6.0 (2026-04-03) + +### Build System + +- **deps**: Drop Python 3.9 support (EOL October 2025) + ([`552cd99`](https://github.com/pythonnative/pythonnative/commit/552cd9958c463a51af9e33f0e254dab18135130f)) + +### Code Style + +- **cli**: Reformat pn.py for Black 2026 stable style + ([`298f884`](https://github.com/pythonnative/pythonnative/commit/298f884ce3e1c58a17c92484c5832ebae6f1beaa)) + +### Continuous Integration + +- **workflows**: Add package build step to verify sdist and wheel before release + ([`7fbf9c0`](https://github.com/pythonnative/pythonnative/commit/7fbf9c07988d4c543253dec8ba28da42c38cc3a9)) + +- **workflows,cli**: Fix e2e workflow script chaining and GitHub API auth + ([`01d1968`](https://github.com/pythonnative/pythonnative/commit/01d19683f41a4b00048dfbce687e510bec2e2d31)) + +### Documentation + +- Align branch prefixes with conventional commit types + ([`c6e0e08`](https://github.com/pythonnative/pythonnative/commit/c6e0e08cb0757dad6495c6fee36063699afba87a)) + +- **repo**: Align conventional commit scopes with module structure + ([`ecc39af`](https://github.com/pythonnative/pythonnative/commit/ecc39af78708bc5a83ba81501c7b65d985890de9)) + +- **repo**: Remove component table from README + ([`ab162c5`](https://github.com/pythonnative/pythonnative/commit/ab162c5b658b2367857ab998d3b3f750eca15b4a)) + +### Features + +- Add function components, hooks, layout, styling, hot reload, native APIs, and new UI components + ([`3bd87de`](https://github.com/pythonnative/pythonnative/commit/3bd87de4a8775e23eb4f081a31b9125f9b20861c)) + +- **cli,templates**: Add pythonVersion config, fix Android build, and wire pip requirements + ([`a529834`](https://github.com/pythonnative/pythonnative/commit/a529834a7bfe817a51ef2a5846c97c2f4deee321)) + +### Testing + +- Increase app startup wait for slow CI emulators + ([`4ff6b94`](https://github.com/pythonnative/pythonnative/commit/4ff6b9453a7687eeaf7777bf4a2ab542b32a7e25)) + + +## v0.5.0 (2026-03-31) + +### Continuous Integration + +- **workflows**: Add Maestro E2E tests for Android and iOS + ([`cfe247e`](https://github.com/pythonnative/pythonnative/commit/cfe247edf99da8ff870e2d4118ef74b2df5521c1)) + +### Features + +- **core**: Replace imperative widget API with declarative component model and reconciler + ([`b6b7721`](https://github.com/pythonnative/pythonnative/commit/b6b77216305202ea0c5197b29e725e14cbe99b5e)) + + +## v0.4.0 (2026-03-18) + +### Bug Fixes + +- **components,templates**: Restore hello-world on iOS and Android + ([`d7ac93b`](https://github.com/pythonnative/pythonnative/commit/d7ac93be202161a5c8328816a5c6ff8a96dde1d5)) + +### Continuous Integration + +- **workflows**: Add semantic-release pipeline and PR commit linting + ([`0711683`](https://github.com/pythonnative/pythonnative/commit/0711683f5b56751027bb1a5a63ee2d9afcd4b620)) + +- **workflows**: Append detailed changes link to release notes + ([`11d50a7`](https://github.com/pythonnative/pythonnative/commit/11d50a75dff850a3855a299f38f5885cf15cefc6)) + +- **workflows**: Fix duplicate release, and use changelog for release notes + ([`1cd5393`](https://github.com/pythonnative/pythonnative/commit/1cd5393e7bf20d5350052cfaa81fd511dc4ca3ca)) + +- **workflows**: Simplify release pipeline to use python-semantic-release defaults + ([`2766f24`](https://github.com/pythonnative/pythonnative/commit/2766f244f84d359e1ae74a4b029e0701fad4b0be)) + +### Documentation + +- **repo**: Rewrite README with banner, structured sections, and badges + ([`7c083f4`](https://github.com/pythonnative/pythonnative/commit/7c083f4e38367c6cd4163e0be8c78da1fdf8d3da)) + +- **repo**: Simplify README with badges and one-paragraph overview + ([`3ac84b1`](https://github.com/pythonnative/pythonnative/commit/3ac84b1a3f541b47121b46a687b78826f8d348f9)) + +### Features + +- **components**: Standardize fluent setters and align base signatures + ([`d236d89`](https://github.com/pythonnative/pythonnative/commit/d236d899690a4033effdcab4862a556a742fa6d1)) + +- **components,core**: Add layout/styling APIs and fluent setters + ([`6962d38`](https://github.com/pythonnative/pythonnative/commit/6962d3881bf091b3494fc2c964f7ea65a99ce606)) + +### Refactoring + +- **components**: Declare abstract static wrap in ScrollViewBase + ([`593fee4`](https://github.com/pythonnative/pythonnative/commit/593fee4fcf66678cb026de58115f959633d859b4)) + +- **core,components,examples**: Add annotations; tighten mypy + ([`86e4ffc`](https://github.com/pythonnative/pythonnative/commit/86e4ffc9e51810997006055434783416784c182f)) + + +## v0.3.0 (2025-10-22) + +### Build System + +- **repo**: Remove invalid PyPI classifier + ([`c8552e1`](https://github.com/pythonnative/pythonnative/commit/c8552e137e0176c0f5c61193e786429e2e93ac7c)) + +### Chores + +- **experiments**: Remove experiments directory + ([`caf6993`](https://github.com/pythonnative/pythonnative/commit/caf69936e085a3f487123ebcb3a6d807fefcc66c)) + +- **repo,core,mkdocs**: Bump version to 0.3.0 + ([`64d7c1c`](https://github.com/pythonnative/pythonnative/commit/64d7c1cfb448797305efc7f4014e56584f92fc1a)) + +### Documentation + +- **mkdocs**: Add Architecture page + ([`6d61ffc`](https://github.com/pythonnative/pythonnative/commit/6d61ffc64ca5db8ae688d09a748ddda2a1bc0af6)) + +### Features + +- **core,templates**: Add push/pop navigation and lifecycle wiring + ([`06ea22d`](https://github.com/pythonnative/pythonnative/commit/06ea22d215a1700685a7ca8070ca2189895ed25c)) + +- **templates,core**: Adopt Fragment-based Android navigation + ([`7a3a695`](https://github.com/pythonnative/pythonnative/commit/7a3a695477ece3cf76afd00f203523990f8789df)) + + +## v0.2.0 (2025-10-14) + +### Build System + +- **templates,cli**: Ship template dirs with package; drop zip artifacts + ([`7725b14`](https://github.com/pythonnative/pythonnative/commit/7725b1462c42d89f27fb4d3d733e73177c55d8ac)) + +### Chores + +- Clean up + ([`6c7a882`](https://github.com/pythonnative/pythonnative/commit/6c7a882895691903457a0a94d33192b6018c77fd)) + +- **core,components,cli**: Align lint, typing, and tests with CI + ([`30037d1`](https://github.com/pythonnative/pythonnative/commit/30037d17ad397952a88e3dfeb8bd003ced7319d8)) + +- **experiments**: Remove unused experiment directories + ([`db06fd1`](https://github.com/pythonnative/pythonnative/commit/db06fd101789392deee8c37263a61ee4d7106853)) + +- **repo,ci,docs**: Rename demo to examples/hello-world and update refs + ([`6d5b78e`](https://github.com/pythonnative/pythonnative/commit/6d5b78ea7dce66b5031b952928aed8d4a713fae8)) + +- **repo,core,mkdocs**: Bump version to 0.2.0 + ([`d3f8d31`](https://github.com/pythonnative/pythonnative/commit/d3f8d31942c3ca5d1657024e3a5cb332787afcd8)) + +- **templates**: Scrub DEVELOPMENT_TEAM from iOS template + ([`64ab266`](https://github.com/pythonnative/pythonnative/commit/64ab2666fe09f036934d3922ab55e8e599df3c35)) + +### Continuous Integration + +- **workflows,mkdocs**: Set CNAME to docs.pythonnative.com for docs deploy + ([`401a076`](https://github.com/pythonnative/pythonnative/commit/401a076dcb1fe0c19771f4a19141ee8da28c80e2)) + +### Documentation + +- **mkdocs**: Add roadmap and link in nav + ([`16ede97`](https://github.com/pythonnative/pythonnative/commit/16ede972d41b549853962c7056b65558c9ebd2f5)) + +- **mkdocs**: Update Getting Started, Hello World, Components, and platform guides + ([`f3a03b0`](https://github.com/pythonnative/pythonnative/commit/f3a03b01986365063535a2f336793cc6f21836db)) + +- **repo**: Add CONTRIBUTING.md + ([`f61cb85`](https://github.com/pythonnative/pythonnative/commit/f61cb85301c7bff57299b4c814319e9262f0f5ef)) + +### Features + +- Update README + ([`e839585`](https://github.com/pythonnative/pythonnative/commit/e8395855acf5d38a0e5987475900f4eeb1eee313)) + +- **cli,mkdocs,tests**: Add pn init/run/clean; use bundled templates + ([`9c61757`](https://github.com/pythonnative/pythonnative/commit/9c61757713fe60b5e98756f552681a782f397f3a)) + +- **cli,templates**: Auto-select iOS sim; guard PythonKit + ([`7b7c59c`](https://github.com/pythonnative/pythonnative/commit/7b7c59c262f2510a5fb46e455c13a2fc56086845)) + +- **cli,templates**: Bundle offline templates; add run --prepare-only + ([`d9dd821`](https://github.com/pythonnative/pythonnative/commit/d9dd821bc18289f1f1a367e737cfe7d5bfaf6ee3)) + +- **cli,templates**: Dev-first templates; stage in-repo lib for pn run + ([`b3dd731`](https://github.com/pythonnative/pythonnative/commit/b3dd731bd5efcca8e1a47f8f888fc6123854a40c)) + +- **cli,templates,core**: Bootstrap entrypoint; pn run shows Hello UI + ([`2805e1d`](https://github.com/pythonnative/pythonnative/commit/2805e1d5c6a58eb718b94ba0ce57c1078a08d578)) + +- **cli,templates,core**: Fetch iOS Python runtime and bootstrap PythonKit + ([`bcc0916`](https://github.com/pythonnative/pythonnative/commit/bcc0916a5b7427874ab7a5971a6a9941c4222c77)) + +- **components,utils**: Unify constructors; set Android context + ([`4c06b67`](https://github.com/pythonnative/pythonnative/commit/4c06b67214ea7fc4530a0d39b7105cfb62d20cf5)) + +- **repo,mkdocs,workflows**: Migrate to src layout; add pyproject and docs scaffold + ([`f273922`](https://github.com/pythonnative/pythonnative/commit/f273922e8a0494df7ba2cd59a3ad2ef54f918d3e)) + +### Refactoring + +- **cli**: Make pn.py typing py3.9-compatible and wrap long lines + ([`b38da78`](https://github.com/pythonnative/pythonnative/commit/b38da78dac52e42968efa6f4115b6b84de65b3b5)) + +- **components,core**: Align component names with docs + ([`a326ceb`](https://github.com/pythonnative/pythonnative/commit/a326ceb23c2cfaba409f11451a1c0000f0afbf5e)) + + +## v0.1.0 (2025-10-14) + + +## v0.0.1 (2025-10-14) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cedf103..37daba4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,7 @@ Thanks for your interest in contributing. This repository contains the PythonNat ## Quick start -Development uses Python ≥ 3.9. +Development uses Python ≥ 3.10. ```bash # create and activate a venv (recommended) @@ -46,7 +46,6 @@ cd examples/hello-world && pn run android - `templates/` – Android/iOS project templates and zips - `examples/` – runnable example apps - `hello-world/` – minimal demo app using the library -- `experiments/` – platform experiments (Android/iOS/Briefcase) - `README.md`, `pyproject.toml` – repo docs and packaging ## Coding guidelines @@ -95,28 +94,31 @@ Accepted types (standard): - `style` – formatting/whitespace (no code behavior) - `test` – add/adjust tests only -Recommended scopes (match the smallest accurate directory/module): - -- Library/CLI scopes: - - `cli` – `src/pythonnative/cli/` (the `pn` command) - - `core` – `src/pythonnative/pythonnative/` package internals - - `components` – UI view modules under `src/pythonnative/pythonnative/` (e.g., `button.py`, `label.py`) - - `utils` – utilities like `utils.py` - - `tests` – `tests/` - -- Templates and examples: - - `templates` – `templates/` (Android/iOS templates, zips) - - `examples` – `examples/` (e.g., `hello-world/`) - - `experiments` – `experiments/` - - - -- Repo‑level and ops: +Recommended scopes (choose the smallest, most accurate unit; prefer module/directory names): + +- Module/directory scopes: + - `cli` – CLI tool and `pn` command (`src/pythonnative/cli/`) + - `components` – declarative element-creating functions (`components.py`) + - `element` – Element descriptor class (`element.py`) + - `hooks` – function components and hooks (`hooks.py`) + - `hot_reload` – file watcher and module reloader (`hot_reload.py`) + - `native_modules` – native API modules for device capabilities (`native_modules/`) + - `native_views` – platform-specific native view creation and updates (`native_views/`) + - `package` – `src/pythonnative/__init__.py` exports and package boundary + - `page` – Page component, lifecycle, and reactive state (`page.py`) + - `reconciler` – virtual view tree diffing and reconciliation (`reconciler.py`) + - `style` – StyleSheet and theming (`style.py`) + - `utils` – shared utilities (`utils.py`) + +- Other scopes: - `deps` – dependency updates and version pins - - `docker` – containerization files (e.g., `Dockerfile`) - - `repo` – top‑level files (`README.md`, `CONTRIBUTING.md`, `.gitignore`, licenses) + - `examples` – example apps under `examples/` - `mkdocs` – documentation site (MkDocs/Material) configuration and content under `docs/` - - `workflows` – CI pipelines (e.g., `.github/workflows/`) + - `pyproject` – `pyproject.toml` packaging/build metadata + - `repo` – repository metadata and top‑level files (`README.md`, `CONTRIBUTING.md`, `.gitignore`, licenses) + - `templates` – Android/iOS project templates under `src/pythonnative/templates/` + - `tests` – unit/integration/E2E tests under `tests/` + - `workflows` – CI pipelines under `.github/workflows/` Note: Avoid redundant type==scope pairs (e.g., `docs(docs)`). Prefer a module scope (e.g., `docs(core)`) or `docs(repo)` for top‑level updates. @@ -126,12 +128,12 @@ Examples: build(deps): refresh pinned versions chore(repo): add contributing guidelines ci(workflows): add publish job -docs(core): clarify ListView data contract -feat(components): add MaterialSearchBar +docs(reconciler): clarify diffing algorithm +feat(components): add Slider element fix(cli): handle missing Android SDK gracefully -perf(core): reduce allocations in list diffing +perf(reconciler): reduce allocations in list diffing refactor(utils): extract path helpers -test(tests): cover ios template copy flow +test: cover iOS template copy flow ``` Examples (no scope): @@ -207,25 +209,32 @@ Co-authored-by: Name ## Pull request checklist +- PR title: Conventional Commits format (CI-enforced by `pr-lint.yml`). - Tests: added/updated; `pytest` passes. - Lint/format: `ruff check .`, `black` pass. -- Docs: update `README.md` and any Django docs pages if behavior changes. +- Docs: update `README.md` if behavior changes. - Templates: update `templates/` if generator output changes. - No generated artifacts committed. ## Versioning and releases -- The library version is tracked in `pyproject.toml` (`project.version`). Use SemVer. -- Workflow: - - Contributors: branch off `main` (or `dev` if used) and open PRs. - - Maintainer (release): bump version, tag, and publish to PyPI. - - Tag on `main`: `git tag -a vX.Y.Z -m "Release vX.Y.Z" && git push --tags`. +- The version is tracked in `pyproject.toml` (`project.version`) and mirrored in `src/pythonnative/__init__.py` as `__version__`. Both files are updated automatically by [python-semantic-release](https://python-semantic-release.readthedocs.io/). +- **Automated release pipeline** (on every merge to `main`): + 1. `python-semantic-release` scans Conventional Commit messages since the last tag. + 2. It determines the next SemVer bump: `feat` → **minor**, `fix`/`perf` → **patch**, `BREAKING CHANGE` → **major** (minor while version < 1.0). + 3. Version files are updated, `CHANGELOG.md` is generated, and a tagged release commit (`chore(release): vX.Y.Z`) is pushed. + 4. A GitHub Release is created with auto-generated release notes and the built sdist/wheel attached. + 5. When drafts are disabled, the package is also published to PyPI via Trusted Publishing. +- **Draft / published toggle**: the `DRAFT_RELEASE` variable at the top of `.github/workflows/release.yml` controls release mode. Set to `"true"` (the default) for draft GitHub Releases with PyPI publishing skipped; flip to `"false"` to publish releases and upload to PyPI immediately. +- Commit types that trigger a release: `feat` (minor), `fix` and `perf` (patch), `BREAKING CHANGE` (major). All other types (`build`, `chore`, `ci`, `docs`, `refactor`, `revert`, `style`, `test`) are recorded in the changelog but do **not** trigger a release on their own. +- Tag format: `v`-prefixed (e.g., `v0.4.0`). +- Manual version bumps are no longer needed — just merge PRs with valid Conventional Commit titles. For ad-hoc runs, use the workflow's **Run workflow** button (`workflow_dispatch`). ### Branch naming (suggested) - Use lowercase kebab‑case; concise (≤ 40 chars). -- Prefix conventions: - - `feature/-` +- Branch prefixes match Conventional Commit types: + - `feat/-` - `fix/-` - `chore/` - `docs/` @@ -234,23 +243,56 @@ Co-authored-by: Name - `test/` - `perf/` - `build/` - - `release/vX.Y.Z` - - `hotfix/` Examples: ```text -feature/cli-init +feat/cli-init fix/core-threading-deadlock-123 docs/contributing ci/publish-pypi build/lock-versions refactor/utils-paths test/templates-android -release/v0.2.0 -hotfix/cli-regression +fix/cli-regression +``` + +### E2E tests (Maestro) + +End-to-end tests use [Maestro](https://maestro.dev/) to drive the hello-world example on real emulators and simulators. + +```bash +# Install Maestro (one-time) +curl -Ls "https://get.maestro.mobile.dev" | bash + +# For iOS, also install idb-companion +brew tap facebook/fb && brew install idb-companion ``` +Build and launch the app first, then run the tests: + +```bash +cd examples/hello-world + +# Android (emulator must be running) +pn run android +maestro test ../../tests/e2e/android.yaml + +# iOS (simulator must be running; --platform ios needed when an Android emulator is also connected) +pn run ios +maestro --platform ios test ../../tests/e2e/ios.yaml +``` + +Test flows live in `tests/e2e/flows/` and cover main page rendering, counter interaction, and multi-page navigation. The `e2e.yml` workflow runs these automatically on pushes to `main` and PRs. + +### CI + +- **CI** (`ci.yml`): runs formatter, linter, type checker, and tests on every push and PR. +- **E2E** (`e2e.yml`): builds the hello-world example on Android (Linux emulator) and iOS (macOS simulator), then runs Maestro flows. Triggers on pushes to `main`, PRs, and manual dispatch. +- **PR Lint** (`pr-lint.yml`): validates the PR title against Conventional Commits format (protects squash merges) and checks individual commit messages via commitlint (protects rebase merges). Recommended: add the **PR title** job as a required status check in branch-protection settings. +- **Release** (`release.yml`): runs on merge to `main`; computes version, generates changelog, tags, creates GitHub Release, and (when `DRAFT_RELEASE` is `"false"`) publishes to PyPI. +- **Docs** (`docs.yml`): deploys documentation to GitHub Pages on push to `main`. + ## Security and provenance - Avoid bundling secrets or credentials in templates or code. diff --git a/LICENSE b/LICENSE index 18fff31..ad12cea 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2025 PythonNative +Copyright (c) 2026 Owen Carey Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 4418c1b..62f5958 100644 --- a/README.md +++ b/README.md @@ -1,74 +1,79 @@ -# PythonNative +

+ PythonNative +

-**PythonNative** is a cross-platform toolkit that allows you to create native Android and iOS apps using Python. Inspired by frameworks like React Native and NativeScript, PythonNative provides a Pythonic interface for building native UI elements, handling lifecycle events, and accessing platform-specific APIs. +

+ Build native Android and iOS apps in Python. +

+ +

+ CI + Release + PyPI Version + Python Versions + License: MIT + Docs +

+ +

+ Documentation · + Getting Started · + Examples · + Contributing +

+ +--- + +## Overview + +PythonNative is a cross-platform toolkit for building native Android and iOS apps in Python. It provides a **declarative, React-like component model** with hooks and automatic reconciliation, powered by Chaquopy on Android and rubicon-objc on iOS. Write function components with `use_state`, `use_effect`, and friends, just like React, and let PythonNative handle creating and updating native views. ## Features -- **Native UI Components**: Create and manage native buttons, labels, lists, and more, all from Python. -- **Cross-Platform**: Write once, run on both Android and iOS. -- **Lifecycle Management**: Handle app lifecycle events with ease. -- **Native API Access**: Access device features like Camera, Geolocation, and Notifications. -- **Powered by Proven Tools**: PythonNative integrates seamlessly with [Rubicon](https://beeware.org/project/projects/bridges/rubicon/) for iOS and [Chaquopy](https://chaquo.com/chaquopy/) for Android, ensuring robust native performance. +- **Declarative UI:** Describe *what* your UI should look like with element functions (`Text`, `Button`, `Column`, `Row`, etc.). PythonNative creates and updates native views automatically. +- **Hooks and function components:** Manage state with `use_state`, side effects with `use_effect`, and navigation with `use_navigation`, all through one consistent pattern. +- **`style` prop:** Pass all visual and layout properties through a single `style` dict, composable via `StyleSheet`. +- **Virtual view tree + reconciler:** Element trees are diffed and patched with minimal native mutations, similar to React's reconciliation. +- **Direct native bindings:** Python calls platform APIs directly through Chaquopy and rubicon-objc, with no JavaScript bridge. +- **CLI scaffolding:** `pn init` creates a ready-to-run project; `pn run android` and `pn run ios` build and launch your app. +- **Navigation:** Push and pop screens with argument passing via the `use_navigation()` hook. +- **Bundled templates:** Android Gradle and iOS Xcode templates are included, so scaffolding requires no network access. ## Quick Start ### Installation -First, install PythonNative via pip: - ```bash pip install pythonnative ``` -### Create Your First App +### Usage -Initialize a new PythonNative app: +```python +import pythonnative as pn -```bash -pn init my_app -``` -Your app directory will look like this: - -```text -my_app/ -├── README.md -├── app -│ ├── __init__.py -│ ├── main_page.py -│ └── resources -├── pythonnative.json -├── requirements.txt -└── tests +@pn.component +def MainPage(): + count, set_count = pn.use_state(0) + return pn.Column( + pn.Text(f"Count: {count}", style={"font_size": 24}), + pn.Button( + "Tap me", + on_click=lambda: set_count(count + 1), + ), + style={"spacing": 12, "padding": 16}, + ) ``` -### Writing Views +## Documentation -In PythonNative, everything is a view. Here's a simple example of how to create a main page with a list view: +Visit [docs.pythonnative.com](https://docs.pythonnative.com/) for the full documentation, including getting started guides, platform-specific instructions for Android and iOS, API reference, and working examples. -```python -import pythonnative as pn - -class MainPage(pn.Page): - def __init__(self, native_instance): - super().__init__(native_instance) - - def on_create(self): - super().on_create() - stack_view = pn.StackView(self.native_instance) - list_data = ["item_{}".format(i) for i in range(100)] - list_view = pn.ListView(self.native_instance, list_data) - stack_view.add_view(list_view) - self.set_root_view(stack_view) -``` +## Contributing -### Run the app +Contributions are welcome. Please see [CONTRIBUTING.md](CONTRIBUTING.md) for setup instructions, coding standards, and guidelines for submitting pull requests. -```bash -pn run android -pn run ios -``` - -## Documentation +## License -For detailed guides and API references, visit the [PythonNative documentation](https://docs.pythonnative.com/). +[MIT](LICENSE) diff --git a/docs/api/component-properties.md b/docs/api/component-properties.md new file mode 100644 index 0000000..9def148 --- /dev/null +++ b/docs/api/component-properties.md @@ -0,0 +1,213 @@ +# Component Property Reference + +All visual and layout properties are passed via the `style` dict (or list of dicts) to element functions. Behavioural properties (callbacks, data, content) remain as keyword arguments. + +## Common layout properties (inside `style`) + +All components accept these layout properties in their `style` dict: + +- `width` — fixed width in dp (Android) / pt (iOS) +- `height` — fixed height +- `flex` — flex grow factor (shorthand for `flex_grow`) +- `flex_grow` — how much a child grows to fill available space +- `flex_shrink` — how much a child shrinks when space is limited +- `margin` — outer spacing (int, float, or dict with `horizontal`, `vertical`, `left`, `top`, `right`, `bottom`) +- `min_width`, `max_width` — width constraints +- `min_height`, `max_height` — height constraints +- `align_self` — override parent alignment (`"stretch"`, `"flex_start"`, `"center"`, `"flex_end"`) +- `key` — stable identity for reconciliation (passed as a kwarg, not inside `style`) + +## View + +```python +pn.View(*children, style={ + "flex_direction": "column", + "justify_content": "center", + "align_items": "center", + "overflow": "hidden", + "spacing": 8, + "padding": 16, + "background_color": "#F5F5F5", +}) +``` + +Universal flex container (like React Native's `View`). Defaults to `flex_direction: "column"`. + +Flex container properties (inside `style`): + +- `flex_direction` — `"column"` (default), `"row"`, `"column_reverse"`, `"row_reverse"` +- `justify_content` — `"flex_start"`, `"center"`, `"flex_end"`, `"space_between"`, `"space_around"`, `"space_evenly"` +- `align_items` — `"stretch"`, `"flex_start"`, `"center"`, `"flex_end"` +- `overflow` — `"visible"` (default), `"hidden"` +- `spacing`, `padding`, `background_color` + +## Text + +```python +pn.Text(text, style={"font_size": 18, "color": "#333", "bold": True, "text_align": "center"}) +``` + +- `text` — display string (positional) +- Style properties: `font_size`, `color`, `bold`, `text_align`, `background_color`, `max_lines` + +## Button + +```python +pn.Button(title, on_click=handler, style={"color": "#FFF", "background_color": "#007AFF", "font_size": 16}) +``` + +- `title` — button label (positional) +- `on_click` — callback `() -> None` +- `enabled` — interactive state (kwarg, default `True`) +- Style properties: `color`, `background_color`, `font_size` + +## Column / Row + +```python +pn.Column(*children, style={"spacing": 12, "padding": 16, "align_items": "center"}) +pn.Row(*children, style={"spacing": 8, "justify_content": "space_between"}) +``` + +Convenience wrappers for `View` with fixed `flex_direction`: + +- `Column` = `View` with `flex_direction: "column"` (always vertical) +- `Row` = `View` with `flex_direction: "row"` (always horizontal) + +- `*children` — child elements (positional) +- Style properties: + - `spacing` — gap between children (dp / pt) + - `padding` — inner padding (int for all sides, or dict with `horizontal`, `vertical`, `left`, `top`, `right`, `bottom`) + - `align_items` — cross-axis alignment: `"stretch"`, `"flex_start"`, `"center"`, `"flex_end"`, `"leading"`, `"trailing"` + - `justify_content` — main-axis distribution: `"flex_start"`, `"center"`, `"flex_end"`, `"space_between"`, `"space_around"`, `"space_evenly"` + - `overflow` — `"visible"` (default), `"hidden"` + - `background_color` — container background + +## SafeAreaView + +```python +pn.SafeAreaView(*children, style={"background_color": "#FFF", "padding": 8}) +``` + +Container that respects safe area insets (notch, status bar). + +## ScrollView + +```python +pn.ScrollView(child, style={"background_color": "#FFF"}) +``` + +## TextInput + +```python +pn.TextInput(value="", placeholder="Enter text", on_change=handler, secure=False, + style={"font_size": 16, "color": "#000", "background_color": "#FFF"}) +``` + +- `on_change` — callback `(str) -> None` receiving new text + +## Image + +```python +pn.Image(source="https://example.com/photo.jpg", 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` + +## Switch + +```python +pn.Switch(value=False, on_change=handler) +``` + +- `on_change` — callback `(bool) -> None` + +## Slider + +```python +pn.Slider(value=0.5, min_value=0.0, max_value=1.0, on_change=handler) +``` + +- `on_change` — callback `(float) -> None` + +## ProgressBar + +```python +pn.ProgressBar(value=0.5, style={"background_color": "#EEE"}) +``` + +- `value` — 0.0 to 1.0 + +## ActivityIndicator + +```python +pn.ActivityIndicator(animating=True) +``` + +## WebView + +```python +pn.WebView(url="https://example.com") +``` + +## Spacer + +```python +pn.Spacer(size=16, flex=1) +``` + +- `size` — fixed dimension in dp / pt +- `flex` — flex grow factor + +## Pressable + +```python +pn.Pressable(child, on_press=handler, on_long_press=handler) +``` + +Wraps any child element with tap/long-press handling. + +## Modal + +```python +pn.Modal(*children, visible=show_modal, on_dismiss=handler, title="Confirm", + style={"background_color": "#FFF"}) +``` + +Overlay dialog shown when `visible=True`. + +## TabBar + +```python +pn.Element("TabBar", { + "items": [ + {"name": "Home", "title": "Home"}, + {"name": "Settings", "title": "Settings"}, + ], + "active_tab": "Home", + "on_tab_select": handler, +}) +``` + +Native tab bar — typically created automatically by `Tab.Navigator`. + +| Platform | Native view | +|----------|--------------------------| +| Android | `BottomNavigationView` | +| iOS | `UITabBar` | + +- `items` — list of `{"name": str, "title": str}` dicts defining each tab +- `active_tab` — the `name` of the currently active tab +- `on_tab_select` — callback `(str) -> None` receiving the selected tab name + +## FlatList + +```python +pn.FlatList(data=items, render_item=render_fn, key_extractor=key_fn, + separator_height=1, style={"background_color": "#FFF"}) +``` + +- `data` — list of items +- `render_item` — `(item, index) -> Element` function +- `key_extractor` — `(item, index) -> str` for stable keys +- `separator_height` — spacing between items diff --git a/docs/api/pythonnative.md b/docs/api/pythonnative.md index 744d7a4..f05178c 100644 --- a/docs/api/pythonnative.md +++ b/docs/api/pythonnative.md @@ -1,9 +1,92 @@ # pythonnative package -API reference will be generated here via mkdocstrings. +## Public API -Key flags and helpers (0.2.0): +### create_page -- `pythonnative.utils.IS_ANDROID`: platform flag with robust detection for Chaquopy/Android. -- `pythonnative.utils.get_android_context()`: returns the current Android Activity/Context when running on Android. -- `pythonnative.utils.set_android_context(ctx)`: set by `pythonnative.Page` on Android; you generally don’t call this directly. +`pythonnative.create_page(...)` — called internally by native templates to bootstrap the root component. You don't call this directly. + +### Element functions + +- `pythonnative.Text`, `Button`, `Column`, `Row`, `ScrollView`, `TextInput`, `Image`, `Switch`, `ProgressBar`, `ActivityIndicator`, `WebView`, `Spacer` +- `pythonnative.View`, `SafeAreaView`, `Modal`, `Slider`, `Pressable`, `FlatList` + +Each returns an `Element` descriptor. Visual and layout properties are passed via `style={...}`. See the Component Property Reference for full details. + +### ErrorBoundary + +`pythonnative.ErrorBoundary(child, fallback=...)` — catches render errors in *child* and displays *fallback* instead. *fallback* may be an `Element` or a callable that receives the exception and returns an `Element`. + +### Element + +`pythonnative.Element` — the descriptor type returned by element functions. You generally don't create these directly. + +### Hooks + +Function component primitives: + +- `pythonnative.component` — decorator to create a function component +- `pythonnative.use_state(initial)` — local component state +- `pythonnative.use_reducer(reducer, initial_state)` — reducer-based state management; returns `(state, dispatch)` +- `pythonnative.use_effect(effect, deps)` — side effects, run after native commit +- `pythonnative.use_navigation()` — navigation handle (navigate/go_back/get_params) +- `pythonnative.use_route()` — convenience hook for current route params +- `pythonnative.use_focus_effect(effect, deps)` — like `use_effect` but only runs when the screen is focused +- `pythonnative.use_memo(factory, deps)` — memoised values +- `pythonnative.use_callback(fn, deps)` — stable function references +- `pythonnative.use_ref(initial)` — mutable ref object +- `pythonnative.use_context(context)` — read from context +- `pythonnative.create_context(default)` — create a new context +- `pythonnative.Provider(context, value, child)` — provide a context value + +### Navigation + +Declarative, component-based navigation system: + +- `pythonnative.NavigationContainer(child)` — root container for the navigation tree +- `pythonnative.create_stack_navigator()` — create a stack-based navigator (returns object with `.Navigator` and `.Screen`) +- `pythonnative.create_tab_navigator()` — create a tab-based navigator +- `pythonnative.create_drawer_navigator()` — create a drawer-based navigator + +### Batching + +- `pythonnative.batch_updates()` — context manager that batches multiple state updates into a single re-render + +### Styling + +- `pythonnative.StyleSheet` — utility for creating and composing style dicts +- `pythonnative.ThemeContext` — built-in theme context (defaults to light theme) + +## Native API modules + +- `pythonnative.native_modules.Camera` — photo capture and gallery picking +- `pythonnative.native_modules.Location` — GPS / location services +- `pythonnative.native_modules.FileSystem` — app-scoped file I/O +- `pythonnative.native_modules.Notifications` — local push notifications + +## Internal helpers + +- `pythonnative.utils.IS_ANDROID` — platform flag with robust detection for Chaquopy/Android. +- `pythonnative.utils.get_android_context()` — returns the current Android `Activity`/`Context` when running on Android. +- `pythonnative.utils.set_android_context(ctx)` — set internally during page bootstrapping; you generally don't call this directly. +- `pythonnative.utils.get_android_fragment_container()` — returns the current Fragment container `ViewGroup` used for page rendering. + +## Reconciler + +`pythonnative.reconciler.Reconciler` — diffs element trees and applies minimal native mutations. Supports key-based child reconciliation, function components, context providers, and error boundaries. Effects are flushed after each mount/reconcile pass. Used internally by `create_page`. + +## Hot reload + +`pythonnative.hot_reload.FileWatcher` — watches a directory for file changes and triggers a callback. Used by `pn run --hot-reload`. + +`pythonnative.hot_reload.ModuleReloader` — reloads changed Python modules on the device and triggers page re-rendering. + +## Native view registry + +`pythonnative.native_views.NativeViewRegistry` — maps element type names to platform-specific handlers. Use `set_registry()` to inject a mock for testing. + +The `native_views` package is organised into submodules: + +- `pythonnative.native_views.base` — shared `ViewHandler` protocol and utilities (`parse_color_int`, `resolve_padding`, `LAYOUT_KEYS`) +- `pythonnative.native_views.android` — Android handlers (only imported at runtime on Android via Chaquopy) +- `pythonnative.native_views.ios` — iOS handlers (only imported at runtime on iOS via rubicon-objc) diff --git a/docs/assets/banner.jpg b/docs/assets/banner.jpg new file mode 100644 index 0000000..d7c4c04 Binary files /dev/null and b/docs/assets/banner.jpg differ diff --git a/docs/concepts/architecture.md b/docs/concepts/architecture.md new file mode 100644 index 0000000..5505fdb --- /dev/null +++ b/docs/concepts/architecture.md @@ -0,0 +1,154 @@ +# Architecture + +PythonNative combines **direct native bindings** with a **declarative reconciler**, giving you React-like ergonomics while calling native platform APIs synchronously from Python. + +## High-level model + +1. **Declarative element tree:** Your `@pn.component` function returns a tree of `Element` descriptors (similar to React elements / virtual DOM nodes). +2. **Function components and hooks:** All UI is built with `@pn.component` functions using `use_state`, `use_reducer`, `use_effect`, `use_navigation`, etc. — inspired by React hooks but designed for Python. +3. **Reconciler:** On first render, the reconciler walks the tree and creates real native views via the platform backend. On subsequent renders (triggered by hook state changes), it diffs the new tree against the old one and applies the minimal set of native mutations. +4. **Post-render effects:** Effects queued via `use_effect` are flushed **after** the reconciler commits native mutations, matching React semantics. This guarantees that effect callbacks interact with the committed native tree. +5. **State batching:** Multiple state updates triggered during a render pass (e.g. from effects) are automatically batched into a single re-render. Explicit batching is available via `pn.batch_updates()`. +6. **Key-based reconciliation:** Children can be assigned stable `key` values to preserve identity across re-renders — critical for lists and dynamic content. +7. **Error boundaries:** `pn.ErrorBoundary` catches render errors in child subtrees and displays fallback UI, preventing a single component failure from crashing the entire page. +8. **Direct bindings:** Under the hood, native views are created and updated through direct platform calls: + - **iOS:** rubicon-objc exposes Objective-C/Swift classes (`UILabel`, `UIButton`, `UIStackView`, etc.). + - **Android:** Chaquopy exposes Java classes (`android.widget.TextView`, `android.widget.Button`, etc.) via the JNI bridge. +9. **Thin native bootstrap:** The host app remains native (Android `Activity` or iOS `UIViewController`). It calls `create_page()` internally to bootstrap your Python component, and the reconciler drives the UI from there. + +## How it works + +``` +@pn.component fn → Element tree → Reconciler → Native views → Flush effects + ↑ +Hook set_state() → schedule render → diff → patch native views → Flush effects + (batched) +``` + +The reconciler uses **key-based diffing** (matching children by key first, then by position). When a child with the same key/type is found, its props are updated in-place on the native view. When the type changes, the old native view is destroyed and a new one is created. + +### Render lifecycle + +1. **Render phase:** Component functions execute. Hooks record state reads, queue effects, and register memos. No native mutations happen yet. +2. **Commit phase:** The reconciler applies the diff to native views — creating, updating, and removing views as needed. +3. **Effect phase:** Pending effects are flushed in depth-first order (children before parents). Cleanup functions from the previous render run before new effect callbacks. +4. **Drain phase:** If effects set state, a new render pass is automatically triggered and the cycle repeats (up to a safety limit to prevent infinite loops). + +## Component model + +PythonNative uses a single component model: **function components** decorated with `@pn.component`. + +```python +@pn.component +def Counter(initial: int = 0): + count, set_count = pn.use_state(initial) + return pn.Column( + pn.Text(f"Count: {count}", style={"font_size": 18}), + pn.Button("+", on_click=lambda: set_count(count + 1)), + style={"spacing": 4}, + ) +``` + +Each component is a Python function that: +- Accepts props as keyword arguments +- Uses hooks for state (`use_state`, `use_reducer`), side effects (`use_effect`), navigation (`use_navigation`), and more +- Returns an `Element` tree describing the UI +- Each call site creates an independent instance with its own hook state + +The entry point `create_page()` is called internally by native templates to bootstrap your root component. You don't call it directly. + +## Styling + +- **`style` prop:** Pass a dict (or list of dicts) to any component — `style={"font_size": 24, "color": "#333"}`. +- **StyleSheet:** Create reusable named style dictionaries with `pn.StyleSheet.create(...)`. +- **Theming:** Use `pn.ThemeContext` with `pn.Provider` and `pn.use_context` to propagate theme values through the tree. + +## Layout + +PythonNative uses a **flexbox-inspired layout model** built on platform-native layout managers. + +`View` is the **universal flex container** (like React Native's `View`). It defaults to `flex_direction: "column"`. `Column` and `Row` are convenience wrappers that fix the direction. + +### Flex container properties (inside `style`) + +- `flex_direction` — `"column"` (default), `"row"`, `"column_reverse"`, `"row_reverse"` +- `justify_content` — main-axis distribution: `"flex_start"`, `"center"`, `"flex_end"`, `"space_between"`, `"space_around"`, `"space_evenly"` +- `align_items` — cross-axis alignment: `"stretch"`, `"flex_start"`, `"center"`, `"flex_end"` +- `overflow` — `"visible"` (default), `"hidden"` +- `spacing` — gap between children (dp / pt) +- `padding` — inner spacing + +### Child layout properties + +- `flex` — flex grow factor (shorthand) +- `flex_grow`, `flex_shrink` — individual flex properties +- `align_self` — override the parent's `align_items` for this child +- `width`, `height` — fixed dimensions +- `min_width`, `min_height` — minimum size constraints +- `margin` — outer spacing + +Under the hood: +- **Android:** `LinearLayout` with gravity, weights, and divider-based spacing +- **iOS:** `UIStackView` with axis, alignment, distribution, and layout margins + +## Native view handlers + +Platform-specific rendering logic lives in the `native_views` package, organised into dedicated submodules: + +- `native_views.base` — shared `ViewHandler` protocol and common utilities (colour parsing, padding resolution, layout keys, flex constants) +- `native_views.android` — Android handlers using Chaquopy's Java bridge (`jclass`, `dynamic_proxy`) +- `native_views.ios` — iOS handlers using rubicon-objc (`ObjCClass`, `objc_method`) + +Column, Row, and View share a single `FlexContainerHandler` class on each platform. The handler reads `flex_direction` from the element's props to configure the native layout container. + +Each handler class maps an element type name (e.g. `"Text"`, `"Button"`) to platform-native widget creation, property updates, and child management. The `NativeViewRegistry` lazily imports only the relevant platform module at runtime, so the package can be imported on any platform for testing. + +## Comparison + +- **Versus React Native:** RN uses JSX + a JavaScript bridge + Yoga layout. PythonNative uses Python + direct native calls + platform layout managers. No JS bridge, no serialisation overhead. +- **Versus NativeScript:** Similar philosophy (direct, synchronous native access), but PythonNative adds a declarative reconciler layer and React-like hooks that NativeScript does not have by default. + +## iOS flow (Rubicon-ObjC) + +- The iOS template (Swift + PythonKit) boots Python and calls `create_page()` internally with the current `UIViewController` pointer. +- The reconciler creates UIKit views and attaches them to the controller's view. +- State changes trigger re-renders; the reconciler patches UIKit views in-place. + +## Android flow (Chaquopy) + +- The Android template (Kotlin + Chaquopy) initializes Python in `MainActivity` and passes the `Activity` to Python. +- `PageFragment` calls `create_page()` internally, which renders the root component and attaches views to the fragment container. +- State changes trigger re-render; the reconciler patches Android views in-place. + +## Hot reload + +During development, `pn run --hot-reload` watches `app/` for file changes and pushes updated Python files to the running app, enabling near-instant UI updates without full rebuilds. + +## Native API modules + +PythonNative provides cross-platform modules for common device APIs: + +- `pythonnative.native_modules.Camera` — photo capture and gallery +- `pythonnative.native_modules.Location` — GPS / location services +- `pythonnative.native_modules.FileSystem` — app-scoped file I/O +- `pythonnative.native_modules.Notifications` — local push notifications + +## Navigation model overview + +PythonNative provides two navigation approaches: + +- **Declarative navigators** (recommended): `NavigationContainer` with `create_stack_navigator()`, `create_tab_navigator()`, and `create_drawer_navigator()`. Navigation state is managed in Python as component state, and navigators are composable — you can nest tabs inside stacks, etc. +- **Page-level navigation**: `use_navigation()` returns a `NavigationHandle` with `.navigate()`, `.go_back()`, and `.get_params()`, delegating to native platform navigation when running on device. + +Both approaches are supported. The declarative system uses the existing reconciler pipeline — navigators are function components that render the active screen via `use_state`, and navigation context is provided via `Provider`. + +See the [Navigation guide](../guides/navigation.md) for full details. + +- iOS: one host `UIViewController` class, many instances pushed on a `UINavigationController`. +- Android: single host `Activity` with a `NavHostFragment` and a stack of generic `PageFragment`s driven by a navigation graph. + +## Related docs + +- Guides / Android: guides/android.md +- Guides / iOS: guides/ios.md +- Concepts / Components: concepts/components.md diff --git a/docs/concepts/components.md b/docs/concepts/components.md index e1e33aa..cbeb421 100644 --- a/docs/concepts/components.md +++ b/docs/concepts/components.md @@ -1,51 +1,220 @@ # Components -High-level overview of PythonNative components and how they map to native UI. +PythonNative uses a **declarative component model** inspired by React. You describe *what* the UI should look like, and the framework handles creating and updating native views. -## Constructor pattern (0.2.0) +## Element functions -- All core components share a consistent, contextless constructor on both platforms. -- On Android, a `Context` is acquired implicitly from the current `Activity` set by `pn.Page`. -- On iOS, UIKit classes are allocated/initialized directly. - -Examples: +UI is built with element-creating functions. Each returns a lightweight `Element` descriptor — no native objects are created until the reconciler mounts the tree. ```python import pythonnative as pn -class MainPage(pn.Page): - def __init__(self, native_instance): - super().__init__(native_instance) +pn.Text("Hello", style={"font_size": 18, "color": "#333333"}) +pn.Button("Tap me", on_click=lambda: print("tapped")) +pn.Column( + pn.Text("First"), + pn.Text("Second"), + style={"spacing": 8, "padding": 16}, +) +``` + +### Available components + +**Layout:** + +- `View(*children, style=...)` — universal flex container (default `flex_direction: "column"`) +- `Column(*children, style=...)` — vertical flex container (fixed `flex_direction: "column"`) +- `Row(*children, style=...)` — horizontal flex container (fixed `flex_direction: "row"`) +- `ScrollView(child, style=...)` — scrollable container +- `SafeAreaView(*children, style=...)` — safe-area-aware container +- `Spacer(size, flex)` — empty space + +**Display:** + +- `Text(text, style=...)` — text display +- `Image(source, style=...)` — image display (supports URLs and resource names) +- `WebView(url)` — embedded web content + +**Input:** + +- `Button(title, on_click, style=...)` — tappable button +- `TextInput(value, placeholder, on_change, secure, style=...)` — text entry +- `Switch(value, on_change)` — toggle switch +- `Slider(value, min_value, max_value, on_change)` — continuous slider +- `Pressable(child, on_press, on_long_press)` — tap handler wrapper + +**Feedback:** + +- `ProgressBar(value)` — determinate progress (0.0–1.0) +- `ActivityIndicator(animating)` — indeterminate spinner + +**Overlay:** + +- `Modal(*children, visible, on_dismiss, title)` — modal dialog + +**Error handling:** + +- `ErrorBoundary(child, fallback)` — catches render errors in child and displays fallback + +**Lists:** + +- `FlatList(data, render_item, key_extractor, separator_height)` — scrollable data list + +### Flex layout model + +PythonNative uses a **flexbox-inspired layout model**. `View` is the universal flex container — `Column` and `Row` are convenience wrappers. + +#### Flex container properties (inside `style`) + +- `flex_direction` — `"column"` (default), `"row"`, `"column_reverse"`, `"row_reverse"` +- `justify_content` — main-axis distribution: `"flex_start"`, `"center"`, `"flex_end"`, `"space_between"`, `"space_around"`, `"space_evenly"` +- `align_items` — cross-axis alignment: `"stretch"`, `"flex_start"`, `"center"`, `"flex_end"` +- `overflow` — `"visible"` (default), `"hidden"` +- `spacing` — gap between children (dp / pt) +- `padding` — inner spacing + +#### Child layout properties + +All components accept these in their `style` dict: + +- `width`, `height` — fixed dimensions (dp / pt) +- `flex` — flex grow factor (shorthand) +- `flex_grow`, `flex_shrink` — individual flex properties +- `margin` — outer margin (int, float, or dict like padding) +- `min_width`, `min_height` — minimum size constraints +- `max_width`, `max_height` — maximum size constraints +- `align_self` — override parent alignment for this child + +#### Example: centering content + +```python +pn.View( + pn.Text("Centered"), + style={"flex": 1, "justify_content": "center", "align_items": "center"}, +) +``` + +#### Example: horizontal row with spacing + +```python +pn.Row( + pn.Button("Cancel"), + pn.Spacer(flex=1), + pn.Button("OK"), + style={"padding": 16, "align_items": "center"}, +) +``` + +## Function components — the building block + +All UI in PythonNative is built with `@pn.component` function components. Each screen is a function component that returns an element tree: + +```python +@pn.component +def MainPage(): + name, set_name = pn.use_state("World") + return pn.Text(f"Hello, {name}!", style={"font_size": 24}) +``` + +The entry point `create_page()` is called internally by native templates to bootstrap your root component. You don't call it directly — just export your component and configure the entry point in `pythonnative.json`. + +## State and re-rendering + +Use `pn.use_state(initial)` to create local component state. Call the setter to update — the framework automatically re-renders the component and applies only the differences to the native views: + +```python +@pn.component +def CounterPage(): + count, set_count = pn.use_state(0) + + return pn.Column( + pn.Text(f"Count: {count}", style={"font_size": 24}), + pn.Button("Increment", on_click=lambda: set_count(count + 1)), + style={"spacing": 12}, + ) +``` + +## Composing components + +Build complex UIs by composing smaller `@pn.component` functions. Each instance has **independent state**: + +```python +@pn.component +def Counter(label: str = "Count", initial: int = 0): + count, set_count = pn.use_state(initial) + + return pn.Column( + pn.Text(f"{label}: {count}", style={"font_size": 18}), + pn.Row( + pn.Button("-", on_click=lambda: set_count(count - 1)), + pn.Button("+", on_click=lambda: set_count(count + 1)), + style={"spacing": 8}, + ), + style={"spacing": 4}, + ) + + +@pn.component +def MainPage(): + return pn.Column( + Counter(label="Apples", initial=0), + Counter(label="Oranges", initial=5), + style={"spacing": 16, "padding": 16}, + ) +``` + +Changing one `Counter` doesn't affect the other — each has its own hook state. + +### Available hooks + +- `use_state(initial)` — local component state; returns `(value, setter)` +- `use_reducer(reducer, initial_state)` — reducer-based state; returns `(state, dispatch)` +- `use_effect(effect, deps)` — side effects, run after native commit (timers, API calls, subscriptions) +- `use_memo(factory, deps)` — memoised computed values +- `use_callback(fn, deps)` — stable function references +- `use_ref(initial)` — mutable ref that persists across renders +- `use_context(context)` — read from a context provider +- `use_navigation()` — navigation handle for navigate/go_back/get_params +- `use_route()` — convenience hook for current route params +- `use_focus_effect(effect, deps)` — like `use_effect` but only runs when the screen is focused + +### Custom hooks - def on_create(self): - super().on_create() - stack = pn.StackView() - stack.add_view(pn.Label("Hello")) - stack.add_view(pn.Button("Tap me")) - stack.add_view(pn.TextField("initial")) - self.set_root_view(stack) +Extract reusable stateful logic into plain functions: + +```python +def use_toggle(initial: bool = False): + value, set_value = pn.use_state(initial) + def toggle(): + set_value(not value) + return value, toggle ``` -Notes: -- `pn.Page` stores the Android `Activity` so components like `pn.Button()` and `pn.Label()` can construct their native counterparts. -- If you construct views before the `Page` is created on Android, a runtime error will be raised because no `Context` is available. +### Context and Provider + +Share values across the tree without prop drilling: + +```python +theme = pn.create_context({"primary": "#007AFF"}) -## Core components (0.2.0) +@pn.component +def App(): + return pn.Provider(theme, {"primary": "#FF0000"}, + MyComponent() + ) -Stabilized with contextless constructors on both platforms: +@pn.component +def MyComponent(): + t = pn.use_context(theme) + return pn.Button("Click", style={"color": t["primary"]}) +``` -- `Page` -- `StackView` -- `Label`, `Button` -- `ImageView` -- `TextField`, `TextView` -- `Switch` -- `ProgressView`, `ActivityIndicatorView` -- `WebView` +## Platform detection -APIs are intentionally small and grow progressively in later releases. Properties and setters are kept consistent where supported by both platforms. +Use `pythonnative.utils.IS_ANDROID` when you need platform-specific logic: -## Platform detection and Android context +```python +from pythonnative.utils import IS_ANDROID -- Use `pythonnative.utils.IS_ANDROID` for platform checks when needed. -- On Android, `Page` records the current `Activity` so child views can acquire a `Context` implicitly. Constructing views before `Page` initialization will raise. +title = "Android App" if IS_ANDROID else "iOS App" +``` diff --git a/docs/concepts/hooks.md b/docs/concepts/hooks.md new file mode 100644 index 0000000..49b75aa --- /dev/null +++ b/docs/concepts/hooks.md @@ -0,0 +1,303 @@ +# Function Components and Hooks + +PythonNative uses React-like function components with hooks for managing state, effects, navigation, memoisation, and context. Function components decorated with `@pn.component` are the only way to build UI in PythonNative. + +## Creating a function component + +Decorate a Python function with `@pn.component`: + +```python +import pythonnative as pn + +@pn.component +def Greeting(name: str = "World"): + return pn.Text(f"Hello, {name}!", style={"font_size": 20}) +``` + +Use it like any other component: + +```python +@pn.component +def MyPage(): + return pn.Column( + Greeting(name="Alice"), + Greeting(name="Bob"), + style={"spacing": 12}, + ) +``` + +## Hooks + +Hooks let function components manage state and side effects. They must be called at the top level of a `@pn.component` function (not inside loops or conditions). + +### use_state + +Local component state. Returns `(value, setter)`. + +```python +@pn.component +def Counter(initial: int = 0): + count, set_count = pn.use_state(initial) + + return pn.Column( + pn.Text(f"Count: {count}"), + pn.Button("+", on_click=lambda: set_count(count + 1)), + ) +``` + +The setter accepts a value or a function that receives the current value: + +```python +set_count(10) # set directly +set_count(lambda prev: prev + 1) # functional update +``` + +If the initial value is expensive to compute, pass a callable: + +```python +count, set_count = pn.use_state(lambda: compute_default()) +``` + +### use_reducer + +For complex state logic, `use_reducer` lets you manage state transitions through a reducer function — similar to React's `useReducer`: + +```python +def reducer(state, action): + if action == "increment": + return state + 1 + if action == "decrement": + return state - 1 + if action == "reset": + return 0 + return state + +@pn.component +def Counter(): + count, dispatch = pn.use_reducer(reducer, 0) + + return pn.Column( + pn.Text(f"Count: {count}"), + pn.Row( + pn.Button("-", on_click=lambda: dispatch("decrement")), + pn.Button("+", on_click=lambda: dispatch("increment")), + pn.Button("Reset", on_click=lambda: dispatch("reset")), + style={"spacing": 8}, + ), + ) +``` + +The reducer receives the current state and an action, and returns the new state. Actions can be any value (strings, dicts, etc.). The component only re-renders when the reducer returns a different state. + +### use_effect + +Run side effects **after** the native view tree is committed. The effect function may return a cleanup callable. + +```python +@pn.component +def Timer(): + seconds, set_seconds = pn.use_state(0) + + def tick(): + import threading + t = threading.Timer(1.0, lambda: set_seconds(seconds + 1)) + t.start() + return t.cancel # cleanup: cancel the timer + + pn.use_effect(tick, [seconds]) + + return pn.Text(f"Elapsed: {seconds}s") +``` + +Effects are **deferred** — they are queued during the render phase and executed after the reconciler finishes committing native view mutations. This means effect callbacks can safely measure layout or interact with the committed native tree. + +Dependency control: + +- `pn.use_effect(fn, None)` — run on every render +- `pn.use_effect(fn, [])` — run on mount only +- `pn.use_effect(fn, [a, b])` — run when `a` or `b` change + +### use_navigation + +Access navigation from any component. Returns a `NavigationHandle` with `.navigate()`, `.go_back()`, and `.get_params()`. + +```python +@pn.component +def HomeScreen(): + nav = pn.use_navigation() + + return pn.Column( + pn.Text("Home", style={"font_size": 24}), + pn.Button( + "Go to Details", + on_click=lambda: nav.navigate("Detail", params={"id": 42}), + ), + style={"spacing": 12, "padding": 16}, + ) + +@pn.component +def DetailScreen(): + nav = pn.use_navigation() + item_id = nav.get_params().get("id", 0) + + return pn.Column( + pn.Text(f"Detail #{item_id}", style={"font_size": 20}), + pn.Button("Back", on_click=nav.go_back), + style={"spacing": 12, "padding": 16}, + ) +``` + +See the [Navigation guide](../guides/navigation.md) for full details. + +### use_route + +Convenience hook to read the current route's parameters: + +```python +@pn.component +def DetailScreen(): + params = pn.use_route() + item_id = params.get("id", 0) + return pn.Text(f"Detail #{item_id}") +``` + +### use_focus_effect + +Like `use_effect` but only runs when the screen is focused. Useful for refreshing data when navigating back to a screen: + +```python +@pn.component +def FeedScreen(): + items, set_items = pn.use_state([]) + pn.use_focus_effect(lambda: load_items(set_items), []) + return pn.FlatList(data=items, render_item=lambda item, i: pn.Text(item)) +``` + +### use_memo + +Memoise an expensive computation: + +```python +sorted_items = pn.use_memo(lambda: sorted(items, key=lambda x: x.name), [items]) +``` + +### use_callback + +Return a stable function reference (avoids unnecessary re-renders of children): + +```python +handle_click = pn.use_callback(lambda: set_count(count + 1), [count]) +``` + +### use_ref + +A mutable container that persists across renders without triggering re-renders: + +```python +render_count = pn.use_ref(0) +render_count["current"] += 1 +``` + +### use_context + +Read a value from the nearest `Provider` ancestor: + +```python +theme = pn.use_context(pn.ThemeContext) +color = theme["primary_color"] +``` + +## Context and Provider + +Share values through the component tree without passing props manually: + +```python +user_context = pn.create_context({"name": "Guest"}) + +@pn.component +def App(): + return pn.Provider(user_context, {"name": "Alice"}, + UserProfile() + ) + +@pn.component +def UserProfile(): + user = pn.use_context(user_context) + return pn.Text(f"Welcome, {user['name']}") +``` + +## Batching state updates + +By default, each state setter call triggers a re-render. When you need to update multiple pieces of state at once, use `pn.batch_updates()` to coalesce them into a single render pass: + +```python +@pn.component +def Form(): + name, set_name = pn.use_state("") + email, set_email = pn.use_state("") + + def on_submit(): + with pn.batch_updates(): + set_name("Alice") + set_email("alice@example.com") + # single re-render here + + return pn.Column( + pn.Text(f"{name} <{email}>"), + pn.Button("Fill", on_click=on_submit), + ) +``` + +State updates triggered by effects during a render pass are automatically batched — the framework drains any pending re-renders after effect flushing completes, so you don't need `batch_updates()` inside effects. + +## Error boundaries + +Wrap risky components in `pn.ErrorBoundary` to catch render errors and display a fallback UI: + +```python +@pn.component +def App(): + return pn.ErrorBoundary( + MyRiskyComponent(), + fallback=lambda err: pn.Text(f"Something went wrong: {err}"), + ) +``` + +Without an error boundary, an exception during rendering crashes the entire page. Error boundaries catch errors during both initial mount and subsequent reconciliation. + +## Custom hooks + +Extract reusable stateful logic into plain functions: + +```python +def use_toggle(initial: bool = False): + value, set_value = pn.use_state(initial) + toggle = pn.use_callback(lambda: set_value(not value), [value]) + return value, toggle + +def use_text_input(initial: str = ""): + text, set_text = pn.use_state(initial) + return text, set_text +``` + +Use them in any component: + +```python +@pn.component +def Settings(): + dark_mode, toggle_dark = use_toggle(False) + + return pn.Column( + pn.Text("Settings", style={"font_size": 24, "bold": True}), + pn.Row( + pn.Text("Dark mode"), + pn.Switch(value=dark_mode, on_change=lambda v: toggle_dark()), + ), + ) +``` + +## Rules of hooks + +1. Only call hooks inside `@pn.component` functions +2. Call hooks at the top level — not inside loops, conditions, or nested functions +3. Hooks must be called in the same order on every render diff --git a/docs/examples.md b/docs/examples.md index 821ee4a..20112d5 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -1,3 +1,52 @@ # Examples -A collection of simple examples showing PythonNative components and patterns. +A collection of examples showing PythonNative's declarative component model and patterns. + +## Quick counter + +```python +import pythonnative as pn + + +@pn.component +def Counter(): + count, set_count = pn.use_state(0) + return pn.Column( + pn.Text(f"Count: {count}", style={"font_size": 24}), + pn.Button( + "Increment", + on_click=lambda: set_count(count + 1), + ), + style={"spacing": 12, "padding": 16}, + ) +``` + +## Reusable components + +```python +import pythonnative as pn + + +@pn.component +def LabeledInput(label: str = "", placeholder: str = ""): + return pn.Column( + pn.Text(label, style={"font_size": 14, "bold": True}), + pn.TextInput(placeholder=placeholder), + style={"spacing": 4}, + ) + + +@pn.component +def FormPage(): + return pn.ScrollView( + pn.Column( + pn.Text("Sign Up", style={"font_size": 24, "bold": True}), + LabeledInput(label="Name", placeholder="Enter your name"), + LabeledInput(label="Email", placeholder="you@example.com"), + pn.Button("Submit", on_click=lambda: print("submitted")), + style={"spacing": 12, "padding": 16}, + ) + ) +``` + +See `examples/hello-world/` for a full multi-page demo with navigation. diff --git a/docs/examples/hello-world.md b/docs/examples/hello-world.md index b939317..73fe9cc 100644 --- a/docs/examples/hello-world.md +++ b/docs/examples/hello-world.md @@ -1,24 +1,22 @@ # Hello World -Create a simple page with a label and a button. +Create a simple component with a counter that increments on tap. ```python import pythonnative as pn -class MainPage(pn.Page): - def __init__(self, native_instance): - super().__init__(native_instance) - - def on_create(self): - super().on_create() - stack = pn.StackView() - label = pn.Label("Hello, world!") - button = pn.Button("Tap me") - button.set_on_click(lambda: print("Hello tapped")) - stack.add_view(label) - stack.add_view(button) - self.set_root_view(stack) +@pn.component +def App(): + count, set_count = pn.use_state(0) + return pn.Column( + pn.Text(f"Count: {count}", style={"font_size": 24}), + pn.Button( + "Tap me", + on_click=lambda: set_count(count + 1), + ), + style={"spacing": 12, "padding": 16}, + ) ``` Run it: diff --git a/docs/getting-started.md b/docs/getting-started.md index bd5fb67..f36d5c5 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -24,27 +24,26 @@ A minimal `app/main_page.py` looks like: import pythonnative as pn -class MainPage(pn.Page): - def __init__(self, native_instance): - super().__init__(native_instance) - - def on_create(self): - super().on_create() - stack = pn.StackView() - stack.add_view(pn.Label("Hello from PythonNative!")) - button = pn.Button("Tap me") - button.set_on_click(lambda: print("Button clicked")) - stack.add_view(button) - self.set_root_view(stack) - - -def bootstrap(native_instance): - """Entry point called by the host app (Activity or ViewController).""" - page = MainPage(native_instance) - page.on_create() - return page +@pn.component +def App(): + count, set_count = pn.use_state(0) + return pn.Column( + pn.Text(f"Count: {count}", style={"font_size": 24}), + pn.Button( + "Tap me", + on_click=lambda: set_count(count + 1), + ), + style={"spacing": 12, "padding": 16}, + ) ``` +Key ideas: + +- **`@pn.component`** marks a function as a PythonNative component. The function returns an element tree describing the UI. PythonNative creates and updates native views automatically. +- **`pn.use_state(initial)`** creates local component state. Call the setter to update it — the UI re-renders automatically. +- **`style={...}`** passes visual and layout properties as a dict (or list of dicts) to any component. +- Element functions like `pn.Text(...)`, `pn.Button(...)`, `pn.Column(...)` create lightweight descriptions, not native objects. + ## Run on a platform ```bash diff --git a/docs/guides/android.md b/docs/guides/android.md index 5e939dd..23b3a6e 100644 --- a/docs/guides/android.md +++ b/docs/guides/android.md @@ -8,6 +8,10 @@ Basic steps to build and run an Android project generated by `pn`. No network is required for the template itself; the template zip is bundled with the package. +## Component model + +Your `app/` directory contains `@pn.component` function components. The native Android template uses `create_page()` internally to bootstrap your root component inside a `PageFragment`. You don't call `create_page()` directly — just export your component and configure the entry point in `pythonnative.json`. + ## Run ```bash diff --git a/docs/guides/ios.md b/docs/guides/ios.md index a0a400f..7409bc4 100644 --- a/docs/guides/ios.md +++ b/docs/guides/ios.md @@ -8,6 +8,10 @@ Basic steps to build and run an iOS project generated by `pn`. The default `ViewController.swift` initializes PythonKit, prints the Python version, and attempts to import `rubicon.objc` if present. +## Component model + +Your `app/` directory contains `@pn.component` function components. The native iOS template uses `create_page()` internally to bootstrap your root component inside a `ViewController`. You don't call `create_page()` directly — just export your component and configure the entry point in `pythonnative.json`. + ## Run / Prepare ```bash diff --git a/docs/guides/navigation.md b/docs/guides/navigation.md new file mode 100644 index 0000000..5d33ec7 --- /dev/null +++ b/docs/guides/navigation.md @@ -0,0 +1,214 @@ +# Navigation + +PythonNative offers two approaches to navigation: + +1. **Declarative navigators** (recommended) — component-based, inspired by React Navigation +2. **Page-level push/pop** — imperative navigation via `use_navigation()` (for native page transitions) + +## Declarative Navigation + +Declarative navigators manage screen state as components. Define your screens once, and the navigator handles rendering, transitions, and state. + +### Stack Navigator + +A stack navigator manages a stack of screens — push to go forward, pop to go back. + +```python +import pythonnative as pn +from pythonnative.navigation import NavigationContainer, create_stack_navigator + +Stack = create_stack_navigator() + +@pn.component +def App(): + return NavigationContainer( + Stack.Navigator( + Stack.Screen("Home", component=HomeScreen), + Stack.Screen("Detail", component=DetailScreen), + initial_route="Home", + ) + ) + +@pn.component +def HomeScreen(): + nav = pn.use_navigation() + return pn.Column( + pn.Text("Home", style={"font_size": 24}), + pn.Button( + "Go to Detail", + on_click=lambda: nav.navigate("Detail", params={"id": 42}), + ), + style={"spacing": 12, "padding": 16}, + ) + +@pn.component +def DetailScreen(): + nav = pn.use_navigation() + params = nav.get_params() + return pn.Column( + pn.Text(f"Detail #{params.get('id')}", style={"font_size": 20}), + pn.Button("Back", on_click=nav.go_back), + style={"spacing": 12, "padding": 16}, + ) +``` + +### Tab Navigator + +A tab navigator renders a **native tab bar** and switches between screens. +On Android the tab bar is a `BottomNavigationView` from Material Components; +on iOS it is a `UITabBar`. + +```python +from pythonnative.navigation import create_tab_navigator + +Tab = create_tab_navigator() + +@pn.component +def App(): + return NavigationContainer( + Tab.Navigator( + Tab.Screen("Home", component=HomeScreen, options={"title": "Home"}), + Tab.Screen("Settings", component=SettingsScreen, options={"title": "Settings"}), + ) + ) +``` + +The tab bar emits a `TabBar` element that maps to platform-native views: + +| Platform | Native view | +|----------|------------------------------| +| Android | `BottomNavigationView` | +| iOS | `UITabBar` | + +### Drawer Navigator + +A drawer navigator provides a side menu for switching screens. + +```python +from pythonnative.navigation import create_drawer_navigator + +Drawer = create_drawer_navigator() + +@pn.component +def App(): + return NavigationContainer( + Drawer.Navigator( + Drawer.Screen("Home", component=HomeScreen, options={"title": "Home"}), + Drawer.Screen("Profile", component=ProfileScreen, options={"title": "Profile"}), + ) + ) + +@pn.component +def HomeScreen(): + nav = pn.use_navigation() + return pn.Column( + pn.Button("Open Menu", on_click=nav.open_drawer), + pn.Text("Home Screen"), + ) +``` + +### Nesting Navigators + +Navigators can be nested — for example, tabs containing stacks. +When a child navigator receives a `navigate()` call for an unknown route, +it automatically **forwards** the request to its parent navigator. +Similarly, `go_back()` at the root of a child stack forwards to the parent. + +```python +Stack = create_stack_navigator() +Tab = create_tab_navigator() + +@pn.component +def HomeStack(): + return Stack.Navigator( + Stack.Screen("Feed", component=FeedScreen), + Stack.Screen("Post", component=PostScreen), + ) + +@pn.component +def App(): + return NavigationContainer( + Tab.Navigator( + Tab.Screen("Home", component=HomeStack, options={"title": "Home"}), + Tab.Screen("Settings", component=SettingsScreen, options={"title": "Settings"}), + ) + ) +``` + +Inside `FeedScreen`, calling `nav.navigate("Settings")` will forward to the +parent tab navigator and switch to the Settings tab. + +## NavigationHandle API + +Inside any screen rendered by a navigator, `pn.use_navigation()` returns a handle with: + +- **`.navigate(route_name, params=...)`** — navigate to a named route with optional params +- **`.go_back()`** — pop the current screen +- **`.get_params()`** — get the current route's params dict +- **`.reset(route_name, params=...)`** — reset the stack to a single route + +### Drawer-specific methods + +When inside a drawer navigator, the handle also provides: + +- **`.open_drawer()`** — open the drawer +- **`.close_drawer()`** — close the drawer +- **`.toggle_drawer()`** — toggle the drawer open/closed + +## Focus-aware Effects + +Use `pn.use_focus_effect()` to run effects only when a screen is focused: + +```python +@pn.component +def DataScreen(): + data, set_data = pn.use_state(None) + + pn.use_focus_effect(lambda: fetch_data(set_data), []) + + return pn.Text(f"Data: {data}") +``` + +## Route Parameters + +Use `pn.use_route()` for convenient access to route params: + +```python +@pn.component +def DetailScreen(): + params = pn.use_route() + item_id = params.get("id", 0) + return pn.Text(f"Item #{item_id}") +``` + +## Lifecycle + +PythonNative forwards lifecycle events from the host: + +- `on_create` — triggers the initial render +- `on_start` +- `on_resume` +- `on_pause` +- `on_stop` +- `on_destroy` +- `on_restart` (Android only) +- `on_save_instance_state` +- `on_restore_instance_state` + +## Platform specifics + +### iOS (UIViewController per page) +- Each PythonNative screen is hosted by a Swift `ViewController` instance. +- Screens are pushed and popped on a root `UINavigationController`. +- Lifecycle is forwarded from Swift to the registered Python component. + +### Android (single Activity, Fragment stack) +- Single host `MainActivity` sets a `NavHostFragment` containing a navigation graph. +- Each PythonNative screen is represented by a generic `PageFragment` which instantiates the Python component and attaches its root view. +- `push`/`pop` delegate to `NavController` (via a small `Navigator` helper). +- Arguments live in Fragment arguments and restore across configuration changes. + +## Comparison to other frameworks +- **React Native:** Android: single `Activity`, screens managed via `Fragment`s. iOS: screens map to `UIViewController`s pushed on `UINavigationController`. +- **NativeScript:** Android: single `Activity`, pages as `Fragment`s. iOS: pages as `UIViewController`s on `UINavigationController`. +- **Flutter:** Android: single `Activity`. iOS: `FlutterViewController` hosts Flutter's navigator. diff --git a/docs/guides/styling.md b/docs/guides/styling.md new file mode 100644 index 0000000..9556fe6 --- /dev/null +++ b/docs/guides/styling.md @@ -0,0 +1,243 @@ +# Styling + +Style properties are passed via the `style` prop as a dict (or list of dicts) to any element function. PythonNative also provides a `StyleSheet` utility for creating reusable styles and a theming system via context. + +## Inline styles + +Pass a `style` dict to components: + +```python +pn.Text("Hello", style={"color": "#FF3366", "font_size": 24, "bold": True}) +pn.Button("Tap", style={"background_color": "#FF1E88E5", "color": "#FFFFFF"}) +pn.Column(pn.Text("Content"), style={"background_color": "#FFF5F5F5"}) +``` + +## StyleSheet + +Create reusable named styles with `StyleSheet.create()`: + +```python +import pythonnative as pn + +styles = pn.StyleSheet.create( + title={"font_size": 28, "bold": True, "color": "#333"}, + subtitle={"font_size": 14, "color": "#666"}, + container={"padding": 16, "spacing": 12, "align_items": "stretch"}, +) + +pn.Text("Welcome", style=styles["title"]) +pn.Column( + pn.Text("Subtitle", style=styles["subtitle"]), + style=styles["container"], +) +``` + +### Composing styles + +Merge multiple style dicts with `StyleSheet.compose()`: + +```python +base = {"font_size": 16, "color": "#000"} +highlight = {"color": "#FF0000", "bold": True} +merged = pn.StyleSheet.compose(base, highlight) +# Result: {"font_size": 16, "color": "#FF0000", "bold": True} +``` + +### Combining styles with a list + +You can also pass a list of dicts to `style`. They are merged left-to-right: + +```python +pn.Text("Highlighted", style=[base, highlight]) +``` + +### Flattening styles + +Flatten a style or list of styles into a single dict: + +```python +pn.StyleSheet.flatten([base, highlight]) +pn.StyleSheet.flatten(None) # returns {} +``` + +## Colors + +Pass hex strings (`#RRGGBB` or `#AARRGGBB`) to color properties inside `style`: + +```python +pn.Text("Hello", style={"color": "#FF3366"}) +pn.Button("Tap", style={"background_color": "#FF1E88E5", "color": "#FFFFFF"}) +``` + +## Text styling + +`Text` and `Button` accept `font_size`, `color`, `bold`, and `text_align` inside `style`: + +```python +pn.Text("Title", style={"font_size": 24, "bold": True, "text_align": "center"}) +pn.Text("Subtitle", style={"font_size": 14, "color": "#666666"}) +``` + +## Flex layout + +PythonNative uses a flexbox-inspired layout model. `View` is the universal flex container, and `Column`/`Row` are convenience wrappers. + +### Flex container properties + +These go in the `style` dict of `View`, `Column`, or `Row`: + +- `flex_direction` — `"column"` (default), `"row"`, `"column_reverse"`, `"row_reverse"` (only for `View`; `Column`/`Row` have fixed directions) +- `justify_content` — main-axis distribution: `"flex_start"`, `"center"`, `"flex_end"`, `"space_between"`, `"space_around"`, `"space_evenly"` +- `align_items` — cross-axis alignment: `"stretch"`, `"flex_start"`, `"center"`, `"flex_end"` +- `overflow` — `"visible"` (default), `"hidden"` +- `spacing` — gap between children (dp / pt) +- `padding` — inner spacing (int for all sides, or dict) + +### Child layout properties + +All components accept these in `style`: + +- `width`, `height` — fixed dimensions in dp (Android) / pt (iOS) +- `flex` — flex grow factor (shorthand for `flex_grow`) +- `flex_grow` — how much a child should grow to fill available space +- `flex_shrink` — how much a child should shrink when space is tight +- `margin` — outer spacing (int for all sides, or dict) +- `min_width`, `min_height` — minimum size constraints +- `max_width`, `max_height` — maximum size constraints +- `align_self` — override parent alignment: `"flex_start"`, `"center"`, `"flex_end"`, `"stretch"` + +### Layout examples + +**Centering content:** + +```python +pn.View( + pn.Text("Centered!"), + style={"flex": 1, "justify_content": "center", "align_items": "center"}, +) +``` + +**Horizontal row with spacer:** + +```python +pn.Row( + pn.Text("Left"), + pn.Spacer(flex=1), + pn.Text("Right"), + style={"padding": 16, "align_items": "center"}, +) +``` + +**Child with flex grow:** + +```python +pn.Column( + pn.Text("Header", style={"font_size": 20, "bold": True}), + pn.View(pn.Text("Content area"), style={"flex": 1}), + pn.Text("Footer"), + style={"flex": 1, "spacing": 8}, +) +``` + +**Horizontal button bar:** + +```python +pn.Row( + pn.Button("Cancel", style={"flex": 1}), + pn.Button("OK", style={"flex": 1, "background_color": "#007AFF", "color": "#FFF"}), + style={"spacing": 8, "padding": 16}, +) +``` + +## Layout with Column and Row + +`Column` (vertical) and `Row` (horizontal) are convenience wrappers for `View`: + +```python +pn.Column( + pn.Text("Username"), + pn.TextInput(placeholder="Enter username"), + pn.Text("Password"), + pn.TextInput(placeholder="Enter password", secure=True), + pn.Button("Login", on_click=handle_login), + style={"spacing": 8, "padding": 16, "align_items": "stretch"}, +) +``` + +### Alignment properties + +Column and Row support `align_items` and `justify_content` inside `style`: + +- **`align_items`** — cross-axis alignment: `"stretch"`, `"flex_start"`, `"center"`, `"flex_end"`, `"leading"`, `"trailing"` +- **`justify_content`** — main-axis distribution: `"flex_start"`, `"center"`, `"flex_end"`, `"space_between"`, `"space_around"`, `"space_evenly"` + +```python +pn.Row( + pn.Text("Left"), + pn.Spacer(flex=1), + pn.Text("Right"), + style={"align_items": "center", "justify_content": "space_between", "padding": 16}, +) +``` + +### Spacing + +- `spacing` sets the gap between children in dp (Android) / points (iOS). + +### Padding + +- `padding: 16` — all sides +- `padding: {"horizontal": 12, "vertical": 8}` — per axis +- `padding: {"left": 8, "top": 16, "right": 8, "bottom": 16}` — per side + +## Theming + +PythonNative includes a built-in theme context with light and dark themes: + +```python +import pythonnative as pn +from pythonnative.style import DEFAULT_DARK_THEME + + +@pn.component +def ThemedText(text: str = ""): + theme = pn.use_context(pn.ThemeContext) + return pn.Text(text, style={"color": theme["text_color"], "font_size": theme["font_size"]}) + + +@pn.component +def DarkPage(): + return pn.Provider(pn.ThemeContext, DEFAULT_DARK_THEME, + pn.Column( + ThemedText(text="Dark mode!"), + style={"spacing": 8}, + ) + ) +``` + +### Theme properties + +Both light and dark themes include: + +- `primary_color`, `secondary_color` — accent colors +- `background_color`, `surface_color` — background colors +- `text_color`, `text_secondary_color` — text colors +- `error_color`, `success_color`, `warning_color` — semantic colors +- `font_size`, `font_size_small`, `font_size_large`, `font_size_title` — typography +- `spacing`, `spacing_large` — layout spacing +- `border_radius` — corner rounding + +## ScrollView + +Wrap content in a `ScrollView`: + +```python +pn.ScrollView( + pn.Column( + pn.Text("Item 1"), + pn.Text("Item 2"), + # ... many items + style={"spacing": 8}, + ) +) +``` diff --git a/docs/index.md b/docs/index.md index 97b6b2b..fa81dfb 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,3 +1,19 @@ # PythonNative -Build native Android and iOS apps with Python. PythonNative provides a Pythonic API for native UI components and a simple CLI to scaffold and run projects. +Build native Android and iOS apps with Python using a declarative, React-like component model. + +PythonNative provides a Pythonic API for native UI components, a virtual view tree with automatic reconciliation, and a simple CLI to scaffold and run projects. + +```python +import pythonnative as pn + + +@pn.component +def App(): + count, set_count = pn.use_state(0) + return pn.Column( + pn.Text(f"Count: {count}", style={"font_size": 24}), + pn.Button("Tap me", on_click=lambda: set_count(count + 1)), + style={"spacing": 12, "padding": 16}, + ) +``` diff --git a/docs/meta/roadmap.md b/docs/meta/roadmap.md deleted file mode 100644 index 5cb3873..0000000 --- a/docs/meta/roadmap.md +++ /dev/null @@ -1,153 +0,0 @@ ---- -title: Roadmap ---- - -# PythonNative Roadmap (v0.2.0 → v0.10.0) - -This roadmap focuses on transforming PythonNative into a workable, React Native / Expo-like framework from a developer-experience and simplicity standpoint. Releases are incremental and designed to be shippable, with DX-first improvements balanced with platform capability. - -Assumptions -- Scope: Android (Chaquopy/Java bridge) and iOS (Rubicon-ObjC), Python 3.9–3.12 -- Goals: Zero-config templates, one CLI, fast iteration loop, portable component API, and a curated subset of native capabilities with room to expand. - -Guiding Principles -- Single CLI for init/run/build/clean. -- Convention over configuration: opinionated project layout (`app/`, `pythonnative.json`, `requirements.txt`). -- Hot reload (where feasible) and rapid feedback. -- Stable component API; platform shims kept internal. -- Progressive enhancement: start with a minimal but complete loop, add breadth and depth over time. - -Milestones - -0.2.0 — Foundations: DX Baseline and Templates -- CLI - - pn init: generate project with `app/`, `pythonnative.json`, `requirements.txt`, `.gitignore`. - - pn run android|ios: scaffold template apps (from bundled zips), copy `app/`, install requirements, build+install/run. - - pn clean: remove `build/` safely. -- Templates - - Bundle `templates/android_template.zip` and `templates/ios_template.zip` into package to avoid network. - - Ensure Android template uses Kotlin+Chaquopy; iOS template uses Swift+PythonKit+Rubicon. -- Core APIs - - Stabilize `Page`, `StackView`, `Label`, `Button`, `ImageView`, `TextField`, `TextView`, `Switch`, `ProgressView`, `ActivityIndicatorView`, `WebView` with consistent ctor patterns. - - Add `utils.IS_ANDROID` fallback detection improvements. -- Docs - - Getting Started (one page), Hello World, Concepts: Components, Guides: Android/iOS quickstart. - - Roadmap (this page). Contributing. - -Success Criteria -- New user can: pn init → pn run android → sees Hello World UI; same for iOS. - -0.3.0 — Navigation and Lifecycle -- API - - Page navigation abstraction with push/pop (Android: Activity/Fragment shim, iOS: UINavigationController). - - Lifecycle events stabilized and wired from host to Python (on_create/start/resume/pause/stop/destroy). -- Templates - - Two-screen sample demonstrating navigation and parameter passing. -- Docs - - Navigation guide with examples. - -Success Criteria -- Sample app navigates between two pages on both platforms using the same Python API. - -0.4.0 — Layout and Styling Pass -- API - - Improve `StackView` configuration: axis, spacing, alignment; add `ScrollView` wrapping helpers. - - Add lightweight style API (padding/margin where supported, background color, text color/size for text components). -- DX - - Component property setters return self for fluent configuration where ergonomic. -- Docs - - Styling guide and component property reference. - -Success Criteria -- Build complex vertical forms and simple horizontal layouts with predictable results on both platforms. - -0.5.0 — Developer Experience: Live Reload Loop -- DX - - pn dev android|ios: dev server watching `app/` with file-sync into running app. - - Implement soft-reload: trigger Python module reload and page re-render without full app restart where possible. - - Fallback to fast reinstall when soft-reload not possible. -- Templates - - Integrate dev menu gesture (e.g., triple-tap or shake) to trigger reload. -- Docs - - Dev workflow: live reload expectations and caveats. - -Success Criteria -- Edit Python in `app/`, trigger near-instant UI update on device/emulator. - -0.6.0 — Forms and Lists -- API - - `ListView` cross-platform wrapper with simple adapter API (Python callback to render rows, handle click). - - Input controls: `DatePicker`, `TimePicker`, basic validation utilities. - - Add `PickerView` parity or mark as experimental if iOS-first. -- Performance - - Ensure cell reuse on Android/iOS to handle 1k-row lists smoothly. -- Docs - - Lists guide, forms guide with validation patterns. - -Success Criteria -- Build a basic todo app with a scrollable list and an add-item form. - -0.7.0 — Networking, Storage, and Permissions Primitives -- API - - Simple `fetch`-like helper (thin wrapper over requests/URLSession with threading off main UI thread). - - Key-value storage abstraction (Android SharedPreferences / iOS UserDefaults). - - Permission prompts helper (camera, location, notifications) with consistent API returning futures/promises. -- DX - - Background threading utilities for long-running tasks with callback to main thread. -- Docs - - Data fetching, local storage, permissions cookbook. - -Success Criteria -- Build a data-driven screen that fetches remote JSON, caches a token, and requests permission. - -0.8.0 — Theming and Material Components (Android parity), iOS polish -- API - - Theme object for colors/typography; propagate defaults to components. - - Material variants: MaterialButton, MaterialProgress, MaterialSearchBar, MaterialSwitch stabilized. - - iOS polishing: ensure UIKit equivalents’ look-and-feel is sensible by default. -- DX - - Dark/light theme toggling hook. -- Docs - - Theming guide with examples. - -Success Criteria -- Switch between light/dark themes and see consistent component styling across screens. - -0.9.0 — Packaging, Testing, and CI -- CLI - - pn build android|ios: produce signed (debug) APK/IPA or x archive guidance; integrate keystore setup helper for Android. - - pn test: run Python unit tests; document UI test strategy (manual/host-level instrumentation later). -- Tooling - - Add ruff/black/mypy default config and `pn fmt`, `pn lint` wrappers. -- Docs - - Release checklist; testing guide. - -Success Criteria -- Produce installable builds via pn build; run unit tests with a single command. - -0.10.0 — Plugin System (Early) and Project Orchestration -- Plugins - - Define `pythonnative.plugins` entry point allowing add-ons (e.g., Camera, Filesystem) to register platform shims. - - pn plugin add : scaffold plugin structure and install dependency. -- Orchestration - - Config-driven `pythonnative.json`: targets, app id/name, icons/splash, permissions, minSDK/iOS version. - - Asset pipeline: copy assets to correct platform locations. -- Docs - - Plugin authoring guide; configuration reference. - -Success Criteria -- Install a community plugin and use it from Python without touching native code. - -Backlog and Stretch (post-0.10) -- Cross-platform navigation stack parity (Fragments vs Activities, or single-activity multi-fragment on Android). -- Advanced layout (ConstraintLayout/AutoLayout helpers) with declarative constraints. -- Gesture/touch handling unification, animations/transitions. -- Expo-like over-the-air updates pipeline. -- Desktop/web exploration via PyObjC/Qt bridges (research). - -Breaking Changes Policy -- Pre-1.0: Minor versions may include breaking changes; provide migration notes and deprecation warnings one release ahead when possible. - -Tracking and Releases -- Each milestone will have a GitHub project board and labeled issues. -- Changelogs maintained per release; upgrade guides in docs. diff --git a/examples/hello-world/app/main_page.py b/examples/hello-world/app/main_page.py index 8a36892..23abd46 100644 --- a/examples/hello-world/app/main_page.py +++ b/examples/hello-world/app/main_page.py @@ -1,65 +1,86 @@ -import pythonnative as pn - -try: - # Optional: used for styling below; safe if rubicon isn't available - from rubicon.objc import ObjCClass - - UIColor = ObjCClass("UIColor") -except Exception: # pragma: no cover - UIColor = None +import emoji +import pythonnative as pn +from pythonnative.navigation import NavigationContainer, create_tab_navigator -class MainPage(pn.Page): - def __init__(self, native_instance): - super().__init__(native_instance) +MEDALS = [":1st_place_medal:", ":2nd_place_medal:", ":3rd_place_medal:"] - def on_create(self): - super().on_create() - stack = pn.StackView() - # Ensure vertical stacking - try: - stack.native_instance.setAxis_(1) # 1 = vertical - except Exception: - pass - stack.add_view(pn.Label("Hello from PythonNative Demo!")) - button = pn.Button("Tap me") - button.set_on_click(lambda: print("Demo button clicked")) - # Make the button visually obvious - try: - if UIColor is not None: - button.native_instance.setBackgroundColor_(UIColor.systemBlueColor()) - button.native_instance.setTitleColor_forState_(UIColor.whiteColor(), 0) - except Exception: - pass - stack.add_view(button) - self.set_root_view(stack) +Tab = create_tab_navigator() - def on_start(self): - super().on_start() +styles = pn.StyleSheet.create( + title={"font_size": 24, "bold": True}, + subtitle={"font_size": 16, "color": "#666666"}, + medal={"font_size": 32}, + card={ + "spacing": 12, + "padding": 16, + "background_color": "#F8F9FA", + "align_items": "center", + }, + section={"spacing": 16, "padding": 24, "align_items": "stretch"}, + button_row={"spacing": 8, "align_items": "center"}, +) - def on_resume(self): - super().on_resume() - def on_pause(self): - super().on_pause() +@pn.component +def counter_badge(initial: int = 0) -> pn.Element: + """Reusable counter component with its own hook-based state.""" + count, set_count = pn.use_state(initial) + medal = emoji.emojize(MEDALS[count] if count < len(MEDALS) else ":star:") - def on_stop(self): - super().on_stop() + return pn.View( + pn.Text(f"Tapped {count} times", style=styles["subtitle"]), + pn.Text(medal, style=styles["medal"]), + pn.Row( + pn.Button("Tap me", on_click=lambda: set_count(count + 1)), + pn.Button("Reset", on_click=lambda: set_count(0)), + style=styles["button_row"], + ), + style=styles["card"], + ) - def on_destroy(self): - super().on_destroy() - def on_restart(self): - super().on_restart() +@pn.component +def HomeTab() -> pn.Element: + """Home tab — counter demo and push-navigation to other pages.""" + nav = pn.use_navigation() + return pn.ScrollView( + pn.Column( + pn.Text("Hello from PythonNative Demo!", style=styles["title"]), + counter_badge(), + pn.Button( + "Go to Second Page", + on_click=lambda: nav.navigate( + "app.second_page.SecondPage", + params={"message": "Greetings from MainPage"}, + ), + ), + style=styles["section"], + ) + ) - def on_save_instance_state(self): - super().on_save_instance_state() - def on_restore_instance_state(self): - super().on_restore_instance_state() +@pn.component +def SettingsTab() -> pn.Element: + """Settings tab — simple placeholder content.""" + return pn.ScrollView( + pn.Column( + pn.Text("Settings", style=styles["title"]), + pn.Text("App version: 0.7.0", style=styles["subtitle"]), + pn.Text( + "This tab uses a native UITabBar on iOS " "and BottomNavigationView on Android.", + style=styles["subtitle"], + ), + style=styles["section"], + ) + ) -def bootstrap(native_instance): - page = MainPage(native_instance) - page.on_create() - return page +@pn.component +def MainPage() -> pn.Element: + return NavigationContainer( + Tab.Navigator( + Tab.Screen("Home", component=HomeTab, options={"title": "Home"}), + Tab.Screen("Settings", component=SettingsTab, options={"title": "Settings"}), + ) + ) diff --git a/examples/hello-world/app/second_page.py b/examples/hello-world/app/second_page.py index 9be9495..40725a2 100644 --- a/examples/hello-world/app/second_page.py +++ b/examples/hello-world/app/second_page.py @@ -1,37 +1,18 @@ import pythonnative as pn -class SecondPage(pn.Page): - def __init__(self, native_instance): - super().__init__(native_instance) - - def on_create(self): - super().on_create() - stack_view = pn.StackView() - label = pn.Label("Second page!") - stack_view.add_view(label) - self.set_root_view(stack_view) - - def on_start(self): - super().on_start() - - def on_resume(self): - super().on_resume() - - def on_pause(self): - super().on_pause() - - def on_stop(self): - super().on_stop() - - def on_destroy(self): - super().on_destroy() - - def on_restart(self): - super().on_restart() - - def on_save_instance_state(self): - super().on_save_instance_state() - - def on_restore_instance_state(self): - super().on_restore_instance_state() +@pn.component +def SecondPage() -> pn.Element: + nav = pn.use_navigation() + message = nav.get_params().get("message", "Second Page") + return pn.ScrollView( + pn.Column( + pn.Text(message, style={"font_size": 24, "bold": True}), + pn.Button( + "Go to Third Page", + on_click=lambda: nav.navigate("app.third_page.ThirdPage"), + ), + pn.Button("Back", on_click=nav.go_back), + style={"spacing": 16, "padding": 24, "align_items": "stretch"}, + ) + ) diff --git a/examples/hello-world/app/third_page.py b/examples/hello-world/app/third_page.py new file mode 100644 index 0000000..62a3388 --- /dev/null +++ b/examples/hello-world/app/third_page.py @@ -0,0 +1,14 @@ +import pythonnative as pn + + +@pn.component +def ThirdPage() -> pn.Element: + nav = pn.use_navigation() + return pn.ScrollView( + pn.Column( + pn.Text("Third Page", style={"font_size": 24, "bold": True}), + pn.Text("You navigated two levels deep."), + pn.Button("Back to Second", on_click=nav.go_back), + style={"spacing": 16, "padding": 24, "align_items": "stretch"}, + ) + ) diff --git a/examples/hello-world/pythonnative.json b/examples/hello-world/pythonnative.json index 04dc187..db72356 100644 --- a/examples/hello-world/pythonnative.json +++ b/examples/hello-world/pythonnative.json @@ -2,6 +2,7 @@ "name": "PythonNative Demo", "appId": "com.pythonnative.demo", "entryPoint": "app/main_page.py", + "pythonVersion": "3.11", "ios": {}, "android": {} } diff --git a/examples/hello-world/requirements.txt b/examples/hello-world/requirements.txt index 7e4d11b..3a917a9 100644 --- a/examples/hello-world/requirements.txt +++ b/examples/hello-world/requirements.txt @@ -1 +1 @@ -pythonnative +emoji diff --git a/experiments/android_pythonnative_3/.gitignore b/experiments/android_pythonnative_3/.gitignore deleted file mode 100644 index aa724b7..0000000 --- a/experiments/android_pythonnative_3/.gitignore +++ /dev/null @@ -1,15 +0,0 @@ -*.iml -.gradle -/local.properties -/.idea/caches -/.idea/libraries -/.idea/modules.xml -/.idea/workspace.xml -/.idea/navEditor.xml -/.idea/assetWizardSettings.xml -.DS_Store -/build -/captures -.externalNativeBuild -.cxx -local.properties diff --git a/experiments/android_pythonnative_3/app/.gitignore b/experiments/android_pythonnative_3/app/.gitignore deleted file mode 100644 index 42afabf..0000000 --- a/experiments/android_pythonnative_3/app/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/experiments/android_pythonnative_3/app/build.gradle b/experiments/android_pythonnative_3/app/build.gradle deleted file mode 100644 index 50bbbfe..0000000 --- a/experiments/android_pythonnative_3/app/build.gradle +++ /dev/null @@ -1,59 +0,0 @@ -plugins { - id 'com.android.application' - id 'org.jetbrains.kotlin.android' - id 'com.chaquo.python' -} - -android { - namespace 'com.pythonnative.pythonnative' - compileSdk 33 - - defaultConfig { - applicationId "com.pythonnative.pythonnative" - minSdk 24 - targetSdk 33 - versionCode 1 - versionName "1.0" - - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - ndk { - abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64" - } - python { - pip { - // https://chaquo.com/chaquopy/doc/current/android.html#android-requirements -// install "matplotlib" -// install "pythonnative" - - // A directory containing a setup.py, relative to the project - // directory (must contain at least one slash): - install "/Users/owenthcarey/Documents/pythonnative-workspace/libs/pythonnative" - } - } - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - kotlinOptions { - jvmTarget = '1.8' - } -} - -dependencies { - - implementation 'androidx.core:core-ktx:1.8.0' - implementation 'androidx.appcompat:appcompat:1.6.1' - implementation 'com.google.android.material:material:1.5.0' - implementation 'androidx.constraintlayout:constraintlayout:2.1.4' - testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.1.5' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' -} \ No newline at end of file diff --git a/experiments/android_pythonnative_3/app/proguard-rules.pro b/experiments/android_pythonnative_3/app/proguard-rules.pro deleted file mode 100644 index 481bb43..0000000 --- a/experiments/android_pythonnative_3/app/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/experiments/android_pythonnative_3/app/src/androidTest/java/com/pythonnative/pythonnative/ExampleInstrumentedTest.kt b/experiments/android_pythonnative_3/app/src/androidTest/java/com/pythonnative/pythonnative/ExampleInstrumentedTest.kt deleted file mode 100644 index e2269ba..0000000 --- a/experiments/android_pythonnative_3/app/src/androidTest/java/com/pythonnative/pythonnative/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.pythonnative.pythonnative - -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.ext.junit.runners.AndroidJUnit4 - -import org.junit.Test -import org.junit.runner.RunWith - -import org.junit.Assert.* - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.pythonnative.pythonnative", appContext.packageName) - } -} \ No newline at end of file diff --git a/experiments/android_pythonnative_3/app/src/main/AndroidManifest.xml b/experiments/android_pythonnative_3/app/src/main/AndroidManifest.xml deleted file mode 100644 index 411d3d1..0000000 --- a/experiments/android_pythonnative_3/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/experiments/android_pythonnative_3/app/src/main/java/com/pythonnative/pythonnative/MainActivity.kt b/experiments/android_pythonnative_3/app/src/main/java/com/pythonnative/pythonnative/MainActivity.kt deleted file mode 100644 index b29e93e..0000000 --- a/experiments/android_pythonnative_3/app/src/main/java/com/pythonnative/pythonnative/MainActivity.kt +++ /dev/null @@ -1,144 +0,0 @@ -package com.pythonnative.pythonnative - -import android.graphics.BitmapFactory -import androidx.appcompat.app.AppCompatActivity -import android.os.Bundle -import android.util.Log -import android.widget.Button -import android.widget.ImageView -import android.widget.TextView -import android.graphics.Color -import android.view.View -import android.widget.LinearLayout -import androidx.constraintlayout.widget.ConstraintLayout -import androidx.recyclerview.widget.RecyclerView -import com.chaquo.python.PyException -import com.chaquo.python.PyObject -import com.chaquo.python.Python -import com.chaquo.python.android.AndroidPlatform -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.json.JSONObject - -class MainActivity : AppCompatActivity() { - private val TAG = javaClass.simpleName - private lateinit var page: PyObject - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - Log.d(TAG, "onCreate() called") - -// setContentView(R.layout.activity_main) -// val layoutMain = findViewById(R.id.layout_main) - - // Initialize Chaquopy - if (!Python.isStarted()) { - Python.start(AndroidPlatform(this)) - } - val py = Python.getInstance() - - // Create an instance of the Page class -// val pyModule = py.getModule("app/main_2") -// page = pyModule.callAttr("Page", this) -// val pyLayout = page.callAttr("on_create").toJava(View::class.java) -// setContentView(pyLayout) - - // Create an instance of the Page class - val pyModule = py.getModule("app/main_3") - page = pyModule.callAttr("MainPage", this) - page.callAttr("on_create") - -// val pyModule = py.getModule("app/main") -// val pyLayout = pyModule.callAttr("on_create", this).toJava(View::class.java) -// setContentView(pyLayout) - -// val createButtonModule = py.getModule("create_button") -// val pyButton = createButtonModule.callAttr("create_button", this).toJava(Button::class.java) -// layoutMain.addView(pyButton) - -// val createWidgetsModule = py.getModule("create_widgets") -// val pyLayout = createWidgetsModule.callAttr("create_widgets", this).toJava(LinearLayout::class.java) -// layoutMain.addView(pyLayout) - -// val createConstraintLayoutModule = py.getModule("create_constraint_layout") -// val pyLayout = createConstraintLayoutModule.callAttr("create_constraint_layout", this).toJava(ConstraintLayout::class.java) -// layoutMain.addView(pyLayout) - -// val createRecyclerViewModule = py.getModule("create_recycler_view") -// val pyRecyclerView = createRecyclerViewModule.callAttr("create_recycler_view", this).toJava(RecyclerView::class.java) -// layoutMain.addView(pyRecyclerView) - - // Existing code for displaying plot -// val imageView = findViewById(R.id.image_home) -// val plotModule = py.getModule("plot") -// val xInput = "1 2 3 4 5" -// val yInput = "1 4 9 16 25" -// CoroutineScope(Dispatchers.Main).launch { -// try { -// val bytes = plotModule.callAttr( -// "plot", -// xInput, -// yInput -// ).toJava(ByteArray::class.java) -// withContext(Dispatchers.IO) { -// val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size) -// withContext(Dispatchers.Main) { -// imageView.setImageBitmap(bitmap) -// } -// } -// } catch (e: PyException) { -// Log.e("Python Error", "Error executing Python code", e) -// } -// } - } - - override fun onStart() { - super.onStart() - Log.d(TAG, "onStart() called") - page.callAttr("on_start") - } - - override fun onResume() { - super.onResume() - Log.d(TAG, "onResume() called") - page.callAttr("on_resume") - } - - override fun onPause() { - super.onPause() - Log.d(TAG, "onPause() called") - page.callAttr("on_pause") - } - - override fun onStop() { - super.onStop() - Log.d(TAG, "onStop() called") - page.callAttr("on_stop") - } - - override fun onDestroy() { - super.onDestroy() - Log.d(TAG, "onDestroy() called") - page.callAttr("on_destroy") - } - - override fun onRestart() { - super.onRestart() - Log.d(TAG, "onRestart() called") - page.callAttr("on_restart") - } - - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - Log.d(TAG, "onSaveInstanceState() called") - page.callAttr("on_save_instance_state") - } - - override fun onRestoreInstanceState(savedInstanceState: Bundle) { - super.onRestoreInstanceState(savedInstanceState) - Log.d(TAG, "onRestoreInstanceState() called") - page.callAttr("on_restore_instance_state") - } -} diff --git a/experiments/android_pythonnative_3/app/src/main/java/com/pythonnative/pythonnative/SecondActivity.kt b/experiments/android_pythonnative_3/app/src/main/java/com/pythonnative/pythonnative/SecondActivity.kt deleted file mode 100644 index e377e5e..0000000 --- a/experiments/android_pythonnative_3/app/src/main/java/com/pythonnative/pythonnative/SecondActivity.kt +++ /dev/null @@ -1,73 +0,0 @@ -package com.pythonnative.pythonnative - -import androidx.appcompat.app.AppCompatActivity -import android.os.Bundle -import android.util.Log -import com.chaquo.python.PyObject -import com.chaquo.python.Python -import com.chaquo.python.android.AndroidPlatform - -class SecondActivity : AppCompatActivity() { - private val TAG = javaClass.simpleName - private lateinit var page: PyObject - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - Log.d(TAG, "onCreate() called") - if (!Python.isStarted()) { - Python.start(AndroidPlatform(this)) - } - val py = Python.getInstance() - val pyModule = py.getModule("app/second_page") - page = pyModule.callAttr("SecondPage", this) - page.callAttr("on_create") - } - - override fun onStart() { - super.onStart() - Log.d(TAG, "onStart() called") - page.callAttr("on_start") - } - - override fun onResume() { - super.onResume() - Log.d(TAG, "onResume() called") - page.callAttr("on_resume") - } - - override fun onPause() { - super.onPause() - Log.d(TAG, "onPause() called") - page.callAttr("on_pause") - } - - override fun onStop() { - super.onStop() - Log.d(TAG, "onStop() called") - page.callAttr("on_stop") - } - - override fun onDestroy() { - super.onDestroy() - Log.d(TAG, "onDestroy() called") - page.callAttr("on_destroy") - } - - override fun onRestart() { - super.onRestart() - Log.d(TAG, "onRestart() called") - page.callAttr("on_restart") - } - - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - Log.d(TAG, "onSaveInstanceState() called") - page.callAttr("on_save_instance_state") - } - - override fun onRestoreInstanceState(savedInstanceState: Bundle) { - super.onRestoreInstanceState(savedInstanceState) - Log.d(TAG, "onRestoreInstanceState() called") - page.callAttr("on_restore_instance_state") - } -} \ No newline at end of file diff --git a/experiments/android_pythonnative_3/app/src/main/python/app/__init__.py b/experiments/android_pythonnative_3/app/src/main/python/app/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/experiments/android_pythonnative_3/app/src/main/python/app/main.py b/experiments/android_pythonnative_3/app/src/main/python/app/main.py deleted file mode 100644 index 2eab767..0000000 --- a/experiments/android_pythonnative_3/app/src/main/python/app/main.py +++ /dev/null @@ -1,109 +0,0 @@ -import pythonnative as pn - - -def on_create(context): - stack_view = pn.StackView(context) - - # label = pn.Label(context, "This is a PythonNative label") - # stack_view.add_view(label) - # - # switch = pn.Switch(context) - # stack_view.add_view(switch) - # - # text_field = pn.TextField(context) - # stack_view.add_view(text_field) - # - # text_view = pn.TextView(context) - # stack_view.add_view(text_view) - - activity_indicator_view = pn.ActivityIndicatorView(context) - activity_indicator_view.start_animating() - stack_view.add_view(activity_indicator_view) - - material_activity_indicator_view = pn.MaterialActivityIndicatorView(context) - material_activity_indicator_view.start_animating() - stack_view.add_view(material_activity_indicator_view) - - progress_view = pn.ProgressView(context) - progress_view.set_progress(0.5) - stack_view.add_view(progress_view) - - material_progress_view = pn.MaterialProgressView(context) - material_progress_view.set_progress(0.5) - stack_view.add_view(material_progress_view) - - material_button = pn.MaterialButton(context, "MaterialButton") - stack_view.add_view(material_button) - - search_bar = pn.SearchBar(context) - stack_view.add_view(search_bar) - - image_view = pn.ImageView(context) - stack_view.add_view(image_view) - - picker_view = pn.PickerView(context) - stack_view.add_view(picker_view) - - # date_picker = pn.DatePicker(context) - # stack_view.add_view(date_picker) - - # time_picker = pn.TimePicker(context) - # stack_view.add_view(time_picker) - - # TODO: fix - # material_time_picker = pn.MaterialTimePicker(context) - # stack_view.add_view(material_time_picker) - - # TODO: fix - # material_date_picker = pn.MaterialDatePicker(context) - # stack_view.add_view(material_date_picker) - - # TODO: fix - # material_switch = pn.MaterialSwitch(context) - # stack_view.add_view(material_switch) - - # TODO: fix - # material_search_bar = pn.MaterialSearchBar(context) - # stack_view.add_view(material_search_bar) - - # web_view = pn.WebView(context) - # web_view.load_url("https://www.djangoproject.com/") - # stack_view.add_view(web_view) - # - # for i in range(100): - # button = pn.Button(context, "Click me") - # stack_view.add_view(button) - - return stack_view.native_instance - - -def on_start(): - print("on_start() called") - - -def on_resume(): - print("on_resume() called") - - -def on_pause(): - print("on_pause() called") - - -def on_stop(): - print("on_stop() called") - - -def on_destroy(): - print("on_destroy() called") - - -def on_restart(): - print("on_restart() called") - - -def on_save_instance_state(): - print("on_save_instance_state() called") - - -def on_restore_instance_state(): - print("on_restore_instance_state() called") diff --git a/experiments/android_pythonnative_3/app/src/main/python/app/main_2.py b/experiments/android_pythonnative_3/app/src/main/python/app/main_2.py deleted file mode 100644 index 1dba4ee..0000000 --- a/experiments/android_pythonnative_3/app/src/main/python/app/main_2.py +++ /dev/null @@ -1,38 +0,0 @@ -import pythonnative as pn - - -class Page: - def __init__(self, context): - self.context = context - - def on_create(self): - print("on_create() called") - stack_view = pn.StackView(self.context) - material_button = pn.MaterialButton(self.context, "MaterialButton") - stack_view.add_view(material_button) - # Create and add other views to the stack_view here - return stack_view.native_instance - - def on_start(self): - print("on_start() called") - - def on_resume(self): - print("on_resume() called") - - def on_pause(self): - print("on_pause() called") - - def on_stop(self): - print("on_stop() called") - - def on_destroy(self): - print("on_destroy() called") - - def on_restart(self): - print("on_restart() called") - - def on_save_instance_state(self): - print("on_save_instance_state() called") - - def on_restore_instance_state(self): - print("on_restore_instance_state() called") diff --git a/experiments/android_pythonnative_3/app/src/main/python/app/main_3.py b/experiments/android_pythonnative_3/app/src/main/python/app/main_3.py deleted file mode 100644 index 6c24d43..0000000 --- a/experiments/android_pythonnative_3/app/src/main/python/app/main_3.py +++ /dev/null @@ -1,42 +0,0 @@ -import pythonnative as pn - - -class MainPage(pn.Page): - def __init__(self, native_instance): - super().__init__(native_instance) - - def on_create(self): - super().on_create() - stack_view = pn.StackView(self.native_instance) - # list_data = ["item_{}".format(i) for i in range(100)] - # list_view = pn.ListView(self.native_instance, list_data) - # stack_view.add_view(list_view) - button = pn.Button(self.native_instance, "Button") - button.set_on_click(lambda: self.navigate_to("")) - # button.set_on_click(lambda: print("Button was clicked!")) - stack_view.add_view(button) - self.set_root_view(stack_view) - - def on_start(self): - super().on_start() - - def on_resume(self): - super().on_resume() - - def on_pause(self): - super().on_pause() - - def on_stop(self): - super().on_stop() - - def on_destroy(self): - super().on_destroy() - - def on_restart(self): - super().on_restart() - - def on_save_instance_state(self): - super().on_save_instance_state() - - def on_restore_instance_state(self): - super().on_restore_instance_state() diff --git a/experiments/android_pythonnative_3/app/src/main/python/app/second_page.py b/experiments/android_pythonnative_3/app/src/main/python/app/second_page.py deleted file mode 100644 index 442fb53..0000000 --- a/experiments/android_pythonnative_3/app/src/main/python/app/second_page.py +++ /dev/null @@ -1,37 +0,0 @@ -import pythonnative as pn - - -class SecondPage(pn.Page): - def __init__(self, native_instance): - super().__init__(native_instance) - - def on_create(self): - super().on_create() - stack_view = pn.StackView(self.native_instance) - label = pn.Label(self.native_instance, "Second page!") - stack_view.add_view(label) - self.set_root_view(stack_view) - - def on_start(self): - super().on_start() - - def on_resume(self): - super().on_resume() - - def on_pause(self): - super().on_pause() - - def on_stop(self): - super().on_stop() - - def on_destroy(self): - super().on_destroy() - - def on_restart(self): - super().on_restart() - - def on_save_instance_state(self): - super().on_save_instance_state() - - def on_restore_instance_state(self): - super().on_restore_instance_state() diff --git a/experiments/android_pythonnative_3/app/src/main/python/create_button.py b/experiments/android_pythonnative_3/app/src/main/python/create_button.py deleted file mode 100644 index 1924e4a..0000000 --- a/experiments/android_pythonnative_3/app/src/main/python/create_button.py +++ /dev/null @@ -1,8 +0,0 @@ -from java import cast, chaquopy, dynamic_proxy, jarray, jclass - - -def create_button(context): - Button = jclass("android.widget.Button") - button = Button(context) - button.setText("Button created in Python") - return button diff --git a/experiments/android_pythonnative_3/app/src/main/python/create_constraint_layout.py b/experiments/android_pythonnative_3/app/src/main/python/create_constraint_layout.py deleted file mode 100644 index 9d295bd..0000000 --- a/experiments/android_pythonnative_3/app/src/main/python/create_constraint_layout.py +++ /dev/null @@ -1,46 +0,0 @@ -from java import jclass - - -BottomNavigationView = jclass( - "com.google.android.material.bottomnavigation.BottomNavigationView" -) -ConstraintLayout = jclass("androidx.constraintlayout.widget.ConstraintLayout") -View = jclass("android.view.View") -ViewGroup = jclass("android.view.ViewGroup") - - -def create_constraint_layout(context): - # Create ConstraintLayout - layout = ConstraintLayout(context) - layout_params = ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT - ) - layout.setLayoutParams(layout_params) - - # Create BottomNavigationView - bottom_nav = BottomNavigationView(context) - bottom_nav.setId( - View.generateViewId() - ) # Add this line to generate unique id for the view - - # Create Menu for BottomNavigationView - menu = bottom_nav.getMenu() - - # Add items to the menu - menu.add(0, 0, 0, "Home") - menu.add(0, 1, 0, "Search") - menu.add(0, 2, 0, "Notifications") - menu.add(0, 3, 0, "Messages") - menu.add(0, 4, 0, "Profile") - - # Add BottomNavigationView to ConstraintLayout - nav_layout_params = ConstraintLayout.LayoutParams( - ConstraintLayout.LayoutParams.MATCH_PARENT, - ConstraintLayout.LayoutParams.WRAP_CONTENT, - ) - # Set the constraints here - nav_layout_params.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID - bottom_nav.setLayoutParams(nav_layout_params) - layout.addView(bottom_nav) - - return layout diff --git a/experiments/android_pythonnative_3/app/src/main/python/create_pn_layout.py b/experiments/android_pythonnative_3/app/src/main/python/create_pn_layout.py deleted file mode 100644 index b4a7fde..0000000 --- a/experiments/android_pythonnative_3/app/src/main/python/create_pn_layout.py +++ /dev/null @@ -1,13 +0,0 @@ -import pythonnative as pn - - -def create_pn_layout(context): - layout = pn.StackView(context) - - label = pn.Label(context, "This is a PythonNative label") - layout.add_view(label) - - button = pn.Button(context, "Click me") - layout.add_view(button) - - return layout.native_instance diff --git a/experiments/android_pythonnative_3/app/src/main/python/create_recycler_view.py b/experiments/android_pythonnative_3/app/src/main/python/create_recycler_view.py deleted file mode 100644 index 973c75a..0000000 --- a/experiments/android_pythonnative_3/app/src/main/python/create_recycler_view.py +++ /dev/null @@ -1,42 +0,0 @@ -from java import jclass, static_proxy, Override - -LinearLayoutManager = jclass("androidx.recyclerview.widget.LinearLayoutManager") -RecyclerView = jclass("androidx.recyclerview.widget.RecyclerView") -TextView = jclass("android.widget.TextView") - - -# RecyclerView ViewHolder -class MyViewHolder(static_proxy(RecyclerView.ViewHolder)): - def __init__(self, item_view): - super(MyViewHolder, self).__init__(item_view) - self.my_text_view = TextView(item_view.getContext()) - - -# RecyclerView Adapter -class MyAdapter(static_proxy(RecyclerView.Adapter)): - def __init__(self, my_dataset): - self.my_dataset = my_dataset - - @Override(RecyclerView.Adapter) - def onCreateViewHolder(self, parent, viewType): - text_view = TextView(parent.getContext()) - return MyViewHolder(text_view) - - @Override(RecyclerView.Adapter) - def onBindViewHolder(self, holder, position): - holder.my_text_view.setText(self.my_dataset[position]) - - @Override(RecyclerView.Adapter) - def getItemCount(self): - return len(self.my_dataset) - - -# Create the RecyclerView -def create_recycler_view(context): - my_recycler_view = RecyclerView(context) - my_layout_manager = LinearLayoutManager(context) - my_recycler_view.setLayoutManager(my_layout_manager) - my_dataset = ["Data 1", "Data 2", "Data 3"] - my_adapter = MyAdapter(my_dataset) - my_recycler_view.setAdapter(my_adapter) - return my_recycler_view diff --git a/experiments/android_pythonnative_3/app/src/main/python/create_widgets.py b/experiments/android_pythonnative_3/app/src/main/python/create_widgets.py deleted file mode 100644 index 22668b3..0000000 --- a/experiments/android_pythonnative_3/app/src/main/python/create_widgets.py +++ /dev/null @@ -1,162 +0,0 @@ -from java import dynamic_proxy, jclass, static_proxy -import random - -# Import View class which contains OnClickListener -View = jclass('android.view.View') -Color = jclass('android.graphics.Color') - - -class ButtonClickListener(dynamic_proxy(View.OnClickListener)): - def __init__(self, button): - super().__init__() - self.button = button - - def onClick(self, view): - # Generate a random hex color. - color = "#" + "".join( - [random.choice("0123456789ABCDEF") for _ in range(6)]) - - # Set the button's background color. - self.button.setBackgroundColor(Color.parseColor(color)) - - # Print something to the console. - print("Button clicked! New color is " + color) - - -def create_widgets(context): - # Java Classes - RelativeLayout = jclass("android.widget.RelativeLayout") - FrameLayout = jclass("android.widget.FrameLayout") - GridLayout = jclass("android.widget.GridLayout") - LinearLayout = jclass("android.widget.LinearLayout") - Button = jclass("android.widget.Button") - TextView = jclass("android.widget.TextView") - EditText = jclass("android.widget.EditText") - CheckBox = jclass("android.widget.CheckBox") - RadioButton = jclass("android.widget.RadioButton") - ImageView = jclass("android.widget.ImageView") - ProgressBar = jclass("android.widget.ProgressBar") - Switch = jclass("android.widget.Switch") - ToggleButton = jclass("android.widget.ToggleButton") - SeekBar = jclass("android.widget.SeekBar") - CardView = jclass("androidx.cardview.widget.CardView") - ViewPager = jclass("androidx.viewpager.widget.ViewPager") - DatePicker = jclass("android.widget.DatePicker") - TimePicker = jclass("android.widget.TimePicker") - Spinner = jclass("android.widget.Spinner") - AutoCompleteTextView = jclass("android.widget.AutoCompleteTextView") - RatingBar = jclass("android.widget.RatingBar") - AbsoluteLayout = jclass("android.widget.AbsoluteLayout") - ScrollView = jclass("android.widget.ScrollView") - HorizontalScrollView = jclass("android.widget.HorizontalScrollView") - TableLayout = jclass("android.widget.TableLayout") - TableRow = jclass("android.widget.TableRow") - ViewFlipper = jclass("android.widget.ViewFlipper") - ViewSwitcher = jclass("android.widget.ViewSwitcher") - WebView = jclass("android.webkit.WebView") - RecyclerView = jclass("androidx.recyclerview.widget.RecyclerView") - DrawerLayout = jclass("androidx.drawerlayout.widget.DrawerLayout") - CoordinatorLayout = jclass("androidx.coordinatorlayout.widget.CoordinatorLayout") - BottomNavigationView = jclass( - "com.google.android.material.bottomnavigation.BottomNavigationView" - ) - Chip = jclass("com.google.android.material.chip.Chip") - FloatingActionButton = jclass( - "com.google.android.material.floatingactionbutton.FloatingActionButton" - ) - Snackbar = jclass("com.google.android.material.snackbar.Snackbar") - NavigationView = jclass("com.google.android.material.navigation.NavigationView") - ConstraintLayout = jclass("androidx.constraintlayout.widget.ConstraintLayout") - TextInputLayout = jclass("com.google.android.material.textfield.TextInputLayout") - MaterialCardView = jclass("com.google.android.material.card.MaterialCardView") - BottomSheetDialogFragment = jclass( - "com.google.android.material.bottomsheet.BottomSheetDialogFragment" - ) - - # Create LinearLayout - layout = LinearLayout(context) - layout_params = LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT - ) - layout.setLayoutParams(layout_params) - layout.setOrientation(LinearLayout.VERTICAL) - - # Create Button - button = Button(context) - button.setText("Button created in Python") - button.setOnClickListener(ButtonClickListener(button)) - layout.addView(button) - - # Create TextView - text_view = TextView(context) - text_view.setText("TextView created in Python") - layout.addView(text_view) - - # Create EditText - edit_text = EditText(context) - edit_text.setHint("EditText created in Python") - layout.addView(edit_text) - - # Create CheckBox - check_box = CheckBox(context) - check_box.setText("CheckBox created in Python") - layout.addView(check_box) - - # Create RadioButton - radio_button = RadioButton(context) - radio_button.setText("RadioButton created in Python") - layout.addView(radio_button) - - # Create ImageView (X) - image_view = ImageView(context) - layout.addView(image_view) - - # Create ProgressBar - progress_bar = ProgressBar(context) - layout.addView(progress_bar) - - # Create Switch - switch = Switch(context) - switch.setText("Switch created in Python") - layout.addView(switch) - - # Create ToggleButton - toggle_button = ToggleButton(context) - toggle_button.setTextOn("On") - toggle_button.setTextOff("Off") - layout.addView(toggle_button) - - # Create SeekBar (X) - seek_bar = SeekBar(context) - layout.addView(seek_bar) - - # Create CardView (X) - card_view = CardView(context) - layout.addView(card_view) - - # Create ViewPager (X) - view_pager = ViewPager(context) - layout.addView(view_pager) - - # Create DatePicker (X) - date_picker = DatePicker(context) - layout.addView(date_picker) - - # Create TimePicker (X) - time_picker = TimePicker(context) - layout.addView(time_picker) - - # Create Spinner (X) - spinner = Spinner(context) - layout.addView(spinner) - - # Create AutoCompleteTextView (X) - auto_complete_text_view = AutoCompleteTextView(context) - layout.addView(auto_complete_text_view) - - # Create RatingBar (X) - rating_bar = RatingBar(context) - layout.addView(rating_bar) - - # Return layout - return layout diff --git a/experiments/android_pythonnative_3/app/src/main/python/plot.py b/experiments/android_pythonnative_3/app/src/main/python/plot.py deleted file mode 100644 index fdfdf94..0000000 --- a/experiments/android_pythonnative_3/app/src/main/python/plot.py +++ /dev/null @@ -1,14 +0,0 @@ -import io -import matplotlib.pyplot as plt - - -def plot(x, y): - xa = [float(word) for word in x.split()] - ya = [float(word) for word in y.split()] - - fig, ax = plt.subplots() - ax.plot(xa, ya) - - f = io.BytesIO() - plt.savefig(f, format="png") - return f.getvalue() diff --git a/experiments/android_pythonnative_3/app/src/main/python/ui_layout.py b/experiments/android_pythonnative_3/app/src/main/python/ui_layout.py deleted file mode 100644 index 4dd882c..0000000 --- a/experiments/android_pythonnative_3/app/src/main/python/ui_layout.py +++ /dev/null @@ -1,24 +0,0 @@ -import json - - -def on_button_click(): - print("Button clicked!") - - -def generate_layout(): - layout = { - "widgets": [ - { - "type": "Button", - "properties": { - "text": "Click me!", - "textColor": "#FFFFFF", - "backgroundColor": "#DB4437", - }, - "eventHandlers": { - "onClick": "on_button_click", - }, - }, - ] - } - return json.dumps(layout) diff --git a/experiments/android_pythonnative_3/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/experiments/android_pythonnative_3/app/src/main/res/drawable-v24/ic_launcher_foreground.xml deleted file mode 100644 index 2b068d1..0000000 --- a/experiments/android_pythonnative_3/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/experiments/android_pythonnative_3/app/src/main/res/drawable/ic_launcher_background.xml b/experiments/android_pythonnative_3/app/src/main/res/drawable/ic_launcher_background.xml deleted file mode 100644 index 07d5da9..0000000 --- a/experiments/android_pythonnative_3/app/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,170 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/experiments/android_pythonnative_3/app/src/main/res/layout/activity_main.xml b/experiments/android_pythonnative_3/app/src/main/res/layout/activity_main.xml deleted file mode 100644 index 0e81562..0000000 --- a/experiments/android_pythonnative_3/app/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/experiments/android_pythonnative_3/app/src/main/res/layout/activity_second.xml b/experiments/android_pythonnative_3/app/src/main/res/layout/activity_second.xml deleted file mode 100644 index 9569f77..0000000 --- a/experiments/android_pythonnative_3/app/src/main/res/layout/activity_second.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - \ No newline at end of file diff --git a/experiments/android_pythonnative_3/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/experiments/android_pythonnative_3/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml deleted file mode 100644 index 6f3b755..0000000 --- a/experiments/android_pythonnative_3/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/experiments/android_pythonnative_3/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/experiments/android_pythonnative_3/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml deleted file mode 100644 index 6f3b755..0000000 --- a/experiments/android_pythonnative_3/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/experiments/android_pythonnative_3/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/experiments/android_pythonnative_3/app/src/main/res/mipmap-hdpi/ic_launcher.webp deleted file mode 100644 index c209e78..0000000 Binary files a/experiments/android_pythonnative_3/app/src/main/res/mipmap-hdpi/ic_launcher.webp and /dev/null differ diff --git a/experiments/android_pythonnative_3/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/experiments/android_pythonnative_3/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp deleted file mode 100644 index b2dfe3d..0000000 Binary files a/experiments/android_pythonnative_3/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and /dev/null differ diff --git a/experiments/android_pythonnative_3/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/experiments/android_pythonnative_3/app/src/main/res/mipmap-mdpi/ic_launcher.webp deleted file mode 100644 index 4f0f1d6..0000000 Binary files a/experiments/android_pythonnative_3/app/src/main/res/mipmap-mdpi/ic_launcher.webp and /dev/null differ diff --git a/experiments/android_pythonnative_3/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/experiments/android_pythonnative_3/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp deleted file mode 100644 index 62b611d..0000000 Binary files a/experiments/android_pythonnative_3/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and /dev/null differ diff --git a/experiments/android_pythonnative_3/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/experiments/android_pythonnative_3/app/src/main/res/mipmap-xhdpi/ic_launcher.webp deleted file mode 100644 index 948a307..0000000 Binary files a/experiments/android_pythonnative_3/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and /dev/null differ diff --git a/experiments/android_pythonnative_3/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/experiments/android_pythonnative_3/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp deleted file mode 100644 index 1b9a695..0000000 Binary files a/experiments/android_pythonnative_3/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/experiments/android_pythonnative_3/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/experiments/android_pythonnative_3/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp deleted file mode 100644 index 28d4b77..0000000 Binary files a/experiments/android_pythonnative_3/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and /dev/null differ diff --git a/experiments/android_pythonnative_3/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/experiments/android_pythonnative_3/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp deleted file mode 100644 index 9287f50..0000000 Binary files a/experiments/android_pythonnative_3/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/experiments/android_pythonnative_3/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/experiments/android_pythonnative_3/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp deleted file mode 100644 index aa7d642..0000000 Binary files a/experiments/android_pythonnative_3/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and /dev/null differ diff --git a/experiments/android_pythonnative_3/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/experiments/android_pythonnative_3/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp deleted file mode 100644 index 9126ae3..0000000 Binary files a/experiments/android_pythonnative_3/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/experiments/android_pythonnative_3/app/src/main/res/values-night/themes.xml b/experiments/android_pythonnative_3/app/src/main/res/values-night/themes.xml deleted file mode 100644 index c128015..0000000 --- a/experiments/android_pythonnative_3/app/src/main/res/values-night/themes.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - \ No newline at end of file diff --git a/experiments/android_pythonnative_3/app/src/main/res/values/colors.xml b/experiments/android_pythonnative_3/app/src/main/res/values/colors.xml deleted file mode 100644 index c8524cd..0000000 --- a/experiments/android_pythonnative_3/app/src/main/res/values/colors.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - #FF000000 - #FFFFFFFF - \ No newline at end of file diff --git a/experiments/android_pythonnative_3/app/src/main/res/values/strings.xml b/experiments/android_pythonnative_3/app/src/main/res/values/strings.xml deleted file mode 100644 index 115bda4..0000000 --- a/experiments/android_pythonnative_3/app/src/main/res/values/strings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - pythonnative - \ No newline at end of file diff --git a/experiments/android_pythonnative_3/app/src/main/res/values/themes.xml b/experiments/android_pythonnative_3/app/src/main/res/values/themes.xml deleted file mode 100644 index 80a3862..0000000 --- a/experiments/android_pythonnative_3/app/src/main/res/values/themes.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - -