Skip to content

Commit dc313c0

Browse files
Implements text align and optional width for VelloTextSection (#137)
Should close one of or both of the following issues: - #50 - #51 Updates the example to demonstrate `VelloTextAlign::Justified` as well. ![image](https://github.com/user-attachments/assets/f14365e0-1c15-4bcf-b583-f2c869bee27a)
1 parent b0ab058 commit dc313c0

File tree

5 files changed

+145
-50
lines changed

5 files changed

+145
-50
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ This release supports Bevy version 0.15 and has an [MSRV][] of 1.85.
2222
- A `parley::FontContext` and `parley::LayoutContext` has been added in a lazy load multi threaded capacity.
2323
- Replaces `Rubik-Medium.ttf` from the `text` example with variable font `RobotoFlex-VariableFont_GRAD,XOPQ,XTRA,YOPQ,YTAS,YTDE,YTFI,YTLC,YTUC,opsz,slnt,wdth,wght.ttf`.
2424
- Updates the `text` example with an interactive, keyboard controlled, variable font example.
25+
- Adds `text_align` and `width` to `VelloTextSection` for controlling text alignment and width.
26+
- Adds `VelloTextAlign` enum for controlling text alignment.
2527

2628
## [0.7.1] - 2025-03-12
2729

examples/text/src/main.rs

+75-44
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ use bevy::{
33
prelude::*,
44
ui::ContentSize,
55
};
6-
use bevy_vello::{VelloPlugin, prelude::*, text::VelloTextAnchor};
6+
use bevy_vello::{
7+
VelloPlugin,
8+
prelude::*,
9+
text::{VelloTextAlign, VelloTextAnchor},
10+
};
711

812
const EMBEDDED_FONT: &str = "embedded://text/assets/RobotoFlex-VariableFont_GRAD,XOPQ,XTRA,YOPQ,YTAS,YTDE,YTFI,YTLC,YTUC,opsz,slnt,wdth,wght.ttf";
913

@@ -32,35 +36,61 @@ fn setup_camera(mut commands: Commands) {
3236
}
3337

3438
fn setup_worldspace_text(mut commands: Commands, asset_server: ResMut<AssetServer>) {
39+
let brush = vello::peniko::Brush::Solid(vello::peniko::Color::WHITE);
40+
41+
commands.spawn((
42+
VelloTextBundle {
43+
text: VelloTextSection {
44+
value: "bevy_vello using RobotoFlex-VariableFont".to_string(),
45+
style: VelloTextStyle {
46+
font: asset_server.load(EMBEDDED_FONT),
47+
brush: brush.clone(),
48+
line_height: 1.5,
49+
word_spacing: 2.0,
50+
letter_spacing: 2.0,
51+
font_size: 32.0,
52+
..default()
53+
},
54+
..default()
55+
},
56+
text_anchor: VelloTextAnchor::Center,
57+
transform: Transform::from_xyz(0.0, 150.0, 0.0),
58+
..default()
59+
},
60+
WithAnimatedFont,
61+
));
62+
3563
commands.spawn(VelloTextBundle {
3664
text: VelloTextSection {
3765
value: "bevy_vello using Bevy's default font".to_string(),
3866
style: VelloTextStyle {
3967
font_size: 24.0,
4068
..default()
4169
},
70+
..default()
4271
},
4372
text_anchor: VelloTextAnchor::Center,
44-
transform: Transform::from_xyz(0.0, -100.0, 0.0),
73+
transform: Transform::from_xyz(0.0, 40.0, 0.0),
4574
..default()
4675
});
4776

48-
let brush = vello::peniko::Brush::Solid(vello::peniko::Color::WHITE);
49-
5077
commands.spawn(VelloTextBundle {
5178
text: VelloTextSection {
52-
value: "bevy_vello using RobotoFlex-VariableFont".to_string(),
79+
value: "Justified text along a width\nbut the last line is not justified".to_string(),
80+
text_align: VelloTextAlign::Justified,
81+
width: Some(720.0),
5382
style: VelloTextStyle {
5483
font: asset_server.load(EMBEDDED_FONT),
5584
brush,
5685
line_height: 1.5,
5786
word_spacing: 2.0,
5887
letter_spacing: 2.0,
59-
font_size: 48.0,
88+
font_size: 32.0,
6089
..default()
6190
},
6291
},
6392
text_anchor: VelloTextAnchor::Center,
93+
transform: Transform::from_xyz(0.0, -100.0, 0.0),
6494
..default()
6595
});
6696
}
@@ -121,9 +151,12 @@ fn toggle_animations(
121151

122152
const ANIMATION_SPEED: f32 = 5.0;
123153

154+
#[derive(Component)]
155+
struct WithAnimatedFont;
156+
124157
fn animate_axes(
125158
time: Res<Time>,
126-
mut query: Query<&mut VelloTextSection>,
159+
mut text_section: Single<&mut VelloTextSection, With<WithAnimatedFont>>,
127160
animation_toggles: Res<AnimationToggles>,
128161
) {
129162
let sin_time = (time.elapsed_secs() * ANIMATION_SPEED)
@@ -144,54 +177,52 @@ fn animate_axes(
144177
let descender_depth = sin_time.remap(0., 1., -98., -305.);
145178
let figure_height = sin_time.remap(0., 1., 560., 788.);
146179

147-
for mut text_section in query.iter_mut() {
148-
if animation_toggles.weight {
149-
text_section.style.font_axes.weight = Some(font_weight);
150-
}
180+
if animation_toggles.weight {
181+
text_section.style.font_axes.weight = Some(font_weight);
182+
}
151183

152-
if animation_toggles.width {
153-
text_section.style.font_axes.width = Some(font_width);
154-
}
184+
if animation_toggles.width {
185+
text_section.style.font_axes.width = Some(font_width);
186+
}
155187

156-
if animation_toggles.slant {
157-
text_section.style.font_axes.slant = Some(slant);
158-
}
188+
if animation_toggles.slant {
189+
text_section.style.font_axes.slant = Some(slant);
190+
}
159191

160-
if animation_toggles.grade {
161-
text_section.style.font_axes.grade = Some(grade);
162-
}
192+
if animation_toggles.grade {
193+
text_section.style.font_axes.grade = Some(grade);
194+
}
163195

164-
if animation_toggles.thick_stroke {
165-
text_section.style.font_axes.thick_stroke = Some(thick_stroke);
166-
}
196+
if animation_toggles.thick_stroke {
197+
text_section.style.font_axes.thick_stroke = Some(thick_stroke);
198+
}
167199

168-
if animation_toggles.thin_stroke {
169-
text_section.style.font_axes.thin_stroke = Some(thin_stroke);
170-
}
200+
if animation_toggles.thin_stroke {
201+
text_section.style.font_axes.thin_stroke = Some(thin_stroke);
202+
}
171203

172-
if animation_toggles.counter_width {
173-
text_section.style.font_axes.counter_width = Some(counter_width);
174-
}
204+
if animation_toggles.counter_width {
205+
text_section.style.font_axes.counter_width = Some(counter_width);
206+
}
175207

176-
if animation_toggles.uppercase_height {
177-
text_section.style.font_axes.uppercase_height = Some(uppercase_height);
178-
}
208+
if animation_toggles.uppercase_height {
209+
text_section.style.font_axes.uppercase_height = Some(uppercase_height);
210+
}
179211

180-
if animation_toggles.lowercase_height {
181-
text_section.style.font_axes.lowercase_height = Some(lowercase_height);
182-
}
212+
if animation_toggles.lowercase_height {
213+
text_section.style.font_axes.lowercase_height = Some(lowercase_height);
214+
}
183215

184-
if animation_toggles.ascender_height {
185-
text_section.style.font_axes.ascender_height = Some(ascender_height);
186-
}
216+
if animation_toggles.ascender_height {
217+
text_section.style.font_axes.ascender_height = Some(ascender_height);
218+
}
187219

188-
if animation_toggles.descender_depth {
189-
text_section.style.font_axes.descender_depth = Some(descender_depth);
190-
}
220+
if animation_toggles.descender_depth {
221+
text_section.style.font_axes.descender_depth = Some(descender_depth);
222+
}
191223

192-
if animation_toggles.figure_height {
193-
text_section.style.font_axes.figure_height = Some(figure_height);
194-
}
224+
if animation_toggles.figure_height {
225+
text_section.style.font_axes.figure_height = Some(figure_height);
195226
}
196227
}
197228

src/text/font.rs

+28-5
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,21 @@ impl VelloFont {
6767
)));
6868

6969
let mut layout = builder.build(&text_section.value);
70-
layout.break_all_lines(None);
71-
72-
Vec2::new(layout.width(), layout.height())
70+
let max_advance = text_section.width;
71+
layout.break_all_lines(max_advance);
72+
layout.align(
73+
max_advance,
74+
text_section.text_align.into(),
75+
parley::AlignmentOptions::default(),
76+
);
77+
78+
let width = if text_section.width.is_some() {
79+
text_section.width.unwrap()
80+
} else {
81+
layout.width()
82+
};
83+
84+
Vec2::new(width, layout.height())
7385
})
7486
})
7587
}
@@ -100,9 +112,20 @@ impl VelloFont {
100112
)));
101113

102114
let mut layout = builder.build(&text_section.value);
103-
layout.break_all_lines(None);
115+
let max_advance = text_section.width;
116+
layout.break_all_lines(max_advance);
117+
layout.align(
118+
max_advance,
119+
text_section.text_align.into(),
120+
parley::AlignmentOptions::default(),
121+
);
122+
123+
let width = if text_section.width.is_some() {
124+
text_section.width.unwrap()
125+
} else {
126+
layout.width()
127+
} as f64;
104128

105-
let width = layout.width() as f64;
106129
let height = layout.height() as f64;
107130

108131
// NOTE: Parley aligns differently than our previous skrifa implementation

src/text/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ mod vello_text;
77

88
pub use font::VelloFont;
99
pub(crate) use font_loader::{VelloFontLoader, load_into_font_context};
10-
pub use vello_text::{VelloTextAnchor, VelloTextSection, VelloTextStyle};
10+
pub use vello_text::{VelloTextAlign, VelloTextAnchor, VelloTextSection, VelloTextStyle};

src/text/vello_text.rs

+39
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ use vello::peniko::{self, Brush};
77
pub struct VelloTextSection {
88
pub value: String,
99
pub style: VelloTextStyle,
10+
pub text_align: VelloTextAlign,
11+
pub width: Option<f32>,
1012
}
1113

1214
#[derive(Clone)]
@@ -140,6 +142,43 @@ pub enum VelloTextAnchor {
140142
TopRight,
141143
}
142144

145+
/// Alignment of a parley layout.
146+
#[derive(Default, Clone, Copy, PartialEq, Eq)]
147+
pub enum VelloTextAlign {
148+
/// This is [`parley::Alignment::Left`] for LTR text and [`parley::Alignment::Right`] for RTL text.
149+
#[default]
150+
Start,
151+
/// This is [`parley::Alignment::Right`] for LTR text and [`parley::Alignment::Left`] for RTL text.
152+
End,
153+
/// Align content to the left edge.
154+
///
155+
/// For alignment that should be aware of text direction, use [`parley::Alignment::Start`] or
156+
/// [`parley::Alignment::End`] instead.
157+
Left,
158+
/// Align each line centered within the container.
159+
Middle,
160+
/// Align content to the right edge.
161+
///
162+
/// For alignment that should be aware of text direction, use [`parley::Alignment::Start`] or
163+
/// [`parley::Alignment::End`] instead.
164+
Right,
165+
/// Justify each line by spacing out content, except for the last line.
166+
Justified,
167+
}
168+
169+
impl From<VelloTextAlign> for parley::Alignment {
170+
fn from(value: VelloTextAlign) -> Self {
171+
match value {
172+
VelloTextAlign::Start => parley::Alignment::Start,
173+
VelloTextAlign::End => parley::Alignment::End,
174+
VelloTextAlign::Left => parley::Alignment::Left,
175+
VelloTextAlign::Middle => parley::Alignment::Middle,
176+
VelloTextAlign::Right => parley::Alignment::Right,
177+
VelloTextAlign::Justified => parley::Alignment::Justified,
178+
}
179+
}
180+
}
181+
143182
impl VelloTextSection {
144183
/// Returns the bounding box in world space
145184
pub fn bb_in_world_space(&self, font: &VelloFont, gtransform: &GlobalTransform) -> Rect {

0 commit comments

Comments
 (0)