RenderCommand for SetMatUiViewBindGroup {
+ type Param = SRes;
+ type ViewWorldQuery = Read;
+ type ItemWorldQuery = ();
+
+ fn render<'w>(
+ _item: &P,
+ view_uniform: &'w ViewUniformOffset,
+ _entity: (),
+ ui_meta: SystemParamItem<'w, '_, Self::Param>,
+ pass: &mut TrackedRenderPass<'w>,
+ ) -> RenderCommandResult {
+ pass.set_bind_group(
+ I,
+ ui_meta.into_inner().view_bind_group.as_ref().unwrap(),
+ &[view_uniform.offset],
+ );
+ RenderCommandResult::Success
+ }
+}
+
+pub struct SetUiMaterialBindGroup(PhantomData);
+impl RenderCommand
+ for SetUiMaterialBindGroup
+{
+ type Param = SRes>;
+ type ViewWorldQuery = ();
+ type ItemWorldQuery = Read>;
+
+ fn render<'w>(
+ _item: &P,
+ _view: (),
+ material_handle: ROQueryItem<'_, Self::ItemWorldQuery>,
+ materials: SystemParamItem<'w, '_, Self::Param>,
+ pass: &mut TrackedRenderPass<'w>,
+ ) -> RenderCommandResult {
+ let material = materials
+ .into_inner()
+ .get(&material_handle.material)
+ .unwrap();
+ pass.set_bind_group(I, &material.bind_group, &[]);
+ RenderCommandResult::Success
+ }
+}
+
+pub struct DrawUiMaterialNode(PhantomData);
+impl RenderCommand for DrawUiMaterialNode {
+ type Param = SRes;
+ type ViewWorldQuery = ();
+ type ItemWorldQuery = Read>;
+
+ #[inline]
+ fn render<'w>(
+ _item: &P,
+ _view: (),
+ batch: &'w UiMaterialBatch,
+ ui_meta: SystemParamItem<'w, '_, Self::Param>,
+ pass: &mut TrackedRenderPass<'w>,
+ ) -> RenderCommandResult {
+ pass.set_vertex_buffer(0, ui_meta.into_inner().vertices.buffer().unwrap().slice(..));
+ pass.draw(batch.range.clone(), 0..1);
+ RenderCommandResult::Success
+ }
+}
+
+pub struct ExtractedUiMaterialNode {
+ pub stack_index: usize,
+ pub transform: Mat4,
+ pub rect: Rect,
+ pub border: [f32; 4],
+ pub material: AssetId,
+ pub clip: Option,
+}
+
+#[derive(Resource)]
+pub struct ExtractedUiMaterialNodes {
+ pub uinodes: SparseSet>,
+}
+
+impl Default for ExtractedUiMaterialNodes {
+ fn default() -> Self {
+ Self {
+ uinodes: Default::default(),
+ }
+ }
+}
+
+pub fn extract_ui_material_nodes(
+ mut extracted_uinodes: ResMut>,
+ materials: Extract>>,
+ ui_stack: Extract>,
+ uinode_query: Extract<
+ Query<(
+ Entity,
+ &Node,
+ &Style,
+ &GlobalTransform,
+ &Handle,
+ &ViewVisibility,
+ Option<&CalculatedClip>,
+ )>,
+ >,
+ windows: Extract>>,
+ ui_scale: Extract>,
+) {
+ let ui_logical_viewport_size = windows
+ .get_single()
+ .map(|window| Vec2::new(window.resolution.width(), window.resolution.height()))
+ .unwrap_or(Vec2::ZERO)
+ // The logical window resolution returned by `Window` only takes into account the window scale factor and not `UiScale`,
+ // so we have to divide by `UiScale` to get the size of the UI viewport.
+ / ui_scale.0 as f32;
+ for (stack_index, entity) in ui_stack.uinodes.iter().enumerate() {
+ if let Ok((entity, uinode, style, transform, handle, view_visibility, clip)) =
+ uinode_query.get(*entity)
+ {
+ // skip invisible nodes
+ if !view_visibility.get() {
+ continue;
+ }
+
+ // Skip loading materials
+ if !materials.contains(handle) {
+ continue;
+ }
+
+ // Both vertical and horizontal percentage border values are calculated based on the width of the parent node
+ //
+ let parent_width = uinode.size().x;
+ let left =
+ resolve_border_thickness(style.border.left, parent_width, ui_logical_viewport_size)
+ / uinode.size().x;
+ let right = resolve_border_thickness(
+ style.border.right,
+ parent_width,
+ ui_logical_viewport_size,
+ ) / uinode.size().y;
+ let top =
+ resolve_border_thickness(style.border.top, parent_width, ui_logical_viewport_size)
+ / uinode.size().y;
+ let bottom = resolve_border_thickness(
+ style.border.bottom,
+ parent_width,
+ ui_logical_viewport_size,
+ ) / uinode.size().y;
+
+ extracted_uinodes.uinodes.insert(
+ entity,
+ ExtractedUiMaterialNode {
+ stack_index,
+ transform: transform.compute_matrix(),
+ material: handle.id(),
+ rect: Rect {
+ min: Vec2::ZERO,
+ max: uinode.calculated_size,
+ },
+ border: [left, right, top, bottom],
+ clip: clip.map(|clip| clip.clip),
+ },
+ );
+ };
+ }
+}
+
+#[allow(clippy::too_many_arguments)]
+pub fn prepare_uimaterial_nodes(
+ mut commands: Commands,
+ render_device: Res,
+ render_queue: Res,
+ mut ui_meta: ResMut,
+ mut extracted_uinodes: ResMut>,
+ view_uniforms: Res,
+ ui_material_pipeline: Res>,
+ mut phases: Query<&mut RenderPhase>,
+ mut previous_len: Local,
+) {
+ if let Some(view_binding) = view_uniforms.uniforms.binding() {
+ let mut batches: Vec<(Entity, UiMaterialBatch)> = Vec::with_capacity(*previous_len);
+
+ ui_meta.vertices.clear();
+ ui_meta.view_bind_group = Some(render_device.create_bind_group(
+ "ui_material_view_bind_group",
+ &ui_material_pipeline.view_layout,
+ &BindGroupEntries::single(view_binding),
+ ));
+ let mut index = 0;
+
+ for mut ui_phase in &mut phases {
+ let mut batch_item_index = 0;
+ let mut batch_shader_handle = AssetId::invalid();
+
+ for item_index in 0..ui_phase.items.len() {
+ let item = &mut ui_phase.items[item_index];
+ if let Some(extracted_uinode) = extracted_uinodes.uinodes.get(item.entity) {
+ let mut existing_batch = batches
+ .last_mut()
+ .filter(|_| batch_shader_handle == extracted_uinode.material);
+
+ if existing_batch.is_none() {
+ batch_item_index = item_index;
+ batch_shader_handle = extracted_uinode.material;
+
+ let new_batch = UiMaterialBatch {
+ range: index..index,
+ material: extracted_uinode.material,
+ };
+
+ batches.push((item.entity, new_batch));
+
+ existing_batch = batches.last_mut();
+ }
+
+ let uinode_rect = extracted_uinode.rect;
+
+ let rect_size = uinode_rect.size().extend(1.0);
+
+ let positions = QUAD_VERTEX_POSITIONS.map(|pos| {
+ (extracted_uinode.transform * (pos * rect_size).extend(1.0)).xyz()
+ });
+
+ let positions_diff = if let Some(clip) = extracted_uinode.clip {
+ [
+ Vec2::new(
+ f32::max(clip.min.x - positions[0].x, 0.),
+ f32::max(clip.min.y - positions[0].y, 0.),
+ ),
+ Vec2::new(
+ f32::min(clip.max.x - positions[1].x, 0.),
+ f32::max(clip.min.y - positions[1].y, 0.),
+ ),
+ Vec2::new(
+ f32::min(clip.max.x - positions[2].x, 0.),
+ f32::min(clip.max.y - positions[2].y, 0.),
+ ),
+ Vec2::new(
+ f32::max(clip.min.x - positions[3].x, 0.),
+ f32::min(clip.max.y - positions[3].y, 0.),
+ ),
+ ]
+ } else {
+ [Vec2::ZERO; 4]
+ };
+
+ let positions_clipped = [
+ positions[0] + positions_diff[0].extend(0.),
+ positions[1] + positions_diff[1].extend(0.),
+ positions[2] + positions_diff[2].extend(0.),
+ positions[3] + positions_diff[3].extend(0.),
+ ];
+
+ let transformed_rect_size =
+ extracted_uinode.transform.transform_vector3(rect_size);
+
+ // Don't try to cull nodes that have a rotation
+ // In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π
+ // In those two cases, the culling check can proceed normally as corners will be on
+ // horizontal / vertical lines
+ // For all other angles, bypass the culling check
+ // This does not properly handles all rotations on all axis
+ if extracted_uinode.transform.x_axis[1] == 0.0 {
+ // Cull nodes that are completely clipped
+ if positions_diff[0].x - positions_diff[1].x >= transformed_rect_size.x
+ || positions_diff[1].y - positions_diff[2].y >= transformed_rect_size.y
+ {
+ continue;
+ }
+ }
+ let uvs = [
+ Vec2::new(
+ uinode_rect.min.x + positions_diff[0].x,
+ uinode_rect.min.y + positions_diff[0].y,
+ ),
+ Vec2::new(
+ uinode_rect.max.x + positions_diff[1].x,
+ uinode_rect.min.y + positions_diff[1].y,
+ ),
+ Vec2::new(
+ uinode_rect.max.x + positions_diff[2].x,
+ uinode_rect.max.y + positions_diff[2].y,
+ ),
+ Vec2::new(
+ uinode_rect.min.x + positions_diff[3].x,
+ uinode_rect.max.y + positions_diff[3].y,
+ ),
+ ]
+ .map(|pos| pos / uinode_rect.max);
+
+ for i in QUAD_INDICES {
+ ui_meta.vertices.push(UiMaterialVertex {
+ position: positions_clipped[i].into(),
+ uv: uvs[i].into(),
+ border_widths: extracted_uinode.border,
+ });
+ }
+
+ index += QUAD_INDICES.len() as u32;
+ existing_batch.unwrap().1.range.end = index;
+ ui_phase.items[batch_item_index].batch_range_mut().end += 1;
+ } else {
+ batch_shader_handle = AssetId::invalid();
+ }
+ }
+ }
+ ui_meta.vertices.write_buffer(&render_device, &render_queue);
+ *previous_len = batches.len();
+ commands.insert_or_spawn_batch(batches);
+ }
+ extracted_uinodes.uinodes.clear();
+}
+
+#[derive(Resource, Deref, DerefMut)]
+pub struct RenderUiMaterials(HashMap, PreparedUiMaterial>);
+
+impl Default for RenderUiMaterials {
+ fn default() -> Self {
+ Self(Default::default())
+ }
+}
+
+pub struct PreparedUiMaterial {
+ pub bindings: Vec<(u32, OwnedBindingResource)>,
+ pub bind_group: BindGroup,
+ pub key: T::Data,
+}
+
+#[derive(Resource)]
+pub struct ExtractedUiMaterials {
+ extracted: Vec<(AssetId, M)>,
+ removed: Vec>,
+}
+
+impl Default for ExtractedUiMaterials {
+ fn default() -> Self {
+ Self {
+ extracted: Default::default(),
+ removed: Default::default(),
+ }
+ }
+}
+
+pub fn extract_ui_materials(
+ mut commands: Commands,
+ mut events: Extract>>,
+ assets: Extract>>,
+) {
+ let mut changed_assets = HashSet::default();
+ let mut removed = Vec::new();
+ for event in events.read() {
+ match event {
+ AssetEvent::Added { id } | AssetEvent::Modified { id } => {
+ changed_assets.insert(*id);
+ }
+ AssetEvent::Removed { id } => {
+ changed_assets.remove(id);
+ removed.push(*id);
+ }
+ AssetEvent::LoadedWithDependencies { .. } => {
+ // not implemented
+ }
+ }
+ }
+
+ let mut extracted_assets = Vec::new();
+ for id in changed_assets.drain() {
+ if let Some(asset) = assets.get(id) {
+ extracted_assets.push((id, asset.clone()));
+ }
+ }
+
+ commands.insert_resource(ExtractedUiMaterials {
+ extracted: extracted_assets,
+ removed,
+ });
+}
+
+pub struct PrepareNextFrameMaterials {
+ assets: Vec<(AssetId, M)>,
+}
+
+impl Default for PrepareNextFrameMaterials {
+ fn default() -> Self {
+ Self {
+ assets: Default::default(),
+ }
+ }
+}
+
+pub fn prepare_ui_materials(
+ mut prepare_next_frame: Local>,
+ mut extracted_assets: ResMut>,
+ mut render_materials: ResMut>,
+ render_device: Res,
+ images: Res>,
+ fallback_image: Res,
+ pipeline: Res>,
+) {
+ let queued_assets = std::mem::take(&mut prepare_next_frame.assets);
+ for (id, material) in queued_assets {
+ match prepare_ui_material(
+ &material,
+ &render_device,
+ &images,
+ &fallback_image,
+ &pipeline,
+ ) {
+ Ok(prepared_asset) => {
+ render_materials.insert(id, prepared_asset);
+ }
+ Err(AsBindGroupError::RetryNextUpdate) => {
+ prepare_next_frame.assets.push((id, material));
+ }
+ }
+ }
+
+ for removed in std::mem::take(&mut extracted_assets.removed) {
+ render_materials.remove(&removed);
+ }
+
+ for (handle, material) in std::mem::take(&mut extracted_assets.extracted) {
+ match prepare_ui_material(
+ &material,
+ &render_device,
+ &images,
+ &fallback_image,
+ &pipeline,
+ ) {
+ Ok(prepared_asset) => {
+ render_materials.insert(handle, prepared_asset);
+ }
+ Err(AsBindGroupError::RetryNextUpdate) => {
+ prepare_next_frame.assets.push((handle, material));
+ }
+ }
+ }
+}
+
+fn prepare_ui_material(
+ material: &M,
+ render_device: &RenderDevice,
+ images: &RenderAssets,
+ fallback_image: &Res,
+ pipeline: &UiMaterialPipeline,
+) -> Result, AsBindGroupError> {
+ let prepared =
+ material.as_bind_group(&pipeline.ui_layout, render_device, images, fallback_image)?;
+ Ok(PreparedUiMaterial {
+ bindings: prepared.bindings,
+ bind_group: prepared.bind_group,
+ key: prepared.data,
+ })
+}
+
+#[allow(clippy::too_many_arguments)]
+pub fn queue_ui_material_nodes(
+ extracted_uinodes: Res>,
+ draw_functions: Res>,
+ ui_material_pipeline: Res>,
+ mut pipelines: ResMut>>,
+ pipeline_cache: Res,
+ render_materials: Res>,
+ mut views: Query<(&ExtractedView, &mut RenderPhase)>,
+) where
+ M::Data: PartialEq + Eq + Hash + Clone,
+{
+ let draw_function = draw_functions.read().id::>();
+
+ for (entity, extracted_uinode) in extracted_uinodes.uinodes.iter() {
+ let material = render_materials.get(&extracted_uinode.material).unwrap();
+ for (view, mut transparent_phase) in &mut views {
+ let pipeline = pipelines.specialize(
+ &pipeline_cache,
+ &ui_material_pipeline,
+ UiMaterialKey {
+ hdr: view.hdr,
+ bind_group_data: material.key.clone(),
+ },
+ );
+ transparent_phase
+ .items
+ .reserve(extracted_uinodes.uinodes.len());
+ transparent_phase.add(TransparentUi {
+ draw_function,
+ pipeline,
+ entity: *entity,
+ sort_key: (
+ FloatOrd(extracted_uinode.stack_index as f32),
+ entity.index(),
+ ),
+ batch_range: 0..0,
+ dynamic_offset: None,
+ });
+ }
+ }
+}
diff --git a/crates/bevy_ui/src/render/ui_vertex_output.wgsl b/crates/bevy_ui/src/render/ui_vertex_output.wgsl
new file mode 100644
index 00000000000000..de41c52819c64a
--- /dev/null
+++ b/crates/bevy_ui/src/render/ui_vertex_output.wgsl
@@ -0,0 +1,9 @@
+#define_import_path bevy_ui::ui_vertex_output
+
+// The Vertex output of the default vertex shader for the Ui Material pipeline.
+struct UiVertexOutput {
+ @location(0) uv: vec2,
+ // The size of the borders in UV space. Order is Left, Right, Top, Bottom.
+ @location(1) border_widths: vec4,
+ @builtin(position) position: vec4,
+};
diff --git a/crates/bevy_ui/src/ui_material.rs b/crates/bevy_ui/src/ui_material.rs
new file mode 100644
index 00000000000000..680d4aa4100e6c
--- /dev/null
+++ b/crates/bevy_ui/src/ui_material.rs
@@ -0,0 +1,145 @@
+use std::hash::Hash;
+
+use bevy_asset::Asset;
+use bevy_render::render_resource::{AsBindGroup, RenderPipelineDescriptor, ShaderRef};
+
+/// Materials are used alongside [`UiMaterialPlugin`](crate::UiMaterialPipeline) and [`MaterialNodeBundle`](crate::prelude::MaterialNodeBundle)
+/// to spawn entities that are rendered with a specific [`UiMaterial`] type. They serve as an easy to use high level
+/// way to render `Node` entities with custom shader logic.
+///
+/// `UiMaterials` must implement [`AsBindGroup`] to define how data will be transferred to the GPU and bound in shaders.
+/// [`AsBindGroup`] can be derived, which makes generating bindings straightforward. See the [`AsBindGroup`] docs for details.
+///
+/// Materials must also implement [`Asset`] so they can be treated as such.
+///
+/// If you are only using the fragment shader, make sure your shader imports the `UiVertexOutput`
+/// from `bevy_ui::ui_vertex_output` and uses it as the input of your fragment shader like the
+/// example below does.
+///
+/// # Example
+///
+/// Here is a simple [`UiMaterial`] implementation. The [`AsBindGroup`] derive has many features. To see what else is available,
+/// check out the [`AsBindGroup`] documentation.
+/// ```
+/// # use bevy_ui::prelude::*;
+/// # use bevy_ecs::prelude::*;
+/// # use bevy_reflect::TypePath;
+/// # use bevy_render::{render_resource::{AsBindGroup, ShaderRef}, texture::Image, color::Color};
+/// # use bevy_asset::{Handle, AssetServer, Assets, Asset};
+///
+/// #[derive(AsBindGroup, Asset, TypePath, Debug, Clone)]
+/// pub struct CustomMaterial {
+/// // Uniform bindings must implement `ShaderType`, which will be used to convert the value to
+/// // its shader-compatible equivalent. Most core math types already implement `ShaderType`.
+/// #[uniform(0)]
+/// color: Color,
+/// // Images can be bound as textures in shaders. If the Image's sampler is also needed, just
+/// // add the sampler attribute with a different binding index.
+/// #[texture(1)]
+/// #[sampler(2)]
+/// color_texture: Handle,
+/// }
+///
+/// // All functions on `UiMaterial` have default impls. You only need to implement the
+/// // functions that are relevant for your material.
+/// impl UiMaterial for CustomMaterial {
+/// fn fragment_shader() -> ShaderRef {
+/// "shaders/custom_material.wgsl".into()
+/// }
+/// }
+///
+/// // Spawn an entity using `CustomMaterial`.
+/// fn setup(mut commands: Commands, mut materials: ResMut>, asset_server: Res) {
+/// commands.spawn(MaterialNodeBundle {
+/// style: Style {
+/// width: Val::Percent(100.0),
+/// ..Default::default()
+/// },
+/// material: materials.add(CustomMaterial {
+/// color: Color::RED,
+/// color_texture: asset_server.load("some_image.png"),
+/// }),
+/// ..Default::default()
+/// });
+/// }
+/// ```
+/// In WGSL shaders, the material's binding would look like this:
+///
+/// If you only use the fragment shader make sure to import `UiVertexOutput` from
+/// `bevy_ui::ui_vertex_output` in your wgsl shader.
+/// Also note that bind group 0 is always bound to the [`View Uniform`](bevy_render::view::ViewUniform).
+///
+/// ```wgsl
+/// #import bevy_ui::ui_vertex_output UiVertexOutput
+///
+/// struct CustomMaterial {
+/// color: vec4,
+/// }
+///
+/// @group(1) @binding(0)
+/// var material: CustomMaterial;
+/// @group(1) @binding(1)
+/// var color_texture: texture_2d;
+/// @group(1) @binding(2)
+/// var color_sampler: sampler;
+///
+/// @fragment
+/// fn fragment(in: UiVertexOutput) -> @location(0) vec4 {
+///
+/// }
+/// ```
+pub trait UiMaterial: AsBindGroup + Asset + Clone + Sized {
+ /// Returns this materials vertex shader. If [`ShaderRef::Default`] is returned, the default UI
+ /// vertex shader will be used.
+ fn vertex_shader() -> ShaderRef {
+ ShaderRef::Default
+ }
+
+ /// Returns this materials fragment shader. If [`ShaderRef::Default`] is returned, the default
+ /// UI fragment shader will be used.
+ fn fragment_shader() -> ShaderRef {
+ ShaderRef::Default
+ }
+
+ #[allow(unused_variables)]
+ #[inline]
+ fn specialize(descriptor: &mut RenderPipelineDescriptor, key: UiMaterialKey) {}
+}
+
+pub struct UiMaterialKey {
+ pub hdr: bool,
+ pub bind_group_data: M::Data,
+}
+
+impl Eq for UiMaterialKey where M::Data: PartialEq {}
+
+impl PartialEq for UiMaterialKey
+where
+ M::Data: PartialEq,
+{
+ fn eq(&self, other: &Self) -> bool {
+ self.hdr == other.hdr && self.bind_group_data == other.bind_group_data
+ }
+}
+
+impl Clone for UiMaterialKey
+where
+ M::Data: Clone,
+{
+ fn clone(&self) -> Self {
+ Self {
+ hdr: self.hdr,
+ bind_group_data: self.bind_group_data.clone(),
+ }
+ }
+}
+
+impl Hash for UiMaterialKey
+where
+ M::Data: Hash,
+{
+ fn hash(&self, state: &mut H) {
+ self.hdr.hash(state);
+ self.bind_group_data.hash(state);
+ }
+}
diff --git a/crates/bevy_window/src/window.rs b/crates/bevy_window/src/window.rs
index b77b633dae62bf..7b5c75d38f6d54 100644
--- a/crates/bevy_window/src/window.rs
+++ b/crates/bevy_window/src/window.rs
@@ -589,7 +589,7 @@ pub struct WindowResolution {
physical_height: u32,
/// Code-provided ratio of physical size to logical size.
///
- /// Should be used instead `scale_factor` when set.
+ /// Should be used instead of `scale_factor` when set.
scale_factor_override: Option,
/// OS-provided ratio of physical size to logical size.
///
diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs
index 955f954a6f45fd..91c9858c79293e 100644
--- a/crates/bevy_winit/src/lib.rs
+++ b/crates/bevy_winit/src/lib.rs
@@ -372,6 +372,7 @@ pub fn winit_runner(mut app: App) {
WindowAndInputEventWriters,
NonSend,
Query<(&mut Window, &mut CachedWindow)>,
+ NonSend,
)> = SystemState::new(&mut app.world);
#[cfg(not(target_arch = "wasm32"))]
@@ -476,7 +477,7 @@ pub fn winit_runner(mut app: App) {
event::Event::WindowEvent {
event, window_id, ..
} => {
- let (mut event_writers, winit_windows, mut windows) =
+ let (mut event_writers, winit_windows, mut windows, access_kit_adapters) =
event_writer_system_state.get_mut(&mut app.world);
let Some(window_entity) = winit_windows.get_window_entity(window_id) else {
@@ -495,6 +496,18 @@ pub fn winit_runner(mut app: App) {
return;
};
+ // Allow AccessKit to respond to `WindowEvent`s before they reach
+ // the engine.
+ if let Some(adapter) = access_kit_adapters.get(&window_entity) {
+ if let Some(window) = winit_windows.get_window(window_entity) {
+ // Somewhat surprisingly, this call has meaningful side effects
+ // See https://github.com/AccessKit/accesskit/issues/300
+ // AccessKit might later need to filter events based on this, but we currently do not.
+ // See https://github.com/bevyengine/bevy/pull/10239#issuecomment-1775572176
+ let _ = adapter.on_event(window, &event);
+ }
+ }
+
runner_state.window_event_received = true;
match event {
@@ -713,20 +726,20 @@ pub fn winit_runner(mut app: App) {
event: DeviceEvent::MouseMotion { delta: (x, y) },
..
} => {
- let (mut event_writers, _, _) = event_writer_system_state.get_mut(&mut app.world);
+ let (mut event_writers, ..) = event_writer_system_state.get_mut(&mut app.world);
event_writers.mouse_motion.send(MouseMotion {
delta: Vec2::new(x as f32, y as f32),
});
}
event::Event::Suspended => {
- let (mut event_writers, _, _) = event_writer_system_state.get_mut(&mut app.world);
+ let (mut event_writers, ..) = event_writer_system_state.get_mut(&mut app.world);
event_writers.lifetime.send(ApplicationLifetime::Suspended);
// Mark the state as `WillSuspend`. This will let the schedule run one last time
// before actually suspending to let the application react
runner_state.active = ActiveState::WillSuspend;
}
event::Event::Resumed => {
- let (mut event_writers, _, _) = event_writer_system_state.get_mut(&mut app.world);
+ let (mut event_writers, ..) = event_writer_system_state.get_mut(&mut app.world);
match runner_state.active {
ActiveState::NotYetStarted => {
event_writers.lifetime.send(ApplicationLifetime::Started);
diff --git a/examples/README.md b/examples/README.md
index ed5a095aebd040..db73c982f060c0 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -369,6 +369,7 @@ Example | Description
[Text Wrap Debug](../examples/ui/text_wrap_debug.rs) | Demonstrates text wrapping
[Transparency UI](../examples/ui/transparency_ui.rs) | Demonstrates transparency for UI
[UI](../examples/ui/ui.rs) | Illustrates various features of Bevy UI
+[UI Material](../examples/ui/ui_material.rs) | Demonstrates creating and using custom Ui materials
[UI Scaling](../examples/ui/ui_scaling.rs) | Illustrates how to scale the UI
[UI Texture Atlas](../examples/ui/ui_texture_atlas.rs) | Illustrates how to use TextureAtlases in UI
[UI Z-Index](../examples/ui/z_index.rs) | Demonstrates how to control the relative depth (z-position) of UI elements
diff --git a/examples/ui/ui_material.rs b/examples/ui/ui_material.rs
new file mode 100644
index 00000000000000..82a2509f836788
--- /dev/null
+++ b/examples/ui/ui_material.rs
@@ -0,0 +1,65 @@
+//! Demonstrates the use of [`UiMaterials`](UiMaterial) and how to change material values
+
+use bevy::prelude::*;
+use bevy::reflect::TypePath;
+use bevy::render::render_resource::*;
+
+fn main() {
+ App::new()
+ .add_plugins(DefaultPlugins)
+ .add_plugins(UiMaterialPlugin::::default())
+ .add_systems(Startup, setup)
+ .add_systems(Update, update)
+ .run();
+}
+
+fn update(time: Res