Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
781ba2d
Added `Strikeout` component.
ickshonpe Oct 15, 2025
bc62a7b
Added strikeout geometry to `FontFaceInfo` and
ickshonpe Oct 15, 2025
b6c50aa
Update strikeout info during relayout.
ickshonpe Oct 15, 2025
9a68edb
Fixed geometry and rendering.
ickshonpe Oct 15, 2025
d4466cf
Update release note
ickshonpe Oct 15, 2025
2171fd7
cargo run -p build-templated-pages -- update examples
ickshonpe Oct 15, 2025
64f853b
y coords should be object centered
ickshonpe Oct 15, 2025
a7e8cb7
Lower impact, less digusting implementation. Fixes incorrect strikeou…
ickshonpe Oct 15, 2025
f896ab4
Merge branch 'main' into strikeout
ickshonpe Oct 15, 2025
e4cdcb8
update comments
ickshonpe Oct 15, 2025
be933d4
Merge branch 'strikeout' of https://github.com/ickshonpe/bevy into st…
ickshonpe Oct 15, 2025
315c157
Add strikeout to text shadows too.
ickshonpe Oct 15, 2025
bfa7067
added text2d strikeout shadow
ickshonpe Oct 15, 2025
1b3459c
use shadow color for shadow underline
ickshonpe Oct 15, 2025
a712d55
Removed useless conversion
ickshonpe Oct 15, 2025
6d12696
Merge branch 'main' into strikeout
ickshonpe Oct 16, 2025
832f3aa
Merge branch 'main' into strikeout
ickshonpe Oct 21, 2025
affe362
Merge branch 'main' into strikeout
ickshonpe Oct 21, 2025
e12c8f3
Merge branch 'main' into strikeout
ickshonpe Oct 21, 2025
5bb335d
Renamed `Strikeout` to `Strikethrough`
ickshonpe Oct 21, 2025
5b20234
Renamed locals and z layer constant in bevy_ui_render lib
ickshonpe Oct 21, 2025
cfd23b8
Renamed release note
ickshonpe Oct 21, 2025
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 @@ -3549,6 +3549,17 @@ description = "Demonstrates how the to use the size constraints to control the s
category = "UI (User Interface)"
wasm = true

[[example]]
name = "strikethrough"
path = "examples/ui/strikethrough.rs"
doc-scrape-examples = true

[package.metadata.example.strikethrough]
name = "Strikethrough"
description = "Demonstrates how to display text with strikethrough."
category = "UI (User Interface)"
wasm = true

[[example]]
name = "text"
path = "examples/ui/text.rs"
Expand Down
66 changes: 64 additions & 2 deletions crates/bevy_sprite_render/src/text2d/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use bevy_camera::visibility::ViewVisibility;
use bevy_color::LinearRgba;
use bevy_ecs::{
entity::Entity,
query::With,
system::{Commands, Query, Res, ResMut},
};
use bevy_image::prelude::*;
Expand All @@ -14,7 +15,8 @@ use bevy_render::sync_world::TemporaryRenderEntity;
use bevy_render::Extract;
use bevy_sprite::{Anchor, Text2dShadow};
use bevy_text::{
ComputedTextBlock, PositionedGlyph, TextBackgroundColor, TextBounds, TextColor, TextLayoutInfo,
ComputedTextBlock, PositionedGlyph, Strikethrough, TextBackgroundColor, TextBounds, TextColor,
TextLayoutInfo,
};
use bevy_transform::prelude::GlobalTransform;

Expand All @@ -39,6 +41,7 @@ pub fn extract_text2d_sprite(
>,
text_colors: Extract<Query<&TextColor>>,
text_background_colors_query: Extract<Query<&TextBackgroundColor>>,
strikethrough_query: Extract<Query<&TextColor, With<Strikethrough>>>,
) {
let mut start = extracted_slices.slices.len();
let mut end = start + 1;
Expand Down Expand Up @@ -68,7 +71,8 @@ pub fn extract_text2d_sprite(

let top_left = (Anchor::TOP_LEFT.0 - anchor.as_vec()) * size;

for &(section_entity, rect) in text_layout_info.section_rects.iter() {
for &(section_index, rect, _, _) in text_layout_info.section_geometry.iter() {
let section_entity = computed_block.entities()[section_index].entity;
let Ok(text_background_color) = text_background_colors_query.get(section_entity) else {
continue;
};
Expand Down Expand Up @@ -144,6 +148,34 @@ pub fn extract_text2d_sprite(

end += 1;
}

for &(section_index, rect, strikethrough_y, stroke) in
text_layout_info.section_geometry.iter()
{
let section_entity = computed_block.entities()[section_index].entity;
let Ok(_) = strikethrough_query.get(section_entity) else {
continue;
};
let render_entity = commands.spawn(TemporaryRenderEntity).id();
let offset = Vec2::new(rect.center().x, -strikethrough_y - 0.5 * stroke);
let transform =
shadow_transform * GlobalTransform::from_translation(offset.extend(0.));
extracted_sprites.sprites.push(ExtractedSprite {
main_entity,
render_entity,
transform,
color,
image_handle_id: AssetId::default(),
flip_x: false,
flip_y: false,
kind: ExtractedSpriteKind::Single {
anchor: Vec2::ZERO,
rect: None,
scaling_mode: None,
custom_size: Some(Vec2::new(rect.size().x, stroke)),
},
});
}
}

let transform =
Expand Down Expand Up @@ -206,5 +238,35 @@ pub fn extract_text2d_sprite(

end += 1;
}

for &(section_index, rect, strikethrough_y, stroke) in
text_layout_info.section_geometry.iter()
{
let section_entity = computed_block.entities()[section_index].entity;
let Ok(text_color) = strikethrough_query.get(section_entity) else {
continue;
};
let render_entity = commands.spawn(TemporaryRenderEntity).id();
let offset = Vec2::new(rect.center().x, -strikethrough_y - 0.5 * stroke);
let transform = *global_transform
* GlobalTransform::from_translation(top_left.extend(0.))
* scaling
* GlobalTransform::from_translation(offset.extend(0.));
extracted_sprites.sprites.push(ExtractedSprite {
main_entity,
render_entity,
transform,
color: text_color.0.into(),
image_handle_id: AssetId::default(),
flip_x: false,
flip_y: false,
kind: ExtractedSpriteKind::Single {
anchor: Vec2::ZERO,
rect: None,
scaling_mode: None,
custom_size: Some(Vec2::new(rect.size().x, stroke)),
},
});
}
}
}
3 changes: 2 additions & 1 deletion crates/bevy_text/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ pub use text_access::*;
pub mod prelude {
#[doc(hidden)]
pub use crate::{
Font, Justify, LineBreak, TextColor, TextError, TextFont, TextLayout, TextSpan,
Font, Justify, LineBreak, Strikethrough, TextColor, TextError, TextFont, TextLayout,
TextSpan,
};
}

Expand Down
47 changes: 36 additions & 11 deletions crates/bevy_text/src/pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ pub struct TextPipeline {
/// See [this dark magic](https://users.rust-lang.org/t/how-to-cache-a-vectors-capacity/94478/10).
spans_buffer: Vec<(usize, &'static str, &'static TextFont, FontFaceInfo)>,
/// Buffered vec for collecting info for glyph assembly.
glyph_info: Vec<(AssetId<Font>, FontSmoothing)>,
glyph_info: Vec<(AssetId<Font>, FontSmoothing, f32, f32, f32)>,
}

impl TextPipeline {
Expand Down Expand Up @@ -238,7 +238,7 @@ impl TextPipeline {
swash_cache: &mut SwashCache,
) -> Result<(), TextError> {
layout_info.glyphs.clear();
layout_info.section_rects.clear();
layout_info.section_geometry.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 All @@ -248,7 +248,13 @@ impl TextPipeline {
let mut glyph_info = core::mem::take(&mut self.glyph_info);
glyph_info.clear();
let text_spans = text_spans.inspect(|(_, _, _, text_font, _)| {
glyph_info.push((text_font.font.id(), text_font.font_smoothing));
glyph_info.push((
text_font.font.id(),
text_font.font_smoothing,
text_font.font_size,
0.,
0.,
));
});

let update_result = self.update_buffer(
Expand All @@ -266,6 +272,20 @@ impl TextPipeline {

update_result?;

for (font, _, size, strike_offset, stroke) in self.glyph_info.iter_mut() {
let Some((id, _)) = self.map_handle_to_font_id.get(font) else {
continue;
};
if let Some(font) = font_system.get_font(*id) {
let swash = font.as_swash();
let metrics = swash.metrics(&[]);
let upem = metrics.units_per_em as f32;
let scalar = *size * scale_factor as f32 / upem;
*strike_offset = (metrics.strikeout_offset * scalar).round();
*stroke = (metrics.stroke_size * scalar).round().max(1.);
}
}

let buffer = &mut computed.buffer;
let box_size = buffer_dimensions(buffer);

Expand All @@ -281,14 +301,16 @@ impl TextPipeline {
match current_section {
Some(section) => {
if section != layout_glyph.metadata {
layout_info.section_rects.push((
computed.entities[section].entity,
layout_info.section_geometry.push((
section,
Rect::new(
start,
run.line_top,
end,
run.line_top + run.line_height,
),
(run.line_y - self.glyph_info[section].3).round(),
self.glyph_info[section].4,
));
start = end.max(layout_glyph.x);
current_section = Some(layout_glyph.metadata);
Expand Down Expand Up @@ -374,9 +396,11 @@ impl TextPipeline {
Ok(())
});
if let Some(section) = current_section {
layout_info.section_rects.push((
computed.entities[section].entity,
layout_info.section_geometry.push((
section,
Rect::new(start, run.line_top, end, run.line_top + run.line_height),
(run.line_y - self.glyph_info[section].3).round(),
self.glyph_info[section].4,
));
}

Expand Down Expand Up @@ -457,9 +481,9 @@ pub struct TextLayoutInfo {
pub scale_factor: f32,
/// 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)>,
/// Geometry of each text segment: (section index, bounding rect, strikeout offset, strikeout stroke thickness)
/// A text section spanning more than one line will have multiple segments.
pub section_geometry: Vec<(usize, Rect, f32, f32)>,
/// The glyphs resulting size
pub size: Vec2,
}
Expand Down Expand Up @@ -516,10 +540,11 @@ pub fn load_font_to_fontdb(
// TODO: it is assumed this is the right font face
let face_id = *ids.last().unwrap();
let face = font_system.db().face(face_id).unwrap();
let family_name = Arc::from(face.families[0].0.as_str());

let family_name = Arc::from(face.families[0].0.as_str());
(face_id, family_name)
});

let face = font_system.db().face(*face_id).unwrap();

FontFaceInfo {
Expand Down
5 changes: 5 additions & 0 deletions crates/bevy_text/src/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,11 @@ pub enum LineBreak {
NoWrap,
}

/// A text entity with this component is drawn with strikethrough.
#[derive(Component, Copy, Clone, Debug, Reflect, Default, Serialize, Deserialize)]
#[reflect(Serialize, Deserialize, Clone, Default)]
pub struct Strikethrough;

/// Determines which antialiasing method to use when rendering text. By default, text is
/// rendered with grayscale antialiasing, but this can be changed to achieve a pixelated look.
///
Expand Down
Loading