Add minimal text layout API using taffy, cosmic-text, and wgpu
Introduces a public App/TextBox builder API that renders word-wrapped ASCII text in taffy-laid-out rectangles via a wgpu GPU pipeline. - App/TextBox builder API (width, height, font_size, text_color, background, padding) - Taffy flex-column layout tree computed per frame - cosmic-text shapes and rasterizes text to a CPU RGBA8 buffer each frame - Two wgpu pipelines: rect (colored quads) and text (textured quads) - Winit event loop wired through ApplicationHandler
This commit is contained in:
Generated
+228
-3
@@ -378,6 +378,39 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core_maths"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30"
|
||||
dependencies = [
|
||||
"libm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cosmic-text"
|
||||
version = "0.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be17b688510d934ce13f48a2beba700e11583e281e0fda99c22bb256a14eda73"
|
||||
dependencies = [
|
||||
"bitflags 2.13.0",
|
||||
"fontdb",
|
||||
"harfrust",
|
||||
"linebender_resource_handle",
|
||||
"log",
|
||||
"rangemap",
|
||||
"rustc-hash 2.1.2",
|
||||
"self_cell",
|
||||
"skrifa 0.40.0",
|
||||
"smol_str 0.3.6",
|
||||
"swash",
|
||||
"sys-locale",
|
||||
"unicode-bidi",
|
||||
"unicode-linebreak",
|
||||
"unicode-script",
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.21"
|
||||
@@ -499,6 +532,38 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||
|
||||
[[package]]
|
||||
name = "font-types"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b38ad915f6dadd993ced50848a8291a543bd41ca62bc10740d5e64e2ab4cfd7"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fontconfig-parser"
|
||||
version = "0.5.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646"
|
||||
dependencies = [
|
||||
"roxmltree",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fontdb"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905"
|
||||
dependencies = [
|
||||
"fontconfig-parser",
|
||||
"log",
|
||||
"memmap2",
|
||||
"slotmap",
|
||||
"tinyvec",
|
||||
"ttf-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.5.0"
|
||||
@@ -649,6 +714,8 @@ name = "gui-layout"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytemuck",
|
||||
"cosmic-text",
|
||||
"env_logger",
|
||||
"log",
|
||||
"pollster",
|
||||
@@ -669,6 +736,19 @@ dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "harfrust"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9da2e5ae821f6e96664977bf974d6d6a2d6682f9ccee23e62ec1d134246845f9"
|
||||
dependencies = [
|
||||
"bitflags 2.13.0",
|
||||
"bytemuck",
|
||||
"core_maths",
|
||||
"read-fonts 0.37.0",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.5"
|
||||
@@ -877,6 +957,12 @@ dependencies = [
|
||||
"redox_syscall 0.8.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linebender_resource_handle"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.4.15"
|
||||
@@ -945,7 +1031,7 @@ dependencies = [
|
||||
"log",
|
||||
"num-traits",
|
||||
"once_cell",
|
||||
"rustc-hash",
|
||||
"rustc-hash 1.1.0",
|
||||
"spirv",
|
||||
"thiserror 2.0.18",
|
||||
"unicode-ident",
|
||||
@@ -1474,6 +1560,12 @@ version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08"
|
||||
|
||||
[[package]]
|
||||
name = "rangemap"
|
||||
version = "1.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "973443cf09a9c8656b574a866ab68dfa19f0867d0340648c7d2f6a71b8a8ea68"
|
||||
|
||||
[[package]]
|
||||
name = "raw-window-handle"
|
||||
version = "0.6.2"
|
||||
@@ -1492,6 +1584,27 @@ dependencies = [
|
||||
"objc2-quartz-core 0.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "read-fonts"
|
||||
version = "0.37.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b634fabf032fab15307ffd272149b622260f55974d9fad689292a5d33df02e5"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"core_maths",
|
||||
"font-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "read-fonts"
|
||||
version = "0.39.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4ed38b89c2c77ff968c524145ad65fb010f38af5c7a224b53b81d47ac2daa81"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"font-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.4.1"
|
||||
@@ -1554,12 +1667,24 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
|
||||
|
||||
[[package]]
|
||||
name = "roxmltree"
|
||||
version = "0.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.1"
|
||||
@@ -1635,6 +1760,12 @@ dependencies = [
|
||||
"tiny-skia",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "self_cell"
|
||||
version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89"
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.28"
|
||||
@@ -1693,6 +1824,26 @@ version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
|
||||
|
||||
[[package]]
|
||||
name = "skrifa"
|
||||
version = "0.40.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fbdfe3d2475fbd7ddd1f3e5cf8288a30eb3e5f95832829570cd88115a7434ac"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"read-fonts 0.37.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "skrifa"
|
||||
version = "0.42.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c34617370ae968efb7161bb2beb517d9084659aae19e24b89e3db25b46e4564"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"read-fonts 0.39.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.12"
|
||||
@@ -1748,6 +1899,12 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smol_str"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4aaa7368fcf4852a4c2dd92df0cace6a71f2091ca0a23391ce7f3a31833f1523"
|
||||
|
||||
[[package]]
|
||||
name = "spirv"
|
||||
version = "0.4.0+sdk-1.4.341.0"
|
||||
@@ -1769,6 +1926,17 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
|
||||
|
||||
[[package]]
|
||||
name = "swash"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0811b01ca2c4e8718760713911feaf4675c24f94e50530a015ec646cfb622f7c"
|
||||
dependencies = [
|
||||
"skrifa 0.42.1",
|
||||
"yazi",
|
||||
"zeno",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
@@ -1780,6 +1948,15 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sys-locale"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "taffy"
|
||||
version = "0.11.0"
|
||||
@@ -1866,6 +2043,21 @@ dependencies = [
|
||||
"strict-num",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
|
||||
dependencies = [
|
||||
"tinyvec_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec_macros"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "1.1.1+spec-1.1.0"
|
||||
@@ -1917,6 +2109,15 @@ name = "ttf-parser"
|
||||
version = "0.25.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
|
||||
dependencies = [
|
||||
"core_maths",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
@@ -1924,6 +2125,18 @@ version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-linebreak"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-script"
|
||||
version = "0.5.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.13.3"
|
||||
@@ -2203,7 +2416,7 @@ dependencies = [
|
||||
"portable-atomic",
|
||||
"profiling",
|
||||
"raw-window-handle",
|
||||
"rustc-hash",
|
||||
"rustc-hash 1.1.0",
|
||||
"smallvec",
|
||||
"thiserror 2.0.18",
|
||||
"wgpu-core-deps-apple",
|
||||
@@ -2564,7 +2777,7 @@ dependencies = [
|
||||
"rustix 0.38.44",
|
||||
"sctk-adwaita",
|
||||
"smithay-client-toolkit",
|
||||
"smol_str",
|
||||
"smol_str 0.2.2",
|
||||
"tracing",
|
||||
"unicode-segmentation",
|
||||
"wasm-bindgen",
|
||||
@@ -2659,6 +2872,18 @@ version = "0.8.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f"
|
||||
|
||||
[[package]]
|
||||
name = "yazi"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e01738255b5a16e78bbb83e7fbba0a1e7dd506905cfc53f4622d89015a03fbb5"
|
||||
|
||||
[[package]]
|
||||
name = "zeno"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524"
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.52"
|
||||
|
||||
@@ -11,3 +11,5 @@ log = "0.4.32"
|
||||
anyhow = "1.0.102"
|
||||
pollster = "0.4.0"
|
||||
taffy = "0.11.0"
|
||||
cosmic-text = "0.19.0"
|
||||
bytemuck = { version = "1.25.0", features = ["derive"] }
|
||||
|
||||
+508
@@ -0,0 +1,508 @@
|
||||
pub mod renderer;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use cosmic_text::{Attrs, Buffer, Color as TextColor, FontSystem, Metrics, Shaping, SwashCache};
|
||||
use taffy::prelude::*;
|
||||
use wgpu::InstanceDescriptor;
|
||||
use winit::{
|
||||
application::ApplicationHandler,
|
||||
event::*,
|
||||
event_loop::{ActiveEventLoop, EventLoop},
|
||||
keyboard::{KeyCode, PhysicalKey},
|
||||
window::Window,
|
||||
};
|
||||
|
||||
use renderer::{DrawRect, DrawText, Renderer};
|
||||
|
||||
/// A fixed-size box that renders word-wrapped text.
|
||||
pub struct TextBox {
|
||||
text: String,
|
||||
width: Option<f32>,
|
||||
height: Option<f32>,
|
||||
font_size: f32,
|
||||
text_color: [f32; 4],
|
||||
bg_color: [f32; 4],
|
||||
padding: f32,
|
||||
}
|
||||
|
||||
impl TextBox {
|
||||
pub fn new(text: impl Into<String>) -> Self {
|
||||
Self {
|
||||
text: text.into(),
|
||||
width: None,
|
||||
height: None,
|
||||
font_size: 16.0,
|
||||
text_color: [1.0, 1.0, 1.0, 1.0],
|
||||
bg_color: [0.15, 0.25, 0.35, 1.0],
|
||||
padding: 8.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn width(mut self, w: f32) -> Self {
|
||||
self.width = Some(w);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn height(mut self, h: f32) -> Self {
|
||||
self.height = Some(h);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn font_size(mut self, size: f32) -> Self {
|
||||
self.font_size = size;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn text_color(mut self, color: [f32; 4]) -> Self {
|
||||
self.text_color = color;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn background(mut self, color: [f32; 4]) -> Self {
|
||||
self.bg_color = color;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn padding(mut self, p: f32) -> Self {
|
||||
self.padding = p;
|
||||
self
|
||||
}
|
||||
|
||||
fn taffy_style(&self) -> taffy::Style {
|
||||
taffy::Style {
|
||||
size: Size {
|
||||
width: match self.width {
|
||||
Some(w) => Dimension::from_length(w),
|
||||
None => Dimension::AUTO,
|
||||
},
|
||||
height: match self.height {
|
||||
Some(h) => Dimension::from_length(h),
|
||||
None => Dimension::AUTO,
|
||||
},
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Top-level application builder. Chain `.child()` calls then call `.run()`.
|
||||
pub struct App {
|
||||
title: String,
|
||||
bg_color: [f32; 4],
|
||||
children: Vec<TextBox>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
title: "App".to_string(),
|
||||
bg_color: [0.08, 0.08, 0.10, 1.0],
|
||||
children: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn title(mut self, title: impl Into<String>) -> Self {
|
||||
self.title = title.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn background(mut self, color: [f32; 4]) -> Self {
|
||||
self.bg_color = color;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn child(mut self, widget: TextBox) -> Self {
|
||||
self.children.push(widget);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn run(self) -> anyhow::Result<()> {
|
||||
env_logger::try_init().ok();
|
||||
let event_loop = EventLoop::new()?;
|
||||
let mut state = AppState::new(self);
|
||||
event_loop.run_app(&mut state)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for App {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
// ── internal ──────────────────────────────────────────────────────────────────
|
||||
|
||||
struct WgpuState {
|
||||
surface: wgpu::Surface<'static>,
|
||||
device: wgpu::Device,
|
||||
queue: wgpu::Queue,
|
||||
config: wgpu::SurfaceConfiguration,
|
||||
surface_format: wgpu::TextureFormat,
|
||||
is_configured: bool,
|
||||
window: Arc<Window>,
|
||||
}
|
||||
|
||||
struct AppState {
|
||||
app: App,
|
||||
wgpu: Option<WgpuState>,
|
||||
renderer: Option<Renderer>,
|
||||
font_system: FontSystem,
|
||||
swash_cache: SwashCache,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
fn new(app: App) -> Self {
|
||||
Self {
|
||||
app,
|
||||
wgpu: None,
|
||||
renderer: None,
|
||||
font_system: FontSystem::new(),
|
||||
swash_cache: SwashCache::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn render(&mut self) {
|
||||
// Phase 1: acquire surface texture (borrows self.wgpu mutably, then releases)
|
||||
let (output, win_w, win_h) = {
|
||||
let Some(wgpu) = &mut self.wgpu else { return };
|
||||
if !wgpu.is_configured || self.renderer.is_none() {
|
||||
return;
|
||||
}
|
||||
let tex = match wgpu.surface.get_current_texture() {
|
||||
wgpu::CurrentSurfaceTexture::Success(t) => t,
|
||||
wgpu::CurrentSurfaceTexture::Suboptimal(t) => {
|
||||
wgpu.surface.configure(&wgpu.device, &wgpu.config);
|
||||
t
|
||||
}
|
||||
wgpu::CurrentSurfaceTexture::Outdated => {
|
||||
wgpu.surface.configure(&wgpu.device, &wgpu.config);
|
||||
return;
|
||||
}
|
||||
_ => return,
|
||||
};
|
||||
let w = wgpu.config.width;
|
||||
let h = wgpu.config.height;
|
||||
(tex, w, h)
|
||||
}; // wgpu borrow released here
|
||||
|
||||
let view = output
|
||||
.texture
|
||||
.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
|
||||
// Phase 2: build draw lists (needs &mut self for font_system / swash_cache)
|
||||
let (rects, texts) = self.build_draw_lists(win_w, win_h);
|
||||
|
||||
// Phase 3: render (immutable borrows of different fields)
|
||||
let bg = self.app.bg_color;
|
||||
let wgpu = self.wgpu.as_ref().unwrap();
|
||||
let renderer = self.renderer.as_ref().unwrap();
|
||||
renderer.render(
|
||||
&wgpu.device,
|
||||
&wgpu.queue,
|
||||
&view,
|
||||
bg,
|
||||
win_w,
|
||||
win_h,
|
||||
&rects,
|
||||
&texts,
|
||||
);
|
||||
|
||||
output.present();
|
||||
}
|
||||
|
||||
fn build_draw_lists(&mut self, win_w: u32, win_h: u32) -> (Vec<DrawRect>, Vec<DrawText>) {
|
||||
let mut tree: TaffyTree<usize> = TaffyTree::new();
|
||||
|
||||
let child_ids: Vec<NodeId> = self
|
||||
.app
|
||||
.children
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, w)| {
|
||||
tree.new_leaf_with_context(w.taffy_style(), i)
|
||||
.expect("taffy leaf")
|
||||
})
|
||||
.collect();
|
||||
|
||||
let root = tree
|
||||
.new_with_children(
|
||||
taffy::Style {
|
||||
display: Display::Flex,
|
||||
flex_direction: FlexDirection::Column,
|
||||
size: Size {
|
||||
width: Dimension::from_percent(1.0),
|
||||
height: Dimension::from_percent(1.0),
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
&child_ids,
|
||||
)
|
||||
.expect("taffy root");
|
||||
|
||||
tree.compute_layout(
|
||||
root,
|
||||
Size {
|
||||
width: AvailableSpace::Definite(win_w as f32),
|
||||
height: AvailableSpace::Definite(win_h as f32),
|
||||
},
|
||||
)
|
||||
.expect("taffy compute");
|
||||
|
||||
// Collect layout + widget params (releasing borrows on self.app.children)
|
||||
struct WidgetDraw {
|
||||
rect: DrawRect,
|
||||
text_x: f32,
|
||||
text_y: f32,
|
||||
inner_w: f32,
|
||||
inner_h: f32,
|
||||
text: String,
|
||||
font_size: f32,
|
||||
text_color: [f32; 4],
|
||||
}
|
||||
|
||||
let widget_data: Vec<WidgetDraw> = self
|
||||
.app
|
||||
.children
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, widget)| {
|
||||
let layout = tree.layout(child_ids[i]).expect("layout");
|
||||
let x = layout.location.x;
|
||||
let y = layout.location.y;
|
||||
let w = layout.size.width;
|
||||
let h = layout.size.height;
|
||||
let inner_w = (w - widget.padding * 2.0).max(0.0);
|
||||
let inner_h = (h - widget.padding * 2.0).max(0.0);
|
||||
WidgetDraw {
|
||||
rect: DrawRect { x, y, width: w, height: h, color: widget.bg_color },
|
||||
text_x: x + widget.padding,
|
||||
text_y: y + widget.padding,
|
||||
inner_w,
|
||||
inner_h,
|
||||
text: widget.text.clone(),
|
||||
font_size: widget.font_size,
|
||||
text_color: widget.text_color,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Now we can mutably borrow self.font_system / self.swash_cache
|
||||
let mut rects = Vec::new();
|
||||
let mut texts = Vec::new();
|
||||
|
||||
for wd in widget_data {
|
||||
rects.push(wd.rect);
|
||||
if wd.inner_w > 0.0 && wd.inner_h > 0.0 {
|
||||
let pixels = self.rasterize_text(
|
||||
&wd.text,
|
||||
wd.font_size,
|
||||
wd.text_color,
|
||||
wd.inner_w,
|
||||
wd.inner_h,
|
||||
);
|
||||
texts.push(DrawText {
|
||||
x: wd.text_x,
|
||||
y: wd.text_y,
|
||||
width: wd.inner_w,
|
||||
height: wd.inner_h,
|
||||
pixels,
|
||||
pixel_width: wd.inner_w as u32,
|
||||
pixel_height: wd.inner_h as u32,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
(rects, texts)
|
||||
}
|
||||
|
||||
fn rasterize_text(
|
||||
&mut self,
|
||||
text: &str,
|
||||
font_size: f32,
|
||||
text_color_f: [f32; 4],
|
||||
w: f32,
|
||||
h: f32,
|
||||
) -> Vec<u8> {
|
||||
let tw = w as u32;
|
||||
let th = h as u32;
|
||||
let mut pixels = vec![0u8; (tw * th * 4) as usize];
|
||||
|
||||
let line_height = font_size * 1.25;
|
||||
let mut buffer =
|
||||
Buffer::new(&mut self.font_system, Metrics::new(font_size, line_height));
|
||||
buffer.set_size(Some(w), Some(h));
|
||||
buffer.set_text(text, &Attrs::new(), Shaping::Basic, None);
|
||||
|
||||
let tc = text_color_f;
|
||||
let text_color = TextColor::rgba(
|
||||
(tc[0] * 255.0) as u8,
|
||||
(tc[1] * 255.0) as u8,
|
||||
(tc[2] * 255.0) as u8,
|
||||
(tc[3] * 255.0) as u8,
|
||||
);
|
||||
|
||||
buffer.draw(
|
||||
&mut self.font_system,
|
||||
&mut self.swash_cache,
|
||||
text_color,
|
||||
|px, py, pw, ph, color| {
|
||||
let a = color.a();
|
||||
if a == 0 {
|
||||
return;
|
||||
}
|
||||
for dy in 0..ph {
|
||||
for dx in 0..pw {
|
||||
let sx = px + dx as i32;
|
||||
let sy = py + dy as i32;
|
||||
if sx < 0 || sy < 0 || sx >= tw as i32 || sy >= th as i32 {
|
||||
continue;
|
||||
}
|
||||
let base = ((sy as u32 * tw + sx as u32) * 4) as usize;
|
||||
let fa = a as f32 / 255.0;
|
||||
let inv = 1.0 - fa;
|
||||
pixels[base] =
|
||||
(pixels[base] as f32 * inv + color.r() as f32 * fa) as u8;
|
||||
pixels[base + 1] =
|
||||
(pixels[base + 1] as f32 * inv + color.g() as f32 * fa) as u8;
|
||||
pixels[base + 2] =
|
||||
(pixels[base + 2] as f32 * inv + color.b() as f32 * fa) as u8;
|
||||
pixels[base + 3] =
|
||||
(pixels[base + 3] as f32 * inv + fa * 255.0) as u8;
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
pixels
|
||||
}
|
||||
}
|
||||
|
||||
impl ApplicationHandler for AppState {
|
||||
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||
let mut attrs = Window::default_attributes();
|
||||
attrs.title = self.app.title.clone();
|
||||
let window = Arc::new(
|
||||
event_loop
|
||||
.create_window(attrs)
|
||||
.expect("create window"),
|
||||
);
|
||||
|
||||
let wgpu_state = pollster::block_on(async {
|
||||
let instance = wgpu::Instance::new(InstanceDescriptor {
|
||||
backends: wgpu::Backends::PRIMARY,
|
||||
flags: Default::default(),
|
||||
memory_budget_thresholds: Default::default(),
|
||||
backend_options: Default::default(),
|
||||
display: None,
|
||||
});
|
||||
|
||||
let surface = instance
|
||||
.create_surface(window.clone())
|
||||
.expect("create surface");
|
||||
|
||||
let adapter = instance
|
||||
.request_adapter(&wgpu::RequestAdapterOptions {
|
||||
power_preference: wgpu::PowerPreference::default(),
|
||||
force_fallback_adapter: false,
|
||||
compatible_surface: Some(&surface),
|
||||
})
|
||||
.await
|
||||
.expect("find adapter");
|
||||
|
||||
let surface_caps = surface.get_capabilities(&adapter);
|
||||
let surface_format = surface_caps
|
||||
.formats
|
||||
.iter()
|
||||
.find(|f| f.is_srgb())
|
||||
.copied()
|
||||
.unwrap_or(surface_caps.formats[0]);
|
||||
|
||||
let (device, queue) = adapter
|
||||
.request_device(&wgpu::DeviceDescriptor {
|
||||
label: None,
|
||||
required_features: wgpu::Features::empty(),
|
||||
experimental_features: wgpu::ExperimentalFeatures::disabled(),
|
||||
required_limits: wgpu::Limits::default(),
|
||||
memory_hints: Default::default(),
|
||||
trace: wgpu::Trace::Off,
|
||||
})
|
||||
.await
|
||||
.expect("create device");
|
||||
|
||||
let size = window.inner_size();
|
||||
let config = wgpu::SurfaceConfiguration {
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
format: surface_format,
|
||||
width: size.width.max(1),
|
||||
height: size.height.max(1),
|
||||
present_mode: surface_caps.present_modes[0],
|
||||
alpha_mode: surface_caps.alpha_modes[0],
|
||||
view_formats: vec![],
|
||||
desired_maximum_frame_latency: 2,
|
||||
};
|
||||
|
||||
WgpuState {
|
||||
surface,
|
||||
device,
|
||||
queue,
|
||||
config,
|
||||
surface_format,
|
||||
is_configured: false,
|
||||
window,
|
||||
}
|
||||
});
|
||||
|
||||
let renderer = Renderer::new(&wgpu_state.device, wgpu_state.surface_format);
|
||||
self.renderer = Some(renderer);
|
||||
self.wgpu = Some(wgpu_state);
|
||||
}
|
||||
|
||||
fn window_event(
|
||||
&mut self,
|
||||
event_loop: &ActiveEventLoop,
|
||||
_window_id: winit::window::WindowId,
|
||||
event: WindowEvent,
|
||||
) {
|
||||
match event {
|
||||
WindowEvent::CloseRequested => event_loop.exit(),
|
||||
|
||||
WindowEvent::KeyboardInput {
|
||||
event:
|
||||
KeyEvent {
|
||||
physical_key: PhysicalKey::Code(KeyCode::Escape),
|
||||
state: ElementState::Pressed,
|
||||
..
|
||||
},
|
||||
..
|
||||
} => event_loop.exit(),
|
||||
|
||||
WindowEvent::Resized(size) => {
|
||||
if let Some(wgpu) = &mut self.wgpu {
|
||||
if size.width > 0 && size.height > 0 {
|
||||
wgpu.config.width = size.width;
|
||||
wgpu.config.height = size.height;
|
||||
wgpu.surface.configure(&wgpu.device, &wgpu.config);
|
||||
wgpu.is_configured = true;
|
||||
}
|
||||
}
|
||||
if let Some(wgpu) = &self.wgpu {
|
||||
wgpu.window.request_redraw();
|
||||
}
|
||||
}
|
||||
|
||||
WindowEvent::RedrawRequested => {
|
||||
self.render();
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
|
||||
if let Some(wgpu) = &self.wgpu {
|
||||
wgpu.window.request_redraw();
|
||||
}
|
||||
}
|
||||
}
|
||||
+28
-273
@@ -1,276 +1,31 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use wgpu::InstanceDescriptor;
|
||||
use winit::{
|
||||
application::ApplicationHandler,
|
||||
event::*,
|
||||
event_loop::{ActiveEventLoop, EventLoop},
|
||||
keyboard::{KeyCode, PhysicalKey},
|
||||
window::Window,
|
||||
};
|
||||
|
||||
// This will store the state of our game
|
||||
pub struct State {
|
||||
surface: wgpu::Surface<'static>,
|
||||
device: wgpu::Device,
|
||||
queue: wgpu::Queue,
|
||||
config: wgpu::SurfaceConfiguration,
|
||||
is_surface_configured: bool,
|
||||
window: Arc<Window>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub async fn new(window: Arc<Window>) -> anyhow::Result<Self> {
|
||||
// main purpose of Instance is to create Adapter and Surface
|
||||
let instance = wgpu::Instance::new(InstanceDescriptor {
|
||||
backends: wgpu::Backends::PRIMARY,
|
||||
flags: Default::default(),
|
||||
memory_budget_thresholds: Default::default(),
|
||||
backend_options: Default::default(),
|
||||
display: None,
|
||||
});
|
||||
|
||||
let surface = instance.create_surface(window.clone())?;
|
||||
println!("Surface: {:?}", surface);
|
||||
|
||||
// list all adatpers
|
||||
for item in instance.enumerate_adapters(wgpu::Backends::all()).await {
|
||||
println!("Adapter: {:?}", item);
|
||||
}
|
||||
|
||||
// handle for actual gfx card
|
||||
let adapter = instance
|
||||
.request_adapter(&wgpu::RequestAdapterOptions {
|
||||
power_preference: wgpu::PowerPreference::HighPerformance,
|
||||
force_fallback_adapter: false,
|
||||
compatible_surface: Some(&surface),
|
||||
})
|
||||
.await?;
|
||||
|
||||
println!("ADAPTER: {:?}", adapter);
|
||||
|
||||
let surface_caps = surface.get_capabilities(&adapter);
|
||||
// Shader code in this tutorial assumes an sRGB surface texture. Using a different
|
||||
// one will result in all the colors coming out darker. If you want to support non
|
||||
// sRGB surfaces, you'll need to account for that when drawing to the frame.
|
||||
let surface_format = surface_caps
|
||||
.formats
|
||||
.iter()
|
||||
.find(|f| f.is_srgb())
|
||||
.copied()
|
||||
.unwrap_or(surface_caps.formats[0]);
|
||||
|
||||
let size = window.inner_size();
|
||||
|
||||
let config = wgpu::SurfaceConfiguration {
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
format: surface_format,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
present_mode: surface_caps.present_modes[0],
|
||||
alpha_mode: surface_caps.alpha_modes[0],
|
||||
view_formats: vec![],
|
||||
desired_maximum_frame_latency: 2,
|
||||
};
|
||||
|
||||
let (device, queue) = adapter
|
||||
.request_device(&wgpu::DeviceDescriptor {
|
||||
label: None,
|
||||
required_features: wgpu::Features::empty(),
|
||||
experimental_features: wgpu::ExperimentalFeatures::disabled(),
|
||||
required_limits: wgpu::Limits::default(),
|
||||
memory_hints: Default::default(),
|
||||
trace: wgpu::Trace::Off,
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(Self {
|
||||
window,
|
||||
surface,
|
||||
device,
|
||||
queue,
|
||||
config,
|
||||
is_surface_configured: false,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, width: u32, height: u32) {
|
||||
if width > 0 && height > 0 {
|
||||
self.config.width = width;
|
||||
self.config.height = height;
|
||||
self.surface.configure(&self.device, &self.config);
|
||||
self.is_surface_configured = true;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(&mut self) -> anyhow::Result<()> {
|
||||
self.window.request_redraw();
|
||||
|
||||
if !self.is_surface_configured {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let output = match self.surface.get_current_texture() {
|
||||
wgpu::CurrentSurfaceTexture::Success(surface_texture) => surface_texture,
|
||||
wgpu::CurrentSurfaceTexture::Suboptimal(surface_texture) => {
|
||||
self.surface.configure(&self.device, &self.config);
|
||||
surface_texture
|
||||
}
|
||||
wgpu::CurrentSurfaceTexture::Timeout
|
||||
| wgpu::CurrentSurfaceTexture::Occluded
|
||||
| wgpu::CurrentSurfaceTexture::Validation => {
|
||||
// Skip this frame
|
||||
return Ok(());
|
||||
}
|
||||
wgpu::CurrentSurfaceTexture::Outdated => {
|
||||
self.surface.configure(&self.device, &self.config);
|
||||
return Ok(());
|
||||
}
|
||||
wgpu::CurrentSurfaceTexture::Lost => {
|
||||
// You could recreate the devices and all resources
|
||||
// created with it here, but we'll just bail
|
||||
anyhow::bail!("Lost device");
|
||||
}
|
||||
};
|
||||
|
||||
let view = output
|
||||
.texture
|
||||
.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
|
||||
let mut encoder = self
|
||||
.device
|
||||
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||
label: Some("Render Encoder"),
|
||||
});
|
||||
|
||||
{
|
||||
let _render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("Render Pass"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &view,
|
||||
resolve_target: None,
|
||||
depth_slice: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(wgpu::Color {
|
||||
r: 0.1,
|
||||
g: 0.2,
|
||||
b: 0.3,
|
||||
a: 1.0,
|
||||
}),
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
})],
|
||||
depth_stencil_attachment: None,
|
||||
occlusion_query_set: None,
|
||||
timestamp_writes: None,
|
||||
multiview_mask: None,
|
||||
});
|
||||
}
|
||||
|
||||
// submit will accept anything that implements IntoIter
|
||||
self.queue.submit(std::iter::once(encoder.finish()));
|
||||
output.present();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_key(&self, event_loop: &ActiveEventLoop, code: KeyCode, is_pressed: bool) {
|
||||
match (code, is_pressed) {
|
||||
(KeyCode::Escape, true) => event_loop.exit(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_mouse(&self, element_state: ElementState, button: MouseButton) {
|
||||
match button {
|
||||
MouseButton::Left => {}
|
||||
MouseButton::Right => {}
|
||||
MouseButton::Middle => (),
|
||||
MouseButton::Back => (),
|
||||
MouseButton::Forward => (),
|
||||
MouseButton::Other(_) => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct App {
|
||||
state: Option<State>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new() -> Self {
|
||||
Self { state: None }
|
||||
}
|
||||
}
|
||||
|
||||
impl ApplicationHandler<State> for App {
|
||||
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||
#[allow(unused_mut)]
|
||||
let mut window_attributes = Window::default_attributes();
|
||||
|
||||
let window = Arc::new(event_loop.create_window(window_attributes).unwrap());
|
||||
self.state = Some(pollster::block_on(State::new(window)).unwrap());
|
||||
}
|
||||
|
||||
#[allow(unused_mut)]
|
||||
fn user_event(&mut self, _event_loop: &ActiveEventLoop, mut event: State) {
|
||||
self.state = Some(event);
|
||||
}
|
||||
|
||||
fn window_event(
|
||||
&mut self,
|
||||
event_loop: &ActiveEventLoop,
|
||||
_window_id: winit::window::WindowId,
|
||||
event: WindowEvent,
|
||||
) {
|
||||
use WindowEvent::*;
|
||||
|
||||
let state = match &mut self.state {
|
||||
Some(state) => state,
|
||||
None => return,
|
||||
};
|
||||
|
||||
match event {
|
||||
CloseRequested => event_loop.exit(),
|
||||
Resized(size) => state.resize(size.width, size.height),
|
||||
RedrawRequested => match state.render() {
|
||||
Ok(_) => (),
|
||||
Err(e) => {
|
||||
log::error!("{e}");
|
||||
event_loop.exit();
|
||||
}
|
||||
},
|
||||
MouseInput {
|
||||
device_id: _,
|
||||
state: mouse_state,
|
||||
button,
|
||||
} => {
|
||||
state.handle_mouse(mouse_state, button);
|
||||
}
|
||||
KeyboardInput {
|
||||
event:
|
||||
KeyEvent {
|
||||
physical_key: PhysicalKey::Code(code),
|
||||
state: key_state,
|
||||
..
|
||||
},
|
||||
..
|
||||
} => state.handle_key(event_loop, code, key_state.is_pressed()),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run() -> anyhow::Result<()> {
|
||||
env_logger::init();
|
||||
|
||||
let event_loop = EventLoop::with_user_event().build()?;
|
||||
let mut app = App::new();
|
||||
event_loop.run_app(&mut app)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
use gui_layout::{App, TextBox};
|
||||
|
||||
fn main() {
|
||||
run().unwrap();
|
||||
App::new()
|
||||
.title("gui-layout demo")
|
||||
.background([0.08, 0.08, 0.10, 1.0])
|
||||
.child(
|
||||
TextBox::new(
|
||||
"Hello! This is a wrapped text box powered by cosmic-text and laid out \
|
||||
with taffy. The text will wrap at the box boundary. You can resize the \
|
||||
window and the layout will reflow.",
|
||||
)
|
||||
.width(400.0)
|
||||
.height(200.0)
|
||||
.font_size(16.0)
|
||||
.text_color([1.0, 1.0, 1.0, 1.0])
|
||||
.background([0.15, 0.25, 0.40, 1.0])
|
||||
.padding(12.0),
|
||||
)
|
||||
.child(
|
||||
TextBox::new("A second box with smaller text and a different color.")
|
||||
.width(400.0)
|
||||
.height(80.0)
|
||||
.font_size(13.0)
|
||||
.text_color([0.9, 0.85, 0.6, 1.0])
|
||||
.background([0.25, 0.15, 0.15, 1.0])
|
||||
.padding(12.0),
|
||||
)
|
||||
.run()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
+358
@@ -0,0 +1,358 @@
|
||||
use std::mem;
|
||||
use wgpu::util::DeviceExt;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
struct RectVertex {
|
||||
position: [f32; 2],
|
||||
color: [f32; 4],
|
||||
}
|
||||
|
||||
impl RectVertex {
|
||||
const LAYOUT: wgpu::VertexBufferLayout<'static> = wgpu::VertexBufferLayout {
|
||||
array_stride: mem::size_of::<Self>() as u64,
|
||||
step_mode: wgpu::VertexStepMode::Vertex,
|
||||
attributes: &wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x4],
|
||||
};
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
struct TextVertex {
|
||||
position: [f32; 2],
|
||||
uv: [f32; 2],
|
||||
}
|
||||
|
||||
impl TextVertex {
|
||||
const LAYOUT: wgpu::VertexBufferLayout<'static> = wgpu::VertexBufferLayout {
|
||||
array_stride: mem::size_of::<Self>() as u64,
|
||||
step_mode: wgpu::VertexStepMode::Vertex,
|
||||
attributes: &wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2],
|
||||
};
|
||||
}
|
||||
|
||||
pub struct DrawRect {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
pub width: f32,
|
||||
pub height: f32,
|
||||
pub color: [f32; 4],
|
||||
}
|
||||
|
||||
pub struct DrawText {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
pub width: f32,
|
||||
pub height: f32,
|
||||
/// RGBA8 pixel data, row-major
|
||||
pub pixels: Vec<u8>,
|
||||
pub pixel_width: u32,
|
||||
pub pixel_height: u32,
|
||||
}
|
||||
|
||||
pub struct Renderer {
|
||||
rect_pipeline: wgpu::RenderPipeline,
|
||||
text_pipeline: wgpu::RenderPipeline,
|
||||
text_bind_group_layout: wgpu::BindGroupLayout,
|
||||
sampler: wgpu::Sampler,
|
||||
}
|
||||
|
||||
impl Renderer {
|
||||
pub fn new(device: &wgpu::Device, surface_format: wgpu::TextureFormat) -> Self {
|
||||
let rect_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("rect_shader"),
|
||||
source: wgpu::ShaderSource::Wgsl(include_str!("shaders/rect.wgsl").into()),
|
||||
});
|
||||
|
||||
let rect_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("rect_pipeline"),
|
||||
layout: None,
|
||||
vertex: wgpu::VertexState {
|
||||
module: &rect_shader,
|
||||
entry_point: Some("vs_main"),
|
||||
buffers: &[RectVertex::LAYOUT],
|
||||
compilation_options: Default::default(),
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &rect_shader,
|
||||
entry_point: Some("fs_main"),
|
||||
compilation_options: Default::default(),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format: surface_format,
|
||||
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
}),
|
||||
primitive: wgpu::PrimitiveState {
|
||||
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||
..Default::default()
|
||||
},
|
||||
depth_stencil: None,
|
||||
multisample: wgpu::MultisampleState::default(),
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
});
|
||||
|
||||
let text_bind_group_layout =
|
||||
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("text_bgl"),
|
||||
entries: &[
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Texture {
|
||||
sample_type: wgpu::TextureSampleType::Float { filterable: true },
|
||||
view_dimension: wgpu::TextureViewDimension::D2,
|
||||
multisampled: false,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 1,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
|
||||
count: None,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
let text_pipeline_layout =
|
||||
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("text_pipeline_layout"),
|
||||
bind_group_layouts: &[Some(&text_bind_group_layout)],
|
||||
immediate_size: 0,
|
||||
});
|
||||
|
||||
let text_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("text_shader"),
|
||||
source: wgpu::ShaderSource::Wgsl(include_str!("shaders/text.wgsl").into()),
|
||||
});
|
||||
|
||||
let text_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("text_pipeline"),
|
||||
layout: Some(&text_pipeline_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &text_shader,
|
||||
entry_point: Some("vs_main"),
|
||||
buffers: &[TextVertex::LAYOUT],
|
||||
compilation_options: Default::default(),
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &text_shader,
|
||||
entry_point: Some("fs_main"),
|
||||
compilation_options: Default::default(),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format: surface_format,
|
||||
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
}),
|
||||
primitive: wgpu::PrimitiveState {
|
||||
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||
..Default::default()
|
||||
},
|
||||
depth_stencil: None,
|
||||
multisample: wgpu::MultisampleState::default(),
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
});
|
||||
|
||||
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
||||
label: Some("text_sampler"),
|
||||
address_mode_u: wgpu::AddressMode::ClampToEdge,
|
||||
address_mode_v: wgpu::AddressMode::ClampToEdge,
|
||||
mag_filter: wgpu::FilterMode::Linear,
|
||||
min_filter: wgpu::FilterMode::Linear,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
Self {
|
||||
rect_pipeline,
|
||||
text_pipeline,
|
||||
text_bind_group_layout,
|
||||
sampler,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_ndc(px: f32, py: f32, win_w: u32, win_h: u32) -> [f32; 2] {
|
||||
[
|
||||
px / win_w as f32 * 2.0 - 1.0,
|
||||
1.0 - py / win_h as f32 * 2.0,
|
||||
]
|
||||
}
|
||||
|
||||
fn rect_quad(r: &DrawRect, win_w: u32, win_h: u32) -> [RectVertex; 6] {
|
||||
let c = r.color;
|
||||
let tl = Self::to_ndc(r.x, r.y, win_w, win_h);
|
||||
let tr = Self::to_ndc(r.x + r.width, r.y, win_w, win_h);
|
||||
let bl = Self::to_ndc(r.x, r.y + r.height, win_w, win_h);
|
||||
let br = Self::to_ndc(r.x + r.width, r.y + r.height, win_w, win_h);
|
||||
[
|
||||
RectVertex { position: tl, color: c },
|
||||
RectVertex { position: tr, color: c },
|
||||
RectVertex { position: bl, color: c },
|
||||
RectVertex { position: tr, color: c },
|
||||
RectVertex { position: br, color: c },
|
||||
RectVertex { position: bl, color: c },
|
||||
]
|
||||
}
|
||||
|
||||
fn text_quad(t: &DrawText, win_w: u32, win_h: u32) -> [TextVertex; 6] {
|
||||
let tl = Self::to_ndc(t.x, t.y, win_w, win_h);
|
||||
let tr = Self::to_ndc(t.x + t.width, t.y, win_w, win_h);
|
||||
let bl = Self::to_ndc(t.x, t.y + t.height, win_w, win_h);
|
||||
let br = Self::to_ndc(t.x + t.width, t.y + t.height, win_w, win_h);
|
||||
[
|
||||
TextVertex { position: tl, uv: [0.0, 0.0] },
|
||||
TextVertex { position: tr, uv: [1.0, 0.0] },
|
||||
TextVertex { position: bl, uv: [0.0, 1.0] },
|
||||
TextVertex { position: tr, uv: [1.0, 0.0] },
|
||||
TextVertex { position: br, uv: [1.0, 1.0] },
|
||||
TextVertex { position: bl, uv: [0.0, 1.0] },
|
||||
]
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
&self,
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
view: &wgpu::TextureView,
|
||||
bg_color: [f32; 4],
|
||||
win_w: u32,
|
||||
win_h: u32,
|
||||
rects: &[DrawRect],
|
||||
texts: &[DrawText],
|
||||
) {
|
||||
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||
label: Some("frame_encoder"),
|
||||
});
|
||||
|
||||
let mut rect_verts: Vec<RectVertex> = Vec::new();
|
||||
for r in rects {
|
||||
rect_verts.extend_from_slice(&Self::rect_quad(r, win_w, win_h));
|
||||
}
|
||||
|
||||
// Build text textures + bind groups (kept alive through submit)
|
||||
let text_data: Vec<(wgpu::Texture, wgpu::TextureView, wgpu::BindGroup, [TextVertex; 6])> =
|
||||
texts
|
||||
.iter()
|
||||
.filter(|t| t.pixel_width > 0 && t.pixel_height > 0)
|
||||
.map(|t| {
|
||||
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("text_texture"),
|
||||
size: wgpu::Extent3d {
|
||||
width: t.pixel_width,
|
||||
height: t.pixel_height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::Rgba8Unorm,
|
||||
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
|
||||
view_formats: &[],
|
||||
});
|
||||
|
||||
queue.write_texture(
|
||||
wgpu::TexelCopyTextureInfo {
|
||||
texture: &texture,
|
||||
mip_level: 0,
|
||||
origin: wgpu::Origin3d::ZERO,
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
},
|
||||
&t.pixels,
|
||||
wgpu::TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(t.pixel_width * 4),
|
||||
rows_per_image: Some(t.pixel_height),
|
||||
},
|
||||
wgpu::Extent3d {
|
||||
width: t.pixel_width,
|
||||
height: t.pixel_height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
);
|
||||
|
||||
let tex_view = texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
|
||||
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("text_bg"),
|
||||
layout: &self.text_bind_group_layout,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: wgpu::BindingResource::TextureView(&tex_view),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: wgpu::BindingResource::Sampler(&self.sampler),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
let quad = Self::text_quad(t, win_w, win_h);
|
||||
(texture, tex_view, bind_group, quad)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let rect_buf = if rect_verts.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("rect_vbuf"),
|
||||
contents: bytemuck::cast_slice(&rect_verts),
|
||||
usage: wgpu::BufferUsages::VERTEX,
|
||||
}))
|
||||
};
|
||||
|
||||
let text_vbufs: Vec<wgpu::Buffer> = text_data
|
||||
.iter()
|
||||
.map(|(_, _, _, quad)| {
|
||||
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("text_vbuf"),
|
||||
contents: bytemuck::cast_slice(quad.as_slice()),
|
||||
usage: wgpu::BufferUsages::VERTEX,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
{
|
||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("main_pass"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view,
|
||||
resolve_target: None,
|
||||
depth_slice: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(wgpu::Color {
|
||||
r: bg_color[0] as f64,
|
||||
g: bg_color[1] as f64,
|
||||
b: bg_color[2] as f64,
|
||||
a: bg_color[3] as f64,
|
||||
}),
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
})],
|
||||
depth_stencil_attachment: None,
|
||||
occlusion_query_set: None,
|
||||
timestamp_writes: None,
|
||||
multiview_mask: None,
|
||||
});
|
||||
|
||||
if let Some(buf) = &rect_buf {
|
||||
pass.set_pipeline(&self.rect_pipeline);
|
||||
pass.set_vertex_buffer(0, buf.slice(..));
|
||||
pass.draw(0..rect_verts.len() as u32, 0..1);
|
||||
}
|
||||
|
||||
pass.set_pipeline(&self.text_pipeline);
|
||||
for (i, (_, _, bind_group, _)) in text_data.iter().enumerate() {
|
||||
pass.set_bind_group(0, bind_group, &[]);
|
||||
pass.set_vertex_buffer(0, text_vbufs[i].slice(..));
|
||||
pass.draw(0..6, 0..1);
|
||||
}
|
||||
}
|
||||
|
||||
queue.submit(std::iter::once(encoder.finish()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
struct VertexInput {
|
||||
@location(0) position: vec2<f32>,
|
||||
@location(1) color: vec4<f32>,
|
||||
}
|
||||
|
||||
struct VertexOutput {
|
||||
@builtin(position) clip_position: vec4<f32>,
|
||||
@location(0) color: vec4<f32>,
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn vs_main(in: VertexInput) -> VertexOutput {
|
||||
var out: VertexOutput;
|
||||
out.clip_position = vec4<f32>(in.position, 0.0, 1.0);
|
||||
out.color = in.color;
|
||||
return out;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
return in.color;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
struct VertexInput {
|
||||
@location(0) position: vec2<f32>,
|
||||
@location(1) uv: vec2<f32>,
|
||||
}
|
||||
|
||||
struct VertexOutput {
|
||||
@builtin(position) clip_position: vec4<f32>,
|
||||
@location(0) uv: vec2<f32>,
|
||||
}
|
||||
|
||||
@group(0) @binding(0)
|
||||
var t_text: texture_2d<f32>;
|
||||
@group(0) @binding(1)
|
||||
var s_text: sampler;
|
||||
|
||||
@vertex
|
||||
fn vs_main(in: VertexInput) -> VertexOutput {
|
||||
var out: VertexOutput;
|
||||
out.clip_position = vec4<f32>(in.position, 0.0, 1.0);
|
||||
out.uv = in.uv;
|
||||
return out;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
return textureSample(t_text, s_text, in.uv);
|
||||
}
|
||||
Reference in New Issue
Block a user