Skip to content

Text background colors #18892

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions crates/bevy_text/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ impl Plugin for TextPlugin {
.register_type::<TextFont>()
.register_type::<LineHeight>()
.register_type::<TextColor>()
.register_type::<TextBackgroundColor>()
.register_type::<TextSpan>()
.register_type::<TextBounds>()
.register_type::<TextLayout>()
Expand Down
40 changes: 39 additions & 1 deletion crates/bevy_text/src/pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -265,11 +266,39 @@ impl TextPipeline {
let box_size = buffer_dimensions(buffer);

let result = buffer.layout_runs().try_for_each(|run| {
let mut current_section: Option<usize> = 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) => {
let section = section as usize;
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;
Expand Down Expand Up @@ -339,6 +368,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
});
Expand Down Expand Up @@ -418,6 +453,9 @@ impl TextPipeline {
pub struct TextLayoutInfo {
/// Scaled and positioned glyphs in screenspace
pub glyphs: Vec<PositionedGlyph>,
/// 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,
}
Expand Down
24 changes: 24 additions & 0 deletions crates/bevy_text/src/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T: Into<Color>> From<T> 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)]
Expand Down
1 change: 1 addition & 0 deletions crates/bevy_ui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ pub mod prelude {
},
// `bevy_sprite` re-exports for texture slicing
bevy_sprite::{BorderRect, SliceScaleMode, SpriteImageMode, TextureSlicer},
bevy_text::TextBackgroundColor,
};
}

Expand Down
71 changes: 70 additions & 1 deletion crates/bevy_ui/src/render/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -105,6 +107,7 @@ pub enum RenderUiSystem {
ExtractImages,
ExtractTextureSlice,
ExtractBorders,
ExtractTextBackgrounds,
ExtractTextShadows,
ExtractText,
ExtractDebug,
Expand Down Expand Up @@ -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,
Expand All @@ -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")]
Expand Down Expand Up @@ -879,6 +884,70 @@ pub fn extract_text_shadows(
}
}

pub fn extract_text_background_colors(
mut commands: Commands,
mut extracted_uinodes: ResMut<ExtractedUiNodes>,
uinode_query: Extract<
Query<(
Entity,
&ComputedNode,
&GlobalTransform,
&InheritedVisibility,
Option<&CalculatedClip>,
&ComputedNodeTarget,
&TextLayoutInfo,
)>,
>,
text_background_colors_query: Extract<Query<&TextBackgroundColor>>,
camera_map: Extract<UiCameraMap>,
) {
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 {
Expand Down
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
77 changes: 77 additions & 0 deletions examples/ui/text_background_colors.rs
Original file line number Diff line number Diff line change
@@ -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<Time>,
children_query: Query<&Children, With<Text>>,
mut text_background_colors_query: Query<&mut TextBackgroundColor>,
) {
let n = time.elapsed_secs() as usize;
let children = children_query.single().unwrap();

for (i, child) in children.iter().enumerate() {
text_background_colors_query.get_mut(child).unwrap().0 = PALETTE[(i + n) % PALETTE.len()];
}
}
7 changes: 7 additions & 0 deletions release-content/release-notes/text-background-colors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
title: Text Background Colors
authors: ["@Ickshonpe"]
pull_requests: [18892]
---

UI Text now supports background colors. Insert the `TextBackgroundColor` component on a UI `Text` or `TextSpan` entity to set a background color for its text section.
Loading