bevy/examples/ui/text_wrap_debug.rs
Marco Buono 8e3db957c5
Add the ability to control font smoothing (#15368)
# Objective

- Fixes #10720
- Adds the ability to control font smoothing of rendered text

## Solution

- Introduce the `FontSmoothing` enum, with two possible variants
(`FontSmoothing::None` and `FontSmoothing::AntiAliased`):
- This is based on `-webkit-font-smoothing`, in line with our practice
of adopting CSS-like properties/names for UI;
- I could have gone instead for the [`font-smooth`
property](https://developer.mozilla.org/en-US/docs/Web/CSS/font-smooth)
that's also supported by browsers, but didn't since it's also
non-standard, has an uglier name, and doesn't allow controlling the type
of antialias applied.
- Having an enum instead of e.g. a boolean, leaves the path open for
adding `FontSmoothing::SubpixelAntiAliased` in the future, without a
breaking change;
- Add all the necessary plumbing to get the `FontSmoothing` information
to where we rasterize the glyphs and store them in the atlas;
- Change the font atlas key to also take into account the smoothing
setting, not only font and font size;
- Since COSMIC Text [doesn't support controlling font
smoothing](https://github.com/pop-os/cosmic-text/issues/279), we roll
out our own threshold-based “implementation”:
- This has the downside of **looking ugly for “regular” vector fonts**
⚠️, since it doesn't properly take the hinting information into account
like a proper implementation on the rasterizer side would.
- However, **for fonts that have been specifically authored to be pixel
fonts, (a common use case in games!) this is not as big of a problem**,
since all lines are vertical/horizontal, and close to the final pixel
boundaries (as long as the font is used at a multiple of the size
originally intended by the author)
- Once COSMIC exposes this functionality, we can switch to using it
directly, and get better results;
- Use a nearest neighbor sampler for atlases with font smoothing
disabled, so that you can scale the text via transform and still get the
pixelated look;
- Add a convenience method to `Text` for setting the font smoothing;
- Add a demonstration of using the `FontSmoothing` property to the
`text2d` example.

## Testing

- Did you test these changes? If so, how?
  - Yes. Via the `text2d`example, and also in my game.
- Are there any parts that need more testing?
- I'd like help from someone for testing this on devices/OSs with
fractional scaling (Android/Windows)
- How can other people (reviewers) test your changes? Is there anything
specific they need to know?
- Both via the `text2d` example and also by using it directly on your
projects.
- If relevant, what platforms did you test these changes on, and are
there any important ones you can't test?
  - macOS

---

## Showcase

```rust
commands.spawn(Text2dBundle {
    text: Text::from_section("Hello, World!", default())
        .with_font_smoothing(FontSmoothing::None),
    ..default()
});
```
![Screenshot 2024-09-22 at 12 33
39](https://github.com/user-attachments/assets/93e19672-b8c0-4cba-a8a3-4525fe2ae1cb)

<img width="740" alt="image"
src="https://github.com/user-attachments/assets/b881b02c-4e43-410b-902f-6985c25140fc">

## Migration Guide

- `Text` now contains a `font_smoothing: FontSmoothing` property, make
sure to include it or add `..default()` when using the struct directly;
- `FontSizeKey` has been renamed to `FontAtlasKey`, and now also
contains the `FontSmoothing` setting;
- The following methods now take an extra `font_smoothing:
FontSmoothing` argument:
  - `FontAtlas::new()`
  - `FontAtlasSet::add_glyph_to_atlas()`
  - `FontAtlasSet::get_glyph_atlas_info()`
  - `FontAtlasSet::get_outlined_glyph_texture()`
2024-09-23 17:28:25 +00:00

147 lines
4.9 KiB
Rust

//! This example demonstrates text wrapping and use of the `LineBreakOn` property.
use argh::FromArgs;
use bevy::prelude::*;
use bevy::text::BreakLineOn;
use bevy::window::WindowResolution;
use bevy::winit::WinitSettings;
#[derive(FromArgs, Resource)]
/// `text_wrap_debug` demonstrates text wrapping and use of the `LineBreakOn` property
struct Args {
#[argh(option)]
/// window scale factor
scale_factor: Option<f32>,
#[argh(option, default = "1.")]
/// ui scale factor
ui_scale: f32,
}
fn main() {
// `from_env` panics on the web
#[cfg(not(target_arch = "wasm32"))]
let args: Args = argh::from_env();
#[cfg(target_arch = "wasm32")]
let args = Args::from_args(&[], &[]).unwrap();
let window = if let Some(scale_factor) = args.scale_factor {
Window {
resolution: WindowResolution::default().with_scale_factor_override(scale_factor),
..Default::default()
}
} else {
Window::default()
};
App::new()
.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(window),
..Default::default()
}))
.insert_resource(WinitSettings::desktop_app())
.insert_resource(UiScale(args.ui_scale))
.add_systems(Startup, spawn)
.run();
}
fn spawn(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn(Camera2dBundle::default());
let text_style = TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 12.0,
..default()
};
let root = commands
.spawn(NodeBundle {
style: Style {
width: Val::Percent(100.),
height: Val::Percent(100.),
flex_direction: FlexDirection::Column,
..Default::default()
},
background_color: Color::BLACK.into(),
..Default::default()
})
.id();
for linebreak_behavior in [
BreakLineOn::AnyCharacter,
BreakLineOn::WordBoundary,
BreakLineOn::WordOrCharacter,
BreakLineOn::NoWrap,
] {
let row_id = commands
.spawn(NodeBundle {
style: Style {
flex_direction: FlexDirection::Row,
justify_content: JustifyContent::SpaceAround,
align_items: AlignItems::Center,
width: Val::Percent(100.),
height: Val::Percent(50.),
..Default::default()
},
..Default::default()
})
.id();
let justifications = vec![
JustifyContent::Center,
JustifyContent::FlexStart,
JustifyContent::FlexEnd,
JustifyContent::SpaceAround,
JustifyContent::SpaceBetween,
JustifyContent::SpaceEvenly,
];
for (i, justification) in justifications.into_iter().enumerate() {
let c = 0.3 + i as f32 * 0.1;
let column_id = commands
.spawn(NodeBundle {
style: Style {
justify_content: justification,
flex_direction: FlexDirection::Column,
width: Val::Percent(16.),
height: Val::Percent(95.),
overflow: Overflow::clip_x(),
..Default::default()
},
background_color: Color::srgb(0.5, c, 1.0 - c).into(),
..Default::default()
})
.id();
let messages = [
format!("JustifyContent::{justification:?}"),
format!("LineBreakOn::{linebreak_behavior:?}"),
"Line 1\nLine 2".to_string(),
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas auctor, nunc ac faucibus fringilla.".to_string(),
"pneumonoultramicroscopicsilicovolcanoconiosis".to_string()
];
for (j, message) in messages.into_iter().enumerate() {
let text = Text {
sections: vec![TextSection {
value: message.clone(),
style: text_style.clone(),
}],
justify: JustifyText::Left,
linebreak_behavior,
..default()
};
let text_id = commands
.spawn(TextBundle {
text,
background_color: Color::srgb(0.8 - j as f32 * 0.2, 0., 0.).into(),
..Default::default()
})
.id();
commands.entity(column_id).add_child(text_id);
}
commands.entity(row_id).add_child(column_id);
}
commands.entity(root).add_child(row_id);
}
}