├── .github └── workflows │ └── clippy_check.yml ├── .gitignore ├── Cargo.toml ├── Changelog.md ├── LICENSE ├── README.md ├── examples ├── data │ ├── fonts │ │ ├── LICENSE.txt │ │ ├── Roboto-Bold.ttf │ │ ├── Roboto-BoldItalic.ttf │ │ ├── Roboto-Italic.ttf │ │ └── Roboto-Medium.ttf │ ├── images │ │ ├── attribution.txt │ │ ├── fantasy.png │ │ ├── golden.png │ │ ├── pixel.png │ │ └── transparent.png │ └── themes │ │ ├── base.yml │ │ ├── demo.yml │ │ ├── fantasy.yml │ │ ├── golden.yml │ │ ├── no_image.yml │ │ ├── pixel.yml │ │ └── transparent.yml ├── demo.rs ├── demo_gl.rs ├── demo_glium.rs ├── hello_gl.rs └── hello_glium.rs ├── screenshot.png └── src ├── app_builder.rs ├── bench.rs ├── context.rs ├── context_builder.rs ├── font.rs ├── frame.rs ├── gl_backend ├── mod.rs ├── program.rs ├── texture.rs └── vertex_buffer.rs ├── glium_backend └── mod.rs ├── image.rs ├── key_event.rs ├── lib.rs ├── log.rs ├── point.rs ├── recipes.rs ├── render.rs ├── resource.rs ├── scrollpane.rs ├── text_area.rs ├── theme.rs ├── theme_definition.rs ├── widget.rs ├── window.rs └── winit_io └── mod.rs /.github/workflows/clippy_check.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: Clippy 3 | jobs: 4 | clippy: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v2 8 | - uses: actions-rs/toolchain@v1 9 | with: 10 | toolchain: stable 11 | components: rustfmt, clippy 12 | override: true 13 | - name: Clippy 14 | run: | 15 | cargo clippy --features "image,glium_backend,gl_backend" -- -D warnings 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "thyme" 3 | version = "0.7.0" 4 | authors = ["Jared Stephen "] 5 | description = "Themable Immediate Mode GUI" 6 | documentation = "https://docs.rs/thyme/" 7 | homepage = "https://github.com/Grokmoo/thyme" 8 | repository = "https://github.com/Grokmoo/thyme" 9 | readme = "README.md" 10 | keywords = ["gamedev", "graphics", "gui"] 11 | categories = ["game-development", "gui", "rendering"] 12 | license = "Apache-2.0" 13 | edition = "2024" 14 | autoexamples = false 15 | 16 | [package.metadata.docs.rs] 17 | all-features = true 18 | 19 | [[example]] 20 | name = "hello_gl" 21 | 22 | [[example]] 23 | name = "hello_glium" 24 | 25 | [[example]] 26 | name = "demo_glium" 27 | 28 | [[example]] 29 | name = "demo_gl" 30 | 31 | [features] 32 | default = ["image", "glium_backend"] 33 | glium_backend = ["glium"] 34 | gl_backend = ["gl", "glutin", "glutin-winit", "memoffset"] 35 | 36 | [dependencies] 37 | bytemuck = { version = "1", optional = true } 38 | futures = { version = "0.3", optional = true } 39 | gl = { version = "0.14", optional = true } 40 | glium = { version = "0.36", optional = true } 41 | glutin = { version = "0.32", optional = true } 42 | glutin-winit = { version = "0.5", optional = true } 43 | image = { version = "0.25", optional = true, default-features = false, features = [ "png", "jpeg" ] } 44 | indexmap = { version = "2", features = ["serde"] } 45 | log = { version = "0.4" } 46 | memoffset = { version = "0.9", optional = true } 47 | notify = { version = "7" } 48 | parking_lot = { version = "0.12" } 49 | pulldown-cmark = { version = "0.12", default-features = false } 50 | rustc-hash = "2" 51 | rusttype = { version = "0.9" } 52 | serde = { version = "1", features = [ "derive" ] } 53 | serde_yaml = "0.8" 54 | winit = "0.30" -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [0.7.0] - 2023-07-17 8 | ### Changed 9 | - Improved handling of custom variables across widgets 10 | - Better formatting and spacing for table widgets 11 | - More flexible benchmarking 12 | - Configurable build options, including tooltip time and line scroll which can be set manually or via AppBuilder 13 | - Better tooltip layout and edge of screen positioning 14 | - Improved render group ordering and ability to specify always top / always bottom 15 | 16 | ### Added 17 | - Handy macro `set_variables` to set multiple variables on a text field 18 | - Text elements within text areas can now specify a text color 19 | - `force_hover` and `force_pressed` methods when building widgets 20 | - An optional `edit` method is now available to improve chaining of certain types of control logic 21 | - AppBuilder now supports GL renderer 22 | - Optionally specify `height` and `width` instead of `size` in theme 23 | - New multiline text widget with simple rendering (as opposed to text area) 24 | - if / else statements supported in text area definition 25 | - Method to obtain mouse position from the context 26 | - Vertical (in addition to horizontal) progress bars 27 | - Support for saving and loading of persistent state to a file / other output 28 | - Text layout option based on text width for single line widgets 29 | - Wrapping spinner widget 30 | - Can now specify a dynamic / theme based image color attribute 31 | - Now fully handle left / right / middle clicks 32 | 33 | ### Fixed 34 | - Fixed text area end of line behavior in some cases 35 | - Color space issues for GL renderer 36 | - Custom ints are now parsed correctly 37 | 38 | ## [0.6.0] - 2021-03-31 39 | ### Changed 40 | - Improved the theme definitions for the demo example 41 | 42 | ### Added 43 | - A third renderer backend, using straight OpenGL, is now available 44 | - Support for dynamic variable substitution in text fields 45 | - A textbox widget that parses a subset of Markdown, including strong / emphasis, headers, and tables 46 | - Added ability to define a theme without any actual image sources, and a demo example 47 | - Image aliases now can be used in the theme definition to avoid repitition 48 | - Multiple simple images can now be quickly defined using image groups 49 | - Method to query the current parent Widget bounds 50 | - Image colors now support transparency / alpha 51 | 52 | ### Fixed 53 | - The first example in the docs actually compiles now 54 | 55 | ## [0.5.0] - 2020-12-01 56 | ### Changed 57 | - Font character cache texture is more appropriately sized 58 | - Example themes are better organized 59 | 60 | ### Added 61 | - Support for user specified arbitrary character ranges in fonts 62 | - AppBuilder helper class allow users to set up a basic app in very few lines of code 63 | - Thyme images can be defined without requiring an actual image on disk 64 | 65 | ## [0.4.0] - 2020-10-18 66 | ### Changed 67 | - Improved performance of wgpu and glium backends. 68 | - wgpu and Glium examples should now be as similar as possible. 69 | - Upgraded winit to 0.23. 70 | 71 | ### Fixed 72 | - unparent method on WidgetBuilder now works correctly with size_from Children. 73 | - Tooltip positions is limited to inside the app window / screen. 74 | - display_size method on the UI Frame now correctly returns its result in logical pixels. 75 | - Cleaned up border issues in the "pixels" theme. 76 | - Tooltips will correctly render on top of all other render groups using the new always_top attribute. 77 | - The Demo apps will now render at a consistent 60 frames per second. 78 | 79 | ### Added 80 | - Keyboard modifers state is now tracked and accessible via the UI Frame. 81 | - screen_pos attribute may now be specified in the theme. 82 | - wants_mouse can now be obtained in the UI Frame as well as from the Context. 83 | - Simple tooltips can be created via the theme or as a single call in WidgetBuilder. 84 | - Expose wants_keyboard to let the client app know if Thyme is using the keyboard input on a given frame. 85 | 86 | ## [0.3.0] - 2020-09-28 87 | ### Changed 88 | - Wgpu backend now takes an Arc instead of Rc. 89 | - Show fewer log messages in the examples. 90 | 91 | ### Fixed 92 | - Cleaned up docs links and typos. 93 | - Glium and wgpu display fonts consistently 94 | - Glium and wgpu do sRGB conversion consistently 95 | 96 | ## [0.2.0] - 2020-09-26 97 | ### Added 98 | - Assets can now be read from files or supplied directly. 99 | - Optional Live Reload support for theme, image, and font files. 100 | - Hot swapping between themes and several new example themes. 101 | - More flexible theme file merging from multiple sources. 102 | - More widgets - tooltip, spinner, tree. 103 | - Improved documentation and added many code examples. 104 | - "Children" size from attribute. 105 | - Image aliases and "empty" image for overriding purposes 106 | 107 | ### Changed 108 | - Improved asset organization for the examples. 109 | - "from" theme references can now be resolved relative to the current theme as well as absolutely. 110 | - Input fields may specify an initial value 111 | - Windows may now optionally specify their title in code. 112 | - Improved querying persistent state. 113 | 114 | ### Fixed 115 | - Modal widgets will always want the mouse. 116 | - Combo boxes should now position and clip correctly and handle non-copy types. 117 | - Fixed several render group ordering issues 118 | - Fixed scaling for collected images 119 | 120 | ## [0.1.0] - 2020-09-01 121 | ### Added 122 | - Initial release with theming, HiDPI support, TTF Fonts, Glium and wgpu based backends. 123 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Thyme - Themable Immediate Mode GUI 2 | 3 | [![Crates.io](https://img.shields.io/crates/v/thyme.svg)](https://crates.io/crates/thyme) 4 | [![Docs.rs](https://docs.rs/thyme/badge.svg)](https://docs.rs/thyme) 5 | 6 | Thyme is a Graphical User Interface (GUI) library written in pure, safe, Rust. All widgets are rendered using image sources, instead of the line art more commonly used by other Immediate Mode GUIs. The image definitions, fonts, and style attributes are all specified in a unified theme. This is generally drawn from a file, but any [Serde](https://serde.rs/) compatible source should work. Live Reload is supported for asset files for a more efficient workflow. 7 | 8 | A composite image showcasing three different themes: 9 | ![Screenshot](screenshot.png) 10 | 11 | Thyme produces a set of Draw Lists which are sent to a swappable graphics backend - currently [Glium](https://github.com/glium/glium) and [Raw GL](https://github.com/brendanzab/gl-rs/) are supported. We have previously supported [wgpu](https://github.com/gfx-rs/wgpu) but the rate of change there has been too great for the author to keep up and support is not current. The I/O backend is also swappable - although currently only [winit](https://github.com/rust-windowing/winit) is supported. Fonts are rendered to a texture on the GPU using [rusttype](https://github.com/redox-os/rusttype). 12 | 13 | Performance is acceptable or better for most use cases, with the complete cycle of generating the widget tree, creating the draw data, and rendering taking less than 1 ms for quite complex UIs. 14 | 15 | ## Getting Started 16 | 17 | ### Running the examples 18 | 19 | The demo contains an example role playing game (RPG) character generator program that uses many of the features of Thyme. 20 | 21 | ```bash 22 | git clone https://github.com/Grokmoo/thyme.git 23 | cd thyme 24 | cargo run --example demo_glium --features glium_backend # Run demo using glium 25 | cargo run --example demo_gl --features gl_backend # Run demo using OpenGL 26 | ``` 27 | 28 | Run the hello_world example with either Glium or GL: 29 | ```bash 30 | cargo run --example hello_glium --features glium_backend 31 | cargo run --example hello_gl --features gl_backend 32 | ``` 33 | 34 | ### Starting your own project 35 | 36 | Add the following to your Cargo.toml file: 37 | 38 | ```toml 39 | [dependencies] 40 | thyme = { version = "0.7", features = ["glium_backend"] } 41 | ``` 42 | 43 | See [hello_glium](examples/hello_glium.rs) for the bare minimum to get started with your preferred renderer. As a starting point, you can copy the [data](examples/data) folder into your own project and import the resources there, as in the example. 44 | 45 | ## [Documentation](https://docs.rs/thyme) 46 | 47 | See the [docs](https://docs.rs/thyme) for the full API reference as well as theme definition format. 48 | 49 | ## Why Thyme? 50 | 51 | At its core, Thyme is an immediate mode GUI library similar to other libraries such as [Dear ImGui](https://github.com/ocornut/imgui). However, 52 | unlike many of those libraries Thyme is focused on extreme customizability and flexibility needed for production applications, especially games. 53 | 54 | With Thyme, you can customize exactly how you want your UI to look and operate. Thyme also focuses a great deal on being performant, while still 55 | retaining the benefits of a true immediate mode GUI. Thyme also implements a number of tricks granting more layout flexibility than traditional 56 | immediate mode libraries, although there are still some limitations. 57 | 58 | This flexibility comes at the cost of needing to specify theme, font, and image files. But, Thyme comes with some such files as examples to help you 59 | get started. Separating assets out in this manner can also significantly improve your workflow, especially with Thyme's built in support for live 60 | reload. This strikes a balance, enabling very fast iteration on layout and appearance while still keeping your UI logic in compiled Rust code. 61 | 62 | This flexibility does come at a cost, of course - There is quite a bit of overhead in getting started compared to similar libraries. Once you get up and 63 | running, though, the overhead is fairly minor. Performance is also very good and should be at least on-par with other immediate mode GUIs. 64 | 65 | Thyme comes with a library of widgets similar to most UI libraries. However, Thyme's widgets are written entirely using the public API, so the 66 | [`source`](src/recipes.rs) for these can serve as examples and templates for your own custom widgets. 67 | 68 | It is also written from scratch in 100% Rust! 69 | 70 | ## License 71 | [License]: #license 72 | 73 | Licensed under Apache License, Version 2.0, ([LICENSE](LICENSE) or http://www.apache.org/licenses/LICENSE-2.0). 74 | 75 | Note that some of the sample theme images are licensed under a Creative Commons license, see [attribution](examples/data/images/attribution.txt). 76 | 77 | ### License of contributions 78 | 79 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be licensed as above, without any additional terms or conditions. 80 | -------------------------------------------------------------------------------- /examples/data/fonts/LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /examples/data/fonts/Roboto-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grokmoo/thyme/0794d44fc665e6206ecb49e14eab484a22512a03/examples/data/fonts/Roboto-Bold.ttf -------------------------------------------------------------------------------- /examples/data/fonts/Roboto-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grokmoo/thyme/0794d44fc665e6206ecb49e14eab484a22512a03/examples/data/fonts/Roboto-BoldItalic.ttf -------------------------------------------------------------------------------- /examples/data/fonts/Roboto-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grokmoo/thyme/0794d44fc665e6206ecb49e14eab484a22512a03/examples/data/fonts/Roboto-Italic.ttf -------------------------------------------------------------------------------- /examples/data/fonts/Roboto-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grokmoo/thyme/0794d44fc665e6206ecb49e14eab484a22512a03/examples/data/fonts/Roboto-Medium.ttf -------------------------------------------------------------------------------- /examples/data/images/attribution.txt: -------------------------------------------------------------------------------- 1 | golden.png 2 | License CC-BY-SA 3.0 https://creativecommons.org/licenses/by-sa/3.0/ 3 | Sampled from "Golden UI - Bigger than Ever Edition" by Buch 4 | Various higher scale icons created based on theme. Buttons adapted for more scaling and new states created. 5 | https://opengameart.org/content/golden-ui-bigger-than-ever-edition 6 | https://opengameart.org/users/buch 7 | 8 | transparent.png 9 | Some elements adapted from gui-fantasy below. 10 | 11 | pixel.png 12 | Base theme taken and expanded upon from "Sci-Fi User Interface" by Buch 13 | https://opengameart.org/content/sci-fi-user-interface 14 | https://opengameart.org/users/buch 15 | 16 | Repeat fill texture taken from "Seamless Textures" by arikel 17 | https://opengameart.org/content/seamless-textures 18 | https://opengameart.org/users/arikel 19 | 20 | fantasy.png 21 | Base theme taken from and edited 22 | https://opengameart.org/content/fantasy-gui-0 23 | https://opengameart.org/users/melle -------------------------------------------------------------------------------- /examples/data/images/fantasy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grokmoo/thyme/0794d44fc665e6206ecb49e14eab484a22512a03/examples/data/images/fantasy.png -------------------------------------------------------------------------------- /examples/data/images/golden.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grokmoo/thyme/0794d44fc665e6206ecb49e14eab484a22512a03/examples/data/images/golden.png -------------------------------------------------------------------------------- /examples/data/images/pixel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grokmoo/thyme/0794d44fc665e6206ecb49e14eab484a22512a03/examples/data/images/pixel.png -------------------------------------------------------------------------------- /examples/data/images/transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grokmoo/thyme/0794d44fc665e6206ecb49e14eab484a22512a03/examples/data/images/transparent.png -------------------------------------------------------------------------------- /examples/data/themes/base.yml: -------------------------------------------------------------------------------- 1 | # A minimal theme with definitions for the provided example fonts. 2 | # No image definitions are included in this theme, instead those are in separate 3 | # files to allow easy hot-swapping 4 | # It includes definitions for all of the basic types of widgets. 5 | # This file works standalone or can be included in a larger theme. 6 | 7 | fonts: 8 | medium: 9 | source: Roboto-Medium 10 | size: 20 11 | # add Greek and Coptic characters to defaults 12 | characters: 13 | - lower: 0x0020 14 | upper: 0x007e 15 | - lower: 0x00A1 16 | upper: 0x00FF 17 | - lower: 0x0370 18 | upper: 0x03FF 19 | small: 20 | source: Roboto-Medium 21 | size: 16 22 | # use default characters: 0x0020 to 0x007e and 0x00A1 to 0x00FF 23 | small_italic: 24 | source: Roboto-Italic 25 | size: 16 26 | small_bold: 27 | source: Roboto-Bold 28 | size: 16 29 | small_bold_italic: 30 | source: Roboto-BoldItalic 31 | size: 16 32 | heading1: 33 | source: Roboto-Medium 34 | size: 24 35 | heading2: 36 | source: Roboto-Medium 37 | size: 22 38 | widgets: 39 | tooltip: 40 | background: gui/small_button_normal 41 | font: small 42 | text_align: Center 43 | size_from: [Text, FontLine] 44 | border: { all: 5 } 45 | greyed_out: 46 | background: gui/greyed_out 47 | horizontal_slider: 48 | size: [0, 15] 49 | width_from: Parent 50 | border: { top: 6, bot: 5, left: 5, right: 5 } 51 | children: 52 | slider_bar: 53 | align: TopLeft 54 | width_from: Parent 55 | height_from: Parent 56 | background: gui/slider_horizontal 57 | slider_button: 58 | from: button 59 | background: gui/slider_button 60 | size: [15, 15] 61 | combo_box: 62 | from: button 63 | children: 64 | expand: 65 | from: dropdown_expand 66 | combo_box_popup: 67 | from: scrollpane_vertical 68 | width_from: Parent 69 | height_from: Normal 70 | size: [10, 120] 71 | pos: [-5, 18] 72 | background: gui/small_button_normal 73 | children: 74 | content: 75 | size: [-18, -10] 76 | pos: [0, 5] 77 | children: 78 | entry: 79 | from: button 80 | width_from: Parent 81 | size: [0, 25] 82 | scrollpane_vertical: 83 | from: scrollpane 84 | children: 85 | content: 86 | size: [-18, 0] 87 | scrollbar_vertical: 88 | from: scrollbar_vertical 89 | size: [20, 0] 90 | scrollpane: 91 | width_from: Parent 92 | height_from: Parent 93 | children: 94 | content: 95 | border: { all: 2 } 96 | height_from: Parent 97 | width_from: Parent 98 | align: TopLeft 99 | layout: Vertical 100 | size: [-18, -20] 101 | pos: [0, 0] 102 | child_align: TopLeft 103 | scrollbar_horizontal: 104 | from: scrollbar_horizontal 105 | scrollbar_vertical: 106 | from: scrollbar_vertical 107 | dropdown_expand: 108 | size: [12, 12] 109 | align: Right 110 | foreground: gui/arrow_down 111 | scroll_left: 112 | from: scroll_button 113 | align: Left 114 | foreground: gui/arrow_left 115 | scroll_right: 116 | from: scroll_button 117 | align: Right 118 | foreground: gui/arrow_right 119 | scroll_up: 120 | from: scroll_button 121 | align: Top 122 | foreground: gui/arrow_up 123 | scroll_down: 124 | from: scroll_button 125 | align: Bot 126 | foreground: gui/arrow_down 127 | scroll_button: 128 | wants_mouse: true 129 | background: gui/scroll_button 130 | size: [20, 20] 131 | border: { all: 4 } 132 | scrollbar_horizontal: 133 | size: [-29, 20] 134 | pos: [0, 0] 135 | align: BotLeft 136 | width_from: Parent 137 | background: gui/scrollbar_horizontal 138 | children: 139 | left: 140 | from: scroll_left 141 | right: 142 | from: scroll_right 143 | scroll: 144 | wants_mouse: true 145 | background: gui/small_button 146 | align: Left 147 | border: { all: 4 } 148 | scrollbar_vertical: 149 | size: [20, -20] 150 | pos: [0, 0] 151 | align: TopRight 152 | height_from: Parent 153 | background: gui/scrollbar_vertical 154 | wants_mouse: true 155 | children: 156 | up: 157 | from: scroll_up 158 | down: 159 | from: scroll_down 160 | scroll: 161 | wants_mouse: true 162 | background: gui/small_button 163 | align: Top 164 | border: { all: 4 } 165 | progress_bar: 166 | size: [100, 24] 167 | background: gui/small_button_normal 168 | border: { all: 4 } 169 | child_align: TopLeft 170 | children: 171 | bar: 172 | background: gui/progress_bar 173 | size_from: [Parent, Parent] 174 | input_field: 175 | font: small 176 | border: { height: 4, width: 5 } 177 | background: gui/input_field 178 | text_align: Left 179 | wants_mouse: true 180 | size: [150, 24] 181 | child_align: TopLeft 182 | children: 183 | caret: 184 | size: [2, -2] 185 | height_from: Parent 186 | background: gui/caret 187 | text_area_item: 188 | from: label 189 | text_align: TopLeft 190 | text_area: 191 | border: { all: 5 } 192 | size_from: [Parent, Children] 193 | custom: 194 | tab_width: 6.0 195 | column_width: 90.0 196 | list_bullet: "* " 197 | children: 198 | paragraph_normal: 199 | from: text_area_item 200 | font: small 201 | paragraph_strong: 202 | from: text_area_item 203 | font: small_bold 204 | paragraph_emphasis: 205 | from: text_area_item 206 | font: small_italic 207 | paragraph_strong_emphasis: 208 | from: text_area_item 209 | font: small_bold_italic 210 | heading1_normal: 211 | from: text_area_item 212 | font: heading1 213 | heading2_normal: 214 | from: text_area_item 215 | font: heading2 216 | bg_label: 217 | from: label 218 | background: gui/small_button_normal 219 | label: 220 | font: small 221 | border: { width: 5 } 222 | text_align: Center 223 | size_from: [Parent, FontLine] 224 | check_button: 225 | from: button 226 | background: gui/small_button_no_active 227 | foreground: gui/check 228 | button: 229 | font: small 230 | wants_mouse: true 231 | background: gui/small_button 232 | text_align: Center 233 | size: [150, 24] 234 | border: { all: 5 } 235 | spinner: 236 | size: [80, 20] 237 | layout: Horizontal 238 | layout_spacing: [5, 5] 239 | child_align: Left 240 | children: 241 | decrease: 242 | from: button 243 | text: "-" 244 | background: gui/small_button 245 | size: [20, 20] 246 | value: 247 | from: label 248 | size: [30, 0] 249 | font: medium 250 | width_from: Normal 251 | increase: 252 | from: button 253 | text: "+" 254 | background: gui/small_button 255 | size: [20, 20] 256 | window_base: 257 | background: gui/window_bg 258 | wants_mouse: true 259 | layout: Vertical 260 | layout_spacing: [5, 5] 261 | border: { left: 5, right: 5, top: 29, bot: 5 } 262 | size: [300, 400] 263 | child_align: Top 264 | children: 265 | titlebar: 266 | wants_mouse: true 267 | background: gui/small_button 268 | size: [10, 30] 269 | pos: [-6, -30] 270 | border: { all: 5 } 271 | width_from: Parent 272 | child_align: Center 273 | align: TopLeft 274 | children: 275 | title: 276 | from: label 277 | text: "Main Window" 278 | font: medium 279 | width_from: Parent 280 | close: 281 | from: window_close 282 | handle: 283 | wants_mouse: true 284 | background: gui/window_handle 285 | size: [12, 12] 286 | align: BotRight 287 | pos: [-2, 0] 288 | window: 289 | from: window_base 290 | window_close: 291 | wants_mouse: true 292 | background: gui/small_button 293 | foreground: gui/close_icon 294 | size: [20, 20] 295 | border: { all: 4 } 296 | align: TopRight 297 | tree: 298 | size_from: [Parent, Children] 299 | border: { all: 5 } 300 | background: gui/frame 301 | children: 302 | expand: 303 | from: button 304 | align: TopLeft 305 | pos: [0, 0] 306 | text: "+" 307 | text_align: Center 308 | size: [20, 20] 309 | collapse: 310 | from: button 311 | align: TopLeft 312 | pos: [0, 0] 313 | text: "-" 314 | text_align: Center 315 | size: [20, 20] -------------------------------------------------------------------------------- /examples/data/themes/demo.yml: -------------------------------------------------------------------------------- 1 | # Specific widget definitions for the demo app. Appended to the base theme 2 | 3 | widgets: 4 | theme_panel: 5 | size: [250, 25] 6 | align: TopRight 7 | pos: [0, 70] 8 | layout: Horizontal 9 | layout_spacing: [5, 5] 10 | children: 11 | live_reload: 12 | from: check_button 13 | height_from: Parent 14 | size: [125, 0] 15 | text: "Live Reload" 16 | theme_choice: 17 | from: combo_box 18 | height_from: Parent 19 | size: [120, 0] 20 | tooltip: "Select a different theme" 21 | bench: 22 | from: label 23 | background: gui/small_button_normal 24 | size: [250, 50] 25 | align: TopRight 26 | width_from: Normal 27 | party_window: 28 | from: window 29 | size: [200, 300] 30 | children: 31 | titlebar: 32 | children: 33 | title: 34 | text: "Form Party" 35 | members_panel: 36 | from: scrollpane 37 | width_from: Parent 38 | height_from: Parent 39 | layout: Vertical 40 | layout_spacing: [5, 5] 41 | children: 42 | content: 43 | children: 44 | add_character_button: 45 | from: button 46 | background: gui/small_button_flash 47 | text: "New Character..." 48 | width_from: Parent 49 | size: [0, 50] 50 | filled_slot_button: 51 | from: button 52 | background: gui/small_button 53 | width_from: Parent 54 | size: [0, 50] 55 | character_window: 56 | from: window 57 | size: [250, 500] 58 | align: Center 59 | children: 60 | titlebar: 61 | children: 62 | title: 63 | text: "Edit Character" 64 | pane: 65 | from: scrollpane 66 | children: 67 | content: 68 | border: { all: 5 } 69 | name_panel: 70 | width_from: Parent 71 | height: 25 72 | layout: Horizontal 73 | children: 74 | name_input: 75 | from: input_field 76 | size: [0, 30] 77 | width_from: Parent 78 | text_align: Center 79 | font: medium 80 | subpanel: 81 | from: tree 82 | background: gui/frame 83 | layout: Vertical 84 | layout_spacing: [5, 5] 85 | children: 86 | title: 87 | from: label 88 | font: medium 89 | description_panel: 90 | from: scrollpane_vertical 91 | layout: Vertical 92 | height_from: Normal 93 | height: 150 94 | description_box: 95 | from: text_area 96 | text: | 97 | # Overview 98 | This is your character's ***very detailed*** description that spans a few lines. 99 | 100 | ## Background 101 | This is another line of text. With some color. 102 | 103 | 1. This is a list item. The text is long enough to wrap around. 104 | 1. This is another list item 105 | * An unordered list item 106 | * A second item 107 | 1. The final list item 108 | 109 | ## Stats 110 | This is a stats table with substituted dynamic values. 111 | 112 | | Stat | Value | 113 | | ------- | ----- | 114 | | Strength | {Strength} | 115 | | Dexterity | {Dexterity} | 116 | | Constitution | {Constitution} | 117 | | Intelligence | {Intelligence} | 118 | | Wisdom | {Wisdom} | 119 | | Charisma | {Charisma} | 120 | age_slider: 121 | from: horizontal_slider 122 | age_label: 123 | from: label 124 | width_from: Parent 125 | tooltip_button: 126 | from: button 127 | text_align: Center 128 | race_selector: 129 | from: combo_box 130 | width_from: Parent 131 | size: [0, 25] 132 | stats_panel: 133 | from: subpanel 134 | children: 135 | title: 136 | text: Stats 137 | roll_button: 138 | from: button 139 | text: Roll 140 | text_align: Right 141 | size: [200, 33] 142 | children: 143 | progress_bar: 144 | from: progress_bar 145 | points_available: 146 | from: label 147 | text_align: Right 148 | stat_panel: 149 | from: tree 150 | background: gui/frame 151 | layout: Horizontal 152 | layout_spacing: [5, 5] 153 | child_align: TopLeft 154 | children: 155 | label: 156 | from: label 157 | size: [110, 20] 158 | text_align: Right 159 | size_from: [Normal, Normal] 160 | decrease: 161 | from: button 162 | text: "-" 163 | background: gui/small_button 164 | size: [20, 20] 165 | value: 166 | from: label 167 | size: [30, 0] 168 | font: medium 169 | width_from: Normal 170 | increase: 171 | from: button 172 | text: "+" 173 | background: gui/small_button 174 | size: [20, 20] 175 | description: 176 | from: label 177 | text_align: Left 178 | size_from: [Parent, Normal] 179 | size: [0, 40] 180 | pos: [0, 20] 181 | text: "This is a detailed description of the Stat." 182 | item_picker: 183 | from: window 184 | align: Center 185 | size: [350, 150] 186 | layout: Horizontal 187 | child_align: Left 188 | children: 189 | titlebar: 190 | children: 191 | title: 192 | text: "Purchase an Item" 193 | item_button: 194 | from: button 195 | layout: Vertical 196 | size: [100, 0] 197 | height_from: Parent 198 | children: 199 | name: 200 | from: label 201 | icon: 202 | size: [32, 32] 203 | price: 204 | from: label 205 | inventory_panel: 206 | from: subpanel 207 | children: 208 | title: 209 | text: Items 210 | top_panel: 211 | size: [0, 25] 212 | width_from: Parent 213 | children: 214 | buy: 215 | from: button 216 | size: [80, 25] 217 | text: Purchase.. 218 | gold: 219 | from: label 220 | size: [100, 25] 221 | align: Right 222 | text_align: Right 223 | width_from: Normal 224 | items_panel: 225 | from: items_panel 226 | items_panel: 227 | from: scrollpane_vertical 228 | layout: Vertical 229 | height_from: Normal 230 | size: [0, 100] 231 | children: 232 | content: 233 | children: 234 | item_button: 235 | from: button 236 | width_from: Parent 237 | size: [0, 25] 238 | inventory_tooltip: 239 | size_from: [Children, Children] 240 | background: gui/small_button_normal 241 | border: { all: 5 } 242 | layout: Vertical 243 | align: TopLeft 244 | children: 245 | label: 246 | from: label 247 | size_from: [Text, FontLine] -------------------------------------------------------------------------------- /examples/data/themes/fantasy.yml: -------------------------------------------------------------------------------- 1 | # Image definitions for "fantasy.png" image. 2 | 3 | image_sets: 4 | gui: 5 | source: fantasy 6 | scale: 0.5 7 | images: 8 | cursor_normal: 9 | position: [132, 194] 10 | size: [42, 42] 11 | cursor_pressed: 12 | position: [178, 212] 13 | size: [42, 42] 14 | cursor: 15 | states: 16 | Normal: cursor_normal 17 | Hover: cursor_normal 18 | Pressed: cursor_pressed 19 | window_bg: 20 | sub_images: 21 | window_bg_base: 22 | position: [0, 0] 23 | size: [0, 0] 24 | window_fill: 25 | position: [6, 6] 26 | size: [-12, -16] 27 | window_bg_base: 28 | position: [0, 0] 29 | grid_size: [64, 64] 30 | window_fill: 31 | position: [256, 0] 32 | size: [256, 256] 33 | fill: Repeat 34 | small_button_normal: 35 | position: [220, 0] 36 | grid_size: [10, 10] 37 | small_button_hover: 38 | position: [220, 30] 39 | grid_size: [10, 10] 40 | small_button_pressed: 41 | position: [220, 60] 42 | grid_size: [10, 10] 43 | small_button_disabled: 44 | position: [220, 90] 45 | grid_size: [10, 10] 46 | small_button_active: 47 | position: [220, 120] 48 | grid_size: [10, 10] 49 | small_button_black: 50 | position: [220, 150] 51 | grid_size: [10, 10] 52 | small_button_flash1: 53 | position: [220, 180] 54 | grid_size: [10, 10] 55 | small_button_flash2: 56 | position: [220, 210] 57 | grid_size: [10, 10] 58 | small_button_normal_flash: 59 | frame_time_millis: 200 60 | frames: 61 | - small_button_flash1 62 | - small_button_flash2 63 | - small_button_flash1 64 | - small_button_normal 65 | input_field: 66 | states: 67 | Normal: small_button_black 68 | Hover: small_button_hover 69 | Pressed: small_button_pressed 70 | Disabled: small_button_disabled 71 | small_button: 72 | states: 73 | Normal: small_button_normal 74 | Hover: small_button_hover 75 | Pressed: small_button_pressed 76 | Disabled: small_button_disabled 77 | Active: small_button_active 78 | Active + Hover: small_button_active 79 | Active + Pressed: small_button_pressed 80 | small_button_no_active: 81 | states: 82 | Normal: small_button_normal 83 | Hover: small_button_hover 84 | Pressed: small_button_pressed 85 | Disabled: small_button_disabled 86 | Active: small_button_normal 87 | Active + Hover: small_button_hover 88 | Active + Pressed: small_button_pressed 89 | small_button_flash: 90 | states: 91 | Normal: small_button_normal_flash 92 | Hover: small_button_hover 93 | Pressed: small_button_pressed 94 | Disabled: small_button_disabled 95 | Active: small_button_active 96 | Active + Hover: small_button_active 97 | Active + Pressed: small_button_pressed 98 | scroll_button: 99 | from: small_button 100 | scrollbar_vertical: 101 | from: empty 102 | scrollbar_horizontal: 103 | from: empty 104 | slider_button: 105 | from: small_button 106 | frame: 107 | from: small_button_normal 108 | close_icon_normal: 109 | position: [194, 132] 110 | size: [24, 24] 111 | close_icon_pressed: 112 | position: [194, 156] 113 | size: [24, 24] 114 | close_icon_disabled: 115 | position: [194, 180] 116 | size: [24, 24] 117 | close_icon: 118 | states: 119 | Normal: close_icon_normal 120 | Hover: close_icon_normal 121 | Pressed: close_icon_pressed 122 | Disabled: close_icon_disabled 123 | progress_bar: 124 | position: [100, 200] 125 | grid_size: [10, 18] 126 | window_handle_normal: 127 | position: [194, 0] 128 | size: [24, 24] 129 | window_handle_hover: 130 | position: [194, 24] 131 | size: [24, 24] 132 | window_handle_pressed: 133 | position: [194, 48] 134 | size: [24, 24] 135 | window_handle_disabled: 136 | position: [194, 72] 137 | size: [24, 24] 138 | window_handle: 139 | states: 140 | Normal: window_handle_normal 141 | Hover: window_handle_hover 142 | Pressed: window_handle_pressed 143 | Disabled: window_handle_disabled 144 | caret_on: 145 | position: [194, 98] 146 | size: [4, 32] 147 | fill: Stretch 148 | caret_off: 149 | position: [200, 98] 150 | size: [4, 32] 151 | fill: Stretch 152 | caret: 153 | frame_time_millis: 500 154 | frames: 155 | - caret_on 156 | - caret_off 157 | arrow_right: 158 | position: [48, 194] 159 | size: [24, 24] 160 | arrow_left: 161 | position: [48, 218] 162 | size: [24, 24] 163 | arrow_down: 164 | position: [72, 194] 165 | size: [24, 24] 166 | arrow_up: 167 | position: [72, 218] 168 | size: [24, 24] 169 | check_normal: 170 | position: [24, 208] 171 | size: [24, 24] 172 | check_active: 173 | position: [24, 232] 174 | size: [24, 24] 175 | check: 176 | states: 177 | Normal: check_normal 178 | Hover: check_normal 179 | Pressed: check_normal 180 | Disabled: check_normal 181 | Active: check_active 182 | Active + Hover: check_active 183 | Active + Pressed: check_active 184 | slider_horizontal: 185 | position: [0, 196] 186 | grid_size_horiz: [10, 8] 187 | slider_vertical: 188 | position: [0, 204] 189 | grid_size_vert: [8, 10] 190 | greyed_out: 191 | position: [34, 196] 192 | size: [10, 10] 193 | fill: Stretch -------------------------------------------------------------------------------- /examples/data/themes/golden.yml: -------------------------------------------------------------------------------- 1 | # Image definitions for "golden.png" image. 2 | 3 | widgets: 4 | window: 5 | border: { left: 5, right: 5, top: 38, bot: 5 } 6 | from: window_base 7 | children: 8 | titlebar: 9 | pos: [-6, -38] 10 | background: gui/empty 11 | children: 12 | close: 13 | background: gui/window_close 14 | foreground: gui/empty 15 | size: [24, 32] 16 | pos: [-8, -4] 17 | title: 18 | pos: [-4, 0] 19 | handle: 20 | pos: [-4, -4] 21 | image_sets: 22 | gui: 23 | source: golden 24 | scale: 0.5 25 | images: 26 | window_bg: 27 | sub_images: 28 | window_bg_top: 29 | position: [0, 0] 30 | size: [-32, 68] 31 | window_bg_base: 32 | position: [0, 68] 33 | size: [0, -68] 34 | window_bg_top: 35 | position: [0, 0] 36 | grid_size_horiz: [32, 68] 37 | window_bg_base: 38 | position: [0, 72] 39 | grid_size: [32, 32] 40 | window_close_normal: 41 | position: [112, 0] 42 | size: [48, 64] 43 | window_close_hover: 44 | position: [112, 64] 45 | size: [48, 64] 46 | window_close_pressed: 47 | position: [112, 128] 48 | size: [48, 64] 49 | window_close_disabled: 50 | position: [112, 192] 51 | size: [48, 64] 52 | window_close: 53 | states: 54 | Normal: window_close_normal 55 | Hover: window_close_hover 56 | Pressed: window_close_pressed 57 | Disabled: window_close_disabled 58 | cursor_normal: 59 | position: [0, 176] 60 | size: [24, 32] 61 | cursor_pressed: 62 | position: [32, 176] 63 | size: [24, 32] 64 | cursor: 65 | states: 66 | Normal: cursor_normal 67 | Hover: cursor_normal 68 | Pressed: cursor_pressed 69 | small_button_normal: 70 | position: [168, 0] 71 | grid_size: [24, 12] 72 | small_button_hover: 73 | position: [168, 36] 74 | grid_size: [24, 12] 75 | small_button_pressed: 76 | position: [168, 72] 77 | grid_size: [24, 12] 78 | small_button_disabled: 79 | position: [168, 108] 80 | grid_size: [24, 12] 81 | small_button_active: 82 | position: [168, 144] 83 | grid_size: [24, 12] 84 | small_button_black: 85 | position: [168, 180] 86 | grid_size: [24, 12] 87 | small_button_flash1: 88 | position: [168, 216] 89 | grid_size: [24, 12] 90 | small_button_normal_flash: 91 | frame_time_millis: 200 92 | frames: 93 | - small_button_hover 94 | - small_button_flash1 95 | - small_button_hover 96 | - small_button_normal 97 | input_field: 98 | states: 99 | Normal: small_button_black 100 | Hover: small_button_hover 101 | Pressed: small_button_pressed 102 | Disabled: small_button_disabled 103 | small_button: 104 | states: 105 | Normal: small_button_normal 106 | Hover: small_button_hover 107 | Pressed: small_button_pressed 108 | Disabled: small_button_disabled 109 | Active: small_button_active 110 | Active + Hover: small_button_active 111 | Active + Pressed: small_button_pressed 112 | small_button_no_active: 113 | states: 114 | Normal: small_button_normal 115 | Hover: small_button_hover 116 | Pressed: small_button_pressed 117 | Disabled: small_button_disabled 118 | Active: small_button_normal 119 | Active + Hover: small_button_hover 120 | Active + Pressed: small_button_pressed 121 | small_button_flash: 122 | states: 123 | Normal: small_button_normal_flash 124 | Hover: small_button_hover 125 | Pressed: small_button_pressed 126 | Disabled: small_button_disabled 127 | Active: small_button_active 128 | Active + Hover: small_button_active 129 | Active + Pressed: small_button_pressed 130 | circle_button_normal: 131 | position: [250, 140] 132 | size: [30, 30] 133 | circle_button_hover: 134 | position: [280, 140] 135 | size: [30, 30] 136 | circle_button_pressed: 137 | position: [310, 140] 138 | size: [30, 30] 139 | circle_button_disabled: 140 | position: [340, 140] 141 | size: [30, 30] 142 | circle_button: 143 | states: 144 | Normal: circle_button_normal 145 | Hover: circle_button_hover 146 | Pressed: circle_button_pressed 147 | Disabled: circle_button_disabled 148 | scroll_button: 149 | from: small_button 150 | scrollbar_vertical: 151 | position: [265, 188] 152 | grid_size_vert: [12, 17] 153 | scrollbar_horizontal: 154 | position: [304, 240] 155 | grid_size_horiz: [17, 12] 156 | slider_button: 157 | from: circle_button 158 | frame: 159 | from: window_bg_base 160 | close_icon: 161 | position: [0, 0] 162 | size: [0, 0] 163 | progress_bar: 164 | position: [252, 0] 165 | grid_size: [8, 8] 166 | window_handle_normal: 167 | position: [252, 36] 168 | size: [24, 24] 169 | window_handle_hover: 170 | position: [252, 60] 171 | size: [24, 24] 172 | window_handle_pressed: 173 | position: [252, 84] 174 | size: [24, 24] 175 | window_handle_disabled: 176 | position: [252, 108] 177 | size: [24, 24] 178 | window_handle: 179 | states: 180 | Normal: window_handle_normal 181 | Hover: window_handle_hover 182 | Pressed: window_handle_pressed 183 | Disabled: window_handle_disabled 184 | caret_on: 185 | position: [288, 0] 186 | size: [4, 14] 187 | fill: Stretch 188 | caret_off: 189 | position: [293, 0] 190 | size: [4, 14] 191 | fill: Stretch 192 | caret: 193 | frame_time_millis: 500 194 | frames: 195 | - caret_on 196 | - caret_off 197 | arrow_right: 198 | position: [288, 24] 199 | size: [24, 24] 200 | arrow_left: 201 | position: [324, 24] 202 | size: [24, 24] 203 | arrow_down: 204 | position: [324, 60] 205 | size: [24, 24] 206 | arrow_up: 207 | position: [288, 60] 208 | size: [24, 24] 209 | check_normal: 210 | position: [288, 96] 211 | size: [24, 24] 212 | check_active: 213 | position: [324, 96] 214 | size: [24, 24] 215 | check: 216 | states: 217 | Normal: check_normal 218 | Hover: check_normal 219 | Pressed: check_normal 220 | Disabled: check_normal 221 | Active: check_active 222 | Active + Hover: check_active 223 | Active + Pressed: check_active 224 | slider_horizontal: 225 | position: [252, 240] 226 | grid_size_horiz: [17, 12] 227 | slider_vertical: 228 | position: [252, 188] 229 | grid_size_vert: [12, 17] 230 | greyed_out: 231 | position: [301, 1] 232 | size: [4, 4] 233 | fill: Stretch -------------------------------------------------------------------------------- /examples/data/themes/no_image.yml: -------------------------------------------------------------------------------- 1 | # Thyme image definitions for a theme that doesn't rely on an actual source image, instead 2 | # constructing images. 3 | 4 | widgets: 5 | dropdown_expand: 6 | size: [12, 12] 7 | pos: [0, -2] 8 | font: medium 9 | align: Right 10 | text_align: Center 11 | text: "v" 12 | scroll_left: 13 | from: scroll_button 14 | font: medium 15 | text_align: Center 16 | align: Left 17 | text: "<" 18 | scroll_right: 19 | from: scroll_button 20 | font: medium 21 | text_align: Center 22 | align: Right 23 | text: ">" 24 | scroll_up: 25 | from: scroll_button 26 | font: medium 27 | text_align: Center 28 | align: Top 29 | text: "Λ" 30 | scroll_down: 31 | from: scroll_button 32 | font: medium 33 | text_align: Center 34 | align: Bot 35 | text: "V" 36 | check_button: 37 | from: button 38 | background: gui/small_button_no_active 39 | foreground: gui/check 40 | window_close: 41 | wants_mouse: true 42 | background: gui/small_button 43 | text: "X" 44 | font: medium 45 | text_align: Center 46 | size: [20, 20] 47 | align: TopRight 48 | image_sets: 49 | gui: 50 | scale: 0.5 51 | images: 52 | bg_red: 53 | solid: true 54 | color: "#800" 55 | bg_dark_red: 56 | solid: true 57 | color: "#400" 58 | bg_white: 59 | solid: true 60 | color: "#FFF" 61 | bg_green: 62 | solid: true 63 | color: "#0F0" 64 | bg_black: 65 | solid: true 66 | color: "#000" 67 | bg_light_grey: 68 | solid: true 69 | color: "#999" 70 | bg_grey: 71 | solid: true 72 | color: "#666" 73 | bg_dark_grey: 74 | solid: true 75 | color: "#333" 76 | bg_active: 77 | solid: true 78 | color: "#A88" 79 | outline_bot: 80 | from: bg_black 81 | outline_top: 82 | from: bg_black 83 | outline_left: 84 | from: bg_black 85 | outline_right: 86 | from: bg_black 87 | outline: 88 | sub_images: 89 | outline_bot: 90 | position: [0, -1] 91 | size: [0, 1] 92 | outline_top: 93 | position: [0, 0] 94 | size: [0, 1] 95 | outline_left: 96 | position: [0, 0] 97 | size: [1, 0] 98 | outline_right: 99 | position: [-1, 0] 100 | size: [1, 0] 101 | window_bg_base: 102 | sub_images: 103 | outline: 104 | position: [0, 0] 105 | size: [0, 0] 106 | bg_dark_grey: 107 | position: [1, 1] 108 | size: [-2, -2] 109 | window_bg: 110 | from: window_bg_base 111 | small_button_normal: 112 | sub_images: 113 | outline: 114 | position: [0, 0] 115 | size: [0, 0] 116 | bg_grey: 117 | position: [1, 1] 118 | size: [-2, -2] 119 | small_button_hover: 120 | solid: true 121 | color: "#AAA" 122 | small_button_pressed: 123 | solid: true 124 | color: "#888" 125 | small_button_disabled: 126 | solid: true 127 | color: "#444" 128 | small_button_active: 129 | sub_images: 130 | outline: 131 | position: [0, 0] 132 | size: [0, 0] 133 | bg_active: 134 | position: [1, 1] 135 | size: [-2, -2] 136 | small_button_black: 137 | solid: true 138 | color: "#000000" 139 | small_button_flash1: 140 | solid: true 141 | color: "#777" 142 | small_button_flash2: 143 | solid: true 144 | color: "#888" 145 | small_button_normal_flash: 146 | frame_time_millis: 200 147 | frames: 148 | - small_button_flash1 149 | - small_button_flash2 150 | - small_button_flash1 151 | - small_button_normal 152 | input_field: 153 | states: 154 | Normal: small_button_black 155 | Hover: small_button_hover 156 | Pressed: small_button_pressed 157 | Disabled: small_button_disabled 158 | small_button: 159 | states: 160 | Normal: small_button_normal 161 | Hover: small_button_hover 162 | Pressed: small_button_pressed 163 | Disabled: small_button_disabled 164 | Active: small_button_active 165 | Active + Hover: small_button_active 166 | Active + Pressed: small_button_pressed 167 | small_button_no_active: 168 | states: 169 | Normal: small_button_normal 170 | Hover: small_button_hover 171 | Pressed: small_button_pressed 172 | Disabled: small_button_disabled 173 | Active: small_button_normal 174 | Active + Hover: small_button_hover 175 | Active + Pressed: small_button_pressed 176 | small_button_flash: 177 | states: 178 | Normal: small_button_normal_flash 179 | Hover: small_button_hover 180 | Pressed: small_button_pressed 181 | Disabled: small_button_disabled 182 | Active: small_button_active 183 | Active + Hover: small_button_active 184 | Active + Pressed: small_button_pressed 185 | scroll_button: 186 | from: small_button 187 | scrollbar_vertical: 188 | from: empty 189 | scrollbar_horizontal: 190 | from: empty 191 | slider_button: 192 | from: small_button 193 | frame: 194 | from: small_button_normal 195 | close_icon_normal: 196 | from: bg_red 197 | close_icon_pressed: 198 | from: bg_dark_red 199 | close_icon_disabled: 200 | from: empty 201 | close_icon: 202 | states: 203 | Normal: close_icon_normal 204 | Hover: close_icon_normal 205 | Pressed: close_icon_pressed 206 | Disabled: close_icon_disabled 207 | progress_bar: 208 | from: bg_green 209 | window_handle: 210 | from: small_button 211 | caret_on: 212 | from: bg_white 213 | caret_off: 214 | from: empty 215 | caret: 216 | frame_time_millis: 500 217 | frames: 218 | - caret_on 219 | - caret_off 220 | arrow_right: 221 | position: [48, 194] 222 | size: [24, 24] 223 | arrow_left: 224 | position: [48, 218] 225 | size: [24, 24] 226 | arrow_down: 227 | position: [72, 194] 228 | size: [24, 24] 229 | arrow_up: 230 | position: [72, 218] 231 | size: [24, 24] 232 | check_normal: 233 | sub_images: 234 | outline: 235 | position: [0, 0] 236 | size: [24, 24] 237 | check_active: 238 | sub_images: 239 | outline: 240 | position: [0, 0] 241 | size: [24, 24] 242 | bg_white: 243 | position: [1, 1] 244 | size: [22, 22] 245 | check: 246 | states: 247 | Normal: check_normal 248 | Hover: check_normal 249 | Pressed: check_normal 250 | Disabled: check_normal 251 | Active: check_active 252 | Active + Hover: check_active 253 | Active + Pressed: check_active 254 | slider_horizontal: 255 | sub_images: 256 | outline: 257 | position: [0, 0] 258 | size: [0, 0] 259 | bg_light_grey: 260 | position: [1, 1] 261 | size: [-2, -2] 262 | slider_vertical: 263 | from: slider_horizontal 264 | greyed_out: 265 | solid: true 266 | color: "#8888" -------------------------------------------------------------------------------- /examples/data/themes/pixel.yml: -------------------------------------------------------------------------------- 1 | # Image definitions for "pixel.png" image. 2 | 3 | image_sets: 4 | gui: 5 | source: pixel 6 | scale: 1.0 7 | images: 8 | cursor_normal: 9 | position: [66, 97] 10 | size: [21, 21] 11 | cursor_pressed: 12 | position: [89, 106] 13 | size: [21, 21] 14 | cursor: 15 | states: 16 | Normal: cursor_normal 17 | Hover: cursor_normal 18 | Pressed: cursor_pressed 19 | window_bg: 20 | sub_images: 21 | window_bg_base: 22 | position: [0, 0] 23 | size: [0, 0] 24 | window_fill: 25 | position: [5, 5] 26 | size: [-10, -10] 27 | window_bg_base: 28 | position: [0, 0] 29 | grid_size: [32, 32] 30 | window_fill: 31 | position: [128, 0] 32 | size: [128, 128] 33 | fill: Repeat 34 | small_button_normal: 35 | position: [110, 0] 36 | grid_size: [5, 5] 37 | small_button_hover: 38 | position: [110, 15] 39 | grid_size: [5, 5] 40 | small_button_pressed: 41 | position: [110, 30] 42 | grid_size: [5, 5] 43 | small_button_disabled: 44 | position: [110, 45] 45 | grid_size: [5, 5] 46 | small_button_active: 47 | position: [110, 60] 48 | grid_size: [5, 5] 49 | small_button_black: 50 | position: [110, 75] 51 | grid_size: [5, 5] 52 | small_button_flash1: 53 | position: [110, 90] 54 | grid_size: [5, 5] 55 | small_button_flash2: 56 | position: [110, 105] 57 | grid_size: [5, 5] 58 | small_button_normal_flash: 59 | frame_time_millis: 200 60 | frames: 61 | - small_button_flash1 62 | - small_button_flash2 63 | - small_button_flash1 64 | - small_button_normal 65 | input_field: 66 | states: 67 | Normal: small_button_black 68 | Hover: small_button_hover 69 | Pressed: small_button_pressed 70 | Disabled: small_button_disabled 71 | small_button_no_active: 72 | states: 73 | Normal: small_button_normal 74 | Hover: small_button_hover 75 | Pressed: small_button_pressed 76 | Disabled: small_button_disabled 77 | Active: small_button_normal 78 | Active + Hover: small_button_hover 79 | Active + Pressed: small_button_pressed 80 | small_button: 81 | states: 82 | Normal: small_button_normal 83 | Hover: small_button_hover 84 | Pressed: small_button_pressed 85 | Disabled: small_button_disabled 86 | Active: small_button_active 87 | Active + Hover: small_button_active 88 | Active + Pressed: small_button_pressed 89 | small_button_flash: 90 | states: 91 | Normal: small_button_normal_flash 92 | Hover: small_button_hover 93 | Pressed: small_button_pressed 94 | Disabled: small_button_disabled 95 | Active: small_button_active 96 | Active + Hover: small_button_active 97 | Active + Pressed: small_button_pressed 98 | scroll_button: 99 | from: small_button 100 | scrollbar_vertical: 101 | from: empty 102 | scrollbar_horizontal: 103 | from: empty 104 | slider_button: 105 | from: small_button 106 | frame: 107 | from: small_button_normal 108 | close_icon_normal: 109 | position: [97, 66] 110 | size: [12, 12] 111 | close_icon_pressed: 112 | position: [97, 78] 113 | size: [12, 12] 114 | close_icon_disabled: 115 | position: [97, 90] 116 | size: [12, 12] 117 | close_icon: 118 | states: 119 | Normal: close_icon_normal 120 | Hover: close_icon_normal 121 | Pressed: close_icon_pressed 122 | Disabled: close_icon_disabled 123 | progress_bar: 124 | position: [50, 100] 125 | grid_size: [5, 9] 126 | window_handle_normal: 127 | position: [97, 0] 128 | size: [12, 12] 129 | window_handle_hover: 130 | position: [97, 13] 131 | size: [12, 12] 132 | window_handle_pressed: 133 | position: [97, 26] 134 | size: [12, 12] 135 | window_handle_disabled: 136 | position: [97, 39] 137 | size: [12, 12] 138 | window_handle: 139 | states: 140 | Normal: window_handle_normal 141 | Hover: window_handle_hover 142 | Pressed: window_handle_pressed 143 | Disabled: window_handle_disabled 144 | caret_on: 145 | position: [5, 111] 146 | size: [2, 16] 147 | fill: Stretch 148 | caret_off: 149 | position: [8, 111] 150 | size: [2, 16] 151 | fill: Stretch 152 | caret: 153 | frame_time_millis: 500 154 | frames: 155 | - caret_on 156 | - caret_off 157 | arrow_right: 158 | position: [24, 97] 159 | size: [12, 12] 160 | arrow_left: 161 | position: [24, 109] 162 | size: [12, 12] 163 | arrow_down: 164 | position: [36, 97] 165 | size: [12, 12] 166 | arrow_up: 167 | position: [36, 109] 168 | size: [12, 12] 169 | check_normal: 170 | position: [12, 104] 171 | size: [12, 12] 172 | check_active: 173 | position: [12, 116] 174 | size: [12, 12] 175 | check: 176 | states: 177 | Normal: check_normal 178 | Hover: check_normal 179 | Pressed: check_normal 180 | Disabled: check_normal 181 | Active: check_active 182 | Active + Hover: check_active 183 | Active + Pressed: check_active 184 | slider_horizontal: 185 | position: [0, 97] 186 | grid_size_horiz: [5, 4] 187 | slider_vertical: 188 | position: [0, 102] 189 | grid_size_vert: [4, 5] 190 | greyed_out: 191 | position: [17, 98] 192 | size: [4, 4] 193 | fill: Stretch -------------------------------------------------------------------------------- /examples/data/themes/transparent.yml: -------------------------------------------------------------------------------- 1 | # Image definitions for "transparent.png" image. 2 | 3 | image_sets: 4 | gui: 5 | source: transparent 6 | scale: 0.5 7 | images: 8 | cursor_normal: 9 | position: [132, 194] 10 | size: [42, 42] 11 | cursor_pressed: 12 | position: [178, 212] 13 | size: [42, 42] 14 | cursor: 15 | states: 16 | Normal: cursor_normal 17 | Hover: cursor_normal 18 | Pressed: cursor_pressed 19 | window_bg_base: 20 | position: [0, 0] 21 | grid_size: [24, 24] 22 | window_bg: 23 | from: window_bg_base 24 | small_button_normal: 25 | position: [0, 84] 26 | grid_size: [12, 12] 27 | small_button_hover: 28 | position: [36, 84] 29 | grid_size: [12, 12] 30 | small_button_pressed: 31 | position: [72, 84] 32 | grid_size: [12, 12] 33 | small_button_disabled: 34 | position: [108, 84] 35 | grid_size: [12, 12] 36 | small_button_active: 37 | position: [0, 120] 38 | grid_size: [12, 12] 39 | small_button_black: 40 | position: [36, 120] 41 | grid_size: [12, 12] 42 | small_button_flash1: 43 | position: [72, 120] 44 | grid_size: [12, 12] 45 | small_button_flash2: 46 | position: [108, 120] 47 | grid_size: [12, 12] 48 | small_button_normal_flash: 49 | frame_time_millis: 200 50 | frames: 51 | - small_button_flash1 52 | - small_button_flash2 53 | - small_button_flash1 54 | - small_button_normal 55 | input_field: 56 | states: 57 | Normal: small_button_black 58 | Hover: small_button_hover 59 | Pressed: small_button_pressed 60 | Disabled: small_button_disabled 61 | small_button: 62 | states: 63 | Normal: small_button_normal 64 | Hover: small_button_hover 65 | Pressed: small_button_pressed 66 | Disabled: small_button_disabled 67 | Active: small_button_active 68 | Active + Hover: small_button_active 69 | Active + Pressed: small_button_pressed 70 | small_button_no_active: 71 | states: 72 | Normal: small_button_normal 73 | Hover: small_button_hover 74 | Pressed: small_button_pressed 75 | Disabled: small_button_disabled 76 | Active: small_button_normal 77 | Active + Hover: small_button_hover 78 | Active + Pressed: small_button_pressed 79 | small_button_flash: 80 | states: 81 | Normal: small_button_normal_flash 82 | Hover: small_button_hover 83 | Pressed: small_button_pressed 84 | Disabled: small_button_disabled 85 | Active: small_button_active 86 | Active + Hover: small_button_active 87 | Active + Pressed: small_button_pressed 88 | scroll_button: 89 | from: small_button 90 | scrollbar_vertical: 91 | from: empty 92 | scrollbar_horizontal: 93 | from: empty 94 | slider_button: 95 | from: small_button 96 | frame: 97 | from: small_button_normal 98 | close_icon_normal: 99 | position: [156, 84] 100 | size: [24, 24] 101 | close_icon_pressed: 102 | position: [156, 108] 103 | size: [24, 24] 104 | close_icon_disabled: 105 | position: [156, 132] 106 | size: [24, 24] 107 | close_icon: 108 | states: 109 | Normal: close_icon_normal 110 | Hover: close_icon_normal 111 | Pressed: close_icon_pressed 112 | Disabled: close_icon_disabled 113 | progress_bar: 114 | position: [100, 200] 115 | grid_size: [10, 18] 116 | window_handle_normal: 117 | position: [194, 0] 118 | size: [24, 24] 119 | window_handle_hover: 120 | position: [194, 24] 121 | size: [24, 24] 122 | window_handle_pressed: 123 | position: [194, 48] 124 | size: [24, 24] 125 | window_handle_disabled: 126 | position: [194, 72] 127 | size: [24, 24] 128 | window_handle: 129 | states: 130 | Normal: window_handle_normal 131 | Hover: window_handle_hover 132 | Pressed: window_handle_pressed 133 | Disabled: window_handle_disabled 134 | caret_on: 135 | position: [194, 98] 136 | size: [4, 32] 137 | fill: Stretch 138 | caret_off: 139 | position: [200, 98] 140 | size: [4, 32] 141 | fill: Stretch 142 | caret: 143 | frame_time_millis: 500 144 | frames: 145 | - caret_on 146 | - caret_off 147 | arrow_right: 148 | position: [48, 194] 149 | size: [24, 24] 150 | arrow_left: 151 | position: [48, 218] 152 | size: [24, 24] 153 | arrow_down: 154 | position: [72, 194] 155 | size: [24, 24] 156 | arrow_up: 157 | position: [72, 218] 158 | size: [24, 24] 159 | check_normal: 160 | position: [24, 208] 161 | size: [24, 24] 162 | check_active: 163 | position: [24, 232] 164 | size: [24, 24] 165 | check: 166 | states: 167 | Normal: check_normal 168 | Hover: check_normal 169 | Pressed: check_normal 170 | Disabled: check_normal 171 | Active: check_active 172 | Active + Hover: check_active 173 | Active + Pressed: check_active 174 | slider_horizontal: 175 | position: [0, 196] 176 | grid_size_horiz: [10, 8] 177 | slider_vertical: 178 | position: [0, 204] 179 | grid_size_vert: [8, 10] 180 | greyed_out: 181 | position: [34, 196] 182 | size: [10, 10] 183 | fill: Stretch -------------------------------------------------------------------------------- /examples/demo_gl.rs: -------------------------------------------------------------------------------- 1 | use glutin::config::ConfigTemplateBuilder; 2 | use glutin::context::{ContextApi, ContextAttributesBuilder, NotCurrentGlContext, PossiblyCurrentContext, Version}; 3 | use glutin::surface::{Surface, WindowSurface, GlSurface}; 4 | use glutin_winit::DisplayBuilder; 5 | use glutin::display::{GlDisplay, GetGlDisplay}; 6 | use winit::application::ApplicationHandler; 7 | use winit::dpi::LogicalSize; 8 | use winit::event::WindowEvent; 9 | use winit::window::Window; 10 | use winit::raw_window_handle::HasWindowHandle; 11 | 12 | use std::ffi::CString; 13 | use std::num::NonZeroU32; 14 | use std::os::raw::c_char; 15 | 16 | use thyme::{bench, Context, GLRenderer, WinitIo}; 17 | 18 | mod demo; 19 | 20 | const OPENGL_MAJOR_VERSION: u8 = 3; 21 | const OPENGL_MINOR_VERSION: u8 = 2; 22 | 23 | /// A basic RPG character sheet, using the "plain" OpenGL backend. 24 | /// This file contains the application setup code and wgpu specifics. 25 | /// the `demo.rs` file contains the Thyme UI code and logic. 26 | /// A simple party creator and character sheet for an RPG. 27 | fn main() -> Result<(), Box> { 28 | // initialize our very basic logger so error messages go to stdout 29 | thyme::log::init(log::Level::Warn).unwrap(); 30 | 31 | // create glium display 32 | let event_loop = glium::winit::event_loop::EventLoop::builder() 33 | .build()?; 34 | 35 | let attrs = Window::default_attributes() 36 | .with_title("Thyme GL Demo") 37 | .with_inner_size(LogicalSize::new(1280, 720)); 38 | 39 | let display_builder = DisplayBuilder::new().with_window_attributes(Some(attrs)); 40 | let config_template_builder = ConfigTemplateBuilder::new(); 41 | 42 | let (window, gl_config) = display_builder.build(&event_loop, config_template_builder, |mut configs| { 43 | configs.next().unwrap() 44 | })?; 45 | let window = window.unwrap(); 46 | 47 | let window_handle = window.window_handle()?; 48 | let raw_window_handle = window_handle.as_raw(); 49 | 50 | let (width, height): (u32, u32) = window.inner_size().into(); 51 | let attrs = 52 | glutin::surface::SurfaceAttributesBuilder::::new() 53 | .build( 54 | raw_window_handle, 55 | NonZeroU32::new(width).unwrap(), 56 | NonZeroU32::new(height).unwrap(), 57 | ); 58 | 59 | let surface = unsafe { 60 | gl_config.display().create_window_surface(&gl_config, &attrs).unwrap() 61 | }; 62 | 63 | let context_attributes = ContextAttributesBuilder::new() 64 | .with_context_api(ContextApi::OpenGl(Some(Version::new(OPENGL_MAJOR_VERSION, OPENGL_MINOR_VERSION)))) 65 | .build(Some(raw_window_handle)); 66 | 67 | let windowed_context = unsafe { 68 | gl_config.display().create_context(&gl_config, &context_attributes)? 69 | }; 70 | 71 | let display_context = windowed_context.make_current(&surface)?; 72 | 73 | { 74 | let gl_context = display_context.display(); 75 | gl::load_with(|ptr| { 76 | let c_str = CString::new(ptr).unwrap(); 77 | gl_context.get_proc_address(&c_str) as *const _ 78 | }) 79 | } 80 | 81 | // create thyme backend 82 | let mut renderer = thyme::GLRenderer::new(); 83 | let mut context_builder = thyme::ContextBuilder::with_defaults(); 84 | 85 | demo::register_assets(&mut context_builder); 86 | 87 | let window_size = [1280.0, 720.0]; 88 | let mut io = thyme::WinitIo::new(&window, window_size.into())?; 89 | let context = context_builder.build(&mut renderer, &mut io)?; 90 | let party = demo::Party::default(); 91 | 92 | let mut app = AppRunner { io, renderer, context, window, surface, display_context, party, frames: 0 }; 93 | 94 | let start = std::time::Instant::now(); 95 | event_loop.run_app(&mut app)?; 96 | let finish = std::time::Instant::now(); 97 | 98 | log::warn!("Drew {} frames in {:.2}s", app.frames, (finish - start).as_secs_f32()); 99 | 100 | Ok(()) 101 | } 102 | 103 | struct AppRunner { 104 | io: WinitIo, 105 | renderer: GLRenderer, 106 | context: Context, 107 | window: winit::window::Window, 108 | surface: Surface, 109 | display_context: PossiblyCurrentContext, 110 | party: demo::Party, 111 | frames: u32, 112 | } 113 | 114 | impl ApplicationHandler for AppRunner { 115 | fn resumed(&mut self, _event_loop: &winit::event_loop::ActiveEventLoop) { } 116 | 117 | fn about_to_wait(&mut self, _event_loop: &winit::event_loop::ActiveEventLoop) { 118 | self.window.request_redraw(); 119 | } 120 | 121 | fn window_event( 122 | &mut self, 123 | event_loop: &winit::event_loop::ActiveEventLoop, 124 | _window_id: winit::window::WindowId, 125 | event: WindowEvent, 126 | ) { 127 | match event { 128 | WindowEvent::RedrawRequested => { 129 | self.party.check_context_changes(&mut self.context, &mut self.renderer); 130 | 131 | self.renderer.clear_color(0.5, 0.5, 0.5, 1.0); 132 | 133 | bench::run("thyme", || { 134 | self.window.set_cursor_visible(!self.party.theme_has_mouse_cursor()); 135 | 136 | let mut ui = self.context.create_frame(); 137 | 138 | bench::run("frame", || { 139 | demo::build_ui(&mut ui, &mut self.party); 140 | }); 141 | 142 | bench::run("draw", || { 143 | self.renderer.draw_frame(ui); 144 | }); 145 | }); 146 | 147 | self.surface.swap_buffers(&self.display_context).unwrap(); 148 | self.frames += 1; 149 | } 150 | WindowEvent::CloseRequested => event_loop.exit(), 151 | event => { 152 | self.io.handle_event(&mut self.context, &event); 153 | } 154 | } 155 | } 156 | } 157 | 158 | // this is passed as a fn pointer to gl::DebugMessageCallback 159 | // and cannot be marked as an "unsafe extern" 160 | #[unsafe(no_mangle)] 161 | #[allow(clippy::not_unsafe_ptr_arg_deref)] 162 | pub extern "system" fn debug_callback( 163 | _: gl::types::GLenum, 164 | err_type: gl::types::GLenum, 165 | id: gl::types::GLuint, 166 | severity: gl::types::GLenum, 167 | _: gl::types::GLsizei, 168 | message: *const c_char, 169 | _: *mut std::ffi::c_void, 170 | ) { 171 | match err_type { 172 | gl::DEBUG_TYPE_ERROR | gl::DEBUG_TYPE_UNDEFINED_BEHAVIOR => unsafe { 173 | let err_text = std::ffi::CStr::from_ptr(message); 174 | println!( 175 | "Type: {:?} ID: {:?} Severity: {:?}:\n {:#?}", 176 | err_type, 177 | id, 178 | severity, 179 | err_text.to_str().unwrap() 180 | ); 181 | }, 182 | _ => {} 183 | } 184 | } -------------------------------------------------------------------------------- /examples/demo_glium.rs: -------------------------------------------------------------------------------- 1 | use winit::{application::ApplicationHandler, dpi::LogicalSize, event::WindowEvent, window::Window}; 2 | use thyme::{bench, Context, ContextBuilder, GliumRenderer, WinitError, WinitIo}; 3 | 4 | mod demo; 5 | 6 | /// A basic RPG character sheet, using the glium backend. 7 | /// This file contains the application setup code and wgpu specifics. 8 | /// the `demo.rs` file contains the Thyme UI code and logic. 9 | /// A simple party creator and character sheet for an RPG. 10 | fn main() -> Result<(), Box> { 11 | // initialize our very basic logger so error messages go to stdout 12 | thyme::log::init(log::Level::Warn).unwrap(); 13 | 14 | let window_size = [1280.0, 720.0]; 15 | 16 | let event_loop = glium::winit::event_loop::EventLoop::builder() 17 | .build().map_err(|e| thyme::Error::Winit(WinitError::EventLoop(e)))?; 18 | 19 | let attrs = Window::default_attributes() 20 | .with_title("Thyme Demo") 21 | .with_inner_size(LogicalSize::new(window_size[0], window_size[1])); 22 | 23 | // create glium display 24 | let (window, display) = glium::backend::glutin::SimpleWindowBuilder::new() 25 | .set_window_builder(attrs) 26 | .build(&event_loop); 27 | 28 | // create thyme backend 29 | let mut renderer = GliumRenderer::new(&display)?; 30 | let mut io = WinitIo::new(&window, window_size.into())?; 31 | let mut context_builder = ContextBuilder::with_defaults(); 32 | 33 | demo::register_assets(&mut context_builder); 34 | 35 | let context = context_builder.build(&mut renderer, &mut io)?; 36 | 37 | let party = demo::Party::default(); 38 | 39 | let mut app = AppRunner { io, renderer, context, display, window, party, frames: 0 }; 40 | 41 | let start = std::time::Instant::now(); 42 | 43 | event_loop.run_app(&mut app)?; 44 | 45 | let finish = std::time::Instant::now(); 46 | 47 | log::warn!("Drew {} frames in {:.2}s", app.frames, (finish - start).as_secs_f32()); 48 | 49 | Ok(()) 50 | } 51 | 52 | struct AppRunner { 53 | io: WinitIo, 54 | renderer: GliumRenderer, 55 | context: Context, 56 | display: glium::Display, 57 | window: winit::window::Window, 58 | party: demo::Party, 59 | frames: u32, 60 | } 61 | 62 | impl ApplicationHandler for AppRunner { 63 | fn resumed(&mut self, _event_loop: &winit::event_loop::ActiveEventLoop) { } 64 | 65 | fn about_to_wait(&mut self, _event_loop: &winit::event_loop::ActiveEventLoop) { 66 | self.window.request_redraw(); 67 | } 68 | 69 | fn window_event( 70 | &mut self, 71 | event_loop: &winit::event_loop::ActiveEventLoop, 72 | _window_id: winit::window::WindowId, 73 | event: WindowEvent, 74 | ) { 75 | use glium::Surface; 76 | match event { 77 | WindowEvent::RedrawRequested => { 78 | self.party.check_context_changes(&mut self.context, &mut self.renderer); 79 | 80 | let mut target = self.display.draw(); 81 | target.clear_color(0.21, 0.21, 0.21, 1.0); 82 | 83 | bench::run("thyme", || { 84 | self.window.set_cursor_visible(!self.party.theme_has_mouse_cursor()); 85 | 86 | let mut ui = self.context.create_frame(); 87 | 88 | bench::run("frame", || { 89 | demo::build_ui(&mut ui, &mut self.party); 90 | }); 91 | 92 | bench::run("draw", || { 93 | self.renderer.draw_frame(&mut target, ui).unwrap(); 94 | }); 95 | }); 96 | 97 | target.finish().unwrap(); 98 | self.frames += 1; 99 | } 100 | WindowEvent::CloseRequested => event_loop.exit(), 101 | event => { 102 | self.io.handle_event(&mut self.context, &event); 103 | } 104 | } 105 | } 106 | } -------------------------------------------------------------------------------- /examples/hello_gl.rs: -------------------------------------------------------------------------------- 1 | fn main() -> Result<(), Box> { 2 | let app = thyme::AppBuilder::new() 3 | .with_logger() 4 | .with_title("Thyme Gl Demo") 5 | .with_window_size(1280.0, 720.0) 6 | .with_base_dir("examples/data") 7 | .with_theme_files(&["themes/base.yml", "themes/pixel.yml"]) 8 | .with_font_dir("fonts") 9 | .with_image_dir("images") 10 | .build_gl()?; 11 | 12 | app.main_loop(|ui| { 13 | ui.window("window", |ui| { 14 | ui.gap(20.0); 15 | 16 | ui.button("label", "Hello, World!"); 17 | }); 18 | })?; 19 | 20 | Ok(()) 21 | } -------------------------------------------------------------------------------- /examples/hello_glium.rs: -------------------------------------------------------------------------------- 1 | fn main() -> Result<(), Box> { 2 | let app = thyme::AppBuilder::new() 3 | .with_logger() 4 | .with_title("Thyme Glium Demo") 5 | .with_window_size(1280.0, 720.0) 6 | .with_base_dir("examples/data") 7 | .with_theme_files(&["themes/base.yml", "themes/pixel.yml"]) 8 | .with_font_dir("fonts") 9 | .with_image_dir("images") 10 | .build_glium()?; 11 | 12 | app.main_loop(|ui| { 13 | ui.window("window", |ui| { 14 | ui.gap(20.0); 15 | 16 | ui.button("label", "Hello, World!"); 17 | }); 18 | })?; 19 | 20 | Ok(()) 21 | } -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grokmoo/thyme/0794d44fc665e6206ecb49e14eab484a22512a03/screenshot.png -------------------------------------------------------------------------------- /src/bench.rs: -------------------------------------------------------------------------------- 1 | //! Simple benchmarking functionality for supporting thyme. 2 | //! 3 | //! Benchmarks consist of a moving average and associated statistics of a given 4 | //! set of timings. Timings that are grouped together share the same tag. 5 | //! You can pass a block to be timed using [`run`](fn.run.html), or create a handle with 6 | //! [`start`](fn.start.html) and end the timing with [`end`](struct.Handle.html#method.end). 7 | //! Use [`stats`](fn.stats.html) to get a [`Stats`](struct.Stats.html), which is the 8 | //! primary interface for reporting on the timings. 9 | 10 | use std::time::{Duration, Instant}; 11 | 12 | use parking_lot::{const_mutex, Mutex}; 13 | 14 | const MOVING_AVG_LEN: usize = 30; 15 | 16 | static BENCH: Mutex = const_mutex(BenchSet::new()); 17 | 18 | /// Configuration values to pass to the benchmark [`report`](fn.report.html) function. 19 | #[derive(Copy, Clone)] 20 | pub struct ReportConfig { 21 | /// The length of the given report 22 | pub length: ReportConfigLength, 23 | 24 | /// The number of samples to average for the given report 25 | pub samples: ReportConfigSamples, 26 | } 27 | 28 | impl Default for ReportConfig { 29 | fn default() -> Self { 30 | Self::new() 31 | } 32 | } 33 | 34 | impl ReportConfig { 35 | /// Constructs a default Report config with full length and all samples 36 | pub fn new() -> Self { 37 | Self { 38 | length: ReportConfigLength::Long, 39 | samples: ReportConfigSamples::All, 40 | } 41 | } 42 | 43 | /// Builds a report config utilizing the specified `length` 44 | pub fn with_length(self, length: ReportConfigLength) -> Self { 45 | Self { 46 | length, 47 | samples: self.samples, 48 | } 49 | } 50 | 51 | /// Builds a report config utilizing the specified `samples` 52 | pub fn with_samples(self, samples: ReportConfigSamples) -> Self { 53 | Self { 54 | length: self.length, 55 | samples, 56 | } 57 | } 58 | 59 | /// Builds a report with short report length 60 | pub fn with_short_length(self) -> Self { 61 | Self { 62 | length: ReportConfigLength::Short, 63 | samples: self.samples, 64 | } 65 | } 66 | 67 | /// Builds a report with long report length. 68 | pub fn with_long_length(self) -> Self { 69 | Self { 70 | length: ReportConfigLength::Long, 71 | samples: self.samples, 72 | } 73 | } 74 | 75 | /// Builds a report using all samples 76 | pub fn with_all_samples(self) -> Self { 77 | Self { 78 | length: self.length, 79 | samples: ReportConfigSamples::All 80 | } 81 | } 82 | 83 | /// Builds a report using a moving average of samples 84 | pub fn with_moving_average_samples(self) -> Self { 85 | Self { 86 | length: self.length, 87 | samples: ReportConfigSamples::MovingAverage, 88 | } 89 | } 90 | } 91 | 92 | /// For a given benchmark report, whether to use the condensed report output 93 | #[derive(Copy, Clone)] 94 | pub enum ReportConfigLength { 95 | /// Use the condensed report output 96 | Short, 97 | 98 | /// Use the full length report output 99 | Long, 100 | } 101 | 102 | /// For a given benchmark report, how many samples to average 103 | #[derive(Copy, Clone)] 104 | pub enum ReportConfigSamples { 105 | /// Average all samples that have ever been taken 106 | All, 107 | 108 | /// Perform a moving average of the most recent samples taken 109 | MovingAverage, 110 | } 111 | 112 | impl ReportConfigSamples { 113 | fn limit(&self) -> Option { 114 | match self { 115 | ReportConfigSamples::All => None, 116 | ReportConfigSamples::MovingAverage => Some(MOVING_AVG_LEN), 117 | } 118 | } 119 | } 120 | 121 | /// A benchmarking handle created by [`start`](fn.start.html). [`end`](#method.end) this to 122 | /// finish the given benchmark timing 123 | pub struct Handle { 124 | index: usize 125 | } 126 | 127 | impl Handle { 128 | /// Finish the timing associated with this handle. 129 | pub fn end(self) { 130 | end(self); 131 | } 132 | } 133 | 134 | /// Runs the specified closure `block` as a benchmark timing 135 | /// with the given `tag`. 136 | pub fn run Ret>(tag: &str, block: F) -> Ret { 137 | let handle = start(tag); 138 | let ret = (block)(); 139 | end(handle); 140 | ret 141 | } 142 | 143 | /// Starts a benchmark timing with the given `tag`. You 144 | /// must [`end`](struct.Handle.html#method.end) the returned [`Handle`](struct.Handle.html) to complete 145 | /// the timing. 146 | #[must_use] 147 | pub fn start(tag: &str) -> Handle { 148 | let mut bench = BENCH.lock(); 149 | bench.start(tag) 150 | } 151 | 152 | /// Returns a `Stats` object for the benchmark timings 153 | /// associated with the given `tag`, utilizing the specified 154 | /// `samples`. 155 | pub fn stats(tag: &str, samples: ReportConfigSamples) -> Stats { 156 | let bench = BENCH.lock(); 157 | 158 | bench.stats(tag, samples.limit()) 159 | } 160 | 161 | /// Returns stats for all benchmark tags that have been benchmarked. 162 | /// See [`stats`](fn.stats.html). 163 | pub fn stats_all(samples: ReportConfigSamples) -> Vec { 164 | let bench = BENCH.lock(); 165 | bench.stats_all(samples.limit()) 166 | } 167 | 168 | /// Generate a report string for all tags that have been 169 | /// benchmarked. See [`report`](fn.report.html) 170 | pub fn report_all(config: ReportConfig) -> Vec { 171 | let bench = BENCH.lock(); 172 | let limit = config.samples.limit(); 173 | 174 | match config.length { 175 | ReportConfigLength::Long => bench.report_all(limit), 176 | ReportConfigLength::Short => bench.short_report_all(limit), 177 | } 178 | } 179 | 180 | /// Generate a report string for the given `tag`, utilizing the 181 | /// specified report `config`. The report will include the 182 | /// data in the [`Stats`](struct.Stats.html) associated with this `tag`, 183 | /// and be formatted with the appropriate units. 184 | pub fn report(tag: &str, config: ReportConfig) -> String { 185 | let bench = BENCH.lock(); 186 | let limit = config.samples.limit(); 187 | 188 | match config.length { 189 | ReportConfigLength::Long => bench.report(tag, limit), 190 | ReportConfigLength::Short => bench.short_report(tag, limit), 191 | } 192 | } 193 | 194 | fn end(handle: Handle) { 195 | let mut bench = BENCH.lock(); 196 | bench.end(handle); 197 | } 198 | 199 | /// Statistics associated with a given set of benchmark timings. 200 | /// These are obtained with the `stats` method for a given tag. 201 | /// Statistics are for a moving average of the last N timings for the 202 | /// tag, where N is currently hardcoded to 30. 203 | #[derive(Debug, Copy, Clone)] 204 | pub struct Stats { 205 | count: usize, 206 | total_s: f32, 207 | average_s: f32, 208 | stdev_s: f32, 209 | max_s: f32, 210 | unit: Unit, 211 | } 212 | 213 | impl Default for Stats { 214 | fn default() -> Self { 215 | Stats { 216 | count: 0, 217 | total_s: 0.0, 218 | average_s: 0.0, 219 | stdev_s: 0.0, 220 | max_s: 0.0, 221 | unit: Unit::Seconds, 222 | } 223 | } 224 | } 225 | 226 | impl Stats { 227 | /// Returns the sum total of the timings, in the current unit 228 | /// of this `Stats`. 229 | pub fn total(&self) -> f32 { 230 | self.total_s * self.unit.multiplier() 231 | } 232 | 233 | /// Returns the average of the timings, in the current unit 234 | /// of this `Stats`. 235 | pub fn average(&self) -> f32 { 236 | self.average_s * self.unit.multiplier() 237 | } 238 | 239 | /// Returns the standard devication of the timings, in the current unit 240 | /// of this `Stats`. 241 | pub fn stdev(&self) -> f32 { 242 | self.stdev_s * self.unit.multiplier() 243 | } 244 | 245 | /// Returns the maximum of the timings, in the current unit 246 | /// of this `Stats`. 247 | pub fn max(&self) -> f32 { 248 | self.max_s * self.unit.multiplier() 249 | } 250 | 251 | /// Returns the postfix string of the Unit associated with this 252 | /// `Stats`, such as "s" for Seconds, "ms" for milliseconds, and 253 | /// "µs" for microseconds. 254 | pub fn unit_postfix(&self) -> &'static str { 255 | self.unit.postfix() 256 | } 257 | 258 | /// Automatically picks an appropriate unit for this `Stats` based 259 | /// on the size of the average value, and converts the stats to 260 | /// use that unit. 261 | pub fn pick_unit(self) -> Stats { 262 | const CHANGE_VALUE: f32 = 0.0999999; 263 | 264 | if self.average_s > CHANGE_VALUE { 265 | self.in_seconds() 266 | } else if self.average_s * Unit::Millis.multiplier() > CHANGE_VALUE { 267 | self.in_millis() 268 | } else { 269 | self.in_micros() 270 | } 271 | } 272 | 273 | /// Converts this `Stats` to use seconds as a unit 274 | pub fn in_seconds(mut self) -> Stats { 275 | self.unit = Unit::Seconds; 276 | self 277 | } 278 | 279 | /// Converts this `Stats` to use milliseconds as a unit 280 | pub fn in_millis(mut self) -> Stats { 281 | self.unit = Unit::Millis; 282 | self 283 | } 284 | 285 | /// Converts this `Stats` to use microseconds as a unit 286 | pub fn in_micros(mut self) -> Stats { 287 | self.unit = Unit::Micros; 288 | self 289 | } 290 | } 291 | 292 | struct BenchSet { 293 | // TODO maybe use HashMap here once we can create a hashmap in const 294 | benches: Vec, 295 | } 296 | 297 | impl BenchSet { 298 | const fn new() -> BenchSet { 299 | BenchSet { 300 | benches: Vec::new() 301 | } 302 | } 303 | 304 | fn start(&mut self, tag: &str) -> Handle { 305 | // TODO handle multiple starts at the same time? 306 | 307 | // check if bench with this tag already exists 308 | for (index, bench) in self.benches.iter_mut().enumerate() { 309 | if bench.tag == tag { 310 | bench.start = Some(Instant::now()); 311 | return Handle { index }; 312 | } 313 | } 314 | 315 | // create new bench 316 | let mut bench = Bench::new(tag.to_string()); 317 | bench.start = Some(Instant::now()); 318 | let index = self.benches.len(); 319 | self.benches.push(bench); 320 | 321 | Handle { index } 322 | } 323 | 324 | fn end(&mut self, handle: Handle) { 325 | let bench = &mut self.benches[handle.index]; 326 | let duration = Instant::now() - bench.start.take().unwrap_or_else(Instant::now); 327 | bench.history.push(duration); 328 | } 329 | 330 | fn stats(&self, tag: &str, limit: Option) -> Stats { 331 | for bench in self.benches.iter() { 332 | if bench.tag == tag { 333 | return bench.stats(limit); 334 | } 335 | } 336 | 337 | Stats::default() 338 | } 339 | 340 | fn stats_all(&self, limit: Option) -> Vec { 341 | let mut out = Vec::new(); 342 | for bench in self.benches.iter() { 343 | out.push(bench.stats(limit)); 344 | } 345 | out 346 | } 347 | 348 | fn report(&self, tag: &str, limit: Option) -> String { 349 | for bench in self.benches.iter() { 350 | if bench.tag == tag { 351 | return bench.report_str(limit); 352 | } 353 | } 354 | 355 | "Bench not found".to_string() 356 | } 357 | 358 | fn short_report(&self, tag: &str, limit: Option) -> String { 359 | for bench in self.benches.iter() { 360 | if bench.tag == tag { 361 | return bench.short_report_str(limit); 362 | } 363 | } 364 | 365 | "Bench not found".to_string() 366 | } 367 | 368 | fn short_report_all(&self, limit: Option) -> Vec { 369 | let mut out = Vec::new(); 370 | for bench in self.benches.iter() { 371 | out.push(bench.short_report_str(limit)); 372 | } 373 | out 374 | } 375 | 376 | fn report_all(&self, limit: Option) -> Vec { 377 | let mut out = Vec::new(); 378 | for bench in self.benches.iter() { 379 | out.push(bench.report_str(limit)); 380 | } 381 | out 382 | } 383 | } 384 | 385 | 386 | #[derive(Copy, Clone, Debug)] 387 | enum Unit { 388 | Seconds, 389 | Millis, 390 | Micros, 391 | } 392 | 393 | impl Unit { 394 | fn postfix(self) -> &'static str { 395 | use Unit::*; 396 | match self { 397 | Seconds => "s", 398 | Millis => "ms", 399 | Micros => "µs" 400 | } 401 | } 402 | 403 | fn multiplier(self) -> f32 { 404 | use Unit::*; 405 | match self { 406 | Seconds => 1.0, 407 | Millis => 1000.0, 408 | Micros => 1_000_000.0, 409 | } 410 | } 411 | } 412 | 413 | struct Bench { 414 | tag: String, 415 | history: Vec, 416 | start: Option, 417 | } 418 | 419 | impl Bench { 420 | fn new(tag: String) -> Bench { 421 | Bench { 422 | history: Vec::new(), 423 | start: None, 424 | tag, 425 | } 426 | } 427 | 428 | fn stats(&self, limit: Option) -> Stats { 429 | let len = self.history.len(); 430 | let count = match limit { 431 | None => len, 432 | Some(limit) => std::cmp::min(len, limit), 433 | }; 434 | 435 | let data = || { self.history.iter().rev().take(count) }; 436 | 437 | let sum = (data)().sum::().as_secs_f32(); 438 | let max = match (data)().max() { 439 | None => 0.0, 440 | Some(dur) => dur.as_secs_f32(), 441 | }; 442 | 443 | let avg = sum / (count as f32); 444 | 445 | let numer: f32 = (data)().map(|d| (d.as_secs_f32() - avg) * (d.as_secs_f32() - avg)).sum(); 446 | 447 | let stdev_sq = numer / (count as f32 - 1.0); 448 | let stdev = stdev_sq.sqrt(); 449 | 450 | Stats { 451 | count, 452 | total_s: sum, 453 | average_s: avg, 454 | stdev_s: stdev, 455 | max_s: max, 456 | unit: Unit::Seconds, 457 | } 458 | } 459 | 460 | fn short_report_str(&self, limit: Option) -> String { 461 | let stats = self.stats(limit).pick_unit(); 462 | if self.history.len() == 1 { 463 | format!("{}: {:.2} {}", self.tag, stats.average(), stats.unit_postfix()) 464 | } else { 465 | format!( 466 | "{}: {:.2} ± {:.2} {}", 467 | self.tag, stats.average(), stats.stdev(), stats.unit_postfix(), 468 | ) 469 | } 470 | } 471 | 472 | fn report_str(&self, limit: Option) -> String { 473 | let stats = self.stats(limit).pick_unit(); 474 | if self.history.len() == 1 { 475 | format!("{}: {:.2} {}", self.tag, stats.average(), stats.unit_postfix()) 476 | } else { 477 | format!( 478 | "{} ({} Samples): {:.2} ± {:.2}; max {:.2}, total {:.2} {}", 479 | self.tag, stats.count, stats.average(), stats.stdev(), stats.max(), stats.total(), stats.unit_postfix(), 480 | ) 481 | } 482 | } 483 | } 484 | -------------------------------------------------------------------------------- /src/context_builder.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use crate::{Error, Context}; 4 | use crate::resource::ResourceSet; 5 | use crate::theme_definition::ThemeDefinition; 6 | use crate::render::{Renderer, IO}; 7 | 8 | /// Global options that may be specified when building the Thyme context with 9 | /// [`ContextBuilder`](struct.ContextBuilder.html). These options 10 | /// cannot be changed afterwards. 11 | #[derive(Clone)] 12 | pub struct BuildOptions { 13 | /// Whether to enable background file monitoring for live reload. Note that 14 | /// to actually make use of this feature, you will need to call 15 | /// [`check_live_reload`](struct.Context.html#method.check_live_reload), typically 16 | /// once between each frame. The default value is `true`. 17 | pub enable_live_reload: bool, 18 | 19 | /// The amount of time in milliseconds that a widget must be hovered for a tooltip 20 | /// to show up. 21 | pub tooltip_time: u32, 22 | 23 | /// The number of lines that scrollbars will scroll per mouse scroll. 24 | pub line_scroll: f32, 25 | } 26 | 27 | impl Default for BuildOptions { 28 | fn default() -> Self { 29 | Self { 30 | enable_live_reload: true, 31 | tooltip_time: 0, 32 | line_scroll: 20.0, 33 | } 34 | } 35 | } 36 | 37 | /// Structure to register resources and ultimately build the main Thyme [`Context`](struct.Context.html). 38 | /// 39 | /// You pass resources to it to register them with Thyme. Once this process is complete, call 40 | /// [`build`](struct.ContextBuilder.html#method.build) to create your [`Context`](struct.Context.html). 41 | pub struct ContextBuilder { 42 | resources: ResourceSet, 43 | options: BuildOptions, 44 | } 45 | 46 | impl ContextBuilder { 47 | /** 48 | Creates a new `ContextBuilder`, using the default [`BuildOptions`](struct.BuildOptions.html) 49 | 50 | # Example 51 | ```no_run 52 | let mut context_builder = thyme::ContextBuilder::with_defaults(); 53 | context_builder.register_theme(theme)?; 54 | ... 55 | ``` 56 | **/ 57 | pub fn with_defaults() -> ContextBuilder { 58 | ContextBuilder::new(BuildOptions::default()) 59 | } 60 | 61 | /// Creates a new `ContextBuilder`, using the specified [`BuildOptions`](struct.BuildOptions.html) 62 | pub fn new(options: BuildOptions) -> ContextBuilder { 63 | ContextBuilder { 64 | resources: ResourceSet::new(options.enable_live_reload), 65 | options, 66 | } 67 | } 68 | 69 | /// Sets the theme for this context. The theme for your UI will be deserialized from 70 | /// `theme`. For example, `theme` could be a [`serde_json Value`](https://docs.serde.rs/serde_json/value/enum.Value.html) or 71 | /// [`serde_yaml Value`](https://docs.serde.rs/serde_yaml/enum.Value.html). See [`the crate root`](index.html) for a 72 | /// discussion of the theme format. If this method is called multiple times, only the last 73 | /// theme is used 74 | pub fn register_theme<'a, T: serde::Deserializer<'a>>(&mut self, theme: T) -> Result<(), T::Error> { 75 | log::debug!("Registering theme"); 76 | 77 | let theme_def: ThemeDefinition = serde::Deserialize::deserialize(theme)?; 78 | self.resources.register_theme(theme_def); 79 | Ok(()) 80 | } 81 | 82 | /// Sets the theme for this context by reading from the file at the specified `path`. The file is 83 | /// deserialized as serde YAML files. See [`register_theme`](#method.register_theme) 84 | pub fn register_theme_from_file( 85 | &mut self, 86 | path: &Path, 87 | ) -> Result<(), Error> { 88 | log::debug!("Reading theme from file: '{:?}'", path); 89 | 90 | self.resources.register_theme_from_files(&[path]); 91 | 92 | Ok(()) 93 | } 94 | 95 | /// Sets the theme for this context by reading from the specified list of files. The files are each read into a 96 | /// string and then concatenated together. The string is then deserialized as serde YAML. See 97 | /// [`register_theme`](#method.register_theme) 98 | pub fn register_theme_from_files( 99 | &mut self, 100 | paths: &[&Path], 101 | ) -> Result<(), Error> { 102 | log::debug!("Reading theme from files: '{:?}'", paths); 103 | 104 | self.resources.register_theme_from_files(paths); 105 | Ok(()) 106 | } 107 | 108 | /// Registers the font data located in the file at the specified `path` with Thyme via the specified `id`. 109 | /// See [`register_font`](#method.register_font) 110 | pub fn register_font_from_file>( 111 | &mut self, 112 | id: T, 113 | path: &Path, 114 | ) { 115 | let id = id.into(); 116 | log::debug!("Reading font source '{}' from file: '{:?}'", id, path); 117 | self.resources.register_font_from_file(id, path); 118 | } 119 | 120 | /// Registers the font data for use with Thyme via the specified `id`. The `data` must consist 121 | /// of the full binary for a valid TTF or OTF file. 122 | /// Once the font has been registered, it can be accessed in your theme file via the font `source`. 123 | pub fn register_font>( 124 | &mut self, 125 | id: T, 126 | data: Vec 127 | ) { 128 | let id = id.into(); 129 | log::debug!("Registering font source '{}'", id); 130 | self.resources.register_font_from_data(id, data); 131 | } 132 | 133 | /// Reads a texture from the specified image file. See [`register_texture`](#method.register_texture). 134 | /// Requires you to enable the `image` feature in `Cargo.toml` to enable the dependancy on the 135 | /// [`image`](https://github.com/image-rs/image) crate. 136 | #[cfg(feature="image")] 137 | pub fn register_texture_from_file>( 138 | &mut self, 139 | id: T, 140 | path: &Path, 141 | ) { 142 | let id = id.into(); 143 | log::debug!("Reading texture '{}' from file: '{:?}'", id, path); 144 | self.resources.register_image_from_file(id, path); 145 | } 146 | 147 | /// Registers the image data for use with Thyme via the specified `id`. The `data` must consist of 148 | /// raw binary image data in RGBA format, with 4 bytes per pixel. The data must start at the 149 | /// bottom-left hand corner pixel and progress left-to-right and bottom-to-top. `data.len()` must 150 | /// equal `dimensions.0 * dimensions.1 * 4` 151 | /// Once the image has been registered, it can be accessed in your theme file via the image `source`. 152 | pub fn register_texture>( 153 | &mut self, 154 | id: T, 155 | data: Vec, 156 | dimensions: (u32, u32), 157 | ) { 158 | let id = id.into(); 159 | log::debug!("Registering texture '{}'", id); 160 | self.resources.register_image_from_data(id, data, dimensions.0, dimensions.1); 161 | } 162 | 163 | /// Consumes this builder and releases the borrows on the [`Renderer`](trait.Renderer.html) and [`IO`](trait.IO.html), 164 | /// so they can be used further. Builds a [`Context`](struct.Context.html). 165 | pub fn build(mut self, renderer: &mut R, io: &mut I) -> Result { 166 | log::info!("Building Thyme Context"); 167 | let scale_factor = io.scale_factor(); 168 | let display_size = io.display_size(); 169 | 170 | self.resources.cache_data()?; 171 | let themes = self.resources.build_assets(renderer, scale_factor)?; 172 | Ok(Context::new(self.resources, self.options, themes, display_size, scale_factor)) 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/font.rs: -------------------------------------------------------------------------------- 1 | use rustc_hash::FxHashMap; 2 | 3 | use crate::theme_definition::CharacterRange; 4 | use crate::render::{TexCoord, DrawList, FontHandle, DummyDrawList}; 5 | use crate::{Point, Rect, Align, Color}; 6 | 7 | pub struct FontSource { 8 | pub(crate) font: rusttype::Font<'static>, 9 | } 10 | 11 | pub struct FontChar { 12 | pub size: Point, 13 | pub(crate) tex_coords: [TexCoord; 2], 14 | pub x_advance: f32, 15 | pub y_offset: f32, 16 | } 17 | 18 | impl Default for FontChar { 19 | fn default() -> Self { 20 | FontChar { 21 | size: Point::default(), 22 | tex_coords: [TexCoord::new(0.0, 0.0), TexCoord::new(0.0, 0.0)], 23 | x_advance: 0.0, 24 | y_offset: 0.0, 25 | } 26 | } 27 | } 28 | 29 | #[derive(Copy, Clone, Debug)] 30 | pub struct FontSummary { 31 | pub handle: FontHandle, 32 | pub line_height: f32, 33 | } 34 | 35 | pub struct Font { 36 | handle: FontHandle, 37 | characters: FxHashMap, 38 | line_height: f32, 39 | ascent: f32, 40 | } 41 | 42 | impl Font { 43 | pub(crate) fn new(handle: FontHandle, characters: FxHashMap, line_height: f32, ascent: f32) -> Font { 44 | Font { 45 | handle, 46 | characters, 47 | line_height, 48 | ascent, 49 | } 50 | } 51 | 52 | fn char(&self, c: char) -> Option<&FontChar> { 53 | self.characters.get(&c) 54 | } 55 | 56 | pub fn line_height(&self) -> f32 { self.line_height } 57 | 58 | pub fn ascent(&self) -> f32 { self.ascent } 59 | 60 | pub fn handle(&self) -> FontHandle { self.handle } 61 | 62 | pub(crate) fn layout( 63 | &self, 64 | params: FontDrawParams, 65 | text: &str, 66 | cursor: &mut Point, 67 | ) { 68 | let mut draw_list = DummyDrawList::new(); 69 | let mut renderer = FontRenderer::new( 70 | self, 71 | &mut draw_list, 72 | params, 73 | Rect::default(), 74 | ); 75 | renderer.render(text); 76 | 77 | if text.is_empty() { 78 | // compute the cursor position for empty text 79 | renderer.adjust_line_x(); 80 | renderer.size.y += 2.0 * renderer.font.line_height; 81 | renderer.adjust_all_y(); 82 | } 83 | 84 | *cursor = renderer.pos; 85 | } 86 | 87 | pub(crate) fn draw( 88 | &self, 89 | draw_list: &mut D, 90 | params: FontDrawParams, 91 | text: &str, 92 | clip: Rect, 93 | ) { 94 | let mut renderer = FontRenderer::new( 95 | self, 96 | draw_list, 97 | params, 98 | clip 99 | ); 100 | renderer.render(text); 101 | } 102 | } 103 | 104 | struct FontRenderer<'a, D> { 105 | font: &'a Font, 106 | draw_list: &'a mut D, 107 | initial_index: usize, 108 | 109 | scale_factor: f32, 110 | clip: Rect, 111 | align: Align, 112 | color: Color, 113 | 114 | area_size: Point, 115 | initial_pos: Point, 116 | 117 | pos: Point, 118 | size: Point, 119 | cur_line_index: usize, 120 | 121 | cur_word: Vec<&'a FontChar>, 122 | cur_word_width: f32, 123 | 124 | is_first_line_with_indent: bool, 125 | } 126 | 127 | impl<'a, D: DrawList> FontRenderer<'a, D> { 128 | fn new( 129 | font: &'a Font, 130 | draw_list: &'a mut D, 131 | params: FontDrawParams, 132 | clip: Rect, 133 | ) -> FontRenderer<'a, D> { 134 | let initial_index = draw_list.len(); 135 | 136 | FontRenderer { 137 | font, 138 | draw_list, 139 | initial_index, 140 | align: params.align, 141 | color: params.color, 142 | scale_factor: params.scale_factor, 143 | clip, 144 | area_size: params.area_size, 145 | initial_pos: params.pos, 146 | pos: Point::new(params.pos.x + params.indent, params.pos.y), 147 | size: Point::new(params.indent, 0.0), 148 | cur_line_index: initial_index, 149 | cur_word: Vec::new(), 150 | cur_word_width: 0.0, 151 | is_first_line_with_indent: params.indent > 0.0, 152 | } 153 | } 154 | 155 | fn render(&mut self, text: &str) { 156 | for c in text.chars() { 157 | let font_char = match self.font.char(c) { 158 | None => continue, // TODO draw a special character here? 159 | Some(char) => char, 160 | }; 161 | 162 | if c == '\n' { 163 | self.draw_cur_word(); 164 | self.next_line(); 165 | } else if c.is_whitespace() { 166 | self.draw_cur_word(); 167 | 168 | // don't draw whitespace at the start of a line 169 | if self.cur_line_index != self.draw_list.len() || self.is_first_line_with_indent { 170 | self.pos.x += font_char.x_advance; 171 | self.size.x += font_char.x_advance; 172 | } 173 | 174 | continue; 175 | } 176 | 177 | self.cur_word_width += font_char.x_advance; 178 | self.cur_word.push(font_char); 179 | 180 | if self.size.x + self.cur_word_width > self.area_size.x { 181 | //if the word was so long that we drew nothing at all 182 | if self.cur_line_index == self.draw_list.len() && !self.is_first_line_with_indent { 183 | self.draw_cur_word(); 184 | self.next_line(); 185 | } else { 186 | self.next_line(); 187 | self.draw_cur_word(); 188 | } 189 | } 190 | } 191 | 192 | self.draw_cur_word(); 193 | 194 | if self.cur_line_index < self.draw_list.len() { 195 | // adjust characters on the last line 196 | self.adjust_line_x(); 197 | self.size.y += self.font.line_height; 198 | } 199 | 200 | self.adjust_all_y(); 201 | } 202 | 203 | fn draw_cur_word(&mut self) { 204 | for font_char in self.cur_word.drain(..) { 205 | let x = (self.pos.x * self.scale_factor).round() / self.scale_factor; 206 | let y = (self.pos.y + font_char.y_offset + self.font.ascent).round(); 207 | 208 | self.draw_list.push_rect( 209 | [x, y], 210 | [font_char.size.x, font_char.size.y], 211 | font_char.tex_coords, 212 | self.color, 213 | self.clip, 214 | ); 215 | self.pos.x += font_char.x_advance; 216 | self.size.x += font_char.x_advance; 217 | } 218 | self.cur_word_width = 0.0; 219 | } 220 | 221 | fn next_line(&mut self) { 222 | self.is_first_line_with_indent = false; 223 | self.pos.y += self.font.line_height; 224 | self.size.y += self.font.line_height; 225 | 226 | self.adjust_line_x(); 227 | self.pos.x = self.initial_pos.x; 228 | self.cur_line_index = self.draw_list.len(); 229 | self.size.x = 0.0; 230 | } 231 | 232 | fn adjust_all_y(&mut self) { 233 | use Align::*; 234 | let y_offset = match self.align { 235 | TopLeft => 0.0, 236 | TopRight => 0.0, 237 | BotLeft => self.area_size.y - self.size.y, 238 | BotRight => self.area_size.y - self.size.y, 239 | Left => (self.area_size.y - self.size.y) / 2.0, 240 | Right => (self.area_size.y - self.size.y) / 2.0, 241 | Bot => self.area_size.y - self.size.y, 242 | Top => 0.0, 243 | Center => (self.area_size.y - self.size.y) / 2.0, 244 | }; 245 | 246 | self.pos.y += y_offset; 247 | self.draw_list.back_adjust_positions( 248 | self.initial_index, 249 | Point { x: 0.0, y: y_offset } 250 | ); 251 | } 252 | 253 | fn adjust_line_x(&mut self) { 254 | use Align::*; 255 | let x_offset = match self.align { 256 | TopLeft => 0.0, 257 | TopRight => self.area_size.x - self.size.x, 258 | BotLeft => 0.0, 259 | BotRight => self.area_size.x - self.size.x, 260 | Left => 0.0, 261 | Right => self.area_size.x - self.size.x, 262 | Bot => (self.area_size.x - self.size.x) / 2.0, 263 | Top => (self.area_size.x - self.size.x) / 2.0, 264 | Center => (self.area_size.x - self.size.x) / 2.0, 265 | }; 266 | 267 | self.pos.x += x_offset; 268 | 269 | let x = (x_offset * self.scale_factor).round() / self.scale_factor; 270 | 271 | self.draw_list.back_adjust_positions( 272 | self.cur_line_index, 273 | Point { x, y: 0.0 } 274 | ); 275 | } 276 | } 277 | 278 | pub(crate) struct FontTextureOut { 279 | pub font: Font, 280 | pub data: Vec, 281 | pub tex_width: u32, 282 | pub tex_height: u32, 283 | } 284 | 285 | pub(crate) struct FontTextureWriter<'a> { 286 | // current state 287 | tex_x: u32, 288 | tex_y: u32, 289 | max_row_height: u32, 290 | 291 | //input 292 | tex_width: u32, 293 | tex_height: u32, 294 | font: &'a rusttype::Font<'a>, 295 | font_scale: rusttype::Scale, 296 | 297 | //output 298 | data: Vec, 299 | characters: FxHashMap, 300 | } 301 | 302 | impl<'a> FontTextureWriter<'a> { 303 | pub fn new(font: &'a rusttype::Font<'a>, ranges: &[CharacterRange], size: f32, scale: f32) -> FontTextureWriter<'a> { 304 | // TODO if the approximation here doesn't work in practice, may need to do 2 passes over the font. 305 | // first pass would just determine the texture bounds. 306 | 307 | // count number of characters and size texture conservatively based on how much space the characters should need 308 | let count = ranges.iter().fold(0, |accum, range| accum + (range.upper - range.lower + 1)); 309 | let rows = (count as f32).sqrt().ceil(); 310 | const FUDGE_FACTOR: f32 = 1.2; // factor for characters with tails and wider than usual characters 311 | let tex_size = (rows * size * FUDGE_FACTOR * scale).ceil() as u32; 312 | log::info!("Using texture of size {} for {} characters in font of size {}.", tex_size, count, size * scale); 313 | 314 | let tex_width = tex_size; 315 | let tex_height = tex_size; 316 | 317 | let data = vec![0u8; (tex_width * tex_height) as usize]; 318 | let font_scale = rusttype::Scale { x: size * scale, y: size * scale }; 319 | 320 | FontTextureWriter { 321 | tex_x: 0, 322 | tex_y: 0, 323 | max_row_height: 0, 324 | tex_width, 325 | tex_height, 326 | font, 327 | font_scale, 328 | data, 329 | characters: FxHashMap::default(), 330 | } 331 | } 332 | 333 | pub fn write(mut self, handle: FontHandle, ranges: &[CharacterRange]) -> Result { 334 | self.characters.insert('\n', FontChar::default()); 335 | 336 | for range in ranges { 337 | for codepoint in range.lower..=range.upper { 338 | let c = match std::char::from_u32(codepoint) { 339 | None => { 340 | log::warn!("Character range {:?} contains invalid codepoint {}", range, codepoint); 341 | break; 342 | }, Some(c) => c, 343 | }; 344 | 345 | let font_char = self.add_char(c); 346 | self.characters.insert(c, font_char); 347 | } 348 | } 349 | 350 | let v_metrics = self.font.v_metrics(self.font_scale); 351 | 352 | let font_out = Font::new( 353 | handle, 354 | self.characters, 355 | v_metrics.ascent - v_metrics.descent + v_metrics.line_gap, 356 | v_metrics.ascent, 357 | ); 358 | 359 | Ok(FontTextureOut { 360 | font: font_out, 361 | data: self.data, 362 | tex_width: self.tex_width, 363 | tex_height: self.tex_height, 364 | }) 365 | } 366 | 367 | fn add_char( 368 | &mut self, 369 | c: char, 370 | ) -> FontChar { 371 | let glyph = self.font.glyph(c) 372 | .scaled(self.font_scale) 373 | .positioned(rusttype::Point { x: 0.0, y: 0.0 }); 374 | 375 | // compute the glyph size. use a minimum size of (1,1) for spaces 376 | let y_offset = glyph.pixel_bounding_box().map_or(0.0, |bb| bb.min.y as f32); 377 | let bounding_box = glyph.pixel_bounding_box() 378 | .map_or((1, 1), |bb| (bb.width() as u32, bb.height() as u32)); 379 | 380 | if self.tex_x + bounding_box.0 >= self.tex_width { 381 | // move to next row 382 | self.tex_x = 0; 383 | self.tex_y = self.tex_y + self.max_row_height + 1; 384 | self.max_row_height = 0; 385 | } 386 | 387 | assert!(bounding_box.0 + self.tex_x < self.tex_width); 388 | assert!(bounding_box.1 + self.tex_y < self.tex_height); 389 | 390 | self.max_row_height = self.max_row_height.max(bounding_box.1); 391 | 392 | glyph.draw(|x, y, val| { 393 | let index = (self.tex_x + x) + (self.tex_y + y) * self.tex_width; 394 | let value = (val * 255.0).round() as u8; 395 | self.data[index as usize] = value; 396 | }); 397 | 398 | let tex_coords = [ 399 | TexCoord::new( 400 | self.tex_x as f32 / self.tex_width as f32, 401 | self.tex_y as f32 / self.tex_height as f32 402 | ), 403 | TexCoord::new( 404 | (self.tex_x + bounding_box.0) as f32 / self.tex_width as f32, 405 | (self.tex_y + bounding_box.1) as f32 / self.tex_height as f32 406 | ), 407 | ]; 408 | 409 | self.tex_x += bounding_box.0 + 1; 410 | 411 | FontChar { 412 | size: (bounding_box.0 as f32, bounding_box.1 as f32).into(), 413 | tex_coords, 414 | x_advance: glyph.unpositioned().h_metrics().advance_width, 415 | y_offset, 416 | } 417 | } 418 | } 419 | 420 | pub struct FontDrawParams { 421 | pub area_size: Point, 422 | pub pos: Point, 423 | pub indent: f32, 424 | pub align: Align, 425 | pub color: Color, 426 | pub scale_factor: f32, 427 | } -------------------------------------------------------------------------------- /src/gl_backend/program.rs: -------------------------------------------------------------------------------- 1 | pub struct Program { 2 | program_handle: u32, 3 | } 4 | 5 | impl Program { 6 | pub fn new(vertex_shader: &str, geom_shader: &str, fragment_shader: &str) -> Program { 7 | let program_handle = unsafe { gl::CreateProgram() }; 8 | 9 | let vertex_shader = unsafe { create_shader(gl::VERTEX_SHADER, vertex_shader) }; 10 | let geom_shader = unsafe { create_shader(gl::GEOMETRY_SHADER, geom_shader) }; 11 | let fragment_shader = unsafe { create_shader(gl::FRAGMENT_SHADER, fragment_shader) }; 12 | 13 | unsafe { 14 | gl::AttachShader(program_handle, vertex_shader); 15 | gl::AttachShader(program_handle, geom_shader); 16 | gl::AttachShader(program_handle, fragment_shader); 17 | 18 | gl::LinkProgram(program_handle); 19 | 20 | gl::DeleteShader(vertex_shader); 21 | gl::DeleteShader(geom_shader); 22 | gl::DeleteShader(fragment_shader); 23 | } 24 | 25 | Program { program_handle } 26 | } 27 | 28 | pub fn uniform_matrix4fv( 29 | &self, 30 | uniform_location: i32, 31 | transposed: bool, 32 | matrix: &[[f32; 4]; 4], 33 | ) { 34 | unsafe { 35 | gl::UniformMatrix4fv(uniform_location, 1, transposed as u8, matrix.as_ptr() as _); 36 | } 37 | } 38 | 39 | pub fn uniform1i(&self, uniform_location: i32, value: i32) { 40 | unsafe { 41 | gl::Uniform1i(uniform_location, value); 42 | } 43 | } 44 | 45 | pub fn get_uniform_location(&self, name: &str) -> i32 { 46 | let name = std::ffi::CString::new(name).unwrap(); 47 | unsafe { gl::GetUniformLocation(self.program_handle, name.as_ptr() as _) } 48 | } 49 | 50 | pub fn use_program(&self) { 51 | unsafe { 52 | gl::UseProgram(self.program_handle); 53 | } 54 | } 55 | } 56 | 57 | unsafe fn create_shader(shader_type: u32, src: &str) -> u32 { unsafe { 58 | let shader_str = std::ffi::CString::new(src).unwrap(); 59 | 60 | let gl_handle = gl::CreateShader(shader_type); 61 | gl::ShaderSource(gl_handle, 1, &shader_str.as_ptr() as _, std::ptr::null()); 62 | gl::CompileShader(gl_handle); 63 | 64 | let mut success = gl::FALSE as gl::types::GLint; 65 | gl::GetShaderiv(gl_handle, gl::COMPILE_STATUS, &mut success); 66 | if success != gl::TRUE as gl::types::GLint { 67 | let info_log = [0u8; 513]; 68 | let mut error_size = 0i32; 69 | gl::GetShaderInfoLog(gl_handle, 512, &mut error_size, info_log.as_ptr() as _); 70 | let info_log = std::str::from_utf8(&info_log[..error_size as usize]); 71 | panic!( 72 | "Error compile failed with error: {:?} for: {:?}", 73 | info_log, gl_handle 74 | ); 75 | } 76 | 77 | gl_handle 78 | }} 79 | 80 | impl Drop for Program { 81 | fn drop(&mut self) { 82 | unsafe { 83 | gl::DeleteProgram(self.program_handle); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/gl_backend/texture.rs: -------------------------------------------------------------------------------- 1 | pub struct GLTexture { 2 | texture_handle: u32, 3 | data: Vec, 4 | } 5 | 6 | impl GLTexture { 7 | pub fn new( 8 | image_data: &[u8], 9 | dimensions: (u32, u32), 10 | filter: u32, 11 | wrap: u32, 12 | format: u32, 13 | internal_format: u32, 14 | ) -> GLTexture { 15 | let mut texture = GLTexture { 16 | texture_handle: 0, 17 | data: image_data.to_vec(), 18 | }; 19 | 20 | unsafe { 21 | gl::GenTextures(1, &mut texture.texture_handle); 22 | gl::BindTexture(gl::TEXTURE_2D, texture.texture_handle); 23 | 24 | gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_S, wrap as _); 25 | gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_T, wrap as _); 26 | gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_R, wrap as _); 27 | gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MIN_FILTER, filter as _); 28 | gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MAG_FILTER, filter as _); 29 | 30 | gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_BASE_LEVEL, 0); 31 | gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MAX_LEVEL, 0); 32 | gl::PixelStorei(gl::UNPACK_ALIGNMENT, 1); 33 | 34 | gl::TexStorage2D( 35 | gl::TEXTURE_2D, 36 | 1, 37 | internal_format as _, 38 | dimensions.0 as _, 39 | dimensions.1 as _, 40 | ); 41 | 42 | gl::TexSubImage2D( 43 | gl::TEXTURE_2D, 44 | 0, 45 | 0, 46 | 0, 47 | dimensions.0 as _, 48 | dimensions.1 as _, 49 | format, 50 | gl::UNSIGNED_BYTE, 51 | texture.data.as_ptr() as _, 52 | ); 53 | 54 | gl::GenerateMipmap(gl::TEXTURE_2D); 55 | } 56 | 57 | texture 58 | } 59 | 60 | pub fn bind(&self, idx: i32) { 61 | let bind_location = match idx { 62 | 0 => gl::TEXTURE0, 63 | 1 => gl::TEXTURE1, 64 | 2 => gl::TEXTURE2, 65 | 3 => gl::TEXTURE3, 66 | 4 => gl::TEXTURE4, 67 | 5 => gl::TEXTURE5, 68 | 6 => gl::TEXTURE6, 69 | _ => panic!("invalid idx"), 70 | }; 71 | 72 | unsafe { 73 | gl::ActiveTexture(bind_location); 74 | gl::BindTexture(gl::TEXTURE_2D, self.texture_handle); 75 | } 76 | } 77 | } 78 | 79 | impl Drop for GLTexture { 80 | fn drop(&mut self) { 81 | unsafe { 82 | gl::DeleteTextures(1, &self.texture_handle); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/gl_backend/vertex_buffer.rs: -------------------------------------------------------------------------------- 1 | use super::GLVertex; 2 | use memoffset::offset_of; 3 | 4 | pub struct VAO { 5 | vao_handle: u32, 6 | vbo_handle: u32, 7 | } 8 | 9 | impl VAO { 10 | pub(crate) fn new(vertices: &[GLVertex]) -> VAO { 11 | let mut vao_handle = 0; 12 | let mut vbo_handle = 0; 13 | unsafe { 14 | gl::GenVertexArrays(1, &mut vao_handle); 15 | 16 | gl::GenBuffers(1, &mut vbo_handle); 17 | // bind the Vertex Array Object first, then bind and set vertex buffer(s), and then configure vertex attributes(s). 18 | gl::BindVertexArray(vao_handle); 19 | 20 | gl::BindBuffer(gl::ARRAY_BUFFER, vbo_handle); 21 | gl::BufferData( 22 | gl::ARRAY_BUFFER, 23 | std::mem::size_of_val(vertices) as _, 24 | vertices.as_ptr() as _, 25 | gl::STATIC_DRAW, 26 | ); 27 | 28 | for idx in 0..=6 { 29 | gl::EnableVertexAttribArray(idx); 30 | } 31 | 32 | gl::VertexAttribPointer( 33 | 0, 34 | 2, 35 | gl::FLOAT, 36 | gl::FALSE, 37 | std::mem::size_of::() as _, 38 | offset_of!(GLVertex, position) as _, 39 | ); 40 | 41 | gl::VertexAttribPointer( 42 | 1, 43 | 2, 44 | gl::FLOAT, 45 | gl::FALSE, 46 | std::mem::size_of::() as _, 47 | offset_of!(GLVertex, size) as _, 48 | ); 49 | 50 | gl::VertexAttribPointer( 51 | 2, 52 | 2, 53 | gl::FLOAT, 54 | gl::FALSE, 55 | std::mem::size_of::() as _, 56 | offset_of!(GLVertex, tex0) as _, 57 | ); 58 | 59 | gl::VertexAttribPointer( 60 | 3, 61 | 2, 62 | gl::FLOAT, 63 | gl::FALSE, 64 | std::mem::size_of::() as _, 65 | offset_of!(GLVertex, tex1) as _, 66 | ); 67 | 68 | gl::VertexAttribPointer( 69 | 4, 70 | 4, 71 | gl::FLOAT, 72 | gl::FALSE, 73 | std::mem::size_of::() as _, 74 | offset_of!(GLVertex, color) as _, 75 | ); 76 | 77 | gl::VertexAttribPointer( 78 | 5, 79 | 2, 80 | gl::FLOAT, 81 | gl::FALSE, 82 | std::mem::size_of::() as _, 83 | offset_of!(GLVertex, clip_pos) as _, 84 | ); 85 | 86 | gl::VertexAttribPointer( 87 | 6, 88 | 2, 89 | gl::FLOAT, 90 | gl::FALSE, 91 | std::mem::size_of::() as _, 92 | offset_of!(GLVertex, clip_size) as _, 93 | ); 94 | 95 | 96 | gl::BindBuffer(gl::ARRAY_BUFFER, 0); 97 | gl::BindVertexArray(0); 98 | } 99 | 100 | VAO { 101 | vao_handle, 102 | vbo_handle, 103 | } 104 | } 105 | 106 | pub fn bind(&self) { 107 | unsafe { 108 | gl::BindVertexArray(self.vao_handle); 109 | } 110 | } 111 | } 112 | 113 | impl Drop for VAO { 114 | fn drop(&mut self) { 115 | unsafe { 116 | gl::DeleteBuffers(1, &self.vbo_handle); 117 | gl::DeleteVertexArrays(1, &self.vao_handle); 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/key_event.rs: -------------------------------------------------------------------------------- 1 | use serde::{Serialize, Deserialize}; 2 | 3 | /// A keyboard key event, representing a virtual key code 4 | #[derive(Copy, Clone, Serialize, Deserialize, Debug)] 5 | pub enum KeyEvent { 6 | /// The insert key 7 | Insert, 8 | 9 | /// The home key 10 | Home, 11 | 12 | /// The delete key 13 | Delete, 14 | 15 | /// The end key 16 | End, 17 | 18 | /// The page down key 19 | PageDown, 20 | 21 | /// The page up key 22 | PageUp, 23 | 24 | /// The left arrow key 25 | Left, 26 | 27 | /// The up arrow key 28 | Up, 29 | 30 | /// The right arrow key 31 | Right, 32 | 33 | /// The down arrow key 34 | Down, 35 | 36 | /// The backspace button 37 | Back, 38 | 39 | /// The enter or return key 40 | Return, 41 | 42 | /// The spacebar 43 | Space, 44 | 45 | /// The escape key 46 | Escape, 47 | 48 | /// The tab key 49 | Tab, 50 | 51 | /// Function key 1 52 | F1, 53 | 54 | /// Function key 2 55 | F2, 56 | 57 | /// Function key 3 58 | F3, 59 | 60 | /// Function key 4 61 | F4, 62 | 63 | /// Function key 5 64 | F5, 65 | 66 | /// Function key 6 67 | F6, 68 | 69 | /// Function key 7 70 | F7, 71 | 72 | /// Function key 8 73 | F8, 74 | 75 | /// Function key 9 76 | F9, 77 | 78 | /// Function key 10 79 | F10, 80 | 81 | /// Function key 11 82 | F11, 83 | 84 | /// Function key 12 85 | F12, 86 | } -------------------------------------------------------------------------------- /src/log.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | A minimal logger for use with Thyme. 3 | 4 | Logs all messages to standard output. 5 | !*/ 6 | 7 | use log::{Level, Log, Record, Metadata, SetLoggerError}; 8 | 9 | struct SimpleLogger { 10 | level: Level, 11 | } 12 | 13 | impl Log for SimpleLogger { 14 | fn enabled(&self, metadata: &Metadata) -> bool { 15 | metadata.level() <= self.level 16 | } 17 | 18 | fn log(&self, record: &Record) { 19 | if !self.enabled(record.metadata()) { return; } 20 | 21 | let target = if !record.target().is_empty() { 22 | record.target() 23 | } else { 24 | record.module_path().unwrap_or_default() 25 | }; 26 | 27 | println!("{:<5} <{}> {}", record.level().to_string(), target, record.args()); 28 | } 29 | 30 | fn flush(&self) { } 31 | } 32 | 33 | /// Initiales the logger at the specified log level. This should only be called once per program. 34 | pub fn init(level: Level) -> Result<(), SetLoggerError> { 35 | let logger = Box::new(SimpleLogger { level }); 36 | 37 | log::set_logger(Box::leak(logger))?; 38 | log::set_max_level(level.to_level_filter()); 39 | Ok(()) 40 | } 41 | 42 | /// Initializes the logger at the `Trace` level. This should only be called once per program. 43 | pub fn init_all() -> Result<(), SetLoggerError> { 44 | init(Level::Trace) 45 | } -------------------------------------------------------------------------------- /src/point.rs: -------------------------------------------------------------------------------- 1 | use std::ops::*; 2 | use std::fmt; 3 | 4 | use serde::{Serialize, Deserialize, Deserializer, de::{self, Error, Visitor, MapAccess}}; 5 | 6 | /// A struct representing a rectangular border around a Widget. 7 | /// In the theme file, border can be deserialzed as a standard mapping, or 8 | /// using `all: {value}` to specify all four values are the same, or 9 | /// `width` and `height` to specify `left` and `right` and `top` and `bot`, 10 | /// respectively. 11 | #[derive(Serialize, Copy, Clone, Default, Debug, PartialEq)] 12 | pub struct Border { 13 | /// The upper edge border 14 | pub top: f32, 15 | 16 | /// The lower edge border 17 | pub bot: f32, 18 | 19 | /// The left edge border 20 | pub left: f32, 21 | 22 | /// The right edge border 23 | pub right: f32, 24 | } 25 | 26 | impl Border { 27 | /// The vertical border, top plus bottom 28 | pub fn vertical(&self) -> f32 { 29 | self.top + self.bot 30 | } 31 | 32 | /// The horizontal border, left plus right 33 | pub fn horizontal(&self) -> f32 { 34 | self.left + self.right 35 | } 36 | 37 | /// The border on the top right corner 38 | pub fn tr(&self) -> Point { 39 | Point { x: self.right, y: self.top } 40 | } 41 | 42 | /// The border on the top left corner 43 | pub fn tl(&self) -> Point { 44 | Point { x: self.left, y: self.top } 45 | } 46 | 47 | /// The border on the bottom left corner 48 | pub fn bl(&self) -> Point { 49 | Point { x: self.left, y: self.bot } 50 | } 51 | 52 | /// The border on the bottom right corner 53 | pub fn br(&self) -> Point { 54 | Point { x: self.right, y: self.bot } 55 | } 56 | } 57 | 58 | struct BorderVisitor; 59 | 60 | impl<'de> Visitor<'de> for BorderVisitor { 61 | type Value = Border; 62 | 63 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 64 | formatter.write_str("Map") 65 | } 66 | 67 | fn visit_map>(self, mut map: M) -> Result { 68 | const ERROR_MSG: &str = 69 | "Unable to parse border from map. Must specify values for: \ 70 | all OR width, height, OR top, bot, left, right \ 71 | Unspecified values are set to 0"; 72 | 73 | let mut data = [f32::MIN; 4]; 74 | #[derive(Copy, Clone, PartialEq)] 75 | enum Mode { 76 | One, 77 | Two, 78 | Four, 79 | } 80 | let mut mode: Option = None; 81 | fn check_mode(mode: &mut Option, must_eq: Mode) -> Result<(), E> { 82 | match mode { 83 | None => { 84 | *mode = Some(must_eq); 85 | Ok(()) 86 | }, 87 | Some(mode) => if *mode == must_eq { 88 | Ok(()) 89 | } else { 90 | Err(E::custom(ERROR_MSG)) 91 | } 92 | } 93 | } 94 | 95 | loop { 96 | let (kind, value) = match map.next_entry::()? { 97 | None => break, 98 | Some(data) => data, 99 | }; 100 | 101 | match &*kind { 102 | "all" => { 103 | check_mode(&mut mode, Mode::One)?; 104 | data[0] = value; 105 | }, 106 | "width" => { 107 | check_mode(&mut mode, Mode::Two)?; 108 | data[0] = value; 109 | }, 110 | "height" => { 111 | check_mode(&mut mode, Mode::Two)?; 112 | data[1] = value; 113 | }, 114 | "top" => { 115 | check_mode(&mut mode, Mode::Four)?; 116 | data[0] = value; 117 | }, 118 | "bot" => { 119 | check_mode(&mut mode, Mode::Four)?; 120 | data[1] = value; 121 | }, 122 | "left" => { 123 | check_mode(&mut mode, Mode::Four)?; 124 | data[2] = value; 125 | }, 126 | "right" => { 127 | check_mode(&mut mode, Mode::Four)?; 128 | data[3] = value; 129 | }, 130 | _ => return Err(M::Error::custom(ERROR_MSG)) 131 | } 132 | } 133 | 134 | // fill in the default values at this point if needed 135 | for val in &mut data { 136 | if *val == f32::MIN { 137 | *val = 0.0; 138 | } 139 | } 140 | 141 | match mode { 142 | Some(Mode::One) => 143 | Ok(Border { top: data[0], bot: data[0], left: data[0], right: data[0] }), 144 | Some(Mode::Two) => 145 | Ok(Border { top: data[1], bot: data[1], left: data[0], right: data[0] }), 146 | Some(Mode::Four) => 147 | Ok(Border { top: data[0], bot: data[1], left: data[2], right: data[3] }), 148 | None => 149 | Err(M::Error::custom(ERROR_MSG)), 150 | } 151 | } 152 | } 153 | 154 | impl<'de> Deserialize<'de> for Border { 155 | fn deserialize>(deserializer: D) -> Result { 156 | deserializer.deserialize_map(BorderVisitor) 157 | } 158 | } 159 | 160 | /// A rectangular area, represented by a position and a size 161 | #[derive(Serialize, Deserialize, Copy, Clone, Default, Debug, PartialEq)] 162 | pub struct Rect { 163 | /// The position of the rectangle 164 | pub pos: Point, 165 | 166 | /// The size of the rectangle 167 | pub size: Point 168 | } 169 | 170 | impl Rect { 171 | /// Construct a new `Rect` with the specified position and size. 172 | pub fn new(pos: Point, size: Point) -> Rect { 173 | Rect { 174 | pos, 175 | size, 176 | } 177 | } 178 | 179 | /// Returns the left edge of this Rect. 180 | pub fn left(&self) -> f32 { 181 | self.pos.x 182 | } 183 | 184 | /// Returns the right edge of this Rect. 185 | pub fn right(&self) -> f32 { 186 | self.pos.x + self.size.x 187 | } 188 | 189 | /// Returns the top edge of this Rect. 190 | pub fn top(&self) -> f32 { 191 | self.pos.y 192 | } 193 | 194 | /// Returns the bottom edge of this Rect. 195 | pub fn bot(&self) -> f32 { 196 | self.pos.y + self.size.y 197 | } 198 | 199 | /// Returns true if the specified point is inside (or on the edge of) 200 | /// this rectangle; false otherwise 201 | pub fn is_inside(&self, pos: Point) -> bool { 202 | pos.x >= self.pos.x && pos.y >= self.pos.y && 203 | pos.x <= self.pos.x + self.size.x && pos.y <= self.pos.y + self.size.y 204 | } 205 | 206 | /// Returns a new `Rect` this is the minimum extent on a component-by-component 207 | /// basis between this and `other`. The returned `Rect` will barely fit inside 208 | /// both this and `other` (if possible - if not it will have size 0) 209 | pub fn min(self, other: Rect) -> Rect { 210 | let min = self.pos.max(other.pos); 211 | let max: Point = (self.pos + self.size).min(other.pos + other.size); 212 | 213 | Rect { 214 | pos: min, 215 | size: (max - min).max(Point::default()), 216 | } 217 | } 218 | 219 | /// Returns a new `Rect` that is the maximum extent on a component-by-component 220 | /// basis between this and `other`. The returned `Rect` will barely contain 221 | /// both this and `other`. 222 | pub fn max(self, other: Rect) -> Rect { 223 | let min = self.pos.min(other.pos); 224 | let max: Point = (self.pos + self.size).max(other.pos + other.size); 225 | 226 | Rect { 227 | pos: min, 228 | size: max - min, 229 | } 230 | } 231 | 232 | /// Returns the center point of this rect 233 | pub fn center(self) -> Point { 234 | Point { 235 | x: self.pos.x + self.size.x * 0.5, 236 | y: self.pos.y + self.size.y * 0.5, 237 | } 238 | } 239 | 240 | /// Returns a `Rect` with all components rounded to the nearest integer. 241 | pub fn round(self) -> Rect { 242 | Rect { 243 | pos: self.pos.round(), 244 | size: self.size.round(), 245 | } 246 | } 247 | 248 | /// Returns true if the specified `other` `Rect` is entirely contained inside this Rect. 249 | pub fn contains_rect(&self, other: Rect) -> bool { 250 | self.pos.x <= other.pos.x && self.pos.x + self.size.x >= other.pos.x + other.size.x && 251 | self.pos.y <= other.pos.y && self.pos.y + self.size.y >= other.pos.y + other.size.y 252 | } 253 | 254 | /// Returns true if the specified `other` `Rect` intersects this rect at any point. 255 | pub fn intersects(&self, other: Rect) -> bool { 256 | if self.pos.x > other.pos.x + other.size.x { return false; } 257 | if other.pos.x > self.pos.x + self.size.x { return false; } 258 | if self.pos.y > other.pos.y + other.size.y { return false; } 259 | if other.pos.y > self.pos.y + self.size.y { return false; } 260 | 261 | true 262 | } 263 | 264 | /// Returns true if the specified `other` `Rect` is `within` the amount specified of intersecting 265 | /// this rect at any point 266 | pub fn intersects_within(&self, other: Rect, within: f32) -> bool { 267 | if self.pos.x > other.pos.x + other.size.x + within { return false; } 268 | if other.pos.x > self.pos.x + self.size.x + within { return false; } 269 | if self.pos.y > other.pos.y + other.size.y + within { return false; } 270 | if other.pos.y > self.pos.y + self.size.y + within { return false; } 271 | 272 | true 273 | } 274 | } 275 | 276 | impl Add for Rect { 277 | type Output = Rect; 278 | 279 | fn add(self, rhs: Point) -> Self::Output { 280 | Rect { 281 | pos: self.pos + rhs, 282 | size: self.size, 283 | } 284 | } 285 | } 286 | 287 | impl Mul for f32 { 288 | type Output = Rect; 289 | fn mul(self, rect: Rect) -> Rect { 290 | Rect { 291 | pos: rect.pos * self, 292 | size: rect.size * self, 293 | } 294 | } 295 | } 296 | 297 | impl Mul for Rect { 298 | type Output = Rect; 299 | fn mul(self, val: f32) -> Rect { 300 | Rect { 301 | pos: self.pos * val, 302 | size: self.size * val, 303 | } 304 | } 305 | } 306 | 307 | /// A two-dimensional point, with `x` and `y` coordinates. 308 | #[derive(Serialize, Deserialize, Copy, Clone, Default, Debug, PartialEq)] 309 | pub struct Point { 310 | /// The `x` cartesian coordinate 311 | pub x: f32, 312 | 313 | /// The `y` cartesian coordinate 314 | pub y: f32, 315 | } 316 | 317 | impl Point { 318 | /// Creates a new point from the specified components. 319 | pub fn new(x: f32, y: f32) -> Point { 320 | Point { x, y } 321 | } 322 | 323 | /// Returns a point with both components rounded to the nearest integer 324 | pub fn round(self) -> Point { 325 | Point { 326 | x: self.x.round(), 327 | y: self.y.round(), 328 | } 329 | } 330 | 331 | /// Returns a per-component maximum of this and `other` 332 | pub fn max(self, other: Point) -> Point { 333 | Point { 334 | x: self.x.max(other.x), 335 | y: self.y.max(other.y) 336 | } 337 | } 338 | 339 | /// Returns a per-component minimum of this and `other` 340 | pub fn min(self, other: Point) -> Point { 341 | Point { 342 | x: self.x.min(other.x), 343 | y: self.y.min(other.y), 344 | } 345 | } 346 | } 347 | 348 | impl From<[f32; 2]> for Point { 349 | fn from(t: [f32; 2]) -> Self { 350 | Point { x: t[0], y: t[1] } 351 | } 352 | } 353 | 354 | impl From for [f32; 2] { 355 | fn from(point: Point) -> Self { 356 | [point.x, point.y] 357 | } 358 | } 359 | 360 | impl From<(f32, f32)> for Point { 361 | fn from(t: (f32, f32)) -> Self { 362 | Point { x: t.0, y: t.1 } 363 | } 364 | } 365 | 366 | impl From for (f32, f32) { 367 | fn from(point: Point) -> Self { 368 | (point.x, point.y) 369 | } 370 | } 371 | 372 | impl Sub for Point { 373 | type Output = Point; 374 | fn sub(self, other: Point) -> Point { 375 | Point { x: self.x - other.x, y: self.y - other.y } 376 | } 377 | } 378 | 379 | impl Add for Point { 380 | type Output = Point; 381 | fn add(self, other: Point) -> Point{ 382 | Point { x: self.x + other.x, y: self.y + other.y } 383 | } 384 | } 385 | 386 | impl Sub<(f32, f32)> for Point { 387 | type Output = Point; 388 | fn sub(self, other: (f32, f32)) -> Point { 389 | Point { x: self.x - other.0, y: self.y - other.0 } 390 | } 391 | } 392 | 393 | impl Sub<[f32; 2]> for Point { 394 | type Output = Point; 395 | fn sub(self, other: [f32; 2]) -> Point { 396 | Point { x: self.x - other[0], y: self.y - other[0] } 397 | } 398 | } 399 | 400 | impl Add<(f32, f32)> for Point { 401 | type Output = Point; 402 | fn add(self, other: (f32, f32)) -> Point { 403 | Point { x: self.x + other.0, y: self.y + other.1 } 404 | } 405 | } 406 | 407 | impl Add<[f32; 2]> for Point { 408 | type Output = Point; 409 | fn add(self, other: [f32; 2]) -> Point { 410 | Point { x: self.x + other[0], y: self.y + other[1] } 411 | } 412 | } 413 | 414 | impl Mul for Point { 415 | type Output = Point; 416 | fn mul(self, val: f32) -> Point { 417 | Point { x: self.x * val, y: self.y * val } 418 | } 419 | } 420 | 421 | impl Div for Point { 422 | type Output = Point; 423 | fn div(self, val: f32) -> Point { 424 | Point { x: self.x / val, y: self.y / val } 425 | } 426 | } 427 | 428 | 429 | impl Sub for (f32, f32) { 430 | type Output = Point; 431 | fn sub(self, other: Point) -> Point { 432 | Point { x: other.x - self.0, y: other.y - self.0 } 433 | } 434 | } 435 | 436 | impl Sub for [f32; 2] { 437 | type Output = Point; 438 | fn sub(self, other: Point) -> Point { 439 | Point { x: other.x - self[0], y: other.y - self[0] } 440 | } 441 | } 442 | 443 | impl Add for (f32, f32) { 444 | type Output = Point; 445 | fn add(self, other: Point) -> Point { 446 | Point { x: other.x + self.0, y: other.y + self.1 } 447 | } 448 | } 449 | 450 | impl Add for [f32; 2] { 451 | type Output = Point; 452 | fn add(self, other: Point) -> Point { 453 | Point { x: other.x + self[0], y: other.y + self[1] } 454 | } 455 | } 456 | 457 | impl Mul for f32 { 458 | type Output = Point; 459 | fn mul(self, val: Point) -> Point { 460 | Point { x: val.x * self, y: val.y * self } 461 | } 462 | } 463 | 464 | impl Div for f32 { 465 | type Output = Point; 466 | fn div(self, val: Point) -> Point { 467 | Point { x: self / val.x, y: self / val.y } 468 | } 469 | } -------------------------------------------------------------------------------- /src/render.rs: -------------------------------------------------------------------------------- 1 | use std::num::NonZeroU16; 2 | 3 | use crate::{Color, Rect, Point, Error}; 4 | use crate::font::{FontSource, Font}; 5 | use crate::theme_definition::CharacterRange; 6 | 7 | /// A trait to be implemented on the type to be used for Event handling. See [`WinitIO`](struct.WinitIO.html) 8 | /// for an example implementation. The IO handles events from an external source and passes them to the Thyme 9 | /// [`Context`](struct.Context.html). 10 | pub trait IO { 11 | /// Returns the current window scale factor (1.0 for logical pixel size = physical pixel size). 12 | fn scale_factor(&self) -> f32; 13 | 14 | /// Returns the current window size in logical pixels. 15 | fn display_size(&self) -> Point; 16 | } 17 | 18 | /// A trait to be implemented on the type to be used for rendering the UI. See [`GliumRenderer`](struct.GliumRenderer.html) 19 | /// for an example implementation. The `Renderer` takes a completed frame and renders the widget tree stored within it. 20 | pub trait Renderer { 21 | /// Register a font with Thyme. This method is called via the [`ContextBuilder`](struct.ContextBuilder.html). 22 | fn register_font( 23 | &mut self, 24 | handle: FontHandle, 25 | source: &FontSource, 26 | ranges: &[CharacterRange], 27 | size: f32, 28 | scale: f32, 29 | ) -> Result; 30 | 31 | /// Register a texture with Thyme. This method is called via the [`ContextBuilder`](struct.ContextBuilder.html). 32 | fn register_texture( 33 | &mut self, 34 | handle: TextureHandle, 35 | image_data: &[u8], 36 | dimensions: (u32, u32), 37 | ) -> Result; 38 | } 39 | 40 | pub(crate) fn view_matrix(display_pos: Point, display_size: Point) -> [[f32; 4]; 4] { 41 | let left = display_pos.x; 42 | let right = display_pos.x + display_size.x; 43 | let top = display_pos.y; 44 | let bot = display_pos.y + display_size.y; 45 | 46 | [ 47 | [ (2.0 / (right - left)), 0.0, 0.0, 0.0], 48 | [ 0.0, (2.0 / (top - bot)), 0.0, 0.0], 49 | [ 0.0, 0.0, -1.0, 0.0], 50 | [(right + left) / (left - right), (top + bot) / (bot - top), 0.0, 1.0], 51 | ] 52 | } 53 | 54 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 55 | pub enum DrawMode { 56 | Image(TextureHandle), 57 | Font(FontHandle), 58 | } 59 | 60 | pub trait DrawList { 61 | fn push_rect( 62 | &mut self, 63 | pos: [f32; 2], 64 | size: [f32; 2], 65 | tex: [TexCoord; 2], 66 | color: Color, 67 | clip: Rect, 68 | ); 69 | 70 | /// the number of vertices currently contained in this list 71 | fn len(&self) -> usize; 72 | 73 | /// adjust the positions of all vertices from the last one in the list 74 | /// to the one at the specified `since_index`, by the specified `amount` 75 | fn back_adjust_positions(&mut self, since_index: usize, amount: Point); 76 | } 77 | 78 | /// An implementation of DrawList that does nothing. It should be (mostly) optimized 79 | /// out when used 80 | pub(crate) struct DummyDrawList { 81 | index: usize, 82 | } 83 | 84 | impl DummyDrawList { 85 | pub fn new() -> DummyDrawList { 86 | DummyDrawList { index: 0 } 87 | } 88 | } 89 | 90 | impl DrawList for DummyDrawList { 91 | fn push_rect( 92 | &mut self, 93 | _pos: [f32; 2], 94 | _size: [f32; 2], 95 | _tex: [TexCoord; 2], 96 | _color: Color, 97 | _clip: Rect, 98 | ) { 99 | self.index += 1; 100 | } 101 | 102 | fn len(&self) -> usize { self.index } 103 | 104 | fn back_adjust_positions(&mut self, _since_index: usize, _amount: Point) {} 105 | } 106 | 107 | pub struct TextureData { 108 | handle: TextureHandle, 109 | size: [u32; 2], 110 | } 111 | 112 | impl TextureData { 113 | pub fn new(handle: TextureHandle, width: u32, height: u32) -> TextureData { 114 | TextureData { 115 | handle, 116 | size: [width, height], 117 | } 118 | } 119 | 120 | pub fn tex_coord(&self, x: u32, y: u32) -> TexCoord { 121 | let x = x as f32 / self.size[0] as f32; 122 | let y = y as f32 / self.size[1] as f32; 123 | TexCoord([x, y]) 124 | } 125 | 126 | pub fn handle(&self) -> TextureHandle { self.handle } 127 | } 128 | 129 | #[derive(Copy, Clone)] 130 | pub struct TexCoord([f32; 2]); 131 | 132 | impl TexCoord { 133 | pub fn new(x: f32, y: f32) -> TexCoord { 134 | TexCoord([x, y]) 135 | } 136 | 137 | pub fn x(&self) -> f32 { self.0[0] } 138 | pub fn y(&self) -> f32 { self.0[1] } 139 | } 140 | 141 | impl Default for TexCoord { 142 | fn default() -> TexCoord { 143 | TexCoord([0.0, 0.0]) 144 | } 145 | } 146 | 147 | impl From for [f32; 2] { 148 | fn from(coord: TexCoord) -> Self { 149 | coord.0 150 | } 151 | } 152 | 153 | #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] 154 | pub struct TextureHandle { 155 | id: NonZeroU16, 156 | } 157 | 158 | impl Default for TextureHandle { 159 | fn default() -> Self { 160 | TextureHandle { id: NonZeroU16::new(1).unwrap() } 161 | } 162 | } 163 | 164 | impl TextureHandle { 165 | pub fn id(self) -> usize { (self.id.get() - 1).into() } 166 | 167 | pub fn next(self) -> TextureHandle { 168 | if self.id.get() == u16::MAX { 169 | panic!("Cannot allocate more than {} textures", u16::MAX); 170 | } 171 | 172 | TextureHandle { 173 | id: NonZeroU16::new(self.id.get() + 1).unwrap() 174 | } 175 | } 176 | } 177 | 178 | #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] 179 | pub struct FontHandle { 180 | id: NonZeroU16, 181 | } 182 | 183 | impl Default for FontHandle { 184 | fn default() -> Self { 185 | FontHandle { id: NonZeroU16::new(1).unwrap() } 186 | } 187 | } 188 | 189 | impl FontHandle { 190 | pub fn id(self) -> usize { (self.id.get() - 1).into() } 191 | 192 | pub fn next(self) -> FontHandle { 193 | if self.id.get() == u16::MAX { 194 | panic!("Cannot allocate more than {} fonts", u16::MAX); 195 | } 196 | FontHandle { 197 | id: NonZeroU16::new(self.id.get() + 1).unwrap() 198 | } 199 | } 200 | } -------------------------------------------------------------------------------- /src/resource.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::io::Read; 3 | use std::path::{Path, PathBuf}; 4 | use std::sync::{atomic::{AtomicBool, Ordering}, mpsc::{Receiver, channel}}; 5 | 6 | use indexmap::IndexMap; 7 | use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher}; 8 | 9 | use crate::Error; 10 | use crate::theme::ThemeSet; 11 | use crate::theme_definition::ThemeDefinition; 12 | use crate::render::{Renderer, TextureData, TextureHandle}; 13 | 14 | static RELOAD_THEME: AtomicBool = AtomicBool::new(false); 15 | 16 | struct ThemeSource { 17 | data: Option, 18 | files: Option>, 19 | } 20 | 21 | struct ImageSource { 22 | data: Option<(Vec, u32, u32)>, 23 | file: Option, 24 | } 25 | 26 | struct FontSource { 27 | font: Option>, 28 | data: Option>, 29 | file: Option, 30 | } 31 | 32 | pub(crate) struct ResourceSet { 33 | // preserve ordering of images and fonts 34 | images: Vec<(String, ImageSource)>, 35 | fonts: Vec<(String, FontSource)>, 36 | theme: ThemeSource, 37 | 38 | watcher: Option, 39 | } 40 | 41 | impl ResourceSet { 42 | pub(crate) fn new(enable_live_reload: bool) -> ResourceSet { 43 | let (tx, rx) = channel(); 44 | 45 | let watcher = if enable_live_reload { 46 | match RecommendedWatcher::new(tx, Config::default()) { 47 | Err(e) => { 48 | log::error!("Unable to initialize file watching for live-reload:"); 49 | log::error!("{}", e); 50 | None 51 | }, Ok(watcher) => Some(watcher), 52 | } 53 | } else { 54 | None 55 | }; 56 | 57 | if watcher.is_some() { 58 | std::thread::spawn(move || watcher_loop(rx) ); 59 | } 60 | 61 | ResourceSet { 62 | images: Vec::new(), 63 | fonts: Vec::new(), 64 | theme: ThemeSource { 65 | data: None, 66 | files: None, 67 | }, 68 | watcher, 69 | } 70 | } 71 | 72 | fn remove_path_from_watcher(&mut self, path: &Path) { 73 | if let Some(watcher) = self.watcher.as_mut() { 74 | if let Err(e) = watcher.unwatch(path) { 75 | log::warn!("Unable to watch path: {:?}", path); 76 | log::warn!("{}", e); 77 | } 78 | } 79 | } 80 | 81 | fn add_path_to_watcher(&mut self, path: &Path) { 82 | if let Some(watcher) = self.watcher.as_mut() { 83 | log::info!("Watching {:?}", path); 84 | if let Err(e) = watcher.watch(path, RecursiveMode::NonRecursive) { 85 | log::warn!("Unable to unwatch path: {:?}", path); 86 | log::warn!("{}", e); 87 | } 88 | } 89 | } 90 | 91 | pub(crate) fn register_theme(&mut self, theme: ThemeDefinition) { 92 | self.theme.data = Some(theme); 93 | self.theme.files = None; 94 | } 95 | 96 | pub(crate) fn register_theme_from_files( 97 | &mut self, 98 | paths: &[&Path], 99 | ) { 100 | let mut paths_out: Vec = Vec::new(); 101 | for path in paths { 102 | self.add_path_to_watcher(path); 103 | paths_out.push((*path).to_owned()); 104 | } 105 | 106 | self.theme.files = Some(paths_out); 107 | } 108 | 109 | pub(crate) fn register_font_from_file(&mut self, id: String, path: &Path) { 110 | self.add_path_to_watcher(path); 111 | self.fonts.push((id, FontSource { font: None, data: None, file: Some(path.to_owned()) })); 112 | } 113 | 114 | pub(crate) fn register_font_from_data(&mut self, id: String, data: Vec) { 115 | self.fonts.push((id, FontSource { font: None, data: Some(data), file: None })); 116 | } 117 | 118 | pub(crate) fn register_image_from_file(&mut self, id: String, path: &Path) { 119 | self.add_path_to_watcher(path); 120 | self.images.push((id, ImageSource { data: None, file: Some(path.to_owned()) })); 121 | } 122 | 123 | pub(crate) fn register_image_from_data(&mut self, id: String, data: Vec, width: u32, height: u32) { 124 | self.images.push((id, ImageSource { data: Some((data, width, height)), file: None })); 125 | } 126 | 127 | pub(crate) fn remove_theme_file(&mut self, path: &Path) { 128 | self.remove_path_from_watcher(path); 129 | if let Some(paths) = self.theme.files.as_mut() { 130 | paths.retain(|p| p != path); 131 | self.theme.data = None; 132 | } 133 | } 134 | 135 | pub(crate) fn add_theme_file(&mut self, path: PathBuf) { 136 | self.add_path_to_watcher(&path); 137 | if let Some(paths) = self.theme.files.as_mut() { 138 | paths.push(path); 139 | self.theme.data = None; 140 | } 141 | } 142 | 143 | /// Checks for a file watch change and rebuilds the theme if neccessary, clearing the data cache 144 | /// and reloading all data. Will return Ok(None) if there was no change, or Err if there was 145 | /// a problem rebuilding the theme. 146 | pub(crate) fn check_live_reload(&mut self, renderer: &mut R, scale_factor: f32) -> Result, Error> { 147 | match RELOAD_THEME.compare_exchange(true, false, Ordering::AcqRel, Ordering::Acquire) { 148 | Ok(true) => (), 149 | _ => return Ok(None), 150 | } 151 | 152 | self.clear_data_cache(); 153 | self.cache_data()?; 154 | 155 | let themes = self.build_assets(renderer, scale_factor)?; 156 | 157 | Ok(Some(themes)) 158 | } 159 | 160 | /// Builds all assets and registers them with the renderer. You must make sure all asset 161 | /// data is cached with [`cache_data`](#method.cache_assets) prior to calling this. 162 | pub(crate) fn build_assets(&mut self, renderer: &mut R, scale_factor: f32) -> Result { 163 | RELOAD_THEME.store(false, Ordering::Release); 164 | 165 | let textures = self.build_images(renderer)?; 166 | let fonts = self.build_fonts(); 167 | 168 | let theme_def = match self.theme.data.as_mut() { 169 | None => { 170 | return Err(Error::Theme("Cannot build assets. No theme specified.".to_string())); 171 | }, 172 | Some(def) => def, 173 | }; 174 | let themes = ThemeSet::new(theme_def, textures, fonts, renderer, scale_factor)?; 175 | 176 | Ok(themes) 177 | } 178 | 179 | pub(crate) fn clear_data_cache(&mut self) { 180 | if self.theme.files.is_some() { 181 | self.theme.data = None; 182 | } 183 | 184 | for (_, src) in self.images.iter_mut() { 185 | if src.file.is_some() { 186 | src.data = None; 187 | } 188 | } 189 | 190 | for (_, src) in self.fonts.iter_mut() { 191 | if src.file.is_some() { 192 | src.data = None; 193 | src.font = None; 194 | } 195 | } 196 | } 197 | 198 | pub(crate) fn cache_data(&mut self) -> Result<(), Error> { 199 | if self.theme.data.is_none() { 200 | if let Some(theme_source) = self.theme.files.as_ref() { 201 | let mut theme_def: Option = None; 202 | 203 | let mut theme_str = String::new(); 204 | for path in theme_source.iter() { 205 | let mut file = match File::open(path) { 206 | Ok(file) => file, 207 | Err(e) => return Err(Error::IO(e)), 208 | }; 209 | 210 | theme_str.clear(); 211 | match file.read_to_string(&mut theme_str) { 212 | Err(e) => return Err(Error::IO(e)), 213 | Ok(count) => { 214 | log::debug!("Read {} bytes from '{:?}' for theme.", count, path); 215 | } 216 | } 217 | 218 | match theme_def.as_mut() { 219 | None => { 220 | theme_def = Some(match serde_yaml::from_str(&theme_str) { 221 | Ok(theme) => theme, 222 | Err(e) => return Err(Error::Serde(e.to_string())), 223 | }); 224 | }, Some(theme) => { 225 | let new_theme_def: ThemeDefinition = match serde_yaml::from_str(&theme_str) { 226 | Ok(theme) => theme, 227 | Err(e) => return Err(Error::Serde(e.to_string())), 228 | }; 229 | 230 | theme.merge(new_theme_def); 231 | } 232 | } 233 | } 234 | 235 | if theme_def.is_none() { 236 | return Err(Error::Theme("No valid theme was specified".to_string())); 237 | } 238 | 239 | self.theme.data = theme_def; 240 | } 241 | } 242 | 243 | for (id, src) in self.images.iter_mut() { 244 | if src.data.is_some() { continue; } 245 | 246 | // file must always be some if data is none 247 | let path = src.file.as_ref().unwrap(); 248 | 249 | let image = match image::open(path) { 250 | Ok(image) => image.into_rgba8(), 251 | Err(error) => return Err(Error::Image(error)), 252 | }; 253 | 254 | let dims = image.dimensions(); 255 | let data = image.into_raw(); 256 | 257 | log::debug!("Read {} bytes from '{:?}' for image '{}'", data.len(), path, id); 258 | 259 | src.data = Some((data, dims.0, dims.1)); 260 | } 261 | 262 | for (id, src) in self.fonts.iter_mut() { 263 | if src.font.is_some() { continue; } 264 | 265 | let data = if let Some(data) = src.data.as_ref() { 266 | data.clone() 267 | } else { 268 | // file must always be some if data is none 269 | let path = src.file.as_ref().unwrap(); 270 | let data = match std::fs::read(path) { 271 | Ok(data) => data, 272 | Err(error) => return Err(Error::IO(error)), 273 | }; 274 | 275 | log::debug!("Read {} bytes from '{:?}' for font '{}'", data.len(), path, id); 276 | 277 | let result = data.clone(); 278 | src.data = Some(data); 279 | result 280 | }; 281 | 282 | let font = match rusttype::Font::try_from_vec(data) { 283 | Some(font) => font, 284 | None => return Err( 285 | Error::FontSource(format!("Unable to parse '{}' as ttf", id)) 286 | ) 287 | }; 288 | 289 | log::debug!("Created rusttype font from '{}'", id); 290 | 291 | src.font = Some(font); 292 | } 293 | 294 | Ok(()) 295 | } 296 | 297 | fn build_fonts(&mut self) -> IndexMap { 298 | let mut output = IndexMap::new(); 299 | 300 | for (id, source) in self.fonts.iter_mut() { 301 | let font = source.font.take().unwrap(); 302 | output.insert(id.to_string(), crate::font::FontSource { font }); 303 | } 304 | 305 | output 306 | } 307 | 308 | fn build_images(&self, renderer: &mut R) -> Result, Error> { 309 | let mut output = IndexMap::new(); 310 | let mut handle = TextureHandle::default(); 311 | 312 | // register a 1x1 pixel texture for use with minimal themes 313 | let tex_data = [0xff, 0xff, 0xff, 0xff]; 314 | let tex_data = renderer.register_texture(handle, &tex_data, (1, 1))?; 315 | output.insert(INTERNAL_SINGLE_PIX_IMAGE_ID.to_string(), tex_data); 316 | handle = handle.next(); 317 | 318 | for (id, source) in self.images.iter() { 319 | let (tex_data, width, height) = source.data.as_ref().unwrap(); 320 | let dims = (*width, *height); 321 | let tex_data = renderer.register_texture(handle, tex_data, dims)?; 322 | output.insert(id.to_string(), tex_data); 323 | 324 | handle = handle.next(); 325 | } 326 | 327 | Ok(output) 328 | } 329 | } 330 | 331 | pub(crate) const INTERNAL_SINGLE_PIX_IMAGE_ID: &str = "__INTERNAL_SINGLE_PIX__"; 332 | 333 | fn watcher_loop(rx: Receiver>) { 334 | for res in rx { 335 | match res { 336 | Ok(event) => { 337 | match event.kind { 338 | EventKind::Any => (), 339 | EventKind::Access(_) => (), 340 | EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_) => { 341 | log::info!("Received file notification: {:?}", event); 342 | RELOAD_THEME.store(true, Ordering::Release); 343 | }, 344 | EventKind::Other => (), 345 | } 346 | }, 347 | Err(e) => { 348 | log::info!("Disconnected live-reload watcher: {}", e); 349 | } 350 | } 351 | } 352 | } -------------------------------------------------------------------------------- /src/scrollpane.rs: -------------------------------------------------------------------------------- 1 | use crate::{Frame, widget::WidgetBuilder, Rect, Point}; 2 | 3 | /** 4 | A [`WidgetBuilder`](struct.WidgetBuilder.html) specifically for creating scrollpanes. 5 | 6 | Create this using [`WidgetBuilder.scrollpane`](struct.WidgetBuilder.html#method.scrollpane). 7 | Scrollpanes can have fairly complex behavior, and can include optional horizontal and vertical scrollbars. 8 | Scrollbars are, by default, only shown when the content size exceeds the pane's inner size. 9 | There is also a [`scrollpane method`](struct.Frame.html#method.scrollpane) on `Frame` as a convenience for simple cases. 10 | 11 | Once you are finished setting up the scrollpane, you call [`children`](#method.children) to add children to the scrollpane 12 | content and add the widget to the frame. Note that the children are added to the scrollpane's content, *not* directly to 13 | the scrollpane itself. 14 | 15 | # Example 16 | ``` 17 | fn build_scrollpane(ui: &mut Frame, unique_id: &str) { 18 | ui.start("scrollpane") 19 | .scrollpane(unique_id) 20 | .show_horizontal_scrollbar(ShowElement::Never) 21 | .children(|ui| { 22 | // scrollable UI here 23 | }) 24 | } 25 | ``` 26 | 27 | # Theme definition 28 | An example of a theme definition for a scrollpane: 29 | 30 | ```yaml 31 | scrollpane: 32 | width_from: Parent 33 | height_from: Parent 34 | border: { all: 5 } 35 | children: 36 | content: 37 | height_from: Parent 38 | width_from: Parent 39 | align: TopLeft 40 | layout: Vertical 41 | size: [-15, -15] 42 | child_align: TopLeft 43 | scrollbar_horizontal: 44 | from: scrollbar_horizontal 45 | scrollbar_vertical: 46 | from: scrollbar_vertical 47 | scroll_button: 48 | wants_mouse: true 49 | background: gui/small_button 50 | size: [20, 20] 51 | border: { all: 4 } 52 | scrollbar_horizontal: 53 | size: [10, 20] 54 | pos: [-5, -5] 55 | align: BotLeft 56 | width_from: Parent 57 | children: 58 | left: 59 | from: scroll_button 60 | align: Left 61 | foreground: gui/arrow_left 62 | right: 63 | from: scroll_button 64 | align: Right 65 | pos: [20, 0] 66 | foreground: gui/arrow_right 67 | scroll: 68 | wants_mouse: true 69 | background: gui/small_button 70 | align: Left 71 | border: { all: 4 } 72 | scrollbar_vertical: 73 | size: [20, 10] 74 | pos: [-5, -5] 75 | align: TopRight 76 | height_from: Parent 77 | children: 78 | up: 79 | from: scroll_button 80 | align: Top 81 | foreground: gui/arrow_up 82 | down: 83 | from: scroll_button 84 | align: Bot 85 | foreground: gui/arrow_down 86 | pos: [0, 20] 87 | scroll: 88 | wants_mouse: true 89 | background: gui/small_button 90 | align: Top 91 | border: { all: 4 } 92 | ``` 93 | */ 94 | pub struct ScrollpaneBuilder<'a> { 95 | builder: WidgetBuilder<'a>, 96 | state: ScrollpaneState, 97 | } 98 | 99 | struct ScrollpaneState { 100 | content_id: String, 101 | show_horiz: ShowElement, 102 | show_vert: ShowElement, 103 | } 104 | 105 | impl<'a> ScrollpaneBuilder<'a> { 106 | pub(crate) fn new(builder: WidgetBuilder<'a>, content_id: &str) -> ScrollpaneBuilder<'a> { 107 | ScrollpaneBuilder { 108 | builder, 109 | state: ScrollpaneState { 110 | content_id: content_id.to_string(), 111 | show_horiz: ShowElement::Sometimes, 112 | show_vert: ShowElement::Sometimes, 113 | } 114 | } 115 | } 116 | 117 | /// Specify when to show the vertical scrollbar in this scrollpane. If `show` is 118 | /// equal to `Sometimes`, will show the vertical scrollbar if the pane content height 119 | /// is greater than the scrollpane's inner height. 120 | pub fn show_vertical_scrollbar(mut self, show: ShowElement) -> ScrollpaneBuilder<'a> { 121 | self.state.show_vert = show; 122 | self 123 | } 124 | 125 | /// Specify when to show the horizontal scrollbar in this scrollpane. If `show` is 126 | /// equal to `Sometimes`, will show the horizontal scrollbar if the pane content width 127 | /// is greater than the scrollpane's inner width. 128 | pub fn show_horizontal_scrollbar(mut self, show: ShowElement) -> ScrollpaneBuilder<'a> { 129 | self.state.show_horiz = show; 130 | self 131 | } 132 | 133 | /// Consumes this builder to create a scrollpane. Calls the specified `children` closure 134 | /// to add children to the scrollpane. 135 | pub fn children(self, children: F) { 136 | let mut min_scroll = Point::default(); 137 | let mut max_scroll = Point::default(); 138 | let mut delta = Point::default(); 139 | 140 | let scrollpane_pos = self.builder.widget.pos(); 141 | let state = self.state; 142 | let content_id = state.content_id; 143 | let horiz = state.show_horiz; 144 | let vert = state.show_vert; 145 | 146 | let (ui, pane_result) = self.builder.finish_with( 147 | Some(|ui: &mut Frame| { 148 | let mut content_bounds = Rect::default(); 149 | 150 | // TODO if horizontal and/or vertical scrollbars aren't present, 151 | // change the scrollpane content size to fill up the available space 152 | 153 | ui.start("content") 154 | .id(&content_id) 155 | .trigger_layout(&mut content_bounds) 156 | .clip(content_bounds) 157 | .children(children); 158 | 159 | let content_min = content_bounds.pos; 160 | let content_max = content_bounds.pos + content_bounds.size; 161 | 162 | let pane_bounds = ui.parent_max_child_bounds(); 163 | let pane_min = pane_bounds.pos; 164 | let pane_max = pane_bounds.pos + pane_bounds.size; 165 | 166 | let mut delta_scroll = Point::default(); 167 | 168 | let enable_horiz = pane_min.x < content_min.x || pane_max.x > content_max.x; 169 | // check whether to show horizontal scrollbar 170 | if horiz.show(enable_horiz) { 171 | let mut scroll_button_center_x = 0.0; 172 | let mut scroll_ratio = 1.0; 173 | 174 | let scrollbar_result = ui.start("scrollbar_horizontal") 175 | .children(|ui| { 176 | let mut right_rect = Rect::default(); 177 | let result = ui.start("right") 178 | .enabled(pane_max.x > content_max.x) 179 | .trigger_layout(&mut right_rect).finish(); 180 | if result.clicked { 181 | delta_scroll.x -= ui.context().options().line_scroll; 182 | } 183 | 184 | let mut left_rect = Rect::default(); 185 | let result = ui.start("left") 186 | .enabled(pane_min.x < content_min.x) 187 | .trigger_layout(&mut left_rect).finish(); 188 | if result.clicked { 189 | delta_scroll.x += ui.context().options().line_scroll; 190 | } 191 | 192 | // compute size and position for main scroll button 193 | let start_frac = ((content_min.x - pane_min.x) / pane_bounds.size.x).max(0.0); 194 | let width_frac = content_bounds.size.x / pane_bounds.size.x; 195 | 196 | // assume left button starts at 0,0 within the parent widget 197 | let size_y = left_rect.size.y; 198 | let min_x = left_rect.size.x; 199 | let mut max_x = right_rect.pos.x - left_rect.pos.x; 200 | let mut size_x = width_frac * (max_x - min_x); 201 | if size_x < size_y { 202 | // don't let scroll button get too small 203 | max_x -= size_y - size_x; 204 | size_x = size_y; 205 | } 206 | 207 | let pos_y = 0.0; 208 | let pos_x = min_x + start_frac * (max_x - min_x); 209 | 210 | scroll_button_center_x = pos_x + size_x * 0.5 + ui.cursor().x; 211 | let scrollbar_dist = max_x - min_x - size_x; // total distance the scrollbar may move 212 | let content_dist = pane_bounds.size.x - content_bounds.size.x; // total distance the content may move 213 | scroll_ratio = content_dist / (2.0 * scrollbar_dist); 214 | 215 | let result = ui.start("scroll") 216 | .size(size_x, size_y) 217 | .pos(pos_x, pos_y) 218 | .enabled(enable_horiz) 219 | .finish(); 220 | 221 | if result.pressed { 222 | delta_scroll.x -= result.moved.x * scroll_ratio; 223 | } 224 | }); 225 | 226 | if scrollbar_result.clicked { 227 | delta_scroll.x -= (ui.mouse_pos().x - scroll_button_center_x) * scroll_ratio; 228 | } 229 | } 230 | 231 | let enable_vertical = pane_min.y < content_min.y || pane_max.y > content_max.y; 232 | // check whether to show vertical scrollbar 233 | if vert.show(enable_vertical) { 234 | let mut scroll_button_center_y = 0.0; 235 | let mut scroll_ratio = 1.0; 236 | 237 | let scrollbar_result = ui.start("scrollbar_vertical") 238 | .children(|ui| { 239 | let mut top_rect = Rect::default(); 240 | let result = ui.start("up") 241 | .enabled(pane_min.y < content_min.y) 242 | .trigger_layout(&mut top_rect).finish(); 243 | if result.clicked { 244 | delta_scroll.y += ui.context().options().line_scroll; 245 | } 246 | 247 | let mut bot_rect = Rect::default(); 248 | let result = ui.start("down") 249 | .enabled(pane_max.y > content_max.y) 250 | .trigger_layout(&mut bot_rect).finish(); 251 | if result.clicked { 252 | delta_scroll.y -= ui.context().options().line_scroll; 253 | } 254 | 255 | // compute size and position for main scroll button 256 | let start_frac = ((content_min.y - pane_min.y) / pane_bounds.size.y).max(0.0); 257 | let height_frac = content_bounds.size.y / pane_bounds.size.y; 258 | 259 | // assume top button starts at 0,0 within the parent widget 260 | let size_x = top_rect.size.x; 261 | let min_y = top_rect.size.y; 262 | let mut max_y = bot_rect.pos.y - top_rect.pos.y; 263 | let mut size_y = height_frac * (max_y - min_y); 264 | if size_y < size_x { 265 | // don't let scroll button get too small 266 | max_y -= size_x - size_y; 267 | size_y = size_x; 268 | } 269 | 270 | let pos_x = 0.0; 271 | let pos_y = min_y + start_frac * (max_y - min_y); 272 | 273 | scroll_button_center_y = pos_y + size_y * 0.5 + ui.cursor().y; 274 | let scrollbar_dist = max_y - min_y - size_y; // total distance the scrollbar may move 275 | let content_dist = pane_bounds.size.y - content_bounds.size.y; // total distance the content may move 276 | scroll_ratio = content_dist / scrollbar_dist; 277 | 278 | let result = ui.start("scroll") 279 | .size(size_x, size_y) 280 | .pos(pos_x, pos_y) 281 | .enabled(enable_vertical) 282 | .finish(); 283 | 284 | if result.pressed && result.moved.y != 0.0 { 285 | delta_scroll.y -= result.moved.y * scroll_ratio; 286 | } 287 | }); 288 | 289 | if scrollbar_result.clicked { 290 | delta_scroll.y -= (ui.mouse_pos().y - scroll_button_center_y - scrollpane_pos.y) * scroll_ratio; 291 | } 292 | } 293 | 294 | min_scroll = content_max - pane_max; 295 | max_scroll = content_min - pane_min; 296 | delta = delta_scroll; 297 | }) 298 | ); 299 | 300 | delta = delta + pane_result.moved; 301 | 302 | // set the scroll every frame to bound it, in case it was modified externally 303 | ui.modify(&content_id, |state| { 304 | let min = min_scroll + state.scroll; 305 | let max = Point::default(); 306 | 307 | state.scroll = (state.scroll + delta).max(min).min(max); 308 | }); 309 | } 310 | } 311 | 312 | /// An enum to define when to show a particular UI element. 313 | #[derive(Debug, Copy, Clone)] 314 | pub enum ShowElement { 315 | /// Never show the element 316 | Never, 317 | 318 | /// Always show the element 319 | Always, 320 | 321 | /// Show the element based on some external condition. For example, 322 | /// for a [`Scrollpane`](struct.ScrollpaneBuilder.html), show the 323 | /// scrollbar based on whether the content is larger than the scrollpane 324 | /// area. 325 | Sometimes, 326 | } 327 | 328 | impl ShowElement { 329 | fn show(self, content: bool) -> bool { 330 | match self { 331 | ShowElement::Never => false, 332 | ShowElement::Sometimes => content, 333 | ShowElement::Always => true, 334 | } 335 | } 336 | } -------------------------------------------------------------------------------- /src/window.rs: -------------------------------------------------------------------------------- 1 | use crate::{Frame, widget::WidgetBuilder, WidgetState, Point}; 2 | 3 | /** 4 | A [`WidgetBuilder`](struct.WidgetBuilder.html) specifically for creating windows. 5 | 6 | Windows can have a titlebar, close button, move, and resizing capabilities. Each window 7 | is automatically part of its own [`render group`](struct.WidgetBuilder.html#method.new_render_group) 8 | and will automatically come on top of other widgets when clicked on. You can create a `WindowBuilder` 9 | from a [`WidgetBuilder`](struct.WidgetBuilder.html) by calling [`window`](struct.WidgetBuilder.html#method.window) 10 | after any calls to general purpose widget layout. 11 | 12 | There is also a [`window method on Frame`](struct.Frame.html#method.window) as a convenience for simple cases. 13 | 14 | Once you are finished setting up the window, you call [`children`](#method.children) to add children and add the widget 15 | to the frame. 16 | 17 | # Example 18 | ``` 19 | fn create_window(ui: &mut Frame, unique_id: &str) { 20 | ui.start("window") 21 | .window(unique_id) 22 | .title("My Window") 23 | .resizable(false) 24 | .children(|ui| { 25 | // window content here 26 | }); 27 | } 28 | ``` 29 | 30 | # Theme definition 31 | An example of a theme definition for a window: 32 | 33 | ```yaml 34 | window: 35 | background: gui/window_bg 36 | wants_mouse: true 37 | layout: Vertical 38 | layout_spacing: [5, 5] 39 | border: { left: 5, right: 5, top: 35, bot: 5 } 40 | size: [300, 400] 41 | child_align: Top 42 | children: 43 | titlebar: 44 | wants_mouse: true 45 | background: gui/small_button 46 | size: [10, 30] 47 | pos: [-6, -36] 48 | border: { all: 5 } 49 | width_from: Parent 50 | child_align: Center 51 | align: TopLeft 52 | children: 53 | title: 54 | from: label 55 | text: "Main Window" 56 | font: medium 57 | width_from: Parent 58 | close: 59 | wants_mouse: true 60 | background: gui/small_button 61 | foreground: gui/close_icon 62 | size: [20, 20] 63 | border: { all: 4 } 64 | align: TopRight 65 | handle: 66 | wants_mouse: true 67 | background: gui/window_handle 68 | size: [12, 12] 69 | align: BotRight 70 | pos: [-1, -1] 71 | ``` 72 | */ 73 | pub struct WindowBuilder<'a> { 74 | builder: WidgetBuilder<'a>, 75 | state: WindowState, 76 | } 77 | 78 | impl<'a> WindowBuilder<'a> { 79 | pub(crate) fn new(builder: WidgetBuilder<'a>) -> WindowBuilder<'a> { 80 | WindowBuilder { 81 | builder, 82 | state: WindowState::default(), 83 | } 84 | } 85 | 86 | /// Specifies that this window will not use a new render group. This can 87 | /// be useful in some cases where you want to handle grouping yourself. 88 | /// See [`WidgetBuilder.new_render_group`](struct.WidgetBuilder.html#method.new_render_group) 89 | #[must_use] 90 | pub fn cancel_render_group(mut self) -> WindowBuilder<'a> { 91 | self.builder.set_next_render_group(None); 92 | self 93 | } 94 | 95 | /// Specifies whether the created window should show a titlebar. 96 | #[must_use] 97 | pub fn with_titlebar(mut self, with_titlebar: bool) -> WindowBuilder<'a> { 98 | self.state.with_titlebar = with_titlebar; 99 | self 100 | } 101 | 102 | /// Specify a title to show in the window's titlebar, if it is present. If the 103 | /// titlebar is not present, does nothing. This will override any text set in the theme. 104 | #[must_use] 105 | pub fn title>(mut self, title: T) -> WindowBuilder<'a> { 106 | self.state.title = Some(title.into()); 107 | self 108 | } 109 | 110 | /// Specifies whether the created window should have a close button. 111 | #[must_use] 112 | pub fn with_close_button(mut self, with_close_button: bool) -> WindowBuilder<'a> { 113 | self.state.with_close_button = with_close_button; 114 | self 115 | } 116 | 117 | /// Specifies whether the user should be able to move the created window 118 | /// by dragging the mouse. Note that if the [`titlebar`](#method.with_titlebar) is not shown, there 119 | /// will be no way to move the window regardless of this setting. 120 | #[must_use] 121 | pub fn moveable(mut self, moveable: bool) -> WindowBuilder<'a> { 122 | self.state.moveable = moveable; 123 | self 124 | } 125 | 126 | /// Specifies whether the user should be able to resize the created window. 127 | /// If false, the resize handle will not be shown. 128 | #[must_use] 129 | pub fn resizable(mut self, resizable: bool) -> WindowBuilder<'a> { 130 | self.state.resizable = resizable; 131 | self 132 | } 133 | 134 | /// Consumes the builder and adds a widget to the current frame. The 135 | /// returned data includes information about the animation state and 136 | /// mouse interactions of the created element. 137 | /// The provided closure is called to enable adding children to this window. 138 | pub fn children(self, children: F) -> WidgetState { 139 | let builder = self.builder; 140 | let state = self.state; 141 | let id = builder.widget.id().to_string(); 142 | 143 | builder.children(|ui| { 144 | (children)(ui); 145 | 146 | let drag_move = if state.with_titlebar { 147 | let result = ui.start("titlebar") 148 | .children(|ui| { 149 | if let Some(title) = state.title.as_ref() { 150 | ui.start("title").text(title).finish(); 151 | } else { 152 | ui.start("title").finish(); 153 | } 154 | 155 | if state.with_close_button { 156 | let clicked = ui.child("close").clicked; 157 | 158 | if clicked { 159 | ui.close(&id); 160 | } 161 | } 162 | }); 163 | 164 | if state.moveable && result.pressed { 165 | result.moved 166 | } else { 167 | Point::default() 168 | } 169 | } else { 170 | Point::default() 171 | }; 172 | 173 | if drag_move != Point::default() { 174 | ui.modify(&id, |state| { 175 | state.moved = state.moved + drag_move; 176 | }); 177 | } 178 | 179 | if state.resizable { 180 | let result = ui.button("handle", ""); 181 | if result.pressed { 182 | ui.modify(&id, |state| { 183 | state.resize = state.resize + result.moved; 184 | }); 185 | } 186 | } 187 | }) 188 | } 189 | } 190 | 191 | struct WindowState { 192 | with_titlebar: bool, 193 | with_close_button: bool, 194 | moveable: bool, 195 | resizable: bool, 196 | title: Option, 197 | } 198 | 199 | impl Default for WindowState { 200 | fn default() -> Self { 201 | Self { 202 | with_titlebar: true, 203 | with_close_button: true, 204 | moveable: true, 205 | resizable: true, 206 | title: None, 207 | } 208 | } 209 | } -------------------------------------------------------------------------------- /src/winit_io/mod.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | 3 | use winit::event::{ElementState, MouseButton, MouseScrollDelta, WindowEvent}; 4 | use winit::keyboard::{Key, NamedKey}; 5 | use winit::window::Window; 6 | 7 | use crate::point::Point; 8 | use crate::context::{InputModifiers, Context}; 9 | use crate::render::IO; 10 | use crate::KeyEvent; 11 | 12 | /** 13 | A Thyme Input/Output adapter for [`winit`](https://github.com/rust-windowing/winit). 14 | 15 | This adapter handles events from `winit` and sends them to the Thyme [`Context`](struct.Context.html). 16 | WindowEvents should be passed to this handler, assuming [`Context.wants_mouse`](struct.Context.html#method.wants_mouse) 17 | returns true for the given frame. 18 | 19 | # Example 20 | ``` 21 | fn main_loop(event_loop: winit::EventLoop<()>, thyme: thyme::Context) { 22 | event_loop.run(move |event, _, control_flow| match event { 23 | Event::MainEventsCleared => { 24 | // Renderer specific code here 25 | 26 | let mut ui = context.create_frame(); 27 | // create UI here 28 | 29 | // draw the frame and finish up rendering here 30 | } 31 | Event::WindowEvent { event: WindowEvent::CloseRequested, .. } => *control_flow = ControlFlow::Exit, 32 | event => { 33 | io.handle_event(&mut context, &event); 34 | } 35 | }) 36 | } 37 | ``` 38 | */ 39 | pub struct WinitIo { 40 | scale_factor: f32, 41 | display_size: Point, 42 | } 43 | 44 | impl IO for WinitIo { 45 | fn scale_factor(&self) -> f32 { self.scale_factor } 46 | 47 | fn display_size(&self) -> Point { self.display_size } 48 | } 49 | 50 | impl WinitIo { 51 | /// Creates a new adapter from the given `EventLoop`, with the specified initial display size, 52 | /// in logical pixels. This may change over time. 53 | pub fn new( 54 | window: &Window, 55 | logical_display_size: Point, 56 | ) -> Result { 57 | let monitor = window.primary_monitor().ok_or(WinitError::PrimaryMonitorNotFound)?; 58 | let scale_factor = monitor.scale_factor() as f32; 59 | Ok(WinitIo { 60 | scale_factor, 61 | display_size: logical_display_size * scale_factor, 62 | }) 63 | } 64 | 65 | /// Handles a winit `Event` and passes it to the Thyme [`Context`](struct.Context.html). 66 | pub fn handle_event(&mut self, context: &mut Context, event: &WindowEvent) { 67 | use WindowEvent::*; 68 | match event { 69 | Resized(size) => { 70 | let (x, y): (u32, u32) = (*size).into(); 71 | let size: Point = (x as f32, y as f32).into(); 72 | self.display_size = size; 73 | context.set_display_size(size); 74 | }, 75 | ModifiersChanged(m) => { 76 | let shift = m.state().shift_key(); 77 | let ctrl = m.state().control_key(); 78 | let alt = m.state().alt_key(); 79 | context.set_input_modifiers(InputModifiers { shift, ctrl, alt }); 80 | }, 81 | WindowEvent::ScaleFactorChanged { scale_factor, .. } => { 82 | let scale = *scale_factor as f32; 83 | self.scale_factor = scale; 84 | context.set_scale_factor(scale); 85 | }, 86 | MouseInput { state, button, .. } => { 87 | let pressed = match state { 88 | ElementState::Pressed => true, 89 | ElementState::Released => false, 90 | }; 91 | 92 | let index: usize = match button { 93 | MouseButton::Left => 0, 94 | MouseButton::Right => 1, 95 | MouseButton::Middle => 2, 96 | MouseButton::Back => 3, 97 | MouseButton::Forward => 4, 98 | MouseButton::Other(index) => *index as usize + 5, 99 | }; 100 | 101 | context.set_mouse_pressed(pressed, index); 102 | }, 103 | MouseWheel { delta, .. } => { 104 | match delta { 105 | MouseScrollDelta::LineDelta(x, y) => { 106 | // TODO configure line delta 107 | context.add_mouse_wheel(Point::new(*x, *y), true); 108 | }, MouseScrollDelta::PixelDelta(pos) => { 109 | let x = pos.x as f32; 110 | let y = pos.y as f32; 111 | context.add_mouse_wheel(Point::new(x, y), false); 112 | } 113 | } 114 | }, 115 | CursorMoved { position, .. } => { 116 | context.set_mouse_pos((position.x as f32 / self.scale_factor, position.y as f32 / self.scale_factor).into()); 117 | }, 118 | KeyboardInput { event, .. } => { 119 | if let Some(str) = event.text.as_ref() { 120 | if let ElementState::Pressed = event.state { 121 | for c in str.chars() { 122 | context.push_character(c); 123 | } 124 | } 125 | } 126 | 127 | match &event.logical_key { 128 | Key::Named(named_key) => { 129 | if let ElementState::Released = event.state { 130 | if let Some(key) = key_event(*named_key) { 131 | context.push_key_event(key); 132 | } 133 | } 134 | }, 135 | Key::Character(_) | Key::Unidentified(_) | Key::Dead(_) => (), 136 | } 137 | }, 138 | _ => (), 139 | } 140 | } 141 | } 142 | 143 | /// An error of several types originating from winit Windowing functions 144 | #[derive(Debug)] 145 | pub enum WinitError { 146 | /// No primary monitor is found 147 | PrimaryMonitorNotFound, 148 | 149 | /// Internal OS error forwarded to winit 150 | Os(winit::error::OsError), 151 | 152 | /// An error in the creation or execution of the EventLoop 153 | EventLoop(winit::error::EventLoopError), 154 | 155 | /// An error getting the window handle associated with a window 156 | HandleError(winit::raw_window_handle::HandleError), 157 | } 158 | 159 | impl std::fmt::Display for WinitError { 160 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 161 | use self::WinitError::*; 162 | match self { 163 | PrimaryMonitorNotFound => write!(f, "Primary monitor not found."), 164 | Os(e) => write!(f, "OS Error: {}", e), 165 | EventLoop(e) => write!(f, "Event Loop error: {}", e), 166 | HandleError(e) => write!(f, "Window handle error: {}", e), 167 | } 168 | } 169 | } 170 | 171 | impl Error for WinitError { 172 | fn source(&self) -> Option<&(dyn Error + 'static)> { 173 | use self::WinitError::*; 174 | match self { 175 | PrimaryMonitorNotFound => None, 176 | Os(e) => Some(e), 177 | EventLoop(e) => Some(e), 178 | HandleError(e) => Some(e), 179 | } 180 | } 181 | } 182 | 183 | fn key_event(input: NamedKey) -> Option { 184 | use NamedKey::*; 185 | Some(match input { 186 | Insert => KeyEvent::Insert, 187 | Home => KeyEvent::Home, 188 | Delete => KeyEvent::Delete, 189 | End => KeyEvent::End, 190 | PageDown => KeyEvent::PageDown, 191 | PageUp => KeyEvent::PageUp, 192 | ArrowLeft => KeyEvent::Left, 193 | ArrowUp => KeyEvent::Up, 194 | ArrowRight => KeyEvent::Right, 195 | ArrowDown => KeyEvent::Down, 196 | Backspace => KeyEvent::Back, 197 | Enter => KeyEvent::Return, 198 | Space => KeyEvent::Space, 199 | Escape => KeyEvent::Escape, 200 | Tab => KeyEvent::Tab, 201 | F1 => KeyEvent::F1, 202 | F2 => KeyEvent::F2, 203 | F3 => KeyEvent::F3, 204 | F4 => KeyEvent::F4, 205 | F5 => KeyEvent::F5, 206 | F6 => KeyEvent::F6, 207 | F7 => KeyEvent::F7, 208 | F8 => KeyEvent::F8, 209 | F9 => KeyEvent::F9, 210 | F10 => KeyEvent::F10, 211 | F11 => KeyEvent::F11, 212 | F12 => KeyEvent::F12, 213 | _ => return None, 214 | }) 215 | } --------------------------------------------------------------------------------