bevy_megaui/
lib.rs

1#![deny(missing_docs)]
2
3//! This crate provides a [megaui](https://crates.io/crates/megaui) integration for the [Bevy](https://github.com/bevyengine/bevy) game engine.
4//!
5//! `bevy_megaui` depends solely on `megaui` and `bevy` with only `render` feature required.
6//!
7//! ## Trying out
8//!
9//! An example WASM project is live at [mvlabat.github.io/bevy_megaui_web_showcase](https://mvlabat.github.io/bevy_megaui_web_showcase/index.html) [[source](https://github.com/mvlabat/bevy_megaui_web_showcase)].
10//!
11//! **Note** that in order to use `bevy_megaui`in WASM you need [bevy_webgl2](https://github.com/mrk-its/bevy_webgl2) of at least `0.4.1` version.
12//!
13//! ## Usage
14//!
15//! Here's a minimal usage example:
16//!
17//! ```no_run
18//! use bevy::prelude::*;
19//! use bevy_megaui::{
20//!     megaui::{hash, Vector2},
21//!     MegaUiContext, MegaUiPlugin,
22//! };
23//!
24//! fn main() {
25//!     App::build()
26//!         .add_plugins(DefaultPlugins)
27//!         .add_plugin(MegaUiPlugin)
28//!         .add_system(ui_example.system())
29//!         .run();
30//! }
31//!
32//! fn ui_example(_world: &mut World, resources: &mut Resources) {
33//!     let mut ui = resources.get_thread_local_mut::<MegaUiContext>().unwrap();
34//!
35//!     ui.draw_window(
36//!         hash!(),
37//!         Vector2::new(5.0, 5.0),
38//!         Vector2::new(100.0, 50.0),
39//!         None,
40//!         |ui| {
41//!             ui.label(None, "Hello world!");
42//!         },
43//!     );
44//! }
45//! ```
46//!
47//! For a more advanced example, see [examples/ui.rs](examples/ui.rs).
48//!
49//! ```bash
50//! cargo run --example ui --features="bevy/x11 bevy/png bevy/bevy_wgpu"
51//! ```
52//!
53//! ## See also
54//!
55//! - [`bevy_egui`](https://github.com/mvlabat/bevy_egui)
56
57pub use megaui;
58
59mod input;
60mod megaui_node;
61mod transform_node;
62
63use crate::{input::process_input, megaui_node::MegaUiNode, transform_node::MegaUiTransformNode};
64use bevy::{
65    app::{stage, AppBuilder, EventReader, Plugin},
66    asset::{Assets, Handle, HandleUntyped},
67    ecs::IntoSystem,
68    log,
69    reflect::TypeUuid,
70    render::{
71        pipeline::{
72            BlendDescriptor, BlendFactor, BlendOperation, ColorStateDescriptor, ColorWrite,
73            CompareFunction, CullMode, DepthStencilStateDescriptor, FrontFace, IndexFormat,
74            PipelineDescriptor, RasterizationStateDescriptor, StencilStateDescriptor,
75            StencilStateFaceDescriptor,
76        },
77        render_graph::{base, base::Msaa, RenderGraph, WindowSwapChainNode, WindowTextureNode},
78        shader::{Shader, ShaderStage, ShaderStages},
79        texture::{Extent3d, Texture, TextureDimension, TextureFormat},
80    },
81    window::{CursorMoved, ReceivedCharacter},
82};
83use megaui::Vector2;
84use std::collections::HashMap;
85
86/// A handle pointing to the megaui [PipelineDescriptor].
87pub const MEGAUI_PIPELINE_HANDLE: HandleUntyped =
88    HandleUntyped::weak_from_u64(PipelineDescriptor::TYPE_UUID, 9404026720151354217);
89/// Name of the transform uniform.
90pub const MEGAUI_TRANSFORM_RESOURCE_BINDING_NAME: &str = "MegaUiTransform";
91/// Name of the texture uniform.
92pub const MEGAUI_TEXTURE_RESOURCE_BINDING_NAME: &str = "MegaUiTexture_texture";
93
94/// Adds all megaui resources and render graph nodes.
95pub struct MegaUiPlugin;
96
97/// A resource containing global UI settings.
98#[derive(Clone, Debug, PartialEq)]
99pub struct MegaUiSettings {
100    /// Global scale factor for megaui widgets (`1.0` by default).
101    ///
102    /// This setting can be used to force the UI to render in physical pixels regardless of DPI as follows:
103    /// ```rust
104    /// use bevy::prelude::*;
105    /// use bevy_megaui::MegaUiSettings;
106    ///
107    /// fn update_ui_scale_factor(mut megaui_settings: ResMut<MegaUiSettings>, windows: Res<Windows>) {
108    ///     if let Some(window) = windows.get_primary() {
109    ///         megaui_settings.scale_factor = 1.0 / window.scale_factor();
110    ///     }
111    /// }
112    /// ```
113    pub scale_factor: f64,
114}
115
116impl Default for MegaUiSettings {
117    fn default() -> Self {
118        Self { scale_factor: 1.0 }
119    }
120}
121
122/// A resource that is used to store `bevy_megaui` context.
123/// Since [megaui::Ui] doesn't implement [Send] + [Sync], it's accessible only from
124/// thread-local systems.
125pub struct MegaUiContext {
126    /// Megaui context.
127    pub ui: megaui::Ui,
128    ui_draw_lists: Vec<megaui::DrawList>,
129    font_texture: Handle<Texture>,
130    megaui_textures: HashMap<u32, Handle<Texture>>,
131
132    mouse_position: (f32, f32),
133    cursor: EventReader<CursorMoved>,
134    received_character: EventReader<ReceivedCharacter>,
135}
136
137impl MegaUiContext {
138    fn new(ui: megaui::Ui, font_texture: Handle<Texture>) -> Self {
139        Self {
140            ui,
141            ui_draw_lists: Vec::new(),
142            font_texture,
143            megaui_textures: Default::default(),
144            mouse_position: (0.0, 0.0),
145            cursor: Default::default(),
146            received_character: Default::default(),
147        }
148    }
149
150    /// A helper function to draw a megaui window.
151    /// You may as well use [megaui::widgets::Window::new] if you prefer a builder pattern.
152    pub fn draw_window(
153        &mut self,
154        id: megaui::Id,
155        position: Vector2,
156        size: Vector2,
157        params: impl Into<Option<WindowParams>>,
158        f: impl FnOnce(&mut megaui::Ui),
159    ) {
160        let params = params.into();
161
162        megaui::widgets::Window::new(id, position, size)
163            .label(params.as_ref().map_or("", |params| &params.label))
164            .titlebar(params.as_ref().map_or(true, |params| params.titlebar))
165            .movable(params.as_ref().map_or(true, |params| params.movable))
166            .close_button(params.as_ref().map_or(false, |params| params.close_button))
167            .ui(&mut self.ui, f);
168    }
169
170    /// Can accept either a strong or a weak handle.
171    ///
172    /// You may want to pass a weak handle if you control removing texture assets in your
173    /// application manually and you don't want to bother with cleaning up textures in megaui.
174    ///
175    /// You'll want to pass a strong handle if a texture is used only in megaui and there's no
176    /// handle copies stored anywhere else.
177    pub fn set_megaui_texture(&mut self, id: u32, texture: Handle<Texture>) {
178        log::debug!("Set megaui texture: {:?}", texture);
179        self.megaui_textures.insert(id, texture);
180    }
181
182    /// Removes a texture handle associated with the id.
183    pub fn remove_megaui_texture(&mut self, id: u32) {
184        let texture_handle = self.megaui_textures.remove(&id);
185        log::debug!("Remove megaui texture: {:?}", texture_handle);
186    }
187
188    // Is called when we get an event that a texture asset is removed.
189    fn remove_texture(&mut self, texture_handle: &Handle<Texture>) {
190        log::debug!("Removing megaui handles: {:?}", texture_handle);
191        self.megaui_textures = self
192            .megaui_textures
193            .iter()
194            .map(|(id, texture)| (*id, texture.clone()))
195            .filter(|(_, texture)| texture != texture_handle)
196            .collect();
197    }
198}
199
200/// Params that are used for defining a window with [MegaUiContext::draw_window].
201#[derive(Debug, Clone, Eq, PartialEq)]
202pub struct WindowParams {
203    /// Window label (empty by default).
204    pub label: String,
205    /// Defines whether a window is movable (`true` by default).
206    pub movable: bool,
207    /// Defines whether a window is closable (`false` by default).
208    pub close_button: bool,
209    /// Defines whether a window has a titlebar (`true` by default).
210    pub titlebar: bool,
211}
212
213impl Default for WindowParams {
214    fn default() -> WindowParams {
215        WindowParams {
216            label: "".to_string(),
217            movable: true,
218            close_button: false,
219            titlebar: true,
220        }
221    }
222}
223
224#[derive(Debug, Default, Clone, PartialEq)]
225struct WindowSize {
226    physical_width: f32,
227    physical_height: f32,
228    scale_factor: f32,
229}
230
231impl WindowSize {
232    fn new(physical_width: f32, physical_height: f32, scale_factor: f32) -> Self {
233        Self {
234            physical_width,
235            physical_height,
236            scale_factor,
237        }
238    }
239
240    #[inline]
241    fn width(&self) -> f32 {
242        self.physical_width / self.scale_factor
243    }
244
245    #[inline]
246    fn height(&self) -> f32 {
247        self.physical_height / self.scale_factor
248    }
249}
250
251impl MegaUiContext {
252    fn render_draw_lists(&mut self) {
253        self.ui_draw_lists.clear();
254        self.ui.render(&mut self.ui_draw_lists);
255    }
256}
257
258/// The names of `bevy_megaui` nodes.
259pub mod node {
260    /// The main megaui pass.
261    pub const MEGAUI_PASS: &str = "megaui_pass";
262    /// Keeps the transform uniform up to date.
263    pub const MEGAUI_TRANSFORM: &str = "megaui_transform";
264}
265
266impl Plugin for MegaUiPlugin {
267    fn build(&self, app: &mut AppBuilder) {
268        app.add_system_to_stage(stage::PRE_UPDATE, process_input.system());
269
270        let resources = app.resources_mut();
271
272        let ui = megaui::Ui::new();
273        let font_texture = {
274            let mut assets = resources.get_mut::<Assets<Texture>>().unwrap();
275            assets.add(Texture::new(
276                Extent3d::new(ui.font_atlas.texture.width, ui.font_atlas.texture.height, 1),
277                TextureDimension::D2,
278                ui.font_atlas.texture.data.clone(),
279                TextureFormat::Rgba8Unorm,
280            ))
281        };
282        resources.get_or_insert_with(MegaUiSettings::default);
283        resources.insert(WindowSize::new(0.0, 0.0, 0.0));
284        resources.insert_thread_local(MegaUiContext::new(ui, font_texture.clone()));
285
286        let mut pipelines = resources.get_mut::<Assets<PipelineDescriptor>>().unwrap();
287        let mut shaders = resources.get_mut::<Assets<Shader>>().unwrap();
288        let msaa = resources.get::<Msaa>().unwrap();
289
290        pipelines.set_untracked(
291            MEGAUI_PIPELINE_HANDLE,
292            build_megaui_pipeline(&mut shaders, msaa.samples),
293        );
294        let mut render_graph = resources.get_mut::<RenderGraph>().unwrap();
295
296        render_graph.add_node(node::MEGAUI_PASS, MegaUiNode::new(&msaa, font_texture));
297        render_graph
298            .add_node_edge(base::node::MAIN_PASS, node::MEGAUI_PASS)
299            .unwrap();
300
301        render_graph
302            .add_slot_edge(
303                base::node::PRIMARY_SWAP_CHAIN,
304                WindowSwapChainNode::OUT_TEXTURE,
305                node::MEGAUI_PASS,
306                if msaa.samples > 1 {
307                    "color_resolve_target"
308                } else {
309                    "color_attachment"
310                },
311            )
312            .unwrap();
313
314        render_graph
315            .add_slot_edge(
316                base::node::MAIN_DEPTH_TEXTURE,
317                WindowTextureNode::OUT_TEXTURE,
318                node::MEGAUI_PASS,
319                "depth",
320            )
321            .unwrap();
322
323        if msaa.samples > 1 {
324            render_graph
325                .add_slot_edge(
326                    base::node::MAIN_SAMPLED_COLOR_ATTACHMENT,
327                    WindowSwapChainNode::OUT_TEXTURE,
328                    node::MEGAUI_PASS,
329                    "color_attachment",
330                )
331                .unwrap();
332        }
333
334        // Transform.
335        render_graph.add_system_node(node::MEGAUI_TRANSFORM, MegaUiTransformNode::new());
336        render_graph
337            .add_node_edge(node::MEGAUI_TRANSFORM, node::MEGAUI_PASS)
338            .unwrap();
339    }
340}
341
342fn build_megaui_pipeline(shaders: &mut Assets<Shader>, sample_count: u32) -> PipelineDescriptor {
343    PipelineDescriptor {
344        rasterization_state: Some(RasterizationStateDescriptor {
345            front_face: FrontFace::Cw,
346            cull_mode: CullMode::None,
347            depth_bias: 0,
348            depth_bias_slope_scale: 0.0,
349            depth_bias_clamp: 0.0,
350            clamp_depth: false,
351        }),
352        depth_stencil_state: Some(DepthStencilStateDescriptor {
353            format: TextureFormat::Depth32Float,
354            depth_write_enabled: true,
355            depth_compare: CompareFunction::LessEqual,
356            stencil: StencilStateDescriptor {
357                front: StencilStateFaceDescriptor::IGNORE,
358                back: StencilStateFaceDescriptor::IGNORE,
359                read_mask: 0,
360                write_mask: 0,
361            },
362        }),
363        color_states: vec![ColorStateDescriptor {
364            format: TextureFormat::default(),
365            color_blend: BlendDescriptor {
366                src_factor: BlendFactor::SrcAlpha,
367                dst_factor: BlendFactor::OneMinusSrcAlpha,
368                operation: BlendOperation::Add,
369            },
370            alpha_blend: BlendDescriptor {
371                src_factor: BlendFactor::OneMinusDstAlpha,
372                dst_factor: BlendFactor::One,
373                operation: BlendOperation::Add,
374            },
375            write_mask: ColorWrite::ALL,
376        }],
377        index_format: IndexFormat::Uint16,
378        sample_count,
379        ..PipelineDescriptor::new(ShaderStages {
380            vertex: shaders.add(Shader::from_glsl(
381                ShaderStage::Vertex,
382                if cfg!(target_arch = "wasm32") {
383                    include_str!("megaui.es.vert")
384                } else {
385                    include_str!("megaui.vert")
386                },
387            )),
388            fragment: Some(shaders.add(Shader::from_glsl(
389                ShaderStage::Fragment,
390                if cfg!(target_arch = "wasm32") {
391                    include_str!("megaui.es.frag")
392                } else {
393                    include_str!("megaui.frag")
394                },
395            ))),
396        })
397    }
398}