mirror of
https://github.com/griffi-gh/hUI.git
synced 2024-11-28 18:08:42 -06:00
Compare commits
278 commits
6eb3d98ad4
...
55d5146826
Author | SHA1 | Date | |
---|---|---|---|
griffi-gh | 55d5146826 | ||
griffi-gh | 485f43eefa | ||
griffi-gh | 1c0bb77788 | ||
griffi-gh | ab6f4b5d31 | ||
griffi-gh | ea9e6d303d | ||
griffi-gh | 2d85b0c422 | ||
griffi-gh | 6b715149d6 | ||
griffi-gh | 2901ca5b82 | ||
griffi-gh | b03e23c439 | ||
griffi-gh | 44824a86f5 | ||
griffi-gh | fa672f422f | ||
griffi-gh | d62207fbd3 | ||
griffi-gh | 4b34137375 | ||
griffi-gh | fda54e01b3 | ||
griffi-gh | f820f7e930 | ||
griffi-gh | bbe5b273d1 | ||
griffi-gh | e7da4a7f0d | ||
griffi-gh | 0625b91735 | ||
griffi-gh | 85ebf67431 | ||
griffi-gh | 7cfc97b434 | ||
griffi-gh | 512042ebd8 | ||
griffi-gh | ef8032c29d | ||
griffi-gh | 632ed35d82 | ||
griffi-gh | bd2f49d7f2 | ||
griffi-gh | 101b516ed1 | ||
griffi-gh | 4550cefb0b | ||
griffi-gh | 54b1c44159 | ||
griffi-gh | 11b5363e26 | ||
griffi-gh | 3dabfdb339 | ||
griffi-gh | e9078ade10 | ||
griffi-gh | 9e61c08706 | ||
griffi-gh | 3f29000136 | ||
griffi-gh | f4ca1ffbd6 | ||
griffi-gh | 5755613e90 | ||
griffi-gh | 9f3d2c7def | ||
griffi-gh | b87a0b2f04 | ||
griffi-gh | e765eb2748 | ||
griffi-gh | a0a24c80dd | ||
griffi-gh | c104f6cc04 | ||
griffi-gh | f52a15009b | ||
griffi-gh | 4bb59b7c5f | ||
griffi-gh | adc5cb5f3b | ||
griffi-gh | f6ff5e7269 | ||
griffi-gh | 0b5df70e2b | ||
griffi-gh | dfcd4af0aa | ||
griffi-gh | ccd8c060c7 | ||
griffi-gh | a9e82fccb5 | ||
griffi-gh | d9b4e20d70 | ||
griffi-gh | eaadf43821 | ||
griffi-gh | d8f38fd5b4 | ||
griffi-gh | f6ea37e402 | ||
griffi-gh | bfb5bfaa0a | ||
griffi-gh | 1666b08873 | ||
griffi-gh | d11f160d55 | ||
griffi-gh | 88a2b56610 | ||
griffi-gh | 84fef8a6f8 | ||
griffi-gh | 72277a0fed | ||
griffi-gh | 7ce24185e5 | ||
griffi-gh | a7f01b1a72 | ||
griffi-gh | aee38bec45 | ||
griffi-gh | 5af38d7a2a | ||
griffi-gh | e488ef70b5 | ||
griffi-gh | e4fc96a2d8 | ||
griffi-gh | 17a313bdb8 | ||
griffi-gh | 4e17d368db | ||
griffi-gh | 0eec840a08 | ||
griffi-gh | 84fdd9d1ae | ||
griffi-gh | 3f8dea24be | ||
griffi-gh | 39746aa566 | ||
griffi-gh | 47c5f1c4a8 | ||
griffi-gh | b18e2a5066 | ||
griffi-gh | 9b7be50880 | ||
griffi-gh | 04ebfd41e8 | ||
griffi-gh | 27a75d6c30 | ||
griffi-gh | 2e49ea443a | ||
griffi-gh | e665a94335 | ||
griffi-gh | 5cb696d3f6 | ||
griffi-gh | f7d8166513 | ||
griffi-gh | 107ccc99a9 | ||
griffi-gh | 65f7cc9328 | ||
griffi-gh | f8bf39342b | ||
griffi-gh | f60087f12e | ||
griffi-gh | 71b160d7aa | ||
griffi-gh | 51366210bf | ||
griffi-gh | 769d045d60 | ||
griffi-gh | 03082213fb | ||
griffi-gh | 80dd442d98 | ||
griffi-gh | 98065f46af | ||
griffi-gh | 1b3671a3bf | ||
griffi-gh | 101d685148 | ||
griffi-gh | 5bc048f911 | ||
griffi-gh | 32cf9dca29 | ||
griffi-gh | bbc4cb00e0 | ||
griffi-gh | 4ce36cc7a9 | ||
griffi-gh | 897a574931 | ||
griffi-gh | 15d92a2c74 | ||
griffi-gh | 1cd2f0ec1a | ||
griffi-gh | 918597585d | ||
griffi-gh | 388b71ebd0 | ||
griffi-gh | d1732fe2b7 | ||
griffi-gh | 64640f215a | ||
griffi-gh | 2c099b5edd | ||
griffi-gh | 84d494f8a8 | ||
griffi-gh | ccb7d22a58 | ||
griffi-gh | fea53226a8 | ||
griffi-gh | 0c46aa2caf | ||
griffi-gh | ac7ec94719 | ||
griffi-gh | 384fddce34 | ||
griffi-gh | d36a78d2ee | ||
griffi-gh | 02558e0695 | ||
griffi-gh | 16d8fd7c6a | ||
griffi-gh | 9600d7f7a9 | ||
griffi-gh | 8a0b8281bc | ||
griffi-gh | 1698961d5c | ||
griffi-gh | 4bd93b3106 | ||
griffi-gh | b9aab4152f | ||
griffi-gh | 243400ee8e | ||
griffi-gh | b21184addf | ||
griffi-gh | c60775ff4d | ||
griffi-gh | 4476307482 | ||
griffi-gh | 7d3932f139 | ||
griffi-gh | 3a58e530b3 | ||
griffi-gh | cd338f101c | ||
griffi-gh | b185dd2bb4 | ||
griffi-gh | 4437eb9878 | ||
griffi-gh | d43fd91a4e | ||
griffi-gh | 509d39ddea | ||
griffi-gh | de9f41c296 | ||
griffi-gh | c3f32b3ddd | ||
griffi-gh | 6b2d4fbe36 | ||
griffi-gh | c2d7542bb8 | ||
griffi-gh | fc458464a7 | ||
griffi-gh | f716433d37 | ||
griffi-gh | 4c137095a0 | ||
griffi-gh | 197b327b1f | ||
griffi-gh | 2d59f76ba7 | ||
griffi-gh | 13785a4946 | ||
griffi-gh | dcdbc0fcbe | ||
griffi-gh | 67290e52ea | ||
griffi-gh | 1fe104e6f3 | ||
griffi-gh | 5d98d7c657 | ||
griffi-gh | 352691c228 | ||
griffi-gh | f25efdb3ca | ||
griffi-gh | a92817cb88 | ||
griffi-gh | 8a6785b688 | ||
griffi-gh | af138760c9 | ||
griffi-gh | 1ac9f4abd0 | ||
griffi-gh | ddf093ed14 | ||
griffi-gh | b6cd7567d7 | ||
griffi-gh | 25f947ba31 | ||
griffi-gh | 788ee5e98f | ||
griffi-gh | 226fac9e8b | ||
griffi-gh | 481fff7dee | ||
griffi-gh | 6064567433 | ||
griffi-gh | 688bc6e489 | ||
griffi-gh | fb60c25385 | ||
griffi-gh | 796e274e32 | ||
griffi-gh | 3731f19380 | ||
griffi-gh | 1fc63cf070 | ||
griffi-gh | f05659e35b | ||
griffi-gh | 047d53016f | ||
griffi-gh | 677cc7d37d | ||
griffi-gh | 2c20da0b3b | ||
griffi-gh | fe093c5db2 | ||
griffi-gh | e2a328f4b7 | ||
griffi-gh | 507eff3f8f | ||
griffi-gh | b9f58285da | ||
griffi-gh | 4d2d947e68 | ||
griffi-gh | b6898d5b79 | ||
griffi-gh | 689541e804 | ||
griffi-gh | 4621f0335b | ||
griffi-gh | 4c26a1297f | ||
griffi-gh | ef0d610764 | ||
griffi-gh | cba0795213 | ||
griffi-gh | 6feefb61fa | ||
griffi-gh | e7edd98d41 | ||
griffi-gh | d1588932e5 | ||
griffi-gh | 6549a6a410 | ||
griffi-gh | 392fed5798 | ||
griffi-gh | ac723bad3f | ||
griffi-gh | 4affd56d7a | ||
griffi-gh | cca9890600 | ||
griffi-gh | 1d48a386cb | ||
griffi-gh | 80282780d5 | ||
griffi-gh | 551dd343ac | ||
griffi-gh | a09a25ba73 | ||
griffi-gh | 91367d54e0 | ||
griffi-gh | be589d302d | ||
griffi-gh | 3a8ff21189 | ||
griffi-gh | 45bbdd57fd | ||
griffi-gh | cd589d29ae | ||
griffi-gh | 27205a93d2 | ||
griffi-gh | dea07fb639 | ||
griffi-gh | e140075549 | ||
griffi-gh | 2a8c6dddb1 | ||
griffi-gh | 661c00ab87 | ||
griffi-gh | 3471a0c382 | ||
griffi-gh | b7a4b82056 | ||
griffi-gh | 35ee24bf06 | ||
griffi-gh | ba083b76f5 | ||
griffi-gh | d86cce035b | ||
griffi-gh | 9f3cc681b5 | ||
griffi-gh | 2c7af890eb | ||
griffi-gh | c726120f82 | ||
griffi-gh | 8759f0169c | ||
griffi-gh | a36c127e61 | ||
griffi-gh | 8c5ba17c9c | ||
griffi-gh | 684cab5ade | ||
griffi-gh | 0f98da2753 | ||
griffi-gh | 6bc912e936 | ||
griffi-gh | 2937f8e7e3 | ||
griffi-gh | 0a7392684a | ||
griffi-gh | 20f89a7567 | ||
griffi-gh | b616548b83 | ||
griffi-gh | 966bcb748f | ||
griffi-gh | 21508c0f76 | ||
griffi-gh | 91c5b99bce | ||
griffi-gh | bb76139598 | ||
griffi-gh | a5cd74e911 | ||
griffi-gh | 1f7685aef5 | ||
griffi-gh | f7366b9bbb | ||
griffi-gh | f9ea777954 | ||
griffi-gh | f8d67da0c9 | ||
griffi-gh | a6b2244461 | ||
griffi-gh | 7df6340531 | ||
griffi-gh | 3f60cbd751 | ||
griffi-gh | 0287e46923 | ||
griffi-gh | 3550c28cc7 | ||
griffi-gh | f95caf37e5 | ||
griffi-gh | fc25818e6b | ||
griffi-gh | f12a59fd31 | ||
griffi-gh | 2fdc075f3a | ||
griffi-gh | ecc137bcd5 | ||
griffi-gh | 8f6a15e244 | ||
griffi-gh | 385d7edb6a | ||
griffi-gh | cbafbec066 | ||
griffi-gh | db4a6435c9 | ||
griffi-gh | a0eb2cd42c | ||
griffi-gh | 23e4be05f5 | ||
griffi-gh | 4e6f64b60e | ||
griffi-gh | af892698b5 | ||
griffi-gh | 27080e80e7 | ||
griffi-gh | 972c086a32 | ||
griffi-gh | 770cae8acc | ||
griffi-gh | f4a747c505 | ||
griffi-gh | 91e8b44dd5 | ||
griffi-gh | 99e1bc5549 | ||
griffi-gh | 83c00e8c13 | ||
griffi-gh | c72a005ff6 | ||
griffi-gh | 81bb44f719 | ||
griffi-gh | ad8496279e | ||
griffi-gh | 5ce6f1c9f4 | ||
griffi-gh | b267655d07 | ||
griffi-gh | 9c3d1d9214 | ||
griffi-gh | 19c705866f | ||
griffi-gh | 3a045fa4c4 | ||
griffi-gh | c189f412bf | ||
griffi-gh | f82d3331a0 | ||
griffi-gh | 6485af80ca | ||
griffi-gh | a9edb9a142 | ||
griffi-gh | 9a917e21e7 | ||
griffi-gh | 704c2ff779 | ||
griffi-gh | e758b2459b | ||
griffi-gh | e7cdc3e58e | ||
griffi-gh | 5174421ae5 | ||
griffi-gh | d21abc6b62 | ||
griffi-gh | 3e4e98eb49 | ||
griffi-gh | c50333805b | ||
griffi-gh | 4828500c45 | ||
griffi-gh | b04694ce83 | ||
griffi-gh | 246b80c0fa | ||
griffi-gh | 57900bc287 | ||
griffi-gh | b65e540f0e | ||
griffi-gh | ea6623f143 | ||
griffi-gh | e88c479a52 | ||
griffi-gh | 4f88c7ed22 | ||
griffi-gh | bb182cc74a | ||
griffi-gh | 793623d088 |
BIN
.assets/000000.png
Normal file
BIN
.assets/000000.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 106 B |
BIN
.assets/exemplaris.png
Normal file
BIN
.assets/exemplaris.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.9 KiB |
16
.devcontainer/devcontainer.json
Normal file
16
.devcontainer/devcontainer.json
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"image": "mcr.microsoft.com/devcontainers/universal:2",
|
||||||
|
"features": {
|
||||||
|
"ghcr.io/devcontainers/features/rust:1": {},
|
||||||
|
"ghcr.io/devcontainers/features/desktop-lite:1": {}
|
||||||
|
},
|
||||||
|
"forwardPorts": [6080, 5901],
|
||||||
|
"portsAttributes": {
|
||||||
|
"6080": {
|
||||||
|
"label": "desktop"
|
||||||
|
},
|
||||||
|
"5901": {
|
||||||
|
"label": "desktop"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
5
.markdownlint.jsonc
Normal file
5
.markdownlint.jsonc
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"MD041": false, //first-line-heading
|
||||||
|
"MD013": false, //line-length
|
||||||
|
"MD033": false //inline-html
|
||||||
|
}
|
6
.vscode/settings.json
vendored
Normal file
6
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"editor.detectIndentation": false,
|
||||||
|
"editor.tabSize": 2,
|
||||||
|
"editor.insertSpaces": true,
|
||||||
|
"editor.wordWrap": "off"
|
||||||
|
}
|
|
@ -1,3 +1,3 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = ["hui", "hui-examples", "hui-glium"]
|
members = ["hui", "hui-derive", "hui-examples", "hui-glium", "hui-wgpu", "hui-winit"]
|
||||||
|
|
177
README.md
177
README.md
|
@ -1,26 +1,167 @@
|
||||||
<img src="./.assets/hui.svg" width="110" align="left">
|
<p></p><p></p>
|
||||||
<h1>
|
<img src="https://raw.githubusercontent.com/griffi-gh/hui/master/.assets/hui.svg" width="120" align="left" alt="logo">
|
||||||
huї
|
<h1>hUI</h1>
|
||||||
</h1>
|
<div>
|
||||||
<div align="right">
|
<span>
|
||||||
<a href="./LICENSE.txt">
|
Simple UI library for games and other interactive applications
|
||||||
<img alt="license" src="https://img.shields.io/github/license/griffi-gh/hui" align="right">
|
</span><a href="https://crates.io/crates/hui" float="right">
|
||||||
</a><br>
|
<img alt="crates.io" src="https://img.shields.io/crates/v/hui.svg?style=flat-square" align="right" height="20">
|
||||||
<a href="https://crates.io/crates/hui">
|
</a><br><a href="./LICENSE.txt" align="right" float="right">
|
||||||
<img alt="crates.io" src="https://img.shields.io/crates/v/hui.svg" align="right">
|
<img alt="license" src="https://img.shields.io/github/license/griffi-gh/hui?style=flat-square" align="right" width="102" height="20">
|
||||||
</a>
|
</a><span>
|
||||||
|
(Formerly <code>kubi-ui</code>)
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p align="left">
|
<p></p>
|
||||||
Simple UI library for games and other interactive applications<br>
|
|
||||||
(formerly kubi-ui)
|
|
||||||
</p>
|
|
||||||
<br clear="all">
|
<br clear="all">
|
||||||
|
|
||||||
<table>
|
<table align="center">
|
||||||
<td>
|
<td>
|
||||||
<img src="./.assets/demo0.gif" width="300">
|
<img src="https://raw.githubusercontent.com/griffi-gh/hui/master/.assets/demo0.gif" width="300" alt="example: mom_downloader">
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<img src="./.assets/demo1.gif" width="300">
|
<img src="https://raw.githubusercontent.com/griffi-gh/hui/master/.assets/demo1.gif" width="300" alt="example: align_test">
|
||||||
</td>
|
</td>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
<h2>Example</h2>
|
||||||
|
<img src="https://raw.githubusercontent.com/griffi-gh/hui/master/.assets/exemplaris.png"
|
||||||
|
height="175" align="right" float="right" alt="code result">
|
||||||
|
<pre lang="rust">Container::default()
|
||||||
|
.with_size(size!(100%, 50%))
|
||||||
|
.with_align(Alignment::Center)
|
||||||
|
.with_padding(5.)
|
||||||
|
.with_gap(10.)
|
||||||
|
.with_background(rect_frame! {
|
||||||
|
color: (0.5, 0.5, 0.5, 1.),
|
||||||
|
corner_radius: 10.,
|
||||||
|
})
|
||||||
|
.with_children(|ui| {
|
||||||
|
Text::default()
|
||||||
|
.with_text("Hello, world")
|
||||||
|
.with_text_size(100)
|
||||||
|
.with_color(color::BLACK)
|
||||||
|
.add_child(ui);
|
||||||
|
Container::default()
|
||||||
|
.with_padding((10., 20.))
|
||||||
|
.with_background(rect_frame! {
|
||||||
|
color: color::DARK_RED,
|
||||||
|
corner_radius: (2.5, 30., 2.5, 2.5),
|
||||||
|
})
|
||||||
|
.with_children(|ui| {
|
||||||
|
Text::default()
|
||||||
|
.with_text("Lorem ipsum dolor sit amet, consectetur adipiscing elit.")
|
||||||
|
.with_text_size(24)
|
||||||
|
.add_child(ui);
|
||||||
|
})
|
||||||
|
.add_child(ui);
|
||||||
|
})
|
||||||
|
.add_root(ui, size);</pre>
|
||||||
|
|
||||||
|
<h2>Backends</h2>
|
||||||
|
<p>
|
||||||
|
Latest stable release:
|
||||||
|
<a href="https://crates.io/crates/hui" float="right">
|
||||||
|
<img alt="crates.io" src="https://img.shields.io/crates/v/hui.svg?style=flat-square&label=&color=0d1117" height="20">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th align="center">
|
||||||
|
<code>hui</code>
|
||||||
|
</th>
|
||||||
|
<th align="center">
|
||||||
|
<code>glium</code> (render)
|
||||||
|
</th>
|
||||||
|
<th align="center">
|
||||||
|
<code>winit</code> (platform)
|
||||||
|
</th>
|
||||||
|
<th align="center">
|
||||||
|
<code>wgpu</code> (render)
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<code>master</code>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<code>hui-glium = <master></code><br>
|
||||||
|
<code>glium = "0.34"</code>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<code>hui-winit = <master></code><br>
|
||||||
|
<code>winit = "0.30"</code> or <code>winit = "0.29"</code>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<code>hui-wgpu = <master></code><br>
|
||||||
|
<code>wgpu = "0.20"</code>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<code>0.1.0-alpha.4</code>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<code>hui-glium = "0.1.0-alpha.4"</code><br>
|
||||||
|
<code>glium = "0.34"</code>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<code>hui-winit = "0.1.0-alpha.4"</code><br>
|
||||||
|
<code>winit = "0.29"</code>
|
||||||
|
</td>
|
||||||
|
<td align="center">N/A</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<code>0.1.0-alpha.3</code>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<code>hui-glium = "0.1.0-alpha.3"</code><br>
|
||||||
|
<code>glium = "0.34"</code>
|
||||||
|
</td>
|
||||||
|
<td align="center" colspan="2">N/A</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<code>0.1.0-alpha.2</code>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<code>hui-glium = "0.1.0-alpha.2"</code><br>
|
||||||
|
<code>glium = "0.34"</code>
|
||||||
|
</td>
|
||||||
|
<td align="center" colspan="2">N/A</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<code>0.1.0-alpha.1</code>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<code>hui-glium = "0.1.0-alpha.1"</code><br>
|
||||||
|
<code>glium = "0.34"</code>
|
||||||
|
</td>
|
||||||
|
<td align="center" colspan="2">N/A</td>
|
||||||
|
</tr>
|
||||||
|
<!-- <tr>
|
||||||
|
<td align="center">
|
||||||
|
<code>0.0.2</code>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<code>hui-glium = "0.0.2"</code><br>
|
||||||
|
<code>glium = "0.34"</code>
|
||||||
|
</td>
|
||||||
|
<td align="center">-</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<code>0.0.1</code>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<code>hui-glium = "0.0.1"</code><br>
|
||||||
|
<code>glium = "0.34"</code>
|
||||||
|
</td>
|
||||||
|
<td align="center">-</td>
|
||||||
|
</tr> -->
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2>MSRV</h2>
|
||||||
|
1.75
|
||||||
|
|
23
hui-derive/Cargo.toml
Normal file
23
hui-derive/Cargo.toml
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
[package]
|
||||||
|
name = "hui-derive"
|
||||||
|
description = "Derive macros for hUI"
|
||||||
|
repository = "https://github.com/griffi-gh/hui"
|
||||||
|
readme = "../README.md"
|
||||||
|
authors = ["griffi-gh <prasol258@gmail.com>"]
|
||||||
|
rust-version = "1.75"
|
||||||
|
version = "0.1.0-alpha.5"
|
||||||
|
edition = "2021"
|
||||||
|
license = "GPL-3.0-or-later"
|
||||||
|
publish = true
|
||||||
|
include = [
|
||||||
|
"assets/**/*",
|
||||||
|
"src/**/*.rs",
|
||||||
|
"Cargo.toml",
|
||||||
|
]
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
proc-macro = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
quote = "1.0"
|
||||||
|
syn = "2.0"
|
21
hui-derive/src/lib.rs
Normal file
21
hui-derive/src/lib.rs
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
extern crate proc_macro;
|
||||||
|
|
||||||
|
use proc_macro::TokenStream;
|
||||||
|
use quote::quote;
|
||||||
|
use syn::{parse_macro_input, DeriveInput};
|
||||||
|
|
||||||
|
/// Implements `Signal` trait for the given type
|
||||||
|
#[proc_macro_derive(Signal)]
|
||||||
|
pub fn derive_signal(input: TokenStream) -> TokenStream {
|
||||||
|
let input = parse_macro_input!(input as DeriveInput);
|
||||||
|
let name = input.ident;
|
||||||
|
quote!(impl ::hui::signal::Signal for #name {}).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implements `State` trait for the given type
|
||||||
|
#[proc_macro_derive(State)]
|
||||||
|
pub fn derive_state(input: TokenStream) -> TokenStream {
|
||||||
|
let input = parse_macro_input!(input as DeriveInput);
|
||||||
|
let name = input.ident;
|
||||||
|
quote!(impl ::hui::state::State for #name {}).into()
|
||||||
|
}
|
|
@ -8,10 +8,12 @@ publish = false
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
hui = { path = "../hui" }
|
hui = { path = "../hui" }
|
||||||
hui-glium = { path = "../hui-glium" }
|
hui-glium = { path = "../hui-glium" }
|
||||||
|
hui-winit = { path = "../hui-winit", features = ["winit_29"] }
|
||||||
kubi-logging = { git = "https://github.com/griffi-gh/kubi", rev = "c162893fd" }
|
kubi-logging = { git = "https://github.com/griffi-gh/kubi", rev = "c162893fd" }
|
||||||
glium = "0.34"
|
glium = "0.34"
|
||||||
winit = "0.29"
|
winit = "0.29"
|
||||||
glam = "0.25"
|
glam = "0.27"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
|
image = { version = "0.25", features = ["jpeg", "png"] }
|
||||||
|
|
||||||
#created as a workaround for rust-analyzer dependency cycle (which should be allowed)
|
#created as a workaround for rust-analyzer dependency cycle (which should be allowed)
|
||||||
|
|
BIN
hui-examples/assets/blink/Blink-ynYZ.otf
Normal file
BIN
hui-examples/assets/blink/Blink-ynYZ.otf
Normal file
Binary file not shown.
94
hui-examples/assets/blink/SIL Open Font License.txt
Normal file
94
hui-examples/assets/blink/SIL Open Font License.txt
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
Copyright (c) 2015, Mew Too/Cannot Into Space Fonts (cannotintospacefonts@gmail.com),
|
||||||
|
with Reserved Font Name Blink.
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
2
hui-examples/assets/blink/info.txt
Normal file
2
hui-examples/assets/blink/info.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
license: SIL Open Font License (OFL)
|
||||||
|
link: https://www.fontspace.com/blink-font-f21809
|
BIN
hui-examples/assets/fira/FiraSans-Light.ttf
Normal file
BIN
hui-examples/assets/fira/FiraSans-Light.ttf
Normal file
Binary file not shown.
94
hui-examples/assets/fira/LICENSE
Normal file
94
hui-examples/assets/fira/LICENSE
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
Digitized data copyright (c) 2012-2015, The Mozilla Foundation and Telefonica S.A.
|
||||||
|
with Reserved Font Name < Fira >,
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
BIN
hui-examples/assets/icons/visual-studio-code-icon_32x32.rgba
Normal file
BIN
hui-examples/assets/icons/visual-studio-code-icon_32x32.rgba
Normal file
Binary file not shown.
BIN
hui-examples/assets/ninepatch_button.png
Normal file
BIN
hui-examples/assets/ninepatch_button.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 638 B |
81
hui-examples/boilerplate.rs
Normal file
81
hui-examples/boilerplate.rs
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
use glam::{UVec2, Vec2};
|
||||||
|
use glium::{Surface, backend::glutin::SimpleWindowBuilder};
|
||||||
|
use winit::{
|
||||||
|
event::{Event, WindowEvent},
|
||||||
|
event_loop::{EventLoopBuilder, ControlFlow}
|
||||||
|
};
|
||||||
|
use hui::UiInstance;
|
||||||
|
use hui_glium::GliumUiRenderer;
|
||||||
|
|
||||||
|
/// Generates a `main` function that initializes glium renderer, `UiInstance`, and runs the event loop.
|
||||||
|
macro_rules! ui_main {
|
||||||
|
($name:literal, init: $closure0:expr, run: $closure1:expr) => {
|
||||||
|
fn main() {
|
||||||
|
$crate::boilerplate::ui($closure0, $closure1, $name);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
(init: $closure0:expr, run: $closure1:expr) => {
|
||||||
|
fn main() {
|
||||||
|
$crate::boilerplate::ui($closure0, $closure1, "hUI example");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
($closure: expr) => {
|
||||||
|
fn main() {
|
||||||
|
$crate::boilerplate::ui(|_|(), $closure, "hUI example");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initializes glium renderer, `UiInstance`, and runs the event loop.
|
||||||
|
pub fn ui<T>(
|
||||||
|
mut init: impl FnMut(&mut UiInstance) -> T,
|
||||||
|
mut draw: impl FnMut(&mut UiInstance, Vec2, &mut T),
|
||||||
|
name: &'static str
|
||||||
|
) {
|
||||||
|
kubi_logging::init();
|
||||||
|
|
||||||
|
let event_loop = EventLoopBuilder::new().build().unwrap();
|
||||||
|
let (window, display) = SimpleWindowBuilder::new()
|
||||||
|
.with_title(name)
|
||||||
|
.build(&event_loop);
|
||||||
|
|
||||||
|
let mut hui = UiInstance::new();
|
||||||
|
let mut backend = GliumUiRenderer::new(&display);
|
||||||
|
|
||||||
|
let mut result = init(&mut hui);
|
||||||
|
|
||||||
|
event_loop.run(|event, window_target| {
|
||||||
|
window.request_redraw();
|
||||||
|
window_target.set_control_flow(ControlFlow::Poll);
|
||||||
|
hui_winit::handle_winit_event(&mut hui, &event);
|
||||||
|
match event {
|
||||||
|
Event::WindowEvent { event, .. } => match event {
|
||||||
|
WindowEvent::CloseRequested => {
|
||||||
|
window_target.exit();
|
||||||
|
},
|
||||||
|
WindowEvent::RedrawRequested => {
|
||||||
|
let mut frame = display.draw();
|
||||||
|
frame.clear_color_srgb(0.5, 0.5, 0.5, 1.);
|
||||||
|
|
||||||
|
hui.begin();
|
||||||
|
|
||||||
|
let size = UVec2::from(display.get_framebuffer_dimensions()).as_vec2();
|
||||||
|
draw(&mut hui, size, &mut result);
|
||||||
|
|
||||||
|
hui.end();
|
||||||
|
|
||||||
|
backend.update(&hui);
|
||||||
|
backend.draw(&mut frame, size);
|
||||||
|
|
||||||
|
frame.finish().unwrap();
|
||||||
|
},
|
||||||
|
_ => (),
|
||||||
|
},
|
||||||
|
Event::Suspended => {
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
window_target.exit();
|
||||||
|
},
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}).unwrap();
|
||||||
|
}
|
148
hui-examples/examples/align_test.rs
Normal file
148
hui-examples/examples/align_test.rs
Normal file
|
@ -0,0 +1,148 @@
|
||||||
|
//WARNING: THIS EXAMPLE IS EXTREMELY OUTDATED AND USES DEPRECATED API
|
||||||
|
|
||||||
|
use std::time::Instant;
|
||||||
|
use glam::{UVec2, vec4};
|
||||||
|
use glium::{backend::glutin::SimpleWindowBuilder, Surface};
|
||||||
|
use winit::{
|
||||||
|
event::{Event, WindowEvent},
|
||||||
|
event_loop::{EventLoopBuilder, ControlFlow}
|
||||||
|
};
|
||||||
|
use hui::{
|
||||||
|
element::{
|
||||||
|
container::Container, frame_view::FrameView, progress_bar::ProgressBar, ElementList, UiElement
|
||||||
|
}, frame::RectFrame, layout::{Alignment, Direction, Size}, rect::{Corners, Sides}, UiInstance
|
||||||
|
};
|
||||||
|
use hui_glium::GliumUiRenderer;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
kubi_logging::init();
|
||||||
|
|
||||||
|
let event_loop = EventLoopBuilder::new().build().unwrap();
|
||||||
|
let (_window, display) = SimpleWindowBuilder::new().build(&event_loop);
|
||||||
|
|
||||||
|
let mut hui = UiInstance::new();
|
||||||
|
let mut backend = GliumUiRenderer::new(&display);
|
||||||
|
|
||||||
|
let instant = Instant::now();
|
||||||
|
event_loop.run(|event, window_target| {
|
||||||
|
window_target.set_control_flow(ControlFlow::Poll);
|
||||||
|
match event {
|
||||||
|
Event::WindowEvent { event: WindowEvent::CloseRequested, .. } => {
|
||||||
|
window_target.exit();
|
||||||
|
},
|
||||||
|
Event::AboutToWait => {
|
||||||
|
let mut frame = display.draw();
|
||||||
|
frame.clear_color_srgb(0.5, 0.5, 0.5, 0.);
|
||||||
|
|
||||||
|
let resolution = UVec2::from(display.get_framebuffer_dimensions()).as_vec2();
|
||||||
|
|
||||||
|
hui.begin();
|
||||||
|
|
||||||
|
let z = instant.elapsed().as_secs_f32().sin().powi(2);
|
||||||
|
|
||||||
|
hui.add(Container {
|
||||||
|
gap: 5.,
|
||||||
|
padding: Sides::all(5.),
|
||||||
|
align: (Alignment::Center, Alignment::Begin).into(),
|
||||||
|
size: (Size::Relative(1.), Size::Relative(1.)).into(),
|
||||||
|
children: ElementList(vec![
|
||||||
|
Box::new(ProgressBar {
|
||||||
|
value: 0.5,
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
..Default::default()
|
||||||
|
}, resolution);
|
||||||
|
|
||||||
|
hui.add(Container {
|
||||||
|
gap: 5.,
|
||||||
|
padding: Sides::all(5.),
|
||||||
|
align: (Alignment::Center, Alignment::End).into(),
|
||||||
|
size: (Size::Relative(1.), Size::Relative(1.)).into(),
|
||||||
|
children: ElementList(vec![
|
||||||
|
Box::new(ProgressBar {
|
||||||
|
value: z,
|
||||||
|
// corner_radius: Corners::all(0.25 * ProgressBar::DEFAULT_HEIGHT),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
Box::new(Container {
|
||||||
|
size: (Size::Relative(1.), Size::Auto).into(),
|
||||||
|
align: (Alignment::End, Alignment::Center).into(),
|
||||||
|
padding: Sides::all(5.),
|
||||||
|
gap: 10.,
|
||||||
|
children: ElementList(vec![
|
||||||
|
Box::new(FrameView {
|
||||||
|
size: (Size::Relative(0.5), Size::Absolute(30.)).into(),
|
||||||
|
frame: Box::new(vec4(0.75, 0., 0., 1.)),
|
||||||
|
}),
|
||||||
|
Box::new(FrameView {
|
||||||
|
size: (Size::Relative(z / 2. + 0.5), Size::Absolute(30.)).into(),
|
||||||
|
frame: Box::new(Corners::left_right(
|
||||||
|
vec4(1., 0., 0., 1.),
|
||||||
|
vec4(0., 1., 0., 1.)
|
||||||
|
)),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
Box::new(FrameView {
|
||||||
|
size: (Size::Relative(z / 2. + 0.5), Size::Absolute(30.)).into(),
|
||||||
|
frame: Box::new(vec4(0., 0.75, 0., 1.)),
|
||||||
|
}),
|
||||||
|
Box::new(Container {
|
||||||
|
gap: 5.,
|
||||||
|
padding: Sides::all(5.),
|
||||||
|
background_frame: Box::new(RectFrame::color(vec4(0., 0., 0., 0.5))),
|
||||||
|
direction: Direction::Horizontal,
|
||||||
|
children: {
|
||||||
|
let mut x: Vec<Box<dyn UiElement>> = vec![];
|
||||||
|
for i in 0..10 {
|
||||||
|
x.push(Box::new(FrameView {
|
||||||
|
size: (Size::Absolute(50.), Size::Absolute(50.)).into(),
|
||||||
|
frame: Box::new(if i == 1 {
|
||||||
|
vec4(0.75, 0.75, 0.75, 0.75)
|
||||||
|
} else {
|
||||||
|
vec4(0.5, 0.5, 0.5, 0.75)
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
ElementList(x)
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
Box::new(Container {
|
||||||
|
background_frame: Box::new(RectFrame::color((1., 0., 0.)).with_corner_radius(Corners {
|
||||||
|
top_left: 0.,
|
||||||
|
top_right: 30.,
|
||||||
|
bottom_left: 0.,
|
||||||
|
bottom_right: 0.,
|
||||||
|
})),
|
||||||
|
padding: Sides {
|
||||||
|
top: 10.,
|
||||||
|
bottom: 20.,
|
||||||
|
left: 30.,
|
||||||
|
right: 40.,
|
||||||
|
},
|
||||||
|
children: ElementList(vec![
|
||||||
|
Box::new(FrameView {
|
||||||
|
size: (Size::Absolute(50.), Size::Absolute(50.)).into(),
|
||||||
|
frame: Box::new(vec4(1., 1., 1., 0.75)),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
..Default::default()
|
||||||
|
}, resolution);
|
||||||
|
|
||||||
|
hui.end();
|
||||||
|
|
||||||
|
backend.update(&hui);
|
||||||
|
backend.draw(&mut frame, resolution);
|
||||||
|
|
||||||
|
frame.finish().unwrap();
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}).unwrap();
|
||||||
|
}
|
|
@ -1,111 +1,84 @@
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use glam::{UVec2, vec4};
|
|
||||||
use glium::{backend::glutin::SimpleWindowBuilder, Surface};
|
|
||||||
use winit::{
|
|
||||||
event::{Event, WindowEvent},
|
|
||||||
event_loop::{EventLoopBuilder, ControlFlow}
|
|
||||||
};
|
|
||||||
use hui::{
|
use hui::{
|
||||||
UiInstance,
|
color, element::{
|
||||||
element::{
|
container::Container,
|
||||||
progress_bar::ProgressBar,
|
progress_bar::ProgressBar,
|
||||||
container::{Container, Sides, Alignment},
|
text::Text,
|
||||||
text::Text
|
UiElementExt,
|
||||||
},
|
}, frame::RectFrame, rect_frame, layout::{Alignment, Direction}, size
|
||||||
UiSize,
|
|
||||||
elements,
|
|
||||||
};
|
};
|
||||||
use hui_glium::GliumUiRenderer;
|
|
||||||
|
|
||||||
fn main() {
|
#[path = "../boilerplate.rs"]
|
||||||
kubi_logging::init();
|
#[macro_use]
|
||||||
|
mod boilerplate;
|
||||||
|
|
||||||
let event_loop = EventLoopBuilder::new().build().unwrap();
|
ui_main!{
|
||||||
let (window, display) = SimpleWindowBuilder::new().build(&event_loop);
|
"Mom downloader 2000",
|
||||||
window.set_title("Mom Downloader 2000");
|
init: |ui| {
|
||||||
|
let font_handle = ui.add_font(include_bytes!("../assets/roboto/Roboto-Regular.ttf"));
|
||||||
let mut hui = UiInstance::new();
|
ui.push_font(font_handle);
|
||||||
let mut backend = GliumUiRenderer::new(&display);
|
Instant::now()
|
||||||
|
|
||||||
let font_handle = hui.add_font_from_bytes(include_bytes!("../assets/roboto/Roboto-Regular.ttf"));
|
|
||||||
|
|
||||||
let instant = Instant::now();
|
|
||||||
|
|
||||||
event_loop.run(|event, window_target| {
|
|
||||||
window_target.set_control_flow(ControlFlow::Poll);
|
|
||||||
match event {
|
|
||||||
Event::WindowEvent { event: WindowEvent::CloseRequested, .. } => {
|
|
||||||
window_target.exit();
|
|
||||||
},
|
},
|
||||||
Event::AboutToWait => {
|
run: |ui, max_size, instant| {
|
||||||
let mut frame = display.draw();
|
|
||||||
frame.clear_color_srgb(0., 0., 0., 1.);
|
|
||||||
|
|
||||||
let resolution = UVec2::from(display.get_framebuffer_dimensions()).as_vec2();
|
|
||||||
|
|
||||||
hui.begin();
|
|
||||||
|
|
||||||
let mom_ratio = (instant.elapsed().as_secs_f32() / 60.).powf(0.5);
|
let mom_ratio = (instant.elapsed().as_secs_f32() / 60.).powf(0.5);
|
||||||
|
|
||||||
hui.add(Container {
|
Container::default()
|
||||||
align: (Alignment::Center, Alignment::Center),
|
.with_align(Alignment::Center)
|
||||||
size: (UiSize::Percentage(1.), UiSize::Percentage(1.)),
|
.with_size(size!(100%))
|
||||||
background: Some(vec4(0.1, 0.1, 0.1, 1.)),
|
.with_background((0.1, 0.1, 0.1))
|
||||||
elements: vec![Box::new(Container {
|
.with_children(|ui| {
|
||||||
gap: 5.,
|
Container::default()
|
||||||
padding: Sides::all(10.),
|
.with_gap(5.)
|
||||||
align: (Alignment::Begin, Alignment::Begin),
|
.with_padding(10.)
|
||||||
size: (UiSize::Pixels(450.), UiSize::Auto),
|
.with_size(size!(450, auto))
|
||||||
background: Some(vec4(0.2, 0.2, 0.5, 1.)),
|
.with_background(rect_frame! {
|
||||||
elements: elements(|el| {
|
color: (0.2, 0.2, 0.5),
|
||||||
if instant.elapsed().as_secs_f32() < 5. {
|
corner_radius: 8.
|
||||||
el.add(Text {
|
|
||||||
text: "Downloading your mom...".into(),
|
|
||||||
font: font_handle,
|
|
||||||
text_size: 32,
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
el.add(ProgressBar {
|
|
||||||
value: mom_ratio,
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
el.add(Text {
|
|
||||||
text: format!("{:.2}% ({:.1} GB)", mom_ratio * 100., mom_ratio * 10000.).into(),
|
|
||||||
font: font_handle,
|
|
||||||
text_size: 24,
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
} else if instant.elapsed().as_secs() < 10 {
|
|
||||||
el.add(Text {
|
|
||||||
text: "Error 413 Request Entity Too Large".into(),
|
|
||||||
font: font_handle,
|
|
||||||
color: vec4(1., 0.125, 0.125, 1.),
|
|
||||||
text_size: 26,
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
el.add(Text {
|
|
||||||
text: format!("Exiting in {}...", 10 - instant.elapsed().as_secs()).into(),
|
|
||||||
font: font_handle,
|
|
||||||
text_size: 24,
|
|
||||||
..Default::default()
|
|
||||||
})
|
})
|
||||||
|
.with_children(|ui| {
|
||||||
|
if instant.elapsed().as_secs_f32() < 5. {
|
||||||
|
Text::default()
|
||||||
|
.with_text("Downloading your mom...")
|
||||||
|
.with_text_size(24)
|
||||||
|
.add_child(ui);
|
||||||
|
ProgressBar::default()
|
||||||
|
.with_value(mom_ratio)
|
||||||
|
.with_background(rect_frame! {
|
||||||
|
color: color::BLACK,
|
||||||
|
corner_radius: 0.125 * ProgressBar::DEFAULT_HEIGHT
|
||||||
|
})
|
||||||
|
.with_foreground(rect_frame! {
|
||||||
|
color: color::BLUE,
|
||||||
|
corner_radius: 0.125 * ProgressBar::DEFAULT_HEIGHT
|
||||||
|
})
|
||||||
|
.add_child(ui);
|
||||||
|
Container::default()
|
||||||
|
.with_direction(Direction::Horizontal)
|
||||||
|
.with_align((Alignment::End, Alignment::Center))
|
||||||
|
.with_size(size!(100%, auto))
|
||||||
|
.with_children(|ui| {
|
||||||
|
Text::default()
|
||||||
|
.with_text(format!("{:.2}% ({:.1} GB)", mom_ratio * 100., mom_ratio * 10000.))
|
||||||
|
.with_text_size(16)
|
||||||
|
.add_child(ui);
|
||||||
|
})
|
||||||
|
.add_child(ui);
|
||||||
|
} else if instant.elapsed().as_secs() < 10 {
|
||||||
|
Text::default()
|
||||||
|
.with_text("Error 413: Request Entity Too Large")
|
||||||
|
.with_color((1., 0.125, 0.125, 1.))
|
||||||
|
.with_text_size(20)
|
||||||
|
.add_child(ui);
|
||||||
|
Text::default()
|
||||||
|
.with_text(format!("Exiting in {}...", 10 - instant.elapsed().as_secs()))
|
||||||
|
.with_text_size(16)
|
||||||
|
.add_child(ui);
|
||||||
} else {
|
} else {
|
||||||
window_target.exit();
|
std::process::exit(0);
|
||||||
}
|
}
|
||||||
}),
|
})
|
||||||
..Default::default()
|
.add_child(ui);
|
||||||
})],
|
})
|
||||||
..Default::default()
|
.add_root(ui, max_size)
|
||||||
}, resolution);
|
|
||||||
|
|
||||||
hui.end();
|
|
||||||
|
|
||||||
backend.update(&hui);
|
|
||||||
backend.draw(&mut frame, resolution);
|
|
||||||
|
|
||||||
frame.finish().unwrap();
|
|
||||||
}
|
}
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
}).unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,90 +0,0 @@
|
||||||
use std::time::Instant;
|
|
||||||
use glam::{Vec2, IVec2, UVec2};
|
|
||||||
use glium::{backend::glutin::SimpleWindowBuilder, Surface};
|
|
||||||
use winit::{
|
|
||||||
event::{Event, WindowEvent},
|
|
||||||
event_loop::{EventLoopBuilder, ControlFlow}
|
|
||||||
};
|
|
||||||
use hui::{
|
|
||||||
UiInstance,
|
|
||||||
element::{
|
|
||||||
UiElement,
|
|
||||||
progress_bar::ProgressBar,
|
|
||||||
container::{Container, Sides}
|
|
||||||
},
|
|
||||||
UiSize
|
|
||||||
};
|
|
||||||
use hui_glium::GliumUiRenderer;
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
kubi_logging::init();
|
|
||||||
|
|
||||||
let event_loop = EventLoopBuilder::new().build().unwrap();
|
|
||||||
let (window, display) = SimpleWindowBuilder::new().build(&event_loop);
|
|
||||||
|
|
||||||
let mut hui = UiInstance::new();
|
|
||||||
let mut backend = GliumUiRenderer::new(&display);
|
|
||||||
|
|
||||||
let instant = Instant::now();
|
|
||||||
let mut pcnt = 0;
|
|
||||||
event_loop.run(|event, window_target| {
|
|
||||||
window_target.set_control_flow(ControlFlow::Poll);
|
|
||||||
match event {
|
|
||||||
Event::WindowEvent { event: WindowEvent::CloseRequested, .. } => {
|
|
||||||
window_target.exit();
|
|
||||||
},
|
|
||||||
Event::AboutToWait => {
|
|
||||||
let mut frame = display.draw();
|
|
||||||
frame.clear_color_srgb(0.5, 0.5, 0.5, 0.);
|
|
||||||
|
|
||||||
let resolution = UVec2::from(display.get_framebuffer_dimensions()).as_vec2();
|
|
||||||
|
|
||||||
hui.begin();
|
|
||||||
|
|
||||||
hui.add(Container {
|
|
||||||
gap: 5.,
|
|
||||||
padding: Sides::all(5.),
|
|
||||||
elements: vec![
|
|
||||||
Box::new(ProgressBar {
|
|
||||||
value: 0.5,
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
Box::new(ProgressBar {
|
|
||||||
value: instant.elapsed().as_secs_f32().sin().powi(2),
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
Box::new(Container {
|
|
||||||
gap: 1.,
|
|
||||||
elements: {
|
|
||||||
let mut elements: Vec<Box<dyn UiElement>> = vec![];
|
|
||||||
let cnt = instant.elapsed().as_secs() * 10000;
|
|
||||||
if pcnt != cnt {
|
|
||||||
log::info!("{cnt}");
|
|
||||||
pcnt = cnt;
|
|
||||||
}
|
|
||||||
for i in 0..cnt {
|
|
||||||
elements.push(Box::new(ProgressBar {
|
|
||||||
value: (instant.elapsed().as_secs_f32() + (i as f32 / 10.)).sin().powi(2),
|
|
||||||
size: (UiSize::Auto, UiSize::Pixels(5.)),
|
|
||||||
..Default::default()
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
elements
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
],
|
|
||||||
..Default::default()
|
|
||||||
}, resolution);
|
|
||||||
|
|
||||||
hui.end();
|
|
||||||
|
|
||||||
backend.update(&hui);
|
|
||||||
backend.draw(&mut frame, resolution);
|
|
||||||
|
|
||||||
frame.finish().unwrap();
|
|
||||||
}
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
}).unwrap();
|
|
||||||
}
|
|
|
@ -1,146 +0,0 @@
|
||||||
use std::time::Instant;
|
|
||||||
use glam::{UVec2, vec4};
|
|
||||||
use glium::{backend::glutin::SimpleWindowBuilder, Surface};
|
|
||||||
use winit::{
|
|
||||||
event::{Event, WindowEvent},
|
|
||||||
event_loop::{EventLoopBuilder, ControlFlow}
|
|
||||||
};
|
|
||||||
use hui::{
|
|
||||||
UiInstance,
|
|
||||||
element::{
|
|
||||||
UiElement,
|
|
||||||
progress_bar::ProgressBar,
|
|
||||||
container::{Container, Sides, Alignment},
|
|
||||||
rect::Rect
|
|
||||||
},
|
|
||||||
interaction::IntoInteractable,
|
|
||||||
UiSize,
|
|
||||||
UiDirection, IfModified,
|
|
||||||
};
|
|
||||||
use hui_glium::GliumUiRenderer;
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
kubi_logging::init();
|
|
||||||
|
|
||||||
let event_loop = EventLoopBuilder::new().build().unwrap();
|
|
||||||
let (window, display) = SimpleWindowBuilder::new().build(&event_loop);
|
|
||||||
|
|
||||||
let mut hui = UiInstance::new();
|
|
||||||
let mut backend = GliumUiRenderer::new(&display);
|
|
||||||
|
|
||||||
let instant = Instant::now();
|
|
||||||
event_loop.run(|event, window_target| {
|
|
||||||
window_target.set_control_flow(ControlFlow::Poll);
|
|
||||||
match event {
|
|
||||||
Event::WindowEvent { event: WindowEvent::CloseRequested, .. } => {
|
|
||||||
window_target.exit();
|
|
||||||
},
|
|
||||||
Event::AboutToWait => {
|
|
||||||
let mut frame = display.draw();
|
|
||||||
frame.clear_color_srgb(0.5, 0.5, 0.5, 0.);
|
|
||||||
|
|
||||||
let resolution = UVec2::from(display.get_framebuffer_dimensions()).as_vec2();
|
|
||||||
|
|
||||||
hui.begin();
|
|
||||||
|
|
||||||
let z = instant.elapsed().as_secs_f32().sin().powi(2);
|
|
||||||
|
|
||||||
hui.add(Container {
|
|
||||||
gap: 5.,
|
|
||||||
padding: Sides::all(5.),
|
|
||||||
align: (Alignment::Begin, Alignment::Center),
|
|
||||||
size: (UiSize::Percentage(1.), UiSize::Percentage(1.)),
|
|
||||||
elements: vec![
|
|
||||||
Box::new(ProgressBar {
|
|
||||||
value: 0.5,
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
..Default::default()
|
|
||||||
}, resolution);
|
|
||||||
|
|
||||||
hui.add(Container {
|
|
||||||
gap: 5.,
|
|
||||||
padding: Sides::all(5.),
|
|
||||||
align: (Alignment::End, Alignment::Center),
|
|
||||||
size: (UiSize::Percentage(1.), UiSize::Percentage(1.)),
|
|
||||||
elements: vec![
|
|
||||||
Box::new(ProgressBar {
|
|
||||||
value: z,
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
Box::new(Container {
|
|
||||||
size: (UiSize::Percentage(1.), UiSize::Auto),
|
|
||||||
align: (Alignment::Center, Alignment::End),
|
|
||||||
padding: Sides::all(5.),
|
|
||||||
gap: 10.,
|
|
||||||
elements: vec![
|
|
||||||
Box::new(Rect {
|
|
||||||
size: (UiSize::Percentage(0.5), UiSize::Pixels(30.)),
|
|
||||||
color: Some(vec4(0.75, 0., 0., 1.))
|
|
||||||
}),
|
|
||||||
Box::new(Rect {
|
|
||||||
size: (UiSize::Percentage(z / 2. + 0.5), UiSize::Pixels(30.)),
|
|
||||||
color: Some(vec4(0., 0.75, 0., 1.))
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
Box::new(Rect {
|
|
||||||
size: (UiSize::Percentage(z / 2. + 0.5), UiSize::Pixels(30.)),
|
|
||||||
color: Some(vec4(0., 0.75, 0., 1.))
|
|
||||||
}),
|
|
||||||
Box::new(Container {
|
|
||||||
gap: 5.,
|
|
||||||
padding: Sides::all(5.),
|
|
||||||
background: Some(vec4(0., 0., 0., 0.5)),
|
|
||||||
direction: UiDirection::Horizontal,
|
|
||||||
elements: {
|
|
||||||
let mut x: Vec<Box<dyn UiElement>> = vec![];
|
|
||||||
for i in 0..10 {
|
|
||||||
x.push(Box::new(Rect {
|
|
||||||
size: (UiSize::Pixels(50.), UiSize::Pixels(50.)),
|
|
||||||
color: if i == 1 {
|
|
||||||
Some(vec4(0.75, 0.75, 0.75, 0.75))
|
|
||||||
} else {
|
|
||||||
Some(vec4(0.5, 0.5, 0.5, 0.75))
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
x
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
Box::new(Container {
|
|
||||||
background: Some(vec4(1., 0., 0., 1.)),
|
|
||||||
padding: Sides {
|
|
||||||
top: 10.,
|
|
||||||
bottom: 20.,
|
|
||||||
left: 30.,
|
|
||||||
right: 40.,
|
|
||||||
},
|
|
||||||
elements: vec![
|
|
||||||
Box::new(Rect {
|
|
||||||
size: (UiSize::Pixels(50.), UiSize::Pixels(50.)),
|
|
||||||
color: Some(vec4(1., 1., 1., 0.75))
|
|
||||||
}.into_interactable().on_click(|| {
|
|
||||||
println!("clicked");
|
|
||||||
}))
|
|
||||||
],
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
],
|
|
||||||
..Default::default()
|
|
||||||
}, resolution);
|
|
||||||
|
|
||||||
hui.end();
|
|
||||||
|
|
||||||
backend.update(&hui);
|
|
||||||
backend.draw(&mut frame, resolution);
|
|
||||||
|
|
||||||
frame.finish().unwrap();
|
|
||||||
}
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
}).unwrap();
|
|
||||||
}
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
//WARNING: THIS EXAMPLE IS EXTREMELY OUTDATED AND USES DEPRECATED API
|
||||||
|
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use glam::{UVec2, vec4};
|
use glam::{UVec2, vec4};
|
||||||
use glium::{backend::glutin::SimpleWindowBuilder, Surface};
|
use glium::{backend::glutin::SimpleWindowBuilder, Surface};
|
||||||
|
@ -6,16 +8,18 @@ use winit::{
|
||||||
event_loop::{EventLoopBuilder, ControlFlow}
|
event_loop::{EventLoopBuilder, ControlFlow}
|
||||||
};
|
};
|
||||||
use hui::{
|
use hui::{
|
||||||
UiInstance,
|
|
||||||
element::{
|
element::{
|
||||||
container::Container,
|
container::Container, frame_view::FrameView, spacer::Spacer, text::Text, ElementList
|
||||||
text::Text, rect::Rect, spacer::Spacer
|
}, frame::RectFrame, layout::Size, UiInstance
|
||||||
},
|
|
||||||
UiSize,
|
|
||||||
elements,
|
|
||||||
};
|
};
|
||||||
use hui_glium::GliumUiRenderer;
|
use hui_glium::GliumUiRenderer;
|
||||||
|
|
||||||
|
fn elements(mut f: impl FnMut(&mut Vec<Box<dyn hui::element::UiElement>>)) -> ElementList {
|
||||||
|
let mut e = vec![];
|
||||||
|
f(&mut e);
|
||||||
|
ElementList(e)
|
||||||
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
kubi_logging::init();
|
kubi_logging::init();
|
||||||
|
|
||||||
|
@ -26,7 +30,7 @@ fn main() {
|
||||||
let mut hui = UiInstance::new();
|
let mut hui = UiInstance::new();
|
||||||
let mut backend = GliumUiRenderer::new(&display);
|
let mut backend = GliumUiRenderer::new(&display);
|
||||||
|
|
||||||
let font_handle = hui.add_font_from_bytes(include_bytes!("../assets/roboto/Roboto-Regular.ttf"));
|
let font_handle = hui.add_font(include_bytes!("../assets/roboto/Roboto-Regular.ttf"));
|
||||||
let instant = Instant::now();
|
let instant = Instant::now();
|
||||||
|
|
||||||
event_loop.run(|event, window_target| {
|
event_loop.run(|event, window_target| {
|
||||||
|
@ -44,59 +48,59 @@ fn main() {
|
||||||
hui.begin();
|
hui.begin();
|
||||||
|
|
||||||
hui.add(Container {
|
hui.add(Container {
|
||||||
size: (UiSize::Percentage(1.), UiSize::Percentage(1.)),
|
size: (Size::Relative(1.), Size::Relative(1.)).into(),
|
||||||
background: Some(vec4(0.1, 0.1, 0.1, 1.)),
|
background_frame: Box::new(RectFrame::color((0.1, 0.1, 0.1, 1.))),
|
||||||
elements: elements(|elem| {
|
children: elements(|elem| {
|
||||||
elem.add(Text {
|
elem.push(Box::new(Text {
|
||||||
text: "THIS LINE SHOULD BE SHARP!".into(),
|
text: "THIS LINE SHOULD BE SHARP!".into(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
}));
|
||||||
elem.add(Text {
|
elem.push(Box::new(Text {
|
||||||
text: "THIS LINE SHOULD BE SHARP!".into(),
|
text: "THIS LINE SHOULD BE SHARP!".into(),
|
||||||
text_size: 32,
|
text_size: 32,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
}));
|
||||||
elem.add(Text {
|
elem.push(Box::new(Text {
|
||||||
text: "All lines except 3 and 6 below will be blurry:".into(),
|
text: "All lines except 3 and 6 below will be blurry:".into(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
}));
|
||||||
for size in [9, 12, 16, 18, 24, 32] {
|
for size in [9, 12, 16, 18, 24, 32] {
|
||||||
elem.add(Text {
|
elem.push(Box::new(Text {
|
||||||
text: "Testing default font, Proggy Tiny".into(),
|
text: "Testing default font, Proggy Tiny".into(),
|
||||||
text_size: size,
|
text_size: size,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
elem.add(Rect {
|
elem.push(Box::new(FrameView {
|
||||||
size: (UiSize::Percentage(1.), UiSize::Pixels(10.)),
|
size: (Size::Relative(1.), Size::Absolute(10.)).into(),
|
||||||
color: Some(vec4(0., 0., 1., 1.)),
|
frame: Box::new(vec4(0., 0., 1., 1.)),
|
||||||
});
|
}));
|
||||||
elem.add(Rect {
|
elem.push(Box::new(FrameView {
|
||||||
size: (UiSize::Percentage(1.), UiSize::Pixels(10.)),
|
size: (Size::Relative(1.), Size::Absolute(10.)).into(),
|
||||||
color: Some(vec4(1., 1., 0., 1.)),
|
frame: Box::new(vec4(1., 1., 0., 1.)),
|
||||||
});
|
}));
|
||||||
elem.add(Text {
|
elem.push(Box::new(Text {
|
||||||
text: "Hello, world!\nżółty liść. życie nie ma sensu i wszyscy zginemy;\nтест кирилиці їїїїїїїїїїї\njapanese text: テスト".into(),
|
text: "Hello, world!\nżółty liść. życie nie ma sensu i wszyscy zginemy;\nтест кирилиці їїїїїїїїїїї\njapanese text: テスト".into(),
|
||||||
font: font_handle,
|
font: Some(font_handle),
|
||||||
text_size: 32,
|
text_size: 32,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
}));
|
||||||
if instant.elapsed().as_secs() & 1 != 0 {
|
if instant.elapsed().as_secs() & 1 != 0 {
|
||||||
elem.add(Rect {
|
elem.push(Box::new(FrameView {
|
||||||
size: (UiSize::Percentage(1.), UiSize::Pixels(10.)),
|
size: (Size::Relative(1.), Size::Absolute(10.)).into(),
|
||||||
color: Some(vec4(1., 0., 0., 1.)),
|
frame: Box::new(vec4(1., 0., 0., 1.)),
|
||||||
});
|
}));
|
||||||
elem.add(Rect {
|
elem.push(Box::new(FrameView {
|
||||||
size: (UiSize::Percentage(1.), UiSize::Pixels(10.)),
|
size: (Size::Relative(1.), Size::Absolute(10.)).into(),
|
||||||
color: Some(vec4(0., 0., 0., 1.)),
|
frame: Box::new(vec4(0., 0., 0., 1.)),
|
||||||
});
|
}));
|
||||||
elem.add(Spacer(100.));
|
elem.push(Box::new(Spacer(100.)));
|
||||||
elem.add(Text {
|
elem.push(Box::new(Text {
|
||||||
text: "FLAG SHOULD NOT OVERLAP WITH TEXT".into(),
|
text: "FLAG SHOULD NOT OVERLAP WITH TEXT".into(),
|
||||||
text_size: 64,
|
text_size: 64,
|
||||||
color: vec4(1., 0., 1., 1.),
|
color: vec4(1., 0., 1., 1.),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
|
43
hui-examples/examples/ui_test.rs
Normal file
43
hui-examples/examples/ui_test.rs
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
use hui::{
|
||||||
|
color, size, rect_frame,
|
||||||
|
element::{container::Container, text::Text, UiElementExt},
|
||||||
|
frame::RectFrame,
|
||||||
|
layout::Alignment,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[path = "../boilerplate.rs"]
|
||||||
|
#[macro_use]
|
||||||
|
mod boilerplate;
|
||||||
|
|
||||||
|
ui_main!(|ui, size, _| {
|
||||||
|
Container::default()
|
||||||
|
.with_size(size!(100%, 50%))
|
||||||
|
.with_align(Alignment::Center)
|
||||||
|
.with_padding(5.)
|
||||||
|
.with_gap(10.)
|
||||||
|
.with_background(rect_frame! {
|
||||||
|
color: (0.5, 0.5, 0.5, 1.),
|
||||||
|
corner_radius: 10.,
|
||||||
|
})
|
||||||
|
.with_children(|ui| {
|
||||||
|
Text::default()
|
||||||
|
.with_text("Hello, world")
|
||||||
|
.with_text_size(100)
|
||||||
|
.with_color(color::BLACK)
|
||||||
|
.add_child(ui);
|
||||||
|
Container::default()
|
||||||
|
.with_padding((10., 20.))
|
||||||
|
.with_background(rect_frame! {
|
||||||
|
color: color::DARK_RED,
|
||||||
|
corner_radius: (2.5, 30., 2.5, 2.5),
|
||||||
|
})
|
||||||
|
.with_children(|ui| {
|
||||||
|
Text::default()
|
||||||
|
.with_text("Lorem ipsum dolor sit amet, consectetur adipiscing elit.")
|
||||||
|
.with_text_size(24)
|
||||||
|
.add_child(ui);
|
||||||
|
})
|
||||||
|
.add_child(ui);
|
||||||
|
})
|
||||||
|
.add_root(ui, size);
|
||||||
|
});
|
133
hui-examples/examples/ui_test_2_loading.rs
Normal file
133
hui-examples/examples/ui_test_2_loading.rs
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
use glam::vec4;
|
||||||
|
use hui::{
|
||||||
|
size, rect_frame,
|
||||||
|
color,
|
||||||
|
element::{
|
||||||
|
container::Container,
|
||||||
|
progress_bar::ProgressBar,
|
||||||
|
text::Text,
|
||||||
|
UiElementExt
|
||||||
|
},
|
||||||
|
frame::RectFrame,
|
||||||
|
layout::Alignment,
|
||||||
|
rect::Corners,
|
||||||
|
text::FontHandle
|
||||||
|
};
|
||||||
|
|
||||||
|
#[path = "../boilerplate.rs"]
|
||||||
|
#[macro_use]
|
||||||
|
mod boilerplate;
|
||||||
|
|
||||||
|
ui_main!(
|
||||||
|
"hUI: Loading screen demo",
|
||||||
|
init: |ui| {
|
||||||
|
let font = ui.add_font(include_bytes!("../assets/blink/Blink-ynYZ.otf"));
|
||||||
|
ui.push_font(font);
|
||||||
|
(std::time::Instant::now(),)
|
||||||
|
},
|
||||||
|
run: |ui, size, (instant,)| {
|
||||||
|
// Background color (gradient)
|
||||||
|
Container::default()
|
||||||
|
.with_size(size!(100%))
|
||||||
|
.with_background(Corners {
|
||||||
|
top_left: vec4(0.2, 0.2, 0.3, 1.),
|
||||||
|
top_right: vec4(0.3, 0.3, 0.4, 1.),
|
||||||
|
bottom_left: vec4(0.2, 0.3, 0.2, 1.),
|
||||||
|
bottom_right: vec4(0.5, 0.4, 0.4, 1.),
|
||||||
|
})
|
||||||
|
.add_root(ui, size);
|
||||||
|
|
||||||
|
// Loading text in the bottom right corner
|
||||||
|
Container::default()
|
||||||
|
.with_size(size!(100%))
|
||||||
|
.with_align(Alignment::End)
|
||||||
|
.with_padding(20.)
|
||||||
|
.with_children(|ui| {
|
||||||
|
Container::default()
|
||||||
|
.with_padding((10., 15.))
|
||||||
|
.with_background(rect_frame! {
|
||||||
|
color: (0., 0., 0., 0.5),
|
||||||
|
corner_radius: 8.,
|
||||||
|
})
|
||||||
|
.with_children(|ui| {
|
||||||
|
let flash = 1. - 0.5 * (4. * instant.elapsed().as_secs_f32()).sin().powi(2);
|
||||||
|
Text::default()
|
||||||
|
.with_text("Loading...")
|
||||||
|
.with_color((1., 1., 1., flash))
|
||||||
|
.with_text_size(24)
|
||||||
|
.add_child(ui);
|
||||||
|
})
|
||||||
|
.add_child(ui);
|
||||||
|
})
|
||||||
|
.add_root(ui, size);
|
||||||
|
|
||||||
|
// Did you know? box in the center
|
||||||
|
Container::default()
|
||||||
|
.with_size(size!(100%))
|
||||||
|
.with_align(Alignment::Center)
|
||||||
|
.with_children(|ui| {
|
||||||
|
Container::default()
|
||||||
|
.with_align((Alignment::Center, Alignment::Begin))
|
||||||
|
.with_padding(15.)
|
||||||
|
.with_gap(10.)
|
||||||
|
.with_background(rect_frame! {
|
||||||
|
color: (0., 0., 0., 0.5),
|
||||||
|
corner_radius: 8.,
|
||||||
|
})
|
||||||
|
.with_children(|ui| {
|
||||||
|
Text::default()
|
||||||
|
.with_text("Did you know?")
|
||||||
|
.with_text_size(18)
|
||||||
|
.add_child(ui);
|
||||||
|
Text::default()
|
||||||
|
.with_text("You can die by jumping into the spike pit! :D\nCheck out the tutorial section for more tips.")
|
||||||
|
.with_text_size(24)
|
||||||
|
.with_font(FontHandle::default())
|
||||||
|
.add_child(ui);
|
||||||
|
})
|
||||||
|
.add_child(ui);
|
||||||
|
})
|
||||||
|
.add_root(ui, size);
|
||||||
|
|
||||||
|
// Progress bar at the bottom
|
||||||
|
Container::default()
|
||||||
|
.with_size(size!(100%))
|
||||||
|
.with_align((Alignment::Center, Alignment::End))
|
||||||
|
.with_children(|ui| {
|
||||||
|
ProgressBar::default()
|
||||||
|
.with_value((instant.elapsed().as_secs_f32() * 0.1) % 1.)
|
||||||
|
.with_size(size!(100%, 5))
|
||||||
|
.with_background((0., 0., 0., 0.5))
|
||||||
|
.with_foreground(color::DARK_GREEN)
|
||||||
|
.add_child(ui);
|
||||||
|
})
|
||||||
|
.add_root(ui, size);
|
||||||
|
|
||||||
|
// Player XP and level (mock) in the top right corner
|
||||||
|
Container::default()
|
||||||
|
.with_size(size!(100%))
|
||||||
|
.with_align((Alignment::End, Alignment::Begin))
|
||||||
|
.with_padding(20.)
|
||||||
|
.with_children(|ui| {
|
||||||
|
Container::default()
|
||||||
|
.with_padding(10.)
|
||||||
|
.with_background(rect_frame!{
|
||||||
|
color: (0., 0., 0., 0.5),
|
||||||
|
corner_radius: 8.,
|
||||||
|
})
|
||||||
|
.with_children(|ui| {
|
||||||
|
Text::default()
|
||||||
|
.with_text("Level 5")
|
||||||
|
.with_text_size(24)
|
||||||
|
.add_child(ui);
|
||||||
|
Text::default()
|
||||||
|
.with_text("XP: 1234 / 5000")
|
||||||
|
.with_text_size(18)
|
||||||
|
.with_font(FontHandle::default())
|
||||||
|
.add_child(ui);
|
||||||
|
})
|
||||||
|
.add_child(ui);
|
||||||
|
})
|
||||||
|
.add_root(ui, size);
|
||||||
|
}
|
||||||
|
);
|
62
hui-examples/examples/ui_test_3_transform.rs
Normal file
62
hui-examples/examples/ui_test_3_transform.rs
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
use std::f32::consts::PI;
|
||||||
|
|
||||||
|
use glam::{vec4, Vec2};
|
||||||
|
use hui::{
|
||||||
|
element::{
|
||||||
|
container::Container,
|
||||||
|
text::Text,
|
||||||
|
transformer::ElementTransformExt,
|
||||||
|
UiElementExt
|
||||||
|
}, frame::RectFrame, rect_frame, layout::Alignment, rect::Corners, size, text::FontHandle
|
||||||
|
};
|
||||||
|
|
||||||
|
#[path = "../boilerplate.rs"]
|
||||||
|
#[macro_use]
|
||||||
|
mod boilerplate;
|
||||||
|
|
||||||
|
ui_main!(
|
||||||
|
"hUI: Transform API demo",
|
||||||
|
init: |ui| {
|
||||||
|
let font = ui.add_font(include_bytes!("../assets/blink/Blink-ynYZ.otf"));
|
||||||
|
ui.push_font(font);
|
||||||
|
(std::time::Instant::now(),)
|
||||||
|
},
|
||||||
|
run: |ui, size, (instant,)| {
|
||||||
|
let elapsed_sec = instant.elapsed().as_secs_f32();
|
||||||
|
Container::default()
|
||||||
|
.with_background(Corners {
|
||||||
|
top_left: vec4(0.2, 0.2, 0.3, 1.),
|
||||||
|
top_right: vec4(0.3, 0.3, 0.4, 1.),
|
||||||
|
bottom_left: vec4(0.2, 0.3, 0.2, 1.),
|
||||||
|
bottom_right: vec4(0.5, 0.4, 0.4, 1.),
|
||||||
|
})
|
||||||
|
.with_size(size!(100%))
|
||||||
|
.with_align(Alignment::Center)
|
||||||
|
.with_children(|ui| {
|
||||||
|
Container::default()
|
||||||
|
.with_align((Alignment::Center, Alignment::Begin))
|
||||||
|
.with_padding(15.)
|
||||||
|
.with_gap(10.)
|
||||||
|
.with_background(rect_frame! {
|
||||||
|
color: (0., 0., 0., 0.5),
|
||||||
|
corner_radius: 8.
|
||||||
|
})
|
||||||
|
.with_children(|ui| {
|
||||||
|
Text::default()
|
||||||
|
.with_text("Did you know?")
|
||||||
|
.with_text_size(18)
|
||||||
|
.add_child(ui);
|
||||||
|
Text::default()
|
||||||
|
.with_text("You can die by jumping into the spike pit! :D\nCheck out the tutorial section for more tips.")
|
||||||
|
.with_text_size(24)
|
||||||
|
.with_font(FontHandle::default())
|
||||||
|
.add_child(ui);
|
||||||
|
})
|
||||||
|
.transform()
|
||||||
|
.scale(Vec2::splat(elapsed_sec.sin() * 0.1 + 1.))
|
||||||
|
.rotate(elapsed_sec * PI / 4.)
|
||||||
|
.add_child(ui);
|
||||||
|
})
|
||||||
|
.add_root(ui, size);
|
||||||
|
}
|
||||||
|
);
|
42
hui-examples/examples/ui_test_4_wrapping.rs
Normal file
42
hui-examples/examples/ui_test_4_wrapping.rs
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
use std::time::Instant;
|
||||||
|
use hui::{
|
||||||
|
color, element::{
|
||||||
|
container::Container,
|
||||||
|
frame_view::FrameView,
|
||||||
|
UiElementExt
|
||||||
|
}, rect_frame, layout::{Alignment, Direction}, size
|
||||||
|
};
|
||||||
|
|
||||||
|
#[path = "../boilerplate.rs"]
|
||||||
|
#[macro_use]
|
||||||
|
mod boilerplate;
|
||||||
|
|
||||||
|
ui_main!(
|
||||||
|
"hUI: Wrapping demo",
|
||||||
|
init: |_| {
|
||||||
|
Instant::now()
|
||||||
|
},
|
||||||
|
run: |ui, size, instant| {
|
||||||
|
let width_ratio = 0.5 + 0.5 * instant.elapsed().as_secs_f32().sin().powi(2);
|
||||||
|
Container::default()
|
||||||
|
.with_size(size!(width_ratio/, 100%))
|
||||||
|
.with_direction(Direction::Horizontal)
|
||||||
|
.with_align(Alignment::Center)
|
||||||
|
.with_padding(5.)
|
||||||
|
.with_gap(10.)
|
||||||
|
.with_background(color::WHITE)
|
||||||
|
.with_wrap(true)
|
||||||
|
.with_children(|ui| {
|
||||||
|
for i in 0..10 {
|
||||||
|
FrameView::default()
|
||||||
|
.with_size(size!((40 + i * 10)))
|
||||||
|
.with_frame(rect_frame! {
|
||||||
|
color: color::DARK_RED,
|
||||||
|
corner_radius: 8.
|
||||||
|
})
|
||||||
|
.add_child(ui);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.add_root(ui, size);
|
||||||
|
}
|
||||||
|
);
|
91
hui-examples/examples/ui_test_5_input.rs
Normal file
91
hui-examples/examples/ui_test_5_input.rs
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
use hui::{
|
||||||
|
color, size,
|
||||||
|
signal::Signal,
|
||||||
|
draw::TextureFormat,
|
||||||
|
layout::{Alignment, Direction},
|
||||||
|
element::{
|
||||||
|
container::Container,
|
||||||
|
text::Text,
|
||||||
|
image::Image,
|
||||||
|
br::Break,
|
||||||
|
interactable::ElementInteractableExt,
|
||||||
|
UiElementExt,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Signal)]
|
||||||
|
enum CounterSignal {
|
||||||
|
Increment,
|
||||||
|
Decrement,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[path = "../boilerplate.rs"]
|
||||||
|
#[macro_use]
|
||||||
|
mod boilerplate;
|
||||||
|
|
||||||
|
const IMAGE_DATA: &[u8] = include_bytes!("../assets/icons/visual-studio-code-icon_32x32.rgba");
|
||||||
|
|
||||||
|
ui_main!(
|
||||||
|
"hUI: Internal input test",
|
||||||
|
init: |ui| {
|
||||||
|
let image = ui.add_image(TextureFormat::Rgba, IMAGE_DATA, 32);
|
||||||
|
(0, image)
|
||||||
|
},
|
||||||
|
run: |ui, size, (ref mut counter, image)| {
|
||||||
|
Container::default()
|
||||||
|
.with_size(size!(100%))
|
||||||
|
.with_padding(10.)
|
||||||
|
.with_align((Alignment::Center, Alignment::Begin))
|
||||||
|
.with_direction(Direction::Horizontal)
|
||||||
|
.with_gap(5.)
|
||||||
|
.with_background((0.1, 0.1, 0.1))
|
||||||
|
.with_wrap(true)
|
||||||
|
.with_children(|ui| {
|
||||||
|
Text::new("Number of images:")
|
||||||
|
.with_text_size(24)
|
||||||
|
.add_child(ui);
|
||||||
|
Break.add_child(ui);
|
||||||
|
Container::default()
|
||||||
|
.with_padding(10.)
|
||||||
|
.with_background(color::ORANGE)
|
||||||
|
.with_children(|ui| {
|
||||||
|
Text::new("-")
|
||||||
|
.with_text_size(32)
|
||||||
|
.add_child(ui);
|
||||||
|
})
|
||||||
|
.on_click(|| CounterSignal::Decrement)
|
||||||
|
.add_child(ui);
|
||||||
|
Container::default()
|
||||||
|
.with_size(size!(60, auto))
|
||||||
|
.with_align(Alignment::Center)
|
||||||
|
.with_children(|ui| {
|
||||||
|
Text::new(counter.to_string())
|
||||||
|
.with_text_size(64)
|
||||||
|
.add_child(ui);
|
||||||
|
})
|
||||||
|
.add_child(ui);
|
||||||
|
Container::default()
|
||||||
|
.with_padding(10.)
|
||||||
|
.with_background(color::ORANGE)
|
||||||
|
.with_children(|ui| {
|
||||||
|
Text::new("+")
|
||||||
|
.with_text_size(32)
|
||||||
|
.add_child(ui);
|
||||||
|
})
|
||||||
|
.on_click(|| CounterSignal::Increment)
|
||||||
|
.add_child(ui);
|
||||||
|
Break.add_child(ui);
|
||||||
|
for _ in 0..*counter {
|
||||||
|
Image::new(*image)
|
||||||
|
.with_size(size!(48, 48))
|
||||||
|
.add_child(ui);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.add_root(ui, size);
|
||||||
|
|
||||||
|
ui.process_signals(|sig| match sig {
|
||||||
|
CounterSignal::Increment => *counter += 1,
|
||||||
|
CounterSignal::Decrement => *counter -= 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
82
hui-examples/examples/ui_test_6_slider.rs
Normal file
82
hui-examples/examples/ui_test_6_slider.rs
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
use hui::{
|
||||||
|
draw::TextureFormat,
|
||||||
|
element::{
|
||||||
|
br::Break,
|
||||||
|
container::Container,
|
||||||
|
image::Image,
|
||||||
|
slider::Slider,
|
||||||
|
text::Text,
|
||||||
|
UiElementExt,
|
||||||
|
},
|
||||||
|
signal::Signal,
|
||||||
|
layout::{Alignment, Direction},
|
||||||
|
size,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Signal)]
|
||||||
|
enum CounterSignal {
|
||||||
|
ChangeValue(u32)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[path = "../boilerplate.rs"]
|
||||||
|
#[macro_use]
|
||||||
|
mod boilerplate;
|
||||||
|
|
||||||
|
const IMAGE_DATA: &[u8] = include_bytes!("../assets/icons/visual-studio-code-icon_32x32.rgba");
|
||||||
|
|
||||||
|
ui_main!(
|
||||||
|
"hUI: Internal input test",
|
||||||
|
init: |ui| {
|
||||||
|
let image = ui.add_image(TextureFormat::Rgba, IMAGE_DATA, 32);
|
||||||
|
(0, image)
|
||||||
|
},
|
||||||
|
run: |ui, size, (ref mut counter, image)| {
|
||||||
|
Container::default()
|
||||||
|
.with_size(size!(100%))
|
||||||
|
.with_padding(10.)
|
||||||
|
.with_align((Alignment::Center, Alignment::Begin))
|
||||||
|
.with_direction(Direction::Horizontal)
|
||||||
|
.with_gap(5.)
|
||||||
|
.with_background((0.1, 0.1, 0.1))
|
||||||
|
.with_wrap(true)
|
||||||
|
.with_children(|ui| {
|
||||||
|
Text::new(format!("Number of images: {counter}"))
|
||||||
|
.with_text_size(32)
|
||||||
|
.add_child(ui);
|
||||||
|
Break.add_child(ui);
|
||||||
|
Text::new("Absolute tracking slider:")
|
||||||
|
.with_text_size(16)
|
||||||
|
.add_child(ui);
|
||||||
|
Break.add_child(ui);
|
||||||
|
Slider::new(*counter as f32 / 100.)
|
||||||
|
.with_size(size!(66%, 20))
|
||||||
|
.on_change(|x| {
|
||||||
|
CounterSignal::ChangeValue((x * 100.).round() as u32)
|
||||||
|
})
|
||||||
|
.add_child(ui);
|
||||||
|
Break.add_child(ui);
|
||||||
|
Text::new("Relative tracking slider (Experimental):")
|
||||||
|
.with_text_size(16)
|
||||||
|
.add_child(ui);
|
||||||
|
Break.add_child(ui);
|
||||||
|
Slider::new(*counter as f32 / 100.)
|
||||||
|
.with_size(size!(66%, 20))
|
||||||
|
.with_follow_mode(hui::element::slider::SliderFollowMode::Relative)
|
||||||
|
.on_change(|x| {
|
||||||
|
CounterSignal::ChangeValue((x * 100.).round() as u32)
|
||||||
|
})
|
||||||
|
.add_child(ui);
|
||||||
|
Break.add_child(ui);
|
||||||
|
for _ in 0..*counter {
|
||||||
|
Image::new(*image)
|
||||||
|
.with_size(size!(48, 48))
|
||||||
|
.add_child(ui);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.add_root(ui, size);
|
||||||
|
|
||||||
|
ui.process_signals(|sig| match sig {
|
||||||
|
CounterSignal::ChangeValue(v) => *counter = v,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
92
hui-examples/examples/ui_test_7_9patch.rs
Normal file
92
hui-examples/examples/ui_test_7_9patch.rs
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
use glam::vec2;
|
||||||
|
use hui::{
|
||||||
|
color,
|
||||||
|
element::{
|
||||||
|
container::Container,
|
||||||
|
frame_view::FrameView,
|
||||||
|
slider::Slider,
|
||||||
|
text::Text,
|
||||||
|
UiElementExt
|
||||||
|
},
|
||||||
|
frame::nine_patch::{NinePatchAsset, NinePatchFrame},
|
||||||
|
layout::Alignment,
|
||||||
|
rect::Rect,
|
||||||
|
signal::Signal,
|
||||||
|
size,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[path = "../boilerplate.rs"]
|
||||||
|
#[macro_use]
|
||||||
|
mod boilerplate;
|
||||||
|
|
||||||
|
#[derive(Signal)]
|
||||||
|
struct SetValue(f32);
|
||||||
|
|
||||||
|
ui_main!(
|
||||||
|
"hUI: 9-Patch demo",
|
||||||
|
init: |ui| {
|
||||||
|
(
|
||||||
|
NinePatchAsset {
|
||||||
|
image: ui.add_image_file_path("./hui-examples/assets/ninepatch_button.png").unwrap(),
|
||||||
|
size: (190, 49),
|
||||||
|
scalable_region: Rect {
|
||||||
|
position: vec2(8. / 190., 8. / 49.),
|
||||||
|
size: vec2(1. - 16. / 190., 1. - 18. / 49.),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
0.33,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
run: |ui, size, (asset, value)| {
|
||||||
|
Container::default()
|
||||||
|
.with_size(size!(100%))
|
||||||
|
.with_align(Alignment::Center)
|
||||||
|
.with_gap(5.)
|
||||||
|
.with_background(color::WHITE)
|
||||||
|
.with_children(|ui| {
|
||||||
|
Container::default()
|
||||||
|
.with_size(size!(300, 100))
|
||||||
|
.with_background(NinePatchFrame::from_asset(*asset).with_color(color::RED))
|
||||||
|
.with_padding(10.)
|
||||||
|
.with_children(|ui| {
|
||||||
|
Text::new("Hello, world!\nThis is a 9-patch frame used as a background \nfor Container with a Text element.\nIt's scalable and looks great!\nBelow, there are two FillRects with the same \n9-patch frame used as the background.")
|
||||||
|
.with_text_size(16)
|
||||||
|
.add_child(ui);
|
||||||
|
})
|
||||||
|
.add_child(ui);
|
||||||
|
FrameView::default()
|
||||||
|
.with_size(size!(600, 75))
|
||||||
|
.with_frame(NinePatchFrame::from_asset(*asset).with_color(color::GREEN))
|
||||||
|
.add_child(ui);
|
||||||
|
Text::new("This one's fancy:")
|
||||||
|
.with_color(color::BLACK)
|
||||||
|
.with_text_size(32)
|
||||||
|
.add_child(ui);
|
||||||
|
FrameView::default()
|
||||||
|
.with_size(size!(700, 50))
|
||||||
|
.with_frame(NinePatchFrame::from_asset(*asset).with_color((
|
||||||
|
(1., 0., 1.),
|
||||||
|
(0., 1., 1.),
|
||||||
|
(1., 1., 0.),
|
||||||
|
(0., 0., 1.),
|
||||||
|
)))
|
||||||
|
.add_child(ui);
|
||||||
|
Text::new("Slider customized with `NinePatchFrame`s:")
|
||||||
|
.with_color(color::BLACK)
|
||||||
|
.with_text_size(32)
|
||||||
|
.add_child(ui);
|
||||||
|
Slider::new(*value)
|
||||||
|
.with_size(size!(50%, 30))
|
||||||
|
.with_track_height(1.)
|
||||||
|
.with_handle_size((20., 1.))
|
||||||
|
.with_handle(NinePatchFrame::from_asset(*asset).with_color(color::CYAN))
|
||||||
|
.with_track(NinePatchFrame::from_asset(*asset))
|
||||||
|
.with_track_active(NinePatchFrame::from_asset(*asset).with_color(color::SKY_BLUE))
|
||||||
|
.on_change(SetValue)
|
||||||
|
.add_child(ui);
|
||||||
|
})
|
||||||
|
.add_root(ui, size);
|
||||||
|
|
||||||
|
ui.process_signals::<SetValue>(|signal| *value = signal.0);
|
||||||
|
}
|
||||||
|
);
|
126
hui-examples/examples/vscode_layout.rs
Normal file
126
hui-examples/examples/vscode_layout.rs
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
//TODO: finish this demo
|
||||||
|
|
||||||
|
use hui::{
|
||||||
|
color, size,
|
||||||
|
draw::{ImageHandle, TextureFormat},
|
||||||
|
layout::{Alignment, Direction},
|
||||||
|
element::{
|
||||||
|
container::Container,
|
||||||
|
frame_view::FrameView,
|
||||||
|
image::Image,
|
||||||
|
text::Text,
|
||||||
|
UiElementExt
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[path = "../boilerplate.rs"]
|
||||||
|
#[macro_use]
|
||||||
|
mod boilerplate;
|
||||||
|
|
||||||
|
struct Stuff {
|
||||||
|
vscode_icon: ImageHandle,
|
||||||
|
}
|
||||||
|
|
||||||
|
ui_main!(
|
||||||
|
"hUI: vscode demo",
|
||||||
|
init: |ui| {
|
||||||
|
let handle = ui.add_font(include_bytes!("../assets/fira/FiraSans-Light.ttf"));
|
||||||
|
ui.push_font(handle);
|
||||||
|
Stuff {
|
||||||
|
vscode_icon: ui.add_image(TextureFormat::Rgba, include_bytes!("../assets/icons/visual-studio-code-icon_32x32.rgba"), 32),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
run: |ui, size, stuff| {
|
||||||
|
Container::default()
|
||||||
|
.with_size(size!(100%))
|
||||||
|
.with_children(|ui| {
|
||||||
|
Container::default()
|
||||||
|
.with_size(size!(100%, auto))
|
||||||
|
.with_direction(Direction::Horizontal)
|
||||||
|
.with_align((Alignment::Begin, Alignment::Center))
|
||||||
|
.with_padding(5.)
|
||||||
|
.with_gap(15.)
|
||||||
|
.with_background(color::rgb_hex(0x3d3c3e))
|
||||||
|
.with_wrap(true) //XXX: not authentic but great for demostration
|
||||||
|
.with_children(|ui| {
|
||||||
|
Image::new(stuff.vscode_icon)
|
||||||
|
.with_size(size!(auto, 24))
|
||||||
|
.add_child(ui);
|
||||||
|
for item in ["File", "Edit", "Selection", "View", "Go", "Run", "Terminal", "Help"] {
|
||||||
|
Text::new(item)
|
||||||
|
.with_text_size(15)
|
||||||
|
.add_child(ui);
|
||||||
|
}
|
||||||
|
Container::default()
|
||||||
|
.with_size(size!(100%=, 100%))
|
||||||
|
.with_align((Alignment::End, Alignment::Center))
|
||||||
|
.with_children(|ui| {
|
||||||
|
Text::new("- ×")
|
||||||
|
.with_text_size(32)
|
||||||
|
.add_child(ui);
|
||||||
|
})
|
||||||
|
.add_child(ui);
|
||||||
|
})
|
||||||
|
.add_child(ui);
|
||||||
|
FrameView::default()
|
||||||
|
.with_size(size!(100%, 1))
|
||||||
|
.with_frame(color::rgb_hex(0x2d2d30))
|
||||||
|
.add_child(ui);
|
||||||
|
Container::default()
|
||||||
|
.with_size(size!(100%, 100%=))
|
||||||
|
.with_direction(Direction::Horizontal)
|
||||||
|
.with_children(|ui| {
|
||||||
|
// Sidebar:
|
||||||
|
Container::default()
|
||||||
|
.with_size(size!(54, 100%))
|
||||||
|
.with_background(color::rgb_hex(0x343334))
|
||||||
|
.add_child(ui);
|
||||||
|
FrameView::default()
|
||||||
|
.with_size(size!(1, 100%))
|
||||||
|
.with_frame(color::rgb_hex(0x2d2d30))
|
||||||
|
.add_child(ui);
|
||||||
|
|
||||||
|
// Explorer pane:
|
||||||
|
Container::default()
|
||||||
|
.with_size(size!(200, 100%))
|
||||||
|
.with_padding((15., 8.))
|
||||||
|
.with_background(color::rgb_hex(0x262526))
|
||||||
|
.with_children(|ui| {
|
||||||
|
Text::new("EXPLORER")
|
||||||
|
.add_child(ui);
|
||||||
|
})
|
||||||
|
.add_child(ui);
|
||||||
|
|
||||||
|
// "Code" pane
|
||||||
|
Container::default()
|
||||||
|
.with_size(size!(100%=, 100%))
|
||||||
|
.with_background(color::rgb_hex(0x1f1e1f))
|
||||||
|
.add_child(ui);
|
||||||
|
})
|
||||||
|
.add_child(ui);
|
||||||
|
|
||||||
|
//Status bar
|
||||||
|
Container::default()
|
||||||
|
.with_size(size!(100%, auto))
|
||||||
|
.with_background(color::rgb_hex(0x0079cc))
|
||||||
|
.with_direction(Direction::Horizontal)
|
||||||
|
.with_gap(5.)
|
||||||
|
.with_children(|ui| {
|
||||||
|
Container::default()
|
||||||
|
.with_background(color::rgb_hex(0x16815e))
|
||||||
|
.with_padding((10., 2.))
|
||||||
|
.with_children(|ui| {
|
||||||
|
Text::new("><")
|
||||||
|
.with_text_size(13)
|
||||||
|
.add_child(ui);
|
||||||
|
})
|
||||||
|
.add_child(ui);
|
||||||
|
Text::new("master")
|
||||||
|
.with_text_size(15)
|
||||||
|
.add_child(ui);
|
||||||
|
})
|
||||||
|
.add_child(ui);
|
||||||
|
})
|
||||||
|
.add_root(ui, size);
|
||||||
|
}
|
||||||
|
);
|
|
@ -1,9 +1,11 @@
|
||||||
[package]
|
[package]
|
||||||
name = "hui-glium"
|
name = "hui-glium"
|
||||||
description = "Glium backend for hUI"
|
description = "glium render backend for `hui`"
|
||||||
repository = "https://github.com/griffi-gh/hui"
|
repository = "https://github.com/griffi-gh/hui"
|
||||||
|
readme = "../README.md"
|
||||||
authors = ["griffi-gh <prasol258@gmail.com>"]
|
authors = ["griffi-gh <prasol258@gmail.com>"]
|
||||||
version = "0.0.2"
|
version = "0.1.0-alpha.5"
|
||||||
|
rust-version = "1.75"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "GPL-3.0-or-later"
|
license = "GPL-3.0-or-later"
|
||||||
publish = true
|
publish = true
|
||||||
|
@ -14,7 +16,7 @@ include = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hui = { version = "^0.0", path = "../hui", default-features = false }
|
hui = { version = "=0.1.0-alpha.5", path = "../hui", default-features = false }
|
||||||
glium = "0.34"
|
glium = { version = "0.34", default-features = false }
|
||||||
glam = "0.25"
|
glam = "0.27"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#version 300 es
|
#version 150 core
|
||||||
|
|
||||||
precision highp float;
|
precision highp float;
|
||||||
precision highp sampler2D;
|
precision highp sampler2D;
|
||||||
|
@ -6,7 +6,8 @@ precision highp sampler2D;
|
||||||
out vec4 out_color;
|
out vec4 out_color;
|
||||||
in vec4 vtx_color;
|
in vec4 vtx_color;
|
||||||
in vec2 vtx_uv;
|
in vec2 vtx_uv;
|
||||||
|
uniform sampler2D tex;
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
out_color = vtx_color;
|
out_color = texture(tex, vtx_uv) * vtx_color;
|
||||||
}
|
}
|
17
hui-glium/shaders/vertex.150.vert
Normal file
17
hui-glium/shaders/vertex.150.vert
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
#version 150 core
|
||||||
|
|
||||||
|
precision highp float;
|
||||||
|
|
||||||
|
uniform vec2 resolution;
|
||||||
|
in vec2 uv;
|
||||||
|
in vec4 color;
|
||||||
|
in vec2 position;
|
||||||
|
out vec4 vtx_color;
|
||||||
|
out vec2 vtx_uv;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vtx_color = color;
|
||||||
|
vtx_uv = uv;
|
||||||
|
vec2 pos2d = (vec2(2., -2.) * (position / resolution)) + vec2(-1, 1);
|
||||||
|
gl_Position = vec4(pos2d, 0., 1.);
|
||||||
|
}
|
|
@ -1,23 +1,17 @@
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use glam::Vec2;
|
use glam::Vec2;
|
||||||
use glium::{
|
use glium::{
|
||||||
Surface, DrawParameters, Blend,
|
backend::{Context, Facade}, implement_vertex, index::PrimitiveType, texture::{RawImage2d, Texture2d}, uniform, uniforms::{MagnifySamplerFilter, MinifySamplerFilter, Sampler, SamplerBehavior, SamplerWrapFunction}, Api, Blend, DrawParameters, IndexBuffer, Program, Surface, VertexBuffer
|
||||||
Program, VertexBuffer, IndexBuffer,
|
|
||||||
backend::{Facade, Context},
|
|
||||||
texture::{SrgbTexture2d, RawImage2d},
|
|
||||||
index::PrimitiveType,
|
|
||||||
implement_vertex,
|
|
||||||
uniform, uniforms::{Sampler, SamplerBehavior, SamplerWrapFunction},
|
|
||||||
};
|
};
|
||||||
use hui::{
|
use hui::{
|
||||||
UiInstance,
|
draw::{TextureAtlasMeta, UiDrawCall, UiVertex}, UiInstance
|
||||||
draw::{UiDrawPlan, UiVertex, BindTexture},
|
|
||||||
text::FontTextureInfo, IfModified,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const VERTEX_SHADER: &str = include_str!("../shaders/vertex.vert");
|
const VERTEX_SHADER_GLES3: &str = include_str!("../shaders/vertex.es.vert");
|
||||||
const FRAGMENT_SHADER: &str = include_str!("../shaders/fragment.frag");
|
const FRAGMENT_SHADER_GLES3: &str = include_str!("../shaders/fragment.es.frag");
|
||||||
const FRAGMENT_SHADER_TEX: &str = include_str!("../shaders/fragment_tex.frag");
|
|
||||||
|
const VERTEX_SHADER_150: &str = include_str!("../shaders/vertex.150.vert");
|
||||||
|
const FRAGMENT_SHADER_150: &str = include_str!("../shaders/fragment.150.frag");
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
|
@ -48,7 +42,7 @@ struct BufferPair {
|
||||||
|
|
||||||
impl BufferPair {
|
impl BufferPair {
|
||||||
pub fn new<F: Facade>(facade: &F) -> Self {
|
pub fn new<F: Facade>(facade: &F) -> Self {
|
||||||
log::debug!("init ui buffers...");
|
log::debug!("init ui buffers (empty)...");
|
||||||
Self {
|
Self {
|
||||||
vertex_buffer: VertexBuffer::empty_dynamic(facade, 1024).unwrap(),
|
vertex_buffer: VertexBuffer::empty_dynamic(facade, 1024).unwrap(),
|
||||||
index_buffer: IndexBuffer::empty_dynamic(facade, PrimitiveType::TrianglesList, 1024).unwrap(),
|
index_buffer: IndexBuffer::empty_dynamic(facade, PrimitiveType::TrianglesList, 1024).unwrap(),
|
||||||
|
@ -57,6 +51,16 @@ impl BufferPair {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn new_with_data<F: Facade>(facade: &F, vtx: &[Vertex], idx: &[u32]) -> Self {
|
||||||
|
log::debug!("init ui buffers (data)...");
|
||||||
|
Self {
|
||||||
|
vertex_buffer: VertexBuffer::dynamic(facade, vtx).unwrap(),
|
||||||
|
index_buffer: IndexBuffer::dynamic(facade, PrimitiveType::TrianglesList, idx).unwrap(),
|
||||||
|
vertex_count: vtx.len(),
|
||||||
|
index_count: idx.len(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn ensure_buffer_size(&mut self, need_vtx: usize, need_idx: usize) {
|
pub fn ensure_buffer_size(&mut self, need_vtx: usize, need_idx: usize) {
|
||||||
let current_vtx_size = self.vertex_buffer.get_size() / std::mem::size_of::<Vertex>();
|
let current_vtx_size = self.vertex_buffer.get_size() / std::mem::size_of::<Vertex>();
|
||||||
let current_idx_size = self.index_buffer.get_size() / std::mem::size_of::<u32>();
|
let current_idx_size = self.index_buffer.get_size() / std::mem::size_of::<u32>();
|
||||||
|
@ -88,10 +92,9 @@ impl BufferPair {
|
||||||
self.vertex_count = vtx.len();
|
self.vertex_count = vtx.len();
|
||||||
self.index_count = idx.len();
|
self.index_count = idx.len();
|
||||||
|
|
||||||
|
if self.vertex_count == 0 || self.index_count == 0 {
|
||||||
self.vertex_buffer.invalidate();
|
self.vertex_buffer.invalidate();
|
||||||
self.index_buffer.invalidate();
|
self.index_buffer.invalidate();
|
||||||
|
|
||||||
if self.vertex_count == 0 || self.index_count == 0 {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,78 +109,55 @@ impl BufferPair {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct GlDrawCall {
|
|
||||||
active: bool,
|
|
||||||
buffer: BufferPair,
|
|
||||||
bind_texture: Option<Rc<SrgbTexture2d>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct GliumUiRenderer {
|
pub struct GliumUiRenderer {
|
||||||
context: Rc<Context>,
|
context: Rc<Context>,
|
||||||
program: glium::Program,
|
program: glium::Program,
|
||||||
program_tex: glium::Program,
|
ui_texture: Option<Texture2d>,
|
||||||
font_texture: Option<Rc<SrgbTexture2d>>,
|
buffer_pair: Option<BufferPair>,
|
||||||
plan: Vec<GlDrawCall>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GliumUiRenderer {
|
impl GliumUiRenderer {
|
||||||
pub fn new<F: Facade>(facade: &F) -> Self {
|
pub fn new<F: Facade>(facade: &F) -> Self {
|
||||||
log::info!("initializing hui glium backend");
|
log::info!("initializing hui-glium");
|
||||||
Self {
|
Self {
|
||||||
program: Program::from_source(facade, VERTEX_SHADER, FRAGMENT_SHADER, None).unwrap(),
|
program: match facade.get_context().get_supported_glsl_version().0 {
|
||||||
program_tex: Program::from_source(facade, VERTEX_SHADER, FRAGMENT_SHADER_TEX, None).unwrap(),
|
Api::Gl => Program::from_source(facade, VERTEX_SHADER_150, FRAGMENT_SHADER_150, None).unwrap(),
|
||||||
|
Api::GlEs => Program::from_source(facade, VERTEX_SHADER_GLES3, FRAGMENT_SHADER_GLES3, None).unwrap(),
|
||||||
|
},
|
||||||
context: Rc::clone(facade.get_context()),
|
context: Rc::clone(facade.get_context()),
|
||||||
font_texture: None,
|
ui_texture: None,
|
||||||
plan: vec![]
|
buffer_pair: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_draw_plan(&mut self, plan: &UiDrawPlan) {
|
fn update_buffers(&mut self, call: &UiDrawCall) {
|
||||||
if plan.calls.len() > self.plan.len() {
|
log::trace!("updating ui buffers (tris: {})", call.indices.len() / 3);
|
||||||
self.plan.resize_with(plan.calls.len(), || {
|
|
||||||
GlDrawCall {
|
|
||||||
buffer: BufferPair::new(&self.context),
|
|
||||||
bind_texture: None,
|
|
||||||
active: false,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
for step in &mut self.plan[plan.calls.len()..] {
|
|
||||||
step.active = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (idx, call) in plan.calls.iter().enumerate() {
|
|
||||||
let data_vtx = &call.vertices.iter().copied().map(Vertex::from).collect::<Vec<_>>()[..];
|
let data_vtx = &call.vertices.iter().copied().map(Vertex::from).collect::<Vec<_>>()[..];
|
||||||
let data_idx = &call.indices[..];
|
let data_idx = &call.indices[..];
|
||||||
self.plan[idx].active = true;
|
if let Some(buffer) = &mut self.buffer_pair {
|
||||||
self.plan[idx].buffer.write_data(data_vtx, data_idx);
|
buffer.write_data(data_vtx, data_idx);
|
||||||
self.plan[idx].bind_texture = match call.bind_texture {
|
} else if !call.indices.is_empty() {
|
||||||
Some(BindTexture::FontTexture) => {
|
self.buffer_pair = Some(BufferPair::new_with_data(&self.context, data_vtx, data_idx));
|
||||||
const NO_FNT_TEX: &str = "Font texture exists in draw plan but not yet inited. Make sure to call update_font_texture() *before* update_draw_plan()";
|
|
||||||
Some(Rc::clone(self.font_texture.as_ref().expect(NO_FNT_TEX)))
|
|
||||||
},
|
|
||||||
None => None,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_font_texture(&mut self, font_texture: &FontTextureInfo) {
|
fn update_texture_atlas(&mut self, atlas: &TextureAtlasMeta) {
|
||||||
log::debug!("updating font texture");
|
log::trace!("updating ui atlas texture");
|
||||||
self.font_texture = Some(Rc::new(SrgbTexture2d::new(
|
self.ui_texture = Some(Texture2d::new(
|
||||||
&self.context,
|
&self.context,
|
||||||
RawImage2d::from_raw_rgba(
|
RawImage2d::from_raw_rgba(
|
||||||
font_texture.data.to_owned(),
|
atlas.data.to_owned(),
|
||||||
(font_texture.size.x, font_texture.size.y)
|
(atlas.size.x, atlas.size.y)
|
||||||
)
|
)
|
||||||
).unwrap()));
|
).unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update(&mut self, hui: &UiInstance) {
|
pub fn update(&mut self, instance: &UiInstance) {
|
||||||
if let Some(texture) = hui.font_texture().if_modified() {
|
if self.ui_texture.is_none() || instance.atlas().modified {
|
||||||
self.update_font_texture(texture);
|
self.update_texture_atlas(&instance.atlas());
|
||||||
}
|
}
|
||||||
if let Some(plan) = hui.draw_plan().if_modified() {
|
if self.buffer_pair.is_none() || instance.draw_call().0 {
|
||||||
self.update_draw_plan(plan);
|
self.update_buffers(instance.draw_call().1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -187,43 +167,30 @@ impl GliumUiRenderer {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
for step in &self.plan {
|
if let Some(buffer) = &self.buffer_pair {
|
||||||
if !step.active {
|
if buffer.is_empty() {
|
||||||
continue
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if step.buffer.is_empty() {
|
let vtx_buffer = buffer.vertex_buffer.slice(0..buffer.vertex_count).unwrap();
|
||||||
continue
|
let idx_buffer = buffer.index_buffer.slice(0..buffer.index_count).unwrap();
|
||||||
}
|
|
||||||
|
|
||||||
let vtx_buffer = step.buffer.vertex_buffer.slice(0..step.buffer.vertex_count).unwrap();
|
|
||||||
let idx_buffer = step.buffer.index_buffer.slice(0..step.buffer.index_count).unwrap();
|
|
||||||
|
|
||||||
if let Some(bind_texture) = step.bind_texture.as_ref() {
|
|
||||||
frame.draw(
|
|
||||||
vtx_buffer,
|
|
||||||
idx_buffer,
|
|
||||||
&self.program_tex,
|
|
||||||
&uniform! {
|
|
||||||
resolution: resolution.to_array(),
|
|
||||||
tex: Sampler(bind_texture.as_ref(), SamplerBehavior {
|
|
||||||
wrap_function: (SamplerWrapFunction::Clamp, SamplerWrapFunction::Clamp, SamplerWrapFunction::Clamp),
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
¶ms,
|
|
||||||
).unwrap();
|
|
||||||
} else {
|
|
||||||
frame.draw(
|
frame.draw(
|
||||||
vtx_buffer,
|
vtx_buffer,
|
||||||
idx_buffer,
|
idx_buffer,
|
||||||
&self.program,
|
&self.program,
|
||||||
&uniform! {
|
&uniform! {
|
||||||
resolution: resolution.to_array(),
|
resolution: resolution.to_array(),
|
||||||
|
tex: Sampler(self.ui_texture.as_ref().unwrap(), SamplerBehavior {
|
||||||
|
max_anisotropy: 1,
|
||||||
|
magnify_filter: MagnifySamplerFilter::Nearest,
|
||||||
|
minify_filter: MinifySamplerFilter::Linear,
|
||||||
|
wrap_function: (SamplerWrapFunction::Clamp, SamplerWrapFunction::Clamp, SamplerWrapFunction::Clamp),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
¶ms,
|
¶ms,
|
||||||
).unwrap();
|
).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
23
hui-wgpu/Cargo.toml
Normal file
23
hui-wgpu/Cargo.toml
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
[package]
|
||||||
|
name = "hui-wgpu"
|
||||||
|
description = "wgpu render backend for `hui`"
|
||||||
|
repository = "https://github.com/griffi-gh/hui"
|
||||||
|
readme = "../README.md"
|
||||||
|
authors = ["griffi-gh <prasol258@gmail.com>"]
|
||||||
|
version = "0.1.0-alpha.5"
|
||||||
|
rust-version = "1.75"
|
||||||
|
edition = "2021"
|
||||||
|
license = "GPL-3.0-or-later"
|
||||||
|
publish = true
|
||||||
|
include = [
|
||||||
|
"shaders/**/*",
|
||||||
|
"src/**/*.rs",
|
||||||
|
"Cargo.toml",
|
||||||
|
]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
hui = { version = "=0.1.0-alpha.5", path = "../hui", default-features = false }
|
||||||
|
wgpu = { version = "0.20", default-features = false, features = ["wgsl"]}
|
||||||
|
bytemuck = "1.15"
|
||||||
|
log = "0.4"
|
||||||
|
glam = "0.27"
|
35
hui-wgpu/shaders/ui.wgsl
Normal file
35
hui-wgpu/shaders/ui.wgsl
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
struct VertexInput {
|
||||||
|
@location(0) position: vec3<f32>,
|
||||||
|
@location(1) uv: vec2<f32>,
|
||||||
|
@location(2) color: vec4<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct VertexOutput {
|
||||||
|
@builtin(position) clip_position: vec4<f32>,
|
||||||
|
@location(0) uv: vec2<f32>,
|
||||||
|
@location(1) color: vec4<f32>,
|
||||||
|
};
|
||||||
|
|
||||||
|
@vertex
|
||||||
|
fn vs_main(
|
||||||
|
in: VertexInput,
|
||||||
|
) -> VertexOutput {
|
||||||
|
var out: VertexOutput;
|
||||||
|
out.clip_position = vec4<f32>(in.position, 1.0);
|
||||||
|
out.uv = in.uv;
|
||||||
|
out.color = in.color;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@group(0) @binding(0)
|
||||||
|
var t_diffuse: texture_2d<f32>;
|
||||||
|
|
||||||
|
@group(0) @binding(1)
|
||||||
|
var s_diffuse: sampler;
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
|
//HACK: This is a hack to convert the color to sRGB
|
||||||
|
let srgb_color = pow(in.color, vec4<f32>(2.2, 2.2, 2.2, 1.0));
|
||||||
|
return textureSample(t_diffuse, s_diffuse, in.uv) * srgb_color;
|
||||||
|
}
|
321
hui-wgpu/src/lib.rs
Normal file
321
hui-wgpu/src/lib.rs
Normal file
|
@ -0,0 +1,321 @@
|
||||||
|
use glam::{vec2, Vec2};
|
||||||
|
use hui::{draw::{TextureAtlasMeta, UiDrawCall, UiVertex}, UiInstance};
|
||||||
|
|
||||||
|
const DEFAULT_BUFFER_SIZE: u64 = 1024;
|
||||||
|
const DEFAULT_TEXTURE_SIZE: u32 = 512;
|
||||||
|
const SHADER_MODULE: &str = include_str!("../shaders/ui.wgsl");
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
#[repr(C, packed)]
|
||||||
|
struct WgpuVertex {
|
||||||
|
position: [f32; 2],
|
||||||
|
uv: [f32; 2],
|
||||||
|
color: [f32; 4],
|
||||||
|
}
|
||||||
|
unsafe impl bytemuck::Pod for WgpuVertex {}
|
||||||
|
unsafe impl bytemuck::Zeroable for WgpuVertex {}
|
||||||
|
|
||||||
|
impl WgpuVertex {
|
||||||
|
pub const LAYOUT: wgpu::VertexBufferLayout<'static> = wgpu::VertexBufferLayout {
|
||||||
|
array_stride: std::mem::size_of::<WgpuVertex>() as wgpu::BufferAddress,
|
||||||
|
step_mode: wgpu::VertexStepMode::Vertex,
|
||||||
|
attributes: &wgpu::vertex_attr_array![
|
||||||
|
0 => Float32x2,
|
||||||
|
1 => Float32x2,
|
||||||
|
2 => Float32x4,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<UiVertex> for WgpuVertex {
|
||||||
|
fn from(v: UiVertex) -> Self {
|
||||||
|
Self {
|
||||||
|
position: v.position.to_array(),
|
||||||
|
uv: v.uv.to_array(),
|
||||||
|
color: v.color.to_array(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct WgpuUiRenderer {
|
||||||
|
pub modified: bool,
|
||||||
|
pub vertex_buffer: wgpu::Buffer,
|
||||||
|
pub index_buffer: wgpu::Buffer,
|
||||||
|
pub vertex_count: usize,
|
||||||
|
pub index_count: usize,
|
||||||
|
pub bind_group_layout: wgpu::BindGroupLayout,
|
||||||
|
pub bind_group: wgpu::BindGroup,
|
||||||
|
pub pipeline: wgpu::RenderPipeline,
|
||||||
|
pub texture: wgpu::Texture,
|
||||||
|
pub texture_view: wgpu::TextureView,
|
||||||
|
pub texture_sampler: wgpu::Sampler,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WgpuUiRenderer {
|
||||||
|
pub fn new(
|
||||||
|
device: &wgpu::Device,
|
||||||
|
surface_format: wgpu::TextureFormat,
|
||||||
|
) -> Self {
|
||||||
|
let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
|
label: Some("ui_vertex_buffer"),
|
||||||
|
size: std::mem::size_of::<WgpuVertex>() as u64 * DEFAULT_BUFFER_SIZE,
|
||||||
|
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
|
||||||
|
mapped_at_creation: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
let index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
|
label: Some("hui_index_buffer"),
|
||||||
|
size: std::mem::size_of::<u32>() as u64 * DEFAULT_BUFFER_SIZE,
|
||||||
|
usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
|
||||||
|
mapped_at_creation: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||||
|
label: Some("ui_texture"),
|
||||||
|
size: wgpu::Extent3d {
|
||||||
|
width: DEFAULT_TEXTURE_SIZE,
|
||||||
|
height: DEFAULT_TEXTURE_SIZE,
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
},
|
||||||
|
mip_level_count: 1,
|
||||||
|
sample_count: 1,
|
||||||
|
dimension: wgpu::TextureDimension::D2,
|
||||||
|
format: wgpu::TextureFormat::Rgba8UnormSrgb,
|
||||||
|
usage: wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::TEXTURE_BINDING,
|
||||||
|
view_formats: &[],
|
||||||
|
});
|
||||||
|
|
||||||
|
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||||
|
label: Some("ui_bind_group_layout"),
|
||||||
|
entries: &[
|
||||||
|
wgpu::BindGroupLayoutEntry {
|
||||||
|
binding: 0,
|
||||||
|
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||||
|
ty: wgpu::BindingType::Texture {
|
||||||
|
sample_type: wgpu::TextureSampleType::Float { filterable: false },
|
||||||
|
view_dimension: wgpu::TextureViewDimension::D2,
|
||||||
|
multisampled: false,
|
||||||
|
},
|
||||||
|
count: None,
|
||||||
|
},
|
||||||
|
wgpu::BindGroupLayoutEntry {
|
||||||
|
binding: 1,
|
||||||
|
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||||
|
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering),
|
||||||
|
count: None,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
let texture_view = texture.create_view(&wgpu::TextureViewDescriptor {
|
||||||
|
label: Some("ui_texture_view"),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
let texture_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
||||||
|
label: Some("ui_texture_sampler"),
|
||||||
|
address_mode_u: wgpu::AddressMode::ClampToEdge,
|
||||||
|
address_mode_v: wgpu::AddressMode::ClampToEdge,
|
||||||
|
address_mode_w: wgpu::AddressMode::ClampToEdge,
|
||||||
|
mag_filter: wgpu::FilterMode::Nearest,
|
||||||
|
min_filter: wgpu::FilterMode::Nearest,
|
||||||
|
mipmap_filter: wgpu::FilterMode::Nearest,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||||
|
label: Some("ui_bind_group"),
|
||||||
|
layout: &bind_group_layout,
|
||||||
|
entries: &[
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 0,
|
||||||
|
resource: wgpu::BindingResource::TextureView(&texture_view),
|
||||||
|
},
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 1,
|
||||||
|
resource: wgpu::BindingResource::Sampler(&texture_sampler),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
let shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||||
|
label: Some("ui_vertex_shader"),
|
||||||
|
source: wgpu::ShaderSource::Wgsl(SHADER_MODULE.into()),
|
||||||
|
});
|
||||||
|
|
||||||
|
let pipeline = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||||
|
label: Some("ui_pipeline_layout"),
|
||||||
|
bind_group_layouts: &[&bind_group_layout],
|
||||||
|
push_constant_ranges: &[],
|
||||||
|
});
|
||||||
|
|
||||||
|
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||||
|
label: Some("ui_pipeline"),
|
||||||
|
layout: Some(&pipeline),
|
||||||
|
vertex: wgpu::VertexState {
|
||||||
|
module: &shader_module,
|
||||||
|
compilation_options: wgpu::PipelineCompilationOptions::default(),
|
||||||
|
entry_point: "vs_main",
|
||||||
|
buffers: &[WgpuVertex::LAYOUT],
|
||||||
|
},
|
||||||
|
fragment: Some(wgpu::FragmentState {
|
||||||
|
module: &shader_module,
|
||||||
|
compilation_options: wgpu::PipelineCompilationOptions::default(),
|
||||||
|
entry_point: "fs_main",
|
||||||
|
targets: &[Some(wgpu::ColorTargetState {
|
||||||
|
format: surface_format,
|
||||||
|
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
|
||||||
|
write_mask: wgpu::ColorWrites::COLOR,
|
||||||
|
})],
|
||||||
|
}),
|
||||||
|
primitive: wgpu::PrimitiveState {
|
||||||
|
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||||
|
strip_index_format: None,
|
||||||
|
front_face: wgpu::FrontFace::Ccw,
|
||||||
|
cull_mode: None,
|
||||||
|
polygon_mode: wgpu::PolygonMode::Fill,
|
||||||
|
conservative: false,
|
||||||
|
unclipped_depth: false,
|
||||||
|
},
|
||||||
|
depth_stencil: None,
|
||||||
|
multisample: wgpu::MultisampleState::default(),
|
||||||
|
multiview: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
modified: true,
|
||||||
|
vertex_buffer,
|
||||||
|
index_buffer,
|
||||||
|
vertex_count: 0,
|
||||||
|
index_count: 0,
|
||||||
|
bind_group_layout,
|
||||||
|
bind_group,
|
||||||
|
texture,
|
||||||
|
texture_view,
|
||||||
|
texture_sampler,
|
||||||
|
pipeline,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_buffers(&mut self, call: &UiDrawCall, queue: &wgpu::Queue, device: &wgpu::Device, resolution: Vec2) {
|
||||||
|
let data_vtx = call.vertices.iter()
|
||||||
|
.copied()
|
||||||
|
.map(|x| {
|
||||||
|
let mut v = x;
|
||||||
|
v.position = vec2(1., -1.) * ((v.position / resolution) * 2.0 - 1.0);
|
||||||
|
v
|
||||||
|
})
|
||||||
|
.map(WgpuVertex::from)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let data_idx = &call.indices[..];
|
||||||
|
|
||||||
|
let data_vtx_view = bytemuck::cast_slice(&data_vtx);
|
||||||
|
let data_idx_view = bytemuck::cast_slice(data_idx);
|
||||||
|
|
||||||
|
self.vertex_count = call.vertices.len();
|
||||||
|
self.index_count = call.indices.len();
|
||||||
|
|
||||||
|
if data_vtx.is_empty() || data_idx.is_empty() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if data_vtx_view.len() as u64 > self.vertex_buffer.size() {
|
||||||
|
self.vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
|
label: Some("ui_vertex_buffer"),
|
||||||
|
size: (data_vtx_view.len() as u64).next_power_of_two(),
|
||||||
|
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
|
||||||
|
mapped_at_creation: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if data_idx_view.len() as u64 > self.index_buffer.size() {
|
||||||
|
self.index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
|
label: Some("ui_index_buffer"),
|
||||||
|
size: (data_idx_view.len() as u64).next_power_of_two(),
|
||||||
|
usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
|
||||||
|
mapped_at_creation: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
queue.write_buffer(&self.vertex_buffer, 0, data_vtx_view);
|
||||||
|
queue.write_buffer(&self.index_buffer, 0, data_idx_view);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_texture(&self, meta: TextureAtlasMeta, queue: &wgpu::Queue) {
|
||||||
|
//TODO URGENCY:HIGH resize texture if needed
|
||||||
|
if meta.data.len() as u32 > (self.texture.size().width * self.texture.size().height * 4) {
|
||||||
|
unimplemented!("texture resize not implemented");
|
||||||
|
}
|
||||||
|
queue.write_texture(
|
||||||
|
wgpu::ImageCopyTexture {
|
||||||
|
texture: &self.texture,
|
||||||
|
mip_level: 0,
|
||||||
|
origin: wgpu::Origin3d::ZERO,
|
||||||
|
aspect: wgpu::TextureAspect::All,
|
||||||
|
},
|
||||||
|
meta.data,
|
||||||
|
wgpu::ImageDataLayout {
|
||||||
|
offset: 0,
|
||||||
|
bytes_per_row: Some(meta.size.x * 4),
|
||||||
|
rows_per_image: Some(meta.size.y),
|
||||||
|
},
|
||||||
|
wgpu::Extent3d {
|
||||||
|
width: meta.size.x,
|
||||||
|
height: meta.size.y,
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(
|
||||||
|
&mut self,
|
||||||
|
instance: &UiInstance,
|
||||||
|
queue: &wgpu::Queue,
|
||||||
|
device: &wgpu::Device,
|
||||||
|
resolution: Vec2,
|
||||||
|
) {
|
||||||
|
let (modified, call) = instance.draw_call();
|
||||||
|
if self.modified || modified {
|
||||||
|
self.update_buffers(call, queue, device, resolution);
|
||||||
|
}
|
||||||
|
|
||||||
|
let meta = instance.atlas();
|
||||||
|
if self.modified || meta.modified {
|
||||||
|
self.update_texture(meta, queue);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.modified = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw(
|
||||||
|
&self,
|
||||||
|
encoder: &mut wgpu::CommandEncoder,
|
||||||
|
surface_view: &wgpu::TextureView,
|
||||||
|
) {
|
||||||
|
if self.vertex_count == 0 || self.index_count == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||||
|
label: Some("ui_render_pass"),
|
||||||
|
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||||
|
view: surface_view,
|
||||||
|
resolve_target: None,
|
||||||
|
ops: wgpu::Operations {
|
||||||
|
load: wgpu::LoadOp::Load,
|
||||||
|
store: wgpu::StoreOp::Store,
|
||||||
|
},
|
||||||
|
})],
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
let vtx_size = self.vertex_count as u64 * std::mem::size_of::<WgpuVertex>() as u64;
|
||||||
|
let idx_size = self.index_count as u64 * std::mem::size_of::<u32>() as u64;
|
||||||
|
|
||||||
|
rpass.set_pipeline(&self.pipeline);
|
||||||
|
rpass.set_bind_group(0, &self.bind_group, &[]);
|
||||||
|
rpass.set_vertex_buffer(0, self.vertex_buffer.slice(0..vtx_size));
|
||||||
|
rpass.set_index_buffer(self.index_buffer.slice(..idx_size), wgpu::IndexFormat::Uint32);
|
||||||
|
rpass.draw_indexed(0..self.index_count as u32, 0, 0..1);
|
||||||
|
}
|
||||||
|
}
|
26
hui-winit/Cargo.toml
Normal file
26
hui-winit/Cargo.toml
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
[package]
|
||||||
|
name = "hui-winit"
|
||||||
|
description = "winit platform backend for `hui`"
|
||||||
|
repository = "https://github.com/griffi-gh/hui"
|
||||||
|
readme = "../README.md"
|
||||||
|
authors = ["griffi-gh <prasol258@gmail.com>"]
|
||||||
|
version = "0.1.0-alpha.5"
|
||||||
|
edition = "2021"
|
||||||
|
license = "GPL-3.0-or-later"
|
||||||
|
publish = true
|
||||||
|
include = [
|
||||||
|
"src/**/*.rs",
|
||||||
|
"Cargo.toml",
|
||||||
|
]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
hui = { version = "=0.1.0-alpha.5", path = "../hui", default-features = false }
|
||||||
|
winit_30 = { package = "winit", version = "0.30", default-features = false, optional = true }
|
||||||
|
winit_29 = { package = "winit", version = "0.29", default-features = false, optional = true }
|
||||||
|
glam = "0.27"
|
||||||
|
log = "0.4"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
winit_30 = ["dep:winit_30"]
|
||||||
|
winit_29 = ["dep:winit_29"]
|
38
hui-winit/src/lib.rs
Normal file
38
hui-winit/src/lib.rs
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
#[cfg(all(feature = "winit_30", feature = "winit_29"))]
|
||||||
|
compile_error!("Only one of the winit_30 and winit_29 features can be enabled at a time");
|
||||||
|
#[cfg(not(any(feature = "winit_30", feature = "winit_29")))]
|
||||||
|
compile_error!("One of the winit_30 and winit_29 features must be enabled");
|
||||||
|
#[cfg(feature = "winit_30")] extern crate winit_30 as winit;
|
||||||
|
#[cfg(feature = "winit_29")] extern crate winit_29 as winit;
|
||||||
|
|
||||||
|
use glam::vec2;
|
||||||
|
use hui::{event::UiEvent, UiInstance};
|
||||||
|
use winit::event::{Event, WindowEvent, MouseButton, ElementState};
|
||||||
|
|
||||||
|
//TODO: check window id
|
||||||
|
pub fn handle_winit_event<T>(ui: &mut UiInstance, event: &Event<T>) {
|
||||||
|
if let Event::WindowEvent { event, .. } = event {
|
||||||
|
match event {
|
||||||
|
WindowEvent::CursorMoved { position, .. } => {
|
||||||
|
ui.push_event(UiEvent::MouseMove(vec2(position.x as f32, position.y as f32)));
|
||||||
|
},
|
||||||
|
WindowEvent::MouseInput { state, button, .. } => {
|
||||||
|
ui.push_event(UiEvent::MouseButton {
|
||||||
|
button: match button {
|
||||||
|
MouseButton::Left => hui::input::MouseButton::Primary,
|
||||||
|
MouseButton::Right => hui::input::MouseButton::Secondary,
|
||||||
|
MouseButton::Middle => hui::input::MouseButton::Middle,
|
||||||
|
MouseButton::Other(id) => hui::input::MouseButton::Other(*id as u8),
|
||||||
|
_ => return,
|
||||||
|
},
|
||||||
|
state: match state {
|
||||||
|
ElementState::Pressed => hui::input::ButtonState::Pressed,
|
||||||
|
ElementState::Released => hui::input::ButtonState::Released,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
//TODO: translate keyboard input
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,9 +2,10 @@
|
||||||
name = "hui"
|
name = "hui"
|
||||||
description = "Simple UI library for games and other interactive applications"
|
description = "Simple UI library for games and other interactive applications"
|
||||||
repository = "https://github.com/griffi-gh/hui"
|
repository = "https://github.com/griffi-gh/hui"
|
||||||
|
readme = "../README.md"
|
||||||
authors = ["griffi-gh <prasol258@gmail.com>"]
|
authors = ["griffi-gh <prasol258@gmail.com>"]
|
||||||
rust-version = "1.75"
|
rust-version = "1.75"
|
||||||
version = "0.0.2"
|
version = "0.1.0-alpha.5"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "GPL-3.0-or-later"
|
license = "GPL-3.0-or-later"
|
||||||
publish = true
|
publish = true
|
||||||
|
@ -15,15 +16,88 @@ include = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
hui-derive = { version = "0.1.0-alpha.5", path = "../hui-derive", optional = true }
|
||||||
hashbrown = "0.14"
|
hashbrown = "0.14"
|
||||||
nohash-hasher = "0.2"
|
nohash-hasher = "0.2"
|
||||||
glam = "0.25"
|
glam = "0.27"
|
||||||
fontdue = "0.8"
|
fontdue = "0.8"
|
||||||
rect_packer = "0.2"
|
rect_packer = "0.2"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
|
document-features = "0.2"
|
||||||
|
derive_setters = "0.1"
|
||||||
|
derive_more = "0.99"
|
||||||
|
tinyset = "0.4"
|
||||||
|
image = { version = "0.25", default-features = false, optional = true }
|
||||||
|
rustc-hash = "1.1"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["builtin_elements", "builtin_font"]
|
default = ["el_all", "image", "builtin_font", "pixel_perfect_text", "derive"]
|
||||||
|
|
||||||
|
## Enable derive macros
|
||||||
|
derive = ["dep:hui-derive"]
|
||||||
|
|
||||||
|
## Enable image loading support using the `image` crate
|
||||||
|
image = ["dep:image"]
|
||||||
|
|
||||||
|
## Enable the built-in font (ProggyTiny, adds *35kb* to the executable)
|
||||||
builtin_font = []
|
builtin_font = []
|
||||||
builtin_elements = []
|
|
||||||
#parallel = ["dep:rayon", "fontdue/parallel"]
|
#! #### Pixel-perfect rendering:
|
||||||
|
|
||||||
|
## Round all vertex positions to nearest integer coordinates (not recommended)
|
||||||
|
pixel_perfect = ["pixel_perfect_text"]
|
||||||
|
|
||||||
|
## Apply pixel-perfect rendering hack to text (fixes blurry text rendering)
|
||||||
|
pixel_perfect_text = []
|
||||||
|
|
||||||
|
#! Make sure to disable both features if you are not rendering UI "as-is" at 1:1 scale\
|
||||||
|
#! For exmaple, you should disable them if using DPI (or any other form of) scaling while passing the virtual resolution to the ui or rendering it in 3d space
|
||||||
|
|
||||||
|
#! #### Built-in elements:
|
||||||
|
|
||||||
|
## Enable all built-in elements
|
||||||
|
el_all = [
|
||||||
|
"el_container",
|
||||||
|
"el_frame_view",
|
||||||
|
"el_spacer",
|
||||||
|
"el_br",
|
||||||
|
"el_text",
|
||||||
|
"el_image",
|
||||||
|
"el_progress_bar",
|
||||||
|
"el_slider",
|
||||||
|
"el_transformer",
|
||||||
|
"el_interactable",
|
||||||
|
]
|
||||||
|
|
||||||
|
## Enable the built-in `Container` element
|
||||||
|
el_container = []
|
||||||
|
|
||||||
|
## Enable the built-in `FrameView` element
|
||||||
|
el_frame_view = []
|
||||||
|
|
||||||
|
## Enable the built-in `Spacer` element
|
||||||
|
el_spacer = []
|
||||||
|
|
||||||
|
## Enable the built-in `Break` element
|
||||||
|
el_br = []
|
||||||
|
|
||||||
|
## Enable the built-in `Text` element
|
||||||
|
el_text = []
|
||||||
|
|
||||||
|
## Enable the built-in `Image` element
|
||||||
|
el_image = []
|
||||||
|
|
||||||
|
## Enable the built-in `ProgressBar` element
|
||||||
|
el_progress_bar = []
|
||||||
|
|
||||||
|
## Enable the built-in `Slider` element
|
||||||
|
el_slider = []
|
||||||
|
|
||||||
|
## Enable the built-in `Transformer` element
|
||||||
|
el_transformer = []
|
||||||
|
|
||||||
|
## Enable the built-in `Interactable` element
|
||||||
|
el_interactable = []
|
||||||
|
|
||||||
|
# ## Enable multi-threading support (currently only affects some 3rd-party libraries)
|
||||||
|
# parallel = ["fontdue/parallel"]
|
||||||
|
|
128
hui/src/color.rs
Normal file
128
hui/src/color.rs
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
//! various predefined color constants and helper functions
|
||||||
|
|
||||||
|
use glam::{vec4, Vec4};
|
||||||
|
|
||||||
|
/// Create a color from red, green, blue components
|
||||||
|
pub fn rgb(r: u8, g: u8, b: u8) -> Vec4 {
|
||||||
|
vec4(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a color from red, green, blue, alpha components
|
||||||
|
pub fn rgba(r: u8, g: u8, b: u8, a: u8) -> Vec4 {
|
||||||
|
vec4(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, a as f32 / 255.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create an RGB color from a u32 (/hex) value
|
||||||
|
pub fn rgb_hex(value: u32) -> Vec4 {
|
||||||
|
let r = (value >> 16) & 0xff;
|
||||||
|
let g = (value >> 8) & 0xff;
|
||||||
|
let b = value & 0xff;
|
||||||
|
vec4(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create an RGBA color from a u32 (/hex) value
|
||||||
|
pub fn rgba_hex(value: u32) -> Vec4 {
|
||||||
|
let r = (value >> 16) & 0xff;
|
||||||
|
let g = (value >> 8) & 0xff;
|
||||||
|
let b = value & 0xff;
|
||||||
|
let a = (value >> 24) & 0xff;
|
||||||
|
vec4(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, a as f32 / 255.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(doc, doc="<span style='display: inline-block; background: repeating-conic-gradient(grey 0 25%,darkgrey 0 50%) 50%/8px 8px; width: 1em; height: 1em; border-radius: 50%; border: 1px solid black; vertical-align: -7%'></span>")]
|
||||||
|
/// `#00000000` Transparent
|
||||||
|
pub const TRANSPARENT: Vec4 = vec4(0.0, 0.0, 0.0, 0.0);
|
||||||
|
|
||||||
|
#[cfg_attr(doc, doc="<span style='display: inline-block; background-color: #000000; width: 1em; height: 1em; border-radius: 50%; border: 1px solid black; vertical-align: -7%'></span>")]
|
||||||
|
/// `#000000` Black
|
||||||
|
pub const BLACK: Vec4 = vec4(0.0, 0.0, 0.0, 1.0);
|
||||||
|
|
||||||
|
#[cfg_attr(doc, doc="<span style='display: inline-block; background-color: #ffffff; width: 1em; height: 1em; border-radius: 50%; border: 1px solid black; vertical-align: -7%'></span>")]
|
||||||
|
/// `#ffffff` White
|
||||||
|
pub const WHITE: Vec4 = vec4(1.0, 1.0, 1.0, 1.0);
|
||||||
|
|
||||||
|
#[cfg_attr(doc, doc="<span style='display: inline-block; background-color: #ff0000; width: 1em; height: 1em; border-radius: 50%; border: 1px solid black; vertical-align: -7%'></span>")]
|
||||||
|
/// `#ff0000` Red
|
||||||
|
pub const RED: Vec4 = vec4(1.0, 0.0, 0.0, 1.0);
|
||||||
|
|
||||||
|
#[cfg_attr(doc, doc="<span style='display: inline-block; background-color: #800000; width: 1em; height: 1em; border-radius: 50%; border: 1px solid black; vertical-align: -7%'></span>")]
|
||||||
|
/// `#800000` Dark red
|
||||||
|
pub const DARK_RED: Vec4 = vec4(0.5, 0.0, 0.0, 1.0);
|
||||||
|
|
||||||
|
#[cfg_attr(doc, doc="<span style='display: inline-block; background-color: #00ff00; width: 1em; height: 1em; border-radius: 50%; border: 1px solid black; vertical-align: -7%'></span>")]
|
||||||
|
/// `#00ff00` Green
|
||||||
|
pub const GREEN: Vec4 = vec4(0.0, 1.0, 0.0, 1.0);
|
||||||
|
|
||||||
|
#[cfg_attr(doc, doc="<span style='display: inline-block; background-color: #008000; width: 1em; height: 1em; border-radius: 50%; border: 1px solid black; vertical-align: -7%'></span>")]
|
||||||
|
/// `#008000` Dark green
|
||||||
|
pub const DARK_GREEN: Vec4 = vec4(0.0, 0.5, 0.0, 1.0);
|
||||||
|
|
||||||
|
#[cfg_attr(doc, doc="<span style='display: inline-block; background-color: #0000ff; width: 1em; height: 1em; border-radius: 50%; border: 1px solid black; vertical-align: -7%'></span>")]
|
||||||
|
/// `#0000ff` Blue
|
||||||
|
pub const BLUE: Vec4 = vec4(0.0, 0.0, 1.0, 1.0);
|
||||||
|
|
||||||
|
#[cfg_attr(doc, doc="<span style='display: inline-block; background-color: #000080; width: 1em; height: 1em; border-radius: 50%; border: 1px solid black; vertical-align: -7%'></span>")]
|
||||||
|
/// `#000080` Dark blue
|
||||||
|
pub const DARK_BLUE: Vec4 = vec4(0.0, 0.0, 0.5, 1.0);
|
||||||
|
|
||||||
|
#[cfg_attr(doc, doc="<span style='display: inline-block; background-color: #ffff00; width: 1em; height: 1em; border-radius: 50%; border: 1px solid black; vertical-align: -7%'></span>")]
|
||||||
|
/// `#ffff00` Yellow
|
||||||
|
pub const YELLOW: Vec4 = vec4(1.0, 1.0, 0.0, 1.0);
|
||||||
|
|
||||||
|
#[cfg_attr(doc, doc="<span style='display: inline-block; background-color: #00ffff; width: 1em; height: 1em; border-radius: 50%; border: 1px solid black; vertical-align: -7%'></span>")]
|
||||||
|
/// `#00ffff` Cyan
|
||||||
|
pub const CYAN: Vec4 = vec4(0.0, 1.0, 1.0, 1.0);
|
||||||
|
|
||||||
|
#[cfg_attr(doc, doc="<span style='display: inline-block; background-color: #ff00ff; width: 1em; height: 1em; border-radius: 50%; border: 1px solid black; vertical-align: -7%'></span>")]
|
||||||
|
/// `#ff00ff` Magenta
|
||||||
|
pub const MAGENTA: Vec4 = vec4(1.0, 0.0, 1.0, 1.0);
|
||||||
|
|
||||||
|
#[cfg_attr(doc, doc="<span style='display: inline-block; background-color: #808080; width: 1em; height: 1em; border-radius: 50%; border: 1px solid black; vertical-align: -7%'></span>")]
|
||||||
|
/// `#808080` Gray
|
||||||
|
pub const GRAY: Vec4 = vec4(0.5, 0.5, 0.5, 1.0);
|
||||||
|
|
||||||
|
#[cfg_attr(doc, doc="<span style='display: inline-block; background-color: #c0c0c0; width: 1em; height: 1em; border-radius: 50%; border: 1px solid black; vertical-align: -7%'></span>")]
|
||||||
|
/// `#c0c0c0` Light gray
|
||||||
|
pub const LIGHT_GRAY: Vec4 = vec4(0.75, 0.75, 0.75, 1.0);
|
||||||
|
|
||||||
|
#[cfg_attr(doc, doc="<span style='display: inline-block; background-color: #404040; width: 1em; height: 1em; border-radius: 50%; border: 1px solid black; vertical-align: -7%'></span>")]
|
||||||
|
/// `#404040` Dark gray
|
||||||
|
pub const DARK_GRAY: Vec4 = vec4(0.25, 0.25, 0.25, 1.0);
|
||||||
|
|
||||||
|
#[cfg_attr(doc, doc="<span style='display: inline-block; background-color: #ff8000; width: 1em; height: 1em; border-radius: 50%; border: 1px solid black; vertical-align: -7%'></span>")]
|
||||||
|
/// `#ff8000` Orange
|
||||||
|
pub const ORANGE: Vec4 = vec4(1.0, 0.5, 0.0, 1.0);
|
||||||
|
|
||||||
|
#[cfg_attr(doc, doc="<span style='display: inline-block; background-color: #804000; width: 1em; height: 1em; border-radius: 50%; border: 1px solid black; vertical-align: -7%'></span>")]
|
||||||
|
/// `#804000` Brown
|
||||||
|
pub const BROWN: Vec4 = vec4(0.5, 0.25, 0.0, 1.0);
|
||||||
|
|
||||||
|
#[cfg_attr(doc, doc="<span style='display: inline-block; background-color: #ff80ff; width: 1em; height: 1em; border-radius: 50%; border: 1px solid black; vertical-align: -7%'></span>")]
|
||||||
|
/// `#ff80ff` Pink
|
||||||
|
pub const PINK: Vec4 = vec4(1.0, 0.5, 1.0, 1.0);
|
||||||
|
|
||||||
|
#[cfg_attr(doc, doc="<span style='display: inline-block; background-color: #800080; width: 1em; height: 1em; border-radius: 50%; border: 1px solid black; vertical-align: -7%'></span>")]
|
||||||
|
/// `#800080` Purple
|
||||||
|
pub const PURPLE: Vec4 = vec4(0.5, 0.0, 0.5, 1.0);
|
||||||
|
|
||||||
|
#[cfg_attr(doc, doc="<span style='display: inline-block; background-color: #80ff00; width: 1em; height: 1em; border-radius: 50%; border: 1px solid black; vertical-align: -7%'></span>")]
|
||||||
|
/// `#80ff00` Lime
|
||||||
|
pub const LIME: Vec4 = vec4(0.5, 1.0, 0.0, 1.0);
|
||||||
|
|
||||||
|
#[cfg_attr(doc, doc="<span style='display: inline-block; background-color: #008080; width: 1em; height: 1em; border-radius: 50%; border: 1px solid black; vertical-align: -7%'></span>")]
|
||||||
|
/// `#008080` Teal
|
||||||
|
pub const TEAL: Vec4 = vec4(0.0, 0.5, 0.5, 1.0);
|
||||||
|
|
||||||
|
#[cfg_attr(doc, doc="<span style='display: inline-block; background-color: #004080; width: 1em; height: 1em; border-radius: 50%; border: 1px solid black; vertical-align: -7%'></span>")]
|
||||||
|
/// `#004080` Indigo
|
||||||
|
pub const INDIGO: Vec4 = vec4(0.0, 0.25, 0.5, 1.0);
|
||||||
|
|
||||||
|
#[cfg_attr(doc, doc="<span style='display: inline-block; background-color: #808000; width: 1em; height: 1em; border-radius: 50%; border: 1px solid black; vertical-align: -7%'></span>")]
|
||||||
|
/// `#808000` Olive
|
||||||
|
pub const OLIVE: Vec4 = vec4(0.5, 0.5, 0.0, 1.0);
|
||||||
|
|
||||||
|
#[cfg_attr(doc, doc="<span style='display: inline-block; background-color: #87ceeb; width: 1em; height: 1em; border-radius: 50%; border: 1px solid black; vertical-align: -7%'></span>")]
|
||||||
|
/// `#87ceeb` Sky blue
|
||||||
|
pub const SKY_BLUE: Vec4 = vec4(0.53, 0.81, 0.92, 1.0);
|
||||||
|
|
||||||
|
//TODO color macro
|
372
hui/src/draw.rs
372
hui/src/draw.rs
|
@ -1,9 +1,26 @@
|
||||||
use crate::{IfModified, text::{TextRenderer, FontHandle}};
|
//! draw commands, tesselation and UI rendering.
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
rect::Corners,
|
||||||
|
text::{FontHandle, TextRenderer}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(crate) mod atlas;
|
||||||
|
use atlas::TextureAtlasManager;
|
||||||
|
pub use atlas::{ImageHandle, TextureAtlasMeta, TextureFormat, ImageCtx};
|
||||||
|
|
||||||
|
mod corner_radius;
|
||||||
|
pub use corner_radius::RoundedCorners;
|
||||||
|
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use fontdue::layout::{Layout, CoordinateSystem, TextStyle};
|
use fontdue::layout::{Layout, CoordinateSystem, TextStyle};
|
||||||
use glam::{Vec2, Vec4, vec2};
|
use glam::{vec2, Vec2, Affine2, Vec4};
|
||||||
|
|
||||||
|
//TODO: circle draw command
|
||||||
|
|
||||||
|
/// Available draw commands
|
||||||
|
/// - Rectangle: Filled, colored rectangle, with optional rounded corners and texture
|
||||||
|
/// - Text: Draw text using the specified font, size, color, and position
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub enum UiDrawCommand {
|
pub enum UiDrawCommand {
|
||||||
///Filled, colored rectangle
|
///Filled, colored rectangle
|
||||||
|
@ -13,13 +30,20 @@ pub enum UiDrawCommand {
|
||||||
///Size in pixels
|
///Size in pixels
|
||||||
size: Vec2,
|
size: Vec2,
|
||||||
///Color (RGBA)
|
///Color (RGBA)
|
||||||
color: Vec4,
|
color: Corners<Vec4>,
|
||||||
|
///Texture
|
||||||
|
texture: Option<ImageHandle>,
|
||||||
|
///Sub-UV coordinates for the texture
|
||||||
|
texture_uv: Option<Corners<Vec2>>,
|
||||||
|
///Rounded corners
|
||||||
|
rounded_corners: Option<RoundedCorners>,
|
||||||
},
|
},
|
||||||
|
/// Draw text using the specified font, size, color, and position
|
||||||
Text {
|
Text {
|
||||||
///Position in pixels
|
///Position in pixels
|
||||||
position: Vec2,
|
position: Vec2,
|
||||||
///Font size
|
///Font size
|
||||||
size: u8,
|
size: u16,
|
||||||
///Color (RGBA)
|
///Color (RGBA)
|
||||||
color: Vec4,
|
color: Vec4,
|
||||||
///Text to draw
|
///Text to draw
|
||||||
|
@ -27,14 +51,21 @@ pub enum UiDrawCommand {
|
||||||
///Font handle to use
|
///Font handle to use
|
||||||
font: FontHandle,
|
font: FontHandle,
|
||||||
},
|
},
|
||||||
|
/// Push a transformation matrix to the stack
|
||||||
|
PushTransform(Affine2),
|
||||||
|
/// Pop a transformation matrix from the stack
|
||||||
|
PopTransform,
|
||||||
|
//TODO PushClip PopClip
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// List of draw commands
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct UiDrawCommands {
|
pub struct UiDrawCommandList {
|
||||||
pub commands: Vec<UiDrawCommand>,
|
pub commands: Vec<UiDrawCommand>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UiDrawCommands {
|
impl UiDrawCommandList {
|
||||||
|
/// Add a draw command to the list
|
||||||
pub fn add(&mut self, command: UiDrawCommand) {
|
pub fn add(&mut self, command: UiDrawCommand) {
|
||||||
self.commands.push(command);
|
self.commands.push(command);
|
||||||
}
|
}
|
||||||
|
@ -47,174 +78,319 @@ impl UiDrawCommands {
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
/// A vertex for UI rendering
|
||||||
pub enum BindTexture {
|
#[derive(Clone, Copy, Debug, PartialEq, Default)]
|
||||||
FontTexture,
|
|
||||||
//UserDefined(usize),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
||||||
pub struct UiVertex {
|
pub struct UiVertex {
|
||||||
pub position: Vec2,
|
pub position: Vec2,
|
||||||
pub color: Vec4,
|
pub color: Vec4,
|
||||||
pub uv: Vec2,
|
pub uv: Vec2,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents a single draw call (vertices + indices), should be handled by the render backend
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct UiDrawCall {
|
pub struct UiDrawCall {
|
||||||
pub vertices: Vec<UiVertex>,
|
pub vertices: Vec<UiVertex>,
|
||||||
pub indices: Vec<u32>,
|
pub indices: Vec<u32>,
|
||||||
pub bind_texture: Option<BindTexture>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
impl UiDrawCall {
|
||||||
pub struct UiDrawPlan {
|
/// Tesselate the UI and build a complete draw plan from a list of draw commands
|
||||||
pub calls: Vec<UiDrawCall>
|
pub(crate) fn build(draw_commands: &UiDrawCommandList, atlas: &mut TextureAtlasManager, text_renderer: &mut TextRenderer) -> Self {
|
||||||
}
|
let mut trans_stack = Vec::new();
|
||||||
|
let mut draw_call = UiDrawCall::default();
|
||||||
|
|
||||||
struct CallSwapper {
|
//HACK: atlas may get resized while creating new glyphs,
|
||||||
calls: Vec<UiDrawCall>,
|
//which invalidates all uvs, causing corrupted-looking texture
|
||||||
call: UiDrawCall,
|
//so we need to pregenerate font textures before generating any vertices
|
||||||
|
//we are doing *a lot* of double work here, but it's the easiest way to avoid the issue
|
||||||
|
for comamnd in &draw_commands.commands {
|
||||||
|
if let UiDrawCommand::Text { text, font: font_handle, size, .. } = comamnd {
|
||||||
|
let mut layout = Layout::new(CoordinateSystem::PositiveYDown);
|
||||||
|
layout.append(
|
||||||
|
&[text_renderer.internal_font(*font_handle)],
|
||||||
|
&TextStyle::new(text, *size as f32, 0)
|
||||||
|
);
|
||||||
|
let glyphs = layout.glyphs();
|
||||||
|
for layout_glyph in glyphs {
|
||||||
|
if !layout_glyph.char_data.rasterize() { continue }
|
||||||
|
text_renderer.glyph(atlas, *font_handle, layout_glyph.parent, layout_glyph.key.px as u8);
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CallSwapper {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
calls: vec![],
|
|
||||||
call: UiDrawCall::default(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn current(&self) -> &UiDrawCall {
|
//note to future self:
|
||||||
&self.call
|
//RESIZING OR ADDING STUFF TO ATLAS AFTER THIS POINT IS A BIG NO-NO,
|
||||||
}
|
//DON'T DO IT EVER AGAIN UNLESS YOU WANT TO SPEND HOURS DEBUGGING
|
||||||
|
|
||||||
pub fn current_mut(&mut self) -> &mut UiDrawCall {
|
atlas.lock_atlas = true;
|
||||||
&mut self.call
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn swap(&mut self) {
|
|
||||||
self.calls.push(std::mem::replace(&mut self.call, UiDrawCall::default()));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn finish(mut self) -> Vec<UiDrawCall> {
|
|
||||||
self.calls.push(self.call);
|
|
||||||
self.calls
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UiDrawPlan {
|
|
||||||
pub fn build(draw_commands: &UiDrawCommands, tr: &mut TextRenderer) -> Self {
|
|
||||||
let mut swapper = CallSwapper::new();
|
|
||||||
let mut prev_command = None;
|
|
||||||
for command in &draw_commands.commands {
|
for command in &draw_commands.commands {
|
||||||
|
match command {
|
||||||
|
UiDrawCommand::PushTransform(trans) => {
|
||||||
|
//Take note of the current index, and the transformation matrix\
|
||||||
|
//We will actually apply the transformation matrix when we pop it,
|
||||||
|
//to all vertices between the current index and the index we pushed
|
||||||
|
trans_stack.push((trans, draw_call.vertices.len() as u32));
|
||||||
|
},
|
||||||
|
UiDrawCommand::PopTransform => {
|
||||||
|
//Pop the transformation matrix and apply it to all vertices between the current index and the index we pushed
|
||||||
|
let (&trans, idx) = trans_stack.pop().expect("Unbalanced push/pop transform");
|
||||||
|
|
||||||
let do_swap = if let Some(prev_command) = prev_command {
|
//If Push is immediately followed by a pop (which is dumb but possible), we don't need to do anything
|
||||||
std::mem::discriminant(prev_command) != std::mem::discriminant(command)
|
//(this can also happen if push and pop are separated by a draw command that doesn't add any vertices, like a text command with an empty string)
|
||||||
|
if idx == draw_call.vertices.len() as u32 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
//Kinda a hack:
|
||||||
|
//We want to apply the transform aronnd the center, so we need to compute the center of the vertices
|
||||||
|
//We won't actually do that, we will compute the center of the bounding box of the vertices
|
||||||
|
let mut min = Vec2::splat(std::f32::INFINITY);
|
||||||
|
let mut max = Vec2::splat(std::f32::NEG_INFINITY);
|
||||||
|
for v in &draw_call.vertices[idx as usize..] {
|
||||||
|
min = min.min(v.position);
|
||||||
|
max = max.max(v.position);
|
||||||
|
}
|
||||||
|
//TODO: make the point of transform configurable
|
||||||
|
let center = (min + max) / 2.;
|
||||||
|
|
||||||
|
//Apply trans mtx to all vertices between idx and the current index
|
||||||
|
for v in &mut draw_call.vertices[idx as usize..] {
|
||||||
|
v.position -= center;
|
||||||
|
v.position = trans.transform_point2(v.position);
|
||||||
|
v.position += center;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
UiDrawCommand::Rectangle { position, size, color, texture, texture_uv, rounded_corners } => {
|
||||||
|
let uvs = texture
|
||||||
|
.map(|x| atlas.get_uv(x))
|
||||||
|
.flatten()
|
||||||
|
.map(|guv| {
|
||||||
|
if let Some(texture_uv) = texture_uv {
|
||||||
|
//XXX: this may not work if the texture is rotated
|
||||||
|
//also is this slow?
|
||||||
|
|
||||||
|
let top = guv.top_left.lerp(guv.top_right, texture_uv.top_left.x);
|
||||||
|
let bottom = guv.bottom_left.lerp(guv.bottom_right, texture_uv.top_left.x);
|
||||||
|
let top_left = top.lerp(bottom, texture_uv.top_left.y);
|
||||||
|
|
||||||
|
let top = guv.top_left.lerp(guv.top_right, texture_uv.top_right.x);
|
||||||
|
let bottom = guv.bottom_left.lerp(guv.bottom_right, texture_uv.top_right.x);
|
||||||
|
let top_right = top.lerp(bottom, texture_uv.top_right.y);
|
||||||
|
|
||||||
|
let top = guv.top_left.lerp(guv.top_right, texture_uv.bottom_left.x);
|
||||||
|
let bottom = guv.bottom_left.lerp(guv.bottom_right, texture_uv.bottom_left.x);
|
||||||
|
let bottom_left = top.lerp(bottom, texture_uv.bottom_left.y);
|
||||||
|
|
||||||
|
let top = guv.top_left.lerp(guv.top_right, texture_uv.bottom_right.x);
|
||||||
|
let bottom = guv.bottom_left.lerp(guv.bottom_right, texture_uv.bottom_right.x);
|
||||||
|
let bottom_right = top.lerp(bottom, texture_uv.bottom_right.y);
|
||||||
|
|
||||||
|
Corners { top_left, top_right, bottom_left, bottom_right }
|
||||||
} else {
|
} else {
|
||||||
false
|
guv
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or(Corners::all(Vec2::ZERO));
|
||||||
|
|
||||||
|
let vidx = draw_call.vertices.len() as u32;
|
||||||
|
if let Some(corner) = rounded_corners.filter(|x| x.radius.max_f32() > 0.0) {
|
||||||
|
//this code is stupid as fuck
|
||||||
|
//but it works... i think?
|
||||||
|
//maybe some verts end up missing, but it's close enough...
|
||||||
|
|
||||||
|
//Random vert in the center for no reason
|
||||||
|
//lol
|
||||||
|
draw_call.vertices.push(UiVertex {
|
||||||
|
position: *position + *size * vec2(0.5, 0.5),
|
||||||
|
color: (color.bottom_left + color.bottom_right + color.top_left + color.top_right) / 4.,
|
||||||
|
//TODO: fix this uv
|
||||||
|
uv: vec2(0., 0.),
|
||||||
|
});
|
||||||
|
|
||||||
|
//TODO: fix some corners tris being invisible (but it's already close enough lol)
|
||||||
|
let rounded_corner_verts = corner.point_count.get() as u32;
|
||||||
|
for i in 0..rounded_corner_verts {
|
||||||
|
let cratio = i as f32 / rounded_corner_verts as f32;
|
||||||
|
let angle = cratio * std::f32::consts::PI * 0.5;
|
||||||
|
let x = angle.sin();
|
||||||
|
let y = angle.cos();
|
||||||
|
|
||||||
|
let mut corner_impl = |rp: Vec2, color: &Corners<Vec4>| {
|
||||||
|
let rrp = rp / *size;
|
||||||
|
let color_at_point =
|
||||||
|
color.bottom_right * rrp.x * rrp.y +
|
||||||
|
color.top_right * rrp.x * (1. - rrp.y) +
|
||||||
|
color.bottom_left * (1. - rrp.x) * rrp.y +
|
||||||
|
color.top_left * (1. - rrp.x) * (1. - rrp.y);
|
||||||
|
let uv_at_point =
|
||||||
|
uvs.bottom_right * rrp.x * rrp.y +
|
||||||
|
uvs.top_right * rrp.x * (1. - rrp.y) +
|
||||||
|
uvs.bottom_left * (1. - rrp.x) * rrp.y +
|
||||||
|
uvs.top_left * (1. - rrp.x) * (1. - rrp.y);
|
||||||
|
draw_call.vertices.push(UiVertex {
|
||||||
|
position: *position + rp,
|
||||||
|
color: color_at_point,
|
||||||
|
uv: uv_at_point,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
if do_swap {
|
//Top-right corner
|
||||||
swapper.swap();
|
corner_impl(
|
||||||
}
|
vec2(x, 1. - y) * corner.radius.top_right + vec2(size.x - corner.radius.top_right, 0.),
|
||||||
|
color,
|
||||||
|
);
|
||||||
|
//Bottom-right corner
|
||||||
|
corner_impl(
|
||||||
|
vec2(x - 1., y) * corner.radius.bottom_right + vec2(size.x, size.y - corner.radius.bottom_right),
|
||||||
|
color,
|
||||||
|
);
|
||||||
|
//Bottom-left corner
|
||||||
|
corner_impl(
|
||||||
|
vec2(1. - x, y) * corner.radius.bottom_left + vec2(0., size.y - corner.radius.bottom_left),
|
||||||
|
color,
|
||||||
|
);
|
||||||
|
//Top-left corner
|
||||||
|
corner_impl(
|
||||||
|
vec2(1. - x, 1. - y) * corner.radius.top_left,
|
||||||
|
color,
|
||||||
|
);
|
||||||
|
|
||||||
if do_swap || prev_command.is_none() {
|
// mental illness:
|
||||||
match command {
|
if i > 0 {
|
||||||
UiDrawCommand::Rectangle { .. } => (),
|
draw_call.indices.extend([
|
||||||
UiDrawCommand::Text { .. } => {
|
//Top-right corner
|
||||||
swapper.current_mut().bind_texture = Some(BindTexture::FontTexture);
|
vidx,
|
||||||
|
vidx + 1 + (i - 1) * 4,
|
||||||
|
vidx + 1 + i * 4,
|
||||||
|
//Bottom-right corner
|
||||||
|
vidx,
|
||||||
|
vidx + 1 + (i - 1) * 4 + 1,
|
||||||
|
vidx + 1 + i * 4 + 1,
|
||||||
|
//Bottom-left corner
|
||||||
|
vidx,
|
||||||
|
vidx + 1 + (i - 1) * 4 + 2,
|
||||||
|
vidx + 1 + i * 4 + 2,
|
||||||
|
//Top-left corner
|
||||||
|
vidx,
|
||||||
|
vidx + 1 + (i - 1) * 4 + 3,
|
||||||
|
vidx + 1 + i * 4 + 3,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
//Fill in the rest
|
||||||
|
//mental illness 2:
|
||||||
match command {
|
draw_call.indices.extend([
|
||||||
UiDrawCommand::Rectangle { position, size, color } => {
|
//Top
|
||||||
let vidx = swapper.current().vertices.len() as u32;
|
vidx,
|
||||||
swapper.current_mut().indices.extend([vidx, vidx + 1, vidx + 2, vidx, vidx + 2, vidx + 3]);
|
vidx + 4,
|
||||||
swapper.current_mut().vertices.extend([
|
vidx + 1,
|
||||||
|
//Right?, i think
|
||||||
|
vidx,
|
||||||
|
vidx + 1 + (rounded_corner_verts - 1) * 4,
|
||||||
|
vidx + 1 + (rounded_corner_verts - 1) * 4 + 1,
|
||||||
|
//Left???
|
||||||
|
vidx,
|
||||||
|
vidx + 1 + (rounded_corner_verts - 1) * 4 + 2,
|
||||||
|
vidx + 1 + (rounded_corner_verts - 1) * 4 + 3,
|
||||||
|
//Bottom???
|
||||||
|
vidx,
|
||||||
|
vidx + 3,
|
||||||
|
vidx + 2,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
//...Normal rectangle
|
||||||
|
draw_call.indices.extend([vidx, vidx + 1, vidx + 2, vidx, vidx + 2, vidx + 3]);
|
||||||
|
draw_call.vertices.extend([
|
||||||
UiVertex {
|
UiVertex {
|
||||||
position: *position,
|
position: *position,
|
||||||
color: *color,
|
color: color.top_left,
|
||||||
uv: vec2(0.0, 0.0),
|
uv: uvs.top_left,
|
||||||
},
|
},
|
||||||
UiVertex {
|
UiVertex {
|
||||||
position: *position + vec2(size.x, 0.0),
|
position: *position + vec2(size.x, 0.0),
|
||||||
color: *color,
|
color: color.top_right,
|
||||||
uv: vec2(1.0, 0.0),
|
uv: uvs.top_right,
|
||||||
},
|
},
|
||||||
UiVertex {
|
UiVertex {
|
||||||
position: *position + *size,
|
position: *position + *size,
|
||||||
color: *color,
|
color: color.bottom_right,
|
||||||
uv: vec2(1.0, 1.0),
|
uv: uvs.bottom_right,
|
||||||
},
|
},
|
||||||
UiVertex {
|
UiVertex {
|
||||||
position: *position + vec2(0.0, size.y),
|
position: *position + vec2(0.0, size.y),
|
||||||
color: *color,
|
color: color.bottom_left,
|
||||||
uv: vec2(0.0, 1.0),
|
uv: uvs.bottom_left,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
UiDrawCommand::Text { position, size, color, text, font } => {
|
UiDrawCommand::Text { position, size, color, text, font: font_handle } => {
|
||||||
|
if text.is_empty() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
//XXX: should we be doing this every time?
|
//XXX: should we be doing this every time?
|
||||||
let mut layout = Layout::new(CoordinateSystem::PositiveYDown);
|
let mut layout = Layout::new(CoordinateSystem::PositiveYDown);
|
||||||
layout.append(
|
layout.append(
|
||||||
&[tr.internal_font(*font)],
|
&[text_renderer.internal_font(*font_handle)],
|
||||||
&TextStyle::new(text, *size as f32, 0)
|
&TextStyle::new(text, *size as f32, 0)
|
||||||
);
|
);
|
||||||
let glyphs = layout.glyphs();
|
let glyphs = layout.glyphs();
|
||||||
|
|
||||||
//let mut rpos_x = 0.;
|
|
||||||
for layout_glyph in glyphs {
|
for layout_glyph in glyphs {
|
||||||
if !layout_glyph.char_data.rasterize() {
|
if !layout_glyph.char_data.rasterize() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
let vidx = swapper.current().vertices.len() as u32;
|
let vidx = draw_call.vertices.len() as u32;
|
||||||
let glyph = tr.glyph(*font, layout_glyph.parent, layout_glyph.key.px as u8);
|
let glyph = text_renderer.glyph(atlas, *font_handle, layout_glyph.parent, layout_glyph.key.px as u8);
|
||||||
//rpos_x += glyph.metrics.advance_width;//glyph.metrics.advance_width;
|
let uv = atlas.get_uv(glyph.texture).unwrap();
|
||||||
swapper.current_mut().indices.extend([vidx, vidx + 1, vidx + 2, vidx, vidx + 2, vidx + 3]);
|
draw_call.indices.extend([vidx, vidx + 1, vidx + 2, vidx, vidx + 2, vidx + 3]);
|
||||||
let p0x = glyph.position.x as f32 / 1024.;
|
draw_call.vertices.extend([
|
||||||
let p1x = (glyph.position.x + glyph.size.x as i32) as f32 / 1024.;
|
|
||||||
let p0y = glyph.position.y as f32 / 1024.;
|
|
||||||
let p1y = (glyph.position.y + glyph.size.y as i32) as f32 / 1024.;
|
|
||||||
swapper.current_mut().vertices.extend([
|
|
||||||
UiVertex {
|
UiVertex {
|
||||||
position: *position + vec2(layout_glyph.x, layout_glyph.y),
|
position: *position + vec2(layout_glyph.x, layout_glyph.y),
|
||||||
color: *color,
|
color: *color,
|
||||||
uv: vec2(p0x, p0y),
|
uv: uv.top_left,
|
||||||
},
|
},
|
||||||
UiVertex {
|
UiVertex {
|
||||||
position: *position + vec2(layout_glyph.x + glyph.metrics.width as f32, layout_glyph.y),
|
position: *position + vec2(layout_glyph.x + glyph.metrics.width as f32, layout_glyph.y),
|
||||||
color: *color,
|
color: *color,
|
||||||
uv: vec2(p1x, p0y),
|
uv: uv.top_right,
|
||||||
},
|
},
|
||||||
UiVertex {
|
UiVertex {
|
||||||
position: *position + vec2(layout_glyph.x + glyph.metrics.width as f32, layout_glyph.y + glyph.metrics.height as f32),
|
position: *position + vec2(layout_glyph.x + glyph.metrics.width as f32, layout_glyph.y + glyph.metrics.height as f32),
|
||||||
color: *color,
|
color: *color,
|
||||||
uv: vec2(p1x, p1y),
|
uv: uv.bottom_right,
|
||||||
},
|
},
|
||||||
UiVertex {
|
UiVertex {
|
||||||
position: *position + vec2(layout_glyph.x, layout_glyph.y + glyph.metrics.height as f32),
|
position: *position + vec2(layout_glyph.x, layout_glyph.y + glyph.metrics.height as f32),
|
||||||
color: *color,
|
color: *color,
|
||||||
uv: vec2(p0x, p1y),
|
uv: uv.bottom_left,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
#[cfg(all(
|
||||||
|
feature = "pixel_perfect_text",
|
||||||
|
not(feature = "pixel_perfect")
|
||||||
|
))] {
|
||||||
|
//Round the position of the vertices to the nearest pixel, unless any transformations are active
|
||||||
|
if trans_stack.is_empty() {
|
||||||
|
for vtx in &mut draw_call.vertices[(vidx as usize)..] {
|
||||||
|
vtx.position = vtx.position.round()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
prev_command = Some(command);
|
|
||||||
}
|
}
|
||||||
Self {
|
|
||||||
calls: swapper.finish()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IfModified<UiDrawPlan> for (bool, &UiDrawPlan) {
|
atlas.lock_atlas = false;
|
||||||
fn if_modified(&self) -> Option<&UiDrawPlan> {
|
|
||||||
match self.0 {
|
#[cfg(feature = "pixel_perfect")]
|
||||||
true => Some(self.1),
|
draw_call.vertices.iter_mut().for_each(|v| {
|
||||||
false => None,
|
v.position = v.position.round()
|
||||||
}
|
});
|
||||||
|
|
||||||
|
draw_call
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
297
hui/src/draw/atlas.rs
Normal file
297
hui/src/draw/atlas.rs
Normal file
|
@ -0,0 +1,297 @@
|
||||||
|
use glam::{uvec2, vec2, UVec2, Vec2};
|
||||||
|
use hashbrown::HashMap;
|
||||||
|
use nohash_hasher::BuildNoHashHasher;
|
||||||
|
use rect_packer::DensePacker;
|
||||||
|
use crate::rect::Corners;
|
||||||
|
|
||||||
|
const RGBA_CHANNEL_COUNT: u32 = 4;
|
||||||
|
//TODO make this work
|
||||||
|
const ALLOW_ROTATION: bool = false;
|
||||||
|
|
||||||
|
/// Texture format of the source texture data
|
||||||
|
#[derive(Default, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum TextureFormat {
|
||||||
|
/// The data is stored in RGBA format, with 1 byte (8 bits) per channel
|
||||||
|
#[default]
|
||||||
|
Rgba,
|
||||||
|
|
||||||
|
/// The data is copied into the Alpha channel, with 1 byte (8 bits) per channel\
|
||||||
|
/// Remaining channels are set to 255 (which can be easily shaded to any color)
|
||||||
|
///
|
||||||
|
/// This format is useful for storing grayscale textures such as icons\
|
||||||
|
/// (Please note that the internal representation is still RGBA, this is just a convenience feature)
|
||||||
|
Grayscale,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Contains a reference to the texture data, and metadata associated with it
|
||||||
|
pub struct TextureAtlasMeta<'a> {
|
||||||
|
/// Texture data\
|
||||||
|
/// The data is stored in RGBA format, with 1 byte (8 bits) per channel
|
||||||
|
pub data: &'a [u8],
|
||||||
|
/// Current size of the texture atlas\
|
||||||
|
/// Please note that this value might change
|
||||||
|
pub size: UVec2,
|
||||||
|
/// True if the atlas has been modified since the beginning of the current frame\
|
||||||
|
/// If this function returns true, the texture atlas should be re-uploaded to the GPU before rendering\
|
||||||
|
pub modified: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Texture handle, stores the internal index of a texture within the texture atlas and can be cheaply copied.
|
||||||
|
///
|
||||||
|
/// Please note that dropping a handle does not deallocate the texture from the atlas, you must do it manually.
|
||||||
|
///
|
||||||
|
/// Only valid for the `UiInstance` that created it.\
|
||||||
|
/// Using it with other instances may result in panics or unexpected behavior.
|
||||||
|
///
|
||||||
|
/// Handle values are not guaranteed to be valid.\
|
||||||
|
/// Creating or transmuting an invalid handle is allowed and is *not* UB.
|
||||||
|
///
|
||||||
|
/// Internal value is an implementation detail and should not be relied upon.
|
||||||
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
|
||||||
|
pub struct ImageHandle {
|
||||||
|
pub(crate) index: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub(crate) struct TextureAllocation {
|
||||||
|
/// Position in the texture atlas\
|
||||||
|
/// (This is an implementation detail and should not be exposed to the user)
|
||||||
|
pub(crate) position: UVec2,
|
||||||
|
|
||||||
|
/// Requested texture size
|
||||||
|
pub size: UVec2,
|
||||||
|
|
||||||
|
/// True if the texture was rotated by 90 degrees\
|
||||||
|
/// (This is an implementation detail and should not be exposed to the user)
|
||||||
|
pub(crate) rotated: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Manages a texture atlas and the allocation of space within it\
|
||||||
|
/// The atlas is alllowed to grow and resize dynamically, as needed
|
||||||
|
pub(crate) struct TextureAtlasManager {
|
||||||
|
packer: DensePacker,
|
||||||
|
count: u32,
|
||||||
|
size: UVec2,
|
||||||
|
data: Vec<u8>,
|
||||||
|
allocations: HashMap<u32, TextureAllocation, BuildNoHashHasher<u32>>,
|
||||||
|
/// Items that have been removed from the allocation list, but still affect
|
||||||
|
remove_queue: Vec<TextureAllocation>,
|
||||||
|
/// True if the atlas has been modified in a way which requires a texture reupload
|
||||||
|
/// since the beginning of the current frame
|
||||||
|
modified: bool,
|
||||||
|
|
||||||
|
/// If true, attempting to modify the atlas in a way which invalidates UVs will cause a panic\
|
||||||
|
/// Used internally to ensure that the UVs do not become invalidated mid-render
|
||||||
|
pub(crate) lock_atlas: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextureAtlasManager {
|
||||||
|
/// Create a new texture atlas with the specified size\
|
||||||
|
/// 512x512 is a good default size for most applications, and the texture atlas can grow dynamically as needed
|
||||||
|
pub fn new(size: UVec2) -> Self {
|
||||||
|
Self {
|
||||||
|
packer: DensePacker::new(size.x as i32, size.y as i32),
|
||||||
|
count: 0,
|
||||||
|
size,
|
||||||
|
data: vec![0; (size.x * size.y * RGBA_CHANNEL_COUNT) as usize],
|
||||||
|
allocations: HashMap::default(),
|
||||||
|
remove_queue: Vec::new(),
|
||||||
|
modified: true,
|
||||||
|
lock_atlas: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resize the texture atlas to the new size in-place, preserving the existing data
|
||||||
|
pub fn resize(&mut self, new_size: UVec2) {
|
||||||
|
if self.lock_atlas {
|
||||||
|
panic!("Attempted to resize the texture atlas while the atlas is locked");
|
||||||
|
}
|
||||||
|
log::trace!("resizing texture atlas to {:?}", new_size);
|
||||||
|
if self.size == new_size {
|
||||||
|
log::warn!("Texture atlas is already the requested size");
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if new_size.x > self.size.x && new_size.y > self.size.y {
|
||||||
|
self.packer.resize(new_size.x as i32, new_size.y as i32);
|
||||||
|
//Resize the data array in-place
|
||||||
|
self.data.resize((new_size.x * new_size.y * RGBA_CHANNEL_COUNT) as usize, 0);
|
||||||
|
for y in (0..self.size.y).rev() {
|
||||||
|
for x in (1..self.size.x).rev() {
|
||||||
|
let idx = ((y * self.size.x + x) * RGBA_CHANNEL_COUNT) as usize;
|
||||||
|
let new_idx = ((y * new_size.x + x) * RGBA_CHANNEL_COUNT) as usize;
|
||||||
|
for c in 0..(RGBA_CHANNEL_COUNT as usize) {
|
||||||
|
self.data[new_idx + c] = self.data[idx + c];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//If scaling down, just recreate the atlas from scratch (since we need to re-pack everything anyway)
|
||||||
|
todo!("Atlas downscaling is not implemented yet");
|
||||||
|
}
|
||||||
|
self.size = new_size;
|
||||||
|
self.modified = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensure that a texture with specified size would fit without resizing on the next allocation attempt\
|
||||||
|
pub fn ensure_fits(&mut self, size: UVec2) {
|
||||||
|
// Plan A: try if any of the existing items in the remove queue would fit the texture
|
||||||
|
// Plan B: purge the remove queue, recreate the packer and try again (might be expensive...!)
|
||||||
|
// TODO: implement these
|
||||||
|
// Plan C: resize the atlas
|
||||||
|
let mut new_size = self.size;
|
||||||
|
while !self.packer.can_pack(size.x as i32, size.y as i32, ALLOW_ROTATION) {
|
||||||
|
new_size *= 2;
|
||||||
|
self.packer.resize(new_size.x as i32, new_size.y as i32);
|
||||||
|
}
|
||||||
|
if new_size != self.size {
|
||||||
|
self.resize(new_size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Allocate a new texture region in the atlas and return a handle to it\
|
||||||
|
/// Returns None if the texture could not be allocated due to lack of space\
|
||||||
|
/// Use `allocate` to allocate a texture and resize the atlas if necessary\
|
||||||
|
/// Does not modify the texture data
|
||||||
|
fn try_allocate(&mut self, size: UVec2) -> Option<ImageHandle> {
|
||||||
|
log::trace!("Allocating texture of size {:?}", size);
|
||||||
|
let result = self.packer.pack(size.x as i32, size.y as i32, ALLOW_ROTATION)?;
|
||||||
|
let index = self.count;
|
||||||
|
self.count += 1;
|
||||||
|
let allocation = TextureAllocation {
|
||||||
|
position: UVec2::new(result.x as u32, result.y as u32),
|
||||||
|
size,
|
||||||
|
//If the size does not match the requested size, the texture was rotated
|
||||||
|
rotated: ALLOW_ROTATION && (result.width != size.x as i32),
|
||||||
|
};
|
||||||
|
self.allocations.insert_unique_unchecked(index, allocation);
|
||||||
|
Some(ImageHandle { index })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Allocate a new texture region in the atlas and resize the atlas if necessary\
|
||||||
|
/// This function should never fail under normal circumstances.\
|
||||||
|
/// May modify the texture data if the atlas is resized
|
||||||
|
pub fn allocate(&mut self, size: UVec2) -> ImageHandle {
|
||||||
|
self.ensure_fits(size);
|
||||||
|
self.try_allocate(size).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Allocate a new texture region in the atlas and copy the data into it\
|
||||||
|
/// This function may resize the atlas as needed, and should never fail under normal circumstances.
|
||||||
|
pub(crate) fn add_rgba(&mut self, width: usize, data: &[u8]) -> ImageHandle {
|
||||||
|
let size = uvec2(width as u32, (data.len() / (width * RGBA_CHANNEL_COUNT as usize)) as u32);
|
||||||
|
let handle: ImageHandle = self.allocate(size);
|
||||||
|
let allocation = self.allocations.get(&handle.index).unwrap();
|
||||||
|
assert!(!allocation.rotated, "Rotated textures are not implemented yet");
|
||||||
|
for y in 0..size.y {
|
||||||
|
for x in 0..size.x {
|
||||||
|
let src_idx = (y * size.x + x) * RGBA_CHANNEL_COUNT;
|
||||||
|
let dst_idx = ((allocation.position.y + y) * self.size.x + allocation.position.x + x) * RGBA_CHANNEL_COUNT;
|
||||||
|
for c in 0..RGBA_CHANNEL_COUNT as usize {
|
||||||
|
self.data[dst_idx as usize + c] = data[src_idx as usize + c];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.modified = true;
|
||||||
|
handle
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Works the same way as [`TextureAtlasManager::add`], but the input data is assumed to be grayscale (1 channel per pixel)\
|
||||||
|
/// The data is copied into the alpha channel of the texture, while all the other channels are set to 255\
|
||||||
|
/// May resize the atlas as needed, and should never fail under normal circumstances.
|
||||||
|
pub(crate) fn add_grayscale(&mut self, width: usize, data: &[u8]) -> ImageHandle {
|
||||||
|
let size = uvec2(width as u32, (data.len() / width) as u32);
|
||||||
|
let handle = self.allocate(size);
|
||||||
|
let allocation = self.allocations.get(&handle.index).unwrap();
|
||||||
|
assert!(!allocation.rotated, "Rotated textures are not implemented yet");
|
||||||
|
for y in 0..size.y {
|
||||||
|
for x in 0..size.x {
|
||||||
|
let src_idx = (y * size.x + x) as usize;
|
||||||
|
let dst_idx = (((allocation.position.y + y) * self.size.x + allocation.position.x + x) * RGBA_CHANNEL_COUNT) as usize;
|
||||||
|
self.data[dst_idx..(dst_idx + RGBA_CHANNEL_COUNT as usize)].copy_from_slice(&[255, 255, 255, data[src_idx]]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.modified = true;
|
||||||
|
handle
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add(&mut self, width: usize, data: &[u8], format: TextureFormat) -> ImageHandle {
|
||||||
|
match format {
|
||||||
|
TextureFormat::Rgba => self.add_rgba(width, data),
|
||||||
|
TextureFormat::Grayscale => self.add_grayscale(width, data),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn add_dummy(&mut self) {
|
||||||
|
let handle = self.allocate((1, 1).into());
|
||||||
|
assert!(handle.index == 0, "Dummy texture handle is not 0");
|
||||||
|
assert!(self.get(handle).unwrap().position == (0, 0).into(), "Dummy texture position is not (0, 0)");
|
||||||
|
self.data[0..4].copy_from_slice(&[255, 255, 255, 255]);
|
||||||
|
self.modified = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn modify(&mut self, handle: ImageHandle) {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove(&mut self, handle: ImageHandle) {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&self, handle: ImageHandle) -> Option<&TextureAllocation> {
|
||||||
|
self.allocations.get(&handle.index)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_uv(&self, handle: ImageHandle) -> Option<Corners<Vec2>> {
|
||||||
|
let info = self.get(handle)?;
|
||||||
|
let atlas_size = self.meta().size.as_vec2();
|
||||||
|
let p0x = info.position.x as f32 / atlas_size.x;
|
||||||
|
let p1x = (info.position.x as f32 + info.size.x as f32) / atlas_size.x;
|
||||||
|
let p0y = info.position.y as f32 / atlas_size.y;
|
||||||
|
let p1y = (info.position.y as f32 + info.size.y as f32) / atlas_size.y;
|
||||||
|
Some(Corners {
|
||||||
|
top_left: vec2(p0x, p0y),
|
||||||
|
top_right: vec2(p1x, p0y),
|
||||||
|
bottom_left: vec2(p0x, p1y),
|
||||||
|
bottom_right: vec2(p1x, p1y),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reset the `is_modified` flag
|
||||||
|
pub(crate) fn reset_modified(&mut self) {
|
||||||
|
self.modified = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn meta(&self) -> TextureAtlasMeta {
|
||||||
|
TextureAtlasMeta {
|
||||||
|
data: &self.data,
|
||||||
|
size: self.size,
|
||||||
|
modified: self.modified,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn context(&self) -> ImageCtx {
|
||||||
|
ImageCtx { atlas: self }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TextureAtlasManager {
|
||||||
|
/// Create a new texture atlas with a default size of 512x512
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new(UVec2::new(512, 512))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Context that allows read-only accss to image metadata
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct ImageCtx<'a> {
|
||||||
|
pub(crate) atlas: &'a TextureAtlasManager,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ImageCtx<'_> {
|
||||||
|
/// Get size of the image with the specified handle
|
||||||
|
///
|
||||||
|
/// Returns None if the handle is invalid for the current context
|
||||||
|
pub fn get_size(&self, handle: ImageHandle) -> Option<UVec2> {
|
||||||
|
self.atlas.get(handle).map(|a| a.size)
|
||||||
|
}
|
||||||
|
}
|
55
hui/src/draw/corner_radius.rs
Normal file
55
hui/src/draw/corner_radius.rs
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
use std::num::NonZeroU16;
|
||||||
|
use crate::rect::Corners;
|
||||||
|
|
||||||
|
//TODO uneven corners (separate width/height for each corner)
|
||||||
|
|
||||||
|
/// Calculate the number of points based on the maximum corner radius
|
||||||
|
fn point_count(corners: Corners<f32>) -> NonZeroU16 {
|
||||||
|
//Increase for higher quality
|
||||||
|
const VTX_PER_CORER_RADIUS_PIXEL: f32 = 0.5;
|
||||||
|
NonZeroU16::new(
|
||||||
|
(corners.max_f32() * VTX_PER_CORER_RADIUS_PIXEL).round() as u16 + 2
|
||||||
|
).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Low-level options for rendering rounded corners
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct RoundedCorners {
|
||||||
|
/// Corner radius of each corner
|
||||||
|
pub radius: Corners<f32>,
|
||||||
|
|
||||||
|
/// Number of points to use for each corner
|
||||||
|
///
|
||||||
|
/// This value affects all corners, regardless of their individual radius
|
||||||
|
pub point_count: NonZeroU16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Corners<f32>> for RoundedCorners {
|
||||||
|
/// Create a new `RoundedCorners` from [`Corners<f32>`](crate::rect::Corners)
|
||||||
|
///
|
||||||
|
/// Point count will be calculated automatically based on the maximum radius
|
||||||
|
fn from(radius: Corners<f32>) -> Self {
|
||||||
|
Self::from_radius(radius)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RoundedCorners {
|
||||||
|
/// Create a new `RoundedCorners` from [`Corners<f32>`](crate::rect::Corners)
|
||||||
|
///
|
||||||
|
/// Point count will be calculated automatically based on the maximum radius
|
||||||
|
pub fn from_radius(radius: Corners<f32>) -> Self {
|
||||||
|
Self {
|
||||||
|
radius,
|
||||||
|
point_count: point_count(radius),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for RoundedCorners {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
radius: Corners::default(),
|
||||||
|
point_count: NonZeroU16::new(8).unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,29 +1,101 @@
|
||||||
use std::any::Any;
|
//! element API and built-in elements like `Container`, `Button`, `Text`, etc.
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
LayoutInfo,
|
draw::{atlas::ImageCtx, UiDrawCommandList},
|
||||||
draw::UiDrawCommands,
|
input::InputCtx,
|
||||||
|
layout::{LayoutInfo, Size2d},
|
||||||
measure::Response,
|
measure::Response,
|
||||||
state::StateRepo
|
rect::Rect,
|
||||||
|
signal::SignalStore,
|
||||||
|
state::StateRepo,
|
||||||
|
text::{FontHandle, TextMeasure},
|
||||||
|
UiInstance,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "builtin_elements")]
|
mod builtin;
|
||||||
mod builtin {
|
|
||||||
pub mod rect;
|
|
||||||
pub mod container;
|
|
||||||
pub mod spacer;
|
|
||||||
pub mod progress_bar;
|
|
||||||
pub mod text;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "builtin_elements")]
|
|
||||||
pub use builtin::*;
|
pub use builtin::*;
|
||||||
|
|
||||||
pub trait UiElement {
|
/// Context for the `Element::measure` function
|
||||||
fn name(&self) -> &'static str { "UiElement" }
|
pub struct MeasureContext<'a> {
|
||||||
fn state_id(&self) -> Option<u64> { None }
|
pub layout: &'a LayoutInfo,
|
||||||
fn is_stateful(&self) -> bool { self.state_id().is_some() }
|
pub state: &'a StateRepo,
|
||||||
fn is_stateless(&self) -> bool { self.state_id().is_none() }
|
pub text_measure: TextMeasure<'a>,
|
||||||
fn init_state(&self) -> Option<Box<dyn Any>> { None }
|
pub current_font: FontHandle,
|
||||||
fn measure(&self, state: &StateRepo, layout: &LayoutInfo) -> Response;
|
pub images: ImageCtx<'a>,
|
||||||
fn process(&self, measure: &Response, state: &mut StateRepo, layout: &LayoutInfo, draw: &mut UiDrawCommands);
|
//XXX: should measure have a reference to input?
|
||||||
|
//pub input: InputCtx<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Context for the `Element::process` function
|
||||||
|
pub struct ProcessContext<'a> {
|
||||||
|
pub measure: &'a Response,
|
||||||
|
pub layout: &'a LayoutInfo,
|
||||||
|
pub draw: &'a mut UiDrawCommandList,
|
||||||
|
pub state: &'a mut StateRepo,
|
||||||
|
pub text_measure: TextMeasure<'a>,
|
||||||
|
pub current_font: FontHandle,
|
||||||
|
pub images: ImageCtx<'a>,
|
||||||
|
pub input: InputCtx<'a>,
|
||||||
|
pub signal: &'a mut SignalStore,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait UiElement {
|
||||||
|
/// Get the name of the element (in lower case)
|
||||||
|
///
|
||||||
|
/// For example, "button" or "progress_bar"
|
||||||
|
fn name(&self) -> &'static str;
|
||||||
|
|
||||||
|
/// Get the requested UiElement size
|
||||||
|
///
|
||||||
|
/// You should implement this function whenever possible, otherwise some features may not work at all, such as the `Remaining` size
|
||||||
|
fn size(&self) -> Option<Size2d> { None }
|
||||||
|
|
||||||
|
/// Measure step, guaranteed to be called before the `process` step\
|
||||||
|
/// May be called multiple times per single frame, so it should not contain any expensive calls\
|
||||||
|
/// This function may not mutate any state.\
|
||||||
|
///
|
||||||
|
/// This function should return the size of the element along with any hints or layout metadata
|
||||||
|
fn measure(&self, ctx: MeasureContext) -> Response;
|
||||||
|
|
||||||
|
/// Process step, guaranteed to be called after the `measure` step\
|
||||||
|
/// You should process the user inputs and render the element here.
|
||||||
|
fn process(&self, ctx: ProcessContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A list of elements\
|
||||||
|
/// Use the [`add`](`ElementList::add`) method to add elements to the list
|
||||||
|
pub struct ElementList(pub Vec<Box<dyn UiElement>>);
|
||||||
|
|
||||||
|
impl ElementList {
|
||||||
|
/// Add an element to the list
|
||||||
|
pub fn add(&mut self, element: impl UiElement + 'static) {
|
||||||
|
self.0.push(Box::new(element))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new `ElementList` from a callback\
|
||||||
|
/// The callback will be called with a reference to the newly list
|
||||||
|
pub(crate) fn from_callback(cb: impl FnOnce(&mut ElementList)) -> Self {
|
||||||
|
let mut list = ElementList(Vec::new());
|
||||||
|
cb(&mut list);
|
||||||
|
list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extension trait for [`UiElement`] that adds the [`add_child`] and [`add_root`] methods
|
||||||
|
pub trait UiElementExt: UiElement {
|
||||||
|
/// Add element as a child/nested element.
|
||||||
|
fn add_child(self, ui: &mut ElementList);
|
||||||
|
|
||||||
|
/// Add element as a ui root.
|
||||||
|
fn add_root(self, ui: &mut UiInstance, max_size: impl Into<Rect>);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: UiElement + 'static> UiElementExt for T {
|
||||||
|
fn add_child(self, ui: &mut ElementList) {
|
||||||
|
ui.add(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_root(self, ui: &mut UiInstance, rect: impl Into<Rect>) {
|
||||||
|
ui.add(self, rect);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
43
hui/src/element/builtin.rs
Normal file
43
hui/src/element/builtin.rs
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
// Layout stuff:
|
||||||
|
|
||||||
|
#[cfg(feature = "el_container")]
|
||||||
|
pub mod container;
|
||||||
|
|
||||||
|
#[cfg(feature = "el_frame_view")]
|
||||||
|
pub mod frame_view;
|
||||||
|
|
||||||
|
#[cfg(feature = "el_spacer")]
|
||||||
|
pub mod spacer;
|
||||||
|
|
||||||
|
#[cfg(feature = "el_br")]
|
||||||
|
pub mod br;
|
||||||
|
|
||||||
|
// Basic elements:
|
||||||
|
|
||||||
|
#[cfg(feature = "el_text")]
|
||||||
|
pub mod text;
|
||||||
|
|
||||||
|
#[cfg(feature = "el_image")]
|
||||||
|
pub mod image;
|
||||||
|
|
||||||
|
// "Extras":
|
||||||
|
// (meant to be replaced if needed)
|
||||||
|
|
||||||
|
#[cfg(feature = "el_progress_bar")]
|
||||||
|
pub mod progress_bar;
|
||||||
|
|
||||||
|
#[cfg(feature = "el_slider")]
|
||||||
|
pub mod slider;
|
||||||
|
|
||||||
|
// Wrappers:
|
||||||
|
|
||||||
|
#[cfg(feature = "el_transformer")]
|
||||||
|
pub mod transformer;
|
||||||
|
|
||||||
|
#[cfg(feature = "el_interactable")]
|
||||||
|
pub mod interactable;
|
||||||
|
|
||||||
|
//TODO add: Image
|
||||||
|
//TODO add: OverlayContainer (for simply laying multiple elements on top of each other)
|
||||||
|
//TODO add: Button, Checkbox, Dropdown, Input, Radio, Slider, Textarea, Toggle, etc.
|
||||||
|
//TODO add: some sort of "flexible" container (like a poor man's flexbox)
|
22
hui/src/element/builtin/br.rs
Normal file
22
hui/src/element/builtin/br.rs
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
use crate::{
|
||||||
|
element::{MeasureContext, ProcessContext, UiElement},
|
||||||
|
measure::Response
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default)]
|
||||||
|
pub struct Break;
|
||||||
|
|
||||||
|
impl UiElement for Break {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"break"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn measure(&self, _: MeasureContext) -> Response {
|
||||||
|
Response {
|
||||||
|
should_wrap: true,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process(&self, _: ProcessContext) {}
|
||||||
|
}
|
|
@ -1,237 +1,525 @@
|
||||||
use glam::{Vec2, vec2, Vec4};
|
//! a container element that can hold and layout multiple children elements
|
||||||
|
|
||||||
|
use derive_setters::Setters;
|
||||||
|
use glam::{Vec2, vec2};
|
||||||
use crate::{
|
use crate::{
|
||||||
UiDirection,
|
element::{ElementList, MeasureContext, ProcessContext, UiElement},
|
||||||
UiSize,
|
frame::{Frame, RectFrame},
|
||||||
LayoutInfo,
|
layout::{compute_size, Alignment, Alignment2d, Direction, LayoutInfo, Size, Size2d, WrapBehavior},
|
||||||
draw::{UiDrawCommand, UiDrawCommands},
|
measure::{Hints, Response},
|
||||||
measure::{Response, Hints},
|
rect::Sides,
|
||||||
state::StateRepo,
|
|
||||||
element::UiElement
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
//XXX: add Order/Direction::Forward/Reverse or sth?
|
||||||
pub enum Alignment {
|
//TODO: clip children flag
|
||||||
Begin,
|
//TODO: borders
|
||||||
Center,
|
//TODO: min/max size
|
||||||
End,
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct CudLine {
|
||||||
|
start_idx: usize,
|
||||||
|
content_size: Vec2,
|
||||||
|
remaining_space: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Border {
|
struct ContainerUserData {
|
||||||
pub color: Vec4,
|
lines: Vec<CudLine>,
|
||||||
pub width: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Clone, Copy, PartialEq, Eq, Debug)]
|
|
||||||
pub struct Sides<T> {
|
|
||||||
pub top: T,
|
|
||||||
pub bottom: T,
|
|
||||||
pub left: T,
|
|
||||||
pub right: T,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Clone> Sides<T> {
|
|
||||||
#[inline]
|
|
||||||
pub fn all(value: T) -> Self {
|
|
||||||
Self {
|
|
||||||
top: value.clone(),
|
|
||||||
bottom: value.clone(),
|
|
||||||
left: value.clone(),
|
|
||||||
right: value,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn horizontal_vertical(horizontal: T, vertical: T) -> Self {
|
|
||||||
Self {
|
|
||||||
top: vertical.clone(),
|
|
||||||
bottom: vertical,
|
|
||||||
left: horizontal.clone(),
|
|
||||||
right: horizontal,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A container element that can hold and layout multiple children elements
|
||||||
|
#[derive(Setters)]
|
||||||
|
#[setters(prefix = "with_")]
|
||||||
pub struct Container {
|
pub struct Container {
|
||||||
// pub min_size: (UiSize, UiSize),
|
/// Size of the container
|
||||||
// pub max_size: (UiSize, UiSize),
|
#[setters(into)]
|
||||||
pub size: (UiSize, UiSize),
|
pub size: Size2d,
|
||||||
pub direction: UiDirection,
|
|
||||||
//pub reverse: bool,
|
/// Layout direction (horizontal/vertical)
|
||||||
|
pub direction: Direction,
|
||||||
|
|
||||||
|
//XXX: should we have separate gap value for primary and secondary (when wrapped, between lines of elements) axis?
|
||||||
|
|
||||||
|
/// Gap between children elements
|
||||||
pub gap: f32,
|
pub gap: f32,
|
||||||
|
|
||||||
|
/// Padding inside the container (distance from the edges to the children elements)
|
||||||
|
#[setters(into)]
|
||||||
pub padding: Sides<f32>,
|
pub padding: Sides<f32>,
|
||||||
///Primary/secondary axis
|
|
||||||
pub align: (Alignment, Alignment),
|
/// Alignment of the children elements on X and Y axis
|
||||||
pub background: Option<Vec4>,
|
#[setters(into)]
|
||||||
pub borders: Sides<Option<Border>>,
|
pub align: Alignment2d,
|
||||||
pub clip: bool,
|
|
||||||
pub elements: Vec<Box<dyn UiElement>>,
|
#[setters(skip)]
|
||||||
|
pub background_frame: Box<dyn Frame>,
|
||||||
|
|
||||||
|
/// Controls if wrapping is enabled
|
||||||
|
#[setters(into)]
|
||||||
|
pub wrap: WrapBehavior,
|
||||||
|
|
||||||
|
/// List of children elements
|
||||||
|
#[setters(skip)]
|
||||||
|
pub children: ElementList,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Container {
|
||||||
|
pub fn with_children(mut self, ui: impl FnOnce(&mut ElementList)) -> Self {
|
||||||
|
self.children.0.extend(ElementList::from_callback(ui).0);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_background(mut self, frame: impl Frame + 'static) -> Self {
|
||||||
|
self.background_frame = Box::new(frame);
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Container {
|
impl Default for Container {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
// min_size: (UiSize::Auto, UiSize::Auto),
|
size: (Size::Auto, Size::Auto).into(),
|
||||||
// max_size: (UiSize::Auto, UiSize::Auto),
|
direction: Direction::Vertical,
|
||||||
size: (UiSize::Auto, UiSize::Auto),
|
|
||||||
direction: UiDirection::Vertical,
|
|
||||||
//reverse: false,
|
|
||||||
gap: 0.,
|
gap: 0.,
|
||||||
padding: Sides::all(0.),
|
padding: Sides::all(0.),
|
||||||
align: (Alignment::Begin, Alignment::Begin),
|
align: Alignment2d::default(),
|
||||||
background: Default::default(),
|
background_frame: Box::<RectFrame>::default(),
|
||||||
borders: Default::default(),
|
wrap: WrapBehavior::Allow,
|
||||||
clip: Default::default(),
|
children: ElementList(Vec::new()),
|
||||||
elements: Vec::new(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Container {
|
impl Container {
|
||||||
pub fn measure_max_inner_size(&self, layout: &LayoutInfo) -> Vec2 {
|
pub fn measure_max_inner_size(&self, layout: &LayoutInfo) -> Vec2 {
|
||||||
let outer_size_x = match self.size.0 {
|
// let outer_size_x = match self.size.width {
|
||||||
UiSize::Auto => layout.max_size.x,
|
// Size::Auto => layout.max_size.x,
|
||||||
UiSize::Percentage(p) => layout.max_size.x * p,
|
// Size::Relative(p) => layout.max_size.x * p,
|
||||||
UiSize::Pixels(p) => p,
|
// Size::Absolute(p) => p,
|
||||||
};
|
// Size::Remaining(p) => match layout.direction {
|
||||||
let outer_size_y = match self.size.1 {
|
// Direction::Horizontal => layout.remaining_space.unwrap_or(layout.max_size.x) * p,
|
||||||
UiSize::Auto => layout.max_size.y,
|
// Direction::Vertical => layout.max_size.x,
|
||||||
UiSize::Percentage(p) => layout.max_size.y * p,
|
// }
|
||||||
UiSize::Pixels(p) => p,
|
// };
|
||||||
};
|
// let outer_size_y = match self.size.height {
|
||||||
|
// Size::Auto => layout.max_size.y,
|
||||||
|
// Size::Relative(p) => layout.max_size.y * p,
|
||||||
|
// Size::Absolute(p) => p,
|
||||||
|
// };
|
||||||
|
let outer_size = compute_size(layout, self.size, layout.max_size);
|
||||||
vec2(
|
vec2(
|
||||||
outer_size_x - (self.padding.left + self.padding.right),
|
outer_size.x - (self.padding.left + self.padding.right),
|
||||||
outer_size_y - (self.padding.top + self.padding.bottom),
|
outer_size.y - (self.padding.top + self.padding.bottom),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UiElement for Container {
|
impl UiElement for Container {
|
||||||
fn measure(&self, state: &StateRepo, layout: &LayoutInfo) -> Response {
|
fn name(&self) -> &'static str {
|
||||||
let mut size = Vec2::ZERO;
|
"container"
|
||||||
//if matches!(self.size.0, UiSize::Auto) || matches!(self.size.1, UiSize::Auto) {
|
}
|
||||||
let mut leftover_gap = Vec2::ZERO;
|
|
||||||
for element in &self.elements {
|
fn size(&self) -> Option<Size2d> {
|
||||||
let measure = element.measure(state, &LayoutInfo {
|
Some(self.size)
|
||||||
position: layout.position + size,
|
}
|
||||||
max_size: self.measure_max_inner_size(layout), // - size TODO
|
|
||||||
direction: self.direction,
|
fn measure(&self, ctx: MeasureContext) -> Response {
|
||||||
});
|
// XXX: If both axes are NOT set to auto, we should be able quickly return the size
|
||||||
match self.direction {
|
// ... but we can't, because we need to measure the children to get the inner_content_size and user_data values
|
||||||
UiDirection::Horizontal => {
|
// this is a potential optimization opportunity, maybe we could postpone this to the process call
|
||||||
size.x += measure.size.x + self.gap;
|
// as it's guaranteed to be called only ONCE, while measure is assumed to be cheap and called multiple times
|
||||||
size.y = size.y.max(measure.size.y);
|
// ... we could also implement some sort of "global" caching for the measure call (to prevent traversal of the same tree multiple times),
|
||||||
leftover_gap.x = self.gap;
|
// but that's a bit more complex and probably impossible with the current design of the measure/process calls
|
||||||
|
|
||||||
|
// In case wrapping is enabled, elements cannot exceed this size on the primary axis
|
||||||
|
let max_line_pri = match self.direction {
|
||||||
|
Direction::Horizontal => match self.size.width {
|
||||||
|
Size::Auto => ctx.layout.max_size.x,
|
||||||
|
Size::Relative(p) => ctx.layout.max_size.x * p,
|
||||||
|
Size::Absolute(p) => p,
|
||||||
|
Size::Remaining(p) => ctx.layout.remaining_space.unwrap_or(ctx.layout.max_size.x) * p,
|
||||||
},
|
},
|
||||||
UiDirection::Vertical => {
|
Direction::Vertical => match self.size.height {
|
||||||
size.x = size.x.max(measure.size.x);
|
Size::Auto => ctx.layout.max_size.y,
|
||||||
size.y += measure.size.y + self.gap;
|
Size::Relative(p) => ctx.layout.max_size.y * p,
|
||||||
leftover_gap.y = self.gap;
|
Size::Absolute(p) => p,
|
||||||
|
Size::Remaining(p) => ctx.layout.remaining_space.unwrap_or(ctx.layout.max_size.y) * p,
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
|
||||||
size -= leftover_gap;
|
|
||||||
|
|
||||||
let inner_content_size = Some(size);
|
//size of AABB containing all lines
|
||||||
|
let mut total_size = Vec2::ZERO;
|
||||||
|
|
||||||
size += vec2(
|
//Size of the current row/column (if wrapping)
|
||||||
|
let mut line_size = Vec2::ZERO;
|
||||||
|
|
||||||
|
//Size of previous sec. axes combined
|
||||||
|
//(basically, in case of the horizontal layout, this is the height of the tallest element in the line)
|
||||||
|
//This is a vec2, but only one axis is used, depending on the layout direction
|
||||||
|
let mut line_sec_offset: Vec2 = Vec2::ZERO;
|
||||||
|
|
||||||
|
//Amount of elements in the current line
|
||||||
|
let mut line_element_count = 0;
|
||||||
|
|
||||||
|
//Leftover gap from the previous element on the primary axis
|
||||||
|
let mut leftover_gap = Vec2::ZERO;
|
||||||
|
|
||||||
|
//line metadata for the user_data
|
||||||
|
let mut lines = vec![
|
||||||
|
CudLine {
|
||||||
|
start_idx: 0,
|
||||||
|
content_size: Vec2::ZERO,
|
||||||
|
remaining_space: 0.,
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
//set to true if in the current line there is an element with Remaining size (line will have to be wrapped)
|
||||||
|
// let mut has_remaining = false;
|
||||||
|
|
||||||
|
for (idx, element) in self.children.0.iter().enumerate() {
|
||||||
|
if let Some(esize) = element.size() {
|
||||||
|
let pri_size = match self.direction {
|
||||||
|
Direction::Horizontal => esize.width,
|
||||||
|
Direction::Vertical => esize.height,
|
||||||
|
};
|
||||||
|
if matches!(pri_size, Size::Remaining(_)) {
|
||||||
|
//XXX: kinda a hack?
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let measure = element.measure(MeasureContext{
|
||||||
|
state: ctx.state,
|
||||||
|
layout: &LayoutInfo {
|
||||||
|
//XXX: if the element gets wrapped, this will be inaccurate.
|
||||||
|
//But, we cant know the size of the line until we measure it, and also
|
||||||
|
//We dont make any guarantees about this value being valid during the `measure` call
|
||||||
|
//For all intents and purposes, this is just a *hint* for the element to use
|
||||||
|
//(and could be just set to 0 for all we care)
|
||||||
|
position: ctx.layout.position + line_size + line_sec_offset,
|
||||||
|
//TODO: subtract size already taken by previous children
|
||||||
|
max_size: self.measure_max_inner_size(ctx.layout),
|
||||||
|
direction: self.direction,
|
||||||
|
remaining_space: None,
|
||||||
|
},
|
||||||
|
text_measure: ctx.text_measure,
|
||||||
|
current_font: ctx.current_font,
|
||||||
|
images: ctx.images,
|
||||||
|
});
|
||||||
|
|
||||||
|
//Check the position of the side of element closest to the end on the primary axis
|
||||||
|
let end_pos_pri = match self.direction {
|
||||||
|
Direction::Horizontal => line_size.x + measure.size.x + self.padding.left + self.padding.right,
|
||||||
|
Direction::Vertical => line_size.y + measure.size.y + self.padding.top + self.padding.bottom,
|
||||||
|
};
|
||||||
|
|
||||||
|
//Wrap the element if it exceeds container's size and is not the first element in the line
|
||||||
|
let should_wrap_overflow = self.wrap.is_enabled() && (end_pos_pri > max_line_pri);
|
||||||
|
if self.wrap.is_allowed() && line_element_count > 0 && (measure.should_wrap || should_wrap_overflow) {
|
||||||
|
// >>>>>>> WRAP THAT B*TCH!
|
||||||
|
|
||||||
|
//Negate the leftover gap from the previous element
|
||||||
|
line_size -= leftover_gap;
|
||||||
|
|
||||||
|
//update the previous line metadata
|
||||||
|
{
|
||||||
|
let last_line = lines.last_mut().unwrap();
|
||||||
|
last_line.content_size = line_size;
|
||||||
|
//HACK: why? - self.gap, may be different for the last element or if it's the only element in the line
|
||||||
|
let will_produce_gap = if line_element_count > 1 { self.gap } else { 0. };
|
||||||
|
last_line.remaining_space = max_line_pri - will_produce_gap - match self.direction {
|
||||||
|
Direction::Horizontal => line_size.x + self.padding.left + self.padding.right,
|
||||||
|
Direction::Vertical => line_size.y + self.padding.top + self.padding.bottom,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//push the line metadata
|
||||||
|
lines.push(CudLine {
|
||||||
|
start_idx: idx,
|
||||||
|
content_size: Vec2::ZERO,
|
||||||
|
remaining_space: 0.,
|
||||||
|
});
|
||||||
|
|
||||||
|
//Update the total size accordingly
|
||||||
|
match self.direction {
|
||||||
|
Direction::Horizontal => {
|
||||||
|
total_size.x = total_size.x.max(line_size.x);
|
||||||
|
total_size.y += line_size.y + self.gap;
|
||||||
|
},
|
||||||
|
Direction::Vertical => {
|
||||||
|
total_size.x += line_size.x + self.gap;
|
||||||
|
total_size.y = total_size.y.max(line_size.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Now, update line_sec_offset
|
||||||
|
match self.direction {
|
||||||
|
Direction::Horizontal => {
|
||||||
|
line_sec_offset.y += measure.size.y + self.gap;
|
||||||
|
},
|
||||||
|
Direction::Vertical => {
|
||||||
|
line_sec_offset.x += measure.size.x + self.gap;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//Reset the line size and element count
|
||||||
|
line_size = Vec2::ZERO;
|
||||||
|
line_element_count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Increment element count
|
||||||
|
line_element_count += 1;
|
||||||
|
|
||||||
|
//Sset the leftover gap in case this is the last element in the line
|
||||||
|
match self.direction {
|
||||||
|
Direction::Horizontal => {
|
||||||
|
line_size.x += measure.size.x + self.gap;
|
||||||
|
line_size.y = line_size.y.max(measure.size.y);
|
||||||
|
leftover_gap = vec2(self.gap, 0.);
|
||||||
|
},
|
||||||
|
Direction::Vertical => {
|
||||||
|
line_size.x = line_size.x.max(measure.size.x);
|
||||||
|
line_size.y += measure.size.y + self.gap;
|
||||||
|
leftover_gap = vec2(0., self.gap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
line_size -= leftover_gap;
|
||||||
|
|
||||||
|
//Update the content size of the last line
|
||||||
|
{
|
||||||
|
//HACK: why? - self.gap, may be different for the last element or if it's the only element in the line
|
||||||
|
let cur_line = lines.last_mut().unwrap();
|
||||||
|
cur_line.content_size = line_size;
|
||||||
|
let will_produce_gap = if line_element_count > 1 { self.gap } else { 0. };
|
||||||
|
cur_line.remaining_space = max_line_pri - will_produce_gap - match self.direction {
|
||||||
|
Direction::Horizontal => line_size.x + self.padding.left + self.padding.right,
|
||||||
|
Direction::Vertical => line_size.y + self.padding.top + self.padding.bottom,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//Update the total size according to the size of the last line
|
||||||
|
match self.direction {
|
||||||
|
Direction::Horizontal => {
|
||||||
|
total_size.x = total_size.x.max(line_size.x);
|
||||||
|
total_size.y += line_size.y;
|
||||||
|
},
|
||||||
|
Direction::Vertical => {
|
||||||
|
total_size.x += line_size.x;
|
||||||
|
total_size.y = total_size.y.max(line_size.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Now, total_size should hold the size of the AABB containing all lines
|
||||||
|
//This is exactly what inner_content_size hint should be set to
|
||||||
|
let inner_content_size = Some(total_size);
|
||||||
|
|
||||||
|
//After setting the inner_content_size, we can calculate the size of the container
|
||||||
|
//Including padding, and in case the size is set to non-auto, override the size
|
||||||
|
|
||||||
|
total_size += vec2(
|
||||||
self.padding.left + self.padding.right,
|
self.padding.left + self.padding.right,
|
||||||
self.padding.top + self.padding.bottom,
|
self.padding.top + self.padding.bottom,
|
||||||
);
|
);
|
||||||
|
|
||||||
match self.size.0 {
|
let computed_size = compute_size(ctx.layout, self.size, total_size);
|
||||||
UiSize::Auto => (),
|
match self.size.width {
|
||||||
UiSize::Percentage(percentage) => size.x = layout.max_size.x * percentage,
|
Size::Auto => (),
|
||||||
UiSize::Pixels(pixels) => size.x = pixels,
|
_ => total_size.x = computed_size.x,
|
||||||
}
|
}
|
||||||
match self.size.1 {
|
match self.size.height {
|
||||||
UiSize::Auto => (),
|
Size::Auto => (),
|
||||||
UiSize::Percentage(percentage) => size.y = layout.max_size.y * percentage,
|
_ => total_size.y = computed_size.y,
|
||||||
UiSize::Pixels(pixels) => size.y = pixels,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// match self.size.width {
|
||||||
|
// Size::Auto => (),
|
||||||
|
// Size::Relative(percentage) => total_size.x = ctx.layout.max_size.x * percentage,
|
||||||
|
// Size::Absolute(pixels) => total_size.x = pixels,
|
||||||
|
// }
|
||||||
|
// match self.size.height {
|
||||||
|
// Size::Auto => (),
|
||||||
|
// Size::Relative(percentage) => total_size.y = ctx.layout.max_size.y * percentage,
|
||||||
|
// Size::Absolute(pixels) => total_size.y = pixels,
|
||||||
|
// }
|
||||||
|
|
||||||
Response {
|
Response {
|
||||||
size,
|
size: total_size,
|
||||||
hints: Hints {
|
hints: Hints {
|
||||||
inner_content_size,
|
inner_content_size,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
user_data: None
|
user_data: Some(Box::new(ContainerUserData { lines })),
|
||||||
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process(&self, measure: &Response, state: &mut StateRepo, layout: &LayoutInfo, draw: &mut UiDrawCommands) {
|
fn process(&self, ctx: ProcessContext) {
|
||||||
let mut position = layout.position;
|
let user_data: &ContainerUserData = ctx.measure.user_data
|
||||||
|
.as_ref().expect("no user data attached to container")
|
||||||
|
.downcast_ref().expect("invalid user data type");
|
||||||
|
|
||||||
|
let mut position = ctx.layout.position;
|
||||||
|
|
||||||
//background
|
//background
|
||||||
if let Some(color) = self.background {
|
// if !self.background.is_transparent() {
|
||||||
draw.add(UiDrawCommand::Rectangle {
|
// let corner_colors = self.background.corners();
|
||||||
position,
|
// ctx.draw.add(UiDrawCommand::Rectangle {
|
||||||
size: measure.size,
|
// position,
|
||||||
color
|
// size: ctx.measure.size,
|
||||||
});
|
// color: corner_colors,
|
||||||
}
|
// texture: self.background_image,
|
||||||
|
// rounded_corners: (self.corner_radius.max_f32() > 0.).then_some({
|
||||||
|
// RoundedCorners::from_radius(self.corner_radius)
|
||||||
|
// }),
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
self.background_frame.draw(ctx.draw, (ctx.layout.position, ctx.measure.size).into());
|
||||||
|
|
||||||
//padding
|
//padding
|
||||||
position += vec2(self.padding.left, self.padding.top);
|
position += vec2(self.padding.left, self.padding.top);
|
||||||
|
|
||||||
//alignment
|
//convert alignment to pri/sec axis based
|
||||||
match (self.align.0, self.direction) {
|
//.0 = primary, .1 = secondary
|
||||||
|
let pri_sec_align = match self.direction {
|
||||||
|
Direction::Horizontal => (self.align.horizontal, self.align.vertical),
|
||||||
|
Direction::Vertical => (self.align.vertical, self.align.horizontal),
|
||||||
|
};
|
||||||
|
|
||||||
|
//alignment (on sec. axis)
|
||||||
|
// match pri_sec_align.1 {
|
||||||
|
// Alignment::Begin => (),
|
||||||
|
// Alignment::Center => {
|
||||||
|
// position += match self.direction {
|
||||||
|
// UiDirection::Horizontal => vec2(0., (ctx.measure.size.y - self.padding.top - self.padding.bottom - user_data.lines.last().unwrap().content_size.y) / 2.),
|
||||||
|
// UiDirection::Vertical => vec2((ctx.measure.size.x - self.padding.left - self.padding.right - user_data.lines.last().unwrap().content_size.x) / 2., 0.),
|
||||||
|
// };
|
||||||
|
// },
|
||||||
|
// Alignment::End => {
|
||||||
|
// position += match self.direction {
|
||||||
|
// UiDirection::Horizontal => vec2(0., ctx.measure.size.y - user_data.lines.last().unwrap().content_size.y - self.padding.bottom - self.padding.top),
|
||||||
|
// UiDirection::Vertical => vec2(ctx.measure.size.x - user_data.lines.last().unwrap().content_size.x - self.padding.right - self.padding.left, 0.),
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
for (line_idx, cur_line) in user_data.lines.iter().enumerate() {
|
||||||
|
let mut local_position = position;
|
||||||
|
|
||||||
|
//alignment on primary axis
|
||||||
|
match (pri_sec_align.0, self.direction) {
|
||||||
(Alignment::Begin, _) => (),
|
(Alignment::Begin, _) => (),
|
||||||
(Alignment::Center, UiDirection::Horizontal) => {
|
(Alignment::Center, Direction::Horizontal) => {
|
||||||
position.x += (measure.size.x - measure.hints.inner_content_size.unwrap().x) / 2.;
|
local_position.x += (ctx.measure.size.x - cur_line.content_size.x) / 2. - self.padding.left;
|
||||||
},
|
},
|
||||||
(Alignment::Center, UiDirection::Vertical) => {
|
(Alignment::Center, Direction::Vertical) => {
|
||||||
position.y += (measure.size.y - measure.hints.inner_content_size.unwrap().y) / 2.;
|
local_position.y += (ctx.measure.size.y - cur_line.content_size.y) / 2. - self.padding.top;
|
||||||
},
|
},
|
||||||
(Alignment::End, UiDirection::Horizontal) => {
|
(Alignment::End, Direction::Horizontal) => {
|
||||||
position.x += measure.size.x - measure.hints.inner_content_size.unwrap().x - self.padding.right - self.padding.left;
|
local_position.x += ctx.measure.size.x - cur_line.content_size.x - self.padding.right - self.padding.left;
|
||||||
},
|
},
|
||||||
(Alignment::End, UiDirection::Vertical) => {
|
(Alignment::End, Direction::Vertical) => {
|
||||||
position.y += measure.size.y - measure.hints.inner_content_size.unwrap().y - self.padding.bottom - self.padding.top;
|
local_position.y += ctx.measure.size.y - cur_line.content_size.y - self.padding.bottom - self.padding.top;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for element in &self.elements {
|
let next_line_begin = user_data.lines
|
||||||
//(passing max size from layout rather than actual bounds for the sake of consistency with measure() above)
|
.get(line_idx + 1)
|
||||||
|
.map(|l| l.start_idx)
|
||||||
|
.unwrap_or(self.children.0.len());
|
||||||
|
|
||||||
|
for element_idx in cur_line.start_idx..next_line_begin {
|
||||||
|
let element = &self.children.0[element_idx];
|
||||||
|
|
||||||
|
//(passing max size from layout rather than actual known bounds for the sake of consistency with measure() above)
|
||||||
|
//... as this must match!
|
||||||
|
|
||||||
let mut el_layout = LayoutInfo {
|
let mut el_layout = LayoutInfo {
|
||||||
position,
|
position: local_position,
|
||||||
max_size: self.measure_max_inner_size(layout),
|
max_size: self.measure_max_inner_size(ctx.layout),
|
||||||
direction: self.direction,
|
direction: self.direction,
|
||||||
|
remaining_space: Some(cur_line.remaining_space),
|
||||||
};
|
};
|
||||||
|
|
||||||
//measure
|
//measure
|
||||||
let el_measure = element.measure(state, &el_layout);
|
let el_measure = element.measure(MeasureContext {
|
||||||
|
layout: &el_layout,
|
||||||
|
state: ctx.state,
|
||||||
|
text_measure: ctx.text_measure,
|
||||||
|
current_font: ctx.current_font,
|
||||||
|
images: ctx.images,
|
||||||
|
});
|
||||||
|
|
||||||
//align (on sec. axis)
|
//align (on sec. axis)
|
||||||
match (self.align.1, self.direction) {
|
//TODO separate align withing the line and align of the whole line
|
||||||
|
let inner_content_size = ctx.measure.hints.inner_content_size.unwrap();
|
||||||
|
match (pri_sec_align.1, self.direction) {
|
||||||
(Alignment::Begin, _) => (),
|
(Alignment::Begin, _) => (),
|
||||||
(Alignment::Center, UiDirection::Horizontal) => {
|
(Alignment::Center, Direction::Horizontal) => {
|
||||||
el_layout.position.y += (measure.size.y - self.padding.bottom - self.padding.top - el_measure.size.y) / 2.;
|
//Align whole row
|
||||||
|
el_layout.position.y += ((ctx.measure.size.y - self.padding.bottom - self.padding.top) - inner_content_size.y) / 2.;
|
||||||
|
//Align within row
|
||||||
|
el_layout.position.y += (cur_line.content_size.y - el_measure.size.y) / 2.;
|
||||||
},
|
},
|
||||||
(Alignment::Center, UiDirection::Vertical) => {
|
(Alignment::Center, Direction::Vertical) => {
|
||||||
el_layout.position.x += (measure.size.x - self.padding.left - self.padding.right - el_measure.size.x) / 2.;
|
//Align whole row
|
||||||
|
el_layout.position.x += ((ctx.measure.size.x - self.padding.left - self.padding.right) - inner_content_size.x) / 2.;
|
||||||
|
//Align within row
|
||||||
|
el_layout.position.x += (cur_line.content_size.x - el_measure.size.x) / 2.;
|
||||||
},
|
},
|
||||||
(Alignment::End, UiDirection::Horizontal) => {
|
//TODO update these two cases:
|
||||||
el_layout.position.y += measure.size.y - el_measure.size.y - self.padding.bottom;
|
(Alignment::End, Direction::Horizontal) => {
|
||||||
|
//Align whole row
|
||||||
|
el_layout.position.y += (ctx.measure.size.y - self.padding.bottom - self.padding.top) - inner_content_size.y;
|
||||||
|
//Align within row
|
||||||
|
el_layout.position.y += cur_line.content_size.y - el_measure.size.y;
|
||||||
},
|
},
|
||||||
(Alignment::End, UiDirection::Vertical) => {
|
(Alignment::End, Direction::Vertical) => {
|
||||||
el_layout.position.x += measure.size.x - el_measure.size.x - self.padding.right;
|
//Align whole row
|
||||||
|
el_layout.position.x += (ctx.measure.size.x - self.padding.right - self.padding.left) - inner_content_size.x;
|
||||||
|
//Align within row
|
||||||
|
el_layout.position.x += cur_line.content_size.x - el_measure.size.x;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//process
|
//process
|
||||||
element.process(&el_measure, state, &el_layout, draw);
|
element.process(ProcessContext {
|
||||||
|
measure: &el_measure,
|
||||||
|
layout: &el_layout,
|
||||||
|
draw: ctx.draw,
|
||||||
|
state: ctx.state,
|
||||||
|
text_measure: ctx.text_measure,
|
||||||
|
current_font: ctx.current_font,
|
||||||
|
images: ctx.images,
|
||||||
|
input: ctx.input,
|
||||||
|
signal: ctx.signal,
|
||||||
|
});
|
||||||
|
|
||||||
//layout
|
//layout
|
||||||
match self.direction {
|
match self.direction {
|
||||||
UiDirection::Horizontal => {
|
Direction::Horizontal => {
|
||||||
position.x += el_measure.size.x + self.gap;
|
local_position.x += el_measure.size.x + self.gap;
|
||||||
},
|
},
|
||||||
UiDirection::Vertical => {
|
Direction::Vertical => {
|
||||||
position.y += el_measure.size.y + self.gap;
|
local_position.y += el_measure.size.y + self.gap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Move to the next line
|
||||||
|
match self.direction {
|
||||||
|
Direction::Horizontal => {
|
||||||
|
position.y += cur_line.content_size.y + self.gap;
|
||||||
|
//position.x -= cur_line.content_size.x;
|
||||||
|
// leftover_line_gap = vec2(0., self.gap);
|
||||||
|
}
|
||||||
|
Direction::Vertical => {
|
||||||
|
position.x += cur_line.content_size.x + self.gap;
|
||||||
|
//position.y -= cur_line.content_size.y;
|
||||||
|
// leftover_line_gap = vec2(self.gap, 0.);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
68
hui/src/element/builtin/frame_view.rs
Normal file
68
hui/src/element/builtin/frame_view.rs
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
//! Simple element that displays the specified frame
|
||||||
|
|
||||||
|
use derive_setters::Setters;
|
||||||
|
use crate::{
|
||||||
|
element::{MeasureContext, ProcessContext, UiElement},
|
||||||
|
frame::{Frame, RectFrame},
|
||||||
|
layout::{compute_size, Size2d},
|
||||||
|
measure::Response,
|
||||||
|
size
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Simple rectangle that displays the specified frame
|
||||||
|
#[derive(Setters)]
|
||||||
|
#[setters(prefix = "with_")]
|
||||||
|
pub struct FrameView {
|
||||||
|
/// Size of the rectangle
|
||||||
|
#[setters(into)]
|
||||||
|
pub size: Size2d,
|
||||||
|
|
||||||
|
/// Frame
|
||||||
|
#[setters(skip)]
|
||||||
|
pub frame: Box<dyn Frame>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FrameView {
|
||||||
|
pub fn new(frame: impl Frame + 'static) -> Self {
|
||||||
|
Self {
|
||||||
|
size: size!(10, 10),
|
||||||
|
frame: Box::new(frame),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//setters:
|
||||||
|
pub fn with_frame(mut self, frame: impl Frame + 'static) -> Self {
|
||||||
|
self.frame = Box::new(frame);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for FrameView {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
size: size!(10, 10),
|
||||||
|
frame: Box::new(RectFrame::color((0., 0., 0., 0.5))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UiElement for FrameView {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"frame_view"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size(&self) -> Option<Size2d> {
|
||||||
|
Some(self.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn measure(&self, ctx: MeasureContext) -> Response {
|
||||||
|
Response {
|
||||||
|
size: compute_size(ctx.layout, self.size, ctx.layout.max_size),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process(&self, ctx: ProcessContext) {
|
||||||
|
self.frame.draw(ctx.draw, (ctx.layout.position, ctx.measure.size).into());
|
||||||
|
}
|
||||||
|
}
|
93
hui/src/element/builtin/image.rs
Normal file
93
hui/src/element/builtin/image.rs
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
use derive_setters::Setters;
|
||||||
|
use glam::vec2;
|
||||||
|
use crate::{
|
||||||
|
draw::{ImageHandle, RoundedCorners, UiDrawCommand},
|
||||||
|
element::{MeasureContext, ProcessContext, UiElement},
|
||||||
|
layout::{compute_size, Size, Size2d},
|
||||||
|
measure::Response,
|
||||||
|
rect::{Corners, FillColor},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Setters)]
|
||||||
|
#[setters(prefix = "with_")]
|
||||||
|
pub struct Image {
|
||||||
|
/// Image handle to draw
|
||||||
|
#[setters(skip)]
|
||||||
|
pub image: ImageHandle,
|
||||||
|
|
||||||
|
/// Size of the image.
|
||||||
|
///
|
||||||
|
/// - If one of the dimensions is `Size::Auto`, the image will be scaled to fit the other dimension\
|
||||||
|
/// (aspect ratio is preserved)
|
||||||
|
/// - If both dimensions are `Size::Auto`, the image will be drawn at its original size
|
||||||
|
/// - All other values behave as expected
|
||||||
|
#[setters(into)]
|
||||||
|
pub size: Size2d,
|
||||||
|
|
||||||
|
/// Color of the image
|
||||||
|
///
|
||||||
|
/// Image will get multiplied/tinted by this color or gradient
|
||||||
|
#[setters(into)]
|
||||||
|
pub color: FillColor,
|
||||||
|
|
||||||
|
/// Corner radius of the image
|
||||||
|
#[setters(into)]
|
||||||
|
pub corner_radius: Corners<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Image {
|
||||||
|
pub fn new(handle: ImageHandle) -> Self {
|
||||||
|
Self {
|
||||||
|
image: handle,
|
||||||
|
size: Size2d {
|
||||||
|
width: Size::Auto,
|
||||||
|
height: Size::Auto,
|
||||||
|
},
|
||||||
|
color: (1., 1., 1.).into(),
|
||||||
|
corner_radius: Corners::all(0.),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UiElement for Image {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"image"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size(&self) -> Option<Size2d> {
|
||||||
|
Some(self.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn measure(&self, ctx: MeasureContext) -> Response {
|
||||||
|
let dim = ctx.images.get_size(self.image).expect("invalid image handle");
|
||||||
|
let pre_size = compute_size(ctx.layout, self.size, dim.as_vec2());
|
||||||
|
Response {
|
||||||
|
size: compute_size(ctx.layout, self.size, vec2(
|
||||||
|
match self.size.height {
|
||||||
|
Size::Auto => dim.x as f32,
|
||||||
|
_ => (pre_size.y / dim.y as f32) * dim.x as f32,
|
||||||
|
},
|
||||||
|
match self.size.height {
|
||||||
|
Size::Auto => dim.x as f32,
|
||||||
|
_ => (pre_size.y / dim.y as f32) * dim.x as f32,
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process(&self, ctx: ProcessContext) {
|
||||||
|
if !self.color.is_transparent() {
|
||||||
|
ctx.draw.add(UiDrawCommand::Rectangle {
|
||||||
|
position: ctx.layout.position,
|
||||||
|
size: ctx.measure.size,
|
||||||
|
color: self.color.corners(),
|
||||||
|
texture: Some(self.image),
|
||||||
|
texture_uv: None,
|
||||||
|
rounded_corners: (self.corner_radius.max_f32() > 0.).then_some({
|
||||||
|
RoundedCorners::from_radius(self.corner_radius)
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
109
hui/src/element/builtin/interactable.rs
Normal file
109
hui/src/element/builtin/interactable.rs
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
//! wrapper that allows adding click and hover events to any element
|
||||||
|
|
||||||
|
// not sure if this is a good idea...
|
||||||
|
// but having the ability to add a click event to any element would be nice, and this is a naive way to do it
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
element::{MeasureContext, ProcessContext, UiElement},
|
||||||
|
signal::{trigger::SignalTrigger, Signal},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[non_exhaustive]
|
||||||
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
|
||||||
|
pub enum InteractableEvent {
|
||||||
|
#[default]
|
||||||
|
Click,
|
||||||
|
Hover,
|
||||||
|
Active,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wrapper that allows adding click and hover events to any element
|
||||||
|
pub struct Interactable {
|
||||||
|
/// The wrapped element that will be interactable
|
||||||
|
pub element: Box<dyn UiElement>,
|
||||||
|
|
||||||
|
/// Event to listen for
|
||||||
|
pub event: InteractableEvent,
|
||||||
|
|
||||||
|
/// Signal that will be called if the element was clicked in the current frame
|
||||||
|
pub signal: SignalTrigger,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Interactable {
|
||||||
|
pub fn new<S: Signal, F: Fn() -> S + 'static>(
|
||||||
|
element: Box<dyn UiElement>,
|
||||||
|
event: InteractableEvent,
|
||||||
|
signal: F
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
element,
|
||||||
|
event,
|
||||||
|
signal: SignalTrigger::new(signal),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UiElement for Interactable {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"interactable"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size(&self) -> Option<crate::layout::Size2d> {
|
||||||
|
self.element.size()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn measure(&self, ctx: MeasureContext) -> crate::measure::Response {
|
||||||
|
self.element.measure(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process(&self, ctx: ProcessContext) {
|
||||||
|
let rect = ctx.measure.rect(ctx.layout.position);
|
||||||
|
|
||||||
|
//XXX: should we do this AFTER normal process call of wrapped element?
|
||||||
|
let event_happened = match self.event {
|
||||||
|
//TODO: actually pass the response
|
||||||
|
InteractableEvent::Click => ctx.input.check_click(rect).is_some(),
|
||||||
|
InteractableEvent::Hover => ctx.input.check_hover(rect),
|
||||||
|
InteractableEvent::Active => ctx.input.check_active(rect).is_some(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if event_happened {
|
||||||
|
self.signal.fire(ctx.signal);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.element.process(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extension trait for [`UiElement`] that adds methods to wrap the element in an [`Interactable`]
|
||||||
|
pub trait ElementInteractableExt: UiElement {
|
||||||
|
/// Wrap the element in an [`Interactable`] that will call the given signal when the specified event occurs
|
||||||
|
fn into_interactable<S: Signal, F: Fn() -> S + 'static>(self, event: InteractableEvent, signal: F) -> Interactable;
|
||||||
|
|
||||||
|
/// Wrap the element in an [`Interactable`] that will call the given signal when clicked
|
||||||
|
fn on_click<S: Signal, F: Fn() -> S + 'static>(self, signal: F) -> Interactable;
|
||||||
|
|
||||||
|
/// Wrap the element in an [`Interactable`] that will call the given signal continuously while hovered
|
||||||
|
fn on_hover<S: Signal, F: Fn() -> S + 'static>(self, signal: F) -> Interactable;
|
||||||
|
|
||||||
|
/// Wrap the element in an [`Interactable`] that will call the given signal continuously while active
|
||||||
|
fn on_active<S: Signal, F: Fn() -> S + 'static>(self, signal: F) -> Interactable;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: UiElement + 'static> ElementInteractableExt for T {
|
||||||
|
fn into_interactable<S: Signal, F: Fn() -> S + 'static>(self, event: InteractableEvent, signal: F) -> Interactable {
|
||||||
|
Interactable::new(Box::new(self), event, signal)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_click<S: Signal, F: Fn() -> S + 'static>(self, signal: F) -> Interactable {
|
||||||
|
self.into_interactable(InteractableEvent::Click, signal)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_hover<S: Signal, F: Fn() -> S + 'static>(self, signal: F) -> Interactable {
|
||||||
|
self.into_interactable(InteractableEvent::Hover, signal)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_active<S: Signal, F: Fn() -> S + 'static>(self, signal: F) -> Interactable {
|
||||||
|
self.into_interactable(InteractableEvent::Active, signal)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,70 +1,120 @@
|
||||||
use glam::{vec2, Vec4, vec4};
|
use derive_setters::Setters;
|
||||||
|
use glam::vec2;
|
||||||
use crate::{
|
use crate::{
|
||||||
UiSize, LayoutInfo,
|
element::{MeasureContext, ProcessContext, UiElement},
|
||||||
draw::{UiDrawCommand, UiDrawCommands},
|
frame::{Frame, RectFrame},
|
||||||
|
layout::{compute_size, Size, Size2d},
|
||||||
measure::Response,
|
measure::Response,
|
||||||
state::StateRepo,
|
|
||||||
element::UiElement
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
//TODO: Use Frames here instead of FillColor
|
||||||
|
|
||||||
|
#[derive(Setters)]
|
||||||
|
#[setters(prefix = "with_")]
|
||||||
pub struct ProgressBar {
|
pub struct ProgressBar {
|
||||||
pub size: (UiSize, UiSize),
|
/// Current progress, should be in the range 0.0..=1.0
|
||||||
pub value: f32,
|
pub value: f32,
|
||||||
pub color_foreground: Vec4,
|
|
||||||
pub color_background: Vec4,
|
/// Size of the progress bar element
|
||||||
|
#[setters(into)]
|
||||||
|
pub size: Size2d,
|
||||||
|
|
||||||
|
/// Foreground (bar) color
|
||||||
|
#[setters(skip)]
|
||||||
|
pub foreground: Box<dyn Frame>,
|
||||||
|
|
||||||
|
/// Background color
|
||||||
|
#[setters(skip)]
|
||||||
|
pub background: Box<dyn Frame>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProgressBar {
|
||||||
|
pub const DEFAULT_HEIGHT: f32 = 20.0;
|
||||||
|
|
||||||
|
pub fn with_background(mut self, frame: impl Frame + 'static) -> Self {
|
||||||
|
self.background = Box::new(frame);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_foreground(mut self, frame: impl Frame + 'static) -> Self {
|
||||||
|
self.foreground = Box::new(frame);
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ProgressBar {
|
impl Default for ProgressBar {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
size: (UiSize::Auto, UiSize::Auto),
|
|
||||||
value: 0.,
|
value: 0.,
|
||||||
color_foreground: vec4(0.0, 0.0, 1.0, 1.0),
|
size: Size::Auto.into(),
|
||||||
color_background: vec4(0.0, 0.0, 0.0, 1.0),
|
foreground: Box::new(RectFrame::color((0.0, 0.0, 1.0, 1.0))),
|
||||||
|
background: Box::new(RectFrame::color((0.0, 0.0, 0.0, 1.0))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const BAR_HEIGHT: f32 = 20.0;
|
|
||||||
|
|
||||||
impl UiElement for ProgressBar {
|
impl UiElement for ProgressBar {
|
||||||
fn name(&self) -> &'static str { "Progress bar" }
|
fn name(&self) -> &'static str {
|
||||||
|
"progress_bar"
|
||||||
fn measure(&self, _: &StateRepo, layout: &LayoutInfo) -> Response {
|
|
||||||
Response {
|
|
||||||
size: vec2(
|
|
||||||
match self.size.0 {
|
|
||||||
UiSize::Auto => layout.max_size.x.max(300.),
|
|
||||||
UiSize::Percentage(p) => layout.max_size.x * p,
|
|
||||||
UiSize::Pixels(p) => p,
|
|
||||||
},
|
|
||||||
match self.size.1 {
|
|
||||||
UiSize::Auto => BAR_HEIGHT,
|
|
||||||
UiSize::Percentage(p) => layout.max_size.y * p,
|
|
||||||
UiSize::Pixels(p) => p,
|
|
||||||
}
|
}
|
||||||
),
|
|
||||||
|
fn measure(&self, ctx: MeasureContext) -> Response {
|
||||||
|
Response {
|
||||||
|
size: compute_size(ctx.layout, self.size, vec2(
|
||||||
|
ctx.layout.max_size.x.max(300.), //XXX: remove .max(300)?
|
||||||
|
Self::DEFAULT_HEIGHT,
|
||||||
|
)),
|
||||||
hints: Default::default(),
|
hints: Default::default(),
|
||||||
user_data: None,
|
user_data: None,
|
||||||
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process(&self, measure: &Response, state: &mut StateRepo, layout: &LayoutInfo, draw: &mut UiDrawCommands) {
|
fn process(&self, ctx: ProcessContext) {
|
||||||
let value = self.value.clamp(0., 1.);
|
let value = self.value.clamp(0., 1.);
|
||||||
if value < 1. {
|
|
||||||
draw.add(UiDrawCommand::Rectangle {
|
//FIXME: these optimizations may not be valid
|
||||||
position: layout.position,
|
if value < 1. || !self.foreground.covers_opaque() {
|
||||||
size: measure.size,
|
self.background.draw(ctx.draw, (ctx.layout.position, ctx.measure.size).into());
|
||||||
color: self.color_background
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if value > 0. {
|
if value > 0. {
|
||||||
draw.add(UiDrawCommand::Rectangle {
|
self.foreground.draw(ctx.draw, (ctx.layout.position, ctx.measure.size * vec2(value, 1.)).into());
|
||||||
position: layout.position,
|
}
|
||||||
size: measure.size * vec2(value, 1.0),
|
|
||||||
color: self.color_foreground
|
// let rounded_corners =
|
||||||
});
|
// (self.corner_radius.max_f32() > 0.).then_some({
|
||||||
}
|
// //HACK: fix clipping issues; //todo: get rid of this
|
||||||
|
// let mut radii = self.corner_radius;
|
||||||
|
// let width = ctx.measure.size.x * value;
|
||||||
|
// if width <= radii.max_f32() * 2. {
|
||||||
|
// radii.bottom_right = 0.;
|
||||||
|
// radii.top_right = 0.;
|
||||||
|
// }
|
||||||
|
// if width <= radii.max_f32() {
|
||||||
|
// radii.bottom_left = 0.;
|
||||||
|
// radii.top_left = 0.;
|
||||||
|
// }
|
||||||
|
// RoundedCorners::from_radius(radii)
|
||||||
|
// });
|
||||||
|
// if value < 1. {
|
||||||
|
// ctx.draw.add(UiDrawCommand::Rectangle {
|
||||||
|
// position: ctx.layout.position,
|
||||||
|
// size: ctx.measure.size,
|
||||||
|
// color: self.background.corners(),
|
||||||
|
// texture: None,
|
||||||
|
// texture_uv: None,
|
||||||
|
// rounded_corners
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// if value > 0. {
|
||||||
|
// ctx.draw.add(UiDrawCommand::Rectangle {
|
||||||
|
// position: ctx.layout.position,
|
||||||
|
// size: ctx.measure.size * vec2(value, 1.0),
|
||||||
|
// color: self.foreground.corners(),
|
||||||
|
// texture: None,
|
||||||
|
// texture_uv: None,
|
||||||
|
// rounded_corners,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,54 +0,0 @@
|
||||||
use glam::{vec2, Vec4};
|
|
||||||
use crate::{
|
|
||||||
LayoutInfo,
|
|
||||||
UiSize,
|
|
||||||
element::UiElement,
|
|
||||||
state::StateRepo,
|
|
||||||
measure::Response,
|
|
||||||
draw::{UiDrawCommand, UiDrawCommands}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct Rect {
|
|
||||||
pub size: (UiSize, UiSize),
|
|
||||||
pub color: Option<Vec4>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Rect {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
size: (UiSize::Pixels(10.), UiSize::Pixels(10.)),
|
|
||||||
color: Some(Vec4::new(0., 0., 0., 0.5)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UiElement for Rect {
|
|
||||||
fn measure(&self, _state: &StateRepo, layout: &LayoutInfo) -> Response {
|
|
||||||
Response {
|
|
||||||
size: vec2(
|
|
||||||
match self.size.0 {
|
|
||||||
UiSize::Auto => layout.max_size.x,
|
|
||||||
UiSize::Percentage(percentage) => layout.max_size.x * percentage,
|
|
||||||
UiSize::Pixels(pixels) => pixels,
|
|
||||||
},
|
|
||||||
match self.size.1 {
|
|
||||||
UiSize::Auto => layout.max_size.y,
|
|
||||||
UiSize::Percentage(percentage) => layout.max_size.y * percentage,
|
|
||||||
UiSize::Pixels(pixels) => pixels,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
hints: Default::default(),
|
|
||||||
user_data: None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn process(&self, measure: &Response, _state: &mut StateRepo, layout: &LayoutInfo, draw: &mut UiDrawCommands) {
|
|
||||||
if let Some(color) = self.color {
|
|
||||||
draw.add(UiDrawCommand::Rectangle {
|
|
||||||
position: layout.position,
|
|
||||||
size: measure.size,
|
|
||||||
color,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
221
hui/src/element/builtin/slider.rs
Normal file
221
hui/src/element/builtin/slider.rs
Normal file
|
@ -0,0 +1,221 @@
|
||||||
|
//! a slider element that allows selecting a value in a range
|
||||||
|
|
||||||
|
use derive_setters::Setters;
|
||||||
|
use glam::{Vec2, vec2};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
draw::UiDrawCommand, element::{MeasureContext, ProcessContext, UiElement}, frame::{Frame, RectFrame}, layout::{compute_size, Size2d}, measure::Response, rect::FillColor, signal::{trigger::SignalTriggerArg, Signal}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
//TODO: use state for slider?
|
||||||
|
// ^ useful if the user only hanldes the drag end event or has large step sizes with relative mode
|
||||||
|
|
||||||
|
//TODO: adopt frame api here
|
||||||
|
|
||||||
|
/// Follow mode for the slider
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
|
||||||
|
pub enum SliderFollowMode {
|
||||||
|
/// Slider will change based on the absolute mouse position in the slider
|
||||||
|
///
|
||||||
|
/// This is the default mode and is recommended for most use cases
|
||||||
|
#[default]
|
||||||
|
Absolute,
|
||||||
|
|
||||||
|
/// Slider will change based on the difference between the current and starting mouse position
|
||||||
|
///
|
||||||
|
/// This is an experimental option and does not currently work well for sliders with large step sizes
|
||||||
|
Relative,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A slider element that allows selecting a value in a range
|
||||||
|
#[derive(Setters)]
|
||||||
|
#[setters(prefix = "with_")]
|
||||||
|
pub struct Slider {
|
||||||
|
/// Value of the slider, should be in range 0..1
|
||||||
|
///
|
||||||
|
/// Out of range values will be clamped
|
||||||
|
pub value: f32,
|
||||||
|
|
||||||
|
/// Size of the element
|
||||||
|
#[setters(into)]
|
||||||
|
pub size: Size2d,
|
||||||
|
|
||||||
|
/// Track frame
|
||||||
|
#[setters(skip)]
|
||||||
|
pub track: Box<dyn Frame>,
|
||||||
|
|
||||||
|
/// Track active frame
|
||||||
|
#[setters(skip)]
|
||||||
|
pub track_active: Box<dyn Frame>,
|
||||||
|
|
||||||
|
/// Handle frame
|
||||||
|
#[setters(skip)]
|
||||||
|
pub handle: Box<dyn Frame>,
|
||||||
|
|
||||||
|
/// Track height *relative to the slider height*\
|
||||||
|
///
|
||||||
|
/// Range: 0.0..=1.0
|
||||||
|
pub track_height: f32,
|
||||||
|
|
||||||
|
/// Handle size
|
||||||
|
///
|
||||||
|
/// Please be aware that:
|
||||||
|
///
|
||||||
|
/// - Width is *static* and specified in *pixels* (e.g. `15.0`)
|
||||||
|
/// - Height is *relative* to the slider height,\
|
||||||
|
/// ...and is specified as a *ratio* in range `0.0..=1.0`
|
||||||
|
pub handle_size: (f32, f32),
|
||||||
|
|
||||||
|
/// Follow mode
|
||||||
|
pub follow_mode: SliderFollowMode,
|
||||||
|
|
||||||
|
#[setters(skip)]
|
||||||
|
pub on_change: Option<SignalTriggerArg<f32>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Slider {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
value: 0.0,
|
||||||
|
size: Size2d::default(),
|
||||||
|
handle: Box::new(RectFrame::color((0.0, 0.0, 1.))),
|
||||||
|
track: Box::new(RectFrame::color((0.5, 0.5, 0.5))),
|
||||||
|
track_active: Box::new(RectFrame::color((0.0, 0.0, 0.75))),
|
||||||
|
track_height: 0.25,
|
||||||
|
handle_size: (15.0, 1.),
|
||||||
|
follow_mode: SliderFollowMode::default(),
|
||||||
|
on_change: None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Slider {
|
||||||
|
pub const DEFAULT_HEIGHT: f32 = 20.0;
|
||||||
|
|
||||||
|
pub fn new(value: f32) -> Self {
|
||||||
|
Self {
|
||||||
|
value,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_change<S: Signal, T: Fn(f32) -> S + 'static>(self, f: T) -> Self {
|
||||||
|
Self {
|
||||||
|
on_change: Some(SignalTriggerArg::new(f)),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_track(mut self, track: impl Frame + 'static) -> Self {
|
||||||
|
self.track = Box::new(track);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_track_active(mut self, track_active: impl Frame + 'static) -> Self {
|
||||||
|
self.track_active = Box::new(track_active);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_handle(mut self, handle: impl Frame + 'static) -> Self {
|
||||||
|
self.handle = Box::new(handle);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UiElement for Slider {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"slider"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn measure(&self, ctx: MeasureContext) -> Response {
|
||||||
|
Response {
|
||||||
|
size: compute_size(ctx.layout, self.size, (ctx.layout.max_size.x, Self::DEFAULT_HEIGHT).into()),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process(&self, ctx: ProcessContext) {
|
||||||
|
//XXX: some of these assumptions are wrong if the corners are rounded
|
||||||
|
|
||||||
|
//Compute handle size:
|
||||||
|
// This is kinda counter-intuitive, but if the handle is transparent, we treat it as completely disabled
|
||||||
|
// To prevent confusing offset from the edge of the slider, we set the handle size to 0
|
||||||
|
// let handle_size = if self.handle_color.is_transparent() {
|
||||||
|
// Vec2::ZERO
|
||||||
|
// } else {
|
||||||
|
// vec2(15., ctx.measure.size.y)
|
||||||
|
// };
|
||||||
|
let handle_size = vec2(self.handle_size.0, self.handle_size.1 * ctx.measure.size.y);
|
||||||
|
|
||||||
|
//Draw the track
|
||||||
|
//If the active part is opaque and value >= 1., we don't need to draw the background as the active part will cover it
|
||||||
|
//However, if the handle is not opaque, we need to draw the background as the active part won't quite reach the end
|
||||||
|
//Of corse, if it's fully transparent, we don't need to draw it either
|
||||||
|
// if !(self.track_color.is_transparent() || (self.track_active_color.is_opaque() && self.handle_color.is_opaque() && self.value >= 1.)) {
|
||||||
|
if !(self.track_active.covers_opaque() && self.handle.covers_opaque() && (self.handle_size.1 >= self.track_height) && self.value >= 1.) {
|
||||||
|
self.track.draw(
|
||||||
|
ctx.draw,
|
||||||
|
(
|
||||||
|
ctx.layout.position + ctx.measure.size * vec2(0., 0.5 - self.track_height / 2.),
|
||||||
|
ctx.measure.size * vec2(1., self.track_height),
|
||||||
|
).into()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
//"Active" part of the track
|
||||||
|
//We can skip drawing it if it's fully transparent or value <= 0.
|
||||||
|
//But if the handle is not opaque, it should be visible even if value is zero
|
||||||
|
// if !(self.track_active_color.is_transparent() || (self.value <= 0. && self.handle_color.is_opaque())) {
|
||||||
|
if !(self.handle.covers_opaque() && (self.handle_size.1 >= self.track_height) && self.value <= 0.) {
|
||||||
|
self.track_active.draw(
|
||||||
|
ctx.draw,
|
||||||
|
(
|
||||||
|
ctx.layout.position + ctx.measure.size * vec2(0., 0.5 - self.track_height / 2.),
|
||||||
|
(ctx.measure.size - handle_size * Vec2::X) * vec2(self.value, self.track_height) + handle_size * Vec2::X / 2.,
|
||||||
|
).into()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The handle
|
||||||
|
// if handle_size.x != 0. && !self.handle_color.is_transparent() {
|
||||||
|
// let value = self.value.clamp(0., 1.);
|
||||||
|
// ctx.draw.add(UiDrawCommand::Rectangle {
|
||||||
|
// position: ctx.layout.position + ((ctx.measure.size.x - handle_size.x) * value) * Vec2::X,
|
||||||
|
// size: handle_size,
|
||||||
|
// color: self.handle_color.into(),
|
||||||
|
// texture: None,
|
||||||
|
// rounded_corners: None,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
if (self.handle_size.0 > 0. && self.handle_size.1 > 0.) {
|
||||||
|
self.handle.draw(
|
||||||
|
ctx.draw,
|
||||||
|
(
|
||||||
|
ctx.layout.position +
|
||||||
|
((ctx.measure.size.x - handle_size.x) * self.value) * Vec2::X +
|
||||||
|
ctx.measure.size.y * ((1. - self.handle_size.1) * 0.5) * Vec2::Y,
|
||||||
|
handle_size,
|
||||||
|
).into()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
//handle events
|
||||||
|
if let Some(res) = ctx.input.check_active(ctx.measure.rect(ctx.layout.position)) {
|
||||||
|
let new_value = match self.follow_mode {
|
||||||
|
SliderFollowMode::Absolute => {
|
||||||
|
((res.position_in_rect.x - handle_size.x / 2.) / (ctx.measure.size.x - handle_size.x)).clamp(0., 1.)
|
||||||
|
},
|
||||||
|
SliderFollowMode::Relative => {
|
||||||
|
let delta = res.position_in_rect.x - res.last_position_in_rect.x;
|
||||||
|
let delta_ratio = delta / (ctx.measure.size.x - handle_size.x);
|
||||||
|
(self.value + delta_ratio).clamp(0., 1.)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Some(signal) = &self.on_change {
|
||||||
|
signal.fire(ctx.signal, new_value);
|
||||||
|
}
|
||||||
|
//TODO call signal with new value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,13 +1,14 @@
|
||||||
|
//! Adds spacing between elements in a layout
|
||||||
|
|
||||||
use glam::vec2;
|
use glam::vec2;
|
||||||
use crate::{
|
use crate::{
|
||||||
LayoutInfo,
|
element::{MeasureContext, ProcessContext, UiElement},
|
||||||
UiDirection,
|
|
||||||
element::UiElement,
|
|
||||||
state::StateRepo,
|
|
||||||
measure::Response,
|
measure::Response,
|
||||||
draw::{UiDrawCommand, UiDrawCommands}
|
layout::Direction
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Adds spacing between elements in a layout\
|
||||||
|
/// (depending on the current layout direction)
|
||||||
pub struct Spacer(pub f32);
|
pub struct Spacer(pub f32);
|
||||||
|
|
||||||
impl Default for Spacer {
|
impl Default for Spacer {
|
||||||
|
@ -17,16 +18,19 @@ impl Default for Spacer {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UiElement for Spacer {
|
impl UiElement for Spacer {
|
||||||
fn measure(&self, state: &StateRepo, layout: &LayoutInfo) -> Response {
|
fn name(&self) -> &'static str {
|
||||||
|
"spacer"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn measure(&self, ctx: MeasureContext) -> Response {
|
||||||
Response {
|
Response {
|
||||||
size: match layout.direction {
|
size: match ctx.layout.direction {
|
||||||
UiDirection::Horizontal => vec2(self.0, 0.),
|
Direction::Horizontal => vec2(self.0, 0.),
|
||||||
UiDirection::Vertical => vec2(0., self.0),
|
Direction::Vertical => vec2(0., self.0),
|
||||||
},
|
},
|
||||||
hints: Default::default(),
|
..Default::default()
|
||||||
user_data: None
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process(&self, _measure: &Response, _state: &mut StateRepo, _layout: &LayoutInfo, _draw: &mut UiDrawCommands) {}
|
fn process(&self, _ctx: ProcessContext) {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,61 +1,107 @@
|
||||||
|
//! simple text element, renders a string of text
|
||||||
|
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
use derive_setters::Setters;
|
||||||
use glam::{vec2, Vec4};
|
use glam::{vec2, Vec4};
|
||||||
use crate::{
|
use crate::{
|
||||||
LayoutInfo,
|
draw::UiDrawCommand,
|
||||||
UiSize,
|
element::{MeasureContext, ProcessContext, UiElement},
|
||||||
element::UiElement,
|
layout::{compute_size, Size, Size2d},
|
||||||
state::StateRepo,
|
|
||||||
measure::Response,
|
measure::Response,
|
||||||
draw::{UiDrawCommand, UiDrawCommands}, text::FontHandle
|
text::FontHandle,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
//TODO: text fit
|
||||||
|
// pub enum TextSize {
|
||||||
|
// FitToWidthRatio(f32),
|
||||||
|
// FitToHeightRatio(f32),
|
||||||
|
// Constant(u8),
|
||||||
|
// }
|
||||||
|
|
||||||
|
/// Simple text element, renders a string of text
|
||||||
|
#[derive(Setters)]
|
||||||
|
#[setters(prefix = "with_")]
|
||||||
pub struct Text {
|
pub struct Text {
|
||||||
|
/// Text to render
|
||||||
|
#[setters(into)]
|
||||||
pub text: Cow<'static, str>,
|
pub text: Cow<'static, str>,
|
||||||
pub size: (UiSize, UiSize),
|
|
||||||
|
/// Size of the text element
|
||||||
|
#[setters(into)]
|
||||||
|
pub size: Size2d,
|
||||||
|
|
||||||
|
/// Color of the text
|
||||||
|
#[setters(into)]
|
||||||
pub color: Vec4,
|
pub color: Vec4,
|
||||||
pub font: FontHandle,
|
|
||||||
pub text_size: u8,
|
/// Font to use for rendering the text\
|
||||||
|
/// If set to `None` either currently selected font or the default font will be used
|
||||||
|
#[setters(into)]
|
||||||
|
pub font: Option<FontHandle>,
|
||||||
|
|
||||||
|
/// Size of the text, in points (these are not pixels)
|
||||||
|
pub text_size: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Text {
|
impl Default for Text {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
text: "".into(),
|
text: "".into(),
|
||||||
size: (UiSize::Auto, UiSize::Auto),
|
size: (Size::Auto, Size::Auto).into(),
|
||||||
color: Vec4::new(1., 1., 1., 1.),
|
color: Vec4::new(1., 1., 1., 1.),
|
||||||
font: FontHandle(0),
|
font: None,
|
||||||
text_size: 16,
|
text_size: 16,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UiElement for Text {
|
impl Text {
|
||||||
fn measure(&self, _state: &StateRepo, layout: &LayoutInfo) -> Response {
|
pub fn new(text: impl Into<Cow<'static, str>>) -> Self {
|
||||||
Response {
|
Self {
|
||||||
size: vec2(
|
text: text.into(),
|
||||||
match self.size.0 {
|
..Default::default()
|
||||||
UiSize::Auto => layout.max_size.x,
|
|
||||||
UiSize::Percentage(percentage) => layout.max_size.x * percentage,
|
|
||||||
UiSize::Pixels(pixels) => pixels,
|
|
||||||
},
|
|
||||||
match self.size.1 {
|
|
||||||
UiSize::Auto => self.text_size as f32,
|
|
||||||
UiSize::Percentage(percentage) => layout.max_size.y * percentage,
|
|
||||||
UiSize::Pixels(pixels) => pixels,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
hints: Default::default(),
|
|
||||||
user_data: None
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process(&self, _measure: &Response, _state: &mut StateRepo, layout: &LayoutInfo, draw: &mut UiDrawCommands) {
|
fn font(&self, f: FontHandle) -> FontHandle {
|
||||||
draw.add(UiDrawCommand::Text {
|
self.font.unwrap_or(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UiElement for Text {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"text"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size(&self) -> Option<Size2d> {
|
||||||
|
Some(self.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn measure(&self, ctx: MeasureContext) -> Response {
|
||||||
|
let mut size = (0., 0.);
|
||||||
|
if matches!(self.size.width, Size::Auto) || matches!(self.size.height, Size::Auto) {
|
||||||
|
//TODO optimized measure if only one of the sizes is auto
|
||||||
|
let res = ctx.text_measure.measure(self.font(ctx.current_font), self.text_size, &self.text);
|
||||||
|
size.0 = res.max_width;
|
||||||
|
size.1 = res.height;
|
||||||
|
}
|
||||||
|
Response {
|
||||||
|
size: compute_size(ctx.layout, self.size, size.into()),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process(&self, ctx: ProcessContext) {
|
||||||
|
if self.text.is_empty() || self.color.w == 0. {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.draw.add(UiDrawCommand::Text {
|
||||||
text: self.text.clone(),
|
text: self.text.clone(),
|
||||||
position: layout.position,
|
position: ctx.layout.position,
|
||||||
size: self.text_size,
|
size: self.text_size,
|
||||||
color: self.color,
|
color: self.color,
|
||||||
font: self.font
|
font: self.font(ctx.current_font),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
80
hui/src/element/builtin/transformer.rs
Normal file
80
hui/src/element/builtin/transformer.rs
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
//! wrapper that allows applying various transformations to an element, such as translation, rotation, or scaling
|
||||||
|
|
||||||
|
use glam::{Affine2, Vec2};
|
||||||
|
use crate::{
|
||||||
|
draw::UiDrawCommand, element::{MeasureContext, ProcessContext, UiElement}, measure::Response
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct Transformer {
|
||||||
|
pub transform: Affine2,
|
||||||
|
pub element: Box<dyn UiElement>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wrapper that allows applying various transformations to an element, such as translation, rotation, or scaling\
|
||||||
|
/// Use sparingly, as this is an experimental feature and may not work as expected\
|
||||||
|
impl Transformer {
|
||||||
|
pub fn new(element: Box<dyn UiElement>) -> Self {
|
||||||
|
Self {
|
||||||
|
transform: Affine2::IDENTITY,
|
||||||
|
element,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn translate(mut self, v: impl Into<Vec2>) -> Self {
|
||||||
|
self.transform *= Affine2::from_translation(v.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scale(mut self, v: impl Into<Vec2>) -> Self {
|
||||||
|
self.transform *= Affine2::from_scale(v.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rotate(mut self, radians: f32) -> Self {
|
||||||
|
self.transform *= Affine2::from_angle(radians);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UiElement for Transformer {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"transformer"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn measure(&self, ctx: MeasureContext) -> Response {
|
||||||
|
self.element.measure(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process(&self, ctx: ProcessContext) {
|
||||||
|
ctx.draw.add(UiDrawCommand::PushTransform(self.transform));
|
||||||
|
//This is stupid:
|
||||||
|
self.element.process(ProcessContext {
|
||||||
|
measure: ctx.measure,
|
||||||
|
state: ctx.state,
|
||||||
|
layout: ctx.layout,
|
||||||
|
draw: ctx.draw,
|
||||||
|
text_measure: ctx.text_measure,
|
||||||
|
current_font: ctx.current_font,
|
||||||
|
images: ctx.images,
|
||||||
|
input: ctx.input,
|
||||||
|
signal: ctx.signal,
|
||||||
|
});
|
||||||
|
ctx.draw.add(UiDrawCommand::PopTransform);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extension trait for [`UiElement`] that adds the [`transform`] method
|
||||||
|
pub trait ElementTransformExt {
|
||||||
|
/// Wrap the element in a [`Transformer`]
|
||||||
|
///
|
||||||
|
/// This allows you to apply various transformations to the element, such as translation, rotation, or scaling\
|
||||||
|
/// Use sparingly, as this is an experimental feature and may not work as expected\
|
||||||
|
/// Transform is applied around the center of the element's bounding box.
|
||||||
|
fn transform(self) -> Transformer;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: UiElement + 'static> ElementTransformExt for T {
|
||||||
|
fn transform(self) -> Transformer {
|
||||||
|
Transformer::new(Box::new(self))
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,37 @@
|
||||||
use glam::Vec2;
|
//! input, window events and event handling
|
||||||
|
|
||||||
|
use glam::Vec2;
|
||||||
|
use crate::input::{MouseButton, ButtonState, KeyboardKey};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub enum UiEvent {
|
pub enum UiEvent {
|
||||||
MouseMove(Vec2),
|
MouseMove(Vec2),
|
||||||
MouseDown(Vec2),
|
MouseButton {
|
||||||
MouseUp(Vec2),
|
button: MouseButton,
|
||||||
KeyDown(u32),
|
state: ButtonState,
|
||||||
KeyUp(u32),
|
},
|
||||||
|
KeyboardButton {
|
||||||
|
key: KeyboardKey,
|
||||||
|
state: ButtonState,
|
||||||
|
},
|
||||||
TextInput(char),
|
TextInput(char),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub(crate) struct EventQueue {
|
||||||
|
events: Vec<UiEvent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventQueue {
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn push(&mut self, event: UiEvent) {
|
||||||
|
self.events.push(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn drain(&mut self) -> std::vec::Drain<UiEvent> {
|
||||||
|
self.events.drain(..)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
27
hui/src/frame.rs
Normal file
27
hui/src/frame.rs
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
//! modular procedural background system
|
||||||
|
|
||||||
|
use crate::{draw::UiDrawCommandList, rect::Rect};
|
||||||
|
|
||||||
|
pub mod point;
|
||||||
|
mod rect;
|
||||||
|
pub mod stack;
|
||||||
|
pub mod nine_patch;
|
||||||
|
mod impls;
|
||||||
|
|
||||||
|
pub use rect::RectFrame;
|
||||||
|
|
||||||
|
/// Trait for a drawable frame
|
||||||
|
pub trait Frame {
|
||||||
|
/// Draw the frame at the given rect's position and size
|
||||||
|
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect);
|
||||||
|
|
||||||
|
/// Check if the frame is guaranteed to be fully opaque and fully cover the parent frame regardless of it's size
|
||||||
|
///
|
||||||
|
/// Returns true if the frame:
|
||||||
|
/// - Is fully opaque (i.e. `alpha >= 1.0`)
|
||||||
|
/// - Completely covers (or exceeds the size of) the frame
|
||||||
|
///
|
||||||
|
/// False negatives are acceptable, but false positives ***are not***.\
|
||||||
|
/// May be used for optimization purposes
|
||||||
|
fn covers_opaque(&self) -> bool { false }
|
||||||
|
}
|
180
hui/src/frame/impls.rs
Normal file
180
hui/src/frame/impls.rs
Normal file
|
@ -0,0 +1,180 @@
|
||||||
|
use glam::{Vec3, Vec4};
|
||||||
|
use super::Frame;
|
||||||
|
use crate::{
|
||||||
|
color,
|
||||||
|
draw::{ImageHandle, UiDrawCommand, UiDrawCommandList},
|
||||||
|
rect::{Rect, Corners, FillColor},
|
||||||
|
};
|
||||||
|
|
||||||
|
impl Frame for ImageHandle {
|
||||||
|
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
|
||||||
|
draw.add(UiDrawCommand::Rectangle {
|
||||||
|
position: rect.position,
|
||||||
|
size: rect.size,
|
||||||
|
color: color::WHITE.into(),
|
||||||
|
texture: Some(*self),
|
||||||
|
texture_uv: None,
|
||||||
|
rounded_corners: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn covers_opaque(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Frame for FillColor {
|
||||||
|
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
|
||||||
|
if self.is_transparent() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
draw.add(UiDrawCommand::Rectangle {
|
||||||
|
position: rect.position,
|
||||||
|
size: rect.size,
|
||||||
|
color: self.corners(),
|
||||||
|
texture: None,
|
||||||
|
texture_uv: None,
|
||||||
|
rounded_corners: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn covers_opaque(&self) -> bool {
|
||||||
|
self.is_opaque()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// impl for various types resembling colors
|
||||||
|
|
||||||
|
// Corners (RGBA):
|
||||||
|
|
||||||
|
impl Frame for Corners<Vec4> {
|
||||||
|
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
|
||||||
|
FillColor::from(*self).draw(draw, rect)
|
||||||
|
}
|
||||||
|
fn covers_opaque(&self) -> bool {
|
||||||
|
FillColor::from(*self).is_opaque()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Frame for (Vec4, Vec4, Vec4, Vec4) {
|
||||||
|
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
|
||||||
|
FillColor::from(*self).draw(draw, rect)
|
||||||
|
}
|
||||||
|
fn covers_opaque(&self) -> bool {
|
||||||
|
FillColor::from(*self).is_opaque()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Frame for ((f32, f32, f32, f32), (f32, f32, f32, f32), (f32, f32, f32, f32), (f32, f32, f32, f32)) {
|
||||||
|
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
|
||||||
|
FillColor::from(*self).draw(draw, rect)
|
||||||
|
}
|
||||||
|
fn covers_opaque(&self) -> bool {
|
||||||
|
FillColor::from(*self).is_opaque()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Frame for [[f32; 4]; 4] {
|
||||||
|
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
|
||||||
|
FillColor::from(*self).draw(draw, rect)
|
||||||
|
}
|
||||||
|
fn covers_opaque(&self) -> bool {
|
||||||
|
FillColor::from(*self).is_opaque()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Corners (RGB):
|
||||||
|
|
||||||
|
impl Frame for Corners<Vec3> {
|
||||||
|
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
|
||||||
|
FillColor::from(*self).draw(draw, rect)
|
||||||
|
}
|
||||||
|
fn covers_opaque(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Frame for (Vec3, Vec3, Vec3, Vec3) {
|
||||||
|
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
|
||||||
|
FillColor::from(*self).draw(draw, rect)
|
||||||
|
}
|
||||||
|
fn covers_opaque(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Frame for ((f32, f32, f32), (f32, f32, f32), (f32, f32, f32), (f32, f32, f32)) {
|
||||||
|
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
|
||||||
|
FillColor::from(*self).draw(draw, rect)
|
||||||
|
}
|
||||||
|
fn covers_opaque(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Frame for [[f32; 3]; 4] {
|
||||||
|
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
|
||||||
|
FillColor::from(*self).draw(draw, rect)
|
||||||
|
}
|
||||||
|
fn covers_opaque(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RGBA:
|
||||||
|
|
||||||
|
impl Frame for Vec4 {
|
||||||
|
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
|
||||||
|
FillColor::from(*self).draw(draw, rect)
|
||||||
|
}
|
||||||
|
fn covers_opaque(&self) -> bool {
|
||||||
|
FillColor::from(*self).is_opaque()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Frame for (f32, f32, f32, f32) {
|
||||||
|
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
|
||||||
|
FillColor::from(*self).draw(draw, rect)
|
||||||
|
}
|
||||||
|
fn covers_opaque(&self) -> bool {
|
||||||
|
FillColor::from(*self).is_opaque()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Frame for [f32; 4] {
|
||||||
|
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
|
||||||
|
FillColor::from(*self).draw(draw, rect)
|
||||||
|
}
|
||||||
|
fn covers_opaque(&self) -> bool {
|
||||||
|
FillColor::from(*self).is_opaque()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RGB:
|
||||||
|
|
||||||
|
impl Frame for Vec3 {
|
||||||
|
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
|
||||||
|
FillColor::from(*self).draw(draw, rect)
|
||||||
|
}
|
||||||
|
fn covers_opaque(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Frame for (f32, f32, f32) {
|
||||||
|
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
|
||||||
|
FillColor::from(*self).draw(draw, rect)
|
||||||
|
}
|
||||||
|
fn covers_opaque(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Frame for [f32; 3] {
|
||||||
|
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
|
||||||
|
FillColor::from(*self).draw(draw, rect)
|
||||||
|
}
|
||||||
|
fn covers_opaque(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
238
hui/src/frame/nine_patch.rs
Normal file
238
hui/src/frame/nine_patch.rs
Normal file
|
@ -0,0 +1,238 @@
|
||||||
|
//! nine-patch frame implementation
|
||||||
|
//!
|
||||||
|
//! A 9-patch image is an image that can be scaled in a way that preserves the corners and edges of the image while scaling the center.
|
||||||
|
//! This is useful for creating scalable UI elements like buttons, windows, etc.
|
||||||
|
|
||||||
|
use glam::{vec2, UVec2, Vec2};
|
||||||
|
use crate::{
|
||||||
|
color,
|
||||||
|
draw::{ImageHandle, UiDrawCommand, UiDrawCommandList},
|
||||||
|
rect::{Rect, Corners, FillColor}
|
||||||
|
};
|
||||||
|
use super::Frame;
|
||||||
|
|
||||||
|
/// Represents a 9-patch image asset
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct NinePatchAsset {
|
||||||
|
pub image: ImageHandle,
|
||||||
|
//TODO: remove this:
|
||||||
|
pub size: (u32, u32),
|
||||||
|
pub scalable_region: Rect,
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO allow scaling/moving corners
|
||||||
|
|
||||||
|
/// A 9-patch frame
|
||||||
|
///
|
||||||
|
/// Can optionally be tinted with a color (works well with grayscale assets)
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct NinePatchFrame {
|
||||||
|
pub asset: NinePatchAsset,
|
||||||
|
pub color: FillColor,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NinePatchFrame {
|
||||||
|
pub fn from_asset(asset: NinePatchAsset) -> Self {
|
||||||
|
Self { asset, ..Default::default() }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_color(mut self, color: impl Into<FillColor>) -> Self {
|
||||||
|
self.color = color.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for NinePatchFrame {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
//This is not supposed to be left out as the default, so just set it to whatever :p
|
||||||
|
asset: NinePatchAsset { image: ImageHandle::default(), size: (0, 0), scalable_region: Rect::default() },
|
||||||
|
color: color::WHITE.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Frame for NinePatchFrame {
|
||||||
|
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
|
||||||
|
// without this, shїt gets messed up when the position is not a whole number
|
||||||
|
//XXX: should we round the size as well?
|
||||||
|
let position = rect.position.round();
|
||||||
|
|
||||||
|
let img_sz = UVec2::from(self.asset.size).as_vec2();
|
||||||
|
|
||||||
|
//Color stuff
|
||||||
|
let interpolate_color_rect = |uvs: Corners<Vec2>| {
|
||||||
|
Corners {
|
||||||
|
top_left: self.color.interpolate(uvs.top_left),
|
||||||
|
top_right: self.color.interpolate(uvs.top_right),
|
||||||
|
bottom_left: self.color.interpolate(uvs.bottom_left),
|
||||||
|
bottom_right: self.color.interpolate(uvs.bottom_right),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Inset coords, in UV space
|
||||||
|
let region_uv = self.asset.scalable_region.corners();
|
||||||
|
|
||||||
|
// Inset coords, in image (px) space
|
||||||
|
let corners_image_px = Corners {
|
||||||
|
top_left: img_sz * region_uv.top_left,
|
||||||
|
top_right: img_sz * region_uv.top_right,
|
||||||
|
bottom_left: img_sz * region_uv.bottom_left,
|
||||||
|
bottom_right: img_sz * region_uv.bottom_right,
|
||||||
|
};
|
||||||
|
|
||||||
|
let size_h = (
|
||||||
|
corners_image_px.top_left.x,
|
||||||
|
rect.size.x - corners_image_px.top_left.x - (img_sz.x - corners_image_px.top_right.x),
|
||||||
|
img_sz.x - corners_image_px.top_right.x,
|
||||||
|
);
|
||||||
|
|
||||||
|
let size_v = (
|
||||||
|
corners_image_px.top_left.y,
|
||||||
|
rect.size.y - corners_image_px.top_left.y - (img_sz.y - corners_image_px.bottom_left.y),
|
||||||
|
img_sz.y - corners_image_px.bottom_left.y,
|
||||||
|
);
|
||||||
|
|
||||||
|
//Top-left patch
|
||||||
|
let top_left_patch_uv = Corners {
|
||||||
|
top_left: vec2(0., 0.),
|
||||||
|
top_right: vec2(region_uv.top_left.x, 0.),
|
||||||
|
bottom_left: vec2(0., region_uv.top_left.y),
|
||||||
|
bottom_right: region_uv.top_left,
|
||||||
|
};
|
||||||
|
draw.add(UiDrawCommand::Rectangle {
|
||||||
|
position,
|
||||||
|
size: vec2(size_h.0, size_v.0),
|
||||||
|
color: interpolate_color_rect(top_left_patch_uv),
|
||||||
|
texture: Some(self.asset.image),
|
||||||
|
texture_uv: Some(top_left_patch_uv),
|
||||||
|
rounded_corners: None
|
||||||
|
});
|
||||||
|
|
||||||
|
//Top patch
|
||||||
|
let top_patch_uv = Corners {
|
||||||
|
top_left: vec2(region_uv.top_left.x, 0.),
|
||||||
|
top_right: vec2(region_uv.top_right.x, 0.),
|
||||||
|
bottom_left: region_uv.top_left,
|
||||||
|
bottom_right: region_uv.top_right,
|
||||||
|
};
|
||||||
|
draw.add(UiDrawCommand::Rectangle {
|
||||||
|
position: position + vec2(size_h.0, 0.),
|
||||||
|
size: vec2(size_h.1, size_v.0),
|
||||||
|
color: interpolate_color_rect(top_patch_uv),
|
||||||
|
texture: Some(self.asset.image),
|
||||||
|
texture_uv: Some(top_patch_uv),
|
||||||
|
rounded_corners: None
|
||||||
|
});
|
||||||
|
|
||||||
|
//Top-right patch
|
||||||
|
let top_right_patch_uv = Corners {
|
||||||
|
top_left: vec2(region_uv.top_right.x, 0.),
|
||||||
|
top_right: vec2(1., 0.),
|
||||||
|
bottom_left: region_uv.top_right,
|
||||||
|
bottom_right: vec2(1., region_uv.top_right.y),
|
||||||
|
};
|
||||||
|
draw.add(UiDrawCommand::Rectangle {
|
||||||
|
position: position + vec2(size_h.0 + size_h.1, 0.),
|
||||||
|
size: vec2(size_h.2, size_v.0),
|
||||||
|
color: interpolate_color_rect(top_right_patch_uv),
|
||||||
|
texture: Some(self.asset.image),
|
||||||
|
texture_uv: Some(top_right_patch_uv),
|
||||||
|
rounded_corners: None
|
||||||
|
});
|
||||||
|
|
||||||
|
//Left patch
|
||||||
|
let left_patch_uv = Corners {
|
||||||
|
top_left: vec2(0., region_uv.top_left.y),
|
||||||
|
top_right: region_uv.top_left,
|
||||||
|
bottom_left: vec2(0., region_uv.bottom_left.y),
|
||||||
|
bottom_right: region_uv.bottom_left,
|
||||||
|
};
|
||||||
|
draw.add(UiDrawCommand::Rectangle {
|
||||||
|
position: position + vec2(0., size_v.0),
|
||||||
|
size: vec2(size_h.0, size_v.1),
|
||||||
|
color: interpolate_color_rect(left_patch_uv),
|
||||||
|
texture: Some(self.asset.image),
|
||||||
|
texture_uv: Some(left_patch_uv),
|
||||||
|
rounded_corners: None
|
||||||
|
});
|
||||||
|
|
||||||
|
// Center patch
|
||||||
|
draw.add(UiDrawCommand::Rectangle {
|
||||||
|
position: position + vec2(size_h.0, size_v.0),
|
||||||
|
size: vec2(size_h.1, size_v.1),
|
||||||
|
color: interpolate_color_rect(region_uv),
|
||||||
|
texture: Some(self.asset.image),
|
||||||
|
texture_uv: Some(region_uv),
|
||||||
|
rounded_corners: None
|
||||||
|
});
|
||||||
|
|
||||||
|
//Right patch
|
||||||
|
let right_patch_uv = Corners {
|
||||||
|
top_left: region_uv.top_right,
|
||||||
|
top_right: vec2(1., region_uv.top_right.y),
|
||||||
|
bottom_left: region_uv.bottom_right,
|
||||||
|
bottom_right: vec2(1., region_uv.bottom_right.y),
|
||||||
|
};
|
||||||
|
draw.add(UiDrawCommand::Rectangle {
|
||||||
|
position: position + vec2(size_h.0 + size_h.1, size_v.0),
|
||||||
|
size: vec2(size_h.2, size_v.1),
|
||||||
|
color: interpolate_color_rect(right_patch_uv),
|
||||||
|
texture: Some(self.asset.image),
|
||||||
|
texture_uv: Some(right_patch_uv),
|
||||||
|
rounded_corners: None
|
||||||
|
});
|
||||||
|
|
||||||
|
//Bottom-left patch
|
||||||
|
let bottom_left_patch_uv = Corners {
|
||||||
|
top_left: vec2(0., region_uv.bottom_left.y),
|
||||||
|
top_right: region_uv.bottom_left,
|
||||||
|
bottom_left: vec2(0., 1.),
|
||||||
|
bottom_right: vec2(region_uv.bottom_left.x, 1.),
|
||||||
|
};
|
||||||
|
draw.add(UiDrawCommand::Rectangle {
|
||||||
|
position: position + vec2(0., size_v.0 + size_v.1),
|
||||||
|
size: vec2(size_h.0, size_v.2),
|
||||||
|
color: interpolate_color_rect(bottom_left_patch_uv),
|
||||||
|
texture: Some(self.asset.image),
|
||||||
|
texture_uv: Some(bottom_left_patch_uv),
|
||||||
|
rounded_corners: None
|
||||||
|
});
|
||||||
|
|
||||||
|
//Bottom patch
|
||||||
|
let bottom_patch_uv = Corners {
|
||||||
|
top_left: region_uv.bottom_left,
|
||||||
|
top_right: region_uv.bottom_right,
|
||||||
|
bottom_left: vec2(region_uv.bottom_left.x, 1.),
|
||||||
|
bottom_right: vec2(region_uv.bottom_right.x, 1.),
|
||||||
|
};
|
||||||
|
draw.add(UiDrawCommand::Rectangle {
|
||||||
|
position: position + vec2(size_h.0, size_v.0 + size_v.1),
|
||||||
|
size: vec2(size_h.1, size_v.2),
|
||||||
|
color: interpolate_color_rect(bottom_patch_uv),
|
||||||
|
texture: Some(self.asset.image),
|
||||||
|
texture_uv: Some(bottom_patch_uv),
|
||||||
|
rounded_corners: None
|
||||||
|
});
|
||||||
|
|
||||||
|
//Bottom-right patch
|
||||||
|
let bottom_right_patch_uv = Corners {
|
||||||
|
top_left: region_uv.bottom_right,
|
||||||
|
top_right: vec2(1., region_uv.bottom_right.y),
|
||||||
|
bottom_left: vec2(region_uv.bottom_right.x, 1.),
|
||||||
|
bottom_right: vec2(1., 1.),
|
||||||
|
};
|
||||||
|
draw.add(UiDrawCommand::Rectangle {
|
||||||
|
position: position + vec2(size_h.0 + size_h.1, size_v.0 + size_v.1),
|
||||||
|
size: vec2(size_h.2, size_v.2),
|
||||||
|
color: interpolate_color_rect(bottom_right_patch_uv),
|
||||||
|
texture: Some(self.asset.image),
|
||||||
|
texture_uv: Some(bottom_right_patch_uv),
|
||||||
|
rounded_corners: None
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn covers_opaque(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
202
hui/src/frame/point.rs
Normal file
202
hui/src/frame/point.rs
Normal file
|
@ -0,0 +1,202 @@
|
||||||
|
//! frame-relative positioning/size
|
||||||
|
|
||||||
|
use glam::{Vec2, vec2};
|
||||||
|
use derive_more::{Add, AddAssign, Sub, SubAssign};
|
||||||
|
use crate::layout::{Size, Size2d};
|
||||||
|
|
||||||
|
/// Point inside a frame
|
||||||
|
///
|
||||||
|
/// Can be absolute, relative, or a combination of both\
|
||||||
|
/// Final coordinate is calculated as `absolute + relative * parent_size`
|
||||||
|
#[derive(Clone, Copy, Debug, Default, Add, AddAssign, Sub, SubAssign)]
|
||||||
|
pub struct FramePoint {
|
||||||
|
/// Absolute positioning
|
||||||
|
pub absolute: f32,
|
||||||
|
|
||||||
|
/// Relative positioning
|
||||||
|
pub relative: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<f32> for FramePoint {
|
||||||
|
fn from(value: f32) -> Self {
|
||||||
|
Self::absolute(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Size> for FramePoint {
|
||||||
|
/// Convert a `Size` into a `FramePoint`
|
||||||
|
///
|
||||||
|
/// This function behaves just as you would expect, but:
|
||||||
|
/// - `Auto` is always treated as `BEGIN`
|
||||||
|
/// - `Remaining` is treated as `Relative`
|
||||||
|
fn from(size: Size) -> Self {
|
||||||
|
match size {
|
||||||
|
Size::Auto => Self::BEGIN,
|
||||||
|
Size::Relative(value) | Size::Remaining(value) => Self::relative(value),
|
||||||
|
Size::Absolute(value) => Self::absolute(value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FramePoint {
|
||||||
|
/// Beginning of the frame axis
|
||||||
|
pub const BEGIN: Self = Self {
|
||||||
|
absolute: 0.0,
|
||||||
|
relative: 0.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Center of the frame axis
|
||||||
|
pub const CENTER: Self = Self {
|
||||||
|
absolute: 0.0,
|
||||||
|
relative: 0.5,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// End of the frame axis
|
||||||
|
pub const END: Self = Self {
|
||||||
|
absolute: 0.0,
|
||||||
|
relative: 1.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Create a new absolutely positioned `FramePoint`
|
||||||
|
pub const fn absolute(value: f32) -> Self {
|
||||||
|
Self {
|
||||||
|
absolute: value,
|
||||||
|
relative: 0.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new relatively positioned `FramePoint`
|
||||||
|
pub const fn relative(value: f32) -> Self {
|
||||||
|
Self {
|
||||||
|
absolute: 0.0,
|
||||||
|
relative: value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new `FramePoint` with both absolute and relative positioning
|
||||||
|
pub const fn relative_absolute(relative: f32, absolute: f32) -> Self {
|
||||||
|
Self {
|
||||||
|
absolute,
|
||||||
|
relative,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve the `FramePoint` into an absolute coordinate
|
||||||
|
pub(crate) fn resolve(&self, parent_size: f32) -> f32 {
|
||||||
|
self.absolute + self.relative * parent_size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A 2-dimensional [`FramePoint`]
|
||||||
|
#[derive(Clone, Copy, Debug, Default, Add, AddAssign, Sub, SubAssign)]
|
||||||
|
pub struct FramePoint2d {
|
||||||
|
pub x: FramePoint,
|
||||||
|
pub y: FramePoint,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<(FramePoint, FramePoint)> for FramePoint2d {
|
||||||
|
fn from((x, y): (FramePoint, FramePoint)) -> Self {
|
||||||
|
Self { x, y }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Size> for FramePoint2d {
|
||||||
|
fn from(size: Size) -> Self {
|
||||||
|
Self {
|
||||||
|
x: size.into(),
|
||||||
|
y: size.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Size2d> for FramePoint2d {
|
||||||
|
fn from(size: Size2d) -> Self {
|
||||||
|
Self {
|
||||||
|
x: size.width.into(),
|
||||||
|
y: size.height.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<(f32, f32)> for FramePoint2d {
|
||||||
|
fn from((x, y): (f32, f32)) -> Self {
|
||||||
|
Self {
|
||||||
|
x: FramePoint::absolute(x),
|
||||||
|
y: FramePoint::absolute(y),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Vec2> for FramePoint2d {
|
||||||
|
fn from(vec: Vec2) -> Self {
|
||||||
|
Self {
|
||||||
|
x: FramePoint::absolute(vec.x),
|
||||||
|
y: FramePoint::absolute(vec.y),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FramePoint2d {
|
||||||
|
/// Top left corner of the frame
|
||||||
|
pub const TOP_LEFT: Self = Self {
|
||||||
|
x: FramePoint::BEGIN,
|
||||||
|
y: FramePoint::BEGIN,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Top center of the frame
|
||||||
|
pub const TOP_CENTER: Self = Self {
|
||||||
|
x: FramePoint::CENTER,
|
||||||
|
y: FramePoint::BEGIN,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Top right corner of the frame
|
||||||
|
pub const TOP_RIGHT: Self = Self {
|
||||||
|
x: FramePoint::END,
|
||||||
|
y: FramePoint::BEGIN,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Center left of the frame
|
||||||
|
pub const CENTER_LEFT: Self = Self {
|
||||||
|
x: FramePoint::BEGIN,
|
||||||
|
y: FramePoint::CENTER,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Center of the frame
|
||||||
|
pub const CENTER: Self = Self {
|
||||||
|
x: FramePoint::CENTER,
|
||||||
|
y: FramePoint::CENTER,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Center right of the frame
|
||||||
|
pub const CENTER_RIGHT: Self = Self {
|
||||||
|
x: FramePoint::END,
|
||||||
|
y: FramePoint::CENTER,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Bottom left corner of the frame
|
||||||
|
pub const BOTTOM_LEFT: Self = Self {
|
||||||
|
x: FramePoint::BEGIN,
|
||||||
|
y: FramePoint::END,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Bottom center of the frame
|
||||||
|
pub const BOTTOM_CENTER: Self = Self {
|
||||||
|
x: FramePoint::CENTER,
|
||||||
|
y: FramePoint::END,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Bottom right corner of the frame
|
||||||
|
pub const BOTTOM_RIGHT: Self = Self {
|
||||||
|
x: FramePoint::END,
|
||||||
|
y: FramePoint::END,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Resolve the `FramePoint2d` into an absolute coordinate within the frame's coordinate system\
|
||||||
|
///
|
||||||
|
/// (point with absolute cordinates, relative to the frame's top-left corner)
|
||||||
|
pub(crate) fn resolve(&self, parent_size: Vec2) -> Vec2 {
|
||||||
|
let x = self.x.resolve(parent_size.x);
|
||||||
|
let y = self.y.resolve(parent_size.y);
|
||||||
|
vec2(x, y)
|
||||||
|
}
|
||||||
|
}
|
150
hui/src/frame/rect.rs
Normal file
150
hui/src/frame/rect.rs
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
use glam::Vec2;
|
||||||
|
use crate::{
|
||||||
|
color,
|
||||||
|
draw::{ImageHandle, RoundedCorners, UiDrawCommand, UiDrawCommandList},
|
||||||
|
rect::{Rect, Corners, FillColor},
|
||||||
|
};
|
||||||
|
use super::{Frame, point::FramePoint2d};
|
||||||
|
|
||||||
|
/// A rectangular frame
|
||||||
|
///
|
||||||
|
/// Can optionally be tinted, textured, and have rounded corners
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct RectFrame {
|
||||||
|
/// Background color of the frame\
|
||||||
|
///
|
||||||
|
/// If the container has a background texture, it will be multiplied by this color
|
||||||
|
pub color: FillColor,
|
||||||
|
|
||||||
|
/// Background texture of the frame
|
||||||
|
///
|
||||||
|
/// Can be used in conjunction with the background color\
|
||||||
|
/// In this case, the texture will be shaded by the color
|
||||||
|
///
|
||||||
|
/// Please note that if the background color is NOT set (or set to transparent), the texture will NOT be visible\
|
||||||
|
/// This is because the texture is multiplied by the color, and if the color is transparent, the texture will be too\
|
||||||
|
pub image: Option<ImageHandle>,
|
||||||
|
|
||||||
|
/// Top left corner of the rectangle
|
||||||
|
pub top_left: FramePoint2d,
|
||||||
|
|
||||||
|
/// Bottom right corner of the rectangle
|
||||||
|
pub bottom_right: FramePoint2d,
|
||||||
|
|
||||||
|
/// Corner radius of the frame
|
||||||
|
pub corner_radius: Corners<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// impl<T: Into<FillColor>> From<T> for RectFrame {
|
||||||
|
// fn from(color: T) -> Self {
|
||||||
|
// Self::from_color(color)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
impl From<FillColor> for RectFrame {
|
||||||
|
fn from(color: FillColor) -> Self {
|
||||||
|
Self::color(color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ImageHandle> for RectFrame {
|
||||||
|
fn from(image: ImageHandle) -> Self {
|
||||||
|
Self::image(image)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RectFrame {
|
||||||
|
/// Create a new [`RectFrame`] with the given color
|
||||||
|
pub fn color(color: impl Into<FillColor>) -> Self {
|
||||||
|
Self {
|
||||||
|
color: color.into(),
|
||||||
|
..Self::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new [`RectFrame`] with the given image\
|
||||||
|
///
|
||||||
|
/// Color will be set to [`WHITE`](crate::color::WHITE) to ensure the image is visible
|
||||||
|
pub fn image(image: ImageHandle) -> Self {
|
||||||
|
Self {
|
||||||
|
color: color::WHITE.into(),
|
||||||
|
image: Some(image),
|
||||||
|
..Self::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new [`RectFrame`] with the given color and image
|
||||||
|
pub fn color_image(color: impl Into<FillColor>, image: ImageHandle) -> Self {
|
||||||
|
Self {
|
||||||
|
color: color.into(),
|
||||||
|
image: Some(image),
|
||||||
|
..Self::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the corner radius of the [`RectFrame`]
|
||||||
|
pub fn with_corner_radius(self, radius: impl Into<Corners<f32>>) -> Self {
|
||||||
|
Self {
|
||||||
|
corner_radius: radius.into(),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: deprecate and replace
|
||||||
|
|
||||||
|
/// Inset the rectangle by the given amount in pixels
|
||||||
|
pub fn with_inset(self, inset: f32) -> Self {
|
||||||
|
Self {
|
||||||
|
top_left: self.top_left + Vec2::splat(inset).into(),
|
||||||
|
bottom_right: self.bottom_right - Vec2::splat(inset).into(),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for RectFrame {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
color: FillColor::transparent(),
|
||||||
|
image: None,
|
||||||
|
top_left: FramePoint2d::TOP_LEFT,
|
||||||
|
bottom_right: FramePoint2d::BOTTOM_RIGHT,
|
||||||
|
corner_radius: Corners::all(0.),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Frame for RectFrame {
|
||||||
|
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
|
||||||
|
if self.color.is_transparent() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
//TODO: handle bottom_right < top_left
|
||||||
|
let top_left = self.top_left.resolve(rect.size);
|
||||||
|
let bottom_right = self.bottom_right.resolve(rect.size);
|
||||||
|
draw.add(UiDrawCommand::Rectangle {
|
||||||
|
position: rect.position + top_left,
|
||||||
|
size: bottom_right - top_left,
|
||||||
|
color: self.color.corners(),
|
||||||
|
texture: self.image,
|
||||||
|
texture_uv: None,
|
||||||
|
rounded_corners: (self.corner_radius.max_f32() > 0.).then_some(
|
||||||
|
RoundedCorners::from_radius(self.corner_radius)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn covers_opaque(&self) -> bool {
|
||||||
|
self.top_left.x.relative <= 0. &&
|
||||||
|
self.top_left.x.absolute <= 0. &&
|
||||||
|
self.top_left.y.relative <= 0. &&
|
||||||
|
self.top_left.y.absolute <= 0. &&
|
||||||
|
self.bottom_right.x.relative >= 1. &&
|
||||||
|
self.bottom_right.x.absolute >= 0. &&
|
||||||
|
self.bottom_right.y.relative >= 1. &&
|
||||||
|
self.bottom_right.y.absolute >= 0. &&
|
||||||
|
self.color.is_opaque() &&
|
||||||
|
self.image.is_none() &&
|
||||||
|
self.corner_radius.max_f32() == 0.
|
||||||
|
}
|
||||||
|
}
|
37
hui/src/frame/stack.rs
Normal file
37
hui/src/frame/stack.rs
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
//! allows stacking two frames on top of each other
|
||||||
|
|
||||||
|
use crate::{draw::UiDrawCommandList, rect::Rect};
|
||||||
|
use super::Frame;
|
||||||
|
|
||||||
|
/// A frame that draws two frames on top of each other
|
||||||
|
pub struct FrameStack(pub Box<dyn Frame>, pub Box<dyn Frame>);
|
||||||
|
|
||||||
|
impl Frame for FrameStack {
|
||||||
|
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
|
||||||
|
self.0.draw(draw, rect);
|
||||||
|
self.1.draw(draw, rect);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn covers_opaque(&self) -> bool {
|
||||||
|
self.0.covers_opaque() ||
|
||||||
|
self.1.covers_opaque()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait FrameStackExt: Frame {
|
||||||
|
/// Stack another frame on top of this one
|
||||||
|
fn stack(self, other: impl Frame + 'static) -> FrameStack;
|
||||||
|
|
||||||
|
/// Stack another frame below this one
|
||||||
|
fn stack_below(self, other: impl Frame + 'static) -> FrameStack;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Frame + 'static> FrameStackExt for T {
|
||||||
|
fn stack(self, other: impl Frame + 'static) -> FrameStack {
|
||||||
|
FrameStack(Box::new(self), Box::new(other))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stack_below(self, other: impl Frame + 'static) -> FrameStack {
|
||||||
|
FrameStack(Box::new(other), Box::new(self))
|
||||||
|
}
|
||||||
|
}
|
337
hui/src/input.rs
Normal file
337
hui/src/input.rs
Normal file
|
@ -0,0 +1,337 @@
|
||||||
|
//! keyboard, mouse, and touch input handling
|
||||||
|
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
|
use glam::Vec2;
|
||||||
|
use hashbrown::HashMap;
|
||||||
|
use nohash_hasher::BuildNoHashHasher;
|
||||||
|
use tinyset::{Fits64, Set64};
|
||||||
|
use crate::{event::{EventQueue, UiEvent}, rect::Rect};
|
||||||
|
|
||||||
|
/// Represents a mouse button.
|
||||||
|
///
|
||||||
|
/// Value of the `Other` variant is currently not standardized\
|
||||||
|
/// and may change depending on the platform or the backend used
|
||||||
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||||
|
pub enum MouseButton {
|
||||||
|
///Primary mouse button (usually left)
|
||||||
|
#[default]
|
||||||
|
Primary,
|
||||||
|
///Secondary mouse button (usually right)
|
||||||
|
Secondary,
|
||||||
|
///Middle mouse button (usually the wheel button)
|
||||||
|
Middle,
|
||||||
|
///Other mouse button (e.g. extra buttons on a gaming mouse)
|
||||||
|
///
|
||||||
|
///Value is not standardized and may change depending on the platform or the backend used
|
||||||
|
Other(u8),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manual hash impl only uses one hash call
|
||||||
|
impl Hash for MouseButton {
|
||||||
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
|
match self {
|
||||||
|
MouseButton::Primary => 0u16.hash(state),
|
||||||
|
MouseButton::Secondary => 1u16.hash(state),
|
||||||
|
MouseButton::Middle => 2u16.hash(state),
|
||||||
|
MouseButton::Other(id) => ((*id as u16) << 8).hash(state),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents the state of a button, such as a mouse button or a keyboard key.\
|
||||||
|
/// Can be either `Pressed` (0) or `Released` (1).
|
||||||
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub enum ButtonState {
|
||||||
|
#[default]
|
||||||
|
Released = 0,
|
||||||
|
Pressed = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<bool> for ButtonState {
|
||||||
|
fn from(b: bool) -> Self {
|
||||||
|
if b { ButtonState::Pressed } else { ButtonState::Released }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ButtonState> for bool {
|
||||||
|
fn from(s: ButtonState) -> Self {
|
||||||
|
s.is_pressed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ButtonState {
|
||||||
|
/// Returns `true` if the button is pressed
|
||||||
|
pub fn is_pressed(self) -> bool {
|
||||||
|
self == ButtonState::Pressed
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if the button is released
|
||||||
|
pub fn is_released(self) -> bool {
|
||||||
|
self == ButtonState::Released
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents a keyboard or other hardware key (for example volume buttons)
|
||||||
|
///
|
||||||
|
/// Values of the `KeyCode` variant are not standardized and may change depending on the platform or the backend used.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum KeyboardKey {
|
||||||
|
//Keyboard buttons:
|
||||||
|
A = 0, B = 1, C = 2, D = 3, E = 4, F = 5, G = 6, H = 7, I = 8, J = 9, K = 10, L = 11, M = 12, N = 13, O = 14, P = 15, Q = 16, R = 17, S = 18, T = 19, U = 20, V = 21, W = 22, X = 23, Y = 24, Z = 25,
|
||||||
|
Num0 = 26, Num1 = 27, Num2 = 28, Num3 = 29, Num4 = 30, Num5 = 31, Num6 = 32, Num7 = 33, Num8 = 34, Num9 = 35,
|
||||||
|
Np0 = 36, Np1 = 37, Np2 = 38, Np3 = 39, Np4 = 40, Np5 = 41, Np6 = 42, Np7 = 43, Np8 = 44, Np9 = 45,
|
||||||
|
NpDivide = 46, NpMultiply = 47, NpSubtract = 48, NpAdd = 49, NpEnter = 50, NpDecimal = 51,
|
||||||
|
F1 = 52, F2 = 53, F3 = 54, F4 = 55, F5 = 56, F6 = 57, F7 = 58, F8 = 59, F9 = 60, F10 = 61, F11 = 62, F12 = 63,
|
||||||
|
Up = 64, Down = 65, Left = 66, Right = 67,
|
||||||
|
Space = 68, Enter = 69, Escape = 70, Backspace = 71, Tab = 72, CapsLock = 73,
|
||||||
|
LControl = 74, RControl = 75, LShift = 76, RShift = 77, LAlt = 78, RAlt = 79, LSuper = 80, RSuper = 81,
|
||||||
|
Grave = 82, Minus = 83, Equals = 84, LeftBracket = 85, RightBracket = 86, Backslash = 87, Semicolon = 88, Apostrophe = 89, Comma = 90, Period = 91, Slash = 92,
|
||||||
|
Insert = 93, Delete = 94, Home = 95, End = 96, PageUp = 97, PageDown = 98, PrintScreen = 99, ScrollLock = 100, Pause = 101, Menu = 102, NumLock = 103,
|
||||||
|
//Multimedia keys and android-specific (e.g. volume keys):
|
||||||
|
Mute = 104, VolumeUp = 105, VolumeDown = 106, MediaPlay = 107, MediaStop = 108, MediaNext = 109, MediaPrevious = 110,
|
||||||
|
//Keycode:
|
||||||
|
/// Represents a key code.
|
||||||
|
///
|
||||||
|
/// This enum variant holds an unsigned 32-bit integer representing a key code.\
|
||||||
|
/// The value of the key code is not standardized and may change depending on the platform or the backend used.
|
||||||
|
KeyCode(u32),
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! impl_fits64_for_keyboard_key {
|
||||||
|
($($i:ident = $v:literal),*) => {
|
||||||
|
impl Fits64 for KeyboardKey {
|
||||||
|
// SAFETY: not actually doing anything unsafe
|
||||||
|
#[allow(unsafe_code)]
|
||||||
|
unsafe fn from_u64(x: u64) -> Self {
|
||||||
|
match x {
|
||||||
|
$( $v => KeyboardKey::$i, )*
|
||||||
|
_ => KeyboardKey::KeyCode(x as u32),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_u64(self) -> u64 {
|
||||||
|
match self {
|
||||||
|
$( KeyboardKey::$i => $v, )*
|
||||||
|
KeyboardKey::KeyCode(x) => x as u64 | 0x8000000000000000u64,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_fits64_for_keyboard_key!(
|
||||||
|
A = 0, B = 1, C = 2, D = 3, E = 4, F = 5, G = 6, H = 7, I = 8, J = 9, K = 10, L = 11, M = 12, N = 13, O = 14, P = 15, Q = 16, R = 17, S = 18, T = 19, U = 20, V = 21, W = 22, X = 23, Y = 24, Z = 25,
|
||||||
|
Num0 = 26, Num1 = 27, Num2 = 28, Num3 = 29, Num4 = 30, Num5 = 31, Num6 = 32, Num7 = 33, Num8 = 34, Num9 = 35,
|
||||||
|
Np0 = 36, Np1 = 37, Np2 = 38, Np3 = 39, Np4 = 40, Np5 = 41, Np6 = 42, Np7 = 43, Np8 = 44, Np9 = 45,
|
||||||
|
NpDivide = 46, NpMultiply = 47, NpSubtract = 48, NpAdd = 49, NpEnter = 50, NpDecimal = 51,
|
||||||
|
F1 = 52, F2 = 53, F3 = 54, F4 = 55, F5 = 56, F6 = 57, F7 = 58, F8 = 59, F9 = 60, F10 = 61, F11 = 62, F12 = 63,
|
||||||
|
Up = 64, Down = 65, Left = 66, Right = 67,
|
||||||
|
Space = 68, Enter = 69, Escape = 70, Backspace = 71, Tab = 72, CapsLock = 73,
|
||||||
|
LControl = 74, RControl = 75, LShift = 76, RShift = 77, LAlt = 78, RAlt = 79, LSuper = 80, RSuper = 81,
|
||||||
|
Grave = 82, Minus = 83, Equals = 84, LeftBracket = 85, RightBracket = 86, Backslash = 87, Semicolon = 88, Apostrophe = 89, Comma = 90, Period = 91, Slash = 92,
|
||||||
|
Insert = 93, Delete = 94, Home = 95, End = 96, PageUp = 97, PageDown = 98, PrintScreen = 99, ScrollLock = 100, Pause = 101, Menu = 102, NumLock = 103,
|
||||||
|
Mute = 104, VolumeUp = 105, VolumeDown = 106, MediaPlay = 107, MediaStop = 108, MediaNext = 109, MediaPrevious = 110
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Information about the current state of a pressed mouse button
|
||||||
|
#[derive(Default, Clone, Copy, Debug)]
|
||||||
|
pub struct MouseButtonMeta {
|
||||||
|
/// Position at which the input was initiated (last time it was pressed **down**)
|
||||||
|
pub start_position: Vec2,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct MouseState {
|
||||||
|
/// Current position of the mouse pointer
|
||||||
|
pub current_position: Vec2,
|
||||||
|
|
||||||
|
/// Position of the mouse pointer on the previous frame
|
||||||
|
pub prev_position: Vec2,
|
||||||
|
|
||||||
|
/// Current state of each mouse button (if down)
|
||||||
|
pub buttons: HashMap<MouseButton, MouseButtonMeta, BuildNoHashHasher<u16>>,
|
||||||
|
|
||||||
|
/// mouse buttons that were released *in the current frame*
|
||||||
|
pub released_buttons: HashMap<MouseButton, MouseButtonMeta, BuildNoHashHasher<u16>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unique identifier of a touch pointer (finger)
|
||||||
|
pub type TouchId = u32;
|
||||||
|
|
||||||
|
pub struct TouchFinger {
|
||||||
|
/// Unique identifier of the pointer/finger
|
||||||
|
pub id: TouchId,
|
||||||
|
/// Current position of the pointer/finger
|
||||||
|
pub current_position: Vec2,
|
||||||
|
pub start_position: Vec2,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct UiInputState {
|
||||||
|
// pointers: HashMap<u32, Pointer, BuildNoHashHasher<u32>>,
|
||||||
|
mouse_pointer: MouseState,
|
||||||
|
keyboard_state: Set64<KeyboardKey>,
|
||||||
|
/// events that happened in the current frame
|
||||||
|
just_happened: Vec<UiEvent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UiInputState {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
// pointers: HashMap::default(),
|
||||||
|
mouse_pointer: MouseState::default(),
|
||||||
|
keyboard_state: Set64::new(),
|
||||||
|
just_happened: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drain the event queue and update the internal input state
|
||||||
|
///
|
||||||
|
/// This function should be called exactly once per frame
|
||||||
|
pub fn update_state(&mut self, event_queue: &mut EventQueue) {
|
||||||
|
self.mouse_pointer.prev_position = self.mouse_pointer.current_position;
|
||||||
|
self.mouse_pointer.released_buttons.clear();
|
||||||
|
self.just_happened.clear();
|
||||||
|
self.just_happened.extend(event_queue.drain());
|
||||||
|
for event in &self.just_happened {
|
||||||
|
#[allow(clippy::single_match)]
|
||||||
|
match event {
|
||||||
|
UiEvent::MouseMove(pos) => {
|
||||||
|
self.mouse_pointer.current_position = *pos;
|
||||||
|
},
|
||||||
|
UiEvent::MouseButton { button, state } => {
|
||||||
|
match state {
|
||||||
|
//wtf should we do with buttons that are pressed and released in the same frame?
|
||||||
|
//i have no fvcking idea
|
||||||
|
ButtonState::Pressed => {
|
||||||
|
let button = self.mouse_pointer.buttons.entry(*button)
|
||||||
|
.or_insert(MouseButtonMeta::default());
|
||||||
|
button.start_position = self.mouse_pointer.current_position;
|
||||||
|
},
|
||||||
|
ButtonState::Released => {
|
||||||
|
//log::trace!("Button {:?} was released", button);
|
||||||
|
if let Some(button_meta) = self.mouse_pointer.buttons.remove(button) {
|
||||||
|
//log::trace!("start pos was: {:?} current pos is: {:?}", button_meta.start_position, self.mouse_pointer.current_position);
|
||||||
|
self.mouse_pointer.released_buttons.insert(*button, button_meta);
|
||||||
|
} else {
|
||||||
|
//huh
|
||||||
|
//this can happen i guess ¯\_(=^・ω・^)_/¯
|
||||||
|
self.mouse_pointer.released_buttons.insert(*button, MouseButtonMeta {
|
||||||
|
start_position: self.mouse_pointer.current_position,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
UiEvent::KeyboardButton { key, state } => {
|
||||||
|
match state {
|
||||||
|
ButtonState::Pressed => self.keyboard_state.insert(*key),
|
||||||
|
ButtonState::Released => self.keyboard_state.remove(key),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
//TODO touch, text input
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ctx(&self) -> InputCtx {
|
||||||
|
InputCtx(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Response for checks that involve an active pointer
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct ActiveCheckResponse {
|
||||||
|
/// Current position of the pointer inside the target rectangle's coordinate space
|
||||||
|
pub position_in_rect: Vec2,
|
||||||
|
|
||||||
|
/// Position of the pointer at the time the start of the input inside the target rectangle's coordinate space
|
||||||
|
pub start_position_in_rect: Vec2,
|
||||||
|
|
||||||
|
/// Position of the pointer on the previous frame inside the target rectangle's coordinate space
|
||||||
|
pub last_position_in_rect: Vec2,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct InputCtx<'a>(&'a UiInputState);
|
||||||
|
|
||||||
|
impl<'a> InputCtx<'a> {
|
||||||
|
/// Get the current position of the mouse pointer
|
||||||
|
///
|
||||||
|
/// Do not use this function to check for hover, use [`InputCtx::check_hover`] instead
|
||||||
|
pub fn mouse_position(&self) -> Vec2 {
|
||||||
|
self.0.mouse_pointer.current_position
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current position of the mouse pointer within a rectangle
|
||||||
|
///
|
||||||
|
/// Do not use this function to check for hover, use [`InputCtx::check_hover`] instead
|
||||||
|
pub fn mouse_position_in_rect(&self, rect: Rect) -> Option<Vec2> {
|
||||||
|
let pos = self.0.mouse_pointer.current_position;
|
||||||
|
rect.contains_point(pos).then_some(pos - rect.position)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the state of a mouse button
|
||||||
|
pub fn mouse_button_down(&self, button: MouseButton) -> ButtonState {
|
||||||
|
self.0.mouse_pointer.buttons.contains_key(&button).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the start position of a mouse button\
|
||||||
|
/// (Position at the last time it was pressed **down**)
|
||||||
|
///
|
||||||
|
/// Returns `None` if the button is not currently down
|
||||||
|
pub fn mouse_button_start_position(&self, button: MouseButton) -> Option<Vec2> {
|
||||||
|
self.0.mouse_pointer.buttons.get(&button).map(|meta| meta.start_position)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the relative movement of the mouse pointer since the button was pressed down
|
||||||
|
///
|
||||||
|
/// This function is similar to [`InputCtx::mouse_button_start_position`], but returns the relative movement instead of the absolute position
|
||||||
|
pub fn mouse_button_relative_movement(&self, button: MouseButton) -> Option<Vec2> {
|
||||||
|
let start = self.mouse_button_start_position(button)?;
|
||||||
|
Some(self.mouse_position() - start)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a rect can be considered "hovered"
|
||||||
|
///
|
||||||
|
/// This can be triggered by multiple input sources, such as mouse, touch, etc.
|
||||||
|
pub fn check_hover(&self, rect: Rect) -> bool {
|
||||||
|
rect.contains_point(self.0.mouse_pointer.current_position)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a rect can be considered "clicked" in the current frame
|
||||||
|
///
|
||||||
|
/// This can be triggered by multiple input sources, such as mouse, touch, etc.\
|
||||||
|
/// In case of a mouse, these conditions must be met:
|
||||||
|
/// - The mouse button got released in the current frame
|
||||||
|
/// - The mouse pointer is currently inside the rectangle
|
||||||
|
/// - The mouse pointer was inside the rectangle at the time the button was pressed down
|
||||||
|
///
|
||||||
|
/// By default, this function only checks for the primary mouse button\
|
||||||
|
/// This is a limitation of the current API and may change in the future\
|
||||||
|
/// (as the current implementation of this function checks for both mouse and touch input, and the touch input quite obviously only supports one "button")
|
||||||
|
pub fn check_click(&self, rect: Rect) -> Option<ActiveCheckResponse> {
|
||||||
|
let pos = self.0.mouse_pointer.current_position;
|
||||||
|
self.0.mouse_pointer.released_buttons.get(&MouseButton::Primary).filter(|meta| {
|
||||||
|
rect.contains_point(meta.start_position) && rect.contains_point(pos)
|
||||||
|
}).map(|mi| ActiveCheckResponse {
|
||||||
|
position_in_rect: pos - rect.position,
|
||||||
|
start_position_in_rect: mi.start_position - rect.position,
|
||||||
|
last_position_in_rect: self.0.mouse_pointer.prev_position - rect.position,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: write better docs
|
||||||
|
|
||||||
|
/// Check if a rect is being actively being interacted with (e.g. dragged)
|
||||||
|
pub fn check_active(&self, rect: Rect) -> Option<ActiveCheckResponse> {
|
||||||
|
self.0.mouse_pointer.buttons.get(&MouseButton::Primary).filter(|mi| {
|
||||||
|
rect.contains_point(mi.start_position)
|
||||||
|
}).map(|mi| ActiveCheckResponse {
|
||||||
|
position_in_rect: self.0.mouse_pointer.current_position - rect.position,
|
||||||
|
start_position_in_rect: mi.start_position - rect.position,
|
||||||
|
last_position_in_rect: self.0.mouse_pointer.prev_position - rect.position,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
308
hui/src/instance.rs
Normal file
308
hui/src/instance.rs
Normal file
|
@ -0,0 +1,308 @@
|
||||||
|
use glam::Vec2;
|
||||||
|
use crate::{
|
||||||
|
element::{MeasureContext, ProcessContext, UiElement},
|
||||||
|
layout::{Direction, LayoutInfo},
|
||||||
|
text::{FontHandle, TextRenderer},
|
||||||
|
draw::{
|
||||||
|
ImageHandle,
|
||||||
|
TextureFormat,
|
||||||
|
UiDrawCall,
|
||||||
|
UiDrawCommandList,
|
||||||
|
atlas::{TextureAtlasManager, TextureAtlasMeta},
|
||||||
|
},
|
||||||
|
signal::{Signal, SignalStore},
|
||||||
|
event::{EventQueue, UiEvent},
|
||||||
|
input::UiInputState,
|
||||||
|
rect::Rect,
|
||||||
|
state::StateRepo,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The main instance of the UI system.
|
||||||
|
///
|
||||||
|
/// In most cases, you should only have one instance of this struct, but multiple instances are allowed\
|
||||||
|
/// (Please note that it's possible to render multiple UI "roots" using a single instance)
|
||||||
|
pub struct UiInstance {
|
||||||
|
stateful_state: StateRepo,
|
||||||
|
prev_draw_commands: UiDrawCommandList,
|
||||||
|
draw_commands: UiDrawCommandList,
|
||||||
|
draw_call: UiDrawCall,
|
||||||
|
draw_call_modified: bool,
|
||||||
|
text_renderer: TextRenderer,
|
||||||
|
atlas: TextureAtlasManager,
|
||||||
|
events: EventQueue,
|
||||||
|
input: UiInputState,
|
||||||
|
signal: SignalStore,
|
||||||
|
/// True if in the middle of a laying out a frame
|
||||||
|
state: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UiInstance {
|
||||||
|
/// Crate and initialize a new instance of the UI
|
||||||
|
///
|
||||||
|
/// In most cases, you should only do this *once*, during the initialization of your application
|
||||||
|
pub fn new() -> Self {
|
||||||
|
UiInstance {
|
||||||
|
//mouse_position: Vec2::ZERO,
|
||||||
|
stateful_state: StateRepo::new(),
|
||||||
|
//event_queue: VecDeque::new(),
|
||||||
|
// root_elements: Vec::new(),
|
||||||
|
prev_draw_commands: UiDrawCommandList::default(),
|
||||||
|
draw_commands: UiDrawCommandList::default(),
|
||||||
|
draw_call: UiDrawCall::default(),
|
||||||
|
draw_call_modified: false,
|
||||||
|
// ftm: FontTextureManager::default(),
|
||||||
|
text_renderer: TextRenderer::new(),
|
||||||
|
atlas: {
|
||||||
|
let mut atlas = TextureAtlasManager::default();
|
||||||
|
atlas.add_dummy();
|
||||||
|
atlas
|
||||||
|
},
|
||||||
|
events: EventQueue::new(),
|
||||||
|
input: UiInputState::new(),
|
||||||
|
signal: SignalStore::new(),
|
||||||
|
state: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse and add a font from a raw byte slice to the UI\
|
||||||
|
/// TrueType (`.ttf`/`.ttc`) and OpenType (`.otf`) fonts are supported\
|
||||||
|
///
|
||||||
|
/// Returns a font handle ([`FontHandle`]).
|
||||||
|
///
|
||||||
|
/// ## Panics:
|
||||||
|
/// If the font data is invalid or corrupt
|
||||||
|
pub fn add_font(&mut self, font: &[u8]) -> FontHandle {
|
||||||
|
self.text_renderer.add_font_from_bytes(font)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add an image to the texture atlas\
|
||||||
|
/// Accepted texture formats are `Rgba` and `Grayscale`
|
||||||
|
///
|
||||||
|
/// Returns an image handle ([`ImageHandle`])\
|
||||||
|
/// This handle can be used to reference the texture in draw commands\
|
||||||
|
/// It's a light reference and can be cloned/copied freely, but will not be cleaned up even when dropped
|
||||||
|
pub fn add_image(&mut self, format: TextureFormat, data: &[u8], width: usize) -> ImageHandle {
|
||||||
|
self.atlas.add(width, data, format)
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO better error handling
|
||||||
|
|
||||||
|
/// Add an image from a file to the texture atlas\
|
||||||
|
/// (experimental, may be removed in the future)
|
||||||
|
///
|
||||||
|
/// Requires the `image` feature
|
||||||
|
///
|
||||||
|
/// # Panics:
|
||||||
|
/// - If the file exists but contains invalid image data\
|
||||||
|
/// (this will change to a soft error in the future)
|
||||||
|
#[cfg(feature = "image")]
|
||||||
|
pub fn add_image_file_path(&mut self, path: impl AsRef<std::path::Path>) -> Result<ImageHandle, std::io::Error> {
|
||||||
|
use std::io::{Read, Seek};
|
||||||
|
|
||||||
|
// Open the file (and wrap it in a bufreader)
|
||||||
|
let mut file = std::io::BufReader::new(std::fs::File::open(path)?);
|
||||||
|
|
||||||
|
//Guess the image format from the magic bytes
|
||||||
|
//Read like 64 bytes, which should be enough for magic byte detection
|
||||||
|
//well this would fail if the image is somehow smaller than 64 bytes, but who the fvck cares...
|
||||||
|
let mut magic = [0; 64];
|
||||||
|
file.read_exact(&mut magic)?;
|
||||||
|
let format = image::guess_format(&magic).expect("Invalid image data (FORMAT)");
|
||||||
|
file.seek(std::io::SeekFrom::Start(0))?;
|
||||||
|
|
||||||
|
//Parse the image and read the raw uncompressed rgba data
|
||||||
|
let image = image::load(file, format).expect("Invalid image data");
|
||||||
|
let image_rgba = image.as_rgba8().unwrap();
|
||||||
|
|
||||||
|
//Add the image to the atlas
|
||||||
|
let handle = self.add_image(
|
||||||
|
TextureFormat::Rgba,
|
||||||
|
image_rgba,
|
||||||
|
image.width() as usize
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Push a font to the font stack\
|
||||||
|
/// The font will be used for all text rendering until it is popped
|
||||||
|
///
|
||||||
|
/// This function is useful for replacing the default font, use sparingly\
|
||||||
|
/// (This library attempts to be stateless, however passing the font to every text element is not very practical)
|
||||||
|
pub fn push_font(&mut self, font: FontHandle) {
|
||||||
|
self.text_renderer.push_font(font);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pop a font from the font stack\
|
||||||
|
///
|
||||||
|
/// ## Panics:
|
||||||
|
/// If the font stack is empty
|
||||||
|
pub fn pop_font(&mut self) {
|
||||||
|
self.text_renderer.pop_font();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current default font
|
||||||
|
pub fn current_font(&self) -> FontHandle {
|
||||||
|
self.text_renderer.current_font()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add an element or an element tree to the UI
|
||||||
|
///
|
||||||
|
/// Use the `rect` parameter to specify the position and size of the element\
|
||||||
|
/// (usually, the size of the window/screen)
|
||||||
|
///
|
||||||
|
/// ## Panics:
|
||||||
|
/// If called while the UI is not active (call [`UiInstance::begin`] first)
|
||||||
|
pub fn add(&mut self, element: impl UiElement, rect: impl Into<Rect>) {
|
||||||
|
assert!(self.state, "must call UiInstance::begin before adding elements");
|
||||||
|
let rect: Rect = rect.into();
|
||||||
|
let layout = LayoutInfo {
|
||||||
|
position: rect.position,
|
||||||
|
max_size: rect.size,
|
||||||
|
direction: Direction::Vertical,
|
||||||
|
remaining_space: None,
|
||||||
|
};
|
||||||
|
let measure = element.measure(MeasureContext {
|
||||||
|
state: &self.stateful_state,
|
||||||
|
layout: &layout,
|
||||||
|
text_measure: self.text_renderer.to_measure(),
|
||||||
|
current_font: self.text_renderer.current_font(),
|
||||||
|
images: self.atlas.context(),
|
||||||
|
});
|
||||||
|
element.process(ProcessContext {
|
||||||
|
measure: &measure,
|
||||||
|
state: &mut self.stateful_state,
|
||||||
|
layout: &layout,
|
||||||
|
draw: &mut self.draw_commands,
|
||||||
|
text_measure: self.text_renderer.to_measure(),
|
||||||
|
current_font: self.text_renderer.current_font(),
|
||||||
|
images: self.atlas.context(),
|
||||||
|
input: self.input.ctx(),
|
||||||
|
signal: &mut self.signal,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prepare the UI for layout and processing\
|
||||||
|
/// You must call this function at the beginning of the frame, before adding any elements\
|
||||||
|
///
|
||||||
|
/// ## Panics:
|
||||||
|
/// If called twice in a row (for example, if you forget to call [`UiInstance::end`])\
|
||||||
|
/// This is an indication of a bug in your code and should be fixed.
|
||||||
|
pub fn begin(&mut self) {
|
||||||
|
//check and update current state
|
||||||
|
assert!(!self.state, "must call UiInstance::end before calling UiInstance::begin again");
|
||||||
|
self.state = true;
|
||||||
|
|
||||||
|
//first, drain and process the event queue
|
||||||
|
self.input.update_state(&mut self.events);
|
||||||
|
|
||||||
|
//then, reset the (remaining) signals
|
||||||
|
self.signal.clear();
|
||||||
|
|
||||||
|
//then, reset the draw commands
|
||||||
|
std::mem::swap(&mut self.prev_draw_commands, &mut self.draw_commands);
|
||||||
|
self.draw_commands.commands.clear();
|
||||||
|
self.draw_call_modified = false;
|
||||||
|
|
||||||
|
//reset atlas modification flag
|
||||||
|
self.atlas.reset_modified();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// End the frame and prepare the UI for rendering\
|
||||||
|
/// You must call this function at the end of the frame, before rendering the UI
|
||||||
|
///
|
||||||
|
/// ## Panics:
|
||||||
|
/// If called without calling [`UiInstance::begin`] first. (or if called twice)\
|
||||||
|
/// This is an indication of a bug in your code and should be fixed.
|
||||||
|
pub fn end(&mut self) {
|
||||||
|
//check and update current state
|
||||||
|
assert!(self.state, "must call UiInstance::begin before calling UiInstance::end");
|
||||||
|
self.state = false;
|
||||||
|
|
||||||
|
//check if the draw commands have been modified
|
||||||
|
if self.draw_commands.commands == self.prev_draw_commands.commands {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//if they have, rebuild the draw call and set the modified flag
|
||||||
|
self.draw_call = UiDrawCall::build(&self.draw_commands, &mut self.atlas, &mut self.text_renderer);
|
||||||
|
self.draw_call_modified = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the draw call information for the current frame
|
||||||
|
///
|
||||||
|
/// This function should only be used by the render backend.\
|
||||||
|
/// You should not call this directly unless you're implementing a custom render backend
|
||||||
|
///
|
||||||
|
/// Returns a tuple with a boolean indicating if the buffers have been modified since the last frame
|
||||||
|
///
|
||||||
|
/// You should only call this function *after* [`UiInstance::end`]\
|
||||||
|
/// Calling it in the middle of a frame will result in a warning but will not cause a panic\
|
||||||
|
/// (please note that doing so is probably a mistake and should be fixed in your code)\
|
||||||
|
/// Doing so anyway will return draw call data for the previous frame, but the `modified` flag will *always* be incorrect until [`UiInstance::end`] is called
|
||||||
|
///
|
||||||
|
pub fn draw_call(&self) -> (bool, &UiDrawCall) {
|
||||||
|
if self.state {
|
||||||
|
log::warn!("UiInstance::draw_call called while in the middle of a frame, this is probably a mistake");
|
||||||
|
}
|
||||||
|
(self.draw_call_modified, &self.draw_call)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the texture atlas size and data for the current frame
|
||||||
|
///
|
||||||
|
/// This function should only be used by the render backend.\
|
||||||
|
/// You should not call this directly unless you're implementing a custom render backend
|
||||||
|
///
|
||||||
|
/// You should only call this function *after* [`UiInstance::end`]\
|
||||||
|
/// Calling it in the middle of a frame will result in a warning but will not cause a panic\
|
||||||
|
/// (please note that doing so is probably a mistake and should be fixed in your code)\
|
||||||
|
/// Using this function in the middle of a frame will return partially modified atlas data that may be outdated or incomplete\
|
||||||
|
/// This will lead to rendering artifacts, 1-frame delays and flashes and is probably not what you want
|
||||||
|
///
|
||||||
|
/// Make sure to check [`TextureAtlasMeta::modified`] to see if the texture has been modified
|
||||||
|
/// since the beginning of the current frame before uploading it to the GPU
|
||||||
|
pub fn atlas(&self) -> TextureAtlasMeta {
|
||||||
|
if self.state {
|
||||||
|
log::warn!("UiInstance::atlas called while in the middle of a frame, this is probably a mistake");
|
||||||
|
}
|
||||||
|
self.atlas.meta()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Push a platform event to the UI event queue
|
||||||
|
///
|
||||||
|
/// You should call this function *before* calling [`UiInstance::begin`] or after calling [`UiInstance::end`]\
|
||||||
|
/// Calling it in the middle of a frame will result in a warning but will not cause a panic\
|
||||||
|
/// (please note that doing so is probably a mistake and should be fixed in your code)\
|
||||||
|
/// In this case, the event will be processed in the next frame, but in some cases may affect the current frame.\
|
||||||
|
/// (The exact behavior is not guaranteed and you should avoid doing this if possible)
|
||||||
|
///
|
||||||
|
/// This function should only be used by the platform backend.\
|
||||||
|
/// You should not call this directly unless you're implementing a custom platform backend
|
||||||
|
/// or have a very specific usecase
|
||||||
|
pub fn push_event(&mut self, event: UiEvent) {
|
||||||
|
if self.state {
|
||||||
|
log::warn!("UiInstance::push_event called while in the middle of a frame, this is probably a mistake");
|
||||||
|
}
|
||||||
|
self.events.push(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Push a "fake" signal to the UI signal queue
|
||||||
|
pub fn push_signal<T: Signal + 'static>(&mut self, signal: T) {
|
||||||
|
self.signal.add(signal);
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: offer a non-consuming version of this function for T: Clone
|
||||||
|
|
||||||
|
/// Process all signals of a given type
|
||||||
|
///
|
||||||
|
/// This clears the signal queue for the given type and iterates over all signals
|
||||||
|
pub fn process_signals<T: Signal + 'static>(&mut self, f: impl FnMut(T)) {
|
||||||
|
self.signal.drain::<T>().for_each(f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for UiInstance {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,51 +0,0 @@
|
||||||
use crate::{element::UiElement, draw::UiDrawCommands};
|
|
||||||
|
|
||||||
pub struct Interactable<T: UiElement> {
|
|
||||||
pub element: T,
|
|
||||||
pub hovered: Option<Box<dyn FnOnce()>>,
|
|
||||||
pub clicked: Option<Box<dyn FnOnce()>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: UiElement> Interactable<T> {
|
|
||||||
pub fn new(element: T) -> Self {
|
|
||||||
Self {
|
|
||||||
element,
|
|
||||||
hovered: None,
|
|
||||||
clicked: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn on_click(self, clicked: impl FnOnce() + 'static) -> Self {
|
|
||||||
Self {
|
|
||||||
clicked: Some(Box::new(clicked)),
|
|
||||||
..self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn on_hover(self, clicked: impl FnOnce() + 'static) -> Self {
|
|
||||||
Self {
|
|
||||||
clicked: Some(Box::new(clicked)),
|
|
||||||
..self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: UiElement> UiElement for Interactable<T> {
|
|
||||||
fn measure(&self, state: &crate::state::StateRepo, layout: &crate::LayoutInfo) -> crate::measure::Response {
|
|
||||||
self.element.measure(state, layout)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn process(&self, measure: &crate::measure::Response, state: &mut crate::state::StateRepo, layout: &crate::LayoutInfo, draw: &mut UiDrawCommands) {
|
|
||||||
self.element.process(measure, state, layout, draw)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait IntoInteractable<T: UiElement>: UiElement {
|
|
||||||
fn into_interactable(self) -> Interactable<T>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: UiElement> IntoInteractable<T> for T {
|
|
||||||
fn into_interactable(self) -> Interactable<Self> {
|
|
||||||
Interactable::new(self)
|
|
||||||
}
|
|
||||||
}
|
|
240
hui/src/layout.rs
Normal file
240
hui/src/layout.rs
Normal file
|
@ -0,0 +1,240 @@
|
||||||
|
//! element layout, alignment and sizing
|
||||||
|
|
||||||
|
use glam::{vec2, Vec2};
|
||||||
|
|
||||||
|
/// Controls wrapping behavior of elements
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord, Default)]
|
||||||
|
pub enum WrapBehavior {
|
||||||
|
/// No wrapping is allowed, even if explicit line breaks is requested by the element
|
||||||
|
Disable = 0,
|
||||||
|
|
||||||
|
/// Allow wrapping if the element explicitly requests it (default behavior)
|
||||||
|
#[default]
|
||||||
|
Allow = 1,
|
||||||
|
|
||||||
|
/// Elements will be wrapped automatically when they reach the maximum width/height of the container
|
||||||
|
Enable = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<bool> for WrapBehavior {
|
||||||
|
#[inline]
|
||||||
|
fn from(value: bool) -> Self {
|
||||||
|
match value {
|
||||||
|
true => Self::Enable,
|
||||||
|
false => Self::Disable,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WrapBehavior {
|
||||||
|
/// Check if wrapping is allowed for the element
|
||||||
|
#[inline]
|
||||||
|
pub fn is_allowed(&self) -> bool {
|
||||||
|
*self != Self::Disable
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if wrapping is enabled for the element
|
||||||
|
///
|
||||||
|
/// (Wrapping will be done automatically when the element reaches the maximum width/height)
|
||||||
|
#[inline]
|
||||||
|
pub fn is_enabled(&self) -> bool {
|
||||||
|
*self == Self::Enable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Alignment along a single axis
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, PartialOrd, Ord)]
|
||||||
|
pub enum Alignment {
|
||||||
|
/// Put the element at the beginning of the axis\
|
||||||
|
/// (left for horizontal, top for vertical alignment)
|
||||||
|
#[default]
|
||||||
|
Begin = 0,
|
||||||
|
|
||||||
|
/// Put the element in the center
|
||||||
|
Center = 1,
|
||||||
|
|
||||||
|
/// Put the element at the end of the axis\
|
||||||
|
/// (right for horizontal, bottom for vertical alignment)
|
||||||
|
End = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents alignment in 2D space
|
||||||
|
///
|
||||||
|
/// - `horizontal` - alignment *along* x-axis (horizontal)\
|
||||||
|
/// - `vertical` - alignment *along* y-axis (vertical)
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, PartialOrd, Ord)]
|
||||||
|
pub struct Alignment2d {
|
||||||
|
/// Alignment *along* horizontal axis (X)
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// ├───────[ ]──────┤
|
||||||
|
/// ↑↑ ↑↑ ↑↑
|
||||||
|
/// Begin Center End
|
||||||
|
/// ```
|
||||||
|
pub horizontal: Alignment,
|
||||||
|
|
||||||
|
/// Alignment *along* vertical axis (Y)
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// ┬ ←─ Begin
|
||||||
|
/// │
|
||||||
|
/// [ ] ←─ Center
|
||||||
|
/// │
|
||||||
|
/// ┴ ←─ End
|
||||||
|
/// ```
|
||||||
|
pub vertical: Alignment,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Alignment2d {
|
||||||
|
/// Create a new `Alignment2d` with the same alignment for both axes
|
||||||
|
#[inline]
|
||||||
|
pub const fn all(alignment: Alignment) -> Self {
|
||||||
|
Self {
|
||||||
|
horizontal: alignment,
|
||||||
|
vertical: alignment,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<(Alignment, Alignment)> for Alignment2d {
|
||||||
|
#[inline]
|
||||||
|
fn from((horizontal, vertical): (Alignment, Alignment)) -> Self {
|
||||||
|
Self { horizontal, vertical }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<[Alignment; 2]> for Alignment2d {
|
||||||
|
#[inline]
|
||||||
|
fn from([horizontal, vertical]: [Alignment; 2]) -> Self {
|
||||||
|
Self { horizontal, vertical }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Alignment> for Alignment2d {
|
||||||
|
#[inline]
|
||||||
|
fn from(alignment: Alignment) -> Self {
|
||||||
|
Self::all(alignment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents a single size dimension of an UI element.\
|
||||||
|
/// Can be either a static size in pixels, a fraction the parent size or auto-calculated\
|
||||||
|
/// (Meaning of `auto` is entirely dependent on the element).
|
||||||
|
#[derive(Default, Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub enum Size {
|
||||||
|
/// Automatically calculate size based on content
|
||||||
|
#[default]
|
||||||
|
Auto,
|
||||||
|
|
||||||
|
/// Static size in pixels
|
||||||
|
Absolute(f32),
|
||||||
|
|
||||||
|
/// Size as a ratio of parent element size
|
||||||
|
///
|
||||||
|
/// Expected range: `0.0..=1.0`
|
||||||
|
Relative(f32),
|
||||||
|
|
||||||
|
/// Size as a ratio of remaining space after all other elements have been laid out
|
||||||
|
///
|
||||||
|
/// Expected range: `0.0..=1.0`
|
||||||
|
///
|
||||||
|
/// - This feature is experimental and may not work as expected;\
|
||||||
|
/// Current `Container` implementation:
|
||||||
|
/// - Assumes that he line is fully filled if any element uses `Remaining` size, even if sum of remaining sizes is less than 1.0
|
||||||
|
/// - Does not support `Remaining` size in the secondary axis, it will be treated as `Relative`
|
||||||
|
/// - In cases where it's not applicable or not supported, it's defined to behave as `Relative`
|
||||||
|
Remaining(f32),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<f32> for Size {
|
||||||
|
#[inline]
|
||||||
|
fn from(value: f32) -> Self {
|
||||||
|
Self::Absolute(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub struct Size2d {
|
||||||
|
pub width: Size,
|
||||||
|
pub height: Size,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<(Size, Size)> for Size2d {
|
||||||
|
#[inline]
|
||||||
|
fn from((width, height): (Size, Size)) -> Self {
|
||||||
|
Self { width, height }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//XXX: should this exist?
|
||||||
|
impl From<Size> for Size2d {
|
||||||
|
#[inline]
|
||||||
|
fn from(size: Size) -> Self {
|
||||||
|
Self {
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents the direction of the layout\
|
||||||
|
/// (for example, the direction of a container's children)\
|
||||||
|
///
|
||||||
|
/// - `Vertical` - Children are laid out from top to bottom\
|
||||||
|
/// - `Horizontal` - Children are laid out from left to right
|
||||||
|
#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub enum Direction {
|
||||||
|
/// Children are laid out from top to bottom
|
||||||
|
#[default]
|
||||||
|
Vertical,
|
||||||
|
/// Children are laid out from left to right
|
||||||
|
Horizontal,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents the layout information required to measure, layout and render an element.\
|
||||||
|
/// Includes the position, maximum size, direction of the layout and other information
|
||||||
|
pub struct LayoutInfo {
|
||||||
|
/// Screen-space coordinates of the top-left corner of the element.\
|
||||||
|
/// Use this value during the layout step to render the element
|
||||||
|
///
|
||||||
|
/// Not available during the measure step (will be set to zero)
|
||||||
|
pub position: Vec2,
|
||||||
|
|
||||||
|
/// Maximum size the element is allowed to take up
|
||||||
|
pub max_size: Vec2,
|
||||||
|
|
||||||
|
/// Current direction of the layout\
|
||||||
|
/// (Usually matches direction of the parent container)
|
||||||
|
pub direction: Direction,
|
||||||
|
|
||||||
|
/// Remaining space in the primary axis\
|
||||||
|
///
|
||||||
|
/// This value is only available during the layout step and is only likely to be present if the element uses `Size::Remaining`
|
||||||
|
///
|
||||||
|
/// (Make sure that LayoutInfo::direction is set to the correct direction!)
|
||||||
|
pub remaining_space: Option<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper function to calculate the size of an element based on its layout and size information\
|
||||||
|
/// Used to help reduce code duplication in the `measure` method of UI elements
|
||||||
|
pub fn compute_size(layout: &LayoutInfo, size: Size2d, comfy_size: Vec2) -> Vec2 {
|
||||||
|
let width = match size.width {
|
||||||
|
Size::Auto => comfy_size.x,
|
||||||
|
Size::Relative(fraction) => layout.max_size.x * fraction,
|
||||||
|
Size::Absolute(size) => size,
|
||||||
|
Size::Remaining(fraction) => match layout.direction {
|
||||||
|
Direction::Horizontal => layout.remaining_space.unwrap_or(layout.max_size.x) * fraction,
|
||||||
|
Direction::Vertical => layout.max_size.x * fraction,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let height = match size.height {
|
||||||
|
Size::Auto => comfy_size.y,
|
||||||
|
Size::Relative(fraction) => layout.max_size.y * fraction,
|
||||||
|
Size::Absolute(size) => size,
|
||||||
|
Size::Remaining(fraction) => match layout.direction {
|
||||||
|
Direction::Horizontal => layout.max_size.y * fraction,
|
||||||
|
Direction::Vertical => layout.remaining_space.unwrap_or(layout.max_size.y) * fraction,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
vec2(width, height)
|
||||||
|
}
|
140
hui/src/lib.rs
140
hui/src/lib.rs
|
@ -1,135 +1,29 @@
|
||||||
#![forbid(unsafe_code)]
|
|
||||||
#![forbid(unsafe_op_in_unsafe_fn)]
|
|
||||||
#![doc(html_logo_url = "https://raw.githubusercontent.com/griffi-gh/hui/master/.assets/hui.svg")]
|
#![doc(html_logo_url = "https://raw.githubusercontent.com/griffi-gh/hui/master/.assets/hui.svg")]
|
||||||
//!
|
//!
|
||||||
//! Simple UI library for games and other interactive applications
|
//! Simple UI library for games and other interactive applications
|
||||||
//!
|
//!
|
||||||
|
//! # Features
|
||||||
|
#![doc = document_features::document_features!()]
|
||||||
|
|
||||||
|
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||||
|
|
||||||
|
#![deny(unsafe_code)]
|
||||||
|
#![forbid(unsafe_op_in_unsafe_fn)]
|
||||||
|
#![allow(unused_parens)]
|
||||||
|
|
||||||
|
mod instance;
|
||||||
|
mod macros;
|
||||||
|
pub mod layout;
|
||||||
|
pub mod rect;
|
||||||
pub mod element;
|
pub mod element;
|
||||||
pub mod event;
|
pub mod event;
|
||||||
|
pub mod input;
|
||||||
pub mod draw;
|
pub mod draw;
|
||||||
pub mod measure;
|
pub mod measure;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
pub mod text;
|
pub mod text;
|
||||||
pub mod interaction;
|
pub mod color;
|
||||||
|
pub mod signal;
|
||||||
|
pub mod frame;
|
||||||
|
|
||||||
use element::UiElement;
|
pub use instance::UiInstance;
|
||||||
use state::StateRepo;
|
|
||||||
use draw::{UiDrawCommands, UiDrawPlan};
|
|
||||||
use text::{TextRenderer, FontTextureInfo, FontHandle};
|
|
||||||
use glam::Vec2;
|
|
||||||
|
|
||||||
// pub struct ElementContext<'a> {
|
|
||||||
// pub state: &'a mut StateRepo,
|
|
||||||
// pub draw: &'a mut UiDrawCommands,
|
|
||||||
// pub text: &'a mut TextRenderer,
|
|
||||||
// }
|
|
||||||
pub trait IfModified<T> {
|
|
||||||
fn if_modified(&self) -> Option<&T>;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct UiInstance {
|
|
||||||
//mouse_position: Vec2,
|
|
||||||
stateful_state: StateRepo,
|
|
||||||
//event_queue: VecDeque<UiEvent>,
|
|
||||||
prev_draw_commands: UiDrawCommands,
|
|
||||||
draw_commands: UiDrawCommands,
|
|
||||||
draw_plan: UiDrawPlan,
|
|
||||||
draw_plan_modified: bool,
|
|
||||||
text_renderer: TextRenderer,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UiInstance {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
UiInstance {
|
|
||||||
//mouse_position: Vec2::ZERO,
|
|
||||||
stateful_state: StateRepo::default(),
|
|
||||||
//event_queue: VecDeque::new(),
|
|
||||||
// root_elements: Vec::new(),
|
|
||||||
prev_draw_commands: UiDrawCommands::default(),
|
|
||||||
draw_commands: UiDrawCommands::default(),
|
|
||||||
draw_plan: UiDrawPlan::default(),
|
|
||||||
draw_plan_modified: false,
|
|
||||||
// ftm: FontTextureManager::default(),
|
|
||||||
text_renderer: TextRenderer::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_font_from_bytes(&mut self, font: &[u8]) -> FontHandle {
|
|
||||||
self.text_renderer.add_font_from_bytes(font)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add<T: UiElement>(&mut self, element: T, max_size: Vec2) {
|
|
||||||
let layout = LayoutInfo {
|
|
||||||
position: Vec2::ZERO,
|
|
||||||
max_size,
|
|
||||||
direction: UiDirection::Vertical,
|
|
||||||
};
|
|
||||||
let measure = element.measure(&self.stateful_state, &layout);
|
|
||||||
element.process(&measure, &mut self.stateful_state, &layout, &mut self.draw_commands);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn begin(&mut self) {
|
|
||||||
std::mem::swap(&mut self.prev_draw_commands, &mut self.draw_commands);
|
|
||||||
self.draw_plan_modified = false;
|
|
||||||
self.draw_commands.commands.clear();
|
|
||||||
self.text_renderer.reset_frame();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn end(&mut self) {
|
|
||||||
if self.draw_commands.commands == self.prev_draw_commands.commands {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
self.draw_plan = UiDrawPlan::build(&self.draw_commands, &mut self.text_renderer);
|
|
||||||
self.draw_plan_modified = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn draw_plan(&self) -> (bool, &UiDrawPlan) {
|
|
||||||
(self.draw_plan_modified, &self.draw_plan)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn font_texture(&self) -> FontTextureInfo {
|
|
||||||
self.text_renderer.font_texture()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for UiInstance {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Copy)]
|
|
||||||
pub enum UiSize {
|
|
||||||
#[default]
|
|
||||||
Auto,
|
|
||||||
Percentage(f32),
|
|
||||||
Pixels(f32),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
|
||||||
pub enum UiDirection {
|
|
||||||
#[default]
|
|
||||||
Vertical,
|
|
||||||
Horizontal,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct LayoutInfo {
|
|
||||||
///Not availabe during measuring step
|
|
||||||
pub position: Vec2,
|
|
||||||
pub max_size: Vec2,
|
|
||||||
pub direction: UiDirection,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ElementList(Vec<Box<dyn UiElement>>);
|
|
||||||
|
|
||||||
impl ElementList {
|
|
||||||
pub fn add(&mut self, element: impl UiElement + 'static) {
|
|
||||||
self.0.push(Box::new(element));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn elements(f: impl FnOnce(&mut ElementList)) -> Vec<Box<dyn UiElement>> {
|
|
||||||
let mut elements = ElementList(Vec::new());
|
|
||||||
f(&mut elements);
|
|
||||||
elements.0
|
|
||||||
}
|
|
||||||
|
|
138
hui/src/macros.rs
Normal file
138
hui/src/macros.rs
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
|
||||||
|
/// Constructs a `Size` or `Size2d` from a literal or expression
|
||||||
|
///
|
||||||
|
/// # Syntax:
|
||||||
|
/// - `auto` - `Size::Auto`
|
||||||
|
/// - `x` - `Size::Absolute(x)`
|
||||||
|
/// - `x%` - `Size::Relative(x / 100.)` *(literal only)*
|
||||||
|
/// - `x/` - `Size::Relative(x)`
|
||||||
|
/// - `x%=` - `Size::Remaining(x / 100.)` *(literal only)*
|
||||||
|
/// - `x/=` - `Size::Remaining(x)`
|
||||||
|
///
|
||||||
|
/// ...where `x` is a literal, identifier or an expression wrapped in parentheses
|
||||||
|
///
|
||||||
|
/// # Note:
|
||||||
|
/// - If a single argument is provided, it creates a `Size` using the rules specified above\
|
||||||
|
/// - If two arguments are provided, it creates a `Size2d` with the first value as width and the second as height\
|
||||||
|
/// Example: `size!(100, 50%)` creates a `Size2d` with width `100` (`Size::Absolute(100.)`) and height `50%` (`Size::Relative(0.5)`)
|
||||||
|
/// - `%` syntax is only valid for literals (`50%`), not expressions or identidiers.\
|
||||||
|
/// Use `/` instead (`(0.5 * x)/`, `x/`), but be aware of the different range (0.0-1.0) \
|
||||||
|
/// - Expressions must be wrapped in parentheses (for example: `(x + 5)`).\
|
||||||
|
/// This does not apply to single identifiers (`x`) or literals (`5`)
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! size {
|
||||||
|
(auto) => {
|
||||||
|
$crate::layout::Size::Auto
|
||||||
|
};
|
||||||
|
|
||||||
|
($x:literal) => {
|
||||||
|
$crate::layout::Size::Absolute($x as f32)
|
||||||
|
};
|
||||||
|
($x:literal %) => {
|
||||||
|
$crate::layout::Size::Relative($x as f32 / 100.)
|
||||||
|
};
|
||||||
|
($x:literal /) => {
|
||||||
|
$crate::layout::Size::Relative($x as f32)
|
||||||
|
};
|
||||||
|
($x:literal %=) => {
|
||||||
|
$crate::layout::Size::Remaining($x as f32 / 100.)
|
||||||
|
};
|
||||||
|
($x:literal /=) => {
|
||||||
|
$crate::layout::Size::Remaining($x as f32)
|
||||||
|
};
|
||||||
|
|
||||||
|
($x:ident) => {
|
||||||
|
$crate::layout::Size::Absolute($x as f32)
|
||||||
|
};
|
||||||
|
($x:ident /) => {
|
||||||
|
$crate::layout::Size::Relative($x as f32)
|
||||||
|
};
|
||||||
|
($x:ident /=) => {
|
||||||
|
$crate::layout::Size::Remaining($x as f32)
|
||||||
|
};
|
||||||
|
|
||||||
|
(($x:expr)) => {
|
||||||
|
$crate::layout::Size::Absolute(($x) as f32)
|
||||||
|
};
|
||||||
|
(($x:expr) /) => {
|
||||||
|
$crate::layout::Size::Relative(($x) as f32)
|
||||||
|
};
|
||||||
|
(($x:expr) /=) => {
|
||||||
|
$crate::layout::Size::Remaining(($x) as f32)
|
||||||
|
};
|
||||||
|
|
||||||
|
($x:tt , $y:tt $($ys:tt)?) => {
|
||||||
|
$crate::layout::Size2d {
|
||||||
|
width: $crate::size!($x),
|
||||||
|
height: $crate::size!($y $($ys)?),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
($x:tt $($xs:tt)? , $y:tt $($ys:tt)?) => {
|
||||||
|
$crate::layout::Size2d {
|
||||||
|
width: $crate::size!($x $($xs)?),
|
||||||
|
height: $crate::size!($y $($ys)?),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper macro for constructing a `RectFrame`
|
||||||
|
///
|
||||||
|
/// # Example:
|
||||||
|
/// ```
|
||||||
|
/// _frame! {
|
||||||
|
/// color: (0.2, 0.2, 0.3, 1.),
|
||||||
|
/// corner_radius: 5.,
|
||||||
|
/// };
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// # Note:
|
||||||
|
/// - If the `image` field is set, but not `color`, the `color` field will default to [`WHITE`](crate::color::WHITE) (to ensure visibility)
|
||||||
|
/// - If both `color` and `image` are not set, the `color` field will default to [`TRANSPARENT`](crate::color::TRANSPARENT)
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! rect_frame {
|
||||||
|
{} => {
|
||||||
|
$crate::frame::RectFrame::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
// () => {
|
||||||
|
// $crate::frame::RectFrame::default()
|
||||||
|
// };
|
||||||
|
|
||||||
|
($expr:expr) => {
|
||||||
|
{
|
||||||
|
let __frame: $crate::frame::RectFrame = $crate::frame::RectFrame::from($expr);
|
||||||
|
__frame
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
($image:expr, $color:expr) => {
|
||||||
|
$crate::frame::RectFrame::color_image($color, $image)
|
||||||
|
};
|
||||||
|
|
||||||
|
{$($ident:ident : $expr:expr),+$(,)?} => {
|
||||||
|
{
|
||||||
|
// ensure all identifiers are unique
|
||||||
|
#[allow(non_upper_case_globals)]
|
||||||
|
{$(const $ident: () = ();)+}
|
||||||
|
|
||||||
|
// construct the RectFrame
|
||||||
|
{
|
||||||
|
let mut _frame = $crate::frame::RectFrame::default();
|
||||||
|
let mut _color_is_set = false;
|
||||||
|
let mut _image_is_set = false;
|
||||||
|
$(
|
||||||
|
{
|
||||||
|
_frame.$ident = ($expr).into();
|
||||||
|
_image_is_set |= stringify!($ident) == "image";
|
||||||
|
_color_is_set |= stringify!($ident) == "color";
|
||||||
|
}
|
||||||
|
)+
|
||||||
|
// set color to white if image is explicitly set to Some(...) but color is left as the default
|
||||||
|
if _frame.image.is_some() && _image_is_set && !_color_is_set {
|
||||||
|
_frame.color = (1., 1., 1., 1.).into();
|
||||||
|
}
|
||||||
|
_frame
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,7 +1,10 @@
|
||||||
use glam::Vec2;
|
//! element measurement, hints and responses
|
||||||
|
|
||||||
|
use glam::Vec2;
|
||||||
|
use crate::rect::Rect;
|
||||||
|
|
||||||
|
// #[non_exhaustive]
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
#[non_exhaustive]
|
|
||||||
pub struct Hints {
|
pub struct Hints {
|
||||||
pub inner_content_size: Option<Vec2>,
|
pub inner_content_size: Option<Vec2>,
|
||||||
pub inner_content_size_cache: Option<Vec<Vec2>>,
|
pub inner_content_size_cache: Option<Vec<Vec2>>,
|
||||||
|
@ -9,7 +12,30 @@ pub struct Hints {
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct Response {
|
pub struct Response {
|
||||||
|
/// Computed size of the element
|
||||||
pub size: Vec2,
|
pub size: Vec2,
|
||||||
|
|
||||||
|
/// Hints for the layout system, can be used to optimize the layout engine.\
|
||||||
|
/// These will never cause the UI to be rendered differently (assuming the values are correct)
|
||||||
pub hints: Hints,
|
pub hints: Hints,
|
||||||
|
|
||||||
|
/// Arbitrary user data, can be used to pass data (for example, cache) between measure and process stages
|
||||||
pub user_data: Option<Box<dyn std::any::Any>>,
|
pub user_data: Option<Box<dyn std::any::Any>>,
|
||||||
|
|
||||||
|
/// If true, the element should always cause the content to wrap to the next line\
|
||||||
|
/// (the element itself gets wrapped to the next line too)
|
||||||
|
///
|
||||||
|
/// You should almost never set this, and the exact behavior may change in the future
|
||||||
|
///
|
||||||
|
/// Currently, this forces wrapping even if Container::wrap is set to false
|
||||||
|
pub should_wrap: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Response {
|
||||||
|
pub fn rect(&self, position: Vec2) -> Rect {
|
||||||
|
Rect {
|
||||||
|
position,
|
||||||
|
size: self.size,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
15
hui/src/rect.rs
Normal file
15
hui/src/rect.rs
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
//! contains types which represent the sides and corners of a rectangular shape.
|
||||||
|
|
||||||
|
//XXX: this is kinda a mess, either move the rect struct here or come up with a better name for this module
|
||||||
|
#[allow(clippy::module_inception)]
|
||||||
|
mod rect;
|
||||||
|
pub use rect::Rect;
|
||||||
|
|
||||||
|
mod sides;
|
||||||
|
pub use sides::Sides;
|
||||||
|
|
||||||
|
mod corners;
|
||||||
|
pub use corners::Corners;
|
||||||
|
|
||||||
|
mod color;
|
||||||
|
pub use color::FillColor;
|
206
hui/src/rect/color.rs
Normal file
206
hui/src/rect/color.rs
Normal file
|
@ -0,0 +1,206 @@
|
||||||
|
use super::Corners;
|
||||||
|
use glam::{Vec2, Vec3, Vec4, vec4};
|
||||||
|
|
||||||
|
/// Represents the fill color of a rectangle
|
||||||
|
///
|
||||||
|
/// Can be a single color or a simple gradient with different colors for each corner
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct FillColor(Corners<Vec4>);
|
||||||
|
|
||||||
|
impl FillColor {
|
||||||
|
pub const fn new(corners: Corners<Vec4>) -> Self {
|
||||||
|
Self(corners)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transparent background (alpha = 0)
|
||||||
|
pub const TRANSPARENT: Self = Self::rgba(0., 0., 0., 0.);
|
||||||
|
|
||||||
|
/// Transparent background (alpha = 0)
|
||||||
|
pub const fn transparent() -> Self {
|
||||||
|
Self::TRANSPARENT
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the fill color is completely transparent
|
||||||
|
///
|
||||||
|
/// (i.e. all corners have an alpha value of 0.0)
|
||||||
|
pub fn is_transparent(&self) -> bool {
|
||||||
|
self.0.top_left.w == 0. &&
|
||||||
|
self.0.top_right.w == 0. &&
|
||||||
|
self.0.bottom_left.w == 0. &&
|
||||||
|
self.0.bottom_right.w == 0.
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the fill color is completely opaque
|
||||||
|
///
|
||||||
|
/// (i.e. all corners have an alpha value of 1.0)
|
||||||
|
pub fn is_opaque(&self) -> bool {
|
||||||
|
self.0.top_left.w == 1. &&
|
||||||
|
self.0.top_right.w == 1. &&
|
||||||
|
self.0.bottom_left.w == 1. &&
|
||||||
|
self.0.bottom_right.w == 1.
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construct a solid color fill from values representing the red, green, blue and alpha channels
|
||||||
|
pub const fn rgba(r: f32, g: f32, b: f32, a: f32) -> Self {
|
||||||
|
Self(Corners {
|
||||||
|
top_left: vec4(r, g, b, a),
|
||||||
|
top_right: vec4(r, g, b, a),
|
||||||
|
bottom_left: vec4(r, g, b, a),
|
||||||
|
bottom_right: vec4(r, g, b, a),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construct a solid color fill from three values representing the red, green and blue channels
|
||||||
|
pub const fn rgb(r: f32, g: f32, b: f32) -> Self {
|
||||||
|
Self(Corners {
|
||||||
|
top_left: vec4(r, g, b, 1.0),
|
||||||
|
top_right: vec4(r, g, b, 1.0),
|
||||||
|
bottom_left: vec4(r, g, b, 1.0),
|
||||||
|
bottom_right: vec4(r, g, b, 1.0),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construct a solid color fill from colors for each corner
|
||||||
|
pub const fn from_corners(corners: Corners<Vec4>) -> Self {
|
||||||
|
Self(corners)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a list of the colors for each corner
|
||||||
|
pub const fn corners(&self) -> Corners<Vec4> {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Interpolate color on position, assuming a linear gradient
|
||||||
|
pub fn interpolate(&self, uv: Vec2) -> Vec4 {
|
||||||
|
let c = self.corners();
|
||||||
|
let top = c.top_left.lerp(c.top_right, uv.x);
|
||||||
|
let bottom = c.bottom_left.lerp(c.bottom_right, uv.x);
|
||||||
|
top.lerp(bottom, uv.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for FillColor {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(Corners::all(vec4(0.0, 0.0, 0.0, 1.0)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Corners<Vec4>> for FillColor {
|
||||||
|
fn from(corners: Corners<Vec4>) -> Self {
|
||||||
|
Self(corners)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<FillColor> for Corners<Vec4> {
|
||||||
|
fn from(corners: FillColor) -> Self {
|
||||||
|
corners.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Vec4> for FillColor {
|
||||||
|
fn from(value: Vec4) -> Self {
|
||||||
|
Self(Corners::all(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<(f32, f32, f32, f32)> for FillColor {
|
||||||
|
fn from((r, g, b, a): (f32, f32, f32, f32)) -> Self {
|
||||||
|
Self(Corners::all(vec4(r, g, b, a)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<[f32; 4]> for FillColor {
|
||||||
|
fn from([r, g, b, a]: [f32; 4]) -> Self {
|
||||||
|
Self(Corners::all(vec4(r, g, b, a)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Vec3> for FillColor {
|
||||||
|
fn from(value: Vec3) -> Self {
|
||||||
|
Self(Corners::all(vec4(value.x, value.y, value.z, 1.0)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<(f32, f32, f32)> for FillColor {
|
||||||
|
fn from((r, g, b): (f32, f32, f32)) -> Self {
|
||||||
|
Self(Corners::all(vec4(r, g, b, 1.0)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<[f32; 3]> for FillColor {
|
||||||
|
fn from([r, g, b]: [f32; 3]) -> Self {
|
||||||
|
Self(Corners::all(vec4(r, g, b, 1.0)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<(Vec4, Vec4, Vec4, Vec4)> for FillColor {
|
||||||
|
fn from((top_left, top_right, bottom_left, bottom_right): (Vec4, Vec4, Vec4, Vec4)) -> Self {
|
||||||
|
Self(Corners { top_left, top_right, bottom_left, bottom_right })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<((f32, f32, f32, f32), (f32, f32, f32, f32), (f32, f32, f32, f32), (f32, f32, f32, f32))> for FillColor {
|
||||||
|
fn from(value: ((f32, f32, f32, f32), (f32, f32, f32, f32), (f32, f32, f32, f32), (f32, f32, f32, f32))) -> Self {
|
||||||
|
Self(Corners {
|
||||||
|
top_left: vec4(value.0.0, value.0.1, value.0.2, value.0.3),
|
||||||
|
top_right: vec4(value.1.0, value.1.1, value.1.2, value.1.3),
|
||||||
|
bottom_left: vec4(value.2.0, value.2.1, value.2.2, value.2.3),
|
||||||
|
bottom_right: vec4(value.3.0, value.3.1, value.3.2, value.3.3),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<[[f32; 4]; 4]> for FillColor {
|
||||||
|
fn from(value: [[f32; 4]; 4]) -> Self {
|
||||||
|
Self(Corners {
|
||||||
|
top_left: vec4(value[0][0], value[0][1], value[0][2], value[0][3]),
|
||||||
|
top_right: vec4(value[1][0], value[1][1], value[1][2], value[1][3]),
|
||||||
|
bottom_left: vec4(value[2][0], value[2][1], value[2][2], value[2][3]),
|
||||||
|
bottom_right: vec4(value[3][0], value[3][1], value[3][2], value[3][3]),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Corners<Vec3>> for FillColor {
|
||||||
|
fn from(corners: Corners<Vec3>) -> Self {
|
||||||
|
Self(Corners {
|
||||||
|
top_left: corners.top_left.extend(1.),
|
||||||
|
top_right: corners.top_right.extend(1.),
|
||||||
|
bottom_left: corners.bottom_left.extend(1.),
|
||||||
|
bottom_right: corners.bottom_right.extend(1.),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<(Vec3, Vec3, Vec3, Vec3)> for FillColor {
|
||||||
|
fn from((top_left, top_right, bottom_left, bottom_right): (Vec3, Vec3, Vec3, Vec3)) -> Self {
|
||||||
|
Self(Corners {
|
||||||
|
top_left: vec4(top_left.x, top_left.y, top_left.z, 1.0),
|
||||||
|
top_right: vec4(top_right.x, top_right.y, top_right.z, 1.0),
|
||||||
|
bottom_left: vec4(bottom_left.x, bottom_left.y, bottom_left.z, 1.0),
|
||||||
|
bottom_right: vec4(bottom_right.x, bottom_right.y, bottom_right.z, 1.0),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<((f32, f32, f32), (f32, f32, f32), (f32, f32, f32), (f32, f32, f32))> for FillColor {
|
||||||
|
fn from(value: ((f32, f32, f32), (f32, f32, f32), (f32, f32, f32), (f32, f32, f32))) -> Self {
|
||||||
|
Self(Corners {
|
||||||
|
top_left: vec4(value.0.0, value.0.1, value.0.2, 1.0),
|
||||||
|
top_right: vec4(value.1.0, value.1.1, value.1.2, 1.0),
|
||||||
|
bottom_left: vec4(value.2.0, value.2.1, value.2.2, 1.0),
|
||||||
|
bottom_right: vec4(value.3.0, value.3.1, value.3.2, 1.0),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<[[f32; 3]; 4]> for FillColor {
|
||||||
|
fn from(value: [[f32; 3]; 4]) -> Self {
|
||||||
|
Self(Corners {
|
||||||
|
top_left: vec4(value[0][0], value[0][1], value[0][2], 1.0),
|
||||||
|
top_right: vec4(value[1][0], value[1][1], value[1][2], 1.0),
|
||||||
|
bottom_left: vec4(value[2][0], value[2][1], value[2][2], 1.0),
|
||||||
|
bottom_right: vec4(value[3][0], value[3][1], value[3][2], 1.0),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
85
hui/src/rect/corners.rs
Normal file
85
hui/src/rect/corners.rs
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
/// Represents 4 corners of a rectangular shape.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
|
||||||
|
pub struct Corners<T> {
|
||||||
|
pub top_left: T,
|
||||||
|
pub top_right: T,
|
||||||
|
pub bottom_left: T,
|
||||||
|
pub bottom_right: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Clone> Corners<T> {
|
||||||
|
#[inline]
|
||||||
|
pub fn all(value: T) -> Self {
|
||||||
|
Self {
|
||||||
|
top_left: value.clone(),
|
||||||
|
top_right: value.clone(),
|
||||||
|
bottom_left: value.clone(),
|
||||||
|
bottom_right: value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn top_bottom(top: T, bottom: T) -> Self {
|
||||||
|
Self {
|
||||||
|
top_left: top.clone(),
|
||||||
|
top_right: top,
|
||||||
|
bottom_left: bottom.clone(),
|
||||||
|
bottom_right: bottom,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn left_right(left: T, right: T) -> Self {
|
||||||
|
Self {
|
||||||
|
top_left: left.clone(),
|
||||||
|
top_right: right.clone(),
|
||||||
|
bottom_left: left,
|
||||||
|
bottom_right: right,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl <T: Ord + Clone> Corners<T> {
|
||||||
|
pub fn max(&self) -> T {
|
||||||
|
self.top_left.clone()
|
||||||
|
.max(self.top_right.clone())
|
||||||
|
.max(self.bottom_left.clone())
|
||||||
|
.max(self.bottom_right.clone())
|
||||||
|
.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Corners<f32> {
|
||||||
|
pub fn max_f32(&self) -> f32 {
|
||||||
|
self.top_left
|
||||||
|
.max(self.top_right)
|
||||||
|
.max(self.bottom_left)
|
||||||
|
.max(self.bottom_right)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Corners<f64> {
|
||||||
|
pub fn max_f64(&self) -> f64 {
|
||||||
|
self.top_left
|
||||||
|
.max(self.top_right)
|
||||||
|
.max(self.bottom_left)
|
||||||
|
.max(self.bottom_right)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Clone> From<T> for Corners<T> {
|
||||||
|
fn from(value: T) -> Self {
|
||||||
|
Self::all(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<(T, T, T, T)> for Corners<T> {
|
||||||
|
fn from((top_left, top_right, bottom_left, bottom_right): (T, T, T, T)) -> Self {
|
||||||
|
Self {
|
||||||
|
top_left,
|
||||||
|
top_right,
|
||||||
|
bottom_left,
|
||||||
|
bottom_right,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
138
hui/src/rect/rect.rs
Normal file
138
hui/src/rect/rect.rs
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
use glam::Vec2;
|
||||||
|
use super::Corners;
|
||||||
|
|
||||||
|
/// Represents a rectangle/AABB with specified position and size
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Default)]
|
||||||
|
pub struct Rect {
|
||||||
|
/// Position of the top-left corner of the rect.
|
||||||
|
pub position: Vec2,
|
||||||
|
/// Size of the rect, should not be negative.
|
||||||
|
pub size: Vec2,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Rect {
|
||||||
|
pub const fn new(position: Vec2, size: Vec2) -> Self {
|
||||||
|
Self { position, size }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn from_position(position: Vec2) -> Self {
|
||||||
|
Self {
|
||||||
|
position,
|
||||||
|
size: Vec2::ZERO,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn from_size(size: Vec2) -> Self {
|
||||||
|
Self {
|
||||||
|
position: Vec2::ZERO,
|
||||||
|
size,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the rect contains a point.
|
||||||
|
pub fn contains_point(&self, point: Vec2) -> bool {
|
||||||
|
point.cmpge(self.position).all() && point.cmple(self.position + self.size).all()
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: return intersect rect
|
||||||
|
/// Check if the rect intersects with another rect.
|
||||||
|
pub fn intersects_rect(&self, other: Rect) -> bool {
|
||||||
|
self.position.x < other.position.x + other.size.x
|
||||||
|
&& self.position.x + self.size.x > other.position.x
|
||||||
|
&& self.position.y < other.position.y + other.size.y
|
||||||
|
&& self.position.y + self.size.y > other.position.y
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get width of the rectangle.
|
||||||
|
///
|
||||||
|
/// To get both width and height, use the `size` property instead.
|
||||||
|
pub fn width(&self) -> f32 {
|
||||||
|
self.size.x
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get height of the rectangle.
|
||||||
|
///
|
||||||
|
/// To get both width and height, use the `size` property instead.
|
||||||
|
pub fn height(&self) -> f32 {
|
||||||
|
self.size.y
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get position of the top-left corner of the rectangle on the x-axis.
|
||||||
|
///
|
||||||
|
/// To get both x and y, use the `position` property instead.
|
||||||
|
pub fn x(&self) -> f32 {
|
||||||
|
self.position.x
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get position of the top-left corner of the rectangle on the y-axis.
|
||||||
|
///
|
||||||
|
/// To get both x and y, use the `position` property instead.
|
||||||
|
pub fn y(&self) -> f32 {
|
||||||
|
self.position.y
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get positions of all 4 corners of the rectangle.
|
||||||
|
pub fn corners(&self) -> Corners<Vec2> {
|
||||||
|
Corners {
|
||||||
|
top_left: self.position,
|
||||||
|
top_right: self.position + Vec2::new(self.size.x, 0.0),
|
||||||
|
bottom_left: self.position + Vec2::new(0.0, self.size.y),
|
||||||
|
bottom_right: self.position + self.size,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Vec2> for Rect {
|
||||||
|
/// Create a new `Rect` from a `Vec2`, where x and y are the width and height of the rect respectively.
|
||||||
|
fn from(size: Vec2) -> Self {
|
||||||
|
Self::from_size(size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<(Vec2, Vec2)> for Rect {
|
||||||
|
/// Create a new `Rect` from a tuple of two `Vec2`s, where the first `Vec2` is the position and the second `Vec2` is the size.
|
||||||
|
fn from((position, size): (Vec2, Vec2)) -> Self {
|
||||||
|
Self { position, size }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<(f32, f32, f32, f32)> for Rect {
|
||||||
|
/// Create a new `Rect` from a tuple of 4 `f32`s, where the first two `f32`s are the x and y positions of the top-left corner and the last two `f32`s are the width and height of the rect respectively.
|
||||||
|
fn from((x, y, width, height): (f32, f32, f32, f32)) -> Self {
|
||||||
|
Self {
|
||||||
|
position: Vec2::new(x, y),
|
||||||
|
size: Vec2::new(width, height),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<[f32; 4]> for Rect {
|
||||||
|
/// Create a new `Rect` from an array of 4 `f32`s, where the first two `f32`s are the x and y positions of the top-left corner and the last two `f32`s are the width and height of the rect respectively.
|
||||||
|
fn from([x, y, width, height]: [f32; 4]) -> Self {
|
||||||
|
Self {
|
||||||
|
position: Vec2::new(x, y),
|
||||||
|
size: Vec2::new(width, height),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Rect> for (Vec2, Vec2) {
|
||||||
|
/// Convert a `Rect` into a tuple of two `Vec2`s, where the first `Vec2` is the position and the second `Vec2` is the size.
|
||||||
|
fn from(rect: Rect) -> Self {
|
||||||
|
(rect.position, rect.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Rect> for (f32, f32, f32, f32) {
|
||||||
|
/// Convert a `Rect` into a tuple of 4 `f32`s, where the first two `f32`s are the x and y positions of the top-left corner and the last two `f32`s are the width and height of the rect respectively.
|
||||||
|
fn from(rect: Rect) -> Self {
|
||||||
|
(rect.position.x, rect.position.y, rect.size.x, rect.size.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Rect> for [f32; 4] {
|
||||||
|
/// Convert a `Rect` into an array of 4 `f32`s, where the first two `f32`s are the x and y positions of the top-left corner and the last two `f32`s are the width and height of the rect respectively.
|
||||||
|
fn from(rect: Rect) -> Self {
|
||||||
|
[rect.position.x, rect.position.y, rect.size.x, rect.size.y]
|
||||||
|
}
|
||||||
|
}
|
49
hui/src/rect/sides.rs
Normal file
49
hui/src/rect/sides.rs
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
|
||||||
|
/// Represents 4 sides of a rectangular shape.
|
||||||
|
#[derive(Default, Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
|
pub struct Sides<T> {
|
||||||
|
pub top: T,
|
||||||
|
pub bottom: T,
|
||||||
|
pub left: T,
|
||||||
|
pub right: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Clone> Sides<T> {
|
||||||
|
#[inline]
|
||||||
|
pub fn all(value: T) -> Self {
|
||||||
|
Self {
|
||||||
|
top: value.clone(),
|
||||||
|
bottom: value.clone(),
|
||||||
|
left: value.clone(),
|
||||||
|
right: value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn horizontal_vertical(horizontal: T, vertical: T) -> Self {
|
||||||
|
Self {
|
||||||
|
top: vertical.clone(),
|
||||||
|
bottom: vertical,
|
||||||
|
left: horizontal.clone(),
|
||||||
|
right: horizontal,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Clone> From<T> for Sides<T> {
|
||||||
|
fn from(value: T) -> Self {
|
||||||
|
Self::all(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Clone> From<(T, T)> for Sides<T> {
|
||||||
|
fn from((horizontal, vertical): (T, T)) -> Self {
|
||||||
|
Self::horizontal_vertical(horizontal, vertical)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<(T, T, T, T)> for Sides<T> {
|
||||||
|
fn from((top, bottom, left, right): (T, T, T, T)) -> Self {
|
||||||
|
Self { top, bottom, left, right }
|
||||||
|
}
|
||||||
|
}
|
105
hui/src/signal.rs
Normal file
105
hui/src/signal.rs
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
//! signal handling for UI events
|
||||||
|
|
||||||
|
use std::any::{Any, TypeId};
|
||||||
|
use hashbrown::HashMap;
|
||||||
|
use nohash_hasher::BuildNoHashHasher;
|
||||||
|
|
||||||
|
pub mod trigger;
|
||||||
|
|
||||||
|
#[cfg(feature = "derive")]
|
||||||
|
pub use hui_derive::Signal;
|
||||||
|
|
||||||
|
/// A marker trait for UI Signals
|
||||||
|
pub trait Signal: Any {}
|
||||||
|
|
||||||
|
// #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
|
||||||
|
// pub(crate) struct DummySignal;
|
||||||
|
// impl UiSignal for DummySignal {}
|
||||||
|
|
||||||
|
/// Internal storage for signals
|
||||||
|
pub struct SignalStore {
|
||||||
|
//TODO use a multithreaded queue instead, to allow easily offloading ui processing to a different thread
|
||||||
|
///XXX: is this truly the most efficient structure?
|
||||||
|
sig: HashMap<TypeId, Vec<Box<dyn Any>>, BuildNoHashHasher<u64>>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SignalStore {
|
||||||
|
/// Create a new [`SigIntStore`]
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
sig: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensure that store for given signal type exists and return a mutable reference to it
|
||||||
|
fn internal_store<T: Signal + 'static>(&mut self) -> &mut Vec<Box<dyn Any>> {
|
||||||
|
let type_id = TypeId::of::<T>();
|
||||||
|
self.sig.entry(type_id).or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a signal to the store
|
||||||
|
///
|
||||||
|
/// Signals are stored in the order they are added
|
||||||
|
pub fn add<T: Signal + 'static>(&mut self, sig: T) {
|
||||||
|
let type_id = TypeId::of::<T>();
|
||||||
|
if let Some(v) = self.sig.get_mut(&type_id) {
|
||||||
|
v.push(Box::new(sig));
|
||||||
|
} else {
|
||||||
|
self.sig.insert(type_id, vec![Box::new(sig)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drain all signals of a given type
|
||||||
|
pub(crate) fn drain<T: Signal + 'static>(&mut self) -> impl Iterator<Item = T> + '_ {
|
||||||
|
self.internal_store::<T>()
|
||||||
|
.drain(..)
|
||||||
|
.map(|x| *x.downcast::<T>().unwrap()) //unchecked?
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear all signals
|
||||||
|
pub(crate) fn clear(&mut self) {
|
||||||
|
//XXX: should we clear the vecs instead?
|
||||||
|
self.sig.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// pub trait Signal {
|
||||||
|
// type Arg;
|
||||||
|
// type Output;
|
||||||
|
// fn call(&self, arg: Self::Arg) -> Self::Output;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// impl<F: Fn() -> T, T> Signal for F {
|
||||||
|
// type Arg = ();
|
||||||
|
// type Output = T;
|
||||||
|
// fn call(&self, _: Self::Arg) -> Self::Output {
|
||||||
|
// self()
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // impl<F: Fn(A) -> T, A, T> Signal for F {
|
||||||
|
// // type Arg = A;
|
||||||
|
// // type Output = T;
|
||||||
|
// // fn call(&self, a: Self::Arg) -> Self::Output {
|
||||||
|
// // self(a)
|
||||||
|
// // }
|
||||||
|
// // }
|
||||||
|
|
||||||
|
// pub struct SignalTrigger<R: UiSignal + 'static, A = ()>(pub(crate) Box<dyn Fn(A) -> R + 'static>);
|
||||||
|
|
||||||
|
// impl<R: UiSignal + 'static, A> SignalTrigger<R, A> {
|
||||||
|
// pub fn new<F: Fn(A) -> R + 'static>(f: F) -> Self {
|
||||||
|
// Self(Box::new(f))
|
||||||
|
// }
|
||||||
|
|
||||||
|
// pub fn call(&self, a: A) -> R {
|
||||||
|
// (self.0)(a)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// impl<R: UiSignal + 'static, A, T: Fn(A) -> R + 'static> From<T> for SignalTrigger<R, A> {
|
||||||
|
// fn from(f: T) -> Self {
|
||||||
|
// Self(Box::new(f))
|
||||||
|
// }
|
||||||
|
// }
|
71
hui/src/signal/trigger.rs
Normal file
71
hui/src/signal/trigger.rs
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
//! Contains the implementation of signal triggers, which simplify creation of custom elements
|
||||||
|
|
||||||
|
use super::{Signal, SignalStore};
|
||||||
|
//use crate::element::UiElement;
|
||||||
|
|
||||||
|
/// Signal trigger that does not take any arguments
|
||||||
|
#[allow(clippy::complexity)]
|
||||||
|
pub struct SignalTrigger(Box<dyn Fn(&mut SignalStore)>);
|
||||||
|
|
||||||
|
impl SignalTrigger {
|
||||||
|
/// Create a new signal trigger from a function or a constructor
|
||||||
|
pub fn new<S: Signal + 'static, F: Fn() -> S + 'static>(f: F) -> Self {
|
||||||
|
Self(Box::new(move |s: &mut SignalStore| {
|
||||||
|
s.add::<S>(f());
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fire the signal
|
||||||
|
pub fn fire(&self, s: &mut SignalStore) {
|
||||||
|
(self.0)(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Signal trigger that takes a single argument and passes it to the signal
|
||||||
|
#[allow(clippy::complexity)]
|
||||||
|
pub struct SignalTriggerArg<T>(Box<dyn Fn(&mut SignalStore, T)>);
|
||||||
|
|
||||||
|
impl<T> SignalTriggerArg<T> {
|
||||||
|
/// Create a new signal trigger from a function or a constructor
|
||||||
|
pub fn new<S: Signal, F: Fn(T) -> S + 'static>(f: F) -> Self {
|
||||||
|
Self(Box::new(move |s: &mut SignalStore, x| {
|
||||||
|
s.add::<S>(f(x));
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fire the signal with the given argument
|
||||||
|
pub fn fire(&self, s: &mut SignalStore, x: T) {
|
||||||
|
(self.0)(s, x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// #[allow(clippy::complexity)]
|
||||||
|
// pub struct SignalTriggerElement<E: UiElement>(Box<dyn Fn(&mut SignalStore, &mut E)>);
|
||||||
|
|
||||||
|
// impl<E: UiElement> SignalTriggerElement<E> {
|
||||||
|
// pub fn new<S: Signal, F: Fn(&mut E) -> S + 'static>(f: F) -> Self {
|
||||||
|
// Self(Box::new(move |s: &mut SignalStore, e: &mut E| {
|
||||||
|
// s.add::<S>(f(e));
|
||||||
|
// }))
|
||||||
|
// }
|
||||||
|
|
||||||
|
// pub fn fire(&self, s: &mut SignalStore, e: &mut E) {
|
||||||
|
// (self.0)(s, e);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// #[allow(clippy::complexity)]
|
||||||
|
// pub struct SignalTriggerElementArg<E: UiElement, T>(Box<dyn Fn(&mut SignalStore, &mut E, T)>);
|
||||||
|
|
||||||
|
// impl<E: UiElement, T> SignalTriggerElementArg<E, T> {
|
||||||
|
// pub fn new<S: Signal, F: Fn(T, &mut E) -> S + 'static>(f: F) -> Self {
|
||||||
|
// Self(Box::new(move |s: &mut SignalStore, e: &mut E, x| {
|
||||||
|
// s.add::<S>(f(x, e));
|
||||||
|
// }))
|
||||||
|
// }
|
||||||
|
|
||||||
|
// pub fn fire(&self, s: &mut SignalStore, e: &mut E, x: T) {
|
||||||
|
// (self.0)(s, e, x);
|
||||||
|
// }
|
||||||
|
// }
|
139
hui/src/state.rs
139
hui/src/state.rs
|
@ -1,9 +1,142 @@
|
||||||
|
//! state managment for stateful elements
|
||||||
|
|
||||||
use hashbrown::{HashMap, HashSet};
|
use hashbrown::{HashMap, HashSet};
|
||||||
use nohash_hasher::BuildNoHashHasher;
|
use nohash_hasher::BuildNoHashHasher;
|
||||||
use std::any::Any;
|
use std::{any::Any, hash::{Hash, Hasher}};
|
||||||
|
use rustc_hash::FxHasher;
|
||||||
|
|
||||||
|
//TODO impl StateRepo functions and automatic cleanup of inactive ids
|
||||||
|
|
||||||
|
#[cfg(feature = "derive")]
|
||||||
|
pub use hui_derive::State;
|
||||||
|
|
||||||
|
/// Marker trait for state objects
|
||||||
|
pub trait State: Any {}
|
||||||
|
|
||||||
|
/// Integer type used to identify a state object
|
||||||
|
type StateId = u64;
|
||||||
|
|
||||||
|
fn hash_local(x: impl Hash, g: &[StateId]) -> StateId {
|
||||||
|
let mut hasher = FxHasher::default();
|
||||||
|
0xdeadbeefu64.hash(&mut hasher);
|
||||||
|
for x in g {
|
||||||
|
x.hash(&mut hasher);
|
||||||
|
}
|
||||||
|
x.hash(&mut hasher);
|
||||||
|
hasher.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hash_global(x: impl Hash) -> StateId {
|
||||||
|
let mut hasher = FxHasher::default();
|
||||||
|
0xcafebabeu64.hash(&mut hasher);
|
||||||
|
x.hash(&mut hasher);
|
||||||
|
hasher.finish()
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct StateRepo {
|
pub struct StateRepo {
|
||||||
state: HashMap<u64, Box<dyn Any>, BuildNoHashHasher<u64>>,
|
/// Stack of ids used to identify state objects
|
||||||
active_ids: HashSet<u64, BuildNoHashHasher<u64>>
|
id_stack: Vec<StateId>,
|
||||||
|
|
||||||
|
/// Implementation detail: used to prevent needlessly reallocating the id stack if the `global`` function is used
|
||||||
|
standby: Vec<StateId>,
|
||||||
|
|
||||||
|
/// Actual state objects
|
||||||
|
state: HashMap<StateId, Box<dyn Any>, BuildNoHashHasher<StateId>>,
|
||||||
|
|
||||||
|
/// IDs that were accessed during the current frame, everything else is considered inactive and can be cleaned up
|
||||||
|
active_ids: HashSet<StateId, BuildNoHashHasher<StateId>>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StateRepo {
|
||||||
|
/// Push an id to the stack
|
||||||
|
pub fn push(&mut self, id: impl Hash) {
|
||||||
|
self.id_stack.push(hash_global(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pop the last id from the stack
|
||||||
|
///
|
||||||
|
/// ## Panics:
|
||||||
|
/// Panics if the stack is empty
|
||||||
|
pub fn pop(&mut self) {
|
||||||
|
self.id_stack.pop().expect("stack is empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new [`StateRepo`]
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a reference to a state object by its id
|
||||||
|
pub fn acquire<T: State>(&mut self, id: impl Hash) -> Option<&T> {
|
||||||
|
let id = hash_local(id, &self.id_stack);
|
||||||
|
self.active_ids.insert(id);
|
||||||
|
self.state.get(&id).unwrap().downcast_ref::<T>()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a reference to a state object by its id or insert a new one
|
||||||
|
pub fn acquire_or_insert<T: State>(&mut self, id: impl Hash, state: T) -> &T {
|
||||||
|
let id = hash_local(id, &self.id_stack);
|
||||||
|
self.state.entry(id)
|
||||||
|
.or_insert_with(|| Box::new(state))
|
||||||
|
.downcast_ref::<T>().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a reference to a state object by its id or insert a new default one
|
||||||
|
pub fn acquire_or_default<T: State + Default>(&mut self, id: impl Hash) -> &T {
|
||||||
|
let id = hash_local(id, &self.id_stack);
|
||||||
|
self.state.entry(id)
|
||||||
|
.or_insert_with(|| Box::<T>::default())
|
||||||
|
.downcast_ref::<T>().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a mutable reference to a state object by its id
|
||||||
|
pub fn acquire_mut<T: State>(&mut self, id: impl Hash) -> Option<&mut T> {
|
||||||
|
let id = hash_local(id, &self.id_stack);
|
||||||
|
self.active_ids.insert(id);
|
||||||
|
self.state.get_mut(&id).unwrap().downcast_mut::<T>()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a mutable reference to a state object by its id or insert a new one
|
||||||
|
pub fn acquire_mut_or_insert<T: State>(&mut self, id: impl Hash, state: T) -> &mut T {
|
||||||
|
let id = hash_local(id, &self.id_stack);
|
||||||
|
self.state.entry(id)
|
||||||
|
.or_insert_with(|| Box::new(state))
|
||||||
|
.downcast_mut::<T>().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a mutable reference to a state object by its id or insert a new default one
|
||||||
|
pub fn acquire_mut_or_default<T: State + Default>(&mut self, id: impl Hash) -> &mut T {
|
||||||
|
let id = hash_local(id, &self.id_stack);
|
||||||
|
self.state.entry(id)
|
||||||
|
.or_insert_with(|| Box::<T>::default())
|
||||||
|
.downcast_mut::<T>().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Temporarily forget about current id stack, and use an empty one (within the context of the closure)
|
||||||
|
///
|
||||||
|
/// Can be useful for state management of non-hierarchical objects, e.g. popups
|
||||||
|
pub fn global<R>(&mut self, f: impl FnOnce(&mut Self) -> R) -> R {
|
||||||
|
self.standby.clear();
|
||||||
|
std::mem::swap(&mut self.id_stack, &mut self.standby);
|
||||||
|
let ret = f(self);
|
||||||
|
std::mem::swap(&mut self.id_stack, &mut self.standby);
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scope the state repo
|
||||||
|
///
|
||||||
|
/// Anything pushed or popped will be discarded after the closure,
|
||||||
|
/// and the stack will be restored to its previous state
|
||||||
|
pub fn scope<R>(&mut self, f: impl FnOnce(&mut Self) -> R) -> R {
|
||||||
|
self.standby.clear();
|
||||||
|
self.standby.extend(self.id_stack.iter().copied());
|
||||||
|
let ret = f(self);
|
||||||
|
std::mem::swap(&mut self.id_stack, &mut self.standby);
|
||||||
|
ret
|
||||||
|
//XXX: this is super efficient, but works only for pushes, if anything is popped, it will be lost
|
||||||
|
// let len = self.id_stack.len();
|
||||||
|
// let ret = f(self);
|
||||||
|
// self.id_stack.truncate(len);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,45 +1,60 @@
|
||||||
|
//! text rendering, styling, measuring
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use fontdue::{Font, FontSettings};
|
||||||
|
use crate::draw::atlas::TextureAtlasManager;
|
||||||
|
|
||||||
mod font;
|
mod font;
|
||||||
mod ftm;
|
mod ftm;
|
||||||
|
mod stack;
|
||||||
|
|
||||||
|
/// Built-in font handle
|
||||||
|
#[cfg(feature="builtin_font")]
|
||||||
|
pub use font::BUILTIN_FONT;
|
||||||
|
pub use font::FontHandle;
|
||||||
|
|
||||||
use font::FontManager;
|
use font::FontManager;
|
||||||
pub use font::FontHandle;
|
|
||||||
use fontdue::{Font, FontSettings};
|
|
||||||
use ftm::FontTextureManager;
|
use ftm::FontTextureManager;
|
||||||
pub use ftm::{FontTextureInfo, GlyphCacheEntry};
|
use ftm::GlyphCacheEntry;
|
||||||
|
use stack::FontStack;
|
||||||
|
|
||||||
pub struct TextRenderer {
|
pub(crate) struct TextRenderer {
|
||||||
fm: FontManager,
|
manager: FontManager,
|
||||||
ftm: FontTextureManager,
|
ftm: FontTextureManager,
|
||||||
|
stack: FontStack,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TextRenderer {
|
impl TextRenderer {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
fm: FontManager::new(),
|
manager: FontManager::new(),
|
||||||
ftm: FontTextureManager::default(),
|
ftm: FontTextureManager::default(),
|
||||||
|
stack: FontStack::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_font_from_bytes(&mut self, font: &[u8]) -> FontHandle {
|
pub fn add_font_from_bytes(&mut self, font: &[u8]) -> FontHandle {
|
||||||
self.fm.add_font(Font::from_bytes(font, FontSettings::default()).unwrap())
|
self.manager.add_font(Font::from_bytes(font, FontSettings::default()).unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reset_frame(&mut self) {
|
pub fn glyph(&mut self, atlas: &mut TextureAtlasManager, font_handle: FontHandle, character: char, size: u8) -> Arc<GlyphCacheEntry> {
|
||||||
self.ftm.reset_modified();
|
self.ftm.glyph(atlas, &self.manager, font_handle, character, size)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn font_texture(&self) -> FontTextureInfo {
|
pub fn push_font(&mut self, font: FontHandle) {
|
||||||
self.ftm.info()
|
self.stack.push(font);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn glyph(&mut self, font_handle: FontHandle, character: char, size: u8) -> Arc<GlyphCacheEntry> {
|
pub fn pop_font(&mut self) {
|
||||||
self.ftm.glyph(&self.fm, font_handle, character, size)
|
self.stack.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current_font(&self) -> FontHandle {
|
||||||
|
self.stack.current_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn internal_font(&self, handle: FontHandle) -> &Font {
|
pub(crate) fn internal_font(&self, handle: FontHandle) -> &Font {
|
||||||
self.fm.get(handle).unwrap()
|
self.manager.get(handle).unwrap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,3 +63,44 @@ impl Default for TextRenderer {
|
||||||
Self::new()
|
Self::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Size of measured text
|
||||||
|
pub struct TextMeasureResponse {
|
||||||
|
pub max_width: f32,
|
||||||
|
pub height: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Context for measuring text
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct TextMeasure<'a>(&'a TextRenderer);
|
||||||
|
|
||||||
|
impl<'a> TextMeasure<'a> {
|
||||||
|
/// Measure the given string of text with the given font and size
|
||||||
|
pub fn measure(&self, font: FontHandle, size: u16, text: &str) -> TextMeasureResponse {
|
||||||
|
use fontdue::layout::{Layout, CoordinateSystem, TextStyle};
|
||||||
|
let mut layout = Layout::new(CoordinateSystem::PositiveYDown);
|
||||||
|
layout.append(
|
||||||
|
&[self.0.internal_font(font)],
|
||||||
|
&TextStyle::new(text, size as f32, 0)
|
||||||
|
);
|
||||||
|
TextMeasureResponse {
|
||||||
|
max_width: layout.lines().map(|lines| {
|
||||||
|
lines.iter().fold(0.0_f32, |acc, x| {
|
||||||
|
let glyph = layout.glyphs().get(x.glyph_end).unwrap();
|
||||||
|
acc.max(glyph.x + glyph.width as f32)
|
||||||
|
})
|
||||||
|
}).unwrap_or(0.),
|
||||||
|
height: layout.height(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextRenderer {
|
||||||
|
pub fn to_measure(&self) -> TextMeasure {
|
||||||
|
TextMeasure(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn measure(&self, font: FontHandle, size: u16, text: &str) -> TextMeasureResponse {
|
||||||
|
TextMeasure(self).measure(font, size, text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,14 +1,32 @@
|
||||||
use fontdue::Font;
|
use fontdue::Font;
|
||||||
|
|
||||||
#[cfg(feature = "builtin_font")]
|
/// Font handle, stores the internal font id and can be cheaply copied.
|
||||||
const BIN_FONT: &[u8] = include_bytes!("../../assets/font/ProggyTiny.ttf");
|
///
|
||||||
|
/// Only valid for the `UiInstance` that created it.\
|
||||||
|
/// Using it with other instances may result in panics or unexpected behavior.
|
||||||
|
///
|
||||||
|
/// Handle values are not guaranteed to be valid.\
|
||||||
|
/// Creating or transmuting an invalid handle is allowed and is *not* UB.
|
||||||
|
///
|
||||||
|
/// Internal value is an implementation detail and should not be relied upon.
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
||||||
pub struct FontHandle(pub(crate) usize);
|
pub struct FontHandle(pub(crate) usize);
|
||||||
|
|
||||||
#[cfg(feature = "builtin_font")]
|
#[cfg(feature = "builtin_font")]
|
||||||
pub const BUILTIN_FONT: FontHandle = FontHandle(0);
|
pub const BUILTIN_FONT: FontHandle = FontHandle(0);
|
||||||
|
|
||||||
|
impl Default for FontHandle {
|
||||||
|
/// Default font handle is the builtin font, if the feature is enabled;\
|
||||||
|
/// Otherwise returns an invalid handle.
|
||||||
|
fn default() -> Self {
|
||||||
|
#[cfg(feature = "builtin_font")] { BUILTIN_FONT }
|
||||||
|
#[cfg(not(feature = "builtin_font"))] { Self(usize::MAX) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "builtin_font")]
|
||||||
|
const BUILTIN_FONT_DATA: &[u8] = include_bytes!("../../assets/font/ProggyTiny.ttf");
|
||||||
|
|
||||||
pub struct FontManager {
|
pub struct FontManager {
|
||||||
fonts: Vec<Font>,
|
fonts: Vec<Font>,
|
||||||
}
|
}
|
||||||
|
@ -20,7 +38,10 @@ impl FontManager {
|
||||||
};
|
};
|
||||||
#[cfg(feature = "builtin_font")]
|
#[cfg(feature = "builtin_font")]
|
||||||
{
|
{
|
||||||
let font = Font::from_bytes(BIN_FONT, fontdue::FontSettings::default()).unwrap();
|
let font = Font::from_bytes(
|
||||||
|
BUILTIN_FONT_DATA,
|
||||||
|
fontdue::FontSettings::default()
|
||||||
|
).unwrap();
|
||||||
this.add_font(font);
|
this.add_font(font);
|
||||||
};
|
};
|
||||||
this
|
this
|
||||||
|
|
|
@ -1,10 +1,7 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use fontdue::Metrics;
|
use fontdue::Metrics;
|
||||||
use glam::{IVec2, UVec2, uvec2, ivec2};
|
|
||||||
use hashbrown::HashMap;
|
use hashbrown::HashMap;
|
||||||
use rect_packer::DensePacker;
|
use crate::draw::atlas::{TextureAtlasManager, ImageHandle};
|
||||||
|
|
||||||
use crate::IfModified;
|
|
||||||
|
|
||||||
use super::font::{FontHandle, FontManager};
|
use super::font::{FontHandle, FontManager};
|
||||||
|
|
||||||
|
@ -16,70 +13,30 @@ struct GlyphCacheKey {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct GlyphCacheEntry {
|
pub struct GlyphCacheEntry {
|
||||||
pub data: Vec<u8>,
|
|
||||||
pub metrics: Metrics,
|
pub metrics: Metrics,
|
||||||
pub position: IVec2,
|
pub texture: ImageHandle,
|
||||||
pub size: UVec2,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
|
||||||
pub struct FontTextureInfo<'a> {
|
|
||||||
pub modified: bool,
|
|
||||||
pub data: &'a [u8],
|
|
||||||
pub size: UVec2,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> IfModified<FontTextureInfo<'a>> for FontTextureInfo<'a> {
|
|
||||||
fn if_modified(&self) -> Option<&Self> {
|
|
||||||
match self.modified {
|
|
||||||
true => Some(self),
|
|
||||||
false => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// impl<'a> FontTextureInfo<'a> {
|
|
||||||
// fn if_modified(&self) -> Option<Self> {
|
|
||||||
// match self.modified {
|
|
||||||
// true => Some(*self),
|
|
||||||
// false => None,
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
pub struct FontTextureManager {
|
pub struct FontTextureManager {
|
||||||
glyph_cache: HashMap<GlyphCacheKey, Arc<GlyphCacheEntry>>,
|
glyph_cache: HashMap<GlyphCacheKey, Arc<GlyphCacheEntry>>
|
||||||
packer: DensePacker,
|
|
||||||
font_texture: Vec<u8>,
|
|
||||||
font_texture_size: UVec2,
|
|
||||||
modified: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FontTextureManager {
|
impl FontTextureManager {
|
||||||
pub fn new(size: UVec2) -> Self {
|
pub fn new() -> Self {
|
||||||
FontTextureManager {
|
FontTextureManager {
|
||||||
glyph_cache: HashMap::new(),
|
glyph_cache: HashMap::new(),
|
||||||
packer: DensePacker::new(size.x as i32, size.y as i32),
|
|
||||||
font_texture: vec![0; (size.x * size.y * 4) as usize],
|
|
||||||
font_texture_size: size,
|
|
||||||
modified: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn reset_modified(&mut self) {
|
|
||||||
self.modified = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn info(&self) -> FontTextureInfo {
|
|
||||||
FontTextureInfo {
|
|
||||||
modified: self.modified,
|
|
||||||
data: &self.font_texture,
|
|
||||||
size: self.font_texture_size,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Either looks up the glyph in the cache or renders it and adds it to the cache.
|
/// Either looks up the glyph in the cache or renders it and adds it to the cache.
|
||||||
pub fn glyph(&mut self, font_manager: &FontManager, font_handle: FontHandle, character: char, size: u8) -> Arc<GlyphCacheEntry> {
|
pub fn glyph(
|
||||||
|
&mut self,
|
||||||
|
atlas: &mut TextureAtlasManager,
|
||||||
|
font_manager: &FontManager,
|
||||||
|
font_handle: FontHandle,
|
||||||
|
character: char,
|
||||||
|
size: u8
|
||||||
|
) -> Arc<GlyphCacheEntry> {
|
||||||
let key = GlyphCacheKey {
|
let key = GlyphCacheKey {
|
||||||
font_index: font_handle.0,
|
font_index: font_handle.0,
|
||||||
character,
|
character,
|
||||||
|
@ -90,38 +47,16 @@ impl FontTextureManager {
|
||||||
}
|
}
|
||||||
let font = font_manager.get(font_handle).unwrap();
|
let font = font_manager.get(font_handle).unwrap();
|
||||||
let (metrics, bitmap) = font.rasterize(character, size as f32);
|
let (metrics, bitmap) = font.rasterize(character, size as f32);
|
||||||
log::debug!("rasterized glyph: {}, {:?}, {:?}", character, metrics, bitmap);
|
log::trace!("rasterized glyph: {}, {:?}, {:?}", character, metrics, bitmap);
|
||||||
let texture_position = self.packer.pack(metrics.width as i32, metrics.height as i32, false).unwrap();
|
let texture = atlas.add_grayscale(metrics.width, &bitmap);
|
||||||
let texture_size = uvec2(metrics.width as u32, metrics.height as u32);
|
|
||||||
let entry = Arc::new(GlyphCacheEntry {
|
let entry = Arc::new(GlyphCacheEntry {
|
||||||
data: bitmap,
|
|
||||||
metrics,
|
metrics,
|
||||||
position: ivec2(texture_position.x, texture_position.y),
|
texture
|
||||||
size: texture_size,
|
|
||||||
});
|
});
|
||||||
self.glyph_cache.insert_unique_unchecked(key, Arc::clone(&entry));
|
self.glyph_cache.insert_unique_unchecked(key, Arc::clone(&entry));
|
||||||
self.glyph_place(&entry);
|
|
||||||
self.modified = true;
|
|
||||||
entry
|
entry
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Place glyph onto the font texture.
|
|
||||||
fn glyph_place(&mut self, entry: &GlyphCacheEntry) {
|
|
||||||
let tex_size = self.font_texture_size;
|
|
||||||
let GlyphCacheEntry { size, position, data, .. } = entry;
|
|
||||||
//println!("{size:?} {position:?}");
|
|
||||||
for y in 0..size.y {
|
|
||||||
for x in 0..size.x {
|
|
||||||
let src = (size.x * y + x) as usize;
|
|
||||||
let dst = (tex_size.x * (y + position.y as u32) + (x + position.x as u32)) as usize * 4;
|
|
||||||
self.font_texture[dst..=(dst + 3)].copy_from_slice(&[255, 255, 255, data[src]]);
|
|
||||||
//print!("{} ", if data[src] > 0 {'#'} else {'.'});
|
|
||||||
//print!("{src} {dst} / ");
|
|
||||||
}
|
|
||||||
//println!();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// pub fn glyph(&mut self, font_manager: &FontManager, font_handle: FontHandle, character: char, size: u8) -> Arc<GlyphCacheEntry> {
|
// pub fn glyph(&mut self, font_manager: &FontManager, font_handle: FontHandle, character: char, size: u8) -> Arc<GlyphCacheEntry> {
|
||||||
// let (is_new, glyph) = self.glyph_allocate(font_manager, font_handle, character, size);
|
// let (is_new, glyph) = self.glyph_allocate(font_manager, font_handle, character, size);
|
||||||
// if is_new {
|
// if is_new {
|
||||||
|
@ -133,7 +68,5 @@ impl FontTextureManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for FontTextureManager {
|
impl Default for FontTextureManager {
|
||||||
fn default() -> Self {
|
fn default() -> Self { Self::new() }
|
||||||
Self::new(uvec2(1024, 1024))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
32
hui/src/text/stack.rs
Normal file
32
hui/src/text/stack.rs
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
use super::FontHandle;
|
||||||
|
|
||||||
|
pub struct FontStack {
|
||||||
|
fonts: Vec<FontHandle>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FontStack {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
#[cfg(not(feature = "builtin_font"))]
|
||||||
|
fonts: Vec::new(),
|
||||||
|
#[cfg(feature = "builtin_font")]
|
||||||
|
fonts: vec![super::BUILTIN_FONT],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push(&mut self, font: FontHandle) {
|
||||||
|
self.fonts.push(font);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pop(&mut self) {
|
||||||
|
assert!(self.fonts.pop().is_some())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current(&self) -> Option<FontHandle> {
|
||||||
|
self.fonts.last().copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current_or_default(&self) -> FontHandle {
|
||||||
|
self.current().unwrap_or_default()
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue