Skip to content

Commit 24c5c0d

Browse files
author
Ericky Dos Santos
committed
feat: add letter_spacing support to text rendering
Add optional letter_spacing field to Text, Span, and Paragraph types. Default to 0.25px when unspecified via to_attributes_with_spacing(). Converts pixel values to EM units for cosmic_text compatibility.
1 parent 2db5545 commit 24c5c0d

14 files changed

Lines changed: 79 additions & 2 deletions

File tree

core/src/text.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ pub struct Text<Content = String, Font = crate::Font> {
4444

4545
/// The [`Wrapping`] strategy of the [`Text`].
4646
pub wrapping: Wrapping,
47+
48+
/// The letter spacing of the [`Text`] in pixels.
49+
///
50+
/// Extra horizontal space added between each character glyph.
51+
/// If `None`, no extra spacing is applied.
52+
pub letter_spacing: Option<f32>,
4753
}
4854

4955
/// The shaping strategy of some text.
@@ -274,6 +280,10 @@ pub struct Span<'a, Link = (), Font = crate::Font> {
274280
pub underline: bool,
275281
/// Whether the [`Span`] should be struck through or not.
276282
pub strikethrough: bool,
283+
/// The letter spacing of the [`Span`] in pixels.
284+
///
285+
/// Per-span override; falls back to paragraph default if `None`.
286+
pub letter_spacing: Option<f32>,
277287
}
278288

279289
/// A text highlight.
@@ -299,6 +309,7 @@ impl<'a, Link, Font> Span<'a, Link, Font> {
299309
padding: Padding::ZERO,
300310
underline: false,
301311
strikethrough: false,
312+
letter_spacing: None,
302313
}
303314
}
304315

@@ -429,6 +440,12 @@ impl<'a, Link, Font> Span<'a, Link, Font> {
429440
self
430441
}
431442

443+
/// Sets the letter spacing of the [`Span`] in pixels.
444+
pub fn letter_spacing(mut self, letter_spacing: f32) -> Self {
445+
self.letter_spacing = Some(letter_spacing);
446+
self
447+
}
448+
432449
/// Turns the [`Span`] into a static one.
433450
pub fn to_static(self) -> Span<'static, Link, Font> {
434451
Span {
@@ -442,6 +459,7 @@ impl<'a, Link, Font> Span<'a, Link, Font> {
442459
padding: self.padding,
443460
underline: self.underline,
444461
strikethrough: self.strikethrough,
462+
letter_spacing: self.letter_spacing,
445463
}
446464
}
447465
}

core/src/text/paragraph.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ impl<P: Paragraph> Plain<P> {
9696
vertical_alignment: text.vertical_alignment,
9797
shaping: text.shaping,
9898
wrapping: text.wrapping,
99+
letter_spacing: text.letter_spacing,
99100
}) {
100101
Difference::None => {}
101102
Difference::Bounds => {

core/src/widget/text.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ where
7474
shaping: Shaping,
7575
wrapping: Wrapping,
7676
class: Theme::Class<'a>,
77+
letter_spacing: Option<f32>,
7778
}
7879

7980
impl<'a, Theme, Renderer> Text<'a, Theme, Renderer>
@@ -96,6 +97,7 @@ where
9697
shaping: Shaping::default(),
9798
wrapping: Wrapping::default(),
9899
class: Theme::default(),
100+
letter_spacing: None,
99101
}
100102
}
101103

@@ -167,6 +169,14 @@ where
167169
self
168170
}
169171

172+
/// Sets the letter spacing of the [`Text`] in pixels.
173+
///
174+
/// Extra horizontal space added between each character glyph.
175+
pub fn letter_spacing(mut self, letter_spacing: f32) -> Self {
176+
self.letter_spacing = Some(letter_spacing);
177+
self
178+
}
179+
170180
/// Sets the style of the [`Text`].
171181
#[must_use]
172182
pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
@@ -251,6 +261,7 @@ where
251261
self.vertical_alignment,
252262
self.shaping,
253263
self.wrapping,
264+
self.letter_spacing,
254265
)
255266
}
256267

@@ -330,6 +341,7 @@ pub fn layout<Renderer>(
330341
vertical_alignment: alignment::Vertical,
331342
shaping: Shaping,
332343
wrapping: Wrapping,
344+
letter_spacing: Option<f32>,
333345
) -> layout::Node
334346
where
335347
Renderer: text::Renderer,
@@ -352,6 +364,7 @@ where
352364
vertical_alignment,
353365
shaping,
354366
wrapping,
367+
letter_spacing,
355368
});
356369

357370
paragraph.min_bounds()

graphics/src/text.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,24 @@ pub fn to_attributes(font: Font) -> cosmic_text::Attrs<'static> {
258258
.style(to_style(font.style))
259259
}
260260

261+
/// Returns the attributes of the given [`Font`] with optional letter spacing.
262+
///
263+
/// Letter spacing is specified in pixels and will be converted to EM units
264+
/// (relative to font size) for the underlying text engine.
265+
pub fn to_attributes_with_spacing(
266+
font: Font,
267+
letter_spacing_px: Option<f32>,
268+
font_size: f32,
269+
) -> cosmic_text::Attrs<'static> {
270+
let attrs = to_attributes(font);
271+
let px = letter_spacing_px.unwrap_or(0.25);
272+
if px != 0.0 && font_size > 0.0 {
273+
attrs.letter_spacing(px / font_size)
274+
} else {
275+
attrs
276+
}
277+
}
278+
261279
fn to_family(family: font::Family) -> cosmic_text::Family<'static> {
262280
match family {
263281
font::Family::Name(name) => cosmic_text::Family::Name(name),

graphics/src/text/paragraph.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ struct Internal {
2323
bounds: Size,
2424
min_bounds: Size,
2525
version: text::Version,
26+
letter_spacing: Option<f32>,
2627
}
2728

2829
impl Paragraph {
@@ -85,7 +86,7 @@ impl core::text::Paragraph for Paragraph {
8586
buffer.set_text(
8687
font_system.raw(),
8788
text.content,
88-
&text::to_attributes(text.font),
89+
&text::to_attributes_with_spacing(text.font, text.letter_spacing, f32::from(text.size)),
8990
text::to_shaping(text.shaping),
9091
None,
9192
);
@@ -102,6 +103,7 @@ impl core::text::Paragraph for Paragraph {
102103
bounds: text.bounds,
103104
min_bounds,
104105
version: font_system.version(),
106+
letter_spacing: text.letter_spacing,
105107
}))
106108
}
107109

@@ -128,7 +130,14 @@ impl core::text::Paragraph for Paragraph {
128130
buffer.set_rich_text(
129131
font_system.raw(),
130132
text.content.iter().enumerate().map(|(i, span)| {
131-
let attrs = text::to_attributes(span.font.unwrap_or(text.font));
133+
let span_font = span.font.unwrap_or(text.font);
134+
let span_size = span.size.unwrap_or(text.size);
135+
let span_letter_spacing = span.letter_spacing.or(text.letter_spacing);
136+
let attrs = text::to_attributes_with_spacing(
137+
span_font,
138+
span_letter_spacing,
139+
f32::from(span_size),
140+
);
132141

133142
let attrs = match (span.size, span.line_height) {
134143
(None, None) => attrs,
@@ -174,6 +183,7 @@ impl core::text::Paragraph for Paragraph {
174183
bounds: text.bounds,
175184
min_bounds,
176185
version: font_system.version(),
186+
letter_spacing: text.letter_spacing,
177187
}))
178188
}
179189

@@ -206,6 +216,7 @@ impl core::text::Paragraph for Paragraph {
206216
|| paragraph.wrapping != text.wrapping
207217
|| paragraph.horizontal_alignment != text.horizontal_alignment
208218
|| paragraph.vertical_alignment != text.vertical_alignment
219+
|| paragraph.letter_spacing != text.letter_spacing
209220
{
210221
core::text::Difference::Shape
211222
} else if paragraph.bounds != text.bounds {
@@ -405,6 +416,7 @@ impl Default for Internal {
405416
bounds: Size::ZERO,
406417
min_bounds: Size::ZERO,
407418
version: text::Version::default(),
419+
letter_spacing: None,
408420
}
409421
}
410422
}

wgpu/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,7 @@ impl Renderer {
410410
vertical_alignment: alignment::Vertical::Top,
411411
shaping: core::text::Shaping::Advanced,
412412
wrapping: core::text::Wrapping::Word,
413+
letter_spacing: None,
413414
};
414415

415416
renderer.fill_text(

widget/src/checkbox.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,7 @@ where
340340
alignment::Vertical::Top,
341341
self.text_shaping,
342342
self.text_wrapping,
343+
None,
343344
)
344345
},
345346
)
@@ -450,6 +451,7 @@ where
450451
vertical_alignment: alignment::Vertical::Center,
451452
shaping: *shaping,
452453
wrapping: text::Wrapping::default(),
454+
letter_spacing: None,
453455
},
454456
bounds.center(),
455457
style.icon_color,

widget/src/overlay/menu.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -544,6 +544,7 @@ where
544544
vertical_alignment: alignment::Vertical::Center,
545545
shaping: self.text_shaping,
546546
wrapping: text::Wrapping::default(),
547+
letter_spacing: None,
547548
},
548549
Point::new(bounds.x + self.padding.left, bounds.center_y()),
549550
if is_selected {

widget/src/pick_list.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,7 @@ where
383383
vertical_alignment: alignment::Vertical::Center,
384384
shaping: self.text_shaping,
385385
wrapping: self.text_wrap,
386+
letter_spacing: None,
386387
};
387388

388389
for (option, paragraph) in options.iter().zip(state.options.iter_mut())
@@ -657,6 +658,7 @@ where
657658
vertical_alignment: alignment::Vertical::Center,
658659
shaping,
659660
wrapping: wrap,
661+
letter_spacing: None,
660662
},
661663
Point::new(
662664
bounds.x + bounds.width - self.padding.right,
@@ -687,6 +689,7 @@ where
687689
vertical_alignment: alignment::Vertical::Center,
688690
shaping: self.text_shaping,
689691
wrapping: self.text_wrap,
692+
letter_spacing: None,
690693
},
691694
Point::new(bounds.x + self.padding.left, bounds.center_y()),
692695
if is_selected {

widget/src/radio.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,7 @@ where
316316
alignment::Vertical::Top,
317317
self.text_shaping,
318318
self.text_wrapping,
319+
None,
319320
)
320321
},
321322
)

0 commit comments

Comments
 (0)