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 [![Crates.io](https://img.shields.io/crates/v/raui.svg)](https://crates.io/crates/raui)[![Docs.rs](https://docs.rs/raui/badge.svg)](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 + Spitfire In-Game](https://github.com/RAUI-labs/raui/blob/master/media/raui-in-game-material-ui.gif?raw=true) - [`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. ![RAUI Todo App](https://github.com/RAUI-labs/raui/blob/master/media/raui-todo-app-material-ui.gif?raw=true) ## 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>, pub decrement_button: View>, } impl ViewState for AppView { // `on_render` method constructs declarative nodes out of // retained node. this is similar to how Unreal's UMG builds // Slate widgets tree. you can do here whatever you would do // normally in RAUI widget component functions. fn on_render(&self, mut context: WidgetContext) -> WidgetNode { // as usual, at least root view should produce navigable // container to enable navigation on the UI, here navigation // being button clicks. context.use_hook(use_nav_container_active); make_widget!(vertical_box) .with_props(VerticalBoxProps { override_slots_layout: Some(FlexBoxItemLayout { basis: Some(48.0), grow: 0.0, shrink: 0.0, ..Default::default() }), ..Default::default() }) .listed_slot(self.counter.component().key("counter")) .listed_slot( make_widget!(horizontal_box) .with_props(HorizontalBoxProps { separation: 50.0, ..Default::default() }) .listed_slot(self.increment_button.component().key("increment")) .listed_slot(self.decrement_button.component().key("decrement")), ) .into() } fn as_any(&self) -> &dyn Any { self } fn as_any_mut(&mut self) -> &mut dyn Any { self } } // counter view stores value that can notify RAUI about this // widget being changed, so it will get re-renderred. // if we don't wrap data that has to be observed in `ViewValue`, // then we would need to find other way to notify RAUI app // about the change in data whenever it happen, usually manually. // alternatively we could use View-Model feature as we would // normally do with RAUI, if we don't want to store host data in // views (which is always good approach to take). struct CounterView { pub counter: ViewValue, } impl ViewState for CounterView { fn on_render(&self, _: WidgetContext) -> WidgetNode { make_widget!(text_box) // to allow `ViewValue` notify RAUI app about changes, // we need to pass its widget ref to RAUI component. // `ViewValue` pass that widget id to change notifications. .idref(self.counter.widget_ref()) .with_props(TextBoxProps { text: self.counter.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() }) .into() } fn as_any(&self) -> &dyn Any { self } fn as_any_mut(&mut self) -> &mut dyn Any { self } } struct LabelView { pub text: String, } impl LabelView { fn new(text: impl ToString) -> Self { Self { text: text.to_string(), } } } impl ViewState for LabelView { fn on_render(&self, _: WidgetContext) -> WidgetNode { make_widget!(text_box) .with_props(TextBoxProps { text: self.text.to_owned(), 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() }) .into() } fn as_any(&self) -> &dyn Any { self } fn as_any_mut(&mut self) -> &mut dyn Any { self } } // button that can store `on click` callback that // gets called whenever RAUI button detects click. struct Button { pub content: View, on_click: Option>, } impl Button { fn new(content: View) -> Self { Self { content, on_click: None, } } fn on_click(mut self, on_click: impl Fn() + Send + Sync + 'static) -> Self { self.on_click = Some(Box::new(on_click)); self } } impl ViewState for Button { fn on_change(&mut self, context: WidgetMountOrChangeContext) { // as usual, we listen for button messages sent to this // widget and call stored callback. if let Some(on_click) = self.on_click.take() { for message in context.messenger.messages { if let Some(message) = message.as_any().downcast_ref::() && message.trigger_start() { on_click(); } } self.on_click = Some(on_click); } } fn on_render(&self, context: WidgetContext) -> WidgetNode { make_widget!(button) .with_props(NavItemActive) // this enables RAUI interaction system to send button // events to same widget .with_props(ButtonNotifyProps(context.id.to_owned().into())) .named_slot( "content", make_widget!(content_box) .listed_slot(make_widget!(image_box).with_props(ImageBoxProps::colored( Color { r: 0.75, g: 0.75, b: 0.75, a: 1.0, }, ))) .listed_slot(self.content.component()), ) .into() } fn as_any(&self) -> &dyn Any { self } fn as_any_mut(&mut self) -> &mut dyn Any { self } } // here we construct application view tree out of view objects. fn create_app(notifier: ChangeNotifier) -> View { // create counter view and get lazy handles to it for buttons to use. // nice thing about lazy views is that they can be shared across // entire application - think of them just as handles/references to // any view you create, that can be accessed from whatever place. let counter = View::new(CounterView { counter: ViewValue::new(0).with_notifier(notifier), }); let lazy_counter_increment = counter.lazy(); let lazy_counter_decrement = counter.lazy(); let increment_button = View::new(Button::new(View::new(LabelView::new("Add"))).on_click( move || { // we can access other views using lazy views. *lazy_counter_increment.write().unwrap().counter += 1; }, )); let decrement_button = View::new(Button::new(View::new(LabelView::new("Subtract"))).on_click( move || { let mut access = lazy_counter_decrement.write().unwrap(); *access.counter = access.counter.saturating_sub(1); }, )); View::new(AppView { counter, increment_button, decrement_button, }) } fn main() { RetainedApp::simple("Retained mode UI", create_app); } ================================================ FILE: crates/_/examples/scroll_box.rs ================================================ use raui_app::app::declarative::DeclarativeApp; use raui_core::{ make_widget, pre_hooks, widget::{ component::{ containers::{ scroll_box::{SideScrollbarsProps, nav_scroll_box, nav_scroll_box_side_scrollbars}, size_box::{SizeBoxProps, size_box}, vertical_box::{VerticalBoxProps, vertical_box}, wrap_box::{WrapBoxProps, wrap_box}, }, image_box::{ImageBoxProps, image_box}, interactive::{ button::{ButtonNotifyMessage, ButtonNotifyProps, button}, navigation::{NavItemActive, use_nav_container_active}, scroll_view::ScrollViewRange, }, }, context::WidgetContext, node::WidgetNode, unit::{ flex::FlexBoxItemLayout, image::{ImageBoxColor, ImageBoxMaterial}, size::SizeBoxSizeValue, }, utils::{Color, Rect}, }, }; // we make this root widget a navigable container to let scrol box perform scrolling. #[pre_hooks(use_nav_container_active)] fn app(mut ctx: WidgetContext) -> WidgetNode { make_widget!(wrap_box) .with_props(WrapBoxProps { margin: Rect { left: 50.0, right: 50.0, top: 75.0, bottom: 25.0, }, ..Default::default() }) .named_slot( "content", make_widget!(nav_scroll_box) // we activate scroll box navigation - it is disabled by default. .with_props(NavItemActive) // apply scroll view range to limit scrolling area (without it you could scroll infinitely). .with_props(ScrollViewRange::default()) .named_slot( "content", // typical use of scroll box is to wrap around some kind of list but we can actually // put there anything and scroll box will scroll that content. make_widget!(vertical_box) .with_props(VerticalBoxProps { override_slots_layout: Some(FlexBoxItemLayout { grow: 0.0, shrink: 0.0, ..Default::default() }), ..Default::default() }) .listed_slot(make_widget!(item).key(0).with_props(true)) .listed_slot(make_widget!(item).key(1).with_props(false)) .listed_slot(make_widget!(item).key(2).with_props(true)) .listed_slot(make_widget!(item).key(3).with_props(false)) .listed_slot(make_widget!(item).key(4).with_props(true)) .listed_slot(make_widget!(item).key(5).with_props(false)), ) .named_slot( "scrollbars", // scrollbars used here are side buttons that you can drag to scroll content on // separate axes, but you could make a custom scrollbars component that for example // uses single button that allows to scroll in both axes at once with dragging. make_widget!(nav_scroll_box_side_scrollbars).with_props(SideScrollbarsProps { size: 20.0, back_material: Some(ImageBoxMaterial::Color(ImageBoxColor { color: Color { r: 0.15, g: 0.15, b: 0.15, a: 1.0, }, ..Default::default() })), front_material: ImageBoxMaterial::Color(ImageBoxColor { color: Color { r: 0.85, g: 0.85, b: 0.85, a: 1.0, }, ..Default::default() }), }), ), ) .into() } fn use_item(ctx: &mut WidgetContext) { ctx.life_cycle.change(|ctx| { for msg in ctx.messenger.messages { if let Some(msg) = msg.as_any().downcast_ref::() && msg.trigger_start() { println!("Button clicked: {:?}", msg.sender.key()); } } }); } #[pre_hooks(use_item)] fn item(mut ctx: WidgetContext) -> WidgetNode { let color = if ctx.props.read_cloned_or_default::() { Color { r: 0.5, g: 0.5, b: 0.5, a: 1.0, } } else { Color { r: 0.25, g: 0.25, b: 0.25, a: 1.0, } }; make_widget!(button) .with_props(NavItemActive) .with_props(ButtonNotifyProps(ctx.id.to_owned().into())) .named_slot( "content", make_widget!(size_box) .with_props(SizeBoxProps { width: SizeBoxSizeValue::Fill, height: SizeBoxSizeValue::Exact(136.0), ..Default::default() }) .named_slot( "content", make_widget!(image_box).with_props(ImageBoxProps::colored(color)), ), ) .into() } fn main() { DeclarativeApp::simple("Scroll Box", make_widget!(app)); } ================================================ FILE: crates/_/examples/scroll_box_adaptive.rs ================================================ use raui_app::app::declarative::DeclarativeApp; use raui_core::{ make_widget, pre_hooks, widget::{ component::{ containers::{ scroll_box::{SideScrollbarsProps, nav_scroll_box, nav_scroll_box_side_scrollbars}, size_box::{SizeBoxProps, size_box}, vertical_box::{VerticalBoxProps, vertical_box}, wrap_box::{WrapBoxProps, wrap_box}, }, image_box::{ImageBoxProps, image_box}, interactive::{ navigation::{NavItemActive, use_nav_container_active}, scroll_view::ScrollViewRange, }, text_box::{TextBoxProps, text_box}, }, context::WidgetContext, node::WidgetNode, unit::{ flex::FlexBoxItemLayout, image::{ ImageBoxAspectRatio, ImageBoxColor, ImageBoxImage, ImageBoxMaterial, ImageBoxSizeValue, }, size::SizeBoxSizeValue, text::{TextBoxFont, TextBoxSizeValue}, }, utils::{Color, Rect}, }, }; #[pre_hooks(use_nav_container_active)] fn app(mut ctx: WidgetContext) -> WidgetNode { make_widget!(wrap_box) .with_props(WrapBoxProps { margin: Rect { left: 100.0, right: 50.0, top: 75.0, bottom: 25.0, }, ..Default::default() }) .named_slot( "content", make_widget!(nav_scroll_box) .with_props(NavItemActive) .with_props(ScrollViewRange::default()) .named_slot( "content", make_widget!(size_box) .with_props(SizeBoxProps { width: SizeBoxSizeValue::Fill, // first we make sure to put content in size box that // uses content height to make it take minimal space. height: SizeBoxSizeValue::Content, ..Default::default() }) .named_slot( "content", make_widget!(vertical_box) .with_props(VerticalBoxProps { // we need to make sure all items are not // growing and shrinking to let size box // calculate correct content size. growing // and shrinking would make items take all // available space, filling all container. override_slots_layout: Some( FlexBoxItemLayout::no_growing_and_shrinking(), ), ..Default::default() }) .listed_slot(make_widget!(image_box).with_props(ImageBoxProps { height: ImageBoxSizeValue::Exact(300.0), material: ImageBoxMaterial::Image(ImageBoxImage { id: "./crates/_/examples/resources/map.png".to_owned(), ..Default::default() }), content_keep_aspect_ratio: Some(ImageBoxAspectRatio { horizontal_alignment: 0.5, vertical_alignment: 0.5, outside: false, }), ..Default::default() })) .listed_slot(make_widget!(text_box).with_props(TextBoxProps { text: include_str!("./resources/long_text.txt").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, }, height: TextBoxSizeValue::Content, ..Default::default() })), ), ) .named_slot( "scrollbars", make_widget!(nav_scroll_box_side_scrollbars).with_props(SideScrollbarsProps { size: 20.0, back_material: Some(ImageBoxMaterial::Color(ImageBoxColor { color: Color { r: 0.15, g: 0.15, b: 0.15, a: 1.0, }, ..Default::default() })), front_material: ImageBoxMaterial::Color(ImageBoxColor { color: Color { r: 0.85, g: 0.85, b: 0.85, a: 1.0, }, ..Default::default() }), }), ), ) .into() } fn main() { DeclarativeApp::simple("Scroll Box - Adaptive content size", make_widget!(app)); } ================================================ FILE: crates/_/examples/setup.rs ================================================ use raui_core::{ application::Application, interactive::default_interactions_engine::{ DefaultInteractionsEngine, Interaction, PointerButton, }, layout::{CoordsMapping, default_layout_engine::DefaultLayoutEngine}, make_widget, widget::{ component::{ containers::content_box::nav_content_box, image_box::image_box, interactive::{button::button, navigation::NavItemActive}, }, setup, utils::{Rect, Vec2}, }, }; use raui_json_renderer::JsonRenderer; fn main() { // Create the application let mut application = Application::default(); // We need to run the "setup" functions for the application to register components and // properties if we want to support serialization of the UI. We pass it a function that // will do the actual registration application.setup(setup); // application.setup(raui_material::setup /* and the raui_material setup if we need it */); // Create the renderer. In this case we render the UI to JSON for simplicity, but usually // you would have a custom renderer for your game engine or renderer. let mut renderer = JsonRenderer { pretty: true }; // Create the interactions engine. The default interactions engine covers typical // pointer + keyboard + gamepad navigation/interactions. let mut interactions = DefaultInteractionsEngine::default(); // We create our widget tree let tree = make_widget!(nav_content_box).key("app").listed_slot( make_widget!(button) .key("button") .with_props(NavItemActive) .named_slot("content", make_widget!(image_box).key("icon")), ); // We apply the tree to the application. This must be done again if we wish to change the // tree. application.apply(tree); // This scope content would need to be called every frame { // Telling the app to `process` will make it perform any necessary updates. application.process(); // To properly handle layout we need to create a mapping of the screen coordinates to // the RAUI coordinates. We would update this with the size of the window every frame. let mapping = CoordsMapping::new(Rect { left: 0.0, right: 1024.0, top: 0.0, bottom: 576.0, }); // we interact with UI by sending interaction messages to the engine. You would hook this up // to whatever game engine or window event loop to perform the proper interactions when // different events are emitted. interactions.interact(Interaction::PointerMove(Vec2 { x: 200.0, y: 100.0 })); interactions.interact(Interaction::PointerDown( PointerButton::Trigger, Vec2 { x: 200.0, y: 100.0 }, )); // We apply the application layout. // We use the default layout engine, but you could make your own layout engine. let mut layout_engine = DefaultLayoutEngine::<()>::default(); application.layout(&mapping, &mut layout_engine).unwrap(); // Since interactions engines require constructed layout to process interactions we // have to process interactions after we layout the UI. application.interact(&mut interactions).unwrap(); // Now we render the app println!( "{}", application .render::<_, String, _>(&mapping, &mut renderer) .unwrap() ); } } ================================================ FILE: crates/_/examples/size_box.rs ================================================ use raui_app::app::declarative::DeclarativeApp; use raui_core::{ make_widget, widget::{ component::{ containers::size_box::{SizeBoxProps, size_box}, image_box::{ImageBoxProps, image_box}, }, unit::size::SizeBoxSizeValue, utils::Color, }, }; fn main() { let tree = make_widget!(size_box) .with_props(SizeBoxProps { // takes the layout box size from its children size. content size is the default one. width: SizeBoxSizeValue::Content, height: SizeBoxSizeValue::Content, ..Default::default() }) .named_slot( "content", make_widget!(size_box) .with_props(SizeBoxProps { // exact size resets layout available size into size defined here. // it simply ignores available size and uses this one down the widget tree. width: SizeBoxSizeValue::Exact(400.0), height: SizeBoxSizeValue::Exact(300.0), ..Default::default() }) .named_slot( "content", make_widget!(size_box) .with_props(SizeBoxProps { // uses layout available size defined by this widget parent node. width: SizeBoxSizeValue::Fill, height: SizeBoxSizeValue::Fill, // we can additionally set margin. margin: 50.0.into(), ..Default::default() }) .named_slot( "content", make_widget!(image_box).with_props(ImageBoxProps::colored(Color { r: 1.0, g: 0.25, b: 0.25, a: 1.0, })), ), ), ); DeclarativeApp::simple("Size Box", tree); } ================================================ FILE: crates/_/examples/size_box_aspect_ratio.rs ================================================ // Make sure you have seen `size_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::size_box::{SizeBoxProps, size_box}, image_box::{ImageBoxProps, image_box}, }, unit::size::{SizeBoxAspectRatio, SizeBoxSizeValue}, utils::Color, }, }; fn main() { let tree = make_widget!(size_box) .with_props(SizeBoxProps { width: SizeBoxSizeValue::Fill, height: SizeBoxSizeValue::Fill, // enforce width to be percentage of height. keep_aspect_ratio: SizeBoxAspectRatio::WidthOfHeight(0.5), ..Default::default() }) .named_slot( "content", make_widget!(image_box).with_props(ImageBoxProps::colored(Color { r: 1.0, g: 0.25, b: 0.25, a: 1.0, })), ); DeclarativeApp::simple("Size Box - Keep Aspect Ratio", tree); } ================================================ FILE: crates/_/examples/slider_view.rs ================================================ use raui_app::app::{App, AppConfig, declarative::DeclarativeApp}; use raui_core::{ Managed, make_widget, pre_hooks, view_model::{ViewModel, ViewModelValue}, widget::{ component::{ containers::{ content_box::content_box, horizontal_box::horizontal_box, vertical_box::{VerticalBoxProps, vertical_box}, }, image_box::{ImageBoxProps, image_box}, interactive::{ navigation::{NavItemActive, use_nav_container_active}, slider_view::{SliderViewDirection, SliderViewProps, slider_view}, }, text_box::{TextBoxProps, text_box}, }, context::WidgetContext, node::WidgetNode, unit::{ content::ContentBoxItemLayout, flex::FlexBoxItemLayout, text::{TextBoxFont, TextBoxHorizontalAlign, TextBoxVerticalAlign}, }, utils::{Color, Rect}, }, }; const DATA: &str = "data"; const FLOAT_INPUT: &str = "float-input"; const INTEGER_INPUT: &str = "integer-input"; const UNSIGNED_INTEGER_INPUT: &str = "unsigned-integer-input"; struct AppData { float_input: Managed>, integer_input: Managed>, unsigned_integer_input: Managed>, } fn use_app(ctx: &mut WidgetContext) { ctx.life_cycle.mount(|mut ctx| { ctx.view_models .bindings(DATA, FLOAT_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()); }); } #[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(); make_widget!(horizontal_box) .listed_slot( make_widget!(input) .with_props(FlexBoxItemLayout { margin: 50.0.into(), ..Default::default() }) .with_props(SliderViewProps { input: Some(app_data.float_input.lazy().into()), from: -10.0, to: 10.0, direction: SliderViewDirection::BottomToTop, }), ) .listed_slot( make_widget!(vertical_box) .with_props(VerticalBoxProps { override_slots_layout: Some(FlexBoxItemLayout { margin: 50.0.into(), ..Default::default() }), ..Default::default() }) .listed_slot(make_widget!(input).with_props(SliderViewProps { input: Some(app_data.integer_input.lazy().into()), from: -2.0, to: 2.0, ..Default::default() })) .listed_slot(make_widget!(input).with_props(SliderViewProps { input: Some(app_data.unsigned_integer_input.lazy().into()), from: -3.0, to: 7.0, direction: SliderViewDirection::RightToLeft, })), ) .into() } fn input(ctx: WidgetContext) -> WidgetNode { let props = ctx.props.read_cloned_or_default::(); let percentage = props.get_percentage(); let value = props.get_value(); let anchors = match props.direction { SliderViewDirection::LeftToRight => Rect { left: 0.0, right: percentage, top: 0.0, bottom: 1.0, }, SliderViewDirection::RightToLeft => Rect { left: 1.0 - percentage, right: 1.0, top: 0.0, bottom: 1.0, }, SliderViewDirection::TopToBottom => Rect { left: 0.0, right: 1.0, top: 0.0, bottom: percentage, }, SliderViewDirection::BottomToTop => Rect { left: 0.0, right: 1.0, top: 1.0 - percentage, bottom: 1.0, }, }; make_widget!(slider_view) .with_props(NavItemActive) .with_props(props) .named_slot( "content", make_widget!(content_box) .listed_slot( make_widget!(image_box).with_props(ImageBoxProps::colored(Color { r: 0.0, g: 0.0, b: 1.0, a: 1.0, })), ) .listed_slot( make_widget!(image_box) .with_props(ContentBoxItemLayout { anchors, ..Default::default() }) .with_props(ImageBoxProps::colored(Color { r: 1.0, g: 0.0, b: 0.0, a: 1.0, })), ) .listed_slot(make_widget!(text_box).with_props(TextBoxProps { text: value.to_string(), horizontal_align: TextBoxHorizontalAlign::Center, vertical_align: TextBoxVerticalAlign::Middle, font: TextBoxFont { name: "./demos/hello-world/resources/verdana.ttf".to_owned(), size: 64.0, }, 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 { float_input: Managed::new(ViewModelValue::new( Default::default(), properties.notifier(FLOAT_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), )), }), ); App::new(AppConfig::default().title("Slider View")).run(app); } ================================================ FILE: crates/_/examples/space_box.rs ================================================ use raui_app::app::declarative::DeclarativeApp; use raui_core::{ make_widget, widget::{ component::{ containers::horizontal_box::horizontal_box, image_box::{ImageBoxProps, image_box}, space_box::{SpaceBoxProps, space_box}, }, unit::flex::FlexBoxItemLayout, utils::Color, }, }; fn main() { let tree = make_widget!(horizontal_box) .listed_slot( make_widget!(image_box).with_props(ImageBoxProps::colored(Color { r: 1.0, g: 0.25, b: 0.25, a: 1.0, })), ) .listed_slot( make_widget!(space_box) // cube spacing means we set same separation both horizontally and vertically. .with_props(SpaceBoxProps::cube(64.0)) // we set clear flex box layout to disallow space box fluidity. .with_props(FlexBoxItemLayout::cleared()), ) .listed_slot( make_widget!(image_box).with_props(ImageBoxProps::colored(Color { r: 0.25, g: 0.25, b: 1.0, a: 1.0, })), ); DeclarativeApp::simple("Space Box", tree); } ================================================ FILE: crates/_/examples/switch_box.rs ================================================ use raui_app::{ app::{App, AppConfig, declarative::DeclarativeApp}, event::{ElementState, Event, VirtualKeyCode, WindowEvent}, }; use raui_core::{ make_widget, pre_hooks, view_model::ViewModel, widget::{ component::{ containers::switch_box::{SwitchBoxProps, switch_box}, image_box::{ImageBoxProps, image_box}, }, context::WidgetContext, node::WidgetNode, 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 { // we read value from view model created with app builder. let active_index = ctx .view_models .view_model(DATA) .unwrap() .read::() .map(|value| *value % 3) .unwrap_or_default(); make_widget!(switch_box) .with_props(SwitchBoxProps { active_index: Some(active_index), ..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, })), ) .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, })), ) .into() } fn main() { let app = DeclarativeApp::default() .tree(make_widget!(app)) .view_model(DATA, ViewModel::new_object(0usize)) .event(move |application, event, _, _| { let mut data = application .view_models .get_mut(DATA) .unwrap() .write_notified::() .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 => { // we modify app data with value that represent active switch index. *data = 0; } VirtualKeyCode::Key2 | VirtualKeyCode::Numpad2 => { *data = 1; } VirtualKeyCode::Key3 | VirtualKeyCode::Numpad3 => { *data = 2; } _ => {} } } true }); App::new(AppConfig::default().title("Switch Box")).run(app); } ================================================ FILE: crates/_/examples/tabs_box.rs ================================================ use raui_app::app::declarative::DeclarativeApp; use raui_core::{ make_widget, pre_hooks, widget::{ component::{ containers::tabs_box::{ TabPlateProps, TabsBoxProps, TabsBoxTabsLocation, nav_tabs_box, }, image_box::{ImageBoxProps, image_box}, interactive::navigation::{NavItemActive, use_nav_container_active}, }, context::WidgetContext, node::WidgetNode, utils::Color, }, }; #[pre_hooks(use_nav_container_active)] fn app(mut ctx: WidgetContext) -> WidgetNode { make_widget!(nav_tabs_box) .with_props(NavItemActive) .with_props(TabsBoxProps { // top tabs location is default one but we can change tabs bar to be on either side of // the tabs box area. tabs_location: TabsBoxTabsLocation::Top, // we set tabs basis to let tabs itself fill into the area that tabs bar gives to layout. tabs_basis: Some(50.0), ..Default::default() }) // we pack pairs of tab plate and its content using tuples and then put them in listed slots. .listed_slot(WidgetNode::pack_tuple([ // first tiple item is always the tab plate that's gonna be put on tabs bar (it's gonna // be wrapped with button component so it's better to not put other buttons in tab plate // widget tree). make_widget!(tab_plate) .with_props(Color { r: 1.0, g: 0.25, b: 0.25, a: 1.0, }) .into(), // second tuple item is always the tab contents (all tabs contents are put into inner // switch box so we make sure there is always only one tab content present at a time). make_widget!(image_box) .with_props(ImageBoxProps::colored(Color { r: 0.75, g: 0.25, b: 0.25, a: 1.0, })) .into(), ])) .listed_slot(WidgetNode::pack_tuple([ make_widget!(tab_plate) .with_props(Color { r: 0.25, g: 1.0, b: 0.25, a: 1.0, }) .into(), make_widget!(image_box) .with_props(ImageBoxProps::colored(Color { r: 0.25, g: 0.75, b: 0.25, a: 1.0, })) .into(), ])) .listed_slot(WidgetNode::pack_tuple([ make_widget!(tab_plate) .with_props(Color { r: 0.25, g: 0.25, b: 1.0, a: 1.0, }) .into(), make_widget!(image_box) .with_props(ImageBoxProps::colored(Color { r: 0.25, g: 0.25, b: 0.75, a: 1.0, })) .into(), ])) .into() } fn tab_plate(ctx: WidgetContext) -> WidgetNode { let mut color = ctx.props.read_cloned_or_default::(); if !ctx.props.read_cloned_or_default::().active { color.r *= 0.5; color.g *= 0.5; color.b *= 0.5; } make_widget!(image_box) .with_props(ImageBoxProps::colored(color)) .into() } fn main() { DeclarativeApp::simple("Tabs Box", make_widget!(app)); } ================================================ FILE: crates/_/examples/text_box.rs ================================================ use raui_app::app::declarative::DeclarativeApp; use raui_core::{ make_widget, widget::{ component::text_box::{TextBoxProps, text_box}, unit::text::{TextBoxFont, TextBoxHorizontalAlign, TextBoxVerticalAlign}, utils::Color, }, }; fn main() { let tree = make_widget!(text_box).with_props(TextBoxProps { text: "RAUI\nText Box".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, vertical_align: TextBoxVerticalAlign::Middle, ..Default::default() }); DeclarativeApp::simple("Text Box", tree); } ================================================ FILE: crates/_/examples/text_box_content_size.rs ================================================ use raui_app::app::declarative::DeclarativeApp; use raui_core::{ make_widget, widget::{ component::{ containers::horizontal_box::horizontal_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!(horizontal_box) .listed_slot( make_widget!(text_box) .with_props(FlexBoxItemLayout { // Disable growing and shrinking of the text box to allow it to // take the size of its content in the list. grow: 0.0, shrink: 0.0, margin: 20.0.into(), ..Default::default() }) .with_props(TextBoxProps { text: "RAUI\nContent Size".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::Right, // Setting text size to its content allows for fitting other // widgets nicely around that text box. width: TextBoxSizeValue::Content, height: TextBoxSizeValue::Content, ..Default::default() }), ) .listed_slot( make_widget!(image_box).with_props(ImageBoxProps::colored(Color { r: 0.5, g: 0.0, b: 0.0, a: 1.0, })), ); DeclarativeApp::simple("Text Box - Content Size", tree); } ================================================ FILE: crates/_/examples/text_field_paper.rs ================================================ use raui_app::app::declarative::DeclarativeApp; use raui_core::{ ManagedGc, make_widget, pre_hooks, view_model::ViewModel, widget::{ component::{ containers::size_box::{SizeBoxProps, size_box}, interactive::{ button::ButtonNotifyProps, input_field::{TextInput, TextInputProps}, navigation::{NavItemActive, use_nav_container_active}, }, }, context::WidgetContext, node::WidgetNode, unit::{size::SizeBoxSizeValue, text::TextBoxFont}, utils::Rect, }, }; use raui_material::{ component::{ containers::paper::paper, interactive::text_field_paper::{TextFieldPaperProps, text_field_paper}, }, theme::{ThemeColor, ThemeProps, ThemedTextMaterial, ThemedWidgetProps, new_dark_theme}, }; const TEXT_INPUT: &str = "text-input"; // 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 use_app(ctx: &mut WidgetContext) { ctx.life_cycle.mount(|mut ctx| { // Initialize the view model for the text input field. let mut view_model = ViewModel::produce(|_| ManagedGc::new("Hello!".to_owned())); view_model .properties .bindings(TEXT_INPUT) .unwrap() .bind(ctx.id.to_owned()); ctx.view_models.widget_register(TEXT_INPUT, view_model); }); } #[pre_hooks(use_nav_container_active, use_app)] fn app(mut ctx: WidgetContext) -> WidgetNode { let WidgetContext { id, mut view_models, .. } = ctx; // Turn the view model into a lazy TextInput for the text field props. let text = view_models.widget_view_model_mut(TEXT_INPUT).and_then(|v| { v.read::>() .map(|v| TextInput::new(v.lazy())) }); make_widget!(paper) .with_shared_props(new_theme()) .listed_slot( make_widget!(size_box) .with_props(SizeBoxProps { width: SizeBoxSizeValue::Fill, height: SizeBoxSizeValue::Exact(50.0), margin: 20.0.into(), ..Default::default() }) .named_slot( "content", make_widget!(text_field_paper) .key("name") .with_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() }) // Make input text editable. .with_props(NavItemActive) // Notify this widget about changes made by input text. .with_props(ButtonNotifyProps(id.to_owned().into())) // Pass the lazy TextInput to the text field paper to edit. .with_props(TextInputProps { text, ..Default::default() }), ), ) .into() } fn main() { DeclarativeApp::simple("Text Field Paper", make_widget!(app)); } ================================================ FILE: crates/_/examples/tooltip_box.rs ================================================ // Make sure you have seen `context_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::{ containers::{ anchor_box::PivotBoxProps, content_box::content_box, horizontal_box::{HorizontalBoxProps, horizontal_box}, portal_box::PortalsContainer, tooltip_box::portals_tooltip_box, }, image_box::{ImageBoxProps, image_box}, interactive::{ button::button, navigation::{NavItemActive, use_nav_container_active}, }, }, context::WidgetContext, node::WidgetNode, unit::{ flex::FlexBoxItemLayout, image::{ImageBoxColor, ImageBoxMaterial, ImageBoxSizeValue}, }, utils::{Color, Vec2}, }, }; // we mark app as an active navigable container to let all buttons down the tree register to the // navigation system so they can react on mouse hovering for example. #[pre_hooks(use_nav_container_active)] fn app(mut ctx: WidgetContext) -> WidgetNode { let idref = WidgetRef::default(); make_widget!(content_box) .idref(idref.clone()) .with_shared_props(PortalsContainer(idref)) .listed_slot( make_widget!(horizontal_box) .with_props(HorizontalBoxProps { separation: 25.0, override_slots_layout: Some(FlexBoxItemLayout::cleared()), ..Default::default() }) .listed_slot(make_widget!(icon).with_props(Color { r: 1.0, g: 0.25, b: 0.25, a: 1.0, })) .listed_slot( make_widget!(icon) .with_props(Color { r: 0.25, g: 1.0, b: 0.25, a: 1.0, }) .with_props(PivotBoxProps { pivot: Vec2 { x: 0.5, y: 1.0 }, align: Vec2 { x: 0.5, y: 0.0 }, }), ) .listed_slot( make_widget!(icon) .with_props(Color { r: 0.25, g: 0.25, b: 1.0, a: 1.0, }) .with_props(PivotBoxProps { pivot: Vec2 { x: 1.0, y: 1.0 }, align: Vec2 { x: 1.0, y: 0.0 }, }), ), ) .into() } fn icon(ctx: WidgetContext) -> WidgetNode { // tooltip box is basically an evolution of context box - what changes is tooltip box is shown // only if this its content gets selected by navigation system (and since buttons can be // selected for example by mouse hover, this tooltip is shown whenever mouse gets over the // widget it wraps). make_widget!(portals_tooltip_box) .with_props(ctx.props.read_cloned_or_default::()) // put colored image box as content widget. .named_slot( "content", // we wrap content with button to allow automated widget selection that will show tooltip, make_widget!(button) // remember that buttons has to be activated to make them receive selection // navigation messages - they are inactive by default. .with_props(NavItemActive) .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 tooltip widget. .named_slot( "tooltip", 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() { DeclarativeApp::simple("Tooltip Box", make_widget!(app)); } ================================================ FILE: crates/_/examples/tracking.rs ================================================ use raui_app::app::declarative::DeclarativeApp; use raui_core::{ make_widget, pre_hooks, widget::{ component::{ containers::horizontal_box::horizontal_box, image_box::{ImageBoxProps, image_box}, interactive::navigation::{ NavTrackingNotifyMessage, NavTrackingNotifyProps, self_tracking, use_nav_container_active, }, }, context::WidgetContext, node::WidgetNode, unit::flex::FlexBoxItemLayout, utils::Color, }, }; fn use_app(ctx: &mut WidgetContext) { // whenever we receive tracking message, we store it's horizontal // component in state for rendering to use. ctx.life_cycle.change(|ctx| { for msg in ctx.messenger.messages { if let Some(msg) = msg.as_any().downcast_ref::() { let _ = ctx.state.write_with(msg.state.factor.x); } } }); } #[pre_hooks(use_nav_container_active, use_app)] fn app(mut ctx: WidgetContext) -> WidgetNode { // possibly read stored horizontal tracking value. let factor = ctx.state.read_cloned::().unwrap_or(0.5); // we use `self_tracking` wrapper widget to allow it to automatically // track pointer position relative to itself. make_widget!(self_tracking) // we tell widget to notify app widget about tracking changes. .with_props(NavTrackingNotifyProps(ctx.id.to_owned().into())) .named_slot( "content", // we make horizontal box items have weights proportional to // horizontal tracking value. make_widget!(horizontal_box) .listed_slot( make_widget!(image_box) .with_props(FlexBoxItemLayout { grow: factor, shrink: factor, ..Default::default() }) .with_props(ImageBoxProps::colored(Color { r: 1.0, g: 0.0, b: 0.0, a: 1.0, })), ) .listed_slot( make_widget!(image_box) .with_props(FlexBoxItemLayout { grow: 1.0 - factor, shrink: 1.0 - factor, ..Default::default() }) .with_props(ImageBoxProps::colored(Color { r: 0.0, g: 0.0, b: 1.0, a: 1.0, })), ), ) .into() } fn main() { DeclarativeApp::simple("Tracking", make_widget!(app)); } ================================================ FILE: crates/_/examples/variant_box.rs ================================================ use raui_app::{ app::{App, AppConfig, declarative::DeclarativeApp}, event::{ElementState, Event, VirtualKeyCode, WindowEvent}, }; use raui_core::{ make_widget, pre_hooks, view_model::ViewModel, widget::{ component::{ containers::variant_box::{VariantBoxProps, variant_box}, image_box::{ImageBoxProps, image_box}, }, context::WidgetContext, node::WidgetNode, 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 { // we read value from view model created with app builder. let variant_name = ctx .view_models .view_model(DATA) .unwrap() .read::() .map(|value| value.to_owned()); make_widget!(variant_box) .with_props(VariantBoxProps { variant_name }) .named_slot( "A", make_widget!(image_box).with_props(ImageBoxProps::colored(Color { r: 1.0, g: 0.25, b: 0.25, a: 1.0, })), ) .named_slot( "S", make_widget!(image_box).with_props(ImageBoxProps::colored(Color { r: 0.25, g: 1.0, b: 0.25, a: 1.0, })), ) .named_slot( "D", make_widget!(image_box).with_props(ImageBoxProps::colored(Color { r: 0.25, g: 0.25, b: 1.0, a: 1.0, })), ) .into() } fn main() { let app = DeclarativeApp::default() .tree(make_widget!(app)) .view_model(DATA, ViewModel::new_object("A".to_owned())) .event(move |application, event, _, _| { let mut data = application .view_models .get_mut(DATA) .unwrap() .write_notified::() .unwrap(); if let Event::WindowEvent { event: WindowEvent::KeyboardInput { input, .. }, .. } = event && input.state == ElementState::Pressed && let Some(key) = input.virtual_keycode { match key { VirtualKeyCode::A => { // we modify app data with value that represent active variant name. *data = "A".to_owned(); } VirtualKeyCode::S => { *data = "S".to_owned(); } VirtualKeyCode::D => { *data = "D".to_owned(); } _ => {} } }; true }); App::new(AppConfig::default().title("Variant Box")).run(app); } ================================================ FILE: crates/_/examples/vertical_box.rs ================================================ use raui_app::app::declarative::DeclarativeApp; use raui_core::{ make_widget, widget::{ component::{ containers::vertical_box::{VerticalBoxProps, vertical_box}, image_box::{ImageBoxProps, image_box}, }, unit::flex::FlexBoxItemLayout, utils::Color, }, }; fn main() { let tree = make_widget!(vertical_box) .with_props(VerticalBoxProps { 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 height of the item. basis: Some(100.0), // weight of the item when its layout box has to grow in height. grow: 0.5, // weight of the item when its layout box has to shrink in height (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("Vertical Box", tree); } ================================================ FILE: crates/_/examples/view_model.rs ================================================ // Make sure you have seen `text_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, ViewModelValue}, widget::{ component::text_box::{TextBoxProps, text_box}, context::WidgetContext, node::WidgetNode, unit::text::TextBoxFont, utils::Color, }, }; // Name of View-Model instance. const DATA: &str = "data"; // Name of View-Model property notification. const COUNTER: &str = "counter"; // View-Model data type. struct AppData { // View-Model value wrapper that automatically notifies View-Model // about change on mutation. counter: ViewModelValue, } // We use hook to bind widget to and unbind from View-Model instance. // This will make RAUI application automatically rebuild widgets tree // on change in View-Model data. // BTW. We could omit unbinding, since widgets unbind automatically // on unmount, but this is here to showcase how to do it manually. fn use_app(ctx: &mut WidgetContext) { ctx.life_cycle.mount(|mut ctx| { ctx.view_models .bindings(DATA, COUNTER) .unwrap() .bind(ctx.id.to_owned()); }); ctx.life_cycle.unmount(|mut ctx| { ctx.view_models .bindings(DATA, COUNTER) .unwrap() .unbind(ctx.id); }); } #[pre_hooks(use_app)] fn app(mut ctx: WidgetContext) -> WidgetNode { // We read app data from view model created with app builder. let app_data = ctx .view_models .view_model(DATA) .unwrap() .read::() .unwrap(); make_widget!(text_box) .with_props(TextBoxProps { // Use View-Model data to render widget on change. text: format!("Counter: {}", *app_data.counter), font: TextBoxFont { name: "./demos/hello-world/resources/verdana.ttf".to_owned(), size: 48.0, }, color: Color { r: 0.0, g: 0.0, b: 0.0, a: 1.0, }, ..Default::default() }) .into() } fn main() { // Create View-Model for `AppData`. let view_model = ViewModel::produce(|properties| AppData { // We use View-Model properties to create notifiers for properties. counter: ViewModelValue::new(0, properties.notifier(COUNTER)), }); // Get lazy shared reference to View-Model data for later use // on the host side of application. let app_data = view_model.lazy::().unwrap(); let app = DeclarativeApp::default() .tree(make_widget!(app)) .view_model(DATA, view_model) .event(move |_, event, _, _| { if let Event::WindowEvent { event: WindowEvent::KeyboardInput { input, .. }, .. } = event && let Some(key) = input.virtual_keycode && input.state == ElementState::Pressed && key == VirtualKeyCode::Space { // Here we use that shared reference to `AppData` // to mutate it and notify UI. *app_data.write().unwrap().counter += 1; }; true }); App::new(AppConfig::default().title("View-Model")).run(app); } ================================================ FILE: crates/_/examples/view_model_hierarchy.rs ================================================ // Make sure you have seen `view_model_widget` code example first, because this is an evolution of that. use raui_app::app::declarative::DeclarativeApp; use raui_core::{ make_widget, pre_hooks, view_model::{ViewModel, ViewModelValue}, widget::{ component::{ containers::vertical_box::nav_vertical_box, image_box::{ImageBoxProps, image_box}, interactive::{ button::{ButtonNotifyMessage, ButtonNotifyProps, button}, navigation::NavItemActive, }, text_box::{TextBoxProps, text_box}, }, context::WidgetContext, node::WidgetNode, unit::text::TextBoxFont, utils::Color, }, }; const DATA: &str = "data"; const COUNTER: &str = "counter"; struct AppData { counter: ViewModelValue, } fn use_app(ctx: &mut WidgetContext) { ctx.life_cycle.mount(|mut ctx| { // We register View-Model to `app` widget. let mut view_model = ViewModel::produce(|properties| AppData { counter: ViewModelValue::new(0, properties.notifier(COUNTER)), }); view_model .properties .bindings(COUNTER) .unwrap() .bind(ctx.id.to_owned()); ctx.view_models.widget_register(DATA, view_model); }); } #[pre_hooks(use_app)] fn app(mut ctx: WidgetContext) -> WidgetNode { // We read View-Model from `app` widget. let counter = ctx .view_models .widget_view_model(DATA) .and_then(|view_model| view_model.read::().map(|data| *data.counter)) .unwrap_or_default(); make_widget!(nav_vertical_box) .listed_slot(make_widget!(text_box).with_props(TextBoxProps { text: format!("Counter: {counter}"), font: TextBoxFont { name: "./demos/hello-world/resources/verdana.ttf".to_owned(), size: 48.0, }, color: Color { r: 0.0, g: 0.0, b: 0.0, a: 1.0, }, ..Default::default() })) .listed_slot(make_widget!(trigger)) .into() } fn use_trigger(ctx: &mut WidgetContext) { ctx.life_cycle.change(|mut ctx| { // We write to View-Model in hierarchy of `app` widget branch, // that happen to be parent of this `trigger` widget. // Useful for data storages cascading down the hierarchy tree. // Each level of the hierarchy can also "override" View-Models. let mut app_data = ctx .view_models .hierarchy_view_model_mut(DATA) .unwrap() .write::() .unwrap(); for msg in ctx.messenger.messages { if let Some(msg) = msg.as_any().downcast_ref::() { if msg.trigger_start() { *app_data.counter = app_data.counter.saturating_add(1); } else if msg.context_start() { *app_data.counter = app_data.counter.saturating_sub(1); } } } }); } #[pre_hooks(use_trigger)] fn trigger(mut ctx: WidgetContext) -> WidgetNode { make_widget!(button) .with_props(NavItemActive) .with_props(ButtonNotifyProps(ctx.id.to_owned().into())) .named_slot( "content", make_widget!(image_box).with_props(ImageBoxProps::colored(Color { r: 0.5, g: 1.0, b: 1.0, a: 1.0, })), ) .into() } fn main() { DeclarativeApp::simple("View-Model - Hierarchy", make_widget!(app)); } ================================================ FILE: crates/_/examples/view_model_widget.rs ================================================ // Make sure you have seen `view_model` code example first, because this is an evolution of that. use raui_app::app::declarative::DeclarativeApp; use raui_core::{ animator::{AnimatedValue, Animation, AnimationMessage}, make_widget, pre_hooks, view_model::{ViewModel, ViewModelValue}, widget::{ component::text_box::{TextBoxProps, text_box}, context::WidgetContext, node::WidgetNode, unit::text::TextBoxFont, utils::Color, }, }; // animation message name used to trigger counter change. const TICK: &str = "tick"; // View-Model name. const DATA: &str = "data"; // View-Model proeprty name. const COUNTER: &str = "counter"; struct AppData { counter: ViewModelValue, } fn use_app(ctx: &mut WidgetContext) { ctx.life_cycle.mount(|mut ctx| { // First we register View-Model owned by this widget. // Widget View-Models are replacements for widget state useful // in situations where widget state should not be serializable props. let mut view_model = ViewModel::produce(|properties| AppData { counter: ViewModelValue::new(0, properties.notifier(COUNTER)), }); view_model .properties .bindings(COUNTER) .unwrap() .bind(ctx.id.to_owned()); ctx.view_models.widget_register(DATA, view_model); // Then we register new looped tick animation to trigger counter ticks. let _ = ctx.animator.change( TICK, Some(Animation::Looped(Box::new(Animation::Sequence(vec![ Animation::Value(AnimatedValue { duration: 1.0, ..Default::default() }), Animation::Message(TICK.to_owned()), ])))), ); }); ctx.life_cycle.change(|mut ctx| { // We get View-Model of this widget. let mut app_data = ctx .view_models .widget_view_model_mut(DATA) .unwrap() .write::() .unwrap(); // And then we react for tick messages from animation. for msg in ctx.messenger.messages { if let Some(msg) = msg.as_any().downcast_ref::() && msg.0 == TICK { *app_data.counter += 1; } } }); } #[pre_hooks(use_app)] fn app(mut ctx: WidgetContext) -> WidgetNode { // Because widget rendering can happen before widget mount, // we need to fallback to some default value in case View-Model // is not yet available. let counter = ctx .view_models .widget_view_model(DATA) .and_then(|view_model| view_model.read::().map(|data| *data.counter)) .unwrap_or_default(); make_widget!(text_box) .with_props(TextBoxProps { text: format!("Counter: {counter}"), font: TextBoxFont { name: "./demos/hello-world/resources/verdana.ttf".to_owned(), size: 48.0, }, color: Color { r: 0.0, g: 0.0, b: 0.0, a: 1.0, }, ..Default::default() }) .into() } fn main() { DeclarativeApp::simple("View-Model - Widget", make_widget!(app)); } ================================================ FILE: crates/_/examples/wrap_box.rs ================================================ use raui_app::app::declarative::DeclarativeApp; use raui_core::{ make_widget, widget::{ component::{ containers::wrap_box::{WrapBoxProps, wrap_box}, image_box::{ImageBoxProps, image_box}, }, utils::Color, }, }; fn main() { let tree = make_widget!(wrap_box) .with_props(WrapBoxProps { // wrap box just wraps its content with margin. margin: 50.0.into(), ..Default::default() }) .named_slot( "content", make_widget!(image_box).with_props(ImageBoxProps::colored(Color { r: 1.0, g: 0.25, b: 0.25, a: 1.0, })), ); DeclarativeApp::simple("Wrap Box", tree); } ================================================ FILE: crates/_/src/import_all.rs ================================================ #![allow(ambiguous_glob_reexports)] #![allow(unused_variables)] pub use raui_core::animator::*; pub use raui_core::application::*; pub use raui_core::interactive::*; pub use raui_core::interactive::default_interactions_engine::*; pub use raui_core::layout::*; pub use raui_core::layout::default_layout_engine::*; pub use raui_core::*; pub use raui_core::messenger::*; pub use raui_core::props::*; pub use raui_core::renderer::*; pub use raui_core::signals::*; pub use raui_core::state::*; pub use raui_core::tester::*; pub use raui_core::view_model::*; pub use raui_core::widget::*; pub use raui_core::widget::component::*; pub use raui_core::widget::component::containers::*; pub use raui_core::widget::component::containers::anchor_box::*; pub use raui_core::widget::component::containers::area_box::*; pub use raui_core::widget::component::containers::content_box::*; pub use raui_core::widget::component::containers::context_box::*; pub use raui_core::widget::component::containers::flex_box::*; pub use raui_core::widget::component::containers::float_box::*; pub use raui_core::widget::component::containers::grid_box::*; pub use raui_core::widget::component::containers::hidden_box::*; pub use raui_core::widget::component::containers::horizontal_box::*; pub use raui_core::widget::component::containers::portal_box::*; pub use raui_core::widget::component::containers::responsive_box::*; pub use raui_core::widget::component::containers::scroll_box::*; pub use raui_core::widget::component::containers::size_box::*; pub use raui_core::widget::component::containers::switch_box::*; pub use raui_core::widget::component::containers::tabs_box::*; pub use raui_core::widget::component::containers::tooltip_box::*; pub use raui_core::widget::component::containers::variant_box::*; pub use raui_core::widget::component::containers::vertical_box::*; pub use raui_core::widget::component::containers::wrap_box::*; pub use raui_core::widget::component::image_box::*; pub use raui_core::widget::component::interactive::*; pub use raui_core::widget::component::interactive::button::*; pub use raui_core::widget::component::interactive::float_view::*; pub use raui_core::widget::component::interactive::input_field::*; pub use raui_core::widget::component::interactive::navigation::*; pub use raui_core::widget::component::interactive::options_view::*; pub use raui_core::widget::component::interactive::scroll_view::*; pub use raui_core::widget::component::interactive::slider_view::*; pub use raui_core::widget::component::space_box::*; pub use raui_core::widget::component::text_box::*; pub use raui_core::widget::context::*; pub use raui_core::widget::node::*; pub use raui_core::widget::unit::*; pub use raui_core::widget::unit::area::*; pub use raui_core::widget::unit::content::*; pub use raui_core::widget::unit::flex::*; pub use raui_core::widget::unit::grid::*; pub use raui_core::widget::unit::image::*; pub use raui_core::widget::unit::portal::*; pub use raui_core::widget::unit::size::*; pub use raui_core::widget::unit::text::*; pub use raui_core::widget::utils::*; #[cfg(feature = "material")] pub use raui_material::component::*; #[cfg(feature = "material")] pub use raui_material::component::containers::*; #[cfg(feature = "material")] pub use raui_material::component::containers::context_paper::*; #[cfg(feature = "material")] pub use raui_material::component::containers::flex_paper::*; #[cfg(feature = "material")] pub use raui_material::component::containers::grid_paper::*; #[cfg(feature = "material")] pub use raui_material::component::containers::horizontal_paper::*; #[cfg(feature = "material")] pub use raui_material::component::containers::modal_paper::*; #[cfg(feature = "material")] pub use raui_material::component::containers::paper::*; #[cfg(feature = "material")] pub use raui_material::component::containers::scroll_paper::*; #[cfg(feature = "material")] pub use raui_material::component::containers::text_tooltip_paper::*; #[cfg(feature = "material")] pub use raui_material::component::containers::tooltip_paper::*; #[cfg(feature = "material")] pub use raui_material::component::containers::vertical_paper::*; #[cfg(feature = "material")] pub use raui_material::component::containers::window_paper::*; #[cfg(feature = "material")] pub use raui_material::component::containers::wrap_paper::*; #[cfg(feature = "material")] pub use raui_material::component::icon_paper::*; #[cfg(feature = "material")] pub use raui_material::component::interactive::*; #[cfg(feature = "material")] pub use raui_material::component::interactive::button_paper::*; #[cfg(feature = "material")] pub use raui_material::component::interactive::icon_button_paper::*; #[cfg(feature = "material")] pub use raui_material::component::interactive::slider_paper::*; #[cfg(feature = "material")] pub use raui_material::component::interactive::switch_button_paper::*; #[cfg(feature = "material")] pub use raui_material::component::interactive::text_button_paper::*; #[cfg(feature = "material")] pub use raui_material::component::interactive::text_field_paper::*; #[cfg(feature = "material")] pub use raui_material::component::switch_paper::*; #[cfg(feature = "material")] pub use raui_material::component::text_paper::*; #[cfg(feature = "material")] pub use raui_material::*; #[cfg(feature = "material")] pub use raui_material::theme::*; #[cfg(feature = "retained")] pub use raui_retained::*; #[cfg(feature = "immediate")] pub use raui_immediate::*; #[cfg(feature = "immediate-widgets")] pub use raui_immediate_widgets::*; #[cfg(feature = "tesselate")] pub use raui_tesselate_renderer::*; #[cfg(feature = "json")] pub use raui_json_renderer::*; #[cfg(feature = "app")] pub use raui_app::app::*; #[cfg(feature = "app")] pub use raui_app::app::declarative::*; #[cfg(feature = "app")] pub use raui_app::app::immediate::*; #[cfg(feature = "app")] pub use raui_app::app::retained::*; #[cfg(feature = "app")] pub use raui_app::components::*; #[cfg(feature = "app")] pub use raui_app::components::canvas::*; #[cfg(feature = "app")] pub use raui_app::*; #[cfg(feature = "app")] pub use raui_app::render_worker::*; ================================================ FILE: crates/_/src/lib.rs ================================================ //! 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 #[doc(inline)] pub use raui_core as core; #[doc(inline)] #[cfg(feature = "material")] pub use raui_material as material; /// Renderer implementations pub mod renderer { #[cfg(feature = "json")] pub mod json { pub use raui_json_renderer::*; } #[cfg(feature = "tesselate")] pub mod tesselate { pub use raui_tesselate_renderer::*; } } #[doc(inline)] #[cfg(feature = "app")] pub use raui_app as app; #[doc(hidden)] #[cfg(feature = "import-all")] pub mod import_all; ================================================ FILE: crates/app/Cargo.toml ================================================ [package] name = "raui-app" version = "0.70.17" authors = ["Patryk 'PsichiX' Budzynski "] edition = "2024" description = "RAUI application layer to focus only on making UI" 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"] [dependencies] raui-core = { path = "../core", version = "0.70" } raui-material = { path = "../material", version = "0.70" } raui-immediate = { path = "../immediate", version = "0.70" } raui-retained = { path = "../retained", version = "0.70" } raui-tesselate-renderer = { path = "../tesselate-renderer", version = "0.70" } spitfire-core = "0.36" spitfire-glow = "0.36" spitfire-fontdue = "0.36" bytemuck = { version = "1", features = ["derive"] } glutin = "0.28" fontdue = "0.9" image = "0.25" vek = "0.17" serde = { version = "1", features = ["derive"] } toml = "0.9" ================================================ FILE: crates/app/src/app/declarative.rs ================================================ use crate::{Vertex, app::SharedApp, interactions::AppInteractionsEngine}; use glutin::{event::Event, window::Window}; use raui_core::{ application::Application, interactive::default_interactions_engine::DefaultInteractionsEngine, layout::CoordsMappingScaling, view_model::ViewModel, widget::{node::WidgetNode, utils::Color}, }; use spitfire_fontdue::TextRenderer; use spitfire_glow::{ app::{App, AppConfig, AppControl, AppState}, graphics::Graphics, }; #[derive(Default)] pub struct DeclarativeApp { shared: SharedApp, } impl DeclarativeApp { pub fn simple(title: impl ToString, root: impl Into) { App::::new(AppConfig::default().title(title)).run(Self::default().tree(root)); } pub fn simple_scaled( title: impl ToString, scaling: CoordsMappingScaling, root: impl Into, ) { App::::new(AppConfig::default().title(title)) .run(Self::default().coords_mapping_scaling(scaling).tree(root)); } pub fn simple_fullscreen(title: impl ToString, root: impl Into) { App::::new(AppConfig::default().title(title).fullscreen(true)) .run(Self::default().tree(root)); } pub fn simple_fullscreen_scaled( title: impl ToString, scaling: CoordsMappingScaling, root: impl Into, ) { App::::new(AppConfig::default().title(title).fullscreen(true)) .run(Self::default().coords_mapping_scaling(scaling).tree(root)); } pub fn update(mut self, f: impl FnMut(&mut Application, &mut AppControl) + 'static) -> Self { self.shared.on_update = Some(Box::new(f)); self } pub fn redraw( mut self, f: impl FnMut(f32, &mut Graphics, &mut TextRenderer, &mut AppControl) + 'static, ) -> Self { self.shared.on_redraw = Some(Box::new(f)); self } pub fn event( mut self, f: impl FnMut(&mut Application, Event<()>, &mut Window, &mut DefaultInteractionsEngine) -> bool + 'static, ) -> Self { self.shared.on_event = Some(Box::new(f)); self } pub fn setup(mut self, mut f: impl FnMut(&mut Application)) -> Self { f(&mut self.shared.application); self } pub fn setup_interactions(mut self, mut f: impl FnMut(&mut AppInteractionsEngine)) -> Self { f(&mut self.shared.interactions); self } pub fn view_model(mut self, name: impl ToString, view_model: ViewModel) -> Self { self.shared .application .view_models .insert(name.to_string(), view_model); self } pub fn tree(mut self, root: impl Into) -> Self { self.shared.application.apply(root); self } pub fn coords_mapping_scaling(mut self, value: CoordsMappingScaling) -> Self { self.shared.coords_mapping_scaling = value; self } } impl AppState for DeclarativeApp { fn on_init(&mut self, graphics: &mut Graphics, _: &mut AppControl) { self.shared.init(graphics); } fn on_redraw(&mut self, graphics: &mut Graphics, control: &mut AppControl) { self.shared.redraw(graphics, control); } fn on_event(&mut self, event: Event<()>, window: &mut Window) -> bool { self.shared.event(event, window) } } ================================================ FILE: crates/app/src/app/immediate.rs ================================================ use crate::{Vertex, app::SharedApp, interactions::AppInteractionsEngine}; use glutin::{event::Event, window::Window}; use raui_core::{ application::Application, interactive::default_interactions_engine::DefaultInteractionsEngine, layout::CoordsMappingScaling, make_widget, tester::{AppCycleFrameRunner, AppCycleTester}, widget::{component::containers::content_box::content_box, utils::Color}, }; use raui_immediate::{ImmediateContext, make_widgets}; use spitfire_fontdue::TextRenderer; use spitfire_glow::{ app::{App, AppConfig, AppControl, AppState}, graphics::Graphics, }; #[derive(Default)] pub struct ImmediateApp { shared: SharedApp, } impl ImmediateApp { pub fn simple(title: impl ToString, callback: impl FnMut(&mut AppControl) + 'static) { App::::new(AppConfig::default().title(title)).run(Self::default().update(callback)); } pub fn simple_scaled( title: impl ToString, scaling: CoordsMappingScaling, callback: impl FnMut(&mut AppControl) + 'static, ) { App::::new(AppConfig::default().title(title)).run( Self::default() .coords_mapping_scaling(scaling) .update(callback), ); } pub fn simple_fullscreen( title: impl ToString, callback: impl FnMut(&mut AppControl) + 'static, ) { App::::new(AppConfig::default().title(title).fullscreen(true)) .run(Self::default().update(callback)); } pub fn simple_fullscreen_scaled( title: impl ToString, scaling: CoordsMappingScaling, callback: impl FnMut(&mut AppControl) + 'static, ) { App::::new(AppConfig::default().title(title).fullscreen(true)).run( Self::default() .coords_mapping_scaling(scaling) .update(callback), ); } pub fn test_frame(f: F) -> ImmediateAppCycleFrameRunner { ImmediateAppCycleFrameRunner(f) } pub fn update(mut self, callback: impl FnMut(&mut AppControl) + 'static) -> Self { let mut callback = Box::new(callback); let context = ImmediateContext::default(); self.shared.on_update = Some(Box::new(move |application, control| { raui_immediate::reset(); let widgets = make_widgets(&context, || { callback(control); }); application.apply(make_widget!(content_box).listed_slots(widgets)); })); self } pub fn redraw( mut self, f: impl FnMut(f32, &mut Graphics, &mut TextRenderer, &mut AppControl) + 'static, ) -> Self { self.shared.on_redraw = Some(Box::new(f)); self } pub fn event( mut self, f: impl FnMut(&mut Application, Event<()>, &mut Window, &mut DefaultInteractionsEngine) -> bool + 'static, ) -> Self { self.shared.on_event = Some(Box::new(f)); self } pub fn setup(mut self, mut f: impl FnMut(&mut Application)) -> Self { f(&mut self.shared.application); self } pub fn setup_interactions(mut self, mut f: impl FnMut(&mut AppInteractionsEngine)) -> Self { f(&mut self.shared.interactions); self } pub fn coords_mapping_scaling(mut self, value: CoordsMappingScaling) -> Self { self.shared.coords_mapping_scaling = value; self } } impl AppState for ImmediateApp { fn on_init(&mut self, graphics: &mut Graphics, _: &mut AppControl) { self.shared.init(graphics); } fn on_redraw(&mut self, graphics: &mut Graphics, control: &mut AppControl) { self.shared.redraw(graphics, control); } fn on_event(&mut self, event: Event<()>, window: &mut Window) -> bool { self.shared.event(event, window) } } pub struct ImmediateAppCycleFrameRunner(F); impl AppCycleFrameRunner for ImmediateAppCycleFrameRunner { fn run_frame(mut self, tester: &mut AppCycleTester) { raui_immediate::reset(); let widgets = make_widgets(&tester.user_data, || { (self.0)(); }); tester .application .apply(make_widget!(content_box).listed_slots(widgets)); } } ================================================ FILE: crates/app/src/app/mod.rs ================================================ pub mod declarative; pub mod immediate; pub mod retained; use crate::{ TesselateToGraphics, Vertex, asset_manager::AssetsManager, interactions::AppInteractionsEngine, render_worker::RenderWorkersViewModel, text_measurements::AppTextMeasurementsEngine, }; use glutin::{ event::{ElementState, Event, VirtualKeyCode, WindowEvent}, window::Window, }; use raui_core::{ application::Application, interactive::default_interactions_engine::DefaultInteractionsEngine, layout::{CoordsMapping, CoordsMappingScaling, default_layout_engine::DefaultLayoutEngine}, view_model::ViewModel, widget::utils::{Color, Rect}, }; use raui_tesselate_renderer::{TesselateRenderer, TessselateRendererDebug}; use spitfire_fontdue::TextRenderer; use spitfire_glow::{ app::AppControl, graphics::{Graphics, GraphicsBatch, Shader, Texture}, renderer::{GlowTextureFormat, GlowUniformValue}, }; use std::{collections::HashMap, time::Instant}; pub use spitfire_glow::app::{App, AppConfig}; #[cfg(debug_assertions)] const DEBUG_VERTEX: &str = r#"#version 300 es layout(location = 0) in vec2 a_position; out vec4 v_color; uniform mat4 u_projection_view; void main() { gl_Position = u_projection_view * vec4(a_position, 0.0, 1.0); } "#; #[cfg(debug_assertions)] const DEBUG_FRAGMENT: &str = r#"#version 300 es precision highp float; precision highp int; out vec4 o_color; uniform float u_time; vec3 hsv2rgb(vec3 c) { vec4 K = vec4(1.0, 2.0/3.0, 1.0/3.0, 3.0); vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); } void main() { vec2 pixel = floor(gl_FragCoord.xy); float hue = fract((floor(pixel.x) + floor(pixel.y)) * 0.01 + u_time); o_color = vec4(hsv2rgb(vec3(hue, 1.0, 1.0)), 1.0); } "#; macro_rules! hash_map { ($($key:ident => $value:expr),* $(,)?) => {{ let mut result = HashMap::default(); $( result.insert(stringify!($key).into(), $value); )* result }}; } pub(crate) struct SharedApp { #[allow(clippy::type_complexity)] on_update: Option>, /// fn(delta time, graphics interface) #[allow(clippy::type_complexity)] on_redraw: Option< Box, &mut TextRenderer, &mut AppControl)>, >, #[allow(clippy::type_complexity)] on_event: Option< Box< dyn FnMut( &mut Application, Event<()>, &mut Window, &mut DefaultInteractionsEngine, ) -> bool, >, >, application: Application, interactions: AppInteractionsEngine, text_renderer: TextRenderer, timer: Instant, time: f32, assets: AssetsManager, coords_mapping: CoordsMapping, pub coords_mapping_scaling: CoordsMappingScaling, missing_texutre: Option, glyphs_texture: Option, colored_shader: Option, textured_shader: Option, text_shader: Option, #[cfg(debug_assertions)] debug_shader: Option, #[cfg(debug_assertions)] pub show_raui_aabb_mode: u8, #[cfg(debug_assertions)] pub show_raui_aabb_key: VirtualKeyCode, #[cfg(debug_assertions)] pub print_raui_tree_key: VirtualKeyCode, #[cfg(debug_assertions)] pub print_raui_layout_key: VirtualKeyCode, #[cfg(debug_assertions)] pub print_raui_interactions_key: VirtualKeyCode, } impl Default for SharedApp { fn default() -> Self { let mut application = Application::default(); application.setup(raui_core::widget::setup); application.setup(raui_material::setup); application.view_models.insert( RenderWorkersViewModel::VIEW_MODEL.to_owned(), ViewModel::new(RenderWorkersViewModel::default(), Default::default()), ); Self { on_update: None, on_redraw: None, on_event: None, application, interactions: Default::default(), text_renderer: TextRenderer::new(1024, 1024), timer: Instant::now(), time: 0.0, assets: Default::default(), coords_mapping: Default::default(), coords_mapping_scaling: Default::default(), missing_texutre: None, glyphs_texture: None, colored_shader: None, textured_shader: None, text_shader: None, #[cfg(debug_assertions)] debug_shader: None, #[cfg(debug_assertions)] show_raui_aabb_mode: 0, #[cfg(debug_assertions)] show_raui_aabb_key: VirtualKeyCode::F9, #[cfg(debug_assertions)] print_raui_tree_key: VirtualKeyCode::F10, #[cfg(debug_assertions)] print_raui_layout_key: VirtualKeyCode::F11, #[cfg(debug_assertions)] print_raui_interactions_key: VirtualKeyCode::F12, } } } impl SharedApp { fn init(&mut self, graphics: &mut Graphics) { self.missing_texutre = Some(graphics.pixel_texture([255, 255, 255]).unwrap()); self.glyphs_texture = Some(graphics.pixel_texture([0, 0, 0]).unwrap()); self.colored_shader = Some( graphics .shader(Shader::COLORED_VERTEX_2D, Shader::PASS_FRAGMENT) .unwrap(), ); self.textured_shader = Some( graphics .shader(Shader::TEXTURED_VERTEX_2D, Shader::TEXTURED_FRAGMENT) .unwrap(), ); self.text_shader = Some( graphics .shader(Shader::TEXT_VERTEX, Shader::TEXT_FRAGMENT) .unwrap(), ); #[cfg(debug_assertions)] { self.debug_shader = Some(graphics.shader(DEBUG_VERTEX, DEBUG_FRAGMENT).unwrap()); } } fn redraw(&mut self, graphics: &mut Graphics, control: &mut AppControl) { let elapsed = self.timer.elapsed(); self.timer = Instant::now(); self.time += elapsed.as_secs_f32(); if let Some(callback) = self.on_update.as_mut() { callback(&mut self.application, control); } self.text_renderer.clear(); if let Some(callback) = self.on_redraw.as_mut() { callback( elapsed.as_secs_f32(), graphics, &mut self.text_renderer, control, ); } { self.application .view_models .get_mut(RenderWorkersViewModel::VIEW_MODEL) .unwrap() .write::() .unwrap() .maintain( graphics, &mut self.assets, self.colored_shader.as_ref().unwrap(), self.textured_shader.as_ref().unwrap(), self.text_shader.as_ref().unwrap(), ); } self.assets.maintain(); self.application.animations_delta_time = elapsed.as_secs_f32(); self.coords_mapping = CoordsMapping::new_scaling( Rect { left: 0.0, right: graphics.state.main_camera.screen_size.x, top: 0.0, bottom: graphics.state.main_camera.screen_size.y, }, self.coords_mapping_scaling, ); if self.application.process() { self.assets.load(self.application.rendered_tree(), graphics); let mut layout_engine = DefaultLayoutEngine::new(AppTextMeasurementsEngine { assets: &self.assets, }); let _ = self .application .layout(&self.coords_mapping, &mut layout_engine); } else { self.assets.load(self.application.rendered_tree(), graphics); } let _ = self.application.interact(&mut self.interactions); self.application.consume_signals(); let matrix = graphics .state .main_camera .world_projection_matrix() .into_col_array(); graphics.state.stream.batch_end(); for shader in [ self.colored_shader.clone(), self.textured_shader.clone(), self.text_shader.clone(), #[cfg(debug_assertions)] self.debug_shader.clone(), ] { graphics.state.stream.batch(GraphicsBatch { shader, uniforms: hash_map! { u_image => GlowUniformValue::I1(0), u_projection_view => GlowUniformValue::M4(matrix), u_time => GlowUniformValue::F1(self.time), }, ..Default::default() }); graphics.state.stream.batch_end(); } let mut converter = TesselateToGraphics { colored_shader: self.colored_shader.as_ref().unwrap(), textured_shader: self.textured_shader.as_ref().unwrap(), text_shader: self.text_shader.as_ref().unwrap(), #[cfg(debug_assertions)] debug_shader: self.debug_shader.as_ref(), glyphs_texture: self.glyphs_texture.as_ref().unwrap(), missing_texture: self.missing_texutre.as_ref().unwrap(), assets: &self.assets, clip_stack: Vec::with_capacity(64), viewport_height: graphics.state.main_camera.screen_size.y as _, projection_view_matrix: matrix, }; let mut renderer = TesselateRenderer::new( &self.assets, &mut converter, &mut graphics.state.stream, &mut self.text_renderer, if cfg!(debug_assertions) { match self.show_raui_aabb_mode { 0 => None, 1 => Some(TessselateRendererDebug { render_non_visual_nodes: false, }), 2 => Some(TessselateRendererDebug { render_non_visual_nodes: true, }), _ => unreachable!(), } } else { None }, ); let _ = self.application.render(&self.coords_mapping, &mut renderer); let [w, h, d] = self.text_renderer.atlas_size(); self.glyphs_texture.as_mut().unwrap().upload( w as _, h as _, d as _, GlowTextureFormat::Monochromatic, Some(self.text_renderer.image()), ); } fn event(&mut self, event: Event<()>, window: &mut Window) -> bool { self.interactions.event(&event, &self.coords_mapping); if let Event::WindowEvent { event: WindowEvent::Resized(_), .. } = &event { self.application.mark_dirty(); } #[cfg(debug_assertions)] if let Event::WindowEvent { event: WindowEvent::KeyboardInput { input, .. }, .. } = &event && input.state == ElementState::Pressed && let Some(key) = input.virtual_keycode { if key == self.show_raui_aabb_key { self.show_raui_aabb_mode = (self.show_raui_aabb_mode + 1) % 3; println!( "* SHOW RAUI LAYOUT AABB MODE: {:#?}", self.show_raui_aabb_mode ); } else if key == self.print_raui_tree_key { println!("* RAUI TREE: {:#?}", self.application.rendered_tree()); } else if key == self.print_raui_layout_key { println!("* RAUI LAYOUT: {:#?}", self.application.layout_data()); } else if key == self.print_raui_interactions_key { println!("* RAUI INTERACTIONS: {:#?}", self.interactions); } } self.on_event .as_mut() .map(|callback| { callback( &mut self.application, event, window, &mut self.interactions.engine, ) }) .unwrap_or(true) } } ================================================ FILE: crates/app/src/app/retained.rs ================================================ use crate::{Vertex, app::SharedApp, interactions::AppInteractionsEngine}; use glutin::{event::Event, window::Window}; use raui_core::{ application::{Application, ChangeNotifier}, interactive::default_interactions_engine::DefaultInteractionsEngine, layout::CoordsMappingScaling, widget::utils::Color, }; use raui_retained::{View, ViewState}; use spitfire_fontdue::TextRenderer; use spitfire_glow::{ app::{App, AppConfig, AppControl, AppState}, graphics::Graphics, }; pub struct RetainedApp { shared: SharedApp, root: Option>, } impl Default for RetainedApp { fn default() -> Self { Self { shared: Default::default(), root: None, } } } impl RetainedApp { pub fn simple(title: impl ToString, producer: impl FnMut(ChangeNotifier) -> View) { App::::new(AppConfig::default().title(title)).run(Self::default().tree(producer)); } pub fn simple_scaled( title: impl ToString, scaling: CoordsMappingScaling, producer: impl FnMut(ChangeNotifier) -> View, ) { App::::new(AppConfig::default().title(title)).run( Self::default() .coords_mapping_scaling(scaling) .tree(producer), ); } pub fn simple_fullscreen( title: impl ToString, producer: impl FnMut(ChangeNotifier) -> View, ) { App::::new(AppConfig::default().title(title).fullscreen(true)) .run(Self::default().tree(producer)); } pub fn simple_fullscreen_scaled( title: impl ToString, scaling: CoordsMappingScaling, producer: impl FnMut(ChangeNotifier) -> View, ) { App::::new(AppConfig::default().title(title).fullscreen(true)).run( Self::default() .coords_mapping_scaling(scaling) .tree(producer), ); } pub fn redraw( mut self, f: impl FnMut(f32, &mut Graphics, &mut TextRenderer, &mut AppControl) + 'static, ) -> Self { self.shared.on_redraw = Some(Box::new(f)); self } pub fn event( mut self, f: impl FnMut(&mut Application, Event<()>, &mut Window, &mut DefaultInteractionsEngine) -> bool + 'static, ) -> Self { self.shared.on_event = Some(Box::new(f)); self } pub fn setup(mut self, mut f: impl FnMut(&mut Application)) -> Self { f(&mut self.shared.application); self } pub fn setup_interactions(mut self, mut f: impl FnMut(&mut AppInteractionsEngine)) -> Self { f(&mut self.shared.interactions); self } pub fn tree(mut self, mut producer: impl FnMut(ChangeNotifier) -> View) -> Self { let root = producer(self.shared.application.notifier()); self.shared.application.apply(root.component().key("root")); self.root = Some(root); self } pub fn coords_mapping_scaling(mut self, value: CoordsMappingScaling) -> Self { self.shared.coords_mapping_scaling = value; self } } impl AppState for RetainedApp { fn on_init(&mut self, graphics: &mut Graphics, _: &mut AppControl) { self.shared.init(graphics); } fn on_redraw(&mut self, graphics: &mut Graphics, control: &mut AppControl) { self.shared.redraw(graphics, control); } fn on_event(&mut self, event: Event<()>, window: &mut Window) -> bool { self.shared.event(event, window) } } ================================================ FILE: crates/app/src/asset_manager.rs ================================================ use crate::Vertex; use fontdue::Font; use image::EncodableLayout; use raui_core::widget::{ unit::{WidgetUnit, image::ImageBoxMaterial, portal::PortalBoxSlot}, utils::{Rect, Vec2}, }; use raui_tesselate_renderer::TesselateResourceProvider; use serde::{Deserialize, Serialize}; use spitfire_glow::{ graphics::{Graphics, Shader, Texture}, renderer::GlowTextureFormat, }; use std::{collections::HashMap, path::PathBuf}; #[derive(Serialize, Deserialize)] pub struct AssetAtlasRegion { x: u32, y: u32, width: u32, height: u32, } #[derive(Serialize, Deserialize)] struct AssetAtlas { image: PathBuf, regions: HashMap, } pub(crate) struct AssetTexture { pub(crate) texture: Texture, /// {name: uvs} regions: HashMap, frames_left: usize, forever_alive: bool, } pub(crate) struct AssetFont { hash: usize, frames_left: usize, forever_alive: bool, } pub(crate) struct AssetShader { pub(crate) shader: Shader, frames_left: usize, forever_alive: bool, } pub(crate) struct AssetsManager { pub frames_alive: usize, pub(crate) root_path: PathBuf, pub(crate) textures: HashMap, pub(crate) font_map: HashMap, pub(crate) fonts: Vec, pub(crate) shaders: HashMap, } impl Default for AssetsManager { fn default() -> Self { Self::new("./", 1024) } } impl AssetsManager { fn new(root_path: impl Into, frames_alive: usize) -> Self { Self { frames_alive, root_path: root_path.into(), textures: Default::default(), font_map: Default::default(), fonts: Default::default(), shaders: Default::default(), } } pub(crate) fn maintain(&mut self) { let to_remove = self .textures .iter_mut() .filter(|(_, texture)| !texture.forever_alive) .filter_map(|(id, texture)| { if texture.frames_left > 0 { texture.frames_left -= 1; None } else { Some(id.to_owned()) } }) .collect::>(); for id in to_remove { self.textures.remove(&id); } let to_remove = self .font_map .iter_mut() .filter(|(_, font)| !font.forever_alive) .filter_map(|(id, font)| { if font.frames_left > 0 { font.frames_left -= 1; None } else { Some(id.to_owned()) } }) .collect::>(); for id in to_remove { let hash = self.font_map.remove(&id).unwrap().hash; if let Some(index) = self.fonts.iter().position(|font| font.file_hash() == hash) { self.fonts.swap_remove(index); } } let to_remove = self .shaders .iter_mut() .filter(|(_, shader)| !shader.forever_alive) .filter_map(|(id, shader)| { if shader.frames_left > 0 { shader.frames_left -= 1; None } else { Some(id.to_owned()) } }) .collect::>(); for id in to_remove { self.shaders.remove(&id); } } pub(crate) fn load(&mut self, node: &WidgetUnit, graphics: &Graphics) { match node { WidgetUnit::None => {} WidgetUnit::AreaBox(node) => { self.load(&node.slot, graphics); } WidgetUnit::PortalBox(node) => match &*node.slot { PortalBoxSlot::Slot(node) => { self.load(node, graphics); } PortalBoxSlot::ContentItem(node) => { self.load(&node.slot, graphics); } PortalBoxSlot::FlexItem(node) => { self.load(&node.slot, graphics); } PortalBoxSlot::GridItem(node) => { self.load(&node.slot, graphics); } }, WidgetUnit::ContentBox(node) => { for item in &node.items { self.load(&item.slot, graphics); } } WidgetUnit::FlexBox(node) => { for item in &node.items { self.load(&item.slot, graphics); } } WidgetUnit::GridBox(node) => { for item in &node.items { self.load(&item.slot, graphics); } } WidgetUnit::SizeBox(node) => { self.load(&node.slot, graphics); } WidgetUnit::ImageBox(node) => match &node.material { ImageBoxMaterial::Image(image) => { let id = Self::parse_image_id(&image.id).0; self.try_load_image(id, graphics, false); } ImageBoxMaterial::Procedural(procedural) => { for id in &procedural.images { self.try_load_image(id, graphics, false); } if !procedural.id.is_empty() { self.try_load_shader(&procedural.id, graphics, false); } } _ => {} }, WidgetUnit::TextBox(node) => { self.try_load_font(&node.font.name, false); } } } pub(crate) fn parse_image_id(id: &str) -> (&str, Option<&str>) { match id.find('@') { Some(index) => (&id[..index], Some(&id[(index + b"@".len())..])), None => (id, None), } } pub(crate) fn add_texture(&mut self, id: impl ToString, texture: Texture) { self.textures.insert( id.to_string(), AssetTexture { texture, regions: Default::default(), frames_left: self.frames_alive, forever_alive: true, }, ); } pub(crate) fn remove_texture(&mut self, id: impl ToString) { self.textures.remove(&id.to_string()); } // pub(crate) fn add_shader(&mut self, id: impl ToString, shader: Shader) { // self.shaders.insert( // id.to_string(), // AssetShader { // shader, // frames_left: self.frames_alive, // forever_alive: true, // }, // ); // } // pub(crate) fn remove_shader(&mut self, id: impl ToString) { // self.shaders.remove(&id.to_string()); // } // pub(crate) fn add_font(&mut self, id: impl ToString, font: Font) { // self.font_map.insert( // id.to_string(), // AssetFont { // hash: font.file_hash(), // frames_left: self.frames_alive, // forever_alive: true, // }, // ); // self.fonts.push(font); // } // pub(crate) fn remove_font(&mut self, id: impl ToString) { // if let Some(font) = self.font_map.remove(&id.to_string()) { // if let Some(index) = self.fonts.iter().position(|f| f.file_hash() == font.hash) { // self.fonts.swap_remove(index); // } // } // } fn try_load_image(&mut self, id: &str, graphics: &Graphics, forever_alive: bool) { if let Some(texture) = self.textures.get_mut(id) { texture.frames_left = self.frames_alive; } else { let mut path = self.root_path.join(id); match path.extension().and_then(|ext| ext.to_str()).unwrap_or("") { "toml" => { let content = match std::fs::read_to_string(&path) { Ok(content) => content, _ => { eprintln!("Could not load image atlas file: {path:?}"); return; } }; let atlas = match toml::from_str::(&content) { Ok(atlas) => atlas, _ => { eprintln!("Could not parse image atlas file: {path:?}"); return; } }; path.pop(); let path = path.join(atlas.image); let image = match image::open(&path) { Ok(image) => image.to_rgba8(), _ => { eprintln!("Could not load image file: {path:?}"); return; } }; let texture = match graphics.texture( image.width(), image.height(), 1, GlowTextureFormat::Rgba, Some(image.as_bytes()), ) { Ok(texture) => texture, _ => { eprintln!("Could not create texture for image file: {path:?}"); return; } }; let regions = atlas .regions .into_iter() .map(|(name, region)| { let left = region.x as f32 / image.width() as f32; let right = (region.x + region.width) as f32 / image.width() as f32; let top = region.y as f32 / image.height() as f32; let bottom = (region.y + region.height) as f32 / image.height() as f32; ( name, Rect { left, right, top, bottom, }, ) }) .collect(); self.textures.insert( id.to_owned(), AssetTexture { texture, regions, frames_left: self.frames_alive, forever_alive, }, ); } _ => { let image = match image::open(&path) { Ok(image) => image.to_rgba8(), _ => { eprintln!("Could not load image file: {path:?}"); return; } }; let texture = match graphics.texture( image.width(), image.height(), 1, GlowTextureFormat::Rgba, Some(image.as_bytes()), ) { Ok(texture) => texture, _ => { eprintln!("Could not create texture for image file: {path:?}"); return; } }; self.textures.insert( id.to_owned(), AssetTexture { texture, regions: Default::default(), frames_left: self.frames_alive, forever_alive, }, ); } } } } fn try_load_font(&mut self, id: &str, forever_alive: bool) { if let Some(font) = self.font_map.get_mut(id) { font.frames_left = self.frames_alive; } else { let path = self.root_path.join(id); let content = match std::fs::read(&path) { Ok(content) => content, _ => { eprintln!("Could not load font file: {path:?}"); return; } }; let font = match Font::from_bytes(content, Default::default()) { Ok(font) => font, _ => return, }; self.font_map.insert( id.to_owned(), AssetFont { hash: font.file_hash(), frames_left: self.frames_alive, forever_alive, }, ); self.fonts.push(font); } } fn try_load_shader(&mut self, id: &str, graphics: &Graphics, forever_alive: bool) { if let Some(shader) = self.shaders.get_mut(id) { shader.frames_left = self.frames_alive; } else { let shader = match id { "@pass" => match graphics.shader(Shader::PASS_VERTEX_2D, Shader::PASS_FRAGMENT) { Ok(shader) => shader, _ => { eprintln!("Could not create shader for: {id:?}"); return; } }, "@colored" => { match graphics.shader(Shader::COLORED_VERTEX_2D, Shader::PASS_FRAGMENT) { Ok(shader) => shader, _ => { eprintln!("Could not create shader for: {id:?}"); return; } } } "@textured" => { match graphics.shader(Shader::TEXTURED_VERTEX_2D, Shader::TEXTURED_FRAGMENT) { Ok(shader) => shader, _ => { eprintln!("Could not create shader for: {id:?}"); return; } } } _ => { let path = self.root_path.join(format!("{id}.vs")); let vertex = match std::fs::read_to_string(&path) { Ok(content) => content, _ => { eprintln!("Could not load vertex shader file: {path:?}"); return; } }; let path = self.root_path.join(format!("{id}.fs")); let fragment = match std::fs::read_to_string(&path) { Ok(content) => content, _ => { eprintln!("Could not load fragment shader file: {path:?}"); return; } }; match graphics.shader(&vertex, &fragment) { Ok(shader) => shader, _ => { eprintln!("Could not create shader for: {id:?}"); return; } } } }; self.shaders.insert( id.to_owned(), AssetShader { shader, frames_left: self.frames_alive, forever_alive, }, ); } } } impl TesselateResourceProvider for AssetsManager { fn image_id_and_uv_and_size_by_atlas_id(&self, id: &str) -> Option<(String, Rect, Vec2)> { let (id, region) = Self::parse_image_id(id); let texture = self.textures.get(id)?; let uvs = region .and_then(|region| texture.regions.get(region)) .copied() .unwrap_or(Rect { left: 0.0, right: 1.0, top: 0.0, bottom: 1.0, }); let size = Vec2 { x: texture.texture.width() as _, y: texture.texture.height() as _, }; Some((id.to_owned(), uvs, size)) } fn fonts(&self) -> &[Font] { &self.fonts } fn font_index_by_id(&self, id: &str) -> Option { let hash = self.font_map.get(id)?.hash; self.fonts.iter().position(|font| font.file_hash() == hash) } } ================================================ FILE: crates/app/src/components/canvas.rs ================================================ use crate::render_worker::{ RenderWorkerDescriptor, RenderWorkerTaskContext, RenderWorkersViewModel, }; use raui_core::{ MessageData, Prefab, PropsData, make_widget, messenger::MessageData, pre_hooks, props::PropsData, widget::{ component::{ ResizeListenerSignal, image_box::{ImageBoxProps, image_box}, use_resize_listener, }, context::WidgetContext, node::WidgetNode, unit::area::AreaBoxNode, utils::Color, }, }; use serde::{Deserialize, Serialize}; use spitfire_glow::renderer::GlowTextureFormat; use std::sync::Arc; #[derive(PropsData, Debug, Default, Copy, Clone, Serialize, Deserialize)] pub struct CanvasProps { #[serde(default)] pub color: Color, #[serde(default)] pub clear: bool, } #[derive(MessageData, Debug, Clone)] pub struct RequestCanvasRedrawMessage; #[derive(MessageData, Clone)] pub enum DrawOnCanvasMessage { Function(fn(RenderWorkerTaskContext)), #[allow(clippy::type_complexity)] Generator(Arc Box + Send + Sync>), } impl DrawOnCanvasMessage { pub fn function(callback: fn(RenderWorkerTaskContext)) -> Self { Self::Function(callback) } pub fn generator( callback: impl Fn() -> Box + Send + Sync + 'static, ) -> Self { Self::Generator(Arc::new(callback)) } } impl std::fmt::Debug for DrawOnCanvasMessage { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("DrawOnCanvasMessage") .finish_non_exhaustive() } } pub fn use_canvas(ctx: &mut WidgetContext) { ctx.life_cycle.mount(|mut ctx| { let props = ctx.props.read_cloned_or_default::(); let mut workers = ctx .view_models .view_model_mut(RenderWorkersViewModel::VIEW_MODEL) .unwrap() .write::() .unwrap(); workers.add_worker(RenderWorkerDescriptor { id: ctx.id.to_string(), width: 1, height: 1, format: GlowTextureFormat::Rgba, color: [props.color.r, props.color.g, props.color.b, props.color.a], }); ctx.messenger .write(ctx.id.to_owned(), RequestCanvasRedrawMessage); }); ctx.life_cycle.unmount(|mut ctx| { let mut workers = ctx .view_models .view_model_mut(RenderWorkersViewModel::VIEW_MODEL) .unwrap() .write::() .unwrap(); workers.remove_worker(ctx.id.as_ref()); }); ctx.life_cycle.change(|mut ctx| { let props = ctx.props.read_cloned_or_default::(); let mut workers = ctx .view_models .view_model_mut(RenderWorkersViewModel::VIEW_MODEL) .unwrap() .write::() .unwrap(); for msg in ctx.messenger.messages { if let Some(ResizeListenerSignal::Change(size)) = msg.as_any().downcast_ref() { workers.add_worker(RenderWorkerDescriptor { id: ctx.id.to_string(), width: size.x as u32, height: size.y as u32, format: GlowTextureFormat::Rgba, color: [props.color.r, props.color.g, props.color.b, props.color.a], }); ctx.messenger .write(ctx.id.to_owned(), RequestCanvasRedrawMessage); } else if let Some(msg) = msg.as_any().downcast_ref::() { match msg { DrawOnCanvasMessage::Function(task) => { workers.schedule_task(ctx.id.as_ref(), props.clear, *task); } DrawOnCanvasMessage::Generator(task) => { workers.schedule_task(ctx.id.as_ref(), props.clear, (*task)()); } } } } }); } #[pre_hooks(use_resize_listener, use_canvas)] pub fn canvas(mut context: WidgetContext) -> WidgetNode { let WidgetContext { id, idref, key, .. } = context; let content = make_widget!(image_box) .key(key) .maybe_idref(idref.cloned()) .with_props(ImageBoxProps::image(id)) .into(); AreaBoxNode { id: id.to_owned(), slot: Box::new(content), } .into() } ================================================ FILE: crates/app/src/components/mod.rs ================================================ pub mod canvas; ================================================ FILE: crates/app/src/interactions.rs ================================================ use glutin::event::{ ElementState, Event, ModifiersState, MouseButton, MouseScrollDelta, VirtualKeyCode, WindowEvent, }; use raui_core::{ application::Application, interactive::{ InteractionsEngine, default_interactions_engine::{ DefaultInteractionsEngine, DefaultInteractionsEngineResult, Interaction, PointerButton, }, }, layout::CoordsMapping, widget::{ component::interactive::navigation::{NavJump, NavScroll, NavSignal, NavTextChange}, utils::Vec2, }, }; #[derive(Debug)] pub struct AppInteractionsEngine { pub engine: DefaultInteractionsEngine, pub single_scroll_units: Vec2, pointer_position: Vec2, modifiers: ModifiersState, } impl Default for AppInteractionsEngine { fn default() -> Self { Self::with_capacity(32, 32, 1024, 32, 32, 32, 32, 32, 32) } } impl AppInteractionsEngine { fn default_single_scroll_units() -> Vec2 { Vec2 { x: 10.0, y: 10.0 } } #[allow(clippy::too_many_arguments)] pub fn with_capacity( resize_listeners: usize, relative_layout_listeners: usize, interactions_queue: usize, containers: usize, buttons: usize, text_inputs: usize, scroll_views: usize, tracking: usize, selected_chain: usize, ) -> Self { let mut engine = DefaultInteractionsEngine::with_capacity( resize_listeners, relative_layout_listeners, interactions_queue, containers, buttons, text_inputs, scroll_views, tracking, selected_chain, ); engine.deselect_when_no_button_found = true; Self { engine, single_scroll_units: Self::default_single_scroll_units(), pointer_position: Default::default(), modifiers: Default::default(), } } pub fn event(&mut self, event: &Event<()>, mapping: &CoordsMapping) { if let Event::WindowEvent { event, .. } = event { match event { WindowEvent::ModifiersChanged(modifiers) => { self.modifiers = *modifiers; } WindowEvent::ReceivedCharacter(character) => { self.engine .interact(Interaction::Navigate(NavSignal::TextChange( NavTextChange::InsertCharacter(*character), ))); } WindowEvent::CursorMoved { position, .. } => { self.pointer_position = mapping.real_to_virtual_vec2( Vec2 { x: position.x as _, y: position.y as _, }, false, ); self.engine .interact(Interaction::PointerMove(self.pointer_position)); } WindowEvent::MouseWheel { delta, .. } => { let value = match delta { MouseScrollDelta::LineDelta(x, y) => Vec2 { x: -self.single_scroll_units.x * *x, y: -self.single_scroll_units.y * *y, }, MouseScrollDelta::PixelDelta(delta) => Vec2 { x: -delta.x as _, y: -delta.y as _, }, }; self.engine .interact(Interaction::Navigate(NavSignal::Jump(NavJump::Scroll( NavScroll::Units(value, true), )))); } WindowEvent::MouseInput { state, button, .. } => match state { ElementState::Pressed => match button { MouseButton::Left => { self.engine.interact(Interaction::PointerDown( PointerButton::Trigger, self.pointer_position, )); } MouseButton::Right => { self.engine.interact(Interaction::PointerDown( PointerButton::Context, self.pointer_position, )); } _ => {} }, ElementState::Released => match button { MouseButton::Left => { self.engine.interact(Interaction::PointerUp( PointerButton::Trigger, self.pointer_position, )); } MouseButton::Right => { self.engine.interact(Interaction::PointerUp( PointerButton::Context, self.pointer_position, )); } _ => {} }, }, WindowEvent::KeyboardInput { input, .. } => { if input.state == ElementState::Pressed { if let Some(key) = input.virtual_keycode { if self.engine.focused_text_input().is_some() { match key { VirtualKeyCode::Left => { self.engine.interact(Interaction::Navigate( NavSignal::TextChange(NavTextChange::MoveCursorLeft), )) } VirtualKeyCode::Right => { self.engine.interact(Interaction::Navigate( NavSignal::TextChange(NavTextChange::MoveCursorRight), )) } VirtualKeyCode::Home => { self.engine.interact(Interaction::Navigate( NavSignal::TextChange(NavTextChange::MoveCursorStart), )) } VirtualKeyCode::End => { self.engine.interact(Interaction::Navigate( NavSignal::TextChange(NavTextChange::MoveCursorEnd), )) } VirtualKeyCode::Back => { self.engine.interact(Interaction::Navigate( NavSignal::TextChange(NavTextChange::DeleteLeft), )) } VirtualKeyCode::Delete => { self.engine.interact(Interaction::Navigate( NavSignal::TextChange(NavTextChange::DeleteRight), )) } VirtualKeyCode::Return | VirtualKeyCode::NumpadEnter => { self.engine.interact(Interaction::Navigate( NavSignal::TextChange(NavTextChange::NewLine), )) } VirtualKeyCode::Escape => { self.engine.interact(Interaction::Navigate( NavSignal::FocusTextInput(().into()), )); } _ => {} } } else { match key { VirtualKeyCode::Up => { self.engine.interact(Interaction::Navigate(NavSignal::Up)) } VirtualKeyCode::Down => { self.engine.interact(Interaction::Navigate(NavSignal::Down)) } VirtualKeyCode::Left => { if self.modifiers.shift() { self.engine .interact(Interaction::Navigate(NavSignal::Prev)); } else { self.engine .interact(Interaction::Navigate(NavSignal::Left)); } } VirtualKeyCode::Right => { if self.modifiers.shift() { self.engine .interact(Interaction::Navigate(NavSignal::Next)); } else { self.engine .interact(Interaction::Navigate(NavSignal::Right)); } } VirtualKeyCode::Return | VirtualKeyCode::NumpadEnter | VirtualKeyCode::Space => { self.engine.interact(Interaction::Navigate( NavSignal::Accept(true), )); } VirtualKeyCode::Escape | VirtualKeyCode::Back => { self.engine.interact(Interaction::Navigate( NavSignal::Cancel(true), )); } _ => {} } } } } else if input.state == ElementState::Released && let Some(key) = input.virtual_keycode && self.engine.focused_text_input().is_none() { match key { VirtualKeyCode::Return | VirtualKeyCode::NumpadEnter | VirtualKeyCode::Space => { self.engine .interact(Interaction::Navigate(NavSignal::Accept(false))); } VirtualKeyCode::Escape | VirtualKeyCode::Back => { self.engine .interact(Interaction::Navigate(NavSignal::Cancel(false))); } _ => {} } } } _ => {} } } } } impl InteractionsEngine for AppInteractionsEngine { fn perform_interactions( &mut self, app: &mut Application, ) -> Result { self.engine.perform_interactions(app) } } ================================================ FILE: crates/app/src/lib.rs ================================================ pub mod app; pub(crate) mod asset_manager; pub mod components; pub(crate) mod interactions; pub mod render_worker; pub(crate) mod text_measurements; use crate::{ asset_manager::AssetsManager, components::canvas::{CanvasProps, canvas}, }; use bytemuck::{Pod, Zeroable}; use raui_core::{ application::Application, widget::{FnWidget, utils::Color}, }; use raui_tesselate_renderer::{TesselateBatch, TesselateBatchConverter, TesselateVertex}; use spitfire_fontdue::TextVertex; use spitfire_glow::{ graphics::{ Texture, {GraphicsBatch, Shader}, }, renderer::{ GlowBlending, GlowTextureFiltering, GlowUniformValue, GlowVertexAttrib, GlowVertexAttribs, }, }; use vek::Rect; #[cfg(not(target_arch = "wasm32"))] pub use glutin::{dpi, event, window}; #[cfg(target_arch = "wasm32")] pub use winit::{dpi, event, window}; pub mod third_party { pub use spitfire_fontdue; pub use spitfire_glow; } #[derive(Debug, Clone, Copy, Pod, Zeroable)] #[repr(C)] pub struct Vertex { pub position: [f32; 2], pub uv: [f32; 3], pub color: [f32; 4], } impl Default for Vertex { fn default() -> Self { Self { position: Default::default(), uv: Default::default(), color: [1.0, 1.0, 1.0, 1.0], } } } impl GlowVertexAttribs for Vertex { const ATTRIBS: &'static [(&'static str, GlowVertexAttrib)] = &[ ( "a_position", GlowVertexAttrib::Float { channels: 2, normalized: false, }, ), ( "a_uv", GlowVertexAttrib::Float { channels: 3, normalized: false, }, ), ( "a_color", GlowVertexAttrib::Float { channels: 4, normalized: false, }, ), ]; } impl TesselateVertex for Vertex { fn apply(&mut self, position: [f32; 2], tex_coord: [f32; 3], color: [f32; 4]) { self.position = position; self.uv = tex_coord; self.color = color; } fn transform(&mut self, matrix: vek::Mat4) { let result = matrix.mul_point(vek::Vec3 { x: self.position[0], y: self.position[1], z: 0.0, }); self.position[0] = result.x; self.position[1] = result.y; } } impl TextVertex for Vertex { fn apply(&mut self, position: [f32; 2], tex_coord: [f32; 3], color: Color) { self.position = position; self.uv = tex_coord; self.color = [color.r, color.g, color.b, color.a]; } } pub(crate) struct TesselateToGraphics<'a> { colored_shader: &'a Shader, textured_shader: &'a Shader, text_shader: &'a Shader, #[cfg(debug_assertions)] debug_shader: Option<&'a Shader>, glyphs_texture: &'a Texture, missing_texture: &'a Texture, assets: &'a AssetsManager, clip_stack: Vec>, viewport_height: i32, projection_view_matrix: [f32; 16], } impl TesselateBatchConverter for TesselateToGraphics<'_> { fn convert(&mut self, batch: TesselateBatch) -> Option { match batch { TesselateBatch::Color => Some(GraphicsBatch { shader: Some(self.colored_shader.clone()), blending: GlowBlending::Alpha, scissor: self.clip_stack.last().copied(), ..Default::default() }), TesselateBatch::Image { id } => { let id = AssetsManager::parse_image_id(&id).0; Some(GraphicsBatch { shader: Some(self.textured_shader.clone()), textures: vec![( self.assets .textures .get(id) .map(|texture| texture.texture.clone()) .unwrap_or_else(|| self.missing_texture.clone()), GlowTextureFiltering::Linear, )], blending: GlowBlending::Alpha, scissor: self.clip_stack.last().copied(), ..Default::default() }) } TesselateBatch::Text => Some(GraphicsBatch { shader: Some(self.text_shader.clone()), textures: vec![(self.glyphs_texture.clone(), GlowTextureFiltering::Linear)], blending: GlowBlending::Alpha, scissor: self.clip_stack.last().copied(), ..Default::default() }), TesselateBatch::Procedural { id, images, parameters, } => Some(GraphicsBatch { shader: self .assets .shaders .get(&id) .map(|shader| shader.shader.clone()), uniforms: parameters .into_iter() .map(|(k, v)| (k.into(), GlowUniformValue::F1(v))) .chain((0..images.len()).map(|index| { ( if index > 0 { format!("u_image{index}").into() } else { "u_image".into() }, GlowUniformValue::I1(index as _), ) })) .chain(std::iter::once(( "u_projection_view".into(), GlowUniformValue::M4(self.projection_view_matrix), ))) .collect(), textures: images .into_iter() .filter_map(|id| { Some(( self.assets.textures.get(&id)?.texture.to_owned(), GlowTextureFiltering::Linear, )) }) .collect(), scissor: self.clip_stack.last().copied(), ..Default::default() }), TesselateBatch::ClipPush { x, y, w, h } => { self.clip_stack.push(vek::Rect { x: x as _, y: self.viewport_height - y as i32 - h as i32, w: w as _, h: h as _, }); None } TesselateBatch::ClipPop => { self.clip_stack.pop(); None } TesselateBatch::Debug => Some(GraphicsBatch { shader: self.debug_shader.cloned(), wireframe: true, ..Default::default() }), } } } pub fn setup(app: &mut Application) { app.register_props::("CanvasProps"); app.register_component("canvas", FnWidget::pointer(canvas)); } ================================================ FILE: crates/app/src/render_worker.rs ================================================ use crate::{AssetsManager, Vertex}; use spitfire_glow::{ graphics::{Graphics, Shader, Surface}, renderer::GlowTextureFormat, }; use std::collections::HashMap; pub struct RenderWorkerDescriptor { pub id: String, pub width: u32, pub height: u32, pub format: GlowTextureFormat, pub color: [f32; 4], } #[derive(Default)] pub struct RenderWorkersViewModel { surfaces: HashMap, commands: Vec, } impl RenderWorkersViewModel { pub const VIEW_MODEL: &str = "RenderWorkersViewModel"; pub fn workers(&self) -> impl Iterator { self.surfaces.keys().map(|s| s.as_str()) } pub fn add_worker(&mut self, worker: RenderWorkerDescriptor) { self.commands.push(Command::Create { worker }); } pub fn add_worker_surface(&mut self, id: impl ToString, surface: Surface) { self.commands.push(Command::Add { id: id.to_string(), surface, }); } pub fn remove_worker(&mut self, worker: &str) { self.commands.push(Command::Remove { worker: worker.to_owned(), }); } pub fn schedule_task( &mut self, worker: &str, clear: bool, task: impl FnOnce(RenderWorkerTaskContext) + 'static, ) { self.commands.push(Command::Schedule { worker: worker.to_owned(), clear, task: Box::new(task), }); } pub(crate) fn maintain( &mut self, graphics: &mut Graphics, assets: &mut AssetsManager, colored_shader: &Shader, textured_shader: &Shader, text_shader: &Shader, ) { for command in self.commands.drain(..) { match command { Command::Create { worker } => { let Ok(texture) = graphics.texture(worker.width, worker.height, 1, worker.format, None) else { continue; }; let Ok(mut surface) = graphics.surface(vec![texture.clone().into()]) else { continue; }; surface.set_color(worker.color); self.surfaces.insert(worker.id.clone(), surface); assets.add_texture(worker.id, texture); } Command::Add { id, surface } => { self.surfaces.insert(id, surface); } Command::Remove { worker } => { self.surfaces.remove(&worker); assets.remove_texture(&worker); } Command::Schedule { worker, clear, task, } => { if let Some(surface) = self.surfaces.get(&worker) { let _ = graphics.draw(); let _ = graphics.push_surface(surface.clone()); let _ = graphics.prepare_frame(clear); (task)(RenderWorkerTaskContext { width: surface.width(), height: surface.height(), format: surface.attachments()[0].texture.format(), graphics, colored_shader, textured_shader, text_shader, }); let _ = graphics.draw(); let _ = graphics.pop_surface(); let _ = graphics.prepare_frame(false); } } } } } } pub struct RenderWorkerTaskContext<'a> { pub width: u32, pub height: u32, pub format: GlowTextureFormat, pub graphics: &'a mut Graphics, pub colored_shader: &'a Shader, pub textured_shader: &'a Shader, pub text_shader: &'a Shader, } enum Command { Create { worker: RenderWorkerDescriptor, }, Add { id: String, surface: Surface, }, Remove { worker: String, }, Schedule { worker: String, clear: bool, #[allow(clippy::type_complexity)] task: Box, }, } ================================================ FILE: crates/app/src/text_measurements.rs ================================================ use crate::AssetsManager; use fontdue::layout::{ CoordinateSystem, HorizontalAlign, Layout, LayoutSettings, TextStyle, VerticalAlign, }; use raui_core::{ layout::{CoordsMapping, default_layout_engine::TextMeasurementEngine}, widget::{ unit::text::{TextBox, TextBoxHorizontalAlign, TextBoxSizeValue, TextBoxVerticalAlign}, utils::{Rect, Vec2}, }, }; use raui_tesselate_renderer::*; use spitfire_fontdue::TextRenderer; pub struct AppTextMeasurementsEngine<'a> { pub assets: &'a AssetsManager, } impl TextMeasurementEngine for AppTextMeasurementsEngine<'_> { fn measure_text( &self, size_available: Vec2, mapping: &CoordsMapping, unit: &TextBox, ) -> Option { let font_index = self.assets.font_index_by_id(&unit.font.name)?; let text = TextStyle::with_user_data( &unit.text, unit.font.size * mapping.scalar_scale(false), font_index, unit.color, ); let max_width = match unit.width { TextBoxSizeValue::Content => None, TextBoxSizeValue::Fill => Some(size_available.x), TextBoxSizeValue::Exact(v) => Some(v), }; let max_height = match unit.height { TextBoxSizeValue::Content => None, TextBoxSizeValue::Fill => Some(size_available.y), TextBoxSizeValue::Exact(v) => Some(v), }; let mut layout = Layout::new(CoordinateSystem::PositiveYDown); layout.reset(&LayoutSettings { max_width, max_height, horizontal_align: match unit.horizontal_align { TextBoxHorizontalAlign::Left => HorizontalAlign::Left, TextBoxHorizontalAlign::Center => HorizontalAlign::Center, TextBoxHorizontalAlign::Right => HorizontalAlign::Right, }, vertical_align: match unit.vertical_align { TextBoxVerticalAlign::Top => VerticalAlign::Top, TextBoxVerticalAlign::Middle => VerticalAlign::Middle, TextBoxVerticalAlign::Bottom => VerticalAlign::Bottom, }, ..Default::default() }); layout.append(self.assets.fonts(), &text); let aabb = TextRenderer::measure(&layout, self.assets.fonts(), false); if aabb.iter().all(|v| v.is_finite()) { Some(Rect { left: aabb[0], top: aabb[1], right: aabb[2], bottom: aabb[3], }) } else { None } } } ================================================ FILE: crates/core/Cargo.toml ================================================ [package] name = "raui-core" version = "0.70.17" authors = ["Patryk 'PsichiX' Budzynski "] edition = "2024" description = "RAUI application layer" 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"] [dependencies] raui-derive = { version = "0.70", path = "../derive" } serde = { version = "1", features = ["derive", "rc"] } serde_json = "1" intuicio-data = "0.51" ================================================ FILE: crates/core/src/animator.rs ================================================ //! Animation engine //! //! RAUI widget components can be animated by updating and adding animations using the [`Animator`] //! inside of widget lifecycle hooks and by reading the progress of those animations from the //! [`AnimatorStates`] provided by the [`WidgetContext`]. //! //! See [`Animator`] and [`AnimatorStates`] for code samples. //! //! [`WidgetContext`]: crate::widget::context::WidgetContext use crate::{MessageData, Scalar, messenger::MessageSender, widget::WidgetId}; use serde::{Deserialize, Serialize}; use std::{collections::HashMap, sync::mpsc::Sender}; /// An error that may occur when animating a value pub enum AnimationError { /// Could not read animation data CouldNotReadData, /// Could not write animation data CouldNotWriteData, } /// Handle to an animation sending channel used internally to update widget animations values in /// lifecycle hooks #[derive(Clone)] pub(crate) struct AnimationUpdate(Sender<(String, Option)>); impl AnimationUpdate { pub fn new(sender: Sender<(String, Option)>) -> Self { Self(sender) } pub fn change(&self, name: &str, data: Option) -> Result<(), AnimationError> { if self.0.send((name.to_owned(), data)).is_err() { Err(AnimationError::CouldNotWriteData) } else { Ok(()) } } } /// Allows manipulating widget animations /// /// An [`Animator`] can be used inside of the [`WidgetMountOrChangeContext`] that is provided when /// setting widget lifecycle handlers. /// /// # Animations & Values /// /// The animator can manage any number of different animations identified by a string `anim_id`. /// Additionally each animation can have more than one _value_ that is animated and each of these /// values has a `value_name` that can be used to get the animated value. /// /// [`WidgetMountOrChangeContext`]: crate::widget::context::WidgetMountOrChangeContext pub struct Animator<'a> { states: &'a AnimatorStates, update: AnimationUpdate, } impl<'a> Animator<'a> { /// Create a new [`Animator`] #[inline] pub(crate) fn new(states: &'a AnimatorStates, update: AnimationUpdate) -> Self { Self { states, update } } /// Check whether or not the widget has an animation with the given `anim_id` #[inline] pub fn has(&self, anim_id: &str) -> bool { self.states.has(anim_id) } /// Change the animation associated to a given `anim_id` #[inline] pub fn change( &self, anim_id: &str, animation: Option, ) -> Result<(), AnimationError> { self.update.change(anim_id, animation) } /// Get the current progress of the animation of a given value /// /// This will return [`None`] if the value is not currently being animated. #[inline] pub fn value_progress(&self, anim_id: &str, value_name: &str) -> Option { self.states.value_progress(anim_id, value_name) } /// Get the current progress factor of the animation of a given value /// /// If the value is currently being animated this will return [`Some`] [`Scalar`] between `0` /// and `1` with `0` meaning just started and `1` meaning finished. /// /// If the value is **not** currently being animated [`None`] will be returned #[inline] pub fn value_progress_factor(&self, anim_id: &str, value_name: &str) -> Option { self.states .value_progress(anim_id, value_name) .map(|x| x.progress_factor) } /// Same as [`value_progress_factor`][Self::value_progress_factor] but returning `default` instead of [`None`] #[inline] pub fn value_progress_factor_or( &self, anim_id: &str, value_name: &str, default: Scalar, ) -> Scalar { self.value_progress_factor(anim_id, value_name) .unwrap_or(default) } /// Same as [`value_progress_factor`][Self::value_progress_factor] but returning `0` instead of [`None`] #[inline] pub fn value_progress_factor_or_zero(&self, anim_id: &str, value_name: &str) -> Scalar { self.value_progress_factor(anim_id, value_name) .unwrap_or(0.) } } /// The amount of progress made for a value in an animation #[derive(Debug, Default, Clone, Copy)] pub struct AnimatedValueProgress { /// How far along this animation is from 0 to 1 pub progress_factor: Scalar, /// The amount of time this animation has been running pub time: Scalar, /// The amount of time that this animation will run for pub duration: Scalar, } /// The current state of animations in a component /// /// The [`AnimatorStates`] can be accessed from the [`WidgetContext`] to get information about the /// current state of all component animations. /// /// # Animations & Values /// /// A component may have any number of different animations identified by a string `anim_id`. /// Additionally each animation can have more than one _value_ that is animated and each of these /// values has a `value_name` that can be used to get the animated value. /// /// [`WidgetContext`]: crate::widget::context::WidgetContext #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct AnimatorStates( #[serde(default)] #[serde(skip_serializing_if = "HashMap::is_empty")] pub HashMap, ); impl AnimatorStates { /// Initialize a new [`AnimatorStates`] that contains a single animation pub(crate) fn new(anim_id: String, animation: Animation) -> Self { let mut result = HashMap::with_capacity(1); result.insert(anim_id, AnimatorState::new(animation)); Self(result) } /// Returns whether or not _any_ of the animations for this component are in-progress pub fn in_progress(&self) -> bool { self.0.values().any(|s| s.in_progress()) } /// Returns `true` if none of this component's animations are currently running #[inline] pub fn is_done(&self) -> bool { !self.in_progress() } /// Returns true if the widget has an animation with the given `anim_id` #[inline] pub fn has(&self, anim_id: &str) -> bool { self.0.contains_key(anim_id) } /// Get the current progress of the animation of a given value /// /// This will return [`None`] if the value is not currently being animated. #[inline] pub fn value_progress(&self, anim_id: &str, value_name: &str) -> Option { if let Some(state) = self.0.get(anim_id) { state.value_progress(value_name) } else { None } } /// Get the current progress factor of the animation of a given value /// /// If the value is currently being animated this will return [`Some`] [`Scalar`] between `0` /// and `1` with `0` meaning just started and `1` meaning finished. /// /// If the value is **not** currently being animated [`None`] will be returned #[inline] pub fn value_progress_factor(&self, anim_id: &str, value_name: &str) -> Option { if let Some(state) = self.0.get(anim_id) { state.value_progress_factor(value_name) } else { None } } /// Same as [`value_progress_factor`][Self::value_progress_factor] but returning `default` instead of [`None`] #[inline] pub fn value_progress_factor_or( &self, anim_id: &str, value_name: &str, default: Scalar, ) -> Scalar { self.value_progress_factor(anim_id, value_name) .unwrap_or(default) } /// Same as [`value_progress_factor`][Self::value_progress_factor] but returning `0` instead of [`None`] #[inline] pub fn value_progress_factor_or_zero(&self, anim_id: &str, value_name: &str) -> Scalar { self.value_progress_factor(anim_id, value_name) .unwrap_or(0.) } /// Update the animation with the given `anim_id` /// /// If `animation` is [`None`] the animation will be removed. pub fn change(&mut self, anim_id: String, animation: Option) { if let Some(animation) = animation { self.0.insert(anim_id, AnimatorState::new(animation)); } else { self.0.remove(&anim_id); } } /// Processes the animations, updating the values of each animation baed on the progressed time pub(crate) fn process( &mut self, delta_time: Scalar, owner: &WidgetId, message_sender: &MessageSender, ) { for state in self.0.values_mut() { state.process(delta_time, owner, message_sender); } } } /// The state of a single animation in a component /// /// This is most often accessed though [`AnimatorStates`] in the [`WidgetContext`]. /// /// [`WidgetContext`]: crate::widget::context::WidgetContext #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct AnimatorState { #[serde(default)] #[serde(skip_serializing_if = "HashMap::is_empty")] sheet: HashMap, #[serde(default)] #[serde(skip_serializing_if = "Vec::is_empty")] messages: Vec<(Scalar, String)>, #[serde(default)] time: Scalar, #[serde(default)] duration: Scalar, #[serde(default)] looped: bool, } impl AnimatorState { /// Initialize a new [`AnimatorState`] given an animation pub(crate) fn new(animation: Animation) -> Self { let mut sheet = HashMap::new(); let mut messages = vec![]; let (time, looped) = Self::include_animation(animation, &mut sheet, &mut messages, 0.0); Self { sheet, messages, time: 0.0, duration: time, looped, } } /// Returns whether or not the animations is in-progress #[inline] pub fn in_progress(&self) -> bool { self.looped || (self.time <= self.duration && !self.sheet.is_empty()) } /// Returns `true` if this animation is not in-progress #[inline] pub fn is_done(&self) -> bool { !self.in_progress() } /// Get the current progress of the animation of a given value /// /// This will return [`None`] if the value is not currently being animated. #[inline] pub fn value_progress(&self, name: &str) -> Option { self.sheet.get(name).map(|p| AnimatedValueProgress { progress_factor: p.cached_progress, time: p.cached_time, duration: p.duration, }) } /// Get the current progress factor of the animation of a given value /// /// If the value is currently being animated this will return [`Some`] [`Scalar`] between `0` /// and `1` with `0` meaning just started and `1` meaning finished. /// /// If the value is **not** currently being animated [`None`] will be returned #[inline] pub fn value_progress_factor(&self, name: &str) -> Option { self.sheet.get(name).map(|p| p.cached_progress) } /// Same as [`value_progress_factor`][Self::value_progress_factor] but returning `default` instead of [`None`] #[inline] pub fn value_progress_factor_or(&self, name: &str, default: Scalar) -> Scalar { self.value_progress_factor(name).unwrap_or(default) } /// Same as [`value_progress_factor`][Self::value_progress_factor] but returning `0` instead of [`None`] #[inline] pub fn value_progress_factor_or_zero(&self, name: &str) -> Scalar { self.value_progress_factor(name).unwrap_or(0.) } /// Processes the animations, updating the values of each animation baed on the progressed time pub(crate) fn process( &mut self, delta_time: Scalar, owner: &WidgetId, message_sender: &MessageSender, ) { if delta_time > 0.0 { if self.looped && self.time > self.duration { self.time = 0.0; } let old_time = self.time; self.time += delta_time; for phase in self.sheet.values_mut() { phase.cached_time = (self.time - phase.start).min(phase.duration).max(0.0); phase.cached_progress = if phase.duration > 0.0 { phase.cached_time / phase.duration } else { 0.0 }; } for (time, message) in &self.messages { if *time >= old_time && *time < self.time { message_sender.write(owner.to_owned(), AnimationMessage(message.to_owned())); } } } } // Add an animation to this [`AnimatorState`] recursively fn include_animation( animation: Animation, sheet: &mut HashMap, messages: &mut Vec<(Scalar, String)>, mut time: Scalar, ) -> (Scalar, bool) { match animation { Animation::Value(value) => { let duration = value.duration.max(0.0); let phase = AnimationPhase { start: time, duration, cached_time: 0.0, cached_progress: 0.0, }; sheet.insert(value.name, phase); (time + duration, false) } Animation::Sequence(anims) => { for anim in anims { time = Self::include_animation(anim, sheet, messages, time).0; } (time, false) } Animation::Parallel(anims) => { let mut result = time; for anim in anims { result = Self::include_animation(anim, sheet, messages, time) .0 .max(result); } (result, false) } Animation::Looped(anim) => { let looped = sheet.is_empty(); time = Self::include_animation(*anim, sheet, messages, time).0; (time, looped) } Animation::TimeShift(v) => ((time - v).max(0.0), false), Animation::Message(message) => { messages.push((time, message)); (time, false) } } } } #[derive(Debug, Default, Clone, Serialize, Deserialize)] struct AnimationPhase { #[serde(default)] pub start: Scalar, #[serde(default)] pub duration: Scalar, #[serde(default)] pub cached_time: Scalar, #[serde(default)] pub cached_progress: Scalar, } /// Defines a widget animation /// /// [`Animation`]'s can be added to widget component's [`AnimatorStates`] to animate values. /// /// Creating an [`Animation`] doesn't actually animate a specific value, but instead gives you a way /// to track the _progress_ of an animated value using the /// [`value_progress`][AnimatorStates::value_progress] function. This allows you to use the progress /// to calculate how to interpolate the real values when you build your widget. #[derive(Debug, Clone, Serialize, Deserialize)] pub enum Animation { /// A single animated value with a name and a duration Value(AnimatedValue), /// A sequence of animations that will be run in a row Sequence(Vec), /// A set of animations that will be run at the same time Parallel(Vec), /// An animation that will play in a loop Looped(Box), /// TODO: Document `TimeShift` TimeShift(Scalar), /// Send an [`AnimationMessage`] Message(String), } impl Default for Animation { fn default() -> Self { Self::TimeShift(0.0) } } /// A single, animated value with a name and a duration #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct AnimatedValue { /// The name of the animated value /// /// This is used to get the progress of the animation value with the /// [`value_progress`][AnimatorStates::value_progress] function. #[serde(default)] pub name: String, /// The duration of the animation #[serde(default)] pub duration: Scalar, } /// A [`MessageData`][crate::messenger::MessageData] implementation sent by running an /// [`Animation::Message`] animation #[derive(MessageData, Debug, Default, Clone)] #[message_data(crate::messenger::MessageData)] pub struct AnimationMessage(pub String); #[cfg(test)] mod tests { use super::*; use std::{str::FromStr, sync::mpsc::channel}; #[test] fn test_animator() { let animation = Animation::Sequence(vec![ Animation::Value(AnimatedValue { name: "fade-in".to_owned(), duration: 0.2, }), Animation::Value(AnimatedValue { name: "delay".to_owned(), duration: 0.6, }), Animation::Value(AnimatedValue { name: "fade-out".to_owned(), duration: 0.2, }), Animation::Message("next".to_owned()), ]); println!("Animation: {animation:#?}"); let mut states = AnimatorStates::new("".to_owned(), animation); println!("States 0: {states:#?}"); let id = WidgetId::from_str("type:/widget").unwrap(); let (sender, receiver) = channel(); let sender = MessageSender::new(sender); states.process(0.5, &id, &sender); println!("States 1: {states:#?}"); states.process(0.6, &id, &sender); println!("States 2: {states:#?}"); println!( "Message: {:#?}", receiver .try_recv() .unwrap() .1 .as_any() .downcast_ref::() .unwrap() ); } } ================================================ FILE: crates/core/src/application.rs ================================================ //! Application foundation used to drive the RAUI interface //! //! An [`Application`] is the struct that pulls together all the pieces of a RAUI ui such as layout, //! interaction, animations, etc. //! //! In most cases users will not need to manually create and manage an [`Application`]. That will //! usually be handled by renderer integration crates like [`raui-tesselation-renderer`]. //! //! [`raui-tesselation-renderer`]: https://docs.rs/raui-tesselation-renderer/ //! //! You _will_ need to interact with [`Application`] if you are building your own RAUI integration //! with another renderer or game engine. //! ``` use crate::{ Prefab, PrefabError, PrefabValue, Scalar, animator::{AnimationUpdate, Animator, AnimatorStates}, interactive::InteractionsEngine, layout::{CoordsMapping, Layout, LayoutEngine}, messenger::{Message, MessageData, MessageSender, Messages, Messenger}, props::{Props, PropsData, PropsRegistry}, renderer::Renderer, signals::{Signal, SignalSender}, state::{State, StateChange, StateUpdate}, view_model::{ViewModel, ViewModelCollection, ViewModelCollectionView}, widget::{ FnWidget, WidgetId, WidgetIdCommon, WidgetLifeCycle, component::{ WidgetComponent, WidgetComponentPrefab, containers::responsive_box::MediaQueryViewModel, }, context::{WidgetContext, WidgetMountOrChangeContext, WidgetUnmountContext}, node::{WidgetNode, WidgetNodePrefab}, unit::{ WidgetUnit, WidgetUnitNode, WidgetUnitNodePrefab, area::{AreaBoxNode, AreaBoxNodePrefab}, content::{ ContentBoxItem, ContentBoxItemNode, ContentBoxItemNodePrefab, ContentBoxNode, ContentBoxNodePrefab, }, flex::{ FlexBoxItem, FlexBoxItemNode, FlexBoxItemNodePrefab, FlexBoxNode, FlexBoxNodePrefab, }, grid::{ GridBoxItem, GridBoxItemNode, GridBoxItemNodePrefab, GridBoxNode, GridBoxNodePrefab, }, image::{ImageBoxNode, ImageBoxNodePrefab}, portal::{ PortalBox, PortalBoxNode, PortalBoxNodePrefab, PortalBoxSlot, PortalBoxSlotNode, PortalBoxSlotNodePrefab, }, size::{SizeBoxNode, SizeBoxNodePrefab}, text::{TextBoxNode, TextBoxNodePrefab}, }, }, }; use std::{ borrow::Cow, collections::{HashMap, HashSet}, convert::TryInto, sync::{ Arc, RwLock, mpsc::{Sender, channel}, }, }; /// Errors that can occur while interacting with an application #[derive(Debug, Clone)] pub enum ApplicationError { Prefab(PrefabError), ComponentMappingNotFound(String), } impl From for ApplicationError { fn from(error: PrefabError) -> Self { Self::Prefab(error) } } /// Indicates the reason that an [`Application`] state was invalidated and had to be re-rendered /// /// You can get the last invalidation cause of an application using [`last_invalidation_cause`] /// /// [`last_invalidation_cause`]: Application::last_invalidation_cause #[derive(Debug, Default, Clone)] pub enum InvalidationCause { /// Application not invalidated #[default] None, /// Application update caused by change in widgets common root. CommonRootUpdate(WidgetIdCommon), } #[derive(Clone)] pub struct ChangeNotifier(Arc>>); impl ChangeNotifier { pub fn notify(&self, id: WidgetId) { if let Ok(mut ids) = self.0.write() { ids.insert(id); } } } /// Contains and orchestrates application layout, animations, interactions, etc. /// /// See the [`application`][self] module for more information and examples. pub struct Application { component_mappings: HashMap, props_registry: PropsRegistry, tree: WidgetNode, rendered_tree: WidgetUnit, layout: Layout, states: HashMap, state_changes: HashMap>, animators: HashMap, messages: HashMap, pending_stack: Vec, done_stack: Vec, signals: Vec, pub view_models: ViewModelCollection, changes: ChangeNotifier, #[allow(clippy::type_complexity)] unmount_closures: HashMap>>, dirty: WidgetIdCommon, render_changed: bool, last_invalidation_cause: InvalidationCause, /// The amount of time between the last update, used when calculating animation progress pub animations_delta_time: Scalar, } impl Default for Application { fn default() -> Self { let mut view_models = ViewModelCollection::default(); view_models.insert( MediaQueryViewModel::VIEW_MODEL.to_string(), ViewModel::produce(MediaQueryViewModel::new), ); Self { component_mappings: Default::default(), props_registry: Default::default(), tree: Default::default(), rendered_tree: Default::default(), layout: Default::default(), states: Default::default(), state_changes: Default::default(), animators: Default::default(), messages: Default::default(), pending_stack: Default::default(), done_stack: Default::default(), signals: Default::default(), view_models, changes: ChangeNotifier(Default::default()), unmount_closures: Default::default(), dirty: Default::default(), render_changed: false, last_invalidation_cause: Default::default(), animations_delta_time: 0.0, } } } impl Application { /// Setup the application with a given a setup function /// /// We need to run the `setup` function for the application to register components and /// properties if we want to support serialization of the UI. We pass it a function that will do /// the actual registration. /// /// > **Note:** RAUI will work fine without running any `setup` if UI serialization is not /// > required. #[inline] pub fn setup(&mut self, mut f: F) where F: FnMut(&mut Self), { (f)(self); } pub fn notifier(&self) -> ChangeNotifier { self.changes.clone() } /// Register's a component under a string name used when serializing the UI /// /// This function is often used in [`setup`][Self::setup] functions for registering batches of /// components. #[inline] pub fn register_component(&mut self, type_name: &str, processor: FnWidget) { self.component_mappings .insert(type_name.to_owned(), processor); } /// Unregisters a component /// /// See [`register_component`][Self::register_component] #[inline] pub fn unregister_component(&mut self, type_name: &str) { self.component_mappings.remove(type_name); } /// Register's a property type under a string name used when serializing the UI /// /// This function is often used in [`setup`][Self::setup] functions for registering batches of /// properties. #[inline] pub fn register_props(&mut self, name: &str) where T: 'static + Prefab + PropsData, { self.props_registry.register_factory::(name); } /// Unregisters a property type /// /// See [`register_props`][Self::register_props] #[inline] pub fn unregister_props(&mut self, name: &str) { self.props_registry.unregister_factory(name); } /// Serialize the given [`Props`] to a [`PrefabValue`] #[inline] pub fn serialize_props(&self, props: &Props) -> Result { self.props_registry.serialize(props) } /// Deserialize [`Props`] from a [`PrefabValue`] #[inline] pub fn deserialize_props(&self, data: PrefabValue) -> Result { self.props_registry.deserialize(data) } /// Serialize a [`WidgetNode`] to a [`PrefabValue`] #[inline] pub fn serialize_node(&self, data: &WidgetNode) -> Result { Ok(self.node_to_prefab(data)?.to_prefab()?) } /// Deserialize a [`WidgetNode`] from a [`PrefabValue`] #[inline] pub fn deserialize_node(&self, data: PrefabValue) -> Result { self.node_from_prefab(WidgetNodePrefab::from_prefab(data)?) } /// Get the reason that the application state was last invalidated and caused to re-process #[inline] pub fn last_invalidation_cause(&self) -> &InvalidationCause { &self.last_invalidation_cause } /// Return's common root widget ID of widgets that has to be to be re-processed #[inline] pub fn dirty(&self) -> &WidgetIdCommon { &self.dirty } /// Force mark the application as needing to re-process its root #[inline] pub fn mark_dirty(&mut self) { self.dirty = WidgetIdCommon::new(WidgetId::empty()); } #[inline] pub fn does_render_changed(&self) -> bool { self.render_changed } /// Get the [`WidgetNode`] for the application tree #[inline] pub fn tree(&self) -> &WidgetNode { &self.tree } /// Get the application widget tree rendered to raw [`WidgetUnit`]'s #[inline] pub fn rendered_tree(&self) -> &WidgetUnit { &self.rendered_tree } /// Get the application [`Layout`] data #[inline] pub fn layout_data(&self) -> &Layout { &self.layout } #[inline] pub fn has_layout_widget(&self, id: &WidgetId) -> bool { self.layout.items.keys().any(|k| k == id) } /// Update the application widget tree #[inline] pub fn apply(&mut self, tree: impl Into) { self.mark_dirty(); self.tree = tree.into(); } /// Render the application #[inline] pub fn render(&self, mapping: &CoordsMapping, renderer: &mut R) -> Result where R: Renderer, { renderer.render(&self.rendered_tree, mapping, &self.layout) } /// Render the application, but only if something effecting the rendering has changed and it /// _needs_ to be re-rendered #[inline] pub fn render_change( &mut self, mapping: &CoordsMapping, renderer: &mut R, ) -> Result, E> where R: Renderer, { if self.render_changed { Ok(Some(self.render(mapping, renderer)?)) } else { Ok(None) } } /// Calculate application layout #[inline] pub fn layout(&mut self, mapping: &CoordsMapping, layout_engine: &mut L) -> Result<(), E> where L: LayoutEngine, { self.layout = layout_engine.layout(mapping, &self.rendered_tree)?; if let Some(view_model) = self.view_models.get_mut(MediaQueryViewModel::VIEW_MODEL) && let Some(mut view_model) = view_model.write::() { view_model .screen_size .set_unique_notify(self.layout.ui_space.size()); } Ok(()) } /// Calculate application layout, but only if something effecting application layout has changed /// and the layout _needs_ to be re-done #[inline] pub fn layout_change( &mut self, mapping: &CoordsMapping, layout_engine: &mut L, ) -> Result where L: LayoutEngine, { if self.render_changed { self.layout(mapping, layout_engine)?; Ok(true) } else { Ok(false) } } /// Perform interactions on the application using the given interaction engine #[inline] pub fn interact(&mut self, interactions_engine: &mut I) -> Result where I: InteractionsEngine, { interactions_engine.perform_interactions(self) } /// Send a message to the given widget #[inline] pub fn send_message(&mut self, id: &WidgetId, data: T) where T: 'static + MessageData, { self.send_message_raw(id, Box::new(data)); } /// Send raw message data to the given widget #[inline] pub fn send_message_raw(&mut self, id: &WidgetId, data: Message) { if let Some(list) = self.messages.get_mut(id) { list.push(data); } else { self.messages.insert(id.to_owned(), vec![data]); } } /// Get the list of [signals][crate::signals] that have been sent by widgets #[inline] pub fn signals(&self) -> &[Signal] { &self.signals } /// Get the list of [signals][crate::signals] that have been sent by widgets, consuming the /// current list so that further calls will not include previously sent signals #[inline] pub fn consume_signals(&mut self) -> Vec { std::mem::take(&mut self.signals) } /// [`process()`][Self::process] application, even if no changes have been detected #[inline] pub fn forced_process(&mut self) -> bool { self.mark_dirty(); self.process() } /// [Process][Self::process] the application. pub fn process(&mut self) -> bool { self.dirty .include_other(&self.view_models.consume_notified_common_root()); if let Ok(mut ids) = self.changes.0.write() { for id in ids.drain() { self.dirty.include(&id); } } self.animations_delta_time = self.animations_delta_time.max(0.0); self.last_invalidation_cause = InvalidationCause::None; self.render_changed = false; let changed_states = std::mem::take(&mut self.state_changes); for id in changed_states.keys() { self.dirty.include(id); } let mut messages = std::mem::take(&mut self.messages); for id in messages.keys() { self.dirty.include(id); } for (id, animator) in &self.animators { if animator.in_progress() { self.dirty.include(id); } } if !self.dirty.is_valid() { return false; } self.last_invalidation_cause = InvalidationCause::CommonRootUpdate(self.dirty.to_owned()); let (message_sender, message_receiver) = channel(); let message_sender = MessageSender::new(message_sender); for (k, a) in &mut self.animators { a.process(self.animations_delta_time, k, &message_sender); } let mut states = std::mem::take(&mut self.states); for (id, changes) in changed_states { let state = states.entry(id).or_default(); for change in changes { match change { StateChange::Set(props) => { *state = props; } StateChange::Include(props) => { state.merge_from(props); } StateChange::Exclude(type_id) => unsafe { state.remove_by_type(type_id); }, } } } let (signal_sender, signal_receiver) = channel(); let tree = self.tree.clone(); let mut used_ids = HashSet::new(); let mut new_states = HashMap::new(); let rendered_tree = self.process_nodes_stack( tree, &states, &mut messages, &mut new_states, &mut used_ids, &message_sender, &signal_sender, ); self.states = states .into_iter() .chain(new_states) .filter(|(id, state)| { if used_ids.contains(id) { true } else { if let Some(closures) = self.unmount_closures.remove(id) { for mut closure in closures { let messenger = &message_sender; let signals = SignalSender::new(id.clone(), signal_sender.clone()); let view_models = ViewModelCollectionView::new(id, &mut self.view_models); let context = WidgetUnmountContext { id, state, messenger, signals, view_models, }; (closure)(context); } } self.animators.remove(id); self.view_models.unbind_all(id); self.view_models.remove_widget_view_models(id); false } }) .collect(); while let Ok((id, message)) = message_receiver.try_recv() { if let Some(list) = self.messages.get_mut(&id) { list.push(message); } else { self.messages.insert(id, vec![message]); } } self.signals.clear(); while let Ok(data) = signal_receiver.try_recv() { self.signals.push(data); } self.animators = std::mem::take(&mut self.animators) .into_iter() .filter_map(|(k, a)| if a.in_progress() { Some((k, a)) } else { None }) .collect(); self.dirty = Default::default(); if let Ok(tree) = rendered_tree.try_into() { self.rendered_tree = Self::teleport_portals(tree); true } else { false } } #[allow(clippy::too_many_arguments)] fn process_nodes_stack( &mut self, root_node: WidgetNode, states: &HashMap, messages: &mut HashMap, new_states: &mut HashMap, used_ids: &mut HashSet, message_sender: &MessageSender, signal_sender: &Sender, ) -> WidgetNode { self.pending_stack.clear(); self.pending_stack.push(WidgetStackItem::Node { node: root_node, path: vec![], possible_key: "<*>".to_string(), master_shared_props: None, }); self.done_stack.clear(); while let Some(item) = self.pending_stack.pop() { match item { WidgetStackItem::Node { node, mut path, possible_key, master_shared_props, } => match node { WidgetNode::None | WidgetNode::Tuple(_) => { self.done_stack.push(node); } WidgetNode::Component(component) => { let WidgetComponent { processor, type_name, key, mut idref, mut props, shared_props, listed_slots, named_slots, } = component; let mut shared_props = match (master_shared_props, shared_props) { (Some(master_shared_props), Some(shared_props)) => { master_shared_props.merge(shared_props) } (None, Some(shared_props)) => shared_props, (Some(master_shared_props), None) => master_shared_props, _ => Default::default(), }; let key = match &key { Some(key) => key.to_owned(), None => possible_key.to_owned(), }; path.push(key.clone().into()); let id = WidgetId::new(&type_name, &path); used_ids.insert(id.clone()); if let Some(idref) = &mut idref { idref.write(id.to_owned()); } let (state_sender, state_receiver) = channel(); let (animation_sender, animation_receiver) = channel(); let messages_list = messages.remove(&id).unwrap_or_default(); let mut life_cycle = WidgetLifeCycle::default(); let default_animator_state = AnimatorStates::default(); let (new_node, mounted) = match states.get(&id) { Some(state) => { let state = State::new(state, StateUpdate::new(state_sender.clone())); let animator = self.animators.get(&id).unwrap_or(&default_animator_state); let view_models = ViewModelCollectionView::new(&id, &mut self.view_models); let context = WidgetContext { id: &id, idref: idref.as_ref(), key: &key, props: &mut props, shared_props: &mut shared_props, state, animator, life_cycle: &mut life_cycle, named_slots, listed_slots, view_models, }; (processor.call(context), false) } None => { let state_data = Props::default(); let state = State::new(&state_data, StateUpdate::new(state_sender.clone())); let animator = self.animators.get(&id).unwrap_or(&default_animator_state); let view_models = ViewModelCollectionView::new(&id, &mut self.view_models); let context = WidgetContext { id: &id, idref: idref.as_ref(), key: &key, props: &mut props, shared_props: &mut shared_props, state, animator, life_cycle: &mut life_cycle, named_slots, listed_slots, view_models, }; let node = processor.call(context); new_states.insert(id.clone(), state_data); (node, true) } }; let (mount, change, unmount) = life_cycle.unwrap(); if mounted { if !mount.is_empty() && let Some(state) = new_states.get(&id) { for mut closure in mount { let state = State::new(state, StateUpdate::new(state_sender.clone())); let messenger = Messenger::new(message_sender.clone(), &messages_list); let signals = SignalSender::new(id.clone(), signal_sender.clone()); let animator = Animator::new( self.animators.get(&id).unwrap_or(&default_animator_state), AnimationUpdate::new(animation_sender.clone()), ); let view_models = ViewModelCollectionView::new(&id, &mut self.view_models); let context = WidgetMountOrChangeContext { id: &id, props: &props, shared_props: &shared_props, state, messenger, signals, animator, view_models, }; (closure)(context); } } } else if !change.is_empty() && let Some(state) = states.get(&id) { for mut closure in change { let state = State::new(state, StateUpdate::new(state_sender.clone())); let messenger = Messenger::new(message_sender.clone(), &messages_list); let signals = SignalSender::new(id.clone(), signal_sender.clone()); let animator = Animator::new( self.animators.get(&id).unwrap_or(&default_animator_state), AnimationUpdate::new(animation_sender.clone()), ); let view_models = ViewModelCollectionView::new(&id, &mut self.view_models); let context = WidgetMountOrChangeContext { id: &id, props: &props, shared_props: &shared_props, state, messenger, signals, animator, view_models, }; (closure)(context); } } if !unmount.is_empty() { self.unmount_closures.insert(id.clone(), unmount); } while let Ok((name, data)) = animation_receiver.try_recv() { if let Some(states) = self.animators.get_mut(&id) { states.change(name, data); } else if let Some(data) = data { self.animators .insert(id.to_owned(), AnimatorStates::new(name, data)); } } while let Ok(data) = state_receiver.try_recv() { self.state_changes .entry(id.to_owned()) .or_default() .push(data); } self.pending_stack.push(WidgetStackItem::Node { node: new_node, path, possible_key, master_shared_props: Some(shared_props), }); } WidgetNode::Unit(unit) => match unit { WidgetUnitNode::None | WidgetUnitNode::ImageBox(_) | WidgetUnitNode::TextBox(_) => { self.done_stack.push(WidgetNode::Unit(unit)); } WidgetUnitNode::AreaBox(mut unit) => { let slot = *std::mem::take(&mut unit.slot); self.pending_stack .push(WidgetStackItem::AreaBox { node: unit }); self.pending_stack.push(WidgetStackItem::Node { node: slot, path, possible_key: ".".to_owned(), master_shared_props, }); } WidgetUnitNode::PortalBox(mut unit) => match &mut *unit.slot { PortalBoxSlotNode::Slot(data) => { let slot = std::mem::take(data); self.pending_stack .push(WidgetStackItem::PortalBox { node: unit }); self.pending_stack.push(WidgetStackItem::Node { node: slot, path, possible_key: ".".to_owned(), master_shared_props, }); } PortalBoxSlotNode::ContentItem(item) => { let slot = std::mem::take(&mut item.slot); self.pending_stack .push(WidgetStackItem::PortalBox { node: unit }); self.pending_stack.push(WidgetStackItem::Node { node: slot, path, possible_key: ".".to_owned(), master_shared_props, }); } PortalBoxSlotNode::FlexItem(item) => { let slot = std::mem::take(&mut item.slot); self.pending_stack .push(WidgetStackItem::PortalBox { node: unit }); self.pending_stack.push(WidgetStackItem::Node { node: slot, path, possible_key: ".".to_owned(), master_shared_props, }); } PortalBoxSlotNode::GridItem(item) => { let slot = std::mem::take(&mut item.slot); self.pending_stack .push(WidgetStackItem::PortalBox { node: unit }); self.pending_stack.push(WidgetStackItem::Node { node: slot, path, possible_key: ".".to_owned(), master_shared_props, }); } }, WidgetUnitNode::ContentBox(mut unit) => { let items = unit .items .iter_mut() .map(|node| std::mem::take(&mut node.slot)) .collect::>(); self.pending_stack .push(WidgetStackItem::ContentBox { node: unit }); for (index, node) in items.into_iter().enumerate() { self.pending_stack.push(WidgetStackItem::Node { node, path: path.clone(), possible_key: format!("<{index}>"), master_shared_props: master_shared_props.clone(), }); } } WidgetUnitNode::FlexBox(mut unit) => { let items = unit .items .iter_mut() .map(|node| std::mem::take(&mut node.slot)) .collect::>(); self.pending_stack .push(WidgetStackItem::FlexBox { node: unit }); for (index, node) in items.into_iter().enumerate() { self.pending_stack.push(WidgetStackItem::Node { node, path: path.clone(), possible_key: format!("<{index}>"), master_shared_props: master_shared_props.clone(), }); } } WidgetUnitNode::GridBox(mut unit) => { let items = unit .items .iter_mut() .map(|node| std::mem::take(&mut node.slot)) .collect::>(); self.pending_stack .push(WidgetStackItem::GridBox { node: unit }); for (index, node) in items.into_iter().enumerate() { self.pending_stack.push(WidgetStackItem::Node { node, path: path.clone(), possible_key: format!("<{index}>"), master_shared_props: master_shared_props.clone(), }); } } WidgetUnitNode::SizeBox(mut unit) => { let slot = *std::mem::take(&mut unit.slot); self.pending_stack .push(WidgetStackItem::SizeBox { node: unit }); self.pending_stack.push(WidgetStackItem::Node { node: slot, path, possible_key: ".".to_owned(), master_shared_props, }); } }, }, WidgetStackItem::AreaBox { mut node } => { node.slot = Box::new(self.done_stack.pop().unwrap_or_default()); self.done_stack .push(WidgetNode::Unit(WidgetUnitNode::AreaBox(node))); } WidgetStackItem::PortalBox { mut node } => { match &mut *node.slot { PortalBoxSlotNode::Slot(node) => { *node = self.done_stack.pop().unwrap_or_default(); } PortalBoxSlotNode::ContentItem(node) => { node.slot = self.done_stack.pop().unwrap_or_default(); } PortalBoxSlotNode::FlexItem(node) => { node.slot = self.done_stack.pop().unwrap_or_default(); } PortalBoxSlotNode::GridItem(node) => { node.slot = self.done_stack.pop().unwrap_or_default(); } } self.done_stack .push(WidgetNode::Unit(WidgetUnitNode::PortalBox(node))); } WidgetStackItem::ContentBox { mut node } => { for item in node.items.iter_mut() { item.slot = self.done_stack.pop().unwrap_or_default(); } self.done_stack .push(WidgetNode::Unit(WidgetUnitNode::ContentBox(node))); } WidgetStackItem::FlexBox { mut node } => { for item in node.items.iter_mut() { item.slot = self.done_stack.pop().unwrap_or_default(); } self.done_stack .push(WidgetNode::Unit(WidgetUnitNode::FlexBox(node))); } WidgetStackItem::GridBox { mut node } => { for item in node.items.iter_mut() { item.slot = self.done_stack.pop().unwrap_or_default(); } self.done_stack .push(WidgetNode::Unit(WidgetUnitNode::GridBox(node))); } WidgetStackItem::SizeBox { mut node } => { node.slot = Box::new(self.done_stack.pop().unwrap_or_default()); self.done_stack .push(WidgetNode::Unit(WidgetUnitNode::SizeBox(node))); } } } assert!( self.pending_stack.is_empty(), "Pending stack should be empty after processing" ); assert_eq!( self.done_stack.len(), 1, "Done stack should have exactly one item after processing" ); self.done_stack.pop().unwrap_or_default() } fn teleport_portals(mut root: WidgetUnit) -> WidgetUnit { let count = Self::estimate_portals(&root); if count == 0 { return root; } let mut portals = Vec::with_capacity(count); Self::consume_portals(&mut root, &mut portals); Self::inject_portals(&mut root, &mut portals); root } fn estimate_portals(unit: &WidgetUnit) -> usize { let mut count = 0; match unit { WidgetUnit::None | WidgetUnit::ImageBox(_) | WidgetUnit::TextBox(_) => {} WidgetUnit::AreaBox(b) => count += Self::estimate_portals(&b.slot), WidgetUnit::PortalBox(b) => { count += Self::estimate_portals(match &*b.slot { PortalBoxSlot::Slot(slot) => slot, PortalBoxSlot::ContentItem(item) => &item.slot, PortalBoxSlot::FlexItem(item) => &item.slot, PortalBoxSlot::GridItem(item) => &item.slot, }) + 1 } WidgetUnit::ContentBox(b) => { for item in &b.items { count += Self::estimate_portals(&item.slot); } } WidgetUnit::FlexBox(b) => { for item in &b.items { count += Self::estimate_portals(&item.slot); } } WidgetUnit::GridBox(b) => { for item in &b.items { count += Self::estimate_portals(&item.slot); } } WidgetUnit::SizeBox(b) => count += Self::estimate_portals(&b.slot), } count } fn consume_portals(unit: &mut WidgetUnit, bucket: &mut Vec<(WidgetId, PortalBoxSlot)>) { match unit { WidgetUnit::None | WidgetUnit::ImageBox(_) | WidgetUnit::TextBox(_) => {} WidgetUnit::AreaBox(b) => Self::consume_portals(&mut b.slot, bucket), WidgetUnit::PortalBox(b) => { let PortalBox { owner, mut slot, .. } = std::mem::take(b); Self::consume_portals( match &mut *slot { PortalBoxSlot::Slot(slot) => slot, PortalBoxSlot::ContentItem(item) => &mut item.slot, PortalBoxSlot::FlexItem(item) => &mut item.slot, PortalBoxSlot::GridItem(item) => &mut item.slot, }, bucket, ); bucket.push((owner, *slot)); } WidgetUnit::ContentBox(b) => { for item in &mut b.items { Self::consume_portals(&mut item.slot, bucket); } } WidgetUnit::FlexBox(b) => { for item in &mut b.items { Self::consume_portals(&mut item.slot, bucket); } } WidgetUnit::GridBox(b) => { for item in &mut b.items { Self::consume_portals(&mut item.slot, bucket); } } WidgetUnit::SizeBox(b) => Self::consume_portals(&mut b.slot, bucket), } } fn inject_portals(unit: &mut WidgetUnit, portals: &mut Vec<(WidgetId, PortalBoxSlot)>) -> bool { if portals.is_empty() { return false; } while let Some(data) = unit.as_data() { let found = portals.iter().position(|(id, _)| data.id() == id); if let Some(index) = found { let slot = portals.swap_remove(index).1; match unit { WidgetUnit::None | WidgetUnit::PortalBox(_) | WidgetUnit::ImageBox(_) | WidgetUnit::TextBox(_) => {} WidgetUnit::AreaBox(b) => { match slot { PortalBoxSlot::Slot(slot) => *b.slot = slot, PortalBoxSlot::ContentItem(item) => *b.slot = item.slot, PortalBoxSlot::FlexItem(item) => *b.slot = item.slot, PortalBoxSlot::GridItem(item) => *b.slot = item.slot, } if !Self::inject_portals(&mut b.slot, portals) { return false; } } WidgetUnit::ContentBox(b) => { b.items.push(match slot { PortalBoxSlot::Slot(slot) => ContentBoxItem { slot, ..Default::default() }, PortalBoxSlot::ContentItem(item) => item, PortalBoxSlot::FlexItem(item) => ContentBoxItem { slot: item.slot, ..Default::default() }, PortalBoxSlot::GridItem(item) => ContentBoxItem { slot: item.slot, ..Default::default() }, }); for item in &mut b.items { if !Self::inject_portals(&mut item.slot, portals) { return false; } } } WidgetUnit::FlexBox(b) => { b.items.push(match slot { PortalBoxSlot::Slot(slot) => FlexBoxItem { slot, ..Default::default() }, PortalBoxSlot::ContentItem(item) => FlexBoxItem { slot: item.slot, ..Default::default() }, PortalBoxSlot::FlexItem(item) => item, PortalBoxSlot::GridItem(item) => FlexBoxItem { slot: item.slot, ..Default::default() }, }); for item in &mut b.items { if !Self::inject_portals(&mut item.slot, portals) { return false; } } } WidgetUnit::GridBox(b) => { b.items.push(match slot { PortalBoxSlot::Slot(slot) => GridBoxItem { slot, ..Default::default() }, PortalBoxSlot::ContentItem(item) => GridBoxItem { slot: item.slot, ..Default::default() }, PortalBoxSlot::FlexItem(item) => GridBoxItem { slot: item.slot, ..Default::default() }, PortalBoxSlot::GridItem(item) => item, }); for item in &mut b.items { if !Self::inject_portals(&mut item.slot, portals) { return false; } } } WidgetUnit::SizeBox(b) => { match slot { PortalBoxSlot::Slot(slot) => *b.slot = slot, PortalBoxSlot::ContentItem(item) => *b.slot = item.slot, PortalBoxSlot::FlexItem(item) => *b.slot = item.slot, PortalBoxSlot::GridItem(item) => *b.slot = item.slot, } if !Self::inject_portals(&mut b.slot, portals) { return false; } } } } else { break; } } true } fn node_to_prefab(&self, data: &WidgetNode) -> Result { Ok(match data { WidgetNode::None => WidgetNodePrefab::None, WidgetNode::Component(data) => { WidgetNodePrefab::Component(self.component_to_prefab(data)?) } WidgetNode::Unit(data) => WidgetNodePrefab::Unit(self.unit_to_prefab(data)?), WidgetNode::Tuple(data) => WidgetNodePrefab::Tuple(self.tuple_to_prefab(data)?), }) } fn component_to_prefab( &self, data: &WidgetComponent, ) -> Result { if self.component_mappings.contains_key(&data.type_name) { Ok(WidgetComponentPrefab { type_name: data.type_name.to_owned(), key: data.key.clone(), props: self.props_registry.serialize(&data.props)?, shared_props: match &data.shared_props { Some(p) => Some(self.props_registry.serialize(p)?), None => None, }, listed_slots: data .listed_slots .iter() .map(|v| self.node_to_prefab(v)) .collect::>()?, named_slots: data .named_slots .iter() .map(|(k, v)| Ok((k.to_owned(), self.node_to_prefab(v)?))) .collect::>()?, }) } else { Err(ApplicationError::ComponentMappingNotFound( data.type_name.to_owned(), )) } } fn unit_to_prefab( &self, data: &WidgetUnitNode, ) -> Result { Ok(match data { WidgetUnitNode::None => WidgetUnitNodePrefab::None, WidgetUnitNode::AreaBox(data) => { WidgetUnitNodePrefab::AreaBox(self.area_box_to_prefab(data)?) } WidgetUnitNode::PortalBox(data) => { WidgetUnitNodePrefab::PortalBox(self.portal_box_to_prefab(data)?) } WidgetUnitNode::ContentBox(data) => { WidgetUnitNodePrefab::ContentBox(self.content_box_to_prefab(data)?) } WidgetUnitNode::FlexBox(data) => { WidgetUnitNodePrefab::FlexBox(self.flex_box_to_prefab(data)?) } WidgetUnitNode::GridBox(data) => { WidgetUnitNodePrefab::GridBox(self.grid_box_to_prefab(data)?) } WidgetUnitNode::SizeBox(data) => { WidgetUnitNodePrefab::SizeBox(self.size_box_to_prefab(data)?) } WidgetUnitNode::ImageBox(data) => { WidgetUnitNodePrefab::ImageBox(self.image_box_to_prefab(data)?) } WidgetUnitNode::TextBox(data) => { WidgetUnitNodePrefab::TextBox(self.text_box_to_prefab(data)?) } }) } fn tuple_to_prefab( &self, data: &[WidgetNode], ) -> Result, ApplicationError> { data.iter() .map(|node| self.node_to_prefab(node)) .collect::>() } fn area_box_to_prefab( &self, data: &AreaBoxNode, ) -> Result { Ok(AreaBoxNodePrefab { id: data.id.to_owned(), slot: Box::new(self.node_to_prefab(&data.slot)?), }) } fn portal_box_to_prefab( &self, data: &PortalBoxNode, ) -> Result { Ok(PortalBoxNodePrefab { id: data.id.to_owned(), slot: Box::new(match &*data.slot { PortalBoxSlotNode::Slot(slot) => { PortalBoxSlotNodePrefab::Slot(self.node_to_prefab(slot)?) } PortalBoxSlotNode::ContentItem(item) => { PortalBoxSlotNodePrefab::ContentItem(ContentBoxItemNodePrefab { slot: self.node_to_prefab(&item.slot)?, layout: item.layout.clone(), }) } PortalBoxSlotNode::FlexItem(item) => { PortalBoxSlotNodePrefab::FlexItem(FlexBoxItemNodePrefab { slot: self.node_to_prefab(&item.slot)?, layout: item.layout.clone(), }) } PortalBoxSlotNode::GridItem(item) => { PortalBoxSlotNodePrefab::GridItem(GridBoxItemNodePrefab { slot: self.node_to_prefab(&item.slot)?, layout: item.layout.clone(), }) } }), owner: data.owner.to_owned(), }) } fn content_box_to_prefab( &self, data: &ContentBoxNode, ) -> Result { Ok(ContentBoxNodePrefab { id: data.id.to_owned(), props: self.props_registry.serialize(&data.props)?, items: data .items .iter() .map(|v| { Ok(ContentBoxItemNodePrefab { slot: self.node_to_prefab(&v.slot)?, layout: v.layout.clone(), }) }) .collect::>()?, clipping: data.clipping, content_reposition: data.content_reposition, transform: data.transform, }) } fn flex_box_to_prefab( &self, data: &FlexBoxNode, ) -> Result { Ok(FlexBoxNodePrefab { id: data.id.to_owned(), props: self.props_registry.serialize(&data.props)?, items: data .items .iter() .map(|v| { Ok(FlexBoxItemNodePrefab { slot: self.node_to_prefab(&v.slot)?, layout: v.layout.clone(), }) }) .collect::>()?, direction: data.direction, separation: data.separation, wrap: data.wrap, transform: data.transform, }) } fn grid_box_to_prefab( &self, data: &GridBoxNode, ) -> Result { Ok(GridBoxNodePrefab { id: data.id.to_owned(), props: self.props_registry.serialize(&data.props)?, items: data .items .iter() .map(|v| { Ok(GridBoxItemNodePrefab { slot: self.node_to_prefab(&v.slot)?, layout: v.layout.clone(), }) }) .collect::>()?, cols: data.cols, rows: data.rows, transform: data.transform, }) } fn size_box_to_prefab( &self, data: &SizeBoxNode, ) -> Result { Ok(SizeBoxNodePrefab { id: data.id.to_owned(), props: self.props_registry.serialize(&data.props)?, slot: Box::new(self.node_to_prefab(&data.slot)?), width: data.width, height: data.height, margin: data.margin, keep_aspect_ratio: data.keep_aspect_ratio, transform: data.transform, }) } fn image_box_to_prefab( &self, data: &ImageBoxNode, ) -> Result { Ok(ImageBoxNodePrefab { id: data.id.to_owned(), props: self.props_registry.serialize(&data.props)?, width: data.width, height: data.height, content_keep_aspect_ratio: data.content_keep_aspect_ratio, material: data.material.clone(), transform: data.transform, }) } fn text_box_to_prefab( &self, data: &TextBoxNode, ) -> Result { Ok(TextBoxNodePrefab { id: data.id.to_owned(), props: self.props_registry.serialize(&data.props)?, text: data.text.clone(), width: data.width, height: data.height, horizontal_align: data.horizontal_align, vertical_align: data.vertical_align, direction: data.direction, font: data.font.clone(), color: data.color, transform: data.transform, }) } fn node_from_prefab(&self, data: WidgetNodePrefab) -> Result { Ok(match data { WidgetNodePrefab::None => WidgetNode::None, WidgetNodePrefab::Component(data) => { WidgetNode::Component(self.component_from_prefab(data)?) } WidgetNodePrefab::Unit(data) => WidgetNode::Unit(self.unit_from_prefab(data)?), WidgetNodePrefab::Tuple(data) => WidgetNode::Tuple(self.tuple_from_prefab(data)?), }) } fn component_from_prefab( &self, data: WidgetComponentPrefab, ) -> Result { if let Some(processor) = self.component_mappings.get(&data.type_name) { Ok(WidgetComponent { processor: processor.clone(), type_name: data.type_name, key: data.key, idref: Default::default(), props: self.deserialize_props(data.props)?, shared_props: match data.shared_props { Some(p) => Some(self.deserialize_props(p)?), None => None, }, listed_slots: data .listed_slots .into_iter() .map(|v| self.node_from_prefab(v)) .collect::>()?, named_slots: data .named_slots .into_iter() .map(|(k, v)| Ok((k, self.node_from_prefab(v)?))) .collect::>()?, }) } else { Err(ApplicationError::ComponentMappingNotFound( data.type_name.clone(), )) } } fn unit_from_prefab( &self, data: WidgetUnitNodePrefab, ) -> Result { Ok(match data { WidgetUnitNodePrefab::None => WidgetUnitNode::None, WidgetUnitNodePrefab::AreaBox(data) => { WidgetUnitNode::AreaBox(self.area_box_from_prefab(data)?) } WidgetUnitNodePrefab::PortalBox(data) => { WidgetUnitNode::PortalBox(self.portal_box_from_prefab(data)?) } WidgetUnitNodePrefab::ContentBox(data) => { WidgetUnitNode::ContentBox(self.content_box_from_prefab(data)?) } WidgetUnitNodePrefab::FlexBox(data) => { WidgetUnitNode::FlexBox(self.flex_box_from_prefab(data)?) } WidgetUnitNodePrefab::GridBox(data) => { WidgetUnitNode::GridBox(self.grid_box_from_prefab(data)?) } WidgetUnitNodePrefab::SizeBox(data) => { WidgetUnitNode::SizeBox(self.size_box_from_prefab(data)?) } WidgetUnitNodePrefab::ImageBox(data) => { WidgetUnitNode::ImageBox(self.image_box_from_prefab(data)?) } WidgetUnitNodePrefab::TextBox(data) => { WidgetUnitNode::TextBox(self.text_box_from_prefab(data)?) } }) } fn tuple_from_prefab( &self, data: Vec, ) -> Result, ApplicationError> { data.into_iter() .map(|data| self.node_from_prefab(data)) .collect::>() } fn area_box_from_prefab( &self, data: AreaBoxNodePrefab, ) -> Result { Ok(AreaBoxNode { id: data.id, slot: Box::new(self.node_from_prefab(*data.slot)?), }) } fn portal_box_from_prefab( &self, data: PortalBoxNodePrefab, ) -> Result { Ok(PortalBoxNode { id: data.id, slot: Box::new(match *data.slot { PortalBoxSlotNodePrefab::Slot(slot) => { PortalBoxSlotNode::Slot(self.node_from_prefab(slot)?) } PortalBoxSlotNodePrefab::ContentItem(item) => { PortalBoxSlotNode::ContentItem(ContentBoxItemNode { slot: self.node_from_prefab(item.slot)?, layout: item.layout, }) } PortalBoxSlotNodePrefab::FlexItem(item) => { PortalBoxSlotNode::FlexItem(FlexBoxItemNode { slot: self.node_from_prefab(item.slot)?, layout: item.layout, }) } PortalBoxSlotNodePrefab::GridItem(item) => { PortalBoxSlotNode::GridItem(GridBoxItemNode { slot: self.node_from_prefab(item.slot)?, layout: item.layout, }) } }), owner: data.owner, }) } fn content_box_from_prefab( &self, data: ContentBoxNodePrefab, ) -> Result { Ok(ContentBoxNode { id: data.id, props: self.props_registry.deserialize(data.props)?, items: data .items .into_iter() .map(|v| { Ok(ContentBoxItemNode { slot: self.node_from_prefab(v.slot)?, layout: v.layout, }) }) .collect::>()?, clipping: data.clipping, content_reposition: data.content_reposition, transform: data.transform, }) } fn flex_box_from_prefab( &self, data: FlexBoxNodePrefab, ) -> Result { Ok(FlexBoxNode { id: data.id, props: self.props_registry.deserialize(data.props)?, items: data .items .into_iter() .map(|v| { Ok(FlexBoxItemNode { slot: self.node_from_prefab(v.slot)?, layout: v.layout, }) }) .collect::>()?, direction: data.direction, separation: data.separation, wrap: data.wrap, transform: data.transform, }) } fn grid_box_from_prefab( &self, data: GridBoxNodePrefab, ) -> Result { Ok(GridBoxNode { id: data.id, props: self.props_registry.deserialize(data.props)?, items: data .items .into_iter() .map(|v| { Ok(GridBoxItemNode { slot: self.node_from_prefab(v.slot)?, layout: v.layout, }) }) .collect::>()?, cols: data.cols, rows: data.rows, transform: data.transform, }) } fn size_box_from_prefab( &self, data: SizeBoxNodePrefab, ) -> Result { Ok(SizeBoxNode { id: data.id, props: self.props_registry.deserialize(data.props)?, slot: Box::new(self.node_from_prefab(*data.slot)?), width: data.width, height: data.height, margin: data.margin, keep_aspect_ratio: data.keep_aspect_ratio, transform: data.transform, }) } fn image_box_from_prefab( &self, data: ImageBoxNodePrefab, ) -> Result { Ok(ImageBoxNode { id: data.id, props: self.props_registry.deserialize(data.props)?, width: data.width, height: data.height, content_keep_aspect_ratio: data.content_keep_aspect_ratio, material: data.material, transform: data.transform, }) } fn text_box_from_prefab( &self, data: TextBoxNodePrefab, ) -> Result { Ok(TextBoxNode { id: data.id, props: self.props_registry.deserialize(data.props)?, text: data.text, width: data.width, height: data.height, horizontal_align: data.horizontal_align, vertical_align: data.vertical_align, direction: data.direction, font: data.font, color: data.color, transform: data.transform, }) } } #[allow(clippy::large_enum_variant)] enum WidgetStackItem { Node { node: WidgetNode, path: Vec>, possible_key: String, master_shared_props: Option, }, AreaBox { node: AreaBoxNode, }, PortalBox { node: PortalBoxNode, }, ContentBox { node: ContentBoxNode, }, FlexBox { node: FlexBoxNode, }, GridBox { node: GridBoxNode, }, SizeBox { node: SizeBoxNode, }, } ================================================ FILE: crates/core/src/interactive/default_interactions_engine.rs ================================================ use crate::{ Scalar, application::Application, interactive::InteractionsEngine, messenger::MessageData, widget::{ WidgetId, component::{ RelativeLayoutListenerSignal, ResizeListenerSignal, interactive::navigation::{NavDirection, NavJump, NavScroll, NavSignal, NavType}, }, unit::WidgetUnit, utils::{Rect, Vec2, lerp}, }, }; use std::collections::{HashMap, HashSet, VecDeque}; #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum PointerButton { Trigger, Context, } #[derive(Debug, Default, Clone)] pub enum Interaction { #[default] None, Navigate(NavSignal), PointerDown(PointerButton, Vec2), PointerUp(PointerButton, Vec2), PointerMove(Vec2), } impl Interaction { pub fn is_none(&self) -> bool { matches!(self, Self::None) } #[inline] pub fn is_some(&self) -> bool { !self.is_none() } } #[derive(Debug, Default, Copy, Clone)] pub struct DefaultInteractionsEngineResult { pub captured_pointer_location: bool, pub captured_pointer_action: bool, pub captured_text_change: bool, } impl DefaultInteractionsEngineResult { #[inline] pub fn is_any(&self) -> bool { self.captured_pointer_action || self.captured_pointer_location || self.captured_text_change } #[inline] pub fn is_none(&self) -> bool { !self.is_any() } } /// Single pointer + Keyboard + Gamepad #[derive(Debug, Default)] pub struct DefaultInteractionsEngine { pub deselect_when_no_button_found: bool, pub unfocus_when_selection_change: bool, resize_listeners: HashMap, relative_layout_listeners: HashMap, interactions_queue: VecDeque, containers: HashMap>, items_owners: HashMap, buttons: HashSet, text_inputs: HashSet, scroll_views: HashSet, scroll_view_contents: HashSet, tracking: HashMap, selected_chain: Vec, locked_widget: Option, focused_text_input: Option, sorted_items_ids: Vec, } impl DefaultInteractionsEngine { #[allow(clippy::too_many_arguments)] pub fn with_capacity( resize_listeners: usize, relative_layout_listeners: usize, interactions_queue: usize, containers: usize, buttons: usize, text_inputs: usize, scroll_views: usize, tracking: usize, selected_chain: usize, ) -> Self { Self { deselect_when_no_button_found: false, unfocus_when_selection_change: true, resize_listeners: HashMap::with_capacity(resize_listeners), relative_layout_listeners: HashMap::with_capacity(relative_layout_listeners), interactions_queue: VecDeque::with_capacity(interactions_queue), containers: HashMap::with_capacity(containers), items_owners: Default::default(), buttons: HashSet::with_capacity(buttons), text_inputs: HashSet::with_capacity(text_inputs), scroll_views: HashSet::with_capacity(scroll_views), scroll_view_contents: HashSet::with_capacity(scroll_views), tracking: HashMap::with_capacity(tracking), selected_chain: Vec::with_capacity(selected_chain), locked_widget: None, focused_text_input: None, sorted_items_ids: vec![], } } pub fn locked_widget(&self) -> Option<&WidgetId> { self.locked_widget.as_ref() } pub fn selected_chain(&self) -> &[WidgetId] { &self.selected_chain } pub fn selected_item(&self) -> Option<&WidgetId> { self.selected_chain.last() } pub fn selected_container(&self) -> Option<&WidgetId> { self.selected_chain .iter() .rev() .find(|id| self.containers.contains_key(id)) } pub fn selected_button(&self) -> Option<&WidgetId> { self.selected_chain .iter() .rev() .find(|id| self.buttons.contains(id)) } pub fn selected_scroll_view(&self) -> Option<&WidgetId> { self.selected_chain .iter() .rev() .find(|id| self.scroll_views.contains(id)) } pub fn selected_scroll_view_content(&self) -> Option<&WidgetId> { self.selected_chain .iter() .rev() .find(|id| self.scroll_view_contents.contains(id)) } pub fn focused_text_input(&self) -> Option<&WidgetId> { self.focused_text_input.as_ref() } pub fn interact(&mut self, interaction: Interaction) { if interaction.is_some() { self.interactions_queue.push_back(interaction); } } pub fn clear_queue(&mut self, put_unselect: bool) { self.interactions_queue.clear(); if put_unselect { self.interactions_queue .push_back(Interaction::Navigate(NavSignal::Unselect)); } } fn cache_sorted_items_ids(&mut self, app: &Application) { self.sorted_items_ids = Vec::with_capacity(self.items_owners.len()); self.cache_sorted_items_ids_inner(app.rendered_tree()); } fn cache_sorted_items_ids_inner(&mut self, unit: &WidgetUnit) { if let Some(data) = unit.as_data() { self.sorted_items_ids.push(data.id().to_owned()); } match unit { WidgetUnit::AreaBox(unit) => { self.cache_sorted_items_ids_inner(&unit.slot); } WidgetUnit::ContentBox(unit) => { for item in &unit.items { self.cache_sorted_items_ids_inner(&item.slot); } } WidgetUnit::FlexBox(unit) => { if unit.direction.is_order_ascending() { for item in &unit.items { self.cache_sorted_items_ids_inner(&item.slot); } } else { for item in unit.items.iter().rev() { self.cache_sorted_items_ids_inner(&item.slot); } } } WidgetUnit::GridBox(unit) => { for item in &unit.items { self.cache_sorted_items_ids_inner(&item.slot); } } WidgetUnit::SizeBox(unit) => { self.cache_sorted_items_ids_inner(&unit.slot); } _ => {} } } pub fn select_item(&mut self, app: &mut Application, id: Option) -> bool { if self.locked_widget.is_some() || self.selected_chain.last() == id.as_ref() { return false; } if let Some(id) = &id && self.containers.contains_key(id) { app.send_message(id, NavSignal::Select(id.to_owned().into())); } match (self.selected_chain.is_empty(), id) { (false, None) => { for id in std::mem::take(&mut self.selected_chain).iter().rev() { app.send_message(id, NavSignal::Unselect); } } (false, Some(mut id)) => { if self.unfocus_when_selection_change { self.focus_text_input(app, None); } let mut chain = Vec::with_capacity(self.selected_chain.len()); while let Some(owner) = self.items_owners.get(&id) { if !chain.contains(&id) { chain.push(id.to_owned()); } if !chain.contains(owner) { chain.push(owner.to_owned()); } id = owner.to_owned(); } chain.reverse(); let mut index = 0; for (a, b) in self.selected_chain.iter().zip(chain.iter()) { if a != b { break; } index += 1; } for id in &self.selected_chain[index..] { app.send_message(id, NavSignal::Unselect); } for id in &chain[index..] { app.send_message(id, NavSignal::Select(().into())); } self.selected_chain = chain; } (true, Some(mut id)) => { if self.unfocus_when_selection_change { self.focus_text_input(app, None); } self.selected_chain.clear(); while let Some(owner) = self.items_owners.get(&id) { if !self.selected_chain.contains(&id) { self.selected_chain.push(id.to_owned()); } if !self.selected_chain.contains(owner) { self.selected_chain.push(owner.to_owned()); } id = owner.to_owned(); } self.selected_chain.reverse(); for id in &self.selected_chain { app.send_message(id, NavSignal::Select(().into())); } } (true, None) => {} } true } pub fn focus_text_input(&mut self, app: &mut Application, id: Option) { if self.focused_text_input == id { return; } if let Some(focused) = &self.focused_text_input { app.send_message(focused, NavSignal::FocusTextInput(().into())); } self.focused_text_input = None; if let Some(id) = id && self.text_inputs.contains(&id) { app.send_message(&id, NavSignal::FocusTextInput(id.to_owned().into())); self.focused_text_input = Some(id); } } pub fn send_to_selected_item(&self, app: &mut Application, data: T) -> bool where T: 'static + MessageData, { if let Some(id) = self.selected_item() { app.send_message(id, data); return true; } false } pub fn send_to_selected_container(&self, app: &mut Application, data: T) -> bool where T: 'static + MessageData, { if let Some(id) = self.selected_container() { app.send_message(id, data); return true; } false } pub fn send_to_selected_button(&self, app: &mut Application, data: T) -> bool where T: 'static + MessageData, { if let Some(id) = self.selected_button() { app.send_message(id, data); return true; } false } pub fn send_to_focused_text_input(&self, app: &mut Application, data: T) -> bool where T: 'static + MessageData, { if let Some(id) = self.focused_text_input() { app.send_message(id, data); return true; } false } fn find_scroll_view_content(&self, id: &WidgetId) -> Option { if self.scroll_views.contains(id) && let Some(items) = self.containers.get(id) { for item in items { if self.scroll_view_contents.contains(item) { return Some(item.to_owned()); } } } None } fn get_item_point(app: &Application, id: &WidgetId) -> Option { if let Some(layout) = app.layout_data().items.get(id) { let x = (layout.ui_space.left + layout.ui_space.right) * 0.5; let y = (layout.ui_space.top + layout.ui_space.bottom) * 0.5; Some(Vec2 { x, y }) } else { None } } fn get_selected_item_point(&self, app: &Application) -> Option { Self::get_item_point(app, self.selected_item()?) } fn get_closest_item_point(app: &Application, id: &WidgetId, mut point: Vec2) -> Option { if let Some(layout) = app.layout_data().items.get(id) { point.x = point.x.max(layout.ui_space.left).min(layout.ui_space.right); point.y = point.y.max(layout.ui_space.top).min(layout.ui_space.bottom); Some(point) } else { None } } fn find_item_closest_to_point( app: &Application, point: Vec2, items: &HashSet, ) -> Option { items .iter() .filter_map(|id| { Self::get_closest_item_point(app, id, point).map(|p| { let dx = p.x - point.x; let dy = p.y - point.y; (id, dx * dx + dy * dy) }) }) .min_by(|a, b| a.1.partial_cmp(&b.1).unwrap()) .map(|m| m.0.to_owned()) } fn find_item_closest_to_direction( app: &Application, point: Vec2, direction: NavDirection, items: &HashSet, ) -> Option { let dir = match direction { NavDirection::Up => Vec2 { x: 0.0, y: -1.0 }, NavDirection::Down => Vec2 { x: 0.0, y: 1.0 }, NavDirection::Left => Vec2 { x: -1.0, y: 0.0 }, NavDirection::Right => Vec2 { x: 1.0, y: 0.0 }, _ => return None, }; items .iter() .filter_map(|id| { Self::get_closest_item_point(app, id, point).map(|p| { let dx = p.x - point.x; let dy = p.y - point.y; let len = (dx * dx + dy * dy).sqrt(); let dot = dx / len * dir.x + dy / len * dir.y; let f = if len > 0.0 { dot / len } else { 0.0 }; (id, f) }) }) .filter(|m| m.1 > 1.0e-6) .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap()) .map(|m| m.0.to_owned()) } fn find_first_item(&self, items: &HashSet) -> Option { self.sorted_items_ids .iter() .find(|id| items.contains(id)) .cloned() } fn find_last_item(&self, items: &HashSet) -> Option { self.sorted_items_ids .iter() .rev() .find(|id| items.contains(id)) .cloned() } fn find_prev_item(&self, id: &WidgetId, items: &HashSet) -> Option { let mut found = false; self.sorted_items_ids .iter() .rev() .find(|i| { if found { if items.contains(i) { return true; } } else if i == &id { found = true; } false }) .cloned() } fn find_next_item(&self, id: &WidgetId, items: &HashSet) -> Option { let mut found = false; self.sorted_items_ids .iter() .find(|i| { if found { if items.contains(i) { return true; } } else if i == &id { found = true; } false }) .cloned() } // TODO: refactor this shit! my eyes are bleeding, like really dude ffs.. fn jump(&mut self, app: &mut Application, id: &WidgetId, data: NavJump) { if let Some(items) = self.containers.get(id) { match data { NavJump::First => { if let Some(id) = self.find_first_item(items) { self.select_item(app, Some(id)); } } NavJump::Last => { if let Some(id) = self.find_last_item(items) { self.select_item(app, Some(id)); } } NavJump::TopLeft => { if let Some(layout) = app.layout_data().items.get(id) { let point = Vec2 { x: layout.ui_space.left, y: layout.ui_space.top, }; if let Some(id) = Self::find_item_closest_to_point(app, point, items) { self.select_item(app, Some(id)); } } } NavJump::TopRight => { if let Some(layout) = app.layout_data().items.get(id) { let point = Vec2 { x: layout.ui_space.right, y: layout.ui_space.top, }; if let Some(id) = Self::find_item_closest_to_point(app, point, items) { self.select_item(app, Some(id)); } } } NavJump::BottomLeft => { if let Some(layout) = app.layout_data().items.get(id) { let point = Vec2 { x: layout.ui_space.left, y: layout.ui_space.bottom, }; if let Some(id) = Self::find_item_closest_to_point(app, point, items) { self.select_item(app, Some(id)); } } } NavJump::BottomRight => { if let Some(layout) = app.layout_data().items.get(id) { let point = Vec2 { x: layout.ui_space.right, y: layout.ui_space.bottom, }; if let Some(id) = Self::find_item_closest_to_point(app, point, items) { self.select_item(app, Some(id)); } } } NavJump::MiddleCenter => { if let Some(layout) = app.layout_data().items.get(id) { let point = Vec2 { x: (layout.ui_space.left + layout.ui_space.right) * 0.5, y: (layout.ui_space.top + layout.ui_space.bottom) * 0.5, }; if let Some(id) = Self::find_item_closest_to_point(app, point, items) { self.select_item(app, Some(id)); } } } NavJump::Loop(direction) => match direction { NavDirection::Up | NavDirection::Down | NavDirection::Left | NavDirection::Right => { if let Some(point) = self.get_selected_item_point(app) { if let Some(id) = Self::find_item_closest_to_direction(app, point, direction, items) { self.select_item(app, Some(id)); } else if let Some(id) = self.items_owners.get(id) { match direction { NavDirection::Up => app.send_message(id, NavSignal::Up), NavDirection::Down => app.send_message(id, NavSignal::Down), NavDirection::Left => app.send_message(id, NavSignal::Left), NavDirection::Right => app.send_message(id, NavSignal::Right), _ => {} } } } } NavDirection::Prev => { if let Some(id) = self.selected_chain.last() { if let Some(id) = self.find_prev_item(id, items) { self.select_item(app, Some(id)); } else if let Some(id) = self.find_last_item(items) { self.select_item(app, Some(id)); } else if let Some(id) = self.items_owners.get(id) { app.send_message(id, NavSignal::Prev); } } } NavDirection::Next => { if let Some(id) = self.selected_chain.last() { if let Some(id) = self.find_next_item(id, items) { self.select_item(app, Some(id)); } else if let Some(id) = self.find_first_item(items) { self.select_item(app, Some(id)); } else if let Some(id) = self.items_owners.get(id) { app.send_message(id, NavSignal::Next); } } } _ => {} }, NavJump::Escape(direction, idref) => match direction { NavDirection::Up | NavDirection::Down | NavDirection::Left | NavDirection::Right => { if let Some(point) = self.get_selected_item_point(app) { if let Some(id) = Self::find_item_closest_to_direction(app, point, direction, items) { self.select_item(app, Some(id)); } else if let Some(id) = idref.read() { self.select_item(app, Some(id)); } else if let Some(id) = self.items_owners.get(id) { match direction { NavDirection::Up => app.send_message(id, NavSignal::Up), NavDirection::Down => app.send_message(id, NavSignal::Down), NavDirection::Left => app.send_message(id, NavSignal::Left), NavDirection::Right => app.send_message(id, NavSignal::Right), _ => {} } } } } NavDirection::Prev => { if let Some(id) = self.selected_chain.last() { if let Some(id) = self.find_prev_item(id, items) { self.select_item(app, Some(id)); } else if let Some(id) = idref.read() { self.select_item(app, Some(id)); } else if let Some(id) = self.items_owners.get(id) { app.send_message(id, NavSignal::Prev); } } } NavDirection::Next => { if let Some(id) = self.selected_chain.last() { if let Some(id) = self.find_next_item(id, items) { self.select_item(app, Some(id)); } else if let Some(id) = idref.read() { self.select_item(app, Some(id)); } else if let Some(id) = self.items_owners.get(id) { app.send_message(id, NavSignal::Next); } } } _ => {} }, NavJump::Scroll(scroll) => { fn factor( this: &DefaultInteractionsEngine, app: &mut Application, id: &WidgetId, v: Vec2, relative: bool, ) { if let Some(oid) = this.find_scroll_view_content(id) { let a = app.layout_data().find_or_ui_space(oid.path()); let b = app.layout_data().find_or_ui_space(id.path()); let asize = a.local_space.size(); let bsize = b.local_space.size(); let f = Vec2 { x: if bsize.x > 0.0 { asize.x / bsize.x } else { 0.0 }, y: if bsize.y > 0.0 { asize.y / bsize.y } else { 0.0 }, }; app.send_message( id, NavSignal::Jump(NavJump::Scroll(NavScroll::Change(v, f, relative))), ); } } fn units( this: &DefaultInteractionsEngine, app: &mut Application, id: &WidgetId, v: Vec2, relative: bool, ) { if let Some(oid) = this.find_scroll_view_content(id) { let a = app.layout_data().find_or_ui_space(oid.path()); let b = app.layout_data().find_or_ui_space(id.path()); let asize = a.local_space.size(); let bsize = b.local_space.size(); let dsize = Vec2 { x: asize.x - bsize.x, y: asize.y - bsize.y, }; let v = Vec2 { x: if dsize.x > 0.0 { v.x / dsize.x } else { 0.0 }, y: if dsize.y > 0.0 { v.y / dsize.y } else { 0.0 }, }; let f = Vec2 { x: if bsize.x > 0.0 { asize.x / bsize.x } else { 0.0 }, y: if bsize.y > 0.0 { asize.y / bsize.y } else { 0.0 }, }; app.send_message( id, NavSignal::Jump(NavJump::Scroll(NavScroll::Change(v, f, relative))), ); } } match scroll { NavScroll::Factor(v, relative) => factor(self, app, id, v, relative), NavScroll::DirectFactor(idref, v, relative) => { if let Some(id) = idref.read() { factor(self, app, &id, v, relative); } } NavScroll::Units(v, relative) => units(self, app, id, v, relative), NavScroll::DirectUnits(idref, v, relative) => { if let Some(id) = idref.read() { units(self, app, &id, v, relative); } } NavScroll::Widget(idref, anchor) => { if let (Some(wid), Some(oid)) = (idref.read(), self.find_scroll_view_content(id)) && let Some(rect) = app.layout_data().rect_relative_to(&wid, &oid) { let aitem = app.layout_data().find_or_ui_space(oid.path()); let bitem = app.layout_data().find_or_ui_space(id.path()); let x = lerp(rect.left, rect.right, anchor.x); let y = lerp(rect.top, rect.bottom, anchor.y); let asize = aitem.local_space.size(); let bsize = bitem.local_space.size(); let v = Vec2 { x: if asize.x > 0.0 { x / asize.x } else { 0.0 }, y: if asize.y > 0.0 { y / asize.y } else { 0.0 }, }; let f = Vec2 { x: if bsize.x > 0.0 { asize.x / bsize.x } else { 0.0 }, y: if bsize.y > 0.0 { asize.y / bsize.y } else { 0.0 }, }; app.send_message( id, NavSignal::Jump(NavJump::Scroll(NavScroll::Change( v, f, false, ))), ); } } _ => {} } } } } } pub fn find_button(&self, app: &Application, x: Scalar, y: Scalar) -> Option<(WidgetId, Vec2)> { self.find_button_inner(app, x, y, app.rendered_tree(), app.layout_data().ui_space) } fn find_button_inner( &self, app: &Application, x: Scalar, y: Scalar, unit: &WidgetUnit, mut clip: Rect, ) -> Option<(WidgetId, Vec2)> { if x < clip.left || x > clip.right || y < clip.top || y > clip.bottom { return None; } let mut result = None; if let Some(data) = unit.as_data() && self.buttons.contains(data.id()) && let Some(layout) = app.layout_data().items.get(data.id()) { let rect = layout.ui_space; if x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom { let size = rect.size(); let pos = Vec2 { x: if size.x > 0.0 { (x - rect.left) / size.x } else { 0.0 }, y: if size.y > 0.0 { (y - rect.top) / size.y } else { 0.0 }, }; result = Some((data.id().to_owned(), pos)); } } match unit { WidgetUnit::AreaBox(unit) => { if let Some(id) = self.find_button_inner(app, x, y, &unit.slot, clip) { result = Some(id); } } WidgetUnit::ContentBox(unit) => { if unit.clipping && let Some(item) = app.layout_data().items.get(&unit.id) { clip = item.ui_space; } for item in &unit.items { if let Some(id) = self.find_button_inner(app, x, y, &item.slot, clip) { result = Some(id); } } } WidgetUnit::FlexBox(unit) => { for item in &unit.items { if let Some(id) = self.find_button_inner(app, x, y, &item.slot, clip) { result = Some(id); } } } WidgetUnit::GridBox(unit) => { for item in &unit.items { if let Some(id) = self.find_button_inner(app, x, y, &item.slot, clip) { result = Some(id); } } } WidgetUnit::SizeBox(unit) => { if let Some(id) = self.find_button_inner(app, x, y, &unit.slot, clip) { result = Some(id); } } _ => {} } result } pub fn does_hover_widget(&self, app: &Application, x: Scalar, y: Scalar) -> bool { Self::does_hover_widget_inner(app, x, y, app.rendered_tree()) } fn does_hover_widget_inner(app: &Application, x: Scalar, y: Scalar, unit: &WidgetUnit) -> bool { if let Some(data) = unit.as_data() && let Some(layout) = app.layout_data().items.get(data.id()) { let rect = layout.ui_space; if x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom { return true; } } match unit { WidgetUnit::AreaBox(unit) => { if Self::does_hover_widget_inner(app, x, y, &unit.slot) { return true; } } WidgetUnit::ContentBox(unit) => { for item in &unit.items { if Self::does_hover_widget_inner(app, x, y, &item.slot) { return true; } } } WidgetUnit::FlexBox(unit) => { for item in &unit.items { if Self::does_hover_widget_inner(app, x, y, &item.slot) { return true; } } } WidgetUnit::GridBox(unit) => { for item in &unit.items { if Self::does_hover_widget_inner(app, x, y, &item.slot) { return true; } } } WidgetUnit::SizeBox(unit) => { if Self::does_hover_widget_inner(app, x, y, &unit.slot) { return true; } } _ => {} } false } } impl InteractionsEngine for DefaultInteractionsEngine { fn perform_interactions( &mut self, app: &mut Application, ) -> Result { let mut to_resize = HashSet::new(); let mut to_relative_layout = HashSet::new(); let mut to_select = None; let mut to_jump = HashMap::new(); let mut to_focus = None; let mut to_send_axis = vec![]; let mut to_send_custom = vec![]; for (id, signal) in app.signals() { if let Some(signal) = signal.as_any().downcast_ref() { match signal { ResizeListenerSignal::Register => { if let Some(item) = app.layout_data().items.get(id) { self.resize_listeners .insert(id.to_owned(), item.local_space.size()); to_resize.insert(id.to_owned()); } } ResizeListenerSignal::Unregister => { self.resize_listeners.remove(id); } _ => {} } } else if let Some(signal) = signal.as_any().downcast_ref() { match signal { RelativeLayoutListenerSignal::Register(relative_to) => { if let (Some(item), Some(rect)) = ( app.layout_data().items.get(relative_to), app.layout_data().rect_relative_to(id, relative_to), ) { self.relative_layout_listeners.insert( id.to_owned(), (relative_to.to_owned(), item.local_space.size(), rect), ); to_relative_layout.insert(id.to_owned()); } } RelativeLayoutListenerSignal::Unregister => { self.relative_layout_listeners.remove(id); } _ => {} } } else if let Some(signal) = signal.as_any().downcast_ref() { match signal { NavSignal::Register(t) => match t { NavType::Container => { self.containers.insert(id.to_owned(), Default::default()); } NavType::Item => { if let Some((key, items)) = self .containers .iter_mut() .filter(|(k, _)| { k.path() != id.path() && id.path().starts_with(k.path()) }) .max_by(|(a, _), (b, _)| a.depth().cmp(&b.depth())) { items.remove(id); items.insert(id.to_owned()); self.items_owners.insert(id.to_owned(), key.to_owned()); } } NavType::Button => { self.buttons.insert(id.to_owned()); } NavType::TextInput => { self.text_inputs.insert(id.to_owned()); } NavType::ScrollView => { self.scroll_views.insert(id.to_owned()); } NavType::ScrollViewContent => { self.scroll_view_contents.insert(id.to_owned()); } NavType::Tracking(who) => { if let Some(who) = who.read() { self.tracking.insert(id.to_owned(), who); } } }, NavSignal::Unregister(t) => match t { NavType::Container => { if let Some(items) = self.containers.remove(id) { for id in items { self.items_owners.remove(&id); } } } NavType::Item => { if let Some(key) = self.items_owners.remove(id) && let Some(items) = self.containers.get_mut(&key) { items.remove(&key); } if let Some(lid) = &self.locked_widget && lid == id { self.locked_widget = None; } } NavType::Button => { self.buttons.remove(id); } NavType::TextInput => { self.text_inputs.remove(id); if let Some(focused) = &self.focused_text_input && focused == id { self.focused_text_input = None; } } NavType::ScrollView => { self.scroll_views.remove(id); } NavType::ScrollViewContent => { self.scroll_view_contents.remove(id); } NavType::Tracking(_) => { self.tracking.remove(id); } }, NavSignal::Select(idref) => to_select = Some(idref.to_owned()), NavSignal::Unselect => to_select = Some(().into()), NavSignal::Lock => { if self.locked_widget.is_none() { self.locked_widget = Some(id.to_owned()); } } NavSignal::Unlock => { if let Some(lid) = &self.locked_widget && lid == id { self.locked_widget = None; } } NavSignal::Jump(data) => { to_jump.insert(id.to_owned(), data.to_owned()); } NavSignal::FocusTextInput(idref) => to_focus = Some(idref.to_owned()), NavSignal::Axis(name, value) => to_send_axis.push((name.to_owned(), *value)), NavSignal::Custom(idref, data) => { to_send_custom.push((idref.to_owned(), data.to_owned())) } _ => {} } } } for (k, v) in &mut self.resize_listeners { if let Some(item) = app.layout_data().items.get(k) { let size = item.local_space.size(); if to_resize.contains(k) || (v.x - size.x).abs() >= 1.0e-6 || (v.y - size.y).abs() >= 1.0e-6 { app.send_message(k, ResizeListenerSignal::Change(size)); *v = size; } } } for (k, (r, s, v)) in &mut self.relative_layout_listeners { if let (Some(item), Some(rect)) = ( app.layout_data().items.get(r), app.layout_data().rect_relative_to(k, r), ) { let size = item.local_space.size(); if to_relative_layout.contains(k) || (s.x - size.x).abs() >= 1.0e-6 || (s.y - size.y).abs() >= 1.0e-6 || (v.left - rect.left).abs() >= 1.0e-6 || (v.right - rect.right).abs() >= 1.0e-6 || (v.top - rect.top).abs() >= 1.0e-6 || (v.bottom - rect.bottom).abs() >= 1.0e-6 { app.send_message(k, RelativeLayoutListenerSignal::Change(size, rect)); *s = size; *v = rect; } } } if !to_jump.is_empty() { self.cache_sorted_items_ids(app); } if let Some(idref) = to_select { self.select_item(app, idref.read()); } for (id, data) in to_jump { self.jump(app, &id, data); } if let Some(idref) = to_focus { self.focus_text_input(app, idref.read()); } for (name, value) in to_send_axis { self.send_to_selected_item(app, NavSignal::Axis(name, value)); } for (idref, data) in to_send_custom { if let Some(id) = idref.read() { app.send_message(&id, NavSignal::Custom(().into(), data)); } else { self.send_to_selected_item(app, NavSignal::Custom(().into(), data)); } } let mut result = DefaultInteractionsEngineResult::default(); while let Some(interaction) = self.interactions_queue.pop_front() { match interaction { Interaction::None => {} Interaction::Navigate(msg) => match msg { NavSignal::Select(idref) => { self.select_item(app, idref.read()); } NavSignal::Unselect => { self.select_item(app, None); } NavSignal::Accept(_) | NavSignal::Context(_) | NavSignal::Cancel(_) => { self.send_to_selected_item(app, msg); } NavSignal::Up | NavSignal::Down | NavSignal::Left | NavSignal::Right | NavSignal::Prev | NavSignal::Next => { self.send_to_selected_container(app, msg); } NavSignal::FocusTextInput(idref) => { self.focus_text_input(app, idref.read()); } NavSignal::TextChange(_) => { if self.send_to_focused_text_input(app, msg) { result.captured_text_change = true; } } NavSignal::Custom(idref, data) => { if let Some(id) = idref.read() { app.send_message(&id, NavSignal::Custom(().into(), data)); } else { self.send_to_selected_item(app, NavSignal::Custom(().into(), data)); } } NavSignal::Jump(jump) => match jump { NavJump::Scroll(NavScroll::Factor(_, _)) | NavJump::Scroll(NavScroll::Units(_, _)) | NavJump::Scroll(NavScroll::Widget(_, _)) => { if let Some(id) = self.selected_scroll_view().cloned() { self.jump(app, &id, jump); } } _ => {} }, _ => {} }, Interaction::PointerMove(Vec2 { x, y }) => { if self.locked_widget.is_some() { if self.selected_button().is_some() { result.captured_pointer_location = true; } } else if let Some((found, _)) = self.find_button(app, x, y) { result.captured_pointer_location = true; self.select_item(app, Some(found)); } else { if self.deselect_when_no_button_found { self.select_item(app, None); } if self.does_hover_widget(app, x, y) { result.captured_pointer_location = true; } } for (id, who) in &self.tracking { if let Some(layout) = app.layout_data().items.get(who) { let rect = layout.ui_space; let size = rect.size(); app.send_message( id, NavSignal::Axis( "pointer-x".to_owned(), if size.x > 0.0 { (x - rect.left) / size.x } else { 0.0 }, ), ); app.send_message( id, NavSignal::Axis( "pointer-y".to_owned(), if size.y > 0.0 { (y - rect.top) / size.y } else { 0.0 }, ), ); app.send_message( id, NavSignal::Axis("pointer-x-unscaled".to_owned(), x - rect.left), ); app.send_message( id, NavSignal::Axis("pointer-y-unscaled".to_owned(), y - rect.top), ); app.send_message(id, NavSignal::Axis("pointer-x-ui".to_owned(), x)); app.send_message(id, NavSignal::Axis("pointer-y-ui".to_owned(), y)); result.captured_pointer_location = true; result.captured_pointer_action = true; } } } Interaction::PointerDown(button, Vec2 { x, y }) => { if let Some((found, _)) = self.find_button(app, x, y) { self.select_item(app, Some(found)); result.captured_pointer_location = true; let action = match button { PointerButton::Trigger => NavSignal::Accept(true), PointerButton::Context => NavSignal::Context(true), }; if self.send_to_selected_button(app, action) { result.captured_pointer_action = true; } } else { if self.deselect_when_no_button_found { self.select_item(app, None); } if self.does_hover_widget(app, x, y) { result.captured_pointer_location = true; } } for (id, who) in &self.tracking { if let Some(layout) = app.layout_data().items.get(who) { let rect = layout.ui_space; let size = rect.size(); app.send_message( id, NavSignal::Axis( "pointer-x".to_owned(), if size.x > 0.0 { (x - rect.left) / size.x } else { 0.0 }, ), ); app.send_message( id, NavSignal::Axis( "pointer-y".to_owned(), if size.y > 0.0 { (y - rect.top) / size.y } else { 0.0 }, ), ); app.send_message( id, NavSignal::Axis("pointer-x-unscaled".to_owned(), x - rect.left), ); app.send_message( id, NavSignal::Axis("pointer-y-unscaled".to_owned(), y - rect.top), ); app.send_message(id, NavSignal::Axis("pointer-x-ui".to_owned(), x)); app.send_message(id, NavSignal::Axis("pointer-y-ui".to_owned(), y)); result.captured_pointer_location = true; result.captured_pointer_action = true; } } } Interaction::PointerUp(button, Vec2 { x, y }) => { let action = match button { PointerButton::Trigger => NavSignal::Accept(false), PointerButton::Context => NavSignal::Context(false), }; if self.send_to_selected_button(app, action) { result.captured_pointer_action = true; } for (id, who) in &self.tracking { if let Some(layout) = app.layout_data().items.get(who) { let rect = layout.ui_space; let size = rect.size(); app.send_message( id, NavSignal::Axis( "pointer-x".to_owned(), if size.x > 0.0 { (x - rect.left) / size.x } else { 0.0 }, ), ); app.send_message( id, NavSignal::Axis( "pointer-y".to_owned(), if size.y > 0.0 { (y - rect.top) / size.y } else { 0.0 }, ), ); app.send_message( id, NavSignal::Axis("pointer-x-unscaled".to_owned(), x - rect.left), ); app.send_message( id, NavSignal::Axis("pointer-y-unscaled".to_owned(), y - rect.top), ); app.send_message(id, NavSignal::Axis("pointer-x-ui".to_owned(), x)); app.send_message(id, NavSignal::Axis("pointer-y-ui".to_owned(), y)); result.captured_pointer_location = true; result.captured_pointer_action = true; } } } } } Ok(result) } } ================================================ FILE: crates/core/src/interactive/mod.rs ================================================ //! Interactivity traits pub mod default_interactions_engine; use crate::application::Application; pub trait InteractionsEngine { fn perform_interactions(&mut self, app: &mut Application) -> Result; } impl InteractionsEngine<(), ()> for () { fn perform_interactions(&mut self, _: &mut Application) -> Result<(), ()> { Ok(()) } } ================================================ FILE: crates/core/src/layout/default_layout_engine.rs ================================================ use crate::{ Scalar, layout::{CoordsMapping, Layout, LayoutEngine, LayoutItem, LayoutNode}, widget::{ WidgetId, unit::{ WidgetUnit, area::AreaBox, content::ContentBox, flex::FlexBox, grid::GridBox, image::{ImageBox, ImageBoxSizeValue}, size::{SizeBox, SizeBoxAspectRatio, SizeBoxSizeValue}, text::{TextBox, TextBoxSizeValue}, }, utils::{Rect, Vec2, lerp}, }, }; use std::collections::HashMap; pub trait TextMeasurementEngine { fn measure_text( &self, size_available: Vec2, mapping: &CoordsMapping, widget: &TextBox, ) -> Option; } impl TextMeasurementEngine for () { fn measure_text(&self, _: Vec2, _: &CoordsMapping, _: &TextBox) -> Option { None } } pub struct DefaultLayoutEngine { text_measurement_engine: TME, } impl Default for DefaultLayoutEngine { fn default() -> Self { Self { text_measurement_engine: TME::default(), } } } impl Clone for DefaultLayoutEngine { fn clone(&self) -> Self { Self { text_measurement_engine: self.text_measurement_engine.clone(), } } } impl Copy for DefaultLayoutEngine {} impl DefaultLayoutEngine { pub fn new(engine: TME) -> Self { Self { text_measurement_engine: engine, } } pub fn layout_node( &self, size_available: Vec2, mapping: &CoordsMapping, unit: &WidgetUnit, ) -> Option { match unit { WidgetUnit::None | WidgetUnit::PortalBox(_) => None, WidgetUnit::AreaBox(b) => self.layout_area_box(size_available, mapping, b), WidgetUnit::ContentBox(b) => self.layout_content_box(size_available, mapping, b), WidgetUnit::FlexBox(b) => self.layout_flex_box(size_available, mapping, b), WidgetUnit::GridBox(b) => self.layout_grid_box(size_available, mapping, b), WidgetUnit::SizeBox(b) => self.layout_size_box(size_available, mapping, b), WidgetUnit::ImageBox(b) => self.layout_image_box(size_available, b), WidgetUnit::TextBox(b) => self.layout_text_box(size_available, mapping, b), } } pub fn layout_area_box( &self, size_available: Vec2, mapping: &CoordsMapping, unit: &AreaBox, ) -> Option { if !unit.id.is_valid() { return None; } let (children, w, h) = if let Some(child) = self.layout_node(size_available, mapping, &unit.slot) { let w = child.local_space.width(); let h = child.local_space.height(); (vec![child], w, h) } else { (vec![], 0.0, 0.0) }; let local_space = Rect { left: 0.0, right: w, top: 0.0, bottom: h, }; Some(LayoutNode { id: unit.id.to_owned(), local_space, children, }) } pub fn layout_content_box( &self, size_available: Vec2, mapping: &CoordsMapping, unit: &ContentBox, ) -> Option { if !unit.id.is_valid() { return None; } let children = unit .items .iter() .filter_map(|item| { let left = lerp(0.0, size_available.x, item.layout.anchors.left); let left = left + item.layout.margin.left + item.layout.offset.x + unit.content_reposition.offset.x; let left = left * unit.content_reposition.scale.x; let right = lerp(0.0, size_available.x, item.layout.anchors.right); let right = right - item.layout.margin.right + item.layout.offset.x + unit.content_reposition.offset.x; let right = right * unit.content_reposition.scale.x; let top = lerp(0.0, size_available.y, item.layout.anchors.top); let top = top + item.layout.margin.top + item.layout.offset.y + unit.content_reposition.offset.y; let top = top * unit.content_reposition.scale.y; let bottom = lerp(0.0, size_available.y, item.layout.anchors.bottom); let bottom = bottom - item.layout.margin.bottom + item.layout.offset.y + unit.content_reposition.offset.y; let bottom = bottom * unit.content_reposition.scale.y; let width = (right - left).max(0.0); let height = (bottom - top).max(0.0); let size = Vec2 { x: width, y: height, }; if let Some(mut child) = self.layout_node(size, mapping, &item.slot) { let diff = child.local_space.width() - width; let ox = lerp(0.0, diff, item.layout.align.x); child.local_space.left += left - ox; child.local_space.right += left - ox; let diff = child.local_space.height() - height; let oy = lerp(0.0, diff, item.layout.align.y); child.local_space.top += top - oy; child.local_space.bottom += top - oy; let w = child.local_space.width().min(size_available.x); let h = child.local_space.height().min(size_available.y); if item.layout.keep_in_bounds.cut.left { child.local_space.left = child.local_space.left.max(0.0); if item.layout.keep_in_bounds.preserve.width { child.local_space.right = child.local_space.left + w; } } if item.layout.keep_in_bounds.cut.right { child.local_space.right = child.local_space.right.min(size_available.x); if item.layout.keep_in_bounds.preserve.width { child.local_space.left = child.local_space.right - w; } } if item.layout.keep_in_bounds.cut.top { child.local_space.top = child.local_space.top.max(0.0); if item.layout.keep_in_bounds.preserve.height { child.local_space.bottom = child.local_space.top + h; } } if item.layout.keep_in_bounds.cut.bottom { child.local_space.bottom = child.local_space.bottom.min(size_available.y); if item.layout.keep_in_bounds.preserve.height { child.local_space.top = child.local_space.bottom - h; } } Some(child) } else { None } }) .collect::>(); Some(LayoutNode { id: unit.id.to_owned(), local_space: Rect { left: 0.0, right: size_available.x, top: 0.0, bottom: size_available.y, }, children, }) } pub fn layout_flex_box( &self, size_available: Vec2, mapping: &CoordsMapping, unit: &FlexBox, ) -> Option { if !unit.id.is_valid() { return None; } if unit.wrap { Some(self.layout_flex_box_wrapping(size_available, mapping, unit)) } else { Some(self.layout_flex_box_no_wrap(size_available, mapping, unit)) } } pub fn layout_flex_box_wrapping( &self, size_available: Vec2, mapping: &CoordsMapping, unit: &FlexBox, ) -> LayoutNode { let main_available = if unit.direction.is_horizontal() { size_available.x } else { size_available.y }; let (lines, count) = { let mut main = 0.0; let mut cross: Scalar = 0.0; let mut grow = 0.0; let items = unit .items .iter() .filter(|item| item.slot.is_some() && item.slot.as_data().unwrap().id().is_valid()) .collect::>(); let count = items.len(); let mut lines = vec![]; let mut line = vec![]; for item in items { let local_main = item.layout.basis.unwrap_or_else(|| { if unit.direction.is_horizontal() { self.calc_unit_min_width(size_available, mapping, &item.slot) } else { self.calc_unit_min_height(size_available, mapping, &item.slot) } }); let local_main = local_main + if unit.direction.is_horizontal() { item.layout.margin.left + item.layout.margin.right } else { item.layout.margin.top + item.layout.margin.bottom }; let local_cross = if unit.direction.is_horizontal() { self.calc_unit_min_height(size_available, mapping, &item.slot) } else { self.calc_unit_min_width(size_available, mapping, &item.slot) }; let local_cross = local_cross + if unit.direction.is_horizontal() { item.layout.margin.top + item.layout.margin.bottom } else { item.layout.margin.left + item.layout.margin.right }; if !line.is_empty() && main + local_main > main_available { main += line.len().saturating_sub(1) as Scalar * unit.separation; lines.push((main, cross, grow, std::mem::take(&mut line))); main = 0.0; cross = 0.0; grow = 0.0; } main += local_main; cross = cross.max(local_cross); grow += item.layout.grow; line.push((item, local_main, local_cross)); } main += line.len().saturating_sub(1) as Scalar * unit.separation; lines.push((main, cross, grow, line)); (lines, count) }; let mut children = Vec::with_capacity(count); let mut main_max: Scalar = 0.0; let mut cross_max = 0.0; for (main, cross_available, grow, items) in lines { let diff = main_available - main; let mut new_main = 0.0; let mut new_cross: Scalar = 0.0; for (item, local_main, local_cross) in items { let child_main = if main < main_available { local_main + if grow > 0.0 { diff * item.layout.grow / grow } else { 0.0 } } else { local_main }; let child_main = (child_main - if unit.direction.is_horizontal() { item.layout.margin.left + item.layout.margin.right } else { item.layout.margin.top + item.layout.margin.bottom }) .max(0.0); let child_cross = (local_cross - if unit.direction.is_horizontal() { item.layout.margin.top + item.layout.margin.bottom } else { item.layout.margin.left + item.layout.margin.right }) .max(0.0); let child_cross = lerp(child_cross, cross_available, item.layout.fill); let rect = if unit.direction.is_horizontal() { Vec2 { x: child_main, y: child_cross, } } else { Vec2 { x: child_cross, y: child_main, } }; if let Some(mut child) = self.layout_node(rect, mapping, &item.slot) { if unit.direction.is_horizontal() { if unit.direction.is_order_ascending() { child.local_space.left += new_main + item.layout.margin.left; child.local_space.right += new_main + item.layout.margin.left; } else { let left = child.local_space.left; let right = child.local_space.right; child.local_space.left = size_available.x - right - new_main - item.layout.margin.right; child.local_space.right = size_available.x - left - new_main - item.layout.margin.right; } new_main += rect.x + item.layout.margin.left + item.layout.margin.right; let diff = lerp( 0.0, cross_available - child.local_space.height(), item.layout.align, ); child.local_space.top += cross_max + item.layout.margin.top + diff; child.local_space.bottom += cross_max + item.layout.margin.top + diff; new_cross = new_cross.max(rect.y); } else { if unit.direction.is_order_ascending() { child.local_space.top += new_main + item.layout.margin.top; child.local_space.bottom += new_main + item.layout.margin.top; } else { let top = child.local_space.top; let bottom = child.local_space.bottom; child.local_space.top = size_available.y - bottom - new_main - item.layout.margin.bottom; child.local_space.bottom = size_available.y - top - new_main - item.layout.margin.bottom; } new_main += rect.y + item.layout.margin.top + item.layout.margin.bottom; let diff = lerp( 0.0, cross_available - child.local_space.width(), item.layout.align, ); child.local_space.left += cross_max + item.layout.margin.left + diff; child.local_space.right += cross_max + item.layout.margin.left + diff; new_cross = new_cross.max(rect.x); } new_main += unit.separation; children.push(child); } } new_main = (new_main - unit.separation).max(0.0); main_max = main_max.max(new_main); cross_max += new_cross + unit.separation; } cross_max = (cross_max - unit.separation).max(0.0); let local_space = if unit.direction.is_horizontal() { Rect { left: 0.0, right: main_max, top: 0.0, bottom: cross_max, } } else { Rect { left: 0.0, right: cross_max, top: 0.0, bottom: main_max, } }; LayoutNode { id: unit.id.to_owned(), local_space, children, } } pub fn layout_flex_box_no_wrap( &self, size_available: Vec2, mapping: &CoordsMapping, unit: &FlexBox, ) -> LayoutNode { let (main_available, cross_available) = if unit.direction.is_horizontal() { (size_available.x, size_available.y) } else { (size_available.y, size_available.x) }; let mut main = 0.0; let mut cross: Scalar = 0.0; let mut grow = 0.0; let mut shrink = 0.0; let items = unit .items .iter() .filter(|item| item.slot.is_some() && item.slot.as_data().unwrap().id().is_valid()) .collect::>(); let mut axis_sizes = Vec::with_capacity(items.len()); for item in &items { let local_main = item.layout.basis.unwrap_or_else(|| { if unit.direction.is_horizontal() { self.calc_unit_min_width(size_available, mapping, &item.slot) } else { self.calc_unit_min_height(size_available, mapping, &item.slot) } }); let local_main = local_main + if unit.direction.is_horizontal() { item.layout.margin.left + item.layout.margin.right } else { item.layout.margin.top + item.layout.margin.bottom }; let local_cross = if unit.direction.is_horizontal() { self.calc_unit_min_height(size_available, mapping, &item.slot) } else { self.calc_unit_min_width(size_available, mapping, &item.slot) }; let local_cross = local_cross + if unit.direction.is_horizontal() { item.layout.margin.top + item.layout.margin.bottom } else { item.layout.margin.left + item.layout.margin.right }; let local_cross = lerp(local_cross, cross_available, item.layout.fill); main += local_main; cross = cross.max(local_cross); grow += item.layout.grow; shrink += item.layout.shrink; axis_sizes.push((local_main, local_cross)); } main += items.len().saturating_sub(1) as Scalar * unit.separation; let diff = main_available - main; let mut new_main = 0.0; let mut new_cross: Scalar = 0.0; let children = items .into_iter() .zip(axis_sizes) .filter_map(|(item, axis_size)| { let child_main = if main < main_available { axis_size.0 + if grow > 0.0 { diff * item.layout.grow / grow } else { 0.0 } } else if main > main_available { axis_size.0 + if shrink > 0.0 { diff * item.layout.shrink / shrink } else { 0.0 } } else { axis_size.0 }; let child_main = (child_main - if unit.direction.is_horizontal() { item.layout.margin.left + item.layout.margin.right } else { item.layout.margin.top + item.layout.margin.bottom }) .max(0.0); let child_cross = (axis_size.1 - if unit.direction.is_horizontal() { item.layout.margin.top + item.layout.margin.bottom } else { item.layout.margin.left + item.layout.margin.right }) .max(0.0); let rect = if unit.direction.is_horizontal() { Vec2 { x: child_main, y: child_cross, } } else { Vec2 { x: child_cross, y: child_main, } }; if let Some(mut child) = self.layout_node(rect, mapping, &item.slot) { if unit.direction.is_horizontal() { if unit.direction.is_order_ascending() { child.local_space.left += new_main + item.layout.margin.left; child.local_space.right += new_main + item.layout.margin.left; } else { let left = child.local_space.left; let right = child.local_space.right; child.local_space.left = size_available.x - right - new_main - item.layout.margin.right; child.local_space.right = size_available.x - left - new_main - item.layout.margin.right; } new_main += rect.x + item.layout.margin.left + item.layout.margin.right; let diff = lerp( 0.0, cross_available - child.local_space.height(), item.layout.align, ); child.local_space.top += item.layout.margin.top + diff; child.local_space.bottom += item.layout.margin.top + diff; new_cross = new_cross.max(rect.y); } else { if unit.direction.is_order_ascending() { child.local_space.top += new_main + item.layout.margin.top; child.local_space.bottom += new_main + item.layout.margin.top; } else { let top = child.local_space.top; let bottom = child.local_space.bottom; child.local_space.top = size_available.y - bottom - new_main - item.layout.margin.bottom; child.local_space.bottom = size_available.y - top - new_main - item.layout.margin.bottom; } new_main += rect.y + item.layout.margin.top + item.layout.margin.bottom; let diff = lerp( 0.0, cross_available - child.local_space.width(), item.layout.align, ); child.local_space.left += item.layout.margin.left + diff; child.local_space.right += item.layout.margin.left + diff; new_cross = new_cross.max(rect.x); } new_main += unit.separation; Some(child) } else { None } }) .collect::>(); new_main = (new_main - unit.separation).max(0.0); let local_space = if unit.direction.is_horizontal() { Rect { left: 0.0, right: new_main, top: 0.0, bottom: new_cross, } } else { Rect { left: 0.0, right: new_cross, top: 0.0, bottom: new_main, } }; LayoutNode { id: unit.id.to_owned(), local_space, children, } } pub fn layout_grid_box( &self, size_available: Vec2, mapping: &CoordsMapping, unit: &GridBox, ) -> Option { if !unit.id.is_valid() { return None; } let cell_width = if unit.cols > 0 { size_available.x / unit.cols as Scalar } else { 0.0 }; let cell_height = if unit.rows > 0 { size_available.y / unit.rows as Scalar } else { 0.0 }; let children = unit .items .iter() .filter_map(|item| { let left = item.layout.space_occupancy.left as Scalar * cell_width; let right = item.layout.space_occupancy.right as Scalar * cell_width; let top = item.layout.space_occupancy.top as Scalar * cell_height; let bottom = item.layout.space_occupancy.bottom as Scalar * cell_height; let width = (right - left - item.layout.margin.left - item.layout.margin.right).max(0.0); let height = (bottom - top - item.layout.margin.top - item.layout.margin.bottom).max(0.0); let size = Vec2 { x: width, y: height, }; if let Some(mut child) = self.layout_node(size, mapping, &item.slot) { let diff = size.x - child.local_space.width(); let ox = lerp(0.0, diff, item.layout.horizontal_align); let diff = size.y - child.local_space.height(); let oy = lerp(0.0, diff, item.layout.vertical_align); child.local_space.left += left + item.layout.margin.left - ox; child.local_space.right += left + item.layout.margin.left - ox; child.local_space.top += top + item.layout.margin.top - oy; child.local_space.bottom += top + item.layout.margin.top - oy; Some(child) } else { None } }) .collect::>(); Some(LayoutNode { id: unit.id.to_owned(), local_space: Rect { left: 0.0, right: size_available.x, top: 0.0, bottom: size_available.y, }, children, }) } pub fn layout_size_box( &self, size_available: Vec2, mapping: &CoordsMapping, unit: &SizeBox, ) -> Option { if !unit.id.is_valid() { return None; } let mut size = Vec2 { x: match unit.width { SizeBoxSizeValue::Content => { self.calc_unit_min_width(size_available, mapping, &unit.slot) } SizeBoxSizeValue::Fill => size_available.x - unit.margin.left - unit.margin.right, SizeBoxSizeValue::Exact(v) => v, }, y: match unit.height { SizeBoxSizeValue::Content => { self.calc_unit_min_height(size_available, mapping, &unit.slot) } SizeBoxSizeValue::Fill => size_available.y - unit.margin.top - unit.margin.bottom, SizeBoxSizeValue::Exact(v) => v, }, }; match unit.keep_aspect_ratio { SizeBoxAspectRatio::None => {} SizeBoxAspectRatio::WidthOfHeight(factor) => { size.x = (size.y * factor).max(0.0); } SizeBoxAspectRatio::HeightOfWidth(factor) => { size.y = (size.x * factor).max(0.0); } } let children = if let Some(mut child) = self.layout_node(size, mapping, &unit.slot) { child.local_space.left += unit.margin.left; child.local_space.right += unit.margin.left; child.local_space.top += unit.margin.top; child.local_space.bottom += unit.margin.top; vec![child] } else { vec![] }; let local_space = Rect { left: 0.0, right: size.x, top: 0.0, bottom: size.y, }; Some(LayoutNode { id: unit.id.to_owned(), local_space, children, }) } pub fn layout_image_box(&self, size_available: Vec2, unit: &ImageBox) -> Option { if !unit.id.is_valid() { return None; } let local_space = Rect { left: 0.0, right: match unit.width { ImageBoxSizeValue::Fill => size_available.x, ImageBoxSizeValue::Exact(v) => v, }, top: 0.0, bottom: match unit.height { ImageBoxSizeValue::Fill => size_available.y, ImageBoxSizeValue::Exact(v) => v, }, }; Some(LayoutNode { id: unit.id.to_owned(), local_space, children: vec![], }) } pub fn layout_text_box( &self, size_available: Vec2, mapping: &CoordsMapping, unit: &TextBox, ) -> Option { if !unit.id.is_valid() { return None; } let aabb = self .text_measurement_engine .measure_text(size_available, mapping, unit) .unwrap_or_default(); let local_space = Rect { left: 0.0, right: match unit.width { TextBoxSizeValue::Content => aabb.width(), TextBoxSizeValue::Fill => size_available.x, TextBoxSizeValue::Exact(v) => v, }, top: 0.0, bottom: match unit.height { TextBoxSizeValue::Content => aabb.height(), TextBoxSizeValue::Fill => size_available.y, TextBoxSizeValue::Exact(v) => v, }, }; Some(LayoutNode { id: unit.id.to_owned(), local_space, children: vec![], }) } fn calc_unit_min_width( &self, size_available: Vec2, mapping: &CoordsMapping, unit: &WidgetUnit, ) -> Scalar { match unit { WidgetUnit::None | WidgetUnit::PortalBox(_) => 0.0, WidgetUnit::AreaBox(b) => self.calc_unit_min_width(size_available, mapping, &b.slot), WidgetUnit::ContentBox(b) => { self.calc_content_box_min_width(size_available, mapping, b) } WidgetUnit::FlexBox(b) => self.calc_flex_box_min_width(size_available, mapping, b), WidgetUnit::GridBox(b) => self.calc_grid_box_min_width(size_available, mapping, b), WidgetUnit::SizeBox(b) => { (match b.width { SizeBoxSizeValue::Content => { self.calc_unit_min_width(size_available, mapping, &b.slot) } SizeBoxSizeValue::Fill => 0.0, SizeBoxSizeValue::Exact(v) => v, }) + b.margin.left + b.margin.right } WidgetUnit::ImageBox(b) => match b.width { ImageBoxSizeValue::Fill => 0.0, ImageBoxSizeValue::Exact(v) => v, }, WidgetUnit::TextBox(b) => { let aabb = self .text_measurement_engine .measure_text(size_available, mapping, b) .unwrap_or_default(); match b.width { TextBoxSizeValue::Content => aabb.width(), TextBoxSizeValue::Fill => 0.0, TextBoxSizeValue::Exact(v) => v, } } } } fn calc_content_box_min_width( &self, size_available: Vec2, mapping: &CoordsMapping, unit: &ContentBox, ) -> Scalar { let mut result: Scalar = 0.0; for item in &unit.items { let size = self.calc_unit_min_width(size_available, mapping, &item.slot) + item.layout.margin.left + item.layout.margin.right; let width = item.layout.anchors.right - item.layout.anchors.left; let size = if width > 0.0 { size / width } else { 0.0 }; result = result.max(size); } result } fn calc_flex_box_min_width( &self, size_available: Vec2, mapping: &CoordsMapping, unit: &FlexBox, ) -> Scalar { if unit.direction.is_horizontal() { self.calc_horizontal_flex_box_min_width(size_available, mapping, unit) } else { self.calc_vertical_flex_box_min_width(size_available, mapping, unit) } } fn calc_horizontal_flex_box_min_width( &self, size_available: Vec2, mapping: &CoordsMapping, unit: &FlexBox, ) -> Scalar { if unit.wrap { let mut result: Scalar = 0.0; let mut line = 0.0; let mut first = true; for item in &unit.items { let size = self.calc_unit_min_width(size_available, mapping, &item.slot) + item.layout.margin.left + item.layout.margin.right; if first || line + size <= size_available.x { line += size; if !first { line += unit.separation; } first = false; } else { result = result.max(line); line = 0.0; first = true; } } result.max(line) } else { let mut result = 0.0; for item in &unit.items { result += self.calc_unit_min_width(size_available, mapping, &item.slot) + item.layout.margin.left + item.layout.margin.right; } result + (unit.items.len().saturating_sub(1) as Scalar) * unit.separation } } fn calc_vertical_flex_box_min_width( &self, size_available: Vec2, mapping: &CoordsMapping, unit: &FlexBox, ) -> Scalar { if unit.wrap { let mut result = 0.0; let mut line_length = 0.0; let mut line: Scalar = 0.0; let mut lines: usize = 0; let mut first = true; for item in &unit.items { let width = self.calc_unit_min_width(size_available, mapping, &item.slot) + item.layout.margin.left + item.layout.margin.right; let height = self.calc_unit_min_height(size_available, mapping, &item.slot) + item.layout.margin.top + item.layout.margin.bottom; if first || line_length + height <= size_available.y { line_length += height; if !first { line_length += unit.separation; } line = line.max(width); first = false; } else { result += line; line_length = 0.0; line = 0.0; lines += 1; first = true; } } result += line; lines += 1; result + (lines.saturating_sub(1) as Scalar) * unit.separation } else { unit.items.iter().fold(0.0, |a, item| { (self.calc_unit_min_width(size_available, mapping, &item.slot) + item.layout.margin.left + item.layout.margin.right) .max(a) }) } } fn calc_grid_box_min_width( &self, size_available: Vec2, mapping: &CoordsMapping, unit: &GridBox, ) -> Scalar { let mut result: Scalar = 0.0; for item in &unit.items { let size = self.calc_unit_min_width(size_available, mapping, &item.slot) + item.layout.margin.left + item.layout.margin.right; let size = if size > 0.0 { (item.layout.space_occupancy.width() as Scalar * size) / unit.cols as Scalar } else { 0.0 }; result = result.max(size); } result } fn calc_unit_min_height( &self, size_available: Vec2, mapping: &CoordsMapping, unit: &WidgetUnit, ) -> Scalar { match unit { WidgetUnit::None | WidgetUnit::PortalBox(_) => 0.0, WidgetUnit::AreaBox(b) => self.calc_unit_min_height(size_available, mapping, &b.slot), WidgetUnit::ContentBox(b) => { self.calc_content_box_min_height(size_available, mapping, b) } WidgetUnit::FlexBox(b) => self.calc_flex_box_min_height(size_available, mapping, b), WidgetUnit::GridBox(b) => self.calc_grid_box_min_height(size_available, mapping, b), WidgetUnit::SizeBox(b) => { (match b.height { SizeBoxSizeValue::Content => { self.calc_unit_min_height(size_available, mapping, &b.slot) } SizeBoxSizeValue::Fill => 0.0, SizeBoxSizeValue::Exact(v) => v, }) + b.margin.top + b.margin.bottom } WidgetUnit::ImageBox(b) => match b.height { ImageBoxSizeValue::Fill => 0.0, ImageBoxSizeValue::Exact(v) => v, }, WidgetUnit::TextBox(b) => { let aabb = self .text_measurement_engine .measure_text(size_available, mapping, b) .unwrap_or_default(); match b.height { TextBoxSizeValue::Content => aabb.height(), TextBoxSizeValue::Fill => 0.0, TextBoxSizeValue::Exact(v) => v, } } } } fn calc_content_box_min_height( &self, size_available: Vec2, mapping: &CoordsMapping, unit: &ContentBox, ) -> Scalar { let mut result: Scalar = 0.0; for item in &unit.items { let size = self.calc_unit_min_height(size_available, mapping, &item.slot) + item.layout.margin.top + item.layout.margin.bottom; let height = item.layout.anchors.bottom - item.layout.anchors.top; let size = if height > 0.0 { size / height } else { 0.0 }; result = result.max(size); } result } fn calc_flex_box_min_height( &self, size_available: Vec2, mapping: &CoordsMapping, unit: &FlexBox, ) -> Scalar { if unit.direction.is_horizontal() { self.calc_horizontal_flex_box_min_height(size_available, mapping, unit) } else { self.calc_vertical_flex_box_min_height(size_available, mapping, unit) } } fn calc_horizontal_flex_box_min_height( &self, size_available: Vec2, mapping: &CoordsMapping, unit: &FlexBox, ) -> Scalar { if unit.wrap { let mut result = 0.0; let mut line_length = 0.0; let mut line: Scalar = 0.0; let mut lines: usize = 0; let mut first = true; for item in &unit.items { let width = self.calc_unit_min_width(size_available, mapping, &item.slot) + item.layout.margin.left + item.layout.margin.right; let height = self.calc_unit_min_height(size_available, mapping, &item.slot) + item.layout.margin.top + item.layout.margin.bottom; if first || line_length + width <= size_available.x { line_length += width; if !first { line_length += unit.separation; } line = line.max(height); first = false; } else { result += line; line_length = 0.0; line = 0.0; lines += 1; first = true; } } result += line; lines += 1; result + (lines.saturating_sub(1) as Scalar) * unit.separation } else { unit.items.iter().fold(0.0, |a, item| { (self.calc_unit_min_height(size_available, mapping, &item.slot) + item.layout.margin.top + item.layout.margin.bottom) .max(a) }) } } fn calc_vertical_flex_box_min_height( &self, size_available: Vec2, mapping: &CoordsMapping, unit: &FlexBox, ) -> Scalar { if unit.wrap { let mut result: Scalar = 0.0; let mut line = 0.0; let mut first = true; for item in &unit.items { let size = self.calc_unit_min_height(size_available, mapping, &item.slot) + item.layout.margin.top + item.layout.margin.bottom; if first || line + size <= size_available.y { line += size; if !first { line += unit.separation; } first = false; } else { result = result.max(line); line = 0.0; first = true; } } result.max(line) } else { let mut result = 0.0; for item in &unit.items { result += self.calc_unit_min_height(size_available, mapping, &item.slot) + item.layout.margin.top + item.layout.margin.bottom; } result + (unit.items.len().saturating_sub(1) as Scalar) * unit.separation } } fn calc_grid_box_min_height( &self, size_available: Vec2, mapping: &CoordsMapping, unit: &GridBox, ) -> Scalar { let mut result: Scalar = 0.0; for item in &unit.items { let size = self.calc_unit_min_height(size_available, mapping, &item.slot) + item.layout.margin.top + item.layout.margin.bottom; let size = if size > 0.0 { (item.layout.space_occupancy.height() as Scalar * size) / unit.cols as Scalar } else { 0.0 }; result = result.max(size); } result } fn unpack_node( parent: Option<&WidgetId>, ui_space: Rect, node: LayoutNode, items: &mut HashMap, ) { let LayoutNode { id, local_space, children, } = node; let ui_space = Rect { left: local_space.left + ui_space.left, right: local_space.right + ui_space.left, top: local_space.top + ui_space.top, bottom: local_space.bottom + ui_space.top, }; for node in children { Self::unpack_node(Some(&id), ui_space, node, items); } items.insert( id, LayoutItem { local_space, ui_space, parent: parent.cloned(), }, ); } } impl LayoutEngine<()> for DefaultLayoutEngine { fn layout(&mut self, mapping: &CoordsMapping, tree: &WidgetUnit) -> Result { let ui_space = mapping.virtual_area(); if let Some(root) = self.layout_node(ui_space.size(), mapping, tree) { let mut items = HashMap::with_capacity(root.count()); Self::unpack_node(None, ui_space, root, &mut items); Ok(Layout { ui_space, items }) } else { Ok(Layout { ui_space, items: Default::default(), }) } } } ================================================ FILE: crates/core/src/layout/mod.rs ================================================ //! Layout engine pub mod default_layout_engine; use crate::{ Scalar, widget::{ WidgetId, unit::WidgetUnit, utils::{Rect, Vec2}, }, }; use serde::{Deserialize, Serialize}; use std::collections::HashMap; pub trait LayoutEngine { fn layout(&mut self, mapping: &CoordsMapping, tree: &WidgetUnit) -> Result; } struct LayoutSortedItems<'a>(Vec<(&'a WidgetId, &'a LayoutItem)>); impl<'a> LayoutSortedItems<'a> { fn new(items: &'a HashMap) -> Self { let mut items = items.iter().collect::>(); items.sort_by(|a, b| a.0.path().cmp(b.0.path())); Self(items) } } impl std::fmt::Debug for LayoutSortedItems<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_map() .entries(self.0.iter().map(|&(k, v)| (k, v))) .finish() } } #[derive(Default, Clone, Serialize, Deserialize)] pub struct Layout { pub ui_space: Rect, pub items: HashMap, } impl std::fmt::Debug for Layout { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Layout") .field("ui_space", &self.ui_space) .field("items", &LayoutSortedItems::new(&self.items)) .finish() } } impl Layout { pub fn find(&self, mut path: &str) -> Option<&LayoutItem> { loop { if let Some(item) = self .items .iter() .find_map(|(k, v)| if k.path() == path { Some(v) } else { None }) { return Some(item); } else if let Some(index) = path.rfind('/') { path = &path[0..index]; } else { break; } } None } pub fn find_or_ui_space(&self, path: &str) -> LayoutItem { match self.find(path) { Some(item) => item.to_owned(), None => LayoutItem { local_space: self.ui_space, ui_space: self.ui_space, parent: None, }, } } pub fn virtual_to_real(&self, mapping: &CoordsMapping) -> Self { Self { ui_space: mapping.virtual_to_real_rect(self.ui_space, false), items: self .items .iter() .map(|(k, v)| (k.to_owned(), v.virtual_to_real(mapping))) .collect::>(), } } pub fn real_to_virtual(&self, mapping: &CoordsMapping) -> Self { Self { ui_space: mapping.real_to_virtual_rect(self.ui_space, false), items: self .items .iter() .map(|(k, v)| (k.to_owned(), v.real_to_virtual(mapping))) .collect::>(), } } pub fn rect_relative_to(&self, id: &WidgetId, to: &WidgetId) -> Option { let a = self.items.get(id)?; let b = self.items.get(to)?; let x = a.ui_space.left - b.ui_space.left; let y = a.ui_space.top - b.ui_space.top; Some(Rect { left: x, right: x + a.ui_space.width(), top: y, bottom: y + a.ui_space.height(), }) } } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct LayoutNode { pub id: WidgetId, pub local_space: Rect, pub children: Vec, } impl LayoutNode { pub fn count(&self) -> usize { 1 + self.children.iter().map(Self::count).sum::() } } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct LayoutItem { pub local_space: Rect, pub ui_space: Rect, pub parent: Option, } impl LayoutItem { pub fn virtual_to_real(&self, mapping: &CoordsMapping) -> Self { Self { local_space: mapping.virtual_to_real_rect(self.local_space, true), ui_space: mapping.virtual_to_real_rect(self.ui_space, false), parent: self.parent.to_owned(), } } pub fn real_to_virtual(&self, mapping: &CoordsMapping) -> Self { Self { local_space: mapping.real_to_virtual_rect(self.local_space, true), ui_space: mapping.real_to_virtual_rect(self.ui_space, false), parent: self.parent.to_owned(), } } } impl LayoutEngine<()> for () { fn layout(&mut self, mapping: &CoordsMapping, _: &WidgetUnit) -> Result { Ok(Layout { ui_space: mapping.virtual_area(), items: Default::default(), }) } } #[derive(Debug, Default, Copy, Clone, Serialize, Deserialize)] pub enum CoordsMappingScaling { #[default] None, Constant(Scalar), Stretch(Vec2), FitHorizontal(Scalar), FitVertical(Scalar), FitMinimum(Vec2), FitMaximum(Vec2), FitToView(Vec2, bool), } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct CoordsMapping { #[serde(default)] scale: Vec2, #[serde(default)] offset: Vec2, #[serde(default)] real_area: Rect, #[serde(default)] virtual_area: Rect, } impl Default for CoordsMapping { fn default() -> Self { Self::new(Default::default()) } } impl CoordsMapping { pub fn new(real_area: Rect) -> Self { Self::new_scaling(real_area, CoordsMappingScaling::None) } pub fn new_scaling(real_area: Rect, scaling: CoordsMappingScaling) -> Self { match scaling { CoordsMappingScaling::None => Self { scale: 1.0.into(), offset: Vec2::default(), real_area, virtual_area: Rect { left: 0.0, right: real_area.width(), top: 0.0, bottom: real_area.height(), }, }, CoordsMappingScaling::Constant(value) => { let value = if value > 0.0 { value } else { 1.0 }; Self { scale: value.into(), offset: Vec2::default(), real_area, virtual_area: Rect { left: 0.0, right: real_area.width() / value, top: 0.0, bottom: real_area.height() / value, }, } } CoordsMappingScaling::Stretch(size) => { let vw = size.x; let vh = size.y; let rw = real_area.width(); let rh = real_area.height(); let scale_x = rw / vw; let scale_y = rh / vh; let w = vw * scale_x; let h = vh * scale_y; Self { scale: Vec2 { x: scale_x, y: scale_y, }, offset: Vec2 { x: (rw - w) * 0.5, y: (rh - h) * 0.5, }, real_area, virtual_area: Rect { left: 0.0, right: vw, top: 0.0, bottom: vh, }, } } CoordsMappingScaling::FitHorizontal(vw) => { let rw = real_area.width(); let rh = real_area.height(); let scale = rw / vw; let vh = rh / scale; Self { scale: scale.into(), offset: Vec2::default(), real_area, virtual_area: Rect { left: 0.0, right: vw, top: 0.0, bottom: vh, }, } } CoordsMappingScaling::FitVertical(vh) => { let rw = real_area.width(); let rh = real_area.height(); let scale = rh / vh; let vw = rw / scale; Self { scale: scale.into(), offset: Vec2::default(), real_area, virtual_area: Rect { left: 0.0, right: vw, top: 0.0, bottom: vh, }, } } CoordsMappingScaling::FitMinimum(size) => { if size.x < size.y { Self::new_scaling(real_area, CoordsMappingScaling::FitHorizontal(size.x)) } else { Self::new_scaling(real_area, CoordsMappingScaling::FitVertical(size.y)) } } CoordsMappingScaling::FitMaximum(size) => { if size.x > size.y { Self::new_scaling(real_area, CoordsMappingScaling::FitHorizontal(size.x)) } else { Self::new_scaling(real_area, CoordsMappingScaling::FitVertical(size.y)) } } CoordsMappingScaling::FitToView(size, keep_aspect_ratio) => { let rw = real_area.width(); let rh = real_area.height(); let av = size.x / size.y; let ar = rw / rh; let (scale, vw, vh) = if keep_aspect_ratio { let vw = size.x; let vh = size.y; let scale = if ar >= av { rh / vh } else { rw / vw }; (scale, vw, vh) } else if ar >= av { (rh / size.y, size.x * rw / rh, size.y) } else { (rw / size.x, size.x, size.y * rh / rw) }; let w = vw * scale; let h = vh * scale; Self { scale: scale.into(), offset: Vec2 { x: (rw - w) * 0.5, y: (rh - h) * 0.5, }, real_area, virtual_area: Rect { left: 0.0, right: vw, top: 0.0, bottom: vh, }, } } } } #[inline] pub fn scale(&self) -> Vec2 { self.scale } #[inline] pub fn scalar_scale(&self, max: bool) -> Scalar { if max { self.scale.x.max(self.scale.y) } else { self.scale.x.min(self.scale.y) } } #[inline] pub fn offset(&self) -> Vec2 { self.offset } #[inline] pub fn virtual_area(&self) -> Rect { self.virtual_area } #[inline] pub fn virtual_to_real_vec2(&self, coord: Vec2, local_space: bool) -> Vec2 { if local_space { Vec2 { x: coord.x * self.scale.x, y: coord.y * self.scale.y, } } else { Vec2 { x: self.offset.x + (coord.x * self.scale.x), y: self.offset.y + (coord.y * self.scale.y), } } } #[inline] pub fn real_to_virtual_vec2(&self, coord: Vec2, local_space: bool) -> Vec2 { if local_space { Vec2 { x: coord.x / self.scale.x, y: coord.y / self.scale.y, } } else { Vec2 { x: (coord.x - self.offset.x) / self.scale.x, y: (coord.y - self.offset.y) / self.scale.y, } } } #[inline] pub fn virtual_to_real_rect(&self, area: Rect, local_space: bool) -> Rect { if local_space { Rect { left: area.left * self.scale.x, right: area.right * self.scale.x, top: area.top * self.scale.y, bottom: area.bottom * self.scale.y, } } else { Rect { left: self.offset.x + (area.left * self.scale.x), right: self.offset.x + (area.right * self.scale.x), top: self.offset.y + (area.top * self.scale.y), bottom: self.offset.y + (area.bottom * self.scale.y), } } } #[inline] pub fn real_to_virtual_rect(&self, area: Rect, local_space: bool) -> Rect { if local_space { Rect { left: area.left / self.scale.x, right: area.right / self.scale.x, top: area.top / self.scale.y, bottom: area.bottom / self.scale.y, } } else { Rect { left: (area.left - self.offset.x) / self.scale.x, right: (area.right - self.offset.x) / self.scale.x, top: (area.top - self.offset.y) / self.scale.y, bottom: (area.bottom - self.offset.y) / self.scale.y, } } } } ================================================ FILE: crates/core/src/lib.rs ================================================ //! RAUI core types and components //! //! The things that most users will be interested in here are the [components][widget::component]. //! Those have more documentation on how to use widgets, components, etc. in your app. pub mod application; #[macro_use] pub mod messenger; #[macro_use] pub mod props; pub mod renderer; pub mod state; #[macro_use] pub mod widget; pub mod animator; pub mod interactive; pub mod layout; pub mod signals; pub mod tester; pub mod view_model; pub type Scalar = f32; pub type Integer = i32; pub type UnsignedInteger = u32; pub use intuicio_data::{ lifetime::*, managed::{gc::*, value::*, *}, type_hash::*, }; pub use raui_derive::*; use serde::{Serialize, de::DeserializeOwned}; #[doc(inline)] pub use serde_json::{Number as PrefabNumber, Value as PrefabValue}; /// An error that can occur while processing a [`Prefab`] #[derive(Debug, Clone)] pub enum PrefabError { CouldNotSerialize(String), CouldNotDeserialize(String), } /// The [`Prefab`] trait is implemented for types that are able to translate to and from /// [`PrefabValue`]'s /// /// [`PrefabValue`]'s can then, in turn, be serialized or deserialized for persistance, transfer, or /// other purposes. pub trait Prefab: Serialize + DeserializeOwned { fn from_prefab(data: PrefabValue) -> Result { match serde_json::from_value(data) { Ok(result) => Ok(result), Err(error) => Err(PrefabError::CouldNotDeserialize(error.to_string())), } } fn to_prefab(&self) -> Result { match serde_json::to_value(self) { Ok(result) => Ok(result), Err(error) => Err(PrefabError::CouldNotSerialize(error.to_string())), } } } #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum LogKind { Info, Warning, Error, } /// Common logging interface that custom log engines should follow to enable their reusability /// across different modules that will log messages to text output targets. /// Objects that implement this trait should be considered text output targets, for example text /// streams, terminal, network-based loggers, even application screen. pub trait Logger { /// Log message to this type of text output target. /// /// # Arguments /// * `kind` - Kind of log message. /// * `message` - Message string slice. fn log(&mut self, _kind: LogKind, _message: &str) {} } impl Logger for () {} /// Prints log messages to terminal via println! macro. pub struct PrintLogger; impl Logger for PrintLogger { fn log(&mut self, kind: LogKind, message: &str) { println!("{kind:?} | {message}"); } } ================================================ FILE: crates/core/src/messenger.rs ================================================ //! Widget messaging use crate::widget::WidgetId; use intuicio_data::type_hash::TypeHash; use std::{any::Any, sync::mpsc::Sender}; pub trait MessageData: std::fmt::Debug + Send + Sync { fn clone_message(&self) -> Box; fn as_any(&self) -> &dyn Any; fn type_hash(&self) -> TypeHash { TypeHash::of::() } } impl Clone for Box { fn clone(&self) -> Self { self.clone_message() } } pub type Message = Box; pub type Messages = Vec; #[derive(Clone)] pub struct MessageSender(Sender<(WidgetId, Message)>); impl MessageSender { pub fn new(sender: Sender<(WidgetId, Message)>) -> Self { Self(sender) } pub fn write(&self, id: WidgetId, message: T) -> bool where T: 'static + MessageData, { self.0.send((id, Box::new(message))).is_ok() } pub fn write_raw(&self, id: WidgetId, message: Message) -> bool { self.0.send((id, message)).is_ok() } pub fn write_raw_all(&self, messages: I) where I: IntoIterator, { for data in messages { let _ = self.0.send(data); } } } pub struct Messenger<'a> { sender: MessageSender, pub messages: &'a [Message], } impl<'a> Messenger<'a> { pub fn new(sender: MessageSender, messages: &'a [Message]) -> Self { Self { sender, messages } } pub fn write(&self, id: WidgetId, message: T) -> bool where T: 'static + MessageData, { self.sender.write(id, message) } pub fn write_raw(&self, id: WidgetId, message: Message) -> bool { self.sender.write_raw(id, message) } pub fn write_raw_all(&self, messages: I) where I: IntoIterator, { self.sender.write_raw_all(messages); } } /// Macro for implementing [`MessageData`]. /// /// You may prefer to use the [derive macro][`macro@crate::MessageData`] instead, but in case of /// auto-implementing MessageData for remote or std types, this might be the macro you find useful. #[macro_export] macro_rules! implement_message_data { ($type_name:ty) => { impl $crate::messenger::MessageData for $type_name where Self: Clone, { fn clone_message(&self) -> Box { Box::new(self.clone()) } fn as_any(&self) -> &dyn Any { self } } }; } implement_message_data!(()); implement_message_data!(i8); implement_message_data!(i16); implement_message_data!(i32); implement_message_data!(i64); implement_message_data!(i128); implement_message_data!(u8); implement_message_data!(u16); implement_message_data!(u32); implement_message_data!(u64); implement_message_data!(u128); implement_message_data!(f32); implement_message_data!(f64); implement_message_data!(bool); implement_message_data!(String); ================================================ FILE: crates/core/src/props.rs ================================================ //! Widget property types use crate::{Prefab, PrefabError, PrefabValue}; use intuicio_data::type_hash::TypeHash; use serde::{Deserialize, Serialize}; use std::{ any::{Any, type_name}, collections::HashMap, }; type PropsSerializeFactory = Box Result + Send + Sync>; type PropsDeserializeFactory = Box Result<(), PrefabError> + Send + Sync>; #[derive(Default)] pub struct PropsRegistry { type_mapping: HashMap, factories: HashMap, } impl PropsRegistry { pub fn register_factory(&mut self, name: &str) where T: 'static + Prefab + PropsData, { let s: PropsSerializeFactory = Box::new(move |data| { if let Some(data) = data.as_any().downcast_ref::() { data.to_prefab() } else { Err(PrefabError::CouldNotSerialize( "Could not downcast to concrete type!".to_owned(), )) } }); let d: PropsDeserializeFactory = Box::new(move |data, props| { props.write(T::from_prefab(data)?); Ok(()) }); self.factories.insert(name.to_owned(), (s, d)); self.type_mapping .insert(TypeHash::of::(), name.to_owned()); } pub fn unregister_factory(&mut self, name: &str) { self.factories.remove(name); } pub fn serialize(&self, props: &Props) -> Result { let mut group = PropsGroupPrefab::default(); for (t, p) in &props.0 { if let Some(name) = self.type_mapping.get(t) { if let Some(factory) = self.factories.get(name) { group.data.insert(name.to_owned(), (factory.0)(p.as_ref())?); } } else { return Err(PrefabError::CouldNotSerialize( "No type mapping found!".to_owned(), )); } } group.to_prefab() } pub fn deserialize(&self, data: PrefabValue) -> Result { let data = if data.is_null() { PropsGroupPrefab::default() } else { PropsGroupPrefab::from_prefab(data)? }; let mut props = Props::default(); for (key, value) in data.data { if let Some(factory) = self.factories.get(&key) { (factory.1)(value, &mut props)?; } else { return Err(PrefabError::CouldNotDeserialize(format!( "Could not find properties factory: {key:?}" ))); } } Ok(props) } } #[derive(Debug, Clone)] pub enum PropsError { CouldNotReadData, HasNoDataOfType(String), } impl Prefab for PrefabValue {} impl PropsData for PrefabValue where Self: Clone, { fn clone_props(&self) -> Box { Box::new(self.clone()) } fn as_any(&self) -> &dyn Any { self } } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct PropsGroupPrefab { #[serde(default)] #[serde(skip_serializing_if = "HashMap::is_empty")] pub data: HashMap, } impl Prefab for PropsGroupPrefab {} impl PropsData for PropsGroupPrefab where Self: Clone, { fn clone_props(&self) -> Box { Box::new(self.clone()) } fn as_any(&self) -> &dyn Any { self } } pub trait PropsData: Any + std::fmt::Debug + Send + Sync { fn clone_props(&self) -> Box; fn as_any(&self) -> &dyn Any; fn type_hash(&self) -> TypeHash { TypeHash::of::() } } impl Clone for Box { fn clone(&self) -> Self { self.clone_props() } } #[derive(Default, Clone)] pub struct Props(HashMap>); impl Props { pub fn new(data: T) -> Self where T: 'static + PropsData, { let mut result = HashMap::with_capacity(1); result.insert(TypeHash::of::(), Box::new(data) as Box); Self(result) } pub fn is_empty(&self) -> bool { self.0.is_empty() } pub fn has(&self) -> bool where T: 'static + PropsData, { let e = TypeHash::of::(); self.0.iter().any(|(t, _)| *t == e) } pub fn remove(&mut self) where T: 'static + PropsData, { self.0.remove(&TypeHash::of::()); } pub(crate) unsafe fn remove_by_type(&mut self, id: TypeHash) { self.0.remove(&id); } pub fn consume(&mut self) -> Result, PropsError> where T: 'static + PropsData, { if let Some(v) = self.0.remove(&TypeHash::of::()) { Ok(v) } else { Err(PropsError::HasNoDataOfType(type_name::().to_owned())) } } pub fn consume_unwrap_cloned(&mut self) -> Result where T: 'static + PropsData + Clone, { if let Some(data) = self.consume::()?.as_any().downcast_ref::() { Ok(data.clone()) } else { Err(PropsError::CouldNotReadData) } } pub fn read(&self) -> Result<&T, PropsError> where T: 'static + PropsData, { let e = TypeHash::of::(); if let Some((_, v)) = self.0.iter().find(|(t, _)| **t == e) { if let Some(data) = v.as_any().downcast_ref::() { Ok(data) } else { Err(PropsError::CouldNotReadData) } } else { Err(PropsError::HasNoDataOfType(type_name::().to_owned())) } } pub fn map_or_default(&self, mut f: F) -> R where T: 'static + PropsData, R: Default, F: FnMut(&T) -> R, { match self.read() { Ok(data) => f(data), Err(_) => R::default(), } } pub fn map_or_else(&self, mut f: F, mut e: E) -> R where T: 'static + PropsData, F: FnMut(&T) -> R, E: FnMut() -> R, { match self.read() { Ok(data) => f(data), Err(_) => e(), } } pub fn read_cloned(&self) -> Result where T: 'static + PropsData + Clone, { self.read::().cloned() } pub fn read_cloned_or_default(&self) -> T where T: 'static + PropsData + Clone + Default, { self.read_cloned().unwrap_or_default() } pub fn read_cloned_or_else(&self, mut f: F) -> T where T: 'static + PropsData + Clone + Default, F: FnMut() -> T, { self.read_cloned().unwrap_or_else(|_| f()) } pub fn write(&mut self, data: T) where T: 'static + PropsData, { self.0 .insert(TypeHash::of::(), Box::new(data) as Box); } pub fn mutate(&mut self, mut f: F) where T: 'static + PropsData, F: FnMut(&T) -> T, { if let Ok(data) = self.read() { let data = f(data); self.write(data); } } pub fn mutate_cloned(&mut self, mut f: F) where T: 'static + PropsData + Clone, F: FnMut(&mut T), { if let Ok(data) = self.read::() { let mut data = data.clone(); f(&mut data); self.write(data); } } pub fn mutate_or_write(&mut self, mut f: F, mut w: W) where T: 'static + PropsData, F: FnMut(&T) -> T, W: FnMut() -> T, { if let Ok(data) = self.read() { let data = f(data); self.write(data); } else { let data = w(); self.write(data); } } pub fn with(mut self, data: T) -> Self where T: 'static + PropsData, { self.write(data); self } pub fn without(mut self) -> Self where T: 'static + PropsData, { self.0.remove(&TypeHash::of::()); self } pub fn merge(self, other: Self) -> Self { let mut result = self.into_inner(); result.extend(other.into_inner()); Self(result) } pub fn merge_from(&mut self, other: Self) { self.0.extend(other.into_inner()); } pub(crate) fn into_inner(self) -> HashMap> { self.0 } } impl std::fmt::Debug for Props { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str("Props ")?; f.debug_set().entries(self.0.values()).finish() } } impl From for Props where T: 'static + PropsData, { fn from(data: T) -> Self { Self::new(data) } } impl From<&Self> for Props { fn from(data: &Self) -> Self { data.clone() } } /// Macro for implementing [`PropsData`]. /// /// You may prefer to use the [derive macro][`macro@crate::PropsData`] instead, but in case of /// auto-implementing PropsData and Prefab traits for remote or std types, you might find this macro /// useful. #[macro_export] macro_rules! implement_props_data { ($type_name:ty) => { impl $crate::props::PropsData for $type_name where Self: Clone, { fn clone_props(&self) -> Box { Box::new(self.clone()) } fn as_any(&self) -> &dyn std::any::Any { self } } impl $crate::Prefab for $type_name {} }; } implement_props_data!(()); implement_props_data!(i8); implement_props_data!(i16); implement_props_data!(i32); implement_props_data!(i64); implement_props_data!(i128); implement_props_data!(u8); implement_props_data!(u16); implement_props_data!(u32); implement_props_data!(u64); implement_props_data!(u128); implement_props_data!(f32); implement_props_data!(f64); implement_props_data!(isize); implement_props_data!(usize); implement_props_data!(bool); implement_props_data!(String); macro_rules! impl_tuple_props_conversion { ($($id:ident),+) => { #[allow(non_snake_case)] impl<$($id: $crate::props::PropsData),+> From<($($id,)+)> for $crate::props::Props { fn from(($($id,)+): ($($id,)+)) -> $crate::props::Props { let mut result = std::collections::HashMap::default(); $( result.insert( $crate::TypeHash::of::<$id>(), Box::new($id) as Box, ); )+ Self(result) } } #[allow(non_snake_case)] impl<$($id: $crate::props::PropsData + Clone + Default),+> From<&$crate::props::Props> for ($($id,)+) { fn from(props: &$crate::props::Props) -> ($($id,)+) { ( $( props.read_cloned_or_default::<$id>(), )+ ) } } }; } impl_tuple_props_conversion!(A); impl_tuple_props_conversion!(A, B); impl_tuple_props_conversion!(A, B, C); impl_tuple_props_conversion!(A, B, C, D); impl_tuple_props_conversion!(A, B, C, D, E); impl_tuple_props_conversion!(A, B, C, D, E, F); impl_tuple_props_conversion!(A, B, C, D, E, F, G); impl_tuple_props_conversion!(A, B, C, D, E, F, G, H); impl_tuple_props_conversion!(A, B, C, D, E, F, G, H, I); impl_tuple_props_conversion!(A, B, C, D, E, F, G, H, I, J); impl_tuple_props_conversion!(A, B, C, D, E, F, G, H, I, J, K); impl_tuple_props_conversion!(A, B, C, D, E, F, G, H, I, J, K, L); impl_tuple_props_conversion!(A, B, C, D, E, F, G, H, I, J, K, L, M); impl_tuple_props_conversion!(A, B, C, D, E, F, G, H, I, J, K, L, M, N); impl_tuple_props_conversion!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O); impl_tuple_props_conversion!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P); impl_tuple_props_conversion!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q); impl_tuple_props_conversion!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R); impl_tuple_props_conversion!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S); impl_tuple_props_conversion!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T); impl_tuple_props_conversion!( A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U ); impl_tuple_props_conversion!( A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V ); impl_tuple_props_conversion!( A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, X ); impl_tuple_props_conversion!( A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, X, Y ); impl_tuple_props_conversion!( A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, X, Y, Z ); ================================================ FILE: crates/core/src/renderer.rs ================================================ //! Renderer traits use crate::{ layout::{CoordsMapping, Layout}, widget::unit::WidgetUnit, }; pub trait Renderer { fn render( &mut self, tree: &WidgetUnit, mapping: &CoordsMapping, layout: &Layout, ) -> Result; } #[derive(Debug, Default, Copy, Clone)] pub struct RawRenderer; impl Renderer for RawRenderer { fn render( &mut self, tree: &WidgetUnit, _: &CoordsMapping, _: &Layout, ) -> Result { Ok(tree.clone()) } } ================================================ FILE: crates/core/src/signals.rs ================================================ //! Widget signals //! //! Signals are a way for widgets to send [messages][crate::messenger] to the RAUI //! [`Application`][crate::application::Application]. This can be used to create custom integrations //! with the RAUI host or rendering backend. //! //! Signals may be sent using the [`SignalSender`] in the widget [change context][change_context] or //! [unmount context][unmount_context]. //! //! [change_context]: crate::widget::context::WidgetMountOrChangeContext //! //! [unmount_context]: crate::widget::context::WidgetUnmountContext use crate::{ messenger::{Message, MessageData}, widget::WidgetId, }; use std::sync::mpsc::Sender; /// A signal is a [message][crate::messenger] sent by a widget that can be read by the /// [`Application`][crate::application::Application] pub type Signal = (WidgetId, Box); /// Used to send [`Signal`]s from a component [change context][change_context] /// /// [change_context]: crate::widget::context::WidgetMountOrChangeContext #[derive(Clone)] pub struct SignalSender { id: WidgetId, sender: Sender, } impl SignalSender { /// Create a new [`SignalSender`] pub(crate) fn new(id: WidgetId, sender: Sender) -> Self { Self { id, sender } } /// Send a message /// /// Returns `false` if the message could not successfully be sent pub fn write(&self, message: T) -> bool where T: 'static + MessageData, { self.sender .send((self.id.clone(), Box::new(message))) .is_ok() } /// Send a raw [`Message`] /// /// Returns `false` if the message could not be successfully sent pub fn write_raw(&self, message: Message) -> bool { self.sender.send((self.id.clone(), message)).is_ok() } /// Sends a set of raw [`Message`]s from an iterator pub fn write_raw_all(&self, messages: I) where I: IntoIterator, { for data in messages { let _ = self.sender.send((self.id.clone(), data)); } } } ================================================ FILE: crates/core/src/state.rs ================================================ //! Widget state types use crate::props::{Props, PropsData, PropsError}; use intuicio_data::type_hash::TypeHash; use std::sync::mpsc::Sender; #[derive(Debug, Clone)] pub enum StateError { Props(PropsError), CouldNotWriteChange, } #[derive(Debug, Clone)] pub enum StateChange { Set(Props), Include(Props), Exclude(TypeHash), } #[derive(Clone)] pub struct StateUpdate(Sender); impl StateUpdate { pub fn new(sender: Sender) -> Self { Self(sender) } pub fn set(&self, data: T) -> Result<(), StateError> where T: Into, { if self.0.send(StateChange::Set(data.into())).is_err() { Err(StateError::CouldNotWriteChange) } else { Ok(()) } } pub fn include(&self, data: T) -> Result<(), StateError> where T: Into, { let data = data.into(); if self.0.send(StateChange::Include(data)).is_err() { Err(StateError::CouldNotWriteChange) } else { Ok(()) } } pub fn exclude(&self) -> Result<(), StateError> where T: 'static + PropsData, { if self .0 .send(StateChange::Exclude(TypeHash::of::())) .is_err() { Err(StateError::CouldNotWriteChange) } else { Ok(()) } } } pub struct State<'a> { data: &'a Props, update: StateUpdate, } impl<'a> State<'a> { pub fn new(data: &'a Props, update: StateUpdate) -> Self { Self { data, update } } #[inline] pub fn data(&self) -> &Props { self.data } pub fn has(&self) -> bool where T: 'static + PropsData, { self.data.has::() } pub fn read(&self) -> Result<&'a T, StateError> where T: 'static + PropsData, { match self.data.read() { Ok(v) => Ok(v), Err(e) => Err(StateError::Props(e)), } } pub fn map_or_default(&self, f: F) -> R where T: 'static + PropsData, R: Default, F: FnMut(&T) -> R, { self.data.map_or_default(f) } pub fn map_or_else(&self, f: F, e: E) -> R where T: 'static + PropsData, F: FnMut(&T) -> R, E: FnMut() -> R, { self.data.map_or_else(f, e) } pub fn read_cloned(&self) -> Result where T: 'static + PropsData + Clone, { match self.data.read_cloned() { Ok(v) => Ok(v), Err(e) => Err(StateError::Props(e)), } } pub fn read_cloned_or_default(&self) -> T where T: 'static + PropsData + Clone + Default, { self.data.read_cloned_or_default() } pub fn read_cloned_or_else(&self, f: F) -> T where T: 'static + PropsData + Clone + Default, F: FnMut() -> T, { self.data.read_cloned_or_else(f) } pub fn write(&self, data: T) -> Result<(), StateError> where T: 'static + PropsData + Send + Sync, { self.update.set(data) } pub fn write_with(&self, data: T) -> Result<(), StateError> where T: 'static + PropsData + Send + Sync, { self.update.include(data) } pub fn write_without(&self) -> Result<(), StateError> where T: 'static + PropsData + Send + Sync, { self.update.exclude::() } pub fn mutate(&self, mut f: F) -> Result<(), StateError> where T: 'static + PropsData + Send + Sync, F: FnMut(&T) -> T, { match self.read() { Ok(data) => { let data = f(data); self.write(data) } Err(error) => Err(error), } } pub fn mutate_cloned(&self, mut f: F) -> Result<(), StateError> where T: 'static + PropsData + Send + Sync + Clone, F: FnMut(&mut T), { match self.read::() { Ok(data) => { let mut data = data.clone(); f(&mut data); self.write(data) } Err(error) => Err(error), } } pub fn update(&self) -> &StateUpdate { &self.update } } ================================================ FILE: crates/core/src/tester.rs ================================================ use crate::{ application::Application, interactive::default_interactions_engine::DefaultInteractionsEngine, layout::{CoordsMapping, default_layout_engine::DefaultLayoutEngine}, }; pub trait AppCycleFrameRunner { fn run_frame(self, tester: &mut AppCycleTester); } impl AppCycleFrameRunner for () { fn run_frame(self, _: &mut AppCycleTester) {} } impl AppCycleFrameRunner for F where F: FnMut(&mut AppCycleTester), { fn run_frame(mut self, tester: &mut AppCycleTester) { (self)(tester); } } pub struct AppCycleTester { pub coords_mapping: CoordsMapping, pub application: Application, pub layout_engine: DefaultLayoutEngine, pub interactions_engine: DefaultInteractionsEngine, pub user_data: T, } impl AppCycleTester { pub fn new(coords_mapping: CoordsMapping, user_data: T) -> Self { Self { coords_mapping, application: Default::default(), layout_engine: Default::default(), interactions_engine: Default::default(), user_data, } } pub fn run_frame(&mut self, frame_runner: impl AppCycleFrameRunner) { frame_runner.run_frame(self); if self.application.process() { self.application .layout(&self.coords_mapping, &mut self.layout_engine) .unwrap(); } self.application .interact(&mut self.interactions_engine) .unwrap(); } } ================================================ FILE: crates/core/src/view_model.rs ================================================ use crate::widget::{WidgetId, WidgetIdCommon}; use intuicio_data::{ lifetime::{ValueReadAccess, ValueWriteAccess}, managed::DynamicManaged, managed::{Managed, ManagedLazy, ManagedRef, ManagedRefMut}, }; use std::{ collections::{HashMap, HashSet}, ops::{Deref, DerefMut}, }; pub struct ViewModelBindings { widgets: HashSet, common_root: WidgetIdCommon, notify: bool, } impl Default for ViewModelBindings { fn default() -> Self { Self { widgets: Default::default(), common_root: Default::default(), notify: true, } } } impl ViewModelBindings { pub fn bind(&mut self, id: WidgetId) { self.widgets.insert(id); self.rebuild_common_root(); } pub fn unbind(&mut self, id: &WidgetId) { self.widgets.remove(id); self.rebuild_common_root(); } pub fn clear(&mut self) { self.widgets.clear(); self.common_root = Default::default(); } pub fn is_empty(&self) -> bool { self.widgets.is_empty() } pub fn is_bound(&self, id: &WidgetId) -> bool { self.widgets.contains(id) } pub fn widgets(&self) -> impl Iterator { self.widgets.iter() } pub fn common_root(&self) -> &WidgetIdCommon { &self.common_root } pub fn notify(&mut self) { self.notify = true; } pub fn is_notified(&self) -> bool { self.notify } pub fn consume_notification(&mut self) -> bool { !self.widgets.is_empty() && std::mem::take(&mut self.notify) } fn rebuild_common_root(&mut self) { self.common_root = WidgetIdCommon::from_iter(self.widgets.iter()); } } #[derive(Default)] pub struct ViewModelProperties { inner: HashMap>, } impl ViewModelProperties { pub fn unbind_all(&mut self, id: &WidgetId) { for bindings in self.inner.values_mut() { if let Some(mut bindings) = bindings.write() { bindings.unbind(id); } } } pub fn remove(&mut self, id: &str) { self.inner.remove(id); } pub fn clear(&mut self) { self.inner.clear(); } pub fn is_empty(&self) -> bool { self.inner.is_empty() } pub fn has(&self, id: &str) -> bool { self.inner.contains_key(id) } pub fn remove_empty_bindings(&mut self) { let to_remove = self .inner .iter() .filter_map(|(key, bindings)| { if let Some(bindings) = bindings.read() && bindings.is_empty() { return Some(key.to_owned()); } None }) .collect::>(); for key in to_remove { self.inner.remove(&key); } } pub fn bindings( &'_ mut self, id: impl ToString, ) -> Option> { self.inner.entry(id.to_string()).or_default().write() } pub fn notifier(&mut self, id: impl ToString) -> ViewModelNotifier { ViewModelNotifier { inner: self.inner.entry(id.to_string()).or_default().lazy(), } } pub fn consume_notification(&mut self) -> bool { self.inner.values_mut().any(|bindings| { bindings .write() .map(|mut bindings| bindings.consume_notification()) .unwrap_or_default() }) } pub fn consume_notified_common_root(&mut self) -> WidgetIdCommon { let mut result = WidgetIdCommon::default(); for bindings in self.inner.values_mut() { if let Some(mut bindings) = bindings.write() && bindings.consume_notification() { let root = bindings.common_root(); result.include_other(root); } } result } } #[derive(Clone)] pub struct ViewModelNotifier { inner: ManagedLazy, } impl ViewModelNotifier { pub fn notify(&mut self) -> bool { if let Some(mut bindings) = self.inner.write() { bindings.notify(); true } else { false } } } pub struct ViewModel { object: DynamicManaged, pub properties: ViewModelProperties, } impl ViewModel { pub fn new(object: T, properties: ViewModelProperties) -> Self { Self { object: DynamicManaged::new(object).ok().unwrap(), properties, } } pub fn new_object(object: T) -> Self { Self::new(object, Default::default()) } pub fn produce(producer: impl FnOnce(&mut ViewModelProperties) -> T) -> Self { let mut properties = Default::default(); let object = DynamicManaged::new(producer(&mut properties)).ok().unwrap(); Self { object, properties } } pub fn borrow(&self) -> Option> { self.object .borrow() .and_then(|object| object.into_typed::().ok()) } pub fn borrow_mut(&mut self) -> Option> { self.object .borrow_mut() .and_then(|object| object.into_typed::().ok()) } pub fn lazy(&self) -> Option> { self.object.lazy().into_typed::().ok() } pub fn read(&'_ self) -> Option> { self.object.read::() } pub fn write(&'_ mut self) -> Option> { self.object.write::() } pub fn write_notified(&'_ mut self) -> Option> { if let Some(access) = self.object.write::() { Some(ViewModelObject { access, notifier: self.properties.notifier(""), }) } else { None } } } #[derive(Default)] pub struct ViewModelCollection { named: HashMap, widgets: HashMap>, } impl ViewModelCollection { pub fn unbind_all(&mut self, id: &WidgetId) { for view_model in self.named.values_mut() { view_model.properties.unbind_all(id); } for view_model in self.widgets.values_mut() { for view_model in view_model.values_mut() { view_model.properties.unbind_all(id); } } } pub fn remove_empty_bindings(&mut self) { for view_model in self.named.values_mut() { view_model.properties.remove_empty_bindings(); } for view_model in self.widgets.values_mut() { for view_model in view_model.values_mut() { view_model.properties.remove_empty_bindings(); } } } pub fn consume_notification(&mut self) -> bool { let mut result = false; for view_model in self.named.values_mut() { result = result || view_model.properties.consume_notification(); } for view_model in self.widgets.values_mut() { for view_model in view_model.values_mut() { result = result || view_model.properties.consume_notification(); } } result } pub fn consume_notified_common_root(&mut self) -> WidgetIdCommon { let mut result = WidgetIdCommon::default(); for view_model in self.named.values_mut() { result.include_other(&view_model.properties.consume_notified_common_root()); } for view_model in self.widgets.values_mut() { for view_model in view_model.values_mut() { result.include_other(&view_model.properties.consume_notified_common_root()); } } result } pub fn remove_widget_view_models(&mut self, id: &WidgetId) { self.widgets.remove(id); } } impl Deref for ViewModelCollection { type Target = HashMap; fn deref(&self) -> &Self::Target { &self.named } } impl DerefMut for ViewModelCollection { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.named } } pub struct ViewModelCollectionView<'a> { id: &'a WidgetId, collection: &'a mut ViewModelCollection, } impl<'a> ViewModelCollectionView<'a> { pub fn new(id: &'a WidgetId, collection: &'a mut ViewModelCollection) -> Self { Self { id, collection } } pub fn id(&self) -> &WidgetId { self.id } pub fn collection(&'a self) -> &'a ViewModelCollection { self.collection } pub fn collection_mut(&'a mut self) -> &'a mut ViewModelCollection { self.collection } pub fn bindings( &'_ mut self, view_model: &str, property: impl ToString, ) -> Option> { self.collection .get_mut(view_model)? .properties .bindings(property) } pub fn view_model(&self, name: &str) -> Option<&ViewModel> { self.collection.get(name) } pub fn view_model_mut(&mut self, name: &str) -> Option<&mut ViewModel> { self.collection.get_mut(name) } pub fn widget_register(&mut self, name: impl ToString, view_model: ViewModel) { self.collection .widgets .entry(self.id.to_owned()) .or_default() .insert(name.to_string(), view_model); } pub fn widget_unregister(&mut self, name: &str) -> Option { let view_models = self.collection.widgets.get_mut(self.id)?; let result = view_models.remove(name)?; if view_models.is_empty() { self.collection.widgets.remove(self.id); } Some(result) } pub fn widget_bindings( &'_ mut self, view_model: &str, property: impl ToString, ) -> Option> { self.collection .widgets .get_mut(self.id)? .get_mut(view_model)? .properties .bindings(property) } pub fn widget_view_model(&self, name: &str) -> Option<&ViewModel> { self.collection.widgets.get(self.id)?.get(name) } pub fn widget_view_model_mut(&mut self, name: &str) -> Option<&mut ViewModel> { self.collection.widgets.get_mut(self.id)?.get_mut(name) } pub fn hierarchy_view_model(&self, name: &str) -> Option<&ViewModel> { self.collection .widgets .iter() .filter_map(|(id, view_models)| { id.distance_to(self.id).ok().and_then(|distance| { if distance <= 0 { Some((distance, view_models.get(name)?)) } else { None } }) }) .min_by(|(a, _), (b, _)| a.cmp(b)) .map(|(_, view_model)| view_model) } pub fn hierarchy_view_model_mut(&mut self, name: &str) -> Option<&mut ViewModel> { self.collection .widgets .iter_mut() .filter_map(|(id, view_models)| { id.distance_to(self.id).ok().and_then(|distance| { if distance <= 0 { Some((distance, view_models.get_mut(name)?)) } else { None } }) }) .min_by(|(a, _), (b, _)| a.cmp(b)) .map(|(_, view_model)| view_model) } } pub struct ViewModelObject<'a, T> { access: ValueWriteAccess<'a, T>, notifier: ViewModelNotifier, } impl ViewModelObject<'_, T> { pub fn set_unique_notify(&mut self, value: T) where T: PartialEq, { if *self.access != value { *self.access = value; self.notifier.notify(); } } } impl Deref for ViewModelObject<'_, T> { type Target = T; fn deref(&self) -> &Self::Target { &self.access } } impl DerefMut for ViewModelObject<'_, T> { fn deref_mut(&mut self) -> &mut Self::Target { self.notifier.notify(); &mut self.access } } pub struct ViewModelValue { value: T, notifier: ViewModelNotifier, } impl ViewModelValue { pub fn new(value: T, notifier: ViewModelNotifier) -> Self { Self { value, notifier } } pub fn consume(self) -> T { self.value } pub fn set_unique_notify(&mut self, value: T) where T: PartialEq, { if self.value != value { self.value = value; self.notifier.notify(); } } } impl Deref for ViewModelValue { type Target = T; fn deref(&self) -> &Self::Target { &self.value } } impl DerefMut for ViewModelValue { fn deref_mut(&mut self) -> &mut Self::Target { self.notifier.notify(); &mut self.value } } impl std::fmt::Display for ViewModelValue where T: std::fmt::Display, { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.value) } } impl std::fmt::Debug for ViewModelValue where T: std::fmt::Debug, { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("ViewModelValue") .field("value", &self.value) .finish() } } #[cfg(test)] mod tests { use super::*; use std::str::FromStr; const FOO_VIEW_MODEL: &str = "foo"; const COUNTER_PROPERTY: &str = "counter"; const FLAG_PROPERTY: &str = "flag"; // view-model data type struct Foo { // can hold view-model value wrapper that implicitly notifies on mutation. counter: ViewModelValue, // or can hold raw notifiers to explicitly notify. flag: bool, flag_notifier: ViewModelNotifier, } impl Foo { fn toggle(&mut self) { self.flag = !self.flag; self.flag_notifier.notify(); } } #[test] fn test_view_model() { let a = WidgetId::from_str("a:root/a").unwrap(); let b = WidgetId::from_str("b:root/b").unwrap(); let mut collection = ViewModelCollection::default(); // create new view-model and add it to collection. // `produce` method allows to setup notifiers as we construct view-model. let view_model = ViewModel::produce(|properties| Foo { counter: ViewModelValue::new(0, properties.notifier(COUNTER_PROPERTY)), flag: false, flag_notifier: properties.notifier(FLAG_PROPERTY), }); // handle to view-model data we can use to share around. // it stays alive as long as its view-model object. let handle = view_model.lazy::().unwrap(); collection.insert(FOO_VIEW_MODEL.to_owned(), view_model); // unbound properties won't trigger notification until we bind widgets to them. assert!(!collection.consume_notified_common_root().is_valid()); handle.write().unwrap().toggle(); assert!(!collection.consume_notified_common_root().is_valid()); assert!( collection .get_mut(FOO_VIEW_MODEL) .unwrap() .properties .bindings(COUNTER_PROPERTY) .unwrap() .is_notified() ); assert!( collection .get_mut(FOO_VIEW_MODEL) .unwrap() .properties .bindings(FLAG_PROPERTY) .unwrap() .is_notified() ); // bind widget to properties. // whenever property gets notified, its widgets will rebuild. collection .get_mut(FOO_VIEW_MODEL) .unwrap() .properties .bindings(COUNTER_PROPERTY) .unwrap() .bind(a); collection .get_mut(FOO_VIEW_MODEL) .unwrap() .properties .bindings(FLAG_PROPERTY) .unwrap() .bind(b); // once we bind properties, notification will be triggered. assert_eq!( collection.consume_notified_common_root().path(), Some("root") ); // automatically notify on view-model value mutation. *handle.write().unwrap().counter += 1; assert_eq!( collection.consume_notified_common_root().path(), Some("root/a"), ); // proxy notify via view-model method call. handle.write().unwrap().toggle(); assert_eq!( collection.consume_notified_common_root().path(), Some("root/b"), ); // rebuilding widgets tree will occur always from common root of notified widgets. *handle.write().unwrap().counter += 1; handle.write().unwrap().toggle(); assert_eq!( collection.consume_notified_common_root().path(), Some("root"), ); } } ================================================ FILE: crates/core/src/widget/component/containers/anchor_box.rs ================================================ use crate::{ MessageData, PropsData, messenger::MessageData, pre_hooks, unpack_named_slots, widget::{ WidgetId, WidgetIdOrRef, component::{RelativeLayoutListenerSignal, use_relative_layout_listener}, context::{WidgetContext, WidgetMountOrChangeContext}, node::WidgetNode, unit::{area::AreaBoxNode, content::ContentBoxItemLayout}, utils::{Rect, Vec2, lerp}, }, }; use serde::{Deserialize, Serialize}; #[derive(PropsData, Debug, Default, Copy, Clone, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct AnchorProps { #[serde(default)] pub outer_box_size: Vec2, #[serde(default)] pub inner_box_rect: Rect, } #[derive(PropsData, Debug, Default, Clone, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct AnchorNotifyProps( #[serde(default)] #[serde(skip_serializing_if = "WidgetIdOrRef::is_none")] pub WidgetIdOrRef, ); #[derive(MessageData, Debug, Clone)] #[message_data(crate::messenger::MessageData)] pub struct AnchorNotifyMessage { pub sender: WidgetId, pub state: AnchorProps, pub prev: AnchorProps, } #[derive(PropsData, Debug, Copy, Clone, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct PivotBoxProps { #[serde(default = "PivotBoxProps::default_pivot")] pub pivot: Vec2, #[serde(default)] pub align: Vec2, } impl Default for PivotBoxProps { fn default() -> Self { Self { pivot: Self::default_pivot(), align: Default::default(), } } } impl PivotBoxProps { fn default_pivot() -> Vec2 { Vec2 { x: 0.0, y: 1.0 } } } pub fn use_anchor_box_notified_state(context: &mut WidgetContext) { context.life_cycle.change(|context| { for msg in context.messenger.messages { if let Some(msg) = msg.as_any().downcast_ref::() { let _ = context.state.write_with(msg.state); } } }); } #[pre_hooks(use_relative_layout_listener)] pub fn use_anchor_box(context: &mut WidgetContext) { fn notify(context: &WidgetMountOrChangeContext, data: T) where T: 'static + MessageData, { if let Ok(AnchorNotifyProps(notify)) = context.props.read() && let Some(to) = notify.read() { context.messenger.write(to, data); } } context.life_cycle.mount(|context| { notify( &context, AnchorNotifyMessage { sender: context.id.to_owned(), state: Default::default(), prev: Default::default(), }, ); let _ = context.state.write_with(AnchorProps::default()); }); context.life_cycle.change(|context| { let mut data = context.state.read_cloned_or_default::(); let prev = data; let mut dirty = false; for msg in context.messenger.messages { if let Some(RelativeLayoutListenerSignal::Change(size, rect)) = msg.as_any().downcast_ref() { data.outer_box_size = *size; data.inner_box_rect = *rect; dirty = true; } } if dirty { notify( &context, AnchorNotifyMessage { sender: context.id.to_owned(), state: data, prev, }, ); let _ = context.state.write_with(data); } }); } #[pre_hooks(use_anchor_box)] pub fn anchor_box(mut context: WidgetContext) -> WidgetNode { let WidgetContext { id, state, named_slots, .. } = context; unpack_named_slots!(named_slots => content); let anchor_props = state.read_cloned_or_default::(); content.remap_props(|props| props.with(anchor_props)); AreaBoxNode { id: id.to_owned(), slot: Box::new(content), } .into() } pub fn pivot_point_to_anchor(pivot: Vec2, anchor: &AnchorProps) -> Vec2 { let x = if anchor.outer_box_size.x > 0.0 { let v = lerp( anchor.inner_box_rect.left, anchor.inner_box_rect.right, pivot.x, ); v / anchor.outer_box_size.x } else { 0.0 }; let y = if anchor.outer_box_size.y > 0.0 { let v = lerp( anchor.inner_box_rect.top, anchor.inner_box_rect.bottom, pivot.y, ); v / anchor.outer_box_size.y } else { 0.0 }; Vec2 { x, y } } /// (anchor point, align factor) pub fn pivot_to_anchor_and_align(pivot: &PivotBoxProps, anchor: &AnchorProps) -> (Vec2, Vec2) { (pivot_point_to_anchor(pivot.pivot, anchor), pivot.align) } pub fn pivot_box(context: WidgetContext) -> WidgetNode { let WidgetContext { id, props, named_slots, .. } = context; unpack_named_slots!(named_slots => content); let anchor_props = props.read_cloned_or_default::(); let pivot_props = props.read_cloned_or_default::(); let (Vec2 { x, y }, align) = pivot_to_anchor_and_align(&pivot_props, &anchor_props); content.remap_props(|content_props| { let mut item_props = content_props.read_cloned_or_default::(); item_props.anchors = Rect { left: x, right: x, top: y, bottom: y, }; item_props.align = align; content_props.with(item_props) }); AreaBoxNode { id: id.to_owned(), slot: Box::new(content), } .into() } ================================================ FILE: crates/core/src/widget/component/containers/area_box.rs ================================================ use crate::{ unpack_named_slots, widget::{context::WidgetContext, node::WidgetNode, unit::area::AreaBoxNode}, }; pub fn area_box(context: WidgetContext) -> WidgetNode { let WidgetContext { id, named_slots, .. } = context; unpack_named_slots!(named_slots => content); AreaBoxNode { id: id.to_owned(), slot: Box::new(content), } .into() } ================================================ FILE: crates/core/src/widget/component/containers/content_box.rs ================================================ //! A generic container for content with optional clipping and transforms use crate::{ PropsData, make_widget, pre_hooks, widget::{ component::interactive::navigation::{ NavContainerActive, NavItemActive, NavJumpActive, use_nav_container_active, use_nav_item, use_nav_jump_direction_active, }, context::WidgetContext, node::WidgetNode, unit::content::{ ContentBoxContentReposition, ContentBoxItemLayout, ContentBoxItemNode, ContentBoxNode, }, utils::Transform, }, }; use serde::{Deserialize, Serialize}; /// The properties of a [`content_box`] component #[derive(PropsData, Debug, Default, Clone, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct ContentBoxProps { /// Whether or not to clip the parts of items that overflow outside of the box bounds #[serde(default)] pub clipping: bool, /// The content repositioning strategy to use. pub content_reposition: ContentBoxContentReposition, /// The transform to apply to the box and it's contents #[serde(default)] pub transform: Transform, } #[pre_hooks(use_nav_container_active, use_nav_jump_direction_active, use_nav_item)] pub fn nav_content_box(mut context: WidgetContext) -> WidgetNode { let WidgetContext { key, props, listed_slots, .. } = context; let props = props .clone() .without::() .without::() .without::(); make_widget!(content_box) .key(key) .merge_props(props) .listed_slots(listed_slots) .into() } /// A generic container for other widgets /// /// [`content_box`]'s serve two basic purposes: allowing you to apply transformations and clipping /// to all contained widgets and giving contained widgets more control over their layout inside of /// the box. /// /// # Transform & Clipping /// /// The transformation and clipping options on the [`content_box`] can be set by setting the /// [`ContentBoxProps`] on the component. /// /// # Child Widget Layout /// /// With a [`content_box`] you can get more control over the layout of it's children by adding the /// [`ContentBoxItemLayout`] properties to any of it's children. pub fn content_box(context: WidgetContext) -> WidgetNode { let WidgetContext { id, props, listed_slots, .. } = context; let ContentBoxProps { clipping, transform, content_reposition, } = props.read_cloned_or_default(); let items = listed_slots .into_iter() .filter_map(|slot| { if let Some(props) = slot.props() { let layout = props.read_cloned_or_default::(); Some(ContentBoxItemNode { slot, layout }) } else { None } }) .collect::>(); ContentBoxNode { id: id.to_owned(), props: props.clone(), items, clipping, content_reposition, transform, } .into() } ================================================ FILE: crates/core/src/widget/component/containers/context_box.rs ================================================ use crate::{ PropsData, make_widget, pre_hooks, unpack_named_slots, widget::{ component::containers::{ anchor_box::{AnchorProps, PivotBoxProps, pivot_to_anchor_and_align, use_anchor_box}, content_box::content_box, portal_box::{portal_box, use_portals_container_relative_layout}, }, context::WidgetContext, node::WidgetNode, unit::{area::AreaBoxNode, content::ContentBoxItemLayout}, utils::{Rect, Vec2}, }, }; use serde::{Deserialize, Serialize}; #[derive(PropsData, Debug, Default, Copy, Clone, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct ContextBoxProps { #[serde(default)] pub show: bool, } #[pre_hooks(use_anchor_box)] pub fn context_box(mut context: WidgetContext) -> WidgetNode { let WidgetContext { id, idref, key, props, state, named_slots, .. } = context; unpack_named_slots!(named_slots => {content, context, backdrop}); let ContextBoxProps { show } = props.read_cloned_or_default(); let anchor_state = state.read_cloned_or_default::(); let pivot_props = props.read_cloned_or_default::(); let (Vec2 { x, y }, align) = pivot_to_anchor_and_align(&pivot_props, &anchor_state); let context = if show { context.remap_props(|content_props| { let mut item_props = content_props.read_cloned_or_default::(); item_props.anchors = Rect { left: x, right: x, top: y, bottom: y, }; item_props.align = align; content_props.with(item_props) }); make_widget!(portal_box) .named_slot( "content", make_widget!(content_box) .key("content") .listed_slot(backdrop) .listed_slot(context), ) .into() } else { WidgetNode::default() }; let content = make_widget!(content_box) .key(key) .maybe_idref(idref.cloned()) .merge_props(props.clone()) .listed_slot(content) .listed_slot(context) .into(); AreaBoxNode { id: id.to_owned(), slot: Box::new(content), } .into() } #[pre_hooks(use_portals_container_relative_layout)] pub fn portals_context_box(mut context: WidgetContext) -> WidgetNode { context_box(context) } ================================================ FILE: crates/core/src/widget/component/containers/flex_box.rs ================================================ use crate::{ PropsData, Scalar, make_widget, pre_hooks, widget::{ component::interactive::navigation::{ NavContainerActive, NavItemActive, NavJumpActive, use_nav_container_active, use_nav_item, use_nav_jump, }, context::WidgetContext, node::WidgetNode, unit::flex::{FlexBoxDirection, FlexBoxItemLayout, FlexBoxItemNode, FlexBoxNode}, utils::Transform, }, }; use serde::{Deserialize, Serialize}; #[derive(PropsData, Debug, Default, Clone, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct FlexBoxProps { #[serde(default)] pub direction: FlexBoxDirection, #[serde(default)] pub separation: Scalar, #[serde(default)] pub wrap: bool, #[serde(default)] pub transform: Transform, } #[pre_hooks(use_nav_container_active, use_nav_jump, use_nav_item)] pub fn nav_flex_box(mut context: WidgetContext) -> WidgetNode { let WidgetContext { key, props, listed_slots, .. } = context; let props = props .clone() .without::() .without::() .without::(); make_widget!(flex_box) .key(key) .merge_props(props) .listed_slots(listed_slots) .into() } pub fn flex_box(context: WidgetContext) -> WidgetNode { let WidgetContext { id, props, listed_slots, .. } = context; let FlexBoxProps { direction, separation, wrap, transform, } = props.read_cloned_or_default(); let items = listed_slots .into_iter() .filter_map(|slot| { if let Some(props) = slot.props() { let layout = props.read_cloned_or_default::(); Some(FlexBoxItemNode { slot, layout }) } else { None } }) .collect::>(); FlexBoxNode { id: id.to_owned(), props: props.clone(), items, direction, separation, wrap, transform, } .into() } ================================================ FILE: crates/core/src/widget/component/containers/float_box.rs ================================================ use crate::{ MessageData, PropsData, Scalar, make_widget, pre_hooks, widget::{ WidgetId, WidgetIdOrRef, component::{ containers::content_box::{ContentBoxProps, content_box}, interactive::navigation::{ NavContainerActive, NavItemActive, NavJumpActive, use_nav_container_active, use_nav_item, use_nav_jump_direction_active, }, }, context::{WidgetContext, WidgetMountOrChangeContext}, node::WidgetNode, unit::content::ContentBoxContentReposition, utils::Vec2, }, }; use serde::{Deserialize, Serialize}; #[derive(PropsData, Debug, Default, Copy, Clone, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct FloatBoxProps { #[serde(default)] pub bounds_left: Option, #[serde(default)] pub bounds_right: Option, #[serde(default)] pub bounds_top: Option, #[serde(default)] pub bounds_bottom: Option, } #[derive(PropsData, Debug, Default, Clone, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct FloatBoxNotifyProps( #[serde(default)] #[serde(skip_serializing_if = "WidgetIdOrRef::is_none")] pub WidgetIdOrRef, ); #[derive(PropsData, Debug, Copy, Clone, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct FloatBoxState { #[serde(default)] pub position: Vec2, #[serde(default = "FloatBoxState::default_zoom")] pub zoom: Scalar, } impl Default for FloatBoxState { fn default() -> Self { Self { position: Default::default(), zoom: Self::default_zoom(), } } } impl FloatBoxState { fn default_zoom() -> Scalar { 1.0 } } #[derive(MessageData, Debug, Default, Clone)] #[message_data(crate::messenger::MessageData)] pub struct FloatBoxNotifyMessage { pub sender: WidgetId, pub state: FloatBoxState, pub prev: FloatBoxState, } #[derive(MessageData, Debug, Clone)] #[message_data(crate::messenger::MessageData)] pub struct FloatBoxChangeMessage { pub sender: WidgetId, pub change: FloatBoxChange, } #[derive(Debug, Clone)] pub enum FloatBoxChange { Absolute(FloatBoxState), RelativePosition(Vec2), RelativeZoom(Scalar), } pub fn use_float_box(context: &mut WidgetContext) { fn notify(context: &WidgetMountOrChangeContext, data: FloatBoxNotifyMessage) { if let Ok(FloatBoxNotifyProps(notify)) = context.props.read() && let Some(to) = notify.read() { context.messenger.write(to, data); } } context.life_cycle.mount(|context| { let props = context.props.read_cloned_or_default::(); let mut data = context.props.read_cloned_or_default::(); if let Some(limit) = props.bounds_left { data.position.x = data.position.x.max(limit); } if let Some(limit) = props.bounds_right { data.position.x = data.position.x.min(limit); } if let Some(limit) = props.bounds_top { data.position.y = data.position.y.max(limit); } if let Some(limit) = props.bounds_bottom { data.position.y = data.position.y.min(limit); } notify( &context, FloatBoxNotifyMessage { sender: context.id.to_owned(), state: data, prev: data, }, ); let _ = context.state.write_with(data); }); context.life_cycle.change(|context| { let props = context.props.read_cloned_or_default::(); let mut dirty = false; let mut data = context.state.read_cloned_or_default::(); let prev = data; for msg in context.messenger.messages { if let Some(msg) = msg.as_any().downcast_ref::() { match msg.change { FloatBoxChange::Absolute(value) => { data = value; dirty = true; } FloatBoxChange::RelativePosition(delta) => { data.position.x -= delta.x; data.position.y -= delta.y; dirty = true; } FloatBoxChange::RelativeZoom(delta) => { data.zoom *= delta; dirty = true; } } } } if dirty { if let Some(limit) = props.bounds_left { data.position.x = data.position.x.max(limit); } if let Some(limit) = props.bounds_right { data.position.x = data.position.x.min(limit); } if let Some(limit) = props.bounds_top { data.position.y = data.position.y.max(limit); } if let Some(limit) = props.bounds_bottom { data.position.y = data.position.y.min(limit); } notify( &context, FloatBoxNotifyMessage { sender: context.id.to_owned(), state: data.to_owned(), prev, }, ); let _ = context.state.write_with(data); } }); } #[pre_hooks(use_nav_container_active, use_nav_jump_direction_active, use_nav_item)] pub fn nav_float_box(mut context: WidgetContext) -> WidgetNode { let WidgetContext { key, props, listed_slots, .. } = context; let props = props .clone() .without::() .without::() .without::(); make_widget!(float_box) .key(key) .merge_props(props) .listed_slots(listed_slots) .into() } #[pre_hooks(use_float_box)] pub fn float_box(mut context: WidgetContext) -> WidgetNode { let WidgetContext { key, props, state, mut listed_slots, .. } = context; let mut props = props.read_cloned_or_default::(); let state = state.read_cloned_or_default::(); props.content_reposition = ContentBoxContentReposition { offset: Vec2 { x: -state.position.x, y: -state.position.y, }, scale: Vec2 { x: state.zoom, y: state.zoom, }, }; for item in listed_slots.iter_mut() { if let Some(p) = item.props_mut() { p.write(state); if !p.has::() { p.write(FloatBoxNotifyProps(context.id.to_owned().into())); } } } make_widget!(content_box) .key(key) .with_props(props) .listed_slots(listed_slots) .into() } ================================================ FILE: crates/core/src/widget/component/containers/grid_box.rs ================================================ use crate::{ PropsData, make_widget, pre_hooks, widget::{ component::interactive::navigation::{ NavContainerActive, NavItemActive, NavJumpActive, use_nav_container_active, use_nav_item, use_nav_jump_direction_active, }, context::WidgetContext, node::WidgetNode, unit::grid::{GridBoxItemLayout, GridBoxItemNode, GridBoxNode}, utils::Transform, }, }; use serde::{Deserialize, Serialize}; #[derive(PropsData, Debug, Default, Clone, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct GridBoxProps { #[serde(default)] pub cols: usize, #[serde(default)] pub rows: usize, #[serde(default)] pub transform: Transform, } #[pre_hooks(use_nav_container_active, use_nav_jump_direction_active, use_nav_item)] pub fn nav_grid_box(mut context: WidgetContext) -> WidgetNode { let WidgetContext { key, props, listed_slots, .. } = context; let props = props .clone() .without::() .without::() .without::(); make_widget!(grid_box) .key(key) .merge_props(props) .listed_slots(listed_slots) .into() } pub fn grid_box(context: WidgetContext) -> WidgetNode { let WidgetContext { id, props, listed_slots, .. } = context; let GridBoxProps { cols, rows, transform, } = props.read_cloned_or_default(); let items = listed_slots .into_iter() .filter_map(|slot| { if let Some(props) = slot.props() { let layout = props.read_cloned_or_default::(); Some(GridBoxItemNode { slot, layout }) } else { None } }) .collect::>(); GridBoxNode { id: id.to_owned(), props: props.clone(), items, cols, rows, transform, } .into() } ================================================ FILE: crates/core/src/widget/component/containers/hidden_box.rs ================================================ use crate::{ PropsData, unpack_named_slots, widget::{context::WidgetContext, node::WidgetNode, unit::area::AreaBoxNode}, }; use serde::{Deserialize, Serialize}; #[derive(PropsData, Debug, Default, Copy, Clone, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct HiddenBoxProps(#[serde(default)] pub bool); pub fn hidden_box(context: WidgetContext) -> WidgetNode { let WidgetContext { id, props, named_slots, .. } = context; unpack_named_slots!(named_slots => content); let HiddenBoxProps(hidden) = props.read_cloned_or_default(); if hidden { Default::default() } else { AreaBoxNode { id: id.to_owned(), slot: Box::new(content), } .into() } } ================================================ FILE: crates/core/src/widget/component/containers/horizontal_box.rs ================================================ use crate::{ PropsData, Scalar, make_widget, pre_hooks, widget::{ component::{ containers::flex_box::{FlexBoxProps, flex_box}, interactive::navigation::{ NavContainerActive, NavItemActive, NavJumpActive, use_nav_container_active, use_nav_item, use_nav_jump_horizontal_step_active, }, }, context::WidgetContext, node::WidgetNode, unit::flex::{FlexBoxDirection, FlexBoxItemLayout}, utils::Transform, }, }; use serde::{Deserialize, Serialize}; #[derive(PropsData, Debug, Default, Clone, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct HorizontalBoxProps { #[serde(default)] pub separation: Scalar, #[serde(default)] pub reversed: bool, #[serde(default)] pub override_slots_layout: Option, #[serde(default)] pub transform: Transform, } #[pre_hooks( use_nav_container_active, use_nav_jump_horizontal_step_active, use_nav_item )] pub fn nav_horizontal_box(mut context: WidgetContext) -> WidgetNode { let WidgetContext { key, props, listed_slots, .. } = context; let props = props .clone() .without::() .without::() .without::(); make_widget!(horizontal_box) .key(key) .merge_props(props) .listed_slots(listed_slots) .into() } pub fn horizontal_box(context: WidgetContext) -> WidgetNode { let WidgetContext { key, props, mut listed_slots, .. } = context; let HorizontalBoxProps { separation, reversed, override_slots_layout, transform, } = props.read_cloned_or_default(); if let Some(layout) = override_slots_layout { for slot in &mut listed_slots { if let Some(props) = slot.props_mut() { props.write(layout.to_owned()); } } } let props = props.clone().with(FlexBoxProps { direction: if reversed { FlexBoxDirection::HorizontalRightToLeft } else { FlexBoxDirection::HorizontalLeftToRight }, separation, wrap: false, transform, }); make_widget!(flex_box) .key(key) .merge_props(props) .listed_slots(listed_slots) .into() } ================================================ FILE: crates/core/src/widget/component/containers/mod.rs ================================================ //! Containers for other components pub mod anchor_box; pub mod area_box; pub mod content_box; pub mod context_box; pub mod flex_box; pub mod float_box; pub mod grid_box; pub mod hidden_box; pub mod horizontal_box; pub mod portal_box; pub mod responsive_box; pub mod scroll_box; pub mod size_box; pub mod switch_box; pub mod tabs_box; pub mod tooltip_box; pub mod variant_box; pub mod vertical_box; pub mod wrap_box; ================================================ FILE: crates/core/src/widget/component/containers/portal_box.rs ================================================ use crate::{ PropsData, unpack_named_slots, widget::{ WidgetRef, component::RelativeLayoutProps, context::WidgetContext, node::WidgetNode, unit::{ content::{ContentBoxItemLayout, ContentBoxItemNode}, flex::{FlexBoxItemLayout, FlexBoxItemNode}, grid::{GridBoxItemLayout, GridBoxItemNode}, portal::{PortalBoxNode, PortalBoxSlotNode}, }, }, }; use serde::{Deserialize, Serialize}; #[derive(PropsData, Debug, Default, Clone, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct PortalsContainer(#[serde(default)] pub WidgetRef); pub fn portal_box(context: WidgetContext) -> WidgetNode { let WidgetContext { id, props, shared_props, named_slots, .. } = context; unpack_named_slots!(named_slots => content); let PortalsContainer(owner) = props.read_cloned_or_else(|| shared_props.read_cloned_or_default()); let slot = if let Ok(layout) = props.read_cloned::() { PortalBoxSlotNode::ContentItem(ContentBoxItemNode { slot: content, layout, }) } else if let Ok(layout) = props.read_cloned::() { PortalBoxSlotNode::FlexItem(FlexBoxItemNode { slot: content, layout, }) } else if let Ok(layout) = props.read_cloned::() { PortalBoxSlotNode::GridItem(GridBoxItemNode { slot: content, layout, }) } else { PortalBoxSlotNode::Slot(content) }; if let Some(owner) = owner.read() { PortalBoxNode { id: id.to_owned(), slot: Box::new(slot), owner, } .into() } else { Default::default() } } pub fn use_portals_container_relative_layout(context: &mut WidgetContext) { let PortalsContainer(owner) = context .props .read_cloned_or_else(|| context.shared_props.read_cloned_or_default()); context.props.write(RelativeLayoutProps { relative_to: owner.into(), }); } ================================================ FILE: crates/core/src/widget/component/containers/responsive_box.rs ================================================ use crate::{ PropsData, Scalar, pre_hooks, unpack_named_slots, view_model::{ViewModelProperties, ViewModelValue}, widget::{ component::{ResizeListenerSignal, use_resize_listener}, context::WidgetContext, node::WidgetNode, unit::area::AreaBoxNode, utils::Vec2, }, }; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; pub struct MediaQueryContext<'a> { pub widget_width: Scalar, pub widget_height: Scalar, pub view_model: Option<&'a MediaQueryViewModel>, } #[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] pub enum MediaQueryOrientation { #[default] Portrait, Landscape, } #[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)] pub enum MediaQueryNumber { Exact(Scalar), Min(Scalar), Max(Scalar), Range { min: Scalar, max: Scalar }, } impl Default for MediaQueryNumber { fn default() -> Self { Self::Exact(0.0) } } impl MediaQueryNumber { pub fn is_valid(&self, value: Scalar) -> bool { match self { Self::Exact(v) => (*v - value).abs() < Scalar::EPSILON, Self::Min(v) => *v <= value, Self::Max(v) => *v >= value, Self::Range { min, max } => *min <= value && *max >= value, } } } #[derive(PropsData, Debug, Default, Clone, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub enum MediaQueryExpression { #[default] Any, And(Vec), Or(Vec), Not(Box), WidgetOrientation(MediaQueryOrientation), WidgetAspectRatio(MediaQueryNumber), WidgetWidth(MediaQueryNumber), WidgetHeight(MediaQueryNumber), ScreenOrientation(MediaQueryOrientation), ScreenAspectRatio(MediaQueryNumber), ScreenWidth(MediaQueryNumber), ScreenHeight(MediaQueryNumber), HasFlag(String), HasNumber(String), Number(String, MediaQueryNumber), } impl MediaQueryExpression { pub fn is_valid(&self, context: &MediaQueryContext) -> bool { match self { Self::Any => true, Self::And(conditions) => conditions .iter() .all(|condition| condition.is_valid(context)), Self::Or(conditions) => conditions .iter() .any(|condition| condition.is_valid(context)), Self::Not(condition) => !condition.is_valid(context), Self::WidgetOrientation(orientation) => { let is_portrait = context.widget_height > context.widget_width; match orientation { MediaQueryOrientation::Portrait => is_portrait, MediaQueryOrientation::Landscape => !is_portrait, } } Self::WidgetAspectRatio(aspect_ratio) => { let ratio = context.widget_width / context.widget_height; aspect_ratio.is_valid(ratio) } Self::WidgetWidth(width) => width.is_valid(context.widget_width), Self::WidgetHeight(height) => height.is_valid(context.widget_height), Self::ScreenOrientation(orientation) => context .view_model .map(|view_model| { let is_portrait = view_model.screen_size.y > view_model.screen_size.x; match orientation { MediaQueryOrientation::Portrait => is_portrait, MediaQueryOrientation::Landscape => !is_portrait, } }) .unwrap_or_default(), Self::ScreenAspectRatio(aspect_ratio) => context .view_model .map(|view_model| { let ratio = view_model.screen_size.x / view_model.screen_size.y; aspect_ratio.is_valid(ratio) }) .unwrap_or_default(), Self::ScreenWidth(width) => context .view_model .map(|view_model| width.is_valid(view_model.screen_size.x)) .unwrap_or_default(), Self::ScreenHeight(height) => context .view_model .map(|view_model| height.is_valid(view_model.screen_size.y)) .unwrap_or_default(), Self::HasFlag(flag) => context .view_model .map(|view_model| view_model.flags.contains(flag)) .unwrap_or_default(), Self::HasNumber(name) => context .view_model .map(|view_model| view_model.numbers.contains_key(name)) .unwrap_or_default(), Self::Number(name, number) => context .view_model .map(|view_model| { view_model .numbers .get(name) .map(|value| number.is_valid(*value)) .unwrap_or_default() }) .unwrap_or_default(), } } } #[derive(PropsData, Debug, Default, Copy, Clone, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct ResponsiveBoxState { pub size: Vec2, } pub fn use_responsive_box(context: &mut WidgetContext) { context.life_cycle.mount(|mut context| { if let Some(mut bindings) = context.view_models.bindings( MediaQueryViewModel::VIEW_MODEL, MediaQueryViewModel::NOTIFIER, ) { bindings.bind(context.id.to_owned()); } }); context.life_cycle.unmount(|mut context| { if let Some(mut bindings) = context.view_models.bindings( MediaQueryViewModel::VIEW_MODEL, MediaQueryViewModel::NOTIFIER, ) { bindings.unbind(context.id); } }); context.life_cycle.change(|context| { for msg in context.messenger.messages { if let Some(ResizeListenerSignal::Change(size)) = msg.as_any().downcast_ref() { let _ = context.state.write(ResponsiveBoxState { size: *size }); } } }); } #[pre_hooks(use_resize_listener, use_responsive_box)] pub fn responsive_box(mut context: WidgetContext) -> WidgetNode { let WidgetContext { id, state, mut listed_slots, view_models, .. } = context; let state = state.read_cloned_or_default::(); let view_model = view_models .view_model(MediaQueryViewModel::VIEW_MODEL) .and_then(|vm| vm.read::()); let ctx = MediaQueryContext { widget_width: state.size.x, widget_height: state.size.y, view_model: view_model.as_deref(), }; let item = if let Some(index) = listed_slots.iter().position(|slot| { slot.props() .map(|props| { props .read::() .ok() .map(|query| query.is_valid(&ctx)) .unwrap_or(true) }) .unwrap_or_default() }) { listed_slots.remove(index) } else { Default::default() }; AreaBoxNode { id: id.to_owned(), slot: Box::new(item), } .into() } #[pre_hooks(use_resize_listener, use_responsive_box)] pub fn responsive_props_box(mut context: WidgetContext) -> WidgetNode { let WidgetContext { id, state, listed_slots, named_slots, view_models, .. } = context; unpack_named_slots!(named_slots => content); let state = state.read_cloned_or_default::(); let view_model = view_models .view_model(MediaQueryViewModel::VIEW_MODEL) .and_then(|vm| vm.read::()); let ctx = MediaQueryContext { widget_width: state.size.x, widget_height: state.size.y, view_model: view_model.as_deref(), }; let props = listed_slots .iter() .find_map(|slot| { slot.props().and_then(|props| { props .read::() .ok() .map(|query| query.is_valid(&ctx)) .unwrap_or(true) .then_some(props.clone()) }) }) .unwrap_or_default(); if let Some(p) = content.props_mut() { p.merge_from(props.without::()); } AreaBoxNode { id: id.to_owned(), slot: Box::new(content), } .into() } #[derive(Debug)] pub struct MediaQueryViewModel { pub screen_size: ViewModelValue, pub flags: ViewModelValue>, pub numbers: ViewModelValue>, } impl MediaQueryViewModel { pub const VIEW_MODEL: &str = "MediaQueryViewModel"; pub const NOTIFIER: &str = ""; pub fn new(properties: &mut ViewModelProperties) -> Self { let notifier = properties.notifier(Self::NOTIFIER); Self { screen_size: ViewModelValue::new(Default::default(), notifier.clone()), flags: ViewModelValue::new(Default::default(), notifier.clone()), numbers: ViewModelValue::new(Default::default(), notifier.clone()), } } } ================================================ FILE: crates/core/src/widget/component/containers/scroll_box.rs ================================================ use crate::{ PropsData, Scalar, make_widget, pre_hooks, props::Props, unpack_named_slots, widget::{ WidgetId, component::{ ResizeListenerSignal, containers::{ content_box::{ContentBoxProps, content_box}, size_box::{SizeBoxProps, size_box}, }, image_box::{ImageBoxProps, image_box}, interactive::{ button::{ ButtonNotifyMessage, ButtonNotifyProps, ButtonProps, button, self_tracked_button, }, navigation::{ NavItemActive, NavJump, NavScroll, NavSignal, NavTrackingNotifyMessage, NavTrackingNotifyProps, use_nav_container_active, use_nav_item, use_nav_item_active, use_nav_scroll_view_content, }, scroll_view::{ScrollViewState, use_scroll_view}, }, use_resize_listener, }, context::WidgetContext, node::WidgetNode, unit::{ area::AreaBoxNode, content::ContentBoxItemLayout, image::ImageBoxMaterial, size::SizeBoxSizeValue, }, utils::{Rect, Vec2, lerp}, }, }; use serde::{Deserialize, Serialize}; #[derive(PropsData, Debug, Default, Clone, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct ScrollBoxOwner( #[serde(default)] #[serde(skip_serializing_if = "WidgetId::is_none")] pub WidgetId, ); #[derive(PropsData, Debug, Clone, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct SideScrollbarsProps { #[serde(default)] pub size: Scalar, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] pub back_material: Option, #[serde(default)] pub front_material: ImageBoxMaterial, } impl Default for SideScrollbarsProps { fn default() -> Self { Self { size: 10.0, back_material: None, front_material: Default::default(), } } } #[derive(PropsData, Debug, Default, Copy, Clone, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct SideScrollbarsState { pub horizontal_state: ButtonProps, pub vertical_state: ButtonProps, } pub fn use_nav_scroll_box_content(context: &mut WidgetContext) { context.life_cycle.change(|context| { for msg in context.messenger.messages { if let Some(ResizeListenerSignal::Change(size)) = msg.as_any().downcast_ref() && let Ok(data) = context.props.read::() { context .messenger .write(data.0.to_owned(), ResizeListenerSignal::Change(*size)); } } }); } #[pre_hooks( use_resize_listener, use_nav_item_active, use_nav_container_active, use_nav_scroll_view_content, use_nav_scroll_box_content )] pub fn nav_scroll_box_content(mut context: WidgetContext) -> WidgetNode { let WidgetContext { id, named_slots, .. } = context; unpack_named_slots!(named_slots => content); AreaBoxNode { id: id.to_owned(), slot: Box::new(content), } .into() } pub fn use_nav_scroll_box(context: &mut WidgetContext) { context.life_cycle.change(|context| { for msg in context.messenger.messages { if let Some(ResizeListenerSignal::Change(_)) = msg.as_any().downcast_ref() && let Ok(data) = context.state.read::() { context .signals .write(NavSignal::Jump(NavJump::Scroll(NavScroll::Factor( data.value, false, )))); } } }); } #[pre_hooks( use_resize_listener, use_nav_item, use_nav_container_active, use_scroll_view, use_nav_scroll_box )] pub fn nav_scroll_box(mut context: WidgetContext) -> WidgetNode { let WidgetContext { id, key, props, state, named_slots, .. } = context; unpack_named_slots!(named_slots => {content, scrollbars}); let scroll_props = state.read_cloned_or_default::(); let content_props = Props::new(ContentBoxItemLayout { align: scroll_props.value, ..Default::default() }) .with(ScrollBoxOwner(id.to_owned())); if let Some(props) = scrollbars.props_mut() { props.write(ScrollBoxOwner(id.to_owned())); props.write(scroll_props); } if !props.has::() { props.write(ContentBoxProps { clipping: true, ..Default::default() }); } let size_props = SizeBoxProps { width: SizeBoxSizeValue::Fill, height: SizeBoxSizeValue::Fill, ..Default::default() }; let content = make_widget!(content_box) .key(key) .merge_props(props.clone()) .listed_slot( make_widget!(button) .key("input-consumer") .with_props(NavItemActive) .named_slot( "content", make_widget!(size_box).key("size").with_props(size_props), ), ) .listed_slot( make_widget!(nav_scroll_box_content) .key("content") .merge_props(content_props) .named_slot("content", content), ) .listed_slot(scrollbars) .into(); AreaBoxNode { id: id.to_owned(), slot: Box::new(content), } .into() } pub fn use_nav_scroll_box_side_scrollbars(context: &mut WidgetContext) { context.life_cycle.mount(|context| { let _ = context.state.write_with(SideScrollbarsState::default()); }); context.life_cycle.unmount(|context| { context.signals.write(NavSignal::Unlock); }); context.life_cycle.change(|context| { let mut dirty = false; let mut notify = false; let mut state = context .state .read_cloned_or_default::(); let mut props = context.props.read_cloned_or_default::(); for msg in context.messenger.messages { if let Some(msg) = msg.as_any().downcast_ref::() { if msg.trigger_start() { context.signals.write(NavSignal::Lock); } if msg.trigger_stop() { context.signals.write(NavSignal::Unlock); } if msg.sender.key() == "hbar" { state.horizontal_state = msg.state; dirty = true; } else if msg.sender.key() == "vbar" { state.vertical_state = msg.state; dirty = true; } } if let Some(msg) = msg.as_any().downcast_ref::() { if msg.sender.key() == "hbar" && state.horizontal_state.selected && (state.horizontal_state.trigger || state.horizontal_state.context) { props.value.x = msg.state.factor.x; notify = true; } else if msg.sender.key() == "vbar" && state.vertical_state.selected && (state.vertical_state.trigger || state.vertical_state.context) { props.value.y = msg.state.factor.y; notify = true; } } } if dirty { let _ = context.state.write_with(state); } if notify { let view = context.props.read_cloned_or_default::().0; context .signals .write(NavSignal::Jump(NavJump::Scroll(NavScroll::DirectFactor( view.into(), props.value, false, )))); } }); } #[pre_hooks( use_nav_item_active, use_nav_container_active, use_nav_scroll_box_side_scrollbars )] pub fn nav_scroll_box_side_scrollbars(mut context: WidgetContext) -> WidgetNode { let WidgetContext { id, key, props, .. } = context; let view_props = props.read_cloned_or_default::(); let SideScrollbarsProps { size, back_material, front_material, } = props.read_cloned_or_default(); let hbar = if view_props.size_factor.x > 1.0 { let length = 1.0 / view_props.size_factor.y; let rest = 1.0 - length; let button_props = Props::new(NavItemActive) .with(ButtonNotifyProps(id.to_owned().into())) .with(NavTrackingNotifyProps(id.to_owned().into())) .with(ContentBoxItemLayout { anchors: Rect { left: 0.0, right: 1.0, top: 1.0, bottom: 1.0, }, margin: Rect { left: 0.0, right: size, top: -size, bottom: 0.0, }, align: Vec2 { x: 0.0, y: 1.0 }, ..Default::default() }); let front_props = Props::new(ImageBoxProps { material: front_material.clone(), ..Default::default() }) .with(ContentBoxItemLayout { anchors: Rect { left: lerp(0.0, rest, view_props.value.x), right: lerp(length, 1.0, view_props.value.x), top: 0.0, bottom: 1.0, }, ..Default::default() }); let back = if let Some(material) = back_material.clone() { let props = ImageBoxProps { material, ..Default::default() }; make_widget!(image_box).key("back").with_props(props).into() } else { WidgetNode::default() }; make_widget!(self_tracked_button) .key("hbar") .merge_props(button_props) .named_slot( "content", make_widget!(content_box) .key("container") .listed_slot(back) .listed_slot( make_widget!(image_box) .key("front") .merge_props(front_props), ), ) .into() } else { WidgetNode::default() }; let vbar = if view_props.size_factor.y > 1.0 { let length = 1.0 / view_props.size_factor.y; let rest = 1.0 - length; let button_props = Props::new(NavItemActive) .with(ButtonNotifyProps(id.to_owned().into())) .with(NavTrackingNotifyProps(id.to_owned().into())) .with(ContentBoxItemLayout { anchors: Rect { left: 1.0, right: 1.0, top: 0.0, bottom: 1.0, }, margin: Rect { left: -size, right: 0.0, top: 0.0, bottom: size, }, align: Vec2 { x: 1.0, y: 0.0 }, ..Default::default() }); let back = if let Some(material) = back_material { let props = ImageBoxProps { material, ..Default::default() }; make_widget!(image_box).key("back").with_props(props).into() } else { WidgetNode::default() }; let front_props = Props::new(ImageBoxProps { material: front_material, ..Default::default() }) .with(ContentBoxItemLayout { anchors: Rect { left: 0.0, right: 1.0, top: lerp(0.0, rest, view_props.value.y), bottom: lerp(length, 1.0, view_props.value.y), }, ..Default::default() }); make_widget!(self_tracked_button) .key("vbar") .merge_props(button_props) .named_slot( "content", make_widget!(content_box) .key("container") .listed_slot(back) .listed_slot( make_widget!(image_box) .key("front") .merge_props(front_props), ), ) .into() } else { WidgetNode::default() }; make_widget!(content_box) .key(key) .listed_slot(hbar) .listed_slot(vbar) .into() } ================================================ FILE: crates/core/src/widget/component/containers/size_box.rs ================================================ use crate::{ PropsData, unpack_named_slots, widget::{ context::WidgetContext, node::WidgetNode, unit::size::{SizeBoxAspectRatio, SizeBoxNode, SizeBoxSizeValue}, utils::{Rect, Transform}, }, }; use serde::{Deserialize, Serialize}; #[derive(PropsData, Debug, Default, Clone, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct SizeBoxProps { #[serde(default)] pub width: SizeBoxSizeValue, #[serde(default)] pub height: SizeBoxSizeValue, #[serde(default)] pub margin: Rect, #[serde(default)] pub keep_aspect_ratio: SizeBoxAspectRatio, #[serde(default)] pub transform: Transform, } pub fn size_box(context: WidgetContext) -> WidgetNode { let WidgetContext { id, props, named_slots, .. } = context; unpack_named_slots!(named_slots => content); let SizeBoxProps { width, height, margin, keep_aspect_ratio, transform, } = props.read_cloned_or_default(); SizeBoxNode { id: id.to_owned(), props: props.clone(), slot: Box::new(content), width, height, margin, keep_aspect_ratio, transform, } .into() } ================================================ FILE: crates/core/src/widget/component/containers/switch_box.rs ================================================ use crate::{ PropsData, make_widget, pre_hooks, widget::{ component::interactive::navigation::{ NavContainerActive, NavItemActive, NavJumpActive, use_nav_container_active, use_nav_item, use_nav_jump_step_pages_active, }, context::WidgetContext, node::WidgetNode, unit::content::{ContentBoxItemNode, ContentBoxNode}, utils::Transform, }, }; use serde::{Deserialize, Serialize}; #[derive(PropsData, Debug, Default, Clone, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct SwitchBoxProps { #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] pub active_index: Option, #[serde(default)] pub clipping: bool, #[serde(default)] pub transform: Transform, } #[pre_hooks(use_nav_container_active, use_nav_jump_step_pages_active, use_nav_item)] pub fn nav_switch_box(mut context: WidgetContext) -> WidgetNode { let WidgetContext { key, props, listed_slots, .. } = context; let props = props .clone() .without::() .without::() .without::(); make_widget!(switch_box) .key(key) .merge_props(props) .listed_slots(listed_slots) .into() } pub fn switch_box(context: WidgetContext) -> WidgetNode { let WidgetContext { id, props, listed_slots, .. } = context; let SwitchBoxProps { active_index, clipping, transform, } = props.read_cloned_or_default(); let items = if let Some(index) = active_index { if let Some(slot) = listed_slots.into_iter().nth(index) { vec![ContentBoxItemNode { slot, ..Default::default() }] } else { vec![] } } else { vec![] }; ContentBoxNode { id: id.to_owned(), props: props.clone(), items, clipping, content_reposition: Default::default(), transform, } .into() } ================================================ FILE: crates/core/src/widget/component/containers/tabs_box.rs ================================================ use crate::{ PropsData, Scalar, make_widget, pre_hooks, props::Props, widget::{ component::{ containers::{ flex_box::{FlexBoxProps, flex_box}, switch_box::{SwitchBoxProps, switch_box}, }, interactive::{ button::{ButtonNotifyMessage, ButtonNotifyProps, button}, navigation::{NavItemActive, use_nav_container_active, use_nav_item}, }, }, context::WidgetContext, node::WidgetNode, unit::flex::{FlexBoxDirection, FlexBoxItemLayout}, utils::Transform, }, }; use serde::{Deserialize, Serialize}; #[derive(Debug, Default, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] pub enum TabsBoxTabsLocation { #[default] Top, Bottom, Left, Right, } #[derive(PropsData, Debug, Default, Clone, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct TabsBoxProps { #[serde(default)] pub tabs_location: TabsBoxTabsLocation, #[serde(default)] pub tabs_and_content_separation: Scalar, #[serde(default)] pub tabs_basis: Option, #[serde(default)] pub contents_clipping: bool, #[serde(default)] pub start_index: usize, #[serde(default)] pub transform: Transform, } #[derive(PropsData, Debug, Default, Copy, Clone, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct TabsState { #[serde(default)] pub active_index: usize, } #[derive(PropsData, Debug, Default, Copy, Clone, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct TabPlateProps { #[serde(default)] pub active: bool, #[serde(default)] pub index: usize, } impl TabsBoxProps { fn to_main_props(&self) -> FlexBoxProps { FlexBoxProps { direction: match self.tabs_location { TabsBoxTabsLocation::Top => FlexBoxDirection::VerticalTopToBottom, TabsBoxTabsLocation::Bottom => FlexBoxDirection::VerticalBottomToTop, TabsBoxTabsLocation::Left => FlexBoxDirection::HorizontalLeftToRight, TabsBoxTabsLocation::Right => FlexBoxDirection::HorizontalRightToLeft, }, separation: self.tabs_and_content_separation, wrap: false, transform: self.transform.to_owned(), } } fn to_tabs_props(&self) -> FlexBoxProps { FlexBoxProps { direction: match self.tabs_location { TabsBoxTabsLocation::Top => FlexBoxDirection::HorizontalLeftToRight, TabsBoxTabsLocation::Bottom => FlexBoxDirection::HorizontalLeftToRight, TabsBoxTabsLocation::Left => FlexBoxDirection::VerticalTopToBottom, TabsBoxTabsLocation::Right => FlexBoxDirection::VerticalTopToBottom, }, ..Default::default() } } } pub fn use_nav_tabs_box(context: &mut WidgetContext) { context.life_cycle.mount(|context| { let _ = context.state.write(TabsState { active_index: context .props .map_or_default::(|p| p.start_index), }); }); context.life_cycle.change(|context| { for msg in context.messenger.messages { if let Some(msg) = msg.as_any().downcast_ref::() && msg.trigger_start() && let Ok(index) = msg.sender.key().parse::() { let _ = context.state.write(TabsState { active_index: index, }); } } }) } #[pre_hooks(use_nav_container_active, use_nav_item, use_nav_tabs_box)] pub fn nav_tabs_box(mut context: WidgetContext) -> WidgetNode { let WidgetContext { id, key, props, state, listed_slots, .. } = context; let main_props = props.read_cloned_or_default::(); let props = props.clone().with(main_props.to_main_props()); let tabs_props = Props::new(main_props.to_tabs_props()).with(FlexBoxItemLayout { basis: main_props.tabs_basis, grow: 0.0, shrink: 0.0, ..Default::default() }); let TabsState { active_index } = state.read_cloned_or_default(); let switch_props = SwitchBoxProps { active_index: if active_index < listed_slots.len() { Some(active_index) } else { None }, clipping: main_props.contents_clipping, ..Default::default() }; let mut tabs = Vec::::with_capacity(listed_slots.len()); let mut contents = Vec::with_capacity(listed_slots.len()); for (index, item) in listed_slots.into_iter().enumerate() { let [mut tab, content] = item.unpack_tuple(); tab.remap_props(|props| { props.with(TabPlateProps { active: active_index == index, index, }) }); let props = Props::new(NavItemActive).with(ButtonNotifyProps(id.to_owned().into())); tabs.push( make_widget!(button) .key(index) .merge_props(props) .named_slot("content", tab) .into(), ); contents.push(content); } make_widget!(flex_box) .key(key) .merge_props(props) .listed_slot( make_widget!(flex_box) .key("tabs") .merge_props(tabs_props) .listed_slots(tabs), ) .listed_slot( make_widget!(switch_box) .key("contents") .with_props(switch_props) .listed_slots(contents), ) .into() } ================================================ FILE: crates/core/src/widget/component/containers/tooltip_box.rs ================================================ use crate::{ PropsData, make_widget, pre_hooks, props::Props, unpack_named_slots, widget::{ component::{ containers::{ anchor_box::{AnchorProps, PivotBoxProps, pivot_box, use_anchor_box}, content_box::content_box, portal_box::{portal_box, use_portals_container_relative_layout}, }, interactive::navigation::{NavSignal, use_nav_container_active, use_nav_item_active}, }, context::WidgetContext, node::WidgetNode, unit::area::AreaBoxNode, }, }; use serde::{Deserialize, Serialize}; #[derive(PropsData, Debug, Default, Copy, Clone, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct TooltipState { #[serde(default)] pub show: bool, } #[pre_hooks(use_nav_container_active, use_nav_item_active, use_anchor_box)] pub fn use_tooltip_box(context: &mut WidgetContext) { context.life_cycle.change(|context| { for msg in context.messenger.messages { if let Some(msg) = msg.as_any().downcast_ref() { match msg { NavSignal::Select(_) => { let _ = context.state.write_with(TooltipState { show: true }); } NavSignal::Unselect => { let _ = context.state.write_with(TooltipState { show: false }); } _ => {} } } } }); } #[pre_hooks(use_tooltip_box)] pub fn tooltip_box(mut context: WidgetContext) -> WidgetNode { let WidgetContext { id, idref, key, props, state, named_slots, .. } = context; unpack_named_slots!(named_slots => {content, tooltip}); let TooltipState { show } = state.read_cloned_or_default(); let anchor_state = state.read_cloned_or_default::(); let pivot_props = Props::new(anchor_state).with(props.read_cloned_or_default::()); let tooltip = if show { make_widget!(pivot_box) .key("pivot") .merge_props(pivot_props) .named_slot( "content", make_widget!(portal_box) .key("portal") .named_slot("content", tooltip), ) .into() } else { WidgetNode::default() }; let content = make_widget!(content_box) .key(key) .maybe_idref(idref.cloned()) .merge_props(props.clone()) .listed_slot(content) .listed_slot(tooltip) .into(); AreaBoxNode { id: id.to_owned(), slot: Box::new(content), } .into() } #[pre_hooks(use_portals_container_relative_layout)] pub fn portals_tooltip_box(mut context: WidgetContext) -> WidgetNode { tooltip_box(context) } ================================================ FILE: crates/core/src/widget/component/containers/variant_box.rs ================================================ use crate::{ PropsData, widget::{context::WidgetContext, node::WidgetNode}, }; use serde::{Deserialize, Serialize}; #[derive(PropsData, Debug, Default, Clone, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct VariantBoxProps { #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] pub variant_name: Option, } pub fn variant_box(context: WidgetContext) -> WidgetNode { let WidgetContext { props, mut named_slots, .. } = context; let VariantBoxProps { variant_name } = props.read_cloned_or_default(); if let Some(variant_name) = variant_name { named_slots.remove(&variant_name).unwrap_or_default() } else { Default::default() } } ================================================ FILE: crates/core/src/widget/component/containers/vertical_box.rs ================================================ use crate::{ PropsData, Scalar, make_widget, pre_hooks, widget::{ component::{ containers::flex_box::{FlexBoxProps, flex_box}, interactive::navigation::{ NavContainerActive, NavItemActive, NavJumpActive, use_nav_container_active, use_nav_item, use_nav_jump_vertical_step_active, }, }, context::WidgetContext, node::WidgetNode, unit::flex::{FlexBoxDirection, FlexBoxItemLayout}, utils::Transform, }, }; use serde::{Deserialize, Serialize}; #[derive(PropsData, Debug, Default, Clone, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct VerticalBoxProps { #[serde(default)] pub separation: Scalar, #[serde(default)] pub reversed: bool, #[serde(default)] pub override_slots_layout: Option, #[serde(default)] pub transform: Transform, } #[pre_hooks( use_nav_container_active, use_nav_jump_vertical_step_active, use_nav_item )] pub fn nav_vertical_box(mut context: WidgetContext) -> WidgetNode { let WidgetContext { key, props, listed_slots, .. } = context; let props = props .clone() .without::() .without::() .without::(); make_widget!(vertical_box) .key(key) .merge_props(props) .listed_slots(listed_slots) .into() } pub fn vertical_box(context: WidgetContext) -> WidgetNode { let WidgetContext { key, props, mut listed_slots, .. } = context; let VerticalBoxProps { separation, reversed, override_slots_layout, transform, } = props.read_cloned_or_default(); if let Some(layout) = override_slots_layout { for slot in &mut listed_slots { if let Some(props) = slot.props_mut() { props.write(layout.to_owned()); } } } let props = props.clone().with(FlexBoxProps { direction: if reversed { FlexBoxDirection::VerticalBottomToTop } else { FlexBoxDirection::VerticalTopToBottom }, separation, wrap: false, transform, }); make_widget!(flex_box) .key(key) .merge_props(props) .listed_slots(listed_slots) .into() } ================================================ FILE: crates/core/src/widget/component/containers/wrap_box.rs ================================================ use crate::{ PropsData, unpack_named_slots, widget::{ context::WidgetContext, node::WidgetNode, unit::size::{SizeBoxNode, SizeBoxSizeValue}, utils::Rect, }, }; use serde::{Deserialize, Serialize}; #[derive(PropsData, Debug, Clone, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct WrapBoxProps { #[serde(default)] pub margin: Rect, #[serde(default = "WrapBoxProps::default_fill")] pub fill: bool, } impl Default for WrapBoxProps { fn default() -> Self { Self { margin: Default::default(), fill: Self::default_fill(), } } } impl WrapBoxProps { fn default_fill() -> bool { true } } pub fn wrap_box(context: WidgetContext) -> WidgetNode { let WidgetContext { id, props, named_slots, .. } = context; unpack_named_slots!(named_slots => content); let WrapBoxProps { margin, fill } = props.read_cloned_or_default(); let (width, height) = if fill { (SizeBoxSizeValue::Fill, SizeBoxSizeValue::Fill) } else { (SizeBoxSizeValue::Content, SizeBoxSizeValue::Content) }; SizeBoxNode { id: id.to_owned(), props: props.clone(), slot: Box::new(content), margin, width, height, ..Default::default() } .into() } ================================================ FILE: crates/core/src/widget/component/image_box.rs ================================================ use crate::{ PropsData, widget::{ component::WidgetAlpha, context::WidgetContext, node::WidgetNode, unit::image::{ ImageBoxAspectRatio, ImageBoxColor, ImageBoxImage, ImageBoxMaterial, ImageBoxNode, ImageBoxSizeValue, }, utils::{Color, Transform}, }, }; use serde::{Deserialize, Serialize}; #[derive(PropsData, Debug, Default, Clone, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct ImageBoxProps { #[serde(default)] pub width: ImageBoxSizeValue, #[serde(default)] pub height: ImageBoxSizeValue, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] pub content_keep_aspect_ratio: Option, #[serde(default)] pub material: ImageBoxMaterial, #[serde(default)] pub transform: Transform, } impl ImageBoxProps { pub fn colored(color: Color) -> Self { Self { material: ImageBoxMaterial::Color(ImageBoxColor { color, ..Default::default() }), ..Default::default() } } pub fn image(id: impl ToString) -> Self { Self { material: ImageBoxMaterial::Image(ImageBoxImage { id: id.to_string(), ..Default::default() }), ..Default::default() } } pub fn image_aspect_ratio(id: impl ToString, outside: bool) -> Self { Self { material: ImageBoxMaterial::Image(ImageBoxImage { id: id.to_string(), ..Default::default() }), content_keep_aspect_ratio: Some(ImageBoxAspectRatio { horizontal_alignment: 0.5, vertical_alignment: 0.5, outside, }), ..Default::default() } } } pub fn image_box(context: WidgetContext) -> WidgetNode { let WidgetContext { id, props, shared_props, .. } = context; let ImageBoxProps { width, height, content_keep_aspect_ratio, mut material, transform, } = props.read_cloned_or_default(); let alpha = shared_props.read_cloned_or_default::().0; match &mut material { ImageBoxMaterial::Color(image) => { image.color.a *= alpha; } ImageBoxMaterial::Image(image) => { image.tint.a *= alpha; } _ => {} } ImageBoxNode { id: id.to_owned(), props: props.clone(), width, height, content_keep_aspect_ratio, material, transform, } .into() } ================================================ FILE: crates/core/src/widget/component/interactive/button.rs ================================================ use crate::{ MessageData, PropsData, pre_hooks, unpack_named_slots, widget::{ WidgetId, WidgetIdOrRef, component::interactive::navigation::{ NavSignal, use_nav_button, use_nav_item, use_nav_tracking, use_nav_tracking_self, }, context::{WidgetContext, WidgetMountOrChangeContext}, node::WidgetNode, unit::area::AreaBoxNode, }, }; use serde::{Deserialize, Serialize}; fn is_false(v: &bool) -> bool { !*v } #[derive(PropsData, Debug, Default, Copy, Clone, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct ButtonProps { #[serde(default)] #[serde(skip_serializing_if = "is_false")] pub selected: bool, #[serde(default)] #[serde(skip_serializing_if = "is_false")] pub trigger: bool, #[serde(default)] #[serde(skip_serializing_if = "is_false")] pub context: bool, } #[derive(PropsData, Debug, Default, Clone, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct ButtonNotifyProps( #[serde(default)] #[serde(skip_serializing_if = "WidgetIdOrRef::is_none")] pub WidgetIdOrRef, ); #[derive(MessageData, Debug, Default, Clone)] #[message_data(crate::messenger::MessageData)] pub struct ButtonNotifyMessage { pub sender: WidgetId, pub state: ButtonProps, pub prev: ButtonProps, } impl ButtonNotifyMessage { pub fn select_start(&self) -> bool { !self.prev.selected && self.state.selected } pub fn select_stop(&self) -> bool { self.prev.selected && !self.state.selected } pub fn select_changed(&self) -> bool { self.prev.selected != self.state.selected } pub fn trigger_start(&self) -> bool { !self.prev.trigger && self.state.trigger } pub fn trigger_stop(&self) -> bool { self.prev.trigger && !self.state.trigger } pub fn trigger_changed(&self) -> bool { self.prev.trigger != self.state.trigger } pub fn context_start(&self) -> bool { !self.prev.context && self.state.context } pub fn context_stop(&self) -> bool { self.prev.context && !self.state.context } pub fn context_changed(&self) -> bool { self.prev.context != self.state.context } } pub fn use_button_notified_state(context: &mut WidgetContext) { context.life_cycle.change(|context| { for msg in context.messenger.messages { if let Some(msg) = msg.as_any().downcast_ref::() { let _ = context.state.write_with(msg.state); } } }); } #[pre_hooks(use_nav_item, use_nav_button)] pub fn use_button(context: &mut WidgetContext) { fn notify(context: &WidgetMountOrChangeContext, data: ButtonNotifyMessage) { if let Ok(ButtonNotifyProps(notify)) = context.props.read() && let Some(to) = notify.read() { context.messenger.write(to, data); } } context.life_cycle.mount(|context| { notify( &context, ButtonNotifyMessage { sender: context.id.to_owned(), state: Default::default(), prev: Default::default(), }, ); let _ = context.state.write_with(ButtonProps::default()); }); context.life_cycle.change(|context| { let mut dirty = false; let mut data = context.state.read_cloned_or_default::(); let prev = data; for msg in context.messenger.messages { if let Some(msg) = msg.as_any().downcast_ref() { match msg { NavSignal::Select(_) => { data.selected = true; dirty = true; } NavSignal::Unselect => { data.selected = false; dirty = true; } NavSignal::Accept(v) => { data.trigger = *v; dirty = true; } NavSignal::Context(v) => { data.context = *v; dirty = true; } _ => {} } } } if dirty { notify( &context, ButtonNotifyMessage { sender: context.id.to_owned(), state: data.to_owned(), prev, }, ); let _ = context.state.write_with(data); } }); } #[pre_hooks(use_button)] pub fn button(mut context: WidgetContext) -> WidgetNode { let WidgetContext { id, state, named_slots, .. } = context; unpack_named_slots!(named_slots => content); if let Some(p) = content.props_mut() { p.write(state.read_cloned_or_default::()); } AreaBoxNode { id: id.to_owned(), slot: Box::new(content), } .into() } #[pre_hooks(use_nav_tracking)] pub fn tracked_button(mut context: WidgetContext) -> WidgetNode { button(context) } #[pre_hooks(use_nav_tracking_self)] pub fn self_tracked_button(mut context: WidgetContext) -> WidgetNode { button(context) } ================================================ FILE: crates/core/src/widget/component/interactive/float_view.rs ================================================ use crate::{ pre_hooks, unpack_named_slots, widget::{ component::{ containers::float_box::{ FloatBoxChange, FloatBoxChangeMessage, FloatBoxNotifyProps, FloatBoxState, }, interactive::{ button::{ButtonNotifyMessage, ButtonNotifyProps, ButtonProps, use_button}, navigation::{ NavSignal, NavTrackingNotifyMessage, NavTrackingNotifyProps, use_nav_item, use_nav_tracking_self, }, }, }, context::WidgetContext, node::WidgetNode, unit::area::AreaBoxNode, utils::Vec2, }, }; #[pre_hooks(use_button, use_nav_tracking_self)] pub fn use_float_view_control(context: &mut WidgetContext) { context .props .write(ButtonNotifyProps(context.id.to_owned().into())); context .props .write(NavTrackingNotifyProps(context.id.to_owned().into())); context.life_cycle.unmount(|context| { context.signals.write(NavSignal::Unlock); }); context.life_cycle.change(|context| { let Some(notify) = context .props .read_cloned_or_default::() .0 .read() else { return; }; let button = context.state.read_cloned_or_default::(); let zoom = context.props.read_cloned_or_default::().zoom; let scale = if zoom > 0.0 { 1.0 / zoom } else { 1.0 }; for msg in context.messenger.messages { if let Some(msg) = msg.as_any().downcast_ref::() { if msg.trigger_start() { context.signals.write(NavSignal::Lock); } if msg.trigger_stop() { context.signals.write(NavSignal::Unlock); } } else if let Some(msg) = msg.as_any().downcast_ref::() && button.selected && button.trigger { let delta = msg.pointer_delta_ui_space(); context.messenger.write( notify.clone(), FloatBoxChangeMessage { sender: context.id.to_owned(), change: FloatBoxChange::RelativePosition(Vec2 { x: delta.x * scale, y: delta.y * scale, }), }, ); } } }); } #[pre_hooks(use_nav_item, use_float_view_control)] pub fn float_view_control(mut context: WidgetContext) -> WidgetNode { let WidgetContext { id, props, state, named_slots, .. } = context; unpack_named_slots!(named_slots => content); if let Some(p) = content.props_mut() { p.merge_from(props.clone()); p.write(state.read_cloned_or_default::()); } AreaBoxNode { id: id.to_owned(), slot: Box::new(content), } .into() } ================================================ FILE: crates/core/src/widget/component/interactive/input_field.rs ================================================ use crate::{ Integer, MessageData, PropsData, Scalar, UnsignedInteger, pre_hooks, unpack_named_slots, view_model::ViewModelValue, widget::{ WidgetId, WidgetIdOrRef, component::interactive::{ button::{ButtonProps, use_button}, navigation::{NavSignal, NavTextChange, use_nav_item, use_nav_text_input}, }, context::{WidgetContext, WidgetMountOrChangeContext}, node::WidgetNode, unit::area::AreaBoxNode, }, }; use intuicio_data::managed::ManagedLazy; use serde::{Deserialize, Serialize}; use std::str::FromStr; fn is_false(v: &bool) -> bool { !*v } fn is_zero(v: &usize) -> bool { *v == 0 } pub trait TextInputProxy: Send + Sync { fn get(&self) -> String; fn set(&mut self, value: String); } impl TextInputProxy for T where T: ToString + FromStr + Send + Sync, { fn get(&self) -> String { self.to_string() } fn set(&mut self, value: String) { if let Ok(value) = value.parse() { *self = value; } } } impl TextInputProxy for ViewModelValue where T: ToString + FromStr + Send + Sync, { fn get(&self) -> String { self.to_string() } fn set(&mut self, value: String) { if let Ok(value) = value.parse() { **self = value; } } } #[derive(Clone)] pub struct TextInput(ManagedLazy); impl TextInput { pub fn new(data: ManagedLazy) -> Self { let (lifetime, data) = data.into_inner(); let data = data as *mut dyn TextInputProxy; unsafe { Self(ManagedLazy::::new_raw(data, lifetime).unwrap()) } } pub fn into_inner(self) -> ManagedLazy { self.0 } pub fn get(&self) -> String { self.0.read().map(|data| data.get()).unwrap_or_default() } pub fn set(&mut self, value: impl ToString) { if let Some(mut data) = self.0.write() { data.set(value.to_string()); } } } impl std::fmt::Debug for TextInput { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_tuple("TextInput") .field(&self.0.read().map(|data| data.get()).unwrap_or_default()) .finish() } } impl From> for TextInput { fn from(value: ManagedLazy) -> Self { Self::new(value) } } #[derive(PropsData, Debug, Default, Clone, Copy, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub enum TextInputMode { #[default] Text, Number, Integer, UnsignedInteger, #[serde(skip)] Filter(fn(usize, char) -> bool), } impl TextInputMode { pub fn is_text(&self) -> bool { matches!(self, Self::Text) } pub fn is_number(&self) -> bool { matches!(self, Self::Number) } pub fn is_integer(&self) -> bool { matches!(self, Self::Integer) } pub fn is_unsigned_integer(&self) -> bool { matches!(self, Self::UnsignedInteger) } pub fn is_filter(&self) -> bool { matches!(self, Self::Filter(_)) } pub fn process(&self, text: &str) -> Option { match self { Self::Text => Some(text.to_owned()), Self::Number => text.parse::().ok().map(|v| v.to_string()), Self::Integer => text.parse::().ok().map(|v| v.to_string()), Self::UnsignedInteger => text.parse::().ok().map(|v| v.to_string()), Self::Filter(f) => { if text.char_indices().any(|(i, c)| !f(i, c)) { None } else { Some(text.to_owned()) } } } } pub fn is_valid(&self, text: &str) -> bool { match self { Self::Text => true, Self::Number => text.parse::().is_ok() || text == "-", Self::Integer => text.parse::().is_ok() || text == "-", Self::UnsignedInteger => text.parse::().is_ok(), Self::Filter(f) => text.char_indices().all(|(i, c)| f(i, c)), } } } #[derive(PropsData, Debug, Default, Clone, Copy, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct TextInputState { #[serde(default)] #[serde(skip_serializing_if = "is_false")] pub focused: bool, #[serde(default)] #[serde(skip_serializing_if = "is_zero")] pub cursor_position: usize, } #[derive(PropsData, Debug, Default, Clone, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct TextInputProps { #[serde(default)] #[serde(skip_serializing_if = "is_false")] pub allow_new_line: bool, #[serde(default)] #[serde(skip)] pub text: Option, } #[derive(PropsData, Debug, Default, Clone, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct TextInputNotifyProps( #[serde(default)] #[serde(skip_serializing_if = "WidgetIdOrRef::is_none")] pub WidgetIdOrRef, ); #[derive(PropsData, Debug, Default, Clone, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct TextInputControlNotifyProps( #[serde(default)] #[serde(skip_serializing_if = "WidgetIdOrRef::is_none")] pub WidgetIdOrRef, ); #[derive(MessageData, Debug, Clone)] #[message_data(crate::messenger::MessageData)] pub struct TextInputNotifyMessage { pub sender: WidgetId, pub state: TextInputState, pub submitted: bool, } #[derive(MessageData, Debug, Clone)] #[message_data(crate::messenger::MessageData)] pub struct TextInputControlNotifyMessage { pub sender: WidgetId, pub character: char, } pub fn use_text_input_notified_state(context: &mut WidgetContext) { context.life_cycle.change(|context| { for msg in context.messenger.messages { if let Some(msg) = msg.as_any().downcast_ref::() { let _ = context.state.write_with(msg.state.to_owned()); } } }); } #[pre_hooks(use_nav_text_input)] pub fn use_text_input(context: &mut WidgetContext) { fn notify(context: &WidgetMountOrChangeContext, data: TextInputNotifyMessage) { if let Ok(notify) = context.props.read::() && let Some(to) = notify.0.read() { context.messenger.write(to, data); } } context.life_cycle.mount(|context| { notify( &context, TextInputNotifyMessage { sender: context.id.to_owned(), state: Default::default(), submitted: false, }, ); let _ = context.state.write_with(TextInputState::default()); }); context.life_cycle.change(|context| { let mode = context.props.read_cloned_or_default::(); let mut props = context.props.read_cloned_or_default::(); let mut state = context.state.read_cloned_or_default::(); let mut text = props .text .as_ref() .map(|text| text.get()) .unwrap_or_default(); let mut dirty_text = false; let mut dirty_state = false; let mut submitted = false; for msg in context.messenger.messages { if let Some(msg) = msg.as_any().downcast_ref() { match msg { NavSignal::FocusTextInput(idref) => { state.focused = idref.is_some(); dirty_state = true; } NavSignal::TextChange(change) => { if state.focused { match change { NavTextChange::InsertCharacter(c) => { if c.is_control() { if let Ok(notify) = context.props.read::() && let Some(to) = notify.0.read() { context.messenger.write( to, TextInputControlNotifyMessage { sender: context.id.to_owned(), character: *c, }, ); } } else { state.cursor_position = state.cursor_position.min(text.chars().count()); let mut iter = text.chars(); let mut new_text = iter .by_ref() .take(state.cursor_position) .collect::(); new_text.push(*c); new_text.extend(iter); if mode.is_valid(&new_text) { state.cursor_position += 1; text = new_text; dirty_text = true; dirty_state = true; } } } NavTextChange::MoveCursorLeft => { if state.cursor_position > 0 { state.cursor_position -= 1; dirty_state = true; } } NavTextChange::MoveCursorRight => { if state.cursor_position < text.chars().count() { state.cursor_position += 1; dirty_state = true; } } NavTextChange::MoveCursorStart => { state.cursor_position = 0; dirty_state = true; } NavTextChange::MoveCursorEnd => { state.cursor_position = text.chars().count(); dirty_state = true; } NavTextChange::DeleteLeft => { if state.cursor_position > 0 { let mut iter = text.chars(); let mut new_text = iter .by_ref() .take(state.cursor_position - 1) .collect::(); iter.by_ref().next(); new_text.extend(iter); if mode.is_valid(&new_text) { state.cursor_position -= 1; text = new_text; dirty_text = true; dirty_state = true; } } } NavTextChange::DeleteRight => { let mut iter = text.chars(); let mut new_text = iter .by_ref() .take(state.cursor_position) .collect::(); iter.by_ref().next(); new_text.extend(iter); if mode.is_valid(&new_text) { text = new_text; dirty_text = true; dirty_state = true; } } NavTextChange::NewLine => { if props.allow_new_line { let mut iter = text.chars(); let mut new_text = iter .by_ref() .take(state.cursor_position) .collect::(); new_text.push('\n'); new_text.extend(iter); if mode.is_valid(&new_text) { state.cursor_position += 1; text = new_text; dirty_text = true; dirty_state = true; } } else { submitted = true; dirty_state = true; } } } } } _ => {} } } } if dirty_state { state.cursor_position = state.cursor_position.min(text.chars().count()); notify( &context, TextInputNotifyMessage { sender: context.id.to_owned(), state, submitted, }, ); let _ = context.state.write_with(state); } if dirty_text && let Some(data) = props.text.as_mut() { data.set(text); context.messenger.write(context.id.to_owned(), ()); } if submitted { context.signals.write(NavSignal::FocusTextInput(().into())); } }); } #[pre_hooks(use_button, use_text_input)] pub fn use_input_field(context: &mut WidgetContext) { context.life_cycle.change(|context| { let focused = context .state .map_or_default::(|s| s.focused); for msg in context.messenger.messages { if let Some(msg) = msg.as_any().downcast_ref() { match msg { NavSignal::Accept(true) => { if !focused { context .signals .write(NavSignal::FocusTextInput(context.id.to_owned().into())); } } NavSignal::Cancel(true) => { if focused { context.signals.write(NavSignal::FocusTextInput(().into())); } } _ => {} } } } }); } #[pre_hooks(use_nav_item, use_text_input)] pub fn text_input(mut context: WidgetContext) -> WidgetNode { let WidgetContext { id, props, state, named_slots, .. } = context; unpack_named_slots!(named_slots => content); if let Some(p) = content.props_mut() { p.write(state.read_cloned_or_default::()); p.write(props.read_cloned_or_default::()); } AreaBoxNode { id: id.to_owned(), slot: Box::new(content), } .into() } #[pre_hooks(use_nav_item, use_input_field)] pub fn input_field(mut context: WidgetContext) -> WidgetNode { let WidgetContext { id, props, state, named_slots, .. } = context; unpack_named_slots!(named_slots => content); if let Some(p) = content.props_mut() { p.write(state.read_cloned_or_default::()); p.write(state.read_cloned_or_default::()); p.write(props.read_cloned_or_default::()); } AreaBoxNode { id: id.to_owned(), slot: Box::new(content), } .into() } pub fn input_text_with_cursor(text: &str, position: usize, cursor: char) -> String { text.chars() .take(position) .chain(std::iter::once(cursor)) .chain(text.chars().skip(position)) .collect() } ================================================ FILE: crates/core/src/widget/component/interactive/mod.rs ================================================ pub mod button; pub mod float_view; pub mod input_field; pub mod navigation; pub mod options_view; pub mod scroll_view; pub mod slider_view; ================================================ FILE: crates/core/src/widget/component/interactive/navigation.rs ================================================ use crate::{ MessageData, PropsData, Scalar, post_hooks, pre_hooks, unpack_named_slots, widget::{ WidgetId, WidgetIdOrRef, component::containers::portal_box::PortalsContainer, context::WidgetContext, node::WidgetNode, unit::area::AreaBoxNode, utils::Vec2, }, }; use serde::{Deserialize, Serialize}; #[derive(PropsData, Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct NavAutoSelect; #[derive(PropsData, Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct NavItemActive; #[derive(PropsData, Debug, Default, Clone, PartialEq, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct NavTrackingActive(#[serde(default)] pub WidgetIdOrRef); #[derive(PropsData, Debug, Default, Clone, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct NavTrackingNotifyProps( #[serde(default)] #[serde(skip_serializing_if = "WidgetIdOrRef::is_none")] pub WidgetIdOrRef, ); #[derive(PropsData, Debug, Default, Clone, Copy, PartialEq, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct NavTrackingProps { #[serde(default)] pub factor: Vec2, #[serde(default)] pub unscaled: Vec2, #[serde(default)] pub ui_space: Vec2, } #[derive(MessageData, Debug, Default, Clone)] #[message_data(crate::messenger::MessageData)] pub struct NavTrackingNotifyMessage { pub sender: WidgetId, pub state: NavTrackingProps, pub prev: NavTrackingProps, } impl NavTrackingNotifyMessage { pub fn pointer_delta_factor(&self) -> Vec2 { Vec2 { x: self.state.factor.x - self.prev.factor.x, y: self.state.factor.y - self.prev.factor.y, } } pub fn pointer_delta_unscaled(&self) -> Vec2 { Vec2 { x: self.state.unscaled.x - self.prev.unscaled.x, y: self.state.unscaled.y - self.prev.unscaled.y, } } pub fn pointer_delta_ui_space(&self) -> Vec2 { Vec2 { x: self.state.ui_space.x - self.prev.ui_space.x, y: self.state.ui_space.y - self.prev.ui_space.y, } } pub fn pointer_moved(&self) -> bool { (self.state.factor.x - self.prev.factor.x) + (self.state.factor.y - self.prev.factor.y) > 1.0e-6 } } #[derive(PropsData, Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct NavLockingActive; #[derive(PropsData, Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct NavContainerActive; #[derive(PropsData, Debug, Clone, PartialEq, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct NavContainerDesiredSelection(#[serde(default)] pub WidgetIdOrRef); #[derive(PropsData, Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct NavJumpActive(#[serde(default)] pub NavJumpMode); #[derive(PropsData, Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct NavJumpLooped; #[derive(Debug, Clone, PartialEq)] pub enum NavType { Container, Item, Button, TextInput, ScrollView, ScrollViewContent, /// (tracked widget) Tracking(WidgetIdOrRef), } #[derive(MessageData, Debug, Default, Clone)] #[message_data(crate::messenger::MessageData)] pub enum NavSignal { #[default] None, Register(NavType), Unregister(NavType), Select(WidgetIdOrRef), Unselect, Lock, Unlock, Accept(bool), Context(bool), Cancel(bool), Up, Down, Left, Right, Prev, Next, Jump(NavJump), FocusTextInput(WidgetIdOrRef), TextChange(NavTextChange), Axis(String, Scalar), Custom(WidgetIdOrRef, String), } #[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum NavJumpMode { #[default] Direction, StepHorizontal, StepVertical, StepPages, } #[derive(PropsData, Debug, Default, Clone, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct NavJumpMapProps { #[serde(default)] #[serde(skip_serializing_if = "WidgetIdOrRef::is_none")] pub up: WidgetIdOrRef, #[serde(default)] #[serde(skip_serializing_if = "WidgetIdOrRef::is_none")] pub down: WidgetIdOrRef, #[serde(default)] #[serde(skip_serializing_if = "WidgetIdOrRef::is_none")] pub left: WidgetIdOrRef, #[serde(default)] #[serde(skip_serializing_if = "WidgetIdOrRef::is_none")] pub right: WidgetIdOrRef, #[serde(default)] #[serde(skip_serializing_if = "WidgetIdOrRef::is_none")] pub prev: WidgetIdOrRef, #[serde(default)] #[serde(skip_serializing_if = "WidgetIdOrRef::is_none")] pub next: WidgetIdOrRef, } #[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum NavDirection { #[default] None, Up, Down, Left, Right, Prev, Next, } #[derive(Debug, Clone)] pub enum NavJump { First, Last, TopLeft, TopRight, BottomLeft, BottomRight, MiddleCenter, Loop(NavDirection), Escape(NavDirection, WidgetIdOrRef), Scroll(NavScroll), } #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum NavTextChange { InsertCharacter(char), MoveCursorLeft, MoveCursorRight, MoveCursorStart, MoveCursorEnd, DeleteLeft, DeleteRight, NewLine, } #[derive(Debug, Clone, Serialize, Deserialize)] pub enum NavScroll { /// (factor location, relative) Factor(Vec2, bool), /// (scroll view id or ref, factor location, relative) DirectFactor(WidgetIdOrRef, Vec2, bool), /// (local space units location, relative) Units(Vec2, bool), /// (scroll view id or ref, local space units location, relative) DirectUnits(WidgetIdOrRef, Vec2, bool), /// (id or ref, widget local space anchor point) Widget(WidgetIdOrRef, Vec2), /// (factor, content to container ratio, relative) Change(Vec2, Vec2, bool), } pub fn use_nav_container(context: &mut WidgetContext) { context.life_cycle.mount(|context| { if context.props.has::() { context .signals .write(NavSignal::Register(NavType::Container)); } if context.props.has::() { context .signals .write(NavSignal::Select(context.id.to_owned().into())); } }); context.life_cycle.unmount(|context| { context .signals .write(NavSignal::Unregister(NavType::Container)); }); context.life_cycle.change(move |context| { if let Ok(props) = context.props.read::() { for msg in context.messenger.messages { if let Some(NavSignal::Select(idref)) = msg.as_any().downcast_ref::() && idref.read().map(|id| &id == context.id).unwrap_or_default() { context.signals.write(NavSignal::Select(props.0.to_owned())); } } } }); } #[post_hooks(use_nav_container)] pub fn use_nav_container_active(context: &mut WidgetContext) { context.props.write(NavContainerActive); } pub fn use_nav_jump_map(context: &mut WidgetContext) { if !context.props.has::() { return; } context.life_cycle.change(|context| { let jump = match context.props.read::() { Ok(jump) => jump, _ => return, }; for msg in context.messenger.messages { if let Some(msg) = msg.as_any().downcast_ref() { match msg { NavSignal::Up => { if jump.up.is_some() { context.signals.write(NavSignal::Select(jump.up.to_owned())); } } NavSignal::Down => { if jump.down.is_some() { context .signals .write(NavSignal::Select(jump.down.to_owned())); } } NavSignal::Left => { if jump.left.is_some() { context .signals .write(NavSignal::Select(jump.left.to_owned())); } } NavSignal::Right => { if jump.right.is_some() { context .signals .write(NavSignal::Select(jump.right.to_owned())); } } NavSignal::Prev => { if jump.prev.is_some() { context .signals .write(NavSignal::Select(jump.prev.to_owned())); } } NavSignal::Next => { if jump.next.is_some() { context .signals .write(NavSignal::Select(jump.next.to_owned())); } } _ => {} } } } }); } pub fn use_nav_jump(context: &mut WidgetContext) { context.life_cycle.change(|context| { let mode = match context.props.read::() { Ok(data) => data.0, Err(_) => return, }; let looped = context.props.has::(); let jump = context.props.read_cloned_or_default::(); for msg in context.messenger.messages { if let Some(msg) = msg.as_any().downcast_ref() { match (mode, msg) { (NavJumpMode::Direction, NavSignal::Up) => { if looped { context .signals .write(NavSignal::Jump(NavJump::Loop(NavDirection::Up))); } else { context.signals.write(NavSignal::Jump(NavJump::Escape( NavDirection::Up, jump.up.to_owned(), ))); } } (NavJumpMode::Direction, NavSignal::Down) => { if looped { context .signals .write(NavSignal::Jump(NavJump::Loop(NavDirection::Down))); } else { context.signals.write(NavSignal::Jump(NavJump::Escape( NavDirection::Down, jump.down.to_owned(), ))); } } (NavJumpMode::Direction, NavSignal::Left) => { if looped { context .signals .write(NavSignal::Jump(NavJump::Loop(NavDirection::Left))); } else { context.signals.write(NavSignal::Jump(NavJump::Escape( NavDirection::Left, jump.left.to_owned(), ))); } } (NavJumpMode::Direction, NavSignal::Right) => { if looped { context .signals .write(NavSignal::Jump(NavJump::Loop(NavDirection::Right))); } else { context.signals.write(NavSignal::Jump(NavJump::Escape( NavDirection::Right, jump.right.to_owned(), ))); } } (NavJumpMode::StepHorizontal, NavSignal::Left) => { if looped { context .signals .write(NavSignal::Jump(NavJump::Loop(NavDirection::Prev))); } else { context.signals.write(NavSignal::Jump(NavJump::Escape( NavDirection::Prev, jump.left.to_owned(), ))); } } (NavJumpMode::StepHorizontal, NavSignal::Right) => { if looped { context .signals .write(NavSignal::Jump(NavJump::Loop(NavDirection::Next))); } else { context.signals.write(NavSignal::Jump(NavJump::Escape( NavDirection::Next, jump.right.to_owned(), ))); } } (NavJumpMode::StepVertical, NavSignal::Up) => { if looped { context .signals .write(NavSignal::Jump(NavJump::Loop(NavDirection::Prev))); } else { context.signals.write(NavSignal::Jump(NavJump::Escape( NavDirection::Prev, jump.up.to_owned(), ))); } } (NavJumpMode::StepVertical, NavSignal::Down) => { if looped { context .signals .write(NavSignal::Jump(NavJump::Loop(NavDirection::Next))); } else { context.signals.write(NavSignal::Jump(NavJump::Escape( NavDirection::Next, jump.down.to_owned(), ))); } } (NavJumpMode::StepPages, NavSignal::Prev) => { if looped { context .signals .write(NavSignal::Jump(NavJump::Loop(NavDirection::Prev))); } else { context.signals.write(NavSignal::Jump(NavJump::Escape( NavDirection::Prev, jump.prev.to_owned(), ))); } } (NavJumpMode::StepPages, NavSignal::Next) => { if looped { context .signals .write(NavSignal::Jump(NavJump::Loop(NavDirection::Next))); } else { context.signals.write(NavSignal::Jump(NavJump::Escape( NavDirection::Next, jump.next.to_owned(), ))); } } _ => {} } } } }); } #[post_hooks(use_nav_jump)] pub fn use_nav_jump_direction_active(context: &mut WidgetContext) { context.props.write(NavJumpActive(NavJumpMode::Direction)); } #[post_hooks(use_nav_jump)] pub fn use_nav_jump_horizontal_step_active(context: &mut WidgetContext) { context .props .write(NavJumpActive(NavJumpMode::StepHorizontal)); } #[post_hooks(use_nav_jump)] pub fn use_nav_jump_vertical_step_active(context: &mut WidgetContext) { context .props .write(NavJumpActive(NavJumpMode::StepVertical)); } #[post_hooks(use_nav_jump)] pub fn use_nav_jump_step_pages_active(context: &mut WidgetContext) { context.props.write(NavJumpActive(NavJumpMode::StepPages)); } pub fn use_nav_item(context: &mut WidgetContext) { context.life_cycle.mount(|context| { if context.props.has::() { context.signals.write(NavSignal::Register(NavType::Item)); } if context.props.has::() { context .signals .write(NavSignal::Select(context.id.to_owned().into())); } }); context.life_cycle.unmount(|context| { context.signals.write(NavSignal::Unregister(NavType::Item)); }); } #[post_hooks(use_nav_item)] pub fn use_nav_item_active(context: &mut WidgetContext) { context.props.write(NavItemActive); } pub fn use_nav_button(context: &mut WidgetContext) { context.life_cycle.mount(|context| { context.signals.write(NavSignal::Register(NavType::Button)); }); context.life_cycle.unmount(|context| { context .signals .write(NavSignal::Unregister(NavType::Button)); }); } pub fn use_nav_tracking(context: &mut WidgetContext) { context.life_cycle.mount(|context| { if let Ok(tracking) = context.props.read::() { context .signals .write(NavSignal::Register(NavType::Tracking(tracking.0.clone()))); let _ = context.state.write_with(NavTrackingProps::default()); } }); context.life_cycle.unmount(|context| { context .signals .write(NavSignal::Unregister(NavType::Tracking(Default::default()))); }); context.life_cycle.change(|context| { if let Ok(tracking) = context.props.read::() { if !context.state.has::() { context .signals .write(NavSignal::Register(NavType::Tracking(tracking.0.clone()))); let _ = context.state.write_with(NavTrackingProps::default()); } let mut dirty = false; let mut data = context.state.read_cloned_or_default::(); let prev = data; for msg in context.messenger.messages { if let Some(NavSignal::Axis(axis, value)) = msg.as_any().downcast_ref::() { match axis.as_str() { "pointer-x" => { data.factor.x = *value; dirty = true; } "pointer-y" => { data.factor.y = *value; dirty = true; } "pointer-x-unscaled" => { data.unscaled.x = *value; dirty = true; } "pointer-y-unscaled" => { data.unscaled.y = *value; dirty = true; } "pointer-x-ui" => { data.ui_space.x = *value; dirty = true; } "pointer-y-ui" => { data.ui_space.y = *value; dirty = true; } _ => {} } } } if dirty { if let Ok(NavTrackingNotifyProps(notify)) = context.props.read() && let Some(to) = notify.read() { context.messenger.write( to, NavTrackingNotifyMessage { sender: context.id.to_owned(), state: data.to_owned(), prev, }, ); } let _ = context.state.write_with(data); } } else if context.state.has::() { context .signals .write(NavSignal::Unregister(NavType::Tracking(Default::default()))); let _ = context.state.write_without::(); } }); } #[pre_hooks(use_nav_tracking)] pub fn use_nav_tracking_self(context: &mut WidgetContext) { context .props .write(NavTrackingActive(context.id.to_owned().into())); } #[pre_hooks(use_nav_tracking)] pub fn use_nav_tracking_active_portals_container(context: &mut WidgetContext) { if let Ok(data) = context.shared_props.read::() { context .props .write(NavTrackingActive(data.0.to_owned().into())); } } pub fn use_nav_tracking_notified_state(context: &mut WidgetContext) { context.life_cycle.change(|context| { for msg in context.messenger.messages { if let Some(msg) = msg.as_any().downcast_ref::() { let _ = context.state.write_with(msg.state); } } }); } pub fn use_nav_locking(context: &mut WidgetContext) { context.life_cycle.mount(|context| { if context.props.has::() { context.signals.write(NavSignal::Lock); let _ = context.state.write_with(NavLockingActive); } }); context.life_cycle.unmount(|context| { context.signals.write(NavSignal::Unlock); }); context.life_cycle.change(|context| { if context.props.has::() { if !context.state.has::() { context.signals.write(NavSignal::Lock); let _ = context.state.write_with(NavLockingActive); } } else if context.state.has::() && !context.props.has::() { context.signals.write(NavSignal::Unlock); let _ = context.state.write_without::(); } }); } pub fn use_nav_text_input(context: &mut WidgetContext) { context.life_cycle.mount(|context| { context .signals .write(NavSignal::Register(NavType::TextInput)); }); context.life_cycle.unmount(|context| { context .signals .write(NavSignal::Unregister(NavType::TextInput)); }); } pub fn use_nav_scroll_view(context: &mut WidgetContext) { context.life_cycle.mount(|context| { context .signals .write(NavSignal::Register(NavType::ScrollView)); }); context.life_cycle.unmount(|context| { context .signals .write(NavSignal::Unregister(NavType::ScrollView)); }); } pub fn use_nav_scroll_view_content(context: &mut WidgetContext) { context.life_cycle.mount(|context| { context .signals .write(NavSignal::Register(NavType::ScrollViewContent)); }); context.life_cycle.unmount(|context| { context .signals .write(NavSignal::Unregister(NavType::ScrollViewContent)); }); } #[pre_hooks(use_nav_button)] pub fn navigation_barrier(mut context: WidgetContext) -> WidgetNode { let WidgetContext { id, named_slots, .. } = context; unpack_named_slots!(named_slots => content); AreaBoxNode { id: id.to_owned(), slot: Box::new(content), } .into() } #[pre_hooks(use_nav_tracking)] pub fn tracking(mut context: WidgetContext) -> WidgetNode { let WidgetContext { id, named_slots, .. } = context; unpack_named_slots!(named_slots => content); AreaBoxNode { id: id.to_owned(), slot: Box::new(content), } .into() } #[pre_hooks(use_nav_tracking_self)] pub fn self_tracking(mut context: WidgetContext) -> WidgetNode { tracking(context) } ================================================ FILE: crates/core/src/widget/component/interactive/options_view.rs ================================================ use crate::{ PropsData, make_widget, pre_hooks, unpack_named_slots, view_model::ViewModelValue, widget::{ WidgetIdMetaParams, component::{ containers::{ anchor_box::PivotBoxProps, context_box::{ContextBoxProps, portals_context_box}, size_box::{SizeBoxProps, size_box}, }, interactive::{ button::{ButtonNotifyMessage, ButtonNotifyProps, button}, navigation::NavItemActive, }, }, context::WidgetContext, node::WidgetNode, }, }; use intuicio_data::managed::ManagedLazy; use serde::{Deserialize, Serialize}; use std::ops::{Deref, DerefMut}; pub trait OptionsViewProxy: Send + Sync { fn get(&self) -> usize; fn set(&mut self, value: usize); } macro_rules! impl_proxy { ($type:ty) => { impl OptionsViewProxy for $type { fn get(&self) -> usize { *self as _ } fn set(&mut self, value: usize) { *self = value as _; } } }; } impl_proxy!(u8); impl_proxy!(u16); impl_proxy!(u32); impl_proxy!(u64); impl_proxy!(u128); impl_proxy!(usize); impl_proxy!(i8); impl_proxy!(i16); impl_proxy!(i32); impl_proxy!(i64); impl_proxy!(i128); impl_proxy!(isize); impl_proxy!(f32); impl_proxy!(f64); impl OptionsViewProxy for ViewModelValue where T: OptionsViewProxy, { fn get(&self) -> usize { self.deref().get() } fn set(&mut self, value: usize) { self.deref_mut().set(value); } } #[derive(Clone)] pub struct OptionsInput(ManagedLazy); impl OptionsInput { pub fn new(data: ManagedLazy) -> Self { let (lifetime, data) = data.into_inner(); let data = data as *mut dyn OptionsViewProxy; unsafe { Self(ManagedLazy::::new_raw(data, lifetime).unwrap()) } } pub fn into_inner(self) -> ManagedLazy { self.0 } pub fn get + Default>(&self) -> T { self.0 .read() .map(|data| data.get()) .and_then(|value| T::try_from(value).ok()) .unwrap_or_default() } pub fn set>(&mut self, value: T) { if let Some(mut data) = self.0.write() && let Ok(value) = value.try_into() { data.set(value); } } } impl std::fmt::Debug for OptionsInput { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_tuple("OptionsInput") .field(&self.0.read().map(|data| data.get()).unwrap_or_default()) .finish() } } impl From> for OptionsInput { fn from(value: ManagedLazy) -> Self { Self::new(value) } } #[derive(PropsData, Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub enum OptionsViewMode { Selected, #[default] Option, } #[derive(PropsData, Debug, Default, Clone, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct OptionsViewProps { #[serde(default)] #[serde(skip)] pub input: Option, } impl OptionsViewProps { pub fn get_index(&self) -> usize { self.input .as_ref() .map(|input| input.get::()) .unwrap_or_default() } pub fn set_index(&mut self, value: usize) { if let Some(input) = self.input.as_mut() { input.set(value); } } } fn use_options_view(context: &mut WidgetContext) { context.life_cycle.change(|context| { for msg in context.messenger.messages { if let Some(msg) = msg.as_any().downcast_ref::() && msg.trigger_stop() { if msg.sender.key() == "button-selected" { let mut state = context.state.read_cloned_or_default::(); state.show = !state.show; let _ = context.state.write_with(state); } else if msg.sender.key() == "button-item" { let mut state = context.state.read_cloned_or_default::(); state.show = !state.show; let _ = context.state.write_with(state); let params = WidgetIdMetaParams::new(msg.sender.meta()); if let Some(value) = params.find_value("index") && let Ok(value) = value.parse::() && let Ok(mut options) = context.props.read_cloned::() { options.set_index(value); } } } } }); } #[pre_hooks(use_options_view)] pub fn options_view(mut context: WidgetContext) -> WidgetNode { let WidgetContext { id, idref, key, props, state, named_slots, listed_slots, .. } = context; unpack_named_slots!(named_slots => content); let state = state.read_cloned_or_default::(); let active = props.read_cloned::().ok(); let options = props.read_cloned_or_default::(); let selected = listed_slots .get(options.get_index()) .cloned() .map(|mut node| { node.remap_props(|props| props.with(OptionsViewMode::Selected)); node }) .unwrap_or_default(); let content = if state.show { let content = match content { WidgetNode::Component(node) => { WidgetNode::Component(node.listed_slots(listed_slots.into_iter().enumerate().map( |(index, mut slot)| { slot.remap_props(|props| props.with(OptionsViewMode::Option)); make_widget!(button) .key(format!("button-item?index={index}")) .merge_props(slot.props().cloned().unwrap_or_default()) .with_props(ButtonNotifyProps(id.to_owned().into())) .named_slot("content", slot) }, ))) } node => node, }; Some( make_widget!(size_box) .key("context") .merge_props(content.props().cloned().unwrap_or_default()) .with_props(props.read_cloned_or_default::()) .named_slot("content", content), ) } else { None }; make_widget!(portals_context_box) .key(key) .maybe_idref(idref.cloned()) .with_props(props.read_cloned_or_default::()) .with_props(state) .named_slot( "content", make_widget!(button) .key("button-selected") .maybe_with_props(active) .with_props(ButtonNotifyProps(id.to_owned().into())) .named_slot("content", selected), ) .maybe_named_slot("context", content) .into() } ================================================ FILE: crates/core/src/widget/component/interactive/scroll_view.rs ================================================ use crate::{ MessageData, PropsData, messenger::MessageData, pre_hooks, widget::{ WidgetId, WidgetIdOrRef, component::interactive::navigation::{NavJump, NavScroll, NavSignal, use_nav_scroll_view}, context::{WidgetContext, WidgetMountOrChangeContext}, utils::Vec2, }, }; use serde::{Deserialize, Serialize}; fn is_zero(v: &Vec2) -> bool { v.x.abs() < 1.0e-6 && v.y.abs() < 1.0e-6 } #[derive(PropsData, Debug, Default, Clone, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct ScrollViewState { #[serde(default)] pub value: Vec2, #[serde(default)] pub size_factor: Vec2, } #[derive(PropsData, Debug, Clone, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct ScrollViewRange { #[serde(default)] #[serde(skip_serializing_if = "is_zero")] pub from: Vec2, #[serde(default)] #[serde(skip_serializing_if = "is_zero")] pub to: Vec2, } impl Default for ScrollViewRange { fn default() -> Self { Self { from: Vec2 { x: 0.0, y: 0.0 }, to: Vec2 { x: 1.0, y: 1.0 }, } } } #[derive(PropsData, Debug, Default, Clone, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct ScrollViewNotifyProps( #[serde(default)] #[serde(skip_serializing_if = "WidgetIdOrRef::is_none")] pub WidgetIdOrRef, ); #[derive(MessageData, Debug, Clone)] #[message_data(crate::messenger::MessageData)] pub struct ScrollViewNotifyMessage { pub sender: WidgetId, pub state: ScrollViewState, } pub fn use_scroll_view_notified_state(context: &mut WidgetContext) { context.life_cycle.change(|context| { for msg in context.messenger.messages { if let Some(msg) = msg.as_any().downcast_ref::() { let _ = context.state.write_with(msg.state.clone()); } } }); } #[pre_hooks(use_nav_scroll_view)] pub fn use_scroll_view(context: &mut WidgetContext) { fn notify(context: &WidgetMountOrChangeContext, data: T) where T: 'static + MessageData, { if let Ok(notify) = context.props.read::() && let Some(to) = notify.0.read() { context.messenger.write(to, data); } } context.life_cycle.mount(|context| { notify( &context, ScrollViewNotifyMessage { sender: context.id.to_owned(), state: ScrollViewState::default(), }, ); let _ = context.state.write_with(ScrollViewState::default()); }); context.life_cycle.change(|context| { let mut dirty = false; let mut data = context.state.read_cloned_or_default::(); let range = context.props.read::(); for msg in context.messenger.messages { if let Some(NavSignal::Jump(NavJump::Scroll(NavScroll::Change( value, factor, relative, )))) = msg.as_any().downcast_ref() { if *relative { data.value.x += value.x; data.value.y += value.y; } else { data.value = *value; } if factor.x <= 1.0 { data.value.x = 0.0; } if factor.y <= 1.0 { data.value.y = 0.0; } if let Ok(range) = &range { data.value.x = data.value.x.max(range.from.x).min(range.to.x); data.value.y = data.value.y.max(range.from.y).min(range.to.y); } data.size_factor = *factor; dirty = true; } } if dirty { notify( &context, ScrollViewNotifyMessage { sender: context.id.to_owned(), state: data.clone(), }, ); let _ = context.state.write_with(data); } }); } ================================================ FILE: crates/core/src/widget/component/interactive/slider_view.rs ================================================ use crate::{ PropsData, Scalar, pre_hooks, unpack_named_slots, view_model::ViewModelValue, widget::{ component::interactive::{ button::{ButtonNotifyMessage, ButtonNotifyProps, ButtonProps, use_button}, navigation::{ NavSignal, NavTrackingNotifyMessage, NavTrackingNotifyProps, use_nav_item, use_nav_tracking_self, }, }, context::WidgetContext, node::WidgetNode, unit::area::AreaBoxNode, }, }; use intuicio_data::managed::ManagedLazy; use serde::{Deserialize, Serialize}; use std::ops::{Deref, DerefMut}; fn is_zero(value: &Scalar) -> bool { value.abs() < 1.0e-6 } pub trait SliderViewProxy: Send + Sync { fn get(&self) -> Scalar; fn set(&mut self, value: Scalar); } macro_rules! impl_proxy { ($type:ty) => { impl SliderViewProxy for $type { fn get(&self) -> Scalar { *self as _ } fn set(&mut self, value: Scalar) { *self = value as _; } } }; (@round $type:ty) => { impl SliderViewProxy for $type { fn get(&self) -> Scalar { *self as _ } fn set(&mut self, value: Scalar) { *self = value.round() as _; } } }; } impl_proxy!(@round u8); impl_proxy!(@round u16); impl_proxy!(@round u32); impl_proxy!(@round u64); impl_proxy!(@round u128); impl_proxy!(@round usize); impl_proxy!(@round i8); impl_proxy!(@round i16); impl_proxy!(@round i32); impl_proxy!(@round i64); impl_proxy!(@round i128); impl_proxy!(@round isize); impl_proxy!(f32); impl_proxy!(f64); impl SliderViewProxy for ViewModelValue where T: SliderViewProxy, { fn get(&self) -> Scalar { self.deref().get() } fn set(&mut self, value: Scalar) { self.deref_mut().set(value); } } #[derive(Clone)] pub struct SliderInput(ManagedLazy); impl SliderInput { pub fn new(data: ManagedLazy) -> Self { let (lifetime, data) = data.into_inner(); let data = data as *mut dyn SliderViewProxy; unsafe { Self(ManagedLazy::::new_raw(data, lifetime).unwrap()) } } pub fn into_inner(self) -> ManagedLazy { self.0 } pub fn get + Default>(&self) -> T { self.0 .read() .map(|data| data.get()) .and_then(|value| T::try_from(value).ok()) .unwrap_or_default() } pub fn set>(&mut self, value: T) { if let Some(mut data) = self.0.write() && let Ok(value) = value.try_into() { data.set(value); } } } impl std::fmt::Debug for SliderInput { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_tuple("SliderInput") .field(&self.0.read().map(|data| data.get()).unwrap_or_default()) .finish() } } impl From> for SliderInput { fn from(value: ManagedLazy) -> Self { Self::new(value) } } #[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum SliderViewDirection { #[default] LeftToRight, RightToLeft, TopToBottom, BottomToTop, } #[derive(PropsData, Debug, Default, Clone, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct SliderViewProps { #[serde(default)] #[serde(skip)] pub input: Option, #[serde(default)] #[serde(skip_serializing_if = "is_zero")] pub from: Scalar, #[serde(default)] #[serde(skip_serializing_if = "is_zero")] pub to: Scalar, #[serde(default)] pub direction: SliderViewDirection, } impl SliderViewProps { pub fn get_value(&self) -> Scalar { self.input .as_ref() .map(|input| input.get::()) .unwrap_or_default() } pub fn set_value(&mut self, value: Scalar) { if let Some(input) = self.input.as_mut() { input.set(value); } } pub fn get_percentage(&self) -> Scalar { (self.get_value() - self.from) / (self.to - self.from) } pub fn set_percentage(&mut self, value: Scalar) { self.set_value(value * (self.to - self.from) + self.from) } } #[pre_hooks(use_button, use_nav_tracking_self)] pub fn use_slider_view(context: &mut WidgetContext) { context .props .write(ButtonNotifyProps(context.id.to_owned().into())); context .props .write(NavTrackingNotifyProps(context.id.to_owned().into())); context.life_cycle.unmount(|context| { context.signals.write(NavSignal::Unlock); }); context.life_cycle.change(|context| { for msg in context.messenger.messages { if let Some(msg) = msg.as_any().downcast_ref::() { if msg.trigger_start() { context.signals.write(NavSignal::Lock); } if msg.trigger_stop() { context.signals.write(NavSignal::Unlock); } } else if let Some(msg) = msg.as_any().downcast_ref::() { let button = context.state.read_cloned_or_default::(); if button.selected && button.trigger { let mut props = context.props.read_cloned_or_default::(); let value = match props.direction { SliderViewDirection::LeftToRight => msg.state.factor.x, SliderViewDirection::RightToLeft => 1.0 - msg.state.factor.x, SliderViewDirection::TopToBottom => msg.state.factor.y, SliderViewDirection::BottomToTop => 1.0 - msg.state.factor.y, } .clamp(0.0, 1.0); let value = value * (props.to - props.from) + props.from; if let Some(input) = props.input.as_mut() { input.set(value); } } } } }); } #[pre_hooks(use_nav_item, use_slider_view)] pub fn slider_view(mut context: WidgetContext) -> WidgetNode { let WidgetContext { id, props, state, named_slots, .. } = context; unpack_named_slots!(named_slots => content); if let Some(p) = content.props_mut() { p.write(state.read_cloned_or_default::()); p.write(props.read_cloned_or_default::()); } AreaBoxNode { id: id.to_owned(), slot: Box::new(content), } .into() } ================================================ FILE: crates/core/src/widget/component/mod.rs ================================================ pub mod containers; pub mod image_box; pub mod interactive; pub mod space_box; pub mod text_box; use crate::{ MessageData, PrefabValue, PropsData, Scalar, messenger::Message, props::{Props, PropsData}, widget::{ FnWidget, WidgetId, WidgetIdOrRef, WidgetRef, context::WidgetContext, node::{WidgetNode, WidgetNodePrefab}, utils::{Rect, Vec2}, }, }; use intuicio_data::type_hash::TypeHash; use serde::{Deserialize, Serialize}; use std::{collections::HashMap, convert::TryFrom}; fn is_false(v: &bool) -> bool { !*v } #[derive(PropsData, Debug, Default, Clone, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct MessageForwardProps { #[serde(default)] #[serde(skip_serializing_if = "WidgetIdOrRef::is_none")] pub to: WidgetIdOrRef, #[serde(default)] #[serde(skip)] pub types: Vec, #[serde(default)] #[serde(skip_serializing_if = "is_false")] pub no_wrap: bool, } impl MessageForwardProps { pub fn with_type(mut self) -> Self { self.types.push(TypeHash::of::()); self } } #[derive(MessageData, Debug, Clone)] #[message_data(crate::messenger::MessageData)] pub struct ForwardedMessage { pub sender: WidgetId, pub data: Message, } pub fn use_message_forward(context: &mut WidgetContext) { context.life_cycle.change(|context| { let (id, no_wrap, types) = match context.props.read::() { Ok(forward) => match forward.to.read() { Some(id) => (id, forward.no_wrap, &forward.types), _ => return, }, _ => match context.shared_props.read::() { Ok(forward) => match forward.to.read() { Some(id) => (id, forward.no_wrap, &forward.types), _ => return, }, _ => return, }, }; for msg in context.messenger.messages { let t = msg.type_hash(); if types.contains(&t) { if no_wrap { context .messenger .write_raw(id.to_owned(), msg.clone_message()); } else { context.messenger.write( id.to_owned(), ForwardedMessage { sender: context.id.to_owned(), data: msg.clone_message(), }, ); } } } }); } #[derive(MessageData, Debug, Copy, Clone, PartialEq)] #[message_data(crate::messenger::MessageData)] pub enum ResizeListenerSignal { Register, Unregister, Change(Vec2), } pub fn use_resize_listener(context: &mut WidgetContext) { context.life_cycle.mount(|context| { context.signals.write(ResizeListenerSignal::Register); }); context.life_cycle.unmount(|context| { context.signals.write(ResizeListenerSignal::Unregister); }); } #[derive(PropsData, Debug, Default, Clone, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct RelativeLayoutProps { #[serde(default)] #[serde(skip_serializing_if = "WidgetIdOrRef::is_none")] pub relative_to: WidgetIdOrRef, } #[derive(MessageData, Debug, Clone, PartialEq)] #[message_data(crate::messenger::MessageData)] pub enum RelativeLayoutListenerSignal { /// (relative to id) Register(WidgetId), Unregister, /// (outer box size, inner box rect) Change(Vec2, Rect), } pub fn use_relative_layout_listener(context: &mut WidgetContext) { context.life_cycle.mount(|context| { if let Ok(props) = context.props.read::() && let Some(relative_to) = props.relative_to.read() { context .signals .write(RelativeLayoutListenerSignal::Register(relative_to)); } }); // TODO: when user will change widget IDs after mounting, we might want to re-register // this widget with new IDs. context.life_cycle.unmount(|context| { context .signals .write(RelativeLayoutListenerSignal::Unregister); }); } #[derive(PropsData, Debug, Copy, Clone, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct WidgetAlpha(pub Scalar); impl Default for WidgetAlpha { fn default() -> Self { Self(1.0) } } impl WidgetAlpha { pub fn multiply(&mut self, alpha: Scalar) { self.0 *= alpha; } } #[derive(Clone)] pub struct WidgetComponent { pub processor: FnWidget, pub type_name: String, pub key: Option, pub idref: Option, pub props: Props, pub shared_props: Option, pub listed_slots: Vec, pub named_slots: HashMap, } impl WidgetComponent { pub fn new(processor: FnWidget, type_name: impl ToString) -> Self { Self { processor, type_name: type_name.to_string(), key: None, idref: None, props: Props::default(), shared_props: None, listed_slots: Vec::new(), named_slots: HashMap::new(), } } pub fn key(mut self, v: T) -> Self where T: ToString, { self.key = Some(v.to_string()); self } pub fn idref(mut self, v: T) -> Self where T: Into, { self.idref = Some(v.into()); self } pub fn maybe_idref(mut self, v: Option) -> Self where T: Into, { self.idref = v.map(|v| v.into()); self } pub fn with_props(mut self, v: T) -> Self where T: 'static + PropsData, { self.props.write(v); self } pub fn maybe_with_props(self, v: Option) -> Self where T: 'static + PropsData, { if let Some(v) = v { self.with_props(v) } else { self } } pub fn merge_props(mut self, v: Props) -> Self { let props = std::mem::take(&mut self.props); self.props = props.merge(v); self } pub fn with_shared_props(mut self, v: T) -> Self where T: 'static + PropsData, { if let Some(props) = &mut self.shared_props { props.write(v); } else { self.shared_props = Some(Props::new(v)); } self } pub fn maybe_with_shared_props(self, v: Option) -> Self where T: 'static + PropsData, { if let Some(v) = v { self.with_shared_props(v) } else { self } } pub fn merge_shared_props(mut self, v: Props) -> Self { if let Some(props) = self.shared_props.take() { self.shared_props = Some(props.merge(v)); } else { self.shared_props = Some(v); } self } pub fn listed_slot(mut self, v: T) -> Self where T: Into, { self.listed_slots.push(v.into()); self } pub fn maybe_listed_slot(mut self, v: Option) -> Self where T: Into, { if let Some(v) = v { self.listed_slots.push(v.into()); } self } pub fn listed_slots(mut self, v: I) -> Self where I: IntoIterator, T: Into, { self.listed_slots.extend(v.into_iter().map(|v| v.into())); self } pub fn named_slot(mut self, k: impl ToString, v: T) -> Self where T: Into, { self.named_slots.insert(k.to_string(), v.into()); self } pub fn maybe_named_slot(mut self, k: impl ToString, v: Option) -> Self where T: Into, { if let Some(v) = v { self.named_slots.insert(k.to_string(), v.into()); } self } pub fn named_slots(mut self, v: I) -> Self where I: IntoIterator, K: ToString, T: Into, { self.named_slots .extend(v.into_iter().map(|(k, v)| (k.to_string(), v.into()))); self } pub fn remap_props(&mut self, mut f: F) where F: FnMut(Props) -> Props, { let props = std::mem::take(&mut self.props); self.props = (f)(props); } pub fn remap_shared_props(&mut self, mut f: F) where F: FnMut(Props) -> Props, { if let Some(shared_props) = &mut self.shared_props { let props = std::mem::take(shared_props); *shared_props = (f)(props); } else { self.shared_props = Some((f)(Default::default())); } } } impl std::fmt::Debug for WidgetComponent { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut s = f.debug_struct("WidgetComponent"); s.field("type_name", &self.type_name); if let Some(key) = &self.key { s.field("key", key); } s.field("props", &self.props); s.field("shared_props", &self.shared_props); if !self.listed_slots.is_empty() { s.field("listed_slots", &self.listed_slots); } if !self.named_slots.is_empty() { s.field("named_slots", &self.named_slots); } s.finish() } } impl TryFrom for WidgetComponent { type Error = (); fn try_from(node: WidgetNode) -> Result { if let WidgetNode::Component(v) = node { Ok(v) } else { Err(()) } } } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub(crate) struct WidgetComponentPrefab { #[serde(default)] pub type_name: String, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] pub key: Option, #[serde(default)] pub props: PrefabValue, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] pub shared_props: Option, #[serde(default)] #[serde(skip_serializing_if = "Vec::is_empty")] pub listed_slots: Vec, #[serde(default)] #[serde(skip_serializing_if = "HashMap::is_empty")] pub named_slots: HashMap, } ================================================ FILE: crates/core/src/widget/component/space_box.rs ================================================ use crate::{ PropsData, Scalar, widget::{ context::WidgetContext, node::WidgetNode, unit::size::{SizeBoxNode, SizeBoxSizeValue}, }, }; use serde::{Deserialize, Serialize}; #[derive(PropsData, Debug, Default, Clone, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct SpaceBoxProps { #[serde(default)] pub width: Scalar, #[serde(default)] pub height: Scalar, } impl SpaceBoxProps { pub fn cube(value: Scalar) -> Self { Self { width: value, height: value, } } pub fn horizontal(width: Scalar) -> Self { Self { width, height: 0.0 } } pub fn vertical(height: Scalar) -> Self { Self { width: 0.0, height } } } pub fn space_box(context: WidgetContext) -> WidgetNode { let WidgetContext { id, props, .. } = context; let SpaceBoxProps { width, height } = props.read_cloned_or_default(); SizeBoxNode { id: id.to_owned(), props: props.clone(), width: SizeBoxSizeValue::Exact(width), height: SizeBoxSizeValue::Exact(height), ..Default::default() } .into() } ================================================ FILE: crates/core/src/widget/component/text_box.rs ================================================ use crate::{ PropsData, widget::{ component::WidgetAlpha, context::WidgetContext, node::WidgetNode, unit::text::{ TextBoxDirection, TextBoxFont, TextBoxHorizontalAlign, TextBoxNode, TextBoxSizeValue, TextBoxVerticalAlign, }, utils::{Color, Transform}, }, }; use serde::{Deserialize, Serialize}; #[derive(PropsData, Debug, Default, Clone, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct TextBoxProps { #[serde(default)] pub text: String, #[serde(default)] pub width: TextBoxSizeValue, #[serde(default)] pub height: TextBoxSizeValue, #[serde(default)] pub horizontal_align: TextBoxHorizontalAlign, #[serde(default)] pub vertical_align: TextBoxVerticalAlign, #[serde(default)] pub direction: TextBoxDirection, #[serde(default)] pub font: TextBoxFont, #[serde(default)] pub color: Color, #[serde(default)] pub transform: Transform, } pub fn text_box(context: WidgetContext) -> WidgetNode { let WidgetContext { id, props, shared_props, .. } = context; let TextBoxProps { width, height, text, horizontal_align, vertical_align, direction, font, mut color, transform, } = props.read_cloned_or_default(); let alpha = shared_props.read_cloned_or_default::().0; color.a *= alpha; TextBoxNode { id: id.to_owned(), props: props.clone(), text, width, height, horizontal_align, vertical_align, direction, font, color, transform, } .into() } ================================================ FILE: crates/core/src/widget/context.rs ================================================ use crate::{ animator::{Animator, AnimatorStates}, messenger::{MessageSender, Messenger}, props::Props, signals::SignalSender, state::State, view_model::ViewModelCollectionView, widget::{WidgetId, WidgetLifeCycle, WidgetRef, node::WidgetNode}, }; use std::collections::HashMap; pub struct WidgetContext<'a> { pub id: &'a WidgetId, pub idref: Option<&'a WidgetRef>, pub key: &'a str, pub props: &'a mut Props, pub shared_props: &'a mut Props, pub state: State<'a>, pub animator: &'a AnimatorStates, pub life_cycle: &'a mut WidgetLifeCycle, pub named_slots: HashMap, pub listed_slots: Vec, pub view_models: ViewModelCollectionView<'a>, } impl WidgetContext<'_> { pub fn take_named_slots(&mut self) -> HashMap { std::mem::take(&mut self.named_slots) } pub fn take_named_slot(&mut self, name: &str) -> WidgetNode { self.named_slots.remove(name).unwrap_or_default() } pub fn take_listed_slots(&mut self) -> Vec { std::mem::take(&mut self.listed_slots) } pub fn use_hook(&mut self, mut f: F) -> &mut Self where F: FnMut(&mut Self), { (f)(self); self } } impl std::fmt::Debug for WidgetContext<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("WidgetContext") .field("id", &self.id) .field("key", &self.key) .field("props", &self.props) .field("shared_props", &self.shared_props) .field("named_slots", &self.named_slots) .field("listed_slots", &self.listed_slots) .finish() } } pub struct WidgetMountOrChangeContext<'a> { pub id: &'a WidgetId, pub props: &'a Props, pub shared_props: &'a Props, pub state: State<'a>, pub messenger: Messenger<'a>, pub signals: SignalSender, pub animator: Animator<'a>, pub view_models: ViewModelCollectionView<'a>, } pub struct WidgetUnmountContext<'a> { pub id: &'a WidgetId, pub state: &'a Props, pub messenger: &'a MessageSender, pub signals: SignalSender, pub view_models: ViewModelCollectionView<'a>, } ================================================ FILE: crates/core/src/widget/mod.rs ================================================ //! Widget types and the core component collection pub mod component; pub mod context; pub mod node; pub mod unit; pub mod utils; use crate::{ Prefab, PropsData, application::Application, props::PropsData, widget::{ context::{WidgetContext, WidgetMountOrChangeContext, WidgetUnmountContext}, node::WidgetNode, }, }; use serde::{Deserialize, Serialize}; use std::{ borrow::Cow, collections::hash_map::DefaultHasher, convert::TryFrom, hash::{Hash, Hasher}, ops::{Deref, Range}, str::FromStr, sync::{Arc, RwLock}, }; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WidgetIdDef(pub String); impl From for WidgetIdDef { fn from(data: WidgetId) -> Self { Self(data.to_string()) } } #[derive(Debug, Clone, Copy)] pub struct WidgetIdMetaParam<'a> { pub name: &'a str, pub value: Option<&'a str>, } impl WidgetIdMetaParam<'_> { pub fn is_flag(&self) -> bool { self.value.is_none() } pub fn has_value(&self) -> bool { self.value.is_some() } } #[derive(Debug, Clone, Copy)] pub struct WidgetIdMetaParams<'a>(&'a str); impl<'a> WidgetIdMetaParams<'a> { pub fn new(meta: &'a str) -> Self { Self(meta) } pub fn iter(&'_ self) -> impl Iterator> { self.0.split('&').filter_map(|part| { if let Some(index) = part.find('=') { let name = &part[0..index]; let value = &part[(index + b"=".len())..]; if name.is_empty() { None } else { Some(WidgetIdMetaParam { name, value: Some(value), }) } } else if part.is_empty() { None } else { Some(WidgetIdMetaParam { name: part, value: None, }) } }) } pub fn find(&'_ self, name: &str) -> Option> { self.iter().find(|param| param.name == name) } pub fn has_flag(&self, name: &str) -> bool { self.iter() .any(|param| param.name == name && param.is_flag()) } pub fn find_value(&self, name: &str) -> Option<&str> { self.iter().find_map(|param| { if param.name == name { param.value } else { None } }) } } #[derive(PropsData, Default, Clone, Serialize, Deserialize)] #[serde(try_from = "WidgetIdDef")] #[serde(into = "WidgetIdDef")] pub struct WidgetId { id: String, type_name: Range, /// [(key range, meta range)] parts: Vec<(Range, Range)>, } impl WidgetId { pub fn empty() -> Self { Self { id: ":".to_owned(), type_name: 0..0, parts: Default::default(), } } pub fn new(type_name: &str, path: &[Cow<'_, str>]) -> Self { if path.is_empty() { return Self { id: format!("{type_name}:"), type_name: 0..type_name.len(), parts: Default::default(), }; } let count = type_name.len() + b":".len() + path.iter().map(|part| part.len()).sum::() + path.len().saturating_sub(1) * b"/".len(); let mut result = String::with_capacity(count); let mut position = result.len(); result.push_str(type_name); let type_name = 0..result.len(); result.push(':'); let parts = path .iter() .enumerate() .map(|(index, part)| { if index > 0 { result.push('/'); } position = result.len(); result.push_str(part); let range = position..result.len(); if let Some(index) = part.find('?') { let key = range.start..(range.start + index); let meta = (range.start + index + b"?".len())..range.end; (key, meta) } else { let meta = range.end..range.end; (range, meta) } }) .collect::>(); Self { id: result, type_name, parts, } } pub fn push(&self, part: &str) -> Self { let count = self.id.len() + b"/".len(); let mut result = String::with_capacity(count); result.push_str(&self.id); if self.depth() > 0 { result.push('/'); } let position = result.len(); result.push_str(part); let range = position..result.len(); let (key, meta) = if let Some(index) = part.find('?') { let key = range.start..(range.start + index); let meta = (range.start + index + b"?".len())..range.end; (key, meta) } else { let meta = range.end..range.end; (range, meta) }; let parts = self .parts .iter() .cloned() .chain(std::iter::once((key, meta))) .collect(); Self { id: result, type_name: self.type_name.to_owned(), parts, } } pub fn pop(&self) -> Self { let parts = self.parts[0..(self.parts.len().saturating_sub(1))].to_owned(); let result = if let Some(range) = parts.last() { self.id[0..range.1.end].to_owned() } else { format!("{}:", self.type_name()) }; Self { id: result, type_name: self.type_name.to_owned(), parts, } } #[inline] pub fn is_valid(&self) -> bool { !self.id.is_empty() } #[inline] pub fn depth(&self) -> usize { self.parts.len() } #[inline] pub fn type_name(&self) -> &str { &self.id.as_str()[self.type_name.clone()] } #[inline] pub fn path(&self) -> &str { if self.parts.is_empty() { &self.id.as_str()[0..0] } else { &self.id.as_str()[self.parts.first().unwrap().0.start..self.parts.last().unwrap().1.end] } } #[inline] pub fn key(&self) -> &str { if self.parts.is_empty() { &self.id.as_str()[0..0] } else { &self.id[self.parts.last().cloned().unwrap().0] } } #[inline] pub fn meta(&self) -> &str { if self.parts.is_empty() { &self.id.as_str()[0..0] } else { &self.id[self.parts.last().cloned().unwrap().1] } } #[inline] pub fn part(&self, index: usize) -> Option<&str> { self.parts .get(index) .cloned() .map(|(key, meta)| &self.id[key.start..meta.end]) } #[inline] pub fn part_key_meta(&self, index: usize) -> Option<(&str, &str)> { self.parts .get(index) .cloned() .map(|(key, meta)| (&self.id[key], &self.id[meta])) } pub fn range(&self, from_inclusive: usize, to_exclusive: usize) -> &str { if self.parts.is_empty() { return &self.id[0..0]; } let start = from_inclusive.min(self.parts.len().saturating_sub(1)); let end = to_exclusive .saturating_sub(1) .max(start) .min(self.parts.len().saturating_sub(1)); let start = self.parts[start].0.start; let end = self.parts[end].1.end; &self.id[start..end] } pub fn is_subset_of(&self, other: &Self) -> bool { match self.distance_to(other) { Ok(v) => v < 0, _ => false, } } pub fn is_superset_of(&self, other: &Self) -> bool { match self.distance_to(other) { Ok(v) => v > 0, _ => false, } } pub fn distance_to(&self, other: &Self) -> Result { for index in 0..self.depth().max(other.depth()) { match (self.part(index), other.part(index)) { (None, None) => return Ok(0), (None, Some(_)) | (Some(_), None) => { return Ok(self.depth() as isize - other.depth() as isize); } (Some(a), Some(b)) => { if a != b { return Err(index as isize - other.depth() as isize); } } } } Ok(0) } #[inline] pub fn parts(&self) -> impl Iterator + '_ { self.parts .iter() .map(move |(key, meta)| &self.id[key.start..meta.end]) } #[inline] pub fn parts_key_meta(&self) -> impl Iterator + '_ { self.parts .iter() .cloned() .map(move |(key, meta)| (&self.id[key], &self.id[meta])) } #[inline] pub fn rparts(&self) -> impl Iterator + '_ { self.parts .iter() .rev() .map(move |(key, meta)| &self.id[key.start..meta.end]) } #[inline] pub fn rparts_key_meta(&self) -> impl Iterator + '_ { self.parts .iter() .rev() .cloned() .map(move |(key, meta)| (&self.id[key], &self.id[meta])) } pub fn hashed_value(&self) -> u64 { let mut hasher = DefaultHasher::new(); self.hash(&mut hasher); hasher.finish() } } impl Hash for WidgetId { fn hash(&self, state: &mut H) { self.id.hash(state); } } impl PartialEq for WidgetId { fn eq(&self, other: &Self) -> bool { self.id == other.id } } impl Eq for WidgetId {} impl Deref for WidgetId { type Target = str; fn deref(&self) -> &Self::Target { &self.id } } impl AsRef for WidgetId { fn as_ref(&self) -> &str { &self.id } } impl FromStr for WidgetId { type Err = (); fn from_str(s: &str) -> Result { if let Some(index) = s.find(':') { let type_name = s[..index].to_owned(); let rest = &s[(index + b":".len())..]; let path = rest.split('/').map(Cow::Borrowed).collect::>(); Ok(Self::new(&type_name, &path)) } else { Err(()) } } } impl std::fmt::Debug for WidgetId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("WidgetId") .field("id", &self.id) .field("type_name", &&self.id[self.type_name.clone()]) .field( "parts", &self .parts .iter() .map(|(key, meta)| (&self.id[key.to_owned()], &self.id[meta.to_owned()])) .collect::>(), ) .finish() } } impl std::fmt::Display for WidgetId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(self.as_ref()) } } impl TryFrom for WidgetId { type Error = String; fn try_from(id: WidgetIdDef) -> Result { match Self::from_str(&id.0) { Ok(id) => Ok(id), Err(_) => Err(format!("Could not parse id: `{}`", id.0)), } } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct WidgetIdCommon { id: Option, count: usize, } impl Default for WidgetIdCommon { fn default() -> Self { Self { id: None, count: usize::MAX, } } } impl WidgetIdCommon { pub fn new(id: WidgetId) -> Self { Self { count: id.depth(), id: Some(id), } } pub fn include(&mut self, id: &WidgetId) -> &mut Self { if self.id.is_none() { self.id = Some(id.to_owned()); self.count = id.depth(); return self; } if let Some(source) = self.id.as_ref() { for index in 0..self.count.min(id.depth()) { if source.part(index) != id.part(index) { self.count = index; return self; } } } self } pub fn include_other(&mut self, other: &Self) -> &mut Self { if let Some(id) = other.id.as_ref() { if self.id.is_none() { self.id = Some(id.to_owned()); self.count = other.count; return self; } if let Some(source) = self.id.as_ref() { for index in 0..self.count.min(other.count) { if source.part(index) != id.part(index) { self.count = index; return self; } } } } self } pub fn is_valid(&self) -> bool { self.id.is_some() } pub fn parts(&self) -> Option> { self.id .as_ref() .map(|id| (0..self.count).map_while(move |index| id.part(index))) } pub fn path(&self) -> Option<&str> { self.id .as_ref() .map(|id| id.range(0, self.count)) .filter(|id| !id.is_empty()) } } impl<'a> FromIterator<&'a WidgetId> for WidgetIdCommon { fn from_iter>(iter: T) -> Self { let mut result = Self::default(); for id in iter { result.include(id); } result } } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct WidgetRefDef(pub Option); impl From for WidgetRefDef { fn from(data: WidgetRef) -> Self { match data.0.read() { Ok(data) => Self(data.clone()), Err(_) => Default::default(), } } } #[derive(PropsData, Debug, Default, Clone, Serialize, Deserialize)] #[serde(from = "WidgetRefDef")] #[serde(into = "WidgetRefDef")] pub struct WidgetRef(#[serde(skip)] Arc>>); impl WidgetRef { pub fn new(id: WidgetId) -> Self { Self(Arc::new(RwLock::new(Some(id)))) } pub(crate) fn write(&mut self, id: WidgetId) { if let Ok(mut data) = self.0.write() { *data = Some(id); } } pub fn read(&self) -> Option { if let Ok(data) = self.0.read() { data.clone() } else { None } } pub fn exists(&self) -> bool { self.0 .read() .ok() .map(|data| data.is_some()) .unwrap_or_default() } } impl PartialEq for WidgetRef { fn eq(&self, other: &Self) -> bool { Arc::ptr_eq(&self.0, &other.0) } } impl From for WidgetRef { fn from(data: WidgetRefDef) -> Self { WidgetRef(Arc::new(RwLock::new(data.0))) } } impl std::fmt::Display for WidgetRef { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { if let Ok(id) = self.0.read() { if let Some(id) = id.as_ref() { write!(f, "{id}") } else { Ok(()) } } else { Ok(()) } } } #[derive(PropsData, Debug, Default, Clone, PartialEq, Serialize, Deserialize)] pub enum WidgetIdOrRef { #[default] None, Id(WidgetId), Ref(WidgetRef), } impl WidgetIdOrRef { #[inline] pub fn new_ref() -> Self { Self::Ref(WidgetRef::default()) } #[inline] pub fn is_none(&self) -> bool { matches!(self, Self::None) } #[inline] pub fn is_some(&self) -> bool { !self.is_none() } pub fn read(&self) -> Option { match self { Self::None => None, Self::Id(id) => Some(id.to_owned()), Self::Ref(idref) => idref.read(), } } } impl From<()> for WidgetIdOrRef { fn from(_: ()) -> Self { Self::None } } impl From for WidgetIdOrRef { fn from(v: WidgetId) -> Self { Self::Id(v) } } impl From for WidgetIdOrRef { fn from(v: WidgetRef) -> Self { Self::Ref(v) } } impl std::fmt::Display for WidgetIdOrRef { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::None => Ok(()), Self::Id(id) => write!(f, "{id}"), Self::Ref(id) => write!(f, "{id}"), } } } #[derive(Clone)] pub enum FnWidget { Pointer(fn(WidgetContext) -> WidgetNode), Closure(Arc WidgetNode + Send + Sync>), } impl FnWidget { pub fn pointer(value: fn(WidgetContext) -> WidgetNode) -> Self { Self::Pointer(value) } pub fn closure(value: impl Fn(WidgetContext) -> WidgetNode + Send + Sync + 'static) -> Self { Self::Closure(Arc::new(value)) } pub fn call(&self, context: WidgetContext) -> WidgetNode { match self { Self::Pointer(value) => value(context), Self::Closure(value) => value(context), } } } #[derive(Default)] pub struct WidgetLifeCycle { #[allow(clippy::type_complexity)] mount: Vec>, #[allow(clippy::type_complexity)] change: Vec>, #[allow(clippy::type_complexity)] unmount: Vec>, } impl WidgetLifeCycle { pub fn mount(&mut self, f: F) where F: 'static + FnMut(WidgetMountOrChangeContext) + Send + Sync, { self.mount.push(Box::new(f)); } pub fn change(&mut self, f: F) where F: 'static + FnMut(WidgetMountOrChangeContext) + Send + Sync, { self.change.push(Box::new(f)); } pub fn unmount(&mut self, f: F) where F: 'static + FnMut(WidgetUnmountContext) + Send + Sync, { self.unmount.push(Box::new(f)); } #[allow(clippy::type_complexity)] pub fn unwrap( self, ) -> ( Vec>, Vec>, Vec>, ) { let Self { mount, change, unmount, } = self; (mount, change, unmount) } } pub fn none_widget(_: WidgetContext) -> WidgetNode { Default::default() } pub fn setup(app: &mut Application) { app.register_props::<()>("()"); app.register_props::("i8"); app.register_props::("i16"); app.register_props::("i32"); app.register_props::("i64"); app.register_props::("i128"); app.register_props::("u8"); app.register_props::("u16"); app.register_props::("u32"); app.register_props::("u64"); app.register_props::("u128"); app.register_props::("f32"); app.register_props::("f64"); app.register_props::("bool"); app.register_props::("String"); app.register_props::("AnchorProps"); app.register_props::("PivotBoxProps"); app.register_props::("ContentBoxProps"); app.register_props::("FlexBoxProps"); app.register_props::("FloatBoxProps"); app.register_props::( "FloatBoxNotifyProps", ); app.register_props::("FloatBoxState"); app.register_props::("GridBoxProps"); app.register_props::( "HorizontalBoxProps", ); app.register_props::("HiddenBoxProps"); app.register_props::("ScrollBoxOwner"); app.register_props::( "SideScrollbarsProps", ); app.register_props::( "SideScrollbarsState", ); app.register_props::("PortalsContainer"); app.register_props::( "MediaQueryExpression", ); app.register_props::( "ResponsiveBoxState", ); app.register_props::("SizeBoxProps"); app.register_props::("SwitchBoxProps"); app.register_props::("TabsBoxProps"); app.register_props::("TabPlateProps"); app.register_props::("TooltipState"); app.register_props::("VariantBoxProps"); app.register_props::("VerticalBoxProps"); app.register_props::("WrapBoxProps"); app.register_props::("ImageBoxProps"); app.register_props::("ButtonProps"); app.register_props::("ButtonNotifyProps"); app.register_props::("TextInputMode"); app.register_props::("TextInputProps"); app.register_props::("TextInputState"); app.register_props::( "TextInputNotifyProps", ); app.register_props::( "TextInputControlNotifyProps", ); app.register_props::("OptionsViewMode"); app.register_props::( "OptionsViewProps", ); app.register_props::("SliderViewProps"); app.register_props::("NavItemActive"); app.register_props::( "NavTrackingActive", ); app.register_props::( "NavContainerActive", ); app.register_props::("NavJumpLooped"); app.register_props::("NavJumpMapProps"); app.register_props::("ScrollViewState"); app.register_props::("ScrollViewRange"); app.register_props::( "ScrollViewNotifyProps", ); app.register_props::("MessageForwardProps"); app.register_props::("WidgetAlpha"); app.register_props::("SpaceBoxProps"); app.register_props::("TextBoxProps"); app.register_props::("ContentBoxItemLayout"); app.register_props::("FlexBoxItemLayout"); app.register_props::("GridBoxItemLayout"); app.register_component("none_widget", FnWidget::pointer(none_widget)); app.register_component( "area_box", FnWidget::pointer(component::containers::area_box::area_box), ); app.register_component( "anchor_box", FnWidget::pointer(component::containers::anchor_box::anchor_box), ); app.register_component( "pivot_box", FnWidget::pointer(component::containers::anchor_box::pivot_box), ); app.register_component( "nav_content_box", FnWidget::pointer(component::containers::content_box::nav_content_box), ); app.register_component( "content_box", FnWidget::pointer(component::containers::content_box::content_box), ); app.register_component( "nav_flex_box", FnWidget::pointer(component::containers::flex_box::nav_flex_box), ); app.register_component( "flex_box", FnWidget::pointer(component::containers::flex_box::flex_box), ); app.register_component( "float_box", FnWidget::pointer(component::containers::float_box::float_box), ); app.register_component( "nav_grid_box", FnWidget::pointer(component::containers::grid_box::nav_grid_box), ); app.register_component( "grid_box", FnWidget::pointer(component::containers::grid_box::grid_box), ); app.register_component( "nav_horizontal_box", FnWidget::pointer(component::containers::horizontal_box::nav_horizontal_box), ); app.register_component( "horizontal_box", FnWidget::pointer(component::containers::horizontal_box::horizontal_box), ); app.register_component( "nav_scroll_box", FnWidget::pointer(component::containers::scroll_box::nav_scroll_box), ); app.register_component( "nav_scroll_box_side_scrollbars", FnWidget::pointer(component::containers::scroll_box::nav_scroll_box_side_scrollbars), ); app.register_component( "portal_box", FnWidget::pointer(component::containers::portal_box::portal_box), ); app.register_component( "responsive_box", FnWidget::pointer(component::containers::responsive_box::responsive_box), ); app.register_component( "responsive_props_box", FnWidget::pointer(component::containers::responsive_box::responsive_props_box), ); app.register_component( "size_box", FnWidget::pointer(component::containers::size_box::size_box), ); app.register_component( "nav_switch_box", FnWidget::pointer(component::containers::switch_box::nav_switch_box), ); app.register_component( "switch_box", FnWidget::pointer(component::containers::switch_box::switch_box), ); app.register_component( "nav_tabs_box", FnWidget::pointer(component::containers::tabs_box::nav_tabs_box), ); app.register_component( "tooltip_box", FnWidget::pointer(component::containers::tooltip_box::tooltip_box), ); app.register_component( "portals_tooltip_box", FnWidget::pointer(component::containers::tooltip_box::portals_tooltip_box), ); app.register_component( "variant_box", FnWidget::pointer(component::containers::variant_box::variant_box), ); app.register_component( "nav_vertical_box", FnWidget::pointer(component::containers::vertical_box::nav_vertical_box), ); app.register_component( "vertical_box", FnWidget::pointer(component::containers::vertical_box::vertical_box), ); app.register_component( "wrap_box", FnWidget::pointer(component::containers::wrap_box::wrap_box), ); app.register_component( "button", FnWidget::pointer(component::interactive::button::button), ); app.register_component( "tracked_button", FnWidget::pointer(component::interactive::button::tracked_button), ); app.register_component( "self_tracked_button", FnWidget::pointer(component::interactive::button::self_tracked_button), ); app.register_component( "text_input", FnWidget::pointer(component::interactive::input_field::text_input), ); app.register_component( "input_field", FnWidget::pointer(component::interactive::input_field::input_field), ); app.register_component( "options_view", FnWidget::pointer(component::interactive::options_view::options_view), ); app.register_component( "slider_view", FnWidget::pointer(component::interactive::slider_view::slider_view), ); app.register_component( "navigation_barrier", FnWidget::pointer(component::interactive::navigation::navigation_barrier), ); app.register_component( "tracking", FnWidget::pointer(component::interactive::navigation::tracking), ); app.register_component( "self_tracking", FnWidget::pointer(component::interactive::navigation::self_tracking), ); app.register_component( "space_box", FnWidget::pointer(component::space_box::space_box), ); app.register_component( "image_box", FnWidget::pointer(component::image_box::image_box), ); app.register_component("text_box", FnWidget::pointer(component::text_box::text_box)); } /// Helper to manually create a [`WidgetComponent`][crate::widget::component::WidgetComponent] /// struct from a function. /// /// Users will not usually need this macro, but it can be useful in some advanced cases or where you /// don't want to use the [`widget`] macro. /// ``` #[macro_export] macro_rules! make_widget { ($type_id:path) => {{ let processor = $type_id; let type_name = stringify!($type_id); $crate::widget::component::WidgetComponent::new( $crate::widget::FnWidget::pointer(processor), type_name, ) }}; } /// A helper for getting the named children out of a widget context. #[macro_export] macro_rules! unpack_named_slots { ($map:expr => $name:ident) => { #[allow(unused_mut)] let mut $name = { let mut map = $map; match map.remove(stringify!($name)) { Some(widget) => widget, None => $crate::widget::node::WidgetNode::None, } }; }; ($map:expr => { $($name:ident),+ }) => { #[allow(unused_mut)] let ( $( mut $name ),+ ) = { let mut map = $map; ( $( { match map.remove(stringify!($name)) { Some(widget) => widget, None => $crate::widget::node::WidgetNode::None, } } ),+ ) }; }; } #[cfg(test)] mod tests { use super::*; #[test] fn test_widget_id() { let id = WidgetId::empty(); assert_eq!(id.type_name(), ""); assert_eq!(id.path(), ""); assert_eq!(id.depth(), 0); let id = WidgetId::new("type", &["parent".into(), "me".into()]); assert_eq!(id.to_string(), "type:parent/me".to_owned()); assert_eq!(id.type_name(), "type"); assert_eq!(id.parts().next().unwrap(), "parent"); assert_eq!(id.key(), "me"); assert_eq!(id.clone(), id); let a = WidgetId::from_str("a:root/a").unwrap(); let b = WidgetId::from_str("b:root/b").unwrap(); let mut common = WidgetIdCommon::default(); assert_eq!(common.path(), None); common.include(&a); assert_eq!(common.path(), Some("root/a")); let mut common = WidgetIdCommon::default(); common.include(&b); assert_eq!(common.path(), Some("root/b")); common.include(&a); assert_eq!(common.path(), Some("root")); let id = WidgetId::from_str("type:parent/me").unwrap(); assert_eq!(&*id, "type:parent/me"); assert_eq!(id.path(), "parent/me"); let id = id.pop(); assert_eq!(&*id, "type:parent"); assert_eq!(id.path(), "parent"); let id = id.pop(); assert_eq!(&*id, "type:"); assert_eq!(id.path(), ""); let id = id.push("parent"); assert_eq!(&*id, "type:parent"); assert_eq!(id.path(), "parent"); let id = id.push("me"); assert_eq!(&*id, "type:parent/me"); assert_eq!(id.path(), "parent/me"); assert_eq!(id.key(), "me"); assert_eq!(id.meta(), ""); let id = id.push("with?meta"); assert_eq!(&*id, "type:parent/me/with?meta"); assert_eq!(id.path(), "parent/me/with?meta"); assert_eq!(id.key(), "with"); assert_eq!(id.meta(), "meta"); let a = WidgetId::from_str("a:root/a/b/c").unwrap(); let b = WidgetId::from_str("b:root/a/b/c").unwrap(); assert_eq!(a.distance_to(&b), Ok(0)); assert!(!a.is_subset_of(&b)); assert!(!a.is_superset_of(&b)); let a = WidgetId::from_str("a:root/a/b").unwrap(); let b = WidgetId::from_str("b:root/a/b/c").unwrap(); assert_eq!(a.distance_to(&b), Ok(-1)); assert!(a.is_subset_of(&b)); assert!(!a.is_superset_of(&b)); let a = WidgetId::from_str("a:root/a").unwrap(); let b = WidgetId::from_str("b:root/a/b/c").unwrap(); assert_eq!(a.distance_to(&b), Ok(-2)); assert!(a.is_subset_of(&b)); assert!(!a.is_superset_of(&b)); let a = WidgetId::from_str("a:root").unwrap(); let b = WidgetId::from_str("b:root/a/b/c").unwrap(); assert_eq!(a.distance_to(&b), Ok(-3)); assert!(a.is_subset_of(&b)); assert!(!a.is_superset_of(&b)); let a = WidgetId::from_str("a:root/a/b/c").unwrap(); let b = WidgetId::from_str("b:root/a/b").unwrap(); assert_eq!(a.distance_to(&b), Ok(1)); assert!(!a.is_subset_of(&b)); assert!(a.is_superset_of(&b)); let a = WidgetId::from_str("a:root/a/b/c").unwrap(); let b = WidgetId::from_str("b:root/a").unwrap(); assert_eq!(a.distance_to(&b), Ok(2)); assert!(!a.is_subset_of(&b)); assert!(a.is_superset_of(&b)); let a = WidgetId::from_str("a:root/a/b/c").unwrap(); let b = WidgetId::from_str("b:root").unwrap(); assert_eq!(a.distance_to(&b), Ok(3)); assert!(!a.is_subset_of(&b)); assert!(a.is_superset_of(&b)); let a = WidgetId::from_str("a:root/a/b/x").unwrap(); let b = WidgetId::from_str("b:root/a/b/c").unwrap(); assert_eq!(a.distance_to(&b), Err(-1)); assert!(!a.is_subset_of(&b)); assert!(!a.is_superset_of(&b)); let a = WidgetId::from_str("a:root/a/x/y").unwrap(); let b = WidgetId::from_str("b:root/a/b/c").unwrap(); assert_eq!(a.distance_to(&b), Err(-2)); assert!(!a.is_subset_of(&b)); assert!(!a.is_superset_of(&b)); let a = WidgetId::from_str("a:root/x/y/z").unwrap(); let b = WidgetId::from_str("b:root/a/b/c").unwrap(); assert_eq!(a.distance_to(&b), Err(-3)); assert!(!a.is_subset_of(&b)); assert!(!a.is_superset_of(&b)); } } ================================================ FILE: crates/core/src/widget/node.rs ================================================ use crate::{ Prefab, props::Props, widget::{ component::{WidgetComponent, WidgetComponentPrefab}, unit::{WidgetUnitNode, WidgetUnitNodePrefab}, }, }; use serde::{Deserialize, Serialize}; #[allow(clippy::large_enum_variant)] #[derive(Debug, Default, Clone)] pub enum WidgetNode { #[default] None, Component(WidgetComponent), Unit(WidgetUnitNode), Tuple(Vec), } impl WidgetNode { pub fn is_none(&self) -> bool { match self { Self::None => true, Self::Unit(unit) => unit.is_none(), Self::Tuple(v) => v.is_empty(), _ => false, } } pub fn is_some(&self) -> bool { match self { Self::None => false, Self::Unit(unit) => unit.is_some(), Self::Tuple(v) => !v.is_empty(), _ => true, } } pub fn as_component(&self) -> Option<&WidgetComponent> { match self { Self::Component(c) => Some(c), _ => None, } } pub fn as_unit(&self) -> Option<&WidgetUnitNode> { match self { Self::Unit(u) => Some(u), _ => None, } } pub fn as_tuple(&self) -> Option<&[WidgetNode]> { match self { Self::Tuple(v) => Some(v), _ => None, } } pub fn props(&self) -> Option<&Props> { match self { Self::Component(c) => Some(&c.props), Self::Unit(u) => u.props(), _ => None, } } pub fn props_mut(&mut self) -> Option<&mut Props> { match self { Self::Component(c) => Some(&mut c.props), Self::Unit(u) => u.props_mut(), _ => None, } } pub fn remap_props(&mut self, f: F) where F: FnMut(Props) -> Props, { match self { Self::Component(c) => c.remap_props(f), Self::Unit(u) => u.remap_props(f), _ => {} } } pub fn shared_props(&self) -> Option<&Props> { match self { Self::Component(c) => c.shared_props.as_ref(), _ => None, } } pub fn shared_props_mut(&mut self) -> Option<&mut Props> { match self { Self::Component(c) => { if c.shared_props.is_none() { c.shared_props = Some(Default::default()); } c.shared_props.as_mut() } _ => None, } } pub fn remap_shared_props(&mut self, f: F) where F: FnMut(Props) -> Props, { if let Self::Component(c) = self { c.remap_shared_props(f); } } pub fn pack_tuple(data: [WidgetNode; N]) -> Self { Self::Tuple(data.into()) } pub fn unpack_tuple(self) -> [WidgetNode; N] { if let WidgetNode::Tuple(mut data) = self { let mut iter = data.drain(..); std::array::from_fn(|_| iter.next().unwrap_or_default()) } else { std::array::from_fn(|_| WidgetNode::None) } } } impl From<()> for WidgetNode { fn from(_: ()) -> Self { Self::None } } impl From<()> for Box { fn from(_: ()) -> Self { Box::new(WidgetNode::None) } } impl From for WidgetNode { fn from(component: WidgetComponent) -> Self { Self::Component(component) } } impl From for WidgetNode { fn from(unit: WidgetUnitNode) -> Self { Self::Unit(unit) } } impl From for Box { fn from(unit: WidgetUnitNode) -> Self { Box::new(WidgetNode::Unit(unit)) } } impl From<[WidgetNode; N]> for WidgetNode { fn from(data: [WidgetNode; N]) -> Self { Self::pack_tuple(data) } } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub(crate) enum WidgetNodePrefab { #[default] None, Component(WidgetComponentPrefab), Unit(WidgetUnitNodePrefab), Tuple(Vec), } impl Prefab for WidgetNodePrefab {} ================================================ FILE: crates/core/src/widget/unit/area.rs ================================================ use crate::widget::{ WidgetId, node::{WidgetNode, WidgetNodePrefab}, unit::{WidgetUnit, WidgetUnitData}, }; use serde::{Deserialize, Serialize}; use std::convert::TryFrom; #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct AreaBox { #[serde(default)] pub id: WidgetId, #[serde(default)] pub slot: Box, } impl WidgetUnitData for AreaBox { fn id(&self) -> &WidgetId { &self.id } fn get_children(&self) -> Vec<&WidgetUnit> { vec![&self.slot] } } impl TryFrom for AreaBox { type Error = (); fn try_from(node: AreaBoxNode) -> Result { let AreaBoxNode { id, slot } = node; Ok(Self { id, slot: Box::new(WidgetUnit::try_from(*slot)?), }) } } #[derive(Debug, Default, Clone)] pub struct AreaBoxNode { pub id: WidgetId, pub slot: Box, } impl From for WidgetNode { fn from(data: AreaBoxNode) -> Self { Self::Unit(data.into()) } } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub(crate) struct AreaBoxNodePrefab { #[serde(default)] pub id: WidgetId, #[serde(default)] pub slot: Box, } ================================================ FILE: crates/core/src/widget/unit/content.rs ================================================ use crate::{ PrefabValue, PropsData, Scalar, props::Props, widget::{ WidgetId, node::{WidgetNode, WidgetNodePrefab}, unit::{WidgetUnit, WidgetUnitData}, utils::{Rect, Transform, Vec2}, }, }; use serde::{Deserialize, Serialize}; use std::convert::TryFrom; #[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ContentBoxItemPreserveInBounds { #[serde(default)] pub width: bool, #[serde(default)] pub height: bool, } impl From for ContentBoxItemPreserveInBounds { fn from(value: bool) -> Self { Self { width: value, height: value, } } } #[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ContentBoxItemCutInBounds { #[serde(default)] pub left: bool, #[serde(default)] pub right: bool, #[serde(default)] pub top: bool, #[serde(default)] pub bottom: bool, } impl From for ContentBoxItemCutInBounds { fn from(value: bool) -> Self { Self { left: value, right: value, top: value, bottom: value, } } } #[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ContentBoxItemKeepInBounds { #[serde(default)] pub preserve: ContentBoxItemPreserveInBounds, #[serde(default)] pub cut: ContentBoxItemCutInBounds, } impl From for ContentBoxItemKeepInBounds { fn from(value: bool) -> Self { Self { preserve: value.into(), cut: value.into(), } } } /// Allows customizing how an item in a [`content_box`] is laid out /// /// [`content_box`]: crate::widget::component::containers::content_box::content_box #[derive(PropsData, Debug, Clone, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct ContentBoxItemLayout { #[serde(default = "ContentBoxItemLayout::default_anchors")] pub anchors: Rect, /// The margins to put on each side of the item #[serde(default)] pub margin: Rect, /// Tells in percentage, where is the center of mass of the widget, relative to it's box size #[serde(default)] pub align: Vec2, /// The amount to offset the item from where it would otherwise be laid out #[serde(default)] pub offset: Vec2, /// The "Z" depth of the item #[serde(default)] pub depth: Scalar, /// Set of constraints that tell if and how to keep item in container bounds #[serde(default)] pub keep_in_bounds: ContentBoxItemKeepInBounds, } impl ContentBoxItemLayout { fn default_anchors() -> Rect { Rect { left: 0.0, right: 1.0, top: 0.0, bottom: 1.0, } } } impl Default for ContentBoxItemLayout { fn default() -> Self { Self { anchors: Self::default_anchors(), margin: Default::default(), align: Default::default(), offset: Default::default(), depth: 0.0, keep_in_bounds: Default::default(), } } } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct ContentBoxItem { #[serde(default)] pub slot: WidgetUnit, #[serde(default)] pub layout: ContentBoxItemLayout, } impl TryFrom for ContentBoxItem { type Error = (); fn try_from(node: ContentBoxItemNode) -> Result { let ContentBoxItemNode { slot, layout } = node; Ok(Self { slot: WidgetUnit::try_from(slot)?, layout, }) } } #[derive(Debug, Default, Clone)] pub struct ContentBoxItemNode { pub slot: WidgetNode, pub layout: ContentBoxItemLayout, } #[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub struct ContentBoxContentReposition { #[serde(default)] pub offset: Vec2, #[serde(default = "ContentBoxContentReposition::default_scale")] pub scale: Vec2, } impl Default for ContentBoxContentReposition { fn default() -> Self { Self { offset: Default::default(), scale: Self::default_scale(), } } } impl ContentBoxContentReposition { fn default_scale() -> Vec2 { 1.0.into() } } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct ContentBox { #[serde(default)] pub id: WidgetId, #[serde(default)] #[serde(skip_serializing_if = "Vec::is_empty")] pub items: Vec, #[serde(default)] pub clipping: bool, #[serde(default)] pub content_reposition: ContentBoxContentReposition, #[serde(default)] pub transform: Transform, } impl WidgetUnitData for ContentBox { fn id(&self) -> &WidgetId { &self.id } fn get_children(&self) -> Vec<&WidgetUnit> { self.items.iter().map(|item| &item.slot).collect() } } impl TryFrom for ContentBox { type Error = (); fn try_from(node: ContentBoxNode) -> Result { let ContentBoxNode { id, items, clipping, content_reposition, transform, .. } = node; let items = items .into_iter() .map(ContentBoxItem::try_from) .collect::>()?; Ok(Self { id, items, clipping, content_reposition, transform, }) } } #[derive(Debug, Default, Clone)] pub struct ContentBoxNode { pub id: WidgetId, pub props: Props, pub items: Vec, pub clipping: bool, pub content_reposition: ContentBoxContentReposition, pub transform: Transform, } impl ContentBoxNode { pub fn remap_props(&mut self, mut f: F) where F: FnMut(Props) -> Props, { let props = std::mem::take(&mut self.props); self.props = (f)(props); } } impl From for WidgetNode { fn from(data: ContentBoxNode) -> Self { Self::Unit(data.into()) } } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub(crate) struct ContentBoxNodePrefab { #[serde(default)] pub id: WidgetId, #[serde(default)] pub props: PrefabValue, #[serde(default)] #[serde(skip_serializing_if = "Vec::is_empty")] pub items: Vec, #[serde(default)] pub clipping: bool, #[serde(default)] pub content_reposition: ContentBoxContentReposition, #[serde(default)] pub transform: Transform, } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub(crate) struct ContentBoxItemNodePrefab { #[serde(default)] pub slot: WidgetNodePrefab, #[serde(default)] pub layout: ContentBoxItemLayout, } ================================================ FILE: crates/core/src/widget/unit/flex.rs ================================================ use crate::{ PrefabValue, PropsData, Scalar, props::Props, widget::{ WidgetId, node::{WidgetNode, WidgetNodePrefab}, unit::{WidgetUnit, WidgetUnitData}, utils::{Rect, Transform}, }, }; use serde::{Deserialize, Serialize}; use std::convert::TryFrom; #[derive(PropsData, Debug, Clone, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct FlexBoxItemLayout { #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] pub basis: Option, #[serde(default = "FlexBoxItemLayout::default_fill")] pub fill: Scalar, #[serde(default = "FlexBoxItemLayout::default_grow")] pub grow: Scalar, #[serde(default = "FlexBoxItemLayout::default_shrink")] pub shrink: Scalar, #[serde(default)] pub margin: Rect, #[serde(default)] pub align: Scalar, } impl FlexBoxItemLayout { fn default_fill() -> Scalar { 1.0 } fn default_grow() -> Scalar { 1.0 } fn default_shrink() -> Scalar { 1.0 } pub fn cleared() -> Self { Self { fill: 0.0, grow: 0.0, shrink: 0.0, ..Default::default() } } pub fn no_growing_and_shrinking() -> Self { Self { grow: 0.0, shrink: 1.0, ..Default::default() } } } impl Default for FlexBoxItemLayout { fn default() -> Self { Self { basis: None, fill: Self::default_fill(), grow: Self::default_grow(), shrink: Self::default_shrink(), margin: Default::default(), align: 0.0, } } } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct FlexBoxItem { #[serde(default)] pub slot: WidgetUnit, #[serde(default)] pub layout: FlexBoxItemLayout, } impl TryFrom for FlexBoxItem { type Error = (); fn try_from(node: FlexBoxItemNode) -> Result { let FlexBoxItemNode { slot, layout } = node; Ok(Self { slot: WidgetUnit::try_from(slot)?, layout, }) } } #[derive(Debug, Default, Clone)] pub struct FlexBoxItemNode { pub slot: WidgetNode, pub layout: FlexBoxItemLayout, } #[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum FlexBoxDirection { #[default] HorizontalLeftToRight, HorizontalRightToLeft, VerticalTopToBottom, VerticalBottomToTop, } impl FlexBoxDirection { pub fn is_horizontal(&self) -> bool { *self == Self::HorizontalLeftToRight || *self == Self::HorizontalRightToLeft } pub fn is_vertical(&self) -> bool { *self == Self::VerticalTopToBottom || *self == Self::VerticalBottomToTop } pub fn is_order_ascending(&self) -> bool { *self == Self::HorizontalLeftToRight || *self == Self::VerticalTopToBottom } pub fn is_order_descending(&self) -> bool { *self == Self::HorizontalRightToLeft || *self == Self::VerticalBottomToTop } } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct FlexBox { #[serde(default)] pub id: WidgetId, #[serde(default)] #[serde(skip_serializing_if = "Vec::is_empty")] pub items: Vec, #[serde(default)] pub direction: FlexBoxDirection, #[serde(default)] pub separation: Scalar, #[serde(default)] pub wrap: bool, #[serde(default)] pub transform: Transform, } impl WidgetUnitData for FlexBox { fn id(&self) -> &WidgetId { &self.id } fn get_children(&self) -> Vec<&WidgetUnit> { self.items.iter().map(|item| &item.slot).collect() } } impl TryFrom for FlexBox { type Error = (); fn try_from(node: FlexBoxNode) -> Result { let FlexBoxNode { id, items, direction, separation, wrap, transform, .. } = node; let items = items .into_iter() .map(FlexBoxItem::try_from) .collect::>()?; Ok(Self { id, items, direction, separation, wrap, transform, }) } } #[derive(Debug, Default, Clone)] pub struct FlexBoxNode { pub id: WidgetId, pub props: Props, pub items: Vec, pub direction: FlexBoxDirection, pub separation: Scalar, pub wrap: bool, pub transform: Transform, } impl FlexBoxNode { pub fn remap_props(&mut self, mut f: F) where F: FnMut(Props) -> Props, { let props = std::mem::take(&mut self.props); self.props = (f)(props); } } impl From for WidgetNode { fn from(data: FlexBoxNode) -> Self { Self::Unit(data.into()) } } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub(crate) struct FlexBoxNodePrefab { #[serde(default)] pub id: WidgetId, #[serde(default)] pub props: PrefabValue, #[serde(default)] #[serde(skip_serializing_if = "Vec::is_empty")] pub items: Vec, #[serde(default)] pub direction: FlexBoxDirection, #[serde(default)] pub separation: Scalar, #[serde(default)] pub wrap: bool, #[serde(default)] pub transform: Transform, } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub(crate) struct FlexBoxItemNodePrefab { #[serde(default)] pub slot: WidgetNodePrefab, #[serde(default)] pub layout: FlexBoxItemLayout, } ================================================ FILE: crates/core/src/widget/unit/grid.rs ================================================ use crate::{ PrefabValue, PropsData, Scalar, props::Props, widget::{ WidgetId, node::{WidgetNode, WidgetNodePrefab}, unit::{WidgetUnit, WidgetUnitData}, utils::{IntRect, Rect, Transform}, }, }; use serde::{Deserialize, Serialize}; use std::convert::TryFrom; #[derive(PropsData, Debug, Default, Clone, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct GridBoxItemLayout { #[serde(default)] pub space_occupancy: IntRect, #[serde(default)] pub margin: Rect, #[serde(default)] pub horizontal_align: Scalar, #[serde(default)] pub vertical_align: Scalar, } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct GridBoxItem { #[serde(default)] pub slot: WidgetUnit, #[serde(default)] pub layout: GridBoxItemLayout, } impl TryFrom for GridBoxItem { type Error = (); fn try_from(node: GridBoxItemNode) -> Result { let GridBoxItemNode { slot, layout } = node; Ok(Self { slot: WidgetUnit::try_from(slot)?, layout, }) } } #[derive(Debug, Default, Clone)] pub struct GridBoxItemNode { pub slot: WidgetNode, pub layout: GridBoxItemLayout, } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct GridBox { #[serde(default)] pub id: WidgetId, #[serde(default)] #[serde(skip_serializing_if = "Vec::is_empty")] pub items: Vec, #[serde(default)] pub cols: usize, #[serde(default)] pub rows: usize, #[serde(default)] pub transform: Transform, } impl WidgetUnitData for GridBox { fn id(&self) -> &WidgetId { &self.id } fn get_children(&self) -> Vec<&WidgetUnit> { self.items.iter().map(|item| &item.slot).collect() } } impl TryFrom for GridBox { type Error = (); fn try_from(node: GridBoxNode) -> Result { let GridBoxNode { id, items, cols, rows, transform, .. } = node; let items = items .into_iter() .map(GridBoxItem::try_from) .collect::>()?; Ok(Self { id, items, cols, rows, transform, }) } } #[derive(Debug, Default, Clone)] pub struct GridBoxNode { pub id: WidgetId, pub props: Props, pub items: Vec, pub cols: usize, pub rows: usize, pub transform: Transform, } impl GridBoxNode { pub fn remap_props(&mut self, mut f: F) where F: FnMut(Props) -> Props, { let props = std::mem::take(&mut self.props); self.props = (f)(props); } } impl From for WidgetNode { fn from(data: GridBoxNode) -> Self { Self::Unit(data.into()) } } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub(crate) struct GridBoxNodePrefab { #[serde(default)] pub id: WidgetId, #[serde(default)] pub props: PrefabValue, #[serde(default)] #[serde(skip_serializing_if = "Vec::is_empty")] pub items: Vec, #[serde(default)] pub cols: usize, #[serde(default)] pub rows: usize, #[serde(default)] pub transform: Transform, } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub(crate) struct GridBoxItemNodePrefab { #[serde(default)] pub slot: WidgetNodePrefab, #[serde(default)] pub layout: GridBoxItemLayout, } ================================================ FILE: crates/core/src/widget/unit/image.rs ================================================ use crate::{ PrefabValue, Scalar, layout::CoordsMappingScaling, props::Props, widget::{ WidgetId, node::WidgetNode, unit::WidgetUnitData, utils::{Color, Rect, Transform, Vec2}, }, }; use serde::{Deserialize, Serialize}; use std::{collections::HashMap, convert::TryFrom, sync::Arc}; #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct ImageBoxFrame { #[serde(default)] pub source: Rect, #[serde(default)] pub destination: Rect, #[serde(default)] pub frame_only: bool, #[serde(default)] pub frame_keep_aspect_ratio: bool, } impl From for ImageBoxFrame { fn from(v: Scalar) -> Self { Self { source: v.into(), destination: v.into(), frame_only: false, frame_keep_aspect_ratio: false, } } } impl From<(Scalar, bool)> for ImageBoxFrame { fn from((v, fo): (Scalar, bool)) -> Self { Self { source: v.into(), destination: v.into(), frame_only: fo, frame_keep_aspect_ratio: false, } } } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub enum ImageBoxImageScaling { #[default] Stretch, Frame(ImageBoxFrame), } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct ImageBoxColor { #[serde(default)] pub color: Color, #[serde(default)] pub scaling: ImageBoxImageScaling, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ImageBoxImage { #[serde(default)] pub id: String, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] pub source_rect: Option, #[serde(default)] pub scaling: ImageBoxImageScaling, #[serde(default = "ImageBoxImage::default_tint")] pub tint: Color, } impl Default for ImageBoxImage { fn default() -> Self { Self { id: Default::default(), source_rect: Default::default(), scaling: Default::default(), tint: Self::default_tint(), } } } impl ImageBoxImage { fn default_tint() -> Color { Color { r: 1.0, g: 1.0, b: 1.0, a: 1.0, } } } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct ImageBoxProceduralVertex { #[serde(default)] pub position: Vec2, #[serde(default)] pub page: Scalar, #[serde(default)] pub tex_coord: Vec2, #[serde(default)] pub color: Color, } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct ImageBoxProceduralMeshData { #[serde(default)] #[serde(skip_serializing_if = "Vec::is_empty")] pub vertices: Vec, #[serde(default)] #[serde(skip_serializing_if = "Vec::is_empty")] pub triangles: Vec<[u32; 3]>, } #[derive(Clone, Serialize, Deserialize)] pub enum ImageBoxProceduralMesh { Owned(ImageBoxProceduralMeshData), Shared(Arc), /// fn(widget local space rect, procedural image parameters map) -> mesh data #[serde(skip)] #[allow(clippy::type_complexity)] Generator( Arc< dyn Fn(Rect, &HashMap) -> ImageBoxProceduralMeshData + 'static + Send + Sync, >, ), } impl std::fmt::Debug for ImageBoxProceduralMesh { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Owned(data) => write!(f, "Owned({data:?})"), Self::Shared(data) => write!(f, "Shared({data:?})"), Self::Generator(_) => write!(f, "Generator(...)"), } } } impl Default for ImageBoxProceduralMesh { fn default() -> Self { Self::Owned(Default::default()) } } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct ImageBoxProcedural { #[serde(default)] pub id: String, #[serde(default)] #[serde(skip_serializing_if = "HashMap::is_empty")] pub parameters: HashMap, #[serde(default)] #[serde(skip_serializing_if = "Vec::is_empty")] pub images: Vec, #[serde(default)] pub mesh: ImageBoxProceduralMesh, #[serde(default)] pub vertex_mapping: CoordsMappingScaling, } impl ImageBoxProcedural { pub fn new(id: impl ToString) -> Self { Self { id: id.to_string(), parameters: Default::default(), images: Default::default(), mesh: Default::default(), vertex_mapping: Default::default(), } } pub fn param(mut self, id: impl ToString, value: Scalar) -> Self { self.parameters.insert(id.to_string(), value); self } pub fn image(mut self, id: impl ToString) -> Self { self.images.push(id.to_string()); self } pub fn mesh(mut self, mesh: ImageBoxProceduralMesh) -> Self { self.mesh = mesh; self } pub fn triangle(mut self, vertices: [ImageBoxProceduralVertex; 3]) -> Self { if let ImageBoxProceduralMesh::Owned(mesh) = &mut self.mesh { let count = mesh.vertices.len() as u32; mesh.vertices.extend(vertices); mesh.triangles.push([count, count + 1, count + 2]); } self } pub fn quad(mut self, vertices: [ImageBoxProceduralVertex; 4]) -> Self { if let ImageBoxProceduralMesh::Owned(mesh) = &mut self.mesh { let count = mesh.vertices.len() as u32; mesh.vertices.extend(vertices); mesh.triangles.push([count, count + 1, count + 2]); mesh.triangles.push([count + 2, count + 3, count]); } self } pub fn extend( mut self, vertices: impl IntoIterator, triangles: impl IntoIterator, ) -> Self { if let ImageBoxProceduralMesh::Owned(mesh) = &mut self.mesh { let count = mesh.vertices.len() as u32; mesh.vertices.extend(vertices); mesh.triangles.extend( triangles .into_iter() .map(|[a, b, c]| [a + count, b + count, c + count]), ); } self } pub fn vertex_mapping(mut self, value: CoordsMappingScaling) -> Self { self.vertex_mapping = value; self } } #[derive(Debug, Clone, Serialize, Deserialize)] pub enum ImageBoxMaterial { Color(ImageBoxColor), Image(ImageBoxImage), Procedural(ImageBoxProcedural), } impl Default for ImageBoxMaterial { fn default() -> Self { Self::Color(Default::default()) } } #[derive(Debug, Default, Copy, Clone, Serialize, Deserialize)] pub enum ImageBoxSizeValue { #[default] Fill, Exact(Scalar), } #[derive(Debug, Default, Copy, Clone, Serialize, Deserialize)] pub struct ImageBoxAspectRatio { #[serde(default)] pub horizontal_alignment: Scalar, #[serde(default)] pub vertical_alignment: Scalar, #[serde(default)] pub outside: bool, } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct ImageBox { #[serde(default)] pub id: WidgetId, #[serde(default)] pub width: ImageBoxSizeValue, #[serde(default)] pub height: ImageBoxSizeValue, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] pub content_keep_aspect_ratio: Option, #[serde(default)] pub material: ImageBoxMaterial, #[serde(default)] pub transform: Transform, } impl WidgetUnitData for ImageBox { fn id(&self) -> &WidgetId { &self.id } } impl TryFrom for ImageBox { type Error = (); fn try_from(node: ImageBoxNode) -> Result { let ImageBoxNode { id, width, height, content_keep_aspect_ratio, material, transform, .. } = node; Ok(Self { id, width, height, content_keep_aspect_ratio, material, transform, }) } } #[derive(Debug, Default, Clone)] pub struct ImageBoxNode { pub id: WidgetId, pub props: Props, pub width: ImageBoxSizeValue, pub height: ImageBoxSizeValue, pub content_keep_aspect_ratio: Option, pub material: ImageBoxMaterial, pub transform: Transform, } impl ImageBoxNode { pub fn remap_props(&mut self, mut f: F) where F: FnMut(Props) -> Props, { let props = std::mem::take(&mut self.props); self.props = (f)(props); } } impl From for WidgetNode { fn from(data: ImageBoxNode) -> Self { Self::Unit(data.into()) } } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub(crate) struct ImageBoxNodePrefab { #[serde(default)] pub id: WidgetId, #[serde(default)] pub props: PrefabValue, #[serde(default)] pub width: ImageBoxSizeValue, #[serde(default)] pub height: ImageBoxSizeValue, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] pub content_keep_aspect_ratio: Option, #[serde(default)] pub material: ImageBoxMaterial, #[serde(default)] pub transform: Transform, } ================================================ FILE: crates/core/src/widget/unit/mod.rs ================================================ pub mod area; pub mod content; pub mod flex; pub mod grid; pub mod image; pub mod portal; pub mod size; pub mod text; use crate::{ props::Props, widget::{ WidgetId, node::WidgetNode, unit::{ area::{AreaBox, AreaBoxNode, AreaBoxNodePrefab}, content::{ContentBox, ContentBoxNode, ContentBoxNodePrefab}, flex::{FlexBox, FlexBoxNode, FlexBoxNodePrefab}, grid::{GridBox, GridBoxNode, GridBoxNodePrefab}, image::{ImageBox, ImageBoxNode, ImageBoxNodePrefab}, portal::{PortalBox, PortalBoxNode, PortalBoxNodePrefab}, size::{SizeBox, SizeBoxNode, SizeBoxNodePrefab}, text::{TextBox, TextBoxNode, TextBoxNodePrefab}, }, }, }; use serde::{Deserialize, Serialize}; use std::convert::TryFrom; #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct WidgetUnitInspectionNode { #[serde(default)] pub id: WidgetId, #[serde(default)] #[serde(skip_serializing_if = "Vec::is_empty")] pub children: Vec, } pub trait WidgetUnitData { fn id(&self) -> &WidgetId; fn get_children(&self) -> Vec<&WidgetUnit> { vec![] } } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub enum WidgetUnit { #[default] None, AreaBox(AreaBox), PortalBox(PortalBox), ContentBox(ContentBox), FlexBox(FlexBox), GridBox(GridBox), SizeBox(SizeBox), ImageBox(ImageBox), TextBox(TextBox), } impl WidgetUnit { pub fn is_none(&self) -> bool { matches!(self, Self::None) } pub fn is_some(&self) -> bool { !matches!(self, Self::None) } pub fn as_data(&self) -> Option<&dyn WidgetUnitData> { match self { Self::None => None, Self::AreaBox(v) => Some(v as &dyn WidgetUnitData), Self::PortalBox(v) => Some(v as &dyn WidgetUnitData), Self::ContentBox(v) => Some(v as &dyn WidgetUnitData), Self::FlexBox(v) => Some(v as &dyn WidgetUnitData), Self::GridBox(v) => Some(v as &dyn WidgetUnitData), Self::SizeBox(v) => Some(v as &dyn WidgetUnitData), Self::ImageBox(v) => Some(v as &dyn WidgetUnitData), Self::TextBox(v) => Some(v as &dyn WidgetUnitData), } } pub fn inspect(&self) -> Option { self.as_data().map(|data| WidgetUnitInspectionNode { id: data.id().to_owned(), children: data .get_children() .into_iter() .filter_map(|child| child.inspect()) .collect::>(), }) } } impl TryFrom for WidgetUnit { type Error = (); fn try_from(node: WidgetUnitNode) -> Result { match node { WidgetUnitNode::None => Ok(Self::None), WidgetUnitNode::AreaBox(n) => Ok(WidgetUnit::AreaBox(AreaBox::try_from(n)?)), WidgetUnitNode::PortalBox(n) => Ok(WidgetUnit::PortalBox(PortalBox::try_from(n)?)), WidgetUnitNode::ContentBox(n) => Ok(WidgetUnit::ContentBox(ContentBox::try_from(n)?)), WidgetUnitNode::FlexBox(n) => Ok(WidgetUnit::FlexBox(FlexBox::try_from(n)?)), WidgetUnitNode::GridBox(n) => Ok(WidgetUnit::GridBox(GridBox::try_from(n)?)), WidgetUnitNode::SizeBox(n) => Ok(WidgetUnit::SizeBox(SizeBox::try_from(n)?)), WidgetUnitNode::ImageBox(n) => Ok(WidgetUnit::ImageBox(ImageBox::try_from(n)?)), WidgetUnitNode::TextBox(n) => Ok(WidgetUnit::TextBox(TextBox::try_from(n)?)), } } } impl TryFrom for WidgetUnit { type Error = (); fn try_from(node: WidgetNode) -> Result { match node { WidgetNode::None | WidgetNode::Tuple(_) => Ok(Self::None), WidgetNode::Component(_) => Err(()), WidgetNode::Unit(u) => Self::try_from(u), } } } #[derive(Debug, Default, Clone)] pub enum WidgetUnitNode { #[default] None, AreaBox(AreaBoxNode), PortalBox(PortalBoxNode), ContentBox(ContentBoxNode), FlexBox(FlexBoxNode), GridBox(GridBoxNode), SizeBox(SizeBoxNode), ImageBox(ImageBoxNode), TextBox(TextBoxNode), } impl WidgetUnitNode { pub fn is_none(&self) -> bool { matches!(self, Self::None) } pub fn is_some(&self) -> bool { !matches!(self, Self::None) } pub fn props(&self) -> Option<&Props> { match self { Self::None | Self::AreaBox(_) | Self::PortalBox(_) => None, Self::ContentBox(v) => Some(&v.props), Self::FlexBox(v) => Some(&v.props), Self::GridBox(v) => Some(&v.props), Self::SizeBox(v) => Some(&v.props), Self::ImageBox(v) => Some(&v.props), Self::TextBox(v) => Some(&v.props), } } pub fn props_mut(&mut self) -> Option<&mut Props> { match self { Self::None | Self::AreaBox(_) | Self::PortalBox(_) => None, Self::ContentBox(v) => Some(&mut v.props), Self::FlexBox(v) => Some(&mut v.props), Self::GridBox(v) => Some(&mut v.props), Self::SizeBox(v) => Some(&mut v.props), Self::ImageBox(v) => Some(&mut v.props), Self::TextBox(v) => Some(&mut v.props), } } pub fn remap_props(&mut self, f: F) where F: FnMut(Props) -> Props, { match self { Self::None | Self::AreaBox(_) | Self::PortalBox(_) => {} Self::ContentBox(v) => v.remap_props(f), Self::FlexBox(v) => v.remap_props(f), Self::GridBox(v) => v.remap_props(f), Self::SizeBox(v) => v.remap_props(f), Self::ImageBox(v) => v.remap_props(f), Self::TextBox(v) => v.remap_props(f), } } } impl TryFrom for WidgetUnitNode { type Error = (); fn try_from(node: WidgetNode) -> Result { if let WidgetNode::Unit(v) = node { Ok(v) } else { Err(()) } } } impl From<()> for WidgetUnitNode { fn from(_: ()) -> Self { Self::None } } macro_rules! implement_from_unit { { $( $type_name:ident => $variant_name:ident ),+ $(,)? } => { $( impl From<$type_name> for WidgetUnitNode { fn from(unit: $type_name) -> Self { Self::$variant_name(unit) } } )+ }; } implement_from_unit! { AreaBoxNode => AreaBox, PortalBoxNode => PortalBox, ContentBoxNode => ContentBox, FlexBoxNode => FlexBox, GridBoxNode => GridBox, SizeBoxNode => SizeBox, ImageBoxNode => ImageBox, TextBoxNode => TextBox, } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub(crate) enum WidgetUnitNodePrefab { #[default] None, AreaBox(AreaBoxNodePrefab), PortalBox(PortalBoxNodePrefab), ContentBox(ContentBoxNodePrefab), FlexBox(FlexBoxNodePrefab), GridBox(GridBoxNodePrefab), SizeBox(SizeBoxNodePrefab), ImageBox(ImageBoxNodePrefab), TextBox(TextBoxNodePrefab), } ================================================ FILE: crates/core/src/widget/unit/portal.rs ================================================ use crate::widget::{ WidgetId, node::{WidgetNode, WidgetNodePrefab}, unit::{ WidgetUnit, WidgetUnitData, content::{ContentBoxItem, ContentBoxItemNode, ContentBoxItemNodePrefab}, flex::{FlexBoxItem, FlexBoxItemNode, FlexBoxItemNodePrefab}, grid::{GridBoxItem, GridBoxItemNode, GridBoxItemNodePrefab}, }, }; use serde::{Deserialize, Serialize}; use std::convert::TryFrom; #[derive(Debug, Clone, Serialize, Deserialize)] pub enum PortalBoxSlot { Slot(WidgetUnit), ContentItem(ContentBoxItem), FlexItem(FlexBoxItem), GridItem(GridBoxItem), } impl Default for PortalBoxSlot { fn default() -> Self { Self::Slot(Default::default()) } } impl TryFrom for PortalBoxSlot { type Error = (); fn try_from(node: PortalBoxSlotNode) -> Result { Ok(match node { PortalBoxSlotNode::Slot(node) => PortalBoxSlot::Slot(WidgetUnit::try_from(node)?), PortalBoxSlotNode::ContentItem(item) => { PortalBoxSlot::ContentItem(ContentBoxItem::try_from(item)?) } PortalBoxSlotNode::FlexItem(item) => { PortalBoxSlot::FlexItem(FlexBoxItem::try_from(item)?) } PortalBoxSlotNode::GridItem(item) => { PortalBoxSlot::GridItem(GridBoxItem::try_from(item)?) } }) } } #[derive(Debug, Clone)] pub enum PortalBoxSlotNode { Slot(WidgetNode), ContentItem(ContentBoxItemNode), FlexItem(FlexBoxItemNode), GridItem(GridBoxItemNode), } impl Default for PortalBoxSlotNode { fn default() -> Self { Self::Slot(Default::default()) } } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct PortalBox { #[serde(default)] pub id: WidgetId, #[serde(default)] pub slot: Box, #[serde(default)] pub owner: WidgetId, } impl WidgetUnitData for PortalBox { fn id(&self) -> &WidgetId { &self.id } fn get_children(&self) -> Vec<&WidgetUnit> { vec![match &*self.slot { PortalBoxSlot::Slot(b) => b, PortalBoxSlot::ContentItem(b) => &b.slot, PortalBoxSlot::FlexItem(b) => &b.slot, PortalBoxSlot::GridItem(b) => &b.slot, }] } } impl TryFrom for PortalBox { type Error = (); fn try_from(node: PortalBoxNode) -> Result { let PortalBoxNode { id, slot, owner } = node; Ok(Self { id, slot: Box::new(PortalBoxSlot::try_from(*slot)?), owner, }) } } #[derive(Debug, Default, Clone)] pub struct PortalBoxNode { pub id: WidgetId, pub slot: Box, pub owner: WidgetId, } impl From for WidgetNode { fn from(data: PortalBoxNode) -> Self { Self::Unit(data.into()) } } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub(crate) struct PortalBoxNodePrefab { #[serde(default)] pub id: WidgetId, #[serde(default)] pub slot: Box, #[serde(default)] pub owner: WidgetId, } #[derive(Debug, Clone, Serialize, Deserialize)] pub(crate) enum PortalBoxSlotNodePrefab { Slot(#[serde(default)] WidgetNodePrefab), ContentItem(#[serde(default)] ContentBoxItemNodePrefab), FlexItem(#[serde(default)] FlexBoxItemNodePrefab), GridItem(#[serde(default)] GridBoxItemNodePrefab), } impl Default for PortalBoxSlotNodePrefab { fn default() -> Self { Self::Slot(Default::default()) } } ================================================ FILE: crates/core/src/widget/unit/size.rs ================================================ use crate::{ PrefabValue, Scalar, props::Props, widget::{ WidgetId, node::{WidgetNode, WidgetNodePrefab}, unit::{WidgetUnit, WidgetUnitData}, utils::{Rect, Transform}, }, }; use serde::{Deserialize, Serialize}; use std::convert::TryFrom; #[derive(Debug, Default, Copy, Clone, Serialize, Deserialize)] pub enum SizeBoxSizeValue { #[default] Content, Fill, Exact(Scalar), } #[derive(Debug, Default, Copy, Clone, Serialize, Deserialize)] pub enum SizeBoxAspectRatio { #[default] None, WidthOfHeight(Scalar), HeightOfWidth(Scalar), } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct SizeBox { #[serde(default)] pub id: WidgetId, #[serde(default)] pub slot: Box, #[serde(default)] pub width: SizeBoxSizeValue, #[serde(default)] pub height: SizeBoxSizeValue, #[serde(default)] pub margin: Rect, #[serde(default)] pub keep_aspect_ratio: SizeBoxAspectRatio, #[serde(default)] pub transform: Transform, } impl WidgetUnitData for SizeBox { fn id(&self) -> &WidgetId { &self.id } fn get_children(&self) -> Vec<&WidgetUnit> { vec![&self.slot] } } impl TryFrom for SizeBox { type Error = (); fn try_from(node: SizeBoxNode) -> Result { let SizeBoxNode { id, slot, width, height, margin, keep_aspect_ratio, transform, .. } = node; Ok(Self { id, slot: Box::new(WidgetUnit::try_from(*slot)?), width, height, margin, keep_aspect_ratio, transform, }) } } #[derive(Debug, Default, Clone)] pub struct SizeBoxNode { pub id: WidgetId, pub props: Props, pub slot: Box, pub width: SizeBoxSizeValue, pub height: SizeBoxSizeValue, pub margin: Rect, pub keep_aspect_ratio: SizeBoxAspectRatio, pub transform: Transform, } impl SizeBoxNode { pub fn remap_props(&mut self, mut f: F) where F: FnMut(Props) -> Props, { let props = std::mem::take(&mut self.props); self.props = (f)(props); } } impl From for WidgetNode { fn from(data: SizeBoxNode) -> Self { Self::Unit(data.into()) } } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub(crate) struct SizeBoxNodePrefab { #[serde(default)] pub id: WidgetId, #[serde(default)] pub props: PrefabValue, #[serde(default)] pub slot: Box, #[serde(default)] pub width: SizeBoxSizeValue, #[serde(default)] pub height: SizeBoxSizeValue, #[serde(default)] pub keep_aspect_ratio: SizeBoxAspectRatio, #[serde(default)] pub margin: Rect, #[serde(default)] pub transform: Transform, } ================================================ FILE: crates/core/src/widget/unit/text.rs ================================================ use crate::{ PrefabValue, Scalar, props::Props, widget::{ WidgetId, node::WidgetNode, unit::WidgetUnitData, utils::{Color, Transform}, }, }; use serde::{Deserialize, Serialize}; use std::convert::TryFrom; #[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum TextBoxHorizontalAlign { #[default] Left, Center, Right, } #[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum TextBoxVerticalAlign { #[default] Top, Middle, Bottom, } #[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum TextBoxDirection { #[default] HorizontalLeftToRight, HorizontalRightToLeft, VerticalTopToBottom, VerticalBottomToTop, } impl TextBoxDirection { pub fn is_horizontal(&self) -> bool { *self == Self::HorizontalLeftToRight || *self == Self::HorizontalRightToLeft } pub fn is_vertical(&self) -> bool { *self == Self::VerticalTopToBottom || *self == Self::VerticalBottomToTop } pub fn is_order_ascending(&self) -> bool { *self == Self::HorizontalLeftToRight || *self == Self::VerticalTopToBottom } pub fn is_order_descending(&self) -> bool { *self == Self::HorizontalRightToLeft || *self == Self::VerticalBottomToTop } } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct TextBoxFont { #[serde(default)] pub name: String, #[serde(default)] pub size: Scalar, } #[derive(Debug, Default, Copy, Clone, Serialize, Deserialize)] pub enum TextBoxSizeValue { Content, #[default] Fill, Exact(Scalar), } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct TextBox { #[serde(default)] pub id: WidgetId, #[serde(default)] pub text: String, #[serde(default)] pub width: TextBoxSizeValue, #[serde(default)] pub height: TextBoxSizeValue, #[serde(default)] pub horizontal_align: TextBoxHorizontalAlign, #[serde(default)] pub vertical_align: TextBoxVerticalAlign, #[serde(default)] pub direction: TextBoxDirection, #[serde(default)] pub font: TextBoxFont, #[serde(default)] pub color: Color, #[serde(default)] pub transform: Transform, } impl WidgetUnitData for TextBox { fn id(&self) -> &WidgetId { &self.id } } impl TryFrom for TextBox { type Error = (); fn try_from(node: TextBoxNode) -> Result { let TextBoxNode { id, text, width, height, horizontal_align, vertical_align, direction, font, color, transform, .. } = node; Ok(Self { id, text, width, height, horizontal_align, vertical_align, direction, font, color, transform, }) } } #[derive(Debug, Default, Clone)] pub struct TextBoxNode { pub id: WidgetId, pub props: Props, pub text: String, pub width: TextBoxSizeValue, pub height: TextBoxSizeValue, pub horizontal_align: TextBoxHorizontalAlign, pub vertical_align: TextBoxVerticalAlign, pub direction: TextBoxDirection, pub font: TextBoxFont, pub color: Color, pub transform: Transform, } impl TextBoxNode { pub fn remap_props(&mut self, mut f: F) where F: FnMut(Props) -> Props, { let props = std::mem::take(&mut self.props); self.props = (f)(props); } } impl From for WidgetNode { fn from(data: TextBoxNode) -> Self { Self::Unit(data.into()) } } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub(crate) struct TextBoxNodePrefab { #[serde(default)] pub id: WidgetId, #[serde(default)] pub props: PrefabValue, #[serde(default)] pub text: String, #[serde(default)] pub width: TextBoxSizeValue, #[serde(default)] pub height: TextBoxSizeValue, #[serde(default)] pub horizontal_align: TextBoxHorizontalAlign, #[serde(default)] pub vertical_align: TextBoxVerticalAlign, #[serde(default)] pub direction: TextBoxDirection, #[serde(default)] pub font: TextBoxFont, #[serde(default)] pub color: Color, #[serde(default)] pub transform: Transform, } ================================================ FILE: crates/core/src/widget/utils.rs ================================================ use crate::{Integer, PropsData, Scalar}; use serde::{Deserialize, Serialize}; #[repr(C)] #[derive(PropsData, Debug, Default, Copy, Clone, PartialEq, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct Vec2 { #[serde(default)] pub x: Scalar, #[serde(default)] pub y: Scalar, } impl From for Vec2 { fn from(v: Scalar) -> Self { Self { x: v, y: v } } } impl From<(Scalar, Scalar)> for Vec2 { fn from((x, y): (Scalar, Scalar)) -> Self { Self { x, y } } } impl From<[Scalar; 2]> for Vec2 { fn from([x, y]: [Scalar; 2]) -> Self { Self { x, y } } } #[repr(C)] #[derive(PropsData, Debug, Default, Copy, Clone, PartialEq, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct IntVec2 { #[serde(default)] pub x: Integer, #[serde(default)] pub y: Integer, } impl From for IntVec2 { fn from(v: Integer) -> Self { Self { x: v, y: v } } } impl From<(Integer, Integer)> for IntVec2 { fn from((x, y): (Integer, Integer)) -> Self { Self { x, y } } } impl From<[Integer; 2]> for IntVec2 { fn from([x, y]: [Integer; 2]) -> Self { Self { x, y } } } #[repr(C)] #[derive(PropsData, Debug, Default, Copy, Clone, PartialEq, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct Rect { #[serde(default)] pub left: Scalar, #[serde(default)] pub right: Scalar, #[serde(default)] pub top: Scalar, #[serde(default)] pub bottom: Scalar, } impl From for Rect { fn from(v: Scalar) -> Self { Self { left: v, right: v, top: v, bottom: v, } } } impl From<(Scalar, Scalar)> for Rect { fn from((w, h): (Scalar, Scalar)) -> Self { Self { left: 0.0, right: w, top: 0.0, bottom: h, } } } impl From<[Scalar; 2]> for Rect { fn from([w, h]: [Scalar; 2]) -> Self { Self { left: 0.0, right: w, top: 0.0, bottom: h, } } } impl From<(Scalar, Scalar, Scalar, Scalar)> for Rect { fn from((left, right, top, bottom): (Scalar, Scalar, Scalar, Scalar)) -> Self { Self { left, right, top, bottom, } } } impl From<[Scalar; 4]> for Rect { fn from([left, right, top, bottom]: [Scalar; 4]) -> Self { Self { left, right, top, bottom, } } } impl Rect { #[inline] pub fn width(&self) -> Scalar { self.right - self.left } #[inline] pub fn height(&self) -> Scalar { self.bottom - self.top } #[inline] pub fn size(&self) -> Vec2 { Vec2 { x: self.width(), y: self.height(), } } } #[repr(C)] #[derive(PropsData, Debug, Default, Copy, Clone, PartialEq, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct IntRect { #[serde(default)] pub left: Integer, #[serde(default)] pub right: Integer, #[serde(default)] pub top: Integer, #[serde(default)] pub bottom: Integer, } impl IntRect { #[inline] pub fn width(&self) -> Integer { self.right - self.left } #[inline] pub fn height(&self) -> Integer { self.bottom - self.top } #[inline] pub fn size(&self) -> IntVec2 { IntVec2 { x: self.width(), y: self.height(), } } } impl From for IntRect { fn from(v: Integer) -> Self { Self { left: v, right: v, top: v, bottom: v, } } } impl From<(Integer, Integer)> for IntRect { fn from((w, h): (Integer, Integer)) -> Self { Self { left: 0, right: w, top: 0, bottom: h, } } } #[repr(C)] #[derive(PropsData, Debug, Copy, Clone, PartialEq, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct Color { #[serde(default)] pub r: Scalar, #[serde(default)] pub g: Scalar, #[serde(default)] pub b: Scalar, #[serde(default)] pub a: Scalar, } impl Default for Color { fn default() -> Self { Self { r: 1.0, g: 1.0, b: 1.0, a: 1.0, } } } impl Color { pub fn transparent() -> Self { Self { r: 0.0, g: 0.0, b: 0.0, a: 0.0, } } } #[derive(PropsData, Debug, Copy, Clone, PartialEq, Serialize, Deserialize)] #[props_data(crate::props::PropsData)] #[prefab(crate::Prefab)] pub struct Transform { /// Rectangle center of mass. Values in range: <0;1> #[serde(default)] pub pivot: Vec2, /// Translation in rectangle fraction units. Values in range: <0;1> #[serde(default)] pub align: Vec2, /// Translation in regular units. #[serde(default)] pub translation: Vec2, /// Rotation in radian angle units. #[serde(default)] pub rotation: Scalar, /// Scale in regular units. #[serde(default)] pub scale: Vec2, /// Skewing in radian angle units. /// {angle X, angle Y} #[serde(default)] pub skew: Vec2, } impl Default for Transform { fn default() -> Self { Self { pivot: Default::default(), align: Default::default(), translation: Default::default(), rotation: Default::default(), scale: Self::default_scale(), skew: Default::default(), } } } impl Transform { fn default_scale() -> Vec2 { Vec2 { x: 1.0, y: 1.0 } } } #[inline] pub fn lerp(from: Scalar, to: Scalar, factor: Scalar) -> Scalar { from + (to - from) * factor } #[inline] pub fn lerp_clamped(from: Scalar, to: Scalar, factor: Scalar) -> Scalar { lerp(from, to, factor.clamp(0.0, 1.0)) } ================================================ FILE: crates/derive/Cargo.toml ================================================ [package] name = "raui-derive" version = "0.70.17" authors = ["Patryk 'PsichiX' Budzynski "] edition = "2024" description = "Macros for 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"] [lib] proc-macro = true [dependencies] quote = "1.0" syn = { version = "1.0", features = ["extra-traits", "full"] } ================================================ FILE: crates/derive/src/lib.rs ================================================ extern crate proc_macro; use proc_macro::TokenStream; use quote::quote; use syn::{ DeriveInput, FnArg, Ident, ItemFn, Pat, PatIdent, Path, Result, Token, Type, TypePath, TypeReference, parse::{Parse, ParseStream}, parse_macro_input, parse_str, punctuated::Punctuated, }; #[derive(Debug, Clone)] struct IdentList { values: Punctuated, } impl Parse for IdentList { fn parse(input: ParseStream) -> Result { Ok(Self { values: input.parse_terminated(Ident::parse)?, }) } } fn unpack_context(ty: &Type, pat: &Pat) -> Option { match ty { Type::Path(TypePath { path, .. }) => { if let Some(segment) = path.segments.iter().next_back() && segment.ident == "WidgetContext" && let Pat::Ident(PatIdent { ident, .. }) = pat { return Some(ident.to_owned()); } } Type::Reference(TypeReference { elem, .. }) => { return unpack_context(elem, pat); } _ => {} } None } fn is_arg_context(arg: &FnArg) -> Option { if let FnArg::Typed(pat) = arg { unpack_context(&pat.ty, &pat.pat) } else { None } } // The links won't be broken when built in the context of the `raui` crate /// An attribute macro that allows you to add hooks that will execute before your component body /// /// > **See Also:** [`macro@post_hooks`] for an alternative that runs _after_ your component body /// /// Hooks allow you to create reusable logic that can be applied to multiple components. #[proc_macro_attribute] pub fn pre_hooks(attr: TokenStream, input: TokenStream) -> TokenStream { let ItemFn { attrs, vis, sig, block, } = parse_macro_input!(input as ItemFn); let context = sig .inputs .iter() .find_map(is_arg_context) .unwrap_or_else(|| panic!("Could not find function context argument!")); let list = parse_macro_input!(attr as IdentList); let hooks = list .values .into_iter() .map(|v| quote! { #context.use_hook(#v); }); let tokens = quote! { #(#attrs)* #vis #sig { #({#hooks})* #block } }; tokens.into() } /// Allows you to execute re-usable logic after your component body /// /// See [`macro@pre_hooks`] #[proc_macro_attribute] pub fn post_hooks(attr: TokenStream, input: TokenStream) -> TokenStream { let ItemFn { attrs, vis, sig, block, } = parse_macro_input!(input as ItemFn); let context = sig .inputs .iter() .find_map(is_arg_context) .unwrap_or_else(|| panic!("Could not find function context argument!")); let list = parse_macro_input!(attr as IdentList); let hooks = list .values .into_iter() .map(|v| quote! { #context.use_hook(#v); }); let tokens = quote! { #(#attrs)* #vis #sig { let result = { #block }; #({#hooks})* result } }; tokens.into() } // The links won't be broken when built in the context of the `raui` crate /// Derive macro for the [`PropsData`][raui_core::props::PropsData] trait /// /// # Example /// /// ```ignore /// #[derive(PropsData, Debug, Default, Copy, Clone, Serialize, Deserialize)] /// #[props_data(crate::props::PropsData)] /// #[prefab(crate::Prefab)] /// pub struct ButtonProps { /// #[serde(default)] /// pub selected: bool, /// #[serde(default)] /// pub trigger: bool, /// #[serde(default)] /// pub context: bool, /// #[serde(default)] /// pub pointer: Vec2, /// } /// ``` #[proc_macro_derive(PropsData, attributes(remote, props_data, prefab))] pub fn derive_props(input: TokenStream) -> TokenStream { let DeriveInput { ident, attrs, .. } = parse_macro_input!(input as DeriveInput); let mut path = Path::from(ident); let mut props_data = parse_str::("PropsData").unwrap(); let mut prefab = parse_str::("Prefab").unwrap(); for attr in attrs { if let Some(ident) = attr.path.get_ident() { if ident == "remote" { path = attr.parse_args::().unwrap(); } else if ident == "props_data" { props_data = attr.parse_args::().unwrap(); } else if ident == "prefab" { prefab = attr.parse_args::().unwrap(); } } } let tokens = quote! { impl #props_data for #path where Self: Clone, { fn clone_props(&self) -> Box { Box::new(self.clone()) } fn as_any(&self) -> &dyn std::any::Any { self } } impl #prefab for #path {} }; tokens.into() } // The links won't be broken when built in the context of the `raui` crate /// Derive macro for the [`MessageData`][raui_core::messenger::MessageData] trait /// /// # Example /// /// ```ignore /// #[derive(MessageData, Debug, Clone)] /// pub enum AppMessage { /// ShowPopup(usize), /// ClosePopup, /// } /// ``` #[proc_macro_derive(MessageData, attributes(remote, message_data))] pub fn derive_message(input: TokenStream) -> TokenStream { let DeriveInput { ident, attrs, .. } = parse_macro_input!(input as DeriveInput); let mut path = Path::from(ident); let mut message_data = parse_str::("MessageData").unwrap(); for attr in attrs { if let Some(ident) = attr.path.get_ident() { if ident == "remote" { path = attr.parse_args::().unwrap(); } else if ident == "message_data" { message_data = attr.parse_args::().unwrap(); } } } let tokens = quote! { impl #message_data for #path where Self: Clone, { fn clone_message(&self) -> Box { Box::new(self.clone()) } fn as_any(&self) -> &dyn std::any::Any { self } } }; tokens.into() } ================================================ FILE: crates/immediate/Cargo.toml ================================================ [package] name = "raui-immediate" version = "0.70.17" authors = ["Patryk 'PsichiX' Budzynski "] edition = "2024" description = "RAUI immediate mode UI layer" 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"] [dependencies] raui-core = { path = "../core", version = "0.70" } serde = { version = "1", features = ["derive"] } ================================================ FILE: crates/immediate/src/lib.rs ================================================ use internal::immediate_effects_box; use raui_core::{ DynamicManaged, DynamicManagedLazy, Lifetime, ManagedLazy, Prefab, PropsData, TypeHash, make_widget, props::{Props, PropsData}, widget::{ WidgetRef, component::WidgetComponent, context::WidgetContext, node::WidgetNode, unit::WidgetUnitNode, }, }; use serde::{Deserialize, Serialize}; use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::Arc}; thread_local! { pub(crate) static STACK: RefCell>> = Default::default(); pub(crate) static STATES: RefCell>>> = Default::default(); pub(crate) static ACCESS_POINTS: RefCell>>> = Default::default(); pub(crate) static PROPS_STACK: RefCell>>>> = Default::default(); } #[derive(Default)] pub struct ImmediateContext { states: Rc>, access_points: Rc>, props_stack: Rc>>, } impl ImmediateContext { pub fn activate(context: &Self) { STATES.with(|states| { context.states.borrow_mut().reset(); *states.borrow_mut() = Some(context.states.clone()); }); ACCESS_POINTS.with(|access_points| { *access_points.borrow_mut() = Some(context.access_points.clone()); }); PROPS_STACK.with(|props_stack| { *props_stack.borrow_mut() = Some(context.props_stack.clone()); }); } pub fn deactivate() { STATES.with(|states| { *states.borrow_mut() = None; }); ACCESS_POINTS.with(|access_points| { if let Some(access_points) = access_points.borrow_mut().as_mut() { access_points.borrow_mut().reset(); } *access_points.borrow_mut() = None; }); PROPS_STACK.with(|props_stack| { if let Some(props_stack) = props_stack.borrow_mut().as_mut() { props_stack.borrow_mut().clear(); } *props_stack.borrow_mut() = None; }); } } #[derive(Default)] struct ImmediateStates { data: Vec, position: usize, } impl ImmediateStates { fn reset(&mut self) { self.data.truncate(self.position); self.position = 0; } fn alloc(&mut self, mut init: impl FnMut() -> T) -> ManagedLazy { let index = self.position; self.position += 1; if let Some(managed) = self.data.get_mut(index) { if managed.type_hash() != &TypeHash::of::() { *managed = DynamicManaged::new(init()).ok().unwrap(); } } else { self.data.push(DynamicManaged::new(init()).ok().unwrap()); } self.data .get(index) .unwrap() .lazy() .into_typed() .ok() .unwrap() } } #[derive(Default)] struct ImmediateAccessPoints { data: HashMap, } impl ImmediateAccessPoints { fn register(&mut self, id: impl ToString, data: &mut T) -> Lifetime { let result = Lifetime::default(); self.data .insert(id.to_string(), DynamicManagedLazy::new(data, result.lazy())); result } fn reset(&mut self) { self.data.clear(); } fn access(&self, id: &str) -> ManagedLazy { self.data .get(id) .unwrap() .clone() .into_typed() .ok() .unwrap() } } #[derive(PropsData, Default, Clone, Serialize, Deserialize)] #[props_data(raui_core::props::PropsData)] pub struct ImmediateHooks { #[serde(default, skip)] pre_hooks: Vec, #[serde(default, skip)] post_hooks: Vec, } impl ImmediateHooks { pub fn with(mut self, pointer: fn(&mut WidgetContext)) -> Self { self.pre_hooks.push(pointer); self } pub fn with_post(mut self, pointer: fn(&mut WidgetContext)) -> Self { self.post_hooks.push(pointer); self } } impl std::fmt::Debug for ImmediateHooks { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct(stringify!(ImmediateHooks)) .finish_non_exhaustive() } } macro_rules! impl_lifecycle_props { ($($id:ident),+ $(,)?) => { $( #[derive(PropsData, Default, Clone, Serialize, Deserialize)] #[props_data(raui_core::props::PropsData)] pub struct $id { #[serde(default, skip)] callback: Option>, } impl $id { pub fn new(callback: impl Fn() + Send + Sync + 'static) -> Self { Self { callback: Some(Arc::new(callback)), } } } impl std::fmt::Debug for $id { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct(stringify!($id)).finish_non_exhaustive() } } )+ }; } impl_lifecycle_props! { ImmediateOnMount, ImmediateOnChange, ImmediateOnUnmount } pub fn use_state(init: impl FnMut() -> T) -> ManagedLazy { STATES.with(|states| { let states = states.borrow(); let mut states = states .as_ref() .unwrap_or_else(|| panic!("You must activate context first for `use_state` to work!")) .borrow_mut(); states.alloc(init) }) } pub fn use_access(id: &str) -> ManagedLazy { ACCESS_POINTS.with(|access_points| { let access_points = access_points.borrow(); let access_points = access_points .as_ref() .unwrap_or_else(|| panic!("You must activate context first for `use_access` to work!")) .borrow(); access_points.access(id) }) } pub fn use_stack_props() -> Option { PROPS_STACK.with(|props_stack| { if let Some(props_stack) = props_stack.borrow().as_ref() { for props in props_stack.borrow().iter().rev() { if let Ok(props) = props.read_cloned::() { return Some(props); } } } None }) } pub fn use_effects(props: impl Into, mut f: impl FnMut() -> R) -> R { begin(); let result = f(); let node = end().pop().unwrap_or_default(); push( make_widget!(immediate_effects_box) .merge_props(props.into()) .named_slot("content", node), ); result } pub fn register_access(id: &str, data: &mut T) -> Lifetime { ACCESS_POINTS.with(|access_points| { let access_points = access_points.borrow(); let mut access_points = access_points .as_ref() .unwrap_or_else(|| panic!("You must activate context first for `use_access` to work!")) .borrow_mut(); access_points.register(id, data) }) } pub fn begin() { STACK.with(|stack| stack.borrow_mut().push(Default::default())); } pub fn end() -> Vec { STACK.with(|stack| stack.borrow_mut().pop().unwrap_or_default()) } pub fn push(widget: impl Into) { STACK.with(|stack| { if let Some(widgets) = stack.borrow_mut().last_mut() { widgets.push(widget.into()); } }); } pub fn extend(iter: impl IntoIterator) { STACK.with(|stack| { if let Some(widgets) = stack.borrow_mut().last_mut() { widgets.extend(iter); } }); } pub fn pop() -> WidgetNode { STACK.with(|stack| { stack .borrow_mut() .last_mut() .and_then(|widgets| widgets.pop()) .unwrap_or_default() }) } pub fn reset() { STACK.with(|stack| { stack.borrow_mut().clear(); }); PROPS_STACK.with(|props_stack| { if let Some(props_stack) = props_stack.borrow_mut().as_mut() { props_stack.borrow_mut().clear(); } }); } pub fn list_component( widget: impl Into, props: impl Into, mut f: impl FnMut() -> R, ) -> R { begin(); let result = f(); let widgets = end(); push( widget .into() .merge_props(props.into()) .listed_slots(widgets), ); result } pub fn slot_component( widget: impl Into, props: impl Into, mut f: impl FnMut() -> R, ) -> R { begin(); let result = f(); let widgets = end(); let mut list_widgets = Vec::new(); let mut slot_widgets = Vec::new(); for widget in widgets { if let Some(w) = widget.as_component() { if let Some(name) = w.key.as_deref() { slot_widgets.push((name.to_owned(), widget)); } else { list_widgets.push(widget); } } } push( widget .into() .merge_props(props.into()) .listed_slots(list_widgets) .named_slots(slot_widgets), ); result } pub fn content_component( widget: impl Into, content_name: &str, props: impl Into, mut f: impl FnMut() -> R, ) -> R { begin(); let result = f(); let node = end().pop().unwrap_or_default(); push( widget .into() .merge_props(props.into()) .named_slot(content_name, node), ); result } pub fn tuple(mut f: impl FnMut() -> R) -> R { begin(); let result = f(); let widgets = end(); push(WidgetNode::Tuple(widgets)); result } pub fn component(widget: impl Into, props: impl Into) { push(widget.into().merge_props(props.into())); } pub fn unit(widget: impl Into) { push(widget.into()); } pub fn make_widgets(context: &ImmediateContext, mut f: impl FnMut()) -> Vec { ImmediateContext::activate(context); begin(); f(); let result = end(); ImmediateContext::deactivate(); result } pub trait ImmediateApply: Sized { fn before(self) -> Self { self } fn after(self) -> Self { self } fn process(self, widgets: Vec) -> Vec { widgets } } macro_rules! impl_tuple_immediate_apply { ($($id:ident),+ $(,)?) => { #[allow(non_snake_case)] impl<$($id: $crate::ImmediateApply),+> $crate::ImmediateApply for ($($id,)+) { fn before(self) -> Self { let ($($id,)+) = self; ( $( $id.before(), )+ ) } fn after(self) -> Self { let ($($id,)+) = self; ( $( $id.after(), )+ ) } fn process(self, mut widgets: Vec) -> Vec { let ($($id,)+) = self; $( widgets = $id.process(widgets); )+ widgets } } }; } impl_tuple_immediate_apply!(A); impl_tuple_immediate_apply!(A, B); impl_tuple_immediate_apply!(A, B, C); impl_tuple_immediate_apply!(A, B, C, D); impl_tuple_immediate_apply!(A, B, C, D, E); impl_tuple_immediate_apply!(A, B, C, D, E, F); impl_tuple_immediate_apply!(A, B, C, D, E, F, G); impl_tuple_immediate_apply!(A, B, C, D, E, F, G, H); impl_tuple_immediate_apply!(A, B, C, D, E, F, G, H, I); impl_tuple_immediate_apply!(A, B, C, D, E, F, G, H, I, J); impl_tuple_immediate_apply!(A, B, C, D, E, F, G, H, I, J, K); impl_tuple_immediate_apply!(A, B, C, D, E, F, G, H, I, J, K, L); impl_tuple_immediate_apply!(A, B, C, D, E, F, G, H, I, J, K, L, M); impl_tuple_immediate_apply!(A, B, C, D, E, F, G, H, I, J, K, L, M, N); impl_tuple_immediate_apply!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O); impl_tuple_immediate_apply!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P); impl_tuple_immediate_apply!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q); impl_tuple_immediate_apply!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R); impl_tuple_immediate_apply!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S); impl_tuple_immediate_apply!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T); impl_tuple_immediate_apply!( A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U ); impl_tuple_immediate_apply!( A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V ); impl_tuple_immediate_apply!( A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, X ); impl_tuple_immediate_apply!( A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, X, Y ); impl_tuple_immediate_apply!( A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, X, Y, Z ); pub struct ImKey(pub T); impl ImmediateApply for ImKey { fn process(self, mut widgets: Vec) -> Vec { let key = self.0.to_string(); match widgets.len() { 0 => {} 1 => { if let WidgetNode::Component(widget) = &mut widgets[0] { widget.key = Some(key); } } _ => { for (index, widget) in widgets.iter_mut().enumerate() { if let WidgetNode::Component(widget) = widget { widget.key = Some(format!("{key}-{index}")); } } } } widgets } } pub struct ImIdRef>(pub T); impl> ImmediateApply for ImIdRef { fn process(self, mut widgets: Vec) -> Vec { let idref = self.0.into(); for widget in &mut widgets { if let WidgetNode::Component(widget) = widget { widget.idref = Some(idref.clone()); } } widgets } } pub struct ImProps>(pub T); impl> ImmediateApply for ImProps { fn process(self, mut widgets: Vec) -> Vec { let props = self.0.into(); for widget in &mut widgets { if let Some(widget) = widget.props_mut() { widget.merge_from(props.clone()); } } widgets } } pub struct ImSharedProps>(pub T); impl> ImmediateApply for ImSharedProps { fn process(self, mut widgets: Vec) -> Vec { let props = self.0.into(); for widget in &mut widgets { if let Some(widget) = widget.shared_props_mut() { widget.merge_from(props.clone()); } } widgets } } pub enum ImStackProps> { Props(T), Done, } impl> ImStackProps { pub fn new(props: T) -> Self { Self::Props(props) } } impl> ImmediateApply for ImStackProps { fn before(self) -> Self { if let Self::Props(props) = self { let props = props.into(); PROPS_STACK.with(|props_stack| { if let Some(props_stack) = props_stack.borrow_mut().as_mut() { props_stack.borrow_mut().push(props.clone()); } }); } Self::Done } fn after(self) -> Self { if let Self::Done = self { PROPS_STACK.with(|props_stack| { if let Some(props_stack) = props_stack.borrow_mut().as_mut() { props_stack.borrow_mut().pop(); } }); } self } } pub fn apply(items: impl ImmediateApply, mut f: impl FnMut() -> R) -> R { begin(); let items = items.before(); let result = f(); let items = items.after(); let widgets = end(); let widgets = items.process(widgets); extend(widgets); result } #[deprecated(note = "Use `apply` with `ImKey` instead")] pub fn apply_key(key: impl ToString, f: impl FnMut() -> R) -> R { apply(ImKey(key), f) } #[deprecated(note = "Use `apply` with `ImIdRef` instead")] pub fn apply_idref(key: impl Into, f: impl FnMut() -> R) -> R { apply(ImIdRef(key), f) } #[deprecated(note = "Use `apply` with `ImProps` instead")] pub fn apply_props(props: impl Into, f: impl FnMut() -> R) -> R { apply(ImProps(props), f) } #[deprecated(note = "Use `apply` with `ImSharedProps` instead")] pub fn apply_shared_props(props: impl Into, f: impl FnMut() -> R) -> R { apply(ImSharedProps(props), f) } #[deprecated(note = "Use `apply` with `ImStackProps` instead")] pub fn stack_props(props: impl Into, f: impl FnMut() -> R) -> R { apply(ImStackProps::new(props), f) } mod internal { use super::*; use raui_core::widget::unit::area::AreaBoxNode; pub(crate) fn immediate_effects_box(mut ctx: WidgetContext) -> WidgetNode { let hooks = ctx.props.read_cloned_or_default::(); for hook in &hooks.pre_hooks { hook(&mut ctx); } if let Ok(event) = ctx.props.read::() && let Some(callback) = event.callback.as_ref() { let callback = callback.clone(); ctx.life_cycle.mount(move |_| { callback(); }); } if let Ok(event) = ctx.props.read::() && let Some(callback) = event.callback.as_ref() { let callback = callback.clone(); ctx.life_cycle.change(move |_| { callback(); }); } if let Ok(event) = ctx.props.read::() && let Some(callback) = event.callback.as_ref() { let callback = callback.clone(); ctx.life_cycle.unmount(move |_| { callback(); }); } let content = ctx.named_slots.remove("content").unwrap_or_default(); let result = AreaBoxNode { id: ctx.id.to_owned(), slot: Box::new(content), }; for hook in &hooks.post_hooks { hook(&mut ctx); } result.into() } } #[cfg(test)] mod tests { use raui_core::widget::component::image_box::{ImageBoxProps, image_box}; use super::*; fn run(frame: usize) { let show_slider = use_state(|| false); let mut show_slider = show_slider.write().unwrap(); let show_text_field = use_state(|| false); let mut show_text_field = show_text_field.write().unwrap(); if frame == 1 { *show_slider = true; } else if frame == 3 { *show_text_field = true; } else if frame == 5 { *show_slider = false; } else if frame == 7 { *show_text_field = false; } else if frame == 9 { *show_slider = true; *show_text_field = true; } println!( "* #{} | HOVERED: {} | CLICKED: {}", frame, *show_slider, *show_text_field ); if *show_slider { slider(); } if *show_text_field { text_field(); } } fn slider() { let value = use_state(|| 0.0); let mut state = value.write().unwrap(); *state += 0.1; println!("* SLIDER VALUE: {}", *state); } fn text_field() { let text = use_state(String::default); let mut text = text.write().unwrap(); text.push('z'); println!("* TEXT FIELD: {}", text.as_str()); } #[test] fn test_use_state() { let context = ImmediateContext::default(); for frame in 0..12 { ImmediateContext::activate(&context); run(frame); ImmediateContext::deactivate(); } } #[test] fn test_apply() { let context = ImmediateContext::default(); ImmediateContext::activate(&context); begin(); apply( ( ImKey("image"), ImProps(ImageBoxProps::colored(Default::default())), ), || { component(make_widget!(image_box), ()); }, ); let widgets = end(); ImmediateContext::deactivate(); assert_eq!(widgets.len(), 1); if let WidgetNode::Component(component) = &widgets[0] { assert_eq!(component.key.as_deref(), Some("image")); assert!(component.props.has::()); } else { panic!("Expected a component node"); } } } ================================================ FILE: crates/immediate-widgets/Cargo.toml ================================================ [package] name = "raui-immediate-widgets" version = "0.70.17" authors = ["Patryk 'PsichiX' Budzynski "] edition = "2024" description = "Widgets library for RAUI immediate mode UI layer" 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"] [dependencies] raui-core = { path = "../core", version = "0.70" } raui-material = { path = "../material", version = "0.70" } raui-immediate = { path = "../immediate", version = "0.70" } serde = { version = "1", features = ["derive"] } ================================================ FILE: crates/immediate-widgets/src/lib.rs ================================================ use raui_immediate::*; macro_rules! impl_imports { () => { #[allow(unused_imports)] use raui_core::widget::component::{ containers::{ anchor_box::*, area_box::*, content_box::*, context_box::*, flex_box::*, float_box::*, grid_box::*, hidden_box::*, horizontal_box::*, portal_box::*, responsive_box::*, scroll_box::*, size_box::*, switch_box::*, tabs_box::*, tooltip_box::*, variant_box::*, vertical_box::*, wrap_box::*, }, interactive::{ button::*, float_view::*, input_field::*, navigation::*, options_view::*, scroll_view::*, slider_view::*, }, }; #[allow(unused_imports)] use raui_core::widget::{ component::{image_box::*, space_box::*, text_box::*}, none_widget, }; #[allow(unused_imports)] use raui_material::component::{ containers::{ context_paper::*, flex_paper::*, grid_paper::*, horizontal_paper::*, modal_paper::*, paper::*, scroll_paper::*, text_tooltip_paper::*, tooltip_paper::*, vertical_paper::*, window_paper::*, wrap_paper::*, }, interactive::{ button_paper::*, icon_button_paper::*, slider_paper::*, switch_button_paper::*, text_button_paper::*, text_field_paper::*, }, }; #[allow(unused_imports)] use raui_material::component::{icon_paper::*, switch_paper::*, text_paper::*}; }; } macro_rules! impl_slot_components { ($($name:ident),+ $(,)?) => { $( pub fn $name( props: impl Into, f: impl FnMut() -> R, ) -> R { impl_imports!(); crate::slot_component(raui_core::make_widget!($name), props, f) } )+ }; } macro_rules! impl_list_components { ($($name:ident),+ $(,)?) => { $( pub fn $name( props: impl Into, f: impl FnMut() -> R, ) -> R { impl_imports!(); crate::list_component(raui_core::make_widget!($name), props, f) } )+ }; } macro_rules! impl_content_components { ($content:literal : $($name:ident),+ $(,)?) => { $( pub fn $name( props: impl Into, f: impl FnMut() -> R, ) -> R { impl_imports!(); crate::content_component(raui_core::make_widget!($name), $content, props, f) } )+ }; } macro_rules! impl_components { ($($name:ident),+ $(,)?) => { $( pub fn $name( props: impl Into, ) { impl_imports!(); crate::component(raui_core::make_widget!($name), props) } )+ }; } pub mod core { impl_components! { image_box, nav_scroll_box_side_scrollbars, none_widget, space_box, text_box, } pub mod containers { impl_content_components! { "content": anchor_box, area_box, hidden_box, nav_scroll_box_content, pivot_box, portal_box, size_box, wrap_box, } impl_slot_components! { context_box, nav_scroll_box, portals_context_box, portals_tooltip_box, responsive_props_box, tooltip_box, variant_box, } impl_list_components! { content_box, flex_box, float_box, grid_box, horizontal_box, nav_content_box, nav_flex_box, nav_float_box, nav_grid_box, nav_horizontal_box, nav_switch_box, nav_tabs_box, nav_vertical_box, responsive_box, switch_box, vertical_box, } } pub mod interactive { use raui_core::{ make_widget, props::Props, widget::{ component::interactive::{ button::ButtonProps, input_field::{TextInputProps, TextInputState}, navigation::NavTrackingProps, options_view::{OptionsViewProps, OptionsViewProxy}, slider_view::{SliderViewProps, SliderViewProxy}, }, utils::Vec2, }, }; use raui_immediate::{begin, end, pop, push, use_state}; use std::str::FromStr; #[derive(Debug, Default, Copy, Clone)] pub struct ImmediateTracking { pub state: NavTrackingProps, pub prev: NavTrackingProps, } impl ImmediateTracking { pub fn pointer_delta_factor(&self) -> Vec2 { Vec2 { x: self.state.factor.x - self.prev.factor.x, y: self.state.factor.y - self.prev.factor.y, } } pub fn pointer_delta_unscaled(&self) -> Vec2 { Vec2 { x: self.state.unscaled.x - self.prev.unscaled.x, y: self.state.unscaled.y - self.prev.unscaled.y, } } pub fn pointer_delta_ui_space(&self) -> Vec2 { Vec2 { x: self.state.ui_space.x - self.prev.ui_space.x, y: self.state.ui_space.y - self.prev.ui_space.y, } } pub fn pointer_moved(&self) -> bool { (self.state.factor.x - self.prev.factor.x) + (self.state.factor.y - self.prev.factor.y) > 1.0e-6 } } #[derive(Debug, Default, Copy, Clone)] pub struct ImmediateButton { pub state: ButtonProps, pub prev: ButtonProps, } impl ImmediateButton { pub fn select_start(&self) -> bool { !self.prev.selected && self.state.selected } pub fn select_stop(&self) -> bool { self.prev.selected && !self.state.selected } pub fn select_changed(&self) -> bool { self.prev.selected != self.state.selected } pub fn trigger_start(&self) -> bool { !self.prev.trigger && self.state.trigger } pub fn trigger_stop(&self) -> bool { self.prev.trigger && !self.state.trigger } pub fn trigger_changed(&self) -> bool { self.prev.trigger != self.state.trigger } pub fn context_start(&self) -> bool { !self.prev.context && self.state.context } pub fn context_stop(&self) -> bool { self.prev.context && !self.state.context } pub fn context_changed(&self) -> bool { self.prev.context != self.state.context } } impl_content_components! { "content": float_view_control, navigation_barrier, } pub fn tracking( props: impl Into, mut f: impl FnMut(ImmediateTracking), ) -> ImmediateTracking { use crate::internal::*; let state = use_state(ImmediateTracking::default); let result = state.read().unwrap().to_owned(); begin(); f(result); let node = end().pop().unwrap_or_default(); push( make_widget!(immediate_tracking) .with_props(ImmediateTrackingProps { state: Some(state) }) .merge_props(props.into()) .named_slot("content", node), ); result } pub fn self_tracking( props: impl Into, mut f: impl FnMut(ImmediateTracking), ) -> ImmediateTracking { use crate::internal::*; let state = use_state(ImmediateTracking::default); let result = state.read().unwrap().to_owned(); begin(); f(result); let node = end().pop().unwrap_or_default(); push( make_widget!(immediate_self_tracking) .with_props(ImmediateTrackingProps { state: Some(state) }) .merge_props(props.into()) .named_slot("content", node), ); result } pub fn button( props: impl Into, mut f: impl FnMut(ImmediateButton), ) -> ImmediateButton { use crate::internal::*; let state = use_state(ImmediateButton::default); let result = state.read().unwrap().to_owned(); begin(); f(result); let node = end().pop().unwrap_or_default(); push( make_widget!(immediate_button) .with_props(ImmediateButtonProps { state: Some(state) }) .merge_props(props.into()) .named_slot("content", node), ); result } pub fn tracked_button( props: impl Into, mut f: impl FnMut(ImmediateButton), ) -> ImmediateButton { use crate::internal::*; let state = use_state(ImmediateButton::default); let result = state.read().unwrap().to_owned(); begin(); f(result); let node = end().pop().unwrap_or_default(); push( make_widget!(immediate_tracked_button) .with_props(ImmediateButtonProps { state: Some(state) }) .merge_props(props.into()) .named_slot("content", node), ); result } pub fn self_tracked_button( props: impl Into, mut f: impl FnMut(ImmediateButton), ) -> ImmediateButton { use crate::internal::*; let state = use_state(ImmediateButton::default); let result = state.read().unwrap().to_owned(); begin(); f(result); let node = end().pop().unwrap_or_default(); push( make_widget!(immediate_self_tracked_button) .with_props(ImmediateButtonProps { state: Some(state) }) .merge_props(props.into()) .named_slot("content", node), ); result } pub fn text_input( value: &T, props: impl Into, mut f: impl FnMut(&str, TextInputState), ) -> (Option, TextInputState) { use crate::internal::*; let content = use_state(|| value.to_string()); let props = props.into(); let TextInputProps { allow_new_line, .. } = props.read_cloned_or_default(); let text_state = use_state(TextInputState::default); let text_result = text_state.read().unwrap().to_owned(); if !text_result.focused { *content.write().unwrap() = value.to_string(); } let result = content.read().unwrap().to_string(); begin(); f(&result, text_result); let node = end().pop().unwrap_or_default(); push( make_widget!(immediate_text_input) .with_props(ImmediateTextInputProps { state: Some(text_state), }) .merge_props(props) .with_props(TextInputProps { allow_new_line, text: Some(content.into()), }) .named_slot("content", node), ); (result.parse().ok(), text_result) } pub fn input_field( value: &T, props: impl Into, mut f: impl FnMut(&str, TextInputState, ImmediateButton), ) -> (Option, TextInputState, ImmediateButton) { use crate::internal::*; let content = use_state(|| value.to_string()); let props = props.into(); let TextInputProps { allow_new_line, .. } = props.read_cloned_or_default(); let text_state = use_state(TextInputState::default); let text_result = text_state.read().unwrap().to_owned(); let button_state = use_state(ImmediateButton::default); let button_result = button_state.read().unwrap().to_owned(); if !text_result.focused { *content.write().unwrap() = value.to_string(); } let result = content.read().unwrap().to_string(); begin(); f(&result, text_result, button_result); let node = end().pop().unwrap_or_default(); push( make_widget!(immediate_input_field) .with_props(ImmediateTextInputProps { state: Some(text_state), }) .with_props(ImmediateButtonProps { state: Some(button_state), }) .merge_props(props) .with_props(TextInputProps { allow_new_line, text: Some(content.into()), }) .named_slot("content", node), ); (result.parse().ok(), text_result, button_result) } pub fn slider_view( value: T, props: impl Into, mut f: impl FnMut(&T, ImmediateButton), ) -> (T, ImmediateButton) { use crate::internal::*; let content = use_state(|| value.to_owned()); let props = props.into(); let SliderViewProps { from, to, direction, .. } = props.read_cloned_or_default(); let button_state = use_state(ImmediateButton::default); let button_result = button_state.read().unwrap().to_owned(); let result = content.read().unwrap().to_owned(); begin(); f(&result, button_result); let node = end().pop().unwrap_or_default(); push( make_widget!(immediate_slider_view) .with_props(ImmediateButtonProps { state: Some(button_state), }) .merge_props(props) .with_props(SliderViewProps { input: Some(content.into()), from, to, direction, }) .named_slot("content", node), ); (result, button_result) } pub fn options_view( value: T, props: impl Into, mut f_items: impl FnMut(&T), mut f_content: impl FnMut(), ) -> T { let content = use_state(|| value.to_owned()); let props = props.into(); let result = content.read().unwrap().to_owned(); begin(); f_items(&result); let nodes = end(); begin(); f_content(); let node = pop(); push( make_widget!(raui_core::widget::component::interactive::options_view::options_view) .merge_props(props) .with_props(OptionsViewProps { input: Some(content.into()), }) .named_slot("content", node) .listed_slots(nodes), ); result } } } pub mod material { impl_components! { icon_paper, scroll_paper_side_scrollbars, switch_paper, text_paper, } pub mod containers { impl_slot_components! { context_paper, scroll_paper, tooltip_paper, window_paper, window_title_controls_paper, } impl_content_components! { "content": modal_paper, text_tooltip_paper, wrap_paper, } impl_list_components! { flex_paper, grid_paper, horizontal_paper, nav_flex_paper, nav_grid_paper, nav_horizontal_paper, nav_paper, nav_vertical_paper, paper, vertical_paper, } } pub mod interactive { use crate::core::interactive::ImmediateButton; use raui_core::{ make_widget, props::Props, widget::component::interactive::{ input_field::{TextInputProps, TextInputState}, slider_view::{SliderViewProps, SliderViewProxy}, }, }; use raui_immediate::{begin, end, push, use_state}; use std::str::FromStr; pub fn button_paper( props: impl Into, mut f: impl FnMut(ImmediateButton), ) -> ImmediateButton { use crate::internal::*; let state = use_state(ImmediateButton::default); let result = state.read().unwrap().to_owned(); begin(); f(result); let node = end().pop().unwrap_or_default(); push( make_widget!(immediate_button_paper) .with_props(ImmediateButtonProps { state: Some(state) }) .merge_props(props.into()) .named_slot("content", node), ); result } pub fn icon_button_paper(props: impl Into) -> ImmediateButton { use crate::internal::*; let state = use_state(ImmediateButton::default); let result = state.read().unwrap().to_owned(); push( make_widget!(immediate_icon_button_paper) .with_props(ImmediateButtonProps { state: Some(state) }) .merge_props(props.into()), ); result } pub fn switch_button_paper(props: impl Into) -> ImmediateButton { use crate::internal::*; let state = use_state(ImmediateButton::default); let result = state.read().unwrap().to_owned(); push( make_widget!(immediate_switch_button_paper) .with_props(ImmediateButtonProps { state: Some(state) }) .merge_props(props.into()), ); result } pub fn text_button_paper(props: impl Into) -> ImmediateButton { use crate::internal::*; let state = use_state(ImmediateButton::default); let result = state.read().unwrap().to_owned(); push( make_widget!(immediate_text_button_paper) .with_props(ImmediateButtonProps { state: Some(state) }) .merge_props(props.into()), ); result } pub fn text_field_paper( value: &T, props: impl Into, ) -> (Option, TextInputState, ImmediateButton) { use crate::internal::*; let content = use_state(|| value.to_string()); let props = props.into(); let TextInputProps { allow_new_line, .. } = props.read_cloned_or_default::(); let text_state = use_state(TextInputState::default); let text_result = text_state.read().unwrap().to_owned(); let button_state = use_state(ImmediateButton::default); let button_result = button_state.read().unwrap().to_owned(); if !text_result.focused { *content.write().unwrap() = value.to_string(); } let result = content.read().unwrap().to_string(); push( make_widget!(immediate_text_field_paper) .with_props(ImmediateTextInputProps { state: Some(text_state), }) .with_props(ImmediateButtonProps { state: Some(button_state), }) .merge_props(props) .with_props(TextInputProps { allow_new_line, text: Some(content.into()), }), ); (result.parse().ok(), text_result, button_result) } pub fn slider_paper( value: T, props: impl Into, mut f: impl FnMut(&T, ImmediateButton), ) -> (T, ImmediateButton) { use crate::internal::*; let content = use_state(|| value.to_owned()); let props = props.into(); let SliderViewProps { from, to, direction, .. } = props.read_cloned_or_default(); let button_state = use_state(ImmediateButton::default); let button_result = button_state.read().unwrap().to_owned(); let result = content.read().unwrap().to_owned(); begin(); f(&result, button_result); let node = end().pop().unwrap_or_default(); push( make_widget!(immediate_slider_paper) .with_props(ImmediateButtonProps { state: Some(button_state), }) .merge_props(props) .with_props(SliderViewProps { input: Some(content.into()), from, to, direction, }) .named_slot("content", node), ); (result, button_result) } pub fn numeric_slider_paper( value: T, props: impl Into, ) -> (T, ImmediateButton) { use crate::internal::*; let content = use_state(|| value.to_owned()); let props = props.into(); let SliderViewProps { from, to, direction, .. } = props.read_cloned_or_default(); let button_state = use_state(ImmediateButton::default); let button_result = button_state.read().unwrap().to_owned(); let result = content.read().unwrap().to_owned(); push( make_widget!(immediate_numeric_slider_paper) .with_props(ImmediateButtonProps { state: Some(button_state), }) .merge_props(props) .with_props(SliderViewProps { input: Some(content.into()), from, to, direction, }), ); (result, button_result) } } } mod internal { use crate::core::interactive::{ImmediateButton, ImmediateTracking}; use raui_core::{ ManagedLazy, Prefab, PropsData, make_widget, pre_hooks, widget::{ component::interactive::{ button::{ ButtonNotifyMessage, ButtonNotifyProps, button, self_tracked_button, tracked_button, }, input_field::{TextInputState, input_field, text_input}, navigation::{ NavTrackingNotifyMessage, NavTrackingNotifyProps, self_tracking, tracking, use_nav_tracking_self, }, slider_view::slider_view, }, context::WidgetContext, node::WidgetNode, }, }; use raui_material::component::interactive::{ button_paper::button_paper_impl, icon_button_paper::icon_button_paper_impl, slider_paper::{numeric_slider_paper_impl, slider_paper_impl}, switch_button_paper::switch_button_paper_impl, text_button_paper::text_button_paper_impl, text_field_paper::text_field_paper_impl, }; use serde::{Deserialize, Serialize}; #[derive(PropsData, Default, Clone, Serialize, Deserialize)] #[props_data(raui_core::props::PropsData)] #[prefab(raui_core::Prefab)] pub struct ImmediateTrackingProps { #[serde(default, skip)] pub state: Option>, } impl std::fmt::Debug for ImmediateTrackingProps { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("ImmediateTrackingProps") .field( "state", &self .state .as_ref() .and_then(|state| state.read()) .map(|state| *state), ) .finish() } } #[derive(PropsData, Default, Clone, Serialize, Deserialize)] #[props_data(raui_core::props::PropsData)] #[prefab(raui_core::Prefab)] pub struct ImmediateButtonProps { #[serde(default, skip)] pub state: Option>, } impl std::fmt::Debug for ImmediateButtonProps { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("ImmediateButtonProps") .field( "state", &self .state .as_ref() .and_then(|state| state.read()) .map(|state| *state), ) .finish() } } #[derive(PropsData, Default, Clone, Serialize, Deserialize)] #[props_data(raui_core::props::PropsData)] pub struct ImmediateTextInputProps { #[serde(default, skip)] pub state: Option>, } impl std::fmt::Debug for ImmediateTextInputProps { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("ImmediateTextInputProps") .finish_non_exhaustive() } } fn use_immediate_tracking(ctx: &mut WidgetContext) { ctx.props .write(NavTrackingNotifyProps(ctx.id.to_owned().into())); if let Ok(props) = ctx.props.read::() { let state = props.state.as_ref().unwrap(); let mut state = state.write().unwrap(); state.prev = state.state; } ctx.life_cycle.change(|ctx| { if let Ok(props) = ctx.props.read::() && let Some(state) = props.state.as_ref() && let Some(mut state) = state.write() { for msg in ctx.messenger.messages { if let Some(msg) = msg.as_any().downcast_ref::() { state.state = msg.state; } } } }); } fn use_immediate_button(ctx: &mut WidgetContext) { ctx.props.write(ButtonNotifyProps(ctx.id.to_owned().into())); if let Ok(props) = ctx.props.read::() { let state = props.state.as_ref().unwrap(); let mut state = state.write().unwrap(); state.prev = state.state; } ctx.life_cycle.change(|ctx| { if let Ok(props) = ctx.props.read::() && let Some(state) = props.state.as_ref() && let Some(mut state) = state.write() { for msg in ctx.messenger.messages { if let Some(msg) = msg.as_any().downcast_ref::() { state.state = msg.state; } } } }); } fn use_immediate_text_input(ctx: &mut WidgetContext) { if let Ok(data) = ctx.state.read_cloned::() && let Ok(props) = ctx.props.read::() { let state = props.state.as_ref().unwrap(); let mut state = state.write().unwrap(); *state = data; } } #[pre_hooks(use_immediate_tracking)] pub(crate) fn immediate_tracking(mut ctx: WidgetContext) -> WidgetNode { tracking(ctx) } #[pre_hooks(use_immediate_tracking, use_nav_tracking_self)] pub(crate) fn immediate_self_tracking(mut ctx: WidgetContext) -> WidgetNode { self_tracking(ctx) } #[pre_hooks(use_immediate_button)] pub(crate) fn immediate_button(mut ctx: WidgetContext) -> WidgetNode { button(ctx) } #[pre_hooks(use_immediate_button)] pub(crate) fn immediate_tracked_button(mut ctx: WidgetContext) -> WidgetNode { tracked_button(ctx) } #[pre_hooks(use_immediate_button)] pub(crate) fn immediate_self_tracked_button(mut ctx: WidgetContext) -> WidgetNode { self_tracked_button(ctx) } #[pre_hooks(use_immediate_text_input)] pub(crate) fn immediate_text_input(mut ctx: WidgetContext) -> WidgetNode { text_input(ctx) } #[pre_hooks(use_immediate_text_input, use_immediate_button)] pub(crate) fn immediate_input_field(mut ctx: WidgetContext) -> WidgetNode { input_field(ctx) } #[pre_hooks(use_immediate_button)] pub(crate) fn immediate_slider_view(mut ctx: WidgetContext) -> WidgetNode { slider_view(ctx) } pub(crate) fn immediate_button_paper(ctx: WidgetContext) -> WidgetNode { button_paper_impl(make_widget!(immediate_button), ctx) } pub(crate) fn immediate_icon_button_paper(ctx: WidgetContext) -> WidgetNode { icon_button_paper_impl(make_widget!(immediate_button_paper), ctx) } pub(crate) fn immediate_switch_button_paper(ctx: WidgetContext) -> WidgetNode { switch_button_paper_impl(make_widget!(immediate_button_paper), ctx) } pub(crate) fn immediate_text_button_paper(ctx: WidgetContext) -> WidgetNode { text_button_paper_impl(make_widget!(immediate_button_paper), ctx) } pub(crate) fn immediate_text_field_paper(ctx: WidgetContext) -> WidgetNode { text_field_paper_impl(make_widget!(immediate_input_field), ctx) } pub(crate) fn immediate_slider_paper(ctx: WidgetContext) -> WidgetNode { slider_paper_impl(make_widget!(immediate_slider_view), ctx) } pub(crate) fn immediate_numeric_slider_paper(ctx: WidgetContext) -> WidgetNode { numeric_slider_paper_impl(make_widget!(immediate_slider_paper), ctx) } } ================================================ FILE: crates/json-renderer/Cargo.toml ================================================ [package] name = "raui-json-renderer" version = "0.70.17" authors = ["Patryk 'PsichiX' Budzynski "] edition = "2024" description = "RAUI renderer for JSON format" 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"] [dependencies] raui-core = { path = "../core", version = "0.70" } serde = { version = "1", features = ["derive"] } serde_json = "1" ================================================ FILE: crates/json-renderer/src/lib.rs ================================================ use raui_core::{ layout::{CoordsMapping, Layout}, renderer::Renderer, widget::unit::WidgetUnit, }; use serde::{Deserialize, Serialize}; #[derive(Debug, Default, Copy, Clone, Serialize, Deserialize)] pub struct JsonRenderer { #[serde(default)] pub pretty: bool, } impl Renderer for JsonRenderer { fn render( &mut self, tree: &WidgetUnit, _: &CoordsMapping, _layout: &Layout, ) -> Result { if self.pretty { serde_json::to_string_pretty(tree) } else { serde_json::to_string(tree) } } } impl Renderer for JsonRenderer { fn render( &mut self, tree: &WidgetUnit, _: &CoordsMapping, _: &Layout, ) -> Result { serde_json::to_value(tree) } } ================================================ FILE: crates/material/Cargo.toml ================================================ [package] name = "raui-material" version = "0.70.17" authors = ["Patryk 'PsichiX' Budzynski "] edition = "2024" description = "Material components library for RAUI" 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"] [dependencies] raui-core = { path = "../core", version = "0.70" } serde = { version = "1", features = ["derive"] } ================================================ FILE: crates/material/src/component/containers/context_paper.rs ================================================ use crate::{ component::containers::{paper::PaperProps, wrap_paper::wrap_paper}, theme::{ThemeColor, ThemedWidgetProps}, }; use raui_core::{ PropsData, Scalar, make_widget, unpack_named_slots, widget::{ WidgetIdOrRef, component::{ containers::{ context_box::portals_context_box, size_box::{SizeBoxProps, size_box}, wrap_box::WrapBoxProps, }, interactive::button::{ButtonNotifyProps, button}, }, context::WidgetContext, node::WidgetNode, unit::{image::ImageBoxFrame, size::SizeBoxSizeValue}, utils::Rect, }, }; use serde::{Deserialize, Serialize}; #[derive(PropsData, Debug, Clone, Serialize, Deserialize)] #[props_data(raui_core::props::PropsData)] #[prefab(raui_core::Prefab)] pub struct ContextPaperProps { #[serde(default = "ContextPaperProps::default_margin")] pub margin: Rect, #[serde(default = "ContextPaperProps::default_frame")] pub frame: Option, #[serde(default)] pub notify_backdrop_accept: WidgetIdOrRef, } impl Default for ContextPaperProps { fn default() -> Self { Self { margin: Self::default_margin(), frame: Self::default_frame(), notify_backdrop_accept: Default::default(), } } } impl ContextPaperProps { fn default_margin() -> Rect { Rect { left: 10.0, right: 10.0, top: 10.0, bottom: 10.0, } } #[allow(clippy::unnecessary_wraps)] fn default_frame() -> Option { Some(2.0) } } pub fn context_paper(context: WidgetContext) -> WidgetNode { let WidgetContext { idref, key, props, named_slots, .. } = context; unpack_named_slots!(named_slots => {content, context}); let ContextPaperProps { margin, frame, notify_backdrop_accept, } = props.read_cloned_or_default(); let context_size_props = SizeBoxProps { width: SizeBoxSizeValue::Content, height: SizeBoxSizeValue::Content, ..Default::default() }; let themed_props = props.read_cloned_or_else(|| ThemedWidgetProps { color: ThemeColor::Primary, ..Default::default() }); let paper_props = props.read_cloned_or_else(|| PaperProps { frame: frame.map(|v| ImageBoxFrame::from((v, true))), ..Default::default() }); let wrap_props = props .clone() .with(themed_props) .with(paper_props) .with(WrapBoxProps { margin, fill: false, }); let backdrop_size_props = SizeBoxProps { width: SizeBoxSizeValue::Fill, height: SizeBoxSizeValue::Fill, ..Default::default() }; make_widget!(portals_context_box) .key(key) .maybe_idref(idref.cloned()) .merge_props(props.clone()) .named_slot("content", content) .named_slot( "context", make_widget!(size_box) .key("size") .with_props(context_size_props) .named_slot( "content", make_widget!(wrap_paper) .key("wrap") .merge_props(wrap_props) .named_slot("content", context), ), ) .named_slot( "backdrop", make_widget!(button) .key("button") .with_props(ButtonNotifyProps(notify_backdrop_accept)) .named_slot( "content", make_widget!(size_box) .key("size") .with_props(backdrop_size_props), ), ) .into() } ================================================ FILE: crates/material/src/component/containers/flex_paper.rs ================================================ use crate::component::containers::paper::paper; use raui_core::{ make_widget, widget::{ component::containers::flex_box::{flex_box, nav_flex_box}, context::WidgetContext, node::WidgetNode, unit::content::ContentBoxItemLayout, }, }; pub fn nav_flex_paper(context: WidgetContext) -> WidgetNode { let WidgetContext { idref, key, props, listed_slots, .. } = context; let inner_props = props.clone().without::(); make_widget!(paper) .key(key) .maybe_idref(idref.cloned()) .merge_props(props.clone()) .listed_slot( make_widget!(nav_flex_box) .key("flex") .merge_props(inner_props) .listed_slots(listed_slots), ) .into() } pub fn flex_paper(context: WidgetContext) -> WidgetNode { let WidgetContext { idref, key, props, listed_slots, .. } = context; let inner_props = props.clone().without::(); make_widget!(paper) .key(key) .maybe_idref(idref.cloned()) .merge_props(props.clone()) .listed_slot( make_widget!(flex_box) .key("flex") .merge_props(inner_props) .listed_slots(listed_slots), ) .into() } ================================================ FILE: crates/material/src/component/containers/grid_paper.rs ================================================ use crate::component::containers::paper::paper; use raui_core::{ make_widget, widget::{ component::containers::grid_box::{grid_box, nav_grid_box}, context::WidgetContext, node::WidgetNode, unit::content::ContentBoxItemLayout, }, }; pub fn nav_grid_paper(context: WidgetContext) -> WidgetNode { let WidgetContext { idref, key, props, listed_slots, .. } = context; let inner_props = props.clone().without::(); make_widget!(paper) .key(key) .maybe_idref(idref.cloned()) .merge_props(props.clone()) .listed_slot( make_widget!(nav_grid_box) .key("grid") .merge_props(inner_props) .listed_slots(listed_slots), ) .into() } pub fn grid_paper(context: WidgetContext) -> WidgetNode { let WidgetContext { idref, key, props, listed_slots, .. } = context; let inner_props = props.clone().without::(); make_widget!(paper) .key(key) .maybe_idref(idref.cloned()) .merge_props(props.clone()) .listed_slot( make_widget!(grid_box) .key("grid") .merge_props(inner_props) .listed_slots(listed_slots), ) .into() } ================================================ FILE: crates/material/src/component/containers/horizontal_paper.rs ================================================ use crate::component::containers::paper::paper; use raui_core::{ make_widget, widget::{ component::containers::horizontal_box::{horizontal_box, nav_horizontal_box}, context::WidgetContext, node::WidgetNode, unit::content::ContentBoxItemLayout, }, }; pub fn nav_horizontal_paper(context: WidgetContext) -> WidgetNode { let WidgetContext { idref, key, props, listed_slots, .. } = context; let inner_props = props.clone().without::(); make_widget!(paper) .key(key) .maybe_idref(idref.cloned()) .merge_props(props.clone()) .listed_slot( make_widget!(nav_horizontal_box) .key("horizontal") .merge_props(inner_props) .listed_slots(listed_slots), ) .into() } pub fn horizontal_paper(context: WidgetContext) -> WidgetNode { let WidgetContext { idref, key, props, listed_slots, .. } = context; let inner_props = props.clone().without::(); make_widget!(paper) .key(key) .maybe_idref(idref.cloned()) .merge_props(props.clone()) .listed_slot( make_widget!(horizontal_box) .key("horizontal") .merge_props(inner_props) .listed_slots(listed_slots), ) .into() } ================================================ FILE: crates/material/src/component/containers/mod.rs ================================================ pub mod context_paper; pub mod flex_paper; pub mod grid_paper; pub mod horizontal_paper; pub mod modal_paper; pub mod paper; pub mod scroll_paper; pub mod text_tooltip_paper; pub mod tooltip_paper; pub mod vertical_paper; pub mod window_paper; pub mod wrap_paper; ================================================ FILE: crates/material/src/component/containers/modal_paper.rs ================================================ use crate::theme::ThemeProps; use raui_core::{ PropsData, make_widget, unpack_named_slots, widget::{ component::{ containers::{content_box::content_box, portal_box::portal_box}, image_box::{ImageBoxProps, image_box}, interactive::navigation::navigation_barrier, }, context::WidgetContext, node::WidgetNode, unit::image::{ImageBoxColor, ImageBoxMaterial}, utils::Color, }, }; use serde::{Deserialize, Serialize}; #[derive(PropsData, Debug, Clone, Serialize, Deserialize)] #[props_data(raui_core::props::PropsData)] #[prefab(raui_core::Prefab)] pub struct ModalPaperProps { #[serde(default = "ModalPaperProps::default_shadow_shown")] pub shadow_shown: bool, #[serde(default)] pub shadow_variant: String, } impl ModalPaperProps { fn default_shadow_shown() -> bool { true } } impl Default for ModalPaperProps { fn default() -> Self { Self { shadow_shown: Self::default_shadow_shown(), shadow_variant: Default::default(), } } } pub fn modal_paper(context: WidgetContext) -> WidgetNode { let WidgetContext { key, props, shared_props, named_slots, .. } = context; unpack_named_slots!(named_slots => content); let ModalPaperProps { shadow_shown, shadow_variant, } = props.read_cloned_or_default(); let mut color = Color::transparent(); if shadow_shown && let Ok(props) = shared_props.read::() && let Some(c) = props.modal_shadow_variants.get(&shadow_variant) { color = *c; } let shadow_image_props = ImageBoxProps { material: ImageBoxMaterial::Color(ImageBoxColor { color, ..Default::default() }), ..Default::default() }; make_widget!(portal_box) .key(key) .named_slot( "content", make_widget!(content_box) .key("container") .listed_slot( make_widget!(navigation_barrier) .key("shadow-barrier") .named_slot( "content", make_widget!(image_box) .key("shadow-image") .with_props(shadow_image_props), ), ) .listed_slot(content), ) .into() } ================================================ FILE: crates/material/src/component/containers/paper.rs ================================================ use crate::theme::{ThemeColor, ThemeProps, ThemeVariant, ThemedImageMaterial, ThemedWidgetProps}; use raui_core::{ PropsData, Scalar, make_widget, props::Props, widget::{ component::{ WidgetComponent, containers::content_box::{content_box, nav_content_box}, image_box::{ImageBoxProps, image_box}, }, context::WidgetContext, node::WidgetNode, unit::{ content::ContentBoxItemLayout, image::{ImageBoxColor, ImageBoxFrame, ImageBoxImageScaling, ImageBoxMaterial}, }, }, }; use serde::{Deserialize, Serialize}; #[derive(PropsData, Debug, Default, Clone, Serialize, Deserialize)] #[props_data(raui_core::props::PropsData)] #[prefab(raui_core::Prefab)] pub struct PaperProps { #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] pub frame: Option, #[serde(default)] pub variant: String, } #[derive(PropsData, Debug, Default, Clone, Serialize, Deserialize)] #[props_data(raui_core::props::PropsData)] #[prefab(raui_core::Prefab)] pub struct PaperContentLayoutProps(pub ContentBoxItemLayout); pub fn nav_paper(context: WidgetContext) -> WidgetNode { paper_impl(make_widget!(nav_content_box), context) } pub fn paper(context: WidgetContext) -> WidgetNode { paper_impl(make_widget!(content_box), context) } pub fn paper_impl(component: WidgetComponent, context: WidgetContext) -> WidgetNode { let WidgetContext { idref, key, props, shared_props, listed_slots, .. } = context; let paper_props = props.read_cloned_or_default::(); let themed_props = props.read_cloned_or_default::(); let listed_slots = listed_slots .into_iter() .map(|mut item| { item.remap_props(|mut props| { if let Ok(PaperContentLayoutProps(layout)) = props.consume_unwrap_cloned() { props.write(layout); } props }); item }) .collect::>(); let items = match themed_props.variant { ThemeVariant::ContentOnly => listed_slots, ThemeVariant::Filled => { let content_background = shared_props.map_or_default::(|props| { props .content_backgrounds .get(&paper_props.variant) .cloned() .unwrap_or_default() }); let background_colors = shared_props .map_or_default::(|props| props.background_colors.clone()); let image = match content_background { ThemedImageMaterial::Color => { let color = match themed_props.color { ThemeColor::Default => background_colors.main.default.main, ThemeColor::Primary => background_colors.main.primary.main, ThemeColor::Secondary => background_colors.main.secondary.main, }; ImageBoxProps { material: ImageBoxMaterial::Color(ImageBoxColor { color, ..Default::default() }), ..Default::default() } } ThemedImageMaterial::Image(material) => ImageBoxProps { material: ImageBoxMaterial::Image(material), ..Default::default() }, ThemedImageMaterial::Procedural(material) => ImageBoxProps { material: ImageBoxMaterial::Procedural(material), ..Default::default() }, }; let props = Props::new(ContentBoxItemLayout { depth: Scalar::NEG_INFINITY, ..Default::default() }) .with(image); let background = make_widget!(image_box) .key("background") .merge_props(props) .into(); if let Some(frame) = paper_props.frame { let color = match themed_props.color { ThemeColor::Default => background_colors.main.default.dark, ThemeColor::Primary => background_colors.main.primary.dark, ThemeColor::Secondary => background_colors.main.secondary.dark, }; let props = Props::new(ContentBoxItemLayout { depth: Scalar::NEG_INFINITY, ..Default::default() }) .with(ImageBoxProps { material: ImageBoxMaterial::Color(ImageBoxColor { color, scaling: ImageBoxImageScaling::Frame(frame), }), ..Default::default() }); let frame = make_widget!(image_box) .key("frame") .merge_props(props) .into(); std::iter::once(background) .chain(std::iter::once(frame)) .chain(listed_slots) .collect::>() } else { std::iter::once(background) .chain(listed_slots) .collect::>() } } ThemeVariant::Outline => { if let Some(frame) = paper_props.frame { let background_colors = shared_props .map_or_default::(|props| props.background_colors.clone()); let color = match themed_props.color { ThemeColor::Default => background_colors.main.default.dark, ThemeColor::Primary => background_colors.main.primary.dark, ThemeColor::Secondary => background_colors.main.secondary.dark, }; let props = Props::new(ContentBoxItemLayout { depth: Scalar::NEG_INFINITY, ..Default::default() }) .with(ImageBoxProps { material: ImageBoxMaterial::Color(ImageBoxColor { color, scaling: ImageBoxImageScaling::Frame(frame), }), ..Default::default() }); let frame = make_widget!(image_box) .key("frame") .merge_props(props) .into(); std::iter::once(frame) .chain(listed_slots) .collect::>() } else { listed_slots } } }; component .key(key) .maybe_idref(idref.cloned()) .merge_props(props.clone()) .listed_slots(items) .into() } ================================================ FILE: crates/material/src/component/containers/scroll_paper.rs ================================================ use crate::{ component::containers::paper::paper, theme::{ThemeColor, ThemeProps, ThemedImageMaterial, ThemedWidgetProps}, }; use raui_core::{ PropsData, Scalar, make_widget, unpack_named_slots, widget::{ component::containers::scroll_box::{ SideScrollbarsProps, nav_scroll_box, nav_scroll_box_side_scrollbars, }, context::WidgetContext, node::WidgetNode, unit::{ content::ContentBoxItemLayout, image::{ImageBoxColor, ImageBoxMaterial}, }, }, }; use serde::{Deserialize, Serialize}; #[derive(PropsData, Debug, Clone, Serialize, Deserialize)] #[props_data(raui_core::props::PropsData)] #[prefab(raui_core::Prefab)] pub struct SideScrollbarsPaperProps { #[serde(default)] pub size: Scalar, #[serde(default)] pub back_variant: Option, #[serde(default)] pub front_variant: String, } impl Default for SideScrollbarsPaperProps { fn default() -> Self { Self { size: 10.0, back_variant: None, front_variant: Default::default(), } } } pub fn scroll_paper(context: WidgetContext) -> WidgetNode { let WidgetContext { idref, key, props, named_slots, .. } = context; unpack_named_slots!(named_slots => {content, scrollbars}); let inner_props = props.clone().without::(); make_widget!(paper) .key(key) .maybe_idref(idref.cloned()) .merge_props(props.clone()) .listed_slot( make_widget!(nav_scroll_box) .key("scroll") .merge_props(inner_props) .named_slot("content", content) .named_slot("scrollbars", scrollbars), ) .into() } pub fn scroll_paper_side_scrollbars(context: WidgetContext) -> WidgetNode { let WidgetContext { idref, key, props, shared_props, .. } = context; let scrollbars_props = props.read_cloned_or_default::(); let themed_props = props.read_cloned_or_default::(); let colors = shared_props.map_or_default::(|props| props.active_colors.clone()); let back_material = if let Some(back_variant) = &scrollbars_props.back_variant { let background = shared_props.map_or_default::(|props| { props .button_backgrounds .get(back_variant) .cloned() .unwrap_or_default() .default }); Some(match background { ThemedImageMaterial::Color => { let color = match themed_props.color { ThemeColor::Default => colors.main.default.main, ThemeColor::Primary => colors.main.primary.main, ThemeColor::Secondary => colors.main.secondary.main, }; ImageBoxMaterial::Color(ImageBoxColor { color, ..Default::default() }) } ThemedImageMaterial::Image(material) => ImageBoxMaterial::Image(material), ThemedImageMaterial::Procedural(material) => ImageBoxMaterial::Procedural(material), }) } else { None }; let front_material = { let background = shared_props.map_or_default::(|props| { props .button_backgrounds .get(&scrollbars_props.front_variant) .cloned() .unwrap_or_default() .trigger }); match background { ThemedImageMaterial::Color => { let color = match themed_props.color { ThemeColor::Default => colors.main.default.main, ThemeColor::Primary => colors.main.primary.main, ThemeColor::Secondary => colors.main.secondary.main, }; ImageBoxMaterial::Color(ImageBoxColor { color, ..Default::default() }) } ThemedImageMaterial::Image(material) => ImageBoxMaterial::Image(material), ThemedImageMaterial::Procedural(material) => ImageBoxMaterial::Procedural(material), } }; props.write(SideScrollbarsProps { size: scrollbars_props.size, back_material, front_material, }); make_widget!(nav_scroll_box_side_scrollbars) .key(key) .maybe_idref(idref.cloned()) .merge_props(props.clone()) .into() } ================================================ FILE: crates/material/src/component/containers/text_tooltip_paper.rs ================================================ use crate::component::{containers::tooltip_paper::tooltip_paper, text_paper::text_paper}; use raui_core::{ make_widget, unpack_named_slots, widget::{context::WidgetContext, node::WidgetNode}, }; pub fn text_tooltip_paper(context: WidgetContext) -> WidgetNode { let WidgetContext { idref, key, props, named_slots, .. } = context; unpack_named_slots!(named_slots => content); make_widget!(tooltip_paper) .key(key) .maybe_idref(idref.cloned()) .merge_props(props.clone()) .named_slot("content", content) .named_slot( "tooltip", make_widget!(text_paper) .key("text") .merge_props(props.clone()), ) .into() } ================================================ FILE: crates/material/src/component/containers/tooltip_paper.rs ================================================ use crate::{ component::containers::{paper::PaperProps, wrap_paper::wrap_paper}, theme::{ThemeColor, ThemedWidgetProps}, }; use raui_core::{ PropsData, Scalar, make_widget, unpack_named_slots, widget::{ component::containers::{ size_box::{SizeBoxProps, size_box}, tooltip_box::portals_tooltip_box, wrap_box::WrapBoxProps, }, context::WidgetContext, node::WidgetNode, unit::{image::ImageBoxFrame, size::SizeBoxSizeValue}, utils::Rect, }, }; use serde::{Deserialize, Serialize}; #[derive(PropsData, Debug, Copy, Clone, Serialize, Deserialize)] #[props_data(raui_core::props::PropsData)] #[prefab(raui_core::Prefab)] pub struct TooltipPaperProps { #[serde(default = "TooltipPaperProps::default_margin")] pub margin: Rect, #[serde(default = "TooltipPaperProps::default_frame")] pub frame: Option, } impl Default for TooltipPaperProps { fn default() -> Self { Self { margin: Self::default_margin(), frame: Self::default_frame(), } } } impl TooltipPaperProps { fn default_margin() -> Rect { Rect { left: 10.0, right: 10.0, top: 10.0, bottom: 10.0, } } #[allow(clippy::unnecessary_wraps)] fn default_frame() -> Option { Some(2.0) } } pub fn tooltip_paper(context: WidgetContext) -> WidgetNode { let WidgetContext { idref, key, props, named_slots, .. } = context; unpack_named_slots!(named_slots => {content, tooltip}); let TooltipPaperProps { margin, frame } = props.read_cloned_or_default(); let size_props = SizeBoxProps { width: SizeBoxSizeValue::Content, height: SizeBoxSizeValue::Content, ..Default::default() }; let themed_props = props.read_cloned_or_else(|| ThemedWidgetProps { color: ThemeColor::Primary, ..Default::default() }); let paper_props = props.read_cloned_or_else(|| PaperProps { frame: frame.map(|v| ImageBoxFrame::from((v, true))), ..Default::default() }); let wrap_props = props .clone() .with(themed_props) .with(paper_props) .with(WrapBoxProps { margin, fill: false, }); make_widget!(portals_tooltip_box) .key(key) .maybe_idref(idref.cloned()) .merge_props(props.clone()) .named_slot("content", content) .named_slot( "tooltip", make_widget!(size_box) .key("size") .with_props(size_props) .named_slot( "content", make_widget!(wrap_paper) .key("wrap") .merge_props(wrap_props) .named_slot("content", tooltip), ), ) .into() } ================================================ FILE: crates/material/src/component/containers/vertical_paper.rs ================================================ use crate::component::containers::paper::paper; use raui_core::{ make_widget, widget::{ component::containers::vertical_box::{nav_vertical_box, vertical_box}, context::WidgetContext, node::WidgetNode, unit::content::ContentBoxItemLayout, }, }; pub fn nav_vertical_paper(context: WidgetContext) -> WidgetNode { let WidgetContext { idref, key, props, listed_slots, .. } = context; let inner_props = props.clone().without::(); make_widget!(paper) .key(key) .maybe_idref(idref.cloned()) .merge_props(props.clone()) .listed_slot( make_widget!(nav_vertical_box) .key("vertical") .merge_props(inner_props) .listed_slots(listed_slots), ) .into() } pub fn vertical_paper(context: WidgetContext) -> WidgetNode { let WidgetContext { idref, key, props, listed_slots, .. } = context; let inner_props = props.clone().without::(); make_widget!(paper) .key(key) .maybe_idref(idref.cloned()) .merge_props(props.clone()) .listed_slot( make_widget!(vertical_box) .key("vertical") .merge_props(inner_props) .listed_slots(listed_slots), ) .into() } ================================================ FILE: crates/material/src/component/containers/window_paper.rs ================================================ use crate::{ component::containers::wrap_paper::wrap_paper, theme::{ThemeColor, ThemedWidgetProps}, }; use raui_core::{ PropsData, Scalar, make_widget, unpack_named_slots, widget::{ component::containers::{ horizontal_box::horizontal_box, vertical_box::vertical_box, wrap_box::WrapBoxProps, }, context::WidgetContext, node::WidgetNode, unit::flex::FlexBoxItemLayout, utils::Rect, }, }; use serde::{Deserialize, Serialize}; #[derive(PropsData, Debug, Clone, Serialize, Deserialize)] #[props_data(raui_core::props::PropsData)] #[prefab(raui_core::Prefab)] pub struct WindowPaperProps { #[serde(default)] pub bar_color: ThemeColor, #[serde(default = "WindowPaperProps::default_bar_margin")] pub bar_margin: Rect, #[serde(default = "WindowPaperProps::default_bar_height")] pub bar_height: Option, #[serde(default = "WindowPaperProps::default_content_margin")] pub content_margin: Rect, } impl Default for WindowPaperProps { fn default() -> Self { Self { bar_color: ThemeColor::Primary, bar_margin: Self::default_bar_margin(), bar_height: Self::default_bar_height(), content_margin: Self::default_content_margin(), } } } impl WindowPaperProps { fn default_bar_margin() -> Rect { Rect { left: 10.0, right: 10.0, top: 4.0, bottom: 4.0, } } fn default_bar_height() -> Option { Some(32.0) } fn default_content_margin() -> Rect { 10.0.into() } } pub fn window_paper(context: WidgetContext) -> WidgetNode { let WidgetContext { idref, key, props, named_slots, .. } = context; unpack_named_slots!(named_slots => {content, bar}); let window_props = props.read_cloned_or_default::(); make_widget!(vertical_box) .key(key) .maybe_idref(idref.cloned()) .merge_props(props.clone()) .listed_slot( make_widget!(wrap_paper) .key("bar") .with_props(ThemedWidgetProps { color: window_props.bar_color, ..Default::default() }) .with_props(WrapBoxProps { margin: window_props.bar_margin, ..Default::default() }) .with_props(FlexBoxItemLayout { basis: window_props.bar_height, grow: 0.0, shrink: 0.0, ..Default::default() }) .named_slot("content", bar), ) .listed_slot( make_widget!(wrap_paper) .key("content") .with_props(WrapBoxProps { margin: window_props.content_margin, ..Default::default() }) .named_slot("content", content), ) .into() } pub fn window_title_controls_paper(context: WidgetContext) -> WidgetNode { let WidgetContext { key, named_slots, .. } = context; unpack_named_slots!(named_slots => {content, title, controls}); let mut controls = if let WidgetNode::Tuple(nodes) = controls { make_widget!(horizontal_box).listed_slots(nodes).into() } else { controls }; controls.remap_props(|p| { if p.has::() { p } else { p.with(FlexBoxItemLayout { grow: 0.0, shrink: 0.0, ..Default::default() }) } }); make_widget!(window_paper) .key(key) .named_slot( "bar", make_widget!(horizontal_box) .key("bar") .listed_slot(title) .listed_slot(controls), ) .named_slot("content", content) .into() } ================================================ FILE: crates/material/src/component/containers/wrap_paper.rs ================================================ use crate::component::containers::paper::paper; use raui_core::{ make_widget, unpack_named_slots, widget::{ component::containers::wrap_box::wrap_box, context::WidgetContext, node::WidgetNode, unit::content::ContentBoxItemLayout, }, }; pub fn wrap_paper(context: WidgetContext) -> WidgetNode { let WidgetContext { idref, key, props, named_slots, .. } = context; unpack_named_slots!(named_slots => content); let inner_props = props.clone().without::(); make_widget!(paper) .key(key) .maybe_idref(idref.cloned()) .merge_props(props.clone()) .listed_slot( make_widget!(wrap_box) .key("wrap") .merge_props(inner_props) .named_slot("content", content), ) .into() } ================================================ FILE: crates/material/src/component/icon_paper.rs ================================================ use crate::theme::{ThemeColor, ThemeProps, ThemedWidgetProps}; use raui_core::{ PropsData, make_widget, widget::{ component::image_box::{ImageBoxProps, image_box}, context::WidgetContext, node::WidgetNode, unit::image::{ ImageBoxAspectRatio, ImageBoxImage, ImageBoxImageScaling, ImageBoxMaterial, ImageBoxSizeValue, }, utils::{Rect, Transform}, }, }; use serde::{Deserialize, Serialize}; #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct IconImage { #[serde(default)] pub id: String, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] pub source_rect: Option, #[serde(default)] pub scaling: ImageBoxImageScaling, } #[derive(PropsData, Debug, Default, Clone, Serialize, Deserialize)] #[props_data(raui_core::props::PropsData)] #[prefab(raui_core::Prefab)] pub struct IconPaperProps { #[serde(default)] pub image: IconImage, #[serde(default)] pub size_level: usize, #[serde(default)] pub transform: Transform, } pub fn icon_paper(context: WidgetContext) -> WidgetNode { let WidgetContext { idref, key, props, shared_props, .. } = context; let themed_props = props.read_cloned_or_default::(); let tint = match shared_props.read::() { Ok(props) => match themed_props.color { ThemeColor::Default => props.active_colors.contrast.default.main, ThemeColor::Primary => props.active_colors.contrast.primary.main, ThemeColor::Secondary => props.active_colors.contrast.secondary.main, }, Err(_) => Default::default(), }; let icon_props = props.read_cloned_or_default::(); let size = match shared_props.read::() { Ok(props) => props .icons_level_sizes .get(icon_props.size_level) .copied() .unwrap_or(24.0), Err(_) => 24.0, }; let IconImage { id, source_rect, scaling, } = icon_props.image; let image = ImageBoxImage { id, source_rect, scaling, tint, }; let props = ImageBoxProps { width: ImageBoxSizeValue::Exact(size), height: ImageBoxSizeValue::Exact(size), content_keep_aspect_ratio: Some(ImageBoxAspectRatio { horizontal_alignment: 0.5, vertical_alignment: 0.5, outside: false, }), material: ImageBoxMaterial::Image(image), transform: icon_props.transform, }; make_widget!(image_box) .key(key) .maybe_idref(idref.cloned()) .with_props(props) .into() } ================================================ FILE: crates/material/src/component/interactive/button_paper.rs ================================================ use crate::{ component::containers::paper::PaperProps, theme::{ThemeColor, ThemeProps, ThemeVariant, ThemedImageMaterial, ThemedWidgetProps}, }; use raui_core::{ PropsData, Scalar, make_widget, props::Props, unpack_named_slots, widget::{ component::{ WidgetComponent, containers::content_box::content_box, image_box::{ImageBoxProps, image_box}, interactive::button::{ButtonProps, button}, }, context::WidgetContext, node::WidgetNode, unit::{ content::ContentBoxItemLayout, image::{ImageBoxColor, ImageBoxImageScaling, ImageBoxMaterial}, }, }, }; use serde::{Deserialize, Serialize}; #[derive(PropsData, Debug, Default, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] #[props_data(raui_core::props::PropsData)] #[prefab(raui_core::Prefab)] pub enum ButtonPaperOverrideStyle { #[default] None, Default, Selected, Triggered, } fn button_paper_content(context: WidgetContext) -> WidgetNode { let WidgetContext { key, props, shared_props, named_slots, .. } = context; unpack_named_slots!(named_slots => content); let mut button_props = props.read_cloned_or_default::(); let paper_props = props.read_cloned_or_default::(); let themed_props = props.read_cloned_or_default::(); let override_style = props.read_cloned_or_default::(); if override_style != ButtonPaperOverrideStyle::None { button_props.selected = override_style == ButtonPaperOverrideStyle::Selected; button_props.trigger = override_style == ButtonPaperOverrideStyle::Triggered; button_props.context = false; } let items = match themed_props.variant { ThemeVariant::ContentOnly => vec![content], ThemeVariant::Filled => { let button_background = shared_props.map_or_default::(|props| { if button_props.trigger || button_props.context { props .button_backgrounds .get(&paper_props.variant) .cloned() .unwrap_or_default() .trigger } else if button_props.selected { props .button_backgrounds .get(&paper_props.variant) .cloned() .unwrap_or_default() .selected } else { props .button_backgrounds .get(&paper_props.variant) .cloned() .unwrap_or_default() .default } }); let button_colors = shared_props .map_or_default::(|props| props.active_colors.clone()); let image = match button_background { ThemedImageMaterial::Color => { let color = match themed_props.color { ThemeColor::Default => button_colors.main.default.main, ThemeColor::Primary => button_colors.main.primary.main, ThemeColor::Secondary => button_colors.main.secondary.main, }; ImageBoxProps { material: ImageBoxMaterial::Color(ImageBoxColor { color, ..Default::default() }), ..Default::default() } } ThemedImageMaterial::Image(material) => ImageBoxProps { material: ImageBoxMaterial::Image(material), ..Default::default() }, ThemedImageMaterial::Procedural(material) => ImageBoxProps { material: ImageBoxMaterial::Procedural(material), ..Default::default() }, }; let props = Props::new(ContentBoxItemLayout { depth: Scalar::NEG_INFINITY, ..Default::default() }) .with(image); let background = make_widget!(image_box) .key("background") .merge_props(props) .into(); if let Some(frame) = paper_props.frame { let color = match themed_props.color { ThemeColor::Default => button_colors.main.default.dark, ThemeColor::Primary => button_colors.main.primary.dark, ThemeColor::Secondary => button_colors.main.secondary.dark, }; let props = Props::new(ContentBoxItemLayout { depth: Scalar::NEG_INFINITY, ..Default::default() }) .with(ImageBoxProps { material: ImageBoxMaterial::Color(ImageBoxColor { color, scaling: ImageBoxImageScaling::Frame(frame), }), ..Default::default() }); let frame = make_widget!(image_box) .key("frame") .merge_props(props) .into(); vec![background, frame, content] } else { vec![background, content] } } ThemeVariant::Outline => { if let Some(frame) = paper_props.frame { let button_colors = shared_props .map_or_default::(|props| props.active_colors.clone()); let color = match themed_props.color { ThemeColor::Default => button_colors.main.default.dark, ThemeColor::Primary => button_colors.main.primary.dark, ThemeColor::Secondary => button_colors.main.secondary.dark, }; let props = Props::new(ContentBoxItemLayout { depth: Scalar::NEG_INFINITY, ..Default::default() }) .with(ImageBoxProps { material: ImageBoxMaterial::Color(ImageBoxColor { color, scaling: ImageBoxImageScaling::Frame(frame), }), ..Default::default() }); let frame = make_widget!(image_box) .key("frame") .merge_props(props) .into(); vec![frame, content] } else { vec![content] } } }; make_widget!(content_box) .key(key) .merge_props(props.clone()) .listed_slots(items) .into() } pub fn button_paper(context: WidgetContext) -> WidgetNode { button_paper_impl(make_widget!(button), context) } pub fn button_paper_impl(component: WidgetComponent, context: WidgetContext) -> WidgetNode { let WidgetContext { idref, key, props, named_slots, .. } = context; unpack_named_slots!(named_slots => content); component .key(key) .maybe_idref(idref.cloned()) .merge_props(props.clone()) .named_slot( "content", make_widget!(button_paper_content) .key("content") .merge_props(props.clone()) .named_slot("content", content), ) .into() } ================================================ FILE: crates/material/src/component/interactive/icon_button_paper.rs ================================================ use crate::component::{icon_paper::icon_paper, interactive::button_paper::button_paper}; use raui_core::{ make_widget, widget::{component::WidgetComponent, context::WidgetContext, node::WidgetNode}, }; pub fn icon_button_paper(context: WidgetContext) -> WidgetNode { icon_button_paper_impl(make_widget!(button_paper), context) } pub fn icon_button_paper_impl(component: WidgetComponent, context: WidgetContext) -> WidgetNode { let WidgetContext { idref, key, props, .. } = context; component .key(key) .maybe_idref(idref.cloned()) .merge_props(props.clone()) .named_slot( "content", make_widget!(icon_paper) .key("icon") .merge_props(props.clone()), ) .into() } ================================================ FILE: crates/material/src/component/interactive/mod.rs ================================================ pub mod button_paper; pub mod icon_button_paper; pub mod slider_paper; pub mod switch_button_paper; pub mod text_button_paper; pub mod text_field_paper; ================================================ FILE: crates/material/src/component/interactive/slider_paper.rs ================================================ use crate::{ component::text_paper::{TextPaperProps, text_paper}, theme::{ThemeColor, ThemeProps, ThemedImageMaterial, ThemedSliderMaterial}, }; use raui_core::{ PropsData, make_widget, unpack_named_slots, widget::{ component::{ WidgetComponent, containers::content_box::content_box, image_box::{ImageBoxProps, image_box}, interactive::slider_view::{SliderViewDirection, SliderViewProps, slider_view}, }, context::WidgetContext, node::WidgetNode, unit::{ content::ContentBoxItemLayout, image::{ImageBoxColor, ImageBoxMaterial}, text::TextBoxSizeValue, }, utils::Rect, }, }; use serde::{Deserialize, Serialize}; #[derive(PropsData, Debug, Clone, Serialize, Deserialize)] #[props_data(raui_core::props::PropsData)] #[prefab(raui_core::Prefab)] pub struct SliderPaperProps { #[serde(default)] pub variant: String, #[serde(default = "SliderPaperProps::default_background_color")] pub background_color: ThemeColor, #[serde(default = "SliderPaperProps::default_filling_color")] pub filling_color: ThemeColor, } impl Default for SliderPaperProps { fn default() -> Self { Self { variant: Default::default(), background_color: Self::default_background_color(), filling_color: Self::default_filling_color(), } } } impl SliderPaperProps { fn default_background_color() -> ThemeColor { ThemeColor::Secondary } fn default_filling_color() -> ThemeColor { ThemeColor::Primary } } #[derive(PropsData, Debug, Default, Clone, Copy, Serialize, Deserialize)] #[props_data(raui_core::props::PropsData)] #[prefab(raui_core::Prefab)] pub struct NumericSliderPaperProps { #[serde(default)] pub fractional_digits_count: Option, } pub fn slider_paper(context: WidgetContext) -> WidgetNode { slider_paper_impl(make_widget!(slider_view), context) } pub fn slider_paper_impl(component: WidgetComponent, context: WidgetContext) -> WidgetNode { let WidgetContext { idref, key, props, shared_props, named_slots, .. } = context; unpack_named_slots!(named_slots => content); let SliderPaperProps { variant, background_color, filling_color, } = props.read_cloned_or_default(); let anchors = props .read::() .ok() .map(|props| { let percentage = props.get_percentage(); match props.direction { SliderViewDirection::LeftToRight => Rect { left: 0.0, right: percentage, top: 0.0, bottom: 1.0, }, SliderViewDirection::RightToLeft => Rect { left: 1.0 - percentage, right: 1.0, top: 0.0, bottom: 1.0, }, SliderViewDirection::TopToBottom => Rect { left: 0.0, right: 1.0, top: 0.0, bottom: percentage, }, SliderViewDirection::BottomToTop => Rect { left: 0.0, right: 1.0, top: 1.0 - percentage, bottom: 1.0, }, } }) .unwrap_or_default(); let (background, filling) = match shared_props.read::() { Ok(props) => { if let Some(material) = props.slider_variants.get(&variant).cloned() { let background_color = match background_color { ThemeColor::Default => props.active_colors.main.default.main, ThemeColor::Primary => props.active_colors.main.primary.main, ThemeColor::Secondary => props.active_colors.main.secondary.main, }; let filling_color = match filling_color { ThemeColor::Default => props.active_colors.main.default.main, ThemeColor::Primary => props.active_colors.main.primary.main, ThemeColor::Secondary => props.active_colors.main.secondary.main, }; let ThemedSliderMaterial { background, filling, } = material; let background = match background { ThemedImageMaterial::Color => ImageBoxProps { material: ImageBoxMaterial::Color(ImageBoxColor { color: background_color, ..Default::default() }), ..Default::default() }, ThemedImageMaterial::Image(mut data) => { data.tint = filling_color; ImageBoxProps { material: ImageBoxMaterial::Image(data), ..Default::default() } } ThemedImageMaterial::Procedural(data) => ImageBoxProps { material: ImageBoxMaterial::Procedural(data), ..Default::default() }, }; let filling = match filling { ThemedImageMaterial::Color => ImageBoxProps { material: ImageBoxMaterial::Color(ImageBoxColor { color: filling_color, ..Default::default() }), ..Default::default() }, ThemedImageMaterial::Image(mut data) => { data.tint = filling_color; ImageBoxProps { material: ImageBoxMaterial::Image(data), ..Default::default() } } ThemedImageMaterial::Procedural(data) => ImageBoxProps { material: ImageBoxMaterial::Procedural(data), ..Default::default() }, }; (background, filling) } else { Default::default() } } Err(_) => Default::default(), }; component .key(key) .maybe_idref(idref.cloned()) .merge_props(props.clone()) .named_slot( "content", make_widget!(content_box) .key("content") .merge_props(props.clone()) .listed_slot( make_widget!(image_box) .key("background") .with_props(background), ) .listed_slot( make_widget!(image_box) .key("filling") .with_props(ContentBoxItemLayout { anchors, ..Default::default() }) .with_props(filling), ) .listed_slot(content), ) .into() } pub fn numeric_slider_paper(context: WidgetContext) -> WidgetNode { numeric_slider_paper_impl(make_widget!(slider_paper), context) } pub fn numeric_slider_paper_impl(component: WidgetComponent, context: WidgetContext) -> WidgetNode { let WidgetContext { idref, key, props, .. } = context; let mut text = props.read_cloned_or_default::(); text.width = TextBoxSizeValue::Fill; text.height = TextBoxSizeValue::Fill; let value = props .read::() .ok() .map(|props| props.get_value()) .unwrap_or_default(); text.text = if let Some(count) = props .read_cloned_or_default::() .fractional_digits_count { format!("{value:.count$}") } else { value.to_string() }; component .key(key) .maybe_idref(idref.cloned()) .merge_props(props.clone()) .named_slot( "content", make_widget!(text_paper) .merge_props(props.clone()) .with_props(text), ) .into() } ================================================ FILE: crates/material/src/component/interactive/switch_button_paper.rs ================================================ use crate::component::{interactive::button_paper::button_paper, switch_paper::switch_paper}; use raui_core::{ make_widget, widget::{component::WidgetComponent, context::WidgetContext, node::WidgetNode}, }; pub fn switch_button_paper(context: WidgetContext) -> WidgetNode { switch_button_paper_impl(make_widget!(button_paper), context) } pub fn switch_button_paper_impl(component: WidgetComponent, context: WidgetContext) -> WidgetNode { let WidgetContext { idref, key, props, .. } = context; component .key(key) .maybe_idref(idref.cloned()) .merge_props(props.clone()) .named_slot( "content", make_widget!(switch_paper) .key("switch") .merge_props(props.clone()), ) .into() } ================================================ FILE: crates/material/src/component/interactive/text_button_paper.rs ================================================ use crate::component::{interactive::button_paper::button_paper, text_paper::text_paper}; use raui_core::{ make_widget, widget::{ component::{ WidgetComponent, containers::wrap_box::{WrapBoxProps, wrap_box}, }, context::WidgetContext, node::WidgetNode, }, }; pub fn text_button_paper(context: WidgetContext) -> WidgetNode { text_button_paper_impl(make_widget!(button_paper), context) } pub fn text_button_paper_impl(component: WidgetComponent, context: WidgetContext) -> WidgetNode { let WidgetContext { idref, key, props, .. } = context; let wrap_props = props.read_cloned_or_default::(); component .key(key) .maybe_idref(idref.cloned()) .merge_props(props.clone()) .named_slot( "content", make_widget!(wrap_box) .key("wrap") .with_props(wrap_props) .named_slot( "content", make_widget!(text_paper) .key("switch") .merge_props(props.clone()), ), ) .into() } ================================================ FILE: crates/material/src/component/interactive/text_field_paper.rs ================================================ use crate::{ component::{ containers::paper::{PaperProps, paper}, text_paper::{TextPaperProps, text_paper}, }, theme::ThemedWidgetProps, }; use raui_core::{ PropsData, Scalar, make_widget, widget::{ component::{ WidgetAlpha, WidgetComponent, interactive::input_field::{ TextInputProps, TextInputState, input_field, input_text_with_cursor, }, }, context::WidgetContext, node::WidgetNode, unit::{ content::ContentBoxItemLayout, text::{TextBoxHorizontalAlign, TextBoxSizeValue, TextBoxVerticalAlign}, }, utils::{Color, Rect, Transform}, }, }; use serde::{Deserialize, Serialize}; #[derive(PropsData, Debug, Clone, Serialize, Deserialize)] #[props_data(raui_core::props::PropsData)] #[prefab(raui_core::Prefab)] pub struct TextFieldPaperProps { #[serde(default)] pub hint: String, #[serde(default)] pub width: TextBoxSizeValue, #[serde(default)] pub height: TextBoxSizeValue, #[serde(default)] pub variant: String, #[serde(default)] pub use_main_color: bool, #[serde(default = "TextFieldPaperProps::default_inactive_alpha")] pub inactive_alpha: Scalar, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] pub horizontal_align_override: Option, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] pub vertical_align_override: Option, #[serde(skip_serializing_if = "Option::is_none")] pub color_override: Option, #[serde(default)] pub transform: Transform, #[serde(default)] pub paper_theme: ThemedWidgetProps, #[serde(default = "TextFieldPaperProps::default_padding")] pub padding: Rect, #[serde(default)] pub password: Option, #[serde(default = "TextFieldPaperProps::default_cursor")] pub cursor: Option, } impl TextFieldPaperProps { fn default_inactive_alpha() -> Scalar { 0.75 } fn default_cursor() -> Option { Some('|') } fn default_padding() -> Rect { 4.0.into() } } impl Default for TextFieldPaperProps { fn default() -> Self { Self { hint: Default::default(), width: Default::default(), height: Default::default(), variant: Default::default(), use_main_color: Default::default(), inactive_alpha: Self::default_inactive_alpha(), horizontal_align_override: Default::default(), vertical_align_override: Default::default(), color_override: Default::default(), transform: Default::default(), paper_theme: Default::default(), padding: Self::default_padding(), password: Default::default(), cursor: Self::default_cursor(), } } } fn text_field_paper_content(context: WidgetContext) -> WidgetNode { let WidgetContext { key, props, .. } = context; let TextFieldPaperProps { hint, width, height, variant, use_main_color, inactive_alpha, horizontal_align_override, vertical_align_override, color_override, transform, paper_theme, padding, password, cursor, } = props.read_cloned_or_default(); let TextInputState { cursor_position, focused, } = props.read_cloned_or_default(); let text = props .read::() .ok() .and_then(|props| props.text.as_ref()) .map(|text| text.get()) .unwrap_or_default(); let text = if let Some(c) = password { std::iter::repeat_n(c, text.chars().count()).collect() } else { text }; let text = if focused { if let Some(cursor) = cursor { input_text_with_cursor(&text, cursor_position, cursor) } else { text } } else if text.is_empty() { hint } else { text }; let paper_variant = props.map_or_default::(|p| p.variant.clone()); let paper_props = props .clone() .with(PaperProps { variant: paper_variant, ..Default::default() }) .with(paper_theme); let text_props = props .clone() .with(TextPaperProps { text, width, height, variant, use_main_color, horizontal_align_override, vertical_align_override, color_override, transform, }) .with(ContentBoxItemLayout { margin: padding, ..Default::default() }); let alpha = if focused { 1.0 } else { inactive_alpha }; make_widget!(paper) .key(key) .merge_props(paper_props) .listed_slot( make_widget!(text_paper) .key("text") .merge_props(text_props) .with_shared_props(WidgetAlpha(alpha)), ) .into() } pub fn text_field_paper(context: WidgetContext) -> WidgetNode { text_field_paper_impl(make_widget!(input_field), context) } pub fn text_field_paper_impl(component: WidgetComponent, context: WidgetContext) -> WidgetNode { let WidgetContext { idref, key, props, .. } = context; component .key(key) .maybe_idref(idref.cloned()) .merge_props(props.clone()) .named_slot( "content", make_widget!(text_field_paper_content) .key("text") .merge_props(props.clone()), ) .into() } ================================================ FILE: crates/material/src/component/mod.rs ================================================ pub mod containers; pub mod icon_paper; pub mod interactive; pub mod switch_paper; pub mod text_paper; ================================================ FILE: crates/material/src/component/switch_paper.rs ================================================ use crate::theme::{ThemeColor, ThemeProps, ThemedImageMaterial, ThemedWidgetProps}; use raui_core::{ PropsData, Scalar, make_widget, widget::{ component::image_box::{ImageBoxProps, image_box}, context::WidgetContext, node::WidgetNode, unit::image::{ImageBoxColor, ImageBoxImageScaling, ImageBoxMaterial, ImageBoxSizeValue}, }, }; use serde::{Deserialize, Serialize}; #[derive(PropsData, Debug, Default, Clone, Serialize, Deserialize)] #[props_data(raui_core::props::PropsData)] #[prefab(raui_core::Prefab)] pub struct SwitchPaperProps { #[serde(default)] pub on: bool, #[serde(default)] pub variant: String, #[serde(default)] pub size_level: usize, } pub fn switch_paper(context: WidgetContext) -> WidgetNode { let WidgetContext { idref, key, props, shared_props, .. } = context; let SwitchPaperProps { on, variant, size_level, } = props.read_cloned_or_default(); let themed_props = props.read_cloned_or_default::(); let color = match shared_props.read::() { Ok(props) => match themed_props.color { ThemeColor::Default => props.active_colors.main.default.main, ThemeColor::Primary => props.active_colors.main.primary.main, ThemeColor::Secondary => props.active_colors.main.secondary.main, }, Err(_) => Default::default(), }; let (size, material) = match shared_props.read::() { Ok(props) => { let size = props .icons_level_sizes .get(size_level) .copied() .unwrap_or(24.0); let material = if let Some(material) = props.switch_variants.get(&variant) { if on { material.on.clone() } else { material.off.clone() } } else { Default::default() }; (size, material) } Err(_) => (24.0, Default::default()), }; let image = match material { ThemedImageMaterial::Color => ImageBoxProps { material: ImageBoxMaterial::Color(ImageBoxColor { color, scaling: if on { ImageBoxImageScaling::Stretch } else { ImageBoxImageScaling::Frame((size_level as Scalar, true).into()) }, }), width: ImageBoxSizeValue::Exact(size), height: ImageBoxSizeValue::Exact(size), ..Default::default() }, ThemedImageMaterial::Image(mut data) => { data.tint = color; ImageBoxProps { material: ImageBoxMaterial::Image(data), width: ImageBoxSizeValue::Exact(size), height: ImageBoxSizeValue::Exact(size), ..Default::default() } } ThemedImageMaterial::Procedural(data) => ImageBoxProps { material: ImageBoxMaterial::Procedural(data), width: ImageBoxSizeValue::Exact(size), height: ImageBoxSizeValue::Exact(size), ..Default::default() }, }; make_widget!(image_box) .key(key) .maybe_idref(idref.cloned()) .with_props(image) .into() } ================================================ FILE: crates/material/src/component/text_paper.rs ================================================ use crate::theme::{ThemeColor, ThemeProps, ThemedTextMaterial, ThemedWidgetProps}; use raui_core::{ PropsData, make_widget, widget::{ component::text_box::{TextBoxProps, text_box}, context::WidgetContext, node::WidgetNode, unit::text::{TextBoxHorizontalAlign, TextBoxSizeValue, TextBoxVerticalAlign}, utils::{Color, Transform}, }, }; use serde::{Deserialize, Serialize}; #[derive(PropsData, Debug, Default, Clone, Serialize, Deserialize)] #[props_data(raui_core::props::PropsData)] #[prefab(raui_core::Prefab)] pub struct TextPaperProps { #[serde(default)] pub text: String, #[serde(default)] pub width: TextBoxSizeValue, #[serde(default)] pub height: TextBoxSizeValue, #[serde(default)] pub variant: String, #[serde(default)] pub use_main_color: bool, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] pub horizontal_align_override: Option, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] pub vertical_align_override: Option, #[serde(skip_serializing_if = "Option::is_none")] pub color_override: Option, #[serde(default)] pub transform: Transform, } pub fn text_paper(context: WidgetContext) -> WidgetNode { let WidgetContext { idref, key, props, shared_props, .. } = context; let TextPaperProps { text, width, height, variant, use_main_color, horizontal_align_override, vertical_align_override, color_override, transform, } = props.read_cloned_or_default(); let themed_props = props.read_cloned_or_default::(); let ThemedTextMaterial { mut horizontal_align, mut vertical_align, direction, font, } = match shared_props.read::() { Ok(props) => props .text_variants .get(&variant) .cloned() .unwrap_or_default(), Err(_) => Default::default(), }; if let Some(horizontal_override) = horizontal_align_override { horizontal_align = horizontal_override; } if let Some(alignment_override) = vertical_align_override { vertical_align = alignment_override; } let color = if let Some(color_override) = color_override { color_override } else { match shared_props.read::() { Ok(props) => { if use_main_color { match themed_props.color { ThemeColor::Default => props.active_colors.main.default.main, ThemeColor::Primary => props.active_colors.main.primary.main, ThemeColor::Secondary => props.active_colors.main.secondary.main, } } else { match themed_props.color { ThemeColor::Default => props.active_colors.contrast.default.main, ThemeColor::Primary => props.active_colors.contrast.primary.main, ThemeColor::Secondary => props.active_colors.contrast.secondary.main, } } } Err(_) => Default::default(), } }; let props = TextBoxProps { text, width, height, horizontal_align, vertical_align, direction, font, color, transform, }; make_widget!(text_box) .key(key) .maybe_idref(idref.cloned()) .with_props(props) .into() } ================================================ FILE: crates/material/src/lib.rs ================================================ //! Theme-able RAUI components pub mod component; pub mod theme; use raui_core::{application::Application, widget::FnWidget}; pub fn setup(app: &mut Application) { app.register_props::( "ContextPaperProps", ); app.register_props::("ModalPaperProps"); app.register_props::("PaperProps"); app.register_props::( "PaperContentLayoutProps", ); app.register_props::( "TooltipPaperProps", ); app.register_props::( "SideScrollbarsPaperProps", ); app.register_props::("WindowPaperProps"); app.register_props::("IconPaperProps"); app.register_props::( "ButtonPaperOverrideStyle", ); app.register_props::( "SliderPaperProps", ); app.register_props::( "NumericSliderPaperProps", ); app.register_props::( "TextFieldPaperProps", ); app.register_props::("SwitchPaperProps"); app.register_props::("TextPaperProps"); app.register_props::("ThemedWidgetProps"); app.register_props::("ThemeProps"); app.register_component( "context_paper", FnWidget::pointer(component::containers::context_paper::context_paper), ); app.register_component( "nav_flex_paper", FnWidget::pointer(component::containers::flex_paper::nav_flex_paper), ); app.register_component( "flex_paper", FnWidget::pointer(component::containers::flex_paper::flex_paper), ); app.register_component( "nav_grid_paper", FnWidget::pointer(component::containers::grid_paper::nav_grid_paper), ); app.register_component( "grid_paper", FnWidget::pointer(component::containers::grid_paper::grid_paper), ); app.register_component( "nav_horizontal_paper", FnWidget::pointer(component::containers::horizontal_paper::nav_horizontal_paper), ); app.register_component( "horizontal_paper", FnWidget::pointer(component::containers::horizontal_paper::horizontal_paper), ); app.register_component( "modal_paper", FnWidget::pointer(component::containers::modal_paper::modal_paper), ); app.register_component( "paper", FnWidget::pointer(component::containers::paper::paper), ); app.register_component( "scroll_paper", FnWidget::pointer(component::containers::scroll_paper::scroll_paper), ); app.register_component( "scroll_paper_side_scrollbars", FnWidget::pointer(component::containers::scroll_paper::scroll_paper_side_scrollbars), ); app.register_component( "text_tooltip_paper", FnWidget::pointer(component::containers::text_tooltip_paper::text_tooltip_paper), ); app.register_component( "tooltip_paper", FnWidget::pointer(component::containers::tooltip_paper::tooltip_paper), ); app.register_component( "nav_vertical_paper", FnWidget::pointer(component::containers::vertical_paper::nav_vertical_paper), ); app.register_component( "vertical_paper", FnWidget::pointer(component::containers::vertical_paper::vertical_paper), ); app.register_component( "window_paper", FnWidget::pointer(component::containers::window_paper::window_paper), ); app.register_component( "window_title_controls_paper", FnWidget::pointer(component::containers::window_paper::window_title_controls_paper), ); app.register_component( "wrap_paper", FnWidget::pointer(component::containers::wrap_paper::wrap_paper), ); app.register_component( "icon_paper", FnWidget::pointer(component::icon_paper::icon_paper), ); app.register_component( "button_paper", FnWidget::pointer(component::interactive::button_paper::button_paper), ); app.register_component( "icon_button_paper", FnWidget::pointer(component::interactive::icon_button_paper::icon_button_paper), ); app.register_component( "slider_paper", FnWidget::pointer(component::interactive::slider_paper::slider_paper), ); app.register_component( "numeric_slider_paper", FnWidget::pointer(component::interactive::slider_paper::numeric_slider_paper), ); app.register_component( "switch_button_paper", FnWidget::pointer(component::interactive::switch_button_paper::switch_button_paper), ); app.register_component( "text_button_paper", FnWidget::pointer(component::interactive::text_button_paper::text_button_paper), ); app.register_component( "text_field_paper", FnWidget::pointer(component::interactive::text_field_paper::text_field_paper), ); app.register_component( "switch_paper", FnWidget::pointer(component::switch_paper::switch_paper), ); app.register_component( "text_paper", FnWidget::pointer(component::text_paper::text_paper), ); } ================================================ FILE: crates/material/src/theme.rs ================================================ use raui_core::{ widget::{ unit::{ image::{ImageBoxImage, ImageBoxProcedural}, text::{TextBoxDirection, TextBoxFont, TextBoxHorizontalAlign, TextBoxVerticalAlign}, }, utils::{Color, lerp_clamped}, }, {PropsData, Scalar}, }; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::f32::consts::PI; const DEFAULT_BACKGROUND_MIXING_FACTOR: Scalar = 0.1; const DEFAULT_VARIANT_MIXING_FACTOR: Scalar = 0.2; #[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum ThemeColor { #[default] Default, Primary, Secondary, } #[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum ThemeColorVariant { #[default] Main, Light, Dark, } #[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum ThemeVariant { ContentOnly, #[default] Filled, Outline, } #[derive(PropsData, Debug, Default, Clone, Serialize, Deserialize)] #[props_data(raui_core::props::PropsData)] #[prefab(raui_core::Prefab)] pub struct ThemedWidgetProps { #[serde(default)] pub color: ThemeColor, #[serde(default)] pub color_variant: ThemeColorVariant, #[serde(default)] pub variant: ThemeVariant, } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct ThemeColorSet { #[serde(default)] pub main: Color, #[serde(default)] pub light: Color, #[serde(default)] pub dark: Color, } impl ThemeColorSet { pub fn uniform(color: Color) -> Self { Self { main: color, light: color, dark: color, } } pub fn get(&self, variant: ThemeColorVariant) -> Color { match variant { ThemeColorVariant::Main => self.main, ThemeColorVariant::Light => self.light, ThemeColorVariant::Dark => self.dark, } } pub fn get_themed(&self, themed: &ThemedWidgetProps) -> Color { self.get(themed.color_variant) } } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct ThemeColors { #[serde(default)] pub default: ThemeColorSet, #[serde(default)] pub primary: ThemeColorSet, #[serde(default)] pub secondary: ThemeColorSet, } impl ThemeColors { pub fn uniform(set: ThemeColorSet) -> Self { Self { default: set.to_owned(), primary: set.to_owned(), secondary: set, } } pub fn get(&self, color: ThemeColor, variant: ThemeColorVariant) -> Color { match color { ThemeColor::Default => self.default.get(variant), ThemeColor::Primary => self.primary.get(variant), ThemeColor::Secondary => self.secondary.get(variant), } } pub fn get_themed(&self, themed: &ThemedWidgetProps) -> Color { self.get(themed.color, themed.color_variant) } } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct ThemeColorsBundle { #[serde(default)] pub main: ThemeColors, #[serde(default)] pub contrast: ThemeColors, } impl ThemeColorsBundle { pub fn uniform(colors: ThemeColors) -> Self { Self { main: colors.to_owned(), contrast: colors, } } pub fn get(&self, use_main: bool, color: ThemeColor, variant: ThemeColorVariant) -> Color { if use_main { self.main.get(color, variant) } else { self.contrast.get(color, variant) } } pub fn get_themed(&self, use_main: bool, themed: &ThemedWidgetProps) -> Color { self.get(use_main, themed.color, themed.color_variant) } } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub enum ThemedImageMaterial { #[default] Color, Image(ImageBoxImage), Procedural(ImageBoxProcedural), } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct ThemedTextMaterial { #[serde(default)] pub horizontal_align: TextBoxHorizontalAlign, #[serde(default)] pub vertical_align: TextBoxVerticalAlign, #[serde(default)] pub direction: TextBoxDirection, #[serde(default)] pub font: TextBoxFont, } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct ThemedButtonMaterial { #[serde(default)] pub default: ThemedImageMaterial, #[serde(default)] pub selected: ThemedImageMaterial, #[serde(default)] pub trigger: ThemedImageMaterial, } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct ThemedSwitchMaterial { #[serde(default)] pub on: ThemedImageMaterial, #[serde(default)] pub off: ThemedImageMaterial, } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct ThemedSliderMaterial { #[serde(default)] pub background: ThemedImageMaterial, #[serde(default)] pub filling: ThemedImageMaterial, } #[derive(PropsData, Debug, Default, Clone, Serialize, Deserialize)] #[props_data(raui_core::props::PropsData)] #[prefab(raui_core::Prefab)] pub struct ThemeProps { #[serde(default)] pub active_colors: ThemeColorsBundle, #[serde(default)] pub background_colors: ThemeColorsBundle, #[serde(default)] #[serde(skip_serializing_if = "HashMap::is_empty")] pub content_backgrounds: HashMap, #[serde(default)] #[serde(skip_serializing_if = "HashMap::is_empty")] pub button_backgrounds: HashMap, #[serde(default)] #[serde(skip_serializing_if = "Vec::is_empty")] pub icons_level_sizes: Vec, #[serde(default)] #[serde(skip_serializing_if = "HashMap::is_empty")] pub text_variants: HashMap, #[serde(default)] #[serde(skip_serializing_if = "HashMap::is_empty")] pub switch_variants: HashMap, #[serde(default)] #[serde(skip_serializing_if = "HashMap::is_empty")] pub slider_variants: HashMap, #[serde(default)] #[serde(skip_serializing_if = "HashMap::is_empty")] pub modal_shadow_variants: HashMap, } impl ThemeProps { pub fn active_colors(mut self, bundle: ThemeColorsBundle) -> Self { self.active_colors = bundle; self } pub fn background_colors(mut self, bundle: ThemeColorsBundle) -> Self { self.background_colors = bundle; self } pub fn content_background(mut self, id: impl ToString, material: ThemedImageMaterial) -> Self { self.content_backgrounds.insert(id.to_string(), material); self } pub fn button_background(mut self, id: impl ToString, material: ThemedButtonMaterial) -> Self { self.button_backgrounds.insert(id.to_string(), material); self } pub fn icons_level_size(mut self, level: usize, size: Scalar) -> Self { self.icons_level_sizes.insert(level, size); self } pub fn text_variant(mut self, id: impl ToString, material: ThemedTextMaterial) -> Self { self.text_variants.insert(id.to_string(), material); self } pub fn switch_variant(mut self, id: impl ToString, material: ThemedSwitchMaterial) -> Self { self.switch_variants.insert(id.to_string(), material); self } pub fn slider_variant(mut self, id: impl ToString, material: ThemedSliderMaterial) -> Self { self.slider_variants.insert(id.to_string(), material); self } pub fn modal_shadow_variant(mut self, id: impl ToString, color: Color) -> Self { self.modal_shadow_variants.insert(id.to_string(), color); self } } pub fn new_light_theme() -> ThemeProps { new_light_theme_parameterized( DEFAULT_BACKGROUND_MIXING_FACTOR, DEFAULT_VARIANT_MIXING_FACTOR, ) } pub fn new_light_theme_parameterized( background_mixing_factor: Scalar, variant_mixing_factor: Scalar, ) -> ThemeProps { new_default_theme_parameterized( color_from_rgba(241, 250, 238, 1.0), color_from_rgba(29, 53, 87, 1.0), color_from_rgba(230, 57, 70, 1.0), color_from_rgba(255, 255, 255, 1.0), background_mixing_factor, variant_mixing_factor, ) } pub fn new_dark_theme() -> ThemeProps { new_dark_theme_parameterized( DEFAULT_BACKGROUND_MIXING_FACTOR, DEFAULT_VARIANT_MIXING_FACTOR, ) } pub fn new_dark_theme_parameterized( background_mixing_factor: Scalar, variant_mixing_factor: Scalar, ) -> ThemeProps { new_default_theme_parameterized( color_from_rgba(64, 64, 64, 1.0), color_from_rgba(255, 98, 86, 1.0), color_from_rgba(0, 196, 228, 1.0), color_from_rgba(32, 32, 32, 1.0), background_mixing_factor, variant_mixing_factor, ) } pub fn new_all_white_theme() -> ThemeProps { new_default_theme( color_from_rgba(255, 255, 255, 1.0), color_from_rgba(255, 255, 255, 1.0), color_from_rgba(255, 255, 255, 1.0), color_from_rgba(255, 255, 255, 1.0), ) } pub fn new_default_theme( default: Color, primary: Color, secondary: Color, background: Color, ) -> ThemeProps { new_default_theme_parameterized( default, primary, secondary, background, DEFAULT_BACKGROUND_MIXING_FACTOR, DEFAULT_VARIANT_MIXING_FACTOR, ) } pub fn new_default_theme_parameterized( default: Color, primary: Color, secondary: Color, background: Color, background_mixing_factor: Scalar, variant_mixing_factor: Scalar, ) -> ThemeProps { let background_primary = color_lerp(background, primary, background_mixing_factor); let background_secondary = color_lerp(background, secondary, background_mixing_factor); let mut background_modal = fluid_polarize_color(background); background_modal.a = 0.75; let mut content_backgrounds = HashMap::with_capacity(1); content_backgrounds.insert(String::new(), Default::default()); let mut button_backgrounds = HashMap::with_capacity(1); button_backgrounds.insert(String::new(), Default::default()); let mut text_variants = HashMap::with_capacity(1); text_variants.insert( String::new(), ThemedTextMaterial { font: TextBoxFont { size: 18.0, ..Default::default() }, ..Default::default() }, ); let mut switch_variants = HashMap::with_capacity(4); switch_variants.insert(String::new(), ThemedSwitchMaterial::default()); switch_variants.insert("checkbox".to_owned(), ThemedSwitchMaterial::default()); switch_variants.insert("toggle".to_owned(), ThemedSwitchMaterial::default()); switch_variants.insert("radio".to_owned(), ThemedSwitchMaterial::default()); let mut slider_variants = HashMap::with_capacity(1); let mut modal_shadow_variants = HashMap::with_capacity(1); slider_variants.insert(String::default(), ThemedSliderMaterial::default()); modal_shadow_variants.insert(String::new(), background_modal); ThemeProps { active_colors: make_colors_bundle( make_color_set(default, variant_mixing_factor, variant_mixing_factor), make_color_set(primary, variant_mixing_factor, variant_mixing_factor), make_color_set(secondary, variant_mixing_factor, variant_mixing_factor), ), background_colors: make_colors_bundle( make_color_set(background, variant_mixing_factor, variant_mixing_factor), make_color_set( background_primary, variant_mixing_factor, variant_mixing_factor, ), make_color_set( background_secondary, variant_mixing_factor, variant_mixing_factor, ), ), content_backgrounds, button_backgrounds, icons_level_sizes: vec![18.0, 24.0, 32.0, 48.0, 64.0, 128.0, 256.0, 512.0, 1024.0], text_variants, switch_variants, slider_variants, modal_shadow_variants, } } pub fn color_from_rgba(r: u8, g: u8, b: u8, a: Scalar) -> Color { Color { r: r as Scalar / 255.0, g: g as Scalar / 255.0, b: b as Scalar / 255.0, a, } } pub fn make_colors_bundle( default: ThemeColorSet, primary: ThemeColorSet, secondary: ThemeColorSet, ) -> ThemeColorsBundle { let contrast = ThemeColors { default: ThemeColorSet { main: contrast_color(default.main), light: contrast_color(default.light), dark: contrast_color(default.dark), }, primary: ThemeColorSet { main: contrast_color(primary.main), light: contrast_color(primary.light), dark: contrast_color(primary.dark), }, secondary: ThemeColorSet { main: contrast_color(secondary.main), light: contrast_color(secondary.light), dark: contrast_color(secondary.dark), }, }; let main = ThemeColors { default, primary, secondary, }; ThemeColorsBundle { main, contrast } } pub fn contrast_color(base_color: Color) -> Color { Color { r: 1.0 - base_color.r, g: 1.0 - base_color.g, b: 1.0 - base_color.b, a: base_color.a, } } pub fn fluid_polarize(v: Scalar) -> Scalar { (v - 0.5 * PI).sin() * 0.5 + 0.5 } pub fn fluid_polarize_color(color: Color) -> Color { Color { r: fluid_polarize(color.r), g: fluid_polarize(color.g), b: fluid_polarize(color.b), a: color.a, } } pub fn make_color_set(base_color: Color, lighter: Scalar, darker: Scalar) -> ThemeColorSet { let main = base_color; let light = Color { r: lerp_clamped(main.r, 1.0, lighter), g: lerp_clamped(main.g, 1.0, lighter), b: lerp_clamped(main.b, 1.0, lighter), a: main.a, }; let dark = Color { r: lerp_clamped(main.r, 0.0, darker), g: lerp_clamped(main.g, 0.0, darker), b: lerp_clamped(main.b, 0.0, darker), a: main.a, }; ThemeColorSet { main, light, dark } } pub fn color_lerp(from: Color, to: Color, factor: Scalar) -> Color { Color { r: lerp_clamped(from.r, to.r, factor), g: lerp_clamped(from.g, to.g, factor), b: lerp_clamped(from.b, to.b, factor), a: lerp_clamped(from.a, to.a, factor), } } ================================================ FILE: crates/retained/Cargo.toml ================================================ [package] name = "raui-retained" version = "0.70.17" authors = ["Patryk 'PsichiX' Budzynski "] edition = "2024" description = "RAUI retained mode UI layer" 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"] [dependencies] raui-core = { path = "../core", version = "0.70" } ================================================ FILE: crates/retained/src/lib.rs ================================================ use raui_core::{ Lifetime, LifetimeLazy, Managed, ValueReadAccess, ValueWriteAccess, application::ChangeNotifier, widget::{FnWidget, WidgetRef, component::WidgetComponent, context::*, node::WidgetNode}, }; use std::{ any::Any, marker::PhantomData, ops::{Deref, DerefMut}, ptr::NonNull, }; #[allow(unused_variables)] pub trait ViewState: Any + Send + Sync { fn on_mount(&mut self, context: WidgetMountOrChangeContext) {} fn on_unmount(&mut self, context: WidgetUnmountContext) {} fn on_change(&mut self, context: WidgetMountOrChangeContext) {} fn on_render(&self, context: WidgetContext) -> WidgetNode; fn as_any(&self) -> &dyn Any; fn as_any_mut(&mut self) -> &mut dyn Any; } pub struct View { inner: Box, lifetime: Box, _phantom: PhantomData T>, } impl Default for View { fn default() -> Self { Self::new(T::default()) } } impl View { pub fn new(state: T) -> Self where T: 'static, { Self { inner: Box::new(state), lifetime: Default::default(), _phantom: Default::default(), } } pub fn into_inner(self) -> Box { self.inner } pub fn as_dyn(&'_ self) -> Option> { self.lifetime.read(&*self.inner) } pub fn as_dyn_mut(&'_ mut self) -> Option> { self.lifetime.write(&mut *self.inner) } pub fn read(&'_ self) -> Option> { self.lifetime.read(self.inner.as_any().downcast_ref::()?) } pub fn write(&'_ mut self) -> Option> { self.lifetime .write(self.inner.as_any_mut().downcast_mut::()?) } pub fn lazy(&self) -> LazyView { unsafe { let ptr = self.inner.as_any().downcast_ref::().unwrap() as *const T as *mut T; LazyView { inner: NonNull::new_unchecked(ptr), lifetime: self.lifetime.lazy(), } } } pub fn component(&self) -> WidgetComponent { WidgetComponent::new(self.widget(), std::any::type_name::()) } pub fn widget(&self) -> FnWidget { let this = self.lazy(); FnWidget::closure(move |context| { let this_mount = this.clone(); let this_unmount = this.clone(); let this_change = this.clone(); context.life_cycle.mount(move |context| { if let Some(mut this) = this_mount.write() { this.on_mount(context); } }); context.life_cycle.unmount(move |context| { if let Some(mut this) = this_unmount.write() { this.on_unmount(context); } }); context.life_cycle.change(move |context| { if let Some(mut this) = this_change.write() { this.on_change(context); } }); this.write() .map(|this| this.on_render(context)) .unwrap_or_default() }) } } pub struct LazyView { inner: NonNull, lifetime: LifetimeLazy, } unsafe impl Send for LazyView {} unsafe impl Sync for LazyView {} impl Clone for LazyView { fn clone(&self) -> Self { Self { inner: self.inner, lifetime: self.lifetime.clone(), } } } impl LazyView { pub fn as_dyn(&'_ self) -> Option> { unsafe { self.lifetime.read(self.inner.as_ref()) } } pub fn as_dyn_mut(&'_ self) -> Option> { unsafe { self.lifetime.write(self.inner.as_ptr().as_mut()?) } } pub fn read(&'_ self) -> Option> { unsafe { self.lifetime.read(self.inner.as_ref()) } } pub fn write(&'_ self) -> Option> { unsafe { self.lifetime.write(self.inner.as_ptr().as_mut()?) } } } pub struct ViewValue { inner: T, notifier: Option, id: WidgetRef, } impl ViewValue { pub fn new(value: T) -> Self { Self { inner: value, notifier: None, id: Default::default(), } } pub fn with_notifier(mut self, notifier: ChangeNotifier) -> Self { self.bind_notifier(notifier); self } pub fn bind_notifier(&mut self, notifier: ChangeNotifier) { if self.notifier.is_none() && let Some(id) = self.id.read() { notifier.notify(id); } self.notifier = Some(notifier); } pub fn unbind_notifier(&mut self) { self.notifier = None; } pub fn bound_notifier(&self) -> Option<&ChangeNotifier> { self.notifier.as_ref() } pub fn widget_ref(&self) -> WidgetRef { self.id.clone() } } impl Deref for ViewValue { type Target = T; fn deref(&self) -> &Self::Target { &self.inner } } impl DerefMut for ViewValue { fn deref_mut(&mut self) -> &mut Self::Target { if let Some(notifier) = self.notifier.as_ref() && let Some(id) = self.id.read() { notifier.notify(id); } &mut self.inner } } pub struct SharedView { inner: Managed>>, } impl Default for SharedView { fn default() -> Self { Self { inner: Default::default(), } } } impl SharedView { pub fn new(view: View) -> Self { Self { inner: Managed::new(Some(view)), } } pub fn replace(&mut self, view: View) { if let Some(mut inner) = self.inner.write() { *inner = Some(view); } } pub fn clear(&mut self) { if let Some(mut inner) = self.inner.write() { *inner = None; } } pub fn read(&'_ self) -> Option>> { self.inner.read()?.remap(|inner| inner.as_ref()).ok() } pub fn write(&'_ mut self) -> Option>> { self.inner.write()?.remap(|inner| inner.as_mut()).ok() } } impl ViewState for SharedView { fn on_render(&self, context: WidgetContext) -> WidgetNode { self.inner .read() .and_then(|inner| { inner .as_ref() .map(|inner| inner.component().key(context.key).into()) }) .unwrap_or_default() } fn as_any(&self) -> &dyn Any { self } fn as_any_mut(&mut self) -> &mut dyn Any { self } } ================================================ FILE: crates/tesselate-renderer/Cargo.toml ================================================ [package] name = "raui-tesselate-renderer" version = "0.70.17" authors = ["Patryk 'PsichiX' Budzynski "] edition = "2024" description = "RAUI renderer that tesselates layout into vertex and index buffers" 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] index32 = [] [dependencies] raui-core = { path = "../core", version = "0.70" } serde = { version = "1", features = ["derive"] } vek = "0.17" spitfire-core = "0.36" spitfire-fontdue = "0.36" bytemuck = { version = "1", features = ["derive"] } fontdue = "0.9" ================================================ FILE: crates/tesselate-renderer/src/lib.rs ================================================ use bytemuck::Pod; use fontdue::{ Font, layout::{ CoordinateSystem, HorizontalAlign, Layout as TextLayout, LayoutSettings, TextStyle, VerticalAlign, }, }; use raui_core::{ Scalar, layout::{CoordsMapping, Layout}, renderer::Renderer, widget::{ WidgetId, unit::{ WidgetUnit, image::{ ImageBoxColor, ImageBoxImage, ImageBoxImageScaling, ImageBoxMaterial, ImageBoxProceduralMesh, }, text::{TextBoxHorizontalAlign, TextBoxVerticalAlign}, }, utils::{Color, Rect, Transform, Vec2, lerp}, }, }; use spitfire_core::{Triangle, VertexStream}; use spitfire_fontdue::{TextRenderer, TextVertex}; use std::collections::HashMap; #[derive(Debug, Clone)] pub enum Error { WidgetHasNoLayout(WidgetId), UnsupportedImageMaterial(Box), FontNotFound(String), ImageNotFound(String), } pub trait TesselateVertex: Pod { fn apply(&mut self, position: [f32; 2], tex_coord: [f32; 3], color: [f32; 4]); fn transform(&mut self, matrix: vek::Mat4); } #[derive(Debug, Clone, PartialEq)] pub enum TesselateBatch { Color, Image { id: String, }, Text, Procedural { id: String, images: Vec, parameters: HashMap, }, ClipPush { x: f32, y: f32, w: f32, h: f32, }, ClipPop, Debug, } pub trait TesselateResourceProvider { fn image_id_and_uv_and_size_by_atlas_id(&self, id: &str) -> Option<(String, Rect, Vec2)>; fn fonts(&self) -> &[Font]; fn font_index_by_id(&self, id: &str) -> Option; } pub trait TesselateBatchConverter { fn convert(&mut self, batch: TesselateBatch) -> Option; } impl TesselateBatchConverter for () { fn convert(&mut self, batch: TesselateBatch) -> Option { Some(batch) } } #[derive(Debug, Default, Clone, Copy)] pub struct TessselateRendererDebug { pub render_non_visual_nodes: bool, } pub struct TesselateRenderer<'a, V, B, P, C> where V: TesselateVertex + TextVertex + Default, B: PartialEq, P: TesselateResourceProvider, C: TesselateBatchConverter, { provider: &'a P, converter: &'a mut C, stream: &'a mut VertexStream, text_renderer: &'a mut TextRenderer, transform_stack: Vec>, debug: Option, } impl<'a, V, B, P, C> TesselateRenderer<'a, V, B, P, C> where V: TesselateVertex + TextVertex + Default, B: PartialEq, C: TesselateBatchConverter, P: TesselateResourceProvider, { pub fn new( provider: &'a P, converter: &'a mut C, stream: &'a mut VertexStream, text_renderer: &'a mut TextRenderer, debug: Option, ) -> Self { Self { provider, converter, stream, text_renderer, transform_stack: Default::default(), debug, } } fn push_transform(&mut self, transform: &Transform, rect: Rect) { let size = rect.size(); let offset = vek::Vec2::new(rect.left, rect.top); let offset = vek::Mat4::::translation_2d(offset); let pivot = vek::Vec2::new( lerp(0.0, size.x, transform.pivot.x), lerp(0.0, size.y, transform.pivot.y), ); let pivot = vek::Mat4::::translation_2d(pivot); let inv_pivot = pivot.inverted(); let align = vek::Vec2::new( lerp(0.0, size.x, transform.align.x), lerp(0.0, size.y, transform.align.y), ); let align = vek::Mat4::::translation_2d(align); let translate = vek::Mat4::::translation_2d(raui_to_vec2(transform.translation)); let rotate = vek::Mat4::::rotation_z(transform.rotation); let scale = vek::Mat4::::scaling_3d(raui_to_vec2(transform.scale).with_z(1.0)); let skew = vek::Mat4::::from(vek::Mat2::new( 1.0, transform.skew.y.tan(), transform.skew.x.tan(), 1.0, )); let matrix = offset * align * pivot * translate * rotate * scale * skew * inv_pivot; self.push_matrix(matrix); } fn push_transform_simple(&mut self, rect: Rect) { let offset = vek::Vec2::new(rect.left, rect.top); let offset = vek::Mat4::::translation_2d(offset); self.push_matrix(offset); } fn push_matrix(&mut self, matrix: vek::Mat4) { let matrix = self.top_transform() * matrix; self.transform_stack.push(matrix); } fn pop_transform(&mut self) { self.transform_stack.pop(); } fn top_transform(&self) -> vek::Mat4 { self.transform_stack.last().cloned().unwrap_or_default() } fn make_vertex(position: Vec2, tex_coord: Vec2, page: Scalar, color: Color) -> V { let mut result = V::default(); TesselateVertex::apply( &mut result, [position.x, position.y], [tex_coord.x, tex_coord.y, page], [color.r, color.g, color.b, color.a], ); result } fn make_tiled_triangle_first(offset: usize) -> Triangle { Triangle { a: 0, b: 1, c: 5 }.offset(offset) } fn make_tiled_triangle_second(offset: usize) -> Triangle { Triangle { a: 5, b: 4, c: 0 }.offset(offset) } fn produce_color_triangles(&mut self, size: Vec2, scale: Vec2, data: &ImageBoxColor) { let matrix = self.top_transform(); let tl = vec2_to_raui(matrix.mul_point(vek::Vec2::new(0.0, 0.0))); let tr = vec2_to_raui(matrix.mul_point(vek::Vec2::new(size.x, 0.0))); let br = vec2_to_raui(matrix.mul_point(vek::Vec2::new(size.x, size.y))); let bl = vec2_to_raui(matrix.mul_point(vek::Vec2::new(0.0, size.y))); let c = data.color; match &data.scaling { ImageBoxImageScaling::Stretch => { if let Some(batch) = self.converter.convert(TesselateBatch::Color) { self.stream.batch_optimized(batch); self.stream.quad([ Self::make_vertex(tl, Default::default(), 0.0, c), Self::make_vertex(tr, Default::default(), 0.0, c), Self::make_vertex(br, Default::default(), 0.0, c), Self::make_vertex(bl, Default::default(), 0.0, c), ]); } } ImageBoxImageScaling::Frame(frame) => { let mut d = frame.destination; d.left *= scale.x; d.right *= scale.x; d.top *= scale.y; d.bottom *= scale.y; if d.left + d.right > size.x { let m = d.left + d.right; d.left = size.x * d.left / m; d.right = size.x * d.right / m; } if d.top + d.bottom > size.y { let m = d.top + d.bottom; d.top = size.y * d.top / m; d.bottom = size.y * d.bottom / m; } let til = vec2_to_raui(matrix.mul_point(vek::Vec2::new(d.left, 0.0))); let tir = vec2_to_raui(matrix.mul_point(vek::Vec2::new(size.x - d.right, 0.0))); let itr = vec2_to_raui(matrix.mul_point(vek::Vec2::new(size.x, d.top))); let ibr = vec2_to_raui(matrix.mul_point(vek::Vec2::new(size.x, size.y - d.bottom))); let bir = vec2_to_raui(matrix.mul_point(vek::Vec2::new(size.x - d.right, size.y))); let bil = vec2_to_raui(matrix.mul_point(vek::Vec2::new(d.left, size.y))); let ibl = vec2_to_raui(matrix.mul_point(vek::Vec2::new(0.0, size.y - d.bottom))); let itl = vec2_to_raui(matrix.mul_point(vek::Vec2::new(0.0, d.top))); let itil = vec2_to_raui(matrix.mul_point(vek::Vec2::new(d.left, d.top))); let itir = vec2_to_raui(matrix.mul_point(vek::Vec2::new(size.x - d.right, d.top))); let ibir = vec2_to_raui( matrix.mul_point(vek::Vec2::new(size.x - d.right, size.y - d.bottom)), ); let ibil = vec2_to_raui(matrix.mul_point(vek::Vec2::new(d.left, size.y - d.bottom))); if let Some(batch) = self.converter.convert(TesselateBatch::Color) { self.stream.batch_optimized(batch); self.stream.extend( [ Self::make_vertex(tl, Default::default(), 0.0, c), Self::make_vertex(til, Default::default(), 0.0, c), Self::make_vertex(tir, Default::default(), 0.0, c), Self::make_vertex(tr, Default::default(), 0.0, c), Self::make_vertex(itl, Default::default(), 0.0, c), Self::make_vertex(itil, Default::default(), 0.0, c), Self::make_vertex(itir, Default::default(), 0.0, c), Self::make_vertex(itr, Default::default(), 0.0, c), Self::make_vertex(ibl, Default::default(), 0.0, c), Self::make_vertex(ibil, Default::default(), 0.0, c), Self::make_vertex(ibir, Default::default(), 0.0, c), Self::make_vertex(ibr, Default::default(), 0.0, c), Self::make_vertex(bl, Default::default(), 0.0, c), Self::make_vertex(bil, Default::default(), 0.0, c), Self::make_vertex(bir, Default::default(), 0.0, c), Self::make_vertex(br, Default::default(), 0.0, c), ], [ Self::make_tiled_triangle_first(0), Self::make_tiled_triangle_second(0), Self::make_tiled_triangle_first(1), Self::make_tiled_triangle_second(1), Self::make_tiled_triangle_first(2), Self::make_tiled_triangle_second(2), Self::make_tiled_triangle_first(4), Self::make_tiled_triangle_second(4), Self::make_tiled_triangle_first(6), Self::make_tiled_triangle_second(6), Self::make_tiled_triangle_first(8), Self::make_tiled_triangle_second(8), Self::make_tiled_triangle_first(9), Self::make_tiled_triangle_second(9), Self::make_tiled_triangle_first(10), Self::make_tiled_triangle_second(10), ] .into_iter() .chain((!frame.frame_only).then(|| Self::make_tiled_triangle_first(5))) .chain((!frame.frame_only).then(|| Self::make_tiled_triangle_second(5))), ); } } } } fn produce_image_triangles( &mut self, id: String, uvs: Rect, size: Vec2, rect: Rect, scale: Vec2, data: &ImageBoxImage, ) { let matrix = self.top_transform(); let tl = vec2_to_raui(matrix.mul_point(vek::Vec2::new(rect.left, rect.top))); let tr = vec2_to_raui(matrix.mul_point(vek::Vec2::new(rect.right, rect.top))); let br = vec2_to_raui(matrix.mul_point(vek::Vec2::new(rect.right, rect.bottom))); let bl = vec2_to_raui(matrix.mul_point(vek::Vec2::new(rect.left, rect.bottom))); let ctl = Vec2 { x: uvs.left, y: uvs.top, }; let ctr = Vec2 { x: uvs.right, y: uvs.top, }; let cbr = Vec2 { x: uvs.right, y: uvs.bottom, }; let cbl = Vec2 { x: uvs.left, y: uvs.bottom, }; let c = data.tint; match &data.scaling { ImageBoxImageScaling::Stretch => { if let Some(batch) = self.converter.convert(TesselateBatch::Image { id }) { self.stream.batch_optimized(batch); self.stream.quad([ Self::make_vertex(tl, ctl, 0.0, c), Self::make_vertex(tr, ctr, 0.0, c), Self::make_vertex(br, cbr, 0.0, c), Self::make_vertex(bl, cbl, 0.0, c), ]); } } ImageBoxImageScaling::Frame(frame) => { let inv_size = Vec2 { x: 1.0 / size.x, y: 1.0 / size.y, }; let mut d = frame.destination; d.left *= scale.x; d.right *= scale.x; d.top *= scale.y; d.bottom *= scale.y; if frame.frame_keep_aspect_ratio { d.left = (frame.source.left * rect.height()) / size.y; d.right = (frame.source.right * rect.height()) / size.y; d.top = (frame.source.top * rect.width()) / size.x; d.bottom = (frame.source.bottom * rect.width()) / size.x; } if d.left + d.right > rect.width() { let m = d.left + d.right; d.left = rect.width() * d.left / m; d.right = rect.width() * d.right / m; } if d.top + d.bottom > rect.height() { let m = d.top + d.bottom; d.top = rect.height() * d.top / m; d.bottom = rect.height() * d.bottom / m; } let til = vec2_to_raui(matrix.mul_point(vek::Vec2::new(rect.left + d.left, rect.top))); let tir = vec2_to_raui(matrix.mul_point(vek::Vec2::new(rect.right - d.right, rect.top))); let itr = vec2_to_raui(matrix.mul_point(vek::Vec2::new(rect.right, rect.top + d.top))); let ibr = vec2_to_raui( matrix.mul_point(vek::Vec2::new(rect.right, rect.bottom - d.bottom)), ); let bir = vec2_to_raui( matrix.mul_point(vek::Vec2::new(rect.right - d.right, rect.bottom)), ); let bil = vec2_to_raui(matrix.mul_point(vek::Vec2::new(rect.left + d.left, rect.bottom))); let ibl = vec2_to_raui( matrix.mul_point(vek::Vec2::new(rect.left, rect.bottom - d.bottom)), ); let itl = vec2_to_raui(matrix.mul_point(vek::Vec2::new(rect.left, rect.top + d.top))); let itil = vec2_to_raui( matrix.mul_point(vek::Vec2::new(rect.left + d.left, rect.top + d.top)), ); let itir = vec2_to_raui( matrix.mul_point(vek::Vec2::new(rect.right - d.right, rect.top + d.top)), ); let ibir = vec2_to_raui( matrix.mul_point(vek::Vec2::new(rect.right - d.right, rect.bottom - d.bottom)), ); let ibil = vec2_to_raui( matrix.mul_point(vek::Vec2::new(rect.left + d.left, rect.bottom - d.bottom)), ); let ctil = Vec2 { x: uvs.left + frame.source.left * inv_size.x, y: uvs.top, }; let ctir = Vec2 { x: uvs.right - frame.source.right * inv_size.x, y: uvs.top, }; let citr = Vec2 { x: uvs.right, y: uvs.top + frame.source.top * inv_size.y, }; let cibr = Vec2 { x: uvs.right, y: uvs.bottom - frame.source.bottom * inv_size.y, }; let cbir = Vec2 { x: uvs.right - frame.source.right * inv_size.x, y: uvs.bottom, }; let cbil = Vec2 { x: uvs.left + frame.source.left * inv_size.x, y: uvs.bottom, }; let cibl = Vec2 { x: uvs.left, y: uvs.bottom - frame.source.bottom * inv_size.y, }; let citl = Vec2 { x: uvs.left, y: uvs.top + frame.source.top * inv_size.y, }; let citil = Vec2 { x: uvs.left + frame.source.left * inv_size.x, y: uvs.top + frame.source.top * inv_size.y, }; let citir = Vec2 { x: uvs.right - frame.source.right * inv_size.x, y: uvs.top + frame.source.top * inv_size.y, }; let cibir = Vec2 { x: uvs.right - frame.source.right * inv_size.x, y: uvs.bottom - frame.source.bottom * inv_size.y, }; let cibil = Vec2 { x: uvs.left + frame.source.left * inv_size.x, y: uvs.bottom - frame.source.bottom * inv_size.y, }; if let Some(batch) = self.converter.convert(TesselateBatch::Image { id }) { self.stream.batch_optimized(batch); self.stream.extend( [ Self::make_vertex(tl, ctl, 0.0, c), Self::make_vertex(til, ctil, 0.0, c), Self::make_vertex(tir, ctir, 0.0, c), Self::make_vertex(tr, ctr, 0.0, c), Self::make_vertex(itl, citl, 0.0, c), Self::make_vertex(itil, citil, 0.0, c), Self::make_vertex(itir, citir, 0.0, c), Self::make_vertex(itr, citr, 0.0, c), Self::make_vertex(ibl, cibl, 0.0, c), Self::make_vertex(ibil, cibil, 0.0, c), Self::make_vertex(ibir, cibir, 0.0, c), Self::make_vertex(ibr, cibr, 0.0, c), Self::make_vertex(bl, cbl, 0.0, c), Self::make_vertex(bil, cbil, 0.0, c), Self::make_vertex(bir, cbir, 0.0, c), Self::make_vertex(br, cbr, 0.0, c), ], [ Self::make_tiled_triangle_first(0), Self::make_tiled_triangle_second(0), Self::make_tiled_triangle_first(1), Self::make_tiled_triangle_second(1), Self::make_tiled_triangle_first(2), Self::make_tiled_triangle_second(2), Self::make_tiled_triangle_first(4), Self::make_tiled_triangle_second(4), Self::make_tiled_triangle_first(6), Self::make_tiled_triangle_second(6), Self::make_tiled_triangle_first(8), Self::make_tiled_triangle_second(8), Self::make_tiled_triangle_first(9), Self::make_tiled_triangle_second(9), Self::make_tiled_triangle_first(10), Self::make_tiled_triangle_second(10), ] .into_iter() .chain((!frame.frame_only).then(|| Self::make_tiled_triangle_first(5))) .chain((!frame.frame_only).then(|| Self::make_tiled_triangle_second(5))), ); } } } } fn produce_debug_wireframe(&mut self, size: Vec2) { if let Some(batch) = self.converter.convert(TesselateBatch::Debug) { let rect = Rect { left: 0.0, right: size.x, top: 0.0, bottom: size.y, }; let matrix = self.top_transform(); let tl = vec2_to_raui(matrix.mul_point(vek::Vec2::new(rect.left, rect.top))); let tr = vec2_to_raui(matrix.mul_point(vek::Vec2::new(rect.right, rect.top))); let br = vec2_to_raui(matrix.mul_point(vek::Vec2::new(rect.right, rect.bottom))); let bl = vec2_to_raui(matrix.mul_point(vek::Vec2::new(rect.left, rect.bottom))); self.stream.batch_optimized(batch); self.stream.quad([ Self::make_vertex(tl, Default::default(), 0.0, Default::default()), Self::make_vertex(tr, Default::default(), 0.0, Default::default()), Self::make_vertex(br, Default::default(), 0.0, Default::default()), Self::make_vertex(bl, Default::default(), 0.0, Default::default()), ]); } } fn render_node( &mut self, unit: &WidgetUnit, mapping: &CoordsMapping, layout: &Layout, local: bool, ) -> Result<(), Error> { match unit { WidgetUnit::None | WidgetUnit::PortalBox(_) => Ok(()), WidgetUnit::AreaBox(unit) => { if let Some(item) = layout.items.get(&unit.id) { let local_space = mapping.virtual_to_real_rect(item.local_space, local); self.push_transform_simple(local_space); self.render_node(&unit.slot, mapping, layout, true)?; self.pop_transform(); Ok(()) } else { Err(Error::WidgetHasNoLayout(unit.id.to_owned())) } } WidgetUnit::ContentBox(unit) => { if let Some(item) = layout.items.get(&unit.id) { let mut items = unit .items .iter() .map(|item| (item.layout.depth, item)) .collect::>(); items.sort_unstable_by(|(a, _), (b, _)| a.partial_cmp(b).unwrap()); let local_space = mapping.virtual_to_real_rect(item.local_space, local); self.push_transform(&unit.transform, local_space); if unit.clipping { let size = local_space.size(); let matrix = self.top_transform(); let tl = matrix.mul_point(vek::Vec2::new(0.0, 0.0)); let tr = matrix.mul_point(vek::Vec2::new(size.x, 0.0)); let br = matrix.mul_point(vek::Vec2::new(size.x, size.y)); let bl = matrix.mul_point(vek::Vec2::new(0.0, size.y)); let x = tl.x.min(tr.x).min(br.x).min(bl.x).round(); let y = tl.y.min(tr.y).min(br.y).min(bl.y).round(); let x2 = tl.x.max(tr.x).max(br.x).max(bl.x).round(); let y2 = tl.y.max(tr.y).max(br.y).max(bl.y).round(); let w = x2 - x; let h = y2 - y; if let Some(batch) = self.converter .convert(TesselateBatch::ClipPush { x, y, w, h }) { self.stream.batch(batch); self.stream.batch_end(); } } for (_, item) in items { self.render_node(&item.slot, mapping, layout, true)?; } if unit.clipping && let Some(batch) = self.converter.convert(TesselateBatch::ClipPop) { self.stream.batch(batch); self.stream.batch_end(); } self.pop_transform(); Ok(()) } else { Err(Error::WidgetHasNoLayout(unit.id.to_owned())) } } WidgetUnit::FlexBox(unit) => { if let Some(item) = layout.items.get(&unit.id) { let local_space = mapping.virtual_to_real_rect(item.local_space, local); self.push_transform(&unit.transform, local_space); for item in &unit.items { self.render_node(&item.slot, mapping, layout, true)?; } self.pop_transform(); Ok(()) } else { Err(Error::WidgetHasNoLayout(unit.id.to_owned())) } } WidgetUnit::GridBox(unit) => { if let Some(item) = layout.items.get(&unit.id) { let local_space = mapping.virtual_to_real_rect(item.local_space, local); self.push_transform(&unit.transform, local_space); for item in &unit.items { self.render_node(&item.slot, mapping, layout, true)?; } self.pop_transform(); Ok(()) } else { Err(Error::WidgetHasNoLayout(unit.id.to_owned())) } } WidgetUnit::SizeBox(unit) => { if let Some(item) = layout.items.get(&unit.id) { let local_space = mapping.virtual_to_real_rect(item.local_space, local); self.push_transform(&unit.transform, local_space); self.render_node(&unit.slot, mapping, layout, true)?; self.pop_transform(); Ok(()) } else { Err(Error::WidgetHasNoLayout(unit.id.to_owned())) } } WidgetUnit::ImageBox(unit) => match &unit.material { ImageBoxMaterial::Color(color) => { if let Some(item) = layout.items.get(&unit.id) { let local_space = mapping.virtual_to_real_rect(item.local_space, local); self.push_transform(&unit.transform, local_space); self.produce_color_triangles(local_space.size(), mapping.scale(), color); self.pop_transform(); Ok(()) } else { Err(Error::WidgetHasNoLayout(unit.id.to_owned())) } } ImageBoxMaterial::Image(image) => { if let Some(item) = layout.items.get(&unit.id) { let local_space = mapping.virtual_to_real_rect(item.local_space, local); let rect = Rect { left: 0.0, right: local_space.width(), top: 0.0, bottom: local_space.height(), }; let (id, uvs, size) = match self .provider .image_id_and_uv_and_size_by_atlas_id(&image.id) { Some(result) => result, None => return Err(Error::ImageNotFound(image.id.to_owned())), }; let rect = if let Some(aspect) = unit.content_keep_aspect_ratio { let ox = rect.left; let oy = rect.top; let iw = rect.width(); let ih = rect.height(); let ra = size.x / size.y; let ia = iw / ih; let scale = if (ra >= ia) != aspect.outside { iw / size.x } else { ih / size.y }; let w = size.x * scale; let h = size.y * scale; let ow = lerp(0.0, iw - w, aspect.horizontal_alignment); let oh = lerp(0.0, ih - h, aspect.vertical_alignment); Rect { left: ox + ow, right: ox + ow + w, top: oy + oh, bottom: oy + oh + h, } } else { rect }; self.push_transform(&unit.transform, local_space); self.produce_image_triangles(id, uvs, size, rect, mapping.scale(), image); self.pop_transform(); Ok(()) } else { Err(Error::WidgetHasNoLayout(unit.id.to_owned())) } } ImageBoxMaterial::Procedural(procedural) => { if let Some(item) = layout.items.get(&unit.id) { let local_space = mapping.virtual_to_real_rect(item.local_space, local); self.push_transform(&unit.transform, local_space); if let Some(batch) = self.converter.convert(TesselateBatch::Procedural { id: procedural.id.to_owned(), images: procedural.images.to_owned(), parameters: procedural.parameters.to_owned(), }) { let image_mapping = CoordsMapping::new_scaling(local_space, procedural.vertex_mapping); self.stream.batch_optimized(batch); match &procedural.mesh { ImageBoxProceduralMesh::Owned(mesh) => { self.stream.extend( mesh.vertices.iter().map(|vertex| { Self::make_vertex( image_mapping .virtual_to_real_vec2(vertex.position, false), vertex.tex_coord, vertex.page, vertex.color, ) }), mesh.triangles.iter().map(|triangle| Triangle { a: triangle[0], b: triangle[1], c: triangle[2], }), ); } ImageBoxProceduralMesh::Shared(mesh) => { self.stream.extend( mesh.vertices.iter().map(|vertex| { Self::make_vertex( image_mapping .virtual_to_real_vec2(vertex.position, false), vertex.tex_coord, vertex.page, vertex.color, ) }), mesh.triangles.iter().map(|triangle| Triangle { a: triangle[0], b: triangle[1], c: triangle[2], }), ); } ImageBoxProceduralMesh::Generator(generator) => { let mesh = (generator)(local_space, &procedural.parameters); self.stream.extend( mesh.vertices.into_iter().map(|vertex| { Self::make_vertex( image_mapping .virtual_to_real_vec2(vertex.position, false), vertex.tex_coord, vertex.page, vertex.color, ) }), mesh.triangles.into_iter().map(|triangle| Triangle { a: triangle[0], b: triangle[1], c: triangle[2], }), ); } } } self.pop_transform(); Ok(()) } else { Err(Error::WidgetHasNoLayout(unit.id.to_owned())) } } }, WidgetUnit::TextBox(unit) => { let font_index = match self.provider.font_index_by_id(&unit.font.name) { Some(index) => index, None => return Err(Error::FontNotFound(unit.font.name.to_owned())), }; if let Some(item) = layout.items.get(&unit.id) { let local_space = mapping.virtual_to_real_rect(item.local_space, local); let size = local_space.size(); self.push_transform(&unit.transform, local_space); let matrix = self.top_transform(); if let Some(batch) = self.converter.convert(TesselateBatch::Text) { self.stream.batch_optimized(batch); self.stream.transformed( |stream| { let text = TextStyle::with_user_data( &unit.text, unit.font.size * mapping.scalar_scale(false), font_index, unit.color, ); let mut layout = TextLayout::new(CoordinateSystem::PositiveYDown); layout.reset(&LayoutSettings { max_width: Some(size.x), max_height: Some(size.y), horizontal_align: match unit.horizontal_align { TextBoxHorizontalAlign::Left => HorizontalAlign::Left, TextBoxHorizontalAlign::Center => HorizontalAlign::Center, TextBoxHorizontalAlign::Right => HorizontalAlign::Right, }, vertical_align: match unit.vertical_align { TextBoxVerticalAlign::Top => VerticalAlign::Top, TextBoxVerticalAlign::Middle => VerticalAlign::Middle, TextBoxVerticalAlign::Bottom => VerticalAlign::Bottom, }, ..Default::default() }); layout.append(self.provider.fonts(), &text); self.text_renderer.include(self.provider.fonts(), &layout); self.text_renderer.render_to_stream(stream); }, |vertex| { vertex.transform(matrix); }, ); } self.pop_transform(); Ok(()) } else { Err(Error::WidgetHasNoLayout(unit.id.to_owned())) } } } } fn debug_render_node( &mut self, unit: &WidgetUnit, mapping: &CoordsMapping, layout: &Layout, local: bool, debug: TessselateRendererDebug, ) { match unit { WidgetUnit::AreaBox(unit) => { if let Some(item) = layout.items.get(&unit.id) { let local_space = mapping.virtual_to_real_rect(item.local_space, local); self.push_transform_simple(local_space); self.debug_render_node(&unit.slot, mapping, layout, true, debug); if debug.render_non_visual_nodes { self.produce_debug_wireframe(local_space.size()); } self.pop_transform(); } } WidgetUnit::ContentBox(unit) => { if let Some(item) = layout.items.get(&unit.id) { let mut items = unit .items .iter() .map(|item| (item.layout.depth, item)) .collect::>(); items.sort_unstable_by(|(a, _), (b, _)| a.partial_cmp(b).unwrap()); let local_space = mapping.virtual_to_real_rect(item.local_space, local); self.push_transform(&unit.transform, local_space); for (_, item) in items { self.debug_render_node(&item.slot, mapping, layout, true, debug); } if debug.render_non_visual_nodes { self.produce_debug_wireframe(local_space.size()); } self.pop_transform(); } } WidgetUnit::FlexBox(unit) => { if let Some(item) = layout.items.get(&unit.id) { let local_space = mapping.virtual_to_real_rect(item.local_space, local); self.push_transform(&unit.transform, local_space); for item in &unit.items { self.debug_render_node(&item.slot, mapping, layout, true, debug); } if debug.render_non_visual_nodes { self.produce_debug_wireframe(local_space.size()); } self.pop_transform(); } } WidgetUnit::GridBox(unit) => { if let Some(item) = layout.items.get(&unit.id) { let local_space = mapping.virtual_to_real_rect(item.local_space, local); self.push_transform(&unit.transform, local_space); for item in &unit.items { self.debug_render_node(&item.slot, mapping, layout, true, debug); } if debug.render_non_visual_nodes { self.produce_debug_wireframe(local_space.size()); } self.pop_transform(); } } WidgetUnit::SizeBox(unit) => { if let Some(item) = layout.items.get(&unit.id) { let local_space = mapping.virtual_to_real_rect(item.local_space, local); self.push_transform(&unit.transform, local_space); self.debug_render_node(&unit.slot, mapping, layout, true, debug); if debug.render_non_visual_nodes { self.produce_debug_wireframe(local_space.size()); } self.pop_transform(); } } WidgetUnit::ImageBox(unit) => { if let Some(item) = layout.items.get(&unit.id) { let local_space = mapping.virtual_to_real_rect(item.local_space, local); self.push_transform(&unit.transform, local_space); self.produce_debug_wireframe(local_space.size()); self.pop_transform(); } } WidgetUnit::TextBox(unit) => { if let Some(item) = layout.items.get(&unit.id) { let local_space = mapping.virtual_to_real_rect(item.local_space, local); self.push_transform(&unit.transform, local_space); self.produce_debug_wireframe(local_space.size()); self.pop_transform(); } } _ => {} } } } impl Renderer<(), Error> for TesselateRenderer<'_, V, B, P, C> where V: TesselateVertex + TextVertex + Default, B: PartialEq, C: TesselateBatchConverter, P: TesselateResourceProvider, { fn render( &mut self, tree: &WidgetUnit, mapping: &CoordsMapping, layout: &Layout, ) -> Result<(), Error> { self.transform_stack.clear(); self.render_node(tree, mapping, layout, false)?; self.stream.batch_end(); if let Some(debug) = self.debug { self.transform_stack.clear(); self.debug_render_node(tree, mapping, layout, false, debug); self.stream.batch_end(); } Ok(()) } } fn raui_to_vec2(v: Vec2) -> vek::Vec2 { vek::Vec2::new(v.x, v.y) } fn vec2_to_raui(v: vek::Vec2) -> Vec2 { Vec2 { x: v.x, y: v.y } } ================================================ FILE: demos/hello-world/Cargo.toml ================================================ [package] name = "hello-world" version = "0.1.0" authors = ["Patryk 'PsichiX' Budzynski "] edition = "2024" publish = false [dependencies] serde = { version = "1", features = ["derive"] } raui = { version = "0.70", path = "../../crates/_", features = ["app"] } ================================================ FILE: demos/hello-world/src/main.rs ================================================ mod ui; use crate::ui::{ components::{app::app, content::content, title_bar::title_bar}, view_models::AppData, }; use raui::{ app::app::{App, AppConfig, declarative::DeclarativeApp}, core::{make_widget, view_model::ViewModel}, }; fn main() { let app = DeclarativeApp::default() .view_model(AppData::VIEW_MODEL, ViewModel::produce(AppData::new)) .tree( make_widget!(app) .named_slot("title", make_widget!(title_bar)) .named_slot("content", make_widget!(content)), ); App::new(AppConfig::default().title("Hello World!")).run(app); } ================================================ FILE: demos/hello-world/src/ui/components/app.rs ================================================ use raui::core::{ make_widget, unpack_named_slots, widget::{ component::{ containers::vertical_box::{VerticalBoxProps, nav_vertical_box}, interactive::navigation::NavJumpLooped, }, context::WidgetContext, node::WidgetNode, unit::flex::FlexBoxItemLayout, }, }; pub fn app(context: WidgetContext) -> WidgetNode { let WidgetContext { key, named_slots, .. } = context; unpack_named_slots!(named_slots => { title, content }); title.remap_props(|props| { props.with(FlexBoxItemLayout { grow: 0.0, shrink: 0.0, ..Default::default() }) }); make_widget!(nav_vertical_box) .key(key) .with_props(VerticalBoxProps { separation: 16.0, ..Default::default() }) .with_props(NavJumpLooped) .listed_slot(title) .listed_slot(content) .into() } ================================================ FILE: demos/hello-world/src/ui/components/color_rect.rs ================================================ use raui::core::{ Prefab, PropsData, make_widget, props::PropsData, widget::{ component::image_box::{ImageBoxProps, image_box}, context::WidgetContext, node::WidgetNode, unit::image::{ImageBoxColor, ImageBoxImageScaling, ImageBoxMaterial}, utils::Color, }, }; use serde::{Deserialize, Serialize}; #[derive(PropsData, Debug, Default, Copy, Clone, Serialize, Deserialize)] pub struct ColorRectProps { #[serde(default)] pub color: Color, } pub fn color_rect(context: WidgetContext) -> WidgetNode { let WidgetContext { key, props, .. } = context; let color = props.read_cloned_or_default::().color; make_widget!(image_box) .key(key) .merge_props(props.clone()) .with_props(ImageBoxProps { material: ImageBoxMaterial::Color(ImageBoxColor { color, scaling: ImageBoxImageScaling::Frame((10.0, true).into()), }), ..Default::default() }) .into() } ================================================ FILE: demos/hello-world/src/ui/components/content.rs ================================================ use crate::ui::components::{ color_rect::{ColorRectProps, color_rect}, image_button::{ImageButtonProps, image_button}, }; use raui::core::{ make_widget, widget::{ component::containers::grid_box::{GridBoxProps, grid_box}, context::WidgetContext, node::WidgetNode, unit::grid::GridBoxItemLayout, utils::{Color, IntRect, Rect}, }, }; pub fn content(context: WidgetContext) -> WidgetNode { let WidgetContext { key, props, .. } = context; make_widget!(grid_box) .key(key) .merge_props(props.clone()) .with_props(GridBoxProps { cols: 2, rows: 2, ..Default::default() }) .listed_slot( make_widget!(image_button) .with_props(ImageButtonProps { image: "./resources/cat.jpg".to_owned(), horizontal_alignment: 1.0, }) .with_props(GridBoxItemLayout { space_occupancy: IntRect { left: 0, right: 1, top: 0, bottom: 1, }, margin: Rect { left: 8.0, right: 8.0, top: 8.0, bottom: 8.0, }, ..Default::default() }), ) .listed_slot( make_widget!(color_rect) .with_props(ColorRectProps { color: Color { r: 1.0, g: 0.0, b: 0.0, a: 0.5, }, }) .with_props(GridBoxItemLayout { space_occupancy: IntRect { left: 1, right: 2, top: 0, bottom: 1, }, margin: 8.0.into(), ..Default::default() }), ) .listed_slot( make_widget!(image_button) .with_props(ImageButtonProps { image: "./resources/cats.jpg".to_owned(), horizontal_alignment: 0.5, }) .with_props(GridBoxItemLayout { space_occupancy: IntRect { left: 0, right: 2, top: 1, bottom: 2, }, margin: 8.0.into(), ..Default::default() }), ) .into() } ================================================ FILE: demos/hello-world/src/ui/components/image_button.rs ================================================ use raui::core::{ Prefab, PropsData, Scalar, make_widget, pre_hooks, props::PropsData, widget::{ component::{ image_box::{ImageBoxProps, image_box}, interactive::{ button::{ButtonNotifyProps, ButtonProps, button, use_button_notified_state}, navigation::NavItemActive, }, }, context::WidgetContext, node::WidgetNode, unit::image::{ImageBoxAspectRatio, ImageBoxImage, ImageBoxMaterial}, utils::{Color, Transform, Vec2}, }, }; use serde::{Deserialize, Serialize}; #[derive(PropsData, Debug, Default, Clone, Serialize, Deserialize)] pub struct ImageButtonProps { #[serde(default)] pub image: String, #[serde(default)] pub horizontal_alignment: Scalar, } #[pre_hooks(use_button_notified_state)] pub fn image_button(mut context: WidgetContext) -> WidgetNode { let WidgetContext { id, key, props, state, .. } = context; let ImageButtonProps { image, horizontal_alignment, } = props.read_cloned_or_default(); let ButtonProps { selected, trigger, context, .. } = state.read_cloned_or_default(); let scale = if trigger || context { Vec2 { x: 1.1, y: 1.1 } } else if selected { Vec2 { x: 1.05, y: 1.05 } } else { Vec2 { x: 1.0, y: 1.0 } }; make_widget!(button) .key(key) .with_props(NavItemActive) .with_props(ButtonNotifyProps(id.to_owned().into())) .named_slot( "content", make_widget!(image_box) .key("image") .with_props(ImageBoxProps { material: ImageBoxMaterial::Image(ImageBoxImage { id: image, tint: if trigger { Color { r: 0.0, g: 1.0, b: 0.0, a: 1.0, } } else if context { Color { r: 1.0, g: 0.0, b: 0.0, a: 1.0, } } else if selected { Color { r: 1.0, g: 1.0, b: 1.0, a: 0.85, } } else { Color::default() }, ..Default::default() }), content_keep_aspect_ratio: Some(ImageBoxAspectRatio { horizontal_alignment, vertical_alignment: 0.5, outside: false, }), transform: Transform { pivot: Vec2 { x: 0.5, y: 0.5 }, scale, ..Default::default() }, ..Default::default() }), ) .into() } ================================================ FILE: demos/hello-world/src/ui/components/mod.rs ================================================ pub mod app; pub mod color_rect; pub mod content; pub mod image_button; pub mod title_bar; ================================================ FILE: demos/hello-world/src/ui/components/title_bar.rs ================================================ use crate::ui::view_models::AppData; use raui::core::{ make_widget, pre_hooks, widget::{ component::{ interactive::{ button::{ButtonNotifyProps, ButtonProps, use_button_notified_state}, input_field::{ TextInputNotifyProps, TextInputProps, TextInputState, input_field, input_text_with_cursor, use_text_input_notified_state, }, navigation::NavItemActive, }, text_box::{TextBoxProps, text_box}, }, context::WidgetContext, node::WidgetNode, unit::text::{TextBoxFont, TextBoxSizeValue}, utils::Color, }, }; fn use_title_bar(context: &mut WidgetContext) { context.life_cycle.mount(|mut context| { context .view_models .bindings(AppData::VIEW_MODEL, AppData::INPUT) .unwrap() .bind(context.id.to_owned()); }); } #[pre_hooks( use_button_notified_state, use_text_input_notified_state, use_title_bar )] pub fn title_bar(mut context: WidgetContext) -> WidgetNode { let WidgetContext { id, key, state, .. } = context; let ButtonProps { selected, trigger, .. } = state.read_cloned_or_default(); let TextInputState { cursor_position, focused, } = state.read_cloned_or_default(); let content = context .view_models .view_model_mut(AppData::VIEW_MODEL) .unwrap() .write::() .unwrap() .input .lazy(); let text = content .read() .map(|text| text.to_string()) .unwrap_or_default(); let text = if focused { input_text_with_cursor(&text, cursor_position, '|') } else if text.is_empty() { "> Focus here and start typing...".to_owned() } else { text }; make_widget!(input_field) .key(key) .with_props(NavItemActive) .with_props(TextInputNotifyProps(id.to_owned().into())) .with_props(ButtonNotifyProps(id.to_owned().into())) .with_props(TextInputProps { text: Some(content.into()), ..Default::default() }) .named_slot( "content", make_widget!(text_box).key("text").with_props(TextBoxProps { text, width: TextBoxSizeValue::Fill, height: TextBoxSizeValue::Exact(32.0), font: TextBoxFont { name: "./resources/verdana.ttf".to_owned(), size: 32.0, }, color: if trigger { Color { r: 1.0, g: 0.0, b: 0.0, a: 1.0, } } else if selected { Color { r: 0.0, g: 1.0, b: 0.0, a: 1.0, } } else if focused { 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, } }, ..Default::default() }), ) .into() } ================================================ FILE: demos/hello-world/src/ui/mod.rs ================================================ pub mod components; pub mod view_models; ================================================ FILE: demos/hello-world/src/ui/view_models.rs ================================================ use raui::core::{ Managed, view_model::{ViewModelProperties, ViewModelValue}, }; pub struct AppData { pub input: Managed>, } impl AppData { pub const VIEW_MODEL: &str = "app-data"; pub const INPUT: &str = "input"; pub fn new(properties: &mut ViewModelProperties) -> Self { Self { input: Managed::new(ViewModelValue::new( Default::default(), properties.notifier(Self::INPUT), )), } } } ================================================ FILE: demos/in-game/Cargo.toml ================================================ [package] name = "in-game" version = "0.1.0" authors = ["Patryk 'PsichiX' Budzynski "] edition = "2024" publish = false [dependencies] serde = { version = "1", features = ["derive"] } serde_json = "1" raui = { version = "0.70", path = "../../crates/_", features = ["app", "material"] } ================================================ FILE: demos/in-game/README.md ================================================ # In-Game UI demo ## Credits - UI icons used: https://crusenho.itch.io/complete-gui-essential-pack ================================================ FILE: demos/in-game/resources/items.json ================================================ { "potion": { "name": "Potion", "icon": "resources/icons/potion.png", "buy": 3, "sell": 1 }, "sword": { "name": "Sword", "icon": "resources/icons/sword.png", "buy": 10, "sell": 4 }, "shield": { "name": "Shield", "icon": "resources/icons/shield.png", "buy": 7, "sell": 3 } } ================================================ FILE: demos/in-game/resources/quests.json ================================================ { "kill-5-monsters": { "name": "Kill 5 monsters" }, "collect-3-potions": { "name": "Collect 3 potions" }, "save-the-world": { "name": "Save the world" } } ================================================ FILE: demos/in-game/src/main.rs ================================================ mod model; mod ui; use model::{ inventory::{Inventory, ItemsDatabase}, menu::{Menu, MenuScreen}, quests::Quests, settings::Settings, }; use raui::{ app::{ app::{App, AppConfig, declarative::DeclarativeApp}, event::{ElementState, Event, VirtualKeyCode, WindowEvent}, }, core::make_widget, }; use ui::app::app; fn main() { let app = DeclarativeApp::default() .tree(make_widget!(app)) .view_model(Menu::VIEW_MODEL, Menu::view_model()) .view_model(Settings::VIEW_MODEL, Settings::view_model()) .view_model( Quests::VIEW_MODEL, Quests::view_model("resources/quests.json"), ) .view_model( ItemsDatabase::VIEW_MODEL, ItemsDatabase::view_model("resources/items.json"), ) .view_model(Inventory::VIEW_MODEL, Inventory::view_model()) .event(|app, event, _, _| { if let Event::WindowEvent { event: WindowEvent::KeyboardInput { input, .. }, .. } = event && input.state == ElementState::Pressed { match input.virtual_keycode { Some(VirtualKeyCode::Key1) => { *app.view_models .get_mut(Menu::VIEW_MODEL) .unwrap() .write::() .unwrap() .screen = MenuScreen::None; println!("Changed menu: None"); } Some(VirtualKeyCode::Key2) => { *app.view_models .get_mut(Menu::VIEW_MODEL) .unwrap() .write::() .unwrap() .screen = MenuScreen::Settings; println!("Changed menu: Settings"); } Some(VirtualKeyCode::Key3) => { *app.view_models .get_mut(Menu::VIEW_MODEL) .unwrap() .write::() .unwrap() .screen = MenuScreen::Quests; println!("Changed menu: Quests"); } Some(VirtualKeyCode::Key4) => { *app.view_models .get_mut(Menu::VIEW_MODEL) .unwrap() .write::() .unwrap() .screen = MenuScreen::Inventory; println!("Changed menu: Inventory"); } Some(VirtualKeyCode::Key5) => { app.view_models .get_mut(Inventory::VIEW_MODEL) .unwrap() .write::() .unwrap() .add("potion", 1); println!("Added item: Potion"); } Some(VirtualKeyCode::Key6) => { app.view_models .get_mut(Inventory::VIEW_MODEL) .unwrap() .write::() .unwrap() .add("sword", 1); println!("Added item: Sword"); } Some(VirtualKeyCode::Key7) => { app.view_models .get_mut(Inventory::VIEW_MODEL) .unwrap() .write::() .unwrap() .add("shield", 1); println!("Added item: Shield"); } Some(VirtualKeyCode::Escape) => { return false; } _ => {} } } true }); App::new( AppConfig::default() .title("In-Game") .color([0.2, 0.2, 0.2, 1.0]), ) .run(app); } ================================================ FILE: demos/in-game/src/model/inventory.rs ================================================ use raui::core::view_model::{ViewModel, ViewModelValue}; use serde::{Deserialize, Serialize}; use std::{collections::HashMap, fs::File, path::Path}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Item { pub name: String, pub icon: String, pub buy: usize, pub sell: usize, } pub struct ItemsDatabase { pub items: HashMap, } impl ItemsDatabase { pub const VIEW_MODEL: &str = "items-database"; pub fn view_model(database_path: impl AsRef) -> ViewModel { let database_path = database_path.as_ref(); let items = File::open(database_path).unwrap_or_else(|err| { panic!("Could not load items database: {database_path:?}. Error: {err}") }); let items = serde_json::from_reader(items).unwrap_or_else(|err| { panic!("Could not deserialize items database: {database_path:?}. Error: {err}") }); ViewModel::new_object(Self { items }) } } pub struct Inventory { owned: ViewModelValue>, } impl Inventory { pub const VIEW_MODEL: &str = "items"; pub const OWNED: &str = "owned"; pub fn view_model() -> ViewModel { ViewModel::produce(|properties| { let mut result = Self { owned: ViewModelValue::new(Default::default(), properties.notifier(Self::OWNED)), }; result.add("potion", 5); result.add("shield", 1); result.add("sword", 2); result }) } pub fn add(&mut self, id: impl ToString, count: usize) { let value = self.owned.entry(id.to_string()).or_default(); *value = value.saturating_add(count); } #[allow(dead_code)] pub fn remove(&mut self, id: &str, count: usize) { if let Some(value) = self.owned.get_mut(id) { *value = value.saturating_sub(count); if *value == 0 { self.owned.remove(id); } } } pub fn owned<'a>( &'a self, database: &'a ItemsDatabase, ) -> impl Iterator { self.owned .iter() .filter_map(|(id, count)| Some((id.as_str(), *count, database.items.get(id)?))) } } ================================================ FILE: demos/in-game/src/model/menu.rs ================================================ use raui::core::view_model::{ViewModel, ViewModelValue}; #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub enum MenuScreen { #[default] None, Settings, Inventory, Quests, } pub struct Menu { pub screen: ViewModelValue, } impl Menu { pub const VIEW_MODEL: &str = "menu"; pub const SCREEN: &str = "screen"; pub fn view_model() -> ViewModel { ViewModel::produce(|properties| Self { screen: ViewModelValue::new(Default::default(), properties.notifier(Self::SCREEN)), }) } } ================================================ FILE: demos/in-game/src/model/mod.rs ================================================ pub mod inventory; pub mod menu; pub mod quests; pub mod settings; ================================================ FILE: demos/in-game/src/model/quests.rs ================================================ use raui::core::view_model::{ViewModel, ViewModelValue}; use serde::{Deserialize, Serialize}; use std::{ collections::{HashMap, HashSet}, fs::File, path::Path, }; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Quest { pub name: String, } pub struct Quests { database: HashMap, completed: ViewModelValue>, } impl Quests { pub const VIEW_MODEL: &str = "quests"; pub const COMPLETED: &str = "completed"; pub fn view_model(database_path: impl AsRef) -> ViewModel { let database_path = database_path.as_ref(); let database = File::open(database_path).unwrap_or_else(|err| { panic!("Could not load quests database: {database_path:?}. Error: {err}") }); let database = serde_json::from_reader(database).unwrap_or_else(|err| { panic!("Could not deserialize quests database: {database_path:?}. Error: {err}") }); ViewModel::produce(|properties| { let mut result = Self { database, completed: ViewModelValue::new( Default::default(), properties.notifier(Self::COMPLETED), ), }; result.toggle("collect-3-potions"); result }) } pub fn toggle(&mut self, id: impl ToString) { let id = id.to_string(); if self.completed.contains(&id) { self.completed.remove(&id); } else { self.completed.insert(id); } } pub fn completed(&self) -> impl Iterator { let completed = &*self.completed; self.database .iter() .filter(|(id, _)| completed.contains(*id)) .map(|(id, quest)| (id.as_str(), quest)) } pub fn available(&self) -> impl Iterator { let completed = &*self.completed; self.database .iter() .filter(|(id, _)| !completed.contains(*id)) .map(|(id, quest)| (id.as_str(), quest)) } } ================================================ FILE: demos/in-game/src/model/settings.rs ================================================ use raui::core::{ Managed, Scalar, view_model::{ViewModel, ViewModelValue}, }; pub struct Settings { pub fullscreen: ViewModelValue, pub volume: Managed>, } impl Settings { pub const VIEW_MODEL: &str = "settings"; const FULLSCREEN: &str = "fullscreen"; const VOLUME: &str = "volume"; pub fn view_model() -> ViewModel { ViewModel::produce(|properties| Self { fullscreen: ViewModelValue::new(false, properties.notifier(Self::FULLSCREEN)), volume: Managed::new(ViewModelValue::new( 100.0, properties.notifier(Self::VOLUME), )), }) } } ================================================ FILE: demos/in-game/src/ui/app.rs ================================================ use super::{inventory::inventory, quests::quests, settings::settings}; use crate::model::menu::{Menu, MenuScreen}; use raui::{ core::{ make_widget, pre_hooks, widget::{ component::{ containers::content_box::content_box, image_box::{ImageBoxProps, image_box}, }, context::WidgetContext, node::WidgetNode, unit::{ image::{ ImageBoxAspectRatio, ImageBoxFrame, ImageBoxImage, ImageBoxImageScaling, ImageBoxMaterial, }, text::{TextBoxFont, TextBoxHorizontalAlign, TextBoxVerticalAlign}, }, utils::Color, }, }, material::theme::{ ThemeColorSet, ThemeColors, ThemeColorsBundle, ThemeProps, ThemedButtonMaterial, ThemedImageMaterial, ThemedSliderMaterial, ThemedSwitchMaterial, ThemedTextMaterial, new_all_white_theme, }, }; fn use_app(context: &mut WidgetContext) { context.life_cycle.mount(|mut context| { context .view_models .bindings(Menu::VIEW_MODEL, Menu::SCREEN) .unwrap() .bind(context.id.to_owned()); }); } #[pre_hooks(use_app)] pub fn app(mut context: WidgetContext) -> WidgetNode { let menu = context .view_models .view_model(Menu::VIEW_MODEL) .unwrap() .read::() .unwrap(); make_widget!(content_box) .key("screen") .with_shared_props(make_theme()) // Let's pretend this image is underlying game world. .listed_slot( make_widget!(image_box) .key("game") .with_props(ImageBoxProps { material: ImageBoxMaterial::Image(ImageBoxImage { id: "resources/images/game-mockup.png".to_owned(), ..Default::default() }), content_keep_aspect_ratio: Some(ImageBoxAspectRatio { horizontal_alignment: 0.5, vertical_alignment: 0.5, outside: true, }), ..Default::default() }), ) // And this is our actual game UI screens. .maybe_listed_slot(match *menu.screen { MenuScreen::None => None, MenuScreen::Settings => Some(make_widget!(settings)), MenuScreen::Inventory => Some(make_widget!(inventory)), MenuScreen::Quests => Some(make_widget!(quests)), }) .into() } fn make_theme() -> ThemeProps { new_all_white_theme() .background_colors(ThemeColorsBundle::uniform(ThemeColors::uniform( ThemeColorSet::uniform(Color { r: 0.0, g: 0.0, b: 0.0, a: 0.7, }), ))) .button_background( "", ThemedButtonMaterial { default: ThemedImageMaterial::Image(ImageBoxImage { id: "resources/images/button-default.png".to_owned(), scaling: ImageBoxImageScaling::Frame(ImageBoxFrame { source: 6.0.into(), destination: 6.0.into(), frame_only: false, ..Default::default() }), ..Default::default() }), selected: ThemedImageMaterial::Image(ImageBoxImage { id: "resources/images/button-selected.png".to_owned(), scaling: ImageBoxImageScaling::Frame(ImageBoxFrame { source: 6.0.into(), destination: 6.0.into(), frame_only: false, ..Default::default() }), ..Default::default() }), trigger: ThemedImageMaterial::Image(ImageBoxImage { id: "resources/images/button-trigger.png".to_owned(), scaling: ImageBoxImageScaling::Frame(ImageBoxFrame { source: 6.0.into(), destination: 6.0.into(), frame_only: false, ..Default::default() }), ..Default::default() }), }, ) .text_variant( "title", ThemedTextMaterial { font: TextBoxFont { name: "resources/fonts/MiKrollFantasy.ttf".to_owned(), size: 64.0, }, ..Default::default() }, ) .text_variant( "option-label", ThemedTextMaterial { font: TextBoxFont { name: "resources/fonts/MiKrollFantasy.ttf".to_owned(), size: 48.0, }, ..Default::default() }, ) .text_variant( "option-slider", ThemedTextMaterial { font: TextBoxFont { name: "resources/fonts/MiKrollFantasy.ttf".to_owned(), size: 32.0, }, horizontal_align: TextBoxHorizontalAlign::Center, vertical_align: TextBoxVerticalAlign::Middle, ..Default::default() }, ) .text_variant( "tab-label", ThemedTextMaterial { font: TextBoxFont { name: "resources/fonts/MiKrollFantasy.ttf".to_owned(), size: 48.0, }, horizontal_align: TextBoxHorizontalAlign::Center, vertical_align: TextBoxVerticalAlign::Middle, ..Default::default() }, ) .text_variant( "task-name", ThemedTextMaterial { font: TextBoxFont { name: "resources/fonts/MiKrollFantasy.ttf".to_owned(), size: 48.0, }, horizontal_align: TextBoxHorizontalAlign::Center, vertical_align: TextBoxVerticalAlign::Middle, ..Default::default() }, ) .text_variant( "inventory-item-count", ThemedTextMaterial { font: TextBoxFont { name: "resources/fonts/MiKrollFantasy.ttf".to_owned(), size: 28.0, }, horizontal_align: TextBoxHorizontalAlign::Right, vertical_align: TextBoxVerticalAlign::Bottom, ..Default::default() }, ) .switch_variant( "checkbox", ThemedSwitchMaterial { on: ThemedImageMaterial::Image(ImageBoxImage { id: "resources/icons/checkbox-on.png".to_owned(), ..Default::default() }), off: ThemedImageMaterial::Image(ImageBoxImage { id: "resources/icons/checkbox-off.png".to_owned(), ..Default::default() }), }, ) .slider_variant( "", ThemedSliderMaterial { background: ThemedImageMaterial::Image(ImageBoxImage { id: "resources/images/slider-background.png".to_owned(), scaling: ImageBoxImageScaling::Frame(ImageBoxFrame { source: 6.0.into(), destination: 6.0.into(), frame_only: false, ..Default::default() }), ..Default::default() }), filling: ThemedImageMaterial::Image(ImageBoxImage { id: "resources/images/slider-filling.png".to_owned(), scaling: ImageBoxImageScaling::Frame(ImageBoxFrame { source: 6.0.into(), destination: 6.0.into(), ..Default::default() }), ..Default::default() }), }, ) } ================================================ FILE: demos/in-game/src/ui/inventory.rs ================================================ use crate::model::inventory::{Inventory, ItemsDatabase}; use raui::{ core::{ make_widget, pre_hooks, widget::{ WidgetIdMetaParams, component::{ containers::{ content_box::content_box, grid_box::GridBoxProps, size_box::{SizeBoxProps, size_box}, }, image_box::{ImageBoxProps, image_box}, interactive::{ button::{ButtonNotifyMessage, ButtonNotifyProps}, navigation::NavItemActive, }, }, context::WidgetContext, node::WidgetNode, unit::{ content::ContentBoxItemLayout, grid::GridBoxItemLayout, image::{ImageBoxImage, ImageBoxMaterial}, size::SizeBoxSizeValue, }, utils::{Color, IntRect, Rect, Vec2}, }, }, material::component::{ containers::{ grid_paper::nav_grid_paper, window_paper::{WindowPaperProps, window_paper}, }, interactive::button_paper::button_paper, text_paper::{TextPaperProps, text_paper}, }, }; #[pre_hooks(use_inventory)] pub fn inventory(mut context: WidgetContext) -> WidgetNode { let inventory = context .view_models .view_model(Inventory::VIEW_MODEL) .unwrap() .read::() .unwrap(); let database = context .view_models .view_model(ItemsDatabase::VIEW_MODEL) .unwrap() .read::() .unwrap(); let items = inventory .owned(&database) .enumerate() .map(|(index, (id, count, item))| { let col = index as i32 % 5; let row = index as i32 / 5; make_widget!(inventory_item) .key(format!("{index}?item={id}")) .with_props(ButtonNotifyProps(context.id.to_owned().into())) .with_props(GridBoxItemLayout { space_occupancy: IntRect { left: col, right: col + 1, top: row, bottom: row + 1, }, margin: 6.0.into(), ..Default::default() }) .with_props(count) .with_props(item.icon.to_owned()) }); make_widget!(window_paper) .key("inventory") .with_props(WindowPaperProps { bar_margin: 20.0.into(), bar_height: Some(80.0), content_margin: 40.0.into(), ..Default::default() }) .named_slot( "bar", make_widget!(text_paper) .key("title") .with_props(TextPaperProps { text: "INVENTORY".to_owned(), variant: "title".to_owned(), use_main_color: true, ..Default::default() }), ) .named_slot( "content", make_widget!(content_box).listed_slot( make_widget!(size_box) .with_props(ContentBoxItemLayout { anchors: Rect { left: 0.5, right: 0.5, top: 0.5, bottom: 0.5, }, align: Vec2 { x: 0.5, y: 0.5 }, ..Default::default() }) .with_props(SizeBoxProps { width: SizeBoxSizeValue::Exact(400.0), height: SizeBoxSizeValue::Exact(400.0), ..Default::default() }) .named_slot( "content", make_widget!(nav_grid_paper) .with_props(GridBoxProps { cols: 5, rows: 5, ..Default::default() }) .listed_slots(items), ), ), ) .into() } fn inventory_item(context: WidgetContext) -> WidgetNode { let WidgetContext { key, props, .. } = context; let notify = props.read_cloned::().ok(); let icon = props.read_cloned_or_default::(); let count = props.read_cloned_or_default::(); make_widget!(button_paper) .key(key) .with_props(NavItemActive) .maybe_with_props(notify) .named_slot( "content", make_widget!(content_box) .key(key) .listed_slot( make_widget!(image_box) .key("icon") .with_props(ContentBoxItemLayout { margin: 16.0.into(), ..Default::default() }) .with_props(ImageBoxProps { material: ImageBoxMaterial::Image(ImageBoxImage { id: icon, tint: Color { r: 0.0, g: 0.0, b: 0.0, a: 1.0, }, ..Default::default() }), ..Default::default() }), ) .listed_slot( make_widget!(text_paper) .with_props(ContentBoxItemLayout { margin: 2.0.into(), ..Default::default() }) .with_props(TextPaperProps { text: count.to_string(), variant: "inventory-item-count".to_owned(), ..Default::default() }), ), ) .into() } fn use_inventory(context: &mut WidgetContext) { context.life_cycle.mount(|mut context| { context .view_models .bindings(Inventory::VIEW_MODEL, Inventory::OWNED) .unwrap() .bind(context.id.to_owned()); }); context.life_cycle.change(|mut context| { let mut inventory = context .view_models .view_model_mut(Inventory::VIEW_MODEL) .unwrap() .write::() .unwrap(); for msg in context.messenger.messages { if let Some(msg) = msg.as_any().downcast_ref::() && let Some(id) = WidgetIdMetaParams::new(msg.sender.meta()).find_value("item") && msg.trigger_start() { inventory.remove(id, 1); } } }); } ================================================ FILE: demos/in-game/src/ui/mod.rs ================================================ pub mod app; pub mod inventory; pub mod quests; pub mod settings; ================================================ FILE: demos/in-game/src/ui/quests.rs ================================================ use crate::model::quests::{Quest, Quests}; use raui::{ core::{ make_widget, pre_hooks, widget::{ WidgetIdMetaParams, component::{ containers::{ content_box::content_box, tabs_box::{TabPlateProps, TabsBoxProps, nav_tabs_box}, vertical_box::{VerticalBoxProps, vertical_box}, }, image_box::{ImageBoxProps, image_box}, interactive::{ button::{ButtonNotifyMessage, ButtonNotifyProps}, navigation::NavItemActive, }, }, context::WidgetContext, node::WidgetNode, unit::flex::FlexBoxItemLayout, }, }, material::component::{ containers::window_paper::{WindowPaperProps, window_paper}, interactive::text_button_paper::text_button_paper, text_paper::{TextPaperProps, text_paper}, }, }; #[pre_hooks(use_quests)] pub fn quests(mut context: WidgetContext) -> WidgetNode { make_widget!(window_paper) .key("quests") .with_props(WindowPaperProps { bar_margin: 20.0.into(), bar_height: Some(80.0), content_margin: 40.0.into(), ..Default::default() }) .named_slot( "bar", make_widget!(text_paper) .key("title") .with_props(TextPaperProps { text: "QUESTS".to_owned(), variant: "title".to_owned(), use_main_color: true, ..Default::default() }), ) .named_slot( "content", make_widget!(nav_tabs_box) .key("tabs") .with_props(NavItemActive) .with_props(TabsBoxProps { tabs_basis: Some(64.0), tabs_and_content_separation: 20.0, ..Default::default() }) .listed_slot([ make_widget!(tab_plate) .with_props("AVAILABLE".to_owned()) .into(), make_widget!(available_tasks) .with_props(ButtonNotifyProps(context.id.to_owned().into())) .into(), ]) .listed_slot([ make_widget!(tab_plate) .with_props("COMPLETED".to_owned()) .into(), make_widget!(completed_tasks) .with_props(ButtonNotifyProps(context.id.to_owned().into())) .into(), ]), ) .into() } fn tab_plate(context: WidgetContext) -> WidgetNode { let WidgetContext { props, .. } = context; let active = props.read_cloned_or_default::().active; let text = props.read_cloned_or_default::(); make_widget!(content_box) .key("plate") .maybe_listed_slot(active.then(|| { make_widget!(image_box) .key("background") .with_props(ImageBoxProps::colored(Default::default())) })) .listed_slot( make_widget!(text_paper) .key("text") .with_props(TextPaperProps { text, variant: "tab-label".to_owned(), use_main_color: !active, ..Default::default() }), ) .into() } fn available_tasks(context: WidgetContext) -> WidgetNode { let quests = context .view_models .view_model(Quests::VIEW_MODEL) .unwrap() .read::() .unwrap(); let notify = context.props.read_cloned_or_default::(); make_tasks_list(notify, "available", quests.available()) } fn completed_tasks(context: WidgetContext) -> WidgetNode { let quests = context .view_models .view_model(Quests::VIEW_MODEL) .unwrap() .read::() .unwrap(); let notify = context.props.read_cloned_or_default::(); make_tasks_list(notify, "completed", quests.completed()) } fn make_tasks_list<'a>( notify: ButtonNotifyProps, key: &str, tasks: impl Iterator, ) -> WidgetNode { make_widget!(vertical_box) .key(key) .with_props(VerticalBoxProps { override_slots_layout: Some(FlexBoxItemLayout { basis: Some(48.0), grow: 0.0, shrink: 0.0, ..Default::default() }), separation: 10.0, ..Default::default() }) .listed_slots(tasks.enumerate().map(|(index, (id, task))| { make_widget!(quest_task) .key(format!("{index}?item={id}")) .with_props(task.name.to_owned()) .with_props(notify.to_owned()) })) .into() } fn quest_task(context: WidgetContext) -> WidgetNode { let WidgetContext { key, props, .. } = context; let notify = props.read_cloned::().ok(); let name = props.read_cloned_or_default::(); make_widget!(text_button_paper) .key(key) .maybe_with_props(notify) .with_props(NavItemActive) .with_props(TextPaperProps { text: name, variant: "task-name".to_owned(), ..Default::default() }) .into() } fn use_quests(context: &mut WidgetContext) { context.life_cycle.mount(|mut context| { context .view_models .bindings(Quests::VIEW_MODEL, Quests::COMPLETED) .unwrap() .bind(context.id.to_owned()); }); context.life_cycle.change(|mut context| { let mut quests = context .view_models .view_model_mut(Quests::VIEW_MODEL) .unwrap() .write::() .unwrap(); for msg in context.messenger.messages { if let Some(msg) = msg.as_any().downcast_ref::() && let Some(id) = WidgetIdMetaParams::new(msg.sender.meta()).find_value("item") && msg.trigger_start() { quests.toggle(id); } } }); } ================================================ FILE: demos/in-game/src/ui/settings.rs ================================================ use crate::model::settings::Settings; use raui::{ core::{ make_widget, pre_hooks, unpack_named_slots, widget::{ WidgetIdMetaParams, component::{ containers::{ horizontal_box::{HorizontalBoxProps, horizontal_box}, vertical_box::{VerticalBoxProps, nav_vertical_box}, }, interactive::{ button::{ButtonNotifyMessage, ButtonNotifyProps}, navigation::NavItemActive, slider_view::{SliderInput, SliderViewDirection, SliderViewProps}, }, }, context::WidgetContext, node::WidgetNode, unit::{flex::FlexBoxItemLayout, text::TextBoxHorizontalAlign}, }, }, material::{ component::{ containers::window_paper::{WindowPaperProps, window_paper}, interactive::{ slider_paper::{NumericSliderPaperProps, numeric_slider_paper}, switch_button_paper::switch_button_paper, }, switch_paper::SwitchPaperProps, text_paper::{TextPaperProps, text_paper}, }, theme::{ThemeColor, ThemeVariant, ThemedWidgetProps}, }, }; #[pre_hooks(use_settings)] pub fn settings(mut context: WidgetContext) -> WidgetNode { let mut settings = context .view_models .view_model_mut(Settings::VIEW_MODEL) .unwrap() .write::() .unwrap(); make_widget!(window_paper) .key("settings") .with_props(WindowPaperProps { bar_margin: 20.0.into(), bar_height: Some(80.0), content_margin: 40.0.into(), ..Default::default() }) .named_slot( "bar", make_widget!(text_paper) .key("title") .with_props(TextPaperProps { text: "SETTINGS".to_owned(), variant: "title".to_owned(), use_main_color: true, ..Default::default() }), ) .named_slot( "content", make_widget!(nav_vertical_box) .key("options") .with_props(VerticalBoxProps { override_slots_layout: Some(FlexBoxItemLayout { grow: 0.0, shrink: 0.0, basis: Some(48.0), ..Default::default() }), ..Default::default() }) .listed_slot( make_widget!(option) .key("fullscreen") .with_props("Fullscreen".to_owned()) .named_slot( "content", make_widget!(switch_button_paper) .key("button?id=fullscreen") .with_props(NavItemActive) .with_props(ButtonNotifyProps(context.id.to_owned().into())) .with_props(SwitchPaperProps { on: *settings.fullscreen, variant: "checkbox".to_owned(), size_level: 3, }) .with_props(ThemedWidgetProps { color: ThemeColor::Primary, variant: ThemeVariant::ContentOnly, ..Default::default() }), ), ) .listed_slot( make_widget!(option) .key("volume") .with_props("Volume".to_owned()) .named_slot( "content", make_widget!(numeric_slider_paper) .key("slider") .with_props(NavItemActive) .with_props(TextPaperProps { variant: "option-slider".to_owned(), use_main_color: true, ..Default::default() }) .with_props(SliderViewProps { input: Some(SliderInput::new(settings.volume.lazy())), from: 0.0, to: 100.0, direction: SliderViewDirection::LeftToRight, }) .with_props(NumericSliderPaperProps { fractional_digits_count: Some(0), }), ), ), ) .into() } fn use_settings(context: &mut WidgetContext) { context.life_cycle.change(|mut context| { for msg in context.messenger.messages { if let Some(msg) = msg.as_any().downcast_ref::() && msg.trigger_start() && let Some(id) = WidgetIdMetaParams::new(msg.sender.meta()).find_value("id") && id == "fullscreen" { let mut settings = context .view_models .view_model_mut(Settings::VIEW_MODEL) .unwrap() .write::() .unwrap(); *settings.fullscreen = !*settings.fullscreen; } } }); } fn option(context: WidgetContext) -> WidgetNode { let WidgetContext { key, props, named_slots, .. } = context; unpack_named_slots!(named_slots => { content }); let label = props.read_cloned_or_default::(); make_widget!(horizontal_box) .key(key) .with_props(HorizontalBoxProps { separation: 50.0, ..Default::default() }) .listed_slot( make_widget!(text_paper) .key("label") .with_props(TextPaperProps { text: label, variant: "option-label".to_owned(), use_main_color: true, horizontal_align_override: Some(TextBoxHorizontalAlign::Right), ..Default::default() }), ) .listed_slot(content) .into() } ================================================ FILE: demos/todo-app/.gitignore ================================================ /state.json *.log ================================================ FILE: demos/todo-app/Cargo.toml ================================================ [package] name = "todo-app" version = "0.1.0" authors = ["Patryk 'PsichiX' Budzynski "] edition = "2024" publish = false [dependencies] serde = { version = "1", features = ["derive"] } serde_json = "1" raui = { version = "0.70", path = "../../crates/_", features = ["app", "material"] } ================================================ FILE: demos/todo-app/resources/fonts/Roboto/LICENSE.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: demos/todo-app/src/main.rs ================================================ mod model; mod ui; use crate::{model::AppState, ui::components::app::app}; use raui::{ app::app::{App, AppConfig, declarative::DeclarativeApp}, core::{make_widget, view_model::ViewModel}, }; fn main() { let app = DeclarativeApp::default() .tree(make_widget!(app)) .view_model( AppState::VIEW_MODEL, ViewModel::produce(|properties| { let mut result = AppState::new(properties); result.load(); result }), ); App::new(AppConfig::default().title("TODO App")).run(app); } ================================================ FILE: demos/todo-app/src/model.rs ================================================ use raui::core::{ Managed, ManagedLazy, Prefab, PropsData, props::PropsData, view_model::{ViewModelProperties, ViewModelValue}, }; use serde::{Deserialize, Serialize}; #[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum ThemeMode { Light, #[default] Dark, } impl ThemeMode { pub fn toggle(&mut self) { *self = match *self { Self::Dark => Self::Light, Self::Light => Self::Dark, } } } #[derive(PropsData, Debug, Default, Clone, Serialize, Deserialize)] pub struct TaskProps { #[serde(default)] pub done: bool, #[serde(default)] pub name: String, } impl TaskProps { pub fn new(name: impl ToString) -> Self { Self { done: false, name: name.to_string(), } } } #[derive(Debug, Default, Clone, Serialize, Deserialize)] struct AppStateSave { theme: ThemeMode, tasks: Vec, } pub struct AppState { theme: ViewModelValue, tasks: ViewModelValue>, creating_task: ViewModelValue, new_task_name: Managed>, } impl AppState { pub const VIEW_MODEL: &str = "app-state"; pub const THEME: &str = "theme"; pub const TASKS: &str = "tasks"; pub const CREATING_TASK: &str = "creating-task"; pub const NEW_TASK_NAME: &str = "new-task-name"; pub fn new(properties: &mut ViewModelProperties) -> Self { Self { theme: ViewModelValue::new(ThemeMode::Dark, properties.notifier(Self::THEME)), tasks: ViewModelValue::new(Default::default(), properties.notifier(Self::TASKS)), creating_task: ViewModelValue::new(false, properties.notifier(Self::CREATING_TASK)), new_task_name: Managed::new(ViewModelValue::new( Default::default(), properties.notifier(Self::NEW_TASK_NAME), )), } } pub fn theme(&self) -> ThemeMode { *self.theme } pub fn tasks(&self) -> impl Iterator { self.tasks.iter() } pub fn toggle_theme(&mut self) { self.theme.toggle(); } pub fn creating_task(&self) -> bool { *self.creating_task } pub fn new_task_name(&mut self) -> ManagedLazy> { self.new_task_name.lazy() } pub fn create_task(&mut self) { *self.creating_task = true; **self.new_task_name.write().unwrap() = Default::default(); } pub fn add_task(&mut self) { if *self.creating_task { *self.creating_task = false; let name = std::mem::take(&mut **self.new_task_name.write().unwrap()); if !name.is_empty() { self.tasks.push(TaskProps::new(name)); } } } pub fn delete_task(&mut self, index: usize) { if index < self.tasks.len() { self.tasks.remove(index); } } pub fn toggle_task(&mut self, index: usize) { if let Some(task) = self.tasks.get_mut(index) { task.done = !task.done; } } pub fn load(&mut self) { if let Ok(content) = std::fs::read_to_string("./state.json") && let Ok(state) = serde_json::from_str::(&content) { *self.theme = state.theme; *self.tasks = state.tasks; } } pub fn save(&self) { let state = AppStateSave { theme: self.theme.to_owned(), tasks: self.tasks.iter().cloned().collect(), }; if let Ok(content) = serde_json::to_string_pretty(&state) { let _ = std::fs::write("./state.json", content); } } } ================================================ FILE: demos/todo-app/src/ui/components/app.rs ================================================ use super::{app_bar::app_bar, tasks_list::tasks_list}; use crate::model::{AppState, ThemeMode}; use raui::{ core::{ make_widget, pre_hooks, widget::{ WidgetRef, component::{ containers::{ portal_box::PortalsContainer, vertical_box::{VerticalBoxProps, vertical_box}, wrap_box::{WrapBoxProps, wrap_box}, }, interactive::navigation::use_nav_container_active, }, context::WidgetContext, node::WidgetNode, unit::{ flex::FlexBoxItemLayout, image::ImageBoxImage, text::{TextBoxFont, TextBoxHorizontalAlign, TextBoxVerticalAlign}, }, }, }, material::{ component::containers::paper::paper, theme::{ ThemeProps, ThemedImageMaterial, ThemedSwitchMaterial, ThemedTextMaterial, new_dark_theme, new_light_theme, }, }, }; fn new_theme(theme: ThemeMode) -> ThemeProps { let mut theme = match theme { ThemeMode::Light => new_light_theme(), ThemeMode::Dark => new_dark_theme(), }; theme.text_variants.insert( "title".to_owned(), ThemedTextMaterial { font: TextBoxFont { name: "resources/fonts/Roboto/Roboto-Black.ttf".to_owned(), size: 24.0, }, vertical_align: TextBoxVerticalAlign::Middle, ..Default::default() }, ); theme.text_variants.insert( "input".to_owned(), ThemedTextMaterial { font: TextBoxFont { name: "resources/fonts/Roboto/Roboto-Regular.ttf".to_owned(), size: 24.0, }, ..Default::default() }, ); theme.text_variants.insert( "tooltip".to_owned(), ThemedTextMaterial { font: TextBoxFont { name: "resources/fonts/Roboto/Roboto-BoldItalic.ttf".to_owned(), size: 18.0, }, ..Default::default() }, ); theme.text_variants.insert( "button".to_owned(), ThemedTextMaterial { font: TextBoxFont { name: "resources/fonts/Roboto/Roboto-Bold.ttf".to_owned(), size: 24.0, }, horizontal_align: TextBoxHorizontalAlign::Center, vertical_align: TextBoxVerticalAlign::Middle, ..Default::default() }, ); theme.switch_variants.insert( "checkbox".to_owned(), ThemedSwitchMaterial { on: ThemedImageMaterial::Image(ImageBoxImage { id: "resources/icons/check-box-on.png".to_owned(), ..Default::default() }), off: ThemedImageMaterial::Image(ImageBoxImage { id: "resources/icons/check-box-off.png".to_owned(), ..Default::default() }), }, ); theme } fn use_app(context: &mut WidgetContext) { context.life_cycle.mount(|mut context| { context .view_models .bindings(AppState::VIEW_MODEL, AppState::THEME) .unwrap() .bind(context.id.to_owned()); }); } #[pre_hooks(use_nav_container_active, use_app)] pub fn app(mut context: WidgetContext) -> WidgetNode { let WidgetContext { key, view_models, .. } = context; let app_state = view_models .view_model(AppState::VIEW_MODEL) .unwrap() .read::() .unwrap(); let idref = WidgetRef::default(); make_widget!(paper) .key(key) .idref(idref.clone()) .with_shared_props(PortalsContainer(idref.clone())) .with_shared_props(new_theme(app_state.theme())) .listed_slot( make_widget!(wrap_box) .key("wrap") .with_props(WrapBoxProps { margin: 32.0.into(), ..Default::default() }) .named_slot( "content", make_widget!(vertical_box) .key("list") .with_props(VerticalBoxProps { separation: 10.0, ..Default::default() }) .listed_slot(make_widget!(app_bar).key("app-bar").with_props( FlexBoxItemLayout { grow: 0.0, shrink: 0.0, ..Default::default() }, )) .listed_slot(make_widget!(tasks_list).key("tasks-list")), ), ) .into() } ================================================ FILE: demos/todo-app/src/ui/components/app_bar.rs ================================================ use crate::model::{AppState, ThemeMode}; use raui::{ core::{ make_widget, pre_hooks, props::Props, widget::{ WidgetId, component::{ containers::{ anchor_box::PivotBoxProps, horizontal_box::{HorizontalBoxProps, horizontal_box}, vertical_box::{VerticalBoxProps, vertical_box}, }, interactive::{ button::{ButtonNotifyMessage, ButtonNotifyProps}, input_field::TextInputProps, navigation::NavItemActive, }, }, context::WidgetContext, node::WidgetNode, unit::{flex::FlexBoxItemLayout, text::TextBoxSizeValue}, utils::{Rect, Vec2}, }, }, material::{ component::{ containers::text_tooltip_paper::text_tooltip_paper, icon_paper::{IconImage, IconPaperProps}, interactive::{ icon_button_paper::icon_button_paper, text_field_paper::{TextFieldPaperProps, text_field_paper}, }, text_paper::{TextPaperProps, text_paper}, }, theme::{ThemeColor, ThemeVariant, ThemedWidgetProps}, }, }; fn use_app_bar(context: &mut WidgetContext) { context.life_cycle.change(|mut context| { let mut app_state = context .view_models .view_model_mut(AppState::VIEW_MODEL) .unwrap() .write::() .unwrap(); for msg in context.messenger.messages { if let Some(msg) = msg.as_any().downcast_ref::() && msg.trigger_start() { match msg.sender.key() { "theme" => { app_state.toggle_theme(); } "save" => { app_state.save(); } "create" => { app_state.create_task(); } "add" => { app_state.add_task(); } _ => {} } } } }); } #[pre_hooks(use_app_bar)] pub fn app_bar(mut context: WidgetContext) -> WidgetNode { let WidgetContext { key, id, mut view_models, .. } = context; let mut app_state = view_models .view_model_mut(AppState::VIEW_MODEL) .unwrap() .write::() .unwrap(); make_widget!(vertical_box) .key(key) .with_props(VerticalBoxProps { separation: 10.0, ..Default::default() }) .listed_slot( make_widget!(horizontal_box) .key("title-bar") .with_props(HorizontalBoxProps { separation: 10.0, ..Default::default() }) .listed_slot( make_widget!(text_paper) .key("title") .with_props(TextPaperProps { text: "TODO App".to_owned(), variant: "title".to_owned(), ..Default::default() }), ) .listed_slot( make_widget!(text_tooltip_paper) .merge_props(make_tooltip_props("Change theme")) .named_slot( "content", make_widget!(icon_button_paper).key("theme").merge_props( make_icon_props( id, if app_state.theme() == ThemeMode::Dark { "resources/icons/light-mode.png" } else { "resources/icons/dark-mode.png" }, ), ), ), ) .listed_slot( make_widget!(text_tooltip_paper) .merge_props(make_tooltip_props("Save changes")) .named_slot( "content", make_widget!(icon_button_paper) .key("save") .merge_props(make_icon_props(id, "resources/icons/save.png")), ), ) .listed_slot(if app_state.creating_task() { WidgetNode::default() } else { make_widget!(text_tooltip_paper) .merge_props(make_tooltip_props("Create task")) .named_slot( "content", make_widget!(icon_button_paper) .key("create") .merge_props(make_icon_props(id, "resources/icons/add.png")), ) .into() }), ) .listed_slot(if app_state.creating_task() { make_widget!(horizontal_box) .key("task-bar") .with_props(HorizontalBoxProps { separation: 10.0, ..Default::default() }) .listed_slot( make_widget!(text_field_paper) .key("name") .with_props(TextFieldPaperProps { hint: "> Type new task name...".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() }) .with_props(NavItemActive) .with_props(ButtonNotifyProps(id.to_owned().into())) .with_props(TextInputProps { text: Some(app_state.new_task_name().into()), ..Default::default() }), ) .listed_slot( make_widget!(text_tooltip_paper) .merge_props(make_tooltip_props("Confirm new task")) .named_slot( "content", make_widget!(icon_button_paper) .key("add") .merge_props(make_icon_props(id, "resources/icons/add.png")), ), ) .into() } else { WidgetNode::default() }) .into() } fn make_tooltip_props(hint: &str) -> Props { Props::new(FlexBoxItemLayout { fill: 0.0, grow: 0.0, shrink: 0.0, align: 0.5, ..Default::default() }) .with(PivotBoxProps { pivot: Vec2 { x: 1.0, y: 1.0 }, align: Vec2 { x: 1.0, y: 0.0 }, }) .with(TextPaperProps { text: hint.to_owned(), width: TextBoxSizeValue::Exact(150.0), height: TextBoxSizeValue::Exact(24.0), variant: "tooltip".to_owned(), ..Default::default() }) } fn make_icon_props(id: &WidgetId, image_id: impl ToString) -> Props { Props::new(IconPaperProps { image: IconImage { id: image_id.to_string(), ..Default::default() }, size_level: 2, ..Default::default() }) .with(ThemedWidgetProps { color: ThemeColor::Secondary, variant: ThemeVariant::ContentOnly, ..Default::default() }) .with(NavItemActive) .with(ButtonNotifyProps(id.to_owned().into())) } ================================================ FILE: demos/todo-app/src/ui/components/confirm_box.rs ================================================ use raui::{ core::{ MessageData, Prefab, PropsData, make_widget, messenger::MessageData, pre_hooks, props::PropsData, widget::{ WidgetId, WidgetIdOrRef, component::{ containers::{ horizontal_box::horizontal_box, vertical_box::VerticalBoxProps, wrap_box::{WrapBoxProps, wrap_box}, }, interactive::{ button::{ButtonNotifyMessage, ButtonNotifyProps}, navigation::NavItemActive, }, }, context::WidgetContext, node::WidgetNode, unit::{content::ContentBoxItemLayout, text::TextBoxSizeValue}, utils::Rect, }, }, material::component::{ containers::{modal_paper::modal_paper, vertical_paper::vertical_paper}, interactive::text_button_paper::text_button_paper, text_paper::{TextPaperProps, text_paper}, }, }; use serde::{Deserialize, Serialize}; #[derive(PropsData, Debug, Default, Clone, Serialize, Deserialize)] pub struct ConfirmBoxProps { #[serde(default)] pub text: String, #[serde(default)] pub notify: WidgetIdOrRef, } #[derive(MessageData, Debug, Clone)] pub struct ConfirmNotifyMessage { #[allow(dead_code)] pub sender: WidgetId, pub confirmed: bool, } fn use_confirm_box(context: &mut WidgetContext) { let notify = context .props .map_or_default::(|p| p.notify.to_owned()); let notify = match notify.read() { Some(id) => id, None => return, }; context.life_cycle.change(move |context| { for msg in context.messenger.messages { if let Some(msg) = msg.as_any().downcast_ref::() && msg.trigger_start() { match msg.sender.key() { "yes" => { context.messenger.write( notify.to_owned(), ConfirmNotifyMessage { sender: context.id.to_owned(), confirmed: true, }, ); } "no" => { context.messenger.write( notify.to_owned(), ConfirmNotifyMessage { sender: context.id.to_owned(), confirmed: false, }, ); } _ => {} } } } }); } #[pre_hooks(use_confirm_box)] pub fn confirm_box(mut context: WidgetContext) -> WidgetNode { let WidgetContext { id, key, props, .. } = context; let ConfirmBoxProps { text, .. } = props.read_cloned_or_default(); make_widget!(modal_paper) .key(key) .named_slot( "content", make_widget!(vertical_paper) .key("list") .with_props(ContentBoxItemLayout { anchors: 0.5.into(), margin: Rect { left: -200.0, right: -200.0, top: -100.0, bottom: -100.0, }, ..Default::default() }) .with_props(VerticalBoxProps { separation: 20.0, ..Default::default() }) .listed_slot( make_widget!(wrap_box) .key("text-wrap") .with_props(WrapBoxProps { margin: 16.0.into(), ..Default::default() }) .named_slot( "content", make_widget!(text_paper) .key("text") .with_props(TextPaperProps { text, height: TextBoxSizeValue::Exact(24.0), variant: "title".to_owned(), ..Default::default() }), ), ) .listed_slot( make_widget!(horizontal_box) .key("buttons") .listed_slot( make_widget!(wrap_box) .key("yes-wrap") .with_props(WrapBoxProps { margin: 16.0.into(), ..Default::default() }) .named_slot( "content", make_widget!(text_button_paper) .key("yes") .with_props(TextPaperProps { text: "YES".to_owned(), height: TextBoxSizeValue::Exact(24.0), variant: "button".to_owned(), ..Default::default() }) .with_props(WrapBoxProps { margin: 16.0.into(), ..Default::default() }) .with_props(NavItemActive) .with_props(ButtonNotifyProps(id.to_owned().into())), ), ) .listed_slot( make_widget!(wrap_box) .key("no-wrap") .with_props(WrapBoxProps { margin: 16.0.into(), ..Default::default() }) .named_slot( "content", make_widget!(text_button_paper) .key("no") .with_props(TextPaperProps { text: "NO".to_owned(), height: TextBoxSizeValue::Exact(24.0), variant: "button".to_owned(), ..Default::default() }) .with_props(WrapBoxProps { margin: 16.0.into(), ..Default::default() }) .with_props(NavItemActive) .with_props(ButtonNotifyProps(id.to_owned().into())), ), ), ), ) .into() } ================================================ FILE: demos/todo-app/src/ui/components/mod.rs ================================================ pub mod app; pub mod app_bar; pub mod confirm_box; pub mod tasks_list; ================================================ FILE: demos/todo-app/src/ui/components/tasks_list.rs ================================================ use crate::{ model::{AppState, TaskProps}, ui::components::confirm_box::{ConfirmBoxProps, ConfirmNotifyMessage, confirm_box}, }; use raui::{ core::{ Prefab, PropsData, make_widget, pre_hooks, props::PropsData, widget::{ component::{ containers::{ hidden_box::{HiddenBoxProps, hidden_box}, horizontal_box::HorizontalBoxProps, vertical_box::{VerticalBoxProps, vertical_box}, }, interactive::{ button::{ButtonNotifyMessage, ButtonNotifyProps}, navigation::{NavContainerActive, NavItemActive}, scroll_view::ScrollViewRange, }, }, context::WidgetContext, node::WidgetNode, unit::{ content::ContentBoxItemLayout, flex::FlexBoxItemLayout, text::TextBoxSizeValue, }, }, }, material::{ component::{ containers::{ horizontal_paper::horizontal_paper, paper::PaperContentLayoutProps, scroll_paper::{scroll_paper, scroll_paper_side_scrollbars}, }, icon_paper::{IconImage, IconPaperProps}, interactive::{ icon_button_paper::icon_button_paper, switch_button_paper::switch_button_paper, }, switch_paper::SwitchPaperProps, text_paper::{TextPaperProps, text_paper}, }, theme::{ThemeColor, ThemeVariant, ThemedWidgetProps}, }, }; use serde::{Deserialize, Serialize}; #[derive(PropsData, Debug, Default, Copy, Clone, Serialize, Deserialize)] struct TaskState { #[serde(default)] deleting: bool, } fn use_task(context: &mut WidgetContext) { context.life_cycle.change(|mut context| { let mut app_state = context .view_models .view_model_mut(AppState::VIEW_MODEL) .unwrap() .write::() .unwrap(); for msg in context.messenger.messages { if let Some(msg) = msg.as_any().downcast_ref::() { if msg.trigger_start() { match msg.sender.key() { "checkbox" => { if let Ok(index) = context.id.key().parse::() { app_state.toggle_task(index); } } "delete" => { let _ = context.state.write_with(TaskState { deleting: true }); } _ => {} } } } else if let Some(msg) = msg.as_any().downcast_ref::() { let _ = context.state.write_with(TaskState { deleting: false }); if msg.confirmed && let Ok(index) = context.id.key().parse::() { app_state.delete_task(index); } } } }); } #[pre_hooks(use_task)] pub fn task(mut context: WidgetContext) -> WidgetNode { let WidgetContext { id, key, props, state, .. } = context; let data = props.read_cloned_or_default::(); let TaskState { deleting } = state.read_cloned_or_default(); make_widget!(horizontal_paper) .key(key) .with_props(HorizontalBoxProps { separation: 10.0, ..Default::default() }) .with_props(ContentBoxItemLayout { margin: 10.0.into(), ..Default::default() }) .listed_slot( make_widget!(switch_button_paper) .key("checkbox") .with_props(FlexBoxItemLayout { fill: 0.0, grow: 0.0, shrink: 0.0, align: 0.5, ..Default::default() }) .with_props(SwitchPaperProps { on: data.done, variant: "checkbox".to_owned(), size_level: 2, }) .with_props(NavItemActive) .with_props(ButtonNotifyProps(id.to_owned().into())) .with_props(ThemedWidgetProps { color: ThemeColor::Primary, variant: ThemeVariant::ContentOnly, ..Default::default() }), ) .listed_slot( make_widget!(text_paper) .key("name") .with_props(TextPaperProps { text: data.name, height: TextBoxSizeValue::Exact(24.0), variant: "title".to_owned(), ..Default::default() }) .with_props(FlexBoxItemLayout { align: 0.5, ..Default::default() }), ) .listed_slot( make_widget!(icon_button_paper) .key("delete") .with_props(FlexBoxItemLayout { fill: 0.0, grow: 0.0, shrink: 0.0, align: 0.5, ..Default::default() }) .with_props(IconPaperProps { image: IconImage { id: "resources/icons/delete.png".to_owned(), ..Default::default() }, size_level: 2, ..Default::default() }) .with_props(NavItemActive) .with_props(ButtonNotifyProps(id.to_owned().into())) .with_props(ThemedWidgetProps { color: ThemeColor::Primary, variant: ThemeVariant::ContentOnly, ..Default::default() }), ) .listed_slot( make_widget!(hidden_box) .with_props(HiddenBoxProps(!deleting)) .named_slot( "content", make_widget!(confirm_box) .key("confirm") .with_props(ConfirmBoxProps { text: "Do you want to remove task?".to_owned(), notify: id.to_owned().into(), }), ), ) .into() } fn use_tasks_list(context: &mut WidgetContext) { context.life_cycle.mount(|mut context| { context .view_models .bindings(AppState::VIEW_MODEL, AppState::TASKS) .unwrap() .bind(context.id.to_owned()); }); } #[pre_hooks(use_tasks_list)] pub fn tasks_list(mut context: WidgetContext) -> WidgetNode { let WidgetContext { key, view_models, .. } = context; let app_state = view_models .view_model(AppState::VIEW_MODEL) .unwrap() .read::() .unwrap(); let mut tasks = app_state .tasks() .enumerate() .map(|(index, item)| { make_widget!(task) .key(index) .with_props(item.to_owned()) .with_props(FlexBoxItemLayout { grow: 0.0, shrink: 0.0, ..Default::default() }) }) .collect::>(); tasks.reverse(); make_widget!(scroll_paper) .key(key) .with_props(NavContainerActive) .with_props(NavItemActive) .with_props(ScrollViewRange::default()) .with_props(PaperContentLayoutProps(ContentBoxItemLayout { margin: 10.0.into(), ..Default::default() })) .named_slot( "content", make_widget!(vertical_box) .key("list") .with_props(VerticalBoxProps { separation: 30.0, ..Default::default() }) .listed_slots(tasks), ) .named_slot( "scrollbars", make_widget!(scroll_paper_side_scrollbars).key("scrollbars"), ) .into() } ================================================ FILE: demos/todo-app/src/ui/mod.rs ================================================ pub mod components; ================================================ FILE: justfile ================================================ # List the just recipe list list: just --list # Bake the README.md from the template readme: cargo readme -r ./crates/_ > README.md format: cargo fmt --all build: cargo build --all cargo build --examples clippy: cargo clippy --all test: cargo test --all --features all cargo test --all --examples --features all example NAME="setup": cargo run --example {{NAME}} demo NAME="todo-app": cd ./demos/{{NAME}} && cargo run guide NAME: cd ./site/rust/guide_{{NAME}} && cargo run # Mandatory checks to run before pushing changes to repository checks: just format just build just clippy just test just readme # Print the documentation coverage for a crate in the workspace doc-coverage crate="raui-core": cargo +nightly rustdoc -p {{crate}} -- -Z unstable-options --show-coverage clean: find . -name target -type d -exec rm -r {} + just remove-lockfiles remove-lockfiles: find . -name Cargo.lock -type f -exec rm {} + list-outdated: cargo outdated -R -w # Run the Rust doctests in the website docs website-doc-tests: cargo build --features all -p raui --target-dir target/doctests @set -e; \ for file in $(find site/content/ -name '*.md'); do \ echo "Testing: $file"; \ rustdoc \ --edition 2018 \ --extern raui \ --crate-name docs-test \ $file \ --test \ -L target/doctests/debug/deps; \ done website-live-dev: cd site && zola serve update: cargo update --manifest-path ./crates/derive/Cargo.toml --aggressive cargo update --manifest-path ./crates/core/Cargo.toml --aggressive cargo update --manifest-path ./crates/material/Cargo.toml --aggressive cargo update --manifest-path ./crates/retained/Cargo.toml --aggressive cargo update --manifest-path ./crates/immediate/Cargo.toml --aggressive cargo update --manifest-path ./crates/immediate-widgets/Cargo.toml --aggressive cargo update --manifest-path ./crates/json-renderer/Cargo.toml --aggressive cargo update --manifest-path ./crates/tesselate-renderer/Cargo.toml --aggressive cargo update --manifest-path ./crates/app/Cargo.toml --aggressive cargo update --manifest-path ./crates/_/Cargo.toml --aggressive publish: cargo publish --no-verify --manifest-path ./crates/derive/Cargo.toml sleep 1 cargo publish --no-verify --manifest-path ./crates/core/Cargo.toml sleep 1 cargo publish --no-verify --manifest-path ./crates/material/Cargo.toml sleep 1 cargo publish --no-verify --manifest-path ./crates/retained/Cargo.toml sleep 1 cargo publish --no-verify --manifest-path ./crates/immediate/Cargo.toml sleep 1 cargo publish --no-verify --manifest-path ./crates/immediate-widgets/Cargo.toml sleep 1 cargo publish --no-verify --manifest-path ./crates/json-renderer/Cargo.toml sleep 1 cargo publish --no-verify --manifest-path ./crates/tesselate-renderer/Cargo.toml sleep 1 cargo publish --no-verify --manifest-path ./crates/app/Cargo.toml sleep 1 cargo publish --no-verify --manifest-path ./crates/_/Cargo.toml ================================================ FILE: site/.gitignore ================================================ public ================================================ FILE: site/.markdownlint.yml ================================================ # Disable unwanted lints MD031: false # Spaces around codeblocks MD013: false # 80 char line length limit ================================================ FILE: site/config.toml ================================================ # The URL the site will be built for base_url = "https://RAUI-labs.github.io/raui" title = "RAUI" description = "Renderer Agnostic User Interface written in Rust" theme = "adidoks" # The default language; used in feeds and search index # Note: the search index doesn't support Chinese/Japanese/Korean Languages default_language = "en" # Whether to automatically compile all Sass files in the sass directory compile_sass = true # Whether to generate a feed file for the site # generate_feed = true # When set to "true", the generated HTML files are minified. minify_html = false # The taxonomies to be rendered for the site and their configuration. taxonomies = [ {name = "authors"}, # Basic definition: no feed or pagination ] # Whether to build a search index to be used later on by a JavaScript library # When set to "true", a search index is built from the pages and section # content for `default_language`. build_search_index = true [search] # Whether to include the title of the page/section in the index include_title = true # Whether to include the description of the page/section in the index include_description = true # Whether to include the rendered content of the page/section in the index include_content = true [markdown] # Whether to do syntax highlighting. # Theme can be customised by setting the `highlight_theme` # variable to a theme supported by Zola highlight_code = true [extra] # Put all your custom variables here author = "PsichiX" github = "https://github.com/PsichiX" twitter = "https://twitter.com/psichix" email = "psichix@gmail.com" # If running on netlify.app site, set to true is_netlify = true # Set HTML file language language_code = "en-US" # Set HTML theme color theme_color = "#fff" # More about site's title title_separator = "|" # set as |, -, _, etc title_addition = "Renderer Agnostic User Interface" # Set date format in blog publish metadata timeformat = "%B %e, %Y" # e.g. June 14, 2021 timezone = "America/New_York" # Edit page on reposity or not edit_page = true docs_repo = "https://github.com/RAUI-labs/raui" repo_branch = "next" ## Math settings # options: true, false. Enable math support globally, # default: false. You can always enable math on a per page. math = false library = "katex" # options: "katex", "mathjax". default is "katex". ## Open Graph + Twitter Cards [extra.open] enable = false # this image will be used as fallback if a page has no image of its own image = "doks.png" twitter_site = "aaranxu" twitter_creator = "aaranxu" facebook_author = "ichunyun" facebook_publisher = "ichunyun" og_locale = "en_US" ## JSON-LD [extra.schema] type = "Organization" logo = "logo-doks.png" twitter = "" linked_in = "" github = "https://github.com/RAUI-labs" section = "blog" # see config.extra.main~url ## Sitelinks Search Box site_links_search_box = false # Menu items [[extra.menu.main]] name = "Docs" section = "docs" url = "docs/about/introduction/" weight = 10 [[extra.menu.main]] name = "Blog" section = "blog" url = "/blog/" weight = 20 # [[extra.menu.main]] # name = "Examples" # section = "examples" # url = "/examples/" # weight = 30 # [[extra.menu.social]] # name = "Twitter" # pre = "" # url = "https://twitter.com/aaranxu" # weight = 10 [[extra.menu.social]] name = "GitHub" pre = "" url = "https://github.com/RAUI-labs/raui" post = "v0.1.0" weight = 20 # Footer contents [extra.footer] info = "Powered by Zola and AdiDoks" # [[extra.footer.nav]] # name = "Privacy" # url = "/privacy-policy/" # weight = 10 # [[extra.footer.nav]] # name = "Code of Conduct" # url = "/docs/contributing/code-of-conduct/" # weight = 20 # The homepage contents [extra.home] title = "RAUI — Renderer Agnostic UI" lead = "RAUI is a renderer agnostic UI, written in Rust, that is heavely inspired by React declarative UI composition and Unreal Engine Slate widget components system" url = "/docs/about/introduction/" url_button = "Get started" repo_version = "Hosted on GitHub." repo_license = "Open-source MIT License." repo_url = "https://github.com/RAUI-labs/raui" [[extra.home.list]] title = "Use With Any Renderer! 🎮" content = "Easily integrate with your favorite Rust game engine or toolkit." [[extra.home.list]] title = "Built-in Tesselation Renderer 📐" content = "RAUI features a built-in tesselator renderer that allows easily hooking into any renderer that can render triangles!" [[extra.home.list]] title = "Or Do it Yourself! 👨‍🏭" content = "If triangles aren't your thing, you can implement custom rendering of RAUI's WidgetNode however you want." [[extra.home.list]] title = "Flexible Design 📈" content = """Easily create your own widget components simply by defining functions""" [[extra.home.list]] title = "Existing Integrations 🏗" content = """RAUI is being used in Oxygengine and Bevy Retro, but slowly new integrations will be made.""" ================================================ FILE: site/content/authors/_index.md ================================================ +++ title = "Authors" description = "The writers of our blog posts." draft = false # If you add a new author page in this section, please add a new item, # and the format is as follows: # # "author-name-in-url" = "the-full-path-of-the-author-page" # # Note: We use quoted keys here. [extra.author_pages] "zicklag" = "authors/zicklag.md" +++ The authors of the blog articles. ================================================ FILE: site/content/authors/psichix.md ================================================ +++ title = "PsichiX" description = "Creator of RAUI" date = 2021-04-01T08:50:45+00:00 updated = 2021-04-01T08:50:45+00:00 draft = false +++ The creator of RAUI. [@PsichiX](https://github.com/PsichiX) ================================================ FILE: site/content/authors/zicklag.md ================================================ +++ title = "Zicklag" description = "Contributor to RAUI" date = 2021-04-01T08:50:45+00:00 updated = 2021-04-01T08:50:45+00:00 draft = false +++ A contributor to RAUI that works on making games [@katharostech]. [@zicklag](https://github.com/zicklag) [@katharostech]: https://katharostech.com ================================================ FILE: site/content/blog/_index.md ================================================ +++ title = "Blog" description = "Blog" sort_by = "date" paginate_by = 5 template = "blog/section.html" +++ ================================================ FILE: site/content/blog/new-documentation-site.md ================================================ +++ title = "New RAUI Documentation Site! 🎉" description = "RAUI has a new documentation site!" date = 2021-05-11T15:07:00+00:00 updated = 2021-05-11T15:07:00+00:00 draft = false template = "blog/page.html" [taxonomies] authors = ["zicklag"] [extra] lead = "We've just finished making RAUI's new documentation site and adding a ton of extra API documentation. We hope this will help make it easier to get started with RAUI bring some more polish to the developer experience." +++ With this new blog section we will also be able to make development announcements to help keep the community in the loop about what's going on with the project. We've made a lot of progress but there's still tons of work to do. If you find bugs, or have feature requests, don't hesitate to open a [GitHub issue][issue]! And if you have questions or comments feel free to open a [discussion]. [issue]: https://github.com/RAUI-labs/raui/issues [discussion]: https://github.com/RAUI-labs/raui/discussions ================================================ FILE: site/content/docs/_index.md ================================================ +++ title = "Docs" description = "Documentation" weight = 1 template = "docs/section.html" +++ ================================================ FILE: site/content/docs/about/_index.md ================================================ +++ title = "About" template = "docs/section.html" weight = 0 draft = false +++ ================================================ FILE: site/content/docs/about/introduction.md ================================================ +++ title = "Introduction" weight = 10 sort_by = "weight" template = "docs/page.html" [extra] lead = "RAUI is a renderer agnostic UI, written in Rust, that is heavely inspired by React declarative UI composition and Unreal Engine Slate widget components system." toc = true top = false +++ ## About RAUI is designed to be flexible and easy-to-use, allowing you to quickly build your own UI components by combining existing components into more elaborate structures. The main idea behind the RAUI architecture is to treat the UI as data that gets transformed into rendering data for your renderer of choice. ## API Docs For a more in-depth explanation of the RAUI architecture see the [API documentation][api]. The API docs have explanations of each piece of RAUI and how the everything is put together. [api]: https://docs.rs/raui ## Guide For a more guide-level explanation, continue on! We're going to walk you through creating your first RAUI app. ================================================ FILE: site/content/docs/getting-started/01-setting-up.md ================================================ +++ title = "Setting Up" description = "Learn how to get a window setup so RAUI can render to it." draft = false weight = 1 template = "docs/page.html" slug = "setting-up" [extra] lead = "First we're going to get a window setup so RAUI can render to it." toc = true top = false +++ ## Creating the Project Let's create a new Rust project and add our dependencies. Create a new cargo project: ```bash cargo new --bin my_project ``` Then add the following dependencies to the `Cargo.toml`: ```toml [dependencies] # The RAUI mother-crate raui = { version = "*", features = ["app"] } ``` ## Initializing The Window Next we need to setup our UI window. Using the [`raui-app`] crate this is super easy! In most cases you will probably want to integrate RAUI with a game engine or other renderer, and in that case you would not use [`raui-app`], you would use an integration crate like [`raui-tesselation-renderer`]. For now, though, we want to get right into RAUI without having to worry about integrations. [`raui-app`]: https://docs.rs/raui-app [`raui-tesselation-renderer`]: https://docs.rs/raui-tesselation-renderer Go ahead and add the following to your `main.rs` file: {{ rust_code_snippet(path="rust/guide_01/src/main.rs") }} We don't add any widgets yet, we'll get to that in the next step. At this point you should be able to `cargo run` and have a blank window pop up! OK, not that cool. We're not here for a blank window, so let's go put some GUI on the screen! > **Note:** You can find the full code for this chapter [here](https://github.com/RAUI-labs/raui/tree/master/site/rust/guide_01) ================================================ FILE: site/content/docs/getting-started/02-your-first-widget/index.md ================================================ +++ title = "Your First Widget" description = "Learn how to create your first widget" draft = false weight = 2 template = "docs/page.html" slug = "your-first-widget" [extra] lead = "Now we get to create our first RAUI widget!" toc = true top = false +++ ## What is a Widget? Before we create a widget, let's talk about what a widget actually _is_. To be precise, there are a few different types of widgets and the type widget that we will be creating is a **_widget component_**. Widget components are made out of normal Rust functions that have a specific signature. The job of widget components is to look at it's properties and child widgets ( if it has any ), to do any processing that it needs to, and to output a new widget tree that will rendered in place of the component. Because components can return whole new widget trees, they can combine the functionality of multiple _other_ components together. This allows you to build components into larger structures like LEGO® bricks, where a complex UI can be made out of many simple pieces put together in different combinations. This high modularity is the true power of RAUI, your imagination is the only limit! ## Making a Widget Without further ado, let's make a widget! Update your `main.rs` file to look like this: {{ rust_code_snippet(path="rust/guide_02/src/main.rs")}} There's plenty of comments in the above example, but there's a lot going on here so lets break it down piece by piece. ### Setting the Widget Tree The first thing we changed was to add our app component to the widget tree in our `DeclarativeApp` instance. {{ rust_code_snippet(path="rust/guide_02/src/main.rs", start=3, end=5)}} Every RAUI app has a root widget tree that holds the whole structure of application in it. It is essentially the "main" of the UI. In this case we set our widget to a single `app` component. This is the `app` component that we define in the function below. Here we use the strategy of keeping our root widget tree very simple and putting all of our real logic in the `app` component. ### The `app` Component Now we get to the definition of our `app` component: {{ rust_code_snippet(path="rust/guide_02/src/main.rs", start=9, end=9)}} As we mentioned before, components are just normal Rust functions with a specific signature. Components are required to take a [`WidgetContext`] as an argument and they must return a [`WidgetNode`]. [`WidgetContext`]: https://docs.rs/raui/latest/raui/core/widget/context/struct.WidgetContext.html [`WidgetNode`]: https://docs.rs/raui/latest/raui/core/widget/node/enum.WidgetNode.html #### [`WidgetContext`] The widget context is the way that a widget can access it's own state, properties, children, and other information that may be important to the function of the widget. It also allows the widget to respond to events or send messages to other widgets. We will learn more about the [`WidgetContext`] later. #### [`WidgetNode`] That brings us to what a **widget _node_** is. As we mentioned above, there are a few different kinds of _widgets_. Widget components are one of them and widget nodes are another. The easiest way to think of a [`WidgetNode`] is that it is a tree of other widgets. [`WidgetNode`]s are most commonly created with the [`make_widget!`] macro and filled with builder pattern of [`WidgetComponent`]. > **Note:** There is a third kind of widget is called a [`WidgetUnit`], but you don't usually need to think about those, since they are the final representation of processed widgets tree. We can see the [`make_widget!`] macro with builder pattern in action in our example: {{ rust_code_snippet(path="rust/guide_02/src/main.rs", start=16, end=33)}} We use that pattern to create a simple tree with a single [`text_box`] component in it and we apply our `text_box_props` to it to configure how the text box renders the text inside. [`text_box`]: https://docs.rs/raui/latest/raui/core/widget/component/text_box/fn.text_box.html [`make_widget!`]: https://docs.rs/raui/latest/raui/core/macro.make_widget.html [`WidgetUnit`]: https://docs.rs/raui/latest/raui/core/widget/unit/enum.WidgetUnit.html [`WidgetComponent`]: https://docs.rs/raui/latest/raui/core/widget/component/enum.WidgetComponent.html #### Properties That brings us to the concept of **_properties_**. Properties are data, made up of Rust structs, that can be applied to components additively to customize their behavior. In this case we created [`TextBoxProps`] data that we used to configure the [`text_box`] component. {{ rust_code_snippet(path="rust/guide_02/src/main.rs", start=17, end=32)}} We use the properties to configure the font, content, and color of our text. We are not limited to using just one struct for our property data. We could add any number of different properties structs to our component, allowing us to configure how the component responds to layout, for instance. We will see how to do more of that later. [`TextBoxProps`]: https://docs.rs/raui/0.34.0/raui/core/widget/component/text_box/struct.TextBoxProps.html > **Note:** You can download the [Verdana] font or use your own font to follow along. We chose to place it in a `resources/` folder adjacent to our `Cargo.toml` file in our Rust project, but you can place it wherever you like as long as you update the path to the font in the [`TextBoxProps`]. [Verdana]: https://github.com/PsichiX/raui/raw/master/site/rust/guide_02/resources/verdana.ttf ## Summary Now that we've explained it all, go try it out! When you `cargo run` you should get a window displaying your "Hello World!". ![hello world screenshot](hello_world.png) > **Note:** You can find the whole code for this chapter [here](https://github.com/RAUI-labs/raui/tree/master/site/rust/guide_02). ## 🚧 Under Construction 👷 There should be more to this guide but it isn't written yet! If you've gotten this far, congratulations and thank you for reading! Come back later and see whether or not more of the guide has been written. If you need help or have questions, feel free to open up a [discussion] on GitHub. 👋 [discussion]: https://github.com/RAUI-labs/raui/discussions ================================================ FILE: site/content/docs/getting-started/03-containers/index.md ================================================ +++ title = "Containers" description = "Learn how to use container widgets." draft = false weight = 3 template = "docs/page.html" slug = "containers" [extra] lead = "Now we're going to try out some built-in container widgets that will allow us to organize our content and build layouts." toc = true top = false +++ RAUI comes with a number of built-in container components that can be used to help group and lay out other components. You can find the list of core container types in the [API documentation][containers]. [containers]: https://docs.rs/raui/latest/raui/core/widget/component/containers/index.html ## Content Box The first container we'll look at is the [`content_box`]. Content boxes are simple containers that can hold multiple items, where each item's position is **not** effected by any of the other items in the box. A couple useful features of [`content_box`]s are that they can have a [transform][`contentboxprops::transform`] applied to them to scale, position, rotate, etc. the box and everything in it, and items inside them can specify [layout attributes][`contentboxitemlayout`] such as margin, alignment, and anchor. > **Note:** A content box's transform is purely a _visual_ effect and has no effect on the layout of the box or the regions used to detect clicks, hovers, etc. for the box or it's children. It is used primarily in animations and visual effects, not for general purpose positioning. In our demo app we've been working on, let's put our text in a content box so that we can set a small margin around our text. [`contentboxitemlayout`]: https://docs.rs/raui/latest/raui/core/widget/unit/content/struct.ContentBoxItemLayout.html [`contentboxprops::transform`]: https://docs.rs/raui/latest/raui/core/widget/component/containers/content_box/struct.ContentBoxProps.html#structfield.transform [`content_box`]: https://docs.rs/raui/latest/raui/core/widget/component/containers/content_box/fn.content_box.html {% rustdoc_test() %} ```rust # use raui::import_all::*; /// Our app widget from earlier pub fn app(_ctx: WidgetContext) -> WidgetNode { // Create our text box properties let text_box_props = Props::new(TextBoxProps { text: "Hello World!".into(), color: Color { r: 0.0, g: 0.0, b: 0.0, a: 1.0, }, font: TextBoxFont { // We specify the path to our font name: "resources/verdana.ttf".to_owned(), size: 60.0, }, // Use the defaults for the rest of the text box settings ..Default::default() }) // Here we use the `with` function on `Props` _add_ to another property to our // text box props list. In this case we add the `ContentBoxItemLayout` struct to // influence its layout when it is a child of a `content_box`. .with(ContentBoxItemLayout { // Specify a margin of 10 on every side margin: 10.0.into(), // Use the default value for the rest of the layout options ..Default::default() }); make_widget!(content_box) .listed_slot(make_widget!(text_box).merge_props(text_box_props)) .into() } ``` {% end %} Now if we run our app we should have a small margin between the text and the sides of the window. But what if we want to add another item to our box, like maybe an image? Let's see what happens if we place an image in the content box with our text: {% rustdoc_test() %} ```rust # use raui::import_all::*; pub fn app(_ctx: WidgetContext) -> WidgetNode { // Create our text box properties // ... # let text_box_props = Props::new(TextBoxProps { # text: "Hello World!".into(), # color: Color { # r: 0.0, # g: 0.0, # b: 0.0, # a: 1.0, # }, # font: TextBoxFont { # // We specify the path to our font # name: "resources/verdana.ttf".to_owned(), # size: 60.0, # }, # // Use the defaults for the rest of the text box settings # ..Default::default() # }) # .with(ContentBoxItemLayout { # // Specify a margin of 10 on every side # margin: 10.0.into(), # // Use the default value for the rest of the layout options # ..Default::default() # }); // Create the props for our image let image_box_props = Props::new(ImageBoxProps { // The material defines what image or color to use for the box material: ImageBoxMaterial::Image(ImageBoxImage { // The path to our image id: "resources/cats.jpg".to_owned(), ..Default::default() }), // Have the image fill it's container width: ImageBoxSizeValue::Fill, height: ImageBoxSizeValue::Fill, ..Default::default() }); make_widget!(content_box) .listed_slot(make_widget!(image_box).merge_props(image_box_props)) .listed_slot(make_widget!(text_box).merge_props(text_box_props)) .into() } ``` {% end %} ![text and image](text-and-image.png) > **Note:** You can download the demo cat image [here](https://github.com/PsichiX/raui/raw/master/site/rust/guide_03/resources/cats.jpg). Notice that the content box didn't make sure there was any "room" for the text or the image to sit side-by-side with each-other, it just stacked them right on top of each-other. Also If we wanted to have them line up without overlapping we would use a [`flex_box`]. [`flex_box`]: https://docs.rs/raui/latest/raui/core/widget/component/containers/flex_box/fn.flex_box.html ## Flex Box Flex boxes in RAUI are similar to [CSS flexboxes][css_flexboxes]. Flex boxes lay things out in a row or a column depending on whether or not their _direction_. For convenience, RAUI has [`vertical_box`] and [`horizontal_box`] components that are simply [`flex_box`] component with their direction set to vertical or horizontal by default. > **Note:** For a more in-depth explanation of how layout works in RAUI see [Layout in Depth](../../layout/layout-in-depth). [css_flexboxes]: https://www.w3schools.com/css/css3_flexbox.asp [`horizontal_box`]: https://docs.rs/raui/latest/raui/core/widget/component/containers/horizontal_box/fn.horizontal_box.html [`vertical_box`]: https://docs.rs/raui/latest/raui/core/widget/component/containers/vertical_box/fn.vertical_box.html Let's go ahead and use a [`vertical_box`] to put our text above our cat photo: {% rustdoc_test() %} ```rust # use raui::import_all::*; pub fn app(_ctx: WidgetContext) -> WidgetNode { // Create our text box properties let text_box_props = Props::new(TextBoxProps { text: "Hello World!".into(), color: Color { r: 0.0, g: 0.0, b: 0.0, a: 1.0, }, font: TextBoxFont { // We specify the path to our font name: "resources/verdana.ttf".to_owned(), size: 60.0, }, // Use the defaults for the rest of the text box settings ..Default::default() }) // Notice that we now use a `FlexBoxItemLayout` instead of a `ContentBoxItemLayout` // because we are putting it in a flex box instead of a content box .with(FlexBoxItemLayout { margin: Rect { // Let's just set a left margin this time left: 30., ..Default::default() }, ..Default::default() }); // Create the props for our image let image_box_props = Props::new(ImageBoxProps { material: ImageBoxMaterial::Image(ImageBoxImage { id: "resources/cats.jpg".to_owned(), ..Default::default() }), ..Default::default() }); // Use a vertical_box instead of a content_box make_widget!(vertical_box) // Now because the text and image won't overlap, let's put // the text above the image .listed_slot(make_widget!(text_box).merge_props(text_box_props)) .listed_slot(make_widget!(image_box).merge_props(image_box_props)) .into() } ``` {% end %} ![flex box layout](flex-box.png) There we go! Notice that with the flex box layout, the text box and the image box are both taking up an equal amount of space vertically. That's how flex boxes work by default. Each item in the box will get an equal amount of space along the flex box's direction. This is configurable using the other settings in the [`FlexBoxItemLayout`] struct. [`flexboxitemlayout`]: htps://docs.rs/raui/latest/raui/core/widget/unit/flex/struct.FlexBoxItemLayout.html Next we'll learn how to make our UI react to user input. > **Note:** You can access the full source code for this chapter [here](https://github.com/RAUI-labs/raui/tree/master/site/rust/guide_03). ## 🚧 Under Construction 👷 There should be more to this guide but it isn't written yet! If you've gotten this far, congratulations and thank you for reading! Come back later and see whether or not more of the guide has been written. If you need help or have questions, feel free to open up a [discussion] on GitHub. 👋 [discussion]: https://github.com/PsichiX/raui/discussions ================================================ FILE: site/content/docs/getting-started/_index.md ================================================ +++ title = "Getting Started" description = "Quick start and guides for getting RAUI up and ready." template = "docs/section.html" sort_by = "weight" weight = 10 draft = false +++ ================================================ FILE: site/content/docs/layout/01-layout-in-depth.md ================================================ +++ title = "Layout In Depth" description = "Detailed explanation of how layout works in RAUI by default" draft = false weight = 1 template = "docs/page.html" slug = "layout-in-depth" [extra] lead = "RAUI comes out of the box with a swap-able layout engine. Here's an explanation of how the default layout engine in RAUI works." toc = true top = false +++ > **🚧 Under Construction:** This section is coming soon! ================================================ FILE: site/content/docs/layout/_index.md ================================================ +++ title = "Layout" description = "Explanations of how layout works in RAUI" template = "docs/section.html" sort_by = "weight" weight = 20 draft = false +++ ================================================ FILE: site/content/examples/_index.md ================================================ +++ title = "Examples" description = "Examples" weight = 2 template = "docs/section.html" +++ List of examples showcasing day-to-day usecases. ================================================ FILE: site/rust/guide_01/Cargo.toml ================================================ [package] name = "guide_01" version = "0.1.0" authors = ["RAUI Contributors"] edition = "2024" publish = false [dependencies] # The main RAUI crate raui = { version = "0.70", path = "../../../crates/_", features = [ "app", "import-all", ] } ================================================ FILE: site/rust/guide_01/src/main.rs ================================================ use raui::{app::app::declarative::DeclarativeApp, core::widget::node::WidgetNode}; fn main() { DeclarativeApp::simple("RAUI Guide", WidgetNode::default()); } ================================================ FILE: site/rust/guide_02/Cargo.toml ================================================ [package] name = "guide_02" version = "0.1.0" authors = ["RAUI Contributors"] edition = "2024" publish = false [dependencies] # The main RAUI crate raui = { version = "0.70", path = "../../../crates/_", features = [ "app", "import-all", ] } ================================================ FILE: site/rust/guide_02/src/main.rs ================================================ use raui::{ app::app::declarative::DeclarativeApp, core::{ make_widget, widget::{ component::text_box::{TextBoxProps, text_box}, context::WidgetContext, node::WidgetNode, unit::text::TextBoxFont, utils::Color, }, }, }; fn main() { DeclarativeApp::simple("RAUI Guide", make_widget!(app)); } /// We create our own widget by making a function that takes a `WidgetContext` /// and that returns `WidgetNode`. pub fn app(_ctx: WidgetContext) -> WidgetNode { // Our _ctx variable starts with an underscore so rust doesn't complain // that it is unused. We will be using the context later in the guide. // We may do any amount of processing in the body of the function. // For now we will simply be creating a text box properties struct that we // will use to configure the `text_box` component. make_widget!(text_box) .with_props(TextBoxProps { text: "Hello world!".to_owned(), color: Color { r: 0.0, g: 0.0, b: 0.0, a: 1.0, }, font: TextBoxFont { // We specify the path to our font name: "resources/verdana.ttf".to_owned(), size: 60.0, }, // Use the defaults for the rest of the text box settings ..Default::default() }) .into() } ================================================ FILE: site/rust/guide_03/Cargo.toml ================================================ [package] name = "guide_03" version = "0.1.0" authors = ["RAUI Contributors"] edition = "2024" publish = false [dependencies] # The main RAUI crate raui = { version = "0.70", path = "../../../crates/_", features = [ "app", "import-all", ] } ================================================ FILE: site/rust/guide_03/src/main.rs ================================================ use raui::{ app::app::declarative::DeclarativeApp, core::{ make_widget, widget::{ component::{ containers::vertical_box::vertical_box, image_box::{ImageBoxProps, image_box}, text_box::{TextBoxProps, text_box}, }, context::WidgetContext, node::WidgetNode, unit::{ flex::FlexBoxItemLayout, image::{ImageBoxAspectRatio, ImageBoxImage, ImageBoxMaterial}, text::{TextBoxFont, TextBoxHorizontalAlign}, }, utils::{Color, Rect}, }, }, }; fn main() { DeclarativeApp::simple("RAUI Guide", make_widget!(app)); } // Our app widget from earlier pub fn app(_ctx: WidgetContext) -> WidgetNode { make_widget!(vertical_box) .listed_slot( make_widget!(text_box) .with_props(TextBoxProps { text: "Hello World!".into(), color: Color { r: 0.0, g: 0.0, b: 0.0, a: 1.0, }, font: TextBoxFont { // We specify the path to our font name: "resources/verdana.ttf".to_owned(), size: 60.0, }, horizontal_align: TextBoxHorizontalAlign::Center, // Use the defaults for the rest of the text box settings ..Default::default() }) // Notice that we now use a `FlexBoxItemLayout` instead of a `ContentBoxItemLayout` // because we are putting it in a flex box instead of a content box .with_props(FlexBoxItemLayout { basis: Some(80.0), grow: 0.0, shrink: 0.0, margin: Rect { // Let's just set a left margin this time left: 30., ..Default::default() }, ..Default::default() }), ) .listed_slot(make_widget!(image_box).with_props(ImageBoxProps { // The material defines what image or color to use for the box material: ImageBoxMaterial::Image(ImageBoxImage { // The path to our image id: "resources/cats.jpg".to_owned(), ..Default::default() }), // this allows image content to not stretch to fill its container. content_keep_aspect_ratio: Some(ImageBoxAspectRatio { horizontal_alignment: 0.5, vertical_alignment: 0.5, ..Default::default() }), ..Default::default() })) .into() } ================================================ FILE: site/static/.nojekyll ================================================ ================================================ FILE: site/templates/shortcodes/code_snippet.md ================================================ {% set lines = load_data(path=path, format="plain") | split(pat="\n") -%} {% set lines_count = lines | length() -%} {% if not start -%} {% set start = 0 -%} {% else %} {% set start = start - 1 %} {% endif -%} {% if not end -%} {% set end = lines_count -%} {% endif -%} {% if not lang -%} {% set lang = "rust" -%} {% endif -%} ```{{lang}} {{ lines | slice(start=start, end=end) | join(sep=" ") | trim_end}} ``` ================================================ FILE: site/templates/shortcodes/include_markdown.md ================================================ {{ load_data(path=path) }} ================================================ FILE: site/templates/shortcodes/rust_code_snippet.md ================================================ {% set lines = load_data(path=path, format="plain") | split(pat="\n") -%} {% set lines_count = lines | length() -%} {% if not start -%} {% set start = 0 -%} {% else %} {% set start = start - 1 %} {% endif -%} {% if not end -%} {% set end = lines_count -%} {% endif -%} ```rust {{ lines | slice(start=start, end=end) | join(sep=" ") | trim_end}} ``` ================================================ FILE: site/templates/shortcodes/rustdoc_test.md ================================================ {% set lines = body | split(pat="\n") -%} {% for line in lines -%} {% if not line is starting_with("# ") -%} {{line | replace(from="```rust,ignore", to="```rust")}} {% endif -%} {% endfor -%} ================================================ FILE: site/templates/shortcodes/toml_code_snippet.md ================================================ {% set lines = load_data(path=path, format="plain") | split(pat="\n") -%} {% set lines_count = lines | length() -%} {% if not start -%} {% set start = 0 -%} {% else %} {% set start = start - 1 %} {% endif -%} {% if not end -%} {% set end = lines_count -%} {% endif -%} ```toml {{ lines | slice(start=start, end=end) | join(sep=" ") | trim_end}} ```