Repository: RAUI-labs/raui
Branch: master
Commit: f236b369ff5a
Files: 243
Total size: 1.1 MB
Directory structure:
gitextract_g9lhrpyd/
├── .github/
│ └── workflows/
│ ├── readme.yml
│ ├── rust.yml
│ └── website.yml
├── .gitignore
├── .gitmodules
├── Cargo.toml
├── LICENSE
├── README.md
├── README.tpl
├── crates/
│ ├── _/
│ │ ├── Cargo.toml
│ │ ├── build.rs
│ │ ├── examples/
│ │ │ ├── anchor_box.rs
│ │ │ ├── app.rs
│ │ │ ├── button_external.rs
│ │ │ ├── button_internal.rs
│ │ │ ├── canvas.rs
│ │ │ ├── content_box.rs
│ │ │ ├── context_box.rs
│ │ │ ├── flex_box.rs
│ │ │ ├── flex_box_content_size.rs
│ │ │ ├── flex_box_wrapping.rs
│ │ │ ├── float_view.rs
│ │ │ ├── grid_box.rs
│ │ │ ├── horizontal_box.rs
│ │ │ ├── image_box_color.rs
│ │ │ ├── image_box_frame.rs
│ │ │ ├── image_box_image.rs
│ │ │ ├── image_box_procedural.rs
│ │ │ ├── immediate_mode.rs
│ │ │ ├── immediate_mode_access_and_tests.rs
│ │ │ ├── immediate_mode_stack_props.rs
│ │ │ ├── immediate_mode_states_and_effects.rs
│ │ │ ├── immediate_text_field_paper.rs
│ │ │ ├── input_field.rs
│ │ │ ├── navigation.rs
│ │ │ ├── options_view.rs
│ │ │ ├── options_view_map.rs
│ │ │ ├── portal_box.rs
│ │ │ ├── render_workers.rs
│ │ │ ├── resources/
│ │ │ │ └── long_text.txt
│ │ │ ├── responsive_box.rs
│ │ │ ├── responsive_props_box.rs
│ │ │ ├── retained_mode.rs
│ │ │ ├── scroll_box.rs
│ │ │ ├── scroll_box_adaptive.rs
│ │ │ ├── setup.rs
│ │ │ ├── size_box.rs
│ │ │ ├── size_box_aspect_ratio.rs
│ │ │ ├── slider_view.rs
│ │ │ ├── space_box.rs
│ │ │ ├── switch_box.rs
│ │ │ ├── tabs_box.rs
│ │ │ ├── text_box.rs
│ │ │ ├── text_box_content_size.rs
│ │ │ ├── text_field_paper.rs
│ │ │ ├── tooltip_box.rs
│ │ │ ├── tracking.rs
│ │ │ ├── variant_box.rs
│ │ │ ├── vertical_box.rs
│ │ │ ├── view_model.rs
│ │ │ ├── view_model_hierarchy.rs
│ │ │ ├── view_model_widget.rs
│ │ │ └── wrap_box.rs
│ │ └── src/
│ │ ├── import_all.rs
│ │ └── lib.rs
│ ├── app/
│ │ ├── Cargo.toml
│ │ └── src/
│ │ ├── app/
│ │ │ ├── declarative.rs
│ │ │ ├── immediate.rs
│ │ │ ├── mod.rs
│ │ │ └── retained.rs
│ │ ├── asset_manager.rs
│ │ ├── components/
│ │ │ ├── canvas.rs
│ │ │ └── mod.rs
│ │ ├── interactions.rs
│ │ ├── lib.rs
│ │ ├── render_worker.rs
│ │ └── text_measurements.rs
│ ├── core/
│ │ ├── Cargo.toml
│ │ └── src/
│ │ ├── animator.rs
│ │ ├── application.rs
│ │ ├── interactive/
│ │ │ ├── default_interactions_engine.rs
│ │ │ └── mod.rs
│ │ ├── layout/
│ │ │ ├── default_layout_engine.rs
│ │ │ └── mod.rs
│ │ ├── lib.rs
│ │ ├── messenger.rs
│ │ ├── props.rs
│ │ ├── renderer.rs
│ │ ├── signals.rs
│ │ ├── state.rs
│ │ ├── tester.rs
│ │ ├── view_model.rs
│ │ └── widget/
│ │ ├── component/
│ │ │ ├── containers/
│ │ │ │ ├── anchor_box.rs
│ │ │ │ ├── area_box.rs
│ │ │ │ ├── content_box.rs
│ │ │ │ ├── context_box.rs
│ │ │ │ ├── flex_box.rs
│ │ │ │ ├── float_box.rs
│ │ │ │ ├── grid_box.rs
│ │ │ │ ├── hidden_box.rs
│ │ │ │ ├── horizontal_box.rs
│ │ │ │ ├── mod.rs
│ │ │ │ ├── portal_box.rs
│ │ │ │ ├── responsive_box.rs
│ │ │ │ ├── scroll_box.rs
│ │ │ │ ├── size_box.rs
│ │ │ │ ├── switch_box.rs
│ │ │ │ ├── tabs_box.rs
│ │ │ │ ├── tooltip_box.rs
│ │ │ │ ├── variant_box.rs
│ │ │ │ ├── vertical_box.rs
│ │ │ │ └── wrap_box.rs
│ │ │ ├── image_box.rs
│ │ │ ├── interactive/
│ │ │ │ ├── button.rs
│ │ │ │ ├── float_view.rs
│ │ │ │ ├── input_field.rs
│ │ │ │ ├── mod.rs
│ │ │ │ ├── navigation.rs
│ │ │ │ ├── options_view.rs
│ │ │ │ ├── scroll_view.rs
│ │ │ │ └── slider_view.rs
│ │ │ ├── mod.rs
│ │ │ ├── space_box.rs
│ │ │ └── text_box.rs
│ │ ├── context.rs
│ │ ├── mod.rs
│ │ ├── node.rs
│ │ ├── unit/
│ │ │ ├── area.rs
│ │ │ ├── content.rs
│ │ │ ├── flex.rs
│ │ │ ├── grid.rs
│ │ │ ├── image.rs
│ │ │ ├── mod.rs
│ │ │ ├── portal.rs
│ │ │ ├── size.rs
│ │ │ └── text.rs
│ │ └── utils.rs
│ ├── derive/
│ │ ├── Cargo.toml
│ │ └── src/
│ │ └── lib.rs
│ ├── immediate/
│ │ ├── Cargo.toml
│ │ └── src/
│ │ └── lib.rs
│ ├── immediate-widgets/
│ │ ├── Cargo.toml
│ │ └── src/
│ │ └── lib.rs
│ ├── json-renderer/
│ │ ├── Cargo.toml
│ │ └── src/
│ │ └── lib.rs
│ ├── material/
│ │ ├── Cargo.toml
│ │ └── src/
│ │ ├── component/
│ │ │ ├── containers/
│ │ │ │ ├── context_paper.rs
│ │ │ │ ├── flex_paper.rs
│ │ │ │ ├── grid_paper.rs
│ │ │ │ ├── horizontal_paper.rs
│ │ │ │ ├── mod.rs
│ │ │ │ ├── modal_paper.rs
│ │ │ │ ├── paper.rs
│ │ │ │ ├── scroll_paper.rs
│ │ │ │ ├── text_tooltip_paper.rs
│ │ │ │ ├── tooltip_paper.rs
│ │ │ │ ├── vertical_paper.rs
│ │ │ │ ├── window_paper.rs
│ │ │ │ └── wrap_paper.rs
│ │ │ ├── icon_paper.rs
│ │ │ ├── interactive/
│ │ │ │ ├── button_paper.rs
│ │ │ │ ├── icon_button_paper.rs
│ │ │ │ ├── mod.rs
│ │ │ │ ├── slider_paper.rs
│ │ │ │ ├── switch_button_paper.rs
│ │ │ │ ├── text_button_paper.rs
│ │ │ │ └── text_field_paper.rs
│ │ │ ├── mod.rs
│ │ │ ├── switch_paper.rs
│ │ │ └── text_paper.rs
│ │ ├── lib.rs
│ │ └── theme.rs
│ ├── retained/
│ │ ├── Cargo.toml
│ │ └── src/
│ │ └── lib.rs
│ └── tesselate-renderer/
│ ├── Cargo.toml
│ └── src/
│ └── lib.rs
├── demos/
│ ├── hello-world/
│ │ ├── Cargo.toml
│ │ └── src/
│ │ ├── main.rs
│ │ └── ui/
│ │ ├── components/
│ │ │ ├── app.rs
│ │ │ ├── color_rect.rs
│ │ │ ├── content.rs
│ │ │ ├── image_button.rs
│ │ │ ├── mod.rs
│ │ │ └── title_bar.rs
│ │ ├── mod.rs
│ │ └── view_models.rs
│ ├── in-game/
│ │ ├── Cargo.toml
│ │ ├── README.md
│ │ ├── resources/
│ │ │ ├── items.json
│ │ │ └── quests.json
│ │ └── src/
│ │ ├── main.rs
│ │ ├── model/
│ │ │ ├── inventory.rs
│ │ │ ├── menu.rs
│ │ │ ├── mod.rs
│ │ │ ├── quests.rs
│ │ │ └── settings.rs
│ │ └── ui/
│ │ ├── app.rs
│ │ ├── inventory.rs
│ │ ├── mod.rs
│ │ ├── quests.rs
│ │ └── settings.rs
│ └── todo-app/
│ ├── .gitignore
│ ├── Cargo.toml
│ ├── resources/
│ │ └── fonts/
│ │ └── Roboto/
│ │ └── LICENSE.txt
│ └── src/
│ ├── main.rs
│ ├── model.rs
│ └── ui/
│ ├── components/
│ │ ├── app.rs
│ │ ├── app_bar.rs
│ │ ├── confirm_box.rs
│ │ ├── mod.rs
│ │ └── tasks_list.rs
│ └── mod.rs
├── justfile
└── site/
├── .gitignore
├── .markdownlint.yml
├── config.toml
├── content/
│ ├── authors/
│ │ ├── _index.md
│ │ ├── psichix.md
│ │ └── zicklag.md
│ ├── blog/
│ │ ├── _index.md
│ │ └── new-documentation-site.md
│ ├── docs/
│ │ ├── _index.md
│ │ ├── about/
│ │ │ ├── _index.md
│ │ │ └── introduction.md
│ │ ├── getting-started/
│ │ │ ├── 01-setting-up.md
│ │ │ ├── 02-your-first-widget/
│ │ │ │ └── index.md
│ │ │ ├── 03-containers/
│ │ │ │ └── index.md
│ │ │ └── _index.md
│ │ └── layout/
│ │ ├── 01-layout-in-depth.md
│ │ └── _index.md
│ └── examples/
│ └── _index.md
├── rust/
│ ├── guide_01/
│ │ ├── Cargo.toml
│ │ └── src/
│ │ └── main.rs
│ ├── guide_02/
│ │ ├── Cargo.toml
│ │ └── src/
│ │ └── main.rs
│ └── guide_03/
│ ├── Cargo.toml
│ └── src/
│ └── main.rs
├── static/
│ └── .nojekyll
└── templates/
└── shortcodes/
├── code_snippet.md
├── include_markdown.md
├── rust_code_snippet.md
├── rustdoc_test.md
└── toml_code_snippet.md
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/readme.yml
================================================
name: Check README
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
# Make sure that the readme has been generated from the `lib.rs` docs
# and is not out-of-sync.
check-readme:
runs-on: ubuntu-latest
container:
image: ghcr.io/msrd0/cargo-readme:latest
steps:
- uses: actions/checkout@v2
- name: Copy README
run: cp README.md README.md.ref
- name: Generate README from lib.rs
run: cargo readme > README.md
- name: Diff Generated README and Copied README
run: diff README.md README.md.ref
================================================
FILE: .github/workflows/rust.yml
================================================
name: Rust
on: [push, pull_request, workflow_dispatch]
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Install alsa and udev
run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev
- name: Build
run: cargo build --all --features all
- name: Run tests
run: cargo test --all --features all
================================================
FILE: .github/workflows/website.yml
================================================
name: "Build & Deploy Website"
on:
push:
branches:
- master
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@master
- name: Install Just
run: curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | sudo bash -s -- --to /usr/local/bin
- name: Run Website Doc Tests
run: just website-doc-tests
build:
runs-on: ubuntu-latest
if: github.ref != 'refs/heads/master'
steps:
- name: Checkout
uses: actions/checkout@master
- name: Build only
uses: shalzz/zola-deploy-action@master
env:
BUILD_DIR: site
GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }}
BUILD_ONLY: true
build_and_deploy:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/master'
steps:
- name: Checkout
uses: actions/checkout@master
- name: Build and Deploy
uses: shalzz/zola-deploy-action@master
env:
PAGES_BRANCH: gh-pages
BUILD_DIR: site
GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }}
================================================
FILE: .gitignore
================================================
/target
Cargo.lock
*.sh
*gitignore*
!.gitignore
================================================
FILE: .gitmodules
================================================
[submodule "site/themes/adidoks"]
path = site/themes/adidoks
url = https://github.com/RAUI-labs/raui_site_theme.git
================================================
FILE: Cargo.toml
================================================
[workspace]
members = [
"crates/*",
"demos/*",
"site/rust/guide_*"
]
resolver = "2"
================================================
FILE: LICENSE
================================================
MIT License
Copyright (C) 2025 Patryk 'PsichiX' Budzyński
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.
================================================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
Copyright (C) 2025 Patryk 'PsichiX' Budzyński
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: README.md
================================================
# raui
RAUI is a renderer agnostic UI system that is heavily inspired by **React**'s declarative UI
composition and the **Unreal Engine Slate** widget components system.
> 🗣 **Pronunciation:** RAUI is pronounced like **"ra"** ( the Egyptian god ) + **"oui"**
> (french for "yes" ) — [Audio Example][pronounciation].
[pronounciation]: https://itinerarium.github.io/phoneme-synthesis/?w=/%27rawi/
The main idea behind RAUI architecture is to treat UI as another data source that you transform
into your target renderable data format used by your rendering engine of choice.
## Architecture
### [`Application`]
[`Application`] is the central point of user interest. It performs whole UI processing logic.
There you apply widget tree that wil be processed, send messages from host application to
widgets and receive signals sent from widgets to host application.
### Widgets
Widgets are divided into three categories:
- **[`WidgetNode`]** - used as source UI trees (variant that can be either a component, unit or
none)
- **[`WidgetComponent`]** - you can think of them as Virtual DOM nodes, they store:
- pointer to _component function_ (that process their data)
- unique _key_ (that is a part of widget ID and will be used to tell the system if it should
carry its _state_ to next processing run)
- boxed cloneable _properties_ data
- _listed slots_ (simply: widget children)
- _named slots_ (similar to listed slots: widget children, but these ones have names assigned
to them, so you can access them by name instead of by index)
- **[`WidgetUnit`]** - an atomic element that renderers use to convert into target renderable
data format for rendering engine of choice.
### Component Function
Component functions are static functions that transforms input data (properties, state or
neither of them) into output widget tree (usually used to simply wrap another components tree
under one simple component, where at some point the simplest components returns final
_[`WidgetUnit`]'s_). They work together as a chain of transforms - root component applies some
properties into children components using data from its own properties or state.
#### States
This may bring up a question: _**"If i use only functions and no objects to tell how to
visualize UI, how do i keep some data between each render run?"**_. For that you use _states_.
State is a data that is stored between each processing calls as long as given widget is alive
(that means: as long as widget id stays the same between two processing calls, to make sure your
widget stays the same, you use keys - if no key is assigned, system will generate one for your
widget but that will make it possible to die at any time if for example number of widget
children changes in your common parent, your widget will change its id when key wasn't
assigned). Some additional notes: While you use _properties_ to send information down the tree
and _states_ to store widget data between processing cals, you can communicate with another
widgets and host application using messages and signals! More than that, you can use hooks to
listen for widget life cycle and perform actions there. It's worth noting that state uses
_properties_ to hold its data, so by that you can for example attach multiple hooks that each of
them uses different data type as widget state, this opens the doors to be very creative when
combining different hooks that operate on the same widget.
### Hooks
Hooks are used to put common widget logic into separate functions that can be chained in widgets
and another hooks (you can build a reusable dependency chain of logic with that). Usually it is
used to listen for life cycle events such as mount, change and unmount, additionally you can
chain hooks to be processed sequentially in order they are chained in widgets and other hooks.
What happens under the hood:
- Application calls `button` on a node
- `button` calls `use_button` hook
- `use_button` calls `use_empty` hook
- `use_button` logic is executed
- `button` logic is executed
### Layouting
RAUI exposes the [`Application::layout()`][core::application::Application::layout] API to allow
use of virtual-to-real coords mapping and custom layout engines to perform widget tree
positioning data, which is later used by custom UI renderers to specify boxes where given
widgets should be placed. Every call to perform layouting will store a layout data inside
Application, you can always access that data at any time. There is a [`DefaultLayoutEngine`]
that does this in a generic way. If you find some part of its pipeline working different than
what you've expected, feel free to create your custom layout engine!
### Interactivity
RAUI allows you to ease and automate interactions with UI by use of Interactions Engine - this
is just a struct that implements [`perform_interactions`] method with reference to Application,
and all you should do there is to send user input related messages to widgets. There is
[`DefaultInteractionsEngine`] that covers widget navigation, button and input field - actions
sent from input devices such as mouse (or any single pointer), keyboard and gamepad. When it
comes to UI navigation you can send raw [`NavSignal`] messages to the default interactions
engine and despite being able to select/unselect widgets at will, you have typical navigation
actions available: up, down, left, right, previous tab/screen, next tab/screen, also being able
to focus text inputs and send text input changes to focused input widget. All interactive widget
components that are provided by RAUI handle all [`NavSignal`] actions in their hooks, so all
user has to do is to just activate navigation features for them (using [`NavItemActive`] unit
props). RAUI integrations that want to just use use default interactions engine should make use
of this struct composed in them and call its [`interact`] method with information about what
input change was made. There is an example of that feature covered in RAUI App crate
(`AppInteractionsEngine` struct).
**NOTE: Interactions engines should use layout for pointer events so make sure that you rebuild
layout before you perform interactions!**
[`Application`]: core::application::Application
[`WidgetNode`]: core::widget::node::WidgetNode
[`WidgetComponent`]: core::widget::component::WidgetComponent
[`WidgetUnit`]: core::widget::unit::WidgetUnit
[`DefaultLayoutEngine`]: core::layout::default_layout_engine::DefaultLayoutEngine
[`NavSignal`]: core::widget::component::interactive::navigation::NavSignal
[`NavItemActive`]: core::widget::component::interactive::navigation::NavItemActive
[`perform_interactions`]: core::interactive::InteractionsEngine::perform_interactions
[`interact`]:
core::interactive::default_interactions_engine::DefaultInteractionsEngine::interact
[`DefaultInteractionsEngine`]:
core::interactive::default_interactions_engine::DefaultInteractionsEngine
License: MIT OR Apache-2.0
================================================
FILE: README.tpl
================================================
# RAUI [](https://crates.io/crates/raui)[](https://docs.rs/raui)
## About
{{readme}}
[`Application`]: https://docs.rs/raui/latest/raui/core/application/struct.Application.html
[`WidgetNode`]: https://docs.rs/raui/latest/raui/core/widget/node/enum.WidgetNode.html
[`WidgetComponent`]: https://docs.rs/raui/latest/raui/core/widget/component/struct.WidgetComponent.html
[`WidgetUnit`]: https://docs.rs/raui/latest/raui/core/widget/unit/enum.WidgetUnit.html
[`DefaultLayoutEngine`]: https://docs.rs/raui/latest/raui/core/layout/default_layout_engine/struct.DefaultLayoutEngine.html
[`NavSignal`]: https://docs.rs/raui/latest/raui/core/widget/component/interactive/navigation/enum.NavSignal.html
[`NavItemActive`]: https://docs.rs/raui/latest/raui/core/widget/component/interactive/navigation/struct.NavItemActive.html
[`perform_interactions`]: https://docs.rs/raui/latest/raui/core/interactive/trait.InteractionsEngine.html#tymethod.perform_interactions
[`interact`]: https://docs.rs/raui/latest/raui/interactive/struct.DefaultInteractionsEngine.html#method.interact
[`DefaultInteractionsEngine`]: https://docs.rs/raui/latest/raui/interactive/struct.DefaultInteractionsEngine.html
## Media
- [`RAUI + Spitfire In-Game`](https://github.com/RAUI-labs/raui/tree/master/demos/in-game)
An example of an In-Game integration of RAUI with custom Material theme, using Spitfire as a renderer.

- [`RAUI Todo App`](https://github.com/RAUI-labs/raui/tree/master/demos/todo-app)
An example of TODO app with dark theme Material component library.

## Contribute
Any contribution that improves quality of the RAUI toolset is highly appreciated.
- If you have a feature request, create an Issue post and explain the goal of the feature along with the reason why it is needed and its pros and cons.
- Whenever you would like to create na PR, please create your feature branch from `next` branch so when it gets approved it can be simply merged using GitHub merge button
- All changes are staged into `next` branch and new versions are made out of its commits, master is considered stable/release branch.
- Changes should pass tests, you run tests with: `cargo test --all --features all`.
- This readme file is generated from the `lib.rs` documentation and can be re-generated by using [`cargo readme`][cargo_readme].
[cargo_readme]: https://github.com/livioribeiro/cargo-readme
## Milestones
RAUI is still in early development phase, so prepare for these changes until v1.0:
- [ ] Integrate RAUI into one public open source Rust game.
- [ ] Write documentation.
- [ ] Write MD book about how to use RAUI properly and make UI efficient.
- [ ] Implement VDOM diffing algorithm for tree rebuilding optimizations.
- [ ] Find a solution (or make it a feature) for moving from trait objects data into strongly typed data for properties and states.
Things that now are done:
- [x] Add suport for layouting.
- [x] Add suport for interactions (user input).
- [x] Create renderer for GGEZ game framework.
- [x] Create basic user components.
- [x] Create basic Hello World example application.
- [x] Decouple shared props from props (don't merge them, put shared props in context).
- [x] Create TODO app as an example.
- [x] Create In-Game app as an example.
- [x] Create renderer for Oxygengine game engine.
- [x] Add complex navigation system.
- [x] Create scroll box widget.
- [x] Add "immediate mode UI" builder to give alternative to macros-based declarative mode UI building (with zero overhead, it is an equivalent to declarative macros used by default, immediate mode and declarative mode widgets can talk to each other without a hassle).
- [x] Add data binding property type to easily mutate data from outside of the application.
- [x] Create tesselation renderer that produces Vertex + Index + Batch buffers ready for mesh renderers.
- [x] Move from `widget_component!` and `widget_hook!` macro rules to `pre_hooks` and `post_hooks` function attributes.
- [x] Add derive `PropsData` and `MessageData` procedural macros to gradually replace the need to call `implement_props_data!` and `implement_message_data!` macros.
- [x] Add support for portals - an easy way to "teleport" sub-tree into another tree node (useful for modals and drag & drop).
- [x] Add support for View-Model for sharing data between host app and UI.
================================================
FILE: crates/_/Cargo.toml
================================================
[package]
name = "raui"
version = "0.70.17"
authors = ["Patryk 'PsichiX' Budzynski "]
edition = "2024"
description = "Renderer Agnostic User Interface"
readme = "../../README.md"
license = "MIT OR Apache-2.0"
repository = "https://github.com/RAUI-labs/raui"
keywords = ["renderer", "agnostic", "ui", "interface", "gamedev"]
categories = ["gui", "rendering::graphics-api"]
[features]
material = ["raui-material"]
retained = ["raui-retained"]
immediate = ["raui-immediate"]
immediate-widgets = ["raui-immediate-widgets"]
json = ["raui-json-renderer"]
tesselate = ["raui-tesselate-renderer"]
app = ["raui-app"]
all = [
"material",
"retained",
"immediate",
"immediate-widgets",
"tesselate",
"json",
"app",
]
import-all = []
[dependencies]
raui-core = { path = "../core", version = "0.70" }
[dependencies.raui-material]
path = "../material"
version = "0.70"
optional = true
[dependencies.raui-retained]
path = "../retained"
version = "0.70"
optional = true
[dependencies.raui-immediate]
path = "../immediate"
version = "0.70"
optional = true
[dependencies.raui-immediate-widgets]
path = "../immediate-widgets"
version = "0.70"
optional = true
[dependencies.raui-json-renderer]
path = "../json-renderer"
version = "0.70"
optional = true
[dependencies.raui-tesselate-renderer]
path = "../tesselate-renderer"
version = "0.70"
optional = true
[dependencies.raui-app]
path = "../app"
version = "0.70"
optional = true
[dev-dependencies]
raui-core = { path = "../core" }
raui-material = { path = "../material" }
raui-immediate = { path = "../immediate" }
raui-immediate-widgets = { path = "../immediate-widgets" }
raui-retained = { path = "../retained" }
raui-app = { path = "../app" }
raui-json-renderer = { path = "../json-renderer" }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
[package.metadata.docs.rs]
features = ["all"]
================================================
FILE: crates/_/build.rs
================================================
use std::fs::File;
use std::io::Write;
use std::path::Path;
fn main() {
let mut output = String::new();
output.push_str("#![allow(ambiguous_glob_reexports)]\n");
output.push_str("#![allow(unused_variables)]\n");
visit_dirs(
Path::new("../core/src"),
"raui_core",
None,
&mut output,
&[],
);
visit_dirs(
Path::new("../material/src"),
"raui_material",
Some("material"),
&mut output,
&[],
);
visit_dirs(
Path::new("../retained/src"),
"raui_retained",
Some("retained"),
&mut output,
&[],
);
visit_dirs(
Path::new("../immediate/src"),
"raui_immediate",
Some("immediate"),
&mut output,
&[],
);
visit_dirs(
Path::new("../immediate-widgets/src"),
"raui_immediate_widgets",
Some("immediate-widgets"),
&mut output,
&[],
);
visit_dirs(
Path::new("../tesselate-renderer/src"),
"raui_tesselate_renderer",
Some("tesselate"),
&mut output,
&[],
);
visit_dirs(
Path::new("../json-renderer/src"),
"raui_json_renderer",
Some("json"),
&mut output,
&[],
);
visit_dirs(
Path::new("../app/src"),
"raui_app",
Some("app"),
&mut output,
&[
"asset_manager.rs",
"interactions.rs",
"text_measurements.rs",
],
);
let out_path = Path::new("src").join("import_all.rs");
let mut file = File::create(&out_path).expect("Failed to create import_all.rs");
file.write_all(output.as_bytes()).expect("Write failed");
}
fn visit_dirs(
dir: &Path,
prefix: &str,
feature: Option<&str>,
output: &mut String,
ignore: &[&str],
) {
for entry in std::fs::read_dir(dir).unwrap() {
let entry = entry.unwrap();
let path = entry.path();
if path.is_dir() {
if path.join("mod.rs").exists() {
let mod_path = path.strip_prefix(dir).unwrap();
let mod_name = mod_path.to_string_lossy().replace("/", "::");
if let Some(feature) = feature {
output.push_str(&format!("#[cfg(feature = \"{feature}\")]\n"));
}
output.push_str(&format!("pub use {prefix}::{mod_name}::*;\n"));
visit_dirs(
&path,
&format!("{prefix}::{mod_name}"),
feature,
output,
ignore,
);
}
} else if let Some(ext) = path.extension()
&& ext == "rs"
{
if path.file_name().unwrap() == "lib.rs" {
if let Some(feature) = feature {
output.push_str(&format!("#[cfg(feature = \"{feature}\")]\n"));
}
output.push_str(&format!("pub use {prefix}::*;\n"));
continue;
}
if path.file_name().unwrap() == "mod.rs"
|| path.file_name().unwrap() == "import_all.rs"
|| ignore.iter().any(|name| path.file_name().unwrap() == *name)
{
continue;
}
let mod_path = path.strip_prefix(dir).unwrap();
let mut mod_name = mod_path.to_string_lossy().replace("/", "::");
mod_name = mod_name.trim_end_matches(".rs").to_string();
if let Some(feature) = feature {
output.push_str(&format!("#[cfg(feature = \"{feature}\")]\n"));
}
output.push_str(&format!("pub use {prefix}::{mod_name}::*;\n"));
}
}
}
================================================
FILE: crates/_/examples/anchor_box.rs
================================================
use raui_app::app::declarative::DeclarativeApp;
use raui_core::{
make_widget,
widget::{
WidgetRef,
component::{
RelativeLayoutProps,
containers::{anchor_box::anchor_box, content_box::content_box},
image_box::{ImageBoxProps, image_box},
},
context::WidgetContext,
node::WidgetNode,
unit::content::ContentBoxItemLayout,
utils::Color,
},
};
fn preview(ctx: WidgetContext) -> WidgetNode {
// we print this widget props to show how AnchorProps values change relative to window resize.
println!("Preview props: {:#?}", ctx.props);
// we create simple colored image that fills available space just to make you see the values.
make_widget!(image_box)
.with_props(ImageBoxProps::colored(Color {
r: 1.0,
g: 0.25,
b: 0.25,
a: 1.0,
}))
.into()
}
fn main() {
// we create widget reference first so we can apply it to some widget and and reference
//that widget in another place - basically what widget reference is, it is a way to read
// some other widget ID in some other place outside the referenced widget scope.
let idref = WidgetRef::default();
let tree = make_widget!(content_box)
// we apply widget reference to the root content box so we can reference that root widget
// later in anchor box to enable it to calculate how anchor box content is lay out relative
// to the root widget - this is the most important thing to setup, because if we won't do
// that, anchor box would not be able to give its content a proper data about its layout
// relative to the referenced widget. Note that, you can reference ANY widget in the widget
// tree - it will always give you a relative location to any widget you provide.
.idref(idref.clone())
.listed_slot(
make_widget!(anchor_box)
// we pass widget reference to anchor box via RelativeLayoutProps, because anchor
// uses relative layout hook to perform calculations of relative layout box.
.with_props(RelativeLayoutProps {
relative_to: idref.into(),
})
// we apply margin to anchor box just to make it not fill entire space by default.
.with_props(ContentBoxItemLayout {
margin: 100.0.into(),
..Default::default()
})
.named_slot("content", make_widget!(preview)),
);
DeclarativeApp::simple("Anchor Box", tree);
}
================================================
FILE: crates/_/examples/app.rs
================================================
use raui_app::app::declarative::DeclarativeApp;
use raui_core::{
make_widget,
widget::{
component::{
containers::{flex_box::FlexBoxProps, vertical_box::vertical_box},
image_box::{ImageBoxProps, image_box},
text_box::{TextBoxProps, text_box},
},
unit::{
flex::FlexBoxItemLayout,
text::{TextBoxFont, TextBoxHorizontalAlign, TextBoxSizeValue},
},
utils::Color,
},
};
fn main() {
let tree = make_widget!(vertical_box)
.with_props(FlexBoxProps {
separation: 50.0,
..Default::default()
})
.listed_slot(
make_widget!(image_box).with_props(ImageBoxProps::image_aspect_ratio(
"./demos/hello-world/resources/cats.jpg",
false,
)),
)
.listed_slot(
make_widget!(text_box)
.with_props(FlexBoxItemLayout::no_growing_and_shrinking())
.with_props(TextBoxProps {
text: "RAUI application example".to_owned(),
font: TextBoxFont {
name: "./demos/hello-world/resources/verdana.ttf".to_owned(),
size: 64.0,
},
color: Color {
r: 0.0,
g: 0.0,
b: 0.5,
a: 1.0,
},
horizontal_align: TextBoxHorizontalAlign::Center,
height: TextBoxSizeValue::Content,
..Default::default()
}),
);
DeclarativeApp::simple("RAUI application example", tree);
}
================================================
FILE: crates/_/examples/button_external.rs
================================================
// Make sure you have seen `button_internal` code example first, because this is an evolution of that.
use raui_app::app::declarative::DeclarativeApp;
use raui_core::{
make_widget, pre_hooks,
widget::{
component::{
image_box::{ImageBoxProps, image_box},
interactive::{
button::{ButtonNotifyMessage, ButtonNotifyProps, button},
navigation::{NavItemActive, use_nav_container_active},
},
},
context::WidgetContext,
node::WidgetNode,
unit::image::{ImageBoxColor, ImageBoxMaterial, ImageBoxSizeValue},
utils::Color,
},
};
// we create app hook that just receives button state change messages and prints them.
fn use_app(ctx: &mut WidgetContext) {
ctx.life_cycle.change(|ctx| {
for msg in ctx.messenger.messages {
if let Some(msg) = msg.as_any().downcast_ref::() {
println!("Button message: {msg:#?}");
}
}
});
}
#[pre_hooks(use_nav_container_active, use_app)]
fn app(mut ctx: WidgetContext) -> WidgetNode {
make_widget!(button)
.with_props(NavItemActive)
// we tell button to notify this component (send messages to it) whenever button state changes.
.with_props(ButtonNotifyProps(ctx.id.to_owned().into()))
.named_slot(
"content",
make_widget!(image_box).with_props(ImageBoxProps {
material: ImageBoxMaterial::Color(ImageBoxColor {
color: Color {
r: 1.0,
g: 0.25,
b: 0.25,
a: 1.0,
},
..Default::default()
}),
width: ImageBoxSizeValue::Exact(400.0),
height: ImageBoxSizeValue::Exact(300.0),
..Default::default()
}),
)
.into()
}
fn main() {
DeclarativeApp::simple("Button - Sending state to other widget", make_widget!(app));
}
================================================
FILE: crates/_/examples/button_internal.rs
================================================
use raui_app::app::declarative::DeclarativeApp;
use raui_core::{
make_widget, pre_hooks,
widget::{
component::{
image_box::{ImageBoxProps, image_box},
interactive::{
button::{ButtonProps, button},
navigation::{NavItemActive, use_nav_container_active},
},
},
context::WidgetContext,
node::WidgetNode,
unit::image::{ImageBoxColor, ImageBoxMaterial, ImageBoxSizeValue},
utils::Color,
},
};
// mark the root widget as navigable container to allow button to subscribe to navigation system.
#[pre_hooks(use_nav_container_active)]
fn app(mut ctx: WidgetContext) -> WidgetNode {
// button is the simplest and the most common in use navigable item that can react to user input.
make_widget!(button)
// enable button navigation (it is disabled by default).
.with_props(NavItemActive)
// by default button state of the button is passed to the content widget with
// `ButtonProps` props data, so content widget can read it and change its appearance.
.named_slot("content", make_widget!(internal))
.into()
}
fn internal(ctx: WidgetContext) -> WidgetNode {
// first we unpack button state from button props.
let ButtonProps {
// selected state means, well..widget has got selected. selection in navigation is more
// complex than that and it deserves separate deeper explanation, but in essence: whenever
// user navigate over the UI, RAUI performs selection on navigable items, navigable items
// may be nested and whenever some widget gets selected, all of its navigable parents
// receive selection event too, so there is not only one widget that might be selected at
// a time, but there might be a chain of selected items, as long as they are on the way
// toward actually selected navigable item in the widget tree.
selected,
// trigger state means navigable item got Accept event, which in context of the button
// means: button is selected and user performed "left mouse button click".
trigger,
// context state is similar to trigger state, in this case it means user performed "right
// mouse button click".
context,
..
} = ctx.props.read_cloned_or_default();
let color = if trigger {
Color {
r: 1.0,
g: 0.25,
b: 0.25,
a: 1.0,
}
} else if context {
Color {
r: 0.25,
g: 1.0,
b: 0.25,
a: 1.0,
}
} else if selected {
Color {
r: 0.25,
g: 0.25,
b: 1.0,
a: 1.0,
}
} else {
Color {
r: 0.25,
g: 0.25,
b: 0.25,
a: 1.0,
}
};
make_widget!(image_box)
.with_props(ImageBoxProps {
material: ImageBoxMaterial::Color(ImageBoxColor {
color,
..Default::default()
}),
width: ImageBoxSizeValue::Exact(400.0),
height: ImageBoxSizeValue::Exact(300.0),
..Default::default()
})
.into()
}
fn main() {
DeclarativeApp::simple("Button - Pass state to its child", make_widget!(app));
}
================================================
FILE: crates/_/examples/canvas.rs
================================================
// Make sure you have seen `render_workers` code example first, because this is an evolution of that.
use raui_app::{
Vertex,
app::declarative::DeclarativeApp,
components::canvas::{CanvasProps, DrawOnCanvasMessage, RequestCanvasRedrawMessage, canvas},
render_worker::RenderWorkerTaskContext,
third_party::spitfire_glow::{
graphics::GraphicsBatch,
renderer::{GlowBlending, GlowUniformValue},
},
};
use raui_core::{
make_widget, pre_hooks,
widget::{context::WidgetContext, node::WidgetNode, utils::Color},
};
fn use_my_canvas(ctx: &mut WidgetContext) {
ctx.life_cycle.change(|ctx| {
// canvas will send redraw request on mount and resize.
// we can react with sending drawing task message to canvas.
for msg in ctx.messenger.messages {
if msg
.as_any()
.downcast_ref::()
.is_some()
{
ctx.messenger.write(
ctx.id.to_owned(),
DrawOnCanvasMessage::function(render_task),
);
}
}
});
}
#[pre_hooks(use_my_canvas)]
fn my_canvas(mut ctx: WidgetContext) -> WidgetNode {
// we are specializing canvas widget by simply executing canvas
// widget function in place, so we have easier times sending
// drawing task to canvas by its id.
canvas(ctx)
}
fn main() {
let tree = make_widget!(my_canvas).with_props(CanvasProps {
color: Color {
r: 0.0,
g: 0.0,
b: 0.0,
a: 0.5,
},
clear: true,
});
DeclarativeApp::simple("Canvas", tree);
}
fn render_task(ctx: RenderWorkerTaskContext) {
ctx.graphics.state.stream.batch_optimized(GraphicsBatch {
shader: Some(ctx.colored_shader.clone()),
uniforms: [(
"u_projection_view".into(),
GlowUniformValue::M4(
ctx.graphics
.state
.main_camera
.world_matrix()
.into_col_array(),
),
)]
.into_iter()
.collect(),
textures: Default::default(),
blending: GlowBlending::Alpha,
scissor: None,
wireframe: false,
});
ctx.graphics.state.stream.quad([
Vertex {
position: [50.0, 50.0],
uv: [0.0, 0.0, 0.0],
color: [1.0, 0.0, 0.0, 1.0],
},
Vertex {
position: [ctx.graphics.state.main_camera.screen_size.x - 50.0, 50.0],
uv: [0.0, 0.0, 0.0],
color: [0.0, 1.0, 0.0, 1.0],
},
Vertex {
position: [
ctx.graphics.state.main_camera.screen_size.x - 50.0,
ctx.graphics.state.main_camera.screen_size.y - 50.0,
],
uv: [0.0, 0.0, 0.0],
color: [0.0, 0.0, 1.0, 1.0],
},
Vertex {
position: [50.0, ctx.graphics.state.main_camera.screen_size.y - 50.0],
uv: [0.0, 0.0, 0.0],
color: [1.0, 1.0, 0.0, 1.0],
},
]);
}
================================================
FILE: crates/_/examples/content_box.rs
================================================
use raui_app::app::declarative::DeclarativeApp;
use raui_core::{
make_widget,
widget::{
component::{
containers::content_box::content_box,
image_box::{ImageBoxProps, image_box},
},
unit::content::ContentBoxItemLayout,
utils::{Color, Rect},
},
};
fn main() {
let tree = make_widget!(content_box)
.listed_slot(
make_widget!(image_box)
.with_props(ImageBoxProps::colored(Color {
r: 1.0,
g: 0.25,
b: 0.25,
a: 1.0,
}))
.with_props(ContentBoxItemLayout {
anchors: Rect {
left: -1.0,
right: 2.0,
top: -1.0,
bottom: 2.0,
},
keep_in_bounds: true.into(),
..Default::default()
}),
)
.listed_slot(
make_widget!(image_box)
.with_props(ImageBoxProps::colored(Color {
r: 0.25,
g: 1.0,
b: 0.25,
a: 1.0,
}))
.with_props(ContentBoxItemLayout {
margin: 64.0.into(),
..Default::default()
}),
)
.listed_slot(
make_widget!(image_box)
.with_props(ImageBoxProps::colored(Color {
r: 0.25,
g: 0.25,
b: 1.0,
a: 1.0,
}))
.with_props(ContentBoxItemLayout {
anchors: Rect {
left: 0.5,
right: 0.75,
top: 0.25,
bottom: 0.75,
},
..Default::default()
}),
);
DeclarativeApp::simple("Content Box", tree);
}
================================================
FILE: crates/_/examples/context_box.rs
================================================
// Make sure you have seen `portal_box` code example first, because this is an evolution of that.
use raui_app::{
app::{App, AppConfig, declarative::DeclarativeApp},
event::{ElementState, Event, VirtualKeyCode, WindowEvent},
};
use raui_core::{
make_widget, pre_hooks,
view_model::ViewModel,
widget::{
WidgetRef,
component::{
containers::{
anchor_box::PivotBoxProps,
content_box::content_box,
context_box::{ContextBoxProps, portals_context_box},
horizontal_box::{HorizontalBoxProps, horizontal_box},
portal_box::PortalsContainer,
},
image_box::{ImageBoxProps, image_box},
},
context::WidgetContext,
node::WidgetNode,
unit::{
flex::FlexBoxItemLayout,
image::{ImageBoxColor, ImageBoxMaterial, ImageBoxSizeValue},
},
utils::Color,
},
};
const DATA: &str = "data";
fn use_app(ctx: &mut WidgetContext) {
ctx.life_cycle.mount(|mut ctx| {
ctx.view_models
.bindings(DATA, "")
.unwrap()
.bind(ctx.id.to_owned());
});
}
#[pre_hooks(use_app)]
fn app(mut ctx: WidgetContext) -> WidgetNode {
let idref = WidgetRef::default();
// we read value from view model created with app builder.
let data = ctx
.view_models
.view_model(DATA)
.unwrap()
.read::<(bool, bool, bool)>()
.unwrap();
make_widget!(content_box)
.idref(idref.clone())
.with_shared_props(PortalsContainer(idref))
.listed_slot(
make_widget!(horizontal_box)
.with_props(HorizontalBoxProps {
separation: 25.0,
..Default::default()
})
.listed_slot(
make_widget!(icon)
// clear this flex box item layout (no growing, shrinking or filling).
.with_props(FlexBoxItemLayout::cleared())
// pass context box state read from app data.
.with_props(data.0)
// set icon color.
.with_props(Color {
r: 1.0,
g: 0.25,
b: 0.25,
a: 1.0,
})
// tell context widget how to position it relative to the content widget.
.with_props(PivotBoxProps {
pivot: 0.0.into(),
align: 0.0.into(),
}),
)
.listed_slot(
make_widget!(icon)
.with_props(FlexBoxItemLayout::cleared())
.with_props(data.1)
.with_props(Color {
r: 0.25,
g: 1.0,
b: 0.25,
a: 1.0,
})
.with_props(PivotBoxProps {
pivot: 0.5.into(),
align: 0.5.into(),
}),
)
.listed_slot(
make_widget!(icon)
.with_props(FlexBoxItemLayout::cleared())
.with_props(data.2)
.with_props(Color {
r: 0.25,
g: 0.25,
b: 1.0,
a: 1.0,
})
.with_props(PivotBoxProps {
pivot: 1.0.into(),
align: 1.0.into(),
}),
),
)
.into()
}
// custom icon component composed out of icon image as its content and context image that we show
// when bool props value is true.
fn icon(ctx: WidgetContext) -> WidgetNode {
// we use `portals_context_box` to allow this context box properly calculate context widget
// relative to the portals container.
make_widget!(portals_context_box)
// pass pivot props to context box,
.with_props(ctx.props.read_cloned_or_default::())
.with_props(ContextBoxProps {
// read bool props value and use it to tell if context widget is gonna be shown.
show: ctx.props.read_cloned_or_default::(),
})
// put colored image box as content widget.
.named_slot(
"content",
make_widget!(image_box).with_props(ImageBoxProps {
material: ImageBoxMaterial::Color(ImageBoxColor {
color: ctx.props.read_cloned_or_default::(),
..Default::default()
}),
width: ImageBoxSizeValue::Exact(100.0),
height: ImageBoxSizeValue::Exact(100.0),
..Default::default()
}),
)
// put gray image box as context widget.
.named_slot(
"context",
make_widget!(image_box).with_props(ImageBoxProps {
material: ImageBoxMaterial::Color(ImageBoxColor {
color: Color {
r: 0.25,
g: 0.25,
b: 0.25,
a: 1.0,
},
..Default::default()
}),
width: ImageBoxSizeValue::Exact(150.0),
height: ImageBoxSizeValue::Exact(50.0),
..Default::default()
}),
)
.into()
}
fn main() {
let app = DeclarativeApp::default()
.tree(make_widget!(app))
// we use tuple of 3 bools that will represent state of individual context box.
.view_model(DATA, ViewModel::new_object((false, true, false)))
.event(move |application, event, _, _| {
let mut data = application
.view_models
.get_mut(DATA)
.unwrap()
.write_notified::<(bool, bool, bool)>()
.unwrap();
if let Event::WindowEvent {
event: WindowEvent::KeyboardInput { input, .. },
..
} = event
&& input.state == ElementState::Pressed
&& let Some(key) = input.virtual_keycode
{
match key {
VirtualKeyCode::Key1 | VirtualKeyCode::Numpad1 => {
// change state of given context box in app data.
data.0 = !data.0;
}
VirtualKeyCode::Key2 | VirtualKeyCode::Numpad2 => {
data.1 = !data.1;
}
VirtualKeyCode::Key3 | VirtualKeyCode::Numpad3 => {
data.2 = !data.2;
}
_ => {}
}
}
true
});
App::new(AppConfig::default().title("Context Box")).run(app);
}
================================================
FILE: crates/_/examples/flex_box.rs
================================================
use raui_app::app::declarative::DeclarativeApp;
use raui_core::{
make_widget,
widget::{
component::{
containers::flex_box::{FlexBoxProps, flex_box},
image_box::{ImageBoxProps, image_box},
},
unit::flex::{FlexBoxDirection, FlexBoxItemLayout},
utils::Color,
},
};
fn main() {
let tree = make_widget!(flex_box)
.with_props(FlexBoxProps {
direction: FlexBoxDirection::VerticalBottomToTop,
..Default::default()
})
.listed_slot(
make_widget!(image_box)
.with_props(ImageBoxProps::colored(Color {
r: 1.0,
g: 0.25,
b: 0.25,
a: 1.0,
}))
.with_props(FlexBoxItemLayout {
// basis sets exact size of the item in main axis.
basis: Some(100.0),
// weight of the item when its layout box has to grow.
grow: 0.5,
// weight of the item when its layout box has to shrink (0.0 means no shrinking).
shrink: 0.0,
// percentage of the item size in cross axis (here how much of horizontal space it fills).
fill: 0.75,
// tells how much to which side item is aligned when there is free space available.
align: 1.0,
..Default::default()
}),
)
.listed_slot(
make_widget!(image_box)
.with_props(ImageBoxProps::colored(Color {
r: 0.25,
g: 1.0,
b: 0.25,
a: 1.0,
}))
.with_props(FlexBoxItemLayout {
margin: 10.0.into(),
..Default::default()
}),
)
.listed_slot(
make_widget!(image_box)
.with_props(ImageBoxProps::colored(Color {
r: 0.25,
g: 0.25,
b: 1.0,
a: 1.0,
}))
.with_props(FlexBoxItemLayout {
basis: Some(100.0),
grow: 0.0,
shrink: 0.5,
fill: 0.5,
align: 0.5,
..Default::default()
}),
);
DeclarativeApp::simple("Flex Box", tree);
}
================================================
FILE: crates/_/examples/flex_box_content_size.rs
================================================
use raui_app::app::declarative::DeclarativeApp;
use raui_core::{
make_widget,
widget::{
component::{
containers::{
flex_box::{FlexBoxProps, flex_box},
size_box::{SizeBoxProps, size_box},
},
image_box::{ImageBoxProps, image_box},
text_box::{TextBoxProps, text_box},
},
unit::{
flex::{FlexBoxDirection, FlexBoxItemLayout},
image::{ImageBoxColor, ImageBoxMaterial, ImageBoxSizeValue},
size::SizeBoxSizeValue,
text::{TextBoxFont, TextBoxSizeValue},
},
utils::Color,
},
};
fn main() {
let tree = make_widget!(size_box)
.with_props(SizeBoxProps {
width: SizeBoxSizeValue::Fill,
height: SizeBoxSizeValue::Content,
..Default::default()
})
.named_slot(
"content",
make_widget!(flex_box)
.with_props(FlexBoxProps {
direction: FlexBoxDirection::VerticalTopToBottom,
..Default::default()
})
.listed_slot(
make_widget!(text_box)
.with_props(FlexBoxItemLayout::no_growing_and_shrinking())
.with_props(TextBoxProps {
text: "Hello\nWorld!".to_owned(),
font: TextBoxFont {
name: "./demos/hello-world/resources/verdana.ttf".to_owned(),
size: 64.0,
},
color: Color {
r: 0.0,
g: 0.0,
b: 0.0,
a: 1.0,
},
height: TextBoxSizeValue::Content,
..Default::default()
}),
)
.listed_slot(
make_widget!(image_box)
.with_props(FlexBoxItemLayout::no_growing_and_shrinking())
.with_props(ImageBoxProps {
height: ImageBoxSizeValue::Exact(100.0),
material: ImageBoxMaterial::Color(ImageBoxColor {
color: Color {
r: 1.0,
g: 0.5,
b: 0.0,
a: 1.0,
},
..Default::default()
}),
..Default::default()
}),
)
// this image should not be visible at all, a zero size layout.
.listed_slot(
make_widget!(image_box).with_props(ImageBoxProps::colored(Color {
r: 0.5,
g: 0.5,
b: 0.5,
a: 1.0,
})),
),
);
DeclarativeApp::simple("Flex Box - Adaptive content size", tree);
}
================================================
FILE: crates/_/examples/flex_box_wrapping.rs
================================================
use raui_app::app::declarative::DeclarativeApp;
use raui_core::{
make_widget,
widget::{
component::{
containers::{
flex_box::{FlexBoxProps, flex_box},
size_box::{SizeBoxProps, size_box},
},
image_box::{ImageBoxProps, image_box},
},
unit::{
flex::{FlexBoxDirection, FlexBoxItemLayout},
size::SizeBoxSizeValue,
},
utils::Color,
},
};
fn main() {
let tree = make_widget!(flex_box)
.with_props(FlexBoxProps {
direction: FlexBoxDirection::VerticalTopToBottom,
// Wrapping makes children fit into multiple rows/columns.
wrap: true,
..Default::default()
})
.listed_slots((0..18).map(|_| {
make_widget!(size_box)
.with_props(FlexBoxItemLayout::cleared())
.with_props(SizeBoxProps {
width: SizeBoxSizeValue::Exact(100.0),
height: SizeBoxSizeValue::Exact(100.0),
margin: 20.0.into(),
..Default::default()
})
.named_slot(
"content",
make_widget!(image_box).with_props(ImageBoxProps::colored(Color {
r: 0.25,
g: 0.25,
b: 0.25,
a: 1.0,
})),
)
}));
DeclarativeApp::simple("Flex Box - Wrapping content", tree);
}
================================================
FILE: crates/_/examples/float_view.rs
================================================
use raui_app::app::{App, AppConfig, declarative::DeclarativeApp};
use raui_core::{
make_widget, pre_hooks,
view_model::{ViewModel, ViewModelValue},
widget::{
component::{
containers::float_box::{
FloatBoxChange, FloatBoxChangeMessage, FloatBoxNotifyProps, FloatBoxProps,
FloatBoxState, float_box,
},
image_box::{ImageBoxProps, image_box},
interactive::{
float_view::float_view_control,
navigation::{NavItemActive, use_nav_container_active},
},
},
context::WidgetContext,
node::WidgetNode,
unit::{
content::ContentBoxItemLayout,
image::{ImageBoxColor, ImageBoxMaterial, ImageBoxSizeValue},
},
utils::{Color, Rect, Vec2},
},
};
const DATA: &str = "data";
const PANELS: &str = "panels";
// AppData holds list of floating panels positions and their color.
struct AppData {
panels: ViewModelValue>,
}
fn use_app(ctx: &mut WidgetContext) {
ctx.life_cycle.mount(|mut ctx| {
ctx.view_models
.bindings(DATA, PANELS)
.unwrap()
.bind(ctx.id.to_owned());
});
ctx.life_cycle.unmount(|mut ctx| {
ctx.view_models
.bindings(DATA, PANELS)
.unwrap()
.unbind(ctx.id);
});
ctx.life_cycle.change(|mut ctx| {
let mut view_model = ctx
.view_models
.view_model_mut(DATA)
.unwrap()
.write::()
.unwrap();
for msg in ctx.messenger.messages {
// We listen for float box change messages sent from `float_view_control`
// widgets and move sender panel by delta of change.
if let Some(msg) = msg.as_any().downcast_ref::()
&& let Ok(index) = msg.sender.key().parse::()
&& let FloatBoxChange::RelativePosition(delta) = msg.change
&& let Some((position, _)) = view_model.panels.get_mut(index)
{
position.x += delta.x;
position.y += delta.y;
}
}
});
}
#[pre_hooks(use_nav_container_active, use_app)]
fn app(mut ctx: WidgetContext) -> WidgetNode {
let view_model = ctx
.view_models
.view_model(DATA)
.unwrap()
.read::()
.unwrap();
make_widget!(float_box)
.with_props(FloatBoxProps {
bounds_left: Some(-300.0),
bounds_right: Some(600.0),
bounds_top: Some(-300.0),
bounds_bottom: Some(400.0),
})
.with_props(FloatBoxState {
position: Vec2 { x: 0.0, y: 0.0 },
zoom: 2.0,
})
.listed_slot(
// `float_view_control` widget reacts to dragging action and sends
// that dragging movement delta to widget that wants to be notified.
// In this case, we want to notify `float_box` widget so it will
// reposition its content panels.
make_widget!(float_view_control)
.key("panning")
.with_props(NavItemActive)
// we make sure panning control fills entire area and stays
// in its bounds no matter how content gets repositioned.
.with_props(ContentBoxItemLayout {
anchors: Rect {
left: 0.0,
top: 0.0,
right: 1.0,
bottom: 1.0,
},
keep_in_bounds: true.into(),
..Default::default()
})
.named_slot(
"content",
make_widget!(image_box).with_props(ImageBoxProps::colored(Color {
r: 0.3,
g: 0.3,
b: 0.3,
a: 1.0,
})),
),
)
.listed_slots(
view_model
.panels
.iter()
.enumerate()
.map(|(index, (position, color))| {
// we also use `float_view_control` widget for panels so
// they can be dragged around float box.
make_widget!(float_view_control)
.key(index)
.with_props(NavItemActive)
.with_props(FloatBoxNotifyProps(ctx.id.to_owned().into()))
.with_props(ContentBoxItemLayout {
offset: *position,
..Default::default()
})
.named_slot(
"content",
make_widget!(image_box).with_props(ImageBoxProps {
width: ImageBoxSizeValue::Exact(200.0),
height: ImageBoxSizeValue::Exact(150.0),
material: ImageBoxMaterial::Color(ImageBoxColor {
color: *color,
..Default::default()
}),
..Default::default()
}),
)
}),
)
.into()
}
fn main() {
let panels = vec![
(
Vec2 { x: 0.0, y: 0.0 },
Color {
r: 1.0,
g: 0.5,
b: 0.5,
a: 1.0,
},
),
(
Vec2 { x: 100.0, y: 100.0 },
Color {
r: 0.5,
g: 1.0,
b: 0.5,
a: 1.0,
},
),
(
Vec2 { x: 200.0, y: 200.0 },
Color {
r: 0.5,
g: 0.5,
b: 1.0,
a: 1.0,
},
),
];
let app = DeclarativeApp::default()
.tree(make_widget!(app))
.view_model(
DATA,
ViewModel::produce(|properties| AppData {
panels: ViewModelValue::new(panels, properties.notifier(PANELS)),
}),
);
App::new(AppConfig::default().title("Float View")).run(app);
}
================================================
FILE: crates/_/examples/grid_box.rs
================================================
use raui_app::app::declarative::DeclarativeApp;
use raui_core::{
make_widget,
widget::{
component::{
containers::grid_box::{GridBoxProps, grid_box},
image_box::{ImageBoxProps, image_box},
},
unit::grid::GridBoxItemLayout,
utils::{Color, IntRect},
},
};
fn main() {
let tree = make_widget!(grid_box)
.with_props(GridBoxProps {
cols: 2,
rows: 2,
..Default::default()
})
.listed_slot(
make_widget!(image_box)
.with_props(ImageBoxProps::colored(Color {
r: 1.0,
g: 0.25,
b: 0.25,
a: 1.0,
}))
.with_props(GridBoxItemLayout {
space_occupancy: IntRect {
left: 0,
right: 1,
top: 0,
bottom: 1,
},
..Default::default()
}),
)
.listed_slot(
make_widget!(image_box)
.with_props(ImageBoxProps::colored(Color {
r: 0.25,
g: 1.0,
b: 0.25,
a: 1.0,
}))
.with_props(GridBoxItemLayout {
space_occupancy: IntRect {
left: 1,
right: 2,
top: 0,
bottom: 1,
},
..Default::default()
}),
)
.listed_slot(
make_widget!(image_box)
.with_props(ImageBoxProps::colored(Color {
r: 0.25,
g: 0.25,
b: 1.0,
a: 1.0,
}))
.with_props(GridBoxItemLayout {
space_occupancy: IntRect {
left: 0,
right: 2,
top: 1,
bottom: 2,
},
..Default::default()
}),
);
DeclarativeApp::simple("Grid Box", tree);
}
================================================
FILE: crates/_/examples/horizontal_box.rs
================================================
use raui_app::app::declarative::DeclarativeApp;
use raui_core::{
make_widget,
widget::{
component::{
containers::horizontal_box::{HorizontalBoxProps, horizontal_box},
image_box::{ImageBoxProps, image_box},
},
unit::flex::FlexBoxItemLayout,
utils::Color,
},
};
fn main() {
let tree = make_widget!(horizontal_box)
.with_props(HorizontalBoxProps {
separation: 50.0,
..Default::default()
})
.listed_slot(
make_widget!(image_box)
.with_props(ImageBoxProps::colored(Color {
r: 1.0,
g: 0.25,
b: 0.25,
a: 1.0,
}))
.with_props(FlexBoxItemLayout {
// basis sets exact width of the item.
basis: Some(100.0),
// weight of the item when its layout box has to grow in width.
grow: 0.5,
// weight of the item when its layout box has to shrink in width (0.0 means no shrinking).
shrink: 0.0,
..Default::default()
}),
)
.listed_slot(
make_widget!(image_box).with_props(ImageBoxProps::colored(Color {
r: 0.25,
g: 1.0,
b: 0.25,
a: 1.0,
})),
)
.listed_slot(
make_widget!(image_box)
.with_props(ImageBoxProps::colored(Color {
r: 0.25,
g: 0.25,
b: 1.0,
a: 1.0,
}))
.with_props(FlexBoxItemLayout {
basis: Some(100.0),
grow: 0.0,
shrink: 0.5,
..Default::default()
}),
);
DeclarativeApp::simple("Horizontal Box", tree);
}
================================================
FILE: crates/_/examples/image_box_color.rs
================================================
use raui_app::app::declarative::DeclarativeApp;
use raui_core::{
make_widget,
widget::{
component::image_box::{ImageBoxProps, image_box},
unit::image::{ImageBoxColor, ImageBoxMaterial},
utils::Color,
},
};
fn main() {
let tree = make_widget!(image_box).with_props(ImageBoxProps {
material: ImageBoxMaterial::Color(ImageBoxColor {
color: Color {
r: 1.0,
g: 0.25,
b: 0.25,
a: 1.0,
},
..Default::default()
}),
..Default::default()
});
DeclarativeApp::simple("Image Box - Color", tree);
}
================================================
FILE: crates/_/examples/image_box_frame.rs
================================================
use raui_app::app::declarative::DeclarativeApp;
use raui_core::{
make_widget,
widget::{
component::image_box::{ImageBoxProps, image_box},
unit::image::{ImageBoxFrame, ImageBoxImage, ImageBoxImageScaling, ImageBoxMaterial},
},
};
fn main() {
let tree = make_widget!(image_box).with_props(ImageBoxProps {
material: ImageBoxMaterial::Image(ImageBoxImage {
id: "./demos/in-game/resources/images/slider-background.png".to_owned(),
// enable nine-slice by setting Frame scaling.
scaling: ImageBoxImageScaling::Frame(ImageBoxFrame {
// rectangle that describes margins of the frame of the source image texture.
source: 3.0.into(),
// rectangle that describes margins of the frame of the UI image being presented.
destination: 64.0.into(),
..Default::default()
}),
..Default::default()
}),
..Default::default()
});
DeclarativeApp::simple("Image Box - Frame", tree);
}
================================================
FILE: crates/_/examples/image_box_image.rs
================================================
use raui_app::app::declarative::DeclarativeApp;
use raui_core::{
make_widget,
widget::{
component::image_box::{ImageBoxProps, image_box},
unit::image::{ImageBoxAspectRatio, ImageBoxImage, ImageBoxMaterial},
},
};
fn main() {
let tree = make_widget!(image_box).with_props(ImageBoxProps {
material: ImageBoxMaterial::Image(ImageBoxImage {
id: "./demos/hello-world/resources/cats.jpg".to_owned(),
..Default::default()
}),
// makes internal image size keeping its aspect ratio.
content_keep_aspect_ratio: Some(ImageBoxAspectRatio {
// horizontal alignment of the content relative to the horizontal free space.
horizontal_alignment: 0.5,
// vertical alignment of the content relative to the vertical free space.
vertical_alignment: 0.5,
// if set to true then content instead of getting smaller to fit inside the layout box,
// it will "leak" outside of the layout box.
outside: true,
}),
..Default::default()
});
DeclarativeApp::simple("Image Box - Image", tree);
}
================================================
FILE: crates/_/examples/image_box_procedural.rs
================================================
use raui_app::app::declarative::DeclarativeApp;
use raui_core::{
layout::CoordsMappingScaling,
make_widget,
widget::{
component::image_box::{ImageBoxProps, image_box},
unit::image::{ImageBoxMaterial, ImageBoxProcedural, ImageBoxProceduralVertex},
utils::{Color, Vec2},
},
};
fn main() {
let tree = make_widget!(image_box).with_props(ImageBoxProps {
// procedural image material allows to draw custom mesh with dedicated
// shader either from statics or from file.
// available static shaders:
// - `@pass`: simple pass through shader that ignores camera matrix.
// - `@colored`: shader that applies camera transform and color vertices.
// - `@textured`: shader that applies camera transform and texture with color vertices.
// if we want to use shader from files, assuming we have two files:
// - `path/to/shader.vs`
// - `path/to/shader.fs`
// then our id would be: `path/to/shader`.
material: ImageBoxMaterial::Procedural(
ImageBoxProcedural::new("@colored")
// if we tell material to remap vertices from its local
// coordinate space to rendered screen space.
// Here we keep mesh inside image box keeping aspect ratio.
.vertex_mapping(CoordsMappingScaling::FitToView(
Vec2 { x: 1.0, y: 1.0 },
true,
))
.quad([
ImageBoxProceduralVertex {
position: Vec2 { x: 0.5, y: 0.0 },
color: Color {
r: 1.0,
g: 0.0,
b: 0.0,
a: 1.0,
},
..Default::default()
},
ImageBoxProceduralVertex {
position: Vec2 { x: 1.0, y: 0.5 },
color: Color {
r: 0.0,
g: 1.0,
b: 0.0,
a: 1.0,
},
..Default::default()
},
ImageBoxProceduralVertex {
position: Vec2 { x: 0.5, y: 1.0 },
color: Color {
r: 1.0,
g: 1.0,
b: 0.0,
a: 1.0,
},
..Default::default()
},
ImageBoxProceduralVertex {
position: Vec2 { x: 0.0, y: 0.5 },
color: Color {
r: 0.0,
g: 0.0,
b: 1.0,
a: 1.0,
},
..Default::default()
},
]),
),
..Default::default()
});
DeclarativeApp::simple("Image Box - Procedural", tree);
}
================================================
FILE: crates/_/examples/immediate_mode.rs
================================================
// Example of immediate mode UI on top of RAUI.
// It's goal is to bring more ergonomics to RAUI by hiding
// declarative interface under simple nested function calls.
// As with retained mode, immediate mode UI can be mixed with
// declarative mode and retained mode widgets.
use raui_app::app::immediate::ImmediateApp;
use raui_core::{
Scalar,
widget::{
component::{
containers::{
horizontal_box::HorizontalBoxProps, vertical_box::VerticalBoxProps,
wrap_box::WrapBoxProps,
},
image_box::ImageBoxProps,
interactive::{
input_field::{TextInputMode, input_text_with_cursor},
navigation::NavItemActive,
},
text_box::TextBoxProps,
},
unit::{flex::FlexBoxItemLayout, text::TextBoxFont},
utils::Color,
},
};
use raui_immediate::{ImProps, apply};
use raui_immediate_widgets::core::{
containers::{content_box, horizontal_box, nav_vertical_box, wrap_box},
image_box,
interactive::{ImmediateButton, button, input_field, self_tracking},
text_box,
};
const FONT: &str = "./demos/hello-world/resources/verdana.ttf";
// app function widget, we pass application state there.
pub fn app(value: &mut usize) {
let props = WrapBoxProps {
margin: 20.0.into(),
..Default::default()
};
wrap_box(props, || {
let props = VerticalBoxProps {
separation: 50.0,
..Default::default()
};
// we can use any "immedietified" RAUI widget we want.
// we can pass Props to parameterize RAUI widget in first param.
// BTW. we should make sure to use any `nav_*` container widget
// somewhere in the app root to make app interactive.
nav_vertical_box(props, || {
let layout = FlexBoxItemLayout {
basis: Some(48.0),
grow: 0.0,
shrink: 0.0,
..Default::default()
};
// we can also apply props on all produced widgets in the scope.
apply(ImProps(layout), || {
counter(value);
let props = HorizontalBoxProps {
separation: 50.0,
..Default::default()
};
horizontal_box(props, || {
// we can react to button-like behavior by reading what
// button-like widgets return of their tracked state.
if text_button("Increment").trigger_start() {
*value = value.saturating_add(1);
}
if text_button("Decrement").trigger_start() {
*value = value.saturating_sub(1);
}
});
});
self_tracking((), |tracking| {
image_box(ImageBoxProps::colored(Color {
r: tracking.state.factor.x,
g: 0.0,
b: tracking.state.factor.y,
a: 1.0,
}));
});
});
});
}
fn text_button(text: &str) -> ImmediateButton {
// buttons use `use_state` hook under the hood to track
// declarative mode button state, that's copy of being
// returned from button function and passed into its
// group closure for children widgets to use.
// BTW. don't forget to apply `NavItemActive` props on
// button if you want to have it enabled for navigation.
button(NavItemActive, |state| {
content_box((), || {
image_box(ImageBoxProps::colored(Color {
r: if state.state.selected { 1.0 } else { 0.75 },
g: if state.state.trigger { 1.0 } else { 0.75 },
b: if state.state.context { 1.0 } else { 0.75 },
a: 1.0,
}));
text_box(TextBoxProps {
text: text.to_string(),
font: TextBoxFont {
name: FONT.to_owned(),
size: 32.0,
},
color: Color {
r: 0.0,
g: 0.0,
b: 0.0,
a: 1.0,
},
..Default::default()
});
});
})
}
fn counter(value: &mut usize) {
// counter widget is a text box wrapped in an input field.
// it works like combination of button (can be focused by
// selection/navigation) and text field (collects keyboard
// text characters when focused).
let props = (NavItemActive, TextInputMode::UnsignedInteger);
let (result, ..) = input_field(value, props, |text, state, button| {
text_box(TextBoxProps {
text: if state.focused {
input_text_with_cursor(text, state.cursor_position, '|')
} else if text.is_empty() {
"...".to_owned()
} else {
text.to_owned()
},
font: TextBoxFont {
name: FONT.to_owned(),
size: 32.0,
},
color: Color {
r: Scalar::from(button.state.trigger),
g: Scalar::from(button.state.selected),
b: Scalar::from(state.focused),
a: 1.0,
},
..Default::default()
});
});
if let Some(result) = result {
*value = result;
}
}
fn main() {
// some applciation state.
let mut counter = 0usize;
ImmediateApp::simple("Immediate mode UI", move |_| {
app(&mut counter);
});
}
================================================
FILE: crates/_/examples/immediate_mode_access_and_tests.rs
================================================
use raui_app::app::immediate::ImmediateApp;
use raui_core::widget::{
component::{image_box::ImageBoxProps, interactive::navigation::NavItemActive},
utils::Color,
};
use raui_immediate::{register_access, use_access};
use raui_immediate_widgets::core::{
containers::nav_content_box,
image_box,
interactive::{ImmediateButton, button},
};
pub fn app() {
nav_content_box((), || {
clickable_button();
});
}
pub fn clickable_button() {
if colored_button().trigger_start() {
// we use access point to some host data
let clicked = use_access::("clicked");
*clicked.write().unwrap() = true;
}
}
fn colored_button() -> ImmediateButton {
button(NavItemActive, |state| {
let props = ImageBoxProps::colored(if state.state.trigger {
Color {
r: 0.5,
g: 0.0,
b: 0.0,
a: 1.0,
}
} else {
Color {
r: 0.0,
g: 0.5,
b: 0.0,
a: 1.0,
}
});
image_box(props);
})
}
fn main() {
let mut clicked = false;
ImmediateApp::simple("Immediate mode UI - Access and tests", move |_| {
// here we register access point to some game state
let _lifetime = register_access("clicked", &mut clicked);
app();
});
}
#[cfg(test)]
mod tests {
use super::*;
use raui_core::{
interactive::default_interactions_engine::{Interaction, PointerButton},
layout::CoordsMapping,
tester::AppCycleTester,
widget::utils::Rect,
};
use raui_immediate::ImmediateContext;
#[test]
fn test_tracked_button() {
let mut tester = AppCycleTester::new(
CoordsMapping::new(Rect {
left: 0.0,
right: 1024.0,
top: 0.0,
bottom: 576.0,
}),
ImmediateContext::default(),
);
let mut mock = false;
tester
.interactions_engine
.interact(Interaction::PointerDown(
PointerButton::Trigger,
[100.0, 100.0].into(),
));
// since RAUI has deferred UI resolution, signal will take
// few frames to go through declarative layer to immediate
// layer and then back to user site.
for _ in 0..4 {
tester.run_frame(ImmediateApp::test_frame(|| {
// and here we register access point to mock data
let _lifetime = register_access("clicked", &mut mock);
app();
}));
}
assert_eq!(mock, true);
}
}
================================================
FILE: crates/_/examples/immediate_mode_stack_props.rs
================================================
use raui_app::app::immediate::ImmediateApp;
use raui_core::widget::{
component::text_box::TextBoxProps,
unit::{
flex::FlexBoxItemLayout,
text::{TextBoxFont, TextBoxHorizontalAlign},
},
utils::Color,
};
use raui_immediate::{ImProps, ImStackProps, apply, use_stack_props};
use raui_immediate_widgets::core::{containers::nav_vertical_box, text_box};
pub fn app() {
let props = TextBoxProps {
font: TextBoxFont {
name: "./demos/hello-world/resources/verdana.ttf".to_owned(),
size: 96.0,
},
color: Color {
r: 1.0,
g: 0.0,
b: 0.0,
a: 1.0,
},
..Default::default()
};
// We can create cascaded styling with stack props.
// The difference between stack props and applied props
// is that applied props are applied directly do its
// children nodes, while stack props are stacked so any
// widget in hierarchy can access the top of the props
// stack - we can easily share style down the hierarchy!
apply(ImStackProps::new(props), || {
nav_vertical_box((), || {
let layout = FlexBoxItemLayout {
basis: Some(100.0),
grow: 0.0,
shrink: 0.0,
margin: 32.0.into(),
..Default::default()
};
// These props apply only to label widgets.
apply(ImProps(layout), || {
label("Hey!");
label("Hi!");
let props = TextBoxProps {
font: TextBoxFont {
name: "./demos/in-game/resources/fonts/MiKrollFantasy.ttf".to_owned(),
size: 100.0,
},
color: Color {
r: 0.0,
g: 0.0,
b: 1.0,
a: 1.0,
},
horizontal_align: TextBoxHorizontalAlign::Center,
..Default::default()
};
// By pushing new props on stack props we override
// what's gonna be used in all chidren in hierarchy.
apply(ImStackProps::new(props), || {
label("Hello!");
label("Ohayo?");
});
});
});
});
}
pub fn label(text: impl ToString) {
// Accessing props from the stack to achieve cascading styles.
let mut props = use_stack_props::().unwrap_or_default();
props.text = text.to_string();
text_box(props);
}
fn main() {
ImmediateApp::simple("Immediate mode UI - Stack props", |_| app());
}
================================================
FILE: crates/_/examples/immediate_mode_states_and_effects.rs
================================================
// Make sure you have seen `immediate_mode` code example first, because this is a continuation of that.
use raui_app::app::immediate::ImmediateApp;
use raui_core::widget::{
component::{
containers::wrap_box::WrapBoxProps, image_box::ImageBoxProps,
interactive::navigation::NavItemActive, text_box::TextBoxProps,
},
unit::text::TextBoxFont,
utils::Color,
};
use raui_immediate::{ImmediateOnMount, ImmediateOnUnmount, use_effects, use_state};
use raui_immediate_widgets::core::{
containers::{content_box, nav_vertical_box, wrap_box},
image_box,
interactive::{ImmediateButton, button},
text_box,
};
const FONT: &str = "./demos/hello-world/resources/verdana.ttf";
pub fn app() {
let props = WrapBoxProps {
margin: 20.0.into(),
..Default::default()
};
wrap_box(props, || {
nav_vertical_box((), || {
// `use_state` allows to keep persistent state across
// multiple frames, as long as order of calls and types
// match between frames.
let flag = use_state(|| false);
let mut flag = flag.write().unwrap();
let counter = use_state(|| 0usize);
let counter_mount = counter.clone();
if text_button("Toggle").trigger_start() {
*flag = !*flag;
}
if *flag {
// effects are passed as props, these are callbacks
// that get executed whenever RAUI widget gets mounted,
// unmounted or changed.
// There is also `ImmediateHooks` props that allow to
// apply RAUI hooks to rendered widget, useful for example
// to render effects widget with any custom behavior.
let effects = (
ImmediateOnMount::new(move || {
println!("Mounted!");
*counter_mount.write().unwrap() += 1;
}),
ImmediateOnUnmount::new(|| {
println!("Unmounted!");
}),
);
use_effects(effects, || {
label(format!("Mounted {} times!", *counter.read().unwrap()));
});
}
});
});
}
fn label(text: impl ToString) {
text_box(TextBoxProps {
text: text.to_string(),
font: TextBoxFont {
name: crate::FONT.to_owned(),
size: 32.0,
},
color: Color {
r: 0.0,
g: 0.0,
b: 0.0,
a: 1.0,
},
..Default::default()
});
}
fn text_button(text: &str) -> ImmediateButton {
button(NavItemActive, |state| {
content_box((), || {
image_box(ImageBoxProps::colored(Color {
r: if state.state.selected { 1.0 } else { 0.75 },
g: if state.state.trigger { 1.0 } else { 0.75 },
b: if state.state.context { 1.0 } else { 0.75 },
a: 1.0,
}));
text_box(TextBoxProps {
text: text.to_string(),
font: TextBoxFont {
name: crate::FONT.to_owned(),
size: 32.0,
},
color: Color {
r: 0.0,
g: 0.0,
b: 0.0,
a: 1.0,
},
..Default::default()
});
});
})
}
fn main() {
ImmediateApp::simple("Immediate mode UI - States and Effects", |_| {
app();
});
}
================================================
FILE: crates/_/examples/immediate_text_field_paper.rs
================================================
use raui_app::app::immediate::ImmediateApp;
use raui_core::widget::{
component::{containers::size_box::SizeBoxProps, interactive::navigation::NavItemActive},
unit::{size::SizeBoxSizeValue, text::TextBoxFont},
utils::Rect,
};
use raui_immediate::{ImSharedProps, apply, use_state};
use raui_immediate_widgets::{
core::containers::size_box,
material::{containers::nav_paper, interactive::text_field_paper},
};
use raui_material::{
component::interactive::text_field_paper::TextFieldPaperProps,
theme::{ThemeColor, ThemeProps, ThemedTextMaterial, ThemedWidgetProps, new_dark_theme},
};
// Create a new theme with a custom text variant for input fields.
fn new_theme() -> ThemeProps {
let mut theme = new_dark_theme();
theme.text_variants.insert(
"input".to_owned(),
ThemedTextMaterial {
font: TextBoxFont {
name: "./demos/hello-world/resources/verdana.ttf".to_owned(),
size: 24.0,
},
..Default::default()
},
);
theme
}
fn main() {
ImmediateApp::simple("Immediate mode Text Field Paper", |_| {
// Apply the custom theme for all UI widgets.
apply(ImSharedProps(new_theme()), || {
// Make navigable paper container for the text field.
// Navigable containers are required to make interactive widgets work.
nav_paper((), || {
let props = SizeBoxProps {
width: SizeBoxSizeValue::Fill,
height: SizeBoxSizeValue::Exact(50.0),
margin: 20.0.into(),
..Default::default()
};
size_box(props, || {
let props = (
TextFieldPaperProps {
hint: "> Type some text...".to_owned(),
paper_theme: ThemedWidgetProps {
color: ThemeColor::Primary,
..Default::default()
},
padding: Rect {
left: 10.0,
right: 10.0,
top: 6.0,
bottom: 6.0,
},
variant: "input".to_owned(),
..Default::default()
},
NavItemActive,
);
// Make state holding the text input value.
let text = use_state(|| "Hello!".to_owned());
// Make the text field paper with the text input state and
// override existing value on change.
let value = text_field_paper(&*text.read().unwrap(), props).0;
if let Some(value) = value {
*text.write().unwrap() = value;
}
});
});
});
});
}
================================================
FILE: crates/_/examples/input_field.rs
================================================
use raui_app::app::{App, AppConfig, declarative::DeclarativeApp};
use raui_core::{
Managed, Scalar, make_widget, pre_hooks,
view_model::{ViewModel, ViewModelValue},
widget::{
component::{
containers::vertical_box::vertical_box,
interactive::{
button::{
ButtonNotifyMessage, ButtonNotifyProps, ButtonProps, use_button_notified_state,
},
input_field::{
TextInputControlNotifyMessage, TextInputControlNotifyProps, TextInputMode,
TextInputNotifyMessage, TextInputNotifyProps, TextInputProps, TextInputState,
input_field, input_text_with_cursor, use_text_input_notified_state,
},
navigation::{NavItemActive, use_nav_container_active},
},
text_box::{TextBoxProps, text_box},
},
context::WidgetContext,
node::WidgetNode,
unit::text::{TextBoxFont, TextBoxSizeValue},
utils::Color,
},
};
const DATA: &str = "data";
const TEXT_INPUT: &str = "text-input";
const NUMBER_INPUT: &str = "number-input";
const INTEGER_INPUT: &str = "integer-input";
const UNSIGNED_INTEGER_INPUT: &str = "unsigned-integer-input";
const FILTER_INPUT: &str = "filter-input";
struct AppData {
text_input: Managed>,
number_input: Managed>,
integer_input: Managed>,
unsigned_integer_input: Managed>,
filter_input: Managed>,
}
fn use_app(ctx: &mut WidgetContext) {
ctx.life_cycle.mount(|mut ctx| {
ctx.view_models
.bindings(DATA, TEXT_INPUT)
.unwrap()
.bind(ctx.id.to_owned());
ctx.view_models
.bindings(DATA, NUMBER_INPUT)
.unwrap()
.bind(ctx.id.to_owned());
ctx.view_models
.bindings(DATA, INTEGER_INPUT)
.unwrap()
.bind(ctx.id.to_owned());
ctx.view_models
.bindings(DATA, UNSIGNED_INTEGER_INPUT)
.unwrap()
.bind(ctx.id.to_owned());
ctx.view_models
.bindings(DATA, FILTER_INPUT)
.unwrap()
.bind(ctx.id.to_owned());
});
}
// we mark root widget as navigable container to let user focus and type in text inputs.
#[pre_hooks(use_nav_container_active, use_app)]
fn app(mut ctx: WidgetContext) -> WidgetNode {
let mut app_data = ctx
.view_models
.view_model_mut(DATA)
.unwrap()
.write::()
.unwrap();
// put inputs with all different types modes.
make_widget!(vertical_box)
.listed_slot(
make_widget!(input)
.with_props(TextInputMode::Text)
.with_props(TextInputProps {
allow_new_line: false,
text: Some(app_data.text_input.lazy().into()),
}),
)
.listed_slot(
make_widget!(input)
.with_props(TextInputMode::Number)
.with_props(TextInputProps {
allow_new_line: false,
text: Some(app_data.number_input.lazy().into()),
}),
)
.listed_slot(
make_widget!(input)
.with_props(TextInputMode::Integer)
.with_props(TextInputProps {
allow_new_line: false,
text: Some(app_data.integer_input.lazy().into()),
}),
)
.listed_slot(
make_widget!(input)
.with_props(TextInputMode::UnsignedInteger)
.with_props(TextInputProps {
allow_new_line: false,
text: Some(app_data.unsigned_integer_input.lazy().into()),
}),
)
.listed_slot(
make_widget!(input)
.with_props(TextInputMode::Filter(|_, character| {
character.is_uppercase()
}))
.with_props(TextInputProps {
allow_new_line: false,
text: Some(app_data.filter_input.lazy().into()),
}),
)
.into()
}
fn use_input(ctx: &mut WidgetContext) {
ctx.life_cycle.change(|ctx| {
for msg in ctx.messenger.messages {
if let Some(msg) = msg.as_any().downcast_ref::() {
println!("* Text input: {msg:#?}");
} else if let Some(msg) = msg.as_any().downcast_ref::() {
println!("* Text input control: {msg:#?}");
} else if let Some(msg) = msg.as_any().downcast_ref::() {
println!("* Button: {msg:#?}");
}
}
});
}
// this component will receive and store button and input text state changes.
#[pre_hooks(use_button_notified_state, use_text_input_notified_state, use_input)]
fn input(mut ctx: WidgetContext) -> WidgetNode {
let ButtonProps {
selected, trigger, ..
} = ctx.state.read_cloned_or_default();
let TextInputState {
cursor_position,
focused,
} = ctx.state.read_cloned_or_default();
let TextInputProps {
allow_new_line,
text,
} = ctx.props.read_cloned_or_default();
let mode = ctx.props.read_cloned_or_default::();
let value = text
.as_ref()
.and_then(|text| mode.process(&text.get()))
.unwrap_or_default();
// input field is an evolution of input text, what changes is input field can be focused
// because it is input text plus button.
make_widget!(input_field)
// as usually we enable this navigation item.
.with_props(NavItemActive)
// pass text input mode to the input field (by default Text mode is used).
.with_props(mode)
// setup text input.
.with_props(TextInputProps {
allow_new_line,
text,
})
// notify this component about input text state change.
.with_props(TextInputNotifyProps(ctx.id.to_owned().into()))
// notify this component about input control characters it receives.
// useful for reacting to Tab key for example.
.with_props(TextInputControlNotifyProps(ctx.id.to_owned().into()))
// notify this component about button state change.
.with_props(ButtonNotifyProps(ctx.id.to_owned().into()))
.named_slot(
"content",
// input field and input text components doesn't assume any content widget for you so
// that's why we create custom input component to make it work and look exactly as we
// want - here we just put a text box.
make_widget!(text_box).with_props(TextBoxProps {
text: if focused {
input_text_with_cursor(&value, cursor_position, '|')
} else if value.is_empty() {
match mode {
TextInputMode::Text => "> Type text...".to_owned(),
TextInputMode::Number => "> Type number...".to_owned(),
TextInputMode::Integer => "> Type integer...".to_owned(),
TextInputMode::UnsignedInteger => "> Type unsigned integer...".to_owned(),
TextInputMode::Filter(_) => "> Type uppercase text...".to_owned(),
}
} else {
value
},
width: TextBoxSizeValue::Fill,
height: TextBoxSizeValue::Exact(48.0),
font: TextBoxFont {
name: "./demos/hello-world/resources/verdana.ttf".to_owned(),
size: 32.0,
},
color: Color {
r: Scalar::from(trigger),
g: Scalar::from(selected),
b: Scalar::from(focused),
a: 1.0,
},
..Default::default()
}),
)
.into()
}
fn main() {
let app = DeclarativeApp::default()
.tree(make_widget!(app))
.view_model(
DATA,
ViewModel::produce(|properties| AppData {
text_input: Managed::new(ViewModelValue::new(
Default::default(),
properties.notifier(TEXT_INPUT),
)),
number_input: Managed::new(ViewModelValue::new(
Default::default(),
properties.notifier(NUMBER_INPUT),
)),
integer_input: Managed::new(ViewModelValue::new(
Default::default(),
properties.notifier(INTEGER_INPUT),
)),
unsigned_integer_input: Managed::new(ViewModelValue::new(
Default::default(),
properties.notifier(UNSIGNED_INTEGER_INPUT),
)),
filter_input: Managed::new(ViewModelValue::new(
Default::default(),
properties.notifier(FILTER_INPUT),
)),
}),
);
App::new(AppConfig::default().title("Input Field")).run(app);
}
================================================
FILE: crates/_/examples/navigation.rs
================================================
use raui_app::app::{App, AppConfig, declarative::DeclarativeApp};
use raui_core::{
make_widget, pre_hooks,
widget::{
component::{
containers::{
horizontal_box::{HorizontalBoxProps, horizontal_box},
vertical_box::{VerticalBoxProps, vertical_box},
},
image_box::{ImageBoxProps, image_box},
interactive::{
button::{ButtonProps, button},
navigation::{
NavAutoSelect, NavItemActive, use_nav_container_active,
use_nav_jump_direction_active,
},
},
},
context::WidgetContext,
node::WidgetNode,
unit::flex::FlexBoxItemLayout,
utils::Color,
},
};
#[pre_hooks(use_nav_container_active, use_nav_jump_direction_active)]
fn app(mut ctx: WidgetContext) -> WidgetNode {
let slots_layout = FlexBoxItemLayout {
margin: 20.0.into(),
..Default::default()
};
make_widget!(vertical_box)
.key("vertical")
.with_props(VerticalBoxProps {
override_slots_layout: Some(slots_layout.clone()),
..Default::default()
})
.listed_slot(
make_widget!(horizontal_box)
.key("horizontal")
.with_props(HorizontalBoxProps {
override_slots_layout: Some(slots_layout),
..Default::default()
})
.listed_slot(make_widget!(button_item).key("a").with_props(NavAutoSelect))
.listed_slot(make_widget!(button_item).key("b"))
.listed_slot(make_widget!(button_item).key("c")),
)
.listed_slot(make_widget!(button_item).key("d"))
.listed_slot(make_widget!(button_item).key("e"))
.into()
}
fn button_item(ctx: WidgetContext) -> WidgetNode {
make_widget!(button)
.key("button")
.merge_props(ctx.props.clone())
.with_props(NavItemActive)
.named_slot("content", make_widget!(button_content))
.into()
}
fn button_content(ctx: WidgetContext) -> WidgetNode {
let ButtonProps {
selected,
trigger,
context,
..
} = ctx.props.read_cloned_or_default();
let color = if trigger {
Color {
r: 1.0,
g: 0.25,
b: 0.25,
a: 1.0,
}
} else if context {
Color {
r: 0.25,
g: 1.0,
b: 0.25,
a: 1.0,
}
} else if selected {
Color {
r: 0.25,
g: 0.25,
b: 1.0,
a: 1.0,
}
} else {
Color {
r: 0.25,
g: 0.25,
b: 0.25,
a: 1.0,
}
};
make_widget!(image_box)
.key("image")
.with_props(ImageBoxProps::colored(color))
.into()
}
fn main() {
App::new(AppConfig::default().title("Navigation")).run(
DeclarativeApp::default()
.tree(make_widget!(app).key("app"))
.setup_interactions(|interactions| {
interactions.engine.deselect_when_no_button_found = false;
}),
);
}
================================================
FILE: crates/_/examples/options_view.rs
================================================
use raui_app::app::{App, AppConfig, declarative::DeclarativeApp};
use raui_core::{
Managed, make_widget, pre_hooks,
view_model::{ViewModel, ViewModelValue},
widget::{
WidgetRef,
component::{
containers::{
anchor_box::PivotBoxProps, content_box::content_box, portal_box::PortalsContainer,
size_box::SizeBoxProps, vertical_box::vertical_box,
},
image_box::{ImageBoxProps, image_box},
interactive::{
button::ButtonProps,
navigation::{NavItemActive, use_nav_container_active},
options_view::{OptionsViewMode, OptionsViewProps, options_view},
},
text_box::{TextBoxProps, text_box},
},
context::WidgetContext,
node::WidgetNode,
unit::{
content::ContentBoxItemLayout,
size::SizeBoxSizeValue,
text::{TextBoxFont, TextBoxHorizontalAlign, TextBoxVerticalAlign},
},
utils::{Color, Rect},
},
};
const DATA: &str = "data";
const INDEX: &str = "index";
struct AppData {
index: Managed>,
}
fn use_app(ctx: &mut WidgetContext) {
ctx.life_cycle.mount(|mut ctx| {
ctx.view_models
.bindings(DATA, INDEX)
.unwrap()
.bind(ctx.id.to_owned());
});
}
#[pre_hooks(use_nav_container_active, use_app)]
fn app(mut ctx: WidgetContext) -> WidgetNode {
let idref = WidgetRef::default();
let mut app_data = ctx
.view_models
.view_model_mut(DATA)
.unwrap()
.write::()
.unwrap();
// We use content box marked with portals container as root to provide space
// for option views to anchor thier content into.
make_widget!(content_box)
.idref(idref.clone())
.with_shared_props(PortalsContainer(idref))
.listed_slot(
// Options view is basically a button that toggles its content anchored
// to itself. You can think of dropdown/context menus, but actually it
// can present any user widgets, not only in a list - content widget can
// be anything that takes listed slots and layouts them in some fashion.
make_widget!(options_view)
.with_props(ContentBoxItemLayout {
anchors: 0.25.into(),
margin: Rect {
left: -150.0,
right: -150.0,
top: -30.0,
bottom: -30.0,
},
..Default::default()
})
// Here we provide options view index source, which tells which option
// has to be shown.
.with_props(OptionsViewProps {
input: Some(app_data.index.lazy().into()),
})
.with_props(NavItemActive)
// Here we tell how to anchor content relatively to options box button.
.with_props(PivotBoxProps {
pivot: [0.0, 1.0].into(),
align: 0.0.into(),
})
// Additionally we might want to provide size of the content.
.with_props(SizeBoxProps {
width: SizeBoxSizeValue::Exact(300.0),
height: SizeBoxSizeValue::Exact(400.0),
..Default::default()
})
// Here we provide content widget. Preferably without existing children,
// because options will be appended, not replacing old children.
// Lists are obvious choice but you could also put slots into a grid,
// or even freeform content box to for example make a map with city
// icons to select!
.named_slot(
"content",
// Since this list will be injected into portal container, which is
// content box, we can make that list kept in bounds of the container.
make_widget!(vertical_box).with_props(ContentBoxItemLayout {
keep_in_bounds: true.into(),
..Default::default()
}),
)
// And last but not least, we provide items as listed slots.
// Each provided widget will be wrapped in button that will notify
// options view about selected option.
.listed_slot(
make_widget!(option)
.with_props("Hello".to_owned())
.with_props(NavItemActive),
)
.listed_slot(
make_widget!(option)
.with_props("World".to_owned())
.with_props(NavItemActive),
)
.listed_slot(
make_widget!(option)
.with_props("this".to_owned())
.with_props(NavItemActive),
)
.listed_slot(
make_widget!(option)
.with_props("is".to_owned())
.with_props(NavItemActive),
)
.listed_slot(
make_widget!(option)
.with_props("dropdown".to_owned())
.with_props(NavItemActive),
),
)
.into()
}
fn option(ctx: WidgetContext) -> WidgetNode {
// Since options are wrapped in buttons, we can read their button state and use it.
let ButtonProps {
selected, trigger, ..
} = ctx.props.read_cloned_or_default();
let color = if trigger {
Color {
r: 1.0,
g: 0.0,
b: 0.0,
a: 1.0,
}
} else if selected {
Color {
r: 0.0,
g: 0.0,
b: 1.0,
a: 1.0,
}
} else {
Color {
r: 0.0,
g: 0.0,
b: 0.0,
a: 1.0,
}
};
let text = ctx.props.read_cloned_or_default::();
// We can also read options view mode property to render our option widget
// diferently, depending if option is shown as selected or as content item.
let text = match ctx.props.read_cloned_or_default::() {
OptionsViewMode::Selected => format!("> {text}"),
OptionsViewMode::Option => format!("# {text}"),
};
make_widget!(content_box)
.listed_slot(make_widget!(image_box).with_props(ImageBoxProps::colored(color)))
.listed_slot(make_widget!(text_box).with_props(TextBoxProps {
text,
font: TextBoxFont {
name: "./demos/hello-world/resources/verdana.ttf".to_owned(),
size: 32.0,
},
horizontal_align: TextBoxHorizontalAlign::Center,
vertical_align: TextBoxVerticalAlign::Middle,
color: Color {
r: 1.0,
g: 1.0,
b: 1.0,
a: 1.0,
},
..Default::default()
}))
.into()
}
fn main() {
let app = DeclarativeApp::default()
.tree(make_widget!(app))
.view_model(
DATA,
ViewModel::produce(|properties| AppData {
index: Managed::new(ViewModelValue::new(
Default::default(),
properties.notifier(INDEX),
)),
}),
);
App::new(AppConfig::default().title("Options View")).run(app);
}
================================================
FILE: crates/_/examples/options_view_map.rs
================================================
// Make sure you have seen `options_view` code example first, because this is an evolution of that.
use raui_app::app::{App, AppConfig, declarative::DeclarativeApp};
use raui_core::{
Managed, Scalar, make_widget, pre_hooks,
view_model::{ViewModel, ViewModelValue},
widget::{
WidgetRef,
component::{
containers::{
anchor_box::PivotBoxProps, content_box::content_box, portal_box::PortalsContainer,
size_box::SizeBoxProps,
},
image_box::{ImageBoxProps, image_box},
interactive::{
button::ButtonProps,
navigation::{NavItemActive, use_nav_container_active},
options_view::{OptionsViewMode, OptionsViewProps, options_view},
},
text_box::{TextBoxProps, text_box},
},
context::WidgetContext,
node::WidgetNode,
unit::{
content::ContentBoxItemLayout,
size::SizeBoxSizeValue,
text::{TextBoxFont, TextBoxHorizontalAlign, TextBoxVerticalAlign},
},
utils::{Color, Rect},
},
};
const DATA: &str = "data";
const INDEX: &str = "index";
struct AppData {
index: Managed>,
}
fn use_app(ctx: &mut WidgetContext) {
ctx.life_cycle.mount(|mut ctx| {
ctx.view_models
.bindings(DATA, INDEX)
.unwrap()
.bind(ctx.id.to_owned());
});
}
#[pre_hooks(use_nav_container_active, use_app)]
fn app(mut ctx: WidgetContext) -> WidgetNode {
let idref = WidgetRef::default();
let mut app_data = ctx
.view_models
.view_model_mut(DATA)
.unwrap()
.write::()
.unwrap();
make_widget!(content_box)
.idref(idref.clone())
.with_shared_props(PortalsContainer(idref))
.listed_slot(
make_widget!(options_view)
.with_props(ContentBoxItemLayout {
anchors: 0.1.into(),
margin: [-200.0, -40.0].into(),
..Default::default()
})
.with_props(OptionsViewProps {
input: Some(app_data.index.lazy().into()),
})
.with_props(NavItemActive)
.with_props(PivotBoxProps {
pivot: [0.0, 1.0].into(),
align: 0.0.into(),
})
.with_props(SizeBoxProps {
width: SizeBoxSizeValue::Exact(500.0),
height: SizeBoxSizeValue::Exact(500.0),
..Default::default()
})
.named_slot(
"content",
make_widget!(content_box)
.with_props(ContentBoxItemLayout {
keep_in_bounds: true.into(),
..Default::default()
})
.listed_slot(make_widget!(image_box).with_props(ImageBoxProps::image(
"./crates/_/examples/resources/map.png",
))),
)
.listed_slot(
make_widget!(option)
.with_props("Vidence".to_owned())
.with_props(NavItemActive)
.with_props(marker_content_layout(0.1, 0.3)),
)
.listed_slot(
make_widget!(option)
.with_props("Yrale".to_owned())
.with_props(NavItemActive)
.with_props(marker_content_layout(0.6, 0.2)),
)
.listed_slot(
make_widget!(option)
.with_props("Qock".to_owned())
.with_props(NavItemActive)
.with_props(marker_content_layout(0.9, 0.6)),
)
.listed_slot(
make_widget!(option)
.with_props("Eryphia".to_owned())
.with_props(NavItemActive)
.with_props(marker_content_layout(0.3, 0.7)),
),
)
.into()
}
fn marker_content_layout(x: Scalar, y: Scalar) -> ContentBoxItemLayout {
ContentBoxItemLayout {
anchors: Rect {
left: x,
right: x,
top: y,
bottom: y,
},
margin: Rect {
left: -50.0,
right: -50.0,
top: -10.0,
bottom: -10.0,
},
align: 0.5.into(),
..Default::default()
}
}
fn option(ctx: WidgetContext) -> WidgetNode {
match ctx.props.read_cloned_or_default::() {
OptionsViewMode::Selected => option_selected(ctx),
OptionsViewMode::Option => option_marker(ctx),
}
}
fn option_selected(ctx: WidgetContext) -> WidgetNode {
let ButtonProps {
selected, trigger, ..
} = ctx.props.read_cloned_or_default();
let color = if trigger {
Color {
r: 1.0,
g: 0.0,
b: 0.0,
a: 1.0,
}
} else if selected {
Color {
r: 0.0,
g: 0.0,
b: 1.0,
a: 1.0,
}
} else {
Color {
r: 0.0,
g: 0.0,
b: 0.0,
a: 1.0,
}
};
let text = ctx.props.read_cloned_or_default::();
make_widget!(content_box)
.listed_slot(make_widget!(image_box).with_props(ImageBoxProps::colored(color)))
.listed_slot(make_widget!(text_box).with_props(TextBoxProps {
text,
font: TextBoxFont {
name: "./demos/hello-world/resources/verdana.ttf".to_owned(),
size: 32.0,
},
horizontal_align: TextBoxHorizontalAlign::Center,
vertical_align: TextBoxVerticalAlign::Middle,
color: Color {
r: 1.0,
g: 1.0,
b: 1.0,
a: 1.0,
},
..Default::default()
}))
.into()
}
fn option_marker(ctx: WidgetContext) -> WidgetNode {
let ButtonProps {
selected, trigger, ..
} = ctx.props.read_cloned_or_default();
let color = if trigger {
Color {
r: 1.0,
g: 1.0,
b: 1.0,
a: 1.0,
}
} else if selected {
Color {
r: 0.5,
g: 0.5,
b: 0.5,
a: 1.0,
}
} else {
Color {
r: 0.0,
g: 0.0,
b: 0.0,
a: 1.0,
}
};
let text = ctx.props.read_cloned_or_default::();
make_widget!(text_box)
.with_props(TextBoxProps {
text,
font: TextBoxFont {
name: "./demos/hello-world/resources/verdana.ttf".to_owned(),
size: 20.0,
},
horizontal_align: TextBoxHorizontalAlign::Center,
vertical_align: TextBoxVerticalAlign::Middle,
color,
..Default::default()
})
.into()
}
fn main() {
let app = DeclarativeApp::default()
.tree(make_widget!(app))
.view_model(
DATA,
ViewModel::produce(|properties| AppData {
index: Managed::new(ViewModelValue::new(
Default::default(),
properties.notifier(INDEX),
)),
}),
);
App::new(AppConfig::default().title("Options View")).run(app);
}
================================================
FILE: crates/_/examples/portal_box.rs
================================================
// Make sure you have seen `anchor_box` code example first, because this is an evolution of that.
use raui_app::app::declarative::DeclarativeApp;
use raui_core::{
make_widget, pre_hooks,
widget::{
WidgetRef,
component::{
RelativeLayoutProps,
containers::{
anchor_box::{
AnchorNotifyProps, AnchorProps, PivotBoxProps, anchor_box, pivot_box,
use_anchor_box_notified_state,
},
content_box::content_box,
portal_box::{PortalsContainer, portal_box},
},
image_box::{ImageBoxProps, image_box},
},
context::WidgetContext,
node::WidgetNode,
unit::{
content::ContentBoxItemLayout,
image::{ImageBoxColor, ImageBoxMaterial, ImageBoxSizeValue},
},
utils::Color,
},
};
// we use this hook that receives anchor box state change and store that in this component state.
#[pre_hooks(use_anchor_box_notified_state)]
fn app(mut ctx: WidgetContext) -> WidgetNode {
let idref = WidgetRef::default();
make_widget!(content_box)
.idref(idref.clone())
// widget rederence marked as portals container and put into root shared props for any
// portal box down the widget tree. More about how portal box works later.
.with_shared_props(PortalsContainer(idref.to_owned()))
.listed_slot(
make_widget!(anchor_box)
.with_props(RelativeLayoutProps {
relative_to: idref.to_owned().into(),
})
// we make this anchor box notify this component about anchor box state change.
.with_props(AnchorNotifyProps(ctx.id.to_owned().into()))
.with_props(ContentBoxItemLayout {
margin: 100.0.into(),
..Default::default()
})
.named_slot(
"content",
make_widget!(image_box).with_props(ImageBoxProps::colored(Color {
r: 0.25,
g: 0.25,
b: 0.25,
a: 1.0,
})),
),
)
.listed_slot(
// pivot box is used to calculate ContentBoxItemLayout that is later passed to its
// content so it works best with things like portal box which then uses that layout to
// position its content in portals container - in other words pivot box and portal box
// works best together.
make_widget!(pivot_box)
// pivot box uses AnchorProps for PivotBoxProps data to calculate a place to
// position the content relative to that area.
.with_props(ctx.state.read_cloned_or_default::())
.with_props(PivotBoxProps {
// percentage of the anchored area to position at.
pivot: 0.0.into(),
// percentage of content area to align relative to pivot position.
align: 0.75.into(),
})
.named_slot(
"content",
// portal box reads PortalsContainer from shared props and use its widget
// reference to "teleport" portal box content into referenced container widget
// (best container to use is content box) - what actually happen, RAUI sees
// portal box, unwraps it, find referenced container and injects that unwrapped
// content widget there.
make_widget!(portal_box).named_slot(
"content",
make_widget!(image_box).with_props(ImageBoxProps {
material: ImageBoxMaterial::Color(ImageBoxColor {
color: Color {
r: 1.0,
g: 0.25,
b: 0.25,
a: 1.0,
},
..Default::default()
}),
width: ImageBoxSizeValue::Exact(100.0),
height: ImageBoxSizeValue::Exact(100.0),
..Default::default()
}),
),
),
)
.listed_slot(
make_widget!(pivot_box)
.with_props(ctx.state.read_cloned_or_default::())
.with_props(PivotBoxProps {
pivot: 0.5.into(),
align: 0.5.into(),
})
.named_slot(
"content",
make_widget!(portal_box).named_slot(
"content",
make_widget!(image_box).with_props(ImageBoxProps {
material: ImageBoxMaterial::Color(ImageBoxColor {
color: Color {
r: 0.25,
g: 1.0,
b: 0.25,
a: 1.0,
},
..Default::default()
}),
width: ImageBoxSizeValue::Exact(200.0),
height: ImageBoxSizeValue::Exact(200.0),
..Default::default()
}),
),
),
)
.listed_slot(
make_widget!(pivot_box)
.with_props(ctx.state.read_cloned_or_default::())
.with_props(PivotBoxProps {
pivot: 1.0.into(),
align: 0.25.into(),
})
.named_slot(
"content",
make_widget!(portal_box).named_slot(
"content",
make_widget!(image_box).with_props(ImageBoxProps {
material: ImageBoxMaterial::Color(ImageBoxColor {
color: Color {
r: 0.25,
g: 0.25,
b: 1.0,
a: 1.0,
},
..Default::default()
}),
width: ImageBoxSizeValue::Exact(100.0),
height: ImageBoxSizeValue::Exact(100.0),
..Default::default()
}),
),
),
)
.into()
}
fn main() {
DeclarativeApp::simple("Portal Box", make_widget!(app));
}
================================================
FILE: crates/_/examples/render_workers.rs
================================================
// This example shows how to render arbitrary geometry "raw" way into a texture
// that can be used as image in the UI - useful for more demanding rendering.
use raui_app::{
Vertex,
app::declarative::DeclarativeApp,
render_worker::{RenderWorkerDescriptor, RenderWorkerTaskContext, RenderWorkersViewModel},
third_party::spitfire_glow::{
graphics::GraphicsBatch,
renderer::{GlowBlending, GlowTextureFormat, GlowUniformValue},
},
};
use raui_core::{
make_widget, pre_hooks,
widget::{
component::image_box::{ImageBoxProps, image_box},
context::WidgetContext,
node::WidgetNode,
},
};
fn use_app(ctx: &mut WidgetContext) {
ctx.life_cycle.mount(|mut ctx| {
// RenderWorkersViewModel is a special view model that stores render worker
// surfaces that we can schedule render tasks to.
let mut workers = ctx
.view_models
.view_model_mut(RenderWorkersViewModel::VIEW_MODEL)
.unwrap()
.write::()
.unwrap();
// First we add worker with the same id as the widget id, to ensure its
// uniqueness - we can use whatever id we want, but if workers are
// intended to be personalized to widgets, it's good to use widget id.
workers.add_worker(RenderWorkerDescriptor {
id: ctx.id.to_string(),
width: 256,
height: 256,
format: GlowTextureFormat::Rgba,
color: [1.0, 1.0, 1.0, 0.0],
});
// Once we added worker, we schedule to render its content first time.
workers.schedule_task(ctx.id.as_ref(), true, render_task);
});
ctx.life_cycle.unmount(|mut ctx| {
let mut workers = ctx
.view_models
.view_model_mut(RenderWorkersViewModel::VIEW_MODEL)
.unwrap()
.write::()
.unwrap();
// When widget is unmounted, we need to remove the worker,
// otherwise it will be left in the view model for ever.
workers.remove_worker(ctx.id.as_ref());
});
ctx.life_cycle.change(|mut ctx| {
let mut workers = ctx
.view_models
.view_model_mut(RenderWorkersViewModel::VIEW_MODEL)
.unwrap()
.write::()
.unwrap();
// When widget is changed, we need to update the worker surface content.
workers.schedule_task(ctx.id.as_ref(), true, render_task);
});
}
#[pre_hooks(use_app)]
fn app(mut ctx: WidgetContext) -> WidgetNode {
// Show rendered worker surface as image with aspect ratio to not stretch it.
make_widget!(image_box)
.with_props(ImageBoxProps::image_aspect_ratio(ctx.id.as_ref(), false))
.into()
}
fn main() {
DeclarativeApp::simple("Render Workers", make_widget!(app));
}
// Function representing render task that will paint some surface content.
fn render_task(ctx: RenderWorkerTaskContext) {
ctx.graphics.state.stream.batch_optimized(GraphicsBatch {
shader: Some(ctx.colored_shader.clone()),
uniforms: [(
"u_projection_view".into(),
GlowUniformValue::M4(
ctx.graphics
.state
.main_camera
.world_matrix()
.into_col_array(),
),
)]
.into_iter()
.collect(),
textures: Default::default(),
blending: GlowBlending::Alpha,
scissor: None,
wireframe: false,
});
ctx.graphics.state.stream.quad([
Vertex {
position: [
ctx.graphics.state.main_camera.screen_size.x * 0.25,
ctx.graphics.state.main_camera.screen_size.y * 0.25,
],
uv: [0.0, 0.0, 0.0],
color: [1.0, 0.0, 0.0, 1.0],
},
Vertex {
position: [
ctx.graphics.state.main_camera.screen_size.x * 0.75,
ctx.graphics.state.main_camera.screen_size.y * 0.25,
],
uv: [0.0, 0.0, 0.0],
color: [0.0, 1.0, 0.0, 1.0],
},
Vertex {
position: [
ctx.graphics.state.main_camera.screen_size.x * 0.75,
ctx.graphics.state.main_camera.screen_size.y * 0.75,
],
uv: [0.0, 0.0, 0.0],
color: [0.0, 0.0, 1.0, 1.0],
},
Vertex {
position: [
ctx.graphics.state.main_camera.screen_size.x * 0.25,
ctx.graphics.state.main_camera.screen_size.y * 0.75,
],
uv: [0.0, 0.0, 0.0],
color: [1.0, 1.0, 0.0, 1.0],
},
]);
}
================================================
FILE: crates/_/examples/resources/long_text.txt
================================================
Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
================================================
FILE: crates/_/examples/responsive_box.rs
================================================
use raui_app::app::declarative::DeclarativeApp;
use raui_core::{
make_widget,
widget::{
component::{
containers::{
content_box::content_box,
responsive_box::{MediaQueryExpression, MediaQueryOrientation, responsive_box},
},
image_box::{ImageBoxProps, image_box},
text_box::{TextBoxProps, text_box},
},
unit::text::{TextBoxFont, TextBoxHorizontalAlign, TextBoxVerticalAlign},
utils::Color,
},
};
fn main() {
let tree = make_widget!(content_box)
.listed_slot(
// responsive box allows to select one of listed slot widgets to
// present, depending on which slot widget's media query expression
// passes. ordering of listed slots is important, the first one that
// passes will be used. media query expressions can be combined with
// logical operator expressions such as `and`, `or` and `not`.
// in case of default case, use `any` expression.
make_widget!(responsive_box)
.listed_slot(
make_widget!(image_box)
.key("landscape")
.with_props(MediaQueryExpression::ScreenOrientation(
MediaQueryOrientation::Landscape,
))
.with_props(ImageBoxProps::colored(Color {
r: 0.25,
g: 1.0,
b: 0.25,
a: 1.0,
})),
)
.listed_slot(make_widget!(image_box).key("portrait").with_props(
ImageBoxProps::colored(Color {
r: 0.25,
g: 0.25,
b: 1.0,
a: 1.0,
}),
)),
)
.listed_slot(make_widget!(text_box).with_props(TextBoxProps {
text: "Change window size to observe responsiveness".to_owned(),
font: TextBoxFont {
name: "./demos/hello-world/resources/verdana.ttf".to_owned(),
size: 64.0,
},
color: Color {
r: 0.25,
g: 0.0,
b: 0.0,
a: 1.0,
},
horizontal_align: TextBoxHorizontalAlign::Center,
vertical_align: TextBoxVerticalAlign::Middle,
..Default::default()
}));
DeclarativeApp::simple("Responsive Box", tree);
}
================================================
FILE: crates/_/examples/responsive_props_box.rs
================================================
// Make sure you have seen `responsive_box` code example first, because this is an evolution of that.
use raui_app::app::declarative::DeclarativeApp;
use raui_core::{
make_widget,
widget::{
component::{
containers::{
content_box::content_box,
responsive_box::{
MediaQueryExpression, MediaQueryOrientation, responsive_props_box,
},
},
image_box::{ImageBoxProps, image_box},
text_box::{TextBoxProps, text_box},
},
context::WidgetContext,
node::WidgetNode,
none_widget,
unit::text::{TextBoxFont, TextBoxHorizontalAlign, TextBoxVerticalAlign},
utils::Color,
},
};
fn widget(context: WidgetContext) -> WidgetNode {
let WidgetContext { key, props, .. } = context;
let landscape = props.read_cloned_or_default::();
let color = if landscape {
Color {
r: 0.25,
g: 1.0,
b: 0.25,
a: 1.0,
}
} else {
Color {
r: 0.25,
g: 0.25,
b: 1.0,
a: 1.0,
}
};
let text = if landscape {
"Landscape".to_owned()
} else {
"Portrait".to_owned()
};
make_widget!(content_box)
.key(key)
.listed_slot(
make_widget!(image_box)
.key("image")
.with_props(ImageBoxProps::colored(color)),
)
.listed_slot(make_widget!(text_box).with_props(TextBoxProps {
text,
font: TextBoxFont {
name: "./demos/hello-world/resources/verdana.ttf".to_owned(),
size: 64.0,
},
color: Color {
r: 0.25,
g: 0.0,
b: 0.0,
a: 1.0,
},
horizontal_align: TextBoxHorizontalAlign::Center,
vertical_align: TextBoxVerticalAlign::Middle,
..Default::default()
}))
.into()
}
fn main() {
// responsive props box allows to select listed slot with media query, but
// instead of selecting that slot as content, it only grabs its props and
// applies them to named `content` slot - this is quite useful if we have
// single kind of widget we wanna present, but its props are what's different.
let tree = make_widget!(responsive_props_box)
.listed_slot(
// since because slot widget is not used, we need use `none_widget`
// to not pollute UI with complex widgets that won't be ever used.
make_widget!(none_widget)
.with_props(MediaQueryExpression::ScreenOrientation(
MediaQueryOrientation::Portrait,
))
.with_props(false),
)
.listed_slot(make_widget!(none_widget).with_props(true))
.named_slot("content", make_widget!(widget));
DeclarativeApp::simple("Responsive Props Box", tree);
}
================================================
FILE: crates/_/examples/retained_mode.rs
================================================
// Example of retained mode UI on top of RAUI.
// It's goals are very similar to Unreal's UMG on top of Slate.
// Evolution of this approach allows to use retained mode views
// within declarative mode widgets and vice versa - they
// interleave quite seamingly.
use std::any::Any;
use raui_app::app::retained::RetainedApp;
use raui_core::{
application::ChangeNotifier,
make_widget,
widget::{
component::{
containers::{
content_box::content_box,
horizontal_box::{HorizontalBoxProps, horizontal_box},
vertical_box::{VerticalBoxProps, vertical_box},
},
image_box::{ImageBoxProps, image_box},
interactive::{
button::{ButtonNotifyMessage, ButtonNotifyProps, button},
navigation::{NavItemActive, use_nav_container_active},
},
text_box::{TextBoxProps, text_box},
},
context::{WidgetContext, WidgetMountOrChangeContext},
node::WidgetNode,
unit::{flex::FlexBoxItemLayout, text::TextBoxFont},
utils::Color,
},
};
use raui_retained::{View, ViewState, ViewValue};
const FONT: &str = "./demos/hello-world/resources/verdana.ttf";
// root view of an application.
struct AppView {
pub counter: View,
pub increment_button: View