new workspace
137
Cargo.lock
generated
@@ -215,15 +215,6 @@ dependencies = [
|
|||||||
"num-traits",
|
"num-traits",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "approx"
|
|
||||||
version = "0.5.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6"
|
|
||||||
dependencies = [
|
|
||||||
"num-traits",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aquamarine"
|
name = "aquamarine"
|
||||||
version = "0.1.12"
|
version = "0.1.12"
|
||||||
@@ -879,7 +870,7 @@ version = "0.18.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1a98d30140e3296250832bbaaff83b27dcd6fa3cc70fb6f1f3e5c9c0023b5317"
|
checksum = "1a98d30140e3296250832bbaaff83b27dcd6fa3cc70fb6f1f3e5c9c0023b5317"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"approx 0.4.0",
|
"approx",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1039,12 +1030,6 @@ dependencies = [
|
|||||||
"yaml-rust2",
|
"yaml-rust2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "const-cstr"
|
|
||||||
version = "0.3.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ed3d0b5ff30645a68f35ece8cea4556ca14ef8a1651455f789a099a0513532a6"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "const-random"
|
name = "const-random"
|
||||||
version = "0.1.18"
|
version = "0.1.18"
|
||||||
@@ -1799,18 +1784,6 @@ dependencies = [
|
|||||||
"roxmltree 0.20.0",
|
"roxmltree 0.20.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "fontconfig-rs"
|
|
||||||
version = "0.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "cb4baadad5111c6820e97fc8bde5077258e6f272b5b38538db4b42e1812f29f3"
|
|
||||||
dependencies = [
|
|
||||||
"const-cstr",
|
|
||||||
"once_cell",
|
|
||||||
"thiserror 1.0.69",
|
|
||||||
"yeslogic-fontconfig-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fontdb"
|
name = "fontdb"
|
||||||
version = "0.16.2"
|
version = "0.16.2"
|
||||||
@@ -1861,28 +1834,6 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "freetype-rs"
|
|
||||||
version = "0.36.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5442dee36ca09604133580dc0553780e867936bb3cbef3275859e889026d2b17"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags 2.9.1",
|
|
||||||
"freetype-sys",
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "freetype-sys"
|
|
||||||
version = "0.20.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0e7edc5b9669349acfda99533e9e0bcf26a51862ab43b08ee7745c55d28eb134"
|
|
||||||
dependencies = [
|
|
||||||
"cc",
|
|
||||||
"libc",
|
|
||||||
"pkg-config",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures"
|
name = "futures"
|
||||||
version = "0.3.31"
|
version = "0.3.31"
|
||||||
@@ -2061,7 +2012,6 @@ version = "0.30.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "50a99dbe56b72736564cfa4b85bf9a33079f16ae8b74983ab06af3b1a3696b11"
|
checksum = "50a99dbe56b72736564cfa4b85bf9a33079f16ae8b74983ab06af3b1a3696b11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"approx 0.5.1",
|
|
||||||
"mint",
|
"mint",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
@@ -4194,6 +4144,40 @@ version = "0.20.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
|
checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rust-embed"
|
||||||
|
version = "8.7.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "025908b8682a26ba8d12f6f2d66b987584a4a87bc024abc5bbc12553a8cd178a"
|
||||||
|
dependencies = [
|
||||||
|
"rust-embed-impl",
|
||||||
|
"rust-embed-utils",
|
||||||
|
"walkdir",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rust-embed-impl"
|
||||||
|
version = "8.7.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6065f1a4392b71819ec1ea1df1120673418bf386f50de1d6f54204d836d4349c"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"rust-embed-utils",
|
||||||
|
"syn 2.0.103",
|
||||||
|
"walkdir",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rust-embed-utils"
|
||||||
|
version = "8.7.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f6cc0c81648b20b70c491ff8cce00c1c3b223bb8ed2b5d41f0e54c6c4c0a3594"
|
||||||
|
dependencies = [
|
||||||
|
"sha2",
|
||||||
|
"walkdir",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rust-ini"
|
name = "rust-ini"
|
||||||
version = "0.21.1"
|
version = "0.21.1"
|
||||||
@@ -5089,6 +5073,21 @@ dependencies = [
|
|||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uidev-vk"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"glam",
|
||||||
|
"log",
|
||||||
|
"rust-embed",
|
||||||
|
"tracing-subscriber",
|
||||||
|
"vulkano",
|
||||||
|
"vulkano-shaders",
|
||||||
|
"wgui",
|
||||||
|
"winit",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-bidi"
|
name = "unicode-bidi"
|
||||||
version = "0.3.18"
|
version = "0.3.18"
|
||||||
@@ -5593,7 +5592,6 @@ checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "wgui"
|
name = "wgui"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/wlx-team/wgui.git?branch=wip#455fabb31931b888f9b976c6a914c6f4f73b7beb"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"cosmic-text",
|
"cosmic-text",
|
||||||
@@ -6140,6 +6138,22 @@ dependencies = [
|
|||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wlx-capture"
|
||||||
|
version = "0.5.3"
|
||||||
|
dependencies = [
|
||||||
|
"ashpd",
|
||||||
|
"drm-fourcc",
|
||||||
|
"idmap",
|
||||||
|
"libc",
|
||||||
|
"log",
|
||||||
|
"pipewire",
|
||||||
|
"rxscreen",
|
||||||
|
"smithay-client-toolkit",
|
||||||
|
"wayland-client",
|
||||||
|
"wayland-protocols",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wlx-capture"
|
name = "wlx-capture"
|
||||||
version = "0.5.3"
|
version = "0.5.3"
|
||||||
@@ -6170,8 +6184,6 @@ dependencies = [
|
|||||||
"config",
|
"config",
|
||||||
"ctrlc",
|
"ctrlc",
|
||||||
"dbus",
|
"dbus",
|
||||||
"fontconfig-rs",
|
|
||||||
"freetype-rs",
|
|
||||||
"futures",
|
"futures",
|
||||||
"glam",
|
"glam",
|
||||||
"idmap",
|
"idmap",
|
||||||
@@ -6192,6 +6204,7 @@ dependencies = [
|
|||||||
"regex",
|
"regex",
|
||||||
"rodio",
|
"rodio",
|
||||||
"rosc",
|
"rosc",
|
||||||
|
"rust-embed",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_json5",
|
"serde_json5",
|
||||||
@@ -6204,12 +6217,14 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"uuid",
|
"uuid",
|
||||||
|
"vulkano",
|
||||||
|
"vulkano-shaders",
|
||||||
"wayland-client",
|
"wayland-client",
|
||||||
"wayland-egl",
|
"wayland-egl",
|
||||||
"wayvr_ipc",
|
"wayvr_ipc",
|
||||||
"wgui",
|
"wgui",
|
||||||
"winit",
|
"winit",
|
||||||
"wlx-capture",
|
"wlx-capture 0.5.3 (git+https://github.com/galister/wlx-capture?tag=v0.5.3)",
|
||||||
"xcb",
|
"xcb",
|
||||||
"xdg 3.0.0",
|
"xdg 3.0.0",
|
||||||
"xkbcommon 0.8.0",
|
"xkbcommon 0.8.0",
|
||||||
@@ -6372,18 +6387,6 @@ version = "0.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e01738255b5a16e78bbb83e7fbba0a1e7dd506905cfc53f4622d89015a03fbb5"
|
checksum = "e01738255b5a16e78bbb83e7fbba0a1e7dd506905cfc53f4622d89015a03fbb5"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "yeslogic-fontconfig-sys"
|
|
||||||
version = "3.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f2bbd69036d397ebbff671b1b8e4d918610c181c5a16073b96f984a38d08c386"
|
|
||||||
dependencies = [
|
|
||||||
"const-cstr",
|
|
||||||
"dlib",
|
|
||||||
"once_cell",
|
|
||||||
"pkg-config",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yoke"
|
name = "yoke"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
|
|||||||
126
Cargo.toml
@@ -1,120 +1,12 @@
|
|||||||
[profile.release-with-debug]
|
[workspace]
|
||||||
inherits = "release"
|
members = ["wgui", "wgui/uidev-vk", "wlx-overlay-s", "wlx-capture"]
|
||||||
debug = true
|
|
||||||
|
|
||||||
[package]
|
[workspace.dependencies]
|
||||||
name = "wlx-overlay-s"
|
anyhow = "1.0.98"
|
||||||
version = "25.4.2"
|
glam = "0.30.3"
|
||||||
edition = "2021"
|
log = "0.4.27"
|
||||||
license = "GPL-3.0-only"
|
vulkano = { version = "0.35.1", default-features = false, features = [
|
||||||
authors = ["galister"]
|
"macros",
|
||||||
description = "Access your Wayland/X11 desktop from Monado/WiVRn/SteamVR. Now with Vulkan!"
|
|
||||||
repository = "https://github.com/galister/wlx-overlay-s"
|
|
||||||
keywords = ["linux", "openvr", "openxr", "x11", "wayland", "openvr-overlay", "openxr-overlay"]
|
|
||||||
categories = ["games"]
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
anyhow = { workspace = true }
|
|
||||||
ash = "^0.38.0" # must match vulkano
|
|
||||||
chrono = "0.4.38"
|
|
||||||
chrono-tz = "0.10.0"
|
|
||||||
clap = { version = "4.5.6", features = ["derive"] }
|
|
||||||
config = "0.15.11"
|
|
||||||
ctrlc = { version = "3.4.4", features = ["termination"] }
|
|
||||||
dbus = { version = "0.9.7" }
|
|
||||||
futures = "0.3.30"
|
|
||||||
glam = { workspace = true, features = ["mint", "serde"] }
|
|
||||||
idmap = { version = "0.2.21", features = ["serde"] }
|
|
||||||
idmap-derive = "0.1.2"
|
|
||||||
input-linux = "0.7.0"
|
|
||||||
json = { version = "0.12.4", optional = true }
|
|
||||||
json5 = "0.4.1"
|
|
||||||
libc = "0.2.155"
|
|
||||||
log = { workspace = true }
|
|
||||||
openxr = { git = "https://github.com/Ralith/openxrs", rev = "d0afdd3365bc1e14de28f6a3a21f457e788a702e", features = [
|
|
||||||
"linked",
|
|
||||||
"mint",
|
|
||||||
], optional = true }
|
|
||||||
ovr_overlay = { features = [
|
|
||||||
"ovr_input",
|
|
||||||
"ovr_system",
|
|
||||||
], git = "https://github.com/galister/ovr_overlay_oyasumi", optional = true }
|
|
||||||
regex = "1.11.1"
|
|
||||||
rodio = { version = "0.20.1", default-features = false, features = [
|
|
||||||
"wav",
|
|
||||||
"hound",
|
|
||||||
] }
|
] }
|
||||||
rosc = { version = "0.11.4", optional = true }
|
vulkano-shaders = "0.35.0"
|
||||||
serde = { version = "1.0.203", features = ["derive", "rc"] }
|
|
||||||
serde_json = "1.0.117"
|
|
||||||
serde_yaml = "0.9.34"
|
|
||||||
smallvec = "1.13.2"
|
|
||||||
strum = { version = "0.27.1", features = ["derive"] }
|
|
||||||
sysinfo = { version = "0.35" }
|
|
||||||
thiserror = "2.0"
|
|
||||||
wlx-capture = { git = "https://github.com/galister/wlx-capture", tag = "v0.5.3", default-features = false }
|
|
||||||
libmonado = { version = "1.3.2", optional = true }
|
|
||||||
winit = { version = "0.30", optional = true }
|
|
||||||
xdg = "3.0"
|
|
||||||
log-panics = { version = "2.1.0", features = ["with-backtrace"] }
|
|
||||||
serde_json5 = "0.2.1"
|
|
||||||
xkbcommon = { version = "0.8.0" }
|
|
||||||
xcb = { version = "1.4.0", optional = true, features = [
|
|
||||||
"as-raw-xcb-connection",
|
|
||||||
] }
|
|
||||||
image_dds = { version = "0.7.2", default-features = false, features = [
|
|
||||||
"ddsfile",
|
|
||||||
] }
|
|
||||||
mint = "0.5.9"
|
|
||||||
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
|
|
||||||
tracing = "0.1.41"
|
|
||||||
vulkano = { workspace = true }
|
|
||||||
vulkano-shaders = { workspace = true }
|
|
||||||
wgui = { path = "../wgui" }
|
|
||||||
|
|
||||||
################################
|
|
||||||
#WayVR-only deps
|
|
||||||
################################
|
|
||||||
khronos-egl = { version = "6.0.0", features = ["static"], optional = true }
|
|
||||||
smithay = { version = "0.5.1", default-features = false, features = [
|
|
||||||
"renderer_gl",
|
|
||||||
"backend_egl",
|
|
||||||
"backend_drm",
|
|
||||||
"xwayland",
|
|
||||||
"wayland_frontend",
|
|
||||||
], optional = true }
|
|
||||||
uuid = { version = "1.10.0", features = ["v4", "fast-rng"], optional = true }
|
|
||||||
wayland-client = { version = "0.31.6", optional = true }
|
|
||||||
wayland-egl = { version = "0.32.4", optional = true }
|
|
||||||
interprocess = { version = "2.2.2", optional = true }
|
|
||||||
bytes = { version = "1.9.0", optional = true }
|
|
||||||
wayvr_ipc = { git = "https://github.com/olekolek1000/wayvr-ipc.git", rev = "a72587d23f3bb8624d9aeb1f13c0a21e65350f51", default-features = false, optional = true }
|
|
||||||
rust-embed = "8.7.2"
|
|
||||||
################################
|
|
||||||
|
|
||||||
[build-dependencies]
|
|
||||||
regex = { version = "1.11.1" }
|
|
||||||
|
|
||||||
[features]
|
|
||||||
default = ["openvr", "openxr", "osc", "x11", "wayland", "wayvr"]
|
|
||||||
openvr = ["dep:ovr_overlay", "dep:json"]
|
|
||||||
openxr = ["dep:openxr", "dep:libmonado"]
|
|
||||||
osc = ["dep:rosc"]
|
|
||||||
x11 = ["dep:xcb", "wlx-capture/xshm", "xkbcommon/x11"]
|
|
||||||
wayland = ["pipewire", "wlx-capture/wlr", "xkbcommon/wayland"]
|
|
||||||
pipewire = ["wlx-capture/pipewire"]
|
|
||||||
uidev = ["dep:winit"]
|
|
||||||
xcb = ["dep:xcb"]
|
|
||||||
wayvr = [
|
|
||||||
"dep:khronos-egl",
|
|
||||||
"dep:smithay",
|
|
||||||
"dep:uuid",
|
|
||||||
"dep:wayland-client",
|
|
||||||
"dep:wayland-egl",
|
|
||||||
"dep:interprocess",
|
|
||||||
"dep:bytes",
|
|
||||||
"dep:wayvr_ipc",
|
|
||||||
]
|
|
||||||
as-raw-xcb-connection = []
|
|
||||||
|
|||||||
8
wgui/.editorconfig
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*.rs]
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = 2
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
insert_final_newline = false
|
||||||
2
wgui/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
target
|
||||||
|
.vscode
|
||||||
1966
wgui/Cargo.lock
generated
Normal file
34
wgui/Cargo.toml
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[package]
|
||||||
|
name = "wgui"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = { workspace = true }
|
||||||
|
cosmic-text = "0.14.2"
|
||||||
|
etagere = "0.2.15"
|
||||||
|
glam = { workspace = true }
|
||||||
|
image = { version = "0.25.6", default-features = false, features = [
|
||||||
|
"gif",
|
||||||
|
"jpeg",
|
||||||
|
"png",
|
||||||
|
"rayon",
|
||||||
|
"webp",
|
||||||
|
] }
|
||||||
|
log = { workspace = true }
|
||||||
|
lru = "0.14.0"
|
||||||
|
resvg = { version = "0.45.1", default-features = false }
|
||||||
|
roxmltree = "0.20.0"
|
||||||
|
rustc-hash = "2.1.1"
|
||||||
|
slotmap = "1.0.7"
|
||||||
|
smallvec = "1.15.0"
|
||||||
|
taffy = "0.8.1"
|
||||||
|
vulkano = { workspace = true }
|
||||||
|
vulkano-shaders = { workspace = true }
|
||||||
|
|
||||||
|
[profile.dev]
|
||||||
|
opt-level = 0
|
||||||
|
debug = true
|
||||||
|
strip = "none"
|
||||||
|
debug-assertions = true
|
||||||
|
incremental = true
|
||||||
9
wgui/README.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<p align="center">
|
||||||
|
<img alt=" logo" src="./contrib/logo.png"/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
# wgui
|
||||||
|
|
||||||
|
an experimental gui library for wlx-overlay-s and WayVR Dashboard
|
||||||
|
|
||||||
|
powered via vulkan
|
||||||
BIN
wgui/contrib/logo.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
105
wgui/doc/widgets.md
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
# Universal widget attributes
|
||||||
|
|
||||||
|
`display`: flex | block | grid
|
||||||
|
|
||||||
|
`position`: absolute | relative
|
||||||
|
|
||||||
|
`flex_grow`: units (3)
|
||||||
|
|
||||||
|
`flex_shrink`: units (3)
|
||||||
|
|
||||||
|
`gap`: units (42) or percent (42%)
|
||||||
|
|
||||||
|
`flex_basis`: units (42) or percent (42%)
|
||||||
|
|
||||||
|
`justify_self`: center | end | flex_end | flex_start | start | stretch
|
||||||
|
|
||||||
|
`justify_content`: center | end | flex_start | flex_end | space_around | space_between | space_evenly | start | stretch
|
||||||
|
|
||||||
|
`flex_wrap`: wrap | no_wrap | wrap_reverse
|
||||||
|
|
||||||
|
`flex_direction`: row | column | column_reverse | row_reverse,
|
||||||
|
|
||||||
|
`align_items`, `align_self`: baseline | center | end | flex_start | flex_end | start | stretch
|
||||||
|
|
||||||
|
`box_sizing`: border_box | content_box
|
||||||
|
|
||||||
|
`margin`, `margin_left`, `margin_right`, `margin_top`, `margin_bottom`: units (42) or percent (42%)
|
||||||
|
|
||||||
|
`padding`, `padding_left`, `padding_right`, `padding_top`, `padding_bottom`: units (42) or percent (42%)
|
||||||
|
|
||||||
|
`overflow_x`, `overflow_y`: hidden | visible | clip | scroll
|
||||||
|
|
||||||
|
`min_width`, `min_height`: units (42) or percent (42%)
|
||||||
|
|
||||||
|
`max_width`, `max_height`: units (42) or percent (42%)
|
||||||
|
|
||||||
|
`width`, `height`: units (42) or percent (42%)
|
||||||
|
|
||||||
|
# Widgets
|
||||||
|
|
||||||
|
### `div`
|
||||||
|
|
||||||
|
The most simple element
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
|
||||||
|
None
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `label`
|
||||||
|
|
||||||
|
Text element
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
|
||||||
|
`text`: abc
|
||||||
|
|
||||||
|
`color`: #FFAABB | #FFAABBCC
|
||||||
|
|
||||||
|
`align`: left | right | center | justified | end
|
||||||
|
|
||||||
|
`weight`: normal | bold
|
||||||
|
|
||||||
|
`size`: _float_
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `rectangle`
|
||||||
|
|
||||||
|
A styled rectangle
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
|
||||||
|
`text`: abc
|
||||||
|
|
||||||
|
`color`: #FFAABB | #FFAABBCC
|
||||||
|
|
||||||
|
_1st gradient color_
|
||||||
|
|
||||||
|
`color2`: #FFAABB | #FFAABBCC
|
||||||
|
|
||||||
|
_2nd gradient color_
|
||||||
|
|
||||||
|
`gradient`: horizontal | vertical | radial | none
|
||||||
|
|
||||||
|
`round`: _float (0.0 - 1.0)_
|
||||||
|
|
||||||
|
`border`: _float_
|
||||||
|
|
||||||
|
`border_color`: #FFAABB | #FFAABBCC
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `sprite`
|
||||||
|
|
||||||
|
Image widget, supports raster and svg vector
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
|
||||||
|
`src`: Internal (assets) image path
|
||||||
|
|
||||||
|
`src_ext`: External image path
|
||||||
|
|
||||||
|
---
|
||||||
2
wgui/rustfmt.toml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
tab_spaces = 2
|
||||||
|
hard_tabs = true
|
||||||
216
wgui/src/animation.rs
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
use glam::FloatExt;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
event::WidgetCallback,
|
||||||
|
layout::{WidgetID, WidgetMap},
|
||||||
|
widget::WidgetObj,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub enum AnimationEasing {
|
||||||
|
Linear,
|
||||||
|
InQuad,
|
||||||
|
OutQuad,
|
||||||
|
OutBack,
|
||||||
|
InBack,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AnimationEasing {
|
||||||
|
fn interpolate(&self, x: f32) -> f32 {
|
||||||
|
match self {
|
||||||
|
AnimationEasing::Linear => x,
|
||||||
|
AnimationEasing::InQuad => x * x,
|
||||||
|
AnimationEasing::OutQuad => 1.0 - (1.0 - x) * (1.0 - x),
|
||||||
|
AnimationEasing::OutBack => {
|
||||||
|
let a = 1.7;
|
||||||
|
let b = a + 1.0;
|
||||||
|
1.0 + b * (x - 1.0).powf(3.0) + a * (x - 1.0).powf(2.0)
|
||||||
|
}
|
||||||
|
AnimationEasing::InBack => {
|
||||||
|
let a = 1.7;
|
||||||
|
let b = a + 1.0;
|
||||||
|
b * x.powf(3.0) - a * x.powf(2.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CallbackData<'a> {
|
||||||
|
pub obj: &'a mut dyn WidgetObj,
|
||||||
|
pub widgets: &'a WidgetMap,
|
||||||
|
pub widget_id: WidgetID,
|
||||||
|
pub pos: f32, // 0.0 (start of animation) - 1.0 (end of animation)
|
||||||
|
pub needs_redraw: bool,
|
||||||
|
pub dirty_nodes: &'a mut Vec<taffy::NodeId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> WidgetCallback<'a> for CallbackData<'a> {
|
||||||
|
fn get_widgets(&self) -> &'a WidgetMap {
|
||||||
|
self.widgets
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mark_redraw(&mut self) {
|
||||||
|
self.needs_redraw = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mark_dirty(&mut self, node_id: taffy::NodeId) {
|
||||||
|
self.dirty_nodes.push(node_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Animation {
|
||||||
|
target_widget: WidgetID,
|
||||||
|
|
||||||
|
animation_id: u32,
|
||||||
|
ticks_remaining: u32,
|
||||||
|
ticks_duration: u32,
|
||||||
|
|
||||||
|
easing: AnimationEasing,
|
||||||
|
|
||||||
|
pos: f32,
|
||||||
|
pos_prev: f32,
|
||||||
|
last_tick: bool,
|
||||||
|
|
||||||
|
callback: Box<dyn Fn(&mut CallbackData)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct CallResult {
|
||||||
|
needs_redraw: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Animation {
|
||||||
|
pub fn new(
|
||||||
|
target_widget: WidgetID,
|
||||||
|
ticks: u32,
|
||||||
|
easing: AnimationEasing,
|
||||||
|
callback: Box<dyn Fn(&mut CallbackData)>,
|
||||||
|
) -> Self {
|
||||||
|
Animation::new_ex(target_widget, 0, ticks, easing, callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_ex(
|
||||||
|
target_widget: WidgetID,
|
||||||
|
animation_id: u32,
|
||||||
|
ticks: u32,
|
||||||
|
easing: AnimationEasing,
|
||||||
|
callback: Box<dyn Fn(&mut CallbackData)>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
target_widget,
|
||||||
|
animation_id,
|
||||||
|
callback,
|
||||||
|
easing,
|
||||||
|
ticks_duration: ticks,
|
||||||
|
ticks_remaining: ticks,
|
||||||
|
last_tick: false,
|
||||||
|
pos: 0.0,
|
||||||
|
pos_prev: 0.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn call(
|
||||||
|
&self,
|
||||||
|
widgets: &WidgetMap,
|
||||||
|
dirty_nodes: &mut Vec<taffy::NodeId>,
|
||||||
|
pos: f32,
|
||||||
|
) -> CallResult {
|
||||||
|
let mut res = CallResult::default();
|
||||||
|
|
||||||
|
if let Some(widget) = widgets.get(self.target_widget).cloned() {
|
||||||
|
let mut widget = widget.lock().unwrap();
|
||||||
|
|
||||||
|
let data = &mut CallbackData {
|
||||||
|
widget_id: self.target_widget,
|
||||||
|
dirty_nodes,
|
||||||
|
widgets,
|
||||||
|
obj: widget.obj.as_mut(),
|
||||||
|
pos,
|
||||||
|
needs_redraw: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
(self.callback)(data);
|
||||||
|
|
||||||
|
if data.needs_redraw {
|
||||||
|
res.needs_redraw = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Animations {
|
||||||
|
running_animations: Vec<Animation>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Animations {
|
||||||
|
pub fn tick(
|
||||||
|
&mut self,
|
||||||
|
widgets: &WidgetMap,
|
||||||
|
dirty_nodes: &mut Vec<taffy::NodeId>,
|
||||||
|
needs_redraw: &mut bool,
|
||||||
|
) {
|
||||||
|
for anim in &mut self.running_animations {
|
||||||
|
let x = 1.0 - (anim.ticks_remaining as f32 / anim.ticks_duration as f32);
|
||||||
|
let pos = if anim.ticks_remaining > 0 {
|
||||||
|
anim.easing.interpolate(x)
|
||||||
|
} else {
|
||||||
|
anim.last_tick = true;
|
||||||
|
1.0
|
||||||
|
};
|
||||||
|
|
||||||
|
anim.pos_prev = anim.pos;
|
||||||
|
anim.pos = pos;
|
||||||
|
|
||||||
|
let res = anim.call(widgets, dirty_nodes, 1.0);
|
||||||
|
|
||||||
|
if anim.last_tick || res.needs_redraw {
|
||||||
|
*needs_redraw = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
anim.ticks_remaining -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
self
|
||||||
|
.running_animations
|
||||||
|
.retain(|anim| anim.ticks_remaining > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn process(
|
||||||
|
&mut self,
|
||||||
|
widgets: &WidgetMap,
|
||||||
|
dirty_nodes: &mut Vec<taffy::NodeId>,
|
||||||
|
alpha: f32,
|
||||||
|
needs_redraw: &mut bool,
|
||||||
|
) {
|
||||||
|
for anim in &mut self.running_animations {
|
||||||
|
let pos = anim.pos_prev.lerp(anim.pos, alpha);
|
||||||
|
let res = anim.call(widgets, dirty_nodes, pos);
|
||||||
|
|
||||||
|
if res.needs_redraw {
|
||||||
|
*needs_redraw = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add(&mut self, anim: Animation) {
|
||||||
|
// prevent running two animations at once
|
||||||
|
self.stop_by_widget(anim.target_widget, Some(anim.animation_id));
|
||||||
|
self.running_animations.push(anim);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stop_by_widget(&mut self, widget_id: WidgetID, animation_id: Option<u32>) {
|
||||||
|
self.running_animations.retain(|anim| {
|
||||||
|
if let Some(animation_id) = &animation_id {
|
||||||
|
if anim.target_widget == widget_id {
|
||||||
|
anim.animation_id != *animation_id
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
anim.target_widget != widget_id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
16
wgui/src/any.rs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
use std::any::Any;
|
||||||
|
|
||||||
|
pub trait AnyTrait: 'static {
|
||||||
|
fn as_any(&self) -> &dyn Any;
|
||||||
|
fn as_any_mut(&mut self) -> &mut dyn Any;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: 'static> AnyTrait for T {
|
||||||
|
fn as_any(&self) -> &dyn Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
3
wgui/src/assets.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
pub trait AssetProvider {
|
||||||
|
fn load_from_path(&mut self, path: &str) -> anyhow::Result<Vec<u8>>;
|
||||||
|
}
|
||||||
168
wgui/src/components/button.rs
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use glam::Vec2;
|
||||||
|
use taffy::{AlignItems, JustifyContent, prelude::length};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
animation::{Animation, AnimationEasing},
|
||||||
|
drawing::{self, Color},
|
||||||
|
event::{EventListener, WidgetCallback},
|
||||||
|
layout::{Layout, WidgetID},
|
||||||
|
renderer_vk::text::{FontWeight, TextStyle},
|
||||||
|
widget::{
|
||||||
|
rectangle::{Rectangle, RectangleParams},
|
||||||
|
text::{TextLabel, TextParams},
|
||||||
|
util::WLength,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct Params<'a> {
|
||||||
|
pub text: &'a str,
|
||||||
|
pub color: drawing::Color,
|
||||||
|
pub size: Vec2,
|
||||||
|
pub text_style: TextStyle,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Params<'_> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
text: "Text",
|
||||||
|
color: drawing::Color::new(1.0, 1.0, 1.0, 1.0),
|
||||||
|
size: Vec2::new(128.0, 32.0),
|
||||||
|
text_style: TextStyle::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Button {
|
||||||
|
color: drawing::Color,
|
||||||
|
pub body: WidgetID, // Rectangle
|
||||||
|
pub text_id: WidgetID, // Text
|
||||||
|
text_node: taffy::NodeId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Button {
|
||||||
|
pub fn set_text<'a, C>(&self, callback_data: &mut C, text: &str)
|
||||||
|
where
|
||||||
|
C: WidgetCallback<'a>,
|
||||||
|
{
|
||||||
|
callback_data.call_on_widget(self.text_id, |label: &mut TextLabel| {
|
||||||
|
label.set_text(text);
|
||||||
|
});
|
||||||
|
callback_data.mark_redraw();
|
||||||
|
callback_data.mark_dirty(self.text_node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn anim_hover_in(button: Arc<Button>, widget_id: WidgetID) -> Animation {
|
||||||
|
Animation::new(
|
||||||
|
widget_id,
|
||||||
|
10,
|
||||||
|
AnimationEasing::OutQuad,
|
||||||
|
Box::new(move |data| {
|
||||||
|
let rect = data.obj.get_as_mut::<Rectangle>();
|
||||||
|
let brightness = data.pos * 0.5;
|
||||||
|
rect.params.color.r = button.color.r + brightness;
|
||||||
|
rect.params.color.g = button.color.g + brightness;
|
||||||
|
rect.params.color.b = button.color.b + brightness;
|
||||||
|
rect.params.border_color = Color::new(1.0, 1.0, 1.0, 1.0);
|
||||||
|
rect.params.border = 1.0 + data.pos;
|
||||||
|
data.needs_redraw = true;
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn anim_hover_out(button: Arc<Button>, widget_id: WidgetID) -> Animation {
|
||||||
|
Animation::new(
|
||||||
|
widget_id,
|
||||||
|
15,
|
||||||
|
AnimationEasing::OutQuad,
|
||||||
|
Box::new(move |data| {
|
||||||
|
let rect = data.obj.get_as_mut::<Rectangle>();
|
||||||
|
let brightness = (1.0 - data.pos) * 0.5;
|
||||||
|
rect.params.color.r = button.color.r + brightness;
|
||||||
|
rect.params.color.g = button.color.g + brightness;
|
||||||
|
rect.params.color.b = button.color.b + brightness;
|
||||||
|
rect.params.border_color = Color::new(1.0, 1.0, 1.0, 1.0);
|
||||||
|
rect.params.border = 1.0 + (1.0 - data.pos) * 2.0;
|
||||||
|
data.needs_redraw = true;
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn construct(
|
||||||
|
layout: &mut Layout,
|
||||||
|
parent: WidgetID,
|
||||||
|
params: Params,
|
||||||
|
) -> anyhow::Result<Arc<Button>> {
|
||||||
|
let (rect_id, _) = layout.add_child(
|
||||||
|
parent,
|
||||||
|
Rectangle::create(RectangleParams {
|
||||||
|
color: params.color,
|
||||||
|
round: WLength::Units(4.0),
|
||||||
|
..Default::default()
|
||||||
|
})?,
|
||||||
|
taffy::Style {
|
||||||
|
size: taffy::Size {
|
||||||
|
width: length(params.size.x),
|
||||||
|
height: length(params.size.y),
|
||||||
|
},
|
||||||
|
align_items: Some(AlignItems::Center),
|
||||||
|
justify_content: Some(JustifyContent::Center),
|
||||||
|
padding: length(1.0),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let light_text = (params.color.r + params.color.g + params.color.b) < 1.5;
|
||||||
|
|
||||||
|
let (text_id, text_node) = layout.add_child(
|
||||||
|
rect_id,
|
||||||
|
TextLabel::create(TextParams {
|
||||||
|
content: String::from(params.text),
|
||||||
|
style: TextStyle {
|
||||||
|
weight: Some(FontWeight::Bold),
|
||||||
|
color: Some(if light_text {
|
||||||
|
Color::new(1.0, 1.0, 1.0, 1.0)
|
||||||
|
} else {
|
||||||
|
Color::new(0.0, 0.0, 0.0, 1.0)
|
||||||
|
}),
|
||||||
|
..params.text_style
|
||||||
|
},
|
||||||
|
})?,
|
||||||
|
taffy::Style {
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mut widget = layout.widget_states.get(rect_id).unwrap().lock().unwrap();
|
||||||
|
|
||||||
|
let button = Arc::new(Button {
|
||||||
|
body: rect_id,
|
||||||
|
color: params.color,
|
||||||
|
text_id,
|
||||||
|
text_node,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Highlight background on mouse enter
|
||||||
|
{
|
||||||
|
let button = button.clone();
|
||||||
|
widget.add_event_listener(EventListener::MouseEnter(Box::new(move |data| {
|
||||||
|
data
|
||||||
|
.animations
|
||||||
|
.push(anim_hover_in(button.clone(), data.widget_id));
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bring back old color on mouse leave
|
||||||
|
{
|
||||||
|
let button = button.clone();
|
||||||
|
widget.add_event_listener(EventListener::MouseLeave(Box::new(move |data| {
|
||||||
|
data
|
||||||
|
.animations
|
||||||
|
.push(anim_hover_out(button.clone(), data.widget_id));
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(button)
|
||||||
|
}
|
||||||
1
wgui/src/components/mod.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub mod button;
|
||||||
200
wgui/src/drawing.rs
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
|
||||||
|
use cosmic_text::Buffer;
|
||||||
|
use glam::{Mat4, Vec2};
|
||||||
|
use taffy::TraversePartialTree;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
layout::BoxWidget,
|
||||||
|
renderer_vk::text::custom_glyph::CustomGlyph,
|
||||||
|
transform_stack::{self, TransformStack},
|
||||||
|
widget::{self},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{layout::Layout, widget::DrawState};
|
||||||
|
|
||||||
|
pub struct ImageHandle {
|
||||||
|
// to be implemented, will contain pixel data (RGB or RGBA) loaded via "ImageBank" or something by the gui
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, Copy, PartialEq)]
|
||||||
|
pub struct Boundary {
|
||||||
|
pub pos: Vec2,
|
||||||
|
pub size: Vec2,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Boundary {
|
||||||
|
pub fn from_pos_size(pos: Vec2, size: Vec2) -> Self {
|
||||||
|
Self { pos, size }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn construct(transform_stack: &TransformStack) -> Self {
|
||||||
|
let transform = transform_stack.get();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
pos: Vec2::new(transform.pos.x, transform.pos.y),
|
||||||
|
size: Vec2::new(transform.dim.x, transform.dim.y),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
pub struct Color {
|
||||||
|
pub r: f32,
|
||||||
|
pub g: f32,
|
||||||
|
pub b: f32,
|
||||||
|
pub a: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Color {
|
||||||
|
pub fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
|
||||||
|
Self { r, g, b, a }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Color {
|
||||||
|
fn default() -> Self {
|
||||||
|
// opaque black
|
||||||
|
Self::new(0.0, 0.0, 0.0, 1.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(u8)]
|
||||||
|
#[derive(Default, Clone, Copy)]
|
||||||
|
pub enum GradientMode {
|
||||||
|
#[default]
|
||||||
|
None,
|
||||||
|
Horizontal,
|
||||||
|
Vertical,
|
||||||
|
Radial,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Clone, Copy)]
|
||||||
|
pub struct Rectangle {
|
||||||
|
pub color: Color,
|
||||||
|
pub color2: Color,
|
||||||
|
pub gradient: GradientMode,
|
||||||
|
|
||||||
|
pub border: f32, // width in pixels
|
||||||
|
pub border_color: Color,
|
||||||
|
|
||||||
|
pub round_units: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RenderPrimitive {
|
||||||
|
pub(super) boundary: Boundary,
|
||||||
|
pub(super) transform: Mat4,
|
||||||
|
pub(super) depth: f32,
|
||||||
|
pub(super) payload: PrimitivePayload,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum PrimitivePayload {
|
||||||
|
Rectangle(Rectangle),
|
||||||
|
Text(Rc<RefCell<Buffer>>),
|
||||||
|
Sprite(Option<CustomGlyph>), //option because we want as_slice
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_widget(
|
||||||
|
layout: &Layout,
|
||||||
|
state: &mut DrawState,
|
||||||
|
node_id: taffy::NodeId,
|
||||||
|
style: &taffy::Style,
|
||||||
|
widget: &BoxWidget,
|
||||||
|
parent_transform: &glam::Mat4,
|
||||||
|
) {
|
||||||
|
let Ok(l) = layout.tree.layout(node_id) else {
|
||||||
|
debug_assert!(false);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut widget_state = widget.lock().unwrap();
|
||||||
|
|
||||||
|
let transform = widget_state.data.transform * *parent_transform;
|
||||||
|
|
||||||
|
let (shift, info) = match widget::get_scrollbar_info(l) {
|
||||||
|
Some(info) => (widget_state.get_scroll_shift(&info, l), Some(info)),
|
||||||
|
None => (Vec2::default(), None),
|
||||||
|
};
|
||||||
|
|
||||||
|
state.transform_stack.push(transform_stack::Transform {
|
||||||
|
pos: Vec2::new(l.location.x, l.location.y) - shift,
|
||||||
|
transform,
|
||||||
|
dim: Vec2::new(l.size.width, l.size.height),
|
||||||
|
});
|
||||||
|
|
||||||
|
let draw_params = widget::DrawParams {
|
||||||
|
node_id,
|
||||||
|
taffy_layout: l,
|
||||||
|
style,
|
||||||
|
};
|
||||||
|
|
||||||
|
widget_state.draw_all(state, &draw_params);
|
||||||
|
|
||||||
|
draw_children(layout, state, node_id, &transform);
|
||||||
|
|
||||||
|
state.transform_stack.pop();
|
||||||
|
|
||||||
|
if let Some(info) = &info {
|
||||||
|
widget_state.draw_scrollbars(state, &draw_params, info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_children(
|
||||||
|
layout: &Layout,
|
||||||
|
state: &mut DrawState,
|
||||||
|
parent_node_id: taffy::NodeId,
|
||||||
|
model: &glam::Mat4,
|
||||||
|
) {
|
||||||
|
for node_id in layout.tree.child_ids(parent_node_id) {
|
||||||
|
let Some(widget_id) = layout.tree.get_node_context(node_id).cloned() else {
|
||||||
|
debug_assert!(false);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Ok(style) = layout.tree.style(node_id) else {
|
||||||
|
debug_assert!(false);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(widget) = layout.widget_states.get(widget_id) else {
|
||||||
|
debug_assert!(false);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
state.depth += 0.01;
|
||||||
|
draw_widget(layout, state, node_id, style, widget, model);
|
||||||
|
state.depth -= 0.01;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw(layout: &Layout) -> anyhow::Result<Vec<RenderPrimitive>> {
|
||||||
|
let mut primitives = Vec::<RenderPrimitive>::new();
|
||||||
|
let mut transform_stack = TransformStack::new();
|
||||||
|
let model = glam::Mat4::IDENTITY;
|
||||||
|
|
||||||
|
let Some(root_widget) = layout.widget_states.get(layout.root_widget) else {
|
||||||
|
panic!();
|
||||||
|
};
|
||||||
|
|
||||||
|
let Ok(style) = layout.tree.style(layout.root_node) else {
|
||||||
|
panic!();
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut params = DrawState {
|
||||||
|
primitives: &mut primitives,
|
||||||
|
transform_stack: &mut transform_stack,
|
||||||
|
layout,
|
||||||
|
depth: 0.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
draw_widget(
|
||||||
|
layout,
|
||||||
|
&mut params,
|
||||||
|
layout.root_node,
|
||||||
|
style,
|
||||||
|
root_widget,
|
||||||
|
&model,
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(primitives)
|
||||||
|
}
|
||||||
108
wgui/src/event.rs
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
use glam::Vec2;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
animation,
|
||||||
|
layout::{WidgetID, WidgetMap},
|
||||||
|
transform_stack::Transform,
|
||||||
|
widget::{WidgetData, WidgetObj},
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: mouse index
|
||||||
|
pub struct MouseDownEvent {
|
||||||
|
pub pos: Vec2,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MouseMotionEvent {
|
||||||
|
pub pos: Vec2,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MouseUpEvent {
|
||||||
|
pub pos: Vec2,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MouseWheelEvent {
|
||||||
|
pub pos: Vec2,
|
||||||
|
pub shift: Vec2,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Event {
|
||||||
|
MouseDown(MouseDownEvent),
|
||||||
|
MouseMotion(MouseMotionEvent),
|
||||||
|
MouseUp(MouseUpEvent),
|
||||||
|
MouseWheel(MouseWheelEvent),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Event {
|
||||||
|
fn test_transform_pos(&self, transform: &Transform, pos: &Vec2) -> bool {
|
||||||
|
pos.x >= transform.pos.x
|
||||||
|
&& pos.x < transform.pos.x + transform.dim.x
|
||||||
|
&& pos.y >= transform.pos.y
|
||||||
|
&& pos.y < transform.pos.y + transform.dim.y
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn test_mouse_within_transform(&self, transform: &Transform) -> bool {
|
||||||
|
match self {
|
||||||
|
Event::MouseDown(evt) => self.test_transform_pos(transform, &evt.pos),
|
||||||
|
Event::MouseMotion(evt) => self.test_transform_pos(transform, &evt.pos),
|
||||||
|
Event::MouseUp(evt) => self.test_transform_pos(transform, &evt.pos),
|
||||||
|
Event::MouseWheel(evt) => self.test_transform_pos(transform, &evt.pos),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait WidgetCallback<'a> {
|
||||||
|
fn call_on_widget<WIDGET, FUNC>(&self, widget_id: WidgetID, func: FUNC)
|
||||||
|
where
|
||||||
|
WIDGET: WidgetObj,
|
||||||
|
FUNC: FnOnce(&mut WIDGET),
|
||||||
|
{
|
||||||
|
let Some(widget) = self.get_widgets().get(widget_id) else {
|
||||||
|
debug_assert!(false);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut lock = widget.lock().unwrap();
|
||||||
|
let m = lock.obj.get_as_mut::<WIDGET>();
|
||||||
|
|
||||||
|
func(m);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_widgets(&self) -> &'a WidgetMap;
|
||||||
|
fn mark_redraw(&mut self);
|
||||||
|
fn mark_dirty(&mut self, node_id: taffy::NodeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CallbackData<'a> {
|
||||||
|
pub obj: &'a mut dyn WidgetObj,
|
||||||
|
pub widget_data: &'a mut WidgetData,
|
||||||
|
pub animations: &'a mut Vec<animation::Animation>,
|
||||||
|
pub widgets: &'a WidgetMap,
|
||||||
|
pub widget_id: WidgetID,
|
||||||
|
pub node_id: taffy::NodeId,
|
||||||
|
pub dirty_nodes: &'a mut Vec<taffy::NodeId>,
|
||||||
|
pub needs_redraw: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> WidgetCallback<'a> for CallbackData<'a> {
|
||||||
|
fn get_widgets(&self) -> &'a WidgetMap {
|
||||||
|
self.widgets
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mark_redraw(&mut self) {
|
||||||
|
self.needs_redraw = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mark_dirty(&mut self, node_id: taffy::NodeId) {
|
||||||
|
self.dirty_nodes.push(node_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type MouseEnterCallback = Box<dyn Fn(&mut CallbackData)>;
|
||||||
|
pub type MouseLeaveCallback = Box<dyn Fn(&mut CallbackData)>;
|
||||||
|
pub type MouseClickCallback = Box<dyn Fn(&mut CallbackData)>;
|
||||||
|
|
||||||
|
pub enum EventListener {
|
||||||
|
MouseEnter(MouseEnterCallback),
|
||||||
|
MouseLeave(MouseLeaveCallback),
|
||||||
|
MouseClick(MouseClickCallback),
|
||||||
|
}
|
||||||
176
wgui/src/gfx/cmd.rs
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
use std::{marker::PhantomData, sync::Arc};
|
||||||
|
|
||||||
|
use vulkano::{
|
||||||
|
DeviceSize,
|
||||||
|
buffer::{Buffer, BufferCreateInfo, BufferUsage, Subbuffer},
|
||||||
|
command_buffer::{
|
||||||
|
AutoCommandBufferBuilder, CommandBufferExecFuture, CopyBufferToImageInfo, CopyImageInfo,
|
||||||
|
PrimaryAutoCommandBuffer, PrimaryCommandBufferAbstract, RenderingAttachmentInfo, RenderingInfo,
|
||||||
|
SubpassContents,
|
||||||
|
},
|
||||||
|
device::Queue,
|
||||||
|
format::Format,
|
||||||
|
image::{Image, ImageCreateInfo, ImageType, ImageUsage, view::ImageView},
|
||||||
|
memory::allocator::{AllocationCreateInfo, MemoryTypeFilter},
|
||||||
|
render_pass::{AttachmentLoadOp, AttachmentStoreOp},
|
||||||
|
sync::{GpuFuture, future::NowFuture},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{WGfx, pass::WGfxPass};
|
||||||
|
|
||||||
|
pub type GfxCommandBuffer = WCommandBuffer<CmdBufGfx>;
|
||||||
|
pub type XferCommandBuffer = WCommandBuffer<CmdBufXfer>;
|
||||||
|
|
||||||
|
pub struct CmdBufGfx;
|
||||||
|
pub struct CmdBufXfer;
|
||||||
|
|
||||||
|
pub struct WCommandBuffer<T> {
|
||||||
|
pub graphics: Arc<WGfx>,
|
||||||
|
pub queue: Arc<Queue>,
|
||||||
|
pub command_buffer: AutoCommandBufferBuilder<PrimaryAutoCommandBuffer>,
|
||||||
|
pub(super) _dummy: PhantomData<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> WCommandBuffer<T> {
|
||||||
|
pub fn build_and_execute(self) -> anyhow::Result<CommandBufferExecFuture<NowFuture>> {
|
||||||
|
let queue = self.queue.clone();
|
||||||
|
Ok(self.command_buffer.build()?.execute(queue)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_and_execute_now(self) -> anyhow::Result<()> {
|
||||||
|
let mut exec = self.build_and_execute()?;
|
||||||
|
exec.flush()?;
|
||||||
|
exec.cleanup_finished();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WCommandBuffer<CmdBufGfx> {
|
||||||
|
pub fn begin_rendering(&mut self, render_target: Arc<ImageView>) -> anyhow::Result<()> {
|
||||||
|
self.command_buffer.begin_rendering(RenderingInfo {
|
||||||
|
contents: SubpassContents::SecondaryCommandBuffers,
|
||||||
|
color_attachments: vec![Some(RenderingAttachmentInfo {
|
||||||
|
load_op: AttachmentLoadOp::Clear,
|
||||||
|
store_op: AttachmentStoreOp::Store,
|
||||||
|
clear_value: Some([0.0, 0.0, 0.0, 0.0].into()),
|
||||||
|
..RenderingAttachmentInfo::image_view(render_target)
|
||||||
|
})],
|
||||||
|
..Default::default()
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build(self) -> anyhow::Result<Arc<PrimaryAutoCommandBuffer>> {
|
||||||
|
Ok(self.command_buffer.build()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_ref<T>(&mut self, pass: &WGfxPass<T>) -> anyhow::Result<()>
|
||||||
|
where
|
||||||
|
T: Sized,
|
||||||
|
{
|
||||||
|
self
|
||||||
|
.command_buffer
|
||||||
|
.execute_commands(pass.command_buffer.clone())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn end_rendering(&mut self) -> anyhow::Result<()> {
|
||||||
|
self.command_buffer.end_rendering()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WCommandBuffer<CmdBufXfer> {
|
||||||
|
pub fn upload_image(
|
||||||
|
&mut self,
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
format: Format,
|
||||||
|
data: &[u8],
|
||||||
|
) -> anyhow::Result<Arc<Image>> {
|
||||||
|
let image = Image::new(
|
||||||
|
self.graphics.memory_allocator.clone(),
|
||||||
|
ImageCreateInfo {
|
||||||
|
image_type: ImageType::Dim2d,
|
||||||
|
format,
|
||||||
|
extent: [width, height, 1],
|
||||||
|
usage: ImageUsage::TRANSFER_DST | ImageUsage::TRANSFER_SRC | ImageUsage::SAMPLED,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
AllocationCreateInfo::default(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let buffer: Subbuffer<[u8]> = Buffer::new_slice(
|
||||||
|
self.graphics.memory_allocator.clone(),
|
||||||
|
BufferCreateInfo {
|
||||||
|
usage: BufferUsage::TRANSFER_SRC,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
AllocationCreateInfo {
|
||||||
|
memory_type_filter: MemoryTypeFilter::PREFER_HOST | MemoryTypeFilter::HOST_SEQUENTIAL_WRITE,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
data.len() as DeviceSize,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
buffer.write()?.copy_from_slice(data);
|
||||||
|
|
||||||
|
self
|
||||||
|
.command_buffer
|
||||||
|
.copy_buffer_to_image(CopyBufferToImageInfo::buffer_image(buffer, image.clone()))?;
|
||||||
|
|
||||||
|
Ok(image)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_image(
|
||||||
|
&mut self,
|
||||||
|
image: Arc<Image>,
|
||||||
|
data: &[u8],
|
||||||
|
offset: [u32; 3],
|
||||||
|
extent: Option<[u32; 3]>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let buffer: Subbuffer<[u8]> = Buffer::new_slice(
|
||||||
|
self.graphics.memory_allocator.clone(),
|
||||||
|
BufferCreateInfo {
|
||||||
|
usage: BufferUsage::TRANSFER_SRC,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
AllocationCreateInfo {
|
||||||
|
memory_type_filter: MemoryTypeFilter::PREFER_HOST | MemoryTypeFilter::HOST_SEQUENTIAL_WRITE,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
data.len() as DeviceSize,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
buffer.write()?.copy_from_slice(data);
|
||||||
|
|
||||||
|
let mut copy_info = CopyBufferToImageInfo::buffer_image(buffer, image.clone());
|
||||||
|
copy_info.regions[0].image_offset = offset;
|
||||||
|
if let Some(extent) = extent {
|
||||||
|
copy_info.regions[0].image_extent = extent;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.command_buffer.copy_buffer_to_image(copy_info)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn copy_image(
|
||||||
|
&mut self,
|
||||||
|
src: Arc<Image>,
|
||||||
|
src_offset: [u32; 3],
|
||||||
|
dst: Arc<Image>,
|
||||||
|
dst_offset: [u32; 3],
|
||||||
|
extent: Option<[u32; 3]>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let mut copy_info = CopyImageInfo::images(src.clone(), dst.clone());
|
||||||
|
|
||||||
|
copy_info.regions[0].src_offset = src_offset;
|
||||||
|
copy_info.regions[0].dst_offset = dst_offset;
|
||||||
|
if let Some(extent) = extent {
|
||||||
|
copy_info.regions[0].extent = extent;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.command_buffer.copy_image(copy_info)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
311
wgui/src/gfx/mod.rs
Normal file
@@ -0,0 +1,311 @@
|
|||||||
|
pub mod cmd;
|
||||||
|
pub mod pass;
|
||||||
|
pub mod pipeline;
|
||||||
|
|
||||||
|
use std::{marker::PhantomData, slice::Iter, sync::Arc};
|
||||||
|
|
||||||
|
use cmd::{GfxCommandBuffer, XferCommandBuffer};
|
||||||
|
use pipeline::WGfxPipeline;
|
||||||
|
use vulkano::{
|
||||||
|
DeviceSize,
|
||||||
|
buffer::{Buffer, BufferContents, BufferCreateInfo, BufferUsage, IndexBuffer, Subbuffer},
|
||||||
|
command_buffer::{
|
||||||
|
AutoCommandBufferBuilder, CommandBufferUsage,
|
||||||
|
allocator::{StandardCommandBufferAllocator, StandardCommandBufferAllocatorCreateInfo},
|
||||||
|
},
|
||||||
|
descriptor_set::allocator::{
|
||||||
|
StandardDescriptorSetAllocator, StandardDescriptorSetAllocatorCreateInfo,
|
||||||
|
},
|
||||||
|
device::{Device, Queue},
|
||||||
|
format::Format,
|
||||||
|
image::{Image, ImageCreateInfo, ImageType, ImageUsage, sampler::Filter},
|
||||||
|
instance::Instance,
|
||||||
|
memory::{
|
||||||
|
MemoryPropertyFlags,
|
||||||
|
allocator::{
|
||||||
|
AllocationCreateInfo, GenericMemoryAllocatorCreateInfo, MemoryTypeFilter,
|
||||||
|
StandardMemoryAllocator,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pipeline::graphics::{
|
||||||
|
color_blend::{AttachmentBlend, BlendFactor, BlendOp},
|
||||||
|
input_assembly::PrimitiveTopology,
|
||||||
|
vertex_input::Vertex,
|
||||||
|
},
|
||||||
|
shader::ShaderModule,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const BLEND_ALPHA: AttachmentBlend = AttachmentBlend {
|
||||||
|
src_color_blend_factor: BlendFactor::SrcAlpha,
|
||||||
|
dst_color_blend_factor: BlendFactor::OneMinusSrcAlpha,
|
||||||
|
color_blend_op: BlendOp::Add,
|
||||||
|
src_alpha_blend_factor: BlendFactor::One,
|
||||||
|
dst_alpha_blend_factor: BlendFactor::One,
|
||||||
|
alpha_blend_op: BlendOp::Max,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub type Vert2Buf = Subbuffer<[Vert2Uv]>;
|
||||||
|
pub type IndexBuf = IndexBuffer;
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(BufferContents, Vertex, Copy, Clone, Debug)]
|
||||||
|
pub struct Vert2Uv {
|
||||||
|
#[format(R32G32_SFLOAT)]
|
||||||
|
pub in_pos: [f32; 2],
|
||||||
|
#[format(R32G32_SFLOAT)]
|
||||||
|
pub in_uv: [f32; 2],
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum QueueType {
|
||||||
|
Graphics,
|
||||||
|
Transfer,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct WGfx {
|
||||||
|
pub instance: Arc<Instance>,
|
||||||
|
pub device: Arc<Device>,
|
||||||
|
|
||||||
|
pub queue_gfx: Arc<Queue>,
|
||||||
|
pub queue_xfer: Arc<Queue>,
|
||||||
|
|
||||||
|
pub texture_filter: Filter,
|
||||||
|
|
||||||
|
pub surface_format: Format,
|
||||||
|
|
||||||
|
pub memory_allocator: Arc<StandardMemoryAllocator>,
|
||||||
|
pub command_buffer_allocator: Arc<StandardCommandBufferAllocator>,
|
||||||
|
pub descriptor_set_allocator: Arc<StandardDescriptorSetAllocator>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WGfx {
|
||||||
|
pub fn new_from_raw(
|
||||||
|
instance: Arc<Instance>,
|
||||||
|
device: Arc<Device>,
|
||||||
|
queue_gfx: Arc<Queue>,
|
||||||
|
queue_xfer: Arc<Queue>,
|
||||||
|
surface_format: Format,
|
||||||
|
) -> Arc<Self> {
|
||||||
|
let memory_allocator = memory_allocator(device.clone());
|
||||||
|
let command_buffer_allocator = Arc::new(StandardCommandBufferAllocator::new(
|
||||||
|
device.clone(),
|
||||||
|
StandardCommandBufferAllocatorCreateInfo {
|
||||||
|
secondary_buffer_count: 32,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
));
|
||||||
|
let descriptor_set_allocator = Arc::new(StandardDescriptorSetAllocator::new(
|
||||||
|
device.clone(),
|
||||||
|
StandardDescriptorSetAllocatorCreateInfo::default(),
|
||||||
|
));
|
||||||
|
|
||||||
|
let quality_filter = if device.enabled_extensions().img_filter_cubic {
|
||||||
|
Filter::Cubic
|
||||||
|
} else {
|
||||||
|
Filter::Linear
|
||||||
|
};
|
||||||
|
|
||||||
|
Arc::new(Self {
|
||||||
|
instance,
|
||||||
|
device,
|
||||||
|
queue_gfx,
|
||||||
|
queue_xfer,
|
||||||
|
surface_format,
|
||||||
|
texture_filter: quality_filter,
|
||||||
|
memory_allocator,
|
||||||
|
command_buffer_allocator,
|
||||||
|
descriptor_set_allocator,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn empty_buffer<T>(&self, usage: BufferUsage, capacity: u64) -> anyhow::Result<Subbuffer<[T]>>
|
||||||
|
where
|
||||||
|
T: BufferContents + Clone,
|
||||||
|
{
|
||||||
|
Ok(Buffer::new_slice(
|
||||||
|
self.memory_allocator.clone(),
|
||||||
|
BufferCreateInfo {
|
||||||
|
usage,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
AllocationCreateInfo {
|
||||||
|
memory_type_filter: MemoryTypeFilter::PREFER_DEVICE
|
||||||
|
| MemoryTypeFilter::HOST_SEQUENTIAL_WRITE,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
capacity,
|
||||||
|
)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_buffer<T>(
|
||||||
|
&self,
|
||||||
|
usage: BufferUsage,
|
||||||
|
contents: Iter<'_, T>,
|
||||||
|
) -> anyhow::Result<Subbuffer<[T]>>
|
||||||
|
where
|
||||||
|
T: BufferContents + Clone,
|
||||||
|
{
|
||||||
|
Ok(Buffer::from_iter(
|
||||||
|
self.memory_allocator.clone(),
|
||||||
|
BufferCreateInfo {
|
||||||
|
usage,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
AllocationCreateInfo {
|
||||||
|
memory_type_filter: MemoryTypeFilter::PREFER_DEVICE
|
||||||
|
| MemoryTypeFilter::HOST_SEQUENTIAL_WRITE,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
contents.cloned(),
|
||||||
|
)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_image(
|
||||||
|
&self,
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
format: Format,
|
||||||
|
usage: ImageUsage,
|
||||||
|
) -> anyhow::Result<Arc<Image>> {
|
||||||
|
Ok(Image::new(
|
||||||
|
self.memory_allocator.clone(),
|
||||||
|
ImageCreateInfo {
|
||||||
|
image_type: ImageType::Dim2d,
|
||||||
|
format,
|
||||||
|
extent: [width, height, 1],
|
||||||
|
usage,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
AllocationCreateInfo::default(),
|
||||||
|
)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_pipeline<V>(
|
||||||
|
self: &Arc<Self>,
|
||||||
|
vert: Arc<ShaderModule>,
|
||||||
|
frag: Arc<ShaderModule>,
|
||||||
|
format: Format,
|
||||||
|
blend: Option<AttachmentBlend>,
|
||||||
|
topology: PrimitiveTopology,
|
||||||
|
instanced: bool,
|
||||||
|
) -> anyhow::Result<Arc<WGfxPipeline<V>>>
|
||||||
|
where
|
||||||
|
V: BufferContents + Vertex,
|
||||||
|
{
|
||||||
|
Ok(Arc::new(WGfxPipeline::new_with_vert_input(
|
||||||
|
self.clone(),
|
||||||
|
vert,
|
||||||
|
frag,
|
||||||
|
format,
|
||||||
|
blend,
|
||||||
|
topology,
|
||||||
|
instanced,
|
||||||
|
)?))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_pipeline_procedural(
|
||||||
|
self: &Arc<Self>,
|
||||||
|
vert: Arc<ShaderModule>,
|
||||||
|
frag: Arc<ShaderModule>,
|
||||||
|
format: Format,
|
||||||
|
blend: Option<AttachmentBlend>,
|
||||||
|
topology: PrimitiveTopology,
|
||||||
|
) -> anyhow::Result<Arc<WGfxPipeline<()>>> {
|
||||||
|
Ok(Arc::new(WGfxPipeline::new_procedural(
|
||||||
|
self.clone(),
|
||||||
|
vert,
|
||||||
|
frag,
|
||||||
|
format,
|
||||||
|
blend,
|
||||||
|
topology,
|
||||||
|
)?))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_gfx_command_buffer(
|
||||||
|
self: &Arc<Self>,
|
||||||
|
usage: CommandBufferUsage,
|
||||||
|
) -> anyhow::Result<GfxCommandBuffer> {
|
||||||
|
self.create_gfx_command_buffer_with_queue(self.queue_gfx.clone(), usage)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_gfx_command_buffer_with_queue(
|
||||||
|
self: &Arc<Self>,
|
||||||
|
queue: Arc<Queue>,
|
||||||
|
usage: CommandBufferUsage,
|
||||||
|
) -> anyhow::Result<GfxCommandBuffer> {
|
||||||
|
let command_buffer = AutoCommandBufferBuilder::primary(
|
||||||
|
self.command_buffer_allocator.clone(),
|
||||||
|
queue.queue_family_index(),
|
||||||
|
usage,
|
||||||
|
)?;
|
||||||
|
Ok(GfxCommandBuffer {
|
||||||
|
graphics: self.clone(),
|
||||||
|
queue,
|
||||||
|
command_buffer,
|
||||||
|
_dummy: PhantomData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_xfer_command_buffer(
|
||||||
|
self: &Arc<Self>,
|
||||||
|
usage: CommandBufferUsage,
|
||||||
|
) -> anyhow::Result<XferCommandBuffer> {
|
||||||
|
self.create_xfer_command_buffer_with_queue(self.queue_gfx.clone(), usage)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_xfer_command_buffer_with_queue(
|
||||||
|
self: &Arc<Self>,
|
||||||
|
queue: Arc<Queue>,
|
||||||
|
usage: CommandBufferUsage,
|
||||||
|
) -> anyhow::Result<XferCommandBuffer> {
|
||||||
|
let command_buffer = AutoCommandBufferBuilder::primary(
|
||||||
|
self.command_buffer_allocator.clone(),
|
||||||
|
queue.queue_family_index(),
|
||||||
|
usage,
|
||||||
|
)?;
|
||||||
|
Ok(XferCommandBuffer {
|
||||||
|
graphics: self.clone(),
|
||||||
|
queue,
|
||||||
|
command_buffer,
|
||||||
|
_dummy: PhantomData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn memory_allocator(device: Arc<Device>) -> Arc<StandardMemoryAllocator> {
|
||||||
|
let props = device.physical_device().memory_properties();
|
||||||
|
|
||||||
|
let mut block_sizes = vec![0; props.memory_types.len()];
|
||||||
|
let mut memory_type_bits = u32::MAX;
|
||||||
|
|
||||||
|
for (index, memory_type) in props.memory_types.iter().enumerate() {
|
||||||
|
const LARGE_HEAP_THRESHOLD: DeviceSize = 1024 * 1024 * 1024;
|
||||||
|
|
||||||
|
let heap_size = props.memory_heaps[memory_type.heap_index as usize].size;
|
||||||
|
|
||||||
|
block_sizes[index] = if heap_size >= LARGE_HEAP_THRESHOLD {
|
||||||
|
48 * 1024 * 1024
|
||||||
|
} else {
|
||||||
|
24 * 1024 * 1024
|
||||||
|
};
|
||||||
|
|
||||||
|
if memory_type.property_flags.intersects(
|
||||||
|
MemoryPropertyFlags::LAZILY_ALLOCATED
|
||||||
|
| MemoryPropertyFlags::PROTECTED
|
||||||
|
| MemoryPropertyFlags::DEVICE_COHERENT
|
||||||
|
| MemoryPropertyFlags::RDMA_CAPABLE,
|
||||||
|
) {
|
||||||
|
// VUID-VkMemoryAllocateInfo-memoryTypeIndex-01872
|
||||||
|
// VUID-vkAllocateMemory-deviceCoherentMemory-02790
|
||||||
|
// Lazily allocated memory would just cause problems for suballocation in general.
|
||||||
|
memory_type_bits &= !(1 << index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let create_info = GenericMemoryAllocatorCreateInfo {
|
||||||
|
block_sizes: &block_sizes,
|
||||||
|
memory_type_bits,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
Arc::new(StandardMemoryAllocator::new(device, create_info))
|
||||||
|
}
|
||||||
187
wgui/src/gfx/pass.rs
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
use std::{marker::PhantomData, ops::Range, sync::Arc};
|
||||||
|
|
||||||
|
use smallvec::smallvec;
|
||||||
|
use vulkano::{
|
||||||
|
buffer::{BufferContents, IndexBuffer, Subbuffer},
|
||||||
|
command_buffer::{
|
||||||
|
AutoCommandBufferBuilder, CommandBufferInheritanceInfo, CommandBufferInheritanceRenderPassType,
|
||||||
|
CommandBufferInheritanceRenderingInfo, CommandBufferUsage, SecondaryAutoCommandBuffer,
|
||||||
|
},
|
||||||
|
descriptor_set::DescriptorSet,
|
||||||
|
pipeline::{
|
||||||
|
Pipeline, PipelineBindPoint,
|
||||||
|
graphics::{vertex_input::Vertex, viewport::Viewport},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::pipeline::WGfxPipeline;
|
||||||
|
|
||||||
|
pub struct WGfxPass<V> {
|
||||||
|
pub command_buffer: Arc<SecondaryAutoCommandBuffer>,
|
||||||
|
_dummy: PhantomData<V>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WGfxPass<()> {
|
||||||
|
pub(super) fn new_procedural(
|
||||||
|
pipeline: Arc<WGfxPipeline<()>>,
|
||||||
|
dimensions: [f32; 2],
|
||||||
|
vertices: Range<u32>,
|
||||||
|
instances: Range<u32>,
|
||||||
|
descriptor_sets: Vec<Arc<DescriptorSet>>,
|
||||||
|
) -> anyhow::Result<Self> {
|
||||||
|
let viewport = Viewport {
|
||||||
|
offset: [0.0, 0.0],
|
||||||
|
extent: dimensions,
|
||||||
|
depth_range: 0.0..=1.0,
|
||||||
|
};
|
||||||
|
let pipeline_inner = pipeline.inner();
|
||||||
|
let mut command_buffer = AutoCommandBufferBuilder::secondary(
|
||||||
|
pipeline.graphics.command_buffer_allocator.clone(),
|
||||||
|
pipeline.graphics.queue_gfx.queue_family_index(),
|
||||||
|
CommandBufferUsage::MultipleSubmit,
|
||||||
|
CommandBufferInheritanceInfo {
|
||||||
|
render_pass: Some(CommandBufferInheritanceRenderPassType::BeginRendering(
|
||||||
|
CommandBufferInheritanceRenderingInfo {
|
||||||
|
color_attachment_formats: vec![Some(pipeline.format)],
|
||||||
|
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
command_buffer
|
||||||
|
.set_viewport(0, smallvec![viewport])?
|
||||||
|
.bind_pipeline_graphics(pipeline_inner)?
|
||||||
|
.bind_descriptor_sets(
|
||||||
|
PipelineBindPoint::Graphics,
|
||||||
|
pipeline.inner().layout().clone(),
|
||||||
|
0,
|
||||||
|
descriptor_sets,
|
||||||
|
)?
|
||||||
|
.draw(
|
||||||
|
vertices.end - vertices.start,
|
||||||
|
instances.end - instances.start,
|
||||||
|
vertices.start,
|
||||||
|
instances.start,
|
||||||
|
)?
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
command_buffer: command_buffer.build()?,
|
||||||
|
_dummy: PhantomData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V> WGfxPass<V>
|
||||||
|
where
|
||||||
|
V: BufferContents + Vertex,
|
||||||
|
{
|
||||||
|
pub(super) fn new_indexed(
|
||||||
|
pipeline: Arc<WGfxPipeline<V>>,
|
||||||
|
dimensions: [f32; 2],
|
||||||
|
vertex_buffer: Subbuffer<[V]>,
|
||||||
|
index_buffer: IndexBuffer,
|
||||||
|
descriptor_sets: Vec<Arc<DescriptorSet>>,
|
||||||
|
) -> anyhow::Result<Self> {
|
||||||
|
let viewport = Viewport {
|
||||||
|
offset: [0.0, 0.0],
|
||||||
|
extent: dimensions,
|
||||||
|
depth_range: 0.0..=1.0,
|
||||||
|
};
|
||||||
|
let pipeline_inner = pipeline.inner();
|
||||||
|
let mut command_buffer = AutoCommandBufferBuilder::secondary(
|
||||||
|
pipeline.graphics.command_buffer_allocator.clone(),
|
||||||
|
pipeline.graphics.queue_gfx.queue_family_index(),
|
||||||
|
CommandBufferUsage::MultipleSubmit,
|
||||||
|
CommandBufferInheritanceInfo {
|
||||||
|
render_pass: Some(CommandBufferInheritanceRenderPassType::BeginRendering(
|
||||||
|
CommandBufferInheritanceRenderingInfo {
|
||||||
|
color_attachment_formats: vec![Some(pipeline.format)],
|
||||||
|
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
command_buffer
|
||||||
|
.set_viewport(0, smallvec![viewport])?
|
||||||
|
.bind_pipeline_graphics(pipeline_inner)?
|
||||||
|
.bind_descriptor_sets(
|
||||||
|
PipelineBindPoint::Graphics,
|
||||||
|
pipeline.inner().layout().clone(),
|
||||||
|
0,
|
||||||
|
descriptor_sets,
|
||||||
|
)?
|
||||||
|
.bind_vertex_buffers(0, vertex_buffer)?
|
||||||
|
.bind_index_buffer(index_buffer.clone())?
|
||||||
|
.draw_indexed(index_buffer.len() as u32, 1, 0, 0, 0)?
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
command_buffer: command_buffer.build()?,
|
||||||
|
_dummy: PhantomData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn new_instanced(
|
||||||
|
pipeline: Arc<WGfxPipeline<V>>,
|
||||||
|
dimensions: [f32; 2],
|
||||||
|
vertex_buffer: Subbuffer<[V]>,
|
||||||
|
vertices: Range<u32>,
|
||||||
|
instances: Range<u32>,
|
||||||
|
descriptor_sets: Vec<Arc<DescriptorSet>>,
|
||||||
|
) -> anyhow::Result<Self> {
|
||||||
|
let viewport = Viewport {
|
||||||
|
offset: [0.0, 0.0],
|
||||||
|
extent: dimensions,
|
||||||
|
depth_range: 0.0..=1.0,
|
||||||
|
};
|
||||||
|
let pipeline_inner = pipeline.inner();
|
||||||
|
let mut command_buffer = AutoCommandBufferBuilder::secondary(
|
||||||
|
pipeline.graphics.command_buffer_allocator.clone(),
|
||||||
|
pipeline.graphics.queue_gfx.queue_family_index(),
|
||||||
|
CommandBufferUsage::MultipleSubmit,
|
||||||
|
CommandBufferInheritanceInfo {
|
||||||
|
render_pass: Some(CommandBufferInheritanceRenderPassType::BeginRendering(
|
||||||
|
CommandBufferInheritanceRenderingInfo {
|
||||||
|
color_attachment_formats: vec![Some(pipeline.format)],
|
||||||
|
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
command_buffer
|
||||||
|
.set_viewport(0, smallvec![viewport])?
|
||||||
|
.bind_pipeline_graphics(pipeline_inner)?
|
||||||
|
.bind_descriptor_sets(
|
||||||
|
PipelineBindPoint::Graphics,
|
||||||
|
pipeline.inner().layout().clone(),
|
||||||
|
0,
|
||||||
|
descriptor_sets,
|
||||||
|
)?
|
||||||
|
.bind_vertex_buffers(0, vertex_buffer)?
|
||||||
|
.draw(
|
||||||
|
vertices.end - vertices.start,
|
||||||
|
instances.end - instances.start,
|
||||||
|
vertices.start,
|
||||||
|
instances.start,
|
||||||
|
)?
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
command_buffer: command_buffer.build()?,
|
||||||
|
_dummy: PhantomData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
286
wgui/src/gfx/pipeline.rs
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
use std::{marker::PhantomData, ops::Range, sync::Arc};
|
||||||
|
|
||||||
|
use smallvec::smallvec;
|
||||||
|
use vulkano::{
|
||||||
|
buffer::{
|
||||||
|
BufferContents, BufferUsage, IndexBuffer, Subbuffer,
|
||||||
|
allocator::{SubbufferAllocator, SubbufferAllocatorCreateInfo},
|
||||||
|
},
|
||||||
|
descriptor_set::{DescriptorSet, WriteDescriptorSet},
|
||||||
|
format::Format,
|
||||||
|
image::{
|
||||||
|
sampler::{Filter, Sampler, SamplerAddressMode, SamplerCreateInfo},
|
||||||
|
view::ImageView,
|
||||||
|
},
|
||||||
|
memory::allocator::MemoryTypeFilter,
|
||||||
|
pipeline::{
|
||||||
|
DynamicState, GraphicsPipeline, Pipeline, PipelineLayout,
|
||||||
|
graphics::{
|
||||||
|
GraphicsPipelineCreateInfo,
|
||||||
|
color_blend::{AttachmentBlend, ColorBlendAttachmentState, ColorBlendState},
|
||||||
|
input_assembly::{InputAssemblyState, PrimitiveTopology},
|
||||||
|
multisample::MultisampleState,
|
||||||
|
rasterization::RasterizationState,
|
||||||
|
subpass::PipelineRenderingCreateInfo,
|
||||||
|
vertex_input::{Vertex, VertexDefinition, VertexInputState},
|
||||||
|
viewport::ViewportState,
|
||||||
|
},
|
||||||
|
layout::PipelineDescriptorSetLayoutCreateInfo,
|
||||||
|
},
|
||||||
|
shader::{EntryPoint, ShaderModule},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{WGfx, pass::WGfxPass};
|
||||||
|
|
||||||
|
pub struct WGfxPipeline<V> {
|
||||||
|
pub graphics: Arc<WGfx>,
|
||||||
|
pub pipeline: Arc<GraphicsPipeline>,
|
||||||
|
pub format: Format,
|
||||||
|
_dummy: PhantomData<V>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V> WGfxPipeline<V>
|
||||||
|
where
|
||||||
|
V: Sized,
|
||||||
|
{
|
||||||
|
fn new_from_stages(
|
||||||
|
graphics: Arc<WGfx>,
|
||||||
|
format: Format,
|
||||||
|
blend: Option<AttachmentBlend>,
|
||||||
|
topology: PrimitiveTopology,
|
||||||
|
vert_entry_point: EntryPoint,
|
||||||
|
frag_entry_point: EntryPoint,
|
||||||
|
vertex_input_state: Option<VertexInputState>,
|
||||||
|
) -> anyhow::Result<Self> {
|
||||||
|
let stages = smallvec![
|
||||||
|
vulkano::pipeline::PipelineShaderStageCreateInfo::new(vert_entry_point),
|
||||||
|
vulkano::pipeline::PipelineShaderStageCreateInfo::new(frag_entry_point),
|
||||||
|
];
|
||||||
|
|
||||||
|
let layout = PipelineLayout::new(
|
||||||
|
graphics.device.clone(),
|
||||||
|
PipelineDescriptorSetLayoutCreateInfo::from_stages(&stages)
|
||||||
|
.into_pipeline_layout_create_info(graphics.device.clone())?,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let subpass = PipelineRenderingCreateInfo {
|
||||||
|
color_attachment_formats: vec![Some(format)],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let pipeline = GraphicsPipeline::new(
|
||||||
|
graphics.device.clone(),
|
||||||
|
None,
|
||||||
|
GraphicsPipelineCreateInfo {
|
||||||
|
stages,
|
||||||
|
vertex_input_state,
|
||||||
|
input_assembly_state: Some(InputAssemblyState {
|
||||||
|
topology,
|
||||||
|
..InputAssemblyState::default()
|
||||||
|
}),
|
||||||
|
viewport_state: Some(ViewportState::default()),
|
||||||
|
rasterization_state: Some(RasterizationState {
|
||||||
|
cull_mode: vulkano::pipeline::graphics::rasterization::CullMode::None,
|
||||||
|
..RasterizationState::default()
|
||||||
|
}),
|
||||||
|
multisample_state: Some(MultisampleState::default()),
|
||||||
|
color_blend_state: Some(ColorBlendState {
|
||||||
|
attachments: vec![ColorBlendAttachmentState {
|
||||||
|
blend,
|
||||||
|
..Default::default()
|
||||||
|
}],
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
dynamic_state: std::iter::once(DynamicState::Viewport).collect(),
|
||||||
|
subpass: Some(subpass.into()),
|
||||||
|
..GraphicsPipelineCreateInfo::layout(layout)
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
graphics,
|
||||||
|
pipeline,
|
||||||
|
format,
|
||||||
|
_dummy: PhantomData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn inner(&self) -> Arc<GraphicsPipeline> {
|
||||||
|
self.pipeline.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn uniform_sampler(
|
||||||
|
&self,
|
||||||
|
set: usize,
|
||||||
|
texture: Arc<ImageView>,
|
||||||
|
filter: Filter,
|
||||||
|
) -> anyhow::Result<Arc<DescriptorSet>> {
|
||||||
|
let sampler = Sampler::new(
|
||||||
|
self.graphics.device.clone(),
|
||||||
|
SamplerCreateInfo {
|
||||||
|
mag_filter: filter,
|
||||||
|
min_filter: filter,
|
||||||
|
address_mode: [SamplerAddressMode::Repeat; 3],
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let layout = self.pipeline.layout().set_layouts().get(set).unwrap(); // want panic
|
||||||
|
|
||||||
|
Ok(DescriptorSet::new(
|
||||||
|
self.graphics.descriptor_set_allocator.clone(),
|
||||||
|
layout.clone(),
|
||||||
|
[WriteDescriptorSet::image_view_sampler(0, texture, sampler)],
|
||||||
|
[],
|
||||||
|
)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
// uniform or storage buffer
|
||||||
|
pub fn buffer<T>(&self, set: usize, buffer: Subbuffer<[T]>) -> anyhow::Result<Arc<DescriptorSet>>
|
||||||
|
where
|
||||||
|
T: BufferContents + Copy,
|
||||||
|
{
|
||||||
|
let layout = self.pipeline.layout().set_layouts().get(set).unwrap(); // want panic
|
||||||
|
Ok(DescriptorSet::new(
|
||||||
|
self.graphics.descriptor_set_allocator.clone(),
|
||||||
|
layout.clone(),
|
||||||
|
[WriteDescriptorSet::buffer(0, buffer)],
|
||||||
|
[],
|
||||||
|
)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn uniform_buffer_upload<T>(
|
||||||
|
&self,
|
||||||
|
set: usize,
|
||||||
|
data: Vec<T>,
|
||||||
|
) -> anyhow::Result<Arc<DescriptorSet>>
|
||||||
|
where
|
||||||
|
T: BufferContents + Copy,
|
||||||
|
{
|
||||||
|
let buf = SubbufferAllocator::new(
|
||||||
|
self.graphics.memory_allocator.clone(),
|
||||||
|
SubbufferAllocatorCreateInfo {
|
||||||
|
buffer_usage: BufferUsage::UNIFORM_BUFFER,
|
||||||
|
memory_type_filter: MemoryTypeFilter::PREFER_DEVICE
|
||||||
|
| MemoryTypeFilter::HOST_SEQUENTIAL_WRITE,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let uniform_buffer_subbuffer = {
|
||||||
|
let subbuffer = buf.allocate_slice(data.len() as _)?;
|
||||||
|
subbuffer.write()?.copy_from_slice(data.as_slice());
|
||||||
|
subbuffer
|
||||||
|
};
|
||||||
|
|
||||||
|
self.buffer(set, uniform_buffer_subbuffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WGfxPipeline<()> {
|
||||||
|
pub(super) fn new_procedural(
|
||||||
|
graphics: Arc<WGfx>,
|
||||||
|
vert: Arc<ShaderModule>,
|
||||||
|
frag: Arc<ShaderModule>,
|
||||||
|
format: Format,
|
||||||
|
blend: Option<AttachmentBlend>,
|
||||||
|
topology: PrimitiveTopology,
|
||||||
|
) -> anyhow::Result<Self> {
|
||||||
|
let vert_entry_point = vert.entry_point("main").unwrap(); // want panic
|
||||||
|
let frag_entry_point = frag.entry_point("main").unwrap(); // want panic
|
||||||
|
|
||||||
|
WGfxPipeline::new_from_stages(
|
||||||
|
graphics,
|
||||||
|
format,
|
||||||
|
blend,
|
||||||
|
topology,
|
||||||
|
vert_entry_point,
|
||||||
|
frag_entry_point,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_pass_procedural(
|
||||||
|
self: &Arc<Self>,
|
||||||
|
dimensions: [f32; 2],
|
||||||
|
vertices: Range<u32>,
|
||||||
|
instances: Range<u32>,
|
||||||
|
descriptor_sets: Vec<Arc<DescriptorSet>>,
|
||||||
|
) -> anyhow::Result<WGfxPass<()>> {
|
||||||
|
WGfxPass::new_procedural(
|
||||||
|
self.clone(),
|
||||||
|
dimensions,
|
||||||
|
vertices,
|
||||||
|
instances,
|
||||||
|
descriptor_sets,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V> WGfxPipeline<V>
|
||||||
|
where
|
||||||
|
V: BufferContents + Vertex,
|
||||||
|
{
|
||||||
|
pub(super) fn new_with_vert_input(
|
||||||
|
graphics: Arc<WGfx>,
|
||||||
|
vert: Arc<ShaderModule>,
|
||||||
|
frag: Arc<ShaderModule>,
|
||||||
|
format: Format,
|
||||||
|
blend: Option<AttachmentBlend>,
|
||||||
|
topology: PrimitiveTopology,
|
||||||
|
instanced: bool,
|
||||||
|
) -> anyhow::Result<Self> {
|
||||||
|
let vert_entry_point = vert.entry_point("main").unwrap(); // want panic
|
||||||
|
let frag_entry_point = frag.entry_point("main").unwrap(); // want panic
|
||||||
|
|
||||||
|
let vertex_input_state = Some(if instanced {
|
||||||
|
V::per_instance().definition(&vert_entry_point)?
|
||||||
|
} else {
|
||||||
|
V::per_vertex().definition(&vert_entry_point)?
|
||||||
|
});
|
||||||
|
|
||||||
|
WGfxPipeline::new_from_stages(
|
||||||
|
graphics,
|
||||||
|
format,
|
||||||
|
blend,
|
||||||
|
topology,
|
||||||
|
vert_entry_point,
|
||||||
|
frag_entry_point,
|
||||||
|
vertex_input_state,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_pass(
|
||||||
|
self: &Arc<Self>,
|
||||||
|
dimensions: [f32; 2],
|
||||||
|
vertex_buffer: Subbuffer<[V]>,
|
||||||
|
vertices: Range<u32>,
|
||||||
|
instances: Range<u32>,
|
||||||
|
descriptor_sets: Vec<Arc<DescriptorSet>>,
|
||||||
|
) -> anyhow::Result<WGfxPass<V>> {
|
||||||
|
WGfxPass::new_instanced(
|
||||||
|
self.clone(),
|
||||||
|
dimensions,
|
||||||
|
vertex_buffer,
|
||||||
|
vertices,
|
||||||
|
instances,
|
||||||
|
descriptor_sets,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_pass_indexed(
|
||||||
|
self: &Arc<Self>,
|
||||||
|
dimensions: [f32; 2],
|
||||||
|
vertex_buffer: Subbuffer<[V]>,
|
||||||
|
index_buffer: IndexBuffer,
|
||||||
|
descriptor_sets: Vec<Arc<DescriptorSet>>,
|
||||||
|
) -> anyhow::Result<WGfxPass<V>> {
|
||||||
|
WGfxPass::new_indexed(
|
||||||
|
self.clone(),
|
||||||
|
dimensions,
|
||||||
|
vertex_buffer,
|
||||||
|
index_buffer,
|
||||||
|
descriptor_sets,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
319
wgui/src/layout.rs
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
sync::{Arc, Mutex},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
animation::{self, Animations},
|
||||||
|
assets::AssetProvider,
|
||||||
|
event::{self, EventListener},
|
||||||
|
transform_stack::{Transform, TransformStack},
|
||||||
|
widget::{self, EventParams, WidgetState, div::Div},
|
||||||
|
};
|
||||||
|
|
||||||
|
use glam::Vec2;
|
||||||
|
use slotmap::HopSlotMap;
|
||||||
|
use taffy::{TaffyTree, TraversePartialTree};
|
||||||
|
|
||||||
|
pub type WidgetID = slotmap::DefaultKey;
|
||||||
|
pub type BoxWidget = Arc<Mutex<WidgetState>>;
|
||||||
|
pub type WidgetMap = HopSlotMap<slotmap::DefaultKey, BoxWidget>;
|
||||||
|
|
||||||
|
struct PushEventState<'a> {
|
||||||
|
pub needs_redraw: bool,
|
||||||
|
pub animations: &'a mut Vec<animation::Animation>,
|
||||||
|
pub transform_stack: &'a mut TransformStack,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Layout {
|
||||||
|
pub tree: TaffyTree<WidgetID>,
|
||||||
|
|
||||||
|
pub assets: Box<dyn AssetProvider>,
|
||||||
|
|
||||||
|
pub widget_states: WidgetMap,
|
||||||
|
pub widget_node_map: HashMap<WidgetID, taffy::NodeId>,
|
||||||
|
|
||||||
|
pub root_widget: WidgetID,
|
||||||
|
pub root_node: taffy::NodeId,
|
||||||
|
|
||||||
|
pub prev_size: Vec2,
|
||||||
|
|
||||||
|
pub needs_redraw: bool,
|
||||||
|
|
||||||
|
pub animations: Animations,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_child_internal(
|
||||||
|
tree: &mut taffy::TaffyTree<WidgetID>,
|
||||||
|
widget_node_map: &mut HashMap<WidgetID, taffy::NodeId>,
|
||||||
|
vec: &mut WidgetMap,
|
||||||
|
parent_node: Option<taffy::NodeId>,
|
||||||
|
widget: WidgetState,
|
||||||
|
style: taffy::Style,
|
||||||
|
) -> anyhow::Result<(WidgetID, taffy::NodeId)> {
|
||||||
|
#[allow(clippy::arc_with_non_send_sync)]
|
||||||
|
let child_id = vec.insert(Arc::new(Mutex::new(widget)));
|
||||||
|
let child_node = tree.new_leaf_with_context(style, child_id)?;
|
||||||
|
|
||||||
|
if let Some(parent_node) = parent_node {
|
||||||
|
tree.add_child(parent_node, child_node)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
widget_node_map.insert(child_id, child_node);
|
||||||
|
|
||||||
|
Ok((child_id, child_node))
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Layout {
|
||||||
|
pub fn add_child(
|
||||||
|
&mut self,
|
||||||
|
parent_widget_id: WidgetID,
|
||||||
|
widget: WidgetState,
|
||||||
|
style: taffy::Style,
|
||||||
|
) -> anyhow::Result<(WidgetID, taffy::NodeId)> {
|
||||||
|
let Some(parent_node) = self.widget_node_map.get(&parent_widget_id).cloned() else {
|
||||||
|
anyhow::bail!("invalid parent widget");
|
||||||
|
};
|
||||||
|
|
||||||
|
self.needs_redraw = true;
|
||||||
|
|
||||||
|
add_child_internal(
|
||||||
|
&mut self.tree,
|
||||||
|
&mut self.widget_node_map,
|
||||||
|
&mut self.widget_states,
|
||||||
|
Some(parent_node),
|
||||||
|
widget,
|
||||||
|
style,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_event_children(
|
||||||
|
&self,
|
||||||
|
parent_node_id: taffy::NodeId,
|
||||||
|
state: &mut PushEventState,
|
||||||
|
event: &event::Event,
|
||||||
|
dirty_nodes: &mut Vec<taffy::NodeId>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
for child_id in self.tree.child_ids(parent_node_id) {
|
||||||
|
self.push_event_widget(state, child_id, event, dirty_nodes)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_event_widget(
|
||||||
|
&self,
|
||||||
|
state: &mut PushEventState,
|
||||||
|
node_id: taffy::NodeId,
|
||||||
|
event: &event::Event,
|
||||||
|
dirty_nodes: &mut Vec<taffy::NodeId>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let l = self.tree.layout(node_id)?;
|
||||||
|
let Some(widget_id) = self.tree.get_node_context(node_id).cloned() else {
|
||||||
|
anyhow::bail!("invalid widget ID");
|
||||||
|
};
|
||||||
|
|
||||||
|
let style = self.tree.style(node_id)?;
|
||||||
|
|
||||||
|
let Some(widget) = self.widget_states.get(widget_id) else {
|
||||||
|
debug_assert!(false);
|
||||||
|
anyhow::bail!("invalid widget");
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut widget = widget.lock().unwrap();
|
||||||
|
|
||||||
|
let transform = Transform {
|
||||||
|
pos: Vec2::new(l.location.x, l.location.y),
|
||||||
|
dim: Vec2::new(l.size.width, l.size.height),
|
||||||
|
transform: glam::Mat4::IDENTITY, // TODO: event transformations? Not needed for now
|
||||||
|
};
|
||||||
|
|
||||||
|
state.transform_stack.push(transform);
|
||||||
|
|
||||||
|
let mut iter_children = true;
|
||||||
|
|
||||||
|
match widget.process_event(
|
||||||
|
widget_id,
|
||||||
|
node_id,
|
||||||
|
event,
|
||||||
|
&mut EventParams {
|
||||||
|
transform_stack: state.transform_stack,
|
||||||
|
widgets: &self.widget_states,
|
||||||
|
tree: &self.tree,
|
||||||
|
animations: state.animations,
|
||||||
|
needs_redraw: &mut state.needs_redraw,
|
||||||
|
node_id,
|
||||||
|
style,
|
||||||
|
taffy_layout: l,
|
||||||
|
dirty_nodes,
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
widget::EventResult::Pass => {
|
||||||
|
// go on
|
||||||
|
}
|
||||||
|
widget::EventResult::Consumed => {
|
||||||
|
iter_children = false;
|
||||||
|
}
|
||||||
|
widget::EventResult::Outside => {
|
||||||
|
iter_children = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drop(widget); // free mutex
|
||||||
|
|
||||||
|
if iter_children {
|
||||||
|
self.push_event_children(node_id, state, event, dirty_nodes)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.transform_stack.pop();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check_toggle_needs_redraw(&mut self) -> bool {
|
||||||
|
if self.needs_redraw {
|
||||||
|
self.needs_redraw = false;
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push_event(&mut self, event: &event::Event) -> anyhow::Result<()> {
|
||||||
|
let mut transform_stack = TransformStack::new();
|
||||||
|
let mut animations_to_add = Vec::<animation::Animation>::new();
|
||||||
|
let mut dirty_nodes = Vec::new();
|
||||||
|
|
||||||
|
let mut state = PushEventState {
|
||||||
|
needs_redraw: false,
|
||||||
|
transform_stack: &mut transform_stack,
|
||||||
|
animations: &mut animations_to_add,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.push_event_widget(&mut state, self.root_node, event, &mut dirty_nodes)?;
|
||||||
|
|
||||||
|
for node in dirty_nodes {
|
||||||
|
self.tree.mark_dirty(node)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if state.needs_redraw {
|
||||||
|
self.needs_redraw = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !animations_to_add.is_empty() {
|
||||||
|
self.needs_redraw = true;
|
||||||
|
for anim in animations_to_add {
|
||||||
|
self.animations.add(anim);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new(assets: Box<dyn AssetProvider>) -> anyhow::Result<Self> {
|
||||||
|
let mut tree = TaffyTree::new();
|
||||||
|
let mut widget_node_map = HashMap::new();
|
||||||
|
let mut widget_states = HopSlotMap::new();
|
||||||
|
|
||||||
|
let (root_widget, root_node) = add_child_internal(
|
||||||
|
&mut tree,
|
||||||
|
&mut widget_node_map,
|
||||||
|
&mut widget_states,
|
||||||
|
None, // no parent
|
||||||
|
Div::create()?,
|
||||||
|
taffy::Style {
|
||||||
|
size: taffy::Size::percent(1.0),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
tree,
|
||||||
|
prev_size: Vec2::default(),
|
||||||
|
root_node,
|
||||||
|
root_widget,
|
||||||
|
widget_node_map,
|
||||||
|
widget_states,
|
||||||
|
needs_redraw: true,
|
||||||
|
animations: Animations::default(),
|
||||||
|
assets,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(&mut self, size: Vec2, timestep_alpha: f32) -> anyhow::Result<()> {
|
||||||
|
let mut dirty_nodes = Vec::new();
|
||||||
|
|
||||||
|
self.animations.process(
|
||||||
|
&self.widget_states,
|
||||||
|
&mut dirty_nodes,
|
||||||
|
timestep_alpha,
|
||||||
|
&mut self.needs_redraw,
|
||||||
|
);
|
||||||
|
|
||||||
|
for node in dirty_nodes {
|
||||||
|
self.tree.mark_dirty(node)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.tree.dirty(self.root_node)? || self.prev_size != size {
|
||||||
|
self.needs_redraw = true;
|
||||||
|
println!("re-computing layout, size {}x{}", size.x, size.y);
|
||||||
|
self.prev_size = size;
|
||||||
|
self.tree.compute_layout_with_measure(
|
||||||
|
self.root_node,
|
||||||
|
taffy::Size {
|
||||||
|
width: taffy::AvailableSpace::Definite(size.x),
|
||||||
|
height: taffy::AvailableSpace::Definite(size.y),
|
||||||
|
},
|
||||||
|
|known_dimensions, available_space, _node_id, node_context, _style| {
|
||||||
|
if let taffy::Size {
|
||||||
|
width: Some(width),
|
||||||
|
height: Some(height),
|
||||||
|
} = known_dimensions
|
||||||
|
{
|
||||||
|
return taffy::Size { width, height };
|
||||||
|
}
|
||||||
|
|
||||||
|
match node_context {
|
||||||
|
None => taffy::Size::ZERO,
|
||||||
|
Some(h) => {
|
||||||
|
if let Some(w) = self.widget_states.get(*h) {
|
||||||
|
w.lock()
|
||||||
|
.unwrap()
|
||||||
|
.obj
|
||||||
|
.measure(known_dimensions, available_space)
|
||||||
|
} else {
|
||||||
|
taffy::Size::ZERO
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tick(&mut self) -> anyhow::Result<()> {
|
||||||
|
let mut dirty_nodes = Vec::new();
|
||||||
|
|
||||||
|
self.animations.tick(
|
||||||
|
&self.widget_states,
|
||||||
|
&mut dirty_nodes,
|
||||||
|
&mut self.needs_redraw,
|
||||||
|
);
|
||||||
|
|
||||||
|
for node in dirty_nodes {
|
||||||
|
self.tree.mark_dirty(node)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// helper function
|
||||||
|
pub fn add_event_listener(&self, widget_id: WidgetID, listener: EventListener) {
|
||||||
|
let Some(widget) = self.widget_states.get(widget_id) else {
|
||||||
|
debug_assert!(false);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
widget.lock().unwrap().add_event_listener(listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
wgui/src/lib.rs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
pub mod animation;
|
||||||
|
pub mod any;
|
||||||
|
pub mod assets;
|
||||||
|
pub mod components;
|
||||||
|
pub mod drawing;
|
||||||
|
pub mod event;
|
||||||
|
pub mod gfx;
|
||||||
|
pub mod layout;
|
||||||
|
pub mod parser;
|
||||||
|
pub mod renderer_vk;
|
||||||
|
pub mod transform_stack;
|
||||||
|
pub mod widget;
|
||||||
|
|
||||||
|
// re-exported libs
|
||||||
|
pub use cosmic_text;
|
||||||
|
pub use taffy;
|
||||||
816
wgui/src/parser.rs
Normal file
@@ -0,0 +1,816 @@
|
|||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
rc::Rc,
|
||||||
|
};
|
||||||
|
|
||||||
|
use taffy::{
|
||||||
|
AlignContent, AlignItems, AlignSelf, BoxSizing, Display, FlexDirection, FlexWrap, JustifyContent,
|
||||||
|
JustifySelf, Overflow,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
drawing::{self, GradientMode},
|
||||||
|
layout::{Layout, WidgetID},
|
||||||
|
renderer_vk::text::{
|
||||||
|
FontWeight, HorizontalAlign,
|
||||||
|
custom_glyph::{CustomGlyphContent, CustomGlyphData},
|
||||||
|
},
|
||||||
|
widget::{
|
||||||
|
div::Div,
|
||||||
|
rectangle::{Rectangle, RectangleParams},
|
||||||
|
sprite::{SpriteBox, SpriteBoxParams},
|
||||||
|
text::{TextLabel, TextParams},
|
||||||
|
util::WLength,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
type VarMap = HashMap<Rc<str>, Rc<str>>;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct ParserState {
|
||||||
|
pub ids: HashMap<Rc<str>, WidgetID>,
|
||||||
|
pub var_map: VarMap,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ParserState {
|
||||||
|
pub fn require_by_id(&self, id: &str) -> anyhow::Result<WidgetID> {
|
||||||
|
match self.ids.get(id) {
|
||||||
|
Some(id) => Ok(*id),
|
||||||
|
None => anyhow::bail!("Widget by ID \"{}\" doesn't exist", id),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ParserContext<'a> {
|
||||||
|
layout: &'a mut Layout,
|
||||||
|
state: &'a mut ParserState,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ParserFile<'a> {
|
||||||
|
path: PathBuf,
|
||||||
|
ctx: &'a mut ParserContext<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parses a color from a HTML hex string
|
||||||
|
pub fn parse_color_hex(html_hex: &str) -> Option<drawing::Color> {
|
||||||
|
if html_hex.len() == 7 {
|
||||||
|
if let (Ok(r), Ok(g), Ok(b)) = (
|
||||||
|
u8::from_str_radix(&html_hex[1..3], 16),
|
||||||
|
u8::from_str_radix(&html_hex[3..5], 16),
|
||||||
|
u8::from_str_radix(&html_hex[5..7], 16),
|
||||||
|
) {
|
||||||
|
return Some(drawing::Color::new(
|
||||||
|
f32::from(r) / 255.,
|
||||||
|
f32::from(g) / 255.,
|
||||||
|
f32::from(b) / 255.,
|
||||||
|
1.,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else if html_hex.len() == 9 {
|
||||||
|
if let (Ok(r), Ok(g), Ok(b), Ok(a)) = (
|
||||||
|
u8::from_str_radix(&html_hex[1..3], 16),
|
||||||
|
u8::from_str_radix(&html_hex[3..5], 16),
|
||||||
|
u8::from_str_radix(&html_hex[5..7], 16),
|
||||||
|
u8::from_str_radix(&html_hex[7..9], 16),
|
||||||
|
) {
|
||||||
|
return Some(drawing::Color::new(
|
||||||
|
f32::from(r) / 255.,
|
||||||
|
f32::from(g) / 255.,
|
||||||
|
f32::from(b) / 255.,
|
||||||
|
f32::from(a) / 255.,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log::warn!("failed to parse color \"{}\"", html_hex);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_tag_by_name<'a>(
|
||||||
|
node: roxmltree::Node<'a, 'a>,
|
||||||
|
name: &str,
|
||||||
|
) -> Option<roxmltree::Node<'a, 'a>> {
|
||||||
|
node
|
||||||
|
.children()
|
||||||
|
.find(|&child| child.tag_name().name() == name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn require_tag_by_name<'a>(
|
||||||
|
node: roxmltree::Node<'a, 'a>,
|
||||||
|
name: &str,
|
||||||
|
) -> anyhow::Result<roxmltree::Node<'a, 'a>> {
|
||||||
|
get_tag_by_name(node, name).ok_or_else(|| anyhow::anyhow!("Tag \"{}\" not found", name))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_invalid_attrib(key: &str, value: &str) {
|
||||||
|
log::warn!("Invalid value \"{}\" in attribute \"{}\"", value, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_missing_attrib(tag_name: &str, attr: &str) {
|
||||||
|
log::warn!("Missing attribute {} in tag <{}>", attr, tag_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_invalid_value(value: &str) {
|
||||||
|
log::warn!("Invalid value \"{}\"", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_val(value: &Rc<str>) -> Option<f32> {
|
||||||
|
let Ok(val) = value.parse::<f32>() else {
|
||||||
|
print_invalid_value(value);
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
Some(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_percent(value: &str) -> bool {
|
||||||
|
value.ends_with("%")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_percent(value: &str) -> Option<f32> {
|
||||||
|
let Some(val_str) = value.split("%").next() else {
|
||||||
|
print_invalid_value(value);
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Ok(val) = val_str.parse::<f32>() else {
|
||||||
|
print_invalid_value(value);
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
Some(val / 100.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_f32(value: &str) -> Option<f32> {
|
||||||
|
value.parse::<f32>().ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_size_unit<T>(value: &str) -> Option<T>
|
||||||
|
where
|
||||||
|
T: taffy::prelude::FromPercent + taffy::prelude::FromLength,
|
||||||
|
{
|
||||||
|
if is_percent(value) {
|
||||||
|
Some(taffy::prelude::percent(parse_percent(value)?))
|
||||||
|
} else {
|
||||||
|
Some(taffy::prelude::length(parse_f32(value)?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn style_from_node<'a>(file: &mut ParserFile, node: roxmltree::Node<'a, 'a>) -> taffy::Style {
|
||||||
|
let mut style = taffy::Style {
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
for (key, value) in iter_attribs(&mut file.ctx.state.var_map, &node) {
|
||||||
|
match &*key {
|
||||||
|
"display" => match &*value {
|
||||||
|
"flex" => style.display = Display::Flex,
|
||||||
|
"block" => style.display = Display::Block,
|
||||||
|
"grid" => style.display = Display::Grid,
|
||||||
|
_ => {
|
||||||
|
print_invalid_attrib(&key, &value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"margin_left" => {
|
||||||
|
if let Some(dim) = parse_size_unit(&value) {
|
||||||
|
style.margin.left = dim;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"margin_right" => {
|
||||||
|
if let Some(dim) = parse_size_unit(&value) {
|
||||||
|
style.margin.right = dim;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"margin_top" => {
|
||||||
|
if let Some(dim) = parse_size_unit(&value) {
|
||||||
|
style.margin.top = dim;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"margin_bottom" => {
|
||||||
|
if let Some(dim) = parse_size_unit(&value) {
|
||||||
|
style.margin.bottom = dim;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"padding_left" => {
|
||||||
|
if let Some(dim) = parse_size_unit(&value) {
|
||||||
|
style.padding.left = dim;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"padding_right" => {
|
||||||
|
if let Some(dim) = parse_size_unit(&value) {
|
||||||
|
style.padding.right = dim;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"padding_top" => {
|
||||||
|
if let Some(dim) = parse_size_unit(&value) {
|
||||||
|
style.padding.top = dim;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"padding_bottom" => {
|
||||||
|
if let Some(dim) = parse_size_unit(&value) {
|
||||||
|
style.padding.bottom = dim;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"margin" => {
|
||||||
|
if let Some(dim) = parse_size_unit(&value) {
|
||||||
|
style.margin.left = dim;
|
||||||
|
style.margin.right = dim;
|
||||||
|
style.margin.top = dim;
|
||||||
|
style.margin.bottom = dim;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"padding" => {
|
||||||
|
if let Some(dim) = parse_size_unit(&value) {
|
||||||
|
style.padding.left = dim;
|
||||||
|
style.padding.right = dim;
|
||||||
|
style.padding.top = dim;
|
||||||
|
style.padding.bottom = dim;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"overflow_x" => match &*value {
|
||||||
|
"hidden" => style.overflow.x = Overflow::Hidden,
|
||||||
|
"visible" => style.overflow.x = Overflow::Visible,
|
||||||
|
"clip" => style.overflow.x = Overflow::Clip,
|
||||||
|
"scroll" => style.overflow.x = Overflow::Scroll,
|
||||||
|
_ => {
|
||||||
|
print_invalid_attrib(&key, &value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overflow_y" => match &*value {
|
||||||
|
"hidden" => style.overflow.y = Overflow::Hidden,
|
||||||
|
"visible" => style.overflow.y = Overflow::Visible,
|
||||||
|
"clip" => style.overflow.y = Overflow::Clip,
|
||||||
|
"scroll" => style.overflow.y = Overflow::Scroll,
|
||||||
|
_ => {
|
||||||
|
print_invalid_attrib(&key, &value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"min_width" => {
|
||||||
|
if let Some(dim) = parse_size_unit(&value) {
|
||||||
|
style.min_size.width = dim;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"min_height" => {
|
||||||
|
if let Some(dim) = parse_size_unit(&value) {
|
||||||
|
style.min_size.height = dim;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"max_width" => {
|
||||||
|
if let Some(dim) = parse_size_unit(&value) {
|
||||||
|
style.max_size.width = dim;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"max_height" => {
|
||||||
|
if let Some(dim) = parse_size_unit(&value) {
|
||||||
|
style.max_size.height = dim;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"width" => {
|
||||||
|
if let Some(dim) = parse_size_unit(&value) {
|
||||||
|
style.size.width = dim;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"height" => {
|
||||||
|
if let Some(dim) = parse_size_unit(&value) {
|
||||||
|
style.size.height = dim;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"gap" => {
|
||||||
|
if let Some(val) = parse_size_unit(&value) {
|
||||||
|
style.gap = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"flex_basis" => {
|
||||||
|
if let Some(val) = parse_size_unit(&value) {
|
||||||
|
style.flex_basis = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"flex_grow" => {
|
||||||
|
if let Some(val) = parse_val(&value) {
|
||||||
|
style.flex_grow = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"flex_shrink" => {
|
||||||
|
if let Some(val) = parse_val(&value) {
|
||||||
|
style.flex_shrink = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"position" => match &*value {
|
||||||
|
"absolute" => style.position = taffy::Position::Absolute,
|
||||||
|
"relative" => style.position = taffy::Position::Relative,
|
||||||
|
_ => {
|
||||||
|
print_invalid_attrib(&key, &value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"box_sizing" => match &*value {
|
||||||
|
"border_box" => style.box_sizing = BoxSizing::BorderBox,
|
||||||
|
"content_box" => style.box_sizing = BoxSizing::ContentBox,
|
||||||
|
_ => {
|
||||||
|
print_invalid_attrib(&key, &value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"align_self" => match &*value {
|
||||||
|
"baseline" => style.align_self = Some(AlignSelf::Baseline),
|
||||||
|
"center" => style.align_self = Some(AlignSelf::Center),
|
||||||
|
"end" => style.align_self = Some(AlignSelf::End),
|
||||||
|
"flex_end" => style.align_self = Some(AlignSelf::FlexEnd),
|
||||||
|
"flex_start" => style.align_self = Some(AlignSelf::FlexStart),
|
||||||
|
"start" => style.align_self = Some(AlignSelf::Start),
|
||||||
|
"stretch" => style.align_self = Some(AlignSelf::Stretch),
|
||||||
|
_ => {
|
||||||
|
print_invalid_attrib(&key, &value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"justify_self" => match &*value {
|
||||||
|
"center" => style.justify_self = Some(JustifySelf::Center),
|
||||||
|
"end" => style.justify_self = Some(JustifySelf::End),
|
||||||
|
"flex_end" => style.justify_self = Some(JustifySelf::FlexEnd),
|
||||||
|
"flex_start" => style.justify_self = Some(JustifySelf::FlexStart),
|
||||||
|
"start" => style.justify_self = Some(JustifySelf::Start),
|
||||||
|
"stretch" => style.justify_self = Some(JustifySelf::Stretch),
|
||||||
|
_ => {
|
||||||
|
print_invalid_attrib(&key, &value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"align_items" => match &*value {
|
||||||
|
"baseline" => style.align_items = Some(AlignItems::Baseline),
|
||||||
|
"center" => style.align_items = Some(AlignItems::Center),
|
||||||
|
"end" => style.align_items = Some(AlignItems::End),
|
||||||
|
"flex_end" => style.align_items = Some(AlignItems::FlexEnd),
|
||||||
|
"flex_start" => style.align_items = Some(AlignItems::FlexStart),
|
||||||
|
"start" => style.align_items = Some(AlignItems::Start),
|
||||||
|
"stretch" => style.align_items = Some(AlignItems::Stretch),
|
||||||
|
_ => {
|
||||||
|
print_invalid_attrib(&key, &value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"align_content" => match &*value {
|
||||||
|
"center" => style.align_content = Some(AlignContent::Center),
|
||||||
|
"end" => style.align_content = Some(AlignContent::End),
|
||||||
|
"flex_end" => style.align_content = Some(AlignContent::FlexEnd),
|
||||||
|
"flex_start" => style.align_content = Some(AlignContent::FlexStart),
|
||||||
|
"space_around" => style.align_content = Some(AlignContent::SpaceAround),
|
||||||
|
"space_between" => style.align_content = Some(AlignContent::SpaceBetween),
|
||||||
|
"space_evenly" => style.align_content = Some(AlignContent::SpaceEvenly),
|
||||||
|
"start" => style.align_content = Some(AlignContent::Start),
|
||||||
|
"stretch" => style.align_content = Some(AlignContent::Stretch),
|
||||||
|
_ => {
|
||||||
|
print_invalid_attrib(&key, &value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"justify_content" => match &*value {
|
||||||
|
"center" => style.justify_content = Some(JustifyContent::Center),
|
||||||
|
"end" => style.justify_content = Some(JustifyContent::End),
|
||||||
|
"flex_end" => style.justify_content = Some(JustifyContent::FlexEnd),
|
||||||
|
"flex_start" => style.justify_content = Some(JustifyContent::FlexStart),
|
||||||
|
"space_around" => style.justify_content = Some(JustifyContent::SpaceAround),
|
||||||
|
"space_between" => style.justify_content = Some(JustifyContent::SpaceBetween),
|
||||||
|
"space_evenly" => style.justify_content = Some(JustifyContent::SpaceEvenly),
|
||||||
|
"start" => style.justify_content = Some(JustifyContent::Start),
|
||||||
|
"stretch" => style.justify_content = Some(JustifyContent::Stretch),
|
||||||
|
_ => {
|
||||||
|
print_invalid_attrib(&key, &value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flex_wrap" => match &*value {
|
||||||
|
"wrap" => style.flex_wrap = FlexWrap::Wrap,
|
||||||
|
"no_wrap" => style.flex_wrap = FlexWrap::NoWrap,
|
||||||
|
"wrap_reverse" => style.flex_wrap = FlexWrap::WrapReverse,
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
"flex_direction" => match &*value {
|
||||||
|
"column_reverse" => style.flex_direction = FlexDirection::ColumnReverse,
|
||||||
|
"column" => style.flex_direction = FlexDirection::Column,
|
||||||
|
"row_reverse" => style.flex_direction = FlexDirection::RowReverse,
|
||||||
|
"row" => style.flex_direction = FlexDirection::Row,
|
||||||
|
_ => {
|
||||||
|
print_invalid_attrib(&key, &value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
style
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_widget_div<'a>(
|
||||||
|
file: &mut ParserFile,
|
||||||
|
node: roxmltree::Node<'a, 'a>,
|
||||||
|
parent_id: WidgetID,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let style = style_from_node(file, node);
|
||||||
|
|
||||||
|
let (new_id, _) = file
|
||||||
|
.ctx
|
||||||
|
.layout
|
||||||
|
.add_child(parent_id, Div::create()?, style)?;
|
||||||
|
|
||||||
|
parse_universal(file, node, new_id)?;
|
||||||
|
parse_children(file, node, new_id)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_widget_rectangle<'a>(
|
||||||
|
file: &mut ParserFile,
|
||||||
|
node: roxmltree::Node<'a, 'a>,
|
||||||
|
parent_id: WidgetID,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let mut params = RectangleParams::default();
|
||||||
|
|
||||||
|
for (key, value) in iter_attribs(&mut file.ctx.state.var_map, &node) {
|
||||||
|
match &*key {
|
||||||
|
"color" => {
|
||||||
|
if let Some(color) = parse_color_hex(&value) {
|
||||||
|
params.color = color;
|
||||||
|
} else {
|
||||||
|
print_invalid_attrib(&key, &value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"color2" => {
|
||||||
|
if let Some(color) = parse_color_hex(&value) {
|
||||||
|
params.color2 = color;
|
||||||
|
} else {
|
||||||
|
print_invalid_attrib(&key, &value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"gradient" => {
|
||||||
|
params.gradient = match &*value {
|
||||||
|
"horizontal" => GradientMode::Horizontal,
|
||||||
|
"vertical" => GradientMode::Vertical,
|
||||||
|
"radial" => GradientMode::Radial,
|
||||||
|
"none" => GradientMode::None,
|
||||||
|
_ => {
|
||||||
|
print_invalid_attrib(&key, &value);
|
||||||
|
GradientMode::None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"round" => {
|
||||||
|
if is_percent(&value) {
|
||||||
|
if let Some(val) = parse_percent(&value) {
|
||||||
|
params.round = WLength::Percent(val);
|
||||||
|
} else {
|
||||||
|
print_invalid_value(&value);
|
||||||
|
}
|
||||||
|
} else if let Some(val) = parse_f32(&value) {
|
||||||
|
params.round = WLength::Units(val);
|
||||||
|
} else {
|
||||||
|
print_invalid_value(&value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"border" => {
|
||||||
|
params.border = value.parse().unwrap_or_else(|_| {
|
||||||
|
print_invalid_attrib(&key, &value);
|
||||||
|
0.0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
"border_color" => {
|
||||||
|
if let Some(color) = parse_color_hex(&value) {
|
||||||
|
params.border_color = color;
|
||||||
|
} else {
|
||||||
|
print_invalid_attrib(&key, &value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let style = style_from_node(file, node);
|
||||||
|
|
||||||
|
let (new_id, _) = file
|
||||||
|
.ctx
|
||||||
|
.layout
|
||||||
|
.add_child(parent_id, Rectangle::create(params)?, style)?;
|
||||||
|
|
||||||
|
parse_universal(file, node, new_id)?;
|
||||||
|
parse_children(file, node, new_id)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_widget_sprite<'a>(
|
||||||
|
file: &mut ParserFile,
|
||||||
|
node: roxmltree::Node<'a, 'a>,
|
||||||
|
parent_id: WidgetID,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let mut params = SpriteBoxParams::default();
|
||||||
|
|
||||||
|
let mut glyph = None;
|
||||||
|
for (key, value) in iter_attribs(&mut file.ctx.state.var_map, &node) {
|
||||||
|
match &*key {
|
||||||
|
"src" => {
|
||||||
|
glyph = match CustomGlyphContent::from_assets(&mut file.ctx.layout.assets, &value) {
|
||||||
|
Ok(glyph) => Some(glyph),
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("failed to load {}: {}", value, e);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"src_ext" => {
|
||||||
|
if std::fs::exists(value.as_ref()).unwrap_or(false) {
|
||||||
|
glyph = CustomGlyphContent::from_file(&value).ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(glyph) = glyph {
|
||||||
|
params.glyph_data = Some(CustomGlyphData::new(glyph));
|
||||||
|
} else {
|
||||||
|
log::warn!("No source for sprite node!");
|
||||||
|
};
|
||||||
|
|
||||||
|
let style = style_from_node(file, node);
|
||||||
|
|
||||||
|
let (new_id, _) = file
|
||||||
|
.ctx
|
||||||
|
.layout
|
||||||
|
.add_child(parent_id, SpriteBox::create(params)?, style)?;
|
||||||
|
|
||||||
|
parse_universal(file, node, new_id)?;
|
||||||
|
parse_children(file, node, new_id)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_widget_label<'a>(
|
||||||
|
file: &mut ParserFile,
|
||||||
|
node: roxmltree::Node<'a, 'a>,
|
||||||
|
parent_id: WidgetID,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let mut params = TextParams::default();
|
||||||
|
|
||||||
|
for (key, value) in iter_attribs(&mut file.ctx.state.var_map, &node) {
|
||||||
|
match &*key {
|
||||||
|
"text" => {
|
||||||
|
params.content = String::from(value.as_ref());
|
||||||
|
}
|
||||||
|
"color" => {
|
||||||
|
if let Some(color) = parse_color_hex(&value) {
|
||||||
|
params.style.color = Some(color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"align" => match &*value {
|
||||||
|
"left" => params.style.align = Some(HorizontalAlign::Left),
|
||||||
|
"right" => params.style.align = Some(HorizontalAlign::Right),
|
||||||
|
"center" => params.style.align = Some(HorizontalAlign::Center),
|
||||||
|
"justified" => params.style.align = Some(HorizontalAlign::Justified),
|
||||||
|
"end" => params.style.align = Some(HorizontalAlign::End),
|
||||||
|
_ => {
|
||||||
|
print_invalid_attrib(&key, &value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"weight" => match &*value {
|
||||||
|
"normal" => params.style.weight = Some(FontWeight::Normal),
|
||||||
|
"bold" => params.style.weight = Some(FontWeight::Bold),
|
||||||
|
_ => {
|
||||||
|
print_invalid_attrib(&key, &value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"size" => {
|
||||||
|
if let Ok(size) = value.parse::<f32>() {
|
||||||
|
params.style.size = Some(size);
|
||||||
|
} else {
|
||||||
|
print_invalid_attrib(&key, &value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let style = style_from_node(file, node);
|
||||||
|
|
||||||
|
let (new_id, _) = file
|
||||||
|
.ctx
|
||||||
|
.layout
|
||||||
|
.add_child(parent_id, TextLabel::create(params)?, style)?;
|
||||||
|
|
||||||
|
parse_universal(file, node, new_id)?;
|
||||||
|
parse_children(file, node, new_id)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_tag_include<'a>(
|
||||||
|
file: &'a mut ParserFile,
|
||||||
|
node: roxmltree::Node<'a, 'a>,
|
||||||
|
parent_id: WidgetID,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
for attrib in node.attributes() {
|
||||||
|
let (key, value) = (attrib.name(), attrib.value());
|
||||||
|
|
||||||
|
#[allow(clippy::single_match)]
|
||||||
|
match key {
|
||||||
|
"src" => {
|
||||||
|
let mut new_path = file.path.parent().unwrap_or(Path::new("/")).to_path_buf();
|
||||||
|
new_path.push(value);
|
||||||
|
parse_from_assets_internal(file, parent_id, new_path.clone())?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
print_invalid_attrib(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_tag_var<'a>(file: &mut ParserFile, node: roxmltree::Node<'a, 'a>) -> anyhow::Result<()> {
|
||||||
|
let mut out_key: Option<&str> = None;
|
||||||
|
let mut out_value: Option<&str> = None;
|
||||||
|
|
||||||
|
for attrib in node.attributes() {
|
||||||
|
let (key, value) = (attrib.name(), attrib.value());
|
||||||
|
|
||||||
|
match key {
|
||||||
|
"key" => {
|
||||||
|
out_key = Some(value);
|
||||||
|
}
|
||||||
|
"value" => {
|
||||||
|
out_value = Some(value);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
print_invalid_attrib(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(key) = out_key else {
|
||||||
|
print_missing_attrib("var", "key");
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(value) = out_value else {
|
||||||
|
print_missing_attrib("var", "value");
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
file
|
||||||
|
.ctx
|
||||||
|
.state
|
||||||
|
.var_map
|
||||||
|
.insert(Rc::from(key), Rc::from(value));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::manual_strip)]
|
||||||
|
pub fn iter_attribs<'a>(
|
||||||
|
var_map: &'a mut VarMap,
|
||||||
|
node: &roxmltree::Node<'a, 'a>,
|
||||||
|
) -> impl Iterator<Item = (/*key*/ Rc<str>, /*value*/ Rc<str>)> + 'a {
|
||||||
|
node.attributes().map(|attrib| {
|
||||||
|
let (key, value) = (attrib.name(), attrib.value());
|
||||||
|
|
||||||
|
if value.starts_with("~") {
|
||||||
|
let name = &value[1..];
|
||||||
|
|
||||||
|
return (
|
||||||
|
Rc::from(key),
|
||||||
|
match var_map.get(name) {
|
||||||
|
Some(name) => name.clone(),
|
||||||
|
None => Rc::from("undefined"),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
(Rc::from(key), Rc::from(value))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_tag_theme<'a>(file: &mut ParserFile, node: roxmltree::Node<'a, 'a>) -> anyhow::Result<()> {
|
||||||
|
for child_node in node.children() {
|
||||||
|
let child_name = child_node.tag_name().name();
|
||||||
|
match child_name {
|
||||||
|
"var" => {
|
||||||
|
parse_tag_var(file, child_node)?;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
print_invalid_value(child_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_universal<'a>(
|
||||||
|
file: &mut ParserFile,
|
||||||
|
node: roxmltree::Node<'a, 'a>,
|
||||||
|
widget_id: WidgetID,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
for (key, value) in iter_attribs(&mut file.ctx.state.var_map, &node) {
|
||||||
|
#[allow(clippy::single_match)]
|
||||||
|
match &*key {
|
||||||
|
"id" => {
|
||||||
|
// Attach a specific widget to name-ID map (just like getElementById)
|
||||||
|
if file
|
||||||
|
.ctx
|
||||||
|
.state
|
||||||
|
.ids
|
||||||
|
.insert(value.clone(), widget_id)
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
log::warn!("duplicate ID \"{}\" in the same layout file!", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_children<'a>(
|
||||||
|
file: &mut ParserFile,
|
||||||
|
node: roxmltree::Node<'a, 'a>,
|
||||||
|
parent_id: WidgetID,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
for child_node in node.children() {
|
||||||
|
match child_node.tag_name().name() {
|
||||||
|
"include" => {
|
||||||
|
parse_tag_include(file, child_node, parent_id)?;
|
||||||
|
}
|
||||||
|
"div" => {
|
||||||
|
parse_widget_div(file, child_node, parent_id)?;
|
||||||
|
}
|
||||||
|
"rectangle" => {
|
||||||
|
parse_widget_rectangle(file, child_node, parent_id)?;
|
||||||
|
}
|
||||||
|
"label" => {
|
||||||
|
parse_widget_label(file, child_node, parent_id)?;
|
||||||
|
}
|
||||||
|
"sprite" => {
|
||||||
|
parse_widget_sprite(file, child_node, parent_id)?;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_from_assets_internal(
|
||||||
|
file: &mut ParserFile,
|
||||||
|
parent_id: WidgetID,
|
||||||
|
path: PathBuf,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let data = file
|
||||||
|
.ctx
|
||||||
|
.layout
|
||||||
|
.assets
|
||||||
|
.load_from_path(&path.to_string_lossy())?;
|
||||||
|
let data = std::str::from_utf8(&data)?;
|
||||||
|
parse_str(file, parent_id, data)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_from_assets(
|
||||||
|
layout: &mut Layout,
|
||||||
|
parent_id: WidgetID,
|
||||||
|
path: &str,
|
||||||
|
) -> anyhow::Result<ParserState> {
|
||||||
|
let path = PathBuf::from(path);
|
||||||
|
let mut result = ParserState::default();
|
||||||
|
let mut ctx = ParserContext {
|
||||||
|
layout,
|
||||||
|
state: &mut result,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut file = ParserFile {
|
||||||
|
ctx: &mut ctx,
|
||||||
|
path: path.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
parse_from_assets_internal(&mut file, parent_id, path)?;
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_str(file: &mut ParserFile, parent_id: WidgetID, xml: &str) -> anyhow::Result<()> {
|
||||||
|
let opt = roxmltree::ParsingOptions {
|
||||||
|
allow_dtd: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let doc = roxmltree::Document::parse_with_options(xml, opt)?;
|
||||||
|
let root = doc.root();
|
||||||
|
let tag_layout = require_tag_by_name(root, "layout")?;
|
||||||
|
|
||||||
|
for child in tag_layout.children() {
|
||||||
|
#[allow(clippy::single_match)]
|
||||||
|
match child.tag_name().name() {
|
||||||
|
/* topmost include directly in <layout> */
|
||||||
|
"include" => parse_tag_include(file, child, parent_id)?,
|
||||||
|
"theme" => parse_tag_theme(file, child)?,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(tag_elements) = get_tag_by_name(tag_layout, "elements") {
|
||||||
|
parse_children(file, tag_elements, parent_id)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
221
wgui/src/renderer_vk/context.rs
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
use std::{cell::RefCell, rc::Rc, sync::Arc};
|
||||||
|
|
||||||
|
use cosmic_text::Buffer;
|
||||||
|
use glam::{Mat4, Vec2, Vec3};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
drawing,
|
||||||
|
gfx::{WGfx, cmd::GfxCommandBuffer},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
rect::{RectPipeline, RectRenderer},
|
||||||
|
text::{
|
||||||
|
DEFAULT_METRICS, FONT_SYSTEM, SWASH_CACHE, TextArea, TextBounds,
|
||||||
|
text_atlas::{TextAtlas, TextPipeline},
|
||||||
|
text_renderer::TextRenderer,
|
||||||
|
},
|
||||||
|
viewport::Viewport,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct RendererPass<'a> {
|
||||||
|
submitted: bool,
|
||||||
|
text_areas: Vec<TextArea<'a>>,
|
||||||
|
text_renderer: TextRenderer,
|
||||||
|
rect_renderer: RectRenderer,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RendererPass<'_> {
|
||||||
|
fn new(text_atlas: &mut TextAtlas, rect_pipeline: RectPipeline) -> anyhow::Result<Self> {
|
||||||
|
let text_renderer = TextRenderer::new(text_atlas)?;
|
||||||
|
let rect_renderer = RectRenderer::new(rect_pipeline)?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
submitted: false,
|
||||||
|
text_renderer,
|
||||||
|
rect_renderer,
|
||||||
|
text_areas: Vec::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn submit(
|
||||||
|
&mut self,
|
||||||
|
gfx: &Arc<WGfx>,
|
||||||
|
viewport: &mut Viewport,
|
||||||
|
cmd_buf: &mut GfxCommandBuffer,
|
||||||
|
text_atlas: &mut TextAtlas,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
if self.submitted {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
self.submitted = true;
|
||||||
|
self.rect_renderer.render(gfx, viewport, cmd_buf)?;
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut font_system = FONT_SYSTEM.lock().unwrap();
|
||||||
|
let mut swash_cache = SWASH_CACHE.lock().unwrap();
|
||||||
|
|
||||||
|
self.text_renderer.prepare(
|
||||||
|
&mut font_system,
|
||||||
|
text_atlas,
|
||||||
|
viewport,
|
||||||
|
std::mem::take(&mut self.text_areas),
|
||||||
|
&mut swash_cache,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.text_renderer.render(text_atlas, viewport, cmd_buf)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Context {
|
||||||
|
viewport: Viewport,
|
||||||
|
text_atlas: TextAtlas,
|
||||||
|
rect_pipeline: RectPipeline,
|
||||||
|
text_pipeline: TextPipeline,
|
||||||
|
pixel_scale: f32,
|
||||||
|
pub dirty: bool,
|
||||||
|
empty_text: Rc<RefCell<Buffer>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Context {
|
||||||
|
pub fn new(
|
||||||
|
gfx: Arc<WGfx>,
|
||||||
|
native_format: vulkano::format::Format,
|
||||||
|
pixel_scale: f32,
|
||||||
|
) -> anyhow::Result<Self> {
|
||||||
|
let rect_pipeline = RectPipeline::new(gfx.clone(), native_format)?;
|
||||||
|
let text_pipeline = TextPipeline::new(gfx.clone(), native_format)?;
|
||||||
|
let viewport = Viewport::new(gfx.clone())?;
|
||||||
|
let text_atlas = TextAtlas::new(text_pipeline.clone())?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
viewport,
|
||||||
|
text_atlas,
|
||||||
|
rect_pipeline,
|
||||||
|
text_pipeline,
|
||||||
|
pixel_scale,
|
||||||
|
dirty: true,
|
||||||
|
empty_text: Rc::new(RefCell::new(Buffer::new_empty(DEFAULT_METRICS))),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn regen(&mut self) -> anyhow::Result<()> {
|
||||||
|
self.text_atlas = TextAtlas::new(self.text_pipeline.clone())?;
|
||||||
|
self.dirty = true;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_viewport(&mut self, resolution: [u32; 2], pixel_scale: f32) -> anyhow::Result<()> {
|
||||||
|
if self.pixel_scale != pixel_scale {
|
||||||
|
self.pixel_scale = pixel_scale;
|
||||||
|
self.regen()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.viewport.resolution() != resolution {
|
||||||
|
self.dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let size = Vec2::new(
|
||||||
|
resolution[0] as f32 / pixel_scale,
|
||||||
|
resolution[1] as f32 / pixel_scale,
|
||||||
|
);
|
||||||
|
|
||||||
|
let fov = 0.4;
|
||||||
|
let aspect_ratio = size.x / size.y;
|
||||||
|
let projection = Mat4::perspective_rh(fov, aspect_ratio, 1.0, 100000.0);
|
||||||
|
|
||||||
|
let b = size.y / 2.0;
|
||||||
|
let angle_half = fov / 2.0;
|
||||||
|
let distance = (std::f32::consts::PI / 2.0 - angle_half).tan() * b;
|
||||||
|
|
||||||
|
let view = Mat4::look_at_rh(
|
||||||
|
Vec3::new(size.x / 2.0, size.y / 2.0, distance),
|
||||||
|
Vec3::new(size.x / 2.0, size.y / 2.0, 0.0),
|
||||||
|
Vec3::new(0.0, 1.0, 0.0),
|
||||||
|
);
|
||||||
|
|
||||||
|
let fin = projection * view;
|
||||||
|
|
||||||
|
self.viewport.update(resolution, &fin, pixel_scale)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_pass(&mut self, passes: &mut Vec<RendererPass>) -> anyhow::Result<()> {
|
||||||
|
passes.push(RendererPass::new(
|
||||||
|
&mut self.text_atlas,
|
||||||
|
self.rect_pipeline.clone(),
|
||||||
|
)?);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn submit_pass(
|
||||||
|
&mut self,
|
||||||
|
gfx: &Arc<WGfx>,
|
||||||
|
cmd_buf: &mut GfxCommandBuffer,
|
||||||
|
pass: &mut RendererPass,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
pass.submit(gfx, &mut self.viewport, cmd_buf, &mut self.text_atlas)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw(
|
||||||
|
&mut self,
|
||||||
|
gfx: &Arc<WGfx>,
|
||||||
|
cmd_buf: &mut GfxCommandBuffer,
|
||||||
|
primitives: &[drawing::RenderPrimitive],
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
self.dirty = false;
|
||||||
|
let mut passes = Vec::<RendererPass>::new();
|
||||||
|
self.new_pass(&mut passes)?;
|
||||||
|
|
||||||
|
for primitive in primitives.iter() {
|
||||||
|
let pass = passes.last_mut().unwrap(); // always safe
|
||||||
|
|
||||||
|
match &primitive.payload {
|
||||||
|
drawing::PrimitivePayload::Rectangle(rectangle) => {
|
||||||
|
pass.rect_renderer.add_rect(
|
||||||
|
primitive.boundary,
|
||||||
|
*rectangle,
|
||||||
|
&primitive.transform,
|
||||||
|
primitive.depth,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
drawing::PrimitivePayload::Text(text) => {
|
||||||
|
pass.text_areas.push(TextArea {
|
||||||
|
buffer: text.clone(),
|
||||||
|
left: primitive.boundary.pos.x * self.pixel_scale,
|
||||||
|
top: primitive.boundary.pos.y * self.pixel_scale,
|
||||||
|
bounds: TextBounds::default(), //FIXME: just using boundary coords here doesn't work
|
||||||
|
scale: self.pixel_scale,
|
||||||
|
default_color: cosmic_text::Color::rgb(0, 0, 0),
|
||||||
|
custom_glyphs: &[],
|
||||||
|
depth: primitive.depth,
|
||||||
|
transform: primitive.transform,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
drawing::PrimitivePayload::Sprite(sprites) => {
|
||||||
|
pass.text_areas.push(TextArea {
|
||||||
|
buffer: self.empty_text.clone(),
|
||||||
|
left: primitive.boundary.pos.x * self.pixel_scale,
|
||||||
|
top: primitive.boundary.pos.y * self.pixel_scale,
|
||||||
|
bounds: TextBounds::default(),
|
||||||
|
scale: self.pixel_scale,
|
||||||
|
custom_glyphs: sprites.as_slice(),
|
||||||
|
default_color: cosmic_text::Color::rgb(255, 0, 255),
|
||||||
|
depth: primitive.depth,
|
||||||
|
transform: primitive.transform,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let pass = passes.last_mut().unwrap();
|
||||||
|
self.submit_pass(gfx, cmd_buf, pass)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
6
wgui/src/renderer_vk/mod.rs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
pub mod context;
|
||||||
|
pub mod model_buffer;
|
||||||
|
pub mod rect;
|
||||||
|
pub mod text;
|
||||||
|
pub mod util;
|
||||||
|
pub mod viewport;
|
||||||
124
wgui/src/renderer_vk/model_buffer.rs
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use glam::{Mat4, Vec3};
|
||||||
|
use vulkano::{
|
||||||
|
buffer::{BufferUsage, Subbuffer},
|
||||||
|
descriptor_set::DescriptorSet,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
gfx,
|
||||||
|
renderer_vk::{rect::RectPipeline, text::text_atlas::TextPipeline},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct ModelBuffer {
|
||||||
|
idx: u32,
|
||||||
|
models: Vec<glam::Mat4>,
|
||||||
|
|
||||||
|
buffer: Subbuffer<[f32]>, //4x4 floats = 1 mat4
|
||||||
|
buffer_capacity_f32: u32,
|
||||||
|
|
||||||
|
rect_descriptor: Option<Arc<DescriptorSet>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ModelBuffer {
|
||||||
|
pub fn new(gfx: &Arc<gfx::WGfx>) -> anyhow::Result<Self> {
|
||||||
|
const INITIAL_CAPACITY_MAT4: u32 = 16;
|
||||||
|
const INITIAL_CAPACITY_F32: u32 = INITIAL_CAPACITY_MAT4 * (4 * 4);
|
||||||
|
|
||||||
|
let buffer = gfx.empty_buffer::<f32>(
|
||||||
|
BufferUsage::STORAGE_BUFFER | BufferUsage::TRANSFER_DST,
|
||||||
|
INITIAL_CAPACITY_F32 as _,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mut models = Vec::<glam::Mat4>::new();
|
||||||
|
models.resize(INITIAL_CAPACITY_MAT4 as _, Default::default());
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
models,
|
||||||
|
idx: 0,
|
||||||
|
buffer,
|
||||||
|
buffer_capacity_f32: INITIAL_CAPACITY_F32,
|
||||||
|
rect_descriptor: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear(&mut self) {
|
||||||
|
self.models.clear(); // note: capacity is being preserved here
|
||||||
|
self.idx = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn upload(&mut self, gfx: &Arc<gfx::WGfx>) -> anyhow::Result<()> {
|
||||||
|
// resize buffer if it's too small
|
||||||
|
let required_capacity_f32 = (self.models.len() * (4 * 4)) as u32;
|
||||||
|
|
||||||
|
if self.buffer_capacity_f32 < required_capacity_f32 {
|
||||||
|
self.buffer_capacity_f32 = required_capacity_f32;
|
||||||
|
self.buffer = gfx.empty_buffer::<f32>(
|
||||||
|
BufferUsage::STORAGE_BUFFER | BufferUsage::TRANSFER_DST,
|
||||||
|
required_capacity_f32 as _,
|
||||||
|
)?;
|
||||||
|
//log::info!("resized to {}", required_capacity_f32);
|
||||||
|
}
|
||||||
|
|
||||||
|
//safe
|
||||||
|
let floats = unsafe {
|
||||||
|
std::slice::from_raw_parts(
|
||||||
|
self.models.as_slice().as_ptr() as *const f32,
|
||||||
|
required_capacity_f32 as usize,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
self.buffer.write()?.copy_from_slice(floats);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns model matrix ID from the model
|
||||||
|
pub fn register(&mut self, model: &glam::Mat4) -> u32 {
|
||||||
|
/*for (idx, iter_model) in self.models.iter().enumerate() {
|
||||||
|
if iter_model == model {
|
||||||
|
return idx as u32;
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
|
||||||
|
if self.idx == self.models.len() as u32 {
|
||||||
|
self
|
||||||
|
.models
|
||||||
|
.resize(self.models.len() * 2, Default::default());
|
||||||
|
//log::info!("ModelBuffer: resized to {}", self.models.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
// insert new
|
||||||
|
self.models[self.idx as usize] = *model;
|
||||||
|
let ret = self.idx;
|
||||||
|
self.idx += 1;
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register_pos_size(
|
||||||
|
&mut self,
|
||||||
|
pos: &glam::Vec2,
|
||||||
|
size: &glam::Vec2,
|
||||||
|
transform: &Mat4,
|
||||||
|
) -> u32 {
|
||||||
|
let mut model = glam::Mat4::from_translation(Vec3::new(pos.x, pos.y, 0.0));
|
||||||
|
model *= *transform;
|
||||||
|
model *= glam::Mat4::from_scale(Vec3::new(size.x, size.y, 1.0));
|
||||||
|
self.register(&model)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_rect_descriptor(&mut self, pipeline: &RectPipeline) -> Arc<DescriptorSet> {
|
||||||
|
self
|
||||||
|
.rect_descriptor
|
||||||
|
.get_or_insert_with(|| pipeline.color_rect.buffer(1, self.buffer.clone()).unwrap())
|
||||||
|
.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_text_descriptor(&mut self, pipeline: &TextPipeline) -> Arc<DescriptorSet> {
|
||||||
|
self
|
||||||
|
.rect_descriptor
|
||||||
|
.get_or_insert_with(|| pipeline.inner.buffer(3, self.buffer.clone()).unwrap())
|
||||||
|
.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
171
wgui/src/renderer_vk/rect.rs
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use glam::Mat4;
|
||||||
|
use vulkano::{
|
||||||
|
buffer::{BufferContents, BufferUsage, Subbuffer},
|
||||||
|
format::Format,
|
||||||
|
pipeline::graphics::{input_assembly::PrimitiveTopology, vertex_input::Vertex},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
drawing::{Boundary, Rectangle},
|
||||||
|
gfx::{BLEND_ALPHA, WGfx, cmd::GfxCommandBuffer, pipeline::WGfxPipeline},
|
||||||
|
renderer_vk::model_buffer::ModelBuffer,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::viewport::Viewport;
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(BufferContents, Vertex, Copy, Clone, Debug)]
|
||||||
|
pub struct RectVertex {
|
||||||
|
#[format(R32_UINT)]
|
||||||
|
pub in_model_idx: u32,
|
||||||
|
#[format(R32_UINT)]
|
||||||
|
pub in_rect_dim: [u16; 2],
|
||||||
|
#[format(R32_UINT)]
|
||||||
|
pub in_color: u32,
|
||||||
|
#[format(R32_UINT)]
|
||||||
|
pub in_color2: u32,
|
||||||
|
#[format(R32_UINT)]
|
||||||
|
pub in_border_color: u32,
|
||||||
|
#[format(R32_UINT)]
|
||||||
|
pub round_border_gradient: [u8; 4],
|
||||||
|
#[format(R32_SFLOAT)]
|
||||||
|
pub depth: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cloneable pipeline & shaders to be shared between RectRenderer instances.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct RectPipeline {
|
||||||
|
gfx: Arc<WGfx>,
|
||||||
|
pub(super) color_rect: Arc<WGfxPipeline<RectVertex>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RectPipeline {
|
||||||
|
pub fn new(gfx: Arc<WGfx>, format: Format) -> anyhow::Result<Self> {
|
||||||
|
let vert = vert_rect::load(gfx.device.clone())?;
|
||||||
|
let frag = frag_rect::load(gfx.device.clone())?;
|
||||||
|
|
||||||
|
let color_rect = gfx.create_pipeline::<RectVertex>(
|
||||||
|
vert,
|
||||||
|
frag,
|
||||||
|
format,
|
||||||
|
Some(BLEND_ALPHA),
|
||||||
|
PrimitiveTopology::TriangleStrip,
|
||||||
|
true,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(Self { gfx, color_rect })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RectRenderer {
|
||||||
|
pipeline: RectPipeline,
|
||||||
|
rect_vertices: Vec<RectVertex>,
|
||||||
|
vert_buffer: Subbuffer<[RectVertex]>,
|
||||||
|
vert_buffer_size: usize,
|
||||||
|
model_buffer: ModelBuffer,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RectRenderer {
|
||||||
|
pub fn new(pipeline: RectPipeline) -> anyhow::Result<Self> {
|
||||||
|
const BUFFER_SIZE: usize = 128;
|
||||||
|
|
||||||
|
let vert_buffer = pipeline.gfx.empty_buffer(
|
||||||
|
BufferUsage::VERTEX_BUFFER | BufferUsage::TRANSFER_DST,
|
||||||
|
BUFFER_SIZE as _,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
model_buffer: ModelBuffer::new(&pipeline.gfx)?,
|
||||||
|
pipeline,
|
||||||
|
rect_vertices: vec![],
|
||||||
|
vert_buffer,
|
||||||
|
vert_buffer_size: BUFFER_SIZE,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_rect(
|
||||||
|
&mut self,
|
||||||
|
boundary: Boundary,
|
||||||
|
rectangle: Rectangle,
|
||||||
|
transform: &Mat4,
|
||||||
|
depth: f32,
|
||||||
|
) {
|
||||||
|
let in_model_idx =
|
||||||
|
self
|
||||||
|
.model_buffer
|
||||||
|
.register_pos_size(&boundary.pos, &boundary.size, transform);
|
||||||
|
|
||||||
|
self.rect_vertices.push(RectVertex {
|
||||||
|
in_model_idx,
|
||||||
|
in_rect_dim: [boundary.size.x as u16, boundary.size.y as u16],
|
||||||
|
in_color: cosmic_text::Color::from(rectangle.color).0,
|
||||||
|
in_color2: cosmic_text::Color::from(rectangle.color2).0,
|
||||||
|
in_border_color: cosmic_text::Color::from(rectangle.border_color).0,
|
||||||
|
round_border_gradient: [
|
||||||
|
rectangle.round_units,
|
||||||
|
(rectangle.border) as u8,
|
||||||
|
rectangle.gradient as u8,
|
||||||
|
0, // unused
|
||||||
|
],
|
||||||
|
depth,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn upload_verts(&mut self) -> anyhow::Result<()> {
|
||||||
|
if self.vert_buffer_size < self.rect_vertices.len() {
|
||||||
|
let new_size = self.vert_buffer_size * 2;
|
||||||
|
self.vert_buffer = self.pipeline.gfx.empty_buffer(
|
||||||
|
BufferUsage::VERTEX_BUFFER | BufferUsage::TRANSFER_DST,
|
||||||
|
new_size as _,
|
||||||
|
)?;
|
||||||
|
self.vert_buffer_size = new_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.vert_buffer.write()?[0..self.rect_vertices.len()].clone_from_slice(&self.rect_vertices);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(
|
||||||
|
&mut self,
|
||||||
|
gfx: &Arc<WGfx>,
|
||||||
|
viewport: &mut Viewport,
|
||||||
|
cmd_buf: &mut GfxCommandBuffer,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let vp = viewport.resolution();
|
||||||
|
|
||||||
|
self.model_buffer.upload(gfx)?;
|
||||||
|
self.upload_verts()?;
|
||||||
|
|
||||||
|
let set0 = viewport.get_rect_descriptor(&self.pipeline);
|
||||||
|
let set1 = self.model_buffer.get_rect_descriptor(&self.pipeline);
|
||||||
|
|
||||||
|
let pass = self.pipeline.color_rect.create_pass(
|
||||||
|
[vp[0] as _, vp[1] as _],
|
||||||
|
self.vert_buffer.clone(),
|
||||||
|
0..4,
|
||||||
|
0..self.rect_vertices.len() as _,
|
||||||
|
vec![set0, set1],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
self.rect_vertices.clear();
|
||||||
|
|
||||||
|
cmd_buf.run_ref(&pass)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod vert_rect {
|
||||||
|
vulkano_shaders::shader! {
|
||||||
|
ty: "vertex",
|
||||||
|
path: "src/renderer_vk/shaders/rect.vert",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod frag_rect {
|
||||||
|
vulkano_shaders::shader! {
|
||||||
|
ty: "fragment",
|
||||||
|
path: "src/renderer_vk/shaders/rect.frag",
|
||||||
|
}
|
||||||
|
}
|
||||||
5
wgui/src/renderer_vk/shaders/model_buffer.glsl
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
layout(std140, set = MODEL_BUFFER_SET,
|
||||||
|
binding = 0) readonly buffer ModelBuffer {
|
||||||
|
mat4 models[];
|
||||||
|
}
|
||||||
|
model_buffer;
|
||||||
54
wgui/src/renderer_vk/shaders/rect.frag
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
#version 450
|
||||||
|
#extension GL_GOOGLE_include_directive : enable
|
||||||
|
|
||||||
|
precision highp float;
|
||||||
|
|
||||||
|
layout(location = 0) in vec4 in_color;
|
||||||
|
layout(location = 1) in vec4 in_color2;
|
||||||
|
layout(location = 2) in vec2 in_uv;
|
||||||
|
layout(location = 3) in vec4 in_border_color;
|
||||||
|
layout(location = 4) in float in_border_size; // in units
|
||||||
|
layout(location = 5) in float in_radius; // in units
|
||||||
|
layout(location = 6) in vec2 in_rect_size;
|
||||||
|
layout(location = 0) out vec4 out_color;
|
||||||
|
|
||||||
|
#define UNIFORM_PARAMS_SET 0
|
||||||
|
#include "uniform.glsl"
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
float rect_aspect = in_rect_size.x / in_rect_size.y;
|
||||||
|
|
||||||
|
vec2 center = in_rect_size / 2.0;
|
||||||
|
vec2 coords = in_uv * in_rect_size;
|
||||||
|
|
||||||
|
float radius = in_radius;
|
||||||
|
|
||||||
|
vec2 sdf_rect_dim = center - vec2(radius);
|
||||||
|
float sdf = length(max(abs(coords - center), sdf_rect_dim) - sdf_rect_dim) -
|
||||||
|
in_radius;
|
||||||
|
|
||||||
|
vec4 color =
|
||||||
|
mix(in_color, in_color2, min(length((in_uv - vec2(0.5)) * 2.0), 1.0));
|
||||||
|
|
||||||
|
float pixel_size = 1.0 / uniforms.pixel_scale;
|
||||||
|
|
||||||
|
if (in_border_size < in_radius) {
|
||||||
|
// rounded border
|
||||||
|
float f = in_border_size > 0.0 ? smoothstep(in_border_size + pixel_size,
|
||||||
|
in_border_size, -sdf) *
|
||||||
|
in_border_color.a
|
||||||
|
: 0.0;
|
||||||
|
out_color = mix(color, in_border_color, f);
|
||||||
|
} else {
|
||||||
|
// square border
|
||||||
|
vec2 a = abs(coords - center);
|
||||||
|
float aa = center.x - in_border_size;
|
||||||
|
float bb = center.y - in_border_size;
|
||||||
|
out_color = (a.x > aa || a.y > bb) ? in_border_color : color;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_radius > 0.0) {
|
||||||
|
// rounding cutout alpha
|
||||||
|
out_color.a *= 1.0 - smoothstep(-pixel_size, 0.0, sdf);
|
||||||
|
}
|
||||||
|
}
|
||||||
93
wgui/src/renderer_vk/shaders/rect.vert
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
#version 450
|
||||||
|
#extension GL_GOOGLE_include_directive : enable
|
||||||
|
|
||||||
|
precision highp float;
|
||||||
|
|
||||||
|
layout(location = 0) in uint in_model_idx;
|
||||||
|
layout(location = 1) in uint in_rect_dim;
|
||||||
|
layout(location = 2) in uint in_color;
|
||||||
|
layout(location = 3) in uint in_color2;
|
||||||
|
layout(location = 4) in uint in_border_color;
|
||||||
|
layout(location = 5) in uint round_border_gradient;
|
||||||
|
layout(location = 6) in float depth;
|
||||||
|
|
||||||
|
layout(location = 0) out vec4 out_color;
|
||||||
|
layout(location = 1) out vec4 out_color2;
|
||||||
|
layout(location = 2) out vec2 out_uv;
|
||||||
|
layout(location = 3) out vec4 out_border_color;
|
||||||
|
layout(location = 4) out float out_border_size;
|
||||||
|
layout(location = 5) out float out_radius;
|
||||||
|
layout(location = 6) out vec2 out_rect_size;
|
||||||
|
|
||||||
|
#define UNIFORM_PARAMS_SET 0
|
||||||
|
#define MODEL_BUFFER_SET 1
|
||||||
|
|
||||||
|
#include "model_buffer.glsl"
|
||||||
|
#include "uniform.glsl"
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
uint v = uint(gl_VertexIndex); // 0-3
|
||||||
|
uint rect_width = in_rect_dim & 0xffffu;
|
||||||
|
uint rect_height = (in_rect_dim & 0xffff0000u) >> 16u;
|
||||||
|
vec2 rect_size = vec2(float(rect_width), float(rect_height));
|
||||||
|
float rect_aspect = rect_size.x / rect_size.y;
|
||||||
|
|
||||||
|
// 0.0 - 1.0 normalized
|
||||||
|
uvec2 corner_pos_u = uvec2(v & 1u, (v >> 1u) & 1u);
|
||||||
|
vec2 corner_pos = vec2(corner_pos_u);
|
||||||
|
out_uv = corner_pos;
|
||||||
|
|
||||||
|
mat4 model_matrix = model_buffer.models[in_model_idx];
|
||||||
|
|
||||||
|
out_rect_size = rect_size;
|
||||||
|
|
||||||
|
gl_Position =
|
||||||
|
uniforms.projection * model_matrix * vec4(corner_pos, depth, 1.0);
|
||||||
|
|
||||||
|
out_border_color =
|
||||||
|
vec4(float((in_border_color & 0x00ff0000u) >> 16u) / 255.0,
|
||||||
|
float((in_border_color & 0x0000ff00u) >> 8u) / 255.0,
|
||||||
|
float(in_border_color & 0x000000ffu) / 255.0,
|
||||||
|
float((in_border_color & 0xff000000u) >> 24u) / 255.0);
|
||||||
|
|
||||||
|
float radius = float(round_border_gradient & 0xffu);
|
||||||
|
out_radius = radius;
|
||||||
|
|
||||||
|
float border_size = float((round_border_gradient & 0xff00u) >> 8);
|
||||||
|
out_border_size = border_size;
|
||||||
|
|
||||||
|
uint gradient_mode = (round_border_gradient & 0x00ff0000u) >> 16;
|
||||||
|
|
||||||
|
uint color;
|
||||||
|
uint color2;
|
||||||
|
switch (gradient_mode) {
|
||||||
|
case 1:
|
||||||
|
// horizontal
|
||||||
|
color = corner_pos_u.x > 0u ? in_color2 : in_color;
|
||||||
|
color2 = color;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
// vertical
|
||||||
|
color = corner_pos_u.y > 0u ? in_color2 : in_color;
|
||||||
|
color2 = color;
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
// radial
|
||||||
|
color = in_color;
|
||||||
|
color2 = in_color2;
|
||||||
|
break;
|
||||||
|
default: // none
|
||||||
|
color = in_color;
|
||||||
|
color2 = in_color;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
out_color = vec4(float((color & 0x00ff0000u) >> 16u) / 255.0,
|
||||||
|
float((color & 0x0000ff00u) >> 8u) / 255.0,
|
||||||
|
float(color & 0x000000ffu) / 255.0,
|
||||||
|
float((color & 0xff000000u) >> 24u) / 255.0);
|
||||||
|
out_color2 = vec4(float((color2 & 0x00ff0000u) >> 16u) / 255.0,
|
||||||
|
float((color2 & 0x0000ff00u) >> 8u) / 255.0,
|
||||||
|
float(color2 & 0x000000ffu) / 255.0,
|
||||||
|
float((color2 & 0xff000000u) >> 24u) / 255.0);
|
||||||
|
}
|
||||||
22
wgui/src/renderer_vk/shaders/styles_buffer.glsl
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
struct StylesData {
|
||||||
|
float radius;
|
||||||
|
|
||||||
|
vec4 color0;
|
||||||
|
vec4 color1;
|
||||||
|
uint gradient_style;
|
||||||
|
vec2 gradient_curve;
|
||||||
|
|
||||||
|
vec4 border_size_tlbr;
|
||||||
|
vec4 border_color0;
|
||||||
|
vec4 border_color1;
|
||||||
|
vec4 border_color2;
|
||||||
|
vec4 border_color3;
|
||||||
|
uint border_gradient_style;
|
||||||
|
vec2 border_gradient_curve;
|
||||||
|
}
|
||||||
|
|
||||||
|
layout(std140, set = STYLES_BUFFER_SET,
|
||||||
|
binding = 0) readonly buffer StylesBuffer {
|
||||||
|
StylesData styles[];
|
||||||
|
}
|
||||||
|
styles_buffer;
|
||||||
22
wgui/src/renderer_vk/shaders/text.frag
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
#version 310 es
|
||||||
|
#extension GL_GOOGLE_include_directive : enable
|
||||||
|
|
||||||
|
precision highp float;
|
||||||
|
|
||||||
|
layout(location = 0) in vec4 in_color;
|
||||||
|
layout(location = 1) in vec2 in_uv;
|
||||||
|
layout(location = 2) flat in uint in_content_type;
|
||||||
|
|
||||||
|
layout(location = 0) out vec4 out_color;
|
||||||
|
|
||||||
|
layout(set = 0, binding = 0) uniform sampler2D color_atlas;
|
||||||
|
layout(set = 1, binding = 0) uniform sampler2D mask_atlas;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
if (in_content_type == 0u) {
|
||||||
|
out_color = texture(color_atlas, in_uv);
|
||||||
|
} else {
|
||||||
|
out_color.rgb = in_color.rgb;
|
||||||
|
out_color.a = in_color.a * texture(mask_atlas, in_uv).r;
|
||||||
|
}
|
||||||
|
}
|
||||||
61
wgui/src/renderer_vk/shaders/text.vert
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
#version 450
|
||||||
|
#extension GL_GOOGLE_include_directive : enable
|
||||||
|
|
||||||
|
precision highp float;
|
||||||
|
|
||||||
|
layout(location = 0) in uint in_model_idx;
|
||||||
|
layout(location = 1) in uint in_rect_dim;
|
||||||
|
layout(location = 2) in uint in_uv;
|
||||||
|
layout(location = 3) in uint in_color;
|
||||||
|
layout(location = 4) in uint in_content_type;
|
||||||
|
layout(location = 5) in float depth;
|
||||||
|
layout(location = 7) in float scale;
|
||||||
|
|
||||||
|
layout(location = 0) out vec4 out_color;
|
||||||
|
layout(location = 1) out vec2 out_uv;
|
||||||
|
layout(location = 2) flat out uint out_content_type;
|
||||||
|
|
||||||
|
layout(set = 0, binding = 0) uniform sampler2D color_atlas;
|
||||||
|
layout(set = 1, binding = 0) uniform sampler2D mask_atlas;
|
||||||
|
|
||||||
|
#define UNIFORM_PARAMS_SET 2
|
||||||
|
#define MODEL_BUFFER_SET 3
|
||||||
|
|
||||||
|
#include "model_buffer.glsl"
|
||||||
|
#include "uniform.glsl"
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
uint v = uint(gl_VertexIndex); // 0-3
|
||||||
|
uint rect_width = in_rect_dim & 0xffffu;
|
||||||
|
uint rect_height = (in_rect_dim & 0xffff0000u) >> 16u;
|
||||||
|
vec2 rect_size = vec2(float(rect_width), float(rect_height));
|
||||||
|
float rect_aspect = rect_size.x / rect_size.y;
|
||||||
|
|
||||||
|
uvec2 uv = uvec2(in_uv & 0xffffu, (in_uv & 0xffff0000u) >> 16u);
|
||||||
|
|
||||||
|
uvec2 corner_pos_u = uvec2(v & 1u, (v >> 1u) & 1u);
|
||||||
|
vec2 corner_pos = vec2(corner_pos_u);
|
||||||
|
uvec2 corner_offset = uvec2(rect_width, rect_height) * corner_pos_u;
|
||||||
|
uv = uv + corner_offset;
|
||||||
|
|
||||||
|
mat4 model_matrix = model_buffer.models[in_model_idx];
|
||||||
|
|
||||||
|
gl_Position =
|
||||||
|
uniforms.projection * model_matrix * vec4(corner_pos * scale, depth, 1.0);
|
||||||
|
|
||||||
|
out_content_type = in_content_type & 0xffffu;
|
||||||
|
|
||||||
|
out_color = vec4(float((in_color & 0x00ff0000u) >> 16u) / 255.0,
|
||||||
|
float((in_color & 0x0000ff00u) >> 8u) / 255.0,
|
||||||
|
float(in_color & 0x000000ffu) / 255.0,
|
||||||
|
float((in_color & 0xff000000u) >> 24u) / 255.0);
|
||||||
|
|
||||||
|
uvec2 dim = uvec2(0, 0);
|
||||||
|
if (in_content_type == 0u) {
|
||||||
|
dim = uvec2(textureSize(color_atlas, 0));
|
||||||
|
} else {
|
||||||
|
dim = uvec2(textureSize(mask_atlas, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
out_uv = vec2(uv) / vec2(dim);
|
||||||
|
}
|
||||||
8
wgui/src/renderer_vk/shaders/uniform.glsl
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
// Viewport
|
||||||
|
layout(std140, set = UNIFORM_PARAMS_SET, binding = 0) uniform UniformParams {
|
||||||
|
uniform uvec2 screen_resolution;
|
||||||
|
uniform float pixel_scale;
|
||||||
|
uniform mat4 projection;
|
||||||
|
}
|
||||||
|
uniforms;
|
||||||
259
wgui/src/renderer_vk/text/custom_glyph.rs
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
use std::{
|
||||||
|
f32,
|
||||||
|
sync::{
|
||||||
|
Arc,
|
||||||
|
atomic::{AtomicUsize, Ordering},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use cosmic_text::SubpixelBin;
|
||||||
|
use image::RgbaImage;
|
||||||
|
use resvg::usvg::{Options, Tree};
|
||||||
|
|
||||||
|
use crate::assets::AssetProvider;
|
||||||
|
|
||||||
|
static AUTO_INCREMENT: AtomicUsize = AtomicUsize::new(0);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum CustomGlyphContent {
|
||||||
|
Svg(Box<Tree>),
|
||||||
|
Image(RgbaImage),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CustomGlyphContent {
|
||||||
|
pub fn from_bin_svg(data: &[u8]) -> anyhow::Result<Self> {
|
||||||
|
let tree = Tree::from_data(data, &Options::default())?;
|
||||||
|
Ok(CustomGlyphContent::Svg(Box::new(tree)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_bin_raster(data: &[u8]) -> anyhow::Result<Self> {
|
||||||
|
let image = image::load_from_memory(data)?.into_rgba8();
|
||||||
|
Ok(CustomGlyphContent::Image(image))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_assets(provider: &mut Box<dyn AssetProvider>, path: &str) -> anyhow::Result<Self> {
|
||||||
|
let data = provider.load_from_path(path)?;
|
||||||
|
if path.ends_with(".svg") || path.ends_with(".svgz") {
|
||||||
|
Ok(CustomGlyphContent::from_bin_svg(&data)?)
|
||||||
|
} else {
|
||||||
|
Ok(CustomGlyphContent::from_bin_raster(&data)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_file(path: &str) -> anyhow::Result<Self> {
|
||||||
|
let data = std::fs::read(path)?;
|
||||||
|
if path.ends_with(".svg") || path.ends_with(".svgz") {
|
||||||
|
Ok(CustomGlyphContent::from_bin_svg(&data)?)
|
||||||
|
} else {
|
||||||
|
Ok(CustomGlyphContent::from_bin_raster(&data)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct CustomGlyphData {
|
||||||
|
pub(super) id: usize,
|
||||||
|
pub(super) content: Arc<CustomGlyphContent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CustomGlyphData {
|
||||||
|
pub fn new(content: CustomGlyphContent) -> Self {
|
||||||
|
Self {
|
||||||
|
id: AUTO_INCREMENT.fetch_add(1, Ordering::Relaxed),
|
||||||
|
content: Arc::new(content),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dim_for_cache_key(&self, width: u16, height: u16) -> (u16, u16) {
|
||||||
|
const MAX_RASTER_DIM: u16 = 256;
|
||||||
|
match self.content.as_ref() {
|
||||||
|
CustomGlyphContent::Svg(..) => (
|
||||||
|
width.next_power_of_two().min(MAX_RASTER_DIM),
|
||||||
|
height.next_power_of_two().min(MAX_RASTER_DIM),
|
||||||
|
),
|
||||||
|
CustomGlyphContent::Image(image) => (image.width() as _, image.height() as _),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for CustomGlyphData {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.id.eq(&other.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A custom glyph to render
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct CustomGlyph {
|
||||||
|
/// The unique identifier for this glyph
|
||||||
|
pub data: CustomGlyphData,
|
||||||
|
/// The position of the left edge of the glyph
|
||||||
|
pub left: f32,
|
||||||
|
/// The position of the top edge of the glyph
|
||||||
|
pub top: f32,
|
||||||
|
/// The width of the glyph
|
||||||
|
pub width: f32,
|
||||||
|
/// The height of the glyph
|
||||||
|
pub height: f32,
|
||||||
|
/// The color of this glyph (only relevant if the glyph is rendered with the
|
||||||
|
/// type [`ContentType::Mask`])
|
||||||
|
///
|
||||||
|
/// Set to `None` to use [`crate::TextArea::default_color`].
|
||||||
|
pub color: Option<cosmic_text::Color>,
|
||||||
|
/// If `true`, then this glyph will be snapped to the nearest whole physical
|
||||||
|
/// pixel and the resulting `SubpixelBin`'s in `RasterizationRequest` will always
|
||||||
|
/// be `Zero` (useful for images and other large glyphs).
|
||||||
|
pub snap_to_physical_pixel: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CustomGlyph {
|
||||||
|
pub fn new(data: CustomGlyphData) -> Self {
|
||||||
|
Self {
|
||||||
|
data,
|
||||||
|
left: 0.0,
|
||||||
|
top: 0.0,
|
||||||
|
width: 0.0,
|
||||||
|
height: 0.0,
|
||||||
|
color: None,
|
||||||
|
snap_to_physical_pixel: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A request to rasterize a custom glyph
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct RasterizeCustomGlyphRequest {
|
||||||
|
/// The unique identifier of the glyph
|
||||||
|
pub data: CustomGlyphData,
|
||||||
|
/// The width of the glyph in physical pixels
|
||||||
|
pub width: u16,
|
||||||
|
/// The height of the glyph in physical pixels
|
||||||
|
pub height: u16,
|
||||||
|
/// Binning of fractional X offset
|
||||||
|
///
|
||||||
|
/// If `CustomGlyph::snap_to_physical_pixel` was set to `true`, then this
|
||||||
|
/// will always be `Zero`.
|
||||||
|
pub x_bin: SubpixelBin,
|
||||||
|
/// Binning of fractional Y offset
|
||||||
|
///
|
||||||
|
/// If `CustomGlyph::snap_to_physical_pixel` was set to `true`, then this
|
||||||
|
/// will always be `Zero`.
|
||||||
|
pub y_bin: SubpixelBin,
|
||||||
|
/// The scaling factor applied to the text area (Note that `width` and
|
||||||
|
/// `height` are already scaled by this factor.)
|
||||||
|
pub scale: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A rasterized custom glyph
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct RasterizedCustomGlyph {
|
||||||
|
/// The raw image data
|
||||||
|
pub data: Vec<u8>,
|
||||||
|
/// The type of image data contained in `data`
|
||||||
|
pub content_type: ContentType,
|
||||||
|
pub width: u16,
|
||||||
|
pub height: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RasterizedCustomGlyph {
|
||||||
|
pub(super) fn try_from(input: &RasterizeCustomGlyphRequest) -> Option<RasterizedCustomGlyph> {
|
||||||
|
match input.data.content.as_ref() {
|
||||||
|
CustomGlyphContent::Svg(tree) => rasterize_svg(tree, input),
|
||||||
|
CustomGlyphContent::Image(data) => rasterize_image(data),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn validate(
|
||||||
|
&self,
|
||||||
|
input: &RasterizeCustomGlyphRequest,
|
||||||
|
expected_type: Option<ContentType>,
|
||||||
|
) {
|
||||||
|
if let Some(expected_type) = expected_type {
|
||||||
|
assert_eq!(
|
||||||
|
self.content_type, expected_type,
|
||||||
|
"Custom glyph rasterizer must always produce the same content type for a given input. Expected {:?}, got {:?}. Input: {:?}",
|
||||||
|
expected_type, self.content_type, input
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
self.data.len(),
|
||||||
|
self.width as usize * self.height as usize * self.content_type.bytes_per_pixel(),
|
||||||
|
"Invalid custom glyph rasterizer output. Expected data of length {}, got length {}. Input: {:?}",
|
||||||
|
self.width as usize * self.height as usize * self.content_type.bytes_per_pixel(),
|
||||||
|
self.data.len(),
|
||||||
|
input,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub struct CustomGlyphCacheKey {
|
||||||
|
/// Font ID
|
||||||
|
pub glyph_id: usize,
|
||||||
|
/// Glyph width
|
||||||
|
pub width: u16,
|
||||||
|
/// Glyph height
|
||||||
|
pub height: u16,
|
||||||
|
/// Binning of fractional X offset
|
||||||
|
pub x_bin: SubpixelBin,
|
||||||
|
/// Binning of fractional Y offset
|
||||||
|
pub y_bin: SubpixelBin,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The type of image data contained in a rasterized glyph
|
||||||
|
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||||
|
pub enum ContentType {
|
||||||
|
/// Each pixel contains 32 bits of rgba data
|
||||||
|
Color,
|
||||||
|
/// Each pixel contains a single 8 bit channel
|
||||||
|
Mask,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContentType {
|
||||||
|
/// The number of bytes per pixel for this content type
|
||||||
|
pub fn bytes_per_pixel(&self) -> usize {
|
||||||
|
match self {
|
||||||
|
Self::Color => 4,
|
||||||
|
Self::Mask => 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rasterize_svg(
|
||||||
|
tree: &Tree,
|
||||||
|
input: &RasterizeCustomGlyphRequest,
|
||||||
|
) -> Option<RasterizedCustomGlyph> {
|
||||||
|
// Calculate the scale based on the "glyph size".
|
||||||
|
let svg_size = tree.size();
|
||||||
|
let scale_x = input.width as f32 / svg_size.width();
|
||||||
|
let scale_y = input.height as f32 / svg_size.height();
|
||||||
|
|
||||||
|
let mut pixmap = resvg::tiny_skia::Pixmap::new(input.width as u32, input.height as u32)?;
|
||||||
|
let mut transform = resvg::usvg::Transform::from_scale(scale_x, scale_y);
|
||||||
|
|
||||||
|
// Offset the glyph by the subpixel amount.
|
||||||
|
let offset_x = input.x_bin.as_float();
|
||||||
|
let offset_y = input.y_bin.as_float();
|
||||||
|
if offset_x != 0.0 || offset_y != 0.0 {
|
||||||
|
transform = transform.post_translate(offset_x, offset_y);
|
||||||
|
}
|
||||||
|
|
||||||
|
resvg::render(tree, transform, &mut pixmap.as_mut());
|
||||||
|
|
||||||
|
Some(RasterizedCustomGlyph {
|
||||||
|
data: pixmap.data().to_vec(),
|
||||||
|
content_type: ContentType::Color,
|
||||||
|
width: input.width,
|
||||||
|
height: input.height,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rasterize_image(image: &RgbaImage) -> Option<RasterizedCustomGlyph> {
|
||||||
|
Some(RasterizedCustomGlyph {
|
||||||
|
data: image.to_vec(),
|
||||||
|
content_type: ContentType::Color,
|
||||||
|
width: image.width() as _,
|
||||||
|
height: image.height() as _,
|
||||||
|
})
|
||||||
|
}
|
||||||
225
wgui/src/renderer_vk/text/mod.rs
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
pub mod custom_glyph;
|
||||||
|
mod shaders;
|
||||||
|
pub mod text_atlas;
|
||||||
|
pub mod text_renderer;
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
cell::RefCell,
|
||||||
|
rc::Rc,
|
||||||
|
sync::{LazyLock, Mutex},
|
||||||
|
};
|
||||||
|
|
||||||
|
use cosmic_text::{
|
||||||
|
Align, Attrs, Buffer, Color, FontSystem, Metrics, Style, SwashCache, Weight, Wrap,
|
||||||
|
};
|
||||||
|
use custom_glyph::{ContentType, CustomGlyph};
|
||||||
|
use etagere::AllocId;
|
||||||
|
use glam::Mat4;
|
||||||
|
|
||||||
|
use crate::drawing::{self};
|
||||||
|
|
||||||
|
pub static FONT_SYSTEM: LazyLock<Mutex<FontSystem>> =
|
||||||
|
LazyLock::new(|| Mutex::new(FontSystem::new()));
|
||||||
|
pub static SWASH_CACHE: LazyLock<Mutex<SwashCache>> =
|
||||||
|
LazyLock::new(|| Mutex::new(SwashCache::new()));
|
||||||
|
|
||||||
|
/// Used in case no font_size is defined
|
||||||
|
const DEFAULT_FONT_SIZE: f32 = 14.;
|
||||||
|
|
||||||
|
/// In case no line_height is defined, use font_size * DEFAULT_LINE_HEIGHT_RATIO
|
||||||
|
const DEFAULT_LINE_HEIGHT_RATIO: f32 = 1.43;
|
||||||
|
|
||||||
|
pub(crate) const DEFAULT_METRICS: Metrics = Metrics::new(
|
||||||
|
DEFAULT_FONT_SIZE,
|
||||||
|
DEFAULT_FONT_SIZE * DEFAULT_LINE_HEIGHT_RATIO,
|
||||||
|
);
|
||||||
|
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
pub struct TextStyle {
|
||||||
|
pub size: Option<f32>,
|
||||||
|
pub line_height: Option<f32>,
|
||||||
|
pub color: Option<drawing::Color>, // TODO: should this be hex?
|
||||||
|
pub style: Option<FontStyle>,
|
||||||
|
pub weight: Option<FontWeight>,
|
||||||
|
pub align: Option<HorizontalAlign>,
|
||||||
|
pub wrap: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&TextStyle> for Attrs<'_> {
|
||||||
|
fn from(style: &TextStyle) -> Self {
|
||||||
|
Attrs::new()
|
||||||
|
.color(style.color.unwrap_or_default().into())
|
||||||
|
.style(style.style.unwrap_or_default().into())
|
||||||
|
.weight(style.weight.unwrap_or_default().into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&TextStyle> for Metrics {
|
||||||
|
fn from(style: &TextStyle) -> Self {
|
||||||
|
let font_size = style.size.unwrap_or(DEFAULT_FONT_SIZE);
|
||||||
|
|
||||||
|
Metrics {
|
||||||
|
font_size,
|
||||||
|
line_height: style
|
||||||
|
.size
|
||||||
|
.unwrap_or_else(|| (font_size * DEFAULT_LINE_HEIGHT_RATIO).round()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&TextStyle> for Wrap {
|
||||||
|
fn from(value: &TextStyle) -> Self {
|
||||||
|
if value.wrap {
|
||||||
|
Wrap::WordOrGlyph
|
||||||
|
} else {
|
||||||
|
Wrap::None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// helper structs for serde
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Copy)]
|
||||||
|
pub enum FontStyle {
|
||||||
|
#[default]
|
||||||
|
Normal,
|
||||||
|
Italic,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<FontStyle> for Style {
|
||||||
|
fn from(value: FontStyle) -> Style {
|
||||||
|
match value {
|
||||||
|
FontStyle::Normal => Style::Normal,
|
||||||
|
FontStyle::Italic => Style::Italic,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Copy)]
|
||||||
|
pub enum FontWeight {
|
||||||
|
#[default]
|
||||||
|
Normal,
|
||||||
|
Bold,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<FontWeight> for Weight {
|
||||||
|
fn from(value: FontWeight) -> Weight {
|
||||||
|
match value {
|
||||||
|
FontWeight::Normal => Weight::NORMAL,
|
||||||
|
FontWeight::Bold => Weight::BOLD,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Copy)]
|
||||||
|
pub enum HorizontalAlign {
|
||||||
|
#[default]
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
Center,
|
||||||
|
Justified,
|
||||||
|
End,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<HorizontalAlign> for Align {
|
||||||
|
fn from(value: HorizontalAlign) -> Align {
|
||||||
|
match value {
|
||||||
|
HorizontalAlign::Left => Align::Left,
|
||||||
|
HorizontalAlign::Right => Align::Right,
|
||||||
|
HorizontalAlign::Center => Align::Center,
|
||||||
|
HorizontalAlign::Justified => Align::Justified,
|
||||||
|
HorizontalAlign::End => Align::End,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<drawing::Color> for cosmic_text::Color {
|
||||||
|
fn from(value: drawing::Color) -> cosmic_text::Color {
|
||||||
|
cosmic_text::Color::rgba(
|
||||||
|
(value.r * 255.999) as _,
|
||||||
|
(value.g * 255.999) as _,
|
||||||
|
(value.b * 255.999) as _,
|
||||||
|
(value.a * 255.999) as _,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<cosmic_text::Color> for drawing::Color {
|
||||||
|
fn from(value: cosmic_text::Color) -> drawing::Color {
|
||||||
|
drawing::Color::new(
|
||||||
|
value.r() as f32 / 255.999,
|
||||||
|
value.g() as f32 / 255.999,
|
||||||
|
value.b() as f32 / 255.999,
|
||||||
|
value.a() as f32 / 255.999,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// glyphon types below
|
||||||
|
|
||||||
|
pub(super) enum GpuCacheStatus {
|
||||||
|
InAtlas {
|
||||||
|
x: u16,
|
||||||
|
y: u16,
|
||||||
|
content_type: ContentType,
|
||||||
|
},
|
||||||
|
SkipRasterization,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) struct GlyphDetails {
|
||||||
|
width: u16,
|
||||||
|
height: u16,
|
||||||
|
gpu_cache: GpuCacheStatus,
|
||||||
|
atlas_id: Option<AllocId>,
|
||||||
|
top: i16,
|
||||||
|
left: i16,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Controls the visible area of the text. Any text outside of the visible area will be clipped.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub struct TextBounds {
|
||||||
|
/// The position of the left edge of the visible area.
|
||||||
|
pub left: i32,
|
||||||
|
/// The position of the top edge of the visible area.
|
||||||
|
pub top: i32,
|
||||||
|
/// The position of the right edge of the visible area.
|
||||||
|
pub right: i32,
|
||||||
|
/// The position of the bottom edge of the visible area.
|
||||||
|
pub bottom: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The default visible area doesn't clip any text.
|
||||||
|
impl Default for TextBounds {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
left: i32::MIN,
|
||||||
|
top: i32::MIN,
|
||||||
|
right: i32::MAX,
|
||||||
|
bottom: i32::MAX,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A text area containing text to be rendered along with its overflow behavior.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct TextArea<'a> {
|
||||||
|
/// The buffer containing the text to be rendered.
|
||||||
|
pub buffer: Rc<RefCell<Buffer>>,
|
||||||
|
/// The left edge of the buffer.
|
||||||
|
pub left: f32,
|
||||||
|
/// The top edge of the buffer.
|
||||||
|
pub top: f32,
|
||||||
|
/// The scaling to apply to the buffer.
|
||||||
|
pub scale: f32,
|
||||||
|
/// The visible bounds of the text area. This is used to clip the text and doesn't have to
|
||||||
|
/// match the `left` and `top` values.
|
||||||
|
pub bounds: TextBounds,
|
||||||
|
/// The default color of the text area.
|
||||||
|
pub default_color: Color,
|
||||||
|
/// Additional custom glyphs to render.
|
||||||
|
pub custom_glyphs: &'a [CustomGlyph],
|
||||||
|
/// Distance from camera, 0.0..=1.0
|
||||||
|
pub depth: f32,
|
||||||
|
/// Text transformation
|
||||||
|
pub transform: Mat4,
|
||||||
|
}
|
||||||
13
wgui/src/renderer_vk/text/shaders.rs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
pub mod vert_atlas {
|
||||||
|
vulkano_shaders::shader! {
|
||||||
|
ty: "vertex",
|
||||||
|
path: "src/renderer_vk/shaders/text.vert",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod frag_atlas {
|
||||||
|
vulkano_shaders::shader! {
|
||||||
|
ty: "fragment",
|
||||||
|
path: "src/renderer_vk/shaders/text.frag",
|
||||||
|
}
|
||||||
|
}
|
||||||
335
wgui/src/renderer_vk/text/text_atlas.rs
Normal file
@@ -0,0 +1,335 @@
|
|||||||
|
use cosmic_text::{FontSystem, SwashCache};
|
||||||
|
use etagere::{Allocation, BucketedAtlasAllocator, size2};
|
||||||
|
use lru::LruCache;
|
||||||
|
use rustc_hash::FxHasher;
|
||||||
|
use std::{collections::HashSet, hash::BuildHasherDefault, sync::Arc};
|
||||||
|
use vulkano::{
|
||||||
|
buffer::BufferContents,
|
||||||
|
command_buffer::CommandBufferUsage,
|
||||||
|
descriptor_set::DescriptorSet,
|
||||||
|
format::Format,
|
||||||
|
image::{Image, ImageCreateInfo, ImageType, ImageUsage, view::ImageView},
|
||||||
|
memory::allocator::AllocationCreateInfo,
|
||||||
|
pipeline::graphics::{input_assembly::PrimitiveTopology, vertex_input::Vertex},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
GlyphDetails, GpuCacheStatus,
|
||||||
|
custom_glyph::ContentType,
|
||||||
|
shaders::{frag_atlas, vert_atlas},
|
||||||
|
text_renderer::GlyphonCacheKey,
|
||||||
|
};
|
||||||
|
use crate::gfx::{BLEND_ALPHA, WGfx, pipeline::WGfxPipeline};
|
||||||
|
|
||||||
|
/// Pipeline & shaders to be reused between TextRenderer instances
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct TextPipeline {
|
||||||
|
pub(super) gfx: Arc<WGfx>,
|
||||||
|
pub(in super::super) inner: Arc<WGfxPipeline<GlyphVertex>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextPipeline {
|
||||||
|
pub fn new(gfx: Arc<WGfx>, format: Format) -> anyhow::Result<Self> {
|
||||||
|
let vert = vert_atlas::load(gfx.device.clone())?;
|
||||||
|
let frag = frag_atlas::load(gfx.device.clone())?;
|
||||||
|
|
||||||
|
let pipeline = gfx.create_pipeline::<GlyphVertex>(
|
||||||
|
vert,
|
||||||
|
frag,
|
||||||
|
format,
|
||||||
|
Some(BLEND_ALPHA),
|
||||||
|
PrimitiveTopology::TriangleStrip,
|
||||||
|
true,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
gfx,
|
||||||
|
inner: pipeline,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(BufferContents, Vertex, Copy, Clone, Debug, Default)]
|
||||||
|
pub struct GlyphVertex {
|
||||||
|
#[format(R32_UINT)]
|
||||||
|
pub in_model_idx: u32,
|
||||||
|
#[format(R32_UINT)]
|
||||||
|
pub in_rect_dim: [u16; 2],
|
||||||
|
#[format(R32_UINT)]
|
||||||
|
pub in_uv: [u16; 2],
|
||||||
|
#[format(R32_UINT)]
|
||||||
|
pub in_color: u32,
|
||||||
|
#[format(R32_UINT)]
|
||||||
|
pub in_content_type: [u16; 2], // 2 bytes unused! TODO
|
||||||
|
#[format(R32_SFLOAT)]
|
||||||
|
pub depth: f32,
|
||||||
|
#[format(R32_SFLOAT)]
|
||||||
|
pub scale: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
type Hasher = BuildHasherDefault<FxHasher>;
|
||||||
|
|
||||||
|
pub(super) struct InnerAtlas {
|
||||||
|
pub kind: Kind,
|
||||||
|
pub image_view: Arc<ImageView>,
|
||||||
|
pub image_descriptor: Arc<DescriptorSet>,
|
||||||
|
pub packer: BucketedAtlasAllocator,
|
||||||
|
pub size: u32,
|
||||||
|
pub glyph_cache: LruCache<GlyphonCacheKey, GlyphDetails, Hasher>,
|
||||||
|
pub glyphs_in_use: HashSet<GlyphonCacheKey, Hasher>,
|
||||||
|
pub max_texture_dimension_2d: u32,
|
||||||
|
common: TextPipeline,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InnerAtlas {
|
||||||
|
const INITIAL_SIZE: u32 = 256;
|
||||||
|
|
||||||
|
fn new(common: TextPipeline, kind: Kind) -> anyhow::Result<Self> {
|
||||||
|
let max_texture_dimension_2d = common
|
||||||
|
.gfx
|
||||||
|
.device
|
||||||
|
.physical_device()
|
||||||
|
.properties()
|
||||||
|
.max_image_dimension2_d;
|
||||||
|
let size = Self::INITIAL_SIZE.min(max_texture_dimension_2d);
|
||||||
|
|
||||||
|
let packer = BucketedAtlasAllocator::new(size2(size as i32, size as i32));
|
||||||
|
|
||||||
|
// Create a texture to use for our atlas
|
||||||
|
let image = Image::new(
|
||||||
|
common.gfx.memory_allocator.clone(),
|
||||||
|
ImageCreateInfo {
|
||||||
|
image_type: ImageType::Dim2d,
|
||||||
|
format: kind.texture_format(),
|
||||||
|
extent: [size, size, 1],
|
||||||
|
usage: ImageUsage::SAMPLED | ImageUsage::TRANSFER_SRC | ImageUsage::TRANSFER_DST,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
AllocationCreateInfo::default(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let image_view = ImageView::new_default(image).unwrap();
|
||||||
|
|
||||||
|
let image_descriptor = common.inner.uniform_sampler(
|
||||||
|
Self::descriptor_set(kind),
|
||||||
|
image_view.clone(),
|
||||||
|
common.gfx.texture_filter,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let glyph_cache = LruCache::unbounded_with_hasher(Hasher::default());
|
||||||
|
let glyphs_in_use = HashSet::with_hasher(Hasher::default());
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
kind,
|
||||||
|
image_view,
|
||||||
|
image_descriptor,
|
||||||
|
packer,
|
||||||
|
size,
|
||||||
|
glyph_cache,
|
||||||
|
glyphs_in_use,
|
||||||
|
max_texture_dimension_2d,
|
||||||
|
common,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn descriptor_set(kind: Kind) -> usize {
|
||||||
|
match kind {
|
||||||
|
Kind::Color => 0,
|
||||||
|
Kind::Mask => 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn try_allocate(&mut self, width: usize, height: usize) -> Option<Allocation> {
|
||||||
|
let size = size2(width as i32, height as i32);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let allocation = self.packer.allocate(size);
|
||||||
|
|
||||||
|
if allocation.is_some() {
|
||||||
|
return allocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to free least recently used allocation
|
||||||
|
let (mut key, mut value) = self.glyph_cache.peek_lru()?;
|
||||||
|
|
||||||
|
// Find a glyph with an actual size
|
||||||
|
while value.atlas_id.is_none() {
|
||||||
|
// All sized glyphs are in use, cache is full
|
||||||
|
if self.glyphs_in_use.contains(key) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = self.glyph_cache.pop_lru();
|
||||||
|
|
||||||
|
(key, value) = self.glyph_cache.peek_lru()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// All sized glyphs are in use, cache is full
|
||||||
|
if self.glyphs_in_use.contains(key) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (_, value) = self.glyph_cache.pop_lru().unwrap();
|
||||||
|
self.packer.deallocate(value.atlas_id.unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn num_channels(&self) -> usize {
|
||||||
|
self.kind.num_channels()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn grow(
|
||||||
|
&mut self,
|
||||||
|
font_system: &mut FontSystem,
|
||||||
|
cache: &mut SwashCache,
|
||||||
|
) -> anyhow::Result<bool> {
|
||||||
|
if self.size >= self.max_texture_dimension_2d {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grow each dimension by a factor of 2. The growth factor was chosen to match the growth
|
||||||
|
// factor of `Vec`.`
|
||||||
|
const GROWTH_FACTOR: u32 = 2;
|
||||||
|
let new_size = (self.size * GROWTH_FACTOR).min(self.max_texture_dimension_2d);
|
||||||
|
log::info!("Grow {:?} atlas {} → {new_size}", self.kind, self.size);
|
||||||
|
|
||||||
|
self.packer.grow(size2(new_size as i32, new_size as i32));
|
||||||
|
|
||||||
|
let old_image = self.image_view.image().clone();
|
||||||
|
|
||||||
|
let image = self.common.gfx.new_image(
|
||||||
|
new_size,
|
||||||
|
new_size,
|
||||||
|
old_image.format(),
|
||||||
|
ImageUsage::SAMPLED | ImageUsage::TRANSFER_SRC | ImageUsage::TRANSFER_DST,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
self.image_view = ImageView::new_default(image.clone()).unwrap();
|
||||||
|
|
||||||
|
let mut cmd_buf = self
|
||||||
|
.common
|
||||||
|
.gfx
|
||||||
|
.create_xfer_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
|
||||||
|
|
||||||
|
// Re-upload glyphs
|
||||||
|
for (&cache_key, glyph) in &self.glyph_cache {
|
||||||
|
let (x, y) = match glyph.gpu_cache {
|
||||||
|
GpuCacheStatus::InAtlas { x, y, .. } => (x, y),
|
||||||
|
GpuCacheStatus::SkipRasterization => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
let (width, height) = match cache_key {
|
||||||
|
GlyphonCacheKey::Text(cache_key) => {
|
||||||
|
let image = cache.get_image_uncached(font_system, cache_key).unwrap();
|
||||||
|
let width = image.placement.width as usize;
|
||||||
|
let height = image.placement.height as usize;
|
||||||
|
(width, height)
|
||||||
|
}
|
||||||
|
GlyphonCacheKey::Custom(cache_key) => (cache_key.width as usize, cache_key.height as usize),
|
||||||
|
};
|
||||||
|
|
||||||
|
let offset = [x as _, y as _, 0];
|
||||||
|
cmd_buf.copy_image(
|
||||||
|
old_image.clone(),
|
||||||
|
offset,
|
||||||
|
image.clone(),
|
||||||
|
offset,
|
||||||
|
Some([width as _, height as _, 1]),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
cmd_buf.build_and_execute_now()?;
|
||||||
|
|
||||||
|
self.size = new_size;
|
||||||
|
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn trim(&mut self) {
|
||||||
|
self.glyphs_in_use.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rebind_descriptor(&mut self) -> anyhow::Result<()> {
|
||||||
|
self.image_descriptor = self.common.inner.uniform_sampler(
|
||||||
|
Self::descriptor_set(self.kind),
|
||||||
|
self.image_view.clone(),
|
||||||
|
self.common.gfx.texture_filter,
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub(super) enum Kind {
|
||||||
|
Mask,
|
||||||
|
Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Kind {
|
||||||
|
fn num_channels(self) -> usize {
|
||||||
|
match self {
|
||||||
|
Kind::Mask => 1,
|
||||||
|
Kind::Color => 4,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn texture_format(self) -> Format {
|
||||||
|
match self {
|
||||||
|
Kind::Mask => Format::R8_UNORM,
|
||||||
|
Kind::Color => Format::R8G8B8A8_UNORM,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An atlas containing a cache of rasterized glyphs that can be rendered.
|
||||||
|
pub struct TextAtlas {
|
||||||
|
pub(super) common: TextPipeline,
|
||||||
|
pub(super) color_atlas: InnerAtlas,
|
||||||
|
pub(super) mask_atlas: InnerAtlas,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextAtlas {
|
||||||
|
/// Creates a new [`TextAtlas`].
|
||||||
|
pub fn new(common: TextPipeline) -> anyhow::Result<Self> {
|
||||||
|
let color_atlas = InnerAtlas::new(common.clone(), Kind::Color)?;
|
||||||
|
let mask_atlas = InnerAtlas::new(common.clone(), Kind::Mask)?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
common,
|
||||||
|
color_atlas,
|
||||||
|
mask_atlas,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn trim(&mut self) {
|
||||||
|
self.mask_atlas.trim();
|
||||||
|
self.color_atlas.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn grow(
|
||||||
|
&mut self,
|
||||||
|
font_system: &mut FontSystem,
|
||||||
|
cache: &mut SwashCache,
|
||||||
|
content_type: ContentType,
|
||||||
|
) -> anyhow::Result<bool> {
|
||||||
|
let did_grow = match content_type {
|
||||||
|
ContentType::Mask => self.mask_atlas.grow(font_system, cache)?,
|
||||||
|
ContentType::Color => self.color_atlas.grow(font_system, cache)?,
|
||||||
|
};
|
||||||
|
|
||||||
|
if did_grow {
|
||||||
|
self.color_atlas.rebind_descriptor()?;
|
||||||
|
self.mask_atlas.rebind_descriptor()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(did_grow)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn inner_for_content_mut(&mut self, content_type: ContentType) -> &mut InnerAtlas {
|
||||||
|
match content_type {
|
||||||
|
ContentType::Color => &mut self.color_atlas,
|
||||||
|
ContentType::Mask => &mut self.mask_atlas,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
484
wgui/src/renderer_vk/text/text_renderer.rs
Normal file
@@ -0,0 +1,484 @@
|
|||||||
|
use crate::{
|
||||||
|
gfx::cmd::GfxCommandBuffer,
|
||||||
|
renderer_vk::{model_buffer::ModelBuffer, viewport::Viewport},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
ContentType, FontSystem, GlyphDetails, GpuCacheStatus, SwashCache, TextArea,
|
||||||
|
custom_glyph::{CustomGlyphCacheKey, RasterizeCustomGlyphRequest, RasterizedCustomGlyph},
|
||||||
|
text_atlas::{GlyphVertex, TextAtlas, TextPipeline},
|
||||||
|
};
|
||||||
|
use cosmic_text::{Color, SubpixelBin, SwashContent};
|
||||||
|
use glam::{Mat4, Vec2, Vec3};
|
||||||
|
use vulkano::{
|
||||||
|
buffer::{BufferUsage, Subbuffer},
|
||||||
|
command_buffer::CommandBufferUsage,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A text renderer that uses cached glyphs to render text into an existing render pass.
|
||||||
|
pub struct TextRenderer {
|
||||||
|
pipeline: TextPipeline,
|
||||||
|
vertex_buffer: Subbuffer<[GlyphVertex]>,
|
||||||
|
vertex_buffer_capacity: usize,
|
||||||
|
glyph_vertices: Vec<GlyphVertex>,
|
||||||
|
model_buffer: ModelBuffer,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextRenderer {
|
||||||
|
/// Creates a new `TextRenderer`.
|
||||||
|
pub fn new(atlas: &mut TextAtlas) -> anyhow::Result<Self> {
|
||||||
|
// A buffer element is a single quad with a glyph on it
|
||||||
|
const INITIAL_CAPACITY: usize = 256;
|
||||||
|
|
||||||
|
let vertex_buffer = atlas.common.gfx.empty_buffer(
|
||||||
|
BufferUsage::VERTEX_BUFFER | BufferUsage::TRANSFER_DST,
|
||||||
|
INITIAL_CAPACITY as _,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
model_buffer: ModelBuffer::new(&atlas.common.gfx)?,
|
||||||
|
pipeline: atlas.common.clone(),
|
||||||
|
vertex_buffer,
|
||||||
|
vertex_buffer_capacity: INITIAL_CAPACITY,
|
||||||
|
glyph_vertices: Vec::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prepares all of the provided text areas for rendering.
|
||||||
|
pub fn prepare<'a>(
|
||||||
|
&mut self,
|
||||||
|
font_system: &mut FontSystem,
|
||||||
|
atlas: &mut TextAtlas,
|
||||||
|
viewport: &Viewport,
|
||||||
|
text_areas: impl IntoIterator<Item = TextArea<'a>>,
|
||||||
|
cache: &mut SwashCache,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
self.glyph_vertices.clear();
|
||||||
|
|
||||||
|
let resolution = viewport.resolution();
|
||||||
|
|
||||||
|
for text_area in text_areas {
|
||||||
|
let bounds_min_x = text_area.bounds.left.max(0);
|
||||||
|
let bounds_min_y = text_area.bounds.top.max(0);
|
||||||
|
let bounds_max_x = text_area.bounds.right.min(resolution[0] as i32);
|
||||||
|
let bounds_max_y = text_area.bounds.bottom.min(resolution[1] as i32);
|
||||||
|
|
||||||
|
for glyph in text_area.custom_glyphs.iter() {
|
||||||
|
let x = text_area.left + (glyph.left * text_area.scale);
|
||||||
|
let y = text_area.top + (glyph.top * text_area.scale);
|
||||||
|
let width = (glyph.width * text_area.scale).round() as u16;
|
||||||
|
let height = (glyph.height * text_area.scale).round() as u16;
|
||||||
|
|
||||||
|
let (x, y, x_bin, y_bin) = if glyph.snap_to_physical_pixel {
|
||||||
|
(
|
||||||
|
x.round() as i32,
|
||||||
|
y.round() as i32,
|
||||||
|
SubpixelBin::Zero,
|
||||||
|
SubpixelBin::Zero,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
let (x, x_bin) = SubpixelBin::new(x);
|
||||||
|
let (y, y_bin) = SubpixelBin::new(y);
|
||||||
|
(x, y, x_bin, y_bin)
|
||||||
|
};
|
||||||
|
|
||||||
|
let (cached_width, cached_height) = glyph.data.dim_for_cache_key(width, height);
|
||||||
|
|
||||||
|
let cache_key = GlyphonCacheKey::Custom(CustomGlyphCacheKey {
|
||||||
|
glyph_id: glyph.data.id,
|
||||||
|
width: cached_width,
|
||||||
|
height: cached_height,
|
||||||
|
x_bin,
|
||||||
|
y_bin,
|
||||||
|
});
|
||||||
|
|
||||||
|
let color = glyph.color.unwrap_or(text_area.default_color);
|
||||||
|
|
||||||
|
if let Some(glyph_to_render) = prepare_glyph(
|
||||||
|
PrepareGlyphParams {
|
||||||
|
label_pos: Vec2::new(text_area.left, text_area.top),
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
line_y: 0.0,
|
||||||
|
color,
|
||||||
|
cache_key,
|
||||||
|
atlas,
|
||||||
|
cache,
|
||||||
|
font_system,
|
||||||
|
model_buffer: &mut self.model_buffer,
|
||||||
|
scale_factor: text_area.scale,
|
||||||
|
glyph_scale: width as f32 / cached_width as f32,
|
||||||
|
bounds_min_x,
|
||||||
|
bounds_min_y,
|
||||||
|
bounds_max_x,
|
||||||
|
bounds_max_y,
|
||||||
|
depth: text_area.depth,
|
||||||
|
transform: &text_area.transform,
|
||||||
|
},
|
||||||
|
|_cache, _font_system| -> Option<GetGlyphImageResult> {
|
||||||
|
if cached_width == 0 || cached_height == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let input = RasterizeCustomGlyphRequest {
|
||||||
|
data: glyph.data.clone(),
|
||||||
|
width: cached_width,
|
||||||
|
height: cached_height,
|
||||||
|
x_bin,
|
||||||
|
y_bin,
|
||||||
|
scale: text_area.scale,
|
||||||
|
};
|
||||||
|
|
||||||
|
let output = RasterizedCustomGlyph::try_from(&input)?;
|
||||||
|
|
||||||
|
output.validate(&input, None);
|
||||||
|
|
||||||
|
Some(GetGlyphImageResult {
|
||||||
|
content_type: output.content_type,
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: output.width,
|
||||||
|
height: output.height,
|
||||||
|
data: output.data,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)? {
|
||||||
|
self.glyph_vertices.push(glyph_to_render);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let is_run_visible = |run: &cosmic_text::LayoutRun| {
|
||||||
|
let start_y_physical = (text_area.top + (run.line_top * text_area.scale)) as i32;
|
||||||
|
let end_y_physical = start_y_physical + (run.line_height * text_area.scale) as i32;
|
||||||
|
|
||||||
|
start_y_physical <= text_area.bounds.bottom && text_area.bounds.top <= end_y_physical
|
||||||
|
};
|
||||||
|
|
||||||
|
let buffer = text_area.buffer.borrow();
|
||||||
|
|
||||||
|
let layout_runs = buffer
|
||||||
|
.layout_runs()
|
||||||
|
.skip_while(|run| !is_run_visible(run))
|
||||||
|
.take_while(is_run_visible);
|
||||||
|
|
||||||
|
for run in layout_runs {
|
||||||
|
for glyph in run.glyphs.iter() {
|
||||||
|
let physical_glyph = glyph.physical((text_area.left, text_area.top), text_area.scale);
|
||||||
|
|
||||||
|
let color = match glyph.color_opt {
|
||||||
|
Some(some) => some,
|
||||||
|
None => text_area.default_color,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(glyph_to_render) = prepare_glyph(
|
||||||
|
PrepareGlyphParams {
|
||||||
|
label_pos: Vec2::new(text_area.left, text_area.top),
|
||||||
|
x: physical_glyph.x,
|
||||||
|
y: physical_glyph.y,
|
||||||
|
line_y: run.line_y,
|
||||||
|
color,
|
||||||
|
cache_key: GlyphonCacheKey::Text(physical_glyph.cache_key),
|
||||||
|
atlas,
|
||||||
|
cache,
|
||||||
|
font_system,
|
||||||
|
model_buffer: &mut self.model_buffer,
|
||||||
|
glyph_scale: 1.0,
|
||||||
|
scale_factor: text_area.scale,
|
||||||
|
bounds_min_x,
|
||||||
|
bounds_min_y,
|
||||||
|
bounds_max_x,
|
||||||
|
bounds_max_y,
|
||||||
|
depth: text_area.depth,
|
||||||
|
transform: &text_area.transform,
|
||||||
|
},
|
||||||
|
|cache, font_system| -> Option<GetGlyphImageResult> {
|
||||||
|
let image = cache.get_image_uncached(font_system, physical_glyph.cache_key)?;
|
||||||
|
|
||||||
|
let content_type = match image.content {
|
||||||
|
SwashContent::Color => ContentType::Color,
|
||||||
|
SwashContent::Mask => ContentType::Mask,
|
||||||
|
SwashContent::SubpixelMask => {
|
||||||
|
// Not implemented yet, but don't panic if this happens.
|
||||||
|
ContentType::Mask
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(GetGlyphImageResult {
|
||||||
|
content_type,
|
||||||
|
top: image.placement.top as i16,
|
||||||
|
left: image.placement.left as i16,
|
||||||
|
width: image.placement.width as u16,
|
||||||
|
height: image.placement.height as u16,
|
||||||
|
data: image.data,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)? {
|
||||||
|
self.glyph_vertices.push(glyph_to_render);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let will_render = !self.glyph_vertices.is_empty();
|
||||||
|
if !will_render {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let vertices = self.glyph_vertices.as_slice();
|
||||||
|
|
||||||
|
while self.vertex_buffer_capacity < vertices.len() {
|
||||||
|
let new_capacity = self.vertex_buffer_capacity * 2;
|
||||||
|
self.vertex_buffer = self.pipeline.gfx.empty_buffer(
|
||||||
|
BufferUsage::VERTEX_BUFFER | BufferUsage::TRANSFER_DST,
|
||||||
|
new_capacity as _,
|
||||||
|
)?;
|
||||||
|
self.vertex_buffer_capacity = new_capacity;
|
||||||
|
}
|
||||||
|
self.vertex_buffer.write()?[..vertices.len()].clone_from_slice(vertices);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renders all layouts that were previously provided to `prepare`.
|
||||||
|
pub fn render(
|
||||||
|
&mut self,
|
||||||
|
atlas: &TextAtlas,
|
||||||
|
viewport: &mut Viewport,
|
||||||
|
cmd_buf: &mut GfxCommandBuffer,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
if self.glyph_vertices.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.model_buffer.upload(&atlas.common.gfx)?;
|
||||||
|
|
||||||
|
let descriptor_sets = vec![
|
||||||
|
atlas.color_atlas.image_descriptor.clone(),
|
||||||
|
atlas.mask_atlas.image_descriptor.clone(),
|
||||||
|
viewport.get_text_descriptor(&self.pipeline),
|
||||||
|
self.model_buffer.get_text_descriptor(&self.pipeline),
|
||||||
|
];
|
||||||
|
|
||||||
|
let res = viewport.resolution();
|
||||||
|
|
||||||
|
let pass = self.pipeline.inner.create_pass(
|
||||||
|
[res[0] as _, res[1] as _],
|
||||||
|
self.vertex_buffer.clone(),
|
||||||
|
0..4,
|
||||||
|
0..self.glyph_vertices.len() as u32,
|
||||||
|
descriptor_sets,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
cmd_buf.run_ref(&pass)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub(super) enum GlyphonCacheKey {
|
||||||
|
Text(cosmic_text::CacheKey),
|
||||||
|
Custom(CustomGlyphCacheKey),
|
||||||
|
}
|
||||||
|
|
||||||
|
struct GetGlyphImageResult {
|
||||||
|
content_type: ContentType,
|
||||||
|
top: i16,
|
||||||
|
left: i16,
|
||||||
|
width: u16,
|
||||||
|
height: u16,
|
||||||
|
data: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PrepareGlyphParams<'a> {
|
||||||
|
label_pos: Vec2,
|
||||||
|
x: i32,
|
||||||
|
y: i32,
|
||||||
|
line_y: f32,
|
||||||
|
color: Color,
|
||||||
|
cache_key: GlyphonCacheKey,
|
||||||
|
atlas: &'a mut TextAtlas,
|
||||||
|
cache: &'a mut SwashCache,
|
||||||
|
font_system: &'a mut FontSystem,
|
||||||
|
model_buffer: &'a mut ModelBuffer,
|
||||||
|
transform: &'a Mat4,
|
||||||
|
scale_factor: f32,
|
||||||
|
glyph_scale: f32,
|
||||||
|
bounds_min_x: i32,
|
||||||
|
bounds_min_y: i32,
|
||||||
|
bounds_max_x: i32,
|
||||||
|
bounds_max_y: i32,
|
||||||
|
depth: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn prepare_glyph(
|
||||||
|
par: PrepareGlyphParams,
|
||||||
|
get_glyph_image: impl FnOnce(&mut SwashCache, &mut FontSystem) -> Option<GetGlyphImageResult>,
|
||||||
|
) -> anyhow::Result<Option<GlyphVertex>> {
|
||||||
|
let gfx = par.atlas.common.gfx.clone();
|
||||||
|
let details = if let Some(details) = par.atlas.mask_atlas.glyph_cache.get(&par.cache_key) {
|
||||||
|
par.atlas.mask_atlas.glyphs_in_use.insert(par.cache_key);
|
||||||
|
details
|
||||||
|
} else if let Some(details) = par.atlas.color_atlas.glyph_cache.get(&par.cache_key) {
|
||||||
|
par.atlas.color_atlas.glyphs_in_use.insert(par.cache_key);
|
||||||
|
details
|
||||||
|
} else {
|
||||||
|
let Some(image) = (get_glyph_image)(par.cache, par.font_system) else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
let should_rasterize = image.width > 0 && image.height > 0;
|
||||||
|
|
||||||
|
let (gpu_cache, atlas_id, inner) = if should_rasterize {
|
||||||
|
let mut inner = par.atlas.inner_for_content_mut(image.content_type);
|
||||||
|
|
||||||
|
// Find a position in the packer
|
||||||
|
let allocation = loop {
|
||||||
|
match inner.try_allocate(image.width as usize, image.height as usize) {
|
||||||
|
Some(a) => break a,
|
||||||
|
None => {
|
||||||
|
if !par
|
||||||
|
.atlas
|
||||||
|
.grow(par.font_system, par.cache, image.content_type)?
|
||||||
|
{
|
||||||
|
anyhow::bail!(
|
||||||
|
"Atlas full. atlas: {:?} cache_key: {:?}",
|
||||||
|
image.content_type,
|
||||||
|
par.cache_key
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
inner = par.atlas.inner_for_content_mut(image.content_type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let atlas_min = allocation.rectangle.min;
|
||||||
|
|
||||||
|
let mut cmd_buf = gfx.create_xfer_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
|
||||||
|
|
||||||
|
cmd_buf.update_image(
|
||||||
|
inner.image_view.image().clone(),
|
||||||
|
&image.data,
|
||||||
|
[atlas_min.x as _, atlas_min.y as _, 0],
|
||||||
|
Some([image.width as _, image.height as _, 1]),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
cmd_buf.build_and_execute_now()?; //TODO: do not wait for fence here
|
||||||
|
|
||||||
|
(
|
||||||
|
GpuCacheStatus::InAtlas {
|
||||||
|
x: atlas_min.x as u16,
|
||||||
|
y: atlas_min.y as u16,
|
||||||
|
content_type: image.content_type,
|
||||||
|
},
|
||||||
|
Some(allocation.id),
|
||||||
|
inner,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
let inner = &mut par.atlas.color_atlas;
|
||||||
|
(GpuCacheStatus::SkipRasterization, None, inner)
|
||||||
|
};
|
||||||
|
|
||||||
|
inner.glyphs_in_use.insert(par.cache_key);
|
||||||
|
// Insert the glyph into the cache and return the details reference
|
||||||
|
inner
|
||||||
|
.glyph_cache
|
||||||
|
.get_or_insert(par.cache_key, || GlyphDetails {
|
||||||
|
width: image.width,
|
||||||
|
height: image.height,
|
||||||
|
gpu_cache,
|
||||||
|
atlas_id,
|
||||||
|
top: image.top,
|
||||||
|
left: image.left,
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut x = par.x + details.left as i32;
|
||||||
|
let mut y = (par.line_y * par.scale_factor).round() as i32 + par.y - details.top as i32;
|
||||||
|
|
||||||
|
let (mut atlas_x, mut atlas_y, content_type) = match details.gpu_cache {
|
||||||
|
GpuCacheStatus::InAtlas { x, y, content_type } => (x, y, content_type),
|
||||||
|
GpuCacheStatus::SkipRasterization => return Ok(None),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut glyph_width = details.width as i32;
|
||||||
|
let mut glyph_height = details.height as i32;
|
||||||
|
|
||||||
|
// Starts beyond right edge or ends beyond left edge
|
||||||
|
let max_x = x + glyph_width;
|
||||||
|
if x > par.bounds_max_x || max_x < par.bounds_min_x {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Starts beyond bottom edge or ends beyond top edge
|
||||||
|
let max_y = y + glyph_height;
|
||||||
|
if y > par.bounds_max_y || max_y < par.bounds_min_y {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clip left ege
|
||||||
|
if x < par.bounds_min_x {
|
||||||
|
let right_shift = par.bounds_min_x - x;
|
||||||
|
|
||||||
|
x = par.bounds_min_x;
|
||||||
|
glyph_width = max_x - par.bounds_min_x;
|
||||||
|
atlas_x += right_shift as u16;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clip right edge
|
||||||
|
if x + glyph_width > par.bounds_max_x {
|
||||||
|
glyph_width = par.bounds_max_x - x;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clip top edge
|
||||||
|
if y < par.bounds_min_y {
|
||||||
|
let bottom_shift = par.bounds_min_y - y;
|
||||||
|
|
||||||
|
y = par.bounds_min_y;
|
||||||
|
glyph_height = max_y - par.bounds_min_y;
|
||||||
|
atlas_y += bottom_shift as u16;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clip bottom edge
|
||||||
|
if y + glyph_height > par.bounds_max_y {
|
||||||
|
glyph_height = par.bounds_max_y - y;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut model = Mat4::IDENTITY;
|
||||||
|
|
||||||
|
// top-left text transform
|
||||||
|
model *= Mat4::from_translation(Vec3::new(
|
||||||
|
par.label_pos.x / par.scale_factor,
|
||||||
|
par.label_pos.y / par.scale_factor,
|
||||||
|
0.0,
|
||||||
|
));
|
||||||
|
|
||||||
|
model *= *par.transform;
|
||||||
|
|
||||||
|
// per-character transform
|
||||||
|
model *= Mat4::from_translation(Vec3::new(
|
||||||
|
((x as f32) - par.label_pos.x) / par.scale_factor,
|
||||||
|
((y as f32) - par.label_pos.y) / par.scale_factor,
|
||||||
|
0.0,
|
||||||
|
));
|
||||||
|
|
||||||
|
model *= glam::Mat4::from_scale(Vec3::new(
|
||||||
|
glyph_width as f32 / par.scale_factor,
|
||||||
|
glyph_height as f32 / par.scale_factor,
|
||||||
|
0.0,
|
||||||
|
));
|
||||||
|
|
||||||
|
let in_model_idx = par.model_buffer.register(&model);
|
||||||
|
|
||||||
|
Ok(Some(GlyphVertex {
|
||||||
|
in_model_idx,
|
||||||
|
in_rect_dim: [glyph_width as u16, glyph_height as u16],
|
||||||
|
in_uv: [atlas_x, atlas_y],
|
||||||
|
in_color: par.color.0,
|
||||||
|
in_content_type: [
|
||||||
|
content_type as u16,
|
||||||
|
0, // unused (TODO!)
|
||||||
|
],
|
||||||
|
depth: par.depth,
|
||||||
|
scale: par.glyph_scale,
|
||||||
|
}))
|
||||||
|
}
|
||||||
18
wgui/src/renderer_vk/util.rs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
use vulkano::buffer::BufferContents;
|
||||||
|
|
||||||
|
// binary compatible mat4 which could be transparently used by vulkano BufferContents
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, BufferContents)]
|
||||||
|
pub struct WMat4(pub [f32; 16]);
|
||||||
|
|
||||||
|
impl WMat4 {
|
||||||
|
pub fn from_glam(mat: &glam::Mat4) -> WMat4 {
|
||||||
|
WMat4(*mat.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for WMat4 {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(*glam::Mat4::IDENTITY.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
103
wgui/src/renderer_vk/viewport.rs
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use vulkano::{
|
||||||
|
buffer::{BufferContents, BufferUsage, Subbuffer},
|
||||||
|
descriptor_set::DescriptorSet,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{gfx::WGfx, renderer_vk::util::WMat4};
|
||||||
|
|
||||||
|
use super::{rect::RectPipeline, text::text_atlas::TextPipeline};
|
||||||
|
|
||||||
|
/// Controls the visible area of all text for a given renderer. Any text outside of the visible
|
||||||
|
/// area will be clipped.
|
||||||
|
pub struct Viewport {
|
||||||
|
params: Params,
|
||||||
|
params_buffer: Subbuffer<[Params]>,
|
||||||
|
text_descriptor: Option<Arc<DescriptorSet>>,
|
||||||
|
rect_descriptor: Option<Arc<DescriptorSet>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Viewport {
|
||||||
|
/// Creates a new `Viewport` with the given `device` and `cache`.
|
||||||
|
pub fn new(gfx: Arc<WGfx>) -> anyhow::Result<Self> {
|
||||||
|
let params = Params {
|
||||||
|
screen_resolution: [0, 0],
|
||||||
|
pixel_scale: 1.0,
|
||||||
|
padding1: [0.0],
|
||||||
|
projection: WMat4::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let params_buffer = gfx.new_buffer(
|
||||||
|
BufferUsage::UNIFORM_BUFFER | BufferUsage::TRANSFER_DST,
|
||||||
|
[params].iter(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
params,
|
||||||
|
params_buffer,
|
||||||
|
text_descriptor: None,
|
||||||
|
rect_descriptor: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_text_descriptor(&mut self, pipeline: &TextPipeline) -> Arc<DescriptorSet> {
|
||||||
|
self
|
||||||
|
.text_descriptor
|
||||||
|
.get_or_insert_with(|| {
|
||||||
|
pipeline
|
||||||
|
.inner
|
||||||
|
.buffer(2, self.params_buffer.clone())
|
||||||
|
.unwrap() // safe unwrap
|
||||||
|
})
|
||||||
|
.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_rect_descriptor(&mut self, pipeline: &RectPipeline) -> Arc<DescriptorSet> {
|
||||||
|
self
|
||||||
|
.rect_descriptor
|
||||||
|
.get_or_insert_with(|| {
|
||||||
|
pipeline
|
||||||
|
.color_rect
|
||||||
|
.buffer(0, self.params_buffer.clone())
|
||||||
|
.unwrap() // safe unwrap
|
||||||
|
})
|
||||||
|
.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates the `Viewport` with the given `resolution` and `projection`.
|
||||||
|
pub fn update(
|
||||||
|
&mut self,
|
||||||
|
resolution: [u32; 2],
|
||||||
|
projection: &glam::Mat4,
|
||||||
|
pixel_scale: f32,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
if self.params.screen_resolution == resolution
|
||||||
|
&& self.params.projection.0 == *projection.as_ref()
|
||||||
|
&& self.params.pixel_scale == pixel_scale
|
||||||
|
{
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.params.screen_resolution = resolution;
|
||||||
|
self.params.projection = WMat4::from_glam(projection);
|
||||||
|
self.params.pixel_scale = pixel_scale;
|
||||||
|
self.params_buffer.write()?.copy_from_slice(&[self.params]);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the current resolution of the `Viewport`.
|
||||||
|
pub fn resolution(&self) -> [u32; 2] {
|
||||||
|
self.params.screen_resolution
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(BufferContents, Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub(crate) struct Params {
|
||||||
|
pub screen_resolution: [u32; 2],
|
||||||
|
pub pixel_scale: f32,
|
||||||
|
pub padding1: [f32; 1], // always zero
|
||||||
|
|
||||||
|
pub projection: WMat4,
|
||||||
|
}
|
||||||
51
wgui/src/transform_stack.rs
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
use glam::Vec2;
|
||||||
|
|
||||||
|
#[derive(Default, Copy, Clone)]
|
||||||
|
pub struct Transform {
|
||||||
|
pub pos: Vec2,
|
||||||
|
pub transform: glam::Mat4,
|
||||||
|
|
||||||
|
pub dim: Vec2, // for convenience
|
||||||
|
}
|
||||||
|
|
||||||
|
const TRANSFORM_STACK_MAX: usize = 64;
|
||||||
|
pub struct TransformStack {
|
||||||
|
pub stack: [Transform; TRANSFORM_STACK_MAX],
|
||||||
|
top: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TransformStack {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
stack: [Default::default(); TRANSFORM_STACK_MAX],
|
||||||
|
top: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push(&mut self, mut t: Transform) {
|
||||||
|
assert!(self.top < TRANSFORM_STACK_MAX as u8);
|
||||||
|
let idx = (self.top - 1) as usize;
|
||||||
|
t.pos += self.stack[idx].pos;
|
||||||
|
self.stack[self.top as usize] = t;
|
||||||
|
self.top += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pop(&mut self) {
|
||||||
|
assert!(self.top > 0);
|
||||||
|
self.top -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&self) -> &Transform {
|
||||||
|
&self.stack[(self.top - 1) as usize]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_pos(&self) -> Vec2 {
|
||||||
|
self.stack[(self.top - 1) as usize].pos
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TransformStack {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
15
wgui/src/widget/div.rs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
use super::{WidgetObj, WidgetState};
|
||||||
|
|
||||||
|
pub struct Div {}
|
||||||
|
|
||||||
|
impl Div {
|
||||||
|
pub fn create() -> anyhow::Result<WidgetState> {
|
||||||
|
WidgetState::new(Box::new(Self {}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WidgetObj for Div {
|
||||||
|
fn draw(&mut self, _state: &mut super::DrawState, _params: &super::DrawParams) {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
}
|
||||||
342
wgui/src/widget/mod.rs
Normal file
@@ -0,0 +1,342 @@
|
|||||||
|
use glam::Vec2;
|
||||||
|
|
||||||
|
use super::drawing::RenderPrimitive;
|
||||||
|
use crate::{
|
||||||
|
animation,
|
||||||
|
any::AnyTrait,
|
||||||
|
drawing,
|
||||||
|
event::{CallbackData, Event, EventListener, MouseWheelEvent},
|
||||||
|
layout::{Layout, WidgetID, WidgetMap},
|
||||||
|
transform_stack::TransformStack,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub mod div;
|
||||||
|
pub mod rectangle;
|
||||||
|
pub mod sprite;
|
||||||
|
pub mod text;
|
||||||
|
pub mod util;
|
||||||
|
|
||||||
|
pub struct WidgetData {
|
||||||
|
pub hovered: bool,
|
||||||
|
pub pressed: bool,
|
||||||
|
pub scrolling: Vec2, // normalized, 0.0-1.0. Not used in case if overflow != scroll
|
||||||
|
pub transform: glam::Mat4,
|
||||||
|
}
|
||||||
|
pub struct WidgetState {
|
||||||
|
pub data: WidgetData,
|
||||||
|
pub obj: Box<dyn WidgetObj>,
|
||||||
|
pub event_listeners: Vec<EventListener>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WidgetState {
|
||||||
|
fn new(obj: Box<dyn WidgetObj>) -> anyhow::Result<WidgetState> {
|
||||||
|
Ok(Self {
|
||||||
|
data: WidgetData {
|
||||||
|
hovered: false,
|
||||||
|
pressed: false,
|
||||||
|
scrolling: Vec2::default(),
|
||||||
|
transform: glam::Mat4::IDENTITY,
|
||||||
|
},
|
||||||
|
event_listeners: Vec::new(),
|
||||||
|
obj,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// global draw params
|
||||||
|
pub struct DrawState<'a> {
|
||||||
|
pub layout: &'a Layout,
|
||||||
|
pub primitives: &'a mut Vec<RenderPrimitive>,
|
||||||
|
pub transform_stack: &'a mut TransformStack,
|
||||||
|
pub depth: f32, //TODO: actually use this in shader
|
||||||
|
}
|
||||||
|
|
||||||
|
// per-widget draw params
|
||||||
|
pub struct DrawParams<'a> {
|
||||||
|
pub node_id: taffy::NodeId,
|
||||||
|
pub style: &'a taffy::Style,
|
||||||
|
pub taffy_layout: &'a taffy::Layout,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait WidgetObj: AnyTrait {
|
||||||
|
fn draw(&mut self, state: &mut DrawState, params: &DrawParams);
|
||||||
|
fn measure(
|
||||||
|
&mut self,
|
||||||
|
_known_dimensions: taffy::Size<Option<f32>>,
|
||||||
|
_available_space: taffy::Size<taffy::AvailableSpace>,
|
||||||
|
) -> taffy::Size<f32> {
|
||||||
|
taffy::Size::ZERO
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct EventParams<'a> {
|
||||||
|
pub node_id: taffy::NodeId,
|
||||||
|
pub style: &'a taffy::Style,
|
||||||
|
pub taffy_layout: &'a taffy::Layout,
|
||||||
|
pub widgets: &'a WidgetMap,
|
||||||
|
pub tree: &'a taffy::TaffyTree<WidgetID>,
|
||||||
|
pub transform_stack: &'a TransformStack,
|
||||||
|
pub animations: &'a mut Vec<animation::Animation>,
|
||||||
|
pub needs_redraw: &'a mut bool,
|
||||||
|
pub dirty_nodes: &'a mut Vec<taffy::NodeId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum EventResult {
|
||||||
|
Pass,
|
||||||
|
Consumed,
|
||||||
|
Outside,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_scroll_enabled(style: &taffy::Style) -> (bool, bool) {
|
||||||
|
(
|
||||||
|
style.overflow.x == taffy::Overflow::Scroll,
|
||||||
|
style.overflow.y == taffy::Overflow::Scroll,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ScrollbarInfo {
|
||||||
|
// total contents size of the currently scrolling widget
|
||||||
|
content_size: Vec2,
|
||||||
|
// 0.0 - 1.0
|
||||||
|
// 1.0: scrollbar handle not visible (inactive)
|
||||||
|
handle_size: Vec2,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_scrollbar_info(l: &taffy::Layout) -> Option<ScrollbarInfo> {
|
||||||
|
let overflow = Vec2::new(l.scroll_width(), l.scroll_height());
|
||||||
|
if overflow.x == 0.0 && overflow.y == 0.0 {
|
||||||
|
return None; // not overflowing
|
||||||
|
}
|
||||||
|
|
||||||
|
let content_size = Vec2::new(l.content_size.width, l.content_size.height);
|
||||||
|
let handle_size = 1.0 - (overflow / content_size);
|
||||||
|
|
||||||
|
Some(ScrollbarInfo {
|
||||||
|
content_size,
|
||||||
|
handle_size,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
impl dyn WidgetObj {
|
||||||
|
pub fn get_as<T: 'static>(&self) -> &T {
|
||||||
|
let any = self.as_any();
|
||||||
|
any.downcast_ref::<T>().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_as_mut<T: 'static>(&mut self) -> &mut T {
|
||||||
|
let any = self.as_any_mut();
|
||||||
|
any.downcast_mut::<T>().unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WidgetState {
|
||||||
|
pub fn add_event_listener(&mut self, listener: EventListener) {
|
||||||
|
self.event_listeners.push(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_scroll_shift(&self, info: &ScrollbarInfo, l: &taffy::Layout) -> Vec2 {
|
||||||
|
Vec2::new(
|
||||||
|
(info.content_size.x - l.content_box_width()) * self.data.scrolling.x,
|
||||||
|
(info.content_size.y - l.content_box_height()) * self.data.scrolling.y,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw_all(&mut self, state: &mut DrawState, params: &DrawParams) {
|
||||||
|
self.obj.draw(state, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw_scrollbars(
|
||||||
|
&mut self,
|
||||||
|
state: &mut DrawState,
|
||||||
|
params: &DrawParams,
|
||||||
|
info: &ScrollbarInfo,
|
||||||
|
) {
|
||||||
|
let (enabled_horiz, enabled_vert) = get_scroll_enabled(params.style);
|
||||||
|
if !enabled_horiz && !enabled_vert {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let transform = state.transform_stack.get();
|
||||||
|
|
||||||
|
let thickness = 6.0;
|
||||||
|
let margin = 4.0;
|
||||||
|
|
||||||
|
let rect_params = drawing::Rectangle {
|
||||||
|
color: drawing::Color::new(1.0, 1.0, 1.0, 0.0),
|
||||||
|
border: 2.0,
|
||||||
|
border_color: drawing::Color::new(1.0, 1.0, 1.0, 1.0),
|
||||||
|
round_units: 2,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Horizontal handle
|
||||||
|
if enabled_horiz && info.handle_size.x < 1.0 {
|
||||||
|
state.primitives.push(drawing::RenderPrimitive {
|
||||||
|
boundary: drawing::Boundary::from_pos_size(
|
||||||
|
Vec2::new(
|
||||||
|
transform.pos.x + transform.dim.x * (1.0 - info.handle_size.x) * self.data.scrolling.x,
|
||||||
|
transform.pos.y + transform.dim.y - thickness - margin,
|
||||||
|
),
|
||||||
|
Vec2::new(transform.dim.x * info.handle_size.x, thickness),
|
||||||
|
),
|
||||||
|
depth: state.depth,
|
||||||
|
transform: transform.transform,
|
||||||
|
payload: drawing::PrimitivePayload::Rectangle(rect_params),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vertical handle
|
||||||
|
if enabled_vert && info.handle_size.y < 1.0 {
|
||||||
|
state.primitives.push(drawing::RenderPrimitive {
|
||||||
|
boundary: drawing::Boundary::from_pos_size(
|
||||||
|
Vec2::new(
|
||||||
|
transform.pos.x + transform.dim.x - thickness - margin,
|
||||||
|
transform.pos.y + transform.dim.y * (1.0 - info.handle_size.y) * self.data.scrolling.y,
|
||||||
|
),
|
||||||
|
Vec2::new(thickness, transform.dim.y * info.handle_size.y),
|
||||||
|
),
|
||||||
|
depth: state.depth,
|
||||||
|
transform: transform.transform,
|
||||||
|
payload: drawing::PrimitivePayload::Rectangle(rect_params),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_wheel(&mut self, params: &mut EventParams, wheel: &MouseWheelEvent) -> bool {
|
||||||
|
let (enabled_horiz, enabled_vert) = get_scroll_enabled(params.style);
|
||||||
|
if !enabled_horiz && !enabled_vert {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let l = params.taffy_layout;
|
||||||
|
let overflow = Vec2::new(l.scroll_width(), l.scroll_height());
|
||||||
|
if overflow.x == 0.0 && overflow.y == 0.0 {
|
||||||
|
return false; // not overflowing
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(info) = get_scrollbar_info(params.taffy_layout) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let step_pixels = 32.0;
|
||||||
|
|
||||||
|
if info.handle_size.x < 1.0 && wheel.pos.x != 0.0 {
|
||||||
|
// Horizontal scrolling
|
||||||
|
let mult = (1.0 / (l.content_box_width() - info.content_size.x)) * step_pixels;
|
||||||
|
let new_scroll = (self.data.scrolling.x + wheel.shift.x * mult).clamp(0.0, 1.0);
|
||||||
|
if self.data.scrolling.x != new_scroll {
|
||||||
|
self.data.scrolling.x = new_scroll;
|
||||||
|
*params.needs_redraw = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.handle_size.y < 1.0 && wheel.pos.y != 0.0 {
|
||||||
|
// Vertical scrolling
|
||||||
|
let mult = (1.0 / (l.content_box_height() - info.content_size.y)) * step_pixels;
|
||||||
|
let new_scroll = (self.data.scrolling.y + wheel.shift.y * mult).clamp(0.0, 1.0);
|
||||||
|
if self.data.scrolling.y != new_scroll {
|
||||||
|
self.data.scrolling.y = new_scroll;
|
||||||
|
*params.needs_redraw = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn process_event(
|
||||||
|
&mut self,
|
||||||
|
widget_id: WidgetID,
|
||||||
|
node_id: taffy::NodeId,
|
||||||
|
event: &Event,
|
||||||
|
params: &mut EventParams,
|
||||||
|
) -> EventResult {
|
||||||
|
let hovered = event.test_mouse_within_transform(params.transform_stack.get());
|
||||||
|
|
||||||
|
let mut just_clicked = false;
|
||||||
|
match &event {
|
||||||
|
Event::MouseDown(_) => {
|
||||||
|
if self.data.hovered {
|
||||||
|
self.data.pressed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::MouseUp(_) => {
|
||||||
|
if self.data.pressed {
|
||||||
|
self.data.pressed = false;
|
||||||
|
just_clicked = self.data.hovered;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::MouseWheel(e) => {
|
||||||
|
if self.process_wheel(params, e) {
|
||||||
|
return EventResult::Consumed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: simplify this behemoth, I gave up arguing with the compiler
|
||||||
|
for listener in &self.event_listeners {
|
||||||
|
match listener {
|
||||||
|
EventListener::MouseEnter(callback) => {
|
||||||
|
if hovered && !self.data.hovered {
|
||||||
|
let mut data = CallbackData {
|
||||||
|
obj: self.obj.as_mut(),
|
||||||
|
widget_data: &mut self.data,
|
||||||
|
widgets: params.widgets,
|
||||||
|
animations: params.animations,
|
||||||
|
dirty_nodes: params.dirty_nodes,
|
||||||
|
widget_id,
|
||||||
|
node_id,
|
||||||
|
needs_redraw: false,
|
||||||
|
};
|
||||||
|
callback(&mut data);
|
||||||
|
if data.needs_redraw {
|
||||||
|
*params.needs_redraw = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EventListener::MouseLeave(callback) => {
|
||||||
|
if !hovered && self.data.hovered {
|
||||||
|
let mut data = CallbackData {
|
||||||
|
obj: self.obj.as_mut(),
|
||||||
|
widget_data: &mut self.data,
|
||||||
|
widgets: params.widgets,
|
||||||
|
animations: params.animations,
|
||||||
|
dirty_nodes: params.dirty_nodes,
|
||||||
|
widget_id,
|
||||||
|
node_id,
|
||||||
|
needs_redraw: false,
|
||||||
|
};
|
||||||
|
callback(&mut data);
|
||||||
|
if data.needs_redraw {
|
||||||
|
*params.needs_redraw = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EventListener::MouseClick(callback) => {
|
||||||
|
if just_clicked {
|
||||||
|
let mut data = CallbackData {
|
||||||
|
obj: self.obj.as_mut(),
|
||||||
|
widget_data: &mut self.data,
|
||||||
|
widgets: params.widgets,
|
||||||
|
animations: params.animations,
|
||||||
|
dirty_nodes: params.dirty_nodes,
|
||||||
|
widget_id,
|
||||||
|
node_id,
|
||||||
|
needs_redraw: false,
|
||||||
|
};
|
||||||
|
callback(&mut data);
|
||||||
|
if data.needs_redraw {
|
||||||
|
*params.needs_redraw = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.data.hovered != hovered {
|
||||||
|
self.data.hovered = hovered;
|
||||||
|
}
|
||||||
|
|
||||||
|
EventResult::Pass
|
||||||
|
}
|
||||||
|
}
|
||||||
55
wgui/src/widget/rectangle.rs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
use crate::{
|
||||||
|
drawing::{self, GradientMode},
|
||||||
|
widget::util::WLength,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{WidgetObj, WidgetState};
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct RectangleParams {
|
||||||
|
pub color: drawing::Color,
|
||||||
|
pub color2: drawing::Color,
|
||||||
|
pub gradient: GradientMode,
|
||||||
|
|
||||||
|
pub border: f32,
|
||||||
|
pub border_color: drawing::Color,
|
||||||
|
|
||||||
|
pub round: WLength,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Rectangle {
|
||||||
|
pub params: RectangleParams,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Rectangle {
|
||||||
|
pub fn create(params: RectangleParams) -> anyhow::Result<WidgetState> {
|
||||||
|
WidgetState::new(Box::new(Rectangle { params }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WidgetObj for Rectangle {
|
||||||
|
fn draw(&mut self, state: &mut super::DrawState, _params: &super::DrawParams) {
|
||||||
|
let boundary = drawing::Boundary::construct(state.transform_stack);
|
||||||
|
|
||||||
|
let round_units = match self.params.round {
|
||||||
|
WLength::Units(units) => units as u8,
|
||||||
|
WLength::Percent(percent) => {
|
||||||
|
(f32::min(boundary.size.x, boundary.size.y) * percent / 2.0) as u8
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
state.primitives.push(drawing::RenderPrimitive {
|
||||||
|
boundary,
|
||||||
|
depth: state.depth,
|
||||||
|
transform: state.transform_stack.get().transform,
|
||||||
|
payload: drawing::PrimitivePayload::Rectangle(drawing::Rectangle {
|
||||||
|
color: self.params.color,
|
||||||
|
color2: self.params.color2,
|
||||||
|
gradient: self.params.gradient,
|
||||||
|
border: self.params.border,
|
||||||
|
border_color: self.params.border_color,
|
||||||
|
round_units,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
82
wgui/src/widget/sprite.rs
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
|
||||||
|
use cosmic_text::{Attrs, Buffer, Color, Shaping, Weight};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
drawing::{self},
|
||||||
|
renderer_vk::text::{
|
||||||
|
DEFAULT_METRICS, FONT_SYSTEM,
|
||||||
|
custom_glyph::{CustomGlyph, CustomGlyphData},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{WidgetObj, WidgetState};
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct SpriteBoxParams {
|
||||||
|
pub glyph_data: Option<CustomGlyphData>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct SpriteBox {
|
||||||
|
params: SpriteBoxParams,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SpriteBox {
|
||||||
|
pub fn create(params: SpriteBoxParams) -> anyhow::Result<WidgetState> {
|
||||||
|
WidgetState::new(Box::new(Self { params }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WidgetObj for SpriteBox {
|
||||||
|
fn draw(&mut self, state: &mut super::DrawState, _params: &super::DrawParams) {
|
||||||
|
let boundary = drawing::Boundary::construct(state.transform_stack);
|
||||||
|
|
||||||
|
if let Some(glyph_data) = self.params.glyph_data.as_ref() {
|
||||||
|
let glyph = CustomGlyph {
|
||||||
|
data: glyph_data.clone(),
|
||||||
|
left: 0.0,
|
||||||
|
top: 0.0,
|
||||||
|
width: boundary.size.x,
|
||||||
|
height: boundary.size.y,
|
||||||
|
color: Some(cosmic_text::Color::rgb(255, 255, 255)),
|
||||||
|
snap_to_physical_pixel: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
state.primitives.push(drawing::RenderPrimitive {
|
||||||
|
boundary,
|
||||||
|
depth: state.depth,
|
||||||
|
payload: drawing::PrimitivePayload::Sprite(Some(glyph)),
|
||||||
|
transform: state.transform_stack.get().transform,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Source not set or not available, display error text
|
||||||
|
let mut buffer = Buffer::new_empty(DEFAULT_METRICS);
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut font_system = FONT_SYSTEM.lock().unwrap(); // safe unwrap
|
||||||
|
let mut buffer = buffer.borrow_with(&mut font_system);
|
||||||
|
let attrs = Attrs::new()
|
||||||
|
.color(Color::rgb(255, 0, 255))
|
||||||
|
.weight(Weight::BOLD);
|
||||||
|
|
||||||
|
// set text last in order to avoid expensive re-shaping
|
||||||
|
buffer.set_text("Error", &attrs, Shaping::Basic);
|
||||||
|
}
|
||||||
|
state.primitives.push(drawing::RenderPrimitive {
|
||||||
|
boundary,
|
||||||
|
depth: state.depth,
|
||||||
|
payload: drawing::PrimitivePayload::Text(Rc::new(RefCell::new(buffer))),
|
||||||
|
transform: state.transform_stack.get().transform,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn measure(
|
||||||
|
&mut self,
|
||||||
|
_known_dimensions: taffy::Size<Option<f32>>,
|
||||||
|
_available_space: taffy::Size<taffy::AvailableSpace>,
|
||||||
|
) -> taffy::Size<f32> {
|
||||||
|
taffy::Size::ZERO
|
||||||
|
}
|
||||||
|
}
|
||||||
117
wgui/src/widget/text.rs
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
|
||||||
|
use cosmic_text::{Attrs, Buffer, Metrics, Shaping, Wrap};
|
||||||
|
use taffy::AvailableSpace;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
drawing::{self, Boundary},
|
||||||
|
renderer_vk::text::{FONT_SYSTEM, TextStyle},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{WidgetObj, WidgetState};
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct TextParams {
|
||||||
|
pub content: String,
|
||||||
|
pub style: TextStyle,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TextLabel {
|
||||||
|
params: TextParams,
|
||||||
|
buffer: Rc<RefCell<Buffer>>,
|
||||||
|
last_boundary: Boundary,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextLabel {
|
||||||
|
pub fn create(params: TextParams) -> anyhow::Result<WidgetState> {
|
||||||
|
let metrics = Metrics::from(¶ms.style);
|
||||||
|
let attrs = Attrs::from(¶ms.style);
|
||||||
|
let wrap = Wrap::from(¶ms.style);
|
||||||
|
|
||||||
|
let mut buffer = Buffer::new_empty(metrics);
|
||||||
|
{
|
||||||
|
let mut font_system = FONT_SYSTEM.lock().unwrap(); // safe unwrap
|
||||||
|
let mut buffer = buffer.borrow_with(&mut font_system);
|
||||||
|
buffer.set_wrap(wrap);
|
||||||
|
|
||||||
|
buffer.set_rich_text(
|
||||||
|
[(params.content.as_str(), attrs)],
|
||||||
|
&Attrs::new(),
|
||||||
|
Shaping::Advanced,
|
||||||
|
params.style.align.map(|a| a.into()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
WidgetState::new(Box::new(Self {
|
||||||
|
params,
|
||||||
|
buffer: Rc::new(RefCell::new(buffer)),
|
||||||
|
last_boundary: Boundary::default(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_text(&mut self, text: &str) {
|
||||||
|
self.params.content = String::from(text);
|
||||||
|
let attrs = Attrs::from(&self.params.style);
|
||||||
|
let mut font_system = FONT_SYSTEM.lock().unwrap(); // safe unwrap
|
||||||
|
|
||||||
|
let mut buffer = self.buffer.borrow_mut();
|
||||||
|
buffer.set_rich_text(
|
||||||
|
&mut font_system,
|
||||||
|
[(self.params.content.as_str(), attrs)],
|
||||||
|
&Attrs::new(),
|
||||||
|
Shaping::Advanced,
|
||||||
|
self.params.style.align.map(|a| a.into()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WidgetObj for TextLabel {
|
||||||
|
fn draw(&mut self, state: &mut super::DrawState, _params: &super::DrawParams) {
|
||||||
|
let boundary = drawing::Boundary::construct(state.transform_stack);
|
||||||
|
|
||||||
|
if self.last_boundary != boundary {
|
||||||
|
self.last_boundary = boundary;
|
||||||
|
let mut font_system = FONT_SYSTEM.lock().unwrap(); // safe unwrap
|
||||||
|
let mut buffer = self.buffer.borrow_mut();
|
||||||
|
buffer.set_size(
|
||||||
|
&mut font_system,
|
||||||
|
Some(boundary.size.x),
|
||||||
|
Some(boundary.size.y),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.primitives.push(drawing::RenderPrimitive {
|
||||||
|
boundary,
|
||||||
|
depth: state.depth,
|
||||||
|
payload: drawing::PrimitivePayload::Text(self.buffer.clone()),
|
||||||
|
transform: state.transform_stack.get().transform,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn measure(
|
||||||
|
&mut self,
|
||||||
|
known_dimensions: taffy::Size<Option<f32>>,
|
||||||
|
available_space: taffy::Size<taffy::AvailableSpace>,
|
||||||
|
) -> taffy::Size<f32> {
|
||||||
|
// Set width constraint
|
||||||
|
let width_constraint = known_dimensions.width.or(match available_space.width {
|
||||||
|
AvailableSpace::MinContent => Some(0.0),
|
||||||
|
AvailableSpace::MaxContent => None,
|
||||||
|
AvailableSpace::Definite(width) => Some(width),
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut font_system = FONT_SYSTEM.lock().unwrap(); // safe unwrap
|
||||||
|
let mut buffer = self.buffer.borrow_mut();
|
||||||
|
|
||||||
|
buffer.set_size(&mut font_system, width_constraint, None);
|
||||||
|
|
||||||
|
// Determine measured size of text
|
||||||
|
let (width, total_lines) = buffer
|
||||||
|
.layout_runs()
|
||||||
|
.fold((0.0, 0usize), |(width, total_lines), run| {
|
||||||
|
(run.line_w.max(width), total_lines + 1)
|
||||||
|
});
|
||||||
|
let height = total_lines as f32 * buffer.metrics().line_height;
|
||||||
|
taffy::Size { width, height }
|
||||||
|
}
|
||||||
|
}
|
||||||
11
wgui/src/widget/util.rs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub enum WLength {
|
||||||
|
Units(f32),
|
||||||
|
Percent(f32), // 0.0 - 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for WLength {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Units(0.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
1
wgui/uidev-vk/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
target
|
||||||
3389
wgui/uidev-vk/Cargo.lock
generated
Normal file
22
wgui/uidev-vk/Cargo.toml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
[package]
|
||||||
|
name = "uidev-vk"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = { workspace = true }
|
||||||
|
glam = { workspace = true }
|
||||||
|
log = { workspace = true }
|
||||||
|
rust-embed = "8.7.2"
|
||||||
|
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
|
||||||
|
wgui = { path = "../" }
|
||||||
|
winit = "0.30.10"
|
||||||
|
vulkano = { workspace = true }
|
||||||
|
vulkano-shaders = { workspace = true }
|
||||||
|
|
||||||
|
[profile.dev]
|
||||||
|
opt-level = 0
|
||||||
|
debug = true
|
||||||
|
strip = "none"
|
||||||
|
debug-assertions = true
|
||||||
|
incremental = true
|
||||||
3
wgui/uidev-vk/assets/dashboard/add.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path fill="white" d="M11 13H5v-2h6V5h2v6h6v2h-6v6h-2z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 150 B |
3
wgui/uidev-vk/assets/dashboard/alphabetical.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path fill="white" d="M6 11a2 2 0 0 1 2 2v4H4a2 2 0 0 1-2-2v-2a2 2 0 0 1 2-2zm-2 2v2h2v-2zm16 0v2h2v2h-2a2 2 0 0 1-2-2v-2a2 2 0 0 1 2-2h2v2zm-8-6v4h2a2 2 0 0 1 2 2v2a2 2 0 0 1-2 2h-2a2 2 0 0 1-2-2V7zm0 8h2v-2h-2z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 308 B |
3
wgui/uidev-vk/assets/dashboard/apps.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path fill="white" fill-rule="evenodd" d="M4.857 3A1.857 1.857 0 0 0 3 4.857v4.286C3 10.169 3.831 11 4.857 11h4.286A1.857 1.857 0 0 0 11 9.143V4.857A1.857 1.857 0 0 0 9.143 3zm10 0A1.857 1.857 0 0 0 13 4.857v4.286c0 1.026.831 1.857 1.857 1.857h4.286A1.857 1.857 0 0 0 21 9.143V4.857A1.857 1.857 0 0 0 19.143 3zm-10 10A1.857 1.857 0 0 0 3 14.857v4.286C3 20.169 3.831 21 4.857 21h4.286A1.857 1.857 0 0 0 11 19.143v-4.286A1.857 1.857 0 0 0 9.143 13zm10 0A1.857 1.857 0 0 0 13 14.857v4.286c0 1.026.831 1.857 1.857 1.857h4.286A1.857 1.857 0 0 0 21 19.143v-4.286A1.857 1.857 0 0 0 19.143 13z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 702 B |
3
wgui/uidev-vk/assets/dashboard/back.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path fill="white" d="m9.55 12l7.35 7.35q.375.375.363.875t-.388.875t-.875.375t-.875-.375l-7.7-7.675q-.3-.3-.45-.675t-.15-.75t.15-.75t.45-.675l7.7-7.7q.375-.375.888-.363t.887.388t.375.875t-.375.875z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 293 B |
3
wgui/uidev-vk/assets/dashboard/bat_10.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path fill="white" d="M16 18H8V6h8m.67-2H15V2H9v2H7.33A1.33 1.33 0 0 0 6 5.33v15.34C6 21.4 6.6 22 7.33 22h9.34A1.33 1.33 0 0 0 18 20.67V5.33C18 4.6 17.4 4 16.67 4" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 258 B |
3
wgui/uidev-vk/assets/dashboard/bat_100.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path fill="white" d="M16.67 4H15V2H9v2H7.33A1.33 1.33 0 0 0 6 5.33v15.34C6 21.4 6.6 22 7.33 22h9.34A1.33 1.33 0 0 0 18 20.67V5.33C18 4.6 17.4 4 16.67 4" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 248 B |
3
wgui/uidev-vk/assets/dashboard/bat_20.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path fill="white" d="M16 17H8V6h8m.67-2H15V2H9v2H7.33A1.33 1.33 0 0 0 6 5.33v15.34C6 21.4 6.6 22 7.33 22h9.34A1.33 1.33 0 0 0 18 20.67V5.33C18 4.6 17.4 4 16.67 4" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 258 B |
3
wgui/uidev-vk/assets/dashboard/bat_30.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path fill="white" d="M16 15H8V6h8m.67-2H15V2H9v2H7.33A1.33 1.33 0 0 0 6 5.33v15.34C6 21.4 6.6 22 7.33 22h9.34A1.33 1.33 0 0 0 18 20.67V5.33C18 4.6 17.4 4 16.67 4" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 258 B |
3
wgui/uidev-vk/assets/dashboard/bat_40.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path fill="white" d="M16 14H8V6h8m.67-2H15V2H9v2H7.33A1.33 1.33 0 0 0 6 5.33v15.34C6 21.4 6.6 22 7.33 22h9.34A1.33 1.33 0 0 0 18 20.67V5.33C18 4.6 17.4 4 16.67 4" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 258 B |
3
wgui/uidev-vk/assets/dashboard/bat_50.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path fill="white" d="M16 13H8V6h8m.67-2H15V2H9v2H7.33A1.33 1.33 0 0 0 6 5.33v15.34C6 21.4 6.6 22 7.33 22h9.34A1.33 1.33 0 0 0 18 20.67V5.33C18 4.6 17.4 4 16.67 4" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 258 B |
3
wgui/uidev-vk/assets/dashboard/bat_60.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path fill="white" d="M16 12H8V6h8m.67-2H15V2H9v2H7.33A1.33 1.33 0 0 0 6 5.33v15.34C6 21.4 6.6 22 7.33 22h9.34A1.33 1.33 0 0 0 18 20.67V5.33C18 4.6 17.4 4 16.67 4" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 258 B |
3
wgui/uidev-vk/assets/dashboard/bat_70.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path fill="white" d="M16 10H8V6h8m.67-2H15V2H9v2H7.33A1.33 1.33 0 0 0 6 5.33v15.34C6 21.4 6.6 22 7.33 22h9.34A1.33 1.33 0 0 0 18 20.67V5.33C18 4.6 17.4 4 16.67 4" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 258 B |
3
wgui/uidev-vk/assets/dashboard/bat_80.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path fill="white" d="M16 9H8V6h8m.67-2H15V2H9v2H7.33A1.33 1.33 0 0 0 6 5.33v15.34C6 21.4 6.6 22 7.33 22h9.34A1.33 1.33 0 0 0 18 20.67V5.33C18 4.6 17.4 4 16.67 4" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 257 B |
3
wgui/uidev-vk/assets/dashboard/bat_90.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path fill="white" d="M16 8H8V6h8m.67-2H15V2H9v2H7.33A1.33 1.33 0 0 0 6 5.33v15.34C6 21.4 6.6 22 7.33 22h9.34A1.33 1.33 0 0 0 18 20.67V5.33C18 4.6 17.4 4 16.67 4" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 257 B |
3
wgui/uidev-vk/assets/dashboard/bat_chr_10.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path fill="white" d="M23.05 11h-3V4l-5 10h3v8M12 18H4l.05-12h8m.67-2h-1.67V2h-6v2H3.38a1.33 1.33 0 0 0-1.33 1.33v15.34c0 .73.6 1.33 1.33 1.33h9.34c.73 0 1.33-.6 1.33-1.33V5.33A1.33 1.33 0 0 0 12.72 4" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 296 B |
3
wgui/uidev-vk/assets/dashboard/bat_chr_100.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path fill="white" d="M23 11h-3V4l-5 10h3v8M12.67 4H11V2H5v2H3.33A1.33 1.33 0 0 0 2 5.33v15.34C2 21.4 2.6 22 3.33 22h9.34c.73 0 1.33-.6 1.33-1.33V5.33A1.33 1.33 0 0 0 12.67 4" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 270 B |
3
wgui/uidev-vk/assets/dashboard/bat_chr_20.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path fill="white" d="M23.05 11h-3V4l-5 10h3v8m-6-5h-8V6h8m.67-2h-1.67V2h-6v2H3.38a1.33 1.33 0 0 0-1.33 1.33v15.34c0 .73.6 1.33 1.33 1.33h9.34c.73 0 1.33-.6 1.33-1.33V5.33A1.33 1.33 0 0 0 12.72 4" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 291 B |
3
wgui/uidev-vk/assets/dashboard/bat_chr_30.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path fill="white" d="M12 15H4V6h8m.67-2H11V2H5v2H3.33A1.33 1.33 0 0 0 2 5.33v15.34C2 21.4 2.6 22 3.33 22h9.34c.73 0 1.33-.6 1.33-1.33V5.33A1.33 1.33 0 0 0 12.67 4M23 11h-3V4l-5 10h3v8z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 281 B |
3
wgui/uidev-vk/assets/dashboard/bat_chr_40.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path fill="white" d="M13 4h-2V2H5v2H3c-.6 0-1 .4-1 1v16c0 .6.4 1 1 1h10c.6 0 1-.4 1-1V5c0-.6-.4-1-1-1m-1 10.5H4V6h8zM23 11h-3V4l-5 10h3v8" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 234 B |
3
wgui/uidev-vk/assets/dashboard/bat_chr_50.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path fill="white" d="M23 11h-3V4l-5 10h3v8m-6-9H4V6h8m.67-2H11V2H5v2H3.33A1.33 1.33 0 0 0 2 5.33v15.34C2 21.4 2.6 22 3.33 22h9.34c.73 0 1.33-.6 1.33-1.33V5.33A1.33 1.33 0 0 0 12.67 4" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 279 B |
3
wgui/uidev-vk/assets/dashboard/bat_chr_60.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path fill="white" d="M12 11H4V6h8m.67-2H11V2H5v2H3.33A1.33 1.33 0 0 0 2 5.33v15.34C2 21.4 2.6 22 3.33 22h9.34c.73 0 1.33-.6 1.33-1.33V5.33A1.33 1.33 0 0 0 12.67 4M23 11h-3V4l-5 10h3v8z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 281 B |
3
wgui/uidev-vk/assets/dashboard/bat_chr_70.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path fill="white" d="M12 10H4V6h8m.67-2H11V2H5v2H3.33A1.33 1.33 0 0 0 2 5.33v15.34C2 21.4 2.6 22 3.33 22h9.34c.73 0 1.33-.6 1.33-1.33V5.33A1.33 1.33 0 0 0 12.67 4M23 11h-3V4l-5 10h3v8z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 281 B |
3
wgui/uidev-vk/assets/dashboard/bat_chr_80.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path fill="white" d="M23 11h-3V4l-5 10h3v8M12 9H4V6h8m.67-2H11V2H5v2H3.33A1.33 1.33 0 0 0 2 5.33v15.34C2 21.4 2.6 22 3.33 22h9.34c.73 0 1.33-.6 1.33-1.33V5.33A1.33 1.33 0 0 0 12.67 4" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 279 B |
3
wgui/uidev-vk/assets/dashboard/bat_chr_90.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path fill="white" d="M23 11h-3V4l-5 10h3v8M12 8H4V6h8m.67-2H11V2H5v2H3.33A1.33 1.33 0 0 0 2 5.33v15.34C2 21.4 2.6 22 3.33 22h9.34c.73 0 1.33-.6 1.33-1.33V5.33A1.33 1.33 0 0 0 12.67 4" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 279 B |
7
wgui/uidev-vk/assets/dashboard/binary.svg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<g fill="none" stroke="white" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
|
||||||
|
<rect width="4" height="6" x="14" y="14" rx="2" />
|
||||||
|
<rect width="4" height="6" x="6" y="4" rx="2" />
|
||||||
|
<path d="M6 20h4m4-10h4M6 14h2v6m6-16h2v6" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 344 B |
3
wgui/uidev-vk/assets/dashboard/burger.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path fill="white" d="M21 18H3v-2h18v2Zm0-5H3v-2h18v2Zm0-5H3V6h18v2Z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 165 B |
3
wgui/uidev-vk/assets/dashboard/category_search.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path fill="white" d="M3 20.5q-.425 0-.712-.288T2 19.5v-6q0-.425.288-.712T3 12.5h6q.425 0 .713.288T10 13.5v6q0 .425-.288.713T9 20.5zm1-2h4v-4H4zM14.725 10h-7.45q-.575 0-.862-.513t.012-1.012L10.15 2.4q.3-.5.85-.5t.85.5l3.725 6.075q.3.5.013 1.013t-.863.512M9.05 8h3.9L11 4.85zm11.825 14.25l-1.95-1.95q-.525.35-1.137.525T16.5 21q-1.875 0-3.187-1.312T12 16.5t1.313-3.187T16.5 12t3.188 1.313T21 16.5q0 .65-.175 1.263t-.5 1.137l1.95 1.95q.275.275.275.7t-.275.7t-.7.275t-.7-.275M16.5 19q1.05 0 1.775-.725T19 16.5t-.725-1.775T16.5 14t-1.775.725T14 16.5t.725 1.775T16.5 19M11 8" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 664 B |
3
wgui/uidev-vk/assets/dashboard/circle.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path fill="white" d="M12 22q-2.075 0-3.9-.788q-1.825-.787-3.175-2.137q-1.35-1.35-2.137-3.175Q2 14.075 2 12t.788-3.9q.787-1.825 2.137-3.175q1.35-1.35 3.175-2.138Q9.925 2 12 2t3.9.787q1.825.788 3.175 2.138q1.35 1.35 2.137 3.175Q22 9.925 22 12t-.788 3.9q-.787 1.825-2.137 3.175q-1.35 1.35-3.175 2.137Q14.075 22 12 22Z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 411 B |
3
wgui/uidev-vk/assets/dashboard/close.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path fill="white" d="M8.4 17L7 15.6l3.6-3.6L7 8.425l1.4-1.4l3.6 3.6l3.575-3.6l1.4 1.4l-3.6 3.575l3.6 3.6l-1.4 1.4L12 13.4z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 219 B |
4
wgui/uidev-vk/assets/dashboard/cpu.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024">
|
||||||
|
<path fill="white" d="M320 256a64 64 0 0 0-64 64v384a64 64 0 0 0 64 64h384a64 64 0 0 0 64-64V320a64 64 0 0 0-64-64zm0-64h384a128 128 0 0 1 128 128v384a128 128 0 0 1-128 128H320a128 128 0 0 1-128-128V320a128 128 0 0 1 128-128" />
|
||||||
|
<path fill="white" d="M512 64a32 32 0 0 1 32 32v128h-64V96a32 32 0 0 1 32-32m160 0a32 32 0 0 1 32 32v128h-64V96a32 32 0 0 1 32-32m-320 0a32 32 0 0 1 32 32v128h-64V96a32 32 0 0 1 32-32m160 896a32 32 0 0 1-32-32V800h64v128a32 32 0 0 1-32 32m160 0a32 32 0 0 1-32-32V800h64v128a32 32 0 0 1-32 32m-320 0a32 32 0 0 1-32-32V800h64v128a32 32 0 0 1-32 32M64 512a32 32 0 0 1 32-32h128v64H96a32 32 0 0 1-32-32m0-160a32 32 0 0 1 32-32h128v64H96a32 32 0 0 1-32-32m0 320a32 32 0 0 1 32-32h128v64H96a32 32 0 0 1-32-32m896-160a32 32 0 0 1-32 32H800v-64h128a32 32 0 0 1 32 32m0-160a32 32 0 0 1-32 32H800v-64h128a32 32 0 0 1 32 32m0 320a32 32 0 0 1-32 32H800v-64h128a32 32 0 0 1 32 32" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 996 B |
3
wgui/uidev-vk/assets/dashboard/display.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path fill="white" d="M2 16V4q0-.825.588-1.412T4 2h14v2H4v12zm9 6v-2H8q-.825 0-1.412-.587T6 18V8q0-.825.588-1.412T8 6h13q.825 0 1.413.588T23 8v10q0 .825-.587 1.413T21 20h-3v2zm-3-4h13V8H8zm6.5-5" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 290 B |
6
wgui/uidev-vk/assets/dashboard/displayport.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 16 16">
|
||||||
|
<g fill="white">
|
||||||
|
<path d="M2.5 7a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 1 0V8h10v.5a.5.5 0 0 0 1 0v-1a.5.5 0 0 0-.5-.5z" />
|
||||||
|
<path d="M1 5a1 1 0 0 0-1 1v3.191a1 1 0 0 0 .553.894l1.618.81a1 1 0 0 0 .447.105H15a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1zm0 1h14v4H2.618L1 9.191z" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 358 B |
6
wgui/uidev-vk/assets/dashboard/eye.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<g fill="none" stroke="white" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
|
||||||
|
<path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0-4 0" />
|
||||||
|
<path d="M21 12q-3.6 6-9 6t-9-6q3.6-6 9-6t9 6" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 300 B |
3
wgui/uidev-vk/assets/dashboard/fix_floor.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path fill="white" d="M6 22q-.825 0-1.412-.587T4 20V4q0-.825.588-1.412T6 2h12q.825 0 1.413.588T20 4v16q0 .825-.587 1.413T18 22zm12-2V4H6v16zm0-16H6zM9 9h6l-3-3zm3 9l3-3H9z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 267 B |
3
wgui/uidev-vk/assets/dashboard/games.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path fill="white" d="M7 6h10a6 6 0 0 1 6 6a6 6 0 0 1-6 6c-1.78 0-3.37-.77-4.47-2h-1.06c-1.1 1.23-2.69 2-4.47 2a6 6 0 0 1-6-6a6 6 0 0 1 6-6M6 9v2H4v2h2v2h2v-2h2v-2H8V9zm9.5 3a1.5 1.5 0 0 0-1.5 1.5a1.5 1.5 0 0 0 1.5 1.5a1.5 1.5 0 0 0 1.5-1.5a1.5 1.5 0 0 0-1.5-1.5m3-3a1.5 1.5 0 0 0-1.5 1.5a1.5 1.5 0 0 0 1.5 1.5a1.5 1.5 0 0 0 1.5-1.5A1.5 1.5 0 0 0 18.5 9" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 450 B |
3
wgui/uidev-vk/assets/dashboard/github.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path fill="white" d="M12 2A10 10 0 0 0 2 12c0 4.42 2.87 8.17 6.84 9.5c.5.08.66-.23.66-.5v-1.69c-2.77.6-3.36-1.34-3.36-1.34c-.46-1.16-1.11-1.47-1.11-1.47c-.91-.62.07-.6.07-.6c1 .07 1.53 1.03 1.53 1.03c.87 1.52 2.34 1.07 2.91.83c.09-.65.35-1.09.63-1.34c-2.22-.25-4.55-1.11-4.55-4.92c0-1.11.38-2 1.03-2.71c-.1-.25-.45-1.29.1-2.64c0 0 .84-.27 2.75 1.02c.79-.22 1.65-.33 2.5-.33s1.71.11 2.5.33c1.91-1.29 2.75-1.02 2.75-1.02c.55 1.35.2 2.39.1 2.64c.65.71 1.03 1.6 1.03 2.71c0 3.82-2.34 4.66-4.57 4.91c.36.31.69.92.69 1.85V21c0 .27.16.59.67.5C19.14 20.16 22 16.42 22 12A10 10 0 0 0 12 2" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 676 B |
3
wgui/uidev-vk/assets/dashboard/globe.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path fill="white" d="M12 22q-2.075 0-3.9-.788t-3.175-2.137T2.788 15.9T2 12t.788-3.9t2.137-3.175T8.1 2.788T12 2t3.9.788t3.175 2.137T21.213 8.1T22 12t-.788 3.9t-2.137 3.175t-3.175 2.138T12 22m0-2q3.35 0 5.675-2.325T20 12q0-.175-.012-.363t-.013-.312q-.125.725-.675 1.2T18 13h-2q-.825 0-1.412-.587T14 11v-1h-4V8q0-.825.588-1.412T12 6h1q0-.575.313-1.012t.762-.713q-.5-.125-1.012-.2T12 4Q8.65 4 6.325 6.325T4 12h5q1.65 0 2.825 1.175T13 16v1h-3v2.75q.5.125.988.188T12 20" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 560 B |
3
wgui/uidev-vk/assets/dashboard/home.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path fill="white" d="M4 21V9l8-6l8 6v12h-6v-7h-4v7Z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 148 B |
3
wgui/uidev-vk/assets/dashboard/knife.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Design Icons by Pictogrammers - https://github.com/Templarian/MaterialDesign/blob/master/LICENSE -->
|
||||||
|
<path fill="#F22" d="M20.62 2c3.35 5.61-8.15 18.15-8.15 18.15L9.6 17.28L4.91 22l-2.14-2.14z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 311 B |
3
wgui/uidev-vk/assets/dashboard/magic_wand.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path fill="none" stroke="white" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 21L21 6l-3-3L3 18zm9-15l3 3M9 3a2 2 0 0 0 2 2a2 2 0 0 0-2 2a2 2 0 0 0-2-2a2 2 0 0 0 2-2m10 10a2 2 0 0 0 2 2a2 2 0 0 0-2 2a2 2 0 0 0-2-2a2 2 0 0 0 2-2" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 348 B |
3
wgui/uidev-vk/assets/dashboard/microphone.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path fill="white" d="M12 2a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3m7 9c0 3.53-2.61 6.44-6 6.93V21h-2v-3.07c-3.39-.49-6-3.4-6-6.93h2a5 5 0 0 0 5 5a5 5 0 0 0 5-5z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 277 B |
3
wgui/uidev-vk/assets/dashboard/minijack.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path fill="white" d="M11 4V3c0-.55.45-1 1-1s1 .45 1 1v1zm2 5V5h-2v4H9v6c0 1.3.84 2.4 2 2.82V22h2v-4.18c1.16-.42 2-1.52 2-2.82V9z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 225 B |
44
wgui/uidev-vk/assets/dashboard/monado.svg
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
id="vector"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 150.72 144.4"
|
||||||
|
version="1.1"
|
||||||
|
sodipodi:docname="ic_monado_notif.svg"
|
||||||
|
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="true"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:zoom="24.5"
|
||||||
|
inkscape:cx="10.265306"
|
||||||
|
inkscape:cy="15.469388"
|
||||||
|
inkscape:window-width="2560"
|
||||||
|
inkscape:window-height="1378"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="vector" />
|
||||||
|
<g
|
||||||
|
transform="translate(6.0288, 8.5612955) scale(0.92, 0.8814225)"
|
||||||
|
id="g_0"
|
||||||
|
style="fill:#ffffff">
|
||||||
|
<path
|
||||||
|
fill="#782b90"
|
||||||
|
d="m143.23,19.75L79.85,0.66c-2.93,-0.88 -6.05,-0.88 -8.97,0L7.49,19.75c-4.45,1.34 -7.49,5.43 -7.49,10.08v85.07c0,4.66 3.07,8.77 7.54,10.09l63.43,18.77c2.87,0.85 5.92,0.85 8.79,0l63.43,-18.77c4.47,-1.32 7.54,-5.43 7.54,-10.09L150.72,29.83c0,-4.64 -3.04,-8.74 -7.49,-10.08ZM49.02,104l-17.99,-5.35c-2.52,-0.75 -4.24,-3.06 -4.24,-5.68v-36.81l22.23,30.57v17.28ZM75.36,108.15v0l-0,-0 -0,0v-0L26.79,41.84l17.99,-5.35c2.35,-0.7 4.88,0.12 6.38,2.06l24.19,31.33 24.19,-31.33c1.5,-1.94 4.04,-2.76 6.38,-2.06l17.99,5.35 -48.56,66.3ZM123.93,92.96c0,2.62 -1.72,4.94 -4.24,5.68l-17.99,5.35v-17.28l22.23,-30.57v36.81Z"
|
||||||
|
id="path1"
|
||||||
|
style="fill:#ffffff" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
3
wgui/uidev-vk/assets/dashboard/panorama.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path fill="white" d="M3 20q-.425 0-.712-.288T2 19V5q0-.425.288-.712T3 4q.2 0 .888.238t1.837.512t2.713.513T12 5.5t3.563-.238t2.712-.512t1.838-.513T21 4q.425 0 .713.288T22 5v14q0 .425-.288.713T21 20q-.2 0-.888-.238t-1.837-.512t-2.712-.513T12 18.5t-3.562.238t-2.713.512t-1.837.513T3 20m1-2.35q1.95-.575 3.963-.862T12 16.5t4.038.288T20 17.65V6.375q-1.95.575-3.963.85T12 7.5t-4.038-.275T4 6.375zM12 12" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 493 B |
3
wgui/uidev-vk/assets/dashboard/play.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path fill="white" d="M8 19V5l11 7z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 131 B |
3
wgui/uidev-vk/assets/dashboard/power.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path fill="white" d="M12 22q-2.075 0-3.9-.788t-3.175-2.137T2.788 15.9T2 12q0-2.1.788-3.912t2.137-3.163l1.4 1.4q-1.1 1.1-1.712 2.55T4 12q0 3.35 2.325 5.675T12 20t5.675-2.325T20 12q0-1.675-.612-3.125t-1.713-2.55l1.4-1.4q1.35 1.35 2.138 3.163T22 12q0 2.075-.788 3.9t-2.137 3.175t-3.175 2.138T12 22m-1-9V2h2v11z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 405 B |
3
wgui/uidev-vk/assets/dashboard/recenter.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path fill="white" d="M11 23v-4.175L9.9 19.9l-1.4-1.4L12 15l3.5 3.5l-1.4 1.4l-1.1-1.075V23zm-5.5-7.5l-1.4-1.4L5.175 13H1v-2h4.175L4.1 9.9l1.4-1.4L9 12zm13 0L15 12l3.5-3.5l1.4 1.4l-1.075 1.1H23v2h-4.175l1.075 1.1zm-6.5-2q-.625 0-1.062-.437T10.5 12t.438-1.062T12 10.5t1.063.438T13.5 12t-.437 1.063T12 13.5M12 9L8.5 5.5l1.4-1.4L11 5.175V1h2v4.175L14.1 4.1l1.4 1.4z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 458 B |
3
wgui/uidev-vk/assets/dashboard/refresh.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||||
|
<path fill="white" d="M12 20q-3.35 0-5.675-2.325T4 12t2.325-5.675T12 4q1.725 0 3.3.712T18 6.75V4h2v7h-7V9h4.2q-.8-1.4-2.187-2.2T12 6Q9.5 6 7.75 7.75T6 12t1.75 4.25T12 18q1.925 0 3.475-1.1T17.65 14h2.1q-.7 2.65-2.85 4.325T12 20" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 323 B |