Replies: 8 comments 9 replies
-
My core criteria here:
Overall, this proposal meets my requirement and is well reasoned and explained! I prefer the distinct components over a monolithic I think I'd prefer using
Agreed: that's the strongest argument against it. I would also really like to not make |
Beta Was this translation helpful? Give feedback.
-
With required components it's necessary to have a dedicated type for pulling in components for the root node. |
Beta Was this translation helpful? Give feedback.
-
Looking for sign-off from the following contributors before attempting an implementation: |
Beta Was this translation helpful? Give feedback.
-
Intuitively entity-per-text-section has always seemed the right direction, my worry with this design is that it could be very awkward to manage the text sections after they've been spawned (unless you just despawn the whole text tree and spawn a new one every time you make a change). Like from the example:
Suppose that you want to change the text to only display the word "World". Ideally you'd just want to be able to delete the "Hello, " text entity. But it's the head of the tree so that would break the text hierarchy. You could despawn the "World" entity, and change the "Hello, " string to "World" but then you also have to remember to update the font, and you have to understand the structure of the text tree and locate the world-text entity. And that's only a very trivial construction. It's not very clear to me how to make the ergonomics as simple as with a vec of sections. |
Beta Was this translation helpful? Give feedback.
-
I'm thinking the proliferation of components might be overly complicated. I propose we merge TextLayout into TextBlock for conceptual simplicity.
Sadly
I'm pretty strongly in opposition to this, as it makes this all needlessly complicated. It essentially makes a "one to many" constructor required for reasonable ergonomics, which I consider to be unexpected / unidiomatic in the new scene system / a missed ergonomic opportunity. I believe this approach came from a desire to share I do like your proposal to make the "cosmic data" / buffer component explicitly encoded via required components, rather than implicitly computing what needs it at runtime. I propose the following, which I think will make this all fit more pleasantly in peoples' heads: #[derive(Component)]
pub struct TextStyle {
pub font: Handle<Font>,
pub size: f32,
pub color: Color,
}
/// This is always an (empty by default) "final destination" for text. It is where you "put" spans of text,
/// it is not a span of text itself.
#[derive(Component)]
#[requires(TextLayoutInfo, ComputedTextBlock)]
pub struct TextBlock
{
pub justify: JustifyText,
pub line_break: LineBreak,
}
#[derive(Component)]
pub struct ComputedTextBlock {
pub(crate) buffer: CosmicBuffer,
pub(crate) entities: SmallVec<[TextEntity; 1]>,
pub(crate) needs_recompute: bool,
}
/// This is the top-level UI text component. If a string is specified, it behaves as if it has a "first" TextSpan child.
#[derive(Component)]
#[require(TextBlock, TextStyle, Node, ContentSize, TextNodeFlags)]
pub struct Text(String);
/// This is the top-level 2D text component. If a string is specified, it behaves as if it has a "first" TextSpan child
#[derive(Component)]
#[require(TextBlock, TextStyle)]
pub struct Text2d(String);
/// This will contribute its text to the parent TextBlock (or ancestor in a TextSpan tree leading to a TextBlock)
#[derive(Component)]
#[require(TextStyle)]
pub struct TextSpan(String);
// 1.a Simple UI text node.
//
// Node, Buffer ["Hello"]
commands.spawn(Text("Hello"));
bsn! {
Text("Hello")
}
// 1.b Simple 2d text node.
//
// Buffer ["Hello"]
commands.spawn(Text2d("Hello"));
bsn! {
Text2d("Hello")
}
// 2.a UI multi-span text.
//
// Node, Buffer ["What a ", "wonderful ", "world"]
commands
.spawn(Text("What a "))
.with_children(|parent| {
parent.spawn(TextSpan("wonderful "));
parent.spawn(TextSpan("world"));
});
bsn! {
Text("Hello") [
TextSpan("wonderful "),
TextSpan("world"),
]
}
// 2.b 2d multi-span text.
//
// Buffer ["What a ", "wonderful ", "world"]
commands
.spawn(Text2d("What a "))
.with_children(|parent| {
parent.spawn(TextSpan("wonderful "));
parent.spawn(TextSpan("world"));
});
bsn! {
Text2d("What a ") [
TextSpan("wonderful "),
TextSpan("world"),
]
}
// 2.c Simple multi-span UI text
//
// Text, Node, Buffer ["Hello, ", "World"]
commands
.spawn(Text::default())
.with_children(|parent| {
parent.spawn(TextSpan("Hello, "));
parent.spawn(TextSpan("World, "));
});
bsn! {
Text [
TextSpan("Hello, "),
TextSpan("World, "),
]
}
// 2.d UI multi-span text expanded from markup "[b]Hello, [i]World[\i][\b]"
//
// Text, Node, Buffer ["Hello, ", "World"]
commands
.spawn(Text::default())
.with_children(|parent| {
parent
.spawn((
TextSpan("Hello, "),
TextStyle {
font: fonts.get(FontFamily::new("Fira Sans").bold(),
..default()
})
));
parent
.spawn(TextSpan::default())
.with_children(|parent| {
parent.spawn((
TextSpan("World"),
TextStyle {
font: fonts.get(FontFamily::new("Fira Sans").italic())
},
));
});
bsn! {
Text [
(TextSpan("Hello, "), TextStyle { font: {fonts.get(FontFamily::new("Fira Sans").bold()} })
TextSpan [
(TextSpan("World"), TextStyle { font: {fonts.get(FontFamily::new("Fira Sans").italic()} }
]
]
}
// 3. Multi-node UI
//
// Node
// - Node, Buffer ["Hello"]
// - Node, Buffer ["World"]
commands
.spawn(Node)
.with_children(|parent| {
parent.spawn(Text("Hello"));
parent.spawn(Text("World"));
});
bsn! {
Node [
Text("Hello"),
Text("World"),
]
}
// 4. Invalid, no TextBlock. Warning emitted.
commands
.spawn(TextSpan("Hello"))
.with_child(|parent| {
parent.spawn(TextSpan("World"));
});
bsn! {
TextSpan("Hello") [
TextSpan("World")
]
}
// 5. Valid: This is a normal text node with a child Text node. They are each their own TextBlock
commands
.spawn(Text("Hello"))
.with_child(|parent| {
parent.spawn(Text("World"));
});
bsn! {
Text("Hello") [
Text("World")
]
} |
Beta Was this translation helpful? Give feedback.
-
This PR for |
Beta Was this translation helpful? Give feedback.
-
Implemented in #15591 |
Beta Was this translation helpful? Give feedback.
-
Seeing how we have |
Beta Was this translation helpful? Give feedback.
-
Continued from the
Lorum Ipsum
discord working group. This is a proposal for a new text API using required components and replacingVec<TextSection>
with a hierarchy of text entities.See cart's comment for the current proposed design. Most of the discussion below remains relevant.
Objectives
Text
into separate components to take advantage of the new required components API. Deprecate existing text bundles.bevy_mod_bbcode
uses a custom multi-entity solution for text.Background
Right now we use
cosmic-text
to control text layout. We feedcosmic-text
an iterator over text sections, and it constructs aBuffer
that knows how to organize the text contents. The buffer is then used to construct aTextLayoutInfo
which has all the glyphs and their positions for rendering text in Bevy (both UI and 2d text).When doing UI layout with
taffy
, a node with text on it is treated as having an opaque 'content block' (see theContentSize
component). Whenever a node has a content block,taffy
asks the block for its computed size using different x/y bounds as part of thetaffy
layout algorithm (CSS grid/flexbox). For a text node, you compute that size by using the text'sBuffer
to recompute text layout.Summary
Currently we store text sections as
Vec<TextSection>
in a singleText
component that has all text layout and style information.In this proposal, we break
Text
into a tree of text nodes. At the root of the tree are the following components:TextLayout
: controls wrapping/line breakingTextBlock
: contains the cosmic text buffer and caches the list of text section entitiesTextSpan
: indicates the node should be tracked as part of text tree collectionAny node of the tree underneath a
TextBlock
(including the entity withTextBlock
) can include the following components (if a child doesn't haveTextSpan
then we don't traverse into that child - only contiguousTextSpan
nodes are considered):Text
: contains a stringTextStyle
: font, font size, colorTextSpan
: indicates the node should be tracked as part of text tree collectionTo delineate between UI and 2d we have:
TextNode
: marker component for UI text, requires components for setting up a UI nodeText2d
: marker component for 2d text, requires components for setting up a 2d nodeThe final bit of magic to make this ergonomic is using
TextNode::new()
andText2d::new()
to produce bundles instead of making just the components. This introduces a small learning curve but makes everything fit together nicely.API
Implementation details
Text
orTextStyle
changes on entity, iterate upwards to findTextBlock
then set itsneeds_update
flag. This flag is used bytext2d
and UI text to decide if the buffer should be rebuilt.TextPipeline::update_buffer
, instead of taking in a&[TextSection]
, take inimpl Iterator<Item = (&Text, &TextStyle)>
. This iterator should internally traverse the entity hierarchy and rebuild theTextBlock::entities
vector in-line. This is an effective way to keep the entities vector accurate without any extra hierarchy traversal/management code.TextLayoutInfo
. The section index can be used withTextBlock::entities
to look up the correct entity for picking events.OnRemove
observer forText
that sets theTextBlock::needs_update
flag on the nearestTextBlock
ancestor.CosmicBuffer
is embedded inTextBlock
and should not be edited by users since any edits will be overwritten by the internal systems that filter forTextBlock
. However, buffer construction,ContentSize
measurements, andTextLayoutInfo
assembly will be opt-in via theTextBlock
,TextNode
, andText2d
components. A user can instead manually construct and updateContentSize
andTextLayoutInfo
by adding a custom component/code for managingcosmic-text
buffers, and simply not including those opt-in components. TheTextPipeline
should be refactored a bit to facilitate this (split upTextPipeline::queue_text
so you can constructTextLayoutInfo
without needing bevy-specific text sections/blocks).Node
,ContentSize
,TextLayoutInfo
,CustomCosmicBuffer
+ code to manage theseSpatial2d
,TextLayoutInfo
,CustomCosmicBuffer
+ code to manage theseOpen questions
taffy
,Visibility
, andTransform
traversal, we want to ignore the childText
entities. One solution would be aSkipChildren
marker component required byTextBlock
. Or perhapsSkipChildren<T>
and then requireSkipChildren<Node>
,SkipChildren<Visibility>
,SkipChildren<Transform>
.Alternatives
Display::Flow
setting to the UIStyle
which would have behavior similar toTextBlock
.Text
component, and auto-detecting the root node of text blocks (possibly withDisplay::Flow
playing a part).Problems with the alternatives:
Text
need to be aware of each other for proper auto-setup (e.g. 2d and UI).Display::Flow
is also desirable for 2d.Text
on all the nodes of a text block. This isn't a hard blocker to the alternatives since they could useTextSpan
like I do in this proposal.Display::Flow
would increase the complexity ofStyle
, which is already quite overly-complected.Beta Was this translation helpful? Give feedback.
All reactions