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..f6d8340 100644 --- a/src/app.rs +++ b/src/app.rs @@ -18,7 +18,7 @@ pub enum CustomEvent { pub struct App<'window> { window: Option>, gpu: Option>, - pub gui: Arc>, + pub gui: Arc>>, } impl App<'_> { @@ -56,6 +56,24 @@ impl<'window> ApplicationHandler for App<'window> { if let Some(gpu) = self.gpu.as_mut() { if let Ok(mut gui) = self.gui.lock() { let size = window.inner_size(); + gui.recompute_layout(size.width, size.height); + + // 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)); + all_text_instances.extend(text_instances); + } + + println!( + "GuiUpdate: updating GPU with {} text instances", + all_text_instances.len() + ); + gpu.update_text_instances(&all_text_instances); gui.compute_layout(size.width, size.height); gpu.update_instance_buffer(gui.into_instances()); @@ -76,6 +94,23 @@ 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() { + // 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)); + all_text_instances.extend(text_instances); + } + + println!( + "Resized: updating GPU with {} text instances", + all_text_instances.len() + ); + gpu.update_text_instances(&all_text_instances); + gui.compute_layout(size.width, size.height); gpu.update_instance_buffer(gui.into_instances()); @@ -83,6 +118,26 @@ 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, 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); + } + + 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 89ac5e5..ed533f2 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,8 @@ pub struct Gpu<'window> { instance_buffer: wgpu::Buffer, instance_count: u32, viewport: [f32; 2], + scale_factor: f64, + text_renderer: TextRenderer, } impl<'window> Gpu<'window> { @@ -58,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]; @@ -217,6 +222,12 @@ impl<'window> Gpu<'window> { cache: None, }); + /* + * text renderer + */ + + let text_renderer = TextRenderer::new(&device, &queue, config.format); + Gpu { surface, config, @@ -226,6 +237,8 @@ impl<'window> Gpu<'window> { instance_buffer, instance_count, viewport, + scale_factor, + text_renderer, } } @@ -239,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); @@ -284,6 +305,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 +324,37 @@ 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 { + // 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, + scaled_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/gui.rs b/src/gui/gui.rs new file mode 100644 index 0000000..7ef3ecc --- /dev/null +++ b/src/gui/gui.rs @@ -0,0 +1,265 @@ +use crate::app::CustomEvent; +use crate::gui::node::BlockNode; +use crate::gui::node::Node; +use cosmic_text::{FontSystem, SwashCache}; +use slotmap::{DefaultKey, SlotMap}; +use std::cell::RefCell; +use std::convert::From; +use std::rc::Rc; +use std::sync::Arc; +use std::sync::Mutex; +use taffy::{ + compute_block_layout, compute_cached_layout, compute_flexbox_layout, compute_grid_layout, + compute_root_layout, prelude::*, Layout, LayoutOutput, Style, +}; +use winit::event_loop::EventLoopProxy; + +pub struct Gui<'a> { + root: NodeId, + nodes: SlotMap>, + event_loop: Arc>>, + pub font_system: Rc>, + pub swash_cache: Rc>, +} + +impl<'a> Gui<'a> { + pub fn new(event_loop: Arc>>) -> Self { + let font_system = Rc::new(RefCell::new(FontSystem::new())); + let swash_cache = Rc::new(RefCell::new(SwashCache::new())); + + let mut nodes = SlotMap::new(); + let root = nodes.insert(Self::create_root_node()).into(); + + Self { + root, + nodes, + event_loop, + font_system, + swash_cache, + } + } + + fn create_root_node() -> Node<'a> { + Node::BlockNode(BlockNode { + style: Style { + display: Display::Block, + size: Size { + width: percent(1.0), + height: percent(1.0), + }, + ..Default::default() + }, + ..BlockNode::default() + }) + } + + pub fn recompute_layout(&mut self, width: u32, height: u32) { + let width = length(width as f32); + let height = length(height as f32); + compute_root_layout(self, self.root, Size { width, height }); + } +} + +impl<'a> Gui<'a> { + #[inline(always)] + pub fn node_from_id(&self, node_id: NodeId) -> &Node<'a> { + &self.nodes.get(node_id.into()).unwrap() + } + + #[inline(always)] + pub fn node_from_id_mut(&mut self, node_id: NodeId) -> &mut Node<'a> { + self.nodes.get_mut(node_id.into()).unwrap() + } +} + +pub struct ChildIter<'a>(std::slice::Iter<'a, NodeId>); + +impl Iterator for ChildIter<'_> { + type Item = NodeId; + fn next(&mut self) -> Option { + self.0.next().copied().map(NodeId::from) + } +} + +impl taffy::TraverseTree for Gui<'_> {} + +impl taffy::TraversePartialTree for Gui<'_> { + type ChildIter<'a> + = ChildIter<'a> + where + Self: 'a; + + fn child_ids(&self, node_id: NodeId) -> Self::ChildIter<'_> { + ChildIter(self.node_from_id(node_id).children().iter()) + } + + fn child_count(&self, node_id: NodeId) -> usize { + self.node_from_id(node_id).children().len() + } + + fn get_child_id(&self, node_id: NodeId, index: usize) -> NodeId { + NodeId::from(self.node_from_id(node_id).children()[index]) + } +} + +impl taffy::LayoutPartialTree for Gui<'_> { + type CoreContainerStyle<'a> + = &'a Style + where + Self: 'a; + + fn get_core_container_style(&self, node_id: NodeId) -> Self::CoreContainerStyle<'_> { + &self.node_from_id(node_id).style() + } + + fn set_unrounded_layout(&mut self, node_id: NodeId, layout: &Layout) { + self.node_from_id_mut(node_id).set_layout(*layout) + } + + fn compute_child_layout( + &mut self, + node_id: NodeId, + inputs: taffy::tree::LayoutInput, + ) -> taffy::tree::LayoutOutput { + compute_cached_layout(self, node_id, inputs, |gui, node_id, inputs| { + // Only borrow font_system mutably when needed, before any mutable borrow of gui + let node_ref = gui.node_from_id(node_id); + + match node_ref { + Node::GridNode(block_node) => compute_grid_layout(gui, node_id, inputs), + Node::FlexNode(block_node) => compute_flexbox_layout(gui, node_id, inputs), + Node::BlockNode(block_node) => compute_block_layout(gui, node_id, inputs), + Node::TextNode(_) => { + let fs = gui.font_system.clone(); + let mut fs = fs.borrow_mut(); + // Get a mutable reference to the TextNode + let text_node_mut = match gui.node_from_id_mut(node_id) { + Node::TextNode(text_node_mut) => text_node_mut, + _ => unreachable!(), + }; + let mut buffer = text_node_mut.buffer.borrow_with(&mut fs); + + // determine the width the text has to fit into + let available_space = inputs.available_space; + let known_dimensions = inputs.known_dimensions; + + let width_constraint = known_dimensions.width.or(match available_space.width { + AvailableSpace::MinContent => Some(0.0), + AvailableSpace::MaxContent => None, + AvailableSpace::Definite(width) => Some(width), + }); + + buffer.set_size(width_constraint, None); + + // Perform shaping as desired + buffer.shape_until_scroll(true); + + // Determine measured width and height of text + let (width, total_lines) = buffer + .layout_runs() + .fold((0.0, 0usize), |(width, total_lines), run| { + (run.line_w.max(width), total_lines + 1) + }); + + let height = total_lines as f32 * buffer.metrics().line_height; + + return LayoutOutput::from_outer_size(Size { width, height }); + } + } + }) + } +} + +impl taffy::LayoutFlexboxContainer for Gui<'_> { + type FlexboxContainerStyle<'a> + = &'a Style + where + Self: 'a; + + type FlexboxItemStyle<'a> + = &'a Style + where + Self: 'a; + + fn get_flexbox_container_style(&self, node_id: NodeId) -> Self::FlexboxContainerStyle<'_> { + &self.node_from_id(node_id).style() + } + + fn get_flexbox_child_style(&self, child_node_id: NodeId) -> Self::FlexboxItemStyle<'_> { + &self.node_from_id(child_node_id).style() + } +} + +impl taffy::LayoutGridContainer for Gui<'_> { + type GridContainerStyle<'a> + = &'a Style + where + Self: 'a; + + type GridItemStyle<'a> + = &'a Style + where + Self: 'a; + + fn get_grid_container_style(&self, node_id: NodeId) -> Self::GridContainerStyle<'_> { + &self.node_from_id(node_id).style() + } + + fn get_grid_child_style(&self, child_node_id: NodeId) -> Self::GridItemStyle<'_> { + &self.node_from_id(child_node_id).style() + } +} + +impl taffy::LayoutBlockContainer for Gui<'_> { + type BlockContainerStyle<'a> + = &'a Style + where + Self: 'a; + + type BlockItemStyle<'a> + = &'a Style + where + Self: 'a; + + fn get_block_container_style(&self, node_id: NodeId) -> Self::BlockContainerStyle<'_> { + &self.node_from_id(node_id).style() + } + + fn get_block_child_style(&self, child_node_id: NodeId) -> Self::BlockItemStyle<'_> { + &self.node_from_id(child_node_id).style() + } +} + +impl taffy::CacheTree for Gui<'_> { + fn cache_get( + &self, + node_id: NodeId, + known_dimensions: Size>, + available_space: Size, + run_mode: taffy::RunMode, + ) -> Option { + self.node_from_id(node_id) + .cache() + .get(known_dimensions, available_space, run_mode) + } + + fn cache_store( + &mut self, + node_id: NodeId, + known_dimensions: Size>, + available_space: Size, + run_mode: taffy::RunMode, + layout_output: taffy::LayoutOutput, + ) { + self.node_from_id_mut(node_id).cache_mut().store( + known_dimensions, + available_space, + run_mode, + layout_output, + ) + } + + fn cache_clear(&mut self, node_id: NodeId) { + self.node_from_id_mut(node_id).cache_mut().clear(); + } +} diff --git a/src/gui/mod.rs b/src/gui/mod.rs new file mode 100644 index 0000000..7b49286 --- /dev/null +++ b/src/gui/mod.rs @@ -0,0 +1,3 @@ +pub use gui::Gui; +mod gui; +mod node; diff --git a/src/gui/node.rs b/src/gui/node.rs new file mode 100644 index 0000000..5e02019 --- /dev/null +++ b/src/gui/node.rs @@ -0,0 +1,147 @@ +use cosmic_text::{Attrs, Buffer, Metrics}; +use taffy::{Cache, Layout, NodeId, Style}; + +pub enum Node<'a> { + GridNode(GridNode), + FlexNode(FlexNode), + BlockNode(BlockNode), + TextNode(TextNode<'a>), +} + +pub struct GridNode { + pub layout: Layout, + pub style: Style, + pub children: Vec, + pub cache: Cache, +} + +pub struct FlexNode { + pub layout: Layout, + pub style: Style, + pub children: Vec, + pub cache: Cache, +} + +pub struct BlockNode { + pub layout: Layout, + pub style: Style, + pub children: Vec, + pub cache: Cache, +} + +pub struct TextNode<'a> { + pub layout: Layout, + pub style: Style, + pub children: Vec, + pub cache: Cache, + + pub text: &'a str, + + pub metrics: Metrics, + pub buffer: Buffer, + pub attrs: Attrs<'a>, +} + +impl Default for GridNode { + fn default() -> Self { + Self { + layout: Layout::default(), + style: Style::default(), + children: Vec::new(), + cache: Cache::default(), + } + } +} + +impl Default for FlexNode { + fn default() -> Self { + Self { + layout: Layout::default(), + style: Style::default(), + children: Vec::new(), + cache: Cache::default(), + } + } +} + +impl Default for BlockNode { + fn default() -> Self { + Self { + layout: Layout::default(), + style: Style::default(), + children: Vec::new(), + cache: Cache::default(), + } + } +} + +/* impl<'a> Default for TextNode<'a> { + fn default() -> Self { + Self { + layout: Layout::default(), + style: Style::default(), + children: Vec::new(), + cache: Cache::default(), + text: "", + metrics: Metrics::default(), + buffer: Buffer::new(), + attrs: Attrs::default(), + } + } +} */ + +impl Node<'_> { + pub fn children(&self) -> &Vec { + match self { + Node::GridNode(block_node) => &block_node.children, + Node::FlexNode(block_node) => &block_node.children, + Node::BlockNode(block_node) => &block_node.children, + Node::TextNode(text_node) => &text_node.children, + } + } + + pub fn layout(&self) -> &Layout { + match self { + Node::GridNode(block_node) => &block_node.layout, + Node::FlexNode(block_node) => &block_node.layout, + Node::BlockNode(block_node) => &block_node.layout, + Node::TextNode(text_node) => &text_node.layout, + } + } + + pub fn set_layout(&mut self, layout: Layout) { + match self { + Node::GridNode(block_node) => block_node.layout = layout, + Node::FlexNode(block_node) => block_node.layout = layout, + Node::BlockNode(block_node) => block_node.layout = layout, + Node::TextNode(text_node) => text_node.layout = layout, + } + } + + pub fn style(&self) -> &Style { + match self { + Node::GridNode(block_node) => &block_node.style, + Node::FlexNode(block_node) => &block_node.style, + Node::BlockNode(block_node) => &block_node.style, + Node::TextNode(text_node) => &text_node.style, + } + } + + pub fn cache(&self) -> &Cache { + match self { + Node::GridNode(block_node) => &block_node.cache, + Node::FlexNode(block_node) => &block_node.cache, + Node::BlockNode(block_node) => &block_node.cache, + Node::TextNode(text_node) => &text_node.cache, + } + } + + pub fn cache_mut(&mut self) -> &mut Cache { + match self { + Node::GridNode(block_node) => &mut block_node.cache, + Node::FlexNode(block_node) => &mut block_node.cache, + Node::BlockNode(block_node) => &mut block_node.cache, + Node::TextNode(text_node) => &mut text_node.cache, + } + } +} diff --git a/src/gui.rs b/src/gui_old.rs similarity index 63% rename from src/gui.rs rename to src/gui_old.rs index 6bc4a81..84fcd78 100644 --- a/src/gui.rs +++ b/src/gui_old.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,65 @@ impl Gui { return instances; } + 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, + 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) = ( + 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 { + // 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, container_width, text_items); + } + } + + let mut text_items = Vec::new(); + 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 + } + fn notify_update(&self) { if let Ok(proxy) = self.event_loop.lock() { proxy.send_event(CustomEvent::GuiUpdate).unwrap(); @@ -250,6 +369,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..e67e3de 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..cb7ee9b 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 = "black"; + const fontSize = 32.0; // Increased from 16.0 to make text more visible + + 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..63403fb 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -4,19 +4,24 @@ import { ReactWGPU } from "./javascript_runtime/react_wgpu.ts"; ReactWGPU.render(
+
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 +} diff --git a/src/text.rs b/src/text.rs new file mode 100644 index 0000000..073bc33 --- /dev/null +++ b/src/text.rs @@ -0,0 +1,644 @@ +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: 2, // Start with padding + current_y: 2, // Start with padding + 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); + } + + // 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 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 + self.current_y += self.row_height + padding; // Add vertical padding too + self.row_height = 0; + } + + // Check if glyph fits in atlas (including padding) + if self.current_y + glyph_height + padding > 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 (add padding to prevent bleeding) + self.current_x += glyph_width + padding; + 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 { + ..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); + + // 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); + + let mut instances = Vec::new(); + + 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; + + 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 + // 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 (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 + // 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 + line_y_offset + glyph.y - glyph_info.bearing_top as f32; + + let instance = TextInstance::new( + render_x, + render_y, + render_width, + render_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, render_width, render_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); +}