-
Notifications
You must be signed in to change notification settings - Fork 42
First pass at C++ docs. #671
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
Tod-Rive
wants to merge
22
commits into
main
Choose a base branch
from
cplusplus
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
848f030
Initial generated C++
Tod-Rive 0186339
Organizational changes.
Tod-Rive 9e7c72d
Replaced ascii graphics with images.
Tod-Rive 6808523
Moved and updated images.
Tod-Rive dedb684
Potential fix for pull request finding
Tod-Rive d1ca176
Potential fix for pull request finding
Tod-Rive 625acab
Potential fix for pull request finding
Tod-Rive fab2231
Potential fix for pull request finding
Tod-Rive 49145c5
Potential fix for pull request finding
Tod-Rive f5d3ea9
Potential fix for pull request finding
Tod-Rive 1d2ba3a
Potential fix for pull request finding
Tod-Rive 35844d2
Potential fix for pull request finding
Tod-Rive fda1ceb
Potential fix for pull request finding
Tod-Rive f3031e3
PR changes.
Tod-Rive 20feacc
Merge branch 'cplusplus' of https://github.com/rive-app/rive-docs int…
Tod-Rive f62426b
PR changes.
Tod-Rive 92df79c
PR Changes
Tod-Rive f3cd4d8
PR fixes
Tod-Rive bba1b43
PR changes
Tod-Rive e177a3d
Updated build instructions.
Tod-Rive cbae5a5
Update renderers.mdx
Tod-Rive 0f15858
PR changes.
Tod-Rive File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,156 @@ | ||
| --- | ||
| title: "Asset Loading" | ||
| description: "Resolve out-of-band images, fonts, and audio." | ||
| --- | ||
|
|
||
| A `.riv` file can either embed asset bytes (images, fonts, audio, scripts) **in-band** | ||
| or reference them by CDN UUID and let the runtime fetch them. Implement | ||
| `FileAssetLoader` to control the second path — resolving from disk, the | ||
| network, or your own asset pipeline. | ||
|
|
||
| ## The `FileAssetLoader` interface | ||
|
|
||
| ```cpp | ||
| #include "rive/file_asset_loader.hpp" | ||
| #include "rive/assets/file_asset.hpp" | ||
|
|
||
| class FileAssetLoader : public RefCnt<FileAssetLoader> { | ||
| public: | ||
| virtual bool loadContents(FileAsset& asset, | ||
| Span<const uint8_t> inBandBytes, | ||
| Factory* factory) = 0; | ||
| }; | ||
| ``` | ||
|
|
||
| `loadContents` is called once per asset during `File::import`. You return: | ||
|
|
||
| - `true` — you handled it. Either you populated `asset` synchronously, or | ||
| you kicked off async work and will populate it later. | ||
| - `false` — fall back to the in-band bytes (if any). | ||
|
|
||
| `asset` arrives typed: cast it to the concrete subclass to populate it. | ||
|
|
||
| | Asset type | Class | How to populate | | ||
| | ---------- | ----- | --------------- | | ||
| | Image | `ImageAsset` | `asset.renderImage(factory->decodeImage(bytes))` | | ||
| | Font | `FontAsset` | `asset.font(factory->decodeFont(bytes))` | | ||
| | Audio | `AudioAsset` | `asset.audioSource(factory->decodeAudio(bytes))` | | ||
|
|
||
| ## Sync example: load by file name | ||
|
|
||
| ```cpp | ||
| #include "rive/file_asset_loader.hpp" | ||
| #include "rive/assets/image_asset.hpp" | ||
| #include "rive/assets/font_asset.hpp" | ||
| #include "rive/assets/audio_asset.hpp" | ||
|
|
||
| #include <filesystem> | ||
| #include <fstream> | ||
|
Tod-Rive marked this conversation as resolved.
|
||
| #include <iterator> | ||
| #include <vector> | ||
|
|
||
| class DiskAssetLoader : public rive::FileAssetLoader { | ||
| public: | ||
| explicit DiskAssetLoader(std::filesystem::path root) | ||
| : m_root(std::move(root)) {} | ||
|
|
||
| bool loadContents(rive::FileAsset& asset, | ||
| rive::Span<const uint8_t> inBandBytes, | ||
| rive::Factory* factory) override | ||
| { | ||
| // Prefer in-band bytes when present. | ||
| if (inBandBytes.size() > 0) return false; | ||
|
|
||
| auto path = m_root / asset.uniqueFilename(); | ||
| std::ifstream f(path, std::ios::binary); | ||
| if (!f) return false; | ||
| std::vector<uint8_t> bytes((std::istreambuf_iterator<char>(f)), {}); | ||
| rive::Span<const uint8_t> span{bytes.data(), bytes.size()}; | ||
|
|
||
| if (auto* img = dynamic_cast<rive::ImageAsset*>(&asset)) { | ||
| img->renderImage(factory->decodeImage(span)); | ||
| return true; | ||
| } | ||
| if (auto* fnt = dynamic_cast<rive::FontAsset*>(&asset)) { | ||
| fnt->font(factory->decodeFont(span)); | ||
| return true; | ||
| } | ||
| if (auto* aud = dynamic_cast<rive::AudioAsset*>(&asset)) { | ||
| aud->audioSource(factory->decodeAudio(span)); | ||
| return true; | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| private: | ||
| std::filesystem::path m_root; | ||
| }; | ||
| ``` | ||
|
|
||
| `asset.uniqueFilename()` is the editor-assigned name with extension; use | ||
| `asset.cdnUuidStr()` if you index by CDN UUID instead. | ||
|
|
||
| ## Wiring it up | ||
|
|
||
| ```cpp | ||
| rcp<FileAssetLoader> loader = make_rcp<DiskAssetLoader>("assets/"); | ||
|
|
||
| ImportResult result; | ||
| rcp<File> file = File::import(bytes, factory, &result, loader); | ||
| ``` | ||
|
|
||
| The loader is reference-counted — `File` keeps it alive for the file's | ||
| lifetime so async loads can complete after `import` returns. | ||
|
|
||
| ## Async loading | ||
|
|
||
| For async (HTTP, decoder thread, etc), return `true` from `loadContents` | ||
| **without** calling `renderImage` / `font` / `audioSource`, then populate the | ||
| asset later from any thread: | ||
|
|
||
| ```cpp | ||
| bool loadContents(FileAsset& asset, Span<const uint8_t>, Factory* factory) override { | ||
| auto* image = dynamic_cast<ImageAsset*>(&asset); | ||
| if (!image) return false; | ||
|
|
||
| rcp<ImageAsset> keepAlive = ref_rcp(image); | ||
|
|
||
| fetchAsync(image->cdnUuidStr(), [keepAlive, factory](std::vector<uint8_t> bytes) { | ||
| // Assumes this callback is dispatched to the render thread — see the | ||
| // warning below about decoder thread-safety. | ||
| rcp<RenderImage> ri = | ||
| factory->decodeImage({bytes.data(), bytes.size()}); | ||
| keepAlive->renderImage(std::move(ri)); | ||
| // The next advanceAndApply will pick up the new image. | ||
|
Tod-Rive marked this conversation as resolved.
|
||
| }); | ||
| return true; | ||
| } | ||
| ``` | ||
|
|
||
| <Warning> | ||
| Decoders (`Factory::decodeImage`, `Factory::decodeFont`, | ||
| `Factory::decodeAudio`) are **not guaranteed thread-safe** for every | ||
| backend. If you decode off-thread, decode into a CPU-side representation | ||
| and finalize the GPU upload back on the render thread, or use a | ||
| thread-safe `Factory` if your backend supports it. | ||
| </Warning> | ||
|
|
||
| ## Built-in loaders | ||
|
|
||
| For simple cases the runtime ships a relative-path loader you can extend: | ||
|
|
||
| ```cpp | ||
| #include "rive/relative_local_asset_loader.hpp" | ||
|
|
||
| rcp<FileAssetLoader> loader = | ||
| make_rcp<RelativeLocalAssetLoader>("/path/to/assets"); | ||
| ``` | ||
|
|
||
| It loads files by `uniqueFilename()` from a directory on disk — handy for | ||
| samples and tooling. | ||
|
|
||
| ## When in-band bytes already cover everything | ||
|
|
||
| If your `.riv` file embeds all of its assets, you can skip the loader | ||
| entirely. `inBandBytes` will be non-empty for those assets and the runtime | ||
| will decode them with the `Factory` you provided. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,185 @@ | ||
| --- | ||
| title: "Data Binding" | ||
| description: "Drive Rive ViewModels from C++." | ||
| --- | ||
|
|
||
| Rive's **View Models** expose strongly-typed properties | ||
| (numbers, strings, colors, booleans, enums, triggers, lists, nested view | ||
| models, image and artboard references) that an artboard binds to. From C++ | ||
| you instance a view model, mutate properties, and the bound visuals update | ||
| on the next `advanceAndApply`. | ||
|
|
||
| ## Concepts | ||
|
|
||
| - `ViewModelRuntime` — schema for a view model defined in the editor. Lives | ||
| in the `File`. | ||
| - `ViewModelInstanceRuntime` — typed wrapper around an instance of that | ||
| schema. Exposes the property API (`propertyNumber`, `propertyString`, etc). | ||
| You own it via `rcp<>`. | ||
| - `ViewModelInstance` — the underlying bindable instance. `Artboard` and | ||
| `Scene` bind to this type. Get one from a `ViewModelInstanceRuntime` with | ||
| `.instance()`. | ||
| - `ViewModelInstance*Runtime` — typed handles for individual properties | ||
| (`…NumberRuntime`, `…StringRuntime`, etc). | ||
|
|
||
| ## Creating an instance | ||
|
|
||
| The simplest path is to ask the file for the artboard's default view model, | ||
| then create a default instance from it: | ||
|
|
||
| ```cpp | ||
| #include "rive/file.hpp" | ||
| #include "rive/viewmodel/runtime/viewmodel_runtime.hpp" | ||
|
|
||
| ViewModelRuntime* vm = file->defaultArtboardViewModel(artboard.get()); | ||
| if (!vm) return; // artboard has no default view model | ||
|
|
||
| rcp<ViewModelInstanceRuntime> instance = vm->createDefaultInstance(); | ||
| if (instance) { | ||
| artboard->bindViewModelInstance(instance->instance()); | ||
| scene ->bindViewModelInstance(instance->instance()); | ||
| } | ||
|
Tod-Rive marked this conversation as resolved.
Tod-Rive marked this conversation as resolved.
|
||
| ``` | ||
|
|
||
| For full control, look up a specific view model schema by index or name and | ||
| create instances from it: | ||
|
|
||
| ```cpp | ||
| ViewModelRuntime* vm = file->viewModelByName("Card"); | ||
| if (!vm) return; // no view model with that name | ||
|
|
||
| size_t propCount = vm->propertyCount(); | ||
| size_t instCount = vm->instanceCount(); | ||
|
|
||
| rcp<ViewModelInstanceRuntime> instance = vm->createDefaultInstance(); | ||
| // alternatives — pick one and replace the line above: | ||
| // rcp<ViewModelInstanceRuntime> instance = vm->createInstanceFromName("Hero"); | ||
| // rcp<ViewModelInstanceRuntime> instance = vm->createInstanceFromIndex(0); | ||
| // rcp<ViewModelInstanceRuntime> instance = vm->createInstance(); // no editor preset; properties at type defaults | ||
|
|
||
| if (!instance) return; | ||
| artboard->bindViewModelInstance(instance->instance()); | ||
| scene ->bindViewModelInstance(instance->instance()); | ||
| ``` | ||
|
Tod-Rive marked this conversation as resolved.
|
||
|
|
||
| <Note> | ||
| Bind the same `ViewModelInstance` to **both** the artboard and the scene. | ||
| The artboard binding drives layout-affecting properties; the scene binding | ||
| drives state-machine inputs and listener conditions. | ||
| </Note> | ||
|
|
||
| ## Reading & writing properties | ||
|
|
||
| All accessors are path-based — `/`-separated for nested view models. | ||
|
|
||
| ```cpp | ||
| auto* card = instance.get(); | ||
|
|
||
| // Number | ||
| if (auto* score = card->propertyNumber("score")) { | ||
| score->value(42.0f); | ||
| float v = score->value(); | ||
| } | ||
|
|
||
| // String | ||
| if (auto* title = card->propertyString("title")) { | ||
| title->value("Hello"); | ||
| } | ||
|
|
||
| // Boolean | ||
| if (auto* on = card->propertyBoolean("isOpen")) { | ||
| on->value(true); | ||
| } | ||
|
|
||
| // Color (ARGB packed) | ||
| if (auto* col = card->propertyColor("accent")) { | ||
| col->value(0xFFE53935); | ||
| } | ||
|
|
||
| // Trigger (edge event) | ||
| if (auto* fire = card->propertyTrigger("fire")) { | ||
| fire->trigger(); | ||
| } | ||
|
|
||
| // Enum (by string label) | ||
| if (auto* mood = card->propertyEnum("mood")) { | ||
| mood->value("happy"); | ||
| } | ||
| ``` | ||
|
|
||
| ### Nested view models | ||
|
|
||
| ```cpp | ||
| // Option 1: deep path string. | ||
| auto* headerTitle = card->propertyString("header/title"); | ||
|
|
||
| // Option 2: walk the tree. | ||
| rcp<ViewModelInstanceRuntime> header = card->propertyViewModel("header"); | ||
| auto* title = header->propertyString("title"); | ||
|
Tod-Rive marked this conversation as resolved.
|
||
| ``` | ||
|
|
||
| You can also **swap** a nested view model wholesale — useful for swapping a | ||
| list cell's data without rebuilding the artboard: | ||
|
|
||
| ```cpp | ||
| rcp<ViewModelInstanceRuntime> newHeader = vm->createDefaultInstance(); | ||
| card->replaceViewModel("header", newHeader.get()); | ||
| ``` | ||
|
|
||
| ### Lists | ||
|
|
||
| ```cpp | ||
| auto* items = card->propertyList("items"); | ||
|
|
||
| // Append, insert, remove, replace, swap, count. | ||
| items->addInstance(rowInstance.get()); | ||
| items->addInstanceAt(rowInstance.get(), 0); | ||
| items->removeInstanceAt(2); | ||
| items->swap(0, 1); | ||
| size_t n = items->size(); | ||
|
|
||
| rcp<ViewModelInstanceRuntime> row = items->instanceAt(0); | ||
|
Tod-Rive marked this conversation as resolved.
|
||
| ``` | ||
|
|
||
| List items are themselves `ViewModelInstanceRuntime`s — same property API as | ||
| above. | ||
|
|
||
| ### Image & artboard properties | ||
|
|
||
| ```cpp | ||
| auto* image = card->propertyImage("avatar"); | ||
| image->value(decodedRenderImage.get()); // RenderImage* | ||
|
|
||
| auto* artboardRef = card->propertyArtboard("badge"); | ||
| artboardRef->value(file->bindableArtboardNamed("Badge")); // rcp<BindableArtboard> | ||
|
Tod-Rive marked this conversation as resolved.
|
||
| ``` | ||
|
|
||
| ## Lifecycle | ||
|
|
||
| - A `ViewModelInstanceRuntime` is a thin wrapper over a `ViewModelInstance`. | ||
| Hold the `rcp<>` for as long as anything binds to it. | ||
| - Property handles (`ViewModelInstanceNumberRuntime*`, etc) are owned by | ||
| the parent instance. Cache the pointer — it stays valid for the | ||
| instance's lifetime. | ||
| - After mutating properties, the next `scene->advanceAndApply(dt)` propagates | ||
| the changes through data binds and into rendering. | ||
|
|
||
| ## When properties don't exist | ||
|
|
||
| Every `propertyX(name)` getter returns `nullptr` if the name doesn't | ||
| resolve, so you can probe an instance safely: | ||
|
|
||
| ```cpp | ||
| if (auto* p = instance->propertyNumber("optional")) { | ||
| p->value(1.0f); | ||
| } | ||
| ``` | ||
|
|
||
| For introspection, walk the schema: | ||
|
|
||
| ```cpp | ||
| for (const PropertyData& p : instance->properties()) { | ||
| // p.name, p.type ∈ { number, string, boolean, color, enum, trigger, | ||
| // list, viewModel, image, artboard, ... } | ||
| } | ||
| ``` | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you put this after Unity?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Architecturally I think it makes sense to have core first followed by the wrappers. At least as a dev, that's how I would expect it to flow. Happy to discuss and hear your thoughts.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, but it's also the most complex, the most intimidating, and the least likely for someone to use.