forte_engine/ui/
mod.rs

1use cgmath::{Quaternion, Vector2, Vector3, Zero};
2use glyphon::*;
3use wgpu::MultisampleState;
4
5use crate::{component_app::EngineComponent, create_pipeline, math::{quaternion::QuaternionExt, transforms::Transform}, primitives::{mesh::Mesh, textures::Texture, transforms::TransformRaw, vertices::Vertex}, render::{pipelines::Pipeline, render_engine::RenderEngine}, utils::resources::Handle};
6
7use self::{elements::{ElementInfo, UIElement}, style::PositionSetting, uniforms::UIInstance};
8
9pub mod elements;
10pub mod uniforms;
11pub mod style;
12
13/// The vertices of a rectangle.
14const VERTICES: &[Vertex] = &[
15    Vertex { position: [ -1.0, -1.0, 0.0 ], tex_coords: [ 0.0, 1.0 ], normal: [0.0, 0.0, 0.0] },
16    Vertex { position: [  1.0, -1.0, 0.0 ], tex_coords: [ 1.0, 1.0 ], normal: [0.0, 0.0, 0.0] },
17    Vertex { position: [ -1.0,  1.0, 0.0 ], tex_coords: [ 0.0, 0.0 ], normal: [0.0, 0.0, 0.0] },
18    Vertex { position: [  1.0,  1.0, 0.0 ], tex_coords: [ 1.0, 0.0 ], normal: [0.0, 0.0, 0.0] }
19];
20
21/// The indices of a rectangle.
22const INDICES: &[u16] = &[
23    0, 1, 2,
24    1, 3, 2
25];
26
27// The engine for rendering UI.
28pub struct UIEngine {
29    mesh: Handle<Mesh>,
30    default_texture: Handle<Texture>,
31    pub elements: Vec<UIElement>,
32
33    // text
34    font_system: FontSystem,
35    font_cache: SwashCache,
36    text_atlas: TextAtlas,
37    text_renderer: TextRenderer
38}
39
40// Some info used for rendering
41#[derive(Debug)]
42pub struct UIRenderInfo {
43    pub position: Vector2<f32>,
44    pub size: Vector2<f32>,
45    pub display_size: Vector2<f32>
46}
47
48/// The UI shader.
49#[include_wgsl_oil::include_wgsl_oil("ui.wgsl")]
50mod ui_shader {}
51
52impl EngineComponent<&mut RenderEngine> for UIEngine {
53    fn create(engine: &mut RenderEngine) -> Self {
54        create_pipeline! {
55            NAME => "forte.ui",
56            ENGINE => engine,
57            SHADER => ui_shader::SOURCE,
58            BUFFER_LAYOUTS => [Vertex::desc(), UIInstance::desc()],
59            BIND_GROUPS => [Texture::BIND_LAYOUT],
60            HAS_DEPTH => false
61        }
62
63        let mesh = engine.create_mesh("ui_engine_mesh", VERTICES, INDICES);
64        let default_texture = engine.create_texture("ui.blank", include_bytes!("empty.png"));
65
66        // setup text resources
67        let font_system = FontSystem::new();
68        let font_cache = SwashCache::new();
69        let mut text_atlas = TextAtlas::new(&engine.device, &engine.queue, engine.config.format);
70        let text_renderer = TextRenderer::new(&mut text_atlas, &engine.device, MultisampleState::default(), None);
71        
72        Self { 
73            mesh, 
74            default_texture, elements: Vec::new(), 
75            font_system, font_cache, 
76            text_atlas, text_renderer
77        }
78    }
79
80    fn update(&mut self, render_engine: &mut RenderEngine) {
81        let size = Vector2 { x: render_engine.size.width as f32, y: render_engine.size.height as f32 };
82        let mut text_areas = Vec::<TextArea>::new();
83        update_ui(render_engine, &UIRenderInfo { position: Vector2::zero(), size, display_size: size }, &self.elements, &mut text_areas, 0.5);
84        let _ = self.text_renderer.prepare(
85            &render_engine.device,
86            &render_engine.queue,
87            &mut self.font_system,
88            &mut self.text_atlas,
89            Resolution { width: render_engine.config.width, height: render_engine.config.height },
90            text_areas,
91            &mut self.font_cache
92        );
93    }
94
95    fn render<'rpass>(&'rpass mut self, render_engine: &'rpass RenderEngine, pass: &mut wgpu::RenderPass<'rpass>) {
96        render_ui(render_engine, pass, render_engine.mesh(&self.mesh), render_engine.texture(&self.default_texture), &self.elements);
97        let _ = self.text_renderer.render(&self.text_atlas, pass);
98    }
99
100    fn start(&mut self, _: &mut RenderEngine) {}
101    fn exit(&mut self, _: &mut RenderEngine) {}
102}
103
104fn render_ui<'rpass>(engine: &'rpass RenderEngine, pass: &mut wgpu::RenderPass<'rpass>, mesh: &'rpass Mesh, default_texture: &'rpass Texture, elements: &'rpass [UIElement]) {
105    elements.iter().for_each(|element| {
106        let texture = match &element.info {
107            ElementInfo::Image(texture) => engine.texture(texture),
108            _ => default_texture
109        };
110        pass.set_bind_group(0, &texture.bind_group, &[]);
111        pass.set_vertex_buffer(0, mesh.vertex_buf.slice(..));
112        pass.set_vertex_buffer(1, element.buffer.slice(..));
113        pass.set_index_buffer(mesh.index_buf.slice(..), wgpu::IndexFormat::Uint16);
114        pass.draw_indexed(0 .. mesh.num_indices, 0, 0 .. 1);
115
116        render_ui(engine, pass, mesh, default_texture, &element.children);
117    });
118}
119
120fn update_ui<'a>(engine: &RenderEngine, info: &UIRenderInfo, elements: &'a [UIElement], text_areas: &mut Vec<TextArea<'a>>, layer: f32) {
121    elements.iter().for_each(|element| {
122        // calculate size and position of this element
123        let (position, size) = calculate_position_size(element, info);
124        let new_info = UIRenderInfo { position, size, display_size: info.display_size };
125
126        // generate transform of UI
127        let pos_x = size.x * 0.5 + position.x;
128        let pos_y = size.y * 0.5 + position.y;
129        let transform = Transform {
130            position: Vector3 { 
131                x: 2.0 * (pos_x / info.display_size.x) - 1.0,
132                y: 2.0 * (pos_y / info.display_size.y) - 1.0,
133                z: layer
134            },
135            rotation: Quaternion::euler_deg_z(element.style.rotation),
136            scale: Vector3 {
137                x: size.x / info.display_size.x,
138                y: size.y / info.display_size.y,
139                z: 0.0
140            }
141        };
142
143        // create instance
144        let raw_transform = TransformRaw::from_generic(&transform).model;
145        let instance = UIInstance([
146            raw_transform[0],
147            raw_transform[1],
148            raw_transform[2],
149            raw_transform[3],
150            element.style.color.to_array(),
151            element.style.border_color.to_array(),
152            [
153                element.style.round.size(&info.display_size) / f32::max(size.x, size.y),
154                element.style.border.size(&info.display_size) / f32::max(size.x, size.y),
155                0.0,
156                0.0
157            ]
158        ]);
159
160        // save instance info
161        engine.queue.write_buffer(&element.buffer, 0, bytemuck::cast_slice(&instance.0));
162
163        // if text, add text area
164        match &element.info {
165            ElementInfo::Text(buffer, color) => {
166                text_areas.push(TextArea {
167                    buffer,
168                    left: position.x + 5.0,
169                    top: info.display_size.y - position.y - size.y,
170                    scale: 1.0,
171                    bounds: TextBounds {
172                        left: 0,
173                        top: 0,
174                        right: size.x as i32,
175                        bottom: size.y as i32,
176                    },
177                    default_color: *color
178                });
179            },
180            _ => {}
181        }
182
183        // update children
184        update_ui(engine, &new_info, &element.children, text_areas, layer - 0.05);
185    });
186}
187
188// calculates the position and size of the given element by taking in its own node and some render info about its parent and display size
189fn calculate_position_size(element: &UIElement, info: &UIRenderInfo) -> (Vector2<f32>, Vector2<f32>) {
190    // calculate my size
191    let size = element.min_size(&info.display_size);
192
193    // calculate initial position
194    let mut position =  Vector2 { 
195        x: info.position.x + ((info.size.x - size.x) * 0.5), 
196        y: info.position.y + ((info.size.y - size.y) * 0.5)
197    };
198
199    // if left positioning given, position based on above info, an offset, and the positioning type
200    if element.style.left_set() {
201        let offset = element.style.left.size(&info.display_size);
202        match element.style.position_setting {
203            PositionSetting::Parent => {
204                position.x = info.position.x + offset;
205            },
206            PositionSetting::Absolute => {
207                position.x = offset;
208            }
209        }
210    } 
211    // otherwise, do the same for the right
212    else if element.style.right_set() {
213        let offset = element.style.right.size(&info.display_size);
214        match element.style.position_setting {
215            PositionSetting::Parent => {
216                position.x = info.position.x + info.size.x - size.x - offset;
217            },
218            PositionSetting::Absolute => {
219                position.x = info.display_size.x - size.x - offset;
220            }
221        }
222    }
223
224    // do top bottom positioning
225    if element.style.top_set() {
226        let offset = element.style.top.size(&info.display_size);
227        match element.style.position_setting {
228            PositionSetting::Parent => {
229                position.y = info.position.y + info.size.y - size.y - offset;
230            },
231            PositionSetting::Absolute => {
232                position.y = info.display_size.y - size.y - offset;
233            }
234        }
235    } else if element.style.bottom_set() {
236        let offset = element.style.bottom.size(&info.display_size);
237        position.y = offset;
238        match element.style.position_setting {
239            PositionSetting::Parent => {
240                position.y = info.position.y + offset;
241            },
242            PositionSetting::Absolute => {
243                position.y = offset;
244            }
245        }
246    }
247
248    (position, size)
249}