From 0fa115f9110487df2f996c6fff6b8dd482bed822 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Mockers?= Date: Wed, 30 Apr 2025 07:19:01 +0200 Subject: [PATCH 01/80] fix new nightly lint on mikktspace (#18988) # Objective - new nightly lint make CI fail ## Solution - Follow the lint: https://github.com/rust-lang/rust/pull/123239 --- crates/bevy_mikktspace/src/generated.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_mikktspace/src/generated.rs b/crates/bevy_mikktspace/src/generated.rs index a726eb5bc8..b246b9668d 100644 --- a/crates/bevy_mikktspace/src/generated.rs +++ b/crates/bevy_mikktspace/src/generated.rs @@ -756,7 +756,7 @@ unsafe fn CompareSubGroups(mut pg1: *const SSubGroup, mut pg2: *const SSubGroup) return false; } while i < (*pg1).iNrFaces as usize && bStillSame { - bStillSame = if (*pg1).pTriMembers[i] == (*pg2).pTriMembers[i] { + bStillSame = if (&(*pg1).pTriMembers)[i] == (&(*pg2).pTriMembers)[i] { true } else { false From b40845d296a78f43a892d6c350e1cc85523895fd Mon Sep 17 00:00:00 2001 From: Rob Parrett Date: Wed, 30 Apr 2025 11:41:17 -0700 Subject: [PATCH 02/80] Fix a few subcrate import paths in examples (#19002) # Objective Tripped over the `directional_navigation` one recently while playing around with that example. Examples should import items from `bevy` rather than the sub-crates directly. ## Solution Use paths re-exported by `bevy`. ## Testing ``` cargo run --example log_diagnostics cargo run --example directional_navigation cargo run --example custom_projection ``` --- examples/camera/custom_projection.rs | 2 +- examples/diagnostics/log_diagnostics.rs | 2 +- examples/ui/directional_navigation.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/camera/custom_projection.rs b/examples/camera/custom_projection.rs index c3bdb49bcd..9e20c48eeb 100644 --- a/examples/camera/custom_projection.rs +++ b/examples/camera/custom_projection.rs @@ -27,7 +27,7 @@ impl CameraProjection for ObliquePerspectiveProjection { mat } - fn get_clip_from_view_for_sub(&self, sub_view: &bevy_render::camera::SubCameraView) -> Mat4 { + fn get_clip_from_view_for_sub(&self, sub_view: &bevy::render::camera::SubCameraView) -> Mat4 { let mut mat = self.perspective.get_clip_from_view_for_sub(sub_view); mat.col_mut(2)[0] = self.horizontal_obliqueness; mat.col_mut(2)[1] = self.vertical_obliqueness; diff --git a/examples/diagnostics/log_diagnostics.rs b/examples/diagnostics/log_diagnostics.rs index fd9be8c04e..f487a87133 100644 --- a/examples/diagnostics/log_diagnostics.rs +++ b/examples/diagnostics/log_diagnostics.rs @@ -21,7 +21,7 @@ fn main() { bevy::diagnostic::SystemInformationDiagnosticsPlugin, // Forwards various diagnostics from the render app to the main app. // These are pretty verbose but can be useful to pinpoint performance issues. - bevy_render::diagnostic::RenderDiagnosticsPlugin, + bevy::render::diagnostic::RenderDiagnosticsPlugin, )) // No rendering diagnostics are emitted unless something is drawn to the screen, // so we spawn a small scene. diff --git a/examples/ui/directional_navigation.rs b/examples/ui/directional_navigation.rs index 937970915c..b6f4a0d051 100644 --- a/examples/ui/directional_navigation.rs +++ b/examples/ui/directional_navigation.rs @@ -388,7 +388,7 @@ fn interact_with_focused_button( // This field isn't used, so we're just setting it to a placeholder value pointer_location: Location { target: NormalizedRenderTarget::Image( - bevy_render::camera::ImageRenderTarget { + bevy::render::camera::ImageRenderTarget { handle: Handle::default(), scale_factor: FloatOrd(1.0), }, From 3631a64a3dca0c8bce68b8bb00163b4ca4921b59 Mon Sep 17 00:00:00 2001 From: Brezak Date: Wed, 30 Apr 2025 22:59:29 +0200 Subject: [PATCH 03/80] Add a method to clear all related entity to `EntityCommands` and friends (#18907) # Objective We have methods to: - Add related entities - Replace related entities - Remove specific related entities We don't have a method the remove all related entities so. ## Solution Add a method to remove all related entities. ## Testing A new test case. --- crates/bevy_ecs/src/hierarchy.rs | 12 +++++++++ .../src/relationship/related_methods.rs | 27 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/crates/bevy_ecs/src/hierarchy.rs b/crates/bevy_ecs/src/hierarchy.rs index 9f4b0d0f8f..ecdf854514 100644 --- a/crates/bevy_ecs/src/hierarchy.rs +++ b/crates/bevy_ecs/src/hierarchy.rs @@ -281,6 +281,12 @@ impl<'w> EntityWorldMut<'w> { self.add_related::(children) } + /// Removes all the children from this entity. + /// See also [`clear_related`](Self::clear_related) + pub fn clear_children(&mut self) -> &mut Self { + self.clear_related::() + } + /// Insert children at specific index. /// See also [`insert_related`](Self::insert_related). pub fn insert_children(&mut self, index: usize, children: &[Entity]) -> &mut Self { @@ -369,6 +375,12 @@ impl<'a> EntityCommands<'a> { self.add_related::(children) } + /// Removes all the children from this entity. + /// See also [`clear_related`](Self::clear_related) + pub fn clear_children(&mut self) -> &mut Self { + self.clear_related::() + } + /// Insert children at specific index. /// See also [`insert_related`](Self::insert_related). pub fn insert_children(&mut self, index: usize, children: &[Entity]) -> &mut Self { diff --git a/crates/bevy_ecs/src/relationship/related_methods.rs b/crates/bevy_ecs/src/relationship/related_methods.rs index 98ef8d0832..de4c01933e 100644 --- a/crates/bevy_ecs/src/relationship/related_methods.rs +++ b/crates/bevy_ecs/src/relationship/related_methods.rs @@ -47,6 +47,11 @@ impl<'w> EntityWorldMut<'w> { self } + /// Removes the relation `R` between this entity and all its related entities. + pub fn clear_related(&mut self) -> &mut Self { + self.remove::() + } + /// Relates the given entities to this entity with the relation `R`, starting at this particular index. /// /// If the `related` has duplicates, a related entity will take the index of its last occurrence in `related`. @@ -376,6 +381,13 @@ impl<'a> EntityCommands<'a> { }) } + /// Removes the relation `R` between this entity and all its related entities. + pub fn clear_related(&mut self) -> &mut Self { + self.queue(|mut entity: EntityWorldMut| { + entity.clear_related::(); + }) + } + /// Relates the given entities to this entity with the relation `R`, starting at this particular index. /// /// If the `related` has duplicates, a related entity will take the index of its last occurrence in `related`. @@ -613,4 +625,19 @@ mod tests { assert!(!world.entity(entity).contains::()); } } + + #[test] + fn remove_all_related() { + let mut world = World::new(); + + let a = world.spawn_empty().id(); + let b = world.spawn(ChildOf(a)).id(); + let c = world.spawn(ChildOf(a)).id(); + + world.entity_mut(a).clear_related::(); + + assert_eq!(world.entity(a).get::(), None); + assert_eq!(world.entity(b).get::(), None); + assert_eq!(world.entity(c).get::(), None); + } } From 21b62d640b0291ddc92fb3b9216a361c5cd08e06 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 30 Apr 2025 22:00:42 +0100 Subject: [PATCH 04/80] Change the default visual box for `OverflowClipMargin` to `PaddingBox` (#18935) # Objective The default should be `OverflowClipBox::PaddingBox` not `OverflowClipBox::ContentBox` `padding-box` is the default in CSS. ## Solution Set the default to `PaddingBox`. ## Testing Compare the `overflow` UI example on main vs with this PR. You should see that on main the outline around the inner node gets clipped. With this PR by default clipping starts at the inner edge of the border (the `padding-box`) and the outlines are visible. Fixes #18934 --- crates/bevy_ui/src/ui_node.rs | 4 ++-- .../overflowclipbox_default_is_now_paddingbox.md | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 release-content/migration-guides/overflowclipbox_default_is_now_paddingbox.md diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index 2486296bac..55ce3eb6a2 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -1178,7 +1178,7 @@ pub struct OverflowClipMargin { impl OverflowClipMargin { pub const DEFAULT: Self = Self { - visual_box: OverflowClipBox::ContentBox, + visual_box: OverflowClipBox::PaddingBox, margin: 0., }; @@ -1224,9 +1224,9 @@ impl OverflowClipMargin { )] pub enum OverflowClipBox { /// Clip any content that overflows outside the content box - #[default] ContentBox, /// Clip any content that overflows outside the padding box + #[default] PaddingBox, /// Clip any content that overflows outside the border box BorderBox, diff --git a/release-content/migration-guides/overflowclipbox_default_is_now_paddingbox.md b/release-content/migration-guides/overflowclipbox_default_is_now_paddingbox.md new file mode 100644 index 0000000000..a733f7f775 --- /dev/null +++ b/release-content/migration-guides/overflowclipbox_default_is_now_paddingbox.md @@ -0,0 +1,7 @@ +--- +title: OverflowClipBox's default is now Paddingbox +pull_requests: [18935] +--- + +The default variant for `OverflowClipBox` is now `PaddingBox`. +The default value for `OverflowClipMargin::visual_box` is now `OverflowClipBox::PaddingBox`. From cd67bac544a874f668c706b837c8b3093dfb9545 Mon Sep 17 00:00:00 2001 From: "Peter S." Date: Wed, 30 Apr 2025 23:24:53 +0200 Subject: [PATCH 05/80] Expose deferred screen edges setting for ios devices (#18729) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Objective - This just exposes the preferred [screen edges deferring system gestures](https://developer.apple.com/documentation/uikit/uiviewcontroller/preferredscreenedgesdeferringsystemgestures) setting from [winit](https://docs.rs/winit/latest/winit/platform/ios/trait.WindowExtIOS.html#tymethod.set_preferred_screen_edges_deferring_system_gestures), making it accessible in bevy apps. This setting is useful for ios apps that make use of the screen edges, letting the app have control of the first edge gesture before relegating to the os. ## Testing - Tested on simulator and on an iPhone Xs --- --------- Co-authored-by: Alice Cecile Co-authored-by: Greeble <166992735+greeble-dev@users.noreply.github.com> Co-authored-by: François Mockers --- crates/bevy_window/src/window.rs | 37 ++++++++++++++++++++++++++ crates/bevy_winit/src/converters.rs | 16 +++++++++++ crates/bevy_winit/src/system.rs | 10 +++++++ crates/bevy_winit/src/winit_windows.rs | 7 +++++ examples/mobile/src/lib.rs | 4 ++- 5 files changed, 73 insertions(+), 1 deletion(-) diff --git a/crates/bevy_window/src/window.rs b/crates/bevy_window/src/window.rs index e09e254d40..31ff212ebe 100644 --- a/crates/bevy_window/src/window.rs +++ b/crates/bevy_window/src/window.rs @@ -443,6 +443,17 @@ pub struct Window { /// /// [`WindowAttributesExtIOS::with_prefers_status_bar_hidden`]: https://docs.rs/winit/latest/x86_64-apple-darwin/winit/platform/ios/trait.WindowAttributesExtIOS.html#tymethod.with_prefers_status_bar_hidden pub prefers_status_bar_hidden: bool, + /// Sets screen edges for which you want your gestures to take precedence + /// over the system gestures. + /// + /// Corresponds to [`WindowAttributesExtIOS::with_preferred_screen_edges_deferring_system_gestures`]. + /// + /// # Platform-specific + /// + /// - Only used on iOS. + /// + /// [`WindowAttributesExtIOS::with_preferred_screen_edges_deferring_system_gestures`]: https://docs.rs/winit/latest/x86_64-apple-darwin/winit/platform/ios/trait.WindowAttributesExtIOS.html#tymethod.with_preferred_screen_edges_deferring_system_gestures + pub preferred_screen_edges_deferring_system_gestures: ScreenEdge, } impl Default for Window { @@ -487,6 +498,7 @@ impl Default for Window { titlebar_show_buttons: true, prefers_home_indicator_hidden: false, prefers_status_bar_hidden: false, + preferred_screen_edges_deferring_system_gestures: Default::default(), } } } @@ -1444,6 +1456,31 @@ impl Default for EnabledButtons { #[derive(Component, Default)] pub struct ClosingWindow; +/// The edges of a screen. Corresponds to [`winit::platform::ios::ScreenEdge`]. +/// +/// # Platform-specific +/// +/// - Only used on iOS. +/// +/// [`winit::platform::ios::ScreenEdge`]: https://docs.rs/winit/latest/x86_64-apple-darwin/winit/platform/ios/struct.ScreenEdge.html +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +pub enum ScreenEdge { + #[default] + /// No edge. + None, + /// The top edge of the screen. + Top, + /// The left edge of the screen. + Left, + /// The bottom edge of the screen. + Bottom, + /// The right edge of the screen. + Right, + /// All edges of the screen. + All, +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/bevy_winit/src/converters.rs b/crates/bevy_winit/src/converters.rs index ba41c62534..3de27162a4 100644 --- a/crates/bevy_winit/src/converters.rs +++ b/crates/bevy_winit/src/converters.rs @@ -10,6 +10,9 @@ use bevy_window::SystemCursorIcon; use bevy_window::{EnabledButtons, WindowLevel, WindowTheme}; use winit::keyboard::{Key, NamedKey, NativeKey}; +#[cfg(target_os = "ios")] +use bevy_window::ScreenEdge; + pub fn convert_keyboard_input( keyboard_input: &winit::event::KeyEvent, window: Entity, @@ -718,3 +721,16 @@ pub fn convert_resize_direction(resize_direction: CompassOctant) -> winit::windo CompassOctant::SouthEast => winit::window::ResizeDirection::SouthEast, } } + +#[cfg(target_os = "ios")] +/// Converts a [`bevy_window::ScreenEdge`] to a [`winit::platform::ios::ScreenEdge`]. +pub(crate) fn convert_screen_edge(edge: ScreenEdge) -> winit::platform::ios::ScreenEdge { + match edge { + ScreenEdge::None => winit::platform::ios::ScreenEdge::NONE, + ScreenEdge::Top => winit::platform::ios::ScreenEdge::TOP, + ScreenEdge::Bottom => winit::platform::ios::ScreenEdge::BOTTOM, + ScreenEdge::Left => winit::platform::ios::ScreenEdge::LEFT, + ScreenEdge::Right => winit::platform::ios::ScreenEdge::RIGHT, + ScreenEdge::All => winit::platform::ios::ScreenEdge::ALL, + } +} diff --git a/crates/bevy_winit/src/system.rs b/crates/bevy_winit/src/system.rs index f4ed1a59a3..32925db26b 100644 --- a/crates/bevy_winit/src/system.rs +++ b/crates/bevy_winit/src/system.rs @@ -568,6 +568,16 @@ pub(crate) fn changed_windows( if window.prefers_status_bar_hidden != cache.window.prefers_status_bar_hidden { winit_window.set_prefers_status_bar_hidden(window.prefers_status_bar_hidden); } + if window.preferred_screen_edges_deferring_system_gestures + != cache + .window + .preferred_screen_edges_deferring_system_gestures + { + use crate::converters::convert_screen_edge; + let preferred_edge = + convert_screen_edge(window.preferred_screen_edges_deferring_system_gestures); + winit_window.set_preferred_screen_edges_deferring_system_gestures(preferred_edge); + } } cache.window = window.clone(); } diff --git a/crates/bevy_winit/src/winit_windows.rs b/crates/bevy_winit/src/winit_windows.rs index 119da10fe1..b1d4b3d7b6 100644 --- a/crates/bevy_winit/src/winit_windows.rs +++ b/crates/bevy_winit/src/winit_windows.rs @@ -145,7 +145,14 @@ impl WinitWindows { #[cfg(target_os = "ios")] { + use crate::converters::convert_screen_edge; use winit::platform::ios::WindowAttributesExtIOS; + + let preferred_edge = + convert_screen_edge(window.preferred_screen_edges_deferring_system_gestures); + + winit_window_attributes = winit_window_attributes + .with_preferred_screen_edges_deferring_system_gestures(preferred_edge); winit_window_attributes = winit_window_attributes .with_prefers_home_indicator_hidden(window.prefers_home_indicator_hidden); winit_window_attributes = winit_window_attributes diff --git a/examples/mobile/src/lib.rs b/examples/mobile/src/lib.rs index c5df6ee3c7..ba93268e86 100644 --- a/examples/mobile/src/lib.rs +++ b/examples/mobile/src/lib.rs @@ -5,7 +5,7 @@ use bevy::{ input::{gestures::RotationGesture, touch::TouchPhase}, log::{Level, LogPlugin}, prelude::*, - window::{AppLifecycle, WindowMode}, + window::{AppLifecycle, ScreenEdge, WindowMode}, winit::WinitSettings, }; @@ -34,6 +34,8 @@ pub fn main() { prefers_home_indicator_hidden: true, // Only has an effect on iOS prefers_status_bar_hidden: true, + // Only has an effect on iOS + preferred_screen_edges_deferring_system_gestures: ScreenEdge::Bottom, ..default() }), ..default() From 2affecdb0701270e7c6a5de64f37600e06a49910 Mon Sep 17 00:00:00 2001 From: Taj Holliday <207123560+taj-holliday@users.noreply.github.com> Date: Sat, 3 May 2025 11:29:38 +0000 Subject: [PATCH 06/80] Audio sink seek adopted (#18971) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adopted #13869 # Objective Fixes #9076 ## Solution Using `rodio`'s `try_seek` ## Testing @ivanstepanovftw added a `seek` system using `AudioSink` to the `audio_control.rs` example. I got it working with .mp3 files, but rodio doesn't support seeking for .ogg and .flac files, so I removed it from the commit (since the assets folder only has .ogg files). Another thing to note is that `try_seek` fails when using `PlaybackMode::Loop`, as `rodio::source::buffered::Buffered` doesn't support `try_seek`. I haven't tested `SpatialAudioSink`. ## Notes I copied the docs for `try_seek` verbatim from `rodio`, and re-exported `rodio::source::SeekError`. I'm not completely confident in those decisions, please let me know if I'm doing anything wrong. --------- Co-authored-by: Ivan Stepanov Co-authored-by: François Mockers Co-authored-by: Jan Hohenheim Co-authored-by: François Mockers --- crates/bevy_audio/src/sinks.rs | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/crates/bevy_audio/src/sinks.rs b/crates/bevy_audio/src/sinks.rs index b0c77456e1..ed51754f86 100644 --- a/crates/bevy_audio/src/sinks.rs +++ b/crates/bevy_audio/src/sinks.rs @@ -1,10 +1,11 @@ +use crate::Volume; use bevy_ecs::component::Component; use bevy_math::Vec3; use bevy_transform::prelude::Transform; +use core::time::Duration; +pub use rodio::source::SeekError; use rodio::{Sink, SpatialSink}; -use crate::Volume; - /// Common interactions with an audio sink. pub trait AudioSinkPlayback { /// Gets the volume of the sound as a [`Volume`]. @@ -41,6 +42,26 @@ pub trait AudioSinkPlayback { /// No effect if not paused. fn play(&self); + /// Attempts to seek to a given position in the current source. + /// + /// This blocks between 0 and ~5 milliseconds. + /// + /// As long as the duration of the source is known, seek is guaranteed to saturate + /// at the end of the source. For example given a source that reports a total duration + /// of 42 seconds calling `try_seek()` with 60 seconds as argument will seek to + /// 42 seconds. + /// + /// # Errors + /// This function will return [`SeekError::NotSupported`] if one of the underlying + /// sources does not support seeking. + /// + /// It will return an error if an implementation ran + /// into one during the seek. + /// + /// When seeking beyond the end of a source, this + /// function might return an error if the duration of the source is not known. + fn try_seek(&self, pos: Duration) -> Result<(), SeekError>; + /// Pauses playback of this sink. /// /// No effect if already paused. @@ -160,6 +181,10 @@ impl AudioSinkPlayback for AudioSink { self.sink.play(); } + fn try_seek(&self, pos: Duration) -> Result<(), SeekError> { + self.sink.try_seek(pos) + } + fn pause(&self) { self.sink.pause(); } @@ -256,6 +281,10 @@ impl AudioSinkPlayback for SpatialAudioSink { self.sink.play(); } + fn try_seek(&self, pos: Duration) -> Result<(), SeekError> { + self.sink.try_seek(pos) + } + fn pause(&self) { self.sink.pause(); } From 8c34cbbb27847a78a6b41b0c983f1b9bb7e36190 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Maita?= <47983254+mnmaita@users.noreply.github.com> Date: Sun, 4 May 2025 10:11:59 +0200 Subject: [PATCH 07/80] Add TextureAtlas convenience methods (#19023) # Objective - Add a few useful methods to `TextureAtlas`. ## Solution - Added `TextureAtlas::with_index()`. - Added `TextureAtlas::with_layout()`. ## Testing - CI checks. --- crates/bevy_image/src/texture_atlas.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crates/bevy_image/src/texture_atlas.rs b/crates/bevy_image/src/texture_atlas.rs index b5b68b0c41..4caeed8c07 100644 --- a/crates/bevy_image/src/texture_atlas.rs +++ b/crates/bevy_image/src/texture_atlas.rs @@ -222,6 +222,18 @@ impl TextureAtlas { let atlas = texture_atlases.get(&self.layout)?; atlas.textures.get(self.index).copied() } + + /// Returns this [`TextureAtlas`] with the specified index. + pub fn with_index(mut self, index: usize) -> Self { + self.index = index; + self + } + + /// Returns this [`TextureAtlas`] with the specified [`TextureAtlasLayout`] handle. + pub fn with_layout(mut self, layout: Handle) -> Self { + self.layout = layout; + self + } } impl From> for TextureAtlas { From 5e2ecf417850405957ed309800379a2b38c7f4aa Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Sun, 4 May 2025 09:18:46 +0100 Subject: [PATCH 08/80] Text background colors (#18892) # Objective Add background colors for text. Fixes #18889 ## Solution New component `TextBackgroundColor`, add it to any UI `Text` or `TextSpan` entity to add a background color to its text. New field on `TextLayoutInfo` `section_rects` holds the list of bounding rects for each text section. The bounding rects are generated in `TextPipeline::queue_text` during text layout, `extract_text_background_colors` extracts the colored background rects for rendering. Didn't include `Text2d` support because of z-order issues. The section rects can also be used to implement interactions targeting individual text sections. ## Testing Includes a basic example that can be used for testing: ``` cargo run --example text_background_colors ``` --- ## Showcase ![tbcm](https://github.com/user-attachments/assets/e584e197-1a8c-4248-82ab-2461d904a85b) Using a proportional font with kerning the results aren't so tidy (since the bounds of adjacent glyphs can overlap) but it still works fine: ![tbc](https://github.com/user-attachments/assets/788bb052-4216-4019-a594-7c1b41164dd5) --------- Co-authored-by: Olle Lukowski Co-authored-by: Gilles Henaux --- Cargo.toml | 11 +++ crates/bevy_text/src/lib.rs | 1 + crates/bevy_text/src/pipeline.rs | 39 +++++++++- crates/bevy_text/src/text.rs | 24 ++++++ crates/bevy_ui/src/lib.rs | 1 + crates/bevy_ui/src/render/mod.rs | 71 ++++++++++++++++- examples/README.md | 1 + examples/ui/text_background_colors.rs | 77 +++++++++++++++++++ .../release-notes/text-background-colors.md | 7 ++ 9 files changed, 230 insertions(+), 2 deletions(-) create mode 100644 examples/ui/text_background_colors.rs create mode 100644 release-content/release-notes/text-background-colors.md diff --git a/Cargo.toml b/Cargo.toml index 8bb16b741d..f764793161 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3344,6 +3344,17 @@ description = "Illustrates creating and updating text" category = "UI (User Interface)" wasm = true +[[example]] +name = "text_background_colors" +path = "examples/ui/text_background_colors.rs" +doc-scrape-examples = true + +[package.metadata.example.text_background_colors] +name = "Text Background Colors" +description = "Demonstrates text background colors" +category = "UI (User Interface)" +wasm = true + [[example]] name = "text_debug" path = "examples/ui/text_debug.rs" diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index 670f793c31..18d7ad01a7 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -110,6 +110,7 @@ impl Plugin for TextPlugin { .register_type::() .register_type::() .register_type::() + .register_type::() .register_type::() .register_type::() .register_type::() diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index f1bdeded91..ebaa10b12b 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -9,7 +9,7 @@ use bevy_ecs::{ }; use bevy_image::prelude::*; use bevy_log::{once, warn}; -use bevy_math::{UVec2, Vec2}; +use bevy_math::{Rect, UVec2, Vec2}; use bevy_platform::collections::HashMap; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; @@ -234,6 +234,7 @@ impl TextPipeline { swash_cache: &mut SwashCache, ) -> Result<(), TextError> { layout_info.glyphs.clear(); + layout_info.section_rects.clear(); layout_info.size = Default::default(); // Clear this here at the focal point of text rendering to ensure the field's lifecycle has strong boundaries. @@ -265,11 +266,38 @@ impl TextPipeline { let box_size = buffer_dimensions(buffer); let result = buffer.layout_runs().try_for_each(|run| { + let mut current_section: Option = None; + let mut start = 0.; + let mut end = 0.; let result = run .glyphs .iter() .map(move |layout_glyph| (layout_glyph, run.line_y, run.line_i)) .try_for_each(|(layout_glyph, line_y, line_i)| { + match current_section { + Some(section) => { + if section != layout_glyph.metadata { + layout_info.section_rects.push(( + computed.entities[section].entity, + Rect::new( + start, + run.line_top, + end, + run.line_top + run.line_height, + ), + )); + start = end.max(layout_glyph.x); + current_section = Some(layout_glyph.metadata); + } + end = layout_glyph.x + layout_glyph.w; + } + None => { + current_section = Some(layout_glyph.metadata); + start = layout_glyph.x; + end = start + layout_glyph.w; + } + } + let mut temp_glyph; let span_index = layout_glyph.metadata; let font_id = glyph_info[span_index].0; @@ -339,6 +367,12 @@ impl TextPipeline { layout_info.glyphs.push(pos_glyph); Ok(()) }); + if let Some(section) = current_section { + layout_info.section_rects.push(( + computed.entities[section].entity, + Rect::new(start, run.line_top, end, run.line_top + run.line_height), + )); + } result }); @@ -418,6 +452,9 @@ impl TextPipeline { pub struct TextLayoutInfo { /// Scaled and positioned glyphs in screenspace pub glyphs: Vec, + /// Rects bounding the text block's text sections. + /// A text section spanning more than one line will have multiple bounding rects. + pub section_rects: Vec<(Entity, Rect)>, /// The glyphs resulting size pub size: Vec2, } diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index faa5d93dc9..e9e78e3ed2 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -407,6 +407,30 @@ impl TextColor { pub const WHITE: Self = TextColor(Color::WHITE); } +/// The background color of the text for this section. +#[derive(Component, Copy, Clone, Debug, Deref, DerefMut, Reflect, PartialEq)] +#[reflect(Component, Default, Debug, PartialEq, Clone)] +pub struct TextBackgroundColor(pub Color); + +impl Default for TextBackgroundColor { + fn default() -> Self { + Self(Color::BLACK) + } +} + +impl> From for TextBackgroundColor { + fn from(color: T) -> Self { + Self(color.into()) + } +} + +impl TextBackgroundColor { + /// Black background + pub const BLACK: Self = TextBackgroundColor(Color::BLACK); + /// White background + pub const WHITE: Self = TextBackgroundColor(Color::WHITE); +} + /// Determines how lines will be broken when preventing text from running out of bounds. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Reflect, Serialize, Deserialize)] #[reflect(Serialize, Deserialize, Clone, PartialEq, Hash, Default)] diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index 56462b6952..4db3073a90 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -64,6 +64,7 @@ pub mod prelude { }, // `bevy_sprite` re-exports for texture slicing bevy_sprite::{BorderRect, SliceScaleMode, SpriteImageMode, TextureSlicer}, + bevy_text::TextBackgroundColor, }; } diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index 8cb61cde21..97ba9cd7ee 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -51,7 +51,9 @@ pub use debug_overlay::UiDebugOptions; use crate::{Display, Node}; use bevy_platform::collections::{HashMap, HashSet}; -use bevy_text::{ComputedTextBlock, PositionedGlyph, TextColor, TextLayoutInfo}; +use bevy_text::{ + ComputedTextBlock, PositionedGlyph, TextBackgroundColor, TextColor, TextLayoutInfo, +}; use bevy_transform::components::GlobalTransform; use box_shadow::BoxShadowPlugin; use bytemuck::{Pod, Zeroable}; @@ -105,6 +107,7 @@ pub enum RenderUiSystem { ExtractImages, ExtractTextureSlice, ExtractBorders, + ExtractTextBackgrounds, ExtractTextShadows, ExtractText, ExtractDebug, @@ -135,6 +138,7 @@ pub fn build_ui_render(app: &mut App) { RenderUiSystem::ExtractImages, RenderUiSystem::ExtractTextureSlice, RenderUiSystem::ExtractBorders, + RenderUiSystem::ExtractTextBackgrounds, RenderUiSystem::ExtractTextShadows, RenderUiSystem::ExtractText, RenderUiSystem::ExtractDebug, @@ -148,6 +152,7 @@ pub fn build_ui_render(app: &mut App) { extract_uinode_background_colors.in_set(RenderUiSystem::ExtractBackgrounds), extract_uinode_images.in_set(RenderUiSystem::ExtractImages), extract_uinode_borders.in_set(RenderUiSystem::ExtractBorders), + extract_text_background_colors.in_set(RenderUiSystem::ExtractTextBackgrounds), extract_text_shadows.in_set(RenderUiSystem::ExtractTextShadows), extract_text_sections.in_set(RenderUiSystem::ExtractText), #[cfg(feature = "bevy_ui_debug")] @@ -879,6 +884,70 @@ pub fn extract_text_shadows( } } +pub fn extract_text_background_colors( + mut commands: Commands, + mut extracted_uinodes: ResMut, + uinode_query: Extract< + Query<( + Entity, + &ComputedNode, + &GlobalTransform, + &InheritedVisibility, + Option<&CalculatedClip>, + &ComputedNodeTarget, + &TextLayoutInfo, + )>, + >, + text_background_colors_query: Extract>, + camera_map: Extract, +) { + let mut camera_mapper = camera_map.get_mapper(); + for (entity, uinode, global_transform, inherited_visibility, clip, camera, text_layout_info) in + &uinode_query + { + // Skip if not visible or if size is set to zero (e.g. when a parent is set to `Display::None`) + if !inherited_visibility.get() || uinode.is_empty() { + continue; + } + + let Some(extracted_camera_entity) = camera_mapper.map(camera) else { + continue; + }; + + let transform = global_transform.affine() + * bevy_math::Affine3A::from_translation(-0.5 * uinode.size().extend(0.)); + + for &(section_entity, rect) in text_layout_info.section_rects.iter() { + let Ok(text_background_color) = text_background_colors_query.get(section_entity) else { + continue; + }; + + extracted_uinodes.uinodes.push(ExtractedUiNode { + render_entity: commands.spawn(TemporaryRenderEntity).id(), + stack_index: uinode.stack_index, + color: text_background_color.0.to_linear(), + rect: Rect { + min: Vec2::ZERO, + max: rect.size(), + }, + clip: clip.map(|clip| clip.clip), + image: AssetId::default(), + extracted_camera_entity, + item: ExtractedUiItem::Node { + atlas_scaling: None, + transform: transform * Mat4::from_translation(rect.center().extend(0.)), + flip_x: false, + flip_y: false, + border: uinode.border(), + border_radius: uinode.border_radius(), + node_type: NodeType::Rect, + }, + main_entity: entity.into(), + }); + } + } +} + #[repr(C)] #[derive(Copy, Clone, Pod, Zeroable)] struct UiVertex { diff --git a/examples/README.md b/examples/README.md index d0e33d957f..aa91006e72 100644 --- a/examples/README.md +++ b/examples/README.md @@ -555,6 +555,7 @@ Example | Description [Size Constraints](../examples/ui/size_constraints.rs) | Demonstrates how the to use the size constraints to control the size of a UI node. [Tab Navigation](../examples/ui/tab_navigation.rs) | Demonstration of Tab Navigation between UI elements [Text](../examples/ui/text.rs) | Illustrates creating and updating text +[Text Background Colors](../examples/ui/text_background_colors.rs) | Demonstrates text background colors [Text Debug](../examples/ui/text_debug.rs) | An example for debugging text layout [Text Wrap Debug](../examples/ui/text_wrap_debug.rs) | Demonstrates text wrapping [Transparency UI](../examples/ui/transparency_ui.rs) | Demonstrates transparency for UI diff --git a/examples/ui/text_background_colors.rs b/examples/ui/text_background_colors.rs new file mode 100644 index 0000000000..caf0b60e85 --- /dev/null +++ b/examples/ui/text_background_colors.rs @@ -0,0 +1,77 @@ +//! This example demonstrates UI text with a background color + +use bevy::{ + color::palettes::css::{BLUE, GREEN, PURPLE, RED, YELLOW}, + prelude::*, +}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup) + .add_systems(Update, cycle_text_background_colors) + .run(); +} + +const PALETTE: [Color; 5] = [ + Color::Srgba(RED), + Color::Srgba(GREEN), + Color::Srgba(BLUE), + Color::Srgba(YELLOW), + Color::Srgba(PURPLE), +]; + +fn setup(mut commands: Commands) { + // UI camera + commands.spawn(Camera2d); + + let message_text = [ + "T", "e", "x", "t\n", "B", "a", "c", "k", "g", "r", "o", "u", "n", "d\n", "C", "o", "l", + "o", "r", "s", "!", + ]; + + commands + .spawn(Node { + width: Val::Percent(100.), + height: Val::Percent(100.), + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + ..Default::default() + }) + .with_children(|commands| { + commands + .spawn(( + Text::default(), + TextLayout { + justify: JustifyText::Center, + ..Default::default() + }, + )) + .with_children(|commands| { + for (i, section_str) in message_text.iter().enumerate() { + commands.spawn(( + TextSpan::new(*section_str), + TextColor::BLACK, + TextFont { + font_size: 100., + ..default() + }, + TextBackgroundColor(PALETTE[i % PALETTE.len()]), + )); + } + }); + }); +} + +fn cycle_text_background_colors( + time: Res