-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Rich text #1245
Rich text #1245
Conversation
Very nice and works well! I'm a little unhappy by the fact that it makes the default case of displaying a simple text less simple, mostly that it now has to be a enum TextType {
Text(Text),
Sections(Vec<TextSection>)
} |
Text alignment needs to go somewhere at the block level. Here's a couple of different variations I've come up with:
From the latter, it might be possible to spread the So far I can't say that the enum approach has made things simpler, but it did lead me to the I'm gradually more convinced that the I also tried creating a trait and implementing it for |
I also did In retrospect, we can do this without enums at all, so we get the old API for text with one section, but just have to call https://github.com/tigregalis/bevy/tree/rich-text-from/crates/bevy_text/src/text.rs #[derive(Debug, Default, Clone)]
pub struct BasicText {
pub value: String,
pub font: Handle<Font>,
pub style: TextStyle,
pub alignment: TextAlignment,
}
impl From<BasicText> for Text {
fn from(source: BasicText) -> Self {
let BasicText {
value,
font,
style,
alignment,
} = source;
Self {
sections: vec![TextSection { value, font, style }],
alignment,
}
}
} commands.spawn(TextBundle {
text: BasicText {
value: "hello world!",
..Default::default()
}.into(),
..Default::default()
}); We can use a similar approach with a HTML-like API as in #1222. |
that looks nicer with the |
So at this stage we've got a working rich text API along with a slightly updated API for basic text with a single style, and which is also extensible in the future. What are our thoughts on either:
pub struct TextSection {
pub value: String,
pub font: Handle<Font>,
// pub style: TextStyle, // -
pub font_size: f32, // +
pub color: Color, // +
}
pub struct TextSection {
pub value: String,
// pub font: Handle<Font>, // -
pub style: TextStyle,
}
pub struct TextStyle {
pub font: Handle<Font>, // +
pub font_size: f32,
pub color: Color,
} I think now's the time to decide this as we're already changing the API as it is. In general, I favour option 1 because it's flatter/simpler (easier to write). One reason I can foresee for keeping a separate pub struct TextSection {
pub value: String,
pub style: Handle<TextStyle>,
}
pub struct TextStyle {
pub font: Handle<Font>,
pub font_size: f32,
pub color: Color,
} |
I prefer option 2, as it allows easier reuse of a style. I don't think moving the style to a
|
Ok, fair. I'm convinced. I've made the change to option 2. The new API is as follows: // Component
pub struct Text {
// pub value: String, // - (moved to `TextSection`)
// pub font: Handle<Font>, // - (moved to `TextStyle`)
// pub style: TextStyle, // - (moved to `TextSection`)
pub sections: Vec<TextSection>, // +
pub alignment: TextAlignment, // +
}
pub struct TextSection { // NEW
pub value: String, // +
pub style: TextStyle, // +
}
pub struct TextStyle {
pub font: Handle<Font>, // +
pub font_size: f32,
pub color: Color,
// pub alignment: TextAlignment, // - (moved to `Text`)
}
// Transient helper type. Construct it then call `.into()` to convert to a `Text` component.
pub struct BasicText { // NEW
pub value: String,
pub style: TextStyle,
pub alignment: TextAlignment,
}
// Text with a single section:
commands.spawn(TextBundle {
// construct a `BasicText`
text: BasicText {
value: "hello world!".to_string(),
style: TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 40.0,
color: Color::WHITE,
},
..Default::default()
}
.into(), // convert it to `Text`
..Default::default()
});
// Updating properties
for mut text in text_query.iter_mut() {
// this no longer works
// text.value = "hey hey".to_string();
// instead, update the only section
text.sections[0].value = "hey hey".to_string();
}
// Rich text with multiple sections:
commands.spawn(TextBundle {
// use `Text` directly
text: Text {
// construct a `Vec` of `TextSection`s
text: vec![
TextSection {
value: "hello ".to_string(),
style: TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 40.0,
color: Color::WHITE,
}
}
TextSection {
value: "world!".to_string(),
style: TextStyle {
font: asset_server.load("fonts/FiraMono-Medium.ttf"),
font_size: 60.0,
color: Color::GREEN,
}
}
],
..Default::default()
},
..Default::default()
});
// Updating properties
for mut text in text_query.iter_mut() {
text.sections[0].value = "hi ".to_string();
text.sections[1].value = "bevy".to_string();
text.sections[1].style.color = Color::BLUE;
} |
FWIW, my project is working great with this PR as-is. Was able to remove ~100 LOC and my text is no longer wiggly! |
Thanks for putting in the work here! This is definitely a huge step forward in usability. In general this all looks great. The only thing I want to discuss is the "single section text construction api". Its worth doing a side by side comparison of some of our options:
I think my personal preference is (1) + (5). People that want shorthand can use the constructor and people that want to be "idiomatic" or want to avoid unnecessary refactors when new sections are added can use the initializer syntax. |
I'm not personally bothered with either way, but is (4) viable if the current Being able to support other from impls like edit: Oh, maybe that's not elegant if plugin authors can't write those impls? Bit of a rust newbie over here. |
It would make that more palatable, but the pattern is still the same. My preference is still for (1) + (5) here.
I'm very open to the
That shouldn't be a problem, provided the plugin author has defined the MdText type in their crate. You just can't impl crate-external traits on crate-external types. |
In general, I don't have any strong preferences with respect to the "single section text construction api", so I'll happily defer to those with stronger opinions on it. However, some comments on each: (1) This approach will always be possible anyway, with this implementation, so I think the question is only whether we consider it idiomatic and use it in examples and documentation. (2) This feels strange to me. But I don't mind the idea of converting from a (2.5) However, for convenience you might provide a method like This would look like: Text::from(TextSection {
value: "hello world!".to_string(),
style: TextStyle {
font: asset_server.load("fira.ttf"),
font_size: 60.0,
color: Color::WHITE,
}
})
.with_alignment(TextAlignment {
vertical: VerticalAlign::Center,
horizontal: Horizontal::Center,
}) If you had this though, you'd likely also want to do the reverse (start with the alignment): Text::from(TextAlignment {
vertical: VerticalAlign::Center,
horizontal: Horizontal::Center,
})
.with_section(TextSection {
value: "hello world!".to_string(),
style: TextStyle {
font: asset_server.load("fira.ttf"),
font_size: 60.0,
color: Color::WHITE,
}
})
// and now this is exactly the same as (3) but with `new` renamed to `from` Do we want Text::default()
.with_alignment(TextAlignment {
vertical: VerticalAlign::Center,
horizontal: Horizontal::Center,
})
.with_section(TextSection {
value: "hello world!".to_string(),
style: TextStyle {
font: asset_server.load("fira.ttf"),
font_size: 60.0,
color: Color::WHITE,
}
}) The main benefit of this approach is that it extends to multiple section text very nicely: Text::default()
.with_alignment(TextAlignment {
vertical: VerticalAlign::Center,
horizontal: Horizontal::Center,
})
.with_section(TextSection {
value: "hello world!".to_string(),
style: TextStyle {
font: asset_server.load("fira.ttf"),
font_size: 60.0,
color: Color::WHITE,
}
})
.with_section(TextSection {
value: " and goodbye...".to_string(),
style: TextStyle {
font: asset_server.load("fira.ttf"),
font_size: 30.0,
color: Color::RED,
}
}) (3) I'm comfortable with builders (see above) but understand that it largely isn't idiomatic in Bevy land. (4) To be honest, I agree with cart's comments here. The part I like the least actually is that there is a difference between how you populate the text initially (5) I do think that this is the nicest approach, but hadn't considered it at the time as constructors didn't feel Bevy-like to me at the time: Bevy encourages and makes use of struct literal syntax everywhere; in retrospect this is not strictly the case, when you consider another complex component |
2.5) I don't personally see the builder pattern as a win over the ..Default pattern here. If we have decided that this is acceptable for the "single section api": Text::default()
.with_alignment(TextAlignment {
vertical: VerticalAlign::Center,
horizontal: Horizontal::Center,
})
.with_section(TextSection {
value: "hello world!".to_string(),
style: TextStyle {
font: asset_server.load("fira.ttf"),
font_size: 60.0,
color: Color::WHITE,
}
}) Then imo this is also acceptable (and preferable): Text {
alignment: TextAlignment {
vertical: VerticalAlign::Center,
horizontal: Horizontal::Center,
}),
sections: vec![TextSection {
value: "hello world!".to_string(),
style: TextStyle {
font: asset_server.load("fira.ttf"),
font_size: 60.0,
color: Color::WHITE,
}
}]
} The main allure of (5) is that you don't need to think about the TextSection type at all. I think the desire for the simplicity of the old api is whats motivating this ergonomics discussion, and so far only (4) and (5) have handled that. I think we should either embrace the extra boilerplate and use (1) everywhere, with the benefit being that the api is consistent across all use cases. Or use (5) (or something like it) for single-text sections to re-capture the simplicity of the old api. For (5) naming, maybe we should roll with the rust |
Sounds like we're in agreement then. Lets replace BasicText with Text::with_section() and get this merged! |
Done! Edit: I need to rebase and update the new example. I'll get to it shortly. |
Looks good to me. Thanks again 😄 |
Rich text support (different fonts / styles within the same text section)
Refer #1222
This takes advantage of glyph_brush_layout's
SectionText
API.Currently the
Text
component has an assumption that there is exactly one section only, so all I've done is create a "sections" API to connect it to glyph_brush_layout's API. I've made as few changes as possible otherwise.However, I've updated all examples with text in them, including enhancing them with multiple text sections where I thought it made sense.
This is a breaking change, so this will need a changelog entry - I wasn't sure where to put this, so please advise.
We can potentially flatten the API further; see the questions below.
New API
The new API is as follows:
Questions
It might make sense to either:
TextStyle
intoTextSection
, orfont
fromTextSection
intoTextStyle
Example
Example below:
The "Score: " and the "10" have different fonts, font sizes, and colours