Repository: actuate-rs/actuate
Branch: main
Commit: 3375218576da
Files: 38
Total size: 177.4 KB
Directory structure:
gitextract_uddujspb/
├── .gitattributes
├── .github/
│ ├── FUNDING.yml
│ └── workflows/
│ └── ci.yml
├── .gitignore
├── CHANGELOG.md
├── Cargo.toml
├── LICENSE-APACHE
├── LICENSE-MIT
├── README.md
├── examples/
│ ├── README.md
│ ├── counter.rs
│ ├── http.rs
│ ├── radio_button.rs
│ └── timer.rs
├── macros/
│ ├── CHANGELOG.md
│ ├── Cargo.toml
│ └── src/
│ └── lib.rs
├── src/
│ ├── animation.rs
│ ├── compose/
│ │ ├── catch.rs
│ │ ├── dyn_compose.rs
│ │ ├── from_fn.rs
│ │ ├── from_iter.rs
│ │ ├── memo.rs
│ │ └── mod.rs
│ ├── composer.rs
│ ├── data.rs
│ ├── ecs/
│ │ ├── mod.rs
│ │ └── spawn.rs
│ ├── executor.rs
│ ├── lib.rs
│ └── ui/
│ ├── material/
│ │ ├── button.rs
│ │ ├── container.rs
│ │ ├── mod.rs
│ │ ├── radio.rs
│ │ ├── text.rs
│ │ └── ui.rs
│ └── mod.rs
└── tests/
└── composer.rs
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitattributes
================================================
* text=auto
*.rs text eol=lf
*.toml text eol=lf
*.wgsl text eol=lf
*.txt text eol=lf
*.png binary
*.ttf binary
================================================
FILE: .github/FUNDING.yml
================================================
github: actuate-rs
open_collective: actuateui
================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
env:
CARGO_TERM_COLOR: always
jobs:
# Build core features
core:
name: Build core
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout sources
uses: actions/checkout@v4
- name: Cache
uses: actions/cache@v4
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-test-${{ hashFiles('**/Cargo.toml') }}
- name: Install stable toolchain
uses: dtolnay/rust-toolchain@stable
- name: Run cargo build
run: cargo build --verbose
# Run cargo fmt --all -- --check
format:
name: Format
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout sources
uses: actions/checkout@v4
- name: Install stable toolchain
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
- name: Run cargo fmt
run: cargo fmt --all -- --check
# Run cargo clippy
clippy:
name: Clippy
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout sources
uses: actions/checkout@v4
- name: Cache
uses: actions/cache@v4
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-clippy-${{ hashFiles('**/Cargo.toml') }}
- name: Install stable toolchain
uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- name: Install Dependencies
run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev
- name: Run clippy
run: cargo clippy -- -D warnings
# Run cargo test
test:
name: Tests
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout sources
uses: actions/checkout@v4
- name: Cache
uses: actions/cache@v4
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-test-${{ hashFiles('**/Cargo.toml') }}
- name: Install stable toolchain
uses: dtolnay/rust-toolchain@stable
- name: Install Dependencies
run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev
- name: Run cargo test
run: cargo test --features full --verbose
# Run cargo test
miri:
name: Miri
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout sources
uses: actions/checkout@v4
- name: Cache
uses: actions/cache@v4
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-test-${{ hashFiles('**/Cargo.toml') }}
- name: Install stable toolchain
uses: dtolnay/rust-toolchain@stable
- name: Install Dependencies
run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev
- name: Install Miri
run: |
rustup toolchain install nightly --component miri
rustup override set nightly
cargo miri setup
- name: Test with Miri
run: cargo miri test --workspace --features full --verbose
================================================
FILE: .gitignore
================================================
/target
dist/
================================================
FILE: CHANGELOG.md
================================================
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.21.0](https://github.com/actuate-rs/actuate/compare/actuate-v0.20.1...actuate-v0.21.0) - 2025-12-18
## Breaking changes
- Update to Bevy v0.17.3
## [0.20.1](https://github.com/actuate-rs/actuate/compare/actuate-v0.20.0...actuate-v0.20.1) - 2024-12-13
## Documentation
- Fix formatting in crate example
## [0.20.0](https://github.com/actuate-rs/actuate/compare/actuate-v0.19.1...actuate-v0.20.0) - 2024-12-13
## Breaking changes
- Fix `Data` impl for `&T` (63d89ed)
- Remove Data from root exports (677b160)
- Replace `Memoize` trait with more specific `Generational` trait (febe238)
## Features
- Impl `Data` for Rc<dyn Fn(..)> and derive `Clone` for `Catch` composable (e038307)
- Impl `Clone` for `from_fn`, `from_iter`, and `memo` composables (21c016f)
- Add `material_ui` composable (5dad9a3)
## Fixes
- Replace `std` deps with `core` (68d44a2)
- Simplify styling in `scroll_view` composable and update `http` example (f90e4c4)
- Check for removed entities in Spawn composable (c23e158)
## Documentation
- Add docs to `use_local_task` (63d89ed)
- Add docs to `use_task` (7ddbe84)
- Update counter example (3b79bb1)
- Update borrowing docs (efcdfe3)
## [0.19.1](https://github.com/actuate-rs/actuate/compare/actuate-v0.19.0...actuate-v0.19.1) - 2024-12-09
## Features
- Add `use_effect` hook (5ae0a51)
- `fn use_effect<D, T>(cx: ScopeState<'_>, dependency: D, effect: impl FnOnce(&D))`
## Fixes
- Remove `AnyItemState` in `from_iter` composable to pass stacked borrows check in miri (2360814)
## Documentation
- Add docs for `from_fn` and `from_iter` composables (5c379e1)
## [0.19.0](https://github.com/actuate-rs/actuate/compare/actuate-v0.18.1...actuate-v0.19.0) - 2024-12-08
## Breaking changes
- Require `'static` items in `from_iter` composable
- This prevents edge cases where an item may have been removed from a collection, but references still exist down the tree.
## Documentation
- Add logo to rustdocs (702b1e0)
- Update material docs (3084286)
- Update link to `core::error::Error` in `Catch` composable (b664206)
## [0.18.1](https://github.com/actuate-rs/actuate/compare/actuate-v0.18.0...actuate-v0.18.1) - 2024-12-07
## Fixes
- Specify `winit` backend for `docs.rs` (62bec2d)
## [0.18.0](https://github.com/actuate-rs/actuate/compare/actuate-v0.17.2...actuate-v0.18.0) - 2024-12-07
## Breaking changes
- Create `ScrollView` composable and new `ui` module (28628f4, ebb17b0)
- The `material` is now located under `ui`
## Features
- Add support for reactive Bevy desktop apps (7c65ba9, 6918000)
- Add more picking handler methods to `Modify` (64404b3)
- More advanced typography with new Text composable (825e007)
- Derive `Clone` for `Spawn` and refactor (6c4e457)
- Add methods to modify all Node fields
## [0.17.2](https://github.com/actuate-rs/actuate/compare/actuate-v0.17.1...actuate-v0.17.2) - 2024-12-07
## Fixes
- Update pending composable ordering and track child index in `Spawn` composable (fdf89ed)
- Reverse node IDs and refactor internals (42e1971)
## Documentation
- Add docs to `spawn` constructor and split up ecs module (9c06bfe)
- Move examples for `catch` and `dyn_compose` to rustdocs (9502a4b)
- Move traits example to data module and add docs, reorganize examples (67ec922)
- Update Data docs (829c6d9)
## [0.17.1](https://github.com/actuate-rs/actuate/compare/actuate-v0.17.0...actuate-v0.17.1) - 2024-12-06
## Features
- Create `FromFn` composable
- You can now create a composable without input using `from_fn` (433ab1d)
- ```rs
fn from_fn<F, C>(f: F) -> FromFn<F, C>
where
F: Fn(ScopeState) -> C,
C: Compose
```
- Derive `Clone` and `Debug` for `Button`, `Container`, and `RadioButton` (4f337ed)
## Documentation
- Update docs for feature flags (869aa89)
## [0.17.0](https://github.com/actuate-rs/actuate/compare/actuate-v0.16.1...actuate-v0.17.0) - 2024-12-06
## Breaking changes
- Move `Modifier` and `Modify` to ecs module (behind new picking feature) (35b10ea)
- These items can be useful for other design systems than Material 3
- Call `on_insert` on every insertion of a spawned bundle (this now requires `Fn` instead of `FnOnce`) (533da07)
## Fixes
- Revert from breadth-first traversal of the composition to depth-first (8b3acd2)
- Update styling for `Container` (9cca3a7)
## [0.16.1](https://github.com/actuate-rs/actuate/compare/actuate-v0.16.0...actuate-v0.16.1) - 2024-12-05
## Features
- Material UI components
- `Button`
- `Container`
- `RadioButton`
- `text`
- `label`
- `heading`
- New scheduling algorithm based on `BTreeSet` (2a457a9)
## [0.16.0](https://github.com/actuate-rs/actuate/compare/actuate-v0.15.0...actuate-v0.16.0) - 2024-12-05
### Breaking changes
- Major internal rewrite! (9ef73eb) The new internals allow for more dynamic control over the composition
, enabling features like pause and resume of a composition.
`Composer::try_compose` will now also skip directly to changed composables, rather than setting change flags.
- Removes exported methods for `ScopeData`
- The `Runtime` struct is now private to ensure safety
## Features
- `Composer` is now an iterator! This allows for stepping through each composable in the composition.
- `Composer` also implements `fmt::Debug`:
```rs
use actuate::prelude::*;
use actuate::composer::Composer;
#[derive(Data)]
struct A;
impl Compose for A {
fn compose(cx: Scope<Self>) -> impl Compose {
(B, C)
}
}
#[derive(Data)]
struct B;
impl Compose for B {
fn compose(cx: Scope<Self>) -> impl Compose {}
}
#[derive(Data)]
struct C;
impl Compose for C {
fn compose(cx: Scope<Self>) -> impl Compose {}
}
let mut composer = Composer::new(A);
composer.try_compose().unwrap();
assert_eq!(format!("{:?}", composer), "Composer(A(B, C))")
```
## [0.15.0](https://github.com/actuate-rs/actuate/compare/actuate-v0.14.2...actuate-v0.15.0) - 2024-12-03
### Breaking changes
- Add `#[actuate(path = "..")]` attribute to `Data` macro and use fully-qualified path to Actuate by default (b159478).
- This allows for use of the `Data` macro without importing the full `prelude`.
- Replace `DynCompose::new` with `dyn_compose` constructor fn (9d65ec8).
- Return `Rc` from use_context
- `fn use_context<T: 'static>(cx: ScopeState<'_>) -> Result<&Rc<T>, ContextError<T>> { .. }`
- This allows for cloning context into `'static` environments.
### Refactors
- Use explicit imports internally to speed up compile times and exclude hidden `Data` traits from prelude (07bfd96).
## [0.14.2](https://github.com/actuate-rs/actuate/compare/actuate-v0.14.1...actuate-v0.14.2) - 2024-12-03
### Features
- Optimize empty composables by skipping creation of ScopeData
### Fixes
- Enable Tokio dependency with animation and ecs features (5263fe4)
## [0.14.1](https://github.com/actuate-rs/actuate/compare/actuate-v0.14.0...actuate-v0.14.1) - 2024-12-03
### Fixes
- Remove unused tokio read lock guard (0ad962f)
## [0.14.0](https://github.com/actuate-rs/actuate/compare/actuate-v0.13.0...actuate-v0.14.0) - 2024-12-03
### Breaking Changes
- Remove unsound `Compose` impl for `Map` and create `MapUnchecked` struct
- The original `Compose` impl for `Map` would cause undefined behavior if multiple references to the same composable were used. The new unsafe `MapUnchecked` keeps this functionality for low-level components, where the documented safety contract can be checked. However, for most composables I now see `Compose + Clone` being a typical pattern (which I think is fine given some composables only copy references when cloned, and references to composables can still be passed around).
### Fixes
- Impl re-composition when the type has changed in `DynCompose` (7d41100)
### Documentation
- Update docs for `Spawn` composable (205b88a)
- Add example to showcase `DynCompose` (7d41100)
## [0.13.0](https://github.com/actuate-rs/actuate/compare/actuate-v0.12.0...actuate-v0.13.0) - 2024-12-02
### Breaking Changes
- Use `PartialEq` in `use_memo` instead of the `Memoize` trait (6539c95)
- This is to memoize tuples and other groups of data.
To use pointer equality, you can still use `Signal::generation` or `Memoize::memoize` to get the current generation.
- Remove unused UseWorld struct (81615cd)
### Documentation
- Add more documentation to the `Catch` composable
- Adds a quick explanation of using `Result` + `Catch`, and links to the `catch` constructor function for more info.
- Add explanation to `compose::from_iter` (dc6715d)
### Other
- Change release procedure and update CI (dd4be8d, fe23aad, 723fe6c)
## [0.12.0](https://github.com/actuate-rs/actuate/compare/actuate-v0.11.0...actuate-v0.12.0) - 2024-12-02
### Other
- `#![no_std]` support ([#100](https://github.com/actuate-rs/actuate/pull/100))
- Clean up and add internal docs
- Remove Sized bound in Compose trait
- Create `Catch` composable and impl `Compose` for `Result` ([#99](https://github.com/actuate-rs/actuate/pull/99))
- Add getter and setter methods to ScopeData
- Update docs
- Remove is_empty from ScopeState in favor of checking for empty types
- Create README.md
## [0.11.0](https://github.com/actuate-rs/actuate/compare/actuate-v0.10.2...actuate-v0.11.0) - 2024-11-29
### Other
- Update to Bevy 0.15.0
- Disable observers after drop
- Add support for standard references in RefMap and Cow
- Fix formatting in README
## [0.10.2](https://github.com/actuate-rs/actuate/compare/actuate-v0.10.1...actuate-v0.10.2) - 2024-11-28
### Other
- Add specialized impl of SystemParamFunction for Triggers
- Export animation channel
- Impl Data for UseAnimated
- Impl Data for Pin
- Impl Data for Box<dyn Future<Output = ()>>
- Allow return values for Data fns
- Create `use_animated` hook ([#88](https://github.com/actuate-rs/actuate/pull/88))
- Fix tasks not running on the ecs
## [0.10.1](https://github.com/actuate-rs/actuate/compare/actuate-v0.10.0...actuate-v0.10.1) - 2024-11-26
### Other
- Apply system params in use_world_once
- Apply deferred system param updates
- Add SignalMut::set_if_neq and generation methods
================================================
FILE: Cargo.toml
================================================
[package]
name = "actuate"
version = "0.21.0"
edition = "2021"
license = "MIT OR Apache-2.0"
description = "A reactive user-interface framework"
repository = "https://github.com/actuate-rs/actuate"
[features]
animation = ["ecs", "dep:bevy_math", "dep:bevy_time", "dep:tokio"]
ecs = ["std", "dep:bevy_app", "dep:bevy_ecs", "dep:bevy_utils", "dep:bevy_winit"]
executor = ["std", "dep:tokio"]
material = ["ecs", "ui", "picking", "dep:bevy_color", "dep:bevy_input", "dep:bevy_text"]
picking = ["dep:bevy_picking"]
rt = ["executor", "tokio/rt-multi-thread"]
std = []
tracing = ["dep:tracing"]
ui = ["dep:bevy_ui"]
full = ["animation", "ecs", "material", "rt", "tracing"]
default = ["std"]
[workspace]
members = [
".",
"macros"
]
[dependencies]
actuate-macros = { version = "0.2.0", path = "macros" }
ahash = { version = "0.8.11", default-features = false }
bevy_app = { version = "0.17.3", optional = true }
bevy_color = { version = "0.17.3", optional = true }
bevy_ecs = { version = "0.17.3", optional = true }
bevy_input = { version = "0.17.3", optional = true }
bevy_math = { version = "0.17.3", optional = true }
bevy_picking = { version = "0.17.3", optional = true }
bevy_text = { version = "0.17.3", optional = true }
bevy_time = { version = "0.17.3", optional = true }
bevy_ui = { version = "0.17.3", optional = true }
bevy_utils = { version = "0.17.3", optional = true }
bevy_winit = { version = "0.17.3", optional = true }
crossbeam-queue = { version = "0.3.11", default-features = false, features = ["alloc"] }
futures = "0.3.31"
hashbrown = "0.15.2"
slotmap = "1.0.7"
thiserror = "2.0.3"
tracing = { version = "0.1.40", optional = true }
tokio = { version = "1.41.1", features = ["sync"], optional = true }
typeid = "1.0.2"
[dev-dependencies]
bevy = { version = "0.17.3" }
reqwest = { version = "0.12.9", features = ["json"] }
serde = { version = "1.0.215", features = ["derive"] }
tracing-subscriber = "0.3.18"
[package.metadata.docs.rs]
all-features = true
features = ["bevy_winit/x11"]
rustdoc-args = ["--cfg", "docsrs"]
[[example]]
name = "counter"
required-features = ["material"]
[[example]]
name = "http"
required-features = ["material", "rt"]
[[example]]
name = "radio_button"
required-features = ["material"]
[[example]]
name = "timer"
required-features = ["ecs"]
================================================
FILE: LICENSE-APACHE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: LICENSE-MIT
================================================
MIT License
Copyright (c) 2023 Matthew Hunzinger
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
<div align="center">
<h1>Actuate</h1>
<a href="https://crates.io/crates/actuate">
<img src="https://img.shields.io/crates/v/actuate?style=flat-square"
alt="Crates.io version" />
</a>
<a href="https://docs.rs/actuate">
<img src="https://img.shields.io/badge/docs-latest-blue.svg?style=flat-square"
alt="docs.rs docs" />
</a>
<a href="https://github.com/actuate-rs/actuate/actions">
<img src="https://github.com/actuate-rs/actuate/actions/workflows/ci.yml/badge.svg"
alt="CI status" />
</a>
</div>
<div align="center">
<a href="https://github.com/actuate-rs/actuate/tree/main/examples">Examples</a>
</div>
<br />
A high-performance and borrow-checker friendly framework for declarative programming in Rust.
This crate provides a generic library that lets you define reactive components (also known as composables).
## Features
- Declarative scenes and UI for [Bevy](https://github.com/bevyengine/bevy)
- Efficient and borrow-checker friendly state management: Manage state with components and hooks, all using zero-cost smart pointers
- Generic core for custom backends
```rust
use actuate::prelude::*;
#[derive(Data)]
struct Counter {
start: i32,
}
impl Compose for Counter {
fn compose(cx: Scope<Self>) -> impl Compose {
let count = use_mut(&cx, || cx.me().start);
material_ui((
text::headline(format!("High five count: {}", count)),
button(text::label("Up high")).on_click(move || SignalMut::update(count, |x| *x += 1)),
button(text::label("Down low")).on_click(move || SignalMut::update(count, |x| *x -= 1)),
if *count == 0 {
Some(text::label("Gimme five!"))
} else {
None
},
))
.align_items(AlignItems::Center)
.justify_content(JustifyContent::Center)
}
}
```
## Borrowing
Composables can borrow from their ancestors, as well as state.
```rs
use actuate::prelude::*;
#[derive(Data)]
struct User<'a> {
// `actuate::Cow` allows for either a borrowed or owned value.
name: Cow<'a, String>,
}
impl Compose for User<'_> {
fn compose(cx: Scope<Self>) -> impl Compose {
text::headline(cx.me().name.to_string())
}
}
#[derive(Data)]
struct App {
name: String
}
impl Compose for App {
fn compose(cx: Scope<Self>) -> impl Compose {
// Get a mapped reference to the app's `name` field.
let name = Signal::map(cx.me(), |me| &me.name).into();
User { name }
}
}
```
## Installation
To add this crate to your project:
```
cargo add actuate --features full
```
For more feature flags, see the crate documentation for [features](https://docs.rs/actuate/latest/actuate/#features).
## Inspiration
This crate is inspired by [Xilem](https://github.com/linebender/xilem) and uses a similar approach to type-safe reactivity. The main difference with this crate is the concept of scopes, components store their state in their own scope and updates to that scope re-render the component.
State management is inspired by React and [Dioxus](https://github.com/DioxusLabs/dioxus).
Previous implementations were in [Concoct](https://github.com/concoct-rs/concoct) but were never very compatible with lifetimes.
================================================
FILE: examples/README.md
================================================
## Examples
Examples combining Actuate and [Bevy](https://github.com/bevyengine/bevy)
You can run these examples with:
```
cargo run --features full --example {EXAMPLE}
```
================================================
FILE: examples/counter.rs
================================================
// Counter UI example.
use actuate::prelude::*;
use bevy::{prelude::*, winit::WinitSettings};
// Counter composable.
#[derive(Data)]
struct Counter {
start: i32,
}
impl Compose for Counter {
fn compose(cx: Scope<Self>) -> impl Compose {
let count = use_mut(&cx, || cx.me().start);
material_ui((
text::headline(format!("High five count: {}", count)),
button(text::label("Up high")).on_click(move || SignalMut::update(count, |x| *x += 1)),
button(text::label("Down low")).on_click(move || SignalMut::update(count, |x| *x -= 1)),
if *count == 0 {
Some(text::label("Gimme five!"))
} else {
None
},
))
.align_items(AlignItems::Center)
.justify_content(JustifyContent::Center)
}
}
fn setup(mut commands: Commands) {
commands.spawn(Camera2d::default());
// Spawn a composition with a `Counter`, adding it to the Actuate runtime.
commands.spawn((
Node {
width: Val::Percent(100.),
height: Val::Percent(100.),
..default()
},
Composition::new(Counter { start: 0 }),
));
}
fn main() {
App::new()
.add_plugins((DefaultPlugins, ActuatePlugin))
.insert_resource(WinitSettings::desktop_app())
.add_systems(Startup, setup)
.run();
}
================================================
FILE: examples/http.rs
================================================
// HTTP UI example
use actuate::{executor::ExecutorContext, prelude::*};
use bevy::{prelude::*, winit::WinitSettings};
use serde::Deserialize;
use std::collections::HashMap;
// Dog breed composable.
#[derive(Data)]
struct Breed {
name: String,
families: Vec<String>,
}
impl Compose for Breed {
fn compose(cx: Scope<Self>) -> impl Compose {
container((
text::headline(cx.me().name.to_owned()),
compose::from_iter(cx.me().families.clone(), |family| {
text::label(family.to_string())
}),
))
}
}
#[derive(Deserialize)]
struct Response {
message: HashMap<String, Vec<String>>,
}
// Dog breed list composable.
#[derive(Data)]
struct BreedList;
impl Compose for BreedList {
fn compose(cx: Scope<Self>) -> impl Compose {
let breeds = use_mut(&cx, HashMap::new);
// Spawn a task that loads dog breeds from an HTTP API.
use_task(&cx, move || async move {
let json: Response = reqwest::get("https://dog.ceo/api/breeds/list/all")
.await
.unwrap()
.json()
.await
.unwrap();
SignalMut::set(breeds, json.message);
});
material_ui(
// Render the currently loaded breeds.
scroll_view(compose::from_iter((*breeds).clone(), |breed| Breed {
name: breed.0.clone(),
families: breed.1.clone(),
}))
.max_width(Val::Px(400.))
.flex_gap(Val::Px(30.)),
)
.align_items(AlignItems::Center)
}
}
#[derive(Data)]
struct Example;
impl Compose for Example {
fn compose(cx: Scope<Self>) -> impl Compose {
// Setup the Tokio executor.
use_provider(&cx, ExecutorContext::default);
BreedList
}
}
fn main() {
App::new()
.add_plugins((DefaultPlugins, ActuatePlugin))
.insert_resource(WinitSettings::desktop_app())
.add_systems(Startup, setup)
.run();
}
fn setup(mut commands: Commands) {
commands.spawn(Camera2d::default());
// Spawn a composition with a `BreedList`, adding it to the Actuate runtime.
commands.spawn((
Node {
width: Val::Percent(100.),
height: Val::Percent(100.),
..default()
},
Composition::new(Example),
));
}
================================================
FILE: examples/radio_button.rs
================================================
// Counter UI example.
use actuate::prelude::*;
use bevy::prelude::*;
// Counter composable.
#[derive(Data)]
struct Example;
impl Compose for Example {
fn compose(cx: Scope<Self>) -> impl Compose {
let is_shown = use_mut(&cx, || true);
radio_button()
.is_enabled(*is_shown)
.on_click(move || SignalMut::update(is_shown, |x| *x = !*x))
}
}
fn setup(mut commands: Commands) {
commands.spawn(Camera2d::default());
// Spawn a composition with a `Counter`, adding it to the Actuate runtime.
commands.spawn((
Node {
flex_direction: FlexDirection::Column,
align_items: AlignItems::Center,
row_gap: Val::Px(10.),
..default()
},
Composition::new(Example),
));
}
fn main() {
App::new()
.add_plugins((DefaultPlugins, ActuatePlugin))
.add_systems(Startup, setup)
.run();
}
================================================
FILE: examples/timer.rs
================================================
// Timer UI example.
use actuate::prelude::*;
use bevy::prelude::*;
// Timer composable.
#[derive(Data)]
struct Timer;
impl Compose for Timer {
fn compose(cx: Scope<Self>) -> impl Compose {
let current_time = use_mut(&cx, Time::default);
// Use the `Time` resource from the ECS world, updating the `current_time`.
use_world(&cx, move |time: Res<Time>| {
SignalMut::set(current_time, *time)
});
// Spawn a `Text` component, updating it when this scope is re-composed.
spawn(Text::new(format!("Elapsed: {:?}", current_time.elapsed())))
}
}
fn main() {
App::new()
.add_plugins((DefaultPlugins, ActuatePlugin))
.add_systems(Startup, setup)
.run();
}
fn setup(mut commands: Commands) {
commands.spawn(Camera2d::default());
// Spawn a composition with a `Timer`, adding it to the Actuate runtime.
commands.spawn((Node::default(), Composition::new(Timer)));
}
================================================
FILE: macros/CHANGELOG.md
================================================
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.2.0](https://github.com/actuate-rs/actuate/compare/actuate-macros-v0.1.6...actuate-macros-v0.2.0) - 2024-12-3
### Breaking changes
- Add `#[actuate(path = "..")]` attribute to `Data` macro and use fully-qualified path to Actuate by default (b159478)
## [0.1.6](https://github.com/actuate-rs/actuate/compare/actuate-macros-v0.1.5...actuate-macros-v0.1.6) - 2024-11-25
### Other
- Add basic support for borrowed trait objects
## [0.1.5](https://github.com/actuate-rs/actuate/compare/actuate-macros-v0.1.4...actuate-macros-v0.1.5) - 2024-11-21
### Other
- Remove Data::Id in favor of typeid crate ([#65](https://github.com/actuate-rs/actuate/pull/65))
## [0.1.4](https://github.com/actuate-rs/actuate/compare/actuate-macros-v0.1.3...actuate-macros-v0.1.4) - 2024-11-18
### Other
- Move macros crate to root and refactor Mut to use NonNull
## [0.1.3](https://github.com/actuate-rs/actuate/compare/actuate-macros-v0.1.2...actuate-macros-v0.1.3) - 2024-11-16
### Other
- Test Memo composable
- Safely impl Compose for Map<C>
- Use Data macro in more places
- Refactor macro
## [0.1.2](https://github.com/actuate-rs/actuate/compare/actuate-macros-v0.1.1...actuate-macros-v0.1.2) - 2024-11-13
### Other
- Create Handler struct
- Create actuate-winit crate and refactor
## [0.1.1](https://github.com/actuate-rs/actuate/compare/actuate-macros-v0.1.0...actuate-macros-v0.1.1) - 2024-11-12
### Other
- Create core crate and refactor
================================================
FILE: macros/Cargo.toml
================================================
[package]
name = "actuate-macros"
description = "Macros for Actuate"
version = "0.2.0"
edition = "2021"
license = "MIT OR Apache-2.0"
repository = "https://github.com/actuate-rs/actuate"
[lib]
proc-macro = true
[dependencies]
quote = "1.0.37"
syn = { version = "2.0.87", features = ["full"] }
================================================
FILE: macros/src/lib.rs
================================================
use proc_macro::TokenStream;
use quote::{format_ident, quote, ToTokens};
use syn::{
parse_macro_input, parse_quote, punctuated::Punctuated, token::Comma, Data, DeriveInput,
GenericParam, ItemTrait, MetaNameValue, TypeParamBound,
};
#[proc_macro_derive(Data, attributes(actuate))]
pub fn derive_data(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let ident = &input.ident;
let generics = &input.generics;
let mut cell = None;
if let Some(attr) = input
.attrs
.iter()
.find(|attr| attr.path().is_ident("actuate"))
{
let args: MetaNameValue = attr.parse_args().unwrap();
if args.path.get_ident().unwrap() == "path" {
let value = args.value.to_token_stream().to_string();
cell = Some(format_ident!("{}", &value[1..value.len() - 1]));
}
}
let actuate = cell.unwrap_or(format_ident!("actuate"));
let generic_params: Punctuated<_, Comma> = generics
.params
.iter()
.map(|param| match param {
GenericParam::Lifetime(lifetime_param) => lifetime_param.to_token_stream(),
GenericParam::Type(type_param) => {
let ident = &type_param.ident;
let mut bounds = type_param.bounds.clone();
bounds.push(parse_quote!(#actuate::data::Data));
quote! {
#ident: #bounds
}
}
GenericParam::Const(const_param) => const_param.to_token_stream(),
})
.collect();
let generic_ty_params: Punctuated<_, Comma> = generics
.params
.iter()
.map(|param| match param {
GenericParam::Lifetime(lifetime_param) => lifetime_param.to_token_stream(),
GenericParam::Type(type_param) => type_param.ident.to_token_stream(),
GenericParam::Const(const_param) => const_param.to_token_stream(),
})
.collect();
let Data::Struct(input_struct) = input.data else {
todo!()
};
let checks = input_struct.fields.iter().map(|field| {
let field_ident = field.ident.as_ref().unwrap();
let check_ident = format_ident!("__check_{}_{}", ident, field_ident);
quote! {
#[doc(hidden)]
#[allow(non_snake_case)]
fn #check_ident <#generic_params> (t: #ident <#generic_ty_params>) {
use #actuate::data::{FieldWrap, DataField, FnField, StaticField};
(&&FieldWrap(t.#field_ident)).check()
}
}
});
let gen = quote! {
#( #checks )*
#[doc(hidden)]
unsafe impl <#generic_params> #actuate::data::Data for #ident <#generic_ty_params> {}
};
gen.into()
}
#[proc_macro_attribute]
pub fn data(_attrs: TokenStream, input: TokenStream) -> TokenStream {
let item = parse_macro_input!(input as ItemTrait);
let contains_data = item.supertraits.iter().any(|x| {
if let TypeParamBound::Trait(trait_bound) = x {
if trait_bound.path.is_ident("Data") {
return true;
}
}
false
});
if !contains_data {
return quote! {
compile_error!("\
Traits used as `Data` must require all implementations to be `Data`. \
To fix this, add `Data` as a supertrait to your trait (i.e trait MyTrait: Data {}).\
");
}
.into();
}
let ident = &item.ident;
quote! {
#item
unsafe impl actuate::data::Data for Box<dyn #ident + '_> {}
}
.into()
}
================================================
FILE: src/animation.rs
================================================
use crate::{
data::Data,
ecs::{use_world, use_world_once},
use_local_task, use_mut, use_ref, ScopeState, Signal, SignalMut,
};
use bevy_ecs::prelude::*;
use bevy_math::VectorSpace;
use bevy_time::Time;
use std::{
cell::{Cell, RefCell},
ops::Deref,
time::Duration,
};
use tokio::sync::{mpsc, oneshot};
struct State<T> {
from: T,
to: T,
duration: Duration,
tx: Option<oneshot::Sender<()>>,
}
/// Use an animated value.
pub fn use_animated<T>(cx: ScopeState<'_>, make_initial: impl FnOnce() -> T) -> UseAnimated<'_, T>
where
T: VectorSpace + Send + 'static,
T::Scalar: From<f32>,
{
let start_cell = use_world_once(cx, |time: Res<Time>| Cell::new(Some(time.elapsed_secs())));
let (controller, rx) = use_ref(cx, || {
let (tx, rx) = mpsc::unbounded_channel();
(AnimationController { tx }, Cell::new(Some(rx)))
});
let state: &RefCell<Option<State<T>>> = use_ref(cx, || RefCell::new(None));
let out = use_mut(cx, make_initial);
let time_cell = use_ref(cx, || Cell::new(start_cell.get().unwrap()));
use_world(cx, |time_res: Res<Time>| {
time_cell.set(time_res.elapsed_secs());
});
use_local_task(cx, move || async move {
let mut rx = rx.take().unwrap();
while let Some((to, duration, tx)) = rx.recv().await {
*state.borrow_mut() = Some(State {
from: *out,
to,
duration,
tx: Some(tx),
});
start_cell.set(Some(time_cell.get()));
}
});
use_world(cx, move |time: Res<Time>| {
if let Some(start) = start_cell.get() {
let mut state_cell = state.borrow_mut();
if let Some(state) = &mut *state_cell {
let elapsed = time.elapsed_secs() - start;
if elapsed < state.duration.as_secs_f32() {
SignalMut::set(
out,
state.from.lerp(
state.to,
T::Scalar::from(elapsed / state.duration.as_secs_f32()),
),
);
} else {
SignalMut::set(out, state.to);
state.tx.take().unwrap().send(()).unwrap();
*state_cell = None;
}
}
}
});
UseAnimated {
value: SignalMut::as_ref(out),
controller,
}
}
/// Hook for [`use_animated`].
pub struct UseAnimated<'a, T> {
value: Signal<'a, T>,
controller: &'a AnimationController<T>,
}
impl<T> UseAnimated<'_, T> {
/// Animate this value over a duration.
pub async fn animate(&self, to: T, duration: Duration) {
self.controller.animate(to, duration).await
}
/// Get the controller for this animation.
pub fn controller(&self) -> AnimationController<T> {
self.controller.clone()
}
}
impl<T> Clone for UseAnimated<'_, T> {
fn clone(&self) -> Self {
*self
}
}
impl<T> Copy for UseAnimated<'_, T> {}
impl<T> Deref for UseAnimated<'_, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.value
}
}
unsafe impl<T> Data for UseAnimated<'_, T> {}
/// Controller for an animation created with [`use_animated`].
pub struct AnimationController<T> {
tx: mpsc::UnboundedSender<(T, Duration, oneshot::Sender<()>)>,
}
impl<T> AnimationController<T> {
/// Animate this value over a duration.
pub async fn animate(&self, to: T, duration: Duration) {
let (tx, rx) = oneshot::channel();
self.tx.send((to, duration, tx)).unwrap();
rx.await.unwrap()
}
}
impl<T> Clone for AnimationController<T> {
fn clone(&self) -> Self {
Self {
tx: self.tx.clone(),
}
}
}
================================================
FILE: src/compose/catch.rs
================================================
use super::CatchContext;
use crate::{compose::Compose, data::Data, use_provider, Scope, Signal};
use alloc::rc::Rc;
use core::mem;
/// Create a composable that catches errors from its children.
/// This will catch all errors from its descendants, until another `catch` is encountered.
///
/// If a child returns a `Result<T, actuate::Error>`,
/// any errors will be caught by this composable by calling `on_error`.
///
/// # Examples
///
/// ```no_run
/// use actuate::prelude::*;
///
/// #[derive(Data)]
/// struct A;
///
/// impl Compose for A {
/// fn compose(_cx: Scope<Self>) -> impl Compose {
/// let _: i32 = "".parse().map_err(Error::new)?;
///
/// Ok(())
/// }
/// }
///
/// #[derive(Data)]
/// struct App;
///
/// impl Compose for App {
/// fn compose(_cx: Scope<Self>) -> impl Compose {
/// catch(
/// |error| {
/// dbg!(error);
/// },
/// A,
/// )
/// }
/// }
/// ```
pub fn catch<'a, C: Compose>(
on_error: impl Fn(Box<dyn core::error::Error>) + 'a,
content: C,
) -> Catch<'a, C> {
Catch {
content,
f: Rc::new(on_error),
}
}
/// Error catch composable.
///
/// See [`catch`] for more.
#[derive(Clone, Data)]
#[actuate(path = "crate")]
pub struct Catch<'a, C> {
/// Content of this composable.
content: C,
/// Function to handle errors.
f: Rc<dyn Fn(Box<dyn core::error::Error>) + 'a>,
}
impl<C: Compose> Compose for Catch<'_, C> {
fn compose(cx: Scope<Self>) -> impl Compose {
let f: &dyn Fn(Box<dyn core::error::Error>) = &*cx.me().f;
// Cast this function to the `'static` lifetime.
// Safety: This function has a lifetime of `'a`, which is guaranteed to outlive this composables descendants.
let f: Rc<dyn Fn(Box<dyn core::error::Error>)> = unsafe { mem::transmute(f) };
use_provider(&cx, move || CatchContext { f: f.clone() });
// Safety: The content of this composable is only returned into the composition once.
unsafe { Signal::map_unchecked(cx.me(), |me| &me.content) }
}
}
================================================
FILE: src/compose/dyn_compose.rs
================================================
use super::{drop_node, AnyCompose, Node, Runtime};
use crate::{compose::Compose, use_ref, Scope, ScopeData};
use alloc::rc::Rc;
use core::{
any::TypeId,
cell::{Cell, RefCell, UnsafeCell},
mem,
};
use slotmap::DefaultKey;
/// Create a new dynamically-typed composable.
///
/// # Examples
///
/// ```
/// use actuate::prelude::*;
///
/// #[derive(Data)]
/// struct A;
///
/// impl Compose for A {
/// fn compose(_cx: Scope<Self>) -> impl Compose {
/// dbg!("A");
/// }
/// }
///
/// #[derive(Data)]
/// struct B;
///
/// impl Compose for B {
/// fn compose(_cx: Scope<Self>) -> impl Compose {
/// dbg!("B");
/// }
/// }
///
/// #[derive(Data)]
/// struct App;
///
/// impl Compose for App {
/// fn compose(cx: Scope<Self>) -> impl Compose {
/// let count = use_mut(&cx, || 0);
///
/// SignalMut::update(count, |x| *x += 1);
///
/// if *count == 0 {
/// dyn_compose(A)
/// } else {
/// dyn_compose(B)
/// }
/// }
/// }
/// ```
pub fn dyn_compose<'a>(content: impl Compose + 'a) -> DynCompose<'a> {
DynCompose {
compose: UnsafeCell::new(Some(Box::new(content))),
}
}
/// Dynamically-typed composable.
#[must_use = "Composables do nothing unless composed or returned from other composables."]
pub struct DynCompose<'a> {
compose: UnsafeCell<Option<Box<dyn AnyCompose + 'a>>>,
}
#[derive(Clone, Copy)]
struct DynComposeState {
key: DefaultKey,
data_id: TypeId,
}
impl Compose for DynCompose<'_> {
fn compose(cx: Scope<Self>) -> impl Compose {
let state: &Cell<Option<DynComposeState>> = use_ref(&cx, || Cell::new(None));
let rt = Runtime::current();
if let Some(state) = state.get() {
let compose: &mut dyn AnyCompose = unsafe { &mut *cx.me().compose.get() }
.as_deref_mut()
.unwrap();
let mut compose: Box<dyn AnyCompose> = unsafe { mem::transmute(compose) };
let data_id = compose.data_id();
if data_id == state.data_id {
{
let nodes = rt.nodes.borrow();
let mut last = nodes[state.key].compose.borrow_mut();
unsafe { compose.reborrow(last.as_ptr_mut()) };
}
rt.queue(state.key)
} else {
let mut nodes = rt.nodes.borrow_mut();
drop_node(&mut nodes, state.key);
}
}
let Some(compose) = unsafe { &mut *cx.me().compose.get() }.take() else {
if let Some(state) = state.get() {
rt.queue(state.key)
}
return;
};
let compose: Box<dyn AnyCompose> = unsafe { mem::transmute(compose) };
let data_id = compose.data_id();
let mut nodes = rt.nodes.borrow_mut();
let key = nodes.insert(Rc::new(Node {
compose: RefCell::new(crate::composer::ComposePtr::Boxed(compose)),
scope: ScopeData::default(),
parent: Some(rt.current_key.get()),
children: RefCell::new(Vec::new()),
child_idx: 0,
}));
state.set(Some(DynComposeState { key, data_id }));
nodes
.get(rt.current_key.get())
.unwrap()
.children
.borrow_mut()
.push(key);
let child_state = &nodes[key].scope;
*child_state.contexts.borrow_mut() = cx.contexts.borrow().clone();
child_state
.contexts
.borrow_mut()
.values
.extend(cx.child_contexts.borrow().values.clone());
drop(nodes);
rt.queue(key);
}
}
================================================
FILE: src/compose/from_fn.rs
================================================
use crate::{compose::Compose, Data, Scope, ScopeState};
use core::marker::PhantomData;
/// Create a composable from a function.
///
/// This will create a composable from a function that takes a [`ScopeState`] and returns some composable content.
///
/// # Examples
///
/// ```
/// use actuate::prelude::*;
///
/// #[derive(Data)]
/// struct User {
/// id: i32,
/// }
///
/// impl Compose for User {
/// fn compose(cx: Scope<Self>) -> impl Compose {}
/// }
///
/// #[derive(Data)]
/// struct App;
///
/// impl Compose for App {
/// fn compose(cx: Scope<Self>) -> impl Compose {
/// compose::from_fn(|_cx| {
/// User { id: 0 }
/// })
/// }
/// }
/// ```
pub fn from_fn<F, C>(f: F) -> FromFn<F, C>
where
F: Fn(ScopeState) -> C,
C: Compose,
{
FromFn {
f,
_marker: PhantomData,
}
}
/// Function composable.
///
/// For more see [`from_fn`].
pub struct FromFn<F, C> {
f: F,
_marker: PhantomData<C>,
}
impl<F: Clone, C> Clone for FromFn<F, C> {
fn clone(&self) -> Self {
Self {
f: self.f.clone(),
_marker: PhantomData,
}
}
}
unsafe impl<F, C> Data for FromFn<F, C>
where
F: Fn(ScopeState) -> C,
C: Compose,
{
}
impl<F, C> Compose for FromFn<F, C>
where
F: Fn(ScopeState) -> C,
C: Compose,
{
fn compose(cx: Scope<Self>) -> impl Compose {
(cx.me().f)(&cx)
}
}
================================================
FILE: src/compose/from_iter.rs
================================================
use super::{AnyCompose, Node, Runtime};
use crate::{compose::Compose, data::Data, use_ref, Scope, ScopeData, Signal};
use alloc::rc::Rc;
use core::{cell::RefCell, mem};
use slotmap::DefaultKey;
/// Create a composable from an iterator.
///
/// `make_item` will be called for each item to produce a composable.
///
/// # Examples
///
/// ```
/// use actuate::prelude::*;
///
/// #[derive(Data)]
/// struct User {
/// id: i32,
/// }
///
/// impl Compose for User {
/// fn compose(cx: Scope<Self>) -> impl Compose {}
/// }
///
/// #[derive(Data)]
/// struct App;
///
/// impl Compose for App {
/// fn compose(cx: Scope<Self>) -> impl Compose {
/// compose::from_iter(0..10, |id| {
/// User { id: *id }
/// })
/// }
/// }
/// ```
pub fn from_iter<'a, I, C>(
iter: I,
make_item: impl Fn(Signal<'a, I::Item>) -> C + 'a,
) -> FromIter<'a, I, I::Item, C>
where
I: IntoIterator + Clone + Data,
I::Item: 'static,
C: Compose,
{
FromIter {
iter,
make_item: Rc::new(make_item),
}
}
/// Composable from an iterator.
///
/// For more see [`from_iter`].
#[must_use = "Composables do nothing unless composed or returned from other composables."]
pub struct FromIter<'a, I, Item, C> {
iter: I,
make_item: Rc<dyn Fn(Signal<'a, Item>) -> C + 'a>,
}
impl<I, Item, C> Clone for FromIter<'_, I, Item, C>
where
I: Clone,
C: Clone,
{
fn clone(&self) -> Self {
Self {
iter: self.iter.clone(),
make_item: self.make_item.clone(),
}
}
}
unsafe impl<I, Item, C> Data for FromIter<'_, I, Item, C>
where
I: Data,
Item: 'static,
C: Data,
{
}
impl<I, Item, C> Compose for FromIter<'_, I, Item, C>
where
I: IntoIterator<Item = Item> + Clone + Data,
Item: 'static,
C: Compose,
{
fn compose(cx: Scope<Self>) -> impl Compose {
let states: &RefCell<Vec<ItemState<Item>>> = use_ref(&cx, || RefCell::new(Vec::new()));
let mut states = states.borrow_mut();
let mut items: Vec<Option<_>> = cx.me().iter.clone().into_iter().map(Some).collect();
let rt = Runtime::current();
if items.len() >= states.len() {
for item in &mut items[states.len()..] {
let item = item.take().unwrap();
let state = ItemState { item, key: None };
states.push(state);
}
} else {
states.truncate(items.len());
}
for (idx, state) in states.iter_mut().enumerate() {
let mut nodes = rt.nodes.borrow_mut();
if state.key.is_none() {
let item_ref: &Item = &state.item;
let item_ref: &Item = unsafe { mem::transmute(item_ref) };
let compose = (cx.me().make_item)(Signal {
value: item_ref,
generation: &cx.generation as _,
});
let any_compose: Box<dyn AnyCompose> = Box::new(compose);
let any_compose: Box<dyn AnyCompose> = unsafe { mem::transmute(any_compose) };
let key = nodes.insert(Rc::new(Node {
compose: RefCell::new(crate::composer::ComposePtr::Boxed(any_compose)),
scope: ScopeData::default(),
parent: Some(rt.current_key.get()),
children: RefCell::new(Vec::new()),
child_idx: idx,
}));
nodes
.get(rt.current_key.get())
.unwrap()
.children
.borrow_mut()
.push(key);
state.key = Some(key);
}
let node = nodes.get(state.key.unwrap()).unwrap().clone();
*node.scope.contexts.borrow_mut() = cx.contexts.borrow().clone();
node.scope
.contexts
.borrow_mut()
.values
.extend(cx.child_contexts.borrow().values.clone());
drop(nodes);
rt.queue(state.key.unwrap());
}
}
}
struct ItemState<T> {
item: T,
key: Option<DefaultKey>,
}
================================================
FILE: src/compose/memo.rs
================================================
use super::{use_node, AnyCompose, Runtime};
use crate::{compose::Compose, composer::ComposePtr, data::Data, use_ref, Scope};
use alloc::borrow::Cow;
use core::{cell::RefCell, mem};
/// Create a new memoized composable.
///
/// The content of the memoized composable is only re-composed when the dependency changes.
///
/// Children of this `Memo` may still be re-composed if their state has changed.
pub fn memo<D, C>(dependency: D, content: C) -> Memo<D, C>
where
D: Data + Clone + PartialEq + 'static,
C: Compose,
{
Memo {
dependency,
content,
}
}
/// Memoized composable.
///
/// See [`memo`] for more.
#[derive(Clone, Data)]
#[actuate(path = "crate")]
#[must_use = "Composables do nothing unless composed or returned from other composables."]
pub struct Memo<T, C> {
dependency: T,
content: C,
}
impl<T, C> Compose for Memo<T, C>
where
T: Clone + Data + PartialEq + 'static,
C: Compose,
{
fn compose(cx: Scope<Self>) -> impl Compose {
let rt = Runtime::current();
let ptr: *const dyn AnyCompose =
unsafe { mem::transmute(&cx.me().content as *const dyn AnyCompose) };
let (key, _) = use_node(&cx, ComposePtr::Ptr(ptr), 0);
let last = use_ref(&cx, RefCell::default);
let mut last = last.borrow_mut();
if let Some(last) = &mut *last {
if cx.me().dependency != *last {
*last = cx.me().dependency.clone();
rt.queue(key);
}
} else {
*last = Some(cx.me().dependency.clone());
rt.queue(key);
}
}
fn name() -> Option<Cow<'static, str>> {
Some(
C::name()
.map(|name| format!("Memo<{}>", name).into())
.unwrap_or("Memo".into()),
)
}
}
================================================
FILE: src/compose/mod.rs
================================================
use crate::{
composer::{ComposePtr, Node, Runtime},
data::Data,
use_context, use_ref, Scope, ScopeData, ScopeState,
};
use alloc::borrow::Cow;
use alloc::rc::Rc;
use core::{
any::TypeId,
cell::{Cell, RefCell, UnsafeCell},
fmt, mem,
};
use slotmap::{DefaultKey, SlotMap};
mod catch;
pub use self::catch::{catch, Catch};
mod dyn_compose;
pub use self::dyn_compose::{dyn_compose, DynCompose};
mod from_fn;
pub use self::from_fn::{from_fn, FromFn};
mod from_iter;
pub use self::from_iter::{from_iter, FromIter};
mod memo;
pub use self::memo::{memo, Memo};
/// A composable function.
///
/// For a dynamically-typed composable, see [`DynCompose`].
///
/// Composables are the building blocks of reactivity in Actuate.
/// A composable is essentially a function that is re-run whenever its state (or its parent state) is changed.
/// Composables may return one or more children, that run after their parent.
///
/// When a composable is re-run, we call that "recomposition".
/// For example, on the initial composition, hooks may initialize their state.
/// Then on recomposition, hooks update their state from the last set value.
///
/// Triggering a state update will recompose each parent, and then each child,
/// until either a [`Memo`] is reached or the composition is complete.
///
/// [`Memo`] is special in that it will only recompose in two cases:
/// 1. It's provided dependencies have changed (see [`memo()`] for more)
/// 2. Its own state has changed, which will then trigger the above parent-to-child process for its children.
#[must_use = "Composables do nothing unless composed or returned from other composables."]
pub trait Compose: Data {
/// Compose this function.
fn compose(cx: Scope<Self>) -> impl Compose;
#[doc(hidden)]
fn name() -> Option<Cow<'static, str>> {
let name = core::any::type_name::<Self>();
Some(
name.split('<')
.next()
.unwrap_or(name)
.split("::")
.last()
.unwrap_or(name)
.into(),
)
}
}
impl Compose for () {
fn compose(cx: Scope<Self>) -> impl Compose {
let _ = cx;
}
}
impl<C: Compose> Compose for Option<C> {
fn compose(cx: Scope<Self>) -> impl Compose {
let child_key = use_ref(&cx, || Cell::new(None));
let rt = Runtime::current();
let mut nodes = rt.nodes.borrow_mut();
if let Some(content) = &*cx.me() {
if let Some(key) = child_key.get() {
let last = nodes.get_mut(key).unwrap();
let ptr = content as *const dyn AnyCompose;
let ptr: *const dyn AnyCompose = unsafe { mem::transmute(ptr) };
*last.compose.borrow_mut() = ComposePtr::Ptr(ptr);
drop(nodes);
rt.queue(key);
} else {
let ptr: *const dyn AnyCompose =
unsafe { mem::transmute(content as *const dyn AnyCompose) };
let key = nodes.insert(Rc::new(Node {
compose: RefCell::new(crate::composer::ComposePtr::Ptr(ptr)),
scope: ScopeData::default(),
parent: Some(rt.current_key.get()),
children: RefCell::new(Vec::new()),
child_idx: 0,
}));
child_key.set(Some(key));
nodes
.get(rt.current_key.get())
.unwrap()
.children
.borrow_mut()
.push(key);
let child_state = &nodes[key].scope;
*child_state.contexts.borrow_mut() = cx.contexts.borrow().clone();
child_state
.contexts
.borrow_mut()
.values
.extend(cx.child_contexts.borrow().values.clone());
drop(nodes);
rt.queue(key);
}
} else if let Some(key) = child_key.get() {
child_key.set(None);
drop_node(&mut nodes, key);
}
}
}
// TODO replace with non-recursive algorithm.
fn drop_node(nodes: &mut SlotMap<DefaultKey, Rc<Node>>, key: DefaultKey) {
let node = nodes[key].clone();
if let Some(parent) = node.parent {
let parent = nodes.get_mut(parent).unwrap();
parent.children.borrow_mut().retain(|&x| x != key);
}
let children = node.children.borrow().clone();
for key in children {
drop_node(nodes, key)
}
nodes.remove(key);
}
/// Composable error.
///
/// This can be handled by a parent composable with [`Catch`].
#[derive(Data, thiserror::Error)]
#[actuate(path = "crate")]
pub struct Error {
make_error: Box<dyn Fn() -> Box<dyn core::error::Error>>,
}
impl Error {
/// Create a new composable error.
pub fn new(error: impl core::error::Error + Clone + 'static) -> Self {
Self {
make_error: Box::new(move || Box::new(error.clone())),
}
}
}
impl fmt::Debug for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
(self.make_error)().fmt(f)
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
(self.make_error)().fmt(f)
}
}
impl<C: Compose> Compose for Result<C, Error> {
fn compose(cx: Scope<Self>) -> impl Compose {
let catch_cx = use_context::<CatchContext>(&cx).unwrap();
let child_key = use_ref(&cx, || Cell::new(None));
let rt = Runtime::current();
match &*cx.me() {
Ok(content) => {
if let Some(key) = child_key.get() {
let mut nodes = rt.nodes.borrow_mut();
let last = nodes.get_mut(key).unwrap();
let ptr = content as *const dyn AnyCompose;
let ptr: *const dyn AnyCompose = unsafe { mem::transmute(ptr) };
*last.compose.borrow_mut() = ComposePtr::Ptr(ptr);
drop(nodes);
rt.queue(key);
} else {
let mut nodes = rt.nodes.borrow_mut();
let ptr: *const dyn AnyCompose =
unsafe { mem::transmute(content as *const dyn AnyCompose) };
let key = nodes.insert(Rc::new(Node {
compose: RefCell::new(crate::composer::ComposePtr::Ptr(ptr)),
scope: ScopeData::default(),
parent: Some(rt.current_key.get()),
children: RefCell::new(Vec::new()),
child_idx: 0,
}));
child_key.set(Some(key));
nodes
.get(rt.current_key.get())
.unwrap()
.children
.borrow_mut()
.push(key);
let child_state = &nodes[key].scope;
*child_state.contexts.borrow_mut() = cx.contexts.borrow().clone();
child_state
.contexts
.borrow_mut()
.values
.extend(cx.child_contexts.borrow().values.clone());
drop(nodes);
rt.queue(key);
}
}
Err(error) => {
let mut nodes = rt.nodes.borrow_mut();
if let Some(key) = child_key.get() {
drop_node(&mut nodes, key);
}
(catch_cx.f)((error.make_error)())
}
}
}
}
pub(crate) struct CatchContext {
f: Rc<dyn Fn(Box<dyn core::error::Error>)>,
}
impl CatchContext {
pub(crate) fn new(f: impl Fn(Box<dyn core::error::Error>) + 'static) -> Self {
Self { f: Rc::new(f) }
}
}
macro_rules! impl_tuples {
($($t:tt : $idx:tt),*) => {
unsafe impl<$($t: Data),*> Data for ($($t,)*) {}
impl<$($t: Compose),*> Compose for ($($t,)*) {
fn compose(cx: Scope<Self>) -> impl Compose {
$({
let ptr: *const dyn AnyCompose = unsafe { mem::transmute(&cx.me().$idx as *const dyn AnyCompose) };
let (key, _) = use_node(&cx, ComposePtr::Ptr(ptr), $idx);
let rt = Runtime::current();
rt.queue(key)
})*
}
fn name() -> Option<Cow<'static, str>> {
None
}
}
};
}
impl_tuples!(T1:0);
impl_tuples!(T1:0, T2:1);
impl_tuples!(T1:0, T2:1, T3:2);
impl_tuples!(T1:0, T2:1, T3:2, T4:3);
impl_tuples!(T1:0, T2:1, T3:2, T4:3, T5:4);
impl_tuples!(T1:0, T2:1, T3:2, T4:3, T5:4, T6:5);
impl_tuples!(T1:0, T2:1, T3:2, T4:3, T5:4, T6:5, T7:6);
impl_tuples!(T1:0, T2:1, T3:2, T4:3, T5:4, T6:5, T7:6, T8:7);
impl<C> Compose for Vec<C>
where
C: Compose,
{
fn compose(cx: Scope<Self>) -> impl Compose {
for (idx, item) in cx.me().iter().enumerate() {
let ptr: *const dyn AnyCompose =
unsafe { mem::transmute(item as *const dyn AnyCompose) };
let (key, _) = use_node(&cx, ComposePtr::Ptr(ptr), idx);
let rt = Runtime::current();
rt.queue(key);
}
}
}
fn use_node(
cx: ScopeState<'_>,
compose_ptr: ComposePtr,
child_idx: usize,
) -> (DefaultKey, &Rc<Node>) {
let mut compose_ptr_cell = Some(compose_ptr);
let (key, node) = use_ref(cx, || {
let rt = Runtime::current();
let mut nodes = rt.nodes.borrow_mut();
let key = nodes.insert(Rc::new(Node {
compose: RefCell::new(compose_ptr_cell.take().unwrap()),
scope: ScopeData::default(),
parent: Some(rt.current_key.get()),
children: RefCell::new(Vec::new()),
child_idx,
}));
nodes
.get(rt.current_key.get())
.unwrap()
.children
.borrow_mut()
.push(key);
let child_state = &nodes[key].scope;
*child_state.contexts.borrow_mut() = cx.contexts.borrow().clone();
child_state
.contexts
.borrow_mut()
.values
.extend(cx.child_contexts.borrow().values.clone());
(key, nodes[key].clone())
});
// Reborrow the pointer to the node's composable.
if let Some(compose_ptr) = compose_ptr_cell.take() {
*node.compose.borrow_mut() = compose_ptr;
}
(*key, node)
}
pub(crate) trait AnyCompose {
fn data_id(&self) -> TypeId;
fn as_ptr_mut(&mut self) -> *mut ();
unsafe fn reborrow(&mut self, ptr: *mut ());
/// Safety: The caller must ensure `&self` is valid for the lifetime of `state`.
unsafe fn any_compose(&self, state: &ScopeData);
fn name(&self) -> Option<Cow<'static, str>>;
}
impl<C> AnyCompose for C
where
C: Compose + Data,
{
fn data_id(&self) -> TypeId {
typeid::of::<C>()
}
fn as_ptr_mut(&mut self) -> *mut () {
self as *mut Self as *mut ()
}
unsafe fn reborrow(&mut self, ptr: *mut ()) {
core::ptr::swap(self, ptr as _);
}
unsafe fn any_compose(&self, state: &ScopeData) {
// Reset the hook index.
state.hook_idx.set(0);
// Increment the scope's current generation.
state.generation.set(state.generation.get() + 1);
// Transmute the lifetime of `&Self`, `&ScopeData`, and the `Scope` containing both to the same`'a`.
// Safety: `self` and `state` are guranteed to have the same lifetime..
let state: ScopeState = unsafe { mem::transmute(state) };
let cx: Scope<'_, C> = Scope { me: self, state };
let cx: Scope<'_, C> = unsafe { mem::transmute(cx) };
// Cell for the Box used to re-allocate this composable.
let cell: &UnsafeCell<Option<Box<dyn AnyCompose>>> = use_ref(&cx, || UnsafeCell::new(None));
// Safety: This cell is only accessed by this composable.
let cell = unsafe { &mut *cell.get() };
let child_key_cell = use_ref(&cx, || Cell::new(None));
let rt = Runtime::current();
if cell.is_none() {
#[cfg(feature = "tracing")]
if let Some(name) = C::name() {
tracing::trace!("Compose: {}", name);
}
let child = C::compose(cx);
if child.data_id() == typeid::of::<()>() {
return;
}
let child: Box<dyn AnyCompose> = Box::new(child);
let mut child: Box<dyn AnyCompose> = unsafe { mem::transmute(child) };
let mut nodes = rt.nodes.borrow_mut();
unsafe {
if let Some(key) = child_key_cell.get() {
let last = nodes.get_mut(key).unwrap();
child.reborrow(last.compose.borrow_mut().as_ptr_mut());
} else {
let child_key = nodes.insert(Rc::new(Node {
compose: RefCell::new(crate::composer::ComposePtr::Boxed(child)),
scope: ScopeData::default(),
parent: Some(rt.current_key.get()),
children: RefCell::new(Vec::new()),
child_idx: 0,
}));
child_key_cell.set(Some(child_key));
nodes
.get(rt.current_key.get())
.unwrap()
.children
.borrow_mut()
.push(child_key);
let child_state = &nodes[child_key].scope;
*child_state.contexts.borrow_mut() = cx.contexts.borrow().clone();
child_state
.contexts
.borrow_mut()
.values
.extend(cx.child_contexts.borrow().values.clone());
}
}
}
if let Some(key) = child_key_cell.get() {
rt.queue(key)
}
}
fn name(&self) -> Option<Cow<'static, str>> {
C::name()
}
}
================================================
FILE: src/composer.rs
================================================
use crate::{
compose::{AnyCompose, CatchContext, Compose},
ScopeData,
};
use alloc::{collections::BTreeSet, rc::Rc, sync::Arc, task::Wake};
use core::{
any::TypeId,
cell::{Cell, RefCell},
cmp::Ordering,
error::Error,
fmt,
future::Future,
mem,
pin::Pin,
task::{Context, Poll, Waker},
};
use crossbeam_queue::SegQueue;
use slotmap::{DefaultKey, SlotMap};
#[cfg(feature = "executor")]
use tokio::sync::RwLock;
type RuntimeFuture = Pin<Box<dyn Future<Output = ()>>>;
pub(crate) enum ComposePtr {
Boxed(Box<dyn AnyCompose>),
Ptr(*const dyn AnyCompose),
}
impl AnyCompose for ComposePtr {
fn data_id(&self) -> TypeId {
match self {
ComposePtr::Boxed(compose) => compose.data_id(),
ComposePtr::Ptr(ptr) => unsafe { (**ptr).data_id() },
}
}
fn as_ptr_mut(&mut self) -> *mut () {
match self {
ComposePtr::Boxed(compose) => compose.as_ptr_mut(),
ComposePtr::Ptr(ptr) => *ptr as *mut (),
}
}
unsafe fn reborrow(&mut self, ptr: *mut ()) {
match self {
ComposePtr::Boxed(compose) => compose.reborrow(ptr),
ComposePtr::Ptr(_) => {}
}
}
unsafe fn any_compose(&self, state: &ScopeData) {
match self {
ComposePtr::Boxed(compose) => compose.any_compose(state),
ComposePtr::Ptr(ptr) => (**ptr).any_compose(state),
}
}
fn name(&self) -> Option<std::borrow::Cow<'static, str>> {
match self {
ComposePtr::Boxed(compose) => compose.name(),
ComposePtr::Ptr(ptr) => unsafe { (**ptr).name() },
}
}
}
// Safety: `scope` must be dropped before `compose`.
pub(crate) struct Node {
pub(crate) compose: RefCell<ComposePtr>,
pub(crate) scope: ScopeData<'static>,
pub(crate) parent: Option<DefaultKey>,
pub(crate) children: RefCell<Vec<DefaultKey>>,
pub(crate) child_idx: usize,
}
/// Runtime for a [`Composer`].
#[derive(Clone)]
pub(crate) struct Runtime {
/// Local task stored on this runtime.
pub(crate) tasks: Rc<RefCell<SlotMap<DefaultKey, RuntimeFuture>>>,
/// Queue for ready local tasks.
pub(crate) task_queue: Arc<SegQueue<DefaultKey>>,
/// Queue for updates that mutate the composition tree.
pub(crate) update_queue: Rc<SegQueue<Box<dyn FnMut()>>>,
#[cfg(feature = "executor")]
/// Update lock for shared tasks.
pub(crate) lock: Arc<RwLock<()>>,
pub(crate) waker: RefCell<Option<Waker>>,
pub(crate) nodes: Rc<RefCell<SlotMap<DefaultKey, Rc<Node>>>>,
pub(crate) current_key: Rc<Cell<DefaultKey>>,
pub(crate) root: DefaultKey,
pub(crate) pending: Rc<RefCell<BTreeSet<Pending>>>,
}
impl Runtime {
/// Get the current [`Runtime`].
///
/// # Panics
/// Panics if called outside of a runtime.
pub fn current() -> Self {
RUNTIME.with(|runtime| {
runtime
.borrow()
.as_ref()
.expect("Runtime::current() called outside of a runtime")
.clone()
})
}
/// Enter this runtime, making it available to [`Runtime::current`].
pub fn enter(&self) {
RUNTIME.with(|runtime| {
*runtime.borrow_mut() = Some(self.clone());
});
}
/// Queue an update to run after [`Composer::compose`].
pub fn update(&self, f: impl FnOnce() + Send + 'static) {
let mut f_cell = Some(f);
#[cfg(feature = "executor")]
let lock = self.lock.clone();
self.update_queue.push(Box::new(move || {
#[cfg(feature = "executor")]
let _guard = lock.blocking_write();
let f = f_cell.take().unwrap();
f()
}));
if let Some(waker) = &*self.waker.borrow() {
waker.wake_by_ref();
}
}
pub fn pending(&self, key: DefaultKey) -> Pending {
let nodes = self.nodes.borrow();
let node = nodes[key].clone();
let mut indices = vec![node.child_idx];
let mut parent = node.parent;
while let Some(key) = parent {
indices.push(nodes.get(key).unwrap().child_idx);
parent = nodes.get(key).unwrap().parent;
}
indices.reverse();
Pending { key, indices }
}
pub fn queue(&self, key: DefaultKey) {
let pending = self.pending(key);
self.pending.borrow_mut().insert(pending);
}
}
thread_local! {
static RUNTIME: RefCell<Option<Runtime>> = const { RefCell::new(None) };
}
struct TaskWaker {
key: DefaultKey,
queue: Arc<SegQueue<DefaultKey>>,
waker: Option<Waker>,
}
impl Wake for TaskWaker {
fn wake(self: Arc<Self>) {
self.queue.push(self.key);
if let Some(waker) = self.waker.as_ref() {
waker.wake_by_ref();
}
}
}
/// Error for [`Composer::try_compose`].
#[derive(Debug)]
pub enum TryComposeError {
/// No updates are ready to be applied.
Pending,
/// An error occurred during composition.
Error(Box<dyn Error>),
}
impl PartialEq for TryComposeError {
fn eq(&self, other: &Self) -> bool {
mem::discriminant(self) == mem::discriminant(other)
}
}
#[derive(Clone, PartialEq, Eq)]
pub(crate) struct Pending {
pub(crate) key: DefaultKey,
pub(crate) indices: Vec<usize>,
}
impl PartialOrd for Pending {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Pending {
fn cmp(&self, other: &Self) -> Ordering {
for (a, b) in self.indices.iter().zip(other.indices.iter()) {
match a.cmp(b) {
Ordering::Equal => {}
x => return x,
}
}
self.indices.len().cmp(&other.indices.len())
}
}
/// Composer for composable content.
///
/// ```
/// use actuate::prelude::*;
/// use actuate::composer::Composer;
///
/// #[derive(Data)]
/// struct A;
///
/// impl Compose for A {
/// fn compose(cx: Scope<Self>) -> impl Compose {
/// (B, C)
/// }
/// }
///
/// #[derive(Data)]
/// struct B;
///
/// impl Compose for B {
/// fn compose(cx: Scope<Self>) -> impl Compose {}
/// }
///
/// #[derive(Data)]
/// struct C;
///
/// impl Compose for C {
/// fn compose(cx: Scope<Self>) -> impl Compose {}
/// }
///
/// let mut composer = Composer::new(A);
/// composer.try_compose().unwrap();
///
/// assert_eq!(format!("{:?}", composer), "Composer(A(B, C))")
/// ```
pub struct Composer {
rt: Runtime,
task_queue: Arc<SegQueue<DefaultKey>>,
update_queue: Rc<SegQueue<Box<dyn FnMut()>>>,
is_initial: bool,
}
impl Composer {
/// Create a new [`Composer`] with the given content, updater, and task executor.
pub fn new(content: impl Compose + 'static) -> Self {
#[cfg(feature = "executor")]
let lock = Arc::new(RwLock::new(()));
let task_queue = Arc::new(SegQueue::new());
let update_queue = Rc::new(SegQueue::new());
let mut nodes = SlotMap::new();
let root_key = nodes.insert(Rc::new(Node {
compose: RefCell::new(ComposePtr::Boxed(Box::new(content))),
scope: ScopeData::default(),
parent: None,
children: RefCell::new(Vec::new()),
child_idx: 0,
}));
Self {
rt: Runtime {
tasks: Rc::new(RefCell::new(SlotMap::new())),
task_queue: task_queue.clone(),
update_queue: update_queue.clone(),
waker: RefCell::new(None),
#[cfg(feature = "executor")]
lock,
nodes: Rc::new(RefCell::new(nodes)),
current_key: Rc::new(Cell::new(root_key)),
root: root_key,
pending: Rc::new(RefCell::new(BTreeSet::new())),
},
task_queue,
update_queue,
is_initial: true,
}
}
/// Try to immediately compose the content in this composer.
pub fn try_compose(&mut self) -> Result<(), TryComposeError> {
let mut is_pending = true;
for res in self.by_ref() {
res.map_err(TryComposeError::Error)?;
is_pending = false;
}
if is_pending {
Err(TryComposeError::Pending)
} else {
Ok(())
}
}
/// Poll a composition of the content in this composer.
pub fn poll_compose(&mut self, cx: &mut Context) -> Poll<Result<(), Box<dyn Error>>> {
*self.rt.waker.borrow_mut() = Some(cx.waker().clone());
match self.try_compose() {
Ok(()) => Poll::Ready(Ok(())),
Err(TryComposeError::Pending) => Poll::Pending,
Err(TryComposeError::Error(error)) => Poll::Ready(Err(error)),
}
}
/// Compose the content of this composer.
pub async fn compose(&mut self) -> Result<(), Box<dyn Error>> {
futures::future::poll_fn(|cx| self.poll_compose(cx)).await
}
}
impl Drop for Composer {
fn drop(&mut self) {
let node = self.rt.nodes.borrow()[self.rt.root].clone();
drop_recursive(&self.rt, self.rt.root, node)
}
}
fn drop_recursive(rt: &Runtime, key: DefaultKey, node: Rc<Node>) {
let children = node.children.borrow().clone();
for child_key in children {
let child = rt.nodes.borrow()[child_key].clone();
drop_recursive(rt, child_key, child)
}
rt.nodes.borrow_mut().remove(key);
}
impl Iterator for Composer {
type Item = Result<(), Box<dyn Error>>;
fn next(&mut self) -> Option<Self::Item> {
self.rt.enter();
let error_cell = Rc::new(Cell::new(None));
let error_cell_handle = error_cell.clone();
let root = self.rt.nodes.borrow().get(self.rt.root).unwrap().clone();
root.scope.contexts.borrow_mut().values.insert(
TypeId::of::<CatchContext>(),
Rc::new(CatchContext::new(move |error| {
error_cell_handle.set(Some(error));
})),
);
if !self.is_initial {
let key_cell = self.rt.pending.borrow_mut().pop_first();
if let Some(pending) = key_cell {
self.rt.current_key.set(pending.key);
let node = self.rt.nodes.borrow().get(pending.key).unwrap().clone();
// Safety: `self.compose` is guaranteed to live as long as `self.scope_state`.
unsafe { node.compose.borrow().any_compose(&node.scope) };
} else {
while let Some(key) = self.task_queue.pop() {
let waker = Waker::from(Arc::new(TaskWaker {
key,
waker: self.rt.waker.borrow().clone(),
queue: self.rt.task_queue.clone(),
}));
let mut cx = Context::from_waker(&waker);
let mut tasks = self.rt.tasks.borrow_mut();
let task = tasks.get_mut(key).unwrap();
let _ = task.as_mut().poll(&mut cx);
}
while let Some(mut update) = self.update_queue.pop() {
update();
}
return None;
}
} else {
self.is_initial = false;
self.rt.current_key.set(self.rt.root);
// Safety: `self.compose` is guaranteed to live as long as `self.scope_state`.
unsafe { root.compose.borrow().any_compose(&root.scope) };
}
Some(error_cell.take().map(Err).unwrap_or(Ok(())))
}
}
impl fmt::Debug for Composer {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut dbg_tuple = f.debug_tuple("Composer");
dbg_composer(&mut dbg_tuple, &self.rt.nodes.borrow(), self.rt.root);
dbg_tuple.finish()
}
}
struct Field<'a> {
name: &'a str,
nodes: &'a SlotMap<DefaultKey, Rc<Node>>,
children: &'a [DefaultKey],
}
impl fmt::Debug for Field<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut dbg_tuple = f.debug_tuple(self.name);
for child_key in self.children {
dbg_composer(&mut dbg_tuple, self.nodes, *child_key);
}
dbg_tuple.finish()
}
}
fn dbg_composer(
dbg_tuple: &mut fmt::DebugTuple,
nodes: &SlotMap<DefaultKey, Rc<Node>>,
key: DefaultKey,
) {
let node = &nodes[key];
if let Some(name) = node.compose.borrow().name() {
dbg_tuple.field(&Field {
name: &name,
nodes,
children: &node.children.borrow(),
});
} else {
for child_key in &*node.children.borrow() {
dbg_composer(dbg_tuple, nodes, *child_key);
}
}
}
================================================
FILE: src/data.rs
================================================
//! Data trait and macros.
//!
//! # Data
//!
//! [`Data`] is a trait that enforces pinned references to compososition state.
//!
//! The `#[derive(Data)]` macro can be used to derive the [`Data`] trait for a struct.
//! This requires the struct's fields either:
//! - Implement the [`Data`] trait.
//! - Are `'static`.
//! - Are functions that take `'static` arguments and return a type that implements the [`Data`] trait.
//!
//! # Trait objects
//!
//! Trait objects can also borrow from state:
//!
//! ```no_run
//! use actuate::prelude::*;
//!
//! #[data]
//! trait MyTrait: Data {
//! fn run(&self);
//! }
//!
//! #[derive(Data)]
//! struct A<'a> {
//! my_trait: Box<dyn MyTrait + 'a>,
//! }
//!
//! impl Compose for A<'_> {
//! fn compose(cx: Scope<Self>) -> impl Compose {
//! cx.me().my_trait.run();
//! }
//! }
//!
//! #[derive(Data)]
//! struct X;
//!
//! impl MyTrait for X {
//! fn run(&self) {
//! dbg!("X");
//! }
//! }
//!
//! #[derive(Data)]
//! struct App;
//!
//! impl Compose for App {
//! fn compose(_cx: Scope<Self>) -> impl Compose {
//! A {
//! my_trait: Box::new(X),
//! }
//! }
//! }
//! ```
use crate::{compose::DynCompose, HashMap};
use core::{error::Error, future::Future, ops::Range, pin::Pin};
pub use actuate_macros::{data, Data};
/// Composable data.
///
/// In most cases, this trait should be derived with `#[derive(Data)]`.
/// For more information, see the [module-level documentation](crate::data).
///
/// # Safety
/// This struct must ensure the lifetime of the data it holds cannot escape while composing children.
///
/// For example, a `RefCell<&'a T>` is unsafe because the compiler will infer the lifetime of a child composable's lifetime (e.g. `'a`)
/// as this struct's lifetime (e.g. `'a`).
pub unsafe trait Data {}
macro_rules! impl_data_for_std {
($($t:ty),*) => {
$(
unsafe impl Data for $t {}
)*
}
}
impl_data_for_std!(
(),
bool,
char,
f32,
f64,
i8,
i16,
i32,
i64,
i128,
isize,
u8,
u16,
u32,
u64,
u128,
usize,
String
);
unsafe impl Data for &str {}
unsafe impl<T: Data> Data for Vec<T> {}
unsafe impl<T: Data, U: Data, S: 'static> Data for HashMap<T, U, S> {}
unsafe impl<T: 'static> Data for &T {}
unsafe impl<T: Data> Data for Option<T> {}
unsafe impl<T: Data, U: Data> Data for Result<T, U> {}
unsafe impl<T: Data> Data for Pin<T> {}
unsafe impl<T: 'static> Data for Range<T> {}
unsafe impl Data for Box<dyn Error> {}
unsafe impl Data for Box<dyn Future<Output = ()> + '_> {}
unsafe impl Data for DynCompose<'_> {}
#[doc(hidden)]
pub struct FieldWrap<T>(pub T);
#[doc(hidden)]
pub unsafe trait FnField<Marker> {
fn check(&self) {
let _ = self;
}
}
macro_rules! impl_data_for_fns {
($($t:tt),*) => {
unsafe impl<$($t: 'static,)* R: Data, F: Fn($($t,)*) -> R> FnField<fn($($t,)*)> for &FieldWrap<F> {}
unsafe impl<$($t: 'static,)* R: Data> FnField<fn($($t,)*)> for &FieldWrap<alloc::rc::Rc<dyn Fn($($t,)*) -> R + '_>> {}
}
}
impl_data_for_fns!();
impl_data_for_fns!(T1);
impl_data_for_fns!(T1, T2);
impl_data_for_fns!(T1, T2, T3);
impl_data_for_fns!(T1, T2, T3, T4);
impl_data_for_fns!(T1, T2, T3, T4, T5);
impl_data_for_fns!(T1, T2, T3, T4, T5, T6);
impl_data_for_fns!(T1, T2, T3, T4, T5, T6, T7);
impl_data_for_fns!(T1, T2, T3, T4, T5, T6, T7, T8);
#[doc(hidden)]
pub unsafe trait DataField {
fn check(&self) {
let _ = self;
}
}
unsafe impl<T: Data> DataField for &FieldWrap<T> {}
#[doc(hidden)]
pub unsafe trait StaticField {
fn check(&self) {
let _ = self;
}
}
unsafe impl<T: 'static> StaticField for &&FieldWrap<T> {}
================================================
FILE: src/ecs/mod.rs
================================================
use crate::{
compose::Compose,
composer::{Composer, Pending},
data::Data,
use_callback, use_drop, use_provider, use_ref, Cow, Scope, ScopeState, Signal,
};
use bevy_app::{App, Plugin};
use bevy_ecs::{
component::{Component, Mutable, StorageType},
entity::Entity,
prelude::*,
system::{SystemParam, SystemParamItem, SystemState},
world::{CommandQueue, World},
};
use bevy_winit::{EventLoopProxy, EventLoopProxyWrapper, WakeUp};
use core::fmt;
use hashbrown::HashMap;
use slotmap::{DefaultKey, SlotMap};
use std::{
cell::{Cell, RefCell},
collections::BTreeSet,
mem, ptr,
rc::Rc,
sync::Arc,
task::{Context, Wake, Waker},
};
#[cfg(feature = "ui")]
use bevy_ui::prelude::*;
#[cfg(feature = "picking")]
use bevy_picking::prelude::*;
mod spawn;
pub use self::spawn::{spawn, Spawn};
macro_rules! impl_trait_for_tuples {
($t:tt) => {
$t!();
$t!(T1);
$t!(T1, T2);
$t!(T1, T2, T3);
$t!(T1, T2, T3, T4);
$t!(T1, T2, T3, T4, T5);
$t!(T1, T2, T3, T4, T5, T6);
$t!(T1, T2, T3, T4, T5, T6, T7);
$t!(T1, T2, T3, T4, T5, T6, T7, T8);
};
}
/// Actuate plugin to run [`Composition`]s.
pub struct ActuatePlugin;
impl Plugin for ActuatePlugin {
fn build(&self, app: &mut App) {
let rt = Runtime {
composers: RefCell::new(HashMap::new()),
};
app.insert_non_send_resource(rt)
.add_systems(bevy_app::prelude::Update, compose);
}
}
type UpdateFn = Box<dyn FnMut(&mut World)>;
type WorldListenerFn = Rc<dyn Fn(&mut World)>;
struct Inner {
world_ptr: *mut World,
listeners: SlotMap<DefaultKey, WorldListenerFn>,
updates: Vec<UpdateFn>,
commands: Rc<RefCell<CommandQueue>>,
}
#[derive(Clone)]
struct RuntimeContext {
inner: Rc<RefCell<Inner>>,
}
impl RuntimeContext {
fn current() -> Self {
RUNTIME_CONTEXT.with(|cell| {
let cell_ref = cell.borrow();
let Some(rt) = cell_ref.as_ref() else {
panic!("Must be called from within a composable.")
};
rt.clone()
})
}
unsafe fn world_mut(&self) -> &'static mut World {
&mut *self.inner.borrow().world_ptr
}
}
thread_local! {
static RUNTIME_CONTEXT: RefCell<Option<RuntimeContext>> = const { RefCell::new(None) };
}
struct RuntimeComposer {
composer: Composer,
}
struct Runtime {
composers: RefCell<HashMap<Entity, RuntimeComposer>>,
}
/// Composition of some composable content.
pub struct Composition<C> {
content: Option<C>,
target: Option<Entity>,
}
impl<C> Composition<C>
where
C: Compose + Send + Sync + 'static,
{
/// Create a new composition from its content.
pub fn new(content: C) -> Self {
Self {
content: Some(content),
target: None,
}
}
/// Get the target entity to spawn the composition into.
///
/// If `None`, this will use the composition's parent (if any).
pub fn target(&self) -> Option<Entity> {
self.target
}
/// Set the target entity to spawn the composition into.
///
/// If `None`, this will use the composition's parent (if any).
pub fn set_target(&mut self, target: Option<Entity>) {
self.target = target;
}
/// Set the target entity to spawn the composition into.
///
/// If `None`, this will use the composition's parent (if any).
pub fn with_target(mut self, target: Entity) -> Self {
self.target = Some(target);
self
}
}
impl<C> Component for Composition<C>
where
C: Compose + Send + Sync + 'static,
{
const STORAGE_TYPE: StorageType = StorageType::SparseSet;
type Mutability = Mutable;
fn on_insert() -> Option<bevy_ecs::lifecycle::ComponentHook> {
Some(|mut world, cx| {
world.commands().queue(move |world: &mut World| {
let mut composition = world.get_mut::<Composition<C>>(cx.entity).unwrap();
let content = composition.content.take().unwrap();
let target = composition.target.unwrap_or(cx.entity);
let rt = world.non_send_resource_mut::<Runtime>();
rt.composers.borrow_mut().insert(
cx.entity,
RuntimeComposer {
composer: Composer::new(CompositionContent { content, target }),
},
);
})
})
}
}
#[derive(Data)]
#[actuate(path = "crate")]
struct CompositionContent<C> {
content: C,
target: Entity,
}
impl<C: Compose> Compose for CompositionContent<C> {
fn compose(cx: Scope<Self>) -> impl Compose {
use_provider(&cx, || SpawnContext {
parent_entity: cx.me().target,
keys: RefCell::new(BTreeSet::new()),
});
unsafe { Signal::map_unchecked(cx.me(), |me| &me.content) }
}
}
struct RuntimeWaker {
proxy: EventLoopProxy<WakeUp>,
}
impl Wake for RuntimeWaker {
fn wake(self: Arc<Self>) {
self.proxy.send_event(WakeUp).unwrap();
}
}
fn compose(world: &mut World) {
RUNTIME_CONTEXT.with(|runtime_cx| {
let mut cell = runtime_cx.borrow_mut();
let runtime_cx = cell.get_or_insert_with(|| RuntimeContext {
inner: Rc::new(RefCell::new(Inner {
world_ptr: ptr::null_mut(),
listeners: SlotMap::new(),
updates: Vec::new(),
commands: Rc::new(RefCell::new(CommandQueue::default())),
})),
});
runtime_cx.inner.borrow_mut().world_ptr = world as *mut World;
for f in runtime_cx.inner.borrow().listeners.values() {
f(world)
}
});
world.increment_change_tick();
let rt_cx = RuntimeContext::current();
let mut rt = rt_cx.inner.borrow_mut();
for f in &mut rt.updates {
f(world);
}
rt.updates.clear();
rt.commands.borrow_mut().apply(world);
drop(rt);
let proxy = (*world
.get_resource::<EventLoopProxyWrapper<WakeUp>>()
.unwrap())
.clone();
let rt = &mut *world.non_send_resource_mut::<Runtime>();
let mut composers = rt.composers.borrow_mut();
for rt_composer in composers.values_mut() {
let waker = Waker::from(Arc::new(RuntimeWaker {
proxy: proxy.clone(),
}));
let mut cx = Context::from_waker(&waker);
// TODO handle composition error.
let _ = rt_composer.composer.poll_compose(&mut cx);
}
}
/// A function that takes a [`SystemParam`] as input.
#[diagnostic::on_unimplemented(
message = "`{Self}` is not a valid system",
label = "invalid system"
)]
pub trait SystemParamFunction<Marker> {
/// The input type to this system. See [`System::In`].
type In;
/// The return type of this system. See [`System::Out`].
type Out;
/// The [`SystemParam`].
type Param: SystemParam + 'static;
/// Run the function with the provided [`SystemParam`]'s item.
fn run(&mut self, input: Self::In, param_value: SystemParamItem<Self::Param>) -> Self::Out;
}
#[doc(hidden)]
pub struct Wrap<T>(T);
macro_rules! impl_system_param_fn {
($($t:tt),*) => {
impl<Out, Func, $($t: SystemParam + 'static),*> SystemParamFunction<Wrap<fn($($t,)*) -> Out>> for Func
where
for <'a> &'a mut Func:
FnMut($($t),*) -> Out +
FnMut($(SystemParamItem<$t>),*) -> Out, Out: 'static
{
type In = ();
type Out = Out;
type Param = ($($t,)*);
#[inline]
#[allow(non_snake_case)]
fn run(&mut self, _input: (), param_value: SystemParamItem< ($($t,)*)>) -> Out {
#[allow(clippy::too_many_arguments)]
fn call_inner<Out, $($t,)*>(mut f: impl FnMut($($t,)*) -> Out, $($t: $t,)*)->Out{
f($($t,)*)
}
let ($($t,)*) = param_value;
call_inner(self, $($t),*)
}
}
#[allow(non_snake_case)]
impl<Input, Out, Func, $($t: SystemParam + 'static),*> SystemParamFunction<fn(In<Input>, $($t,)*) -> Out> for Func
where
for <'a> &'a mut Func:
FnMut(In<Input>, $($t),*) -> Out +
FnMut(In<Input>, $(SystemParamItem<$t>),*) -> Out, Out: 'static
{
type In = Input;
type Out = Out;
type Param = ($($t,)*);
#[inline]
fn run(&mut self, input: Input, param_value: SystemParamItem< ($($t,)*)>) -> Out {
#[allow(clippy::too_many_arguments)]
fn call_inner<Input, Out, $($t,)*>(
mut f: impl FnMut(In<Input>, $($t,)*)->Out,
input: In<Input>,
$($t: $t,)*
)->Out{
f(input, $($t,)*)
}
let ($($t,)*) = param_value;
call_inner(self, In(input), $($t),*)
}
}
#[allow(non_snake_case)]
impl<E: Event, B: Bundle, Out, Func, $($t: SystemParam + 'static),*> SystemParamFunction<fn(On<E, B>, $($t,)*) -> Out> for Func
where
for <'a> &'a mut Func:
FnMut(On<E, B>, $($t),*) -> Out +
FnMut(On<E, B>, $(SystemParamItem<$t>),*) -> Out, Out: 'static
{
type In = On<'static, 'static,E, B>;
type Out = Out;
type Param = ($($t,)*);
#[inline]
fn run(&mut self, input: On<E, B>, param_value: SystemParamItem< ($($t,)*)>) -> Out {
#[allow(clippy::too_many_arguments)]
fn call_inner<E: Event, B: Bundle, Out, $($t,)*>(
mut f: impl FnMut(On<E, B>, $($t,)*)->Out,
input: On<E, B>,
$($t: $t,)*
)->Out{
f(input, $($t,)*)
}
let ($($t,)*) = param_value;
call_inner(self, input, $($t),*)
}
}
};
}
impl_trait_for_tuples!(impl_system_param_fn);
/// Use one or more [`SystemParam`]s from the ECS world.
///
/// `with_world` will be called on every frame with the latest query.
///
/// Change detection is implemented as a traditional system parameter.
///
/// # Examples
///
/// ```no_run
/// use actuate::prelude::*;
/// use bevy::prelude::*;
///
/// // Timer composable.
/// #[derive(Data)]
/// struct Timer;
///
/// impl Compose for Timer {
/// fn compose(cx: Scope<Self>) -> impl Compose {
/// let current_time = use_mut(&cx, Time::default);
///
/// // Use the `Time` resource from the ECS world, updating the `current_time`.
/// use_world(&cx, move |time: Res<Time>| {
/// SignalMut::set(current_time, *time)
/// });
///
/// // Spawn a `Text` component, updating it when this scope is re-composed.
/// spawn(Text::new(format!("Elapsed: {:?}", current_time.elapsed())))
/// }
/// }
/// ```
pub fn use_world<'a, Marker, F>(cx: ScopeState<'a>, mut with_world: F)
where
F: SystemParamFunction<Marker, In = (), Out = ()> + 'a,
{
let system_state_cell = use_ref(cx, || RefCell::new(None));
let f: Rc<dyn Fn(&'static mut World)> = use_callback(cx, move |world: &'static mut World| {
let mut system_state_cell = system_state_cell.borrow_mut();
let system_state =
system_state_cell.get_or_insert_with(|| SystemState::<F::Param>::new(world));
let params = system_state.get_mut(world);
with_world.run((), params);
system_state.apply(world);
})
.clone();
let key = *use_ref(cx, || {
let f: Rc<dyn Fn(&mut World)> = unsafe { mem::transmute(f) };
RuntimeContext::current()
.inner
.borrow_mut()
.listeners
.insert(f)
});
use_drop(cx, move || {
RuntimeContext::current()
.inner
.borrow_mut()
.listeners
.remove(key);
});
}
/// A function that takes a [`SystemParam`] as input.
#[diagnostic::on_unimplemented(
message = "`{Self}` is not a valid system",
label = "invalid system"
)]
pub trait SystemParamFunctionOnce<Marker> {
/// The [`SystemParam`].
type Param: SystemParam + 'static;
/// The return type of this function.
type Output: 'static;
/// Run the function with the provided [`SystemParam`]'s item.
fn run(self, param: <Self::Param as SystemParam>::Item<'_, '_>) -> Self::Output;
}
macro_rules! impl_system_param_fn_once {
($($t:tt),*) => {
impl<$($t: SystemParam + 'static,)* R: 'static, F: FnOnce($($t),*) -> R + FnOnce($($t::Item<'_, '_>),*) -> R> SystemParamFunctionOnce<fn($($t),*)> for F {
type Param = ($($t,)*);
type Output = R;
fn run(self, param: <Self::Param as SystemParam>::Item<'_, '_>) -> Self::Output {
#[allow(non_snake_case)]
let ($($t,)*) = param;
self($($t,)*)
}
}
};
}
impl_trait_for_tuples!(impl_system_param_fn_once);
/// Use one or more [`SystemParam`]s from the ECS world.
///
/// `with_world` will be called once during the first composition.
pub fn use_world_once<Marker, F>(cx: ScopeState<'_>, with_world: F) -> &F::Output
where
F: SystemParamFunctionOnce<Marker>,
{
use_ref(cx, || {
let world = unsafe { RuntimeContext::current().world_mut() };
let mut param = SystemState::<F::Param>::new(world);
let item = param.get_mut(world);
let output = with_world.run(item);
param.apply(world);
output
})
}
/// Hook for [`use_commands`].
pub struct UseCommands {
commands: Rc<RefCell<CommandQueue>>,
}
impl UseCommands {
/// Push a [`Command`] to the command queue.
pub fn push<C>(&mut self, command: C)
where
C: Command,
{
self.commands.borrow_mut().push(command);
}
}
/// Use access to the current [`Command`] queue.
pub fn use_commands(cx: ScopeState<'_>) -> &UseCommands {
use_ref(cx, || {
let commands = RuntimeContext::current().inner.borrow().commands.clone();
UseCommands { commands }
})
}
struct SpawnContext {
parent_entity: Entity,
keys: RefCell<BTreeSet<Pending>>,
}
/// Use a spawned bundle.
///
/// `make_bundle` is called once to create the bundle.
pub fn use_bundle<B: Bundle>(cx: ScopeState<'_>, make_bundle: impl FnOnce() -> B) -> Entity {
use_bundle_inner(cx, |world, cell| {
let bundle = make_bundle();
if let Some(entity) = cell {
world.entity_mut(*entity).insert(bundle);
} else {
*cell = Some(world.spawn(bundle).id());
}
})
}
fn use_bundle_inner(
cx: ScopeState<'_>,
spawn: impl FnOnce(&mut World, &mut Option<Entity>),
) -> Entity {
let mut f_cell = Some(spawn);
let entity = *use_ref(cx, || {
let world = unsafe { RuntimeContext::current().world_mut() };
let mut cell = None;
f_cell.take().unwrap()(world, &mut cell);
cell.unwrap()
});
if let Some(f) = f_cell {
let world = unsafe { RuntimeContext::current().world_mut() };
f(world, &mut Some(entity));
}
use_drop(cx, move || {
let world = unsafe { RuntimeContext::current().world_mut() };
world.try_despawn(entity).ok();
});
entity
}
/// ECS bundle modifier.
#[derive(Clone, Default)]
pub struct Modifier<'a> {
fns: Vec<Rc<dyn Fn(Spawn<'a>) -> Spawn<'a> + 'a>>,
}
impl<'a> Modifier<'a> {
/// Apply this modifier.
pub fn apply(&self, spawn: Spawn<'a>) -> Spawn<'a> {
self.fns
.iter()
.fold(spawn, |spawn, modifier| modifier(spawn))
}
/// Append another stack of modifiers to this modifier.
pub fn append(&mut self, modifier: Cow<'a, Modifier>) {
let modifier: Modifier<'_> = modifier.into_owned();
let modifier: Modifier<'a> = unsafe { mem::transmute(modifier) };
self.fns.extend(modifier.fns);
}
}
impl fmt::Debug for Modifier<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Modifier").finish()
}
}
unsafe impl Data for Modifier<'_> {}
macro_rules! ui_methods {
($($i:ident: $t:path),*) => {
$(
#[cfg(feature = "ui")]
#[cfg_attr(docsrs, doc(cfg(feature = "ui")))]
#[doc = concat!("Set the `", stringify!($i), "` of this composable's spawned [`Node`].")]
fn $i(self, $i: $t) -> Self
where
Self: Sized,
{
self.modify(move |spawn| {
let $i = $i.clone();
spawn.on_insert(move |mut entity| {
let mut node = entity.get_mut::<Node>().unwrap();
node.$i = $i.clone();
})
})
}
)*
};
}
macro_rules! handler_methods {
($($i:ident: $e:ident),*) => {
$(
#[cfg(feature = "picking")]
#[cfg_attr(docsrs, doc(cfg(feature = "picking")))]
#[doc = concat!("Add an observer for `", stringify!($e), "` events to this composable's bundle.")]
fn $i(self, f: impl Fn() + Send + Sync + 'a) -> Self
where
Self: Sized,
{
self.observe(move |_: On<Pointer<$e>>| f())
}
)*
};
}
/// Modifiable composable.
pub trait Modify<'a> {
/// Get a mutable reference to the modifier of this button.
fn modifier(&mut self) -> &mut Modifier<'a>;
/// Modify this composable with a function.
fn modify(mut self, f: impl Fn(Spawn<'a, ()>) -> Spawn<'a, ()> + 'a) -> Self
where
Self: Sized,
{
self.modifier().fns.push(Rc::new(f));
self
}
/// Append a modifier to this composable.
fn append(mut self, modifier: Cow<'a, Modifier>) -> Self
where
Self: Sized,
{
self.modifier().append(modifier);
self
}
/// Add a function to run when this composable's bundle is spawned.
fn on_insert<F>(self, f: F) -> Self
where
Self: Sized,
F: Fn(EntityWorldMut) + 'a,
{
let f = Rc::new(f);
self.modify(move |spawn| {
let f = f.clone();
spawn.on_insert(move |e| f(e))
})
}
#[cfg(feature = "ui")]
#[cfg_attr(docsrs, doc(cfg(feature = "ui")))]
/// Set the flex gap of this composable's spawned [`Node`].
///
/// This will set the `column_gap` for a `FlexDirection::Row` or `FlexDirection::RowReverse`
/// and the `row_gap` for a `FlexDirection::Column` or `FlexDirection::ColumnReverse`.
fn flex_gap(self, gap: Val) -> Self
where
Self: Sized,
{
self.modify(move |spawn| {
spawn.on_insert(move |mut entity| {
let mut node = entity.get_mut::<Node>().unwrap();
match node.flex_direction {
FlexDirection::Row | FlexDirection::RowReverse => node.column_gap = gap,
FlexDirection::Column | FlexDirection::ColumnReverse => node.row_gap = gap,
}
})
})
}
ui_methods!(
display: Display,
position_type: PositionType,
overflow: Overflow,
overflow_clip_margin: OverflowClipMargin,
left: Val,
right: Val,
top: Val,
bottom: Val,
width: Val,
height: Val,
min_width: Val,
min_height: Val,
max_width: Val,
max_height: Val,
aspect_ratio: Option<f32>,
align_items: AlignItems,
justify_items: JustifyItems,
align_self: AlignSelf,
justify_self: JustifySelf,
align_content: AlignContent,
justify_content: JustifyContent,
margin: UiRect,
padding: UiRect,
border: UiRect,
flex_direction: FlexDirection,
flex_wrap: FlexWrap,
flex_grow: f32,
flex_shrink: f32,
flex_basis: Val,
row_gap: Val,
column_gap: Val,
grid_auto_flow: GridAutoFlow,
grid_template_rows: Vec<RepeatedGridTrack>,
grid_template_columns: Vec<RepeatedGridTrack>,
grid_auto_rows: Vec<GridTrack>,
grid_auto_columns: Vec<GridTrack>,
grid_row: GridPlacement,
grid_column: GridPlacement
);
/// Add an observer to this composable's bundle.
fn observe<F, E, B, Marker>(self, observer: F) -> Self
where
Self: Sized,
F: SystemParamFunction<Marker, In = On<'static, 'static, E, B>, Out = ()>
+ Send
+ Sync
+ 'a,
E: EntityEvent,
B: Bundle,
{
let observer_cell = Cell::new(Some(observer));
self.modify(move |spawn| {
let observer = observer_cell.take().unwrap();
spawn.observe(observer)
})
}
handler_methods!(
on_mouse_in: Over,
on_mouse_out: Out,
on_click: Click,
on_mouse_down: Press,
on_mouse_up: Release,
on_drag: Drag,
on_drag_start: DragStart,
on_drag_end: DragEnd,
on_drag_enter: DragEnter,
on_drag_over: DragOver,
on_drag_drop: DragDrop,
on_drag_leave: DragLeave
);
}
================================================
FILE: src/ecs/spawn.rs
================================================
use super::{use_bundle_inner, RuntimeContext, SpawnContext, SystemParamFunction};
use crate::{
compose::Compose, composer::Runtime, data::Data, use_context, use_drop, use_provider, use_ref,
Scope, Signal,
};
use bevy_ecs::{entity::Entity, prelude::*, world::World};
use std::{
cell::{Cell, RefCell},
collections::BTreeSet,
mem,
rc::Rc,
sync::{Arc, Mutex},
};
/// Create a [`Spawn`] composable that spawns the provided `bundle` when composed.
///
/// On re-composition, the spawned entity is updated to the latest provided value.
///
/// # Examples
///
/// ```no_run
/// use actuate::prelude::*;
/// use bevy::prelude::*;
///
/// #[derive(Data)]
/// struct Button {
/// label: String,
/// color: Color
/// }
///
/// impl Compose for Button {
/// fn compose(cx: Scope<Self>) -> impl Compose {
/// // Spawn an entity with a `Text` and `BackgroundColor` component.
/// spawn((Text::new(cx.me().label.clone()), BackgroundColor(cx.me().color)))
/// }
/// }
/// ```
pub fn spawn<'a, B>(bundle: B) -> Spawn<'a>
where
B: Bundle + Clone,
{
Spawn {
spawn_fn: Rc::new(move |world, cell| {
if let Some(entity) = cell {
world.entity_mut(*entity).insert(bundle.clone());
} else {
*cell = Some(world.spawn(bundle.clone()).id())
}
}),
content: (),
target: None,
observer_fns: Vec::new(),
observer_guard: Arc::new(Mutex::new(true)),
on_spawn: Vec::new(),
on_insert: Vec::new(),
}
}
type SpawnFn = Rc<dyn Fn(&mut World, &mut Option<Entity>)>;
type ObserverFn<'a> = Rc<dyn Fn(&mut EntityWorldMut) + 'a>;
type OnInsertFn<'a> = Rc<dyn Fn(EntityWorldMut) + 'a>;
/// Composable to spawn an entity.
///
/// See [`spawn`] for more information.
#[derive(Clone)]
#[must_use = "Composables do nothing unless composed or returned from other composables."]
pub struct Spawn<'a, C = ()> {
spawn_fn: SpawnFn,
content: C,
target: Option<Entity>,
observer_fns: Vec<ObserverFn<'a>>,
on_spawn: Vec<OnInsertFn<'a>>,
on_insert: Vec<OnInsertFn<'a>>,
observer_guard: Arc<Mutex<bool>>,
}
impl<'a, C> Spawn<'a, C> {
/// Set the target entity to spawn the composition into.
///
/// If `None`, this will use the composition's parent (if any).
pub fn target(mut self, target: Entity) -> Self {
self.target = Some(target);
self
}
/// Set the child content.
pub fn content<C2>(self, content: C2) -> Spawn<'a, C2> {
Spawn {
spawn_fn: self.spawn_fn,
content,
target: self.target,
observer_fns: self.observer_fns,
observer_guard: Arc::new(Mutex::new(false)),
on_spawn: self.on_spawn,
on_insert: self.on_insert,
}
}
/// Add a function to be called when this bundle is initially spawned.
pub fn on_spawn(mut self, f: impl Fn(EntityWorldMut) + 'a) -> Self {
self.on_insert.push(Rc::new(f));
self
}
/// Add a function to be called on every insert.
pub fn on_insert(mut self, f: impl Fn(EntityWorldMut) + 'a) -> Self {
self.on_insert.push(Rc::new(f));
self
}
/// Add an observer to the spawned entity.
pub fn observe<F, E, B, Marker>(mut self, observer: F) -> Self
where
F: SystemParamFunction<Marker, In = On<'static, 'static, E, B>, Out = ()>
+ Send
+ Sync
+ 'a,
E: EntityEvent,
B: Bundle,
{
let cell = Cell::new(Some(observer));
let guard = self.observer_guard.clone();
self.observer_fns.push(Rc::new(move |entity| {
let mut observer = cell.take().unwrap();
let guard = guard.clone();
type SpawnObserveFn<'a, F, E, B, Marker> = Box<
dyn FnMut(
On<'_, '_, E, B>,
ParamSet<'_, '_, (<F as SystemParamFunction<Marker>>::Param,)>,
) + Send
+ Sync
+ 'a,
>;
let f: SpawnObserveFn<'a, F, E, B, Marker> = Box::new(move |trigger, mut params| {
let guard = guard.lock().unwrap();
if !*guard {
panic!("Actuate observer called after its scope was dropped.")
}
// Safety: The event will be accessed under a shortened lifetime.
let trigger: On<'static, 'static, E, B> = unsafe { mem::transmute(trigger) };
observer.run(trigger, params.p0())
});
// Safety: The observer will be disabled after this scope is dropped.
let f: SpawnObserveFn<'static, F, E, B, Marker> = unsafe { mem::transmute(f) };
entity.observe(f);
}));
self
}
}
unsafe impl<C: Data> Data for Spawn<'_, C> {}
impl<C: Compose> Compose for Spawn<'_, C> {
fn compose(cx: Scope<Self>) -> impl Compose {
let rt = Runtime::current();
let spawn_cx = use_context::<SpawnContext>(&cx);
let is_initial = use_ref(&cx, || Cell::new(true));
let entity = use_bundle_inner(&cx, |world, entity| {
if let Some(target) = cx.me().target {
*entity = Some(target);
}
// Check if this entity has been removed externally.
if let Some(entity) = entity {
if world.get_entity(*entity).is_err() {
return;
}
}
(cx.me().spawn_fn)(world, entity);
for f in &cx.me().on_insert {
f(world.entity_mut(entity.unwrap()));
}
if is_initial.get() {
for f in &cx.me().on_spawn {
f(world.entity_mut(entity.unwrap()));
}
let mut entity_mut = world.entity_mut(entity.unwrap());
for f in &cx.me().observer_fns {
f(&mut entity_mut);
}
is_initial.set(false);
}
});
let key = use_ref(&cx, || rt.pending(rt.current_key.get()));
use_provider(&cx, || {
if cx.me().target.is_none() {
if let Ok(spawn_cx) = spawn_cx {
spawn_cx.keys.borrow_mut().insert(key.clone());
if let Some(idx) = spawn_cx
.keys
.borrow()
.iter()
.position(|pending| pending.key == rt.current_key.get())
{
let world = unsafe { RuntimeContext::current().world_mut() };
world
.entity_mut(spawn_cx.parent_entity)
.insert_children(idx, &[entity]);
}
}
}
SpawnContext {
parent_entity: entity,
keys: RefCell::new(BTreeSet::new()),
}
});
// Use the initial guard.
let guard = use_ref(&cx, || cx.me().observer_guard.clone());
use_drop(&cx, move || {
*guard.lock().unwrap() = false;
if let Ok(spawn_cx) = spawn_cx {
spawn_cx.keys.borrow_mut().remove(key);
}
});
unsafe { Signal::map_unchecked(cx.me(), |me| &me.content) }
}
}
================================================
FILE: src/executor.rs
================================================
use alloc::{rc::Rc, sync::Arc};
use core::{future::Future, pin::Pin};
/// Executor for async tasks.
pub trait Executor {
/// Spawn a boxed future on this executor.
fn spawn(&self, future: Pin<Box<dyn Future<Output = ()> + Send>>);
}
#[cfg(feature = "rt")]
#[cfg_attr(docsrs, doc(cfg(feature = "rt")))]
impl Executor for tokio::runtime::Runtime {
fn spawn(&self, future: Pin<Box<dyn Future<Output = ()> + Send>>) {
self.spawn(future);
}
}
macro_rules! impl_executor {
($($t:tt),*) => {
$(
impl<T: Executor + ?Sized> Executor for $t<T> {
fn spawn(&self, future: Pin<Box<dyn Future<Output = ()> + Send>>) {
(**self).spawn(future);
}
}
)*
};
}
impl_executor!(Box, Rc, Arc);
/// Context that contains the current [`Executor`].
pub struct ExecutorContext {
pub(crate) executor: Box<dyn Executor>,
}
#[cfg(feature = "rt")]
impl Default for ExecutorContext {
fn default() -> Self {
Self::new(tokio::runtime::Runtime::new().unwrap())
}
}
impl ExecutorContext {
/// Create a new [`ExecutorContext`] with the provided [`Executor`].
pub fn new(executor: impl Executor + 'static) -> Self {
Self {
executor: Box::new(executor),
}
}
/// Spawn a future on the current runtime.
pub fn spawn<F>(&self, future: F)
where
F: Future<Output = ()> + Send + 'static,
{
self.spawn_boxed(Box::pin(future))
}
/// Spawn a boxed future on the current runtime.
pub fn spawn_boxed(&self, future: Pin<Box<dyn Future<Output = ()> + Send>>) {
self.executor.spawn(future);
}
}
================================================
FILE: src/lib.rs
================================================
#![deny(missing_docs)]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![doc(
html_logo_url = "https://avatars.githubusercontent.com/u/161107368",
html_favicon_url = "https://avatars.githubusercontent.com/u/161107368"
)]
//! # Actuate
//! A high-performance and borrow-checker friendly framework for declarative programming in Rust.
//! This crate provides a generic library that lets you define reactive components
//! (also known as composables, for more see [`Compose`]).
//!
//! ```no_run
//! use actuate::prelude::*;
//! use bevy::prelude::*;
//!
//! // Counter composable.
//! #[derive(Data)]
//! struct Counter {
//! start: i32,
//! }
//!
//! impl Compose for Counter {
//! fn compose(cx: Scope<Self>) -> impl Compose {
//! let count = use_mut(&cx, || cx.me().start);
//!
//! material_ui((
//! text::headline(format!("High five count: {}", count)),
//! button(text::label("Up high")).on_click(move || SignalMut::update(count, |x| *x += 1)),
//! button(text::label("Down low")).on_click(move || SignalMut::update(count, |x| *x -= 1)),
//! if *count == 0 {
//! Some(text::label("Gimme five!"))
//! } else {
//! None
//! },
//! ))
//! .align_items(AlignItems::Center)
//! .justify_content(JustifyContent::Center)
//! }
//! }
//!```
//!
//! ## Borrowing
//! Composables can borrow from their ancestors, as well as state.
//! ```no_run
//! use actuate::prelude::*;
//! use bevy::prelude::*;
//!
//! #[derive(Data)]
//! struct User<'a> {
//! // `actuate::Cow` allows for either a borrowed or owned value.
//! name: Cow<'a, String>,
//! }
//!
//! impl Compose for User<'_> {
//! fn compose(cx: Scope<Self>) -> impl Compose {
//! text::headline(cx.me().name.to_string())
//! }
//! }
//!
//! #[derive(Data)]
//! struct App {
//! name: String
//! }
//!
//! impl Compose for App {
//! fn compose(cx: Scope<Self>) -> impl Compose {
//! // Get a mapped reference to the app's `name` field.
//! let name = Signal::map(cx.me(), |me| &me.name).into();
//!
//! User { name }
//! }
//! }
//! ```
//!
//! ## Hooks
//! Functions that begin with `use_` are called `hooks` in Actuate.
//! Hooks are used to manage state and side effects in composables.
//!
//! Hooks must be used in the same order for every re-compose.
//! Don’t use hooks inside loops, conditions, nested functions, or match blocks.
//! Instead, always use hooks at the top level of your composable, before any early returns.
//!
//! ## Installation
//! To add this crate to your project:
//! ```sh
//! cargo add actuate --features full
//! ```
//!
//! ## Features
//! - `std`: Enables features that use Rust's standard library (default). With this feature disabled Actuate can be used in `#![no_std]` environments.
//! - `animation`: Enables the `animation` module for animating values from the [Bevy](https://crates.io/crates/bevy) ECS.
//! (enables the `ecs` feature).
//! - `ecs`: Enables the `ecs` module for bindings to the [Bevy](https://crates.io/crates/bevy) ECS.
//! - `executor`: Enables the `executor` module for multi-threaded tasks.
//! - `material`: Enables the `material` module for Material UI (enables the `ecs` and `ui` features).
//! - `picking`: Enables support for picking event handlers with `Modify` (requires the `ecs` feature).
//! - `rt` Enables support for the [Tokio](https://crates.io/crates/tokio) runtime with the Executor trait.
//! (enables the `executor` feature).
//! - `tracing`: Enables the logging through the `tracing` crate.
//! - `ui`: Enables the `ui` module for user interface components.
//! - `full`: Enables all features above.
extern crate alloc;
use ahash::AHasher;
use alloc::rc::Rc;
use core::{
any::{Any, TypeId},
cell::{Cell, RefCell, UnsafeCell},
fmt,
future::Future,
hash::{BuildHasherDefault, Hash, Hasher},
marker::PhantomData,
mem,
ops::Deref,
pin::Pin,
ptr::NonNull,
};
use slotmap::DefaultKey;
use thiserror::Error;
#[cfg(not(feature = "std"))]
use hashbrown::HashMap;
#[cfg(feature = "std")]
use std::collections::HashMap;
/// Prelude of commonly used items.
pub mod prelude {
pub use crate::{
compose::{self, catch, dyn_compose, memo, Compose, DynCompose, Error, Memo},
data::{data, Data},
use_callback, use_context, use_drop, use_local_task, use_memo, use_mut, use_provider,
use_ref, Cow, Generational, Map, RefMap, Scope, ScopeState, Signal, SignalMut,
};
#[cfg(feature = "animation")]
#[cfg_attr(docsrs, doc(cfg(feature = "animation")))]
pub use crate::animation::{use_animated, UseAnimated};
#[cfg(feature = "ecs")]
#[cfg_attr(docsrs, doc(cfg(feature = "ecs")))]
pub use crate::ecs::{
spawn, use_bundle, use_commands, use_world, use_world_once, ActuatePlugin, Composition,
Modifier, Modify, Spawn, UseCommands,
};
#[cfg(feature = "executor")]
#[cfg_attr(docsrs, doc(cfg(feature = "executor")))]
pub use crate::use_task;
#[cfg(feature = "ui")]
#[cfg_attr(docsrs, doc(cfg(feature = "ui")))]
pub use crate::ui::{scroll_view, ScrollView};
#[cfg(feature = "material")]
#[cfg_attr(docsrs, doc(cfg(feature = "material")))]
pub use crate::ui::material::{
button, container, material_ui, radio_button, text, Button, MaterialUi, RadioButton, Theme,
TypographyKind, TypographyStyleKind,
};
}
#[cfg(feature = "animation")]
#[cfg_attr(docsrs, doc(cfg(feature = "animation")))]
/// Animation hooks.
pub mod animation;
/// Composable functions.
pub mod compose;
use self::compose::{AnyCompose, Compose};
/// Low-level composer.
pub mod composer;
use self::composer::Runtime;
/// Data trait and macros.
pub mod data;
use crate::data::Data;
#[cfg(feature = "ecs")]
#[cfg_attr(docsrs, doc(cfg(feature = "ecs")))]
/// Bevy ECS integration.
pub mod ecs;
#[cfg(feature = "executor")]
#[cfg_attr(docsrs, doc(cfg(feature = "executor")))]
/// Task execution context.
pub mod executor;
#[cfg(feature = "ui")]
#[cfg_attr(docsrs, doc(cfg(feature = "ui")))]
/// User interface components.
pub mod ui;
/// Clone-on-write value.
///
/// This represents either a borrowed or owned value.
/// A borrowed value is stored as a [`RefMap`], which can be either a reference or a mapped reference.
#[derive(Debug)]
pub enum Cow<'a, T> {
/// Borrowed value, contained inside either a [`Signal`] or [`Map`].
Borrowed(RefMap<'a, T>),
/// Owned value.
Owned(T),
}
impl<T> Cow<'_, T> {
/// Clone this value to an owned value.
pub fn to_owned(&self) -> T
where
T: Clone,
{
self.clone().into_owned()
}
/// Convert or clone this value to an owned value.
pub fn into_owned(self) -> T
where
T: Clone,
{
match self {
Cow::Borrowed(value) => (*value).clone(),
Cow::Owned(value) => value,
}
}
}
impl<T> Clone for Cow<'_, T>
where
T: Clone,
{
fn clone(&self) -> Self {
match self {
Cow::Borrowed(value) => Cow::Borrowed(*value),
Cow::Owned(value) => Cow::Owned(value.clone()),
}
}
}
impl<T> Deref for Cow<'_, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
match self {
Cow::Borrowed(ref_map) => ref_map,
Cow::Owned(value) => value,
}
}
}
impl<'a, T> From<RefMap<'a, T>> for Cow<'a, T> {
fn from(value: RefMap<'a, T>) -> Self {
Cow::Borrowed(value)
}
}
impl<'a, T> From<Signal<'a, T>> for Cow<'a, T> {
fn from(value: Signal<'a, T>) -> Self {
RefMap::from(value).into()
}
}
impl<'a, T> From<Map<'a, T>> for Cow<'a, T> {
fn from(value: Map<'a, T>) -> Self {
RefMap::from(value).into()
}
}
impl<T: fmt::Display> fmt::Display for Cow<'_, T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Cow::Borrowed(value) => value.fmt(f),
Cow::Owned(value) => value.fmt(f),
}
}
}
unsafe impl<T: Data> Data for Cow<'_, T> {}
/// Immutable reference or mapped reference to a value.
#[derive(Debug)]
pub enum RefMap<'a, T> {
/// Reference to a value.
Ref(&'a T),
/// Signal value.
Signal(Signal<'a, T>),
/// Mapped reference to a value.
Map(Map<'a, T>),
}
impl<T> Clone for RefMap<'_, T> {
fn clone(&self) -> Self {
*self
}
}
impl<T> Copy for RefMap<'_, T> {}
impl<T> Deref for RefMap<'_, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
match self {
RefMap::Ref(r) => r,
RefMap::Signal(s) => s,
RefMap::Map(map) => map,
}
}
}
impl<T: Hash> Hash for RefMap<'_, T> {
fn hash<H: Hasher>(&self, state: &mut H) {
(**self).hash(state);
}
}
impl<'a, T> From<Signal<'a, T>> for RefMap<'a, T> {
fn from(value: Signal<'a, T>) -> Self {
RefMap::Signal(value)
}
}
impl<'a, T> From<Map<'a, T>> for RefMap<'a, T> {
fn from(value: Map<'a, T>) -> Self {
RefMap::Map(value)
}
}
impl<T: fmt::Display> fmt::Display for RefMap<'_, T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
(**self).fmt(f)
}
}
unsafe impl<T: Data> Data for RefMap<'_, T> {}
/// Mapped immutable reference to a value of type `T`.
///
/// This can be created with [`Signal::map`].
pub struct Map<'a, T> {
ptr: *const (),
map_fn: *const (),
deref_fn: fn(*const (), *const ()) -> &'a T,
generation: *const Cell<u64>,
}
impl<T> Deref for Map<'_, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
(self.deref_fn)(self.ptr, self.map_fn)
}
}
/// Unchecked, mapped immutable reference to a value of type `T`.
///
/// This can be created with [`Signal::map_unchecked`].
pub struct MapUnchecked<'a, T> {
map: Map<'a, T>,
}
unsafe impl<T> Data for MapUnchecked<'_, T> {}
impl<C: Compose> Compose for MapUnchecked<'_, C> {
fn compose(cx: Scope<Self>) -> impl Compose {
// Safety: The `Map` is dereferenced every re-compose, so it's guranteed not to point to
// an invalid memory location (e.g. an `Option` that previously returned `Some` is now `None`).
unsafe { (*cx.me().map).any_compose(cx.state) }
}
fn name() -> Option<std::borrow::Cow<'static, str>> {
C::name()
}
}
impl<T> Hash for Map<'_, T> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.ptr.hash(state);
self.generation.hash(state);
}
}
/// Immutable reference to a value of type `T`.
///
/// Memoizing this value will use pointer-equality for higher-performance.
///
/// This reference can be mapped to inner values with [`Signal::map`].
pub struct Signal<'a, T> {
/// Pinned reference to the value.
value: &'a T,
/// Pointer to this value's current generation.
generation: *const Cell<u64>,
}
impl<'a, T> Signal<'a, T> {
/// Map this reference to a value of type `U`.
pub fn map<U>(me: Self, f: fn(&T) -> &U) -> Map<'a, U> {
Map {
ptr: me.value as *const _ as _,
map_fn: f as _,
deref_fn: |ptr, g| {
// Safety: `f` is guranteed to be a valid function pointer.
unsafe {
let g: fn(&T) -> &U = mem::transmute(g);
g(&*(ptr as *const T))
}
},
generation: me.generation,
}
}
/// Unsafely map this reference to a value of type `U`.
/// The returned `MapUnchecked` implements `Compose` to allow for borrowed child composables.
///
/// # Safety
/// The returned `MapUnchecked` must only be returned once.
/// Composing the same `MapUnchecked` at multiple locations in the tree at the same time will result in undefined behavior.
pub unsafe fn map_unchecked<U>(me: Self, f: fn(&T) -> &U) -> MapUnchecked<'a, U> {
MapUnchecked {
map: Signal::map(me, f),
}
}
}
impl<T> Deref for Signal<'_, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
self.value
}
}
impl<T> Hash for Signal<'_, T> {
fn hash<H: Hasher>(&self, state: &mut H) {
(self.value as *const T).hash(state);
self.generation.hash(state);
}
}
#[derive(Clone, Copy)]
struct UnsafeWrap<T: ?Sized>(T);
unsafe impl<T: ?Sized> Send for UnsafeWrap<T> {}
unsafe impl<T: ?Sized> Sync for UnsafeWrap<T> {}
/// Mutable reference to a value of type `T`.
pub struct SignalMut<'a, T> {
/// Pointer to the boxed value.
ptr: NonNull<T>,
/// Key to this signal's scope.
scope_key: DefaultKey,
/// Pointer to this value's generation.
generation: *const Cell<u64>,
/// Marker for the lifetime of this immutable reference.
_marker: PhantomData<&'a ()>,
}
impl<'a, T: 'static> SignalMut<'a, T> {
/// Queue an update to this value, triggering an update to the component owning this value.
pub fn update(me: Self, f: impl FnOnce(&mut T) + Send + 'static) {
let scope_key = me.scope_key;
Self::with(me, move |value| {
let rt = Runtime::current();
rt.queue(scope_key);
f(value)
})
}
/// Queue an update to this value, triggering an update to the component owning this value.
pub fn set(me: Self, value: T)
where
T: Send,
{
SignalMut::update(me, |x| *x = value)
}
/// Queue an update to this value if it is not equal to the given value.
pub fn set_if_neq(me: Self, value: T)
where
T: PartialEq + Send,
{
if *me != value {
SignalMut::set(me, value);
}
}
/// Queue an update to this value wtihout triggering an update.
pub fn with(me: Self, f: impl FnOnce(&mut T) + Send + 'static) {
let cell = UnsafeWrap(Some(f));
let ptr = UnsafeWrap(me.ptr);
let generation_ptr = UnsafeWrap(me.generation);
Runtime::current().update(move || {
let mut cell = cell;
let mut ptr = ptr;
let generation_ptr = generation_ptr;
// Safety: Updates are guaranteed to be called before any structural changes of the composition tree.
let value = unsafe { ptr.0.as_mut() };
cell.0.take().unwrap()(value);
// Increment the generation of this value.
// Safety: the pointer to this scope's generation is guranteed to outlive `me`.
let generation = unsafe { &*generation_ptr.0 };
generation.set(generation.get() + 1)
});
}
/// Convert this mutable reference to an immutable reference.
pub fn as_ref(me: Self) -> Signal<'a, T> {
Signal {
value: unsafe { me.ptr.as_ref() },
generation: me.generation,
}
}
}
impl<T> Deref for SignalMut<'_, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
unsafe { self.ptr.as_ref() }
}
}
macro_rules! impl_pointer {
($($t:ident),*) => {
$(
impl<T> Clone for $t<'_, T> {
fn clone(&self) -> Self {
*self
}
}
impl<T> Copy for $t<'_, T> {}
impl<T: fmt::Debug> fmt::Debug for $t<'_, T> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct(stringify!($t))
.field("value", &**self)
.field("generation", &unsafe { &*self.generation }.get())
.finish()
}
}
impl<T: fmt::Display> fmt::Display for $t<'_, T> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
(&**self).fmt(f)
}
}
unsafe impl<T: Send + Sync> Send for $t<'_, T> {}
unsafe impl<T: Sync + Sync> Sync for $t<'_, T> {}
impl<'a, T: 'a> IntoIterator for $t<'a, T>
where
&'a T: IntoIterator,
{
type Item = <&'a T as IntoIterator>::Item;
type IntoIter = <&'a T as IntoIterator>::IntoIter;
fn into_iter(self) -> Self::IntoIter {
let value: &T = &self;
// Safety: the reference to `value` is guranteed to live as long as `self`.
let value: &T = unsafe { mem::transmute(value) };
value.into_iter()
}
}
unsafe impl<T: Data> Data for $t<'_, T> {}
)*
};
}
impl_pointer!(Signal, Map, SignalMut);
/// Map of [`TypeId`] to context values.
#[derive(Clone, Default)]
struct Contexts {
values: HashMap<TypeId, Rc<dyn Any>, BuildHasherDefault<AHasher>>,
}
/// Scope state of a composable function.
pub type ScopeState<'a> = &'a ScopeData<'a>;
/// State of a composable.
#[derive(Default)]
pub struct ScopeData<'a> {
/// Hook values stored in this scope.
hooks: UnsafeCell<Vec<Box<dyn Any>>>,
/// Current hook index.
hook_idx: Cell<usize>,
/// Context values stored in this scope.
contexts: RefCell<Contexts>,
/// Context values for child composables.
child_contexts: RefCell<Contexts>,
/// Drop functions to run just before this scope is dropped.
drops: RefCell<Vec<usize>>,
/// Current generation of this scope.
generation: Cell<u64>,
/// Marker for the invariant lifetime of this scope.
_marker: PhantomData<&'a fn(ScopeData<'a>) -> ScopeData<'a>>,
}
impl Drop for ScopeData<'_> {
fn drop(&mut self) {
for idx in &*self.drops.borrow() {
let hooks = unsafe { &mut *self.hooks.get() };
let any = hooks.get_mut(*idx).unwrap();
(**any).downcast_mut::<Box<dyn FnMut()>>().unwrap()();
}
}
}
/// Composable scope.
pub struct Scope<'a, C: ?Sized> {
me: &'a C,
state: ScopeState<'a>,
}
impl<'a, C> Scope<'a, C> {
/// Get a [`Signal`] to this composable.
pub fn me(self) -> Signal<'a, C> {
Signal {
value: self.me,
generation: &self.state.generation,
}
}
/// Get the state of this composable.
pub fn state(self) -> ScopeState<'a> {
self.state
}
}
impl<C> Clone for Scope<'_, C> {
fn clone(&self) -> Self {
*self
}
}
impl<C> Copy for Scope<'_, C> {}
impl<'a, C> Deref for Scope<'a, C> {
type Target = ScopeState<'a>;
fn deref(&self) -> &Self::Target {
&self.state
}
}
/// Use an immutable reference to a value of type `T`.
///
/// `make_value` will only be called once to initialize this value.
pub fn use_ref<T: 'static>(cx: ScopeState<'_>, make_value: impl FnOnce() -> T) -> &T {
let hooks = unsafe { &mut *cx.hooks.get() };
let idx = cx.hook_idx.get();
cx.hook_idx.set(idx + 1);
let any = if idx >= hooks.len() {
hooks.push(Box::new(make_value()));
hooks.last().unwrap()
} else {
hooks.get(idx).unwrap()
};
(**any).downcast_ref().unwrap()
}
struct MutState<T> {
value: T,
generation: Cell<u64>,
}
/// Use a mutable reference to a value of type `T`.
///
/// `make_value` will only be called once to initialize this value.
pub fn use_mut<T: 'static>(cx: ScopeState<'_>, make_value: impl FnOnce() -> T) -> SignalMut<'_, T> {
let hooks = unsafe { &mut *cx.hooks.get() };
let idx = cx.hook_idx.get();
cx.hook_idx.set(idx + 1);
let any = if idx >= hooks.len() {
let state = MutState {
value: make_value(),
generation: Cell::new(0),
};
hooks.push(Box::new(state));
hooks.last_mut().unwrap()
} else {
hooks.get_mut(idx).unwrap()
};
let state: &mut MutState<T> = any.downcast_mut().unwrap();
SignalMut {
ptr: unsafe { NonNull::new_unchecked(&mut state.value as *mut _) },
scope_key: Runtime::current().current_key.get(),
generation: &state.generation,
_marker: PhantomData,
}
}
/// Use a callback function.
/// The returned function will be updated to `f` whenever this component is re-composed.
pub fn use_callback<'a, T, R>(
cx: ScopeState<'a>,
f: impl FnMut(T) -> R + 'a,
) -> &'a Rc<dyn Fn(T) -> R + 'a>
where
T: 'static,
R: 'static,
{
let f_cell: Option<Box<dyn FnMut(T) -> R + 'a>> = Some(Box::new(f));
let mut f_cell: Option<Box<dyn FnMut(T) -> R>> = unsafe { mem::transmute(f_cell) };
let callback = use_ref(cx, || Rc::new(RefCell::new(f_cell.take().unwrap()))).clone();
if let Some(f) = f_cell {
*callback.borrow_mut() = f;
}
use_ref(cx, move || {
let f = callback.clone();
Rc::new(move |input| f.borrow_mut()(input)) as Rc<dyn Fn(T) -> R>
})
}
#[derive(Error)]
/// Error for a missing context.
pub struct ContextError<T> {
_marker: PhantomData<T>,
}
impl<T> Clone for ContextError<T> {
fn clone(&self) -> Self {
*self
}
}
impl<T> Copy for ContextError<T> {}
impl<T> fmt::Debug for ContextError<T> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_tuple("ContextError")
.field(&core::any::type_name::<T>())
.finish()
}
}
impl<T> fmt::Display for ContextError<T> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(&format!(
"Context value not found for type: {}",
core::any::type_name::<T>()
))
}
}
/// Use a context value of type `T`.
///
/// This context must have already been provided by a parent composable with [`use_provider`],
/// otherwise this function will return a [`ContextError`].
pub fn use_context<T: 'static>(cx: ScopeState<'_>) -> Result<&Rc<T>, ContextError<T>> {
let result = use_ref(cx, || {
let Some(any) = cx.contexts.borrow().values.get(&TypeId::of::<T>()).cloned() else {
return Err(ContextError {
_marker: PhantomData,
});
};
let value: Rc<T> = Rc::downcast(any).unwrap();
Ok(value)
});
result.as_ref().map_err(|e| *e)
}
/// Provide a context value of type `T`.
///
/// This value will be available to [`use_context`] to all children of this composable.
pub fn use_provider<T: 'static>(cx: ScopeState<'_>, make_value: impl FnOnce() -> T) -> &Rc<T> {
use_ref(cx, || {
let value = Rc::new(make_value());
cx.child_contexts
.borrow_mut()
.values
.insert(TypeId::of::<T>(), value.clone());
value
})
}
/// Generational reference.
/// This can be used to compare expensive values by pointer equality.
///
/// This trait is implemented for:
/// - [`Signal`]
/// - [`Map`]
/// - [`SignalMut`]
pub trait Generational {
/// Get the current generation of this value.
fn generation(self) -> u64;
}
impl<T> Generational for Signal<'_, T> {
fn generation(self) -> u64 {
// Safety: This pointer is valid for `'a`.
unsafe { &*self.generation }.get()
}
}
impl<T> Generational for Map<'_, T> {
fn generation(self) -> u64 {
// Safety: This pointer is valid for `'a`.
unsafe { &*self.generation }.get()
}
}
impl<T> Generational for SignalMut<'_, T> {
fn generation(self) -> u64 {
// Safety: This pointer is valid for `'a`.
unsafe { &*self.generation }.get()
}
}
/// Use an effect that will run whenever the provided dependency is changed.
pub fn use_effect<D, T>(cx: ScopeState<'_>, dependency: D, effect: impl FnOnce(&D))
where
D: PartialEq + Send + 'static,
{
let mut dependency_cell = Some(dependency);
let last_mut = use_mut(cx, || dependency_cell.take().unwrap());
if let Some(dependency) = dependency_cell.take() {
if dependency != *last_mut {
effect(&dependency);
SignalMut::set(last_mut, dependency);
}
} else {
effect(&last_mut);
}
}
/// Use a memoized value of type `T` with a dependency of type `D`.
///
/// `make_value` will update the returned value whenver `dependency` is changed.
pub fn use_memo<D, T>(
cx: ScopeState<'_>,
dependency: D,
make_value: impl FnOnce() -> T,
) -> Signal<'_, T>
where
D: PartialEq + Send + 'static,
T: Send + 'static,
{
let mut dependency_cell = Some(dependency);
let mut make_value_cell = Some(make_value);
let value_mut = use_mut(cx, || make_value_cell.take().unwrap()());
let last_mut = use_mut(cx, || dependency_cell.take().unwrap());
if let Some(make_value) = make_value_cell {
if let Some(dependency) = dependency_cell.take() {
if dependency != *last_mut {
let value = make_value();
SignalMut::with(value_mut, move |update| *update = value);
SignalMut::with(last_mut, move |dst| *dst = dependency);
}
}
}
SignalMut::as_ref(value_mut)
}
/// Use a function that will be called when this scope is dropped.
pub fn use_drop<'a>(cx: ScopeState<'a>, f: impl FnOnce() + 'a) {
let mut f_cell = Some(f);
let cell = use_ref(cx, || {
let f: Box<dyn FnOnce()> = Box::new(f_cell.take().unwrap());
// Safety `f` is guranteed to live as long as `cx`.
let f: Box<dyn FnOnce()> = unsafe { mem::transmute(f) };
RefCell::new(Some(f))
});
let idx = cx.hook_idx.get();
use_ref(cx, || {
cx.drops.borrow_mut().push(idx);
let f: Box<dyn FnMut()> = Box::new(move || {
cell.borrow_mut().take().unwrap()();
});
// Safety `f` is guranteed to live as long as `cx`.
let f: Box<dyn FnMut()> = unsafe { mem::transmute(f) };
f
});
if let Some(f) = f_cell {
let f: Box<dyn FnOnce()> = Box::new(f);
// Safety `f` is guranteed to live as long as `cx`.
let f: Box<dyn FnOnce()> = unsafe { mem::transmute(f) };
*cell.borrow_mut() = Some(f);
}
}
/// Use a local task that runs on the current thread.
///
/// This will run on the window event loop, polling the task until it completes.
///
/// # Examples
///
/// Sending child state to parents.
///
/// ```
/// use actuate::prelude::*;
/// use tokio::sync::mpsc;
/// use std::cell::Cell;
///
/// #[derive(Data)]
/// struct Child<'a> {
/// idx: usize,
/// tx: &'a mpsc::UnboundedSender<usize>,
/// }
///
/// impl Compose for Child<'_> {
/// fn compose(cx: Scope<Self>) -> impl Compose {
/// cx.me().tx.send(cx.me().idx).unwrap();
/// }
/// }
///
/// #[derive(Data)]
/// struct App;
///
/// impl Compose for App {
/// fn compose(cx: Scope<Self>) -> impl Compose {
/// let (tx, ref rx_cell) = use_ref(&cx, || {
/// let (tx, rx) = mpsc::unbounded_channel();
/// (tx, Cell::new(Some(rx)))
/// });
///
/// use_local_task(&cx, move || async move {
/// let mut rx = rx_cell.take().unwrap();
/// while let Some(id) = rx.recv().await {
/// dbg!("Composed: {}", id);
/// }
/// });
///
/// (
/// Child { idx: 0, tx },
/// Child { idx: 1, tx }
/// )
/// }
/// }
/// ```
pub fn use_local_task<'a, F>(cx: ScopeState<'a>, make_task: impl FnOnce() -> F)
where
F: Future<Output = ()> + 'a,
{
let key = *use_ref(cx, || {
let task: Pin<Box<dyn Future<Output = ()>>> = Box::pin(make_task());
let task: Pin<Box<dyn Future<Output = ()>>> = unsafe { mem::transmute(task) };
let rt = Runtime::current();
let key = rt.tasks.borrow_mut().insert(task);
rt.task_queue.push(key);
key
});
use_drop(cx, move || {
Runtime::current().tasks.borrow_mut().remove(key);
})
}
#[cfg(feature = "executor")]
type BoxedFuture = Pin<Box<dyn Future<Output = ()> + Send>>;
#[cfg(feature = "executor")]
struct TaskFuture {
task: alloc::sync::Arc<std::sync::Mutex<Option<BoxedFuture>>>,
rt: Runtime,
}
#[cfg(feature = "executor")]
impl Future for TaskFuture {
type Output = ();
fn poll(
mut self: Pin<&mut Self>,
cx: &mut std::task::Context,
) -> std::task::Poll<Self::Output> {
let me = &mut *self;
// Lock the guard on this task.
// This is to ensure the scope for this task is not dropped while polling.
let mut guard = me.task.lock().unwrap();
if let Some(task) = &mut *guard {
me.rt.enter();
let _guard = Box::pin(me.rt.lock.read()).as_mut().poll(cx);
task.as_mut().poll(cx)
} else {
// The scope is dropped, we must complete this task early.
std::task::Poll::Ready(())
}
}
}
#[cfg(feature = "executor")]
unsafe impl Send for TaskFuture {}
#[cfg(feature = "executor")]
#[cfg_attr(docsrs, doc(cfg(feature = "executor")))]
/// Use a multi-threaded task that runs on a separate thread.
///
/// This will run on the current [`Executor`](`crate::executor::Executor`), polling the task until it completes.
///
/// # Examples
///
/// ```
/// use actuate::prelude::*;
/// use bevy::prelude::*;
/// use serde::Deserialize;
/// use std::collections::HashMap;
///
/// // Dog breed composable.
/// #[derive(Data)]
/// struct Breed {
/// name: String,
/// families: Vec<String>,
/// }
///
/// impl Compose for Breed {
/// fn compose(cx: Scope<Self>) -> impl Compose {
/// container((
/// text::headline(cx.me().name.to_owned()),
/// compose::from_iter(cx.me().families.clone(), |family| {
/// text::label(family.to_string())
/// }),
/// ))
/// }
/// }
///
/// #[derive(Deserialize)]
/// struct Response {
/// message: HashMap<String, Vec<String>>,
/// }
///
/// // Dog breed list composable.
/// #[derive(Data)]
/// struct BreedList;
///
/// impl Compose for BreedList {
/// fn compose(cx: Scope<Self>) -> impl Compose {
/// let breeds = use_mut(&cx, HashMap::new);
///
/// // Spawn a task that loads dog breeds from an HTTP API.
/// use_task(&cx, move || async move {
/// let json: Response = reqwest::get("https://dog.ceo/api/breeds/list/all")
/// .await
/// .unwrap()
/// .json()
/// .await
/// .unwrap();
///
/// SignalMut::set(breeds, json.message);
/// });
///
/// // Render the currently loaded breeds.
/// scroll_view(compose::from_iter((*breeds).clone(), |breed| Breed {
/// name: breed.0.clone(),
/// families: breed.1.clone(),
/// }))
/// .flex_gap(Val::Px(30.))
/// }
/// }
/// ```
pub fn use_task<'a, F>(cx: ScopeState<'a>, make_task: impl FnOnce() -> F)
where
F: Future<Output = ()> + Send + 'a,
{
let runtime_cx = use_context::<executor::ExecutorContext>(cx).unwrap();
let task_lock = use_ref(cx, || {
// Safety: `task`` is guaranteed to live as long as `cx`, and is disabled after the scope is dropped.
let task: Pin<Box<dyn Future<Output = ()> + Send>> = Box::pin(make_task());
let task: Pin<Box<dyn Future<Output = ()> + Send>> = unsafe { mem::transmute(task) };
let task_lock = std::sync::Arc::new(std::sync::Mutex::new(Some(task)));
runtime_cx.executor.spawn(Box::pin(TaskFuture {
task: task_lock.clone(),
rt: Runtime::current(),
}));
task_lock
});
// Disable this task after the scope is dropped.
use_drop(cx, || {
*task_lock.lock().unwrap() = None;
});
}
================================================
FILE: src/ui/material/button.rs
================================================
use super::{container, Theme};
use crate::{
compose::Compose,
ecs::{Modifier, Modify},
use_context, Data, Scope, Signal,
};
use bevy_color::Color;
use bevy_ui::{BorderRadius, Node, UiRect, Val};
/// Create a material UI button.
pub fn button<'a, C>(content: C) -> Button<'a, C> {
Button {
content,
background_color: None,
elevation: 0.,
height: Val::Px(40.),
padding: UiRect::left(Val::Px(24.)).with_right(Val::Px(24.)),
modifier: Modifier::default(),
}
}
/// Material UI button.
#[derive(Clone, Debug, Data)]
#[actuate(path = "crate")]
pub struct Button<'a, C> {
content: C,
background_color: Option<Color>,
padding: UiRect,
height: Val,
elevation: f32,
modifier: Modifier<'a>,
}
impl<'a, C> Button<'a, C> {
/// Set the background color of this button.
pub fn background_color(mut self, background_color: Color) -> Self {
self.background_color = Some(background_color);
self
}
/// Set the elevation of this button.
pub fn elevation(mut self, elevation: f32) -> Self {
self.elevation = elevation;
self
}
/// Set the padding of this button.
pub fn padding(mut self, padding: UiRect) -> Self {
self.padding = padding;
self
}
}
impl<C: Compose> Compose for Button<'_, C> {
fn compose(cx: Scope<Self>) -> impl Compose {
let theme = use_context::<Theme>(&cx).cloned().unwrap_or_default();
container(unsafe { Signal::map_unchecked(cx.me(), |me| &me.content) })
.background_color(cx.me().background_color.unwrap_or(theme.colors.primary))
.border_radius(
BorderRadius::all(Val::Px(10.))
.with_left(Val::Px(20.))
.with_right(Val::Px(20.)),
)
.on_insert(move |mut entity| {
let mut node = entity.get_mut::<Node>().unwrap();
node.height = cx.me().height;
})
.append(Signal::map(cx.me(), |me| &me.modifier).into())
}
}
impl<'a, C: Compose> Modify<'a> for Button<'a, C> {
fn modifier(&mut self) -> &mut Modifier<'a> {
&mut self.modifier
}
}
================================================
FILE: src/ui/material/container.rs
================================================
use super::Theme;
use crate::{
compose::Compose,
ecs::spawn,
ecs::{Modifier, Modify},
use_context, Data, Scope, Signal,
};
use bevy_color::Color;
use bevy_ui::{
AlignItems, BackgroundColor, BorderRadius, BoxShadow, FlexDirection, JustifyContent, Node,
Overflow, UiRect, Val,
};
/// Create a material UI container.
pub fn container<'a, C>(content: C) -> Container<'a, C> {
Container {
content,
elevation: 0.,
padding: UiRect::all(Val::Px(12.))
.with_left(Val::Px(24.))
.with_right(Val::Px(24.)),
background_color: None,
border_radius: BorderRadius::all(Val::Px(12.)),
modifier: Modifier::default(),
}
}
/// Material UI container.
///
/// For more see [`container`].
#[derive(Clone, Debug, Data)]
#[actuate(path = "crate")]
pub struct Container<'a, C> {
content: C,
padding: UiRect,
elevation: f32,
modifier: Modifier<'a>,
background_color: Option<Color>,
border_radius: BorderRadius,
}
impl<'a, C> Container<'a, C> {
/// Set the background color of this button.
pub fn background_color(mut self, background_color: Color) -> Self {
self.background_color = Some(background_color);
self
}
/// Set the border radius of this button.
pub fn border_radius(mut self, border_radius: BorderRadius) -> Self {
self.border_radius = border_radius;
self
}
/// Set the elevation of this button.
pub fn elevation(mut self, elevation: f32) -> Self {
self.elevation = elevation;
self
}
/// Set the padding of this button.
pub fn padding(mut self, padding: UiRect) -> Self {
self.padding = padding;
self
}
}
impl<C: Compose> Compose for Container<'_, C> {
fn compose(cx: Scope<Self>) -> impl Compose {
let theme = use_context::<Theme>(&cx).cloned().unwrap_or_default();
cx.me()
.modifier
.apply(spawn((
Node {
flex_direction: FlexDirection::Column,
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
padding: cx.me().padding,
overflow: Overflow::clip(),
..Default::default()
},
cx.me().border_radius,
BackgroundColor(
cx.me()
.background_color
.unwrap_or(theme.colors.surface_container),
),
BoxShadow::new(
Color::srgba(0., 0., 0., 0.12 * cx.me().elevation),
Val::Px(0.),
Val::Px(1.),
Val::Px(0.),
Val::Px(3. * cx.me().elevation),
),
)))
.content(unsafe { Signal::map_unchecked(cx.me(), |me| &me.content) })
}
}
impl<'a, C: Compose> Modify<'a> for Container<'a, C> {
fn modifier(&mut self) -> &mut Modifier<'a> {
&mut self.modifier
}
}
================================================
FILE: src/ui/material/mod.rs
================================================
use bevy_color::Color;
use std::ops::Index;
mod button;
pub use self::button::{button, Button};
mod container;
pub use self::container::{container, Container};
mod radio;
pub use self::radio::{radio_button, RadioButton};
mod ui;
pub use self::ui::{material_ui, MaterialUi};
/// Text composables.
pub mod text;
/// Colors for a [`MaterialTheme`].
#[derive(Clone, PartialEq)]
pub struct Colors {
/// Background color.
pub background: Color,
/// Primary color.
pub primary: Color,
/// Surface container color.
pub surface_container: Color,
/// Text color.
pub text: Color,
}
/// Typography style.
#[derive(Clone, PartialEq)]
pub struct TypographyStyle {
/// Font size.
pub font_size: f32,
/// Font weight.
pub font_weight: f32,
/// Line height.
pub line_height: f32,
}
/// Typography style kind.
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum TypographyStyleKind {
/// Small typography style.
Small,
/// Medium typography style.
Medium,
/// Large typography style.
Large,
}
/// Typography design token.
#[derive(Clone, PartialEq)]
pub struct TypographyToken {
/// Small typography style.
pub small: TypographyStyle,
/// Medium typography style.
pub medium: TypographyStyle,
/// Large typography style.
pub large: TypographyStyle,
}
impl Index<TypographyStyleKind> for TypographyToken {
type Output = TypographyStyle;
fn index(&self, index: TypographyStyleKind) -> &Self::Output {
match index {
TypographyStyleKind::Small => &self.small,
TypographyStyleKind::Medium => &self.medium,
TypographyStyleKind::Large => &self.large,
}
}
}
/// Typography kind.
#[derive(Clone, Copy)]
pub enum TypographyKind {
/// Body typography.
Body,
/// Headline typography.
Headline,
/// Label typography.
Label,
/// Title typography.
Title,
}
/// Typography for a [`MaterialTheme`].
#[derive(Clone, PartialEq)]
pub struct Typography {
/// Body typography.
pub body: TypographyToken,
/// Headline typography.
pub headline: TypographyToken,
/// Label typography.
pub label: TypographyToken,
/// Title typography.
pub title: TypographyToken,
}
impl Index<TypographyKind> for Typography {
type Output = TypographyToken;
fn index(&self, index: TypographyKind) -> &Self::Output {
match index {
TypographyKind::Body => &self.body,
TypographyKind::Headline => &self.headline,
TypographyKind::Label => &self.label,
TypographyKind::Title => &self.title,
}
}
}
/// Material UI theme.
#[derive(Clone, PartialEq)]
pub struct Theme {
/// Theme colors.
pub colors: Colors,
/// Theme typography.
pub typography: Typography,
}
impl Default for Theme {
fn default() -> Self {
Self {
colors: Colors {
background: Color::WHITE,
primary: Color::srgb_u8(103, 80, 164),
surface_container: Color::srgb_u8(230, 224, 233),
text: Color::BLACK,
},
typography: Typography {
body: TypographyToken {
small: TypographyStyle {
font_size: 12.,
font_weight: 400.,
line_height: 16.,
},
medium: TypographyStyle {
font_size: 14.,
font_weight: 400.,
line_height: 20.,
},
large: TypographyStyle {
font_size: 16.,
font_weight: 400.,
line_height: 24.,
},
},
headline: TypographyToken {
small: TypographyStyle {
font_size: 24.,
font_weight: 400.,
line_height: 32.,
},
medium: TypographyStyle {
font_size: 28.,
font_weight: 400.,
line_height: 36.,
},
large: TypographyStyle {
font_size: 32.,
font_weight: 400.,
line_height: 40.,
},
},
label: TypographyToken {
small: TypographyStyle {
font_size: 11.,
font_weight: 500.,
line_height: 16.,
},
medium: TypographyStyle {
font_size: 12.,
font_weight: 500.,
line_height: 16.,
},
large: TypographyStyle {
font_size: 14.,
font_weight: 500.,
line_height: 20.,
},
},
title: TypographyToken {
small: TypographyStyle {
font_size: 14.,
font_weight: 500.,
line_height: 20.,
},
medium: TypographyStyle {
font_size: 16.,
font_weight: 500.,
line_height: 24.,
},
large: TypographyStyle {
font_size: 22.,
font_weight: 400.,
line_height: 28.,
},
},
},
}
}
}
================================================
FILE: src/ui/material/radio.rs
================================================
use super::Theme;
use crate::{
compose::Compose,
ecs::spawn,
ecs::{Modifier, Modify},
use_context, Data, Scope,
};
use bevy_color::Color;
use bevy_ui::{BackgroundColor, BorderColor, BorderRadius, BoxShadow, Node, UiRect, Val};
/// Create a material UI radio button.
pub fn radio_button<'a>() -> RadioButton<'a> {
RadioButton {
is_enabled: true,
inner_radius: 10.,
outer_radius: 20.,
border_width: 2.,
elevation: 0.,
modifier: Modifier::default(),
}
}
/// Material UI radio button.
#[derive(Clone, Debug, Data)]
#[actuate(path = "crate")]
pub struct RadioButton<'a> {
is_enabled: bool,
inner_radius: f32,
outer_radius: f32,
border_width: f32,
elevation: f32,
modifier: Modifier<'a>,
}
impl RadioButton<'_> {
/// Set the enabled state of this radio button.
pub fn is_enabled(mut self, is_enabled: bool) -> Self {
self.is_enabled = is_enabled;
self
}
/// Set the inner radius of this radio button.
pub fn inner_radius(mut self, inner_radius: f32) -> Self {
self.inner_radius = inner_radius;
self
}
/// Set the outer radius of this radio button.
pub fn outer_radius(mut self, outer_radius: f32) -> Self {
self.outer_radius = outer_radius;
self
}
/// Set the border width of this radio button.
pub fn border_width(mut self, border_width: f32) -> Self {
self.border_width = border_width;
self
}
/// Set the elevation of this radio button.
pub fn elevation(mut self, elevation: f32) -> Self {
self.elevation = elevation;
self
}
}
impl Compose for RadioButton<'_> {
fn compose(cx: Scope<Self>) -> impl Compose {
let theme = use_context::<Theme>(&cx).cloned().unwrap_or_default();
let size = Val::Px(cx.me().outer_radius * 2.);
let inner_size = Val::Px(cx.me().inner_radius * 2.);
let offset = Val::Px((cx.me().outer_radius - cx.me().inner_radius) - 2.);
cx.me()
.modifier
.apply(spawn((
Node {
width: size,
height: size,
border: UiRect::all(Val::Px(cx.me().border_width)),
..Default::default()
},
BorderRadius::MAX,
BorderColor::all(theme.colors.primary),
BoxShadow::new(
Color::srgba(0., 0., 0., 0.12 * cx.me().elevation),
Val::Px(0.),
Val::Px(1.),
Val::Px(0.),
Val::Px(3. * cx.me().elevation),
),
)))
.content(if cx.me().is_enabled {
Some(spawn((
Node {
width: inner_size,
height: inner_size,
top: offset,
left: offset,
..Default::default()
},
BackgroundColor(theme.colors.primary),
BorderRadius::MAX,
)))
} else {
None
})
}
}
impl<'a> Modify<'a> for RadioButton<'a> {
fn modifier(&mut self) -> &mut Modifier<'a> {
&mut self.modifier
}
}
================================================
FILE: src/ui/material/text.rs
================================================
use super::{Theme, TypographyKind, TypographyStyleKind};
use crate::{
ecs::{spawn, Modifier, Modify},
prelude::Compose,
use_context,
};
use actuate_macros::Data;
use bevy_text::{TextColor, TextFont};
use bevy_ui::prelude::Text as UiText;
/// Create a material UI text body.
pub fn body<'a>(content: impl Into<String>) -> Text<'a> {
text(content).typography(TypographyKind::Body)
}
/// Create a material UI text headline.
pub fn headline<'a>(content: impl Into<String>) -> Text<'a> {
text(content).typography(TypographyKind::Headline)
}
/// Create a material UI text label.
pub fn label<'a>(content: impl Into<String>) -> Text<'a> {
text(content).typography(TypographyKind::Label)
}
/// Create a material UI text title.
pub fn title<'a>(content: impl Into<String>) -> Text<'a> {
text(content).typography(TypographyKind::Title)
}
/// Create a material UI text label.
pub fn text<'a>(content: impl Into<String>) -> Text<'a> {
Text {
content: content.into(),
modifier: Modifier::default(),
typography: TypographyKind::Label,
typography_style: TypographyStyleKind::Medium,
}
}
/// Material UI text composable.
#[derive(Data)]
#[actuate(path = "crate")]
pub struct Text<'a> {
content: String,
typography: TypographyKind,
typography_style: TypographyStyleKind,
modifier: Modifier<'a>,
}
impl Text<'_> {
/// Set the typography of this text.
pub fn typography(mut self, typography: TypographyKind) -> Self {
self.typography = typography;
self
}
/// Set the typography style of this text.
pub fn typography_style(mut self, typography_style: TypographyStyleKind) -> Self {
self.typography_style = typography_style;
self
}
}
impl Compose for Text<'_> {
fn compose(cx: crate::Scope<Self>) -> impl Compose {
let theme = use_context::<Theme>(&cx).cloned().unwrap_or_default();
let style = &theme.typography[cx.me().typography][cx.me().typography_style];
spawn((
UiText::new(cx.me().content.clone()),
TextColor(theme.colors.text),
TextFont {
font_size: style.font_size,
..Default::default()
},
))
}
}
impl<'a> Modify<'a> for Text<'a> {
fn modifier(&mut self) -> &mut Modifier<'a> {
&mut self.modifier
}
}
================================================
FILE: src/ui/material/ui.rs
================================================
use super::Theme;
use crate::{
ecs::{spawn, Modifier, Modify},
prelude::Compose,
use_provider, Scope, Signal,
};
use actuate_macros::Data;
use bevy_ui::{BackgroundColor, FlexDirection, Node, Val};
/// Create a material UI composable.
///
/// This will provide a [`Theme`] and set the background for its content.
pub fn material_ui<'a, C: Compose>(content: C) -> MaterialUi<'a, C> {
MaterialUi {
content,
theme: Theme::default(),
modifier: Modifier::default(),
}
}
/// Material UI composable.
///
/// For more see [`material_ui`].
#[derive(Data)]
#[actuate(path = "crate")]
pub struct MaterialUi<'a, C> {
content: C,
theme: Theme,
modifier: Modifier<'a>,
}
impl<'a, C> MaterialUi<'a, C> {
/// Set the theme of this composable.
pub fn theme(mut self, theme: Theme) -> Self {
self.theme = theme;
self
}
}
impl<'a, C: Compose> Compose for MaterialUi<'a, C> {
fn compose(cx: Scope<Self>) -> impl Compose {
let theme = use_provider(&cx, || cx.me().theme.clone());
cx.me()
.modifier
.apply(spawn((
Node {
flex_direction: FlexDirection::Column,
width: Val::Percent(100.),
height: Val::Percent(100.),
..Default::default()
},
BackgroundColor(theme.colors.background),
)))
.content(unsafe { Signal::map_unchecked(cx.me(), |me| &me.content) })
}
}
impl<'a, C> Modify<'a> for MaterialUi<'a, C> {
fn modifier(&mut self) -> &mut Modifier<'a> {
&mut self.modifier
}
}
================================================
FILE: src/ui/mod.rs
================================================
use crate::{
ecs::{spawn, use_world, Modifier, Modify},
prelude::Compose,
use_mut, Scope, Signal, SignalMut,
};
use actuate_macros::Data;
use bevy_ecs::prelude::*;
use bevy_input::{
mouse::{MouseScrollUnit, MouseWheel},
prelude::*,
};
use bevy_picking::prelude::*;
use bevy_ui::prelude::*;
use std::mem;
#[cfg(feature = "material")]
#[cfg_attr(docsrs, doc(cfg(feature = "material")))]
/// Material UI.
pub mod material;
/// Create a scroll view.
pub fn scroll_view<'a, C: Compose>(content: C) -> ScrollView<'a, C> {
ScrollView {
content,
line_size: 30.,
modifier: Modifier::default(),
scroll_x: true,
scroll_y: true,
}
}
#[derive(Data)]
#[actuate(path = "crate")]
/// Scroll view composable.
pub struct ScrollView<'a, C> {
content: C,
line_size: f32,
scroll_x: bool,
scroll_y: bool,
modifier: Modifier<'a>,
}
impl<C> ScrollView<'_, C> {
/// Set the line size to scroll (default: 30).
pub fn line_size(mut self, size: f32) -> Self {
self.line_size = size;
self
}
/// Enable or disable horizontal scrolling (default: true).
pub fn scroll_x(mut self, scroll_x: bool) -> Self {
self.scroll_x = scroll_x;
self
}
/// Enable or disable vertical scrolling (default: true).
pub fn scroll_y(mut self, scroll_y: bool) -> Self {
self.scroll_y = scroll_y;
self
}
}
impl<C: Compose> Compose for ScrollView<'_, C> {
fn compose(cx: Scope<Self>) -> impl Compose {
let is_hovered = use_mut(&cx, || false);
let entity_cell = use_mut(&cx, || None);
use_world(
&cx,
move |mut mouse_wheel_events: MessageReader<MouseWheel>,
mut scrolled_node_query: Query<&mut ScrollPosition>,
keyboard_input: Res<ButtonInput<KeyCode>>| {
for mouse_wheel_event in mouse_wheel_events.read() {
let (mut dx, mut dy) = match mouse_wheel_event.unit {
MouseScrollUnit::Line => (
mouse_wheel_event.x * cx.me().line_size,
mouse_wheel_event.y * cx.me().line_size,
),
MouseScrollUnit::Pixel => (mouse_wheel_event.x, mouse_wheel_event.y),
};
if cx.me().scroll_x
&& cx.me().scroll_y
&& (keyboard_input.pressed(KeyCode::ControlLeft)
|| keyboard_input.pressed(KeyCode::ControlRight))
{
std::mem::swap(&mut dx, &mut dy)
}
if *is_hovered {
if let Some(entity) = *entity_cell {
if let Ok(mut scroll_position) = scrolled_node_query.get_mut(entity) {
if cx.me().scroll_x {
scroll_position.x -= dx;
}
if cx.me().scroll_y {
scroll_position.y -= dy;
}
}
}
}
}
},
);
let modifier = &cx.me().modifier;
let modifier: &Modifier = unsafe { mem::transmute(modifier) };
modifier
.apply(
spawn(Node {
flex_direction: FlexDirection::Column,
overflow: Overflow::scroll_y(),
..Default::default()
})
.on_spawn(move |entity| SignalMut::set(entity_cell, Some(entity.id())))
.observe(move |_: On<Pointer<Over>>| SignalMut::set(is_hovered, true))
.observe(move |_: On<Pointer<Out>>| SignalMut::set(is_hovered, false)),
)
.content(unsafe { Signal::map_unchecked(cx.me(), |me| &me.content) })
}
}
impl<'a, C: Compose> Modify<'a> for ScrollView<'a, C> {
fn modifier(&mut self) -> &mut Modifier<'a> {
&mut self.modifier
}
}
================================================
FILE: tests/composer.rs
================================================
use actuate::{
composer::{Composer, TryComposeError},
prelude::*,
};
use std::{
cell::{Cell, RefCell},
rc::Rc,
};
#[derive(Data)]
struct Counter {
x: Rc<Cell<i32>>,
}
impl Compose for Counter {
fn compose(cx: Scope<Self>) -> impl Compose {
let updater = use_mut(&cx, || ());
SignalMut::set(updater, ());
cx.me().x.set(cx.me().x.get() + 1);
}
}
#[derive(Data)]
struct NonUpdateCounter {
x: Rc<Cell<i32>>,
}
impl Compose for NonUpdateCounter {
fn compose(cx: Scope<Self>) -> impl Compose {
cx.me().x.set(cx.me().x.get() + 1);
}
}
#[test]
fn it_composes() {
#[derive(Data)]
struct Wrap {
x: Rc<Cell<i32>>,
}
impl Compose for Wrap {
fn compose(cx: Scope<Self>) -> impl Compose {
Counter {
x: cx.me().x.clone(),
}
}
}
let x = Rc::new(Cell::new(0));
let mut composer = Composer::new(Wrap { x: x.clone() });
composer.try_compose().unwrap();
assert_eq!(x.get(), 1);
composer.try_compose().unwrap();
assert_eq!(x.get(), 2);
}
#[test]
fn it_composes_depth_first() {
let a = Rc::new(Cell::new(0));
let out = a.clone();
let mut composer = Composer::new(compose::from_fn(move |_| {
a.set(0);
let b = a.clone();
let e = a.clone();
(
compose::from_fn(move |_| {
b.set(1);
let c = b.clone();
let d = b.clone();
(
compose::from_fn(move |_| c.set(2)),
compose::from_fn(move |_| d.set(3)),
)
}),
compose::from_fn(move |_| {
e.set(4);
let f = e.clone();
let g = e.clone();
(
compose::from_fn(move |_| f.set(5)),
compose::from_fn(move |_| g.set(6)),
)
}),
)
}));
composer.next().unwrap().unwrap();
assert_eq!(out.get(), 0);
// Compose (1, 4)
composer.next().unwrap().unwrap();
composer.next().unwrap().unwrap();
assert_eq!(out.get(), 1);
// Compose (2, 3)
composer.next().unwrap().unwrap();
composer.next().unwrap().unwrap();
assert_eq!(out.get(), 2);
composer.next().unwrap().unwrap();
assert_eq!(out.get(), 3);
composer.next().unwrap().unwrap();
assert_eq!(out.get(), 4);
// Compose (5, 6)
composer.next().unwrap().unwrap();
composer.next().unwrap().unwrap();
assert_eq!(out.get(), 5);
composer.next().unwrap().unwrap();
assert_eq!(out.get(), 6);
}
#[test]
fn it_skips_recomposes() {
#[derive(Data)]
struct Wrap {
x: Rc<Cell<i32>>,
}
impl Compose for Wrap {
fn compose(cx: Scope<Self>) -> impl Compose {
NonUpdateCounter {
x: cx.me().x.clone(),
}
}
}
let x = Rc::new(Cell::new(0));
let mut composer = Composer::new(Wrap { x: x.clone() });
composer.try_compose().unwrap();
assert_eq!(x.get(), 1);
assert_eq!(composer.try_compose(), Err(TryComposeError::Pending));
assert_eq!(x.get(), 1);
}
#[test]
fn it_composes_dyn_compose() {
#[derive(Data)]
struct Wrap {
x: Rc<Cell<i32>>,
}
impl Compose for Wrap {
fn compose(cx: crate::Scope<Self>) -> impl Compose {
dyn_compose(Counter {
x: cx.me().x.clone(),
})
}
}
let x = Rc::new(Cell::new(0));
let mut composer = Composer::new(Wrap { x: x.clone() });
composer.try_compose().unwrap();
assert_eq!(x.get(), 1);
composer.try_compose().unwrap();
assert_eq!(x.get(), 2);
}
#[test]
fn it_composes_from_iter() {
#[derive(Data)]
struct Wrap {
x: Rc<Cell<i32>>,
}
impl Compose for Wrap {
fn compose(cx: crate::Scope<Self>) -> impl Compose {
compose::from_iter(0..2, move |_| Counter {
x: cx.me().x.clone(),
})
}
}
let x = Rc::new(Cell::new(0));
let mut composer = Composer::new(Wrap { x: x.clone() });
composer.try_compose().unwrap();
assert_eq!(x.get(), 2);
composer.try_compose().unwrap();
assert_eq!(x.get(), 4);
}
#[test]
fn it_composes_memo() {
#[derive(Data)]
struct B {
x: Rc<RefCell<i32>>,
}
impl Compose for B {
fn compose(cx: Scope<Self>) -> impl Compose {
*cx.me().x.borrow_mut() += 1;
}
}
#[derive(Data)]
struct A {
x: Rc<RefCell<i32>>,
}
impl Compose for A {
fn compose(cx: Scope<Self>) -> impl Compose {
let x = cx.me().x.clone();
memo((), B { x })
}
}
let x = Rc::new(RefCell::new(0));
let mut composer = Composer::new(A { x: x.clone() });
composer.try_compose().unwrap();
assert_eq!(*x.borrow(), 1);
assert_eq!(composer.try_compose(), Err(TryComposeError::Pending));
assert_eq!(*x.borrow(), 1);
}
gitextract_uddujspb/
├── .gitattributes
├── .github/
│ ├── FUNDING.yml
│ └── workflows/
│ └── ci.yml
├── .gitignore
├── CHANGELOG.md
├── Cargo.toml
├── LICENSE-APACHE
├── LICENSE-MIT
├── README.md
├── examples/
│ ├── README.md
│ ├── counter.rs
│ ├── http.rs
│ ├── radio_button.rs
│ └── timer.rs
├── macros/
│ ├── CHANGELOG.md
│ ├── Cargo.toml
│ └── src/
│ └── lib.rs
├── src/
│ ├── animation.rs
│ ├── compose/
│ │ ├── catch.rs
│ │ ├── dyn_compose.rs
│ │ ├── from_fn.rs
│ │ ├── from_iter.rs
│ │ ├── memo.rs
│ │ └── mod.rs
│ ├── composer.rs
│ ├── data.rs
│ ├── ecs/
│ │ ├── mod.rs
│ │ └── spawn.rs
│ ├── executor.rs
│ ├── lib.rs
│ └── ui/
│ ├── material/
│ │ ├── button.rs
│ │ ├── container.rs
│ │ ├── mod.rs
│ │ ├── radio.rs
│ │ ├── text.rs
│ │ └── ui.rs
│ └── mod.rs
└── tests/
└── composer.rs
SYMBOL INDEX (330 symbols across 26 files)
FILE: examples/counter.rs
type Counter (line 8) | struct Counter {
method compose (line 13) | fn compose(cx: Scope<Self>) -> impl Compose {
function setup (line 31) | fn setup(mut commands: Commands) {
function main (line 45) | fn main() {
FILE: examples/http.rs
type Breed (line 10) | struct Breed {
method compose (line 16) | fn compose(cx: Scope<Self>) -> impl Compose {
type Response (line 27) | struct Response {
type BreedList (line 33) | struct BreedList;
method compose (line 36) | fn compose(cx: Scope<Self>) -> impl Compose {
type Example (line 65) | struct Example;
method compose (line 68) | fn compose(cx: Scope<Self>) -> impl Compose {
function main (line 76) | fn main() {
function setup (line 84) | fn setup(mut commands: Commands) {
FILE: examples/radio_button.rs
type Example (line 8) | struct Example;
method compose (line 11) | fn compose(cx: Scope<Self>) -> impl Compose {
function setup (line 20) | fn setup(mut commands: Commands) {
function main (line 35) | fn main() {
FILE: examples/timer.rs
type Timer (line 8) | struct Timer;
method compose (line 11) | fn compose(cx: Scope<Self>) -> impl Compose {
function main (line 24) | fn main() {
function setup (line 31) | fn setup(mut commands: Commands) {
FILE: macros/src/lib.rs
function derive_data (line 9) | pub fn derive_data(input: TokenStream) -> TokenStream {
function data (line 86) | pub fn data(_attrs: TokenStream, input: TokenStream) -> TokenStream {
FILE: src/animation.rs
type State (line 16) | struct State<T> {
function use_animated (line 24) | pub fn use_animated<T>(cx: ScopeState<'_>, make_initial: impl FnOnce() -...
type UseAnimated (line 88) | pub struct UseAnimated<'a, T> {
function animate (line 95) | pub async fn animate(&self, to: T, duration: Duration) {
function controller (line 100) | pub fn controller(&self) -> AnimationController<T> {
method clone (line 106) | fn clone(&self) -> Self {
type Target (line 114) | type Target = T;
method deref (line 116) | fn deref(&self) -> &Self::Target {
type AnimationController (line 124) | pub struct AnimationController<T> {
function animate (line 130) | pub async fn animate(&self, to: T, duration: Duration) {
method clone (line 138) | fn clone(&self) -> Self {
FILE: src/compose/catch.rs
function catch (line 42) | pub fn catch<'a, C: Compose>(
type Catch (line 57) | pub struct Catch<'a, C> {
method compose (line 66) | fn compose(cx: Scope<Self>) -> impl Compose {
FILE: src/compose/dyn_compose.rs
function dyn_compose (line 53) | pub fn dyn_compose<'a>(content: impl Compose + 'a) -> DynCompose<'a> {
type DynCompose (line 61) | pub struct DynCompose<'a> {
type DynComposeState (line 66) | struct DynComposeState {
method compose (line 72) | fn compose(cx: Scope<Self>) -> impl Compose {
FILE: src/compose/from_fn.rs
function from_fn (line 33) | pub fn from_fn<F, C>(f: F) -> FromFn<F, C>
type FromFn (line 47) | pub struct FromFn<F, C> {
method clone (line 53) | fn clone(&self) -> Self {
method compose (line 73) | fn compose(cx: Scope<Self>) -> impl Compose {
FILE: src/compose/from_iter.rs
function from_iter (line 36) | pub fn from_iter<'a, I, C>(
type FromIter (line 55) | pub struct FromIter<'a, I, Item, C> {
method clone (line 65) | fn clone(&self) -> Self {
method compose (line 87) | fn compose(cx: Scope<Self>) -> impl Compose {
type ItemState (line 152) | struct ItemState<T> {
FILE: src/compose/memo.rs
function memo (line 11) | pub fn memo<D, C>(dependency: D, content: C) -> Memo<D, C>
type Memo (line 28) | pub struct Memo<T, C> {
method compose (line 38) | fn compose(cx: Scope<Self>) -> impl Compose {
method name (line 61) | fn name() -> Option<Cow<'static, str>> {
FILE: src/compose/mod.rs
type Compose (line 49) | pub trait Compose: Data {
method compose (line 51) | fn compose(cx: Scope<Self>) -> impl Compose;
method name (line 54) | fn name() -> Option<Cow<'static, str>> {
method compose (line 69) | fn compose(cx: Scope<Self>) -> impl Compose {
method compose (line 75) | fn compose(cx: Scope<Self>) -> impl Compose {
method compose (line 180) | fn compose(cx: Scope<Self>) -> impl Compose {
method compose (line 293) | fn compose(cx: Scope<Self>) -> impl Compose {
function drop_node (line 134) | fn drop_node(nodes: &mut SlotMap<DefaultKey, Rc<Node>>, key: DefaultKey) {
type Error (line 154) | pub struct Error {
method new (line 160) | pub fn new(error: impl core::error::Error + Clone + 'static) -> Self {
method fmt (line 168) | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
method fmt (line 174) | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
type CatchContext (line 248) | pub(crate) struct CatchContext {
method new (line 253) | pub(crate) fn new(f: impl Fn(Box<dyn core::error::Error>) + 'static) -...
function use_node (line 305) | fn use_node(
type AnyCompose (line 350) | pub(crate) trait AnyCompose {
method data_id (line 351) | fn data_id(&self) -> TypeId;
method as_ptr_mut (line 353) | fn as_ptr_mut(&mut self) -> *mut ();
method reborrow (line 355) | unsafe fn reborrow(&mut self, ptr: *mut ());
method any_compose (line 358) | unsafe fn any_compose(&self, state: &ScopeData);
method name (line 360) | fn name(&self) -> Option<Cow<'static, str>>;
method data_id (line 367) | fn data_id(&self) -> TypeId {
method as_ptr_mut (line 371) | fn as_ptr_mut(&mut self) -> *mut () {
method reborrow (line 375) | unsafe fn reborrow(&mut self, ptr: *mut ()) {
method any_compose (line 379) | unsafe fn any_compose(&self, state: &ScopeData) {
method name (line 456) | fn name(&self) -> Option<Cow<'static, str>> {
FILE: src/composer.rs
type RuntimeFuture (line 23) | type RuntimeFuture = Pin<Box<dyn Future<Output = ()>>>;
type ComposePtr (line 25) | pub(crate) enum ComposePtr {
method data_id (line 31) | fn data_id(&self) -> TypeId {
method as_ptr_mut (line 38) | fn as_ptr_mut(&mut self) -> *mut () {
method reborrow (line 45) | unsafe fn reborrow(&mut self, ptr: *mut ()) {
method any_compose (line 52) | unsafe fn any_compose(&self, state: &ScopeData) {
method name (line 59) | fn name(&self) -> Option<std::borrow::Cow<'static, str>> {
type Node (line 68) | pub(crate) struct Node {
type Runtime (line 78) | pub(crate) struct Runtime {
method current (line 108) | pub fn current() -> Self {
method enter (line 119) | pub fn enter(&self) {
method update (line 126) | pub fn update(&self, f: impl FnOnce() + Send + 'static) {
method pending (line 145) | pub fn pending(&self, key: DefaultKey) -> Pending {
method queue (line 162) | pub fn queue(&self, key: DefaultKey) {
type TaskWaker (line 172) | struct TaskWaker {
method wake (line 179) | fn wake(self: Arc<Self>) {
type TryComposeError (line 189) | pub enum TryComposeError {
method eq (line 198) | fn eq(&self, other: &Self) -> bool {
type Pending (line 204) | pub(crate) struct Pending {
method partial_cmp (line 210) | fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
method cmp (line 216) | fn cmp(&self, other: &Self) -> Ordering {
type Composer (line 262) | pub struct Composer {
method new (line 271) | pub fn new(content: impl Compose + 'static) -> Self {
method try_compose (line 307) | pub fn try_compose(&mut self) -> Result<(), TryComposeError> {
method poll_compose (line 324) | pub fn poll_compose(&mut self, cx: &mut Context) -> Poll<Result<(), Bo...
method compose (line 335) | pub async fn compose(&mut self) -> Result<(), Box<dyn Error>> {
method fmt (line 417) | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
method drop (line 341) | fn drop(&mut self) {
function drop_recursive (line 347) | fn drop_recursive(rt: &Runtime, key: DefaultKey, node: Rc<Node>) {
type Item (line 358) | type Item = Result<(), Box<dyn Error>>;
method next (line 360) | fn next(&mut self) -> Option<Self::Item> {
type Field (line 426) | struct Field<'a> {
function fmt (line 433) | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
function dbg_composer (line 444) | fn dbg_composer(
FILE: src/data.rs
type Data (line 72) | pub unsafe trait Data {}
type FieldWrap (line 126) | pub struct FieldWrap<T>(pub T);
type FnField (line 129) | pub unsafe trait FnField<Marker> {
method check (line 130) | fn check(&self) {
type DataField (line 154) | pub unsafe trait DataField {
method check (line 155) | fn check(&self) {
type StaticField (line 163) | pub unsafe trait StaticField {
method check (line 164) | fn check(&self) {
FILE: src/ecs/mod.rs
type ActuatePlugin (line 52) | pub struct ActuatePlugin;
method build (line 55) | fn build(&self, app: &mut App) {
type UpdateFn (line 65) | type UpdateFn = Box<dyn FnMut(&mut World)>;
type WorldListenerFn (line 67) | type WorldListenerFn = Rc<dyn Fn(&mut World)>;
type Inner (line 69) | struct Inner {
type RuntimeContext (line 77) | struct RuntimeContext {
method current (line 82) | fn current() -> Self {
method world_mut (line 92) | unsafe fn world_mut(&self) -> &'static mut World {
type RuntimeComposer (line 101) | struct RuntimeComposer {
type Runtime (line 105) | struct Runtime {
type Composition (line 110) | pub struct Composition<C> {
function new (line 120) | pub fn new(content: C) -> Self {
function target (line 130) | pub fn target(&self) -> Option<Entity> {
function set_target (line 137) | pub fn set_target(&mut self, target: Option<Entity>) {
function with_target (line 144) | pub fn with_target(mut self, target: Entity) -> Self {
constant STORAGE_TYPE (line 154) | const STORAGE_TYPE: StorageType = StorageType::SparseSet;
type Mutability (line 156) | type Mutability = Mutable;
method on_insert (line 158) | fn on_insert() -> Option<bevy_ecs::lifecycle::ComponentHook> {
type CompositionContent (line 181) | struct CompositionContent<C> {
method compose (line 187) | fn compose(cx: Scope<Self>) -> impl Compose {
type RuntimeWaker (line 197) | struct RuntimeWaker {
method wake (line 202) | fn wake(self: Arc<Self>) {
function compose (line 207) | fn compose(world: &mut World) {
type SystemParamFunction (line 260) | pub trait SystemParamFunction<Marker> {
method run (line 271) | fn run(&mut self, input: Self::In, param_value: SystemParamItem<Self::...
type Wrap (line 275) | pub struct Wrap<T>(T);
function use_world (line 385) | pub fn use_world<'a, Marker, F>(cx: ScopeState<'a>, mut with_world: F)
type SystemParamFunctionOnce (line 427) | pub trait SystemParamFunctionOnce<Marker> {
method run (line 435) | fn run(self, param: <Self::Param as SystemParam>::Item<'_, '_>) -> Sel...
function use_world_once (line 459) | pub fn use_world_once<Marker, F>(cx: ScopeState<'_>, with_world: F) -> &...
type UseCommands (line 475) | pub struct UseCommands {
method push (line 481) | pub fn push<C>(&mut self, command: C)
function use_commands (line 490) | pub fn use_commands(cx: ScopeState<'_>) -> &UseCommands {
type SpawnContext (line 497) | struct SpawnContext {
function use_bundle (line 505) | pub fn use_bundle<B: Bundle>(cx: ScopeState<'_>, make_bundle: impl FnOnc...
function use_bundle_inner (line 516) | fn use_bundle_inner(
type Modifier (line 544) | pub struct Modifier<'a> {
function apply (line 550) | pub fn apply(&self, spawn: Spawn<'a>) -> Spawn<'a> {
function append (line 557) | pub fn append(&mut self, modifier: Cow<'a, Modifier>) {
function fmt (line 565) | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
type Modify (line 611) | pub trait Modify<'a> {
method modifier (line 613) | fn modifier(&mut self) -> &mut Modifier<'a>;
method modify (line 616) | fn modify(mut self, f: impl Fn(Spawn<'a, ()>) -> Spawn<'a, ()> + 'a) -...
method append (line 625) | fn append(mut self, modifier: Cow<'a, Modifier>) -> Self
method on_insert (line 634) | fn on_insert<F>(self, f: F) -> Self
method flex_gap (line 652) | fn flex_gap(self, gap: Val) -> Self
method observe (line 709) | fn observe<F, E, B, Marker>(self, observer: F) -> Self
FILE: src/ecs/spawn.rs
function spawn (line 38) | pub fn spawn<'a, B>(bundle: B) -> Spawn<'a>
type SpawnFn (line 59) | type SpawnFn = Rc<dyn Fn(&mut World, &mut Option<Entity>)>;
type ObserverFn (line 61) | type ObserverFn<'a> = Rc<dyn Fn(&mut EntityWorldMut) + 'a>;
type OnInsertFn (line 63) | type OnInsertFn<'a> = Rc<dyn Fn(EntityWorldMut) + 'a>;
type Spawn (line 70) | pub struct Spawn<'a, C = ()> {
function target (line 84) | pub fn target(mut self, target: Entity) -> Self {
function content (line 90) | pub fn content<C2>(self, content: C2) -> Spawn<'a, C2> {
function on_spawn (line 103) | pub fn on_spawn(mut self, f: impl Fn(EntityWorldMut) + 'a) -> Self {
function on_insert (line 109) | pub fn on_insert(mut self, f: impl Fn(EntityWorldMut) + 'a) -> Self {
function observe (line 115) | pub fn observe<F, E, B, Marker>(mut self, observer: F) -> Self
method compose (line 163) | fn compose(cx: Scope<Self>) -> impl Compose {
FILE: src/executor.rs
type Executor (line 5) | pub trait Executor {
method spawn (line 7) | fn spawn(&self, future: Pin<Box<dyn Future<Output = ()> + Send>>);
method spawn (line 13) | fn spawn(&self, future: Pin<Box<dyn Future<Output = ()> + Send>>) {
type ExecutorContext (line 33) | pub struct ExecutorContext {
method new (line 46) | pub fn new(executor: impl Executor + 'static) -> Self {
method spawn (line 53) | pub fn spawn<F>(&self, future: F)
method spawn_boxed (line 61) | pub fn spawn_boxed(&self, future: Pin<Box<dyn Future<Output = ()> + Se...
method default (line 39) | fn default() -> Self {
FILE: src/lib.rs
type Cow (line 202) | pub enum Cow<'a, T> {
function to_owned (line 211) | pub fn to_owned(&self) -> T
function into_owned (line 219) | pub fn into_owned(self) -> T
method clone (line 234) | fn clone(&self) -> Self {
type Target (line 243) | type Target = T;
method deref (line 245) | fn deref(&self) -> &Self::Target {
function from (line 254) | fn from(value: RefMap<'a, T>) -> Self {
function from (line 260) | fn from(value: Signal<'a, T>) -> Self {
function from (line 266) | fn from(value: Map<'a, T>) -> Self {
function fmt (line 272) | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
type RefMap (line 284) | pub enum RefMap<'a, T> {
method clone (line 294) | fn clone(&self) -> Self {
type Target (line 302) | type Target = T;
method deref (line 304) | fn deref(&self) -> &Self::Target {
method hash (line 314) | fn hash<H: Hasher>(&self, state: &mut H) {
function from (line 320) | fn from(value: Signal<'a, T>) -> Self {
function from (line 326) | fn from(value: Map<'a, T>) -> Self {
function fmt (line 332) | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
type Map (line 342) | pub struct Map<'a, T> {
type Target (line 350) | type Target = T;
method deref (line 352) | fn deref(&self) -> &Self::Target {
type MapUnchecked (line 360) | pub struct MapUnchecked<'a, T> {
method compose (line 367) | fn compose(cx: Scope<Self>) -> impl Compose {
method name (line 373) | fn name() -> Option<std::borrow::Cow<'static, str>> {
method hash (line 379) | fn hash<H: Hasher>(&self, state: &mut H) {
type Signal (line 390) | pub struct Signal<'a, T> {
function map (line 400) | pub fn map<U>(me: Self, f: fn(&T) -> &U) -> Map<'a, U> {
function map_unchecked (line 421) | pub unsafe fn map_unchecked<U>(me: Self, f: fn(&T) -> &U) -> MapUnchecke...
type Target (line 429) | type Target = T;
method deref (line 431) | fn deref(&self) -> &Self::Target {
method hash (line 437) | fn hash<H: Hasher>(&self, state: &mut H) {
type UnsafeWrap (line 444) | struct UnsafeWrap<T: ?Sized>(T);
type SignalMut (line 451) | pub struct SignalMut<'a, T> {
function update (line 467) | pub fn update(me: Self, f: impl FnOnce(&mut T) + Send + 'static) {
function set (line 479) | pub fn set(me: Self, value: T)
function set_if_neq (line 487) | pub fn set_if_neq(me: Self, value: T)
function with (line 497) | pub fn with(me: Self, f: impl FnOnce(&mut T) + Send + 'static) {
function as_ref (line 519) | pub fn as_ref(me: Self) -> Signal<'a, T> {
type Target (line 528) | type Target = T;
method deref (line 530) | fn deref(&self) -> &Self::Target {
type Contexts (line 589) | struct Contexts {
type ScopeState (line 594) | pub type ScopeState<'a> = &'a ScopeData<'a>;
type ScopeData (line 598) | pub struct ScopeData<'a> {
method drop (line 622) | fn drop(&mut self) {
type Scope (line 632) | pub struct Scope<'a, C: ?Sized> {
function me (line 639) | pub fn me(self) -> Signal<'a, C> {
function state (line 647) | pub fn state(self) -> ScopeState<'a> {
method clone (line 653) | fn clone(&self) -> Self {
type Target (line 661) | type Target = ScopeState<'a>;
method deref (line 663) | fn deref(&self) -> &Self::Target {
function use_ref (line 671) | pub fn use_ref<T: 'static>(cx: ScopeState<'_>, make_value: impl FnOnce()...
type MutState (line 686) | struct MutState<T> {
function use_mut (line 694) | pub fn use_mut<T: 'static>(cx: ScopeState<'_>, make_value: impl FnOnce()...
function use_callback (line 722) | pub fn use_callback<'a, T, R>(
type ContextError (line 747) | pub struct ContextError<T> {
method clone (line 752) | fn clone(&self) -> Self {
function fmt (line 760) | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
function fmt (line 768) | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
function use_context (line 780) | pub fn use_context<T: 'static>(cx: ScopeState<'_>) -> Result<&Rc<T>, Con...
function use_provider (line 798) | pub fn use_provider<T: 'static>(cx: ScopeState<'_>, make_value: impl FnO...
type Generational (line 816) | pub trait Generational {
method generation (line 818) | fn generation(self) -> u64;
method generation (line 822) | fn generation(self) -> u64 {
method generation (line 829) | fn generation(self) -> u64 {
method generation (line 836) | fn generation(self) -> u64 {
function use_effect (line 843) | pub fn use_effect<D, T>(cx: ScopeState<'_>, dependency: D, effect: impl ...
function use_memo (line 865) | pub fn use_memo<D, T>(
function use_drop (line 895) | pub fn use_drop<'a>(cx: ScopeState<'a>, f: impl FnOnce() + 'a) {
function use_local_task (line 979) | pub fn use_local_task<'a, F>(cx: ScopeState<'a>, make_task: impl FnOnce(...
type BoxedFuture (line 999) | type BoxedFuture = Pin<Box<dyn Future<Output = ()> + Send>>;
type TaskFuture (line 1002) | struct TaskFuture {
type Output (line 1009) | type Output = ();
method poll (line 1011) | fn poll(
function use_task (line 1103) | pub fn use_task<'a, F>(cx: ScopeState<'a>, make_task: impl FnOnce() -> F)
FILE: src/ui/material/button.rs
function button (line 11) | pub fn button<'a, C>(content: C) -> Button<'a, C> {
type Button (line 25) | pub struct Button<'a, C> {
function background_color (line 36) | pub fn background_color(mut self, background_color: Color) -> Self {
function elevation (line 42) | pub fn elevation(mut self, elevation: f32) -> Self {
function padding (line 48) | pub fn padding(mut self, padding: UiRect) -> Self {
method compose (line 55) | fn compose(cx: Scope<Self>) -> impl Compose {
function modifier (line 74) | fn modifier(&mut self) -> &mut Modifier<'a> {
FILE: src/ui/material/container.rs
function container (line 15) | pub fn container<'a, C>(content: C) -> Container<'a, C> {
type Container (line 33) | pub struct Container<'a, C> {
function background_color (line 44) | pub fn background_color(mut self, background_color: Color) -> Self {
function border_radius (line 50) | pub fn border_radius(mut self, border_radius: BorderRadius) -> Self {
function elevation (line 56) | pub fn elevation(mut self, elevation: f32) -> Self {
function padding (line 62) | pub fn padding(mut self, padding: UiRect) -> Self {
method compose (line 69) | fn compose(cx: Scope<Self>) -> impl Compose {
function modifier (line 102) | fn modifier(&mut self) -> &mut Modifier<'a> {
FILE: src/ui/material/mod.rs
type Colors (line 21) | pub struct Colors {
type TypographyStyle (line 37) | pub struct TypographyStyle {
type TypographyStyleKind (line 50) | pub enum TypographyStyleKind {
type TypographyToken (line 63) | pub struct TypographyToken {
type Output (line 75) | type Output = TypographyStyle;
method index (line 77) | fn index(&self, index: TypographyStyleKind) -> &Self::Output {
type TypographyKind (line 88) | pub enum TypographyKind {
type Typography (line 104) | pub struct Typography {
type Output (line 119) | type Output = TypographyToken;
method index (line 121) | fn index(&self, index: TypographyKind) -> &Self::Output {
type Theme (line 133) | pub struct Theme {
method default (line 142) | fn default() -> Self {
FILE: src/ui/material/radio.rs
function radio_button (line 12) | pub fn radio_button<'a>() -> RadioButton<'a> {
type RadioButton (line 26) | pub struct RadioButton<'a> {
function is_enabled (line 37) | pub fn is_enabled(mut self, is_enabled: bool) -> Self {
function inner_radius (line 43) | pub fn inner_radius(mut self, inner_radius: f32) -> Self {
function outer_radius (line 49) | pub fn outer_radius(mut self, outer_radius: f32) -> Self {
function border_width (line 55) | pub fn border_width(mut self, border_width: f32) -> Self {
function elevation (line 61) | pub fn elevation(mut self, elevation: f32) -> Self {
method compose (line 68) | fn compose(cx: Scope<Self>) -> impl Compose {
function modifier (line 114) | fn modifier(&mut self) -> &mut Modifier<'a> {
FILE: src/ui/material/text.rs
function body (line 12) | pub fn body<'a>(content: impl Into<String>) -> Text<'a> {
function headline (line 17) | pub fn headline<'a>(content: impl Into<String>) -> Text<'a> {
function label (line 22) | pub fn label<'a>(content: impl Into<String>) -> Text<'a> {
function title (line 27) | pub fn title<'a>(content: impl Into<String>) -> Text<'a> {
function text (line 32) | pub fn text<'a>(content: impl Into<String>) -> Text<'a> {
type Text (line 44) | pub struct Text<'a> {
function typography (line 53) | pub fn typography(mut self, typography: TypographyKind) -> Self {
function typography_style (line 59) | pub fn typography_style(mut self, typography_style: TypographyStyleKind)...
method compose (line 66) | fn compose(cx: crate::Scope<Self>) -> impl Compose {
function modifier (line 83) | fn modifier(&mut self) -> &mut Modifier<'a> {
FILE: src/ui/material/ui.rs
function material_ui (line 13) | pub fn material_ui<'a, C: Compose>(content: C) -> MaterialUi<'a, C> {
type MaterialUi (line 26) | pub struct MaterialUi<'a, C> {
function theme (line 34) | pub fn theme(mut self, theme: Theme) -> Self {
method compose (line 41) | fn compose(cx: Scope<Self>) -> impl Compose {
function modifier (line 60) | fn modifier(&mut self) -> &mut Modifier<'a> {
FILE: src/ui/mod.rs
function scroll_view (line 22) | pub fn scroll_view<'a, C: Compose>(content: C) -> ScrollView<'a, C> {
type ScrollView (line 35) | pub struct ScrollView<'a, C> {
function line_size (line 45) | pub fn line_size(mut self, size: f32) -> Self {
function scroll_x (line 51) | pub fn scroll_x(mut self, scroll_x: bool) -> Self {
function scroll_y (line 57) | pub fn scroll_y(mut self, scroll_y: bool) -> Self {
method compose (line 64) | fn compose(cx: Scope<Self>) -> impl Compose {
function modifier (line 127) | fn modifier(&mut self) -> &mut Modifier<'a> {
FILE: tests/composer.rs
type Counter (line 11) | struct Counter {
method compose (line 16) | fn compose(cx: Scope<Self>) -> impl Compose {
type NonUpdateCounter (line 25) | struct NonUpdateCounter {
method compose (line 30) | fn compose(cx: Scope<Self>) -> impl Compose {
function it_composes (line 36) | fn it_composes() {
function it_composes_depth_first (line 61) | fn it_composes_depth_first() {
function it_skips_recomposes (line 128) | fn it_skips_recomposes() {
function it_composes_dyn_compose (line 153) | fn it_composes_dyn_compose() {
function it_composes_from_iter (line 178) | fn it_composes_from_iter() {
function it_composes_memo (line 203) | fn it_composes_memo() {
Condensed preview — 38 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (190K chars).
[
{
"path": ".gitattributes",
"chars": 113,
"preview": "* text=auto\n\n*.rs text eol=lf\n*.toml text eol=lf\n*.wgsl text eol=lf\n*.txt text eol=lf\n\n*.png binary\n*.ttf binary\n"
},
{
"path": ".github/FUNDING.yml",
"chars": 46,
"preview": "github: actuate-rs\nopen_collective: actuateui\n"
},
{
"path": ".github/workflows/ci.yml",
"chars": 3715,
"preview": "name: CI\n\non:\n push:\n branches: [\"main\"]\n pull_request:\n branches: [\"main\"]\n\nenv:\n CARGO_TERM_COLOR: always\n\njo"
},
{
"path": ".gitignore",
"chars": 13,
"preview": "/target\ndist/"
},
{
"path": "CHANGELOG.md",
"chars": 10408,
"preview": "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Change"
},
{
"path": "Cargo.toml",
"chars": 2296,
"preview": "[package]\nname = \"actuate\"\nversion = \"0.21.0\"\nedition = \"2021\"\nlicense = \"MIT OR Apache-2.0\"\ndescription = \"A reactive u"
},
{
"path": "LICENSE-APACHE",
"chars": 11357,
"preview": " Apache License\n Version 2.0, January 2004\n "
},
{
"path": "LICENSE-MIT",
"chars": 1074,
"preview": "MIT License\n\nCopyright (c) 2023 Matthew Hunzinger\n\nPermission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "README.md",
"chars": 3263,
"preview": "<div align=\"center\">\n <h1>Actuate</h1>\n <a href=\"https://crates.io/crates/actuate\">\n <img src=\"https://img.shields."
},
{
"path": "examples/README.md",
"chars": 176,
"preview": "## Examples\n\nExamples combining Actuate and [Bevy](https://github.com/bevyengine/bevy)\n\nYou can run these examples with:"
},
{
"path": "examples/counter.rs",
"chars": 1392,
"preview": "// Counter UI example.\n\nuse actuate::prelude::*;\nuse bevy::{prelude::*, winit::WinitSettings};\n\n// Counter composable.\n#"
},
{
"path": "examples/http.rs",
"chars": 2396,
"preview": "// HTTP UI example\n\nuse actuate::{executor::ExecutorContext, prelude::*};\nuse bevy::{prelude::*, winit::WinitSettings};\n"
},
{
"path": "examples/radio_button.rs",
"chars": 932,
"preview": "// Counter UI example.\n\nuse actuate::prelude::*;\nuse bevy::prelude::*;\n\n// Counter composable.\n#[derive(Data)]\nstruct Ex"
},
{
"path": "examples/timer.rs",
"chars": 969,
"preview": "// Timer UI example.\n\nuse actuate::prelude::*;\nuse bevy::prelude::*;\n\n// Timer composable.\n#[derive(Data)]\nstruct Timer;"
},
{
"path": "macros/CHANGELOG.md",
"chars": 1700,
"preview": "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Change"
},
{
"path": "macros/Cargo.toml",
"chars": 295,
"preview": "[package]\nname = \"actuate-macros\"\ndescription = \"Macros for Actuate\"\nversion = \"0.2.0\"\nedition = \"2021\"\nlicense = \"MIT O"
},
{
"path": "macros/src/lib.rs",
"chars": 3636,
"preview": "use proc_macro::TokenStream;\nuse quote::{format_ident, quote, ToTokens};\nuse syn::{\n parse_macro_input, parse_quote, "
},
{
"path": "src/animation.rs",
"chars": 3838,
"preview": "use crate::{\n data::Data,\n ecs::{use_world, use_world_once},\n use_local_task, use_mut, use_ref, ScopeState, Sig"
},
{
"path": "src/compose/catch.rs",
"chars": 2114,
"preview": "use super::CatchContext;\nuse crate::{compose::Compose, data::Data, use_provider, Scope, Signal};\nuse alloc::rc::Rc;\nuse "
},
{
"path": "src/compose/dyn_compose.rs",
"chars": 3708,
"preview": "use super::{drop_node, AnyCompose, Node, Runtime};\nuse crate::{compose::Compose, use_ref, Scope, ScopeData};\nuse alloc::"
},
{
"path": "src/compose/from_fn.rs",
"chars": 1421,
"preview": "use crate::{compose::Compose, Data, Scope, ScopeState};\nuse core::marker::PhantomData;\n\n/// Create a composable from a f"
},
{
"path": "src/compose/from_iter.rs",
"chars": 4176,
"preview": "use super::{AnyCompose, Node, Runtime};\nuse crate::{compose::Compose, data::Data, use_ref, Scope, ScopeData, Signal};\nus"
},
{
"path": "src/compose/memo.rs",
"chars": 1817,
"preview": "use super::{use_node, AnyCompose, Runtime};\nuse crate::{compose::Compose, composer::ComposePtr, data::Data, use_ref, Sco"
},
{
"path": "src/compose/mod.rs",
"chars": 14280,
"preview": "use crate::{\n composer::{ComposePtr, Node, Runtime},\n data::Data,\n use_context, use_ref, Scope, ScopeData, Scop"
},
{
"path": "src/composer.rs",
"chars": 12777,
"preview": "use crate::{\n compose::{AnyCompose, CatchContext, Compose},\n ScopeData,\n};\nuse alloc::{collections::BTreeSet, rc::"
},
{
"path": "src/data.rs",
"chars": 3757,
"preview": "//! Data trait and macros.\n//!\n//! # Data\n//!\n//! [`Data`] is a trait that enforces pinned references to compososition s"
},
{
"path": "src/ecs/mod.rs",
"chars": 21452,
"preview": "use crate::{\n compose::Compose,\n composer::{Composer, Pending},\n data::Data,\n use_callback, use_drop, use_pr"
},
{
"path": "src/ecs/spawn.rs",
"chars": 7464,
"preview": "use super::{use_bundle_inner, RuntimeContext, SpawnContext, SystemParamFunction};\nuse crate::{\n compose::Compose, com"
},
{
"path": "src/executor.rs",
"chars": 1690,
"preview": "use alloc::{rc::Rc, sync::Arc};\nuse core::{future::Future, pin::Pin};\n\n/// Executor for async tasks.\npub trait Executor "
},
{
"path": "src/lib.rs",
"chars": 31711,
"preview": "#![deny(missing_docs)]\n#![cfg_attr(docsrs, feature(doc_cfg))]\n#![doc(\n html_logo_url = \"https://avatars.githubusercon"
},
{
"path": "src/ui/material/button.rs",
"chars": 2212,
"preview": "use super::{container, Theme};\nuse crate::{\n compose::Compose,\n ecs::{Modifier, Modify},\n use_context, Data, Sc"
},
{
"path": "src/ui/material/container.rs",
"chars": 3075,
"preview": "use super::Theme;\nuse crate::{\n compose::Compose,\n ecs::spawn,\n ecs::{Modifier, Modify},\n use_context, Data,"
},
{
"path": "src/ui/material/mod.rs",
"chars": 5786,
"preview": "use bevy_color::Color;\nuse std::ops::Index;\n\nmod button;\npub use self::button::{button, Button};\n\nmod container;\npub use"
},
{
"path": "src/ui/material/radio.rs",
"chars": 3340,
"preview": "use super::Theme;\nuse crate::{\n compose::Compose,\n ecs::spawn,\n ecs::{Modifier, Modify},\n use_context, Data,"
},
{
"path": "src/ui/material/text.rs",
"chars": 2377,
"preview": "use super::{Theme, TypographyKind, TypographyStyleKind};\nuse crate::{\n ecs::{spawn, Modifier, Modify},\n prelude::C"
},
{
"path": "src/ui/material/ui.rs",
"chars": 1658,
"preview": "use super::Theme;\nuse crate::{\n ecs::{spawn, Modifier, Modify},\n prelude::Compose,\n use_provider, Scope, Signal"
},
{
"path": "src/ui/mod.rs",
"chars": 4162,
"preview": "use crate::{\n ecs::{spawn, use_world, Modifier, Modify},\n prelude::Compose,\n use_mut, Scope, Signal, SignalMut,"
},
{
"path": "tests/composer.rs",
"chars": 5052,
"preview": "use actuate::{\n composer::{Composer, TryComposeError},\n prelude::*,\n};\nuse std::{\n cell::{Cell, RefCell},\n r"
}
]
About this extraction
This page contains the full source code of the actuate-rs/actuate GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 38 files (177.4 KB), approximately 46.0k tokens, and a symbol index with 330 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.