Skip to main content

anvilkit_render/renderer/
ui.rs

1//! # UI 系统
2//!
3//! 提供保留模式 UI 节点树、Flexbox 布局和文本渲染数据结构。
4//!
5//! ## 核心类型
6//!
7//! - [`UiNode`]: UI 元素组件(矩形、文本、图像)
8//! - [`UiStyle`]: 布局样式(Flexbox 属性)
9//! - [`UiText`]: 文本内容和字体配置
10
11use bevy_ecs::prelude::*;
12use bytemuck::{Pod, Zeroable};
13use wgpu::util::DeviceExt;
14/// Flexbox 排列方向
15///
16/// # 示例
17///
18/// ```rust
19/// use anvilkit_render::renderer::ui::FlexDirection;
20/// assert_ne!(FlexDirection::Row, FlexDirection::Column);
21/// ```
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum FlexDirection {
24    Row,
25    RowReverse,
26    Column,
27    ColumnReverse,
28}
29
30/// Flexbox 对齐
31///
32/// # 示例
33///
34/// ```rust
35/// use anvilkit_render::renderer::ui::Align;
36/// let center = Align::Center;
37/// ```
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum Align {
40    Start,
41    Center,
42    End,
43    Stretch,
44    SpaceBetween,
45    SpaceAround,
46}
47
48/// 尺寸值(像素或百分比)
49///
50/// # 示例
51///
52/// ```rust
53/// use anvilkit_render::renderer::ui::Val;
54/// let px = Val::Px(100.0);
55/// let pct = Val::Percent(50.0);
56/// let auto = Val::Auto;
57/// ```
58#[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/// UI 布局样式
70///
71/// Flexbox 属性集合,控制 UI 元素的布局行为。
72///
73/// # 示例
74///
75/// ```rust
76/// use anvilkit_render::renderer::ui::{UiStyle, FlexDirection, Align, Val};
77///
78/// let style = UiStyle {
79///     flex_direction: FlexDirection::Column,
80///     justify_content: Align::Center,
81///     align_items: Align::Center,
82///     width: Val::Percent(100.0),
83///     height: Val::Px(50.0),
84///     ..Default::default()
85/// };
86/// ```
87#[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],  // top, right, bottom, left
99    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/// UI 文本内容
127///
128/// # 示例
129///
130/// ```rust
131/// use anvilkit_render::renderer::ui::UiText;
132///
133/// let text = UiText::new("Hello, AnvilKit!").with_font_size(24.0);
134/// assert_eq!(text.content, "Hello, AnvilKit!");
135/// assert_eq!(text.font_size, 24.0);
136/// ```
137#[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/// UI 节点组件
167///
168/// 表示 UI 树中的一个元素。可包含背景色、文本或图像。
169///
170/// # 示例
171///
172/// ```rust
173/// use anvilkit_render::renderer::ui::{UiNode, UiText};
174///
175/// let button = UiNode {
176///     background_color: [0.2, 0.4, 0.8, 1.0],
177///     border_radius: 8.0,
178///     text: Some(UiText::new("Click Me")),
179///     visible: true,
180///     ..Default::default()
181/// };
182/// ```
183#[derive(Debug, Clone, Component)]
184pub struct UiNode {
185    /// 背景色 [R, G, B, A]
186    pub background_color: [f32; 4],
187    /// 边框圆角半径
188    pub border_radius: f32,
189    /// 边框宽度
190    pub border_width: f32,
191    /// 边框颜色
192    pub border_color: [f32; 4],
193    /// 文本内容
194    pub text: Option<UiText>,
195    /// 布局样式
196    pub style: UiStyle,
197    /// 是否可见
198    pub visible: bool,
199    /// 计算后的布局矩形(由布局系统填充)
200    pub computed_rect: [f32; 4], // x, y, width, height
201}
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
218// ---------------------------------------------------------------------------
219//  UiLayoutEngine — taffy-based layout computation
220// ---------------------------------------------------------------------------
221
222use taffy::prelude as tf;
223
224/// UI 布局引擎
225///
226/// 将 UiNode 树转换为 taffy 布局树,计算 computed_rect。
227pub 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    /// 将 UiStyle 转换为 taffy Style
239    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; // node is reserved for text sizing hints in the future
277
278        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    /// 计算一组根级 UiNode 的布局
318    ///
319    /// 返回 `(entity, [x, y, width, height])` 列表
320    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        // Root container
337        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
378// ---------------------------------------------------------------------------
379//  UiRenderer — GPU pipeline for UI rectangles
380// ---------------------------------------------------------------------------
381
382const UI_SHADER: &str = include_str!("../shaders/ui.wgsl");
383
384/// UI 矩形 GPU 顶点
385#[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], // border_radius, border_width, 0, 0
394}
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
414/// GPU UI 渲染器
415pub struct UiRenderer {
416    pub pipeline: wgpu::RenderPipeline,
417    pub ortho_buffer: wgpu::Buffer,
418    pub ortho_bind_group: wgpu::BindGroup,
419    /// Cached vertex buffer for per-frame reuse
420    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    /// 从 computed_rect 列表渲染 UI 矩形
509    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        // Update ortho
523        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        // Build vertices
530        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            // 6 vertices (2 triangles)
539            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        // Reuse cached buffer if large enough
560        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}