mirror of
https://github.com/bevyengine/bevy.git
synced 2026-05-06 06:06:42 -04:00
6ca4769128
# Objective
Add responsive font sizes supporting rem and viewport units to
`bevy_text` with minimal changes to the APIs and systems.
## Solution
Introduce a new `FontSize` enum:
```rust
pub enum FontSize {
/// Font Size in logical pixels.
Px(f32),
/// Font size as a percentage of the viewport width.
Vw(f32),
/// Font size as a percentage of the viewport height.
Vh(f32),
/// Font size as a percentage of the smaller of the viewport width and height.
VMin(f32),
/// Font size as a percentage of the larger of the viewport width and height.
VMax(f32),
/// Font Size relative to the value of the `RemSize` resource.
Rem(f32),
}
```
This replaces the `f32` value of `TextFont`'s `font_size` field.
The viewport variants work the same way as their respective `Val`
counterparts.
`Rem` values are multiplied by the value of the `RemSize` resource
(which newtypes an `f32`).
`FontSize` provides an `eval` method that takes a logical viewport size
and rem base size and returns an `f32` logical font size. The resolved
logical font size is then written into the `Attributes` passed to Cosmic
Text by `TextPipeline::update_buffer`.
Any text implementation using `bevy_text` must now provide viewport and
rem base values when calling `TextPipeline::update_buffer` or
`create_measure`.
`Text2d` uses the size of the primary window to resolve viewport values
(or `Vec2::splat(1000)` if no primary window is found). This is a
deliberate compromise, a single `Text2d` can be rendered to multiple
viewports using `RenderLayers`, so it's difficult to find a rule for
which viewport size should be chosen.
### Change detection
`ComputedTextBlock` has two new fields: `uses_viewport_sizes` and
`uses_rem_sizes`, which are set to true in `TextPipeline::update_buffer`
iff any text section in the block uses viewport or rem font sizes,
respectively.
The `ComputedTextBlock::needs_rerender` method has been modified to take
take two bool parameters:
```rust
pub fn needs_rerender(
&self,
is_viewport_size_changed: bool,
is_rem_size_changed: bool,
) -> bool {
self.needs_rerender
|| (is_viewport_size_changed && self.uses_viewport_sizes)
|| (is_rem_size_changed && self.uses_rem_sizes)
}
```
This ensures that text reupdates will also be scheduled if one of the text section's uses a viewport font size and the local viewport size changed, or if one of the text section's uses a rem font size and the rem size changed.
#### Limitations
There are some limitations because we don't have any sort of font style inheritance yet:
* "rem" units aren't proper rem units, and just based on the value of a resource.
* "em" units are resolved based on inherited font size, so can't be implemented without inheritance support.
#### Notes
* This PR is quite small and not very technical. Reviewers don't need to be especially familiar with `bevy_text`. Most of the changes are to the examples.
* We could consider using `Val` instead of `FontSize`, then we could use `Val`'s constructor functions which would be much nicer, but some variants might not have sensible interpretations in both UI and Text2d contexts. Also we'd have to make `Val` accessible to `bevy_text`.
## Testing
The changes to the text systems are relatively trivial and easy to understand. I already added a minor change to the `text` example to use `Vh` font size for the "hello bevy" text in the bottom right corner. If you change the size of the window, you should see the text change size in response. The text initially flickers before it updates because of some unrelated asset/image changes that mean that font textures aren't ready until the frame after the text update that changes the font size.
Most of the example migrations were automated using regular expressions, and there are bound to be mistakes in those changes. It's infeasible to check every single example thoroughly, but it's early enough in the release cycle that I don't think we should be too worried if a few bugs slip in.
---------
Co-authored-by: Kevin Chen <chen.kevin.f@gmail.com>
195 lines
5.8 KiB
Rust
195 lines
5.8 KiB
Rust
//! Simple widgets for example UI.
|
|
//!
|
|
//! Unlike other examples, which demonstrate an application, this demonstrates a plugin library.
|
|
|
|
use bevy::prelude::*;
|
|
|
|
/// An event that's sent whenever the user changes one of the settings by
|
|
/// clicking a radio button.
|
|
#[derive(Clone, Message, Deref, DerefMut)]
|
|
pub struct WidgetClickEvent<T>(T);
|
|
|
|
/// A marker component that we place on all widgets that send
|
|
/// [`WidgetClickEvent`]s of the given type.
|
|
#[derive(Clone, Component, Deref, DerefMut)]
|
|
pub struct WidgetClickSender<T>(pub T)
|
|
where
|
|
T: Clone + Send + Sync + 'static;
|
|
|
|
/// A marker component that we place on all radio `Button`s.
|
|
#[derive(Clone, Copy, Component)]
|
|
pub struct RadioButton;
|
|
|
|
/// A marker component that we place on all `Text` inside radio buttons.
|
|
#[derive(Clone, Copy, Component)]
|
|
pub struct RadioButtonText;
|
|
|
|
/// The size of the border that surrounds buttons.
|
|
pub const BUTTON_BORDER: UiRect = UiRect::all(Val::Px(1.0));
|
|
|
|
/// The color of the border that surrounds buttons.
|
|
pub const BUTTON_BORDER_COLOR: BorderColor = BorderColor {
|
|
left: Color::WHITE,
|
|
right: Color::WHITE,
|
|
top: Color::WHITE,
|
|
bottom: Color::WHITE,
|
|
};
|
|
|
|
/// The amount of rounding to apply to button corners.
|
|
pub const BUTTON_BORDER_RADIUS_SIZE: Val = Val::Px(6.0);
|
|
|
|
/// The amount of space between the edge of the button and its label.
|
|
pub const BUTTON_PADDING: UiRect = UiRect::axes(Val::Px(12.0), Val::Px(6.0));
|
|
|
|
/// Returns a [`Node`] appropriate for the outer main UI node.
|
|
///
|
|
/// This UI is in the bottom left corner and has flex column support
|
|
pub fn main_ui_node() -> Node {
|
|
Node {
|
|
flex_direction: FlexDirection::Column,
|
|
position_type: PositionType::Absolute,
|
|
row_gap: px(6),
|
|
left: px(10),
|
|
bottom: px(10),
|
|
..default()
|
|
}
|
|
}
|
|
|
|
/// Spawns a single radio button that allows configuration of a setting.
|
|
///
|
|
/// The type parameter specifies the value that will be packaged up and sent in
|
|
/// a [`WidgetClickEvent`] when the radio button is clicked.
|
|
pub fn option_button<T>(
|
|
option_value: T,
|
|
option_name: &str,
|
|
is_selected: bool,
|
|
is_first: bool,
|
|
is_last: bool,
|
|
) -> impl Bundle
|
|
where
|
|
T: Clone + Send + Sync + 'static,
|
|
{
|
|
let (bg_color, fg_color) = if is_selected {
|
|
(Color::WHITE, Color::BLACK)
|
|
} else {
|
|
(Color::BLACK, Color::WHITE)
|
|
};
|
|
|
|
// Add the button node.
|
|
(
|
|
Button,
|
|
Node {
|
|
border: BUTTON_BORDER.with_left(if is_first { px(1) } else { px(0) }),
|
|
justify_content: JustifyContent::Center,
|
|
align_items: AlignItems::Center,
|
|
padding: BUTTON_PADDING,
|
|
border_radius: BorderRadius::ZERO
|
|
.with_left(if is_first {
|
|
BUTTON_BORDER_RADIUS_SIZE
|
|
} else {
|
|
px(0)
|
|
})
|
|
.with_right(if is_last {
|
|
BUTTON_BORDER_RADIUS_SIZE
|
|
} else {
|
|
px(0)
|
|
}),
|
|
..default()
|
|
},
|
|
BUTTON_BORDER_COLOR,
|
|
BackgroundColor(bg_color),
|
|
RadioButton,
|
|
WidgetClickSender(option_value.clone()),
|
|
children![(
|
|
ui_text(option_name, fg_color),
|
|
RadioButtonText,
|
|
WidgetClickSender(option_value),
|
|
)],
|
|
)
|
|
}
|
|
|
|
/// Spawns the buttons that allow configuration of a setting.
|
|
///
|
|
/// The user may change the setting to any one of the labeled `options`. The
|
|
/// value of the given type parameter will be packaged up and sent as a
|
|
/// [`WidgetClickEvent`] when one of the radio buttons is clicked.
|
|
pub fn option_buttons<T>(title: &str, options: &[(T, &str)]) -> impl Bundle
|
|
where
|
|
T: Clone + Send + Sync + 'static,
|
|
{
|
|
let buttons = options
|
|
.iter()
|
|
.cloned()
|
|
.enumerate()
|
|
.map(|(option_index, (option_value, option_name))| {
|
|
option_button(
|
|
option_value,
|
|
option_name,
|
|
option_index == 0,
|
|
option_index == 0,
|
|
option_index == options.len() - 1,
|
|
)
|
|
})
|
|
.collect::<Vec<_>>();
|
|
// Add the parent node for the row.
|
|
(
|
|
Node {
|
|
align_items: AlignItems::Center,
|
|
..default()
|
|
},
|
|
Children::spawn((
|
|
Spawn((
|
|
ui_text(title, Color::WHITE),
|
|
Node {
|
|
width: px(150),
|
|
..default()
|
|
},
|
|
)),
|
|
SpawnIter(buttons.into_iter()),
|
|
)),
|
|
)
|
|
}
|
|
|
|
/// Creates a text bundle for the UI.
|
|
pub fn ui_text(label: &str, color: Color) -> impl Bundle + use<> {
|
|
(
|
|
Text::new(label),
|
|
TextFont {
|
|
font_size: FontSize::Px(18.0),
|
|
..default()
|
|
},
|
|
TextColor(color),
|
|
)
|
|
}
|
|
|
|
/// Checks for clicks on the radio buttons and sends `RadioButtonChangeEvent`s
|
|
/// as necessary.
|
|
pub fn handle_ui_interactions<T>(
|
|
mut interactions: Query<(&Interaction, &WidgetClickSender<T>), With<Button>>,
|
|
mut widget_click_events: MessageWriter<WidgetClickEvent<T>>,
|
|
) where
|
|
T: Clone + Send + Sync + 'static,
|
|
{
|
|
for (interaction, click_event) in interactions.iter_mut() {
|
|
if *interaction == Interaction::Pressed {
|
|
widget_click_events.write(WidgetClickEvent((**click_event).clone()));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Updates the style of the button part of a radio button to reflect its
|
|
/// selected status.
|
|
pub fn update_ui_radio_button(background_color: &mut BackgroundColor, selected: bool) {
|
|
background_color.0 = if selected { Color::WHITE } else { Color::BLACK };
|
|
}
|
|
|
|
/// Updates the color of the label of a radio button to reflect its selected
|
|
/// status.
|
|
pub fn update_ui_radio_button_text(entity: Entity, writer: &mut TextUiWriter, selected: bool) {
|
|
let text_color = if selected { Color::BLACK } else { Color::WHITE };
|
|
|
|
writer.for_each_color(entity, |mut color| {
|
|
color.0 = text_color;
|
|
});
|
|
}
|