1use bevy_ecs::prelude::*;
12use bytemuck::{Pod, Zeroable};
13use wgpu::util::DeviceExt;
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum FlexDirection {
24 Row,
25 RowReverse,
26 Column,
27 ColumnReverse,
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum Align {
40 Start,
41 Center,
42 End,
43 Stretch,
44 SpaceBetween,
45 SpaceAround,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq)]
59pub enum Val {
60 Auto,
61 Px(f32),
62 Percent(f32),
63}
64
65impl Default for Val {
66 fn default() -> Self { Val::Auto }
67}
68
69#[derive(Debug, Clone)]
88pub struct UiStyle {
89 pub flex_direction: FlexDirection,
90 pub justify_content: Align,
91 pub align_items: Align,
92 pub width: Val,
93 pub height: Val,
94 pub min_width: Val,
95 pub min_height: Val,
96 pub max_width: Val,
97 pub max_height: Val,
98 pub padding: [f32; 4], pub margin: [f32; 4],
100 pub gap: f32,
101 pub flex_grow: f32,
102 pub flex_shrink: f32,
103}
104
105impl Default for UiStyle {
106 fn default() -> Self {
107 Self {
108 flex_direction: FlexDirection::Row,
109 justify_content: Align::Start,
110 align_items: Align::Stretch,
111 width: Val::Auto,
112 height: Val::Auto,
113 min_width: Val::Auto,
114 min_height: Val::Auto,
115 max_width: Val::Auto,
116 max_height: Val::Auto,
117 padding: [0.0; 4],
118 margin: [0.0; 4],
119 gap: 0.0,
120 flex_grow: 0.0,
121 flex_shrink: 1.0,
122 }
123 }
124}
125
126#[derive(Debug, Clone)]
138pub struct UiText {
139 pub content: String,
140 pub font_size: f32,
141 pub color: [f32; 4],
142 pub font_family: String,
143}
144
145impl UiText {
146 pub fn new(content: &str) -> Self {
147 Self {
148 content: content.to_string(),
149 font_size: 16.0,
150 color: [1.0, 1.0, 1.0, 1.0],
151 font_family: "default".to_string(),
152 }
153 }
154
155 pub fn with_font_size(mut self, size: f32) -> Self {
156 self.font_size = size;
157 self
158 }
159
160 pub fn with_color(mut self, color: [f32; 4]) -> Self {
161 self.color = color;
162 self
163 }
164}
165
166#[derive(Debug, Clone, Component)]
184pub struct UiNode {
185 pub background_color: [f32; 4],
187 pub border_radius: f32,
189 pub border_width: f32,
191 pub border_color: [f32; 4],
193 pub text: Option<UiText>,
195 pub style: UiStyle,
197 pub visible: bool,
199 pub computed_rect: [f32; 4], }
202
203impl Default for UiNode {
204 fn default() -> Self {
205 Self {
206 background_color: [0.0, 0.0, 0.0, 0.0],
207 border_radius: 0.0,
208 border_width: 0.0,
209 border_color: [0.0; 4],
210 text: None,
211 style: UiStyle::default(),
212 visible: true,
213 computed_rect: [0.0; 4],
214 }
215 }
216}
217
218use taffy::prelude as tf;
223
224pub struct UiLayoutEngine {
228 taffy: tf::TaffyTree,
229}
230
231impl UiLayoutEngine {
232 pub fn new() -> Self {
233 Self {
234 taffy: tf::TaffyTree::new(),
235 }
236 }
237
238 fn convert_style(style: &UiStyle, node: &UiNode) -> tf::Style {
240 let to_dim = |v: &Val| match v {
241 Val::Auto => tf::Dimension::Auto,
242 Val::Px(px) => tf::Dimension::Length(*px),
243 Val::Percent(pct) => tf::Dimension::Percent(*pct / 100.0),
244 };
245
246 let to_len_pct_auto = |v: &Val| match v {
247 Val::Auto => tf::LengthPercentageAuto::Auto,
248 Val::Px(px) => tf::LengthPercentageAuto::Length(*px),
249 Val::Percent(pct) => tf::LengthPercentageAuto::Percent(*pct / 100.0),
250 };
251
252 let flex_dir = match style.flex_direction {
253 FlexDirection::Row => tf::FlexDirection::Row,
254 FlexDirection::RowReverse => tf::FlexDirection::RowReverse,
255 FlexDirection::Column => tf::FlexDirection::Column,
256 FlexDirection::ColumnReverse => tf::FlexDirection::ColumnReverse,
257 };
258
259 let justify = match style.justify_content {
260 Align::Start => Some(tf::JustifyContent::Start),
261 Align::Center => Some(tf::JustifyContent::Center),
262 Align::End => Some(tf::JustifyContent::End),
263 Align::SpaceBetween => Some(tf::JustifyContent::SpaceBetween),
264 Align::SpaceAround => Some(tf::JustifyContent::SpaceAround),
265 _ => Some(tf::JustifyContent::Start),
266 };
267
268 let align = match style.align_items {
269 Align::Start => Some(tf::AlignItems::Start),
270 Align::Center => Some(tf::AlignItems::Center),
271 Align::End => Some(tf::AlignItems::End),
272 Align::Stretch => Some(tf::AlignItems::Stretch),
273 _ => Some(tf::AlignItems::Start),
274 };
275
276 let _ = node; tf::Style {
279 display: tf::Display::Flex,
280 flex_direction: flex_dir,
281 justify_content: justify,
282 align_items: align,
283 size: tf::Size {
284 width: to_dim(&style.width),
285 height: to_dim(&style.height),
286 },
287 min_size: tf::Size {
288 width: to_dim(&style.min_width),
289 height: to_dim(&style.min_height),
290 },
291 max_size: tf::Size {
292 width: to_dim(&style.max_width),
293 height: to_dim(&style.max_height),
294 },
295 padding: tf::Rect {
296 top: tf::LengthPercentage::Length(style.padding[0]),
297 right: tf::LengthPercentage::Length(style.padding[1]),
298 bottom: tf::LengthPercentage::Length(style.padding[2]),
299 left: tf::LengthPercentage::Length(style.padding[3]),
300 },
301 margin: tf::Rect {
302 top: to_len_pct_auto(&Val::Px(style.margin[0])),
303 right: to_len_pct_auto(&Val::Px(style.margin[1])),
304 bottom: to_len_pct_auto(&Val::Px(style.margin[2])),
305 left: to_len_pct_auto(&Val::Px(style.margin[3])),
306 },
307 gap: tf::Size {
308 width: tf::LengthPercentage::Length(style.gap),
309 height: tf::LengthPercentage::Length(style.gap),
310 },
311 flex_grow: style.flex_grow,
312 flex_shrink: style.flex_shrink,
313 ..Default::default()
314 }
315 }
316
317 pub fn compute_layout(
321 &mut self,
322 nodes: &[(Entity, &UiNode)],
323 container_width: f32,
324 container_height: f32,
325 ) -> Vec<(Entity, [f32; 4])> {
326 self.taffy = tf::TaffyTree::new();
327 let mut results = Vec::new();
328 let mut children = Vec::new();
329
330 for (entity, node) in nodes {
331 let style = Self::convert_style(&node.style, node);
332 let taffy_node = self.taffy.new_leaf(style).unwrap();
333 children.push((*entity, taffy_node));
334 }
335
336 let child_ids: Vec<_> = children.iter().map(|(_, n)| *n).collect();
338 let root = self.taffy.new_with_children(
339 tf::Style {
340 display: tf::Display::Flex,
341 flex_direction: tf::FlexDirection::Column,
342 size: tf::Size {
343 width: tf::Dimension::Length(container_width),
344 height: tf::Dimension::Length(container_height),
345 },
346 ..Default::default()
347 },
348 &child_ids,
349 ).unwrap();
350
351 let available = tf::Size {
352 width: tf::AvailableSpace::Definite(container_width),
353 height: tf::AvailableSpace::Definite(container_height),
354 };
355 self.taffy.compute_layout(root, available).ok();
356
357 for (entity, taffy_node) in &children {
358 if let Ok(layout) = self.taffy.layout(*taffy_node) {
359 results.push((*entity, [
360 layout.location.x,
361 layout.location.y,
362 layout.size.width,
363 layout.size.height,
364 ]));
365 }
366 }
367
368 results
369 }
370}
371
372impl Default for UiLayoutEngine {
373 fn default() -> Self {
374 Self::new()
375 }
376}
377
378const UI_SHADER: &str = include_str!("../shaders/ui.wgsl");
383
384#[repr(C)]
386#[derive(Copy, Clone, Debug, Pod, Zeroable)]
387pub struct UiVertex {
388 pub position: [f32; 2],
389 pub rect_min: [f32; 2],
390 pub rect_size: [f32; 2],
391 pub color: [f32; 4],
392 pub border_color: [f32; 4],
393 pub params: [f32; 4], }
395
396impl UiVertex {
397 fn layout() -> wgpu::VertexBufferLayout<'static> {
398 const ATTRIBUTES: &[wgpu::VertexAttribute] = &[
399 wgpu::VertexAttribute { offset: 0, shader_location: 0, format: wgpu::VertexFormat::Float32x2 },
400 wgpu::VertexAttribute { offset: 8, shader_location: 1, format: wgpu::VertexFormat::Float32x2 },
401 wgpu::VertexAttribute { offset: 16, shader_location: 2, format: wgpu::VertexFormat::Float32x2 },
402 wgpu::VertexAttribute { offset: 24, shader_location: 3, format: wgpu::VertexFormat::Float32x4 },
403 wgpu::VertexAttribute { offset: 40, shader_location: 4, format: wgpu::VertexFormat::Float32x4 },
404 wgpu::VertexAttribute { offset: 56, shader_location: 5, format: wgpu::VertexFormat::Float32x4 },
405 ];
406 wgpu::VertexBufferLayout {
407 array_stride: std::mem::size_of::<UiVertex>() as u64,
408 step_mode: wgpu::VertexStepMode::Vertex,
409 attributes: ATTRIBUTES,
410 }
411 }
412}
413
414pub struct UiRenderer {
416 pub pipeline: wgpu::RenderPipeline,
417 pub ortho_buffer: wgpu::Buffer,
418 pub ortho_bind_group: wgpu::BindGroup,
419 cached_vb: Option<(wgpu::Buffer, u64)>,
421}
422
423#[repr(C)]
424#[derive(Copy, Clone, Pod, Zeroable)]
425struct UiOrthoUniform {
426 projection: [[f32; 4]; 4],
427}
428
429impl UiRenderer {
430 pub fn new(device: &super::RenderDevice, format: wgpu::TextureFormat) -> Self {
431 let shader = device.device().create_shader_module(wgpu::ShaderModuleDescriptor {
432 label: Some("UI Shader"),
433 source: wgpu::ShaderSource::Wgsl(UI_SHADER.into()),
434 });
435
436 let ortho_bgl = device.device().create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
437 label: Some("UI Ortho BGL"),
438 entries: &[wgpu::BindGroupLayoutEntry {
439 binding: 0,
440 visibility: wgpu::ShaderStages::VERTEX,
441 ty: wgpu::BindingType::Buffer {
442 ty: wgpu::BufferBindingType::Uniform,
443 has_dynamic_offset: false,
444 min_binding_size: None,
445 },
446 count: None,
447 }],
448 });
449
450 let pipeline_layout = device.device().create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
451 label: Some("UI Pipeline Layout"),
452 bind_group_layouts: &[&ortho_bgl],
453 push_constant_ranges: &[],
454 });
455
456 let pipeline = device.device().create_render_pipeline(&wgpu::RenderPipelineDescriptor {
457 label: Some("UI Pipeline"),
458 layout: Some(&pipeline_layout),
459 vertex: wgpu::VertexState {
460 module: &shader,
461 entry_point: "vs_main",
462 buffers: &[UiVertex::layout()],
463 },
464 fragment: Some(wgpu::FragmentState {
465 module: &shader,
466 entry_point: "fs_main",
467 targets: &[Some(wgpu::ColorTargetState {
468 format,
469 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
470 write_mask: wgpu::ColorWrites::ALL,
471 })],
472 }),
473 primitive: wgpu::PrimitiveState {
474 topology: wgpu::PrimitiveTopology::TriangleList,
475 ..Default::default()
476 },
477 depth_stencil: None,
478 multisample: wgpu::MultisampleState::default(),
479 multiview: None,
480 });
481
482 let initial = UiOrthoUniform {
483 projection: glam::Mat4::IDENTITY.to_cols_array_2d(),
484 };
485 let ortho_buffer = device.device().create_buffer_init(&wgpu::util::BufferInitDescriptor {
486 label: Some("UI Ortho UB"),
487 contents: bytemuck::bytes_of(&initial),
488 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
489 });
490
491 let ortho_bg = device.device().create_bind_group(&wgpu::BindGroupDescriptor {
492 label: Some("UI Ortho BG"),
493 layout: &ortho_bgl,
494 entries: &[wgpu::BindGroupEntry {
495 binding: 0,
496 resource: ortho_buffer.as_entire_binding(),
497 }],
498 });
499
500 Self {
501 pipeline,
502 ortho_buffer,
503 ortho_bind_group: ortho_bg,
504 cached_vb: None,
505 }
506 }
507
508 pub fn render(
510 &mut self,
511 device: &super::RenderDevice,
512 encoder: &mut wgpu::CommandEncoder,
513 target: &wgpu::TextureView,
514 nodes: &[&UiNode],
515 screen_width: f32,
516 screen_height: f32,
517 ) {
518 if nodes.is_empty() {
519 return;
520 }
521
522 let ortho = glam::Mat4::orthographic_lh(0.0, screen_width, screen_height, 0.0, -1.0, 1.0);
524 let uniform = UiOrthoUniform {
525 projection: ortho.to_cols_array_2d(),
526 };
527 device.queue().write_buffer(&self.ortho_buffer, 0, bytemuck::bytes_of(&uniform));
528
529 let mut vertices = Vec::new();
531 for node in nodes {
532 if !node.visible || node.computed_rect[2] <= 0.0 || node.computed_rect[3] <= 0.0 {
533 continue;
534 }
535 let [x, y, w, h] = node.computed_rect;
536 let params = [node.border_radius, node.border_width, 0.0, 0.0];
537
538 let corners = [
540 [0.0f32, 0.0], [1.0, 0.0], [1.0, 1.0],
541 [0.0, 0.0], [1.0, 1.0], [0.0, 1.0],
542 ];
543 for corner in &corners {
544 vertices.push(UiVertex {
545 position: *corner,
546 rect_min: [x, y],
547 rect_size: [w, h],
548 color: node.background_color,
549 border_color: node.border_color,
550 params,
551 });
552 }
553 }
554
555 if vertices.is_empty() {
556 return;
557 }
558
559 let data = bytemuck::cast_slice(&vertices);
561 let needed = data.len() as u64;
562 let reuse = self.cached_vb.as_ref().map_or(false, |(_, cap)| *cap >= needed);
563 if !reuse {
564 self.cached_vb = Some((
565 device.device().create_buffer(&wgpu::BufferDescriptor {
566 label: Some("UI VB (cached)"),
567 size: needed,
568 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
569 mapped_at_creation: false,
570 }),
571 needed,
572 ));
573 }
574 let vb = &self.cached_vb.as_ref().unwrap().0;
575 device.queue().write_buffer(vb, 0, data);
576
577 {
578 let mut rp = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
579 label: Some("UI Pass"),
580 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
581 view: target,
582 resolve_target: None,
583 ops: wgpu::Operations {
584 load: wgpu::LoadOp::Load,
585 store: wgpu::StoreOp::Store,
586 },
587 })],
588 depth_stencil_attachment: None,
589 timestamp_writes: None,
590 occlusion_query_set: None,
591 });
592
593 rp.set_pipeline(&self.pipeline);
594 rp.set_bind_group(0, &self.ortho_bind_group, &[]);
595 rp.set_vertex_buffer(0, vb.slice(..));
596 rp.draw(0..vertices.len() as u32, 0..1);
597 }
598 }
599}
600
601#[cfg(test)]
602mod tests {
603 use super::*;
604
605 #[test]
606 fn test_ui_text() {
607 let text = UiText::new("Hello").with_font_size(32.0).with_color([1.0, 0.0, 0.0, 1.0]);
608 assert_eq!(text.content, "Hello");
609 assert_eq!(text.font_size, 32.0);
610 assert_eq!(text.color[0], 1.0);
611 }
612
613 #[test]
614 fn test_ui_node_default() {
615 let node = UiNode::default();
616 assert!(node.visible);
617 assert!(node.text.is_none());
618 assert_eq!(node.background_color, [0.0, 0.0, 0.0, 0.0]);
619 }
620
621 #[test]
622 fn test_val() {
623 let auto = Val::Auto;
624 let px = Val::Px(100.0);
625 let pct = Val::Percent(50.0);
626 assert_ne!(auto, px);
627 assert_ne!(px, pct);
628 }
629}