GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
Remove taffy, add origin-relative Column layout system
- Rewrite clear-ui/src/layout.rs: replace taffy-based Layout with
origin-relative Column/Row builder + RenderTarget trait
- clear-system-interface: add RenderTarget impl for PageContent
- Delete local column.rs (replaced by clear-ui layout module)
- Remove taffy dependency from both Cargo.toml files
- Fix bg color label overlap with header (cy: 12 -> 28)
- Fix clear-ui demo: replace taffy Layout with hardcoded positions
Cargo.lock | 381 +++++++++++++++++++++++-
Cargo.toml | 7 +-
src/color.rs | 35 +++
src/layout.rs | 112 +++++++
src/lib.rs | 9 +
src/main.rs | 937 ++++++++++++++++------------------------------------------
src/widget.rs | 675 ++++++++++++++++++++++++++++++++++++++++++
7 files changed, 1464 insertions(+), 692 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index 6e7527e..55463fd 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -236,8 +236,10 @@ name = "clear-ui"
version = "0.1.0"
dependencies = [
"bytemuck",
+ "glyphon",
"pollster",
"taffy",
+ "tokio",
"wgpu",
"winit",
]
@@ -311,6 +313,48 @@ dependencies = [
"libc",
]
+[[package]]
+name = "cosmic-text"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59fd57d82eb4bfe7ffa9b1cec0c05e2fd378155b47f255a67983cb4afe0e80c2"
+dependencies = [
+ "bitflags 2.11.1",
+ "fontdb",
+ "log",
+ "rangemap",
+ "rayon",
+ "rustc-hash 1.1.0",
+ "rustybuzz",
+ "self_cell",
+ "swash",
+ "sys-locale",
+ "ttf-parser 0.21.1",
+ "unicode-bidi",
+ "unicode-linebreak",
+ "unicode-script",
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
+dependencies = [
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
+dependencies = [
+ "crossbeam-utils",
+]
+
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
@@ -359,6 +403,12 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76"
+[[package]]
+name = "either"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
+
[[package]]
name = "equivalent"
version = "1.0.2"
@@ -375,6 +425,25 @@ dependencies = [
"windows-sys 0.61.2",
]
+[[package]]
+name = "etagere"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc89bf99e5dc15954a60f707c1e09d7540e5cd9af85fa75caa0b510bc08c5342"
+dependencies = [
+ "euclid",
+ "svg_fmt",
+]
+
+[[package]]
+name = "euclid"
+version = "0.22.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06"
+dependencies = [
+ "num-traits",
+]
+
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
@@ -387,6 +456,38 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+[[package]]
+name = "font-types"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3971f9a5ca983419cdc386941ba3b9e1feba01a0ab888adf78739feb2798492"
+dependencies = [
+ "bytemuck",
+]
+
+[[package]]
+name = "fontconfig-parser"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646"
+dependencies = [
+ "roxmltree",
+]
+
+[[package]]
+name = "fontdb"
+version = "0.16.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0299020c3ef3f60f526a4f64ab4a3d4ce116b1acbf24cdd22da0068e5d81dc3"
+dependencies = [
+ "fontconfig-parser",
+ "log",
+ "memmap2",
+ "slotmap",
+ "tinyvec",
+ "ttf-parser 0.20.0",
+]
+
[[package]]
name = "foreign-types"
version = "0.5.0"
@@ -492,6 +593,19 @@ dependencies = [
"gl_generator",
]
+[[package]]
+name = "glyphon"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc83cc30ed66a0a9e77b7974c91360a13cad5e7ab65aa51e299c383749a018ab"
+dependencies = [
+ "cosmic-text",
+ "etagere",
+ "lru",
+ "rustc-hash 2.1.2",
+ "wgpu",
+]
+
[[package]]
name = "gpu-alloc"
version = "0.6.0"
@@ -705,6 +819,12 @@ dependencies = [
"windows-link",
]
+[[package]]
+name = "libm"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
+
[[package]]
name = "libredox"
version = "0.1.16"
@@ -750,6 +870,12 @@ version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
+[[package]]
+name = "lru"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
+
[[package]]
name = "malloc_buf"
version = "0.0.6"
@@ -789,6 +915,17 @@ dependencies = [
"paste",
]
+[[package]]
+name = "mio"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
+dependencies = [
+ "libc",
+ "wasi",
+ "windows-sys 0.61.2",
+]
+
[[package]]
name = "naga"
version = "24.0.0"
@@ -803,7 +940,7 @@ dependencies = [
"hexf-parse",
"indexmap",
"log",
- "rustc-hash",
+ "rustc-hash 1.1.0",
"spirv",
"strum",
"termcolor",
@@ -1124,7 +1261,7 @@ version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b"
dependencies = [
- "ttf-parser",
+ "ttf-parser 0.25.1",
]
[[package]]
@@ -1280,12 +1417,48 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08"
+[[package]]
+name = "rangemap"
+version = "1.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "973443cf09a9c8656b574a866ab68dfa19f0867d0340648c7d2f6a71b8a8ea68"
+
[[package]]
name = "raw-window-handle"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
+[[package]]
+name = "rayon"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
+dependencies = [
+ "either",
+ "rayon-core",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
+dependencies = [
+ "crossbeam-deque",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "read-fonts"
+version = "0.22.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69aacb76b5c29acfb7f90155d39759a29496aebb49395830e928a9703d2eec2f"
+dependencies = [
+ "bytemuck",
+ "font-types",
+]
+
[[package]]
name = "redox_syscall"
version = "0.4.1"
@@ -1319,12 +1492,24 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
+[[package]]
+name = "roxmltree"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
+
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
+[[package]]
+name = "rustc-hash"
+version = "2.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
+
[[package]]
name = "rustc_version"
version = "0.4.1"
@@ -1366,6 +1551,23 @@ version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+[[package]]
+name = "rustybuzz"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfb9cf8877777222e4a3bc7eb247e398b56baba500c38c1c46842431adc8b55c"
+dependencies = [
+ "bitflags 2.11.1",
+ "bytemuck",
+ "libm",
+ "smallvec",
+ "ttf-parser 0.21.1",
+ "unicode-bidi-mirroring",
+ "unicode-ccc",
+ "unicode-properties",
+ "unicode-script",
+]
+
[[package]]
name = "same-file"
version = "1.0.6"
@@ -1400,6 +1602,12 @@ dependencies = [
"tiny-skia",
]
+[[package]]
+name = "self_cell"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89"
+
[[package]]
name = "semver"
version = "1.0.28"
@@ -1442,6 +1650,16 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
+dependencies = [
+ "errno",
+ "libc",
+]
+
[[package]]
name = "simd_cesu8"
version = "1.1.1"
@@ -1458,6 +1676,16 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
+[[package]]
+name = "skrifa"
+version = "0.22.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e1c44ad1f6c5bdd4eefed8326711b7dbda9ea45dfd36068c427d332aa382cbe"
+dependencies = [
+ "bytemuck",
+ "read-fonts",
+]
+
[[package]]
name = "slab"
version = "0.4.12"
@@ -1513,6 +1741,16 @@ dependencies = [
"serde",
]
+[[package]]
+name = "socket2"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
[[package]]
name = "spirv"
version = "0.3.0+sdk-1.3.268.0"
@@ -1556,6 +1794,23 @@ dependencies = [
"syn",
]
+[[package]]
+name = "svg_fmt"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb"
+
+[[package]]
+name = "swash"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cbd59f3f359ddd2c95af4758c18270eddd9c730dde98598023cdabff472c2ca2"
+dependencies = [
+ "skrifa",
+ "yazi",
+ "zeno",
+]
+
[[package]]
name = "syn"
version = "2.0.117"
@@ -1567,6 +1822,15 @@ dependencies = [
"unicode-ident",
]
+[[package]]
+name = "sys-locale"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4"
+dependencies = [
+ "libc",
+]
+
[[package]]
name = "taffy"
version = "0.10.1"
@@ -1653,6 +1917,49 @@ dependencies = [
"strict-num",
]
+[[package]]
+name = "tinyvec"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "tokio"
+version = "1.52.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
+dependencies = [
+ "bytes",
+ "libc",
+ "mio",
+ "parking_lot",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "socket2",
+ "tokio-macros",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "toml_datetime"
version = "1.1.1+spec-1.1.0"
@@ -1699,18 +2006,66 @@ version = "0.1.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
+[[package]]
+name = "ttf-parser"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17f77d76d837a7830fe1d4f12b7b4ba4192c1888001c7164257e4bc6d21d96b4"
+
+[[package]]
+name = "ttf-parser"
+version = "0.21.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8"
+
[[package]]
name = "ttf-parser"
version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
+[[package]]
+name = "unicode-bidi"
+version = "0.3.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
+
+[[package]]
+name = "unicode-bidi-mirroring"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23cb788ffebc92c5948d0e997106233eeb1d8b9512f93f41651f52b6c5f5af86"
+
+[[package]]
+name = "unicode-ccc"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1df77b101bcc4ea3d78dafc5ad7e4f58ceffe0b2b16bf446aeb50b6cb4157656"
+
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+[[package]]
+name = "unicode-linebreak"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f"
+
+[[package]]
+name = "unicode-properties"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d"
+
+[[package]]
+name = "unicode-script"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee"
+
[[package]]
name = "unicode-segmentation"
version = "1.13.2"
@@ -1745,6 +2100,12 @@ dependencies = [
"winapi-util",
]
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
[[package]]
name = "wasip2"
version = "1.0.3+wasi-0.2.9"
@@ -1982,7 +2343,7 @@ dependencies = [
"parking_lot",
"profiling",
"raw-window-handle",
- "rustc-hash",
+ "rustc-hash 1.1.0",
"smallvec",
"thiserror 2.0.18",
"wgpu-hal",
@@ -2025,7 +2386,7 @@ dependencies = [
"range-alloc",
"raw-window-handle",
"renderdoc-sys",
- "rustc-hash",
+ "rustc-hash 1.1.0",
"smallvec",
"thiserror 2.0.18",
"wasm-bindgen",
@@ -2347,6 +2708,18 @@ version = "0.8.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f"
+[[package]]
+name = "yazi"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c94451ac9513335b5e23d7a8a2b61a7102398b8cca5160829d313e84c9d98be1"
+
+[[package]]
+name = "zeno"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd15f8e0dbb966fd9245e7498c7e9e5055d9e5c8b676b95bd67091cd11a1e697"
+
[[package]]
name = "zerocopy"
version = "0.8.48"
diff --git a/Cargo.toml b/Cargo.toml
index 6c06c0a..e0bd151 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -3,9 +3,14 @@ name = "clear-ui"
version = "0.1.0"
edition = "2021"
+[lib]
+name = "clear_ui"
+path = "src/lib.rs"
+
[dependencies]
winit = "0.30"
wgpu = "24"
bytemuck = { version = "1", features = ["derive"] }
pollster = "0.4"
-taffy = "0.10.1"
+glyphon = "0.8"
+tokio = { version = "1", features = ["full"] }
diff --git a/src/color.rs b/src/color.rs
new file mode 100644
index 0000000..3dd05c3
--- /dev/null
+++ b/src/color.rs
@@ -0,0 +1,35 @@
+pub const HEADER_BG: [f32; 4] = [0.08, 0.08, 0.12, 1.0];
+pub const HEADER_ACCENT: [f32; 4] = [0.60, 0.40, 0.20, 1.0];
+pub const SIDEBAR_BG: [f32; 4] = [0.10, 0.10, 0.13, 1.0];
+pub const CONTENT_BG: [f32; 4] = [0.13, 0.13, 0.16, 1.0];
+pub const PANEL_IDLE: [f32; 4] = [0.14, 0.70, 0.38, 1.0];
+pub const PANEL_DRAG: [f32; 4] = [0.24, 0.85, 0.50, 1.0];
+pub const BUTTON_IDLE: [f32; 4] = [0.20, 0.40, 0.65, 1.0];
+pub const BUTTON_HOVER: [f32; 4] = [0.30, 0.52, 0.78, 1.0];
+pub const BUTTON_PRESS: [f32; 4] = [0.12, 0.28, 0.50, 1.0];
+pub const STATUS_BG: [f32; 4] = [0.06, 0.06, 0.10, 1.0];
+pub const STATUS_ACCENT: [f32; 4] = [0.20, 0.20, 0.25, 1.0];
+pub const RESET_BTN_IDLE: [f32; 4] = [0.55, 0.20, 0.20, 1.0];
+pub const RESET_BTN_HOVER: [f32; 4] = [0.70, 0.30, 0.30, 1.0];
+pub const RESET_BTN_PRESS: [f32; 4] = [0.40, 0.12, 0.12, 1.0];
+pub const CHECKBOX_BG: [f32; 4] = [0.18, 0.18, 0.22, 1.0];
+pub const CHECKBOX_CHECKED: [f32; 4] = [0.20, 0.50, 0.75, 1.0];
+pub const CHECKBOX_HOVER: [f32; 4] = [0.25, 0.25, 0.30, 1.0];
+pub const TOGGLE_OFF: [f32; 4] = [0.25, 0.25, 0.30, 1.0];
+pub const TOGGLE_ON: [f32; 4] = [0.14, 0.70, 0.38, 1.0];
+pub const TOGGLE_HOVER: [f32; 4] = [0.30, 0.30, 0.35, 1.0];
+pub const SLIDER_TRACK: [f32; 4] = [0.18, 0.18, 0.22, 1.0];
+pub const SLIDER_THUMB: [f32; 4] = [0.60, 0.60, 0.65, 1.0];
+pub const SLIDER_THUMB_DRAG: [f32; 4] = [0.80, 0.80, 0.85, 1.0];
+pub const PROGRESS_BG: [f32; 4] = [0.18, 0.18, 0.22, 1.0];
+pub const PROGRESS_FILL: [f32; 4] = [0.20, 0.50, 0.75, 1.0];
+
+pub const SPINBOX_BG: [f32; 4] = [0.18, 0.18, 0.22, 1.0];
+pub const SPINBOX_BUTTON: [f32; 4] = [0.25, 0.25, 0.32, 1.0];
+pub const SPINBOX_BUTTON_HOVER: [f32; 4] = [0.35, 0.35, 0.42, 1.0];
+pub const SPINBOX_DISPLAY: [f32; 4] = [0.12, 0.12, 0.16, 1.0];
+
+pub const TEXT_FG: [f32; 4] = [0.80, 0.80, 0.85, 1.0];
+pub const TEXT_DIM: [f32; 4] = [0.53, 0.53, 0.60, 1.0];
+pub const TEXT_HEADER: [f32; 4] = [0.90, 0.90, 0.95, 1.0];
+pub const TEXT_ACCENT: [f32; 4] = [0.56, 0.83, 0.56, 1.0];
diff --git a/src/layout.rs b/src/layout.rs
new file mode 100644
index 0000000..8a167c6
--- /dev/null
+++ b/src/layout.rs
@@ -0,0 +1,112 @@
+use crate::widget::Widget;
+
+pub trait RenderTarget {
+ fn rect(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32);
+ fn text(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4]);
+}
+
+fn render_widget<T: Widget>(pc: &mut dyn RenderTarget, w: &mut T, x: f32, y: f32, ww: f32, wh: f32) {
+ w.set_rect(x, y, ww, wh);
+ for (qx, qy, qw, qh, qc) in w.extra_quads() {
+ pc.rect(qc, qx, qy, qw, qh);
+ }
+ for label in w.text_labels() {
+ pc.text(
+ &label.text,
+ label.x,
+ label.y,
+ label.font_size,
+ [
+ label.color[0] as f32 / 255.0,
+ label.color[1] as f32 / 255.0,
+ label.color[2] as f32 / 255.0,
+ 1.0,
+ ],
+ );
+ }
+}
+
+pub struct Column<'a> {
+ pc: &'a mut dyn RenderTarget,
+ ox: f32,
+ oy: f32,
+ cx: f32,
+ pub y: f32,
+ pub cw: f32,
+}
+
+impl<'a> Column<'a> {
+ pub fn new(pc: &'a mut dyn RenderTarget, ox: f32, oy: f32, cx: f32, cy: f32, cw: f32) -> Self {
+ Self { pc, ox, oy, cx, y: cy, cw }
+ }
+
+ fn ax(&self, x_off: f32) -> f32 {
+ self.ox + self.cx + x_off
+ }
+
+ fn ay(&self) -> f32 {
+ self.oy + self.y
+ }
+
+ pub fn spacing(&mut self, dy: f32) {
+ self.y += dy;
+ }
+
+ pub fn separator(&mut self) {
+ let x = self.ax(8.0);
+ let y = self.ay();
+ self.pc
+ .rect([0.18, 0.18, 0.27, 1.0], x, y, self.cw - 16.0, 1.0);
+ self.y += 8.0;
+ }
+
+ pub fn header(&mut self, text: &str, x_off: f32) {
+ let x = self.ax(x_off);
+ let y = self.ay();
+ self.pc
+ .text(text, x, y, 14.0, [0.83, 0.83, 0.83, 1.0]);
+ self.y += 22.0;
+ }
+
+ pub fn text(&mut self, text: &str, x_off: f32, y_off: f32, font_size: f32, color: [f32; 4]) {
+ let x = self.ax(x_off);
+ let y = self.ay() + y_off;
+ self.pc
+ .text(text, x, y, font_size, color);
+ }
+
+ pub fn widget<T: Widget>(&mut self, w: &mut T, x_off: f32, ww: f32, wh: f32) {
+ let x = self.ax(x_off);
+ let y = self.ay();
+ render_widget(self.pc, w, x, y, ww, wh);
+ self.y += wh;
+ }
+
+ pub fn row<F: FnOnce(&mut Row)>(&mut self, h: f32, f: F) {
+ let row_y = self.ay();
+ let mut row = Row {
+ pc: &mut *self.pc,
+ base_x: self.ox + self.cx,
+ y: row_y,
+ };
+ f(&mut row);
+ self.y = self.y + h;
+ }
+}
+
+pub struct Row<'a> {
+ pc: &'a mut dyn RenderTarget,
+ base_x: f32,
+ y: f32,
+}
+
+impl<'a> Row<'a> {
+ pub fn text(&mut self, text: &str, x_off: f32, y_off: f32, font_size: f32, color: [f32; 4]) {
+ self.pc
+ .text(text, self.base_x + x_off, self.y + y_off, font_size, color);
+ }
+
+ pub fn widget<T: Widget>(&mut self, w: &mut T, x_off: f32, ww: f32, wh: f32) {
+ render_widget(self.pc, w, self.base_x + x_off, self.y, ww, wh);
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..b6515ea
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,9 @@
+pub mod color;
+pub mod widget;
+pub mod layout;
+
+pub mod colors {
+ pub use crate::color::*;
+}
+
+pub const SHADER: &str = include_str!("shader.wgsl");
diff --git a/src/main.rs b/src/main.rs
index d5d17a7..5b69fc1 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -4,7 +4,13 @@ use winit::application::ApplicationHandler;
use winit::event::{ElementState, MouseButton, WindowEvent};
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
use winit::window::{Window, WindowAttributes};
-use taffy::prelude::TaffyMaxContent;
+
+use clear_ui::widget::{Button, Checkbox, ContentBg, Header, Panel, ProgressBar, Sidebar, Slider, Spinbox, StatusBar, Toggle, Widget};
+
+use glyphon::{
+ Attrs, Buffer, Cache, FontSystem, Metrics, Resolution, SwashCache, TextArea, TextAtlas,
+ TextBounds, TextRenderer, Viewport,
+};
#[repr(C)]
#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
@@ -48,658 +54,17 @@ fn quad_vertices(
]
}
-mod colors {
- pub const HEADER_BG: [f32; 4] = [0.08, 0.08, 0.12, 1.0];
- pub const HEADER_ACCENT: [f32; 4] = [0.60, 0.40, 0.20, 1.0];
- pub const SIDEBAR_BG: [f32; 4] = [0.10, 0.10, 0.13, 1.0];
- pub const CONTENT_BG: [f32; 4] = [0.13, 0.13, 0.16, 1.0];
- pub const PANEL_IDLE: [f32; 4] = [0.14, 0.70, 0.38, 1.0];
- pub const PANEL_DRAG: [f32; 4] = [0.24, 0.85, 0.50, 1.0];
- pub const BUTTON_IDLE: [f32; 4] = [0.20, 0.40, 0.65, 1.0];
- pub const BUTTON_HOVER: [f32; 4] = [0.30, 0.52, 0.78, 1.0];
- pub const BUTTON_PRESS: [f32; 4] = [0.12, 0.28, 0.50, 1.0];
- pub const STATUS_BG: [f32; 4] = [0.06, 0.06, 0.10, 1.0];
- pub const STATUS_ACCENT: [f32; 4] = [0.20, 0.20, 0.25, 1.0];
- pub const RESET_BTN_IDLE: [f32; 4] = [0.55, 0.20, 0.20, 1.0];
- pub const RESET_BTN_HOVER: [f32; 4] = [0.70, 0.30, 0.30, 1.0];
- pub const RESET_BTN_PRESS: [f32; 4] = [0.40, 0.12, 0.12, 1.0];
- pub const CHECKBOX_BG: [f32; 4] = [0.18, 0.18, 0.22, 1.0];
- pub const CHECKBOX_CHECKED: [f32; 4] = [0.20, 0.50, 0.75, 1.0];
- pub const CHECKBOX_HOVER: [f32; 4] = [0.25, 0.25, 0.30, 1.0];
- pub const TOGGLE_OFF: [f32; 4] = [0.25, 0.25, 0.30, 1.0];
- pub const TOGGLE_ON: [f32; 4] = [0.14, 0.70, 0.38, 1.0];
- pub const TOGGLE_HOVER: [f32; 4] = [0.30, 0.30, 0.35, 1.0];
- pub const SLIDER_TRACK: [f32; 4] = [0.18, 0.18, 0.22, 1.0];
- pub const SLIDER_THUMB: [f32; 4] = [0.60, 0.60, 0.65, 1.0];
- pub const SLIDER_THUMB_DRAG: [f32; 4] = [0.80, 0.80, 0.85, 1.0];
- pub const PROGRESS_BG: [f32; 4] = [0.18, 0.18, 0.22, 1.0];
- pub const PROGRESS_FILL: [f32; 4] = [0.20, 0.50, 0.75, 1.0];
-}
-
-trait Widget {
- fn rect(&self) -> (f32, f32, f32, f32);
- fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32);
-
- fn hit_test(&self, px: f32, py: f32) -> bool {
- let (x, y, w, h) = self.rect();
- px >= x && px <= x + w && py >= y && py <= y + h
- }
-
- fn cursor_moved(&mut self, _px: f32, _py: f32) -> bool { false }
- fn mouse_input(&mut self, _button: MouseButton, _state: ElementState, _px: f32, _py: f32) -> bool { false }
-
- fn vertices(&self, sw: f32, sh: f32) -> Vec<Vertex> {
- let (x, y, w, h) = self.rect();
- quad_vertices(x, y, w, h, sw, sh, self.color()).to_vec()
- }
-
- fn color(&self) -> [f32; 4];
-
- fn is_dragging(&self) -> bool { false }
- fn drag_update(&mut self, _px: f32, _py: f32) -> bool { false }
- fn drag_begin(&mut self, _px: f32, _py: f32) {}
- fn drag_end(&mut self) {}
- fn take_click(&mut self) -> bool { false }
- fn draggable(&self) -> bool { false }
-}
-
-struct Header {
- x: f32, y: f32, w: f32, h: f32,
-}
-
-impl Header {
- fn new() -> Self { Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0 } }
-}
-
-impl Widget for Header {
- fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
- fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
- fn color(&self) -> [f32; 4] { colors::HEADER_BG }
-
- fn vertices(&self, sw: f32, sh: f32) -> Vec<Vertex> {
- let (x, y, w, h) = self.rect();
- let mut verts = Vec::new();
- verts.extend(quad_vertices(x, y, w, h, sw, sh, colors::HEADER_BG));
- verts.extend(quad_vertices(x, y + h - 2.0, w, 2.0, sw, sh, colors::HEADER_ACCENT));
- verts
- }
-}
-
-struct StatusBar {
- x: f32, y: f32, w: f32, h: f32,
-}
-
-impl StatusBar {
- fn new() -> Self { Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0 } }
-}
-
-impl Widget for StatusBar {
- fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
- fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
- fn color(&self) -> [f32; 4] { colors::STATUS_BG }
-
- fn vertices(&self, sw: f32, sh: f32) -> Vec<Vertex> {
- let (x, y, w, h) = self.rect();
- let mut verts = Vec::new();
- verts.extend(quad_vertices(x, y, w, h, sw, sh, colors::STATUS_BG));
- verts.extend(quad_vertices(x, y, w, 2.0, sw, sh, colors::STATUS_ACCENT));
- verts
- }
-}
-
-struct Panel {
- x: f32, y: f32, w: f32, h: f32,
- dragging: bool,
- drag_ox: f32, drag_oy: f32,
- drag_start_x: f32, drag_start_y: f32,
-}
-
-impl Panel {
- fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
- Self { x, y, w, h, dragging: false, drag_ox: 0.0, drag_oy: 0.0, drag_start_x: 0.0, drag_start_y: 0.0 }
- }
-}
-
-impl Widget for Panel {
- fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
- fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
- fn color(&self) -> [f32; 4] { if self.dragging { colors::PANEL_DRAG } else { colors::PANEL_IDLE } }
-
- fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
- if button != MouseButton::Left { return false; }
- match state {
- ElementState::Pressed => {
- if self.hit_test(px, py) {
- self.drag_begin(px, py);
- return true;
- }
- }
- ElementState::Released => {
- if self.dragging { self.drag_end(); return true; }
- }
- }
- false
- }
-
- fn is_dragging(&self) -> bool { self.dragging }
- fn draggable(&self) -> bool { true }
-
- fn drag_update(&mut self, px: f32, py: f32) -> bool {
- let nx = px - self.drag_ox;
- let ny = py - self.drag_oy;
- if (nx - self.x).abs() > 0.01 || (ny - self.y).abs() > 0.01 {
- self.x = nx;
- self.y = ny;
- return true;
- }
- false
- }
-
- fn drag_begin(&mut self, px: f32, py: f32) {
- self.dragging = true;
- self.drag_ox = px - self.x;
- self.drag_oy = py - self.y;
- self.drag_start_x = self.x;
- self.drag_start_y = self.y;
- }
-
- fn drag_end(&mut self) { self.dragging = false; }
-}
-
-enum ButtonKind {
- Primary,
- Reset,
-}
-
-struct Button {
- x: f32, y: f32, w: f32, h: f32,
- hovering: bool,
- pressed: bool,
- just_clicked: bool,
- kind: ButtonKind,
+fn widget_vertices(w: &dyn Widget, sw: f32, sh: f32) -> Vec<Vertex> {
+ let (x, y, ww, h) = w.rect();
+ quad_vertices(x, y, ww, h, sw, sh, w.color()).to_vec()
}
-impl Button {
- fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
- Self { x, y, w, h, hovering: false, pressed: false, just_clicked: false, kind: ButtonKind::Primary }
- }
-
- fn new_reset(x: f32, y: f32, w: f32, h: f32) -> Self {
- Self { x, y, w, h, hovering: false, pressed: false, just_clicked: false, kind: ButtonKind::Reset }
- }
-}
-
-impl Widget for Button {
- fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
- fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
- fn color(&self) -> [f32; 4] {
- match self.kind {
- ButtonKind::Primary => {
- if self.pressed { colors::BUTTON_PRESS }
- else if self.hovering { colors::BUTTON_HOVER }
- else { colors::BUTTON_IDLE }
- }
- ButtonKind::Reset => {
- if self.pressed { colors::RESET_BTN_PRESS }
- else if self.hovering { colors::RESET_BTN_HOVER }
- else { colors::RESET_BTN_IDLE }
- }
- }
- }
-
- fn cursor_moved(&mut self, px: f32, py: f32) -> bool {
- let was = self.hovering;
- self.hovering = self.hit_test(px, py);
- was != self.hovering
- }
-
- fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
- if button != MouseButton::Left { return false; }
- match state {
- ElementState::Pressed => {
- if self.hit_test(px, py) {
- self.pressed = true;
- return true;
- }
- }
- ElementState::Released => {
- if self.pressed && self.hit_test(px, py) {
- self.just_clicked = true;
- }
- let was = self.pressed;
- self.pressed = false;
- return was;
- }
- }
- false
- }
-
- fn take_click(&mut self) -> bool {
- if self.just_clicked { self.just_clicked = false; true } else { false }
- }
-}
-
-struct Sidebar {
- x: f32, y: f32, w: f32, h: f32,
-}
-
-impl Sidebar {
- fn new(w: f32) -> Self { Self { x: 0.0, y: 0.0, w, h: 0.0 } }
-}
-
-impl Widget for Sidebar {
- fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
- fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
- fn color(&self) -> [f32; 4] { colors::SIDEBAR_BG }
-}
-
-struct ContentBg {
- x: f32, y: f32, w: f32, h: f32,
-}
-
-impl ContentBg {
- fn new() -> Self { Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0 } }
-}
-
-impl Widget for ContentBg {
- fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
- fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
- fn color(&self) -> [f32; 4] { colors::CONTENT_BG }
-}
-
-struct Checkbox {
- x: f32, y: f32, w: f32, h: f32,
- hovering: bool,
- checked: bool,
- just_clicked: bool,
-}
-
-impl Checkbox {
- fn new() -> Self {
- Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, hovering: false, checked: false, just_clicked: false }
- }
-}
-
-impl Widget for Checkbox {
- fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
- fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
- fn color(&self) -> [f32; 4] { colors::CHECKBOX_BG }
-
- fn vertices(&self, sw: f32, sh: f32) -> Vec<Vertex> {
- let (x, y, w, h) = self.rect();
- let mut verts = Vec::new();
- let bg = if self.hovering { colors::CHECKBOX_HOVER } else { colors::CHECKBOX_BG };
- verts.extend(quad_vertices(x, y, w, h, sw, sh, bg));
- if self.checked {
- let inset = w * 0.2;
- verts.extend(quad_vertices(x + inset, y + inset, w - inset * 2.0, h - inset * 2.0, sw, sh, colors::CHECKBOX_CHECKED));
- }
- verts
- }
-
- fn cursor_moved(&mut self, px: f32, py: f32) -> bool {
- let was = self.hovering;
- self.hovering = self.hit_test(px, py);
- was != self.hovering
- }
-
- fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
- if button != MouseButton::Left { return false; }
- match state {
- ElementState::Pressed => {}
- ElementState::Released => {
- if self.hit_test(px, py) {
- self.checked = !self.checked;
- self.just_clicked = true;
- return true;
- }
- }
- }
- false
- }
-
- fn take_click(&mut self) -> bool {
- if self.just_clicked { self.just_clicked = false; true } else { false }
- }
-}
-
-struct Toggle {
- x: f32, y: f32, w: f32, h: f32,
- hovering: bool,
- toggled: bool,
- just_toggled: bool,
-}
-
-impl Toggle {
- fn new() -> Self {
- Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, hovering: false, toggled: false, just_toggled: false }
- }
-}
-
-impl Widget for Toggle {
- fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
- fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
- fn color(&self) -> [f32; 4] { if self.toggled { colors::TOGGLE_ON } else { colors::TOGGLE_OFF } }
-
- fn vertices(&self, sw: f32, sh: f32) -> Vec<Vertex> {
- let (x, y, w, h) = self.rect();
- let mut verts = Vec::new();
- let bg = if self.toggled { colors::TOGGLE_ON } else if self.hovering { colors::TOGGLE_HOVER } else { colors::TOGGLE_OFF };
- verts.extend(quad_vertices(x, y, w, h, sw, sh, bg));
- let thumb_size = h * 0.7;
- let thumb_y = y + (h - thumb_size) * 0.5;
- let thumb_x = if self.toggled { x + w - thumb_size - (h * 0.15) } else { x + (h * 0.15) };
- verts.extend(quad_vertices(thumb_x, thumb_y, thumb_size, thumb_size, sw, sh, [0.95, 0.95, 0.97, 1.0]));
- verts
- }
-
- fn cursor_moved(&mut self, px: f32, py: f32) -> bool {
- let was = self.hovering;
- self.hovering = self.hit_test(px, py);
- was != self.hovering
- }
-
- fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
- if button != MouseButton::Left { return false; }
- match state {
- ElementState::Pressed => {}
- ElementState::Released => {
- if self.hit_test(px, py) {
- self.toggled = !self.toggled;
- self.just_toggled = true;
- return true;
- }
- }
- }
- false
- }
-}
-
-struct Slider {
- x: f32, y: f32, w: f32, h: f32,
- dragging: bool,
- value: f32,
- drag_offset: f32,
-}
-
-impl Slider {
- fn new() -> Self {
- Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, dragging: false, value: 0.5, drag_offset: 0.0 }
- }
-}
-
-impl Widget for Slider {
- fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
- fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
- fn color(&self) -> [f32; 4] { colors::SLIDER_TRACK }
-
- fn vertices(&self, sw: f32, sh: f32) -> Vec<Vertex> {
- let (x, y, w, h) = self.rect();
- let mut verts = Vec::new();
- let track_h = h * 0.3;
- let track_y = y + (h - track_h) * 0.5;
- verts.extend(quad_vertices(x, track_y, w, track_h, sw, sh, colors::SLIDER_TRACK));
-
- let thumb_size = h * 0.9;
- let range = w - thumb_size;
- let thumb_x = x + self.value * range;
- let thumb_y = y + (h - thumb_size) * 0.5;
- let thumb_color = if self.dragging { colors::SLIDER_THUMB_DRAG } else { colors::SLIDER_THUMB };
- verts.extend(quad_vertices(thumb_x, thumb_y, thumb_size, thumb_size, sw, sh, thumb_color));
- verts
- }
-
- fn draggable(&self) -> bool { true }
- fn is_dragging(&self) -> bool { self.dragging }
-
- fn drag_update(&mut self, px: f32, _py: f32) -> bool {
- let (sx, _, sw, _) = self.rect();
- let thumb_size = self.h * 0.9;
- let range = sw - thumb_size;
- let raw = (px - self.drag_offset - sx) / range;
- let new_val = raw.clamp(0.0, 1.0);
- if (new_val - self.value).abs() > 0.001 {
- self.value = new_val;
- return true;
- }
- false
- }
-
- fn drag_begin(&mut self, px: f32, _py: f32) {
- self.dragging = true;
- let thumb_size = self.h * 0.9;
- let thumb_x = self.x + self.value * (self.w - thumb_size);
- self.drag_offset = px - thumb_x;
- }
-
- fn drag_end(&mut self) { self.dragging = false; }
-}
-
-struct ProgressBar {
- x: f32, y: f32, w: f32, h: f32,
- value: f32,
-}
-
-impl ProgressBar {
- fn new(value: f32) -> Self {
- Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, value }
- }
-}
-
-impl Widget for ProgressBar {
- fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
- fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
- fn color(&self) -> [f32; 4] { colors::PROGRESS_BG }
-
- fn vertices(&self, sw: f32, sh: f32) -> Vec<Vertex> {
- let (x, y, w, h) = self.rect();
- let mut verts = Vec::new();
- verts.extend(quad_vertices(x, y, w, h, sw, sh, colors::PROGRESS_BG));
- let fill_w = w * self.value;
- if fill_w > 0.0 {
- verts.extend(quad_vertices(x, y, fill_w, h, sw, sh, colors::PROGRESS_FILL));
- }
- verts
- }
-}
-
-use taffy::geometry::{Rect, Size};
-use taffy::style::{Dimension, FlexDirection, LengthPercentage, Style};
-
-struct Layout {
- tree: taffy::TaffyTree<()>,
- root: taffy::NodeId,
- widget_nodes: Vec<taffy::NodeId>,
-}
-
-impl Layout {
- fn new(width: f32, height: f32) -> Self {
- let mut tree = taffy::TaffyTree::new();
-
- let header = tree.new_leaf(Style {
- size: Size { width: Dimension::percent(1.0), height: Dimension::length(40.0) },
- ..Default::default()
- }).unwrap();
-
- let sidebar = tree.new_leaf(Style {
- size: Size { width: Dimension::length(60.0), height: Dimension::percent(1.0) },
- ..Default::default()
- }).unwrap();
-
- let content_bg = tree.new_leaf(Style {
- flex_grow: 1.0,
- size: Size { width: Dimension::percent(1.0), height: Dimension::percent(1.0) },
- ..Default::default()
- }).unwrap();
-
- let btn_a = tree.new_leaf(Style {
- size: Size { width: Dimension::length(140.0), height: Dimension::length(40.0) },
- ..Default::default()
- }).unwrap();
-
- let btn_b = tree.new_leaf(Style {
- size: Size { width: Dimension::length(140.0), height: Dimension::length(40.0) },
- ..Default::default()
- }).unwrap();
-
- let btn_c = tree.new_leaf(Style {
- size: Size { width: Dimension::length(140.0), height: Dimension::length(40.0) },
- ..Default::default()
- }).unwrap();
-
- let panel = tree.new_leaf(Style {
- size: Size { width: Dimension::length(400.0), height: Dimension::length(250.0) },
- ..Default::default()
- }).unwrap();
-
- let click_me = tree.new_leaf(Style {
- size: Size { width: Dimension::length(140.0), height: Dimension::length(40.0) },
- ..Default::default()
- }).unwrap();
-
- let reset = tree.new_leaf(Style {
- size: Size { width: Dimension::length(140.0), height: Dimension::length(40.0) },
- ..Default::default()
- }).unwrap();
-
- let checkbox = tree.new_leaf(Style {
- size: Size { width: Dimension::length(24.0), height: Dimension::length(24.0) },
- ..Default::default()
- }).unwrap();
-
- let toggle = tree.new_leaf(Style {
- size: Size { width: Dimension::length(48.0), height: Dimension::length(24.0) },
- ..Default::default()
- }).unwrap();
-
- let progress_bar = tree.new_leaf(Style {
- flex_grow: 1.0,
- size: Size { width: Dimension::length(160.0), height: Dimension::length(24.0) },
- ..Default::default()
- }).unwrap();
-
- let slider = tree.new_leaf(Style {
- size: Size { width: Dimension::length(300.0), height: Dimension::length(32.0) },
- ..Default::default()
- }).unwrap();
-
- let status_bar = tree.new_leaf(Style {
- size: Size { width: Dimension::percent(1.0), height: Dimension::length(28.0) },
- ..Default::default()
- }).unwrap();
-
- let btn_row_1 = tree.new_with_children(
- Style {
- display: taffy::style::Display::Flex,
- flex_direction: FlexDirection::Row,
- gap: Size { width: LengthPercentage::length(10.0), height: LengthPercentage::length(0.0) },
- ..Default::default()
- },
- &[btn_a, btn_b, btn_c],
- ).unwrap();
-
- let btn_row_2 = tree.new_with_children(
- Style {
- display: taffy::style::Display::Flex,
- flex_direction: FlexDirection::Row,
- gap: Size { width: LengthPercentage::length(10.0), height: LengthPercentage::length(0.0) },
- ..Default::default()
- },
- &[click_me, reset],
- ).unwrap();
-
- let controls_row = tree.new_with_children(
- Style {
- display: taffy::style::Display::Flex,
- flex_direction: FlexDirection::Row,
- align_items: Some(taffy::style::AlignItems::Center),
- gap: Size { width: LengthPercentage::length(16.0), height: LengthPercentage::length(0.0) },
- ..Default::default()
- },
- &[checkbox, toggle, progress_bar],
- ).unwrap();
-
- let _content = tree.new_with_children(
- Style {
- display: taffy::style::Display::Flex,
- flex_direction: FlexDirection::Column,
- flex_grow: 1.0,
- padding: Rect {
- top: LengthPercentage::length(20.0),
- left: LengthPercentage::length(20.0),
- bottom: LengthPercentage::length(20.0),
- right: LengthPercentage::length(20.0),
- },
- gap: Size { width: LengthPercentage::length(0.0), height: LengthPercentage::length(12.0) },
- ..Default::default()
- },
- &[btn_row_1, panel, btn_row_2, controls_row, slider],
- ).unwrap();
-
- let body = tree.new_with_children(
- Style {
- display: taffy::style::Display::Flex,
- flex_direction: FlexDirection::Row,
- flex_grow: 1.0,
- size: Size { width: Dimension::percent(1.0), height: Dimension::auto() },
- ..Default::default()
- },
- &[sidebar, content_bg],
- ).unwrap();
-
- let root = tree.new_with_children(
- Style {
- display: taffy::style::Display::Flex,
- flex_direction: FlexDirection::Column,
- size: Size { width: Dimension::length(width), height: Dimension::length(height) },
- ..Default::default()
- },
- &[header, body, status_bar],
- ).unwrap();
-
- let widget_nodes = vec![
- header,
- sidebar,
- content_bg,
- btn_a,
- btn_b,
- btn_c,
- panel,
- click_me,
- reset,
- checkbox,
- toggle,
- progress_bar,
- slider,
- status_bar,
- ];
-
- let mut layout = Self { tree, root, widget_nodes };
- layout.compute(width, height);
- layout
- }
-
- fn compute(&mut self, width: f32, height: f32) {
- self.tree.set_style(self.root, Style {
- size: Size { width: Dimension::length(width), height: Dimension::length(height) },
- ..Default::default()
- }).unwrap();
-
- self.tree.compute_layout(self.root, Size::MAX_CONTENT).unwrap();
- }
-
- fn widget_rect(&self, node_id: taffy::NodeId) -> (f32, f32, f32, f32) {
- let layout = self.tree.layout(node_id).unwrap();
- let x = layout.location.x;
- let y = layout.location.y;
- let w = layout.size.width;
- let h = layout.size.height;
- (x, y, w, h)
- }
-
- fn resize(&mut self, width: f32, height: f32) {
- self.compute(width, height);
- }
+fn make_text_buffer(font_system: &mut FontSystem, text: &str, size: f32) -> Buffer {
+ let metrics = Metrics::new(size, size * 1.4);
+ let mut buffer = Buffer::new(font_system, metrics);
+ buffer.set_text(font_system, text, Attrs::new(), glyphon::Shaping::Advanced);
+ buffer.shape_until_scroll(font_system, true);
+ buffer
}
struct State {
@@ -713,23 +78,41 @@ struct State {
vertex_count: u32,
widgets: Vec<Box<dyn Widget>>,
- layout: Layout,
+ positions: Vec<(f32, f32, f32, f32)>,
+
+ font_system: FontSystem,
+ swash_cache: SwashCache,
+ text_atlas: TextAtlas,
+ text_renderer: TextRenderer,
+ text_viewport: Viewport,
+
+ label_buffer: Buffer,
+ status_buffer: Buffer,
drag_widget: Option<usize>,
+ focused_widget: Option<usize>,
click_count: u32,
cursor_x: f32,
cursor_y: f32,
- width: u32,
- height: u32,
+ width: f32,
+ height: f32,
+ physical_width: u32,
+ physical_height: u32,
+ scale: f64,
}
impl State {
async fn new(window: Arc<Window>) -> Self {
- let size = window.inner_size();
- let sw = size.width as f32;
- let sh = size.height as f32;
+ let scale = window.scale_factor();
+ let physical_size = window.inner_size();
+ let pw = physical_size.width.max(1);
+ let ph = physical_size.height.max(1);
+ let lw = pw as f32 / scale as f32;
+ let lh = ph as f32 / scale as f32;
+ let sw = lw;
+ let sh = lh;
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
backends: wgpu::Backends::VULKAN,
@@ -764,7 +147,7 @@ impl State {
.expect("Failed to create device");
let config = surface
- .get_default_config(&adapter, size.width.max(1), size.height.max(1))
+ .get_default_config(&adapter, pw, ph)
.expect("Failed to get surface config");
surface.configure(&device, &config);
@@ -817,25 +200,39 @@ impl State {
cache: None,
});
- let layout = Layout::new(sw, sh);
+ // Initialize text rendering
+ let mut font_system = FontSystem::new();
+ let swash_cache = SwashCache::new();
+ let cache = Cache::new(&device);
+ let mut text_atlas = TextAtlas::new(&device, &queue, &cache, config.format);
+ let text_renderer = TextRenderer::new(&mut text_atlas, &device, wgpu::MultisampleState::default(), None);
+
+ let mut text_viewport = Viewport::new(&device, &cache);
+ text_viewport.update(&queue, Resolution { width: pw, height: ph });
+
+ let label_buffer = make_text_buffer(&mut font_system, "Hello, Clear UI!", 16.0);
+ let status_buffer = make_text_buffer(&mut font_system, "Click a button to interact", 12.0);
let widgets: Vec<Box<dyn Widget>> = vec![
- Box::new(Header::new()), // 0
- Box::new(Sidebar::new(60.0)), // 1
- Box::new(ContentBg::new()), // 2
- Box::new(Button::new(0.0, 0.0, 140.0, 40.0)), // 3
- Box::new(Button::new(0.0, 0.0, 140.0, 40.0)), // 4
- Box::new(Button::new(0.0, 0.0, 140.0, 40.0)), // 5
- Box::new(Panel::new(0.0, 0.0, 400.0, 250.0)), // 6
- Box::new(Button::new(0.0, 0.0, 140.0, 40.0)), // 7
- Box::new(Button::new_reset(0.0, 0.0, 140.0, 40.0)), // 8
- Box::new(Checkbox::new()), // 9
- Box::new(Toggle::new()), // 10
- Box::new(ProgressBar::new(0.65)), // 11
- Box::new(Slider::new()), // 12
- Box::new(StatusBar::new()), // 13
+ Box::new(Header::new()),
+ Box::new(Sidebar::new(60.0)),
+ Box::new(ContentBg::new()),
+ Box::new(Button::new(0.0, 0.0, 140.0, 40.0)),
+ Box::new(Button::new(0.0, 0.0, 140.0, 40.0)),
+ Box::new(Button::new(0.0, 0.0, 140.0, 40.0)),
+ Box::new(Panel::new(0.0, 0.0, 400.0, 250.0)),
+ Box::new(Button::new(0.0, 0.0, 140.0, 40.0)),
+ Box::new(Button::new_reset(0.0, 0.0, 140.0, 40.0)),
+ Box::new(Checkbox::new()),
+ Box::new(Toggle::new()),
+ Box::new(ProgressBar::new(0.65)),
+ Box::new(Slider::new()),
+ Box::new(StatusBar::new()),
+ Box::new(Spinbox::new(0, -10, 10, 1)),
];
+ let positions = demo_positions(sw, sh);
+
let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Vertex Buffer"),
size: 1,
@@ -853,13 +250,24 @@ impl State {
vertex_buffer,
vertex_count: 0,
widgets,
- layout,
+ positions,
+ font_system,
+ swash_cache,
+ text_atlas,
+ text_renderer,
+ text_viewport,
+ label_buffer,
+ status_buffer,
drag_widget: None,
+ focused_widget: None,
click_count: 0,
cursor_x: 0.0,
cursor_y: 0.0,
- width: size.width,
- height: size.height,
+ width: lw,
+ height: lh,
+ physical_width: pw,
+ physical_height: ph,
+ scale,
};
state.apply_layout();
@@ -868,23 +276,26 @@ impl State {
}
fn apply_layout(&mut self) {
- for (i, node_id) in self.layout.widget_nodes.iter().enumerate() {
+ for (i, pos) in self.positions.iter().enumerate() {
if let Some(widget) = self.widgets.get_mut(i) {
if widget.is_dragging() {
continue;
}
- let (x, y, w, h) = self.layout.widget_rect(*node_id);
+ let (x, y, w, h) = *pos;
widget.set_rect(x, y, w, h);
}
}
}
fn collect_vertices(&self) -> Vec<Vertex> {
- let sw = self.width as f32;
- let sh = self.height as f32;
+ let sw = self.width;
+ let sh = self.height;
let mut verts = Vec::new();
for w in &self.widgets {
- verts.extend(w.vertices(sw, sh));
+ verts.extend(widget_vertices(w.as_ref(), sw, sh));
+ for (qx, qy, qw, qh, qc) in w.extra_quads() {
+ verts.extend(quad_vertices(qx, qy, qw, qh, sw, sh, qc));
+ }
}
verts
}
@@ -905,14 +316,104 @@ impl State {
self.queue.write_buffer(&self.vertex_buffer, 0, data);
}
+ fn update_status_text(&mut self, text: &str) {
+ self.status_buffer = make_text_buffer(&mut self.font_system, text, 12.0);
+ }
+
+ fn prepare_text(&mut self) {
+ let Self {
+ ref mut text_renderer,
+ ref device,
+ ref queue,
+ ref mut font_system,
+ ref mut text_atlas,
+ ref mut text_viewport,
+ ref mut swash_cache,
+ ref label_buffer,
+ ref status_buffer,
+ physical_width,
+ physical_height,
+ scale,
+ ..
+ } = self;
+
+ let viewport = Resolution { width: *physical_width, height: *physical_height };
+ text_viewport.update(queue, viewport);
+
+ let scale_f32 = *scale as f32;
+
+ let mut areas: Vec<TextArea> = vec![
+ TextArea {
+ buffer: label_buffer,
+ left: 80.0 * scale_f32,
+ top: 12.0 * scale_f32,
+ scale: scale_f32,
+ bounds: TextBounds {
+ left: 0,
+ top: 0,
+ right: *physical_width as i32,
+ bottom: *physical_height as i32,
+ },
+ default_color: glyphon::Color::rgb(0xcc, 0xcc, 0xd4),
+ custom_glyphs: &[],
+ },
+ TextArea {
+ buffer: status_buffer,
+ left: 12.0 * scale_f32,
+ top: *physical_height as f32 - 24.0 * scale_f32,
+ scale: scale_f32,
+ bounds: TextBounds {
+ left: 0,
+ top: 0,
+ right: *physical_width as i32,
+ bottom: *physical_height as i32,
+ },
+ default_color: glyphon::Color::rgb(0x55, 0x55, 0x66),
+ custom_glyphs: &[],
+ },
+ ];
+
+ let mut widget_buffers: Vec<Buffer> = Vec::new();
+ let mut widget_labels: Vec<TextLabel> = Vec::new();
+ for w in self.widgets.iter() {
+ for label in w.text_labels() {
+ widget_buffers.push(make_text_buffer(font_system, &label.text, label.font_size));
+ widget_labels.push(label);
+ }
+ }
+
+ for (buf, label) in widget_buffers.iter().zip(widget_labels.iter()) {
+ areas.push(TextArea {
+ buffer: buf,
+ left: label.x * scale_f32,
+ top: label.y * scale_f32,
+ scale: scale_f32,
+ bounds: TextBounds {
+ left: 0,
+ top: 0,
+ right: *physical_width as i32,
+ bottom: *physical_height as i32,
+ },
+ default_color: glyphon::Color::rgb(label.color[0], label.color[1], label.color[2]),
+ custom_glyphs: &[],
+ });
+ }
+
+ text_renderer
+ .prepare(device, queue, font_system, text_atlas, text_viewport, areas, swash_cache)
+ .unwrap();
+ }
+
fn resize(&mut self, new_size: winit::dpi::PhysicalSize<u32>) {
if new_size.width > 0 && new_size.height > 0 {
- self.width = new_size.width;
- self.height = new_size.height;
+ self.physical_width = new_size.width;
+ self.physical_height = new_size.height;
+ self.width = new_size.width as f32 / self.scale as f32;
+ self.height = new_size.height as f32 / self.scale as f32;
self.config.width = new_size.width;
self.config.height = new_size.height;
self.surface.configure(&self.device, &self.config);
- self.layout.resize(new_size.width as f32, new_size.height as f32);
+ self.positions = demo_positions(self.width, self.height);
self.apply_layout();
self.upload_vertices();
}
@@ -921,8 +422,8 @@ impl State {
fn handle_event(&mut self, event: &WindowEvent) -> bool {
match event {
WindowEvent::CursorMoved { position, .. } => {
- self.cursor_x = position.x as f32;
- self.cursor_y = position.y as f32;
+ self.cursor_x = position.x as f32 / self.scale as f32;
+ self.cursor_y = position.y as f32 / self.scale as f32;
let mut changed = false;
if let Some(idx) = self.drag_widget {
@@ -946,6 +447,9 @@ impl State {
match btn_state {
ElementState::Pressed => {
+ if let Some(old) = self.focused_widget.take() {
+ self.widgets[old].unfocus();
+ }
for i in (0..self.widgets.len()).rev() {
if self.widgets[i].hit_test(self.cursor_x, self.cursor_y) {
if self.widgets[i].mouse_input(*button, *btn_state, self.cursor_x, self.cursor_y) {
@@ -955,6 +459,8 @@ impl State {
self.widgets[i].drag_begin(self.cursor_x, self.cursor_y);
self.drag_widget = Some(i);
}
+ self.widgets[i].focus();
+ self.focused_widget = Some(i);
break;
}
}
@@ -970,20 +476,40 @@ impl State {
changed = true;
}
}
+ let mut clicked = false;
for w in &mut self.widgets {
if w.take_click() {
- self.click_count += 1;
+ clicked = true;
}
}
+ if clicked {
+ self.click_count += 1;
+ self.update_status_text(&format!("Clicks: {}", self.click_count));
+ }
}
}
changed
}
+ WindowEvent::KeyboardInput { event, .. } => {
+ if let Some(idx) = self.focused_widget {
+ let val = self.widgets[idx].value();
+ let changed = self.widgets[idx].keyboard_input(event);
+ if self.widgets[idx].value() != val {
+ changed || true
+ } else {
+ changed
+ }
+ } else {
+ false
+ }
+ }
_ => false,
}
}
fn render(&mut self) {
+ self.prepare_text();
+
let output = match self.surface.get_current_texture() {
Ok(t) => t,
Err(wgpu::SurfaceError::Lost | wgpu::SurfaceError::Outdated) => {
@@ -1021,9 +547,13 @@ impl State {
occlusion_query_set: None,
});
+ // Draw colored quads
pass.set_pipeline(&self.render_pipeline);
pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
pass.draw(0..self.vertex_count, 0..1);
+
+ // Draw text
+ self.text_renderer.render(&self.text_atlas, &self.text_viewport, &mut pass).unwrap();
}
self.queue.submit(std::iter::once(encoder.finish()));
@@ -1083,6 +613,19 @@ impl ApplicationHandler for App {
}
true
}
+ WindowEvent::ScaleFactorChanged { scale_factor, mut inner_size_writer } => {
+ if let Some(state) = &mut self.state {
+ let new_physical = winit::dpi::PhysicalSize::new(
+ (state.width as f64 * scale_factor) as u32,
+ (state.height as f64 * scale_factor) as u32,
+ );
+ let _ = inner_size_writer.request_inner_size(new_physical);
+ state.scale = scale_factor;
+ state.resize(new_physical);
+ state.window.request_redraw();
+ }
+ true
+ }
_ => {
if let Some(state) = &mut self.state {
let prev = state.click_count;
@@ -1110,6 +653,26 @@ impl ApplicationHandler for App {
}
}
+fn demo_positions(sw: f32, sh: f32) -> Vec<(f32, f32, f32, f32)> {
+ vec![
+ (0.0, 0.0, sw, 40.0), // 0 header
+ (0.0, 40.0, 60.0, sh - 68.0), // 1 sidebar
+ (60.0, 40.0, sw - 60.0, sh - 68.0), // 2 content_bg
+ (70.0, 50.0, 140.0, 40.0), // 3 btn_a
+ (220.0, 50.0, 140.0, 40.0), // 4 btn_b
+ (370.0, 50.0, 140.0, 40.0), // 5 btn_c
+ (70.0, 100.0, 400.0, 250.0), // 6 panel
+ (70.0, 360.0, 140.0, 40.0), // 7 click_me
+ (220.0, 360.0, 140.0, 40.0), // 8 reset
+ (70.0, 410.0, 24.0, 24.0), // 9 checkbox
+ (104.0, 410.0, 48.0, 24.0), // 10 toggle
+ (162.0, 410.0, 160.0, 24.0), // 11 progress_bar
+ (70.0, 444.0, 300.0, 32.0), // 12 slider
+ (70.0, 486.0, 120.0, 32.0), // 13 spinbox
+ (0.0, sh - 28.0, sw, 28.0), // 14 status_bar
+ ]
+}
+
fn main() {
let event_loop = EventLoop::new().unwrap();
event_loop.set_control_flow(ControlFlow::Poll);
diff --git a/src/widget.rs b/src/widget.rs
new file mode 100644
index 0000000..45af781
--- /dev/null
+++ b/src/widget.rs
@@ -0,0 +1,675 @@
+use winit::event::{ElementState, KeyEvent, MouseButton};
+use winit::keyboard::{Key, NamedKey};
+
+use crate::colors;
+
+pub struct TextLabel {
+ pub text: String,
+ pub x: f32,
+ pub y: f32,
+ pub font_size: f32,
+ pub color: [u8; 3],
+}
+
+pub trait Widget {
+ fn rect(&self) -> (f32, f32, f32, f32);
+ fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32);
+
+ fn hit_test(&self, px: f32, py: f32) -> bool {
+ let (x, y, w, h) = self.rect();
+ px >= x && px <= x + w && py >= y && py <= y + h
+ }
+
+ fn cursor_moved(&mut self, _px: f32, _py: f32) -> bool { false }
+ fn mouse_input(&mut self, _button: MouseButton, _state: ElementState, _px: f32, _py: f32) -> bool { false }
+
+ fn color(&self) -> [f32; 4];
+
+ fn is_dragging(&self) -> bool { false }
+ fn drag_update(&mut self, _px: f32, _py: f32) -> bool { false }
+ fn drag_begin(&mut self, _px: f32, _py: f32) {}
+ fn drag_end(&mut self) {}
+ fn take_click(&mut self) -> bool { false }
+ fn draggable(&self) -> bool { false }
+
+ fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> { Vec::new() }
+ fn text_labels(&self) -> Vec<TextLabel> { Vec::new() }
+ fn value(&self) -> i32 { 0 }
+
+ fn focus(&mut self) {}
+ fn unfocus(&mut self) {}
+ fn keyboard_input(&mut self, _event: &KeyEvent) -> bool { false }
+}
+
+pub struct Header {
+ x: f32, y: f32, w: f32, h: f32,
+}
+
+impl Header {
+ pub fn new() -> Self { Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0 } }
+}
+
+impl Widget for Header {
+ fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
+ fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
+ fn color(&self) -> [f32; 4] { colors::HEADER_BG }
+}
+
+pub struct ContentBg {
+ x: f32, y: f32, w: f32, h: f32,
+}
+
+impl ContentBg {
+ pub fn new() -> Self { Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0 } }
+}
+
+impl Widget for ContentBg {
+ fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
+ fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
+ fn color(&self) -> [f32; 4] { colors::CONTENT_BG }
+}
+
+pub enum ButtonKind {
+ Primary,
+ Reset,
+}
+
+pub struct Button {
+ x: f32, y: f32, w: f32, h: f32,
+ hovering: bool,
+ pressed: bool,
+ just_clicked: bool,
+ kind: ButtonKind,
+}
+
+impl Button {
+ pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
+ Self { x, y, w, h, hovering: false, pressed: false, just_clicked: false, kind: ButtonKind::Primary }
+ }
+
+ pub fn new_reset(x: f32, y: f32, w: f32, h: f32) -> Self {
+ Self { x, y, w, h, hovering: false, pressed: false, just_clicked: false, kind: ButtonKind::Reset }
+ }
+}
+
+impl Widget for Button {
+ fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
+ fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
+ fn color(&self) -> [f32; 4] {
+ match self.kind {
+ ButtonKind::Primary => {
+ if self.pressed { colors::BUTTON_PRESS }
+ else if self.hovering { colors::BUTTON_HOVER }
+ else { colors::BUTTON_IDLE }
+ }
+ ButtonKind::Reset => {
+ if self.pressed { colors::RESET_BTN_PRESS }
+ else if self.hovering { colors::RESET_BTN_HOVER }
+ else { colors::RESET_BTN_IDLE }
+ }
+ }
+ }
+
+ fn cursor_moved(&mut self, px: f32, py: f32) -> bool {
+ let was = self.hovering;
+ self.hovering = self.hit_test(px, py);
+ was != self.hovering
+ }
+
+ fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
+ if button != MouseButton::Left { return false; }
+ match state {
+ ElementState::Pressed => {
+ if self.hit_test(px, py) {
+ self.pressed = true;
+ return true;
+ }
+ }
+ ElementState::Released => {
+ if self.pressed && self.hit_test(px, py) {
+ self.just_clicked = true;
+ }
+ let was = self.pressed;
+ self.pressed = false;
+ return was;
+ }
+ }
+ false
+ }
+
+ fn take_click(&mut self) -> bool {
+ if self.just_clicked { self.just_clicked = false; true } else { false }
+ }
+}
+
+pub enum PageButton {
+ Active,
+ Inactive,
+}
+
+pub struct Sidebar {
+ x: f32, y: f32, w: f32, h: f32,
+}
+
+impl Sidebar {
+ pub fn new(w: f32) -> Self { Self { x: 0.0, y: 0.0, w, h: 0.0 } }
+}
+
+impl Widget for Sidebar {
+ fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
+ fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
+ fn color(&self) -> [f32; 4] { colors::SIDEBAR_BG }
+}
+
+pub struct Panel {
+ x: f32, y: f32, w: f32, h: f32,
+ dragging: bool,
+ drag_ox: f32, drag_oy: f32,
+ drag_start_x: f32, drag_start_y: f32,
+}
+
+impl Panel {
+ pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
+ Self { x, y, w, h, dragging: false, drag_ox: 0.0, drag_oy: 0.0, drag_start_x: 0.0, drag_start_y: 0.0 }
+ }
+}
+
+impl Widget for Panel {
+ fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
+ fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
+ fn color(&self) -> [f32; 4] { if self.dragging { colors::PANEL_DRAG } else { colors::PANEL_IDLE } }
+
+ fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
+ if button != MouseButton::Left { return false; }
+ match state {
+ ElementState::Pressed => {
+ if self.hit_test(px, py) {
+ self.drag_begin(px, py);
+ return true;
+ }
+ }
+ ElementState::Released => {
+ if self.dragging { self.drag_end(); return true; }
+ }
+ }
+ false
+ }
+
+ fn is_dragging(&self) -> bool { self.dragging }
+ fn draggable(&self) -> bool { true }
+
+ fn drag_update(&mut self, px: f32, py: f32) -> bool {
+ let nx = px - self.drag_ox;
+ let ny = py - self.drag_oy;
+ if (nx - self.x).abs() > 0.01 || (ny - self.y).abs() > 0.01 {
+ self.x = nx;
+ self.y = ny;
+ return true;
+ }
+ false
+ }
+
+ fn drag_begin(&mut self, px: f32, py: f32) {
+ self.dragging = true;
+ self.drag_ox = px - self.x;
+ self.drag_oy = py - self.y;
+ self.drag_start_x = self.x;
+ self.drag_start_y = self.y;
+ }
+
+ fn drag_end(&mut self) { self.dragging = false; }
+}
+
+pub struct Checkbox {
+ x: f32, y: f32, w: f32, h: f32,
+ hovering: bool,
+ checked: bool,
+ just_clicked: bool,
+}
+
+impl Checkbox {
+ pub fn new() -> Self {
+ Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, hovering: false, checked: false, just_clicked: false }
+ }
+}
+
+impl Widget for Checkbox {
+ fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
+ fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
+ fn color(&self) -> [f32; 4] { colors::CHECKBOX_BG }
+
+ fn cursor_moved(&mut self, px: f32, py: f32) -> bool {
+ let was = self.hovering;
+ self.hovering = self.hit_test(px, py);
+ was != self.hovering
+ }
+
+ fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
+ if button != MouseButton::Left { return false; }
+ match state {
+ ElementState::Released => {
+ if self.hit_test(px, py) {
+ self.checked = !self.checked;
+ self.just_clicked = true;
+ return true;
+ }
+ }
+ _ => {}
+ }
+ false
+ }
+
+ fn take_click(&mut self) -> bool {
+ if self.just_clicked { self.just_clicked = false; true } else { false }
+ }
+}
+
+pub struct Toggle {
+ x: f32, y: f32, w: f32, h: f32,
+ hovering: bool,
+ toggled: bool,
+ just_toggled: bool,
+}
+
+impl Toggle {
+ pub fn new() -> Self {
+ Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, hovering: false, toggled: false, just_toggled: false }
+ }
+}
+
+impl Widget for Toggle {
+ fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
+ fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
+ fn color(&self) -> [f32; 4] { if self.toggled { colors::TOGGLE_ON } else { colors::TOGGLE_OFF } }
+
+ fn cursor_moved(&mut self, px: f32, py: f32) -> bool {
+ let was = self.hovering;
+ self.hovering = self.hit_test(px, py);
+ was != self.hovering
+ }
+
+ fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
+ if button != MouseButton::Left { return false; }
+ match state {
+ ElementState::Released => {
+ if self.hit_test(px, py) {
+ self.toggled = !self.toggled;
+ self.just_toggled = true;
+ return true;
+ }
+ }
+ _ => {}
+ }
+ false
+ }
+}
+
+pub struct Slider {
+ x: f32, y: f32, w: f32, h: f32,
+ dragging: bool,
+ value: f32,
+ drag_offset: f32,
+}
+
+impl Slider {
+ pub fn new() -> Self {
+ Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, dragging: false, value: 0.5, drag_offset: 0.0 }
+ }
+}
+
+impl Widget for Slider {
+ fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
+ fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
+ fn color(&self) -> [f32; 4] { colors::SLIDER_TRACK }
+
+ fn draggable(&self) -> bool { true }
+ fn is_dragging(&self) -> bool { self.dragging }
+
+ fn drag_update(&mut self, px: f32, _py: f32) -> bool {
+ let (sx, _, sw, _) = self.rect();
+ let thumb_size = self.h * 0.9;
+ let range = sw - thumb_size;
+ let raw = (px - self.drag_offset - sx) / range;
+ let new_val = raw.clamp(0.0, 1.0);
+ if (new_val - self.value).abs() > 0.001 {
+ self.value = new_val;
+ return true;
+ }
+ false
+ }
+
+ fn drag_begin(&mut self, px: f32, _py: f32) {
+ self.dragging = true;
+ let thumb_size = self.h * 0.9;
+ let thumb_x = self.x + self.value * (self.w - thumb_size);
+ self.drag_offset = px - thumb_x;
+ }
+
+ fn drag_end(&mut self) { self.dragging = false; }
+}
+
+pub struct ProgressBar {
+ x: f32, y: f32, w: f32, h: f32,
+ value: f32,
+}
+
+impl ProgressBar {
+ pub fn new(value: f32) -> Self {
+ Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, value }
+ }
+}
+
+impl Widget for ProgressBar {
+ fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
+ fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
+ fn color(&self) -> [f32; 4] { colors::PROGRESS_BG }
+}
+
+pub struct StatusBar {
+ x: f32, y: f32, w: f32, h: f32,
+}
+
+impl StatusBar {
+ pub fn new() -> Self { Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0 } }
+}
+
+impl Widget for StatusBar {
+ fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
+ fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
+ fn color(&self) -> [f32; 4] { colors::STATUS_BG }
+}
+
+#[derive(Debug, Clone)]
+pub struct Spinbox {
+ x: f32, y: f32, w: f32, h: f32,
+ pub value: i32,
+ min: i32,
+ max: i32,
+ step: i32,
+ editing: bool,
+ edit_buffer: String,
+ hover_dec: bool,
+ hover_inc: bool,
+}
+
+impl Spinbox {
+ pub fn new(value: i32, min: i32, max: i32, step: i32) -> Self {
+ Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, value, min, max, step, editing: false, edit_buffer: String::new(), hover_dec: false, hover_inc: false }
+ }
+}
+
+impl Widget for Spinbox {
+ fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
+ fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
+ fn color(&self) -> [f32; 4] { colors::SPINBOX_BG }
+ fn value(&self) -> i32 { self.value }
+
+ fn cursor_moved(&mut self, px: f32, _py: f32) -> bool {
+ if !self.hit_test(px, _py) {
+ let changed = self.hover_dec || self.hover_inc;
+ self.hover_dec = false;
+ self.hover_inc = false;
+ return changed;
+ }
+ let split = self.x + self.w * 0.55;
+ let hd = px >= split && px < split + self.w * 0.225;
+ let hi = px >= split + self.w * 0.225;
+ let changed = hd != self.hover_dec || hi != self.hover_inc;
+ self.hover_dec = hd;
+ self.hover_inc = hi;
+ changed
+ }
+
+ fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
+ if button != MouseButton::Left { return false; }
+ if !self.hit_test(px, py) { return false; }
+ match state {
+ ElementState::Pressed => {
+ let split = self.x + self.w * 0.55;
+ if px >= split && px < split + self.w * 0.225 {
+ self.value = (self.value + self.step).min(self.max);
+ true
+ } else if px >= split + self.w * 0.225 {
+ self.value = (self.value - self.step).max(self.min);
+ true
+ } else if px < split {
+ self.editing = true;
+ self.edit_buffer = self.value.to_string();
+ true
+ } else {
+ false
+ }
+ }
+ ElementState::Released => {
+ false
+ }
+ }
+ }
+
+ fn focus(&mut self) {
+ self.editing = true;
+ self.edit_buffer = self.value.to_string();
+ }
+
+ fn unfocus(&mut self) {
+ if self.editing {
+ self.editing = false;
+ if let Ok(val) = self.edit_buffer.parse::<i32>() {
+ self.value = val.clamp(self.min, self.max);
+ }
+ }
+ }
+
+ fn keyboard_input(&mut self, event: &KeyEvent) -> bool {
+ if !self.editing { return false; }
+ if event.state != ElementState::Pressed { return false; }
+ match &event.logical_key {
+ Key::Named(NamedKey::Backspace) => {
+ self.edit_buffer.pop();
+ true
+ }
+ Key::Named(NamedKey::Enter) => {
+ if let Ok(val) = self.edit_buffer.parse::<i32>() {
+ self.value = val.clamp(self.min, self.max);
+ }
+ self.editing = false;
+ true
+ }
+ Key::Named(NamedKey::Escape) => {
+ self.editing = false;
+ true
+ }
+ _ => {
+ if let Some(text) = &event.text {
+ if !event.repeat {
+ for ch in text.chars() {
+ match ch {
+ '-' if self.edit_buffer.is_empty() => self.edit_buffer.push('-'),
+ '0'..='9' => self.edit_buffer.push(ch),
+ _ => {}
+ }
+ }
+ }
+ }
+ true
+ }
+ }
+ }
+
+ fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+ let split = self.x + self.w * 0.55;
+ let btn_w = self.w * 0.225;
+ let inc_col = if self.hover_inc { colors::SPINBOX_BUTTON_HOVER } else { colors::SPINBOX_BUTTON };
+ let dec_col = if self.hover_dec { colors::SPINBOX_BUTTON_HOVER } else { colors::SPINBOX_BUTTON };
+ vec![
+ (self.x, self.y, self.w * 0.55, self.h, colors::SPINBOX_DISPLAY),
+ (split, self.y, btn_w, self.h, inc_col),
+ (split + btn_w, self.y, btn_w, self.h, dec_col),
+ ]
+ }
+
+ fn text_labels(&self) -> Vec<TextLabel> {
+ let value_text = if self.editing { self.edit_buffer.clone() } else { self.value.to_string() };
+ vec![
+ TextLabel {
+ text: value_text,
+ x: self.x + 4.0,
+ y: self.y + 2.0,
+ font_size: 14.0,
+ color: [0xcc, 0xcc, 0xd4],
+ },
+ TextLabel {
+ text: "+".to_string(),
+ x: self.x + self.w * 0.6625 - 4.0,
+ y: self.y + 2.0,
+ font_size: 12.0,
+ color: [0xcc, 0xcc, 0xd4],
+ },
+ TextLabel {
+ text: "-".to_string(),
+ x: self.x + self.w * 0.8875 - 4.0,
+ y: self.y + 2.0,
+ font_size: 12.0,
+ color: [0xcc, 0xcc, 0xd4],
+ },
+ ]
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct ColorPicker {
+ x: f32, y: f32, w: f32, h: f32,
+ pub color: [u8; 3],
+ just_clicked: bool,
+ editing: bool,
+ edit_buffer: String,
+ label: Option<String>,
+}
+
+impl ColorPicker {
+ pub fn new(color: [u8; 3]) -> Self {
+ Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, color, just_clicked: false, editing: false, edit_buffer: String::new(), label: None }
+ }
+
+ pub fn with_label(mut self, label: &str) -> Self {
+ self.label = Some(label.to_string());
+ self
+ }
+}
+
+impl Widget for ColorPicker {
+ fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
+ fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
+
+ fn color(&self) -> [f32; 4] {
+ [self.color[0] as f32 / 255.0, self.color[1] as f32 / 255.0, self.color[2] as f32 / 255.0, 1.0]
+ }
+
+ fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32) -> bool {
+ if button != MouseButton::Left { return false; }
+ if state != ElementState::Pressed { return false; }
+ if !self.hit_test(px, py) { return false; }
+ if px >= self.x + self.w * 0.65 {
+ self.just_clicked = true;
+ return true;
+ }
+ self.focus();
+ true
+ }
+
+ fn take_click(&mut self) -> bool {
+ if self.just_clicked { self.just_clicked = false; true } else { false }
+ }
+
+ fn focus(&mut self) {
+ self.editing = true;
+ self.edit_buffer = format!("#{:02x}{:02x}{:02x}", self.color[0], self.color[1], self.color[2]);
+ }
+
+ fn unfocus(&mut self) {
+ if self.editing {
+ self.editing = false;
+ if let Some(c) = parse_hex(&self.edit_buffer) {
+ self.color = c;
+ }
+ }
+ }
+
+ fn keyboard_input(&mut self, event: &KeyEvent) -> bool {
+ if !self.editing { return false; }
+ if event.state != ElementState::Pressed { return false; }
+ match &event.logical_key {
+ Key::Named(NamedKey::Backspace) => {
+ self.edit_buffer.pop();
+ true
+ }
+ Key::Named(NamedKey::Enter) => {
+ if let Some(c) = parse_hex(&self.edit_buffer) {
+ self.color = c;
+ }
+ self.editing = false;
+ true
+ }
+ Key::Named(NamedKey::Escape) => {
+ self.editing = false;
+ true
+ }
+ _ => {
+ if let Some(text) = &event.text {
+ if !event.repeat {
+ for ch in text.chars() {
+ match ch {
+ '#' if self.edit_buffer.is_empty() => self.edit_buffer.push('#'),
+ '0'..='9' | 'a'..='f' | 'A'..='F' => {
+ if self.edit_buffer.len() < 7 { self.edit_buffer.push(ch.to_ascii_lowercase()); }
+ }
+ _ => {}
+ }
+ }
+ }
+ }
+ true
+ }
+ }
+ }
+
+ fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+ let pick_x = self.x + self.w * 0.65;
+ let pick_w = self.w * 0.35;
+ let (r, g, b, _) = (self.color[0] as f32 / 255.0, self.color[1] as f32 / 255.0, self.color[2] as f32 / 255.0, 1.0);
+ vec![
+ (pick_x, self.y, pick_w, self.h, [r, g, b, 1.0]),
+ ]
+ }
+
+ fn text_labels(&self) -> Vec<TextLabel> {
+ let mut labels = Vec::new();
+ if let Some(ref label) = self.label {
+ labels.push(TextLabel {
+ text: label.clone(),
+ x: self.x + 4.0,
+ y: self.y - 16.0,
+ font_size: 12.0,
+ color: [0x83, 0x83, 0x8a],
+ });
+ }
+ let hex = if self.editing { self.edit_buffer.clone() } else { format!("#{:02x}{:02x}{:02x}", self.color[0], self.color[1], self.color[2]) };
+ labels.push(TextLabel {
+ text: hex,
+ x: self.x + 4.0,
+ y: self.y + 3.0,
+ font_size: 12.0,
+ color: [0xcc, 0xcc, 0xd4],
+ });
+ labels
+ }
+}
+
+fn parse_hex(s: &str) -> Option<[u8; 3]> {
+ let s = s.trim_start_matches('#');
+ if s.len() != 6 { return None; }
+ let r = u8::from_str_radix(&s[0..2], 16).ok()?;
+ let g = u8::from_str_radix(&s[2..4], 16).ok()?;
+ let b = u8::from_str_radix(&s[4..6], 16).ok()?;
+ Some([r, g, b])
+}