From ec536ca3d62a2c81dec3899de16460199793ef8a Mon Sep 17 00:00:00 2001 From: Lukas Bombach Date: Sun, 1 Jun 2025 00:07:36 +0200 Subject: [PATCH 01/13] first wokring version --- Cargo.lock | 193 ++++++++- Cargo.toml | 1 + src/app.rs | 26 ++ src/gpu.rs | 43 ++ src/gui.rs | 125 +++++- src/javascript_runtime/mod.rs | 24 ++ src/javascript_runtime/reconciler.ts | 24 +- src/main.rs | 1 + src/main.tsx | 9 +- src/text.rs | 624 +++++++++++++++++++++++++++ src/text_shader.wgsl | 74 ++++ 11 files changed, 1126 insertions(+), 18 deletions(-) create mode 100644 src/text.rs create mode 100644 src/text_shader.wgsl diff --git a/Cargo.lock b/Cargo.lock index 66b7375..3afdea6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1020,6 +1020,29 @@ dependencies = [ "libc", ] +[[package]] +name = "cosmic-text" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e418dd4f5128c3e93eab12246391c54a20c496811131f85754dc8152ee207892" +dependencies = [ + "bitflags 2.9.0", + "fontdb", + "log", + "rangemap", + "rustc-hash 1.1.0", + "rustybuzz", + "self_cell", + "smol_str", + "swash", + "sys-locale", + "ttf-parser 0.21.1", + "unicode-bidi", + "unicode-linebreak", + "unicode-script", + "unicode-segmentation", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -2951,6 +2974,38 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "font-types" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02a596f5713680923a2080d86de50fe472fb290693cf0f701187a1c8b36996b7" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "fontconfig-parser" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646" +dependencies = [ + "roxmltree", +] + +[[package]] +name = "fontdb" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0299020c3ef3f60f526a4f64ab4a3d4ce116b1acbf24cdd22da0068e5d81dc3" +dependencies = [ + "fontconfig-parser", + "log", + "memmap2", + "slotmap", + "tinyvec", + "ttf-parser 0.20.0", +] + [[package]] name = "foreign-types" version = "0.5.0" @@ -5290,7 +5345,7 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec719bbf3b2a81c109a4e20b1f129b5566b7dce654bc3872f6a05abf82b2c4" dependencies = [ - "ttf-parser", + "ttf-parser 0.25.1", ] [[package]] @@ -5888,6 +5943,12 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d6831663a5098ea164f89cff59c6284e95f4e3c76ce9848d4529f5ccca9bde" +[[package]] +name = "rangemap" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60fcc7d6849342eff22c4350c8b9a989ee8ceabc4b481253e8946b9fe83d684" + [[package]] name = "raw-window-handle" version = "0.6.2" @@ -5920,6 +5981,7 @@ version = "0.1.0" dependencies = [ "bytemuck", "color", + "cosmic-text", "deno_core", "deno_error", "notify 8.0.0", @@ -5933,6 +5995,16 @@ dependencies = [ "winit", ] +[[package]] +name = "read-fonts" +version = "0.29.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f96bfbb7df43d34a2b7b8582fcbcb676ba02a763265cb90bc8aabfd62b57d64" +dependencies = [ + "bytemuck", + "font-types", +] + [[package]] name = "redox_syscall" version = "0.4.1" @@ -6132,6 +6204,12 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "roxmltree" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" + [[package]] name = "rsa" version = "0.9.8" @@ -6323,6 +6401,23 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" +[[package]] +name = "rustybuzz" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfb9cf8877777222e4a3bc7eb247e398b56baba500c38c1c46842431adc8b55c" +dependencies = [ + "bitflags 2.9.0", + "bytemuck", + "libm", + "smallvec", + "ttf-parser 0.21.1", + "unicode-bidi-mirroring", + "unicode-ccc", + "unicode-properties", + "unicode-script", +] + [[package]] name = "rustyline" version = "13.0.0" @@ -6528,6 +6623,12 @@ dependencies = [ "libc", ] +[[package]] +name = "self_cell" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749" + [[package]] name = "semver" version = "0.9.0" @@ -6761,6 +6862,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" +[[package]] +name = "skrifa" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbeb4ca4399663735553a09dd17ce7e49a0a0203f03b706b39628c4d913a8607" +dependencies = [ + "bytemuck", + "read-fonts", +] + [[package]] name = "slab" version = "0.4.9" @@ -7026,6 +7137,17 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "swash" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f745de914febc7c9ab4388dfaf94bbc87e69f57bb41133a9b0c84d4be49856f3" +dependencies = [ + "skrifa", + "yazi", + "zeno", +] + [[package]] name = "swc_allocator" version = "4.0.0" @@ -7480,6 +7602,15 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "sys-locale" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" +dependencies = [ + "libc", +] + [[package]] name = "sys_traits" version = "0.1.8" @@ -7974,6 +8105,18 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "ttf-parser" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17f77d76d837a7830fe1d4f12b7b4ba4192c1888001c7164257e4bc6d21d96b4" + +[[package]] +name = "ttf-parser" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8" + [[package]] name = "ttf-parser" version = "0.25.1" @@ -8044,6 +8187,24 @@ dependencies = [ "unic-common", ] +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-bidi-mirroring" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cb788ffebc92c5948d0e997106233eeb1d8b9512f93f41651f52b6c5f5af86" + +[[package]] +name = "unicode-ccc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df77b101bcc4ea3d78dafc5ad7e4f58ceffe0b2b16bf446aeb50b6cb4157656" + [[package]] name = "unicode-id" version = "0.3.5" @@ -8062,6 +8223,24 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-properties" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" + +[[package]] +name = "unicode-script" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb421b350c9aff471779e262955939f565ec18b86c15364e6bdf0d662ca7c1f" + [[package]] name = "unicode-segmentation" version = "1.12.0" @@ -9193,6 +9372,12 @@ version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5b940ebc25896e71dd073bad2dbaa2abfe97b0a391415e22ad1326d9c54e3c4" +[[package]] +name = "yazi" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01738255b5a16e78bbb83e7fbba0a1e7dd506905cfc53f4622d89015a03fbb5" + [[package]] name = "yoke" version = "0.7.5" @@ -9217,6 +9402,12 @@ dependencies = [ "synstructure 0.13.1", ] +[[package]] +name = "zeno" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524" + [[package]] name = "zerocopy" version = "0.7.35" diff --git a/Cargo.toml b/Cargo.toml index 1b67207..0114fec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,3 +19,4 @@ notify = "8.0.0" taffy = { version = "0.7.7", features = ["serde"]} slotmap = "1.0.7" color = "0.3.1" +cosmic-text = "0.13.0" diff --git a/src/app.rs b/src/app.rs index 87df86e..f836568 100644 --- a/src/app.rs +++ b/src/app.rs @@ -59,6 +59,19 @@ impl<'window> ApplicationHandler for App<'window> { gui.compute_layout(size.width, size.height); gpu.update_instance_buffer(gui.into_instances()); + + // Collect and render text instances + let text_items = gui.collect_text_instances(); + println!("GuiUpdate: collected {} text items", text_items.len()); + let mut all_text_instances = Vec::new(); + + for (text, x, y, font_size, color) in text_items { + let text_instances = gpu.render_text(&text, x, y, font_size, color, None); + all_text_instances.extend(text_instances); + } + + println!("GuiUpdate: updating GPU with {} text instances", all_text_instances.len()); + gpu.update_text_instances(&all_text_instances); window.request_redraw(); } @@ -78,6 +91,19 @@ impl<'window> ApplicationHandler for App<'window> { if let Ok(mut gui) = self.gui.lock() { gui.compute_layout(size.width, size.height); gpu.update_instance_buffer(gui.into_instances()); + + // Collect and render text instances + let text_items = gui.collect_text_instances(); + println!("Resized: collected {} text items", text_items.len()); + let mut all_text_instances = Vec::new(); + + for (text, x, y, font_size, color) in text_items { + let text_instances = gpu.render_text(&text, x, y, font_size, color, None); + all_text_instances.extend(text_instances); + } + + println!("Resized: updating GPU with {} text instances", all_text_instances.len()); + gpu.update_text_instances(&all_text_instances); gpu.set_size(size.width, size.height); } diff --git a/src/gpu.rs b/src/gpu.rs index 89ac5e5..af14d5d 100644 --- a/src/gpu.rs +++ b/src/gpu.rs @@ -9,6 +9,8 @@ use wgpu::MemoryHints::Performance; use wgpu::ShaderSource; use winit::window::Window; +use crate::text::TextRenderer; + #[repr(C)] #[derive(Copy, Clone, Debug, Pod, Zeroable)] pub struct Instance { @@ -45,6 +47,7 @@ pub struct Gpu<'window> { instance_buffer: wgpu::Buffer, instance_count: u32, viewport: [f32; 2], + text_renderer: TextRenderer, } impl<'window> Gpu<'window> { @@ -217,6 +220,12 @@ impl<'window> Gpu<'window> { cache: None, }); + /* + * text renderer + */ + + let text_renderer = TextRenderer::new(&device, &queue, config.format); + Gpu { surface, config, @@ -226,6 +235,7 @@ impl<'window> Gpu<'window> { instance_buffer, instance_count, viewport, + text_renderer, } } @@ -284,6 +294,9 @@ impl<'window> Gpu<'window> { rpass.set_vertex_buffer(0, self.instance_buffer.slice(..)); rpass.draw(0..6, 0..self.instance_count); } + + // Render text + self.text_renderer.draw(&mut rpass, self.viewport); } self.queue.submit(Some(encoder.finish())); @@ -300,4 +313,34 @@ impl<'window> Gpu<'window> { }); self.instance_count = instances.len() as u32; } + + pub fn render_text( + &mut self, + text: &str, + x: f32, + y: f32, + font_size: f32, + color: [f32; 4], + max_width: Option, + ) -> Vec { + self.text_renderer.render_text( + &self.device, + &self.queue, + text, + x, + y, + font_size, + color, + max_width, + ) + } + + pub fn update_text_instances(&mut self, instances: &[crate::text::TextInstance]) { + println!( + "GPU::update_text_instances called with {} instances", + instances.len() + ); + self.text_renderer + .update_instances(&self.device, &self.queue, instances); + } } diff --git a/src/gui.rs b/src/gui.rs index 6bc4a81..7081a8e 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -14,6 +14,7 @@ use winit::event_loop::EventLoopProxy; enum NodeKind { Flexbox, Grid, + Text, } pub struct Node { @@ -21,6 +22,9 @@ pub struct Node { style: Style, background_color: [f32; 4], border_radius: f32, + text: Option, + text_color: [f32; 4], + font_size: f32, cache: Cache, pub layout: Layout, pub children: Vec, @@ -33,6 +37,9 @@ impl Default for Node { style: Style::default(), background_color: [0.0, 0.0, 0.0, 0.0], border_radius: 0.0, + text: None, + text_color: [1.0, 1.0, 1.0, 1.0], // White by default + font_size: 16.0, cache: Cache::new(), layout: Layout::with_order(0), children: Vec::new(), @@ -87,6 +94,25 @@ impl Gui { style: Style, background_color: [f32; 4], border_radius: u32, + ) -> NodeId { + self.create_node_with_text( + style, + background_color, + border_radius, + None, + [1.0, 1.0, 1.0, 1.0], + 16.0, + ) + } + + pub fn create_node_with_text( + &mut self, + style: Style, + background_color: [f32; 4], + border_radius: u32, + text: Option, + text_color: [f32; 4], + font_size: f32, ) -> NodeId { // todo block layout let kind = if style.display == Display::Grid { @@ -99,6 +125,9 @@ impl Gui { style, background_color, border_radius: border_radius as f32, + text, + text_color, + font_size, kind, ..Node::default() }; @@ -108,6 +137,34 @@ impl Gui { id.into() } + pub fn create_text_node( + &mut self, + text: String, + text_color: [f32; 4], + font_size: f32, + ) -> NodeId { + let node = Node { + kind: NodeKind::Text, + style: Style::default(), + background_color: [0.0, 0.0, 0.0, 0.0], // Transparent background for text + border_radius: 0.0, + text: Some(text), + text_color, + font_size, + cache: Cache::new(), + layout: Layout::with_order(0), + children: Vec::new(), + }; + + println!( + "create_text_node {:?} {:?} {:?}", + node.text, node.text_color, node.font_size + ); + + let id = self.nodes.insert(node); + id.into() + } + pub fn append_child_to_root(&mut self, child_id: NodeId) -> () { self.append_child(self.root, child_id); self.notify_update(); @@ -170,16 +227,19 @@ impl Gui { offset_x + node.layout.location.x, offset_y + node.layout.location.y, ); - let instance = Instance::new( - x, - y, - node.layout.size.width, - node.layout.size.height, - node.background_color, - node.border_radius, - ); - instances.push(instance); + // Only create background rectangles for non-text nodes + if !matches!(node.kind, NodeKind::Text) { + let instance = Instance::new( + x, + y, + node.layout.size.width, + node.layout.size.height, + node.background_color, + node.border_radius, + ); + instances.push(instance); + } for child_id in gui.children_from_id(node_id) { collect_instances(gui, *child_id, x, y, instances); @@ -191,6 +251,37 @@ impl Gui { return instances; } + pub fn collect_text_instances(&self) -> Vec<(String, f32, f32, f32, [f32; 4])> { + fn collect_text( + gui: &Gui, + node_id: taffy::NodeId, + offset_x: f32, + offset_y: f32, + text_items: &mut Vec<(String, f32, f32, f32, [f32; 4])>, + ) { + let node = gui.node_from_id(node_id); + let (x, y) = ( + offset_x + node.layout.location.x, + offset_y + node.layout.location.y, + ); + + // Only collect text from Text nodes + if matches!(node.kind, NodeKind::Text) { + if let Some(ref text) = node.text { + text_items.push((text.clone(), x, y, node.font_size, node.text_color)); + } + } + + for child_id in gui.children_from_id(node_id) { + collect_text(gui, *child_id, x, y, text_items); + } + } + + let mut text_items = Vec::new(); + collect_text(&self, self.root, 0.0, 0.0, &mut text_items); + text_items + } + fn notify_update(&self) { if let Ok(proxy) = self.event_loop.lock() { proxy.send_event(CustomEvent::GuiUpdate).unwrap(); @@ -250,6 +341,22 @@ impl taffy::LayoutPartialTree for Gui { match node.kind { NodeKind::Flexbox => compute_flexbox_layout(gui, node_id, inputs), NodeKind::Grid => compute_grid_layout(gui, node_id, inputs), + NodeKind::Text => { + // Text nodes are leaf nodes with intrinsic size + taffy::tree::LayoutOutput { + size: taffy::Size { + width: node.font_size + * node.text.as_ref().map_or(0, |t| t.len()) as f32 + * 0.6, // Rough text width estimation + height: node.font_size, + }, + content_size: taffy::Size::ZERO, + first_baselines: taffy::Point::NONE, + top_margin: taffy::CollapsibleMarginSet::ZERO, + bottom_margin: taffy::CollapsibleMarginSet::ZERO, + margins_can_collapse_through: false, + } + } } }) } diff --git a/src/javascript_runtime/mod.rs b/src/javascript_runtime/mod.rs index 0346905..4d77047 100644 --- a/src/javascript_runtime/mod.rs +++ b/src/javascript_runtime/mod.rs @@ -77,10 +77,34 @@ fn op_append_child( Ok(()) } +#[op2(fast)] +#[bigint] +fn op_create_text_node( + state: &mut OpState, + #[string] text: String, + #[string] text_color: String, + font_size: f32, +) -> Result { + let default_text_color: &str = "white"; + + let parsed_text_color = parse_color(&text_color) + .unwrap_or(DynamicColor::from_str(default_text_color).unwrap()) + .components; + + let node_id = state + .borrow::>>() + .lock() + .unwrap() + .create_text_node(text, parsed_text_color, font_size); + + Ok(usize::from(node_id)) +} + extension!( rect_extension, ops = [ op_create_instance, + op_create_text_node, op_append_child_to_container, op_append_child, ], diff --git a/src/javascript_runtime/reconciler.ts b/src/javascript_runtime/reconciler.ts index 84aac97..eecc8b4 100644 --- a/src/javascript_runtime/reconciler.ts +++ b/src/javascript_runtime/reconciler.ts @@ -4,6 +4,8 @@ import { taffyFromCss } from "./taffy.ts"; // @ts-expect-error not typed yet export const create_instance = Deno.core.ops.op_create_instance; // @ts-expect-error not typed yet +export const create_text_node = Deno.core.ops.op_create_text_node; +// @ts-expect-error not typed yet export const append_child_to_container = Deno.core.ops.op_append_child_to_container; // @ts-expect-error not typed yet export const append_child = Deno.core.ops.op_append_child; @@ -21,7 +23,7 @@ type Type = Pick; type Props = RectProps; type Container = { type: "container" }; type Instance = { type: "div"; id: RectId }; -type TextInstance = { type: "text" }; +type TextInstance = { type: "text"; id: RectId }; type SuspenseInstance = never; type HydratableInstance = never; type PublicInstance = { type: string }; @@ -66,7 +68,7 @@ export const reconciler = ReactReconciler< }, appendChildToContainer(_container, child) { - if (child.type === "div") { + if (child.type === "div" || child.type === "text") { append_child_to_container(child.id); } else { console.warn("appendChildToContainer: Ignoring child", child); @@ -74,7 +76,7 @@ export const reconciler = ReactReconciler< }, appendInitialChild(parent, child) { - if (child.type === "div") { + if (child.type === "div" || child.type === "text") { append_child(parent.id, child.id); } else { console.warn("appendInitialChild: Ignoring child", child); @@ -82,15 +84,25 @@ export const reconciler = ReactReconciler< }, appendChild(parent, child) { - if (child.type === "div") { + if (child.type === "div" || child.type === "text") { append_child(parent.id, child.id); } else { console.warn("appendChild: Ignoring child", child); } }, - createTextInstance(_text, _rootContainerInstance, _hostContext, _internalInstanceHandle) { - return { type: "text" }; + createTextInstance(text, _rootContainerInstance, _hostContext, _internalInstanceHandle) { + // Default text properties - could be configurable via context or props in the future + const textColor = "white"; + const fontSize = 16.0; + + console.debug("createTextInstance", text, { + textColor, + fontSize, + }); + + const id = create_text_node(text, textColor, fontSize); + return { type: "text", id }; }, clearContainer: () => false, diff --git a/src/main.rs b/src/main.rs index 61e60d7..a6ed141 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,6 +12,7 @@ mod app; mod gpu; mod gui; mod javascript_runtime; +mod text; fn main() -> Result<(), EventLoopError> { let event_loop = EventLoop::::with_user_event().build()?; diff --git a/src/main.tsx b/src/main.tsx index 62f8fab..e263dca 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -14,9 +14,14 @@ ReactWGPU.render( style={{ width: "100%", aspectRatio: "16/9", - backgroundColor: "#fff", + backgroundColor: "transparent", borderRadius: 10, + padding: "20px", + flexDirection: "column", + justifyContent: "center", }} - > + > + Hello, World! This is rendered text using WGPU! + ); diff --git a/src/text.rs b/src/text.rs new file mode 100644 index 0000000..2e98f37 --- /dev/null +++ b/src/text.rs @@ -0,0 +1,624 @@ +use cosmic_text::{Attrs, Buffer, Family, FontSystem, Metrics, Shaping, SwashCache, Weight, Wrap}; +use std::collections::HashMap; +use wgpu::util::DeviceExt; + +pub struct TextAtlas { + texture: wgpu::Texture, + texture_view: wgpu::TextureView, + width: u32, + height: u32, + // Maps cache key to position in atlas + glyph_cache: HashMap, // Using u64 as simplified cache key + // Current position for packing new glyphs + current_x: u32, + current_y: u32, + row_height: u32, +} + +#[derive(Clone, Copy)] +pub struct GlyphInfo { + pub x: u32, + pub y: u32, + pub width: u32, + pub height: u32, + pub bearing_left: i32, // horizontal bearing from placement + pub bearing_top: i32, // vertical bearing from placement +} + +impl TextAtlas { + pub fn new(device: &wgpu::Device, width: u32, height: u32) -> Self { + let texture = device.create_texture(&wgpu::TextureDescriptor { + label: Some("Text Atlas"), + size: wgpu::Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::R8Unorm, + usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, + view_formats: &[], + }); + + let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default()); + + Self { + texture, + texture_view, + width, + height, + glyph_cache: HashMap::new(), + current_x: 0, + current_y: 0, + row_height: 0, + } + } + + pub fn get_or_insert_glyph( + &mut self, + _device: &wgpu::Device, + queue: &wgpu::Queue, + cache_key: u64, + glyph_data: &[u8], + glyph_width: u32, + glyph_height: u32, + ) -> Option { + if let Some(glyph_info) = self.glyph_cache.get(&cache_key) { + return Some(*glyph_info); + } + + // Check if glyph fits in current row + if self.current_x + glyph_width > self.width { + // Move to next row + self.current_x = 0; + self.current_y += self.row_height; + self.row_height = 0; + } + + // Check if glyph fits in atlas + if self.current_y + glyph_height > self.height { + return None; // Atlas is full + } + + let glyph_info = GlyphInfo { + x: self.current_x, + y: self.current_y, + width: glyph_width, + height: glyph_height, + bearing_left: 0, // Default for fallback rectangles + bearing_top: 0, // Default for fallback rectangles + }; + + // Upload glyph data to texture + queue.write_texture( + wgpu::TexelCopyTextureInfo { + texture: &self.texture, + mip_level: 0, + origin: wgpu::Origin3d { + x: self.current_x, + y: self.current_y, + z: 0, + }, + aspect: wgpu::TextureAspect::All, + }, + glyph_data, + wgpu::TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(glyph_width), + rows_per_image: Some(glyph_height), + }, + wgpu::Extent3d { + width: glyph_width, + height: glyph_height, + depth_or_array_layers: 1, + }, + ); + + self.glyph_cache.insert(cache_key, glyph_info); + + // Update position for next glyph + self.current_x += glyph_width; + self.row_height = self.row_height.max(glyph_height); + + Some(glyph_info) + } + + pub fn texture_view(&self) -> &wgpu::TextureView { + &self.texture_view + } +} + +#[repr(C)] +#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)] +pub struct TextInstance { + pub pos: [f32; 2], + pub size: [f32; 2], + pub tex_coords: [f32; 4], // x, y, width, height in texture coordinates + pub color: [f32; 4], +} + +impl TextInstance { + pub fn new( + x: f32, + y: f32, + width: f32, + height: f32, + tex_x: f32, + tex_y: f32, + tex_width: f32, + tex_height: f32, + color: [f32; 4], + ) -> Self { + Self { + pos: [x, y], + size: [width, height], + tex_coords: [tex_x, tex_y, tex_width, tex_height], + color, + } + } +} + +pub struct TextRenderer { + pub font_system: FontSystem, + pub swash_cache: SwashCache, + pub atlas: TextAtlas, + pub render_pipeline: wgpu::RenderPipeline, + pub bind_group: wgpu::BindGroup, + pub sampler: wgpu::Sampler, + pub instance_buffer: wgpu::Buffer, + pub instance_count: u32, +} + +impl TextRenderer { + pub fn new(device: &wgpu::Device, _queue: &wgpu::Queue, format: wgpu::TextureFormat) -> Self { + let font_system = FontSystem::new(); + let swash_cache = SwashCache::new(); + let atlas = TextAtlas::new(device, 1024, 1024); + + // Create sampler for text texture + let sampler = device.create_sampler(&wgpu::SamplerDescriptor { + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Linear, + mipmap_filter: wgpu::FilterMode::Nearest, + ..Default::default() + }); + + // Create bind group layout + let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + label: Some("Text Bind Group Layout"), + }); + + // Create bind group + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + layout: &bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(atlas.texture_view()), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&sampler), + }, + ], + label: Some("Text Bind Group"), + }); + + // Create shader + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("Text Shader"), + source: wgpu::ShaderSource::Wgsl(include_str!("text_shader.wgsl").into()), + }); + + // Create pipeline layout + let push_constant_range = wgpu::PushConstantRange { + stages: wgpu::ShaderStages::VERTEX, + range: 0..std::mem::size_of::<[f32; 2]>() as u32, + }; + + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("Text Pipeline Layout"), + bind_group_layouts: &[&bind_group_layout], + push_constant_ranges: &[push_constant_range], + }); + + // Create render pipeline + let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("Text Render Pipeline"), + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: Some("vs_main"), + buffers: &[wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::() as wgpu::BufferAddress, + step_mode: wgpu::VertexStepMode::Instance, + attributes: &[ + // pos + wgpu::VertexAttribute { + offset: 0, + shader_location: 0, + format: wgpu::VertexFormat::Float32x2, + }, + // size + wgpu::VertexAttribute { + offset: std::mem::size_of::<[f32; 2]>() as wgpu::BufferAddress, + shader_location: 1, + format: wgpu::VertexFormat::Float32x2, + }, + // tex_coords + wgpu::VertexAttribute { + offset: std::mem::size_of::<[f32; 4]>() as wgpu::BufferAddress, + shader_location: 2, + format: wgpu::VertexFormat::Float32x4, + }, + // color + wgpu::VertexAttribute { + offset: std::mem::size_of::<[f32; 8]>() as wgpu::BufferAddress, + shader_location: 3, + format: wgpu::VertexFormat::Float32x4, + }, + ], + }], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: Some("fs_main"), + compilation_options: Default::default(), + targets: &[Some(wgpu::ColorTargetState { + 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: None, + cache: None, + }); + + // Create initial empty instance buffer + let instance_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Text Instance Buffer"), + contents: &[], + usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + }); + + Self { + font_system, + swash_cache, + atlas, + render_pipeline, + bind_group, + sampler, + instance_buffer, + instance_count: 0, + } + } + + pub fn render_text( + &mut self, + device: &wgpu::Device, + queue: &wgpu::Queue, + text: &str, + x: f32, + y: f32, + font_size: f32, + color: [f32; 4], + max_width: Option, + ) -> Vec { + println!( + "render_text called: '{}' at ({}, {}) size {} color {:?}", + text, x, y, font_size, color + ); + + let mut buffer = Buffer::new(&mut self.font_system, Metrics::new(font_size, font_size)); + + let wrap = if max_width.is_some() { + Wrap::Word + } else { + Wrap::None + }; + + buffer.set_size(&mut self.font_system, max_width, None); + buffer.set_text( + &mut self.font_system, + text, + Attrs::new() + .family(Family::SansSerif) + .weight(Weight::NORMAL), + Shaping::Advanced, + ); + buffer.set_wrap(&mut self.font_system, wrap); + buffer.shape_until_scroll(&mut self.font_system, false); + + // Ensure we have at least a white pixel in the atlas for fallback rendering + self.ensure_white_pixel(device, queue); + + let mut instances = Vec::new(); + + for run in buffer.layout_runs() { + println!("Layout run with {} glyphs", run.glyphs.len()); + for glyph in run.glyphs.iter() { + let glyph_width = glyph.w; + + println!( + "Glyph: id={} x={} y={} w={}", + glyph.glyph_id, glyph.x, glyph.y, glyph_width + ); + + if glyph_width > 0.0 { + // Generate a simple cache key for this specific glyph (glyph_id + font_size) + let cache_key = ((glyph.glyph_id as u64) << 32) | (font_size as u64); + + // Try to get the glyph from cache, or render it if not cached + let glyph_info = if let Some(cached_glyph) = + self.atlas.glyph_cache.get(&cache_key) + { + *cached_glyph + } else { + // Render the glyph using cosmic_text + println!("Rendering glyph {} to texture atlas", glyph.glyph_id); + + // Create cache key for this glyph + let (swash_cache_key, _, _) = cosmic_text::CacheKey::new( + glyph.font_id, + glyph.glyph_id, + glyph.font_size, + (0.0, 0.0), // subpixel offset + cosmic_text::CacheKeyFlags::empty(), // flags + ); + + // Get glyph image using the swash cache + let image_option = self + .swash_cache + .get_image(&mut self.font_system, swash_cache_key); + + if let Some(image) = image_option { + println!( + "Got glyph image: {}x{} format {:?}, placement: left={}, top={}", + image.placement.width, image.placement.height, image.content, + image.placement.left, image.placement.top + ); + + // Convert the image data to R8 format (alpha only) + let alpha_data = match image.content { + cosmic_text::SwashContent::Mask => { + // Mask data is already single-channel alpha + image.data.to_vec() + } + cosmic_text::SwashContent::Color => { + // Color data is BGRA, extract alpha channel + image.data.chunks(4).map(|bgra| bgra[3]).collect() + } + cosmic_text::SwashContent::SubpixelMask => { + // SubpixelMask is RGB, convert to grayscale + image + .data + .chunks(3) + .map(|rgb| { + (rgb[0] as f32 * 0.299 + + rgb[1] as f32 * 0.587 + + rgb[2] as f32 * 0.114) + as u8 + }) + .collect() + } + }; + + println!("Converted glyph to {} alpha bytes", alpha_data.len()); + + // Add to atlas + if let Some(atlas_info) = self.atlas.get_or_insert_glyph( + device, + queue, + cache_key, + &alpha_data, + image.placement.width, + image.placement.height, + ) { + println!( + "Added glyph to atlas at ({}, {})", + atlas_info.x, atlas_info.y + ); + // Store the glyph info with bearing information from placement + let glyph_info_with_bearing = GlyphInfo { + x: atlas_info.x, + y: atlas_info.y, + width: atlas_info.width, + height: atlas_info.height, + bearing_left: image.placement.left, + bearing_top: image.placement.top, + }; + self.atlas.glyph_cache.insert(cache_key, glyph_info_with_bearing); + glyph_info_with_bearing + } else { + println!( + "Failed to add glyph to atlas, using white rectangle fallback" + ); + // Fallback to white rectangle + GlyphInfo { + x: 0, + y: 0, + width: 8, + height: 8, + bearing_left: 0, + bearing_top: 0, + } + } + } else { + println!( + "Failed to render glyph {}, using white rectangle fallback", + glyph.glyph_id + ); + // Fallback to white rectangle for space characters or missing glyphs + GlyphInfo { + x: 0, + y: 0, + width: 8, + height: 8, + bearing_left: 0, + bearing_top: 0, + } + } + }; + + // Create text instance with proper texture coordinates + let tex_x = glyph_info.x as f32 / self.atlas.width as f32; + let tex_y = glyph_info.y as f32 / self.atlas.height as f32; + let tex_width = glyph_info.width as f32 / self.atlas.width as f32; + let tex_height = glyph_info.height as f32 / self.atlas.height as f32; + + // Use the actual glyph bitmap dimensions for rendering + let glyph_height = glyph_info.height as f32; + + // Calculate proper baseline-aligned position using bearing information + // bearing_left: horizontal offset from logical position to bitmap left edge + // bearing_top: distance from baseline to bitmap top edge (subtract to position correctly) + let render_x = x + glyph.x + glyph_info.bearing_left as f32; + let render_y = y + glyph.y - glyph_info.bearing_top as f32; + + let instance = TextInstance::new( + render_x, + render_y, + glyph_width, + glyph_height, + tex_x, + tex_y, + tex_width, + tex_height, + color, + ); + + instances.push(instance); + println!("Created text instance at ({}, {}) size ({}, {}) with tex coords ({}, {}, {}, {}) bearing ({}, {})", + render_x, render_y, glyph_width, glyph_height, + tex_x, tex_y, tex_width, tex_height, + glyph_info.bearing_left, glyph_info.bearing_top); + } + } + } + + println!("render_text created {} instances", instances.len()); + instances + } + + // Helper method to ensure we have a white pixel in the atlas for fallback rendering + fn ensure_white_pixel(&mut self, device: &wgpu::Device, queue: &wgpu::Queue) { + println!("ensure_white_pixel called"); + + // Check if we already have a white rectangle at key 0 + if !self.atlas.glyph_cache.contains_key(&0) { + println!("Creating white rectangle in atlas"); + // Create a 8x8 white rectangle for better sampling + let white_rect_data = vec![255u8; 8 * 8]; // 8x8 white rectangle for R8Unorm format + if let Some(glyph_info) = + self.atlas + .get_or_insert_glyph(device, queue, 0, &white_rect_data, 8, 8) + { + println!( + "White rectangle created at ({}, {}) size {}x{} in atlas", + glyph_info.x, glyph_info.y, glyph_info.width, glyph_info.height + ); + } else { + println!("Failed to create white rectangle in atlas"); + } + } else { + println!("White rectangle already exists in atlas"); + } + } + + pub fn update_instances( + &mut self, + device: &wgpu::Device, + queue: &wgpu::Queue, + instances: &[TextInstance], + ) { + println!("update_instances called with {} instances", instances.len()); + + if instances.is_empty() { + self.instance_count = 0; + return; + } + + let contents = bytemuck::cast_slice(instances); + + // Create new buffer if needed or if the current buffer is too small + if contents.len() > self.instance_buffer.size() as usize { + self.instance_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Text Instance Buffer"), + contents, + usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + }); + } else { + // Update existing buffer + queue.write_buffer(&self.instance_buffer, 0, contents); + } + + self.instance_count = instances.len() as u32; + println!( + "Text instance buffer updated with {} instances", + self.instance_count + ); + } + + pub fn draw(&self, render_pass: &mut wgpu::RenderPass, viewport: [f32; 2]) { + println!( + "Text draw called with {} instances, viewport: {:?}", + self.instance_count, viewport + ); + + if self.instance_count == 0 { + println!("No text instances to draw"); + return; + } + + println!("Setting up text render pass..."); + render_pass.set_pipeline(&self.render_pipeline); + render_pass.set_bind_group(0, &self.bind_group, &[]); + render_pass.set_push_constants( + wgpu::ShaderStages::VERTEX, + 0, + bytemuck::bytes_of(&viewport), + ); + render_pass.set_vertex_buffer(0, self.instance_buffer.slice(..)); + + println!( + "Drawing {} text instances with 6 vertices each", + self.instance_count + ); + render_pass.draw(0..6, 0..self.instance_count); + + println!( + "Text draw executed successfully for {} instances", + self.instance_count + ); + } +} diff --git a/src/text_shader.wgsl b/src/text_shader.wgsl new file mode 100644 index 0000000..eee8218 --- /dev/null +++ b/src/text_shader.wgsl @@ -0,0 +1,74 @@ +var viewport: vec2; + +struct VertexOutput { + @builtin(position) clip_position: vec4, + @location(0) tex_coords: vec2, + @location(1) color: vec4, +}; + +@vertex +fn vs_main( + @builtin(vertex_index) vertex_index: u32, + @location(0) instance_pos: vec2, + @location(1) instance_size: vec2, + @location(2) instance_tex_coords: vec4, // x, y, width, height + @location(3) instance_color: vec4, +) -> VertexOutput { + var vertex_pos: vec2; + var tex_coord: vec2; + + switch vertex_index % 6u { + case 0u: { + vertex_pos = vec2(0.0, 1.0); + tex_coord = vec2(0.0, 1.0); + } + case 1u: { + vertex_pos = vec2(0.0, 0.0); + tex_coord = vec2(0.0, 0.0); + } + case 2u: { + vertex_pos = vec2(1.0, 0.0); + tex_coord = vec2(1.0, 0.0); + } + case 3u: { + vertex_pos = vec2(0.0, 1.0); + tex_coord = vec2(0.0, 1.0); + } + case 4u: { + vertex_pos = vec2(1.0, 0.0); + tex_coord = vec2(1.0, 0.0); + } + case 5u, default: { + vertex_pos = vec2(1.0, 1.0); + tex_coord = vec2(1.0, 1.0); + } + } + + let pos = instance_pos + vertex_pos * instance_size; + let ndc_x = (pos.x / viewport.x) * 2.0 - 1.0; + let ndc_y = 1.0 - (pos.y / viewport.y) * 2.0; + + var output: VertexOutput; + output.clip_position = vec4(ndc_x, ndc_y, 0.0, 1.0); + + // Calculate texture coordinates + let tex_start = instance_tex_coords.xy; + let tex_size = instance_tex_coords.zw; + output.tex_coords = tex_start + tex_coord * tex_size; + + output.color = instance_color; + + return output; +} + +@group(0) @binding(0) +var text_texture: texture_2d; + +@group(0) @binding(1) +var text_sampler: sampler; + +@fragment +fn fs_main(vs_output: VertexOutput) -> @location(0) vec4 { + let alpha = textureSample(text_texture, text_sampler, vs_output.tex_coords).r; + return vec4(vs_output.color.rgb, vs_output.color.a * alpha); +} From 46457ca34f35bc1a16c11abf76772e7dba4b5943 Mon Sep 17 00:00:00 2001 From: Lukas Bombach Date: Sun, 1 Jun 2025 00:22:32 +0200 Subject: [PATCH 02/13] better font rendering --- src/app.rs | 19 +++ src/gpu.rs | 16 +- src/javascript_runtime/reconciler.ts | 2 +- src/text.rs | 228 ++++++++++++++------------- 4 files changed, 152 insertions(+), 113 deletions(-) diff --git a/src/app.rs b/src/app.rs index f836568..0fe5b9c 100644 --- a/src/app.rs +++ b/src/app.rs @@ -109,6 +109,25 @@ impl<'window> ApplicationHandler for App<'window> { } } } + WindowEvent::ScaleFactorChanged { scale_factor, .. } => { + if let Some(gpu) = self.gpu.as_mut() { + println!("Scale factor changed to: {}", scale_factor); + gpu.update_scale_factor(scale_factor); + + // Re-render text with new scale factor + if let Ok(gui) = self.gui.lock() { + let text_items = gui.collect_text_instances(); + let mut all_text_instances = Vec::new(); + + for (text, x, y, font_size, color) in text_items { + let text_instances = gpu.render_text(&text, x, y, font_size, color, None); + all_text_instances.extend(text_instances); + } + + gpu.update_text_instances(&all_text_instances); + } + } + } WindowEvent::RedrawRequested => { if let Some(gpu) = self.gpu.as_mut() { gpu.draw(); diff --git a/src/gpu.rs b/src/gpu.rs index af14d5d..ed533f2 100644 --- a/src/gpu.rs +++ b/src/gpu.rs @@ -47,6 +47,7 @@ pub struct Gpu<'window> { instance_buffer: wgpu::Buffer, instance_count: u32, viewport: [f32; 2], + scale_factor: f64, text_renderer: TextRenderer, } @@ -61,6 +62,7 @@ impl<'window> Gpu<'window> { */ let size = window.inner_size(); + let scale_factor = window.scale_factor(); let width = size.width.max(1); let height = size.height.max(1); let viewport = [width as f32, height as f32]; @@ -235,6 +237,7 @@ impl<'window> Gpu<'window> { instance_buffer, instance_count, viewport, + scale_factor, text_renderer, } } @@ -249,6 +252,14 @@ impl<'window> Gpu<'window> { self.viewport = [width as f32, height as f32]; } + pub fn update_scale_factor(&mut self, scale_factor: f64) { + self.scale_factor = scale_factor; + } + + pub fn get_scale_factor(&self) -> f64 { + self.scale_factor + } + pub fn draw(&mut self) { self.device.poll(wgpu::Maintain::Wait); @@ -323,13 +334,16 @@ impl<'window> Gpu<'window> { color: [f32; 4], max_width: Option, ) -> Vec { + // Apply DPI scaling to font size + let scaled_font_size = font_size * self.scale_factor as f32; + self.text_renderer.render_text( &self.device, &self.queue, text, x, y, - font_size, + scaled_font_size, color, max_width, ) diff --git a/src/javascript_runtime/reconciler.ts b/src/javascript_runtime/reconciler.ts index eecc8b4..51c349f 100644 --- a/src/javascript_runtime/reconciler.ts +++ b/src/javascript_runtime/reconciler.ts @@ -94,7 +94,7 @@ export const reconciler = ReactReconciler< createTextInstance(text, _rootContainerInstance, _hostContext, _internalInstanceHandle) { // Default text properties - could be configurable via context or props in the future const textColor = "white"; - const fontSize = 16.0; + const fontSize = 32.0; // Increased from 16.0 to make text more visible console.debug("createTextInstance", text, { textColor, diff --git a/src/text.rs b/src/text.rs index 2e98f37..4fe2c03 100644 --- a/src/text.rs +++ b/src/text.rs @@ -21,8 +21,8 @@ pub struct GlyphInfo { pub y: u32, pub width: u32, pub height: u32, - pub bearing_left: i32, // horizontal bearing from placement - pub bearing_top: i32, // vertical bearing from placement + pub bearing_left: i32, // horizontal bearing from placement + pub bearing_top: i32, // vertical bearing from placement } impl TextAtlas { @@ -50,8 +50,8 @@ impl TextAtlas { width, height, glyph_cache: HashMap::new(), - current_x: 0, - current_y: 0, + current_x: 2, // Start with padding + current_y: 2, // Start with padding row_height: 0, } } @@ -69,16 +69,19 @@ impl TextAtlas { return Some(*glyph_info); } - // Check if glyph fits in current row - if self.current_x + glyph_width > self.width { + // Add padding between glyphs to prevent texture bleeding + let padding = 2u32; // 2-pixel padding around each glyph + + // Check if glyph fits in current row (including padding) + if self.current_x + glyph_width + padding > self.width { // Move to next row - self.current_x = 0; - self.current_y += self.row_height; + self.current_x = padding; // Start new row with padding + self.current_y += self.row_height + padding; // Add vertical padding too self.row_height = 0; } - // Check if glyph fits in atlas - if self.current_y + glyph_height > self.height { + // Check if glyph fits in atlas (including padding) + if self.current_y + glyph_height + padding > self.height { return None; // Atlas is full } @@ -87,8 +90,8 @@ impl TextAtlas { y: self.current_y, width: glyph_width, height: glyph_height, - bearing_left: 0, // Default for fallback rectangles - bearing_top: 0, // Default for fallback rectangles + bearing_left: 0, // Default for fallback rectangles + bearing_top: 0, // Default for fallback rectangles }; // Upload glyph data to texture @@ -118,8 +121,8 @@ impl TextAtlas { self.glyph_cache.insert(cache_key, glyph_info); - // Update position for next glyph - self.current_x += glyph_width; + // Update position for next glyph (add padding to prevent bleeding) + self.current_x += glyph_width + padding; self.row_height = self.row_height.max(glyph_height); Some(glyph_info) @@ -379,91 +382,107 @@ impl TextRenderer { let cache_key = ((glyph.glyph_id as u64) << 32) | (font_size as u64); // Try to get the glyph from cache, or render it if not cached - let glyph_info = if let Some(cached_glyph) = - self.atlas.glyph_cache.get(&cache_key) - { - *cached_glyph - } else { - // Render the glyph using cosmic_text - println!("Rendering glyph {} to texture atlas", glyph.glyph_id); - - // Create cache key for this glyph - let (swash_cache_key, _, _) = cosmic_text::CacheKey::new( - glyph.font_id, - glyph.glyph_id, - glyph.font_size, - (0.0, 0.0), // subpixel offset - cosmic_text::CacheKeyFlags::empty(), // flags - ); - - // Get glyph image using the swash cache - let image_option = self - .swash_cache - .get_image(&mut self.font_system, swash_cache_key); - - if let Some(image) = image_option { - println!( + let glyph_info = + if let Some(cached_glyph) = self.atlas.glyph_cache.get(&cache_key) { + *cached_glyph + } else { + // Render the glyph using cosmic_text + println!("Rendering glyph {} to texture atlas", glyph.glyph_id); + + // Create cache key for this glyph + let (swash_cache_key, _, _) = cosmic_text::CacheKey::new( + glyph.font_id, + glyph.glyph_id, + glyph.font_size, + (0.0, 0.0), // subpixel offset + cosmic_text::CacheKeyFlags::empty(), // flags + ); + + // Get glyph image using the swash cache + let image_option = self + .swash_cache + .get_image(&mut self.font_system, swash_cache_key); + + if let Some(image) = image_option { + println!( "Got glyph image: {}x{} format {:?}, placement: left={}, top={}", image.placement.width, image.placement.height, image.content, image.placement.left, image.placement.top ); - // Convert the image data to R8 format (alpha only) - let alpha_data = match image.content { - cosmic_text::SwashContent::Mask => { - // Mask data is already single-channel alpha - image.data.to_vec() - } - cosmic_text::SwashContent::Color => { - // Color data is BGRA, extract alpha channel - image.data.chunks(4).map(|bgra| bgra[3]).collect() - } - cosmic_text::SwashContent::SubpixelMask => { - // SubpixelMask is RGB, convert to grayscale - image - .data - .chunks(3) - .map(|rgb| { - (rgb[0] as f32 * 0.299 - + rgb[1] as f32 * 0.587 - + rgb[2] as f32 * 0.114) - as u8 - }) - .collect() - } - }; - - println!("Converted glyph to {} alpha bytes", alpha_data.len()); - - // Add to atlas - if let Some(atlas_info) = self.atlas.get_or_insert_glyph( - device, - queue, - cache_key, - &alpha_data, - image.placement.width, - image.placement.height, - ) { - println!( - "Added glyph to atlas at ({}, {})", - atlas_info.x, atlas_info.y - ); - // Store the glyph info with bearing information from placement - let glyph_info_with_bearing = GlyphInfo { - x: atlas_info.x, - y: atlas_info.y, - width: atlas_info.width, - height: atlas_info.height, - bearing_left: image.placement.left, - bearing_top: image.placement.top, + // Convert the image data to R8 format (alpha only) + let alpha_data = match image.content { + cosmic_text::SwashContent::Mask => { + // Mask data is already single-channel alpha + image.data.to_vec() + } + cosmic_text::SwashContent::Color => { + // Color data is BGRA, extract alpha channel + image.data.chunks(4).map(|bgra| bgra[3]).collect() + } + cosmic_text::SwashContent::SubpixelMask => { + // SubpixelMask is RGB, convert to grayscale + image + .data + .chunks(3) + .map(|rgb| { + (rgb[0] as f32 * 0.299 + + rgb[1] as f32 * 0.587 + + rgb[2] as f32 * 0.114) + as u8 + }) + .collect() + } }; - self.atlas.glyph_cache.insert(cache_key, glyph_info_with_bearing); - glyph_info_with_bearing + + println!("Converted glyph to {} alpha bytes", alpha_data.len()); + + // Add to atlas + if let Some(atlas_info) = self.atlas.get_or_insert_glyph( + device, + queue, + cache_key, + &alpha_data, + image.placement.width, + image.placement.height, + ) { + println!( + "Added glyph to atlas at ({}, {})", + atlas_info.x, atlas_info.y + ); + // Store the glyph info with bearing information from placement + let glyph_info_with_bearing = GlyphInfo { + x: atlas_info.x, + y: atlas_info.y, + width: atlas_info.width, + height: atlas_info.height, + bearing_left: image.placement.left, + bearing_top: image.placement.top, + }; + self.atlas + .glyph_cache + .insert(cache_key, glyph_info_with_bearing); + glyph_info_with_bearing + } else { + println!( + "Failed to add glyph to atlas, using white rectangle fallback" + ); + // Fallback to white rectangle + GlyphInfo { + x: 0, + y: 0, + width: 8, + height: 8, + bearing_left: 0, + bearing_top: 0, + } + } } else { println!( - "Failed to add glyph to atlas, using white rectangle fallback" + "Failed to render glyph {}, using white rectangle fallback", + glyph.glyph_id ); - // Fallback to white rectangle + // Fallback to white rectangle for space characters or missing glyphs GlyphInfo { x: 0, y: 0, @@ -473,31 +492,18 @@ impl TextRenderer { bearing_top: 0, } } - } else { - println!( - "Failed to render glyph {}, using white rectangle fallback", - glyph.glyph_id - ); - // Fallback to white rectangle for space characters or missing glyphs - GlyphInfo { - x: 0, - y: 0, - width: 8, - height: 8, - bearing_left: 0, - bearing_top: 0, - } - } - }; + }; // Create text instance with proper texture coordinates + // No artificial padding needed since glyphs are spaced apart in atlas let tex_x = glyph_info.x as f32 / self.atlas.width as f32; let tex_y = glyph_info.y as f32 / self.atlas.height as f32; let tex_width = glyph_info.width as f32 / self.atlas.width as f32; let tex_height = glyph_info.height as f32 / self.atlas.height as f32; - // Use the actual glyph bitmap dimensions for rendering - let glyph_height = glyph_info.height as f32; + // Use the actual glyph bitmap dimensions for rendering (not advance width) + let render_width = glyph_info.width as f32; + let render_height = glyph_info.height as f32; // Calculate proper baseline-aligned position using bearing information // bearing_left: horizontal offset from logical position to bitmap left edge @@ -508,8 +514,8 @@ impl TextRenderer { let instance = TextInstance::new( render_x, render_y, - glyph_width, - glyph_height, + render_width, + render_height, tex_x, tex_y, tex_width, @@ -519,7 +525,7 @@ impl TextRenderer { instances.push(instance); println!("Created text instance at ({}, {}) size ({}, {}) with tex coords ({}, {}, {}, {}) bearing ({}, {})", - render_x, render_y, glyph_width, glyph_height, + render_x, render_y, render_width, render_height, tex_x, tex_y, tex_width, tex_height, glyph_info.bearing_left, glyph_info.bearing_top); } From fa9efdeb5ffc2b824ad4329f00734cf44f4c2ad2 Mon Sep 17 00:00:00 2001 From: Lukas Bombach Date: Sun, 1 Jun 2025 00:33:36 +0200 Subject: [PATCH 03/13] better --- src/main.tsx | 16 ++-------------- src/text.rs | 8 +------- 2 files changed, 3 insertions(+), 21 deletions(-) diff --git a/src/main.tsx b/src/main.tsx index e263dca..24ff760 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -7,21 +7,9 @@ ReactWGPU.render( width: "100%", height: "100%", alignItems: "start", - padding: "100px", + padding: "70px 0px", }} > -
- Hello, World! This is rendered text using WGPU! -
+ Hello, World! This is rendered text using WGPU! ); diff --git a/src/text.rs b/src/text.rs index 4fe2c03..3c5919f 100644 --- a/src/text.rs +++ b/src/text.rs @@ -72,7 +72,7 @@ impl TextAtlas { // Add padding between glyphs to prevent texture bleeding let padding = 2u32; // 2-pixel padding around each glyph - // Check if glyph fits in current row (including padding) + // Check if glyph fits in current row (including padding on both sides) if self.current_x + glyph_width + padding > self.width { // Move to next row self.current_x = padding; // Start new row with padding @@ -182,12 +182,6 @@ impl TextRenderer { // Create sampler for text texture let sampler = device.create_sampler(&wgpu::SamplerDescriptor { - address_mode_u: wgpu::AddressMode::ClampToEdge, - address_mode_v: wgpu::AddressMode::ClampToEdge, - address_mode_w: wgpu::AddressMode::ClampToEdge, - mag_filter: wgpu::FilterMode::Linear, - min_filter: wgpu::FilterMode::Linear, - mipmap_filter: wgpu::FilterMode::Nearest, ..Default::default() }); From aefda83cb2e5e67b8014c52acf4ba719129f41f2 Mon Sep 17 00:00:00 2001 From: Lukas Bombach Date: Sun, 1 Jun 2025 00:42:32 +0200 Subject: [PATCH 04/13] text positioniing --- src/main.tsx | 1 - src/text.rs | 17 ++++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/main.tsx b/src/main.tsx index 24ff760..f9df07e 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -7,7 +7,6 @@ ReactWGPU.render( width: "100%", height: "100%", alignItems: "start", - padding: "70px 0px", }} > Hello, World! This is rendered text using WGPU! diff --git a/src/text.rs b/src/text.rs index 3c5919f..7429b3b 100644 --- a/src/text.rs +++ b/src/text.rs @@ -356,6 +356,21 @@ impl TextRenderer { buffer.set_wrap(&mut self.font_system, wrap); buffer.shape_until_scroll(&mut self.font_system, false); + // Get font metrics to calculate proper baseline offset for top-aligned text + let font_metrics = buffer.metrics(); + println!( + "Font metrics: font_size={}, line_height={}", + font_metrics.font_size, font_metrics.line_height + ); + + // Use a reasonable approximation for ascent based on typical font proportions + // Most fonts have ascent of about 70-80% of the line height + let ascent = font_metrics.line_height * 0.75; + + // When positioning text at the "top" of a container, we want the ascent + // (the height above baseline) to start at the Y coordinate + let baseline_y = y + ascent; + // Ensure we have at least a white pixel in the atlas for fallback rendering self.ensure_white_pixel(device, queue); @@ -503,7 +518,7 @@ impl TextRenderer { // bearing_left: horizontal offset from logical position to bitmap left edge // bearing_top: distance from baseline to bitmap top edge (subtract to position correctly) let render_x = x + glyph.x + glyph_info.bearing_left as f32; - let render_y = y + glyph.y - glyph_info.bearing_top as f32; + let render_y = baseline_y + glyph.y - glyph_info.bearing_top as f32; let instance = TextInstance::new( render_x, From 5b7c1ca3b695511330411d61c7515407d53ee86d Mon Sep 17 00:00:00 2001 From: Lukas Bombach Date: Sun, 1 Jun 2025 00:55:25 +0200 Subject: [PATCH 05/13] wor wrap --- src/app.rs | 12 ++++++------ src/gui.rs | 38 +++++++++++++++++++++++++++++++++----- src/text.rs | 11 ++++++++--- 3 files changed, 47 insertions(+), 14 deletions(-) diff --git a/src/app.rs b/src/app.rs index 0fe5b9c..98caed8 100644 --- a/src/app.rs +++ b/src/app.rs @@ -65,8 +65,8 @@ impl<'window> ApplicationHandler for App<'window> { println!("GuiUpdate: collected {} text items", text_items.len()); let mut all_text_instances = Vec::new(); - for (text, x, y, font_size, color) in text_items { - let text_instances = gpu.render_text(&text, x, y, font_size, color, None); + for (text, x, y, font_size, color, max_width) in text_items { + let text_instances = gpu.render_text(&text, x, y, font_size, color, Some(max_width)); all_text_instances.extend(text_instances); } @@ -97,8 +97,8 @@ impl<'window> ApplicationHandler for App<'window> { println!("Resized: collected {} text items", text_items.len()); let mut all_text_instances = Vec::new(); - for (text, x, y, font_size, color) in text_items { - let text_instances = gpu.render_text(&text, x, y, font_size, color, None); + for (text, x, y, font_size, color, max_width) in text_items { + let text_instances = gpu.render_text(&text, x, y, font_size, color, Some(max_width)); all_text_instances.extend(text_instances); } @@ -119,8 +119,8 @@ impl<'window> ApplicationHandler for App<'window> { let text_items = gui.collect_text_instances(); let mut all_text_instances = Vec::new(); - for (text, x, y, font_size, color) in text_items { - let text_instances = gpu.render_text(&text, x, y, font_size, color, None); + for (text, x, y, font_size, color, max_width) in text_items { + let text_instances = gpu.render_text(&text, x, y, font_size, color, Some(max_width)); all_text_instances.extend(text_instances); } diff --git a/src/gui.rs b/src/gui.rs index 7081a8e..84fcd78 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -251,13 +251,14 @@ impl Gui { return instances; } - pub fn collect_text_instances(&self) -> Vec<(String, f32, f32, f32, [f32; 4])> { + pub fn collect_text_instances(&self) -> Vec<(String, f32, f32, f32, [f32; 4], f32)> { fn collect_text( gui: &Gui, node_id: taffy::NodeId, offset_x: f32, offset_y: f32, - text_items: &mut Vec<(String, f32, f32, f32, [f32; 4])>, + parent_width: f32, + text_items: &mut Vec<(String, f32, f32, f32, [f32; 4], f32)>, ) { let node = gui.node_from_id(node_id); let (x, y) = ( @@ -268,17 +269,44 @@ impl Gui { // Only collect text from Text nodes if matches!(node.kind, NodeKind::Text) { if let Some(ref text) = node.text { - text_items.push((text.clone(), x, y, node.font_size, node.text_color)); + // Use the parent container's width for text wrapping + println!( + "Text node: '{}' at ({}, {}) parent_width: {}, node_width: {}", + text, x, y, parent_width, node.layout.size.width + ); + text_items.push(( + text.clone(), + x, + y, + node.font_size, + node.text_color, + parent_width, + )); } + } else { + println!( + "Container node at ({}, {}) width: {}, height: {}", + x, y, node.layout.size.width, node.layout.size.height + ); } + // Pass this node's width as the container width for its children + let container_width = node.layout.size.width; for child_id in gui.children_from_id(node_id) { - collect_text(gui, *child_id, x, y, text_items); + collect_text(gui, *child_id, x, y, container_width, text_items); } } let mut text_items = Vec::new(); - collect_text(&self, self.root, 0.0, 0.0, &mut text_items); + let root_node = self.node_from_id(self.root); + collect_text( + &self, + self.root, + 0.0, + 0.0, + root_node.layout.size.width, + &mut text_items, + ); text_items } diff --git a/src/text.rs b/src/text.rs index 7429b3b..073bc33 100644 --- a/src/text.rs +++ b/src/text.rs @@ -376,8 +376,12 @@ impl TextRenderer { let mut instances = Vec::new(); - for run in buffer.layout_runs() { - println!("Layout run with {} glyphs", run.glyphs.len()); + for (line_index, run) in buffer.layout_runs().enumerate() { + println!("Layout run {} with {} glyphs", line_index, run.glyphs.len()); + + // Calculate Y offset for each line based on line height + let line_y_offset = line_index as f32 * font_metrics.line_height; + for glyph in run.glyphs.iter() { let glyph_width = glyph.w; @@ -518,7 +522,8 @@ impl TextRenderer { // bearing_left: horizontal offset from logical position to bitmap left edge // bearing_top: distance from baseline to bitmap top edge (subtract to position correctly) let render_x = x + glyph.x + glyph_info.bearing_left as f32; - let render_y = baseline_y + glyph.y - glyph_info.bearing_top as f32; + let render_y = + baseline_y + line_y_offset + glyph.y - glyph_info.bearing_top as f32; let instance = TextInstance::new( render_x, From 0b7c9167ee399efa8f7840ad99bab45802e04e72 Mon Sep 17 00:00:00 2001 From: Lukas Bombach Date: Sun, 1 Jun 2025 08:11:44 +0200 Subject: [PATCH 06/13] xyz --- src/javascript_runtime/reconciler.ts | 2 +- src/main.tsx | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/javascript_runtime/reconciler.ts b/src/javascript_runtime/reconciler.ts index 51c349f..cb7ee9b 100644 --- a/src/javascript_runtime/reconciler.ts +++ b/src/javascript_runtime/reconciler.ts @@ -93,7 +93,7 @@ export const reconciler = ReactReconciler< createTextInstance(text, _rootContainerInstance, _hostContext, _internalInstanceHandle) { // Default text properties - could be configurable via context or props in the future - const textColor = "white"; + const textColor = "black"; const fontSize = 32.0; // Increased from 16.0 to make text more visible console.debug("createTextInstance", text, { diff --git a/src/main.tsx b/src/main.tsx index f9df07e..1ebbb89 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -4,11 +4,15 @@ import { ReactWGPU } from "./javascript_runtime/react_wgpu.ts"; ReactWGPU.render(
- Hello, World! This is rendered text using WGPU! +
Hello, World!
+
This is rendered text using WGPU!
); From 4beabb0818a78e938e301ed6e1a0262555a99624 Mon Sep 17 00:00:00 2001 From: Lukas Bombach Date: Sun, 1 Jun 2025 10:28:48 +0200 Subject: [PATCH 07/13] added a reference rect --- src/main.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main.tsx b/src/main.tsx index 1ebbb89..c543e16 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -10,8 +10,17 @@ ReactWGPU.render( height: "100%", alignItems: "start", backgroundColor: "#000", + padding: 20, }} > +
Hello, World!
This is rendered text using WGPU!
From b3ce1858cc43f54c5c939974f64e2ce6756d40eb Mon Sep 17 00:00:00 2001 From: Lukas Bombach Date: Sun, 1 Jun 2025 10:32:33 +0200 Subject: [PATCH 08/13] rm bg color --- src/main.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main.tsx b/src/main.tsx index c543e16..15db200 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -9,7 +9,6 @@ ReactWGPU.render( width: "100%", height: "100%", alignItems: "start", - backgroundColor: "#000", padding: 20, }} > From b2c49795e27cd393941fc6eb811ac861a551d984 Mon Sep 17 00:00:00 2001 From: Lukas Bombach Date: Sun, 1 Jun 2025 13:03:47 +0200 Subject: [PATCH 09/13] dunno --- src/app.rs | 37 ++++++++++++++++++++++------------- src/javascript_runtime/mod.rs | 2 +- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/src/app.rs b/src/app.rs index 98caed8..1c749b0 100644 --- a/src/app.rs +++ b/src/app.rs @@ -59,18 +59,22 @@ impl<'window> ApplicationHandler for App<'window> { gui.compute_layout(size.width, size.height); gpu.update_instance_buffer(gui.into_instances()); - + // Collect and render text instances let text_items = gui.collect_text_instances(); println!("GuiUpdate: collected {} text items", text_items.len()); let mut all_text_instances = Vec::new(); - + for (text, x, y, font_size, color, max_width) in text_items { - let text_instances = gpu.render_text(&text, x, y, font_size, color, Some(max_width)); + let text_instances = + gpu.render_text(&text, x, y, font_size, color, Some(max_width)); all_text_instances.extend(text_instances); } - - println!("GuiUpdate: updating GPU with {} text instances", all_text_instances.len()); + + println!( + "GuiUpdate: updating GPU with {} text instances", + all_text_instances.len() + ); gpu.update_text_instances(&all_text_instances); window.request_redraw(); @@ -91,18 +95,22 @@ impl<'window> ApplicationHandler for App<'window> { if let Ok(mut gui) = self.gui.lock() { gui.compute_layout(size.width, size.height); gpu.update_instance_buffer(gui.into_instances()); - + // Collect and render text instances let text_items = gui.collect_text_instances(); println!("Resized: collected {} text items", text_items.len()); let mut all_text_instances = Vec::new(); - + for (text, x, y, font_size, color, max_width) in text_items { - let text_instances = gpu.render_text(&text, x, y, font_size, color, Some(max_width)); + let text_instances = + gpu.render_text(&text, x, y, font_size, color, Some(max_width)); all_text_instances.extend(text_instances); } - - println!("Resized: updating GPU with {} text instances", all_text_instances.len()); + + println!( + "Resized: updating GPU with {} text instances", + all_text_instances.len() + ); gpu.update_text_instances(&all_text_instances); gpu.set_size(size.width, size.height); @@ -113,17 +121,18 @@ impl<'window> ApplicationHandler for App<'window> { if let Some(gpu) = self.gpu.as_mut() { println!("Scale factor changed to: {}", scale_factor); gpu.update_scale_factor(scale_factor); - + // Re-render text with new scale factor if let Ok(gui) = self.gui.lock() { let text_items = gui.collect_text_instances(); let mut all_text_instances = Vec::new(); - + for (text, x, y, font_size, color, max_width) in text_items { - let text_instances = gpu.render_text(&text, x, y, font_size, color, Some(max_width)); + let text_instances = + gpu.render_text(&text, x, y, font_size, color, Some(max_width)); all_text_instances.extend(text_instances); } - + gpu.update_text_instances(&all_text_instances); } } diff --git a/src/javascript_runtime/mod.rs b/src/javascript_runtime/mod.rs index 4d77047..e67e3de 100644 --- a/src/javascript_runtime/mod.rs +++ b/src/javascript_runtime/mod.rs @@ -86,7 +86,7 @@ fn op_create_text_node( font_size: f32, ) -> Result { let default_text_color: &str = "white"; - + let parsed_text_color = parse_color(&text_color) .unwrap_or(DynamicColor::from_str(default_text_color).unwrap()) .components; From c9e299bdaa00712284de21fef0a26ee6f26ea4fd Mon Sep 17 00:00:00 2001 From: Lukas Bombach Date: Sun, 8 Jun 2025 11:31:11 +0200 Subject: [PATCH 10/13] it was the borderRadius, that made the rects invisible --- src/main.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main.tsx b/src/main.tsx index 15db200..0753322 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -20,7 +20,7 @@ ReactWGPU.render( borderRadius: 10, }} > -
Hello, World!
-
This is rendered text using WGPU!
+
Hello, World!
+
This is rendered text using WGPU!
); From 8a28bfcd7c55e059b30b13f78bd9a0c1fd0f0a63 Mon Sep 17 00:00:00 2001 From: Lukas Bombach Date: Sun, 8 Jun 2025 11:33:52 +0200 Subject: [PATCH 11/13] x --- src/main.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.tsx b/src/main.tsx index 0753322..dd3ed05 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -20,7 +20,7 @@ ReactWGPU.render( borderRadius: 10, }} > -
Hello, World!
+
Hello, World!
This is rendered text using WGPU!
); From 820a82cab38cabc16e051874f47c796dc1480662 Mon Sep 17 00:00:00 2001 From: Lukas Bombach Date: Sun, 8 Jun 2025 11:36:34 +0200 Subject: [PATCH 12/13] fixed the border radius --- src/main.tsx | 5 +++-- src/shader.wgsl | 20 ++++++++++++-------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/main.tsx b/src/main.tsx index dd3ed05..63403fb 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -10,6 +10,7 @@ ReactWGPU.render( height: "100%", alignItems: "start", padding: 20, + gap: 20, }} >
-
Hello, World!
-
This is rendered text using WGPU!
+
Hello, World!
+
This is rendered text using WGPU!
); diff --git a/src/shader.wgsl b/src/shader.wgsl index 800dffb..37c5d88 100644 --- a/src/shader.wgsl +++ b/src/shader.wgsl @@ -50,13 +50,17 @@ fn fs_main(vs_output: VertexOutput) -> @location(0) vec4f { // Calculate distance from corners for rounded rectangle let corner_radius = min(radius, min(rect_size.x, rect_size.y) * 0.5); - - // Calculate the distance to the nearest corner - let corner_distance = length(max(abs(rect_pos - rect_size * 0.5) - (rect_size * 0.5 - corner_radius), vec2(0.0, 0.0))); - + + var alpha: f32; + + if (corner_radius == 0.0) { + alpha = 1.0; + } else { + let corner_distance = length(max(abs(rect_pos - rect_size * 0.5) - (rect_size * 0.5 - corner_radius), vec2(0.0, 0.0))); // Anti-aliased edge - let edge_softness = 1.0; - let alpha = 1.0 - smoothstep(corner_radius - edge_softness, corner_radius, corner_distance); - + let edge_softness = 1.0; + alpha = 1.0 - smoothstep(corner_radius - edge_softness, corner_radius, corner_distance); + } + return vec4(vs_output.background_color.rgb, vs_output.background_color.a * alpha); -} \ No newline at end of file +} From da1244e4f43c8553d1a01caf5ecce92228b2b420 Mon Sep 17 00:00:00 2001 From: Lukas Bombach Date: Mon, 9 Jun 2025 11:04:44 +0200 Subject: [PATCH 13/13] wip --- src/app.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/app.rs b/src/app.rs index 1c749b0..acfe78f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -57,9 +57,6 @@ impl<'window> ApplicationHandler for App<'window> { if let Ok(mut gui) = self.gui.lock() { let size = window.inner_size(); - gui.compute_layout(size.width, size.height); - gpu.update_instance_buffer(gui.into_instances()); - // Collect and render text instances let text_items = gui.collect_text_instances(); println!("GuiUpdate: collected {} text items", text_items.len()); @@ -77,6 +74,9 @@ impl<'window> ApplicationHandler for App<'window> { ); gpu.update_text_instances(&all_text_instances); + gui.compute_layout(size.width, size.height); + gpu.update_instance_buffer(gui.into_instances()); + window.request_redraw(); } } @@ -93,9 +93,6 @@ impl<'window> ApplicationHandler for App<'window> { WindowEvent::Resized(size) => { if let Some(gpu) = self.gpu.as_mut() { if let Ok(mut gui) = self.gui.lock() { - gui.compute_layout(size.width, size.height); - gpu.update_instance_buffer(gui.into_instances()); - // Collect and render text instances let text_items = gui.collect_text_instances(); println!("Resized: collected {} text items", text_items.len()); @@ -113,6 +110,9 @@ impl<'window> ApplicationHandler for App<'window> { ); gpu.update_text_instances(&all_text_instances); + gui.compute_layout(size.width, size.height); + gpu.update_instance_buffer(gui.into_instances()); + gpu.set_size(size.width, size.height); } }