#Lorem Ipsum: A New Text ~~Backend~~ Frontend for Bevy
1 messages · Page 2 of 1
Well supposedly that would be handled by some sort of reactive system
And it depends on how it's implemented. In html you have to have a separate element like <b>my bold text</b> to create bold text
Because a block of text can only have one style.
does the html pattern make sense with queries? In an ECS, you kind of want components to be something you can operate upon in a big block.
I have a difficult time seeing myself ever reaching for a query of all the bold spans in my app.
we can have markup elements that do not become structural ecs features
But what if you want to do fancier stuff like add marker components to specific pieces of text? Like how you can query css classes and id's
I'm not saying there shouldn't be a way to turn a block of text (or a sub-block or a sub-sub-block) into an entity. I'm just asking if it should be required to format stuff.
If we build a general rich text renderer and we have an orthogonal entity hierarchy for laying out of blocks of rich text, then you could implement a component that makes a block bold but you would not have to use that component to produce bold text.
And while I'm excited about BSN and reactivity and better tools for working with hierarchies, I want the low level to be nice too
The most granular unit in the ECS (a single entity) should correspond to something relatively stable and useful, and I think that "rich text section" lines up really well for things like swapping out text or localization
I used to think that spans as entities was better, but I've come around on it. Performance is very secondary for me here, and I think with a markup-style syntax the current architecture could be really pleasant to author, modify and hook into tools for writers
I think querying for a LocalizedText component is simpler
And a markup syntax can still compile down to entities
And only using entities for "stable" use-cases leaves a lot of interesting use-cases on the table.
Personally I think of an entity as a database entry, and a TextSpans table makes sense to me.
I think at the very least having text span entities doesn't provide much benefit to end users by itself. Querying a block of rich text is a lot more useful here. But the representation also needs to play well with more advanced features. Can we easily introduce custom effects for spans, including animated ones, if it's part of a rich text block for example?
And the editor aspect of building a continuous selection of text for example
And selection across blocks. As a user, if I see two paragraphs of text my expectation is I should be able to select both even if they're separate blocks
Also what about things like on-click, on-hover, etc. handlers?
I think maybe we are thinking too much about UI as it is done on websites, rather than our actual immediate use-case: Games and The Editor.
Text selection is important; browser style text selection of static text that spans across multiple blocks probably isn't.
Blender doesn't really support this as far as I can tell.
When I was thinking about links I was thinking about crusader kings
Links sure, we want links probably. But cross block selection, maybe not. When was the last time you tried to select multiple paragraphs of text in a game?
Yea we'll 100% need the ability to had something like links. Dialog systems might need them, and a lot of help infos link trough to other sections
Multiple paragraphs is also not the same as multiple blocks. You could totally have two paragraphs in a single rich text block 🤔
Well without it, that means if you want the player to select a block of text it all has to be in the same block
I'm cool with whatever feature list we want to come up with, and if you can find a use of something in a game or 3d modeling software, I think we should support it. But I don't want to fall into the bottomless pit of doing whatever the web does.
And I'm not sure that's the best line of reasoning. Lots of games don't have spatial audio so should we drop it?
I never mentioned web though (except the bold example but the point wasn't to say we should copy web)
And I really don't think it's helpful to immediately associate features with web and shoot them down
I just can't remember the last time I selected text outside of a text-entry box in a game.
Ditto for a 3d editor.
I associate the ability to just select big random swaths of rendered text strongly with the web and html.
I remember there were cases where I would've really wanted to select text in-game, but had to copy-type it...
To be clear, I am not trying to shoot down your idea: I am trying to apply a test. "Can I imagine a use for this in one of our target applications?" Currently my answer is no, and maybe I was a little harsh in the way I expressed that. I'm open to examples.
I guess multiplayer chatboxes often require text selection.
We have to consider more than just games of course, but plenty of desktop apps have the same limitation, you often can't select multiple blocks of text, and it's hardly an issue since usually it's all one block even if it's multiple paragraphs
On the web it would be a much bigger issue since you split up paragraphs into <p> blocks 🤔
In the input yes, many of them don't even allow selecting text others sent tho
debug consoles need good text selection and copy imo
else we're going back to println! everything
which kinda defeats the point of an ingame debug console in the first place
Yep, mind changed. Cross-block text selection is useful within "pages" of text, it seems to me. Like a console or a chat box window, or maybe item lore.
I feel like item lore would all be 1 block. But yea we'd at the very least need constraints for what can and can't be selected
Text selection becomes very difficult when multiple blocks are involved. I'm sure everyone has experienced the absolute backwards text selection logic PDFs throw at you sometimes 😂
Usually you have a big box with a jumble of text content inside and very little else. Being able to select labels and non-content text is imo an anti-pattern of the web and pdfs.
I think that's a fair stopping point as far as scope.
more importantly html has <a href="/">link<a/>. i'm motivated by both the ability to mutate individual sections (consider the FPS counter in some of our examples), as well as attaching arbitrary behaviours (focus, on click, hyperlinks), both of which would be made possible through components. spans in html are very flexible. ecs is even more flexible... if you use it. maybe it's a case of "when all you have is an ecs, everything looks like a system".
maybe it's a case of "when all you have is an ecs, everything looks like a system".
Until someone unironically suggests Letters as Entities I don't think we're there yet
i think that it would be up to the creator to know where they would be bolding or unbolding text, and that would be its own span.
{"FPS: "}{"60",bold?}
if you were implementing an editor it would be different of course, but in an editor you're not operating directly on the "text tree" provided by bevy, you're operating on a string or rope buffer or some sort of rich text tree. and that generates the bevy "view" text tree. I think the simplest approach would just be to do some diffing and patching in that case.
tbh this depends strongly on our reactivity story. If we ship a built-in solution for fearless ECS reactivity, then I'm fine to have the entire formatting tree expressed as entities. If not, and currently we are still at the prototyping stage, then using the entity hierarchy for everything is less compelling.
Yeah, until that solution is in hand I don't want to lean on reactivity too heavily
And maybe not even then
Being able to peel back the abstraction is nice and things shouldn't be horrible when you do so
text selection is handled by cosmic-text, they provide a Cursor abstraction
and a way to get the rectangles
If the low-level api is “horrible” then I think the move would be to introduce helper methods instead of restricting the capabilities of the system just for that use-case.
also @white monolith handles selection and copy-paste etc. in bevy_cosmic_edit, so we can learn from that
But this is theoretical since we don’t have the new bsn proposal yet
I’m curious what Construct’s are and what we can do with them. It might make the non-bsn api better by offering a way to easily turn data into entities
i have a feeling that bsn can lower to spans-as-entities much easier than special-casing text
That doesn't help us solve all the issues created by having selections across multiple separate blocks of text right? 🤔
also something comes to mind. it's not strictly true that cosmic-text keeps one string allocation, and attrs+indices.
it stores each line as a string (since it's optimised for an editor use-case).
buffer -> [bufferline(string, [attr]) -> [visual line]]
which is to say that even if we went in the opposite direction per @crude tinsel suggestion (one string, styles+ranges), we'd still have to allocate, especially since we're not sharing allocations. and then we're also having to implement a lot of helper behaviour to be able to mutate text like
{"Frame rate: ", const}{"60", mut}{" fps", const}
depends... are we talking about different paragraphs in the same buffer, or are we saying they're in different buffers
I think it would be different buffers here
it's up to us to send the "selection events" to cosmic-text, so if they're in different buffers it just means we're starting in the middle of one buffer and finishing in the middle of the next... which looked at differently is the middle of buffer A until the end, and the start of buffer B until the middle
browsers probably have some cursed algorithm we could borrow from
also side note, PDFs are the worst format ever invented
Browsers do have some different UI layout logic than we do, so it might not be easy to borrow
they're like bad SVGs
Yes they are, which is why broken selection across paragraphs in a PDF is an example many people can relate to 😂
i was shocked when i learnt that many PDFs store text as vector paths, and PDF readers largely just OCR the paths to give you something you can interact with as text
Text selection across different text contexts is a pretty hard problem I think
Esp. If you combine with text in different orientations, rtl, etc.
Side note: don't look too closely at my implementation of bevy_text3d
As requested, I've written up a requirements doc for the issues around font families and system fonts: https://hackmd.io/@dreamertalin/HkBGJx8vC
Agreed, some rich text system would be very useful for localization, otherwise its hard to format parts of the text while making it look right in all languages.
But that may also be implemented on top of a more lower level text span feature and not necessarily be a first party feature.
I guess the rich text syntax could be the subject of a lot of bikeshedding and opinion wars :D
A rich text system with localization support probably needs a first party option (as well as of course being replacable), making people search trough the third party options just makes people not localize their game/app or port it over later (which is generally not fun and a source of many non-localized things)
I'm also not sure how much of an opinion war there would be on this ... We're a community that uses discord and Rust, everyone is familiar with using markdown for all basic rich text needs ... Kind of similar to how godot has bbcode for their rich text, and the GDScript docs also use bbcode 🤔
I mean, if anyone wants the spans as entites, That would be nice to try out in a third party crate I assume. Basically just boil those entites down to the spans inside bevy
But imho, it feels quite close to "every char" as an entity
excuse my ignorance, but how is a span defined anyway?
and regarding the rich text section, markdown seems like a natural choice. Im not advocating against limiting other implementations , but I do believe that having a recommended way inside bevy is a good starting point.
every app or game has a lot of things it needs to do, so giving a default answer for most things is quite a good thing in getting a large project off the ground. Then also providing mechanics for going into the details and fixing/exchanging parts when they need to work in a specific way
A couple issues with Markdown as the "frontend" for bevy_text:
-
Markdown is too limited to be effective for the *overwhelming *majority of rich text scenarios. It is designed as an authoring format for documentation. As an alternative, something like asciidoc would be better, or some other format that lets you attach arbitrary attributes to spans and blocks. custom bbcode could work as well. XML or HTML (or something that maps to it) would be better.
-
(As someone who just spent a couple hours trying to implement a markdown widget in bevy_ui) a Markdown snippet doesn't translate to a Text widget... it translates to a "Document", and from its original definition a HTML document: it is a nested format with blocks, including images, tables, list elements, etc. So for "inline attributes", you would be limited to: bold, emphasis... and that's about it.
A span is
In bevy_text:
(Component)
Text {
sections: [TextSection { // one TextSection == one span
value: String,
style: TextStyle {
font: _,
font_size: _,
color: _,
}
}]
}
It gets translated to cosmic-text:
Buffer {
lines: [BufferLine {
text: String,
attrs_list: AttrsList {
spans: RangeMap<usize, AttrsOwned> // AttrsOwned is roughly equivalent to bevy_text::TextStyle
}
}]
}
so it's a piece of text with constant attributes for all characters in it
yeah in bevy text a span is just string with an associated style
Agreed, markdown seems fairly limited for our use cases. Colored text would be an important one that's not built into markdown but common in games.
I definitely agree that localization support should be built into bevy at some point, but its a complex topic and no obvious solution emerged yet. IMO its best to experiment with things in third party crates first and then later upstream something that works well.
Anyway, I don't want to derail the conversation, that's something to think about later on :)
I’m curious how you think we can solve things like links inside a body of text without entities
If selection across text blocks is too hard then if I want a piece of text to be selectable it must be in the same block. This puts extra emphasis on customizing spans imo
Because i can’t just break a block into two parts and put a custom thing in the middle (like a link)
If I want it to be selectable.
Just make a plain-old-rust data structure for a text body that stores a vector of marked-up spans? Then put that data-structure in a component?
Obviously we want a hierarchy of text. But the question is, do we want to require that the text hierarchy be represented entirely with the ecs hierarchy? Or do we want to allow the text hierarchy to be represented using the ecs hierarchy, but also allow entity-internal text hierarchies on ECS leaf nodes.
There's real value in making text-hierarchies a component in-and-of-itself, and it shouldn't prevent you from ignoring the internal-hierarchy and just using the ecs.
Does that make any sense?
That ignores the whole “text selection across blocks is hard” problem
Over time, I've come to appreciate apis that can stand on their own but are extended and improved by an ECS layer on top. As opposed to a tightly coupled ECS only api.
I don't see why that is the case. Can you walk me through it?
If you want to have every span be a component on an entity, then you need to solve the cross-ecs-hiarchy selection anyway
I’m just repeating what was said above:
#1248074018612051978 message
Not necessarily. You can just require a text block only have text spans as children
Then the parent aggregates text from its children
Right... how is that in any way incompatible with having nodes also contain a hierarchy of text internally in addition to having a hierarchy of child text elements?
That’s the solution you’re advocating for but that’s not the impression I get from others.
Fair enough. I think I must be misunderstanding something. I'll have to come back to this later, maybe I will grok it better.
The main issue is I’d like to create custom behaviors for text spans AND have them be selectable with other spans.
Oh and also being able to query spans would be nice. Like creating a system that does some sort of animation just for specific spans.
You could have a custom spans system but that feels like a poor man’s ecs and it’s still not queryable.
Where possible experimenting outside the repo is definitely the better option, but it's important that we know what features we are working towards so we make something that works for the future, rather than something that's only technologically cool but useless for real games and apps ... Regardless of how we build translation or the format we choose for rich text, the design of bevy_text shouldn't be meaningfully impacted by those (since the other things should be optional layers on top)
This illustrates how things work today and the two current proposals for changing how things work:
but consider that we might want more behaviour: we want to mutate the first span, we want the second span to be a link, we want the third span to change colour on hover, here's how one might tackle these requirements in each of these designs:
I think in all of these cases we want some more ergonomic API to construct Text... but that same API could generate any of these forms, so what's the easiest to work with after that?
In the style ranges case, how would you handle multiple links in the same block?
the "inverted" components, so to speak
Oh I see you have an array
yeah it's clunky
So is the only gripe with spans as entities the syntax?
in the style ranges case I don't know how you mutate the sub-string... you'd have to provide some helper methods like "insert text here"
since you need the range to be updated as well as the string
all the ranges*
And I'm curious, as someone using this api, how you compute the ranges?
again, it'd have to be provided by bevy
Just making sure I'm clear on how it works
and considering it doesn't map 1-to-1 with the cosmic-text representation, I'm not sure we gain that much
in the style ranges case I think you'd have to provide rich-text-tree-manipulation APIs, or treat it as an immutable kind of API, user just provides the representation and you just regenerate it every time
That is certainly my problem with it, but I feel that it's probably the least-worst option.
I think you could have an api similar to the spans api that produces entities instead.
Imagine someone provides a block of text like [A]He[B]llo Wor[/B]ld[/A] and also a mapping between span -> a list of components
I want to give my perspective on this discussion as a third-party crate author. Given a good foundation for rendering text, things like selection (even multi-block selection), markdown, and so on are all problems I can solve in my crate. While it might be nice if Bevy supported these features, I am in no way blocked by the lack of them.
What I am more focused on, for purposes of this working group, are the things that cannot be done without changes to Bevy. This includes how fonts are loaded and how styles are represented at the entity level.
For complex editing scenarios, you'd almost certainly want to edit a higher-level representation of styled text, and then compile it down to whatever format Bevy likes to display. A smart version of this could even cache individual paragraphs or sections, so performance would not be an issue even for large documents. You don't have to try and manipulate styled span lists directly.
I agree
I think the more interesting scenario is the "throbbing hyperlink": where you have a single word in a paragraph of text whose color and opacity is being driven by an animation curve or timer. Admittedly this scenario is completely made up - it's not something I have actually seen - but it's conceivable. I bring it up simply because it's a case which is easier to implement in some style representations than others.
Why not just try to expose the cosmic-text API as much as possible (1-2-1 )?
1 entity = 1 cosmic-text buffer/editor with array of array spans, something like this: https://github.com/Dimchikkk/bevy_cosmic_edit/blob/main/examples/font_per_widget.rs#L24
And bevy exposes editor/buffer proxies via components... basically to give user access to low level api to build anything cosmic-text is capable of on top of that.
One of the things we've talked about is possibly switching from cosmic-text to parley in the future. At the moment we decided to go with comic because it's the more mature solution. However, parley shows great potential (for example, support for variable fonts).
For this reason, we might want to leave the door open to support such a future migration, in which case we don't want to expose too many details of the underlying solution.
TextSection are essentially that thought process, but applied to your previous text stack. It just happened to be intuitive enough that it worked as a mid-ish level API for users.
I feel like we would definitely need something on top of cosmic's API to match the userfriendliness there
Yeah I understand, I am not suggesting to expose the cosmic text API directly... but via wrappers so to speak...basically mimic it as much as possible.
I feel like spans-as-entities has a lot going for it, but our current tools for working with hierarchies might make it a net dev-ex downgrade at the moment, and it seems like there'd be a huge amount of overhead in creating / modifying text to get back down to cosmic spans.
Could be worth trying out though to see how it looks.
Yeah, in the first version of the crate I had some simple helper func to set text like set_text("hello world") if user doesn't want to have fancy styles
Here's an example of what I mean: from what I understand, both cosmic and parley are similar in how they represent style spans. However (and I don't remember which one does this) one allows overlapping spans, while the other does not - meaning that you have to normalize the spans if you have overlapping styles.
In Quill, it's even easier, since both String and &str have implementations of the View trait:
Element::<NodeBundle>::new().children((
"Hello, "
name,
"!"
));
In other words, you can just pass in a raw text string and the text node will get created automatically.
The problem, for me, with the existing system is that I can't mix text and non-text elements in a single paragraph. For example, if you replace name in the previous example with the user's profile photo, that's an entity, and you can't put an entity in the middle of a text node. Right now I simply punt on this issue: every string creates a text entity with a single TextSection, I don't even try to support multiple sections with different styles.
While it may be a lot of work to to convert a list of entities into a set of text spans that cosmic-text can consume, there are some important use cases that simply cannot be supported without doing that work.
What I would propose is a new type of layout option: right now we have Flex and Grid. The new type would be called Block or Flow, and would mean "layout your children using a word-wrapping algorithm, combining text entities into merged spans where possible." So the algorithm to convert text entities to spans would be strictly opt-in; if you put text entities in a flex or grid container they would still be separate entiries and would get their own cells.
Inline images in text should be supported from cosmic-text first... to allow arbitrary "empty" boxes... there is a github issue that already exists.... then basically using cosmic-text info of location&size of that box for image user can position/put image/etc... so basically layout handling inside text is leveraged to cosmic-text. That's my understanding how it might be implemented
That's not incompatible with what I proposed 🙂 Basically, the rules would be:
- For text entities in Flex or Grid containers, the cosmic-text layout algorithm is done on each individual child. The layout between children is still controlled by Taffy.
- For text entities in flow containers, the cosmic-text layout algorithm is run on all the children as a group, replacing Taffy.
I've prototyped spans-as-entities so you can see how you feel about it: https://github.com/tigregalis/bevy_spans_ent
It's built as a layer on top of the current bevy_text (we just update the Text component on the parent, based on the changes in its children), but ideally it's not a layer at all.
Ideally there's a better way to construct these, but that can be built as a layer on top of that.
What would really prove the idea is span-level hover and click events. That shouldn't be too hard to do, assuming we can access the hit detection methods from cosmic-text's Buffer (if not... we would have to expose it out of bevy_text).
Holy crap, dude, that's awesome!
Just curious: How does spans as entities work with line breaks, how does the span know where the line starts?
Or is this going to use a parent "text box" node for the layout stuff?
The same way it works now. bevy_text preserves line breaks, cosmic-text drops them.
Nice!
To further elaborate, it's possible to have a span in bevy_text with nothing but a "\n". Cosmic-text still handles the layout.
Something else that comes to mind... with spans as entities, mixed content (e.g. inline boxes like images, which would also be entities that are children of the parent Text entity) makes more sense.
Theoretically it should be possible right now to hack in mixed content (inline boxes) with this method. We'd just need to somehow provide a fake font to cosmic-text that lets us draw transparent glyphs of arbitrary size. Of course, it'd be better if cosmic-text supported it out of the box.
That's great, thanks for putting that together. I will play with this later. One scenario I'd like to see is:
There are many monsters with HitPoints components.
They have health bar entities as children with text like HP: 5 / 10. (Possibly with further indirection from an additional sprite component for the bar itself)
I want to update a the text for a specific monster's HP value when their HitPoints component changes.
My intuition is that this actually wouldn't really be worse than current-bevy. You're having to do hierarchy traversal either way and we do have helpers for that.
But there may be examples where you previously didn't need to do that and now you do.
We're assuming the numbers have their own styles?
Yeah, they are broken into separate spans for that reason.
With spans as entities you should be able to target a specific span with a marker component
Yeah, that's demonstrated really well in tigregalis' repo above. It's just the extra hierarchy traversal to get the right entity to modify that's a little less friendly, I think, that might be a slightly dev-ex downgrade.
Yep so I'd have (TextSpan, CurrentHealthDisplay(Entity)) and (TextSpan, MaxHealthDisplay(Entity)) , then a query (Monster, CurrentHealth(u32), MaxHealth(u32))
I don’t think you’d need a traversal for 👆
But I suspect that the situations where this comes up might be a little more rare than I thought and that overall it looks pretty great.
Ah, I hadn't thought about doing something like CurrentHealthDisplay(Entity) -- just using bevy's hierarchy.
That's interesting I guess.
it's a 1 to 1 relationship so you can get away with it
Spawning that seems a bit annoying
does Bevy not provide a helper for referencing the parent entity in children?
ah wait that's not relevant
they're not parent-child
actually it would be
Monster (sprite) -> Text2d -> TextSpan
theoretically you should have the monster entity at the time you generate the textspan
We should pin this
Yeah, that seems likely
And like i mentioned above you could make this a lot more ergonomic by using a very simplified bbcode style syntax and a map that maps named spans to a list of components
#1248074018612051978 message
Perhaps in a macro
I wonder if spawn_batch makes this block look any prettier: https://github.com/tigregalis/bevy_spans_ent/blob/main/examples/animated.rs#L18
edit: ChildBuilder doesn't have spawn_batch
It seems like people are thinking that we might end up with "spans as entities" OR something a "bbcode/markdown/html" interface as frontends to text I'm Bevy.
Whereas I reckon it probably makes sense to have both.
My thinking is that the "bbcode/markdown/html" angle is less interesting / totally open for experimentation in third party crates, and I'm more interested in what the midlevel APIs look like.
What I’ve been advocating for is both. Spans as entity and a convenient way to create them.
I guess I've been thinking that spans-as-entites will need to be lowered to vec-of-spans anyway. So the convenience layers wouldn't necessarily use spans-as-entities (some might, some might not)
Well ultimately it all needs to be converted to cosmic text data so yeah
But i think a bbcode interface should be on top of spans as entities
What I'm advocating for can barely be called bbcode. Literally just the [A][/A] bracket syntax. If you can attach components to spans then there's not much need for a fancy template format
Oh that's interesting: just use components is really powerful
Very flexible and intuitive
Are we imagining something like a TextStyle component?
So far it looks like it. @tulip hinge created an interesting protoype featuring animated spans where you animate the span's color style
Yeah so if you have [A][/A] you could also provide a map that specifies span A gets X, Y, and Z components.
And you could use hooks or observers to do this cleanly
[A][/A] implies nesting of styles, and then you'd have to handle e.g. two styles providing different values for the same component
if no nesting is allowed, it can just be [A] with no [/A]
(not to imply that i think nesting should or shouldn't be supported)
Nesting would definitely be useful but I'm not an expert on cosmic text or parley
cosmic-text accepts non-overlapping spans. So any nesting needs to have been "resolved" into a flat structure by the time it makes it there. But that can be done quite easily when translating the tree into flat style spans.
I have a somewhat messy implementation for parley here: https://github.com/linebender/parley/pull/76/files#diff-c155f6c39f6827d80c25c603e4a1fbf0ac6015f68d9ed87ad7c96d57555abe65
Perhaps. It might be possible to do it even simpler like:
commands.entity(parent_entity)
.spawn_bbcode("[A]Hello [B]World[/B][/A]", |span_entity_map| {
commands.entity(span_entity_map["A"]).insert((X, Y));
commands.entity(span_entity_map["B"]).insert((X, Y, Z));
});
Although nesting might make this difficult
The tricky thing is how to deal with a user that writes:
[A]Hello[B]Wor[/A]ld[/B]
Is that valid? I think that should error honestly
I suppose that would be one easy way to deal with it
Well I imagine we'd have some sort of validation
Seems implied that there would be some error cases
If you're using a "ranged styles" model then it can be valid. If you're using a tree model then you'd need something like what the HTML5 spec does to disambiguate the parsing.
Can you elaborate on this?
To make it more concrete, let's consider:
[b]Hello[i]Wor[/b]ld[/i]
where [b] indicated bold and [i] indicates italics.
If you treat these as flat, overlapping style ranges then you can render this as:
Hello_Wor_ld
If you try to parse it into a tree then you end up with problem. You have:
- B
- Hello
- I
- Wor
Then how to do you complete the tree?
I believe BBCode is typically compiled to HTML. So you'd get a tree, and you'd leave the HTML parser to deal with ambiguity. But something like MS Word would typically use something more like the ranged styles model.
tree style seems like the way. I like this api, but I will say it doesn't seem super general. Is there a way to refer to generalized components names in the string without having to manually add components to each of the entities? How will this work with BSN, where the components on each subspan need to be encoded statically?
Perhaps that stuff would have to operate on a higher level?
A tree with spans-as-entities definitely seems to me like it would be the way to go for BSN
You could providing a span -> component list map.
Assuming BSN can contain maps that might work.
I think adding components to the template string itself is route to a lot more complexity
And it potentially adds a lot of noise to the string. Keeping styling and components separate means the template string can be much more terse and readable
In my "bbcode-ish" rich text stuff, I sidestep this and allow only hello [red]world[blue]!. hello is default style, world is red, ! is blue. Really inflexible, but that's all I need.
almost looks like we're trying to come up with some kind of extensible markup language 🤔
if it supports inline annotations like that it will also need to consider escaping and such
Personally I’d prefer to avoid blowing up the scope of the markup. I just see it as a tool to mark which parts of text belong to which spans in a terse and readable way, but that’s just my approach.
I don't think we can accept anything unless we can work out how it will be represented in bsn. That's a hard requirement for any sort of markup. But I think it's probably safe to move forward on the spans-as-entities idea with hand-waves about markup for now.
just trying to understand what is the A in [A][/A]? is that literally just an arbitrary symbol/name? and then you have some other registry that maps components to symbol, or is A somehow the typename of a component?
Yeah in my example it was an arbitrary name, could be anything.
I think most systems don't try to solve this problem, just treat it as malformed input (HTML being the obvious exception but... in many surprising ways... consider how lone <p> and <div> tags differ)
IMO we should aim for a flat structure, i.e.
[b]Hello[/b][bi]Wor[/bi][i]ld[/i]
That would be reasonably straightforward to support
consider some other scenario, [x]foo[y]bar[/y]baz[/x]:
This would become the tree
x [
foo
y [bar]
baz
]
intuitively I'd expect to be able to address "x" as a unit, but there's not a clear way to support that
I'm not saying we can't eventually support a deeply nested tree of nodes, but it's much less clear how to handle it compared to a flat list
And anyway a well-formed or malformed tree structure can be flattened into a flat list, so someone determined enough could build it as a layer on top if they so chose.
Ok, building on top of spans as entities, I've now got:
- A convenience macro
- An initial implementation of hover for bevy_ui text
- An initial implementation of "link" clicks for bevy_ui text
It's not perfect, but it's a starting point
Sweet!
It's cool to see the buffer hit detection all wired up. Though it makes me wonder about something like "spans as interactable UI nodes."
The monster example turned out pretty clean too -- I hadn't really thought about redundantly storing Entity links within a Bevy hierarchy. I think overall it feels a bit better than using iter_descendants to find the span. Though I tried this and it wasn't nearly as bad as I was imagining, the entire thing is +1 LoC.
Also TIL about ChildBuilder::parent_entity().
Yeah I always just assume the library I'm using has the convenience methods I need and search the docs, and most of the time my faith is rewarded. Rust is the best that way.
I see an Entity as just a glorified reference/pointer/index. The ECS handles the safety for us, so we can store those "pointers" wherever we want.
If we had fast and ergonomic entity Relations that might be how I would prefer to model it though. When we do get that, spans-as-entities become even more compelling as the base representation.
Bevy has all the tools needed, so with spans-as-entities as the foundation, writing those extensions was easy; it basically wrote itself.
Without it, I'm not sure (for example) how to model links in a nice or at least obvious/simple way. It's certainly possible if users can attach data to spans, similar to what cosmic-text does (we pass the span index usize to cosmic-text, which stores it as Attrs::metadata) then you could have any sort of Map<usize, T> but then I feel like we're getting into "it's just worse ECS" territory.
I suppose it's relatively safe in this particular example without Relations because the pointer is within an existing Bevy hierarchy / everything is cleaned up by despawn_recursive, etc.
It would be slightly painful if spans-and-entities and BSN happened on separate release cycles, but spans-and-entities seems quite useful so maybe that shouldn't be too much of a consideration.
Awesome! This is looking great
I can't wait to delete bevy_simple_text_input and its janky cursor, lol.
might need some help "productionising" this though
selections should be fairly straightforward
yeah, same for bevy cosmic edit 😄
Hi, I've encountered an annoying issue with fluent where they insert directionality characters that are rendered as rectangles by Bevy's default font (and by FiraSans). Context: https://github.com/projectfluent/fluent-rs/issues/172 and https://docs.rs/fluent-bundle/0.15.3/fluent_bundle/bundle/struct.FluentBundle.html#method.set_use_isolating and https://unicode.org/reports/tr9/#Explicit_Directional_Isolates .
It looks like these characters should not be stripped at the library/localization framework level, so I'm wondering if the displayed rectangles should be considered a bevy_text bug.
Bevy main or 0.14? If main, can you repro in cosmic-text?
In 0.14, does main already have cosmic-text merged? I can try patching
Yeah
Bevy main screwed up my asset loaders so I'll have to wait until that gets figured out https://github.com/bevyengine/bevy/pull/14082#issuecomment-2219546976 .
Nvm figured it out.
Here's my v0.14
And here's my bevy main. Idk where the text went
Hey it works! (I needed a CosmicBuffer in my custom text inserter)
https://www.reddit.com/r/rust/comments/1e0dfj6/google_is_rewriting_harfbuzz_and_freetype_in_rust/ Oh well that seems very helpful
isn't rustybuzz already pretty much that?
Yeah, but it's explicitly a catch-up effort, and not Google-backed
I meant more in the sense that google could use/improve rustybuzz instead of make a competitor
but I'm not complaining either, more rust project being funded is always nice to hear
or, more accurately, I'm mostly curious why they didn't do that. It's not mentioned anywhere in their docs. At least I haven't seen it
Yeah, I wish it was more clearly stated
https://docs.google.com/document/d/1UnR2zKf3Z_DDRS6vLgBkSHUeqI3IGOEhWYh7rAIvsb8/preview
It looks like google is going bottom-up along the dependency stack. They are not competing with RustyBuzz, this make it seem like they are adopting it.
Oh, looks like some people are looking at porting rustybuzz to use fontations https://github.com/googlefonts/fontations/issues/956 in place of some dependencies. That's cool
Yeah RustyBuzz will be able to use Fontations, instead of ttf-parser.
So google is just trying to unify the stack, looks like.
My understanding is that there are a couple of possibilities for a google-sponsored shaper:
- A version of Rustybuzz. Probably a fork (because Google).
- A "next generation" shaper based on Swash. Which has the potential to unlock performance improvements.
- Some kind of hybrid of the two.
None of which are happening right now (google are continuing to use Harfbuzz) because there is no funding approved for such a project, and because the team that would be likely to work on such a project is still focussed on their Freetype replacement (skrifa/read-fonts from the fontations repo).
a little more progress
This looks awesome. Can I get you to post a summary of where we are at, status-wise? What's been checked in so far, what is pending review?
At some point I want to make a fork of my UI framework that uses the new cosmic stuff. However, given that I've only just released a version that works with Bevy 0.14, I don't want to immediately change the deps back to Bevy mainline, except in an experimental branch.
Also, a related question is how are you routing key press events? Simply reading key events from the global event queue won't work if you have more than one input field. The way I handle keyboard events in bevy_quill_obsidian is that there's a system which uses the bevy_a11y Focus resource to decide which entity to dispatch keyboard events to. This consists of several parts:
- A set of bubble-able events for key chars and key presses - these are similar to the existing Bevy events, but can be bubbled via bevy_eventlistener.
- A system which reads the global keyboard events, converts them to the bubbled versions, and dispatches them to the entity stored in
Focus. - If no entity has focus (
Focus.0.is_none()), then it targets the entity with aDefaultKeyListenercomponent.
This means, for example, that you can have global shortcut keys, but if a text field has focus, then it can intercept those events and consume them (using.stop_propagation()) rather than invoking the shortcut.
Quill also has a system for supporting tab-navigation between fields, but that is well outside of the scope of this channel.
Code is here: https://github.com/tigregalis/bevy_spans_ent/blob/main/examples/editor.rs, there are some TODO(s) on topic with your messages.
Personally, I feel like if we can expose cosmic's editor API in a way that allows users to solve these problems, that would be great progress.
There just needs to be a notion of Focus or Active (which can be components or a resource) I think. I would be starting with bevy_ui's own Focus.
I've deliberately been building it outside of bevy_ui first, partly to see if there is anything further that needs to be exposed directly, and so far there isn't. Although there are some more things cosmic-text may potentially expose directly, since I had to copy-paste some snippets from it.
Currently there's no integration with spans as entities as well. But I will separate those efforts anyway, it was just convenient when I started it and evolved naturally.
I'm mindful of this but also not too clear of how best to expose it. My current thinking is I'll build a very capable TextEditor UI node, so we can see what the blockers are (so far none), then we can split up the implementation into multiple steps (e.g. via events or components or resources) to expose the intermediate representation for end user extension.
I've been using the Focus resource that already exists in bevy_a11y: https://docs.rs/bevy/latest/bevy/a11y/struct.Focus.html - it's just an Option<Entity>, which is all that you really need.
Resource representing which entity has keyboard focus, if any.
There's no dependency between that and bevy_ui.
Not that I am aware of, but I'm not 100% sure. Unfortunately, the word "focus" is used in a number of places in the Bevy code to mean different things. For example, in bevy_ui, the word "focus" is often confused with "hover".
@tame aspen updated the docs (copied it from fontdb where it originates) and added the comment: https://github.com/bevyengine/bevy/pull/14150
let me know what you think pls
Re-reviewed 🙂 I think we should probably just clean this up and merge it: we can iterate on the fancier system font support down the line.
It's a very simple solution, since it relies on functionality from other crates, which means that the tech debt cost is low
been quite sick for a while, ugh, but finally up and walking about again.
I agree this can be merged, however, me and I assume anyone else using this would wonder how to use the installed system fonts? I mean there could be quite a few?
And the example still uses the FiraSans-bold.ttf loaded through the assets server, so I assume that loading system fonts only gives us fallback in the case of a missing glyph?
So while the tech-debt is extremely low, what we do except increasing startup time by a second, and as it seems overriding the chosen font due to a setting is a bit confusing and would have to be clarified
if it increases the startup time at all noticeably, the default should be false imo
Seems unfortunate that emoji only render correctly if the text color is pure-white. As a user (this is pretty likely not universal) I would rather have support for font fallback where I manually provide fonts via the asset server. And none of my projects have a use for it at the moment, so off-by-default makes sense to me. (this is the current behavior of that PR as far as I can tell).
That's a good point, maybe it should be renamed from load_system_fonts to fallback_to_system_fonts to better indicate the purpose of the flag 🤔
That's right, it's off by default
I agree to this as well
I like this quite a bit
I rather have a way to define the font-fallback from within bevy as well.
But this PR enables the cosmic-text way of falling back to missing glyphs using the system default, and thus the documentation,and namings should reflect this.
So maybe having TextPipeline::with_system_font_fallback() and TextPipeline::default() would make more sense, currently, until the rest of the features are implemented.
The member variable actually is correctly named, its mostly that this also enables the fallback thingies because cosmic text by default tries to find a fallback glyph.
an interesting test, would be to find the font used from the systemfonts, load this manually into bevy, use both fonts in text, and then try to use the font without the smiley, and I would actually expect that cosmic text would manage to fallback even without system fonts
my 2 cents 🙂
Right, so this PR doesn't enable the font fallback feature, it's the default behavior. This PR allows to load system fonts that can be used as font fallback. I suggested rename the member variable to indicate that system fonts are loaded primarily for font fallback... because we don't expose yet any other features like querying loaded fonts from font db for it to be useful.
defining custom font fallback lists is not yet supported I think, blocked on API design (and someone to want it I guess):
https://github.com/pop-os/cosmic-text/issues/126
Sorry if this has been answered but which branch are y’all working on? If I want to test the work in progress which one would it be?
cosmic-text integration has been merged into main.
minimal system fonts (font fallback) by @white monolith is on a PR to main.
i've made a proof of concept of spans as entities as a plugin in a separate github repo. if people like the concept in general, i can make a PR on bevy main to properly implement it, but I think there's some reticence without a story around ergonomic construction of these text nodes (e.g. macros or a dsl, though I've made a first attempt at that too). personally i really like it. i'm not really working on it anymore, since the general idea is done and it just needs some consensus on the way forward, and anyway i think a text editor widget is more impactful in the near term.
the editor widget i've just got as an example in spans as entities (though no relation really). i'll pull it into its own repo at some point (probably the weekend) so it can be consumed as a plugin, and iterate on it from there. once it's got enough features and extension points i plan to then upstream to bevy in a PR. it seems orthogonal to everything else that's being discussed here, and so far i'm implementing it entirely outside of bevy without any need for changes to bevy
Yeah looking at the code for bevy_spans_ent I can see where it shines, and also where it really doesnt shine.
Modification and targeted changes to parts of text mainly is where i see this shining, since just putting a marker component onto the part makes it easily targeted by both specific to that part, or entire categories of text-parts.
Where it doesn't shine is where every part of every text is now a global variable, and would, if implemented in bevy mean that as a user that are always interested in the whole text-section, with different formatting for different parts, I would have to assemble that from the floating around parts, and the parent. Compared to just getting the TextEntity.
So basically, the path forward would have to be decided around the most common usage pattern.
Will people want the animation of text, more than they want the simplicity of the entire text section accessible.
IMHO I think having the spans as entities as a separate or addition API on top of the one we already have makes more sense, and for me that is much due to the fact that in my usecases, the entire textfield is almost always what I want to accesss. Clickable links is an exception to this, but its also kinda similar to writing something like "underscore text underscore" and have that automatically be formatted as "italic"
And these kind of things are kind of text-aware styling parts, that I do find important, but also best experimented inside third-party crates for now.
Maybe spans as entities would be much easier and the APIs for text much clearer once we have better entity-relations, and better spawning of hierarchies.
So for me: having text-editing input such as tigregalis is implementing inside bevy_spans_ent (doesnt seem to be too much work to build ontop the current textsections way from what I can see at a glance) would be the highest priority.
And actually having the bevy_spans_ent as a third-party plugin, would allow us to gather some feedback from the community on which pattern is nicer to work with.
I'd go with addressing font families/stretch/weight/etc as the highest priority. We could just adopt @tulip hinge's code changes that was included in the original PR (https://github.com/bevyengine/bevy/pull/8808/files#diff-e2a5fe455b29f763bc8e86fe638afcd72f4ae755f151cde60621abe0d53c4d5cR61), clean it up & merge and iterate on other completely new APIs as the next step after that... once we gather feedback via 3d plugins.
I’m curious what use case you have in mind where you need to assemble text frequently?
We’ve already discussed using helper methods for these kind of operations so I’m not sure why that wouldn’t work.
yeah, had the same question in mind, why not helper methods
But as long as spans as entities exists and is well supported then I don’t really care if it’s built on a lower level api.
I had a couple spare hours and ripped https://github.com/tigregalis/bevy_text_editor out of https://github.com/tigregalis/bevy_spans_ent and pluginified it, so now the example is (where setup, animate_cursor and animate_selection are userland systems):
use bevy::prelude::*;
use bevy_text_editor::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(TextEditorPlugin)
.add_systems(Startup, setup)
.add_systems(Update, (animate_cursor, animate_selection))
.run();
}
fn setup(mut commands: Commands) {
commands.spawn(Camera2dBundle::default());
let style = TextStyle {
font_size: 70.0,
..Default::default()
};
commands.spawn(TextEditorBundle::from_sections([
TextSection::new(
"Hello, ",
TextStyle {
font_size: 40.0,
..Default::default()
},
),
TextSection::new(
"World!\n",
TextStyle {
font_size: 60.0,
color: Color::srgb(1.0, 0.0, 0.0),
..Default::default()
},
),
TextSection::new(
"Hello, Bevy!\n",
TextStyle {
font_size: 50.0,
..Default::default()
},
),
TextSection::new("and so on and so forth...", style),
]));
}
Could you explain the TempEditor stuff? I'm assuming there's some technical limitation with storing a persistent Editor?
Editor<'buffer> either:
- borrows a buffer (via
&mut) which we can't pass around (i.e. into the ECS / across frames), or - it owns a buffer which means there are two copies of the buffer, or
- it holds an
Arc<Buffer>which means the other buffer also needs to beArc'd
I didn't like any of those options.
Actually #3 ends up cloning the buffer when you call Arc::make_mut, so it's effectively the same as #2 but worse. Arc::get_mut wouldn't be possible since there would always be one other Arc.
I ended up raising this issue in cosmic-text about this very thing (though I didn't need any of the solutions I proposed there, in the end) https://github.com/pop-os/cosmic-text/issues/285
So the canonical approach would probably be to impl Edit for MyType but it would basically involve copying/reimplementing Editor<'buffer> almost entirely, and a lot of that code is logic-dense and non-trivial. But I have thought about looking at that approach at a later stage.
Another approach is to simply not make use of cosmic-text's Edit or Editor and build it from scratch for bevy but until there's a compelling need for that I'd rather just use the tools available.
Oh and there was also the option of replacing the Buffer with Editor<'static> in bevy_text, for everyone (then everything that reads/mutates the buffer accesses it via the editor), but partly I wanted to avoid making changes to bevy if possible and partly I just wasn't a fan of that approach.
Cool, thanks for the explanation.
replacing the
BufferwithEditor<'static>inbevy_text
That's the sort of solution I was initially imagining, but none of the options seem great.
One thing to bear in mind in terms of editor performance and complexity: For every "full-page" text editor widget, there's probably 100x as many widgets that merely edit a single line of text - 256 characters or less. (If you consider all the form field inputs on the web, the true ratio is probably more in the range of 10,000x.) A full page editor typically has a complex data structure for a buffer, whereas most text input fields just edit a simple string. One should expect that someone who is taking on the challenge of building a source code editor will have to put a lot of thought into their data structures, whereas for the vast majority of users who simply wants to enter the name of a player or saved game label, things should "just work" in the simplest way possible. What I would not expect is a single universal editor widget that somehow manages to optimize for the full range of use cases.
Typically modern UI renderers will omit data that doesn't need to be rendered. Things like virtualized tables, lists, etc do exist and egui has them currently. The actually amount of on screen data is usually more dependent on screen size rather than number of widgets.
I just started an experimental crate which uses bbcode for text formatting https://github.com/TimJentzsch/bevy_mod_bbcode
Still in its early stages, but seems to work so far
Nice! Looks like the parser is pretty simple. Although I will say quite a few of the choices are the opposite of what I was suggesting.
When I suggested bbcode the only purpose was to assign labels to spans of text. IMO users should supply a TextStyle component instead of shorthand like [c=#ff00ff]. I really don't think bevy should go down the road of supporting an advanced text notation.
Instead of a hard-coded settings struct you could supply a mapping from label -> list of components. When that span is spawned you'd just look up the label in your map and insert the given components onto the span.
A big benefit of this approach is you don't have to re-parse bbcode when you want to change a piece of text (like in the dynamic example). You just parse it once, then do an ECS query for the piece of text you want and change it that way.
For example, this template: FPS: [fps]0.0[/fps] and this span-component map "fps" -> [FpsValue]
This could create a text-span with the FpsValue component. Then whenever you want to update, you query for FpsValue and change the text for the given span.
I figure there are basically infinite ways in which users might want a text DSL, and they're fairly easy to build on top of the core text support. So many solutions is probably fine/good.
there are basically infinite ways in which users might want a text DSL
That's a big reason why I'm suggesting the DSL should be minimal and we should use components to configure spans
Taken to the extreme you basically have another version of BSN just for text.
As someone who has been down this road before, allowing [img]s might be a can of worms you're not wanting to open (maybe a feature toggle?) -- things may be different from 2002 though, nice crate
Ah I see, that's interesting.
I suppose there is different trade offs one can make, your approach is probably more flexible (although nesting might be more difficult) and performing, while the other one is a bit easier / less boilerplaty to use.
But I agree with Nico, this is quite easy to build on top of bevy text so we can just keep multiple solutions in third party crates (maybe sharing the parser)
Heh, I figure there might be layout problems when in lining the images... Can you expand a bit on the problems you ran into?
Yes, probably. Cosmic-Text doesn't support inline boxes yet. Although I could point someone in the right direction if they wanted to have a go at adding it.
Regarding the parser, is there a reason you didn't use one of the existing bbcode parser crates?
I had a brief look at them and it seems like most of them focus on HTML conversion, which we won't need. So I thought its simpler to just write a custom one.
But I haven't looked at them in detail so maybe this can be replaced with a crate in the future
https://github.com/bevyengine/bevy/discussions/14437 seems to be endorsing spans-as-entities
Corrupted images / not checking image headers properly, remote execution attacks (I think this is more applicable to browser environments but don't see why a 0-day couldn't exist in other environments), remote tracking (think of an empty gif) and then managerial issues (for games w/ communities) on NSFW images [out of scope for this crate either way]
there is discussion happening in github as well as #1264881140007702558
Sorry commented in wrong place. Deleted and moved
Ah, I was thinking about using the asset system for the images instead of loading from a URL, so I don't think we have to worry about this. Or at least not more than we already do.
I expect the system to be used more by the developers themselves rather than user provided strings
Yep, it seems it does, mostly because it lends itself much better to the BSN things.
So I guess the spans as entities question/discussion will probably hinge much on the how the BSN things fall out.
If the BSN macro/fileformat is the way to go (seems very likely), I guess spans as entities matches the bevy scenes way better and makes more sense.
The impl @tulip hinge has could probably be ported over quite quickly then.
And with quickly, I mean when the BSN path forward is certain, (maybe it is idk) we might as well move the text-apis in that direction as well.
Cart's comments on the BSN proposal heavily imply that a text representation that's not entities-based is too challenging to work with. But as @prisma hemlock has said previously, it could just be an implementation detail i.e. there could be an underlying hidden implementation which looks closer to what it is today (or closer to Buffer) but the public API even at an engine-dev level is spans as entities. Working on the editor widget, where I have to "write back" into the Text component, I've run into a couple bugs where maybe a "managed" representation that's closer to Buffer could be easier to integrate. But the obvious thing there is to make Buffer itself the representation, and so the current Text component largely disappears (except for config that applies to the whole "Text Area").
tl;dr: currently Text { sections: Vec<TextSection> } -> cosmic_text::Buffer, proposed (Entity, TextSpan,) -> cosmic_text::Buffer
Hmm, on second thought it wouldn't make writing back any easier, but it does make an intermediate Text component (between spans and buffer) redundant
one more thing that's unlocked is turning TextStyle into 3 or more components: TextColor, FontSize, LineHeight (new), and 1 or more optional components to represent Font Face selection like Handle<FontFace> vs FontFamily(String), Emphasis, Stretch, Weight: I think if there's a Handle<FontFace> we prefer that selection, otherwise we query for a font based on these components.
there could be an underlying hidden implementation which looks closer to what it is today (or closer to Buffer) but the public API even at an engine-dev level is spans as entities.
This seems to be the common pattern for codebases that represent text as entities. Chrome does this. Servo is switching to do this.
I don't think anyway really wants TextSections? They just haven't been removed yet post cosmic-text
I also want to lobby support for inheritable text styles, which can be opt-in. Most games use one font. It's much less onerous if the user can specify that font one time, at the root of the entity tree, than having to pass an asset handle to every bit of text.
Agreed, although the new Construct trait Cart's cooking up will make the passing around handles bit nicer
But still, being able to set default fonts is great
(and I think that most games probably use two fonts)
Inheritable text styles is nice. Although if there is decent support for reusable "components/widgets" (ala React) then text styles can also be passed around that way.
Here's a good stress test for your designs: "when a button is hovered, bold its text"
The existing abstraction fails completely here: there's no way to preserve the font family
And there's also no general way to look up "the bold variant of a given font"
BTW, on the topic of rich text: I think the right approach is not to focus on BBCode or HTML, but to ask the question: "What rich text encoding format does my localization library support?"
Localization libraries do support limited forms of rich text.
However, because translators are often sub-sub-contractors (or in the case of open source, volunteers) they are outside the inner circle of trust of developers. For this reason, localization pipelines are set up in a way such that translators cannot see the actual style markup, but instead see placeholders which are then replaced when the text is loaded. This prevents translators from damaging the markup, or inserting malicious code (such as a <script> tag in a web app).
I don't know which localization library is preferred for Rust / Bevy projects, but whichever one is likely to be used I would start by investigating what kinds of markup it supports for localized text strings, and then build a text-span converter based on that.
Fluent
There is a clearer winner, we should align with it
I'm not familiar with Fluent. I've worked extensively with i18next and other JavaScript-based solutions. Generally, the frameworks that I have worked with are more than just a library for consuming localized content, they are also part of a CI translation pipeline which extracts the source text resources from the project repo, maintains a database and dashboard showing which strings have been translated into which languages, and interfaces with various translation-as-a-service APIs 🙂
Yep, I think that we may ultimately need to build out the latter half of the pipeline 🙂
When I worked at AWS, they had an internal service that used machine translation for a first pass, then a second pass that used human review. This service was compatible with a bunch of different formats: JSON, XML, HTML etc.
In any case, I just briefly glanced at the docs for fluent - it seems like it's the lowest-level piece of what I have described - a runtime for loading strings - and I'm not sure what kind of markup, if any, it supports. It seems rather unopinionated, which may or may not be a good thing in this case.
I think Fluent does not have any rich text built-in, so we could use anything we want (at the cost of translators having to understand how to use the markup language)
For Text entities, it replaces the TextSection if it changed. (It doesn't handle multiple sections, for the same reasons that cart pointed out in the BSN doc).
@finite magnet so I take it you feel strongly that spans-as-entities is the way forward?
Yes very much
The reasoning given was "this is too cumbersome to work with when using scenes"
Which makes sense to me
Fairly unfortunate in the short time scale, due to how much pain more hierarchies are
But I think that's something we can work out
Let me elaborate on that a bit.
And it makes the component design really nice
The reason it's complicated is because you've done all this work to resolve diffs/patches to entity hierarchies - and now you have to do it all again to work on a different hierarchy made up of different things called "text sections".
Right: you're modelling something that's fundamentally the same using an arbitrarily inconsistent framework
Which is also something I've felt as a user
And for most of my text I just want a single section
the one thing I don't really understand about spans-as-entities is how you do ordering of the children?
Children is just a vec. It's ordered / sortable.
I feel like maybe we have gotten off track, we've talked about a lot of things but there's been little tangible progress since the cosmic-text PR landed. I think the most important next step - and one I think most people can agree on - is the whole "font-family" abstraction, that is, being able to say "I want a bold version of the current font" without having to explicitly name the bold font asset.
My first question would be, is anyone actively working on this, or plans to in the near future?
One problem that I think Bevy has is that the ratio of core devs to volunteers is very small, which leads to a classic "diffusion of responsibility" problem - everyone sees what the problems are, but thinks that someone else is going to fix it. The general solution for the DoR problem is to assign tasks to people, but you can't assign tasks to volunteers, they can only...volunteer.
Seconding all of that 🙂
That font family abstraction is the next thing I'd like
And I'd love a concrete proposal
I think this is relatively straightforward, so I trust y'all to come up with something nice
What sort of API would you like to see? This is how font faces are selected in cosmic-text, so we just need to be able to construct one of these:
pub struct Attrs<'a> {
pub color_opt: Option<Color>, // already exposed in bevy_text::TextStyle
pub family: Family<'a>, // the family, needs to be exposed in bevy_text::TextStyle instead of Handle<Font>
pub stretch: Stretch, // variant within a family, needs to be exposed in bevy_text::TextStyle instead of Handle<Font>
pub style: Style, // variant within a family, needs to be exposed in bevy_text::TextStyle instead of Handle<Font>
pub weight: Weight, // variant within a family, needs to be exposed in bevy_text::TextStyle instead of Handle<Font>
pub metadata: usize, // not exposed to bevy_text users, we use it to store the span index
pub cache_key_flags: CacheKeyFlags, // internal to cosmic-text
pub metrics_opt: Option<CacheMetrics>, // internal to cosmic-text
}
pub enum Family<'a> {
Name(&'a str), // e.g. "FiraSans"
Serif, // represents a predefined (common) list of families
SansSerif, // represents a predefined (common) list of families
Cursive, // represents a predefined (common) list of families
Fantasy, // represents a predefined (common) list of families
Monospace, // represents a predefined (common) list of families
}
but we should continue to allow a font to be selected via Handle<Font> if the user desires.
my original implementation did something like:
struct TextStyle {
font: FontFace,
...
}
enum FontFace {
Asset(Handle<Font>),
FontQuery(FontQuery),
}
struct FontQuery {
family: Family,
stretch: Stretch,
style: Style,
weight: Weight,
}
impl From...
I am working on it for my BBCode crate, but probably not in a format that can be adopted by bevy as-is.
I have built a FontRegistry resource which tracks all Fonts which are loaded and then allows you to query them via family, weight etc using fontdb.
This allows the user to just specify a folder path where all fonts are stored and the BBCode transformation can choose the best matching font for the format
The load tracking can probably be adopted because its very convenient to just load the entire font folder.
But the font selection should probably be built in like tigregalis suggests
but we should continue to allow a font to be selected via Handle<Font> if the user desires
I honestly don't think we should. I think we should allow users to register a Handle<Font> with the global font database, get a FontId back (newtype around usize or similar) and use that if they wish.
IMO the interface ought to be a struct consisting of the weight, style, stretch, and family as above (except family possibly ought to be String or Arc<str> rather than &str?)
How do these fit together?
Let us say that we add another step. Something like this perhaps
let handle = asset_server.load("fonts/FiraSans-Bold.ttf");
let font_id = text_pipeline.register(handle);
Now, how should users use FontId to select a font face for their text spans?
I was imagining an additional variant to the Family enum
I guess there are technically two cases here:
- Specifying a font-family (e.g.
Helvetica) by ID - Specifying a specific font within a family (e.g.
Helvetica Bold Condensed) by ID
I think they both should
The ID's are not inherent to the fonts or families. They're just the key in the vec/hashmap that the font library stores the font/families in.
If you load a collection of fonts, fontdb gives you a collection of font id's, rather than one id
If fontdb doesn't support this atm we could add it. fontdb is super-simple. The whole lib is <1000 LoC
(not that we currently support loading font collections)
Interesting. I'm more familiar with fontique which works a little differently.
If it's giving you a specific font, then I guess we ought not to support bold, italic, modifiers being specified at the same time?
Or we just ignore them?
right
Some other "styles" would still make sense (e.g. ones controling wrapping behaviour)
I think wrapping behaviour would stay at the "parent" level
currently TextStyle (Span Style) just specifies color, font face and font size (and could further support line height: size and line height get mapped to Metrics in cosmic-text)
we just need to redesign the font face selection
Some ideas with questions for font selection. Notice I used a singleton pattern, but I think this could be applied onto the Assets<Font> resource as well through trait implementation, or by creating a SystemParam for the fonts and maybe even abstracting away having to use Handles directly.
Not a big fan of having fonts as an enum, where you either specify the handle, or specify a font specification.
// this is how fonts are registred for cusom fonts, or maybe the fontfile itself can provide these parameters instead of having to manually input them?
// this could also be done for all systemfonts, at startup as an option.
Fonts::register("fonts/Noto_Sans.ttf", FontSpecification {
family: "Noto Sans",
bold: false,
italic: false,
});
// bold italic font
let font: Option<Handle<Font>> = Fonts::family("Noto Sans").bold(true).italic(true).try();
// the fallback font basically, since this would also allow unwrap_or_default() to work.
let font: Handle<Font> = Fonts::default();
// probalby the most common usecase
let font = Fonts::family("Noto Sans").try();
// toggling the font boldness
let mut font_modified: FontSpecification = Fonts::specification_from_handle(font: Handle<Font>);
let font: Handle<Font> = Fonts::from_specification(font_modified).unwrap_or_default();
// even nicer would ofcourse be some way to do this. This is where maybe using the handles is a bit to low level
// and we should specify fonts some other way, and let the font selection (FontSpecification in this example) resolve itself to an handle somehow
// maybe through component hooks or something.
let font = font.set_bold(true);
This is not to far from what we are doing with the asset server today I would say, except that this would require more of a singleton pattern, which might not be wanted. But I am quite certain this could be implemented on as traits onto the Asset<Font>.
This pattern of font selection is based upon me opening libreoffice, and making a font look like I want to, through selecting the font, then toggling the italic/boldness until it looks like I want to.
From my perspective, I think to be able to select fonts nicely, we need to have it like this:
- load/enumerate fonts in a db (FontDB)
- select fonts from this db through an API
- apply selected font to the entity in question
maybe 1 and 2 are the only things the user have to do, and 3 is done behind the scenes
compared to how we do it today:
- load a font from file directly to handle
- apply loaded font to entity.
would this be a way?
Taking into account font localization, I agree this is the only path. You have to hide fonts behind an API.
So @true zephyr, currently, text is handled via the Text component, which is composed of multiple TextSection parts in a vec
Both @finite magnet and @mental tree have independently found that this is very frustrating to work with in more complex workflows: you end up with two separate hierarchies that need to be managed
yeah that makes sense
Instead, each TextSection should be its own entity, with a consistent TextStyle
In 90% of cases, a chunk of text will have a single text style: we should optimize for making that case easy and ergonomic
alright, do we have an API in mind? or is that still up for debate
That's the main bit of work: we have broad consensus on "vaguely what it should look like", but not fully mocked API
(I have not followed the conversation, but) the hard part is not the shared style but that it must be considered as a single entity for layout
As I see it, there are basically 3 patterns for text:
- A simple label with a single text style
- Rich text for things like dialog, with interspersed bolds and italics
- Complex forms and document editors
Yeah, and for this PR the focus is the first case, with the second one sprinkled in there too, as I understand it?
Yep 🙂
Ideally this change can be as scoped as possible
It's likely to be quite breaking already
So I would probably just reused the TextStyle type here for example, despite the fact that it's very likely that we want to break apart the different properties into components immediately after
I think I will just whip up something to get started, and open a draft PR. that way everyone can take a look, and once most people agree with the API, we will go for actual code reviews. does that sound good?
That sounds perfect 🙂
I think for now I'll keep it simple, simply make a Text component only hold one TextSection, and make necesarry adjustments to examples, tests and docs. We will go from there
There:
https://github.com/bevyengine/bevy/pull/14572
it is NOT done yet, still many examples that need to be fixed. but I am going to get something to eat first, anyone who wants to can take a look
It is probably important to keep section_index?
I might have time to work on this this weekend, if nobody else is already.
I have built a similar system for my rich text experimentation already so I hope to be able to port things over
why? there is only one section per Text now?
we still need to be able to map glyph layout data to the correct section somehow
Would it be possible to use the entity ID now?
that might be the play, yeah. cc @tulip hinge
also sorry, may not get a chance to look closer until after work.
tigregalis' editor prototype makes heavy use of the section_index stored as metadata in the cosmic buffer, but as far as I can tell putting an Entity would be fine / more convenient.
i'm not sure if that's possible though -- metadata is a usize.
@true zephyr could you add Fixes #7714 to the PR description?
yep, will do
Cool, there's a bunch of relevant old discussion in there that should be linked
@true zephyr With the current setup in that PR, we end up with a bunch of individual "Texts" being laid out by Taffy, but we need all of the spans associated with a particular Text to be added to the same cosmic-text Buffer so that cosmic-text handles the layout.
There probably needs to be a hierarchy of
Text
Span
Span
Span
Where queue_text iterates the Text's children and adds spans to the buffer.
(and yeah, I'd just pick a few examples to update for now)
This sort of thing seems less than ideal:
https://github.com/bevyengine/bevy/pull/14572/files#diff-97f4f79f5065c9dbc0c2f53cba9cb98fcd3bcc3b26daad5d0d6e98b40e552495R310
We should probably experiment with paramsets or something akin to
mut text: Query<&mut Text>
aperture_text: Query<Entity, With<ApertureText>>
/*...*/
if let Some(mut text) = aperture_text
.get_single()
.ok()
.and_then(|e| text.get(e).ok())
{
/* ... */
}
and see what feels best there.
possibly some sort of helper SystemParam for that sort of pattern
The better way to write this is to use Query::get
And then only one query with &mut Text
Q: Do we anticipate any performance issues with word-wrapping multiple text entities? I'm trying to decide if the existing display types such as 'Display::Block' should automatically do this, or if we need a new display type ("Display::TextFlow") to opt in to this behavior. Flex and Grid would probably treat text entities like any other entity: each entity is a separate flex/grid cell. If you want a flex cell with multiple text entities, you'd probably need to wrap them in a parent entity.
I think we can (and should) make the change to multiple text sections as entities in one "hop", rather than incrementally, since it would involve less code changes:
TextPipeline::queue_text() and TextPipeline::create_text_measure() both take a parameter, sections: &[TextSection].
I suggest redefining Text to:
#[derive(Component)] // the parent component
pub struct Text {
// pub sections: Vec<TextSection>, // <- removed
pub justify: JustifyText,
pub linebreak_behavior: BreakLineOn,
}
and adding #[derive(Component)] to pub struct TextSection {}.
Wherever we are currently calling TextPipeline::queue_text() and TextPipeline::create_text_measure(), first we simply collect an additional query Query<(&TextSection, &Parent)> into a let sections: Vec<TextSection> = ..., then pass sections.as_slice() into TextPipeline::queue_text() and TextPipeline::create_text_measure() .
That should be the extent of the "implementation" side of the change (sans some compilation errors).
Then the "user-facing API" side of the change can be handled from there.
section_index is used in cosmic_text::Attrs::metadata and will be required to map back to the individual TextSection, but that's handled within TextPipeline::queue_text() and TextPipeline::create_text_measure()
Btw, considering how breaking this is, this is the opportunity to rename from section to a span, which I think we all agree on?
Changing the name will actually lead to less confusion, I think, if it reflects a corresponding change in functionality. It's confusing when the old name is used to mean something new.
do you think it's feasible or desirable to store an entity reference instead of a section index? (I think technically we can't, as metadata may not be 64 bits on some platforms?)
Might have to store it on a component on the text entity, perhaps lazily created.
Feasible, other than the 64 bit vs 32 bit issue. I can see that it might be desirable to easily grab an entity as a global reference. It's unclear if a per-text index/key or a global index/key is better; that might be informed by some of the more complex and interactive use-cases. I think it should always identify the span in some way though.
Definitely think section index is adequate for now.
The way I am anticipating this working is a TextFlow (name TBD) component that marks the root of a text flow, with separate TextSpan and Text components on child entities that "belong" to the TextFlow. TextSpans would come with an associated set of styles and Text components would have the actual text String or similar. You could also have Nodes as children of a TextFlow/TextSpan which would behave like inline-block/inline-grid/inline-flex boxes on the web (once this feature is implemented in cosmic text).
These would then get "collected" up into a cosmic-text Buffer (as a single string + style spans that index into it) that lives in the TextFlow component for layout, line-breaking, etc.
Web effectively does this too. "Flow" layout consists of Block and Inline layout. Which are implemented largely separately. Block layout is the simple vertical layout of paragraphs (+margin collapsing). Inline layout is the text-based layout (+ inline boxes which flow with the text). The difference on web is that this separation is somewhat hidden from the end-user API: the browser will wrap consecutive sets of inline / text nodes in an "anonymous" block node to separate things out before performing layout.
If we wanted to fully support rendering HTML then we'd need this fixup step too. But if we just want to support a "clean subset" of web layout then we could just enforce that text can only appear within a TextFlow.
^ Which btw is basically what things like React Native do
What is the advantage of separating the String and the spans?
You only need one allocation
I think it's also easier to do shaping/bidi analysis on a single continuous string.
But given that we're using cosmic-text there is a simpler answer: it's the format it uses.
Oh wait. You mean TextSpan vs Text. This is to allow for nesting. For example:
<p><b>the<i>quick</i>brown</b>fox</p> is going to become:
TextFlow
- TextSpan (Bold)
- Text (the)
- TextSpan (Italic)
- Text (quick)
- Text (brown)
- Text (fox)
You can also mix in Nodes once inline boxes are supported
It is not the format it uses.
cosmic-text keeps a separate string allocation for each "paragraph"
(which is also lossy, it turns out)
I suppose that's true. But that detail is not exposed externally (is it?)
I get what you're saying but having Text alongside TextSpan feels like an implementation detail that's being exposed externally
As I see it "text-as-entities" in general will be a low-level API that happens to be exposed externally
I wouldn't expect anyone to be spawning these manually
I generally have the opposite expectation. In order to enable the more useful behaviours emerging from spans as entities, people will be adding arbitrary components to text spans.
I think being able to nest spans within other spans could come in handy, but it could also be provided by a higher level API
or we find a different way of representing children as such
on the web, a text node (unstyled) is the most atomic form of text I suppose
essentially:
<p>
<span>
#text
we could represent it that way I suppose
which is to imply that Text is a child of TextSpan
I think I'm in agreement here, I prefer the more flat representation at the lowest level. However, I'm open to argument either way.
The templating language can convert from a hierarchical representation to a flat one if that's the desirable ergonomic.
TextFlow <p>
- TextSpan <b>
- Text "the"
- TextSpan <i>
- Text "quick"
- Text "brown"
- Text "fox"
That's the same tree as I had above (#1248074018612051978 message)
Ah right so it is
my mistake
but where is the string allocation?
i think each Text would have a string allocation
Yes. But I'm imagining they'd be another one (or one per paragraph as you point out) within the cosmic-text Buffer. Which I'm imagining would be a field in TextFlow.
I may have misunderstood and we might be talking across one another
I want to inject into this discussion something that Alice and I were discussing earlier. Specifically that Quill UI currently only supports a single text section in the text elements, and why that is. Quill has a lot of code for reconciling ECS hierarchies: that is, given a template and an existing entity tree, there's code that determines the minimal set of mutations for bringing that entity tree into conformance with the current state of the template. This includes both the entity parent/child hierarchy and the components.
Now, given that the templating system does all this work to reconcile the entity tree, with text sections as they exist today we have to do all that work again to reconcile another kind of hierarchy with another kind of node - text section. It's even worse, however, because unlike entities, text sections don't have any kind of id, nor is there any easy way to associate an id with a text span. So the reconciler has to work solely on values. Given that the vast majority of widget labels are a single string with a single style, I decided to punt on the issue until we sorted out all the issues that are being discussed right now.
And I get the impression that cart has run into similar issues in the design of BSN. So my ask, then, is that when designing the representation for text that we consider these kinds of use cases - that is, the use case where a sequence of text strings are being synchronized with some (potentially dynamic) source of truth.
Note that converting a hierarchical template into a flat representation is a fairly simple problem, and in fact Quill templates already do this for entities.
wouldn't this require a TextSection to always have a parent with a Text? not sure if this is bad, but just wondering
commands
.spawn((TextFlow, P))
.with_children(p|
p.spawn((TextSpan, B))
.with_children(|b|
b.spawn(Text("the".into()));
b.spawn((TextSpan, I))
.with_children(|i|
i.spawn(Text("quick".into()));
);
b.spawn(Text("brown".into()));
);
p.spawn(Text("fox".into()));
);
yes it would
is that desired? Seems annoying for the common case where there is just one section for one Text
BTW, in quill templates the children of a node can be any value that implements the View trait - and both String and &str implement that trait. So you can write things like:
Element::new().children(("Hello, ", child_element, "World"));
The strings get converted automatically into text nodes.
Right, it is, and that's the tradeoff, but that's sort of why we've put it off until BSN came along
But you bring up an interesting possibility there... we could special case it - would we want to?
as an example:
// standard
commands.spawn(Text).with_children(|text| text.spawn(TextSection::from("foo")));
// special case
commands.spawn((Text, TextSection::from("foo")));
definitely like this API, but of course there's overlap with EntityPatch (and BSN)
I think that is not bad, just a check to see if the parent Text also has a TextSection component and add it to the list if it does
only question I have is how should we handle this case
commands.spawn((Text, TextSection::from("foo"))).with_children(|text| text.spawn(TextSection::from("bar")));
I guess we just make the TextSection of the parent the first element of the slice we pass to TextPipeline::queue_text(). seems like the most logical choice to me
Yeah that seems logical
Should I go make this change, or wait for some more feedback?
I think you should go for it and we can see how it feels. The only thing that may be controversial here is the special case, but that seems like a good ergonomics win.
So just to recap:
- we make
Textno longer hold any sections. - we make
TextSectiona component. - The process for queueing the text is:
-- check if theTextparent also has aTextSection, if so, make it the first element of the list
-- add all childTextSectionsto the list
-- submit it
right?
Yep that's my thought
I had a quick try at doing this (not including the special case) and I'm not quite sure how to make the TextSection live long enough... I think maybe queue_text needs to take an iterator instead
Hmm, the meaning of section_index starts to get a bit confused too, with the special case, now that I think of it.
I will see how far I come, if I run into issues, I will let you know
how do we map from section_index to the correct entity? specifically in this system, I really don't know how to access the right TextSection for the color....
https://github.com/bevyengine/bevy/blob/main/crates/bevy_text/src/text2d.rs#L67C1-L134C2
this is what I did:
text2d_query: Extract<
Query<
(
Entity,
&ViewVisibility,
&TextLayoutInfo,
&Anchor,
&GlobalTransform,
&Children,
),
With<Text>,
>,
>,
text2d_span_query: Extract<Query<&TextSection, With<Parent>>>,
let section = text2d_span_query.get(children[*section_index]).unwrap();
if *section_index != current_section {
color = LinearRgba::from(section.style.color);
but you'll potentially run into this (if you've already added this)
should we just reserve section_index 0 for the section on the parent, even if it isnt present?
Yeah I think that could work.
let section = if *section_index == 0 {
maybe_section.unwrap()
} else {
text2d_span_query.get(children[*section_index - 1]).unwrap()
};
we do need TextPipeline::queue_text() to know if the special case is present, is a simple bool parameter fine? I don't see a cleaner way
Yeah queue_text is an implementation detail so I don't see why not
How should we handle this file in bevy_ui? I am not too familiar with its purpose, so can someone tell me what we want here?
https://github.com/bevyengine/bevy/blob/main/crates/bevy_ui/src/accessibility.rs
also, should we make TextBundle have a TextSection?
it's the access_kit (accessibility, e.g. for screen readers) integration. it seems that all it needs is to construct a single string from the text
even in the calc_name function? do I just grab the TextSection on the parent (and its children), join them together, and done?
I think that would map to the same behaviour yeah
alright, and I assume we want this right?
hmm personally I don't think so
are there optional bundle components?
https://github.com/bevyengine/bevy/issues/2157 doesn't look like it
I think I would be more inclined to have separate bundles, or just document it
otherwise we'll basically always have an empty span in the parent
true, I guess we should just document it.
Never mind, we should create a new TextSectionBundle (name up for debate) because it needs the proper UI components, like Style. otherwise bevy will complain like this:
2024-08-02T09:44:02.485636Z WARN bevy_ui::layout::ui_surface: Unstyled child `5v1` in a UI entity hierarchy. You are using an entity without UI components as a child of an entity with UI components, results may be unexpected.
Not sure what should be in the bundle though...
I honestly think this warning should not happen for child text sections. Because what is a Style on a child of the parent Text going to do?
I commited what I have so far, I'll go and take a break, and wait for feedback.
think we just be changing what triggers these warnings here but I'm not familiar with this.
Appreciate the explanation of Span -- I have been thinking more in terms of cosmic spans and exposing that to normal users felt a bit wild.
I do think that
Text (optional default style)
- Section
- Section
Seems like a good place to start though. One
commands
.spawn((TextBundle::default(), LARGE_TEXT))
.with_children(|parent|
parent.spawn(TextSection::from("Hello, "));
parent.spawn((
TextSection::from(format!("{name}")),
GREEN_TEXT,
NameText
));
);
commands
.spawn((TextBundle::default(), LARGE_TEXT))
.with_children(|parent|
parent.spawn(TextSection::from("Hello World"))
);
Really doesn't seem bad to me. Ergonomics in spawning are offset by ergonomics in modifying specific sections.
Maybe with a hypothetical with_child, the case for a single section could be simplified.
commands
.spawn((TextBundle::default(), LARGE_TEXT))
.with_child(TextSection::from("Hello"));
// without the "default text bundle style"
commands
.spawn(TextBundle::default())
.with_child((TextSection::from("Hello"), LARGE_TEXT));
Or else with a macro like https://github.com/tigregalis/bevy_spans_ent/blob/main/examples/macro.rs#L20, but I'd rather just wait for bsn! personally.
Maybe with a hypothetical with_child, the case for a single section could be simplified.
Yeah, I'd love to see this
How does this multi-entity approach mesh with localization, in the case that a paragraph needs to be rearranged? Would the localization text be expected to have a markup that gets parsed at runtime into the entity structure? The problem is basically the same for current text sections honestly.
So you’d write the dynamic localization template to the root entity, and then behind the scenes an aggregate localized string is produced, which is then split using a markup into sections.
This feels like a case where the templates that @finite magnet loves would be great
Related question: how would you implement a (potentially localized) paragraph where certain keywords have an on-hover or on-click effect?
Each of the keywords would need to be its own span, and I would give each of them a marker component for this behavior that's update by a system that checks Interaction or its replacement.
The main challenge with this sort of thing IME (including for button highlights) is that the original data for "what was the standard color / material / text style" gets thrown away, so you either need to generate all of the cases based on state, or cache it in another component and swap
with_child: https://github.com/bevyengine/bevy/pull/14594
The design discussed above would not have Node on the spans (or am I mistaken?), so how is interaction detected?
We would need a text-picking backend
#1236111180624297984 should make that easy to implement though
Especially with the span-based design
Probably opt-in: picking specific text spans is relatively rare
yes
I think in the localized case its hard to avoid using a rich text wrapper like bevy_mod_bbcode.
Depending on the locale the keywords might have a different order, so a static structure is no longer feasible
given this has landed, should we rip out the special case in @true zephyr's PR? if we do, it will be easier to build on top of, to maintain and to teach
I personally am not a fan of the (Text,TextSection) special case even without with_child.
I bet that a lot of people try to do that though. I guess it's sort of neat if it worked, but... it feels dirty.
I agree, we don't typically do this elsewhere, we just hadn't thought of that approach at the time
With respect to text picking, I have this implemented here
https://github.com/tigregalis/bevy_text_editor/blob/97194f432a44343a8763aaaa4a1b1b25c5d5ab29/src/lib.rs#L911
or here
https://github.com/tigregalis/bevy_spans_ent/blob/e520da1ab18a498bdc97e8c7ebe06540174d0e97/examples/advanced.rs#L267
for UI only.
These are almost the same, but with slightly different needs. They can easily be rolled into one. Additionally the "inner logic" can be made to be "relative to the buffer's frame of reference" so we could easily support things like 2d text: the math, without rotation at least, is very simple, but presumably that is handled by bevy_mod_picking
effectively:
- what window is the cursor in?
- where is the cursor within that window?
- (translating the window to the current coordinate system) where is the cursor within the current coordinate system (UI or 2d world space, or a ray in 3d space)
- what entity/ies does the cursor intersect with (the parent entity)
- (translating to the entity's local coordinate system) where is the cursor within the entity's local coordinate system
- which span, under the cursor, is intersected?
info you could expose: window coords, current coord system coords, parent entity, local coord system coords, the span index, the span entity, the glyph
that same example handles interacting with specific spans (e.g. hover, hyperlinks), and spans as entities (assuming the hit detection is taken care of) makes that very easy to implement.
with respect to hover styles, I did have to do this:
#[derive(Component)]
struct HoverConfig {
base_color: Color,
hover_color: Color,
}
which sort of implies that perhaps we need a ComputedStyle (a la the web, or bevy's GlobalTransform). ComputedStyle would make things like animations/transitions easier too. the current TextStyle on a span is the computed style but we are editing it directly
It seems to me this concept of computed (underlying) values would be applicable to UI more generally, and perhaps even to other areas in bevy engine or plugins or userland. it's essentially "derived state", in the reactivity sense. not sure if there is any abstraction that would work for all cases though.
does project fluent provide us with any built-in way to apply attributes to spans?
Hi <bold>{ $name }</bold>!
Regarding computed styles. There are some potentially significant memory optimisations that can be made if we add a style system to compute them:
- The uncomputed styles can be
Vec<Style>where style is an enum of style properties. So you only pay a memory cost for styles you're actually using. - The computed styles would probably still want to be a great big struct, but could be wrapped in
Arcand deduplicated so entities with the same styles can share memory. If structs are particularly large then you can also have subsections wrapped in their own Arc
I like this but I have a slightly different idea. Each style property is its own component on the entity.
I will go and do this in a bit
Can we have a little poll maybe to see if everyone agrees with the changes?
(I will push the last few changes soon)
To recap:
Textdoes not contain ANYTextSections anymore.- Instead, the children of an entity with
Texteach have 1TextSectionnow. - We were originally going to support having a
TextSectionon the parent (where theTextis) for the common case where you only need 1 section, but since we haveCommands::add_child()now, that is just not worth the ugly code.
Maybe react with a 👍 or 👎 to let me know what you think, and any text feedback to go with it is nice too!
(based on the feedback I can start fixing up examples, writing a small migration guide, etc)
No, I dont think so.
So either we have to apply the attributes after we get the localized string, or we can try to abuse the functions in Fluent for this purpose: https://projectfluent.org/fluent/guide/functions.html
that seems... not ideal
I was envisioning something quite different.
I was thinking something like this:
command.spawn((NodeBundle {
style: Style: { display: Display:TextFlow } })
.add_children((
commands.spawn((TextNode("Hello, "), TextStyle::default())),
commands.spawn((TextNode(name), TextStyle { bold: true, ..default })),
commands.spawn((TextNode("!"), TextStyle::default())),
commands.spawn((ImageBundle { image: image_handle })),
))
In other words:
TextNodeis a component which contains a text string.TextStyleis a component which specifies a text style (bold, italic, font family, etc.)- Text nodes are just regular entities.
- The list of text nodes is a flat list. The template system is responsible for handling hierarchies like
<b>Hello, <i>World</i></b>and generating text nodes in the right order. - Any UI node can have TextNode child entities, there's no special container type.
- 3D scene nodes may also be able to support TextNode children for 3d text.
- `TextNode entities can have non-text siblings: icons, form fields, emoji, etc. That is, you can have both text and non-text within the same parent.
- The
Display::TextFlowdisplay style causes all of the children of the entity to be treated as a single, merged inline block, with line-breaking, word wrapping and so on. - Other display types (
Display::Flex,Display::Grid,Display::Block) treat eachTextNodechild as a separate layout block - so for example if you have a Flex container with rows, each text item appears on a separate row. Word-wrapping and line-breaking still happens, but only within a singleTextNodeentity.
I think these aren't incompatible
let's start with what we have... then next we can deconstruct the TextSection into the value and the style
then (imo) deconstruct the style into the individual properties
I'm fine with the text string and the style being a single component.
What I'm getting at is:
- There's no hierarchy of text nodes, it's just a flat array of children.
- Nested style spans are handled by the template layer. There are no overlapping styles.
- Text and non-text nodes can be freely mixed (this isn't supported in cosmic-text today, but we should design the system to accommodate this in anticipation that it will be supported).
There isn't really a deep hierarchy of text nodes, we just translated this
Text 1v0 {
sections: [SpanA, SpanB, SpanC]
}
into this
Text 1v0 {
children: [2v0, 3v0, 4v0]
}
SpanA 2v0
SpanB 3v0
SpanC 4v0
There needs to be a block node entity which will hold the CosmicBuffer, the block-level styles, attributes and layout info (the TextBundle), and the spans are direct (flat list of) children of that text bundle
If you want a deeply nested hierarchy that is up to the user or plugin author
so if we had something like Display::TextFlow and it's just a normal Node, we'd need to dynamically construct TextBundles
(add the relevant components based on the value of the Display property/component)
I think a nested hierarchy is out of scope for this PR, though we might want it later
For that, I would say that a component that contains the buffer can be inserted automatically, sort of like how GlobalVisibility or GlobalTransform works. If the TextNode has a parent which is TextFlow, then the buffer component is inserted on the parent. If the TextNode parent has any other kind of display, then the buffer component is inserted on the TextNode.
How does it work with Taffy? I assume that there needs to be various hidden data structures to calculate flex layouts, grids and so on.
Does anyone know how we can get rid of these warnings?
sort of like how GlobalVisibility or GlobalTransform works
those aren't automatically inserted though. they are present because all our relevant bundles contain them.
not saying that's impossible, but I do think this PR is a good step while we debate how "deep text hierarchies" could work / if we even want that
Nested style spans are handled by the template layer. There are no overlapping styles.
I am not seeing this as a showstopper personally.
yeah, and I assumed that adding all the TextBundle components to Node by default would not be acceptable
doing some brief research on this, there's a@fluent/dom which allows for a limited subset of inline html, and add attributes directly to html. we would need to, i assume, develop a fluent_bevy or something along those lines
https://projectfluent.org/dom-l10n-documentation/background.html#high-level-dom-localization-and-friends
above should have been a reply to this
We do have https://github.com/kgv/bevy_fluent and https://github.com/TimJentzsch/bevy_mod_localization experimenting with Fluent integration
In my opinion, the API is suboptimal though at this stage
looks like we can only get plain string resources out of it. i guess i just assumed fluent would allow for arbitrary attributes on spans for the application to make sense of (i.e. generate trees, or lists of fragments, rather than just whole strings).
which implies we would need some form of text-based templating language built on top
That was my thought as well
Hence my bbcode experimentation
Of course performance is a concern though, especially when we get into interpolation of variables
Things like numbers and dates need to be localized, so whenever they change, first Fluent needs to be processed and then whatever templating we build on top of it
So I suppose we have to make it performant enough to be executed every frame in the worst case
... do we fork project fluent's rust implementation
so we could:
- support arbitrary attributes on spans (fragments): i don't think we would even need to change the language:
bolded = world
.components = Bold
message = hello { bolded }
- instead of a string, generate fragments, and work with that, and only dynamic fragments need to be recomputed
[
{
text: "hello "
},
{
text: "world",
attributes: {
components: "Bold"
}
},
]
Have a look at how 18next handles styles for the Trans component: https://react.i18next.com/latest/trans-component
This is what the developer writes:
<Trans i18nKey="userMessagesUnread" count={count}>
Hello <strong title={t('nameTitle')}>{{name}}</strong>, you have {{count}} unread message. <Link to="/msgs">Go to messages</Link>.
</Trans>
And this is what the translator sees:
"userMessagesUnread_one": "Hello <1>{{name}}</1>, you have {{count}} unread message. <5>Go to message</5>.",
In other words, the extraction pipeline strips out the style tags so that the translator can't modify them, and replaces the with numeric placeholders; these placeholders are then restored during the build using the data that comes back from the translator.
Localization is the process of swapping out text, fonts, images, sounds, etc. based on the language preferences of a user.
Is that right? Is it not the other way around? I thought that fluent files were written first and imported as resources.
That is for i18n, not fluent
Ok. What's the workflow? Write your app with Trans components, extract a localisation file out of it somehow, localisers localise it, store the localisations somewhere in your app, now, given a different locale the same Trans component displays the appropriate localised text.
Looks like it has some compatibility with project fluent
title = Welcome to react using react-i18next with fluent
description_1 = To get started, edit <1>src/App.js</1> and save to reload.
description_2 = Switch language between english and german using buttons above.
emails =
{ $unreadEmails ->
[one] You have <1>one</1> unread email.
*[other] You have <1>{ $unreadEmails }</1> unread emails.
}
I don't want to get too far ahead of ourselves here. We're not ready to work on localization or parsing of style markup just yet - the only reason for bringing this up now is that it might have an impact on the low-level text span API design. These APIs really need to provide a decent developer experience for three kinds of users:
- direct spawning of text spans in code.
- creation of text spans from a templating language (BSN or something else).
- conversion of localized strings with markup in assets into text spans.
All three of these use cases have both a static and dynamic variant. The static one - where the text is spawned once and never changes - is easy and can work with just about any API. The dynamic one is the more interesting problem.
Hmm I don’t like the duplication between the reference and the English localization (which would have to be kept in sync constantly…). The number tags are very nice though.
In fluent you assign dynamic values like format!("counter-text?count={}", 5). Maybe markup tags could be assigned in a similar way.
Fluent resource:
counter-text = <{$1}>Count: {$count}<\{$1}>
In-code:
format!("counter-text?1=bold&count={}", 5);
Not sure if numbers can be arguments. It would also be nice to elide the extra braces and $.
So are you saying something along these lines:
format!("counter-text?1=bold&count={}", 5);
counter-text = <{$1}>Count: {$count}<\{$1}>
->
{ "counter-text": "<bold>Count: 5<\bold>" }
->
let localized_text = fluent.localize(format!("counter-text?1=bold&count={}", 5), "en-US").unwrap(); // "<bold>Count: 5<\bold>"
let spans: Vec<(String, String)> = bevy::templating::parse(localized_text).unwrap();
for (attrs, text) in spans {
match attrs {
"bold" => { parent.spawn(TextSection::new(text, TextStyle { font: bold.clone(), ..default()})) }
_ => { parent.spawn(TextSection::new(text, default()) }
}
}
In examples and in-engine, should we prefer TextSection::new(TEXT, default()) or TextSection::new(TEXT, TextStyle::default())?
I am thinking of something like this:
- User creates a component like
LocalizedText::message("counter-text").with_arg("count", 5)
- Based on a
Localeresource, the corresponding asset file is selected automatically - The asset file is parsed and the corresponding UI hierarchy is constructed. In particular, the
countarg has it's ownTextSectionwith a marker component. (This would likely require us to fork/hook intofluent-rs) - The user can dynamically update
LocalizedText, e.g. update thecountarg. The localization system will then use ECS queries as usual to update the correspondingTextSectionefficiently.
The advantage would be that dynamic updates don't require us do parse everything again, we can just use queries.
The disadvantage is that this is a lot more complicated to "get right", especially with the formatting functions and selectors coming into play
But probably something we need to tackle incrementally
It’s not clear to me if this is more efficient than keeping insertables in a single string. You’d need additional allocations to store each insertable separately, and the text systems would need to do more work every tick to assemble spans.
You also may lose access to some of rust’s built-in formatting by setting args that way, rather than with format!() or write!().
There’s also an issue of duplication of logic for setting args at construction vs update sites.
Right, although ideally with a lot less allocation.
Hmm right, I guess it would need benchmarks
We can build the simplest solution first and then improve it based on what results we get
Although we should avoid changing the API too much. Until we have higher layers that build on the lower level text API, every change will create a high amount of churn for the users
Another thing: I like to write the initial template to Text and then copy that into LocalizedText because it better matches how you write non-localized text. Requiring users who switch from non-localized to localized to move where the text is written will definitely be a perpetual source of bugs.
If we go for a markup language, then I think we’d expect users to write out single strings anyway which are split into spans behind the scenes. (and only manually write to spans at a lower level)
Wouldn't it also be a source of bugs if the user first writes into the struct which is later modified due to the localization?
Then Text wouldn't contain anymore what the user put into it initially, which could also be a source of bugs or confusion.
Maybe I'm misunderstanding what you're suggesting
The expectation is if you have a LocalizedText component on an entity, then text auto-localization will happen. If you try to mix LocalizedText with text that fails to localize, then an error/warning will be logged.
What would the workflow be in that case?
So the user puts the key of the message in Text, that is copied over to LocalizedText and then the Fluent output is copied into Text?
How will the localization system detect that the user changed the localization args as opposed to the internal system changed the Text?
That’s the thing, you can’t use change detection (at least right now) on Text because otherwise animating text color is impossible. So you need to auto-localize in a custom system param. If Text is split up into components then the question changes a bit.
I designed bevy_cobweb_ui’s localization based on current Text ^
Ah, right.
I'm operating under the assumption that we will get spans as entities and TextStyle as component
Even if those changes are made, I don’t right now see bevy_cobweb_ui’s API being improved. The TextEditor system param makes text editing immensely easier.
However relying on it does lock you into some limitations, since TextEditor internally has a mutable query. So you can’t query Text in the same system without a ParamSet.
Which would become even worse if Children come into play.
It’s very likely we need markup for localization, which begs the question if markup should also be a mainline feature for non-localized text. The problem I see is how to support both markup text (which gets parsed automatically into spans, and unused spans are deleted) and manual text (where users fully control their own text). I think we’d need an additional MarkupText component.
Another issue is localizing fonts, which has to be done any time the user sets a new font (the language needs to be detected from the text, it can’t be inferred from negotiated langs) and whenever a text’s language changes.
Relying on change detection, I think you’d need to set and store the requested font in LocalizedText.
Another problem: if you want to animate text color on a specific subspan of a localized/markup text, how do you identify the right section/child entity? I think the markup language would need tags of some kind.
For bevy_mod_bbcode I added support for a tag which allows you to insert marker components on a text, so that part is possible to implement
Here is an example showcasing the current API for changing text and color dynamically: https://github.com/TimJentzsch/bevy_mod_bbcode/blob/main/examples%2Fdynamic.rs
Ok I’m coming around to writing the template in LocalizedText (and MarkupText). You’d also need to write the font family and primary font style. Then when the system that auto-localizes text runs, it should bypass change detection on LocalizedText when updating the internally-cached language ID and other cached info.
But I still think storing args separately from the template is not ideal.
That's essentially what I did with BBcode, although its a bit hacky right now.
The BBCode entity that the user creates essentially becomes the root of a UI tree where the text entities are inserted with the correct styling. I suppose the localized text with markup could work similarly
(Right now the "tree" is flat though and only has text as children)
Yeah sounds right to me.
It does change the layouting though, we probably need the inline box layout for it to work correctly
Change layouting relative to what?
To sibling UI entities around the markup text
I didn't experiment with that part yet though
Why does bbcode need separate entities? Can’t you just write to text sections?
Sorry I still need to look at your repo properly
To insert the marker components for the dynamic updates
Right now its quite unoptimized though and never creates multiple sections in a text, I focused on functionality over performance for now
Yea I also need to look at your repo to understand how you handle localization right now
@true zephyr in relation to this:
https://github.com/bevyengine/bevy/pull/14572#discussion_r1705331294
Within bevy_text::TextPipeline::queue_text():
section_indexis born inbevy_text::TextPipeline::update_buffer(): (sections.iter().enumerate())- copied into
cosmic_text::Attrs::metadatainget_attrs(...) - cosmic-text copies it into
cosmic_text::LayoutGlyph::metadataduring buffer update/creation - copied into
bevy_text::PositionedGlyph::section_indexfor later use
Since section_index is no longer really used as an actual "index" (just as a key to reference the section), it can just be a "metadata" field (we should perhaps even rename it).
You could just "recreate the conditions" at usage so that the index means the same thing as they did at creation, but the most robust solution is probably to create the "index" from outside TextPipeline and pass it in:
- enumerate all the Children of the
Textto get the "Children index", i.e.
text_section_query: Query<Option<Ref<TextSection>>, With<Parent>>,
sections.clear();
sections.extend(
text_section_query
.iter_many(children)
.enumerate()
.filter_map(|(index, maybe_section)| maybe_section.map(|section| (index, section)))
);
- pass in the
&[(usize, Ref<TextSection>)]toTextPipeline::queue_text()instead of&[Ref<TextSection>](or you could potentially just pass the iterator directly, and you won't need thesectionsscratch buffer) - use this index through
metadataand ultimately intoPositionedGlyph - now
text2d_section_query.get(children.unwrap()[*section_index]).unwrap()should be fine (sort of... that section could have been despawned between updating the text layout and drawing the text, perhaps throw a warning, and skip)
The other idea, that was discussed recently, was to use the entity itself as the metadata... but:
- if we take the
entity.index() as usizeas the metadata (u32 as usize), we wouldn't even need the parent to retrieve the data (but we can always get to the parent from the child's Parent component if required), but I've no idea how to get back to an entity from the index (doesn't seem to be supported), and strictly speaking that section could have been despawned between updating the text layout and drawing the text - if we used
entity.to_bits() as usizeand laterEntity::from_bits()these are 64-bit numbers that may not be compatible on all (i.e. 32-bit) targets - alternatively we could create a global resource
HashMap<usize, Entity>or justVec<Entity>, and we use that to look up the actualEntity.
Actually this
section could have been despawned between updating the text layout and drawing the text
could be an issue with all the approaches. Generally between running theupdate_text_layoutsystems and the rendering of the text, users shouldn't be messing with the hierarchies, but they could.
Well, I guess it's no different from today - users could mutate the Text component between running the update_text_layout systems and the rendering of the text
So maybe I'm overthinking it
re: 1, yeah, I don't think it's appropriate to do an entity -> index -> entity conversion at runtime.
re: 2 yeah, we probably shouldn't do this. Would be totally fine if we could convince cosmic-text to make metadata a u64, I think.
re: 3 I may be wrong but this doesn't feel much better than getting the entity from Children.
So maybe I'm overthinking it
yeah, probably. though having friendly warnings when this happens would be ideal though
is this a cosmic text help thread? 🙂
what's the best way to embed a square sized icon inside a cosmic text layout_run with an arbitrary font? (on the layout level, rendering is a custom implementation)
it is not
Given the "square" constraint, this probably shouldn't be too hard. Assuming you have a "square glyph", you'd just make the font size your intended height, make the glyph colour fully transparent, find the position of it and draw your icon on top. Or, instead of a transparent glyph, you could have your metadata indicate to your renderer that this is an icon.
thanks! making a square glyph in the font makes sense for my own game, but if I want to publish my rich text implementation I wish there are better options (is m or M a good choice? since em is supposedly the width of an m)
wait no I guess there's kerning
You could do the "embedded font" thing like Bevy uses for its default font.
And that font could specifically select exactly one glyph.
(There's a proper term for this, I forget what it is)
Subsetting I think
that's a great idea 🩵
It'd be easiest either to find some font with an exactly-square glyph, or make that font yourself with just that one glyph.
cute lil 2kb font
Apparently an "em dash" character is what you want perhaps
Supposed to be exactly 1 em wide
I suppose if you got your custom single-glyph font it doesn't matter though
oh, "em" is the height of an M btw, not the width, so since the height is known (the font size, or 1 em) you just need to control the width
Ok, sorry for not really being active, but I can get back to the PR now. can someone give me a recap of what should still be changed?
(other than fixing examples)
(I'll take a look at this in a couple hours if no one beats me to it)
afaik nothing new has come up beyond the reviews on GitHub. tigregalis' message above is a suggestion on how to deal with some of that.
there was some discussion about Entity vs. section index which you can ignore -- nothing to be done there.
Once we get text entities and font families out of the way, I'd like to discuss a very basic editing API. Doesn't need to be very elaborate. The idea is to enable third-party crate authors to implement text input widgets. As a third-party crate author, I would expect to be responsible for updating the text entities when the user types a key, drag states, insertion/deletion, undo and so on. What the Bevy API should provide is:
- picking: takes pointer coords as input and returns the character index of the nearest glyph hitbox. Both box-style and i-beam style picking should be supported. If the coords are outside of the text block, it returns the nearest result.
- selection: given a (start..end) range of character indices, returns a list of rectangles representing the area of the selected glyphs. If the text is bidirectional there may be multiple rectangles returned.
I presume cosmic-text already has functions like this, so potentially these APIs can just be thin wrappers around what's already there.
I would expect to be responsible for updating the text entities when the user types a key, drag states, insertion/deletion, undo and so on.
While I feel like text-as-entities is generally a good model, I feel like you're doing things in hard mode if you implement editing like this. If you just have the "text input" field be one entity that contains it's own state then you can use Cosmic-Text's entire editor abstraction which has the following available actions: https://docs.rs/cosmic-text/latest/cosmic_text/enum.Action.html
OK, that could work. And it doesn't seem so specific to CT that you couldn't make something similar for Parley if we decided to switch later. It should be possible with an API like that to implement "controlled" (in React terms) widgets - that is widgets where the reactive framework decides when a mutation happens, as opposed to the native widget doing all mutations internally and the reactive framework only getting notified after the fact. The fact that controlled text inputs actually work in the browser seems like a bit of an accident, and I can well imagine that if we're not careful in our design we might foreclose that possibility; this would mean that only "uncontrolled" text widgets were possible for third-party devs to implement.
For those not familiar with this:
- Controlled widgets are ones where the application code is part of the editing loop for the widget, meaning that the app code listens to input events and decides whether or not to update the widget state.
- Uncontrolled widgets are ones where the native widget is entirely self-contained, and the app code can only (a) initialize the widget state to some value, and (b) get notifications after the fact when the widget updates itself.
Many developers prefer controlled widgets, as it makes it a lot easier to implement things like form field validation, restricted numeric inputs, and so on.
Uncontrolled widgets also have this weird ambiguity because sometimes you need to change the widget state programmatically, but the widget is also changing its own state programmatically, so you have to be careful not to cross the streams.
React supports both styles, which is a nice flexibility.
The simplest example is a checkbox: in the controlled case, there's only one source of truth, which is some state variable in your app; the checkbox never sets its own state, but instead emits an event which tells your app to update its state, and this in turn causes the checkbox to redraw in response. In the uncontrolled case, the checkbox sets its own internal checked state, and then sends an event telling the app that something changed; this means you potentially now have two source of truth which have to be kept in sync.
All of the widgets in quill/obsidian are controlled; for example, the open/closed state of a dialog is a parameter which is passed in from the outside, not internal private state.
So how this relates to text is that I'd like the option to be able to write a controlled text input widget.
Ok, I'll go and implement this once all examples are ported over (i FINALLY managed to fix all the problems on my dev machine, so I can work on this again)
I am seeing a pattern that the following is REALLY common:
commands.spawn(TextBundle::default()).with_child(TextSection::new(...));
Maybe we need a follow-up PR where we add some form of shorthand for this
Actually think this should be solved right away, because adding a PR that increases ergonomics, that decreases ergonomics is a bit of a pain.
But then again, adding things with children is generally speaking not very ergonomic in bevy right now
I think that adding even more just increases the scope of the PR more than necessary. It's already breaking enough
I think perhaps the examples may only show the extra ceremony, but don't as often show the benefits. Perhaps we could port the examples from https://github.com/tigregalis/bevy_spans_ent ?
Yep great idea, but once again I don't think this PR
I think bsn / required components / EntityPatch might cover the ergonomics side of things, but if we were to do a follow-up PR, maybe an extension trait for Commands so you could do something like commands.spawn_text.
I agree / think the way to make things actually happen is probably to leave it as is for now. Ergonomics of spawning a bundle with a child will be a bigger discussion depending on how much of cart's plans for scenes make it in before 0.15.
I was trying to just pass the iterator directly just now, but I ran into an issue, because you can only use an iterator once. Also, storing the sections vec as a local was not really possible (at least I couldnt figure it out) due to lifetimes, that is why I wanted to pass the iterator along.
I could just collect it, or do a .cloned(), but that doesnt seem ideal...
can anyone help me out with this? this is like the last thing before the PR is actually done
What error do you get when trying to store it as a local?
ah, right... sections lives outside the system / across time, and Rust can't tell that we throw away the contents of it before the end of the system
what should we do instead?
It's starting to get clunky, but because it's an immutable query, you can safely construct an iterator more than once:
let sections_first_use = section_query.iter_many(children.iter());
let sections_second_use = section_query.iter_many(children.iter());
let sections_third_use = section_query.iter_many(children.iter());
There's probably a cleaner solution than this though
maybe you can just pass the section_query and the children as references into queue_text
then queue_text can construct the iterator as needed
Either way would get you around the issue of only being able to use an iterator once
But hopefully someone else has a better idea
yeah I was also thinking of just passing that query to the pipeline, but it just seems messy
should I just mark the PR as ready for review, to see if anyone has a better idea?
I guess so, maybe just leave a comment in the description in relation to the allocations
Yeah this, it irks me a bit when we are adding something to increase ergonomics, but what is shown mostly seems like a decrease in ergonomics...
Which I agree is much because we are banking on the BSN thing to solve things.
The extension trait is a potential solution to this, since I really don't want to end up in a 0.15 release where text is getting less ergonomic for most cases.
And changing all the examples to a new ergonomic with an extension trait is probably quite boring and hard to find volunteers to do.
Well to be fair, I just sat through updating practically every example lol
not doing that again
Fully understandable, which is why I'm afraid to decrease ergonomics for all users, and not having any solution/volunteers wanting to fix it
One change that will reduce the total number of allocations @true zephyr
construct the sections outside the for loop
and trust me, right now it already is an increase in ergonomics for every single non-trivial text application, because you dont work with raw indices into a sections array anymore
Oh I get that, I am just of the belief that most things made in bevy are trivial text applications
As in single section texts being the norm
that is true, we will need some way to do that in a good way
Yeah agreed, I rather the api break happens once, not twice though.. Basically I don't feel we have an equivalent to Text::from_section() in this PR.
And I'm not sure, but I think this is the first occurrence where we take something and force a parent/child hierarchy even in its basest form.
We lose a lot of ergonomics for the most common case, to gain ergonomics in other less common cases. Even if we all agree that this is the path to take, we shouldn't really introduce more papercuts in adding text.
true, we probably should make sure we land another PR that fixes this is ASAP. if we can agree on an approach to take soon, I can add it to this PR.
should we just do this for now?
Open an "Improve single section text ergonomics" issue. "P-high. May be fixed by bsn if bsn happens." Slap it on the 0.15 milestone.
want me to do that now?
If you want, but it won't make a whole lot of sense until your PR gets merged.
That's just my opinion anyhow.
I'll just open it, otherwise ill just forget about it
But I think the bikeshedding is gonna be intense with or without bsn, and we shouldn't delay this work / let it rot while we sort through that.
yeah, I am not entirely convinced that bsn will land before 0.15, and dont wanna put all the eggs of solving creating a simple text on that
added some thoughts / notes to that ticket: https://github.com/bevyengine/bevy/issues/14854#issuecomment-2302637712
yeah
opinion: I personally don't feel like it is the time to rename to Span. There seem to be two concepts associated with the word:
"an html span": a nestable/styleable container for text data or other spans or other objects undergoing "text layout"
"a cosmic-text span": a styleable (possibly overlapping?) range of indices into text data
and what we have now is not either of those things, although they are very related to cosmic spans. I think they are most accurately described as sections. non-overlapping stylable chunks of text.
Hmm, okay
can someone give me a quick list of what I still need to do, there has been so much mentioned I lost track. (I really want to get this done soon, I have already spent a lot of time on this)
Yeah, I’d also like an update on the state of this group
Hmm, okay
If the goal is to give users a clean break from the way we previously did text by giving these new things a new name and thus preventing confusion, then I think it's better done later. Because these TextSections are the same thing as the old ones, they are just stored in a different place. We haven't really changed anything, IMO and renaming would not accomplish that goal.
They are used differently to the old way, because of the hierarchy requirement
Yeah, I get that. I don't feel super strongly about this. I also just want to keep this moving and I don't want to pile more work onto Preon.
in any case I think TextSection may "disappear" soon
if we're talking about turning it into a bundle
But if they are going to be changing conceptually into an "html span" or "cosmic span" later then I think we should rename them at that time
As I see it:
- Rename the parent node to the suggested textlayout or something
- Fix docs
- Create follow up issue for breaking apart text section
- Create follow up issue for renaming textsections
Create follow up issue for breaking apart text section
https://github.com/bevyengine/bevy/issues/14875
I think there's valid/outstanding comments from UkhoeHB regarding text2d.
https://github.com/bevyengine/bevy/pull/14572#discussion_r1704459139
https://github.com/bevyengine/bevy/pull/14572#discussion_r1704460922
Tigregalis also has a PR targeting the branch here:
https://github.com/Olle-Lukowski/bevy/pull/1
Thank you both 🙂 @true zephyr those all seem very reasonable. Let us know if you'd like a hand on the implementation side!
Oh awesome, ill take a look soon! (Probably tomorrow, i have had a long day)
I think its worth discussing the ergonomics issue of:
TextBundle::default().with_child(TextSection::new("Hello", TextStyle::default()))
Lets pretend first that TextStyle is broken out into a separate style component, as I think thats very likely to happen / in line with the general direction we're headed in:
TextBundle::default().with_child((TextSection::new("Hello"), TextStyle::default()))
I personally don't think BSN on its own is the solution to this problem, although it does help. With Required Components + BSN, it would look like this:
Text [ TextSection::new("Hello") ]
Text (just a marker component) will pull in the remaining components in TextBundle. TextSection would require TextStyle, so specifying it directly is no longer required.
That being said, in the world of UI, this is still "too much boilerplate" for simple text scenarios. In general the "BSN way" to improve ergonomics is not to build new abstractions / builders ... it is to improve the core data API (I've seen some people discussing the abstraction-building path above).
People will want to be able to write text like:
Text::new("Hello")
So I think the only question is "how do we make that happen". For example, is it possible to make floating / standalone TextSections work as expected? Can we make the "manager parent" pattern optional?
Another way to put this is "if it feels bad without BSN, it will probably also feel bad with it, just to a lesser degree". Ex: with Required Components, but without BSN, it would look like this:
commands.spawn(Text::default()).with_child(TextSection::new("Hello"))
The core issue here isn't a syntax or "scene system capabilities" problem. Its a data-relationship-complexity problem
This is something that I'm concerned about too. I'd really like to make spawning simple text simple
It does introduce a fair bit of implementation complexity to be able to support parentless text sections like this, but the UX is worth it IMO
As a thought, can we make TextSection be the "one" text entity type (i'll rename it to Text for simplicity), with support for hierarchies?
Ex:
// This gets flattened into a single layout object
Text::new("Hello") [
Text::new("World"),
Text::new("!"),
]
What would the semantics be for things like font / color? Would each still have its own completely seperate value?
Correct:
(Text::new("Hello"), TextStyle { color: Color::RED } ) [
(Text::new("World"), TextStyle { color: Color::BLUE } ),
Text::new("!"),
]
Yeah okay, I'm down for that 🤔
Internally the top-level Text would behave as if it were the "grouping text entity" we have today
And it could store that on some generated component
I'm not dictating this as a solution ... I haven't been in the weeds of Cosmic Text so feel free to tell me how this might break that model
Just trying to imagine one potential world where people will be satisfied with the ergonomics
could we special-case strings without type annotation to resolve to Text? This would get us a lot closer to the clean look of html.
We could (and that has been on my mind). But I think solving the current data-relationship problem is a higher/more immediate priority
Setting that aside, I think I pushed for something similar to this a few months ago, and am on board with the single text component.
I'd prefer to not paper over this with sugar, as implicitly creating entity hierarchies for "Hello" feels hard to grok
Some ergonomics discussion here if you missed it: https://github.com/bevyengine/bevy/issues/14854
There's agreement that the current state is bad and that bsn won't necessarily fix it, so I think we're on the same page there. But there was some lively discussion about whether the sections-as-entities PR should go forward before addressing that.
My take is that we should reach consensus on where we're headed first, then start working toward it. Its possible that the current Text Section as Entities PR is a good incremental step, but I can't see how we could know that when the fundamental path hasn't been determined yet
I am pretty sure we won't want to do this, but it's worth mentioning that we could make these more like s-expression by moving the first brace up
[
(Text::new("Hello"), TextStyle { color: Color::RED })
Text::new("World")
Text::new("!")
]
could translate into a tree as before. Maybe thats too much of a conceptual shift, but it more closely follows how I would expect this sort of thing to be layed out.
The way this works in Quill currently is that String and &str both implement the View trait (this idea is stolen directly from Xilem), so you can pass in "Hello" any place you can pass in a widget:
Button::new().children((
"Cancel",
Icon::new("cancel.png")
))
That is already valid BSN and that would just make them all be children / siblings of each other
right i'm suggesting [ parent | children... ] vs parent [ children... ]. Is there any drawback to changing where the hierarchy split happens?
just curious really, not pushing for this change. Feel free to ignore if this is obviously unwanted.
Gotcha. Biggest downside I see is that the parent now looks like it is a sibling of the children
Which feels like a pretty big downside
I think we'd need much clearer syntactic distinctions if we embraced a flat list
idk, maybe just me but it makes the ordering look more explicit and matches the form i generally expect from lisp/html. in the current impl the parent is in the place where I would expect a 'tag' to be in markup languages, but it's semantically very different from a tag.
The ergonomic "vision" that I had for text was something like the following:
- If the text node is a child of an entity with
Display::Flex,Display::GridorDisplay::Block, then each text item is treated as a separate "cell" or "box" and laid out in its own grid cell or row/column. This means that the cosmic-text layout is performed on that single entity. - If the text node is a child of an entity with
Display::Flow(a new display type), then the cosmic-text layout is performed on the parent, with all the children (both text and non-text) being part of the flow. This would mean that Bevy would need to automatically insert whatever components necessary to maintain the cosmic-text state.
So Display::Flow behaves something like a paragraph.
Seems reasonable. In the same vein as the implicit "flatten hierarchy" approach, but more explicit / opt-in (and more generic)
So in the button example, the button is Display::Flex, with FlexDirection::Row, with a small Gap. The text label and the icon are arranged in a horizontal row.
It also means you could make any node a "parent text layout manager", which feels nice
The previous implementation of
commands.spawn((TextBundle::default(), TextSection::new("")));
Involved a bunch of special casing around text section enumeration and the hierarchy. Automatically converting this sort of thing to an entity with one child when spawning seems much more reasonable, though surprising in its own way.
(nevermind me [[X Y] Z] == [X Y Z] in the alternate format, this seems undesired)
Automatically messing with hierarchy is something I would like to avoid generally. I think allowing people to create a Text::new("Hello") and know exactly where it lives in the hierarchy will be important for styling
Styling: In Quill, the automatic conversion of String/&str doesn't allow for the insertion of style components, but you can explictly call TextStatic::new("string").style(...). Most text styles are specified through inheritance - in my framework, text styles are the only inheritable styles, because it's so tedious to assign a font handle to every text span. This inherited styling is a separate system, independent of Quill.
Ah, sorry, just understood your previous message on that topic.
How would Display::Flow behave in a text-less context?
(Node, Style { display: Display::Flow }) [
(Node, Style { width: px(100.) }),
Img(@"logo.png"),
]
Basically it would perform a line-wrapping algorithm on the boxes. Not much different from flex with wrap.
And how would it handle:
(Node, Style { display: Display::Flow }) [
(Node, Style { width: px(100.) }),
Text::new("Hello"),
Img(@"logo.png"),
]
Reasonable
What would this do?
Node [
Text::new("World"),
Text::new("!"),
]
In fact the only real difference between Display::Flow and flex-wrap is how it treats adjacent text spans.
Hmm each Text would be its own Display::default() ... (Block? ) laid out text section?
Actually that's not the only difference, sorry
Yeah. I guess that is not too confusing.
Effectively, Display::Flow would merge adjacent spans, and then allow the line-breaks to be within a single merged span, according to whatever the cosmic-text layout calculation produces.
This is not much different than the behavior of display: block in CSS.
Although I'm not advocating that we support explicit line breaks
At this point in the conversation I'm liking this path a lot (in combination with a flattened/standalone Text::new("Hello") entity)
What I'm going for here, BTW, is not to regurgitate the web. I'm going for the quest log / character bios in Witcher 3 or Jedi: Last Survivor 🙂
Anyone want to put together a quick design doc for this? Aka describe the new set of components, how they relate to each other, Display::Flow, and how this ties into the details of Cosmic Text layout?
I can do this, but my plate is already overflowing so it would take some time
I could, but I'd like to see someone else's take on this. If no one else volunteers, I will.
This should also explicitly cover Required Components
Also probably good to wait a bit longer to see if anyone else here weighs in with new compelling info
~a day or so
Sure. I can also help polish the prose if that's needed.
I'm sure the implementers will have something to say. Text layout requires keeping extra data around, mapping entity ids to cosmic text spans and other fiddly bits that may present challenges.
Yeah, my brain is crumpling slightly under the load of trying to figure out how that will work
The key to innovation is often letting go of old constraints that we thought were important 🙂
It seems pretty likely that we would want to bug cosmic about u64 metadata so we can just store entities in there. At least I think that would probably simplify things.
Detecting when Display::Flow is set, then adding the things that do this for the current TextBundle seems like one path. Idk if its the right path, but it feels reasonably compatible conceptually?
Combine that with detecting when that isn't the case for a given Text, and treating it as flat / adding that to that entity
Again, not saying its the path to take, but seems to map approximately 1:1 to the impl in the TextSections-as-entities PR
Does this seem like an accurate representation of things-we-want-to-be-able-to-do in non-bsn (but w/ required components) terms?
// 1. [Hello]
commands.spawn(Text::new("Hello"));
// 2. [Hello, World]
commands
.spawn(Text::default())
.with_children(|parent| {
parent.spawn(Text::new("Hello"));
parent.spawn(Text::new("World"));
});
// 3. [Hello, World]
commands
.spawn((Node, Style { Display::Flow, ..default() }))
.with_children(|parent| {
parent.spawn(Text::new("Hello"));
parent.spawn(Text::new("World"));
});
// 4. [Hello, World]
commands
.spawn(Text::new("Hello"))
.with_child(Text::new("World"));
// 5. [H, ello, W, orld]
commands
.spawn(Text::new("H"))
.with_children(|parent| {
parent.spawn(Text::new("ello"));
parent.spawn(Text::new("W"))
.with_child(Text::new("orld"));
});
And do you see that situation as an acceptable intermediate target?
I'm not sure the last example makes any sense, I tend to think of text entities as leaves of the tree. At least, this is true in HTML, they can't have children.
Actually I should say the last two examples
The second to last was explicitly discussed above somewhere
Yup this was part of my "everything is Text-driven" proposal. Less important / maybe not desirable at all in the context of Display::Flow
And that seemed to imply that the last was possible too, but are we limiting to that one special case?
In my "unified Text" proposal, it would have walked up until it found a root Text entity
(at least, thats what it was in my head)
Given that this is ECS, nothing prevents you from giving a text node a child - but for my purposes it can just be "undefined behavior" or not supported.
I think in the context of Display::Flow, it is desirable for the last two examples to resolve using the algorithm we already defined
Aka they would use the default Display / be treated as standalone text sections
BTW, another thing I like about Display::Flow is that the extra cost of layout is explicit - that is, users who don't use it don't pay the cost.
We want the ergonomics to be as simple as possible - but not so simple that they hide important consequences from the user.
@mental tree Do you have any thoughts on @shadow marsh 's comment about needing u64 ids in cosmic text? This seems like a potential roadblock, or at best a delay.
I don't think it's a blocker or anything
Yeah given that "TextSection as Entities" already exists without it, I suspect we don't need it
I'd want to have a clearer understanding of what that buys us before using political capital to drive that change upstream
But we are using "section indices" now in cosmic metadata because it's a usize. If we can still get a consistent section index while traversing the hierarchy then it's no problem.
Given that logically / algorithmically we'd want a given Entity to translate to an ordered list of things (a specific position in an ordered list), it feels like we can (and probably should) handle that mapping on our end
Unless the cosmic "section index" is decoupled from ordering
section index is entirely on our side, stored in a generic metadata field for each span in the cosmic buffer so we can map back and forth.
what gets ugly is when users want to do stuff with the cosmic buffer, and they now need to find the associated entity. but all they have is that section index we made while traversing the hierarchy.
when it was just one list of children, it was very easy for users to look up
but now that it's an arbitrary hierarchy, it sure would be convenient to store an Entity in the metadata instead.
actually now that I have typed it out this seems like it may be a problem
guess we could maintain a map component as a convenience
Maybe only create the map when users use those apis? Or require the user to create the map explicitly when they start using the cosmic-text buffer?
There will be in-engine uses too
What about Text2d? I suppose that needs to be
// 1
commands.spawn(Text2d::new("Hello"));
Or some sort of variation of
// 2
commands.spawn((Text::new("Hello"), Text2d));
Hmmm ok. The main use case I know of is the text input widget, and those will typically be written by library authors - and I don't mind putting extra work on library authors if it reduces the load on the average user.
If we're already traversing the hierarchy once and building the indices, we might as well store a map and just make that "convenient" for everyone. It would still be slightly obnoxious to look up entities, but you're right that an average user probably won't have to deal with it.
1 seems like the obvious answer if I'm a user, but it implies a somewhat different implementation than the current one.
IMO the best way to think about Display::Flow (which the web calls display: inline - flow refers to the combination of block and inline layout) is that it's just text layout, and child boxes/inages/widgets are essentially laid out as if they are a single giant glyph.
I think UkhoeHB's comments have been accounted for in the current branch: queue_text now takes a &[(usize, Ref<TextSection>)] (usize being the actual index into the parent, regardless of whether it is a text section or not). maybe just need a test
Hey, I woke up a bit sick, not sure if I have it in me to finish up the PR today. I would really appreciate if someone could rename Text to TextLayout (and make the needed updates) for me, and update the docs accordingly.
I'm not too sure I agree with this. I tend to think Bevy is successful because user code is so similar to engine code.
(or vice versa)
Okay, so if I'm caught up correctly, we decided that
// 4. [Hello, World]
commands
.spawn(Text::new("Hello"))
.with_child(Text::new("World"));
Was perhaps not important / undesired.
But how do we reconcile that with another example from that code block:
// 2. [Hello, World]
commands
.spawn(Text::default())
.with_children(|parent| {
parent.spawn(Text::new("Hello"));
parent.spawn(Text::new("World"));
});
?
Is that not allowed / should be written w/ the text as children of a Display::Flow node instead?
Canonically, text nodes are leaves of the tree. Any children of a text node are ignored by the layout.
Okay, so it seems that example 2 was also considered invalid. That is what has been confusing me, but that should have been pretty obvious, so my bad.
IMO the HTML/CSS model of text is bad in that it doesn't allow you to explicitly specify "block" or "inline" layout on a parent node. It only allows you specify "flow" (= block or inline), and then determines the layout mode automatically based on the contents, which is both expensive and confusing.
IMO it would be much better to have an explicit inline display mode, which is what things like React Native do with their Text component.
I do spend too much time in RN which might have added to my confusion ^^
For those who are not familiar, React Native uses a Text component both for creating a "text layout context", and for "spans" within that context.
I'd personally lean towards having these be separate ecs components, but IMO the model of a dedicated component for a text context is a good one.
Where I'm coming from is based around the API for widgets: a label for a Button can be a text string, or it can be an icon, or it can be some combination of both. Yes, I know some UI libraries (even on the web) require you to wrap the button label in an explicit <Text> or <Typography> element, but this is rare, and I'm not fond of these.
Now, whether this is handled in the widget, or lower down, I feel less strongly about. (Although handling in the widget is easier to do in JS than in Rust).
Does this now seem like an accurate representation of the proposal?
// 1.
//
// Buffer [Hello]
commands.spawn(Text::new("Hello"));
// 2.
//
// Node
// - Buffer [Hello, World]
commands
.spawn((Node, Style { Display::Flow, ..default() }))
.with_children(|parent| {
parent.spawn(Text::new("Hello"));
parent.spawn(Text::new("World"));
});
// 3.
//
// Node
// - Buffer [Hello]
// - Buffer [World]
commands
.spawn(Node)
.with_children(|parent| {
parent.spawn(Text::new("Hello"));
parent.spawn(Text::new("World"));
});
// 4. (Invalid. Child node ignored. Possibly, a warning is raised.)
//
// Buffer [Hello]
commands
.spawn(Text::new("Hello"))
.with_child(|parent| {
parent.spawn(Text::new("World"));
});
I typed up a more formalized specification here: https://github.com/bevyengine/bevy/issues/14854#issuecomment-2308535983
LGTM
One open issue is: how the various entity style properties - margin and so on - map to cosmic-text features for inline spans, if at all
If the answer is, "they don't" that's acceptable, but should be documented.
obviously there are some obscure edge cases - what happens to a ligature if the unicode chars are in different spans, and they have a non-zero margin. But these don't need to be part of an MVP.
I would expect inline spans to have a different style component. But padding/border/margin should be implementable in cosmic text
If we break up the style component, this is less of an issue - a Padding component can be usable for both inline and non-inline elements.
That's true. I'm a little concerned about performance with that approach, but we'll see.
Another option is a Style component containing a Vec<StyleProperty>
Been down that road! https://github.com/viridia/bevy_peacock/blob/main/crates/bevy_peacock_style/src/style.rs#L25
You didn't like it?
It's not so much that I didn't like it, but other people on discord didn't like the other baggage that came with it - CSS-style selectors for dynamic styles and so on.
Instead, I went with a "styles are functions" approach.
Ah, we could have Vec without selectors if we wanted
The question is how do you compose styles?
With styles-as-structs, you have some upper layer which can be Vec<StyleProp> or a function, and this gets applied to the struct in some defined order, so the composition becomes simply the order in which the mutations happen. But if the style is itself a vec, now the only way you have to compose things is concatenating (and possibly de-duping) the vectors.
Composition is used not just for theming, even basic tasks like "I want a standard button, but with this margin".
What Servo/Firefox do is have a computed struct which is not a Vec (but which uses Arc for deduplication).
You could potentially combine that with a Vec<Vec_>>
By "standard button" I'm talking about some kind of opinionated reusable button, not a raw unstyled button.
All this being said, you are right about one thing - we do need a sparse representation. Specifically a sparse representation that is serializable. Whether that is part of the ECS or a layer on top I feel less strongly about.
Sparse representation + serialization is the key to both editing and composition.
Back to the topic at hand: if a proliferation of style components is a concern, perhaps we could split Style into 2-3 components, grouped by some logical criteria.
Although naming would be a challenge
"The two hardest problems in programming are naming, cache invalidation, and off-by-one errors."
Also, TBH things like margin and padding are almost never used for inline elements in web apps. If you want to add space between paragraph lines, use leading. Background color is used for highlights, of course, and sometimes you see outlines, but only as an overlay. So perhaps the best answer is to simply not support properties that are inconvenient.
One of the most massively productive things a software engineer can do is recognize when a problem doesn't need to be solved.
Looking at this it is quite confusing how you'd implement it. Here's an alternative API that makes more sense to me. There is a small learning curve but overall it's straightforward and easy to understand.
I think an MVP for v0.15 doesn't need Display::Flow
That seems quite reasonable to me and deals with some of the head scratching I was doing when I was imagining an implementation. IMO TextNode should do text / flow layout of children by default. And if we don't implement "flow"ing non-text things, we don't need an explicit Display::Flow.
Idk enough about flow layout to comment. At the very least it should be possible to punt that question without causing API problems.
It’s pretty unclear to me what Display actually does to a text node, whether Flex/Grid/etc.
Display::Flow isn't just about mixing text and non-text items. It's also about how adjacent text nodes are line-wrapped. If you have multiple text entities in a flexbox with row layout, even if that flexbox has flex wrap enabled, it's going to always wrap at the entity boundaries. With flow, the adjacent text entities are treated as one long string, so wrapping can happen anywhere (well, on a word boundary anyway).
I guess it affects whether Style fields related to Flex/Grid will be activated or not.
Is the goal with Display::Flow to also allow extra UI formatting inside the aggregate buffer? Not sure how this would differ from just putting everything under one TextNode w/ Display::Flex.
And using TextLayout to control linebreak/wrapping.
It would be easier to explain with an illustration
Let's put aside the issue of style for a moment and just talk about what happens with plain text. In a flexbox, each text node creates a separate "flex cell". Those flex cells are laid out in a row (or column), and their size is calculated based on the calculated min/max width of the cell. For text nodes, the max width is the width of the string; the min width is, I don't know, something small I think. If the total width of the text is larger than the available flex space, the the text string gets word wrapped, but only within the flex cell - it doesn't affect the wrapping of neighboring cells.
Now, flexboxes also have a "wrap" feature that allows the cells themselves to wrap to the next line. However, this never splits a cell, the entire cell always goes on a single row.
So if I have two long strings that are adjacent to each other, and each one is a text node, if I put them in a flex row, either one of two things will happen: either the flexbox will wrap each string separately, and then put the wrapped lines side-by-side; or it will put the second string on its own row (if flex wrapping is enabled).
Neither of these is the intutive thing that you would expect, which is to join the two strings together in a single string, whose wrap point is then calculated based on the width of the joined text.
For something like a table or a toolbar, the existing flex behavior is exactly right. But it's wrong for something like a paragraph.
Right, so my question is what different results you'd get from these:
/// 1. Display::Flex (implicit)
commands
.spawn(TextNode)
.with_children(|parent| {
parent.spawn(Text::new("Hello"));
parent.spawn(Text::new("World"));
});
/// 2. Display::Flow
commands
.spawn((TextNode, Style{ display: Display::Flow, ..default() }))
.with_children(|parent| {
parent.spawn(TextNode::new("Hello"));
parent.spawn(TextNode::new("World"));
});
And if you have Display::Flow, what happens to the Style components on child nodes?
Honestly I'm a bit puzzled if you are suggesting the child nodes would be separate TextNodes, or separate Text (i.e. text spans) under a TextNode that has Display::Flow.
I don't think that's what I'm suggesting 🙂 Let me try to address points one at a time.
So there are certain styles that make obvious sense in an inline context: bold, italic, font size, font color, and so on. Other kinds of styles such as margin and paddings are supported by browsers, but almost never used, and whether or not we support them I don't much care. Finally, there are some styles like left and right which, in a browser, are simply ignored in an inline context.
Akkktually, on web, adjacent text-node children of flexbox items are merged and laid out as one. But I'd like to avoid these kind of "tree shape fixups" in bevy if possible.
That's true, I was lying for the sake of simplicity 🙂
I see. IMO if we want to support text-specific styling then it should be embedded in text-specific components. Excessive overloading is part of why Style is so clumsy...
In the simplest sense, Display::Flow is just a way to build a paragraph out of multiple text entities.
Make sense. In my vision, TextBlock (for TextNode and Text2d) signifies the root of a tree of text spans that should be assembled into a paragraph.
So the deeper question here is if there's more fine-grained settings on text construction that aren't captured by 'collect all the contiguous text in this hierarchy'.
Sure, and I understand why, from an implementation standpoint, you want that explicit component rather than just a display type.
You need somewhere to put the buffer
Or maybe a better way to put it: are there special settings for UI text assembly that wouldn't be present for 2d?
I'm coming at this from the other end, which is: for a given templating system (sickle_ui, quill, BSN) what kind of experience do we want to give the user - and then work backwards from there.
I'll use quill/obsidian for an example. The Button widget has flexbox layout - that is, if the button has multiple children they are laid out side-by-side, with a small gap in between them.
Most buttons will have a single child which is a text string. But buttons sometimes have a string and an icon - like a cancel button with a cancel icon.
So in Quill, you can say either:
Button::new().children("Cancel")
or
Button::new().children(("Cancel", Icon::new("cancel.png")))
Now, we can use various tricks with Rust .into() to automagically convert that text string into a TextNode. So really what we are saying is:
Button::new().children(TextNode::new("Cancel"))
Would it be possible to have:
Button::new("cancel")
And have Button deal with setting the children?
I'm going to punt on that question for now because I don't want to get off track (and I have to leave soon).
The question then becomes: does the .into() magic, or templating system or whatever, need to understand that there are multiple flavors of text nodes, which are context-specific? That is, depending on where you pass in the text string, it gets converted into a different kind of node? Conversely, do we just get rid of the magic and make the user always wrap the text string in the correct kind of text node for the current context?
It sounds like you want Button to 'auto-detect' when its children are text spans and behind-the-scenes convert itself into a text node. And the suggested idea is for Display::Flow to do this for you.
I'm not sure I know what you're getting at by context-specific text nodes/text node flavors. As in 2d vs UI?
It should be possible to implement this without Display::Flow. Add a marker component to Button to detect when its children include a Text component, then insert TextNode to the button entity (or its content sub-entity where children are added). Using Display::Flow would have the same effect (you'd need to poll for Changed<Style>, which is probably less efficient). That is, unless I'm misunderstanding and there's additional magic/complexity around how Display::Flow would affect nodes.
The magic/complexity of Display::Flow kind of puts me off. It seems to add ambiguity to an already opaque UI API surface.
With flow, the adjacent text entities are treated as one long string, so wrapping can happen anywhere (well, on a word boundary anyway).
Not saying we "don't need flow" just that "we already have flow and don't necessarily need Display::Flow if text is the only thing we're laying out. It could just be implicit behavior of children of a TextNode like how the current sections-as-ents works now. No need to introduce this new concept if the behavior is the same as it has always been.
Actually, now that I have had a chance to think about it, I'm leaning towards the inverse. In games, paragraph-style inline/flow layout is uncommon - the vast majority of text usages are single words or short strings - button labels, character names, and so on. So I don't care if it's extra work on the user to have to prepare text strings for a paragraph (as long as it's possible), so long as the most common case has the best developer experience.
This means that the automatic conversion feature of the templating language should default to creating a self-sufficient TextNode. Thus, strings are automatically converted to TextNodes when used as template children. For cases where you want Text but not a TextNode, the user can do the extra work of wrapping the string in the appropriate component type - it won't auto-convert it once it is wrapped.
Looking at this again, I think that this example is not quite right. A flexbox would have TextNode children, it would not by itself be a TextNode. (Assuming that I understand correctly what a TextNode is)
Each textual child of a flexbox would have to have its own separate layout context. The flexbox simply treats them as blocks.
Ended up working on perf updates to cosmic-text today (will get back to you @finite magnet tomorrow).
- https://github.com/pop-os/cosmic-text/pull/303 This is a big one, removing a ton of excess allocations when existing buffers are updated.
- https://github.com/pop-os/cosmic-text/pull/304 This clears an old issue that was causing layout to run twice every time (which compounds with all those allocations...).
- https://github.com/pop-os/cosmic-text/pull/302 Small but important.
BTW, I think we are close. What you propose is a bit different than what I originally described, but I think I can get the templating system to paper over the differences in most cases.
I am digging deeper into the taffy integration to better understand how it all fits together. Running into so many perf issues (which I feel compelled to fix)...
It's like a mountain of allocations every time you insert or modify text.
I'm very happy to get fixes on the taffy side for that stuff fyi
(and I mean on the Bevy side too of course)
If you are digging into the taffy code, I would be very interested in your thoughts on the feasibility of https://github.com/bevyengine/bevy/issues/14826 (this has nothing to do with text, so probably should go in ui-dev).
One thing I've seen is it would be nice if taffy ids could map 1:1 to entity ids.
Actually, not that important I found another way.
Based on my reading of the taffy integration, text nodes are treated as 'taffy nodes' without any distinction from non-text nodes. You specify a Style and an optional NodeMeasure for all nodes that need to be laid out. From the layout algo's point of view, text is an opaque box as part of a taffy node that can be dynamically measured and that's it. Once layout is done, the final calculated size of the node contents is stored in Node::unrounded_size on the bevy side, which is used by the text system to finalize text layout (see queue_text()).
From what I can tell, it would not make sense for multiple text buffers to be referenced as the content of a taffy node. So we only need one buffer on leaf nodes, which is what I proposed above. Conceptually it would seem that 'text content' should be a solitary sub-entity of a leaf taffy node, but in practice we can have any number of children so it's better to merge the text components into the leaf node.
Understood. My original idea for Display::Flow is that the internal layout of these nodes would not be controlled by Taffy. Instead, the layout would be controlled by cosmic-text. If that requires an extra component to be inserted to hold the buffer, rather than a display type, that's fine by me. However, you can't have both: either taffy controls the layout, or cosmic-text.
That is, Taffy would only be used for Flexbox and Grid (not sure about Block). Display::None is also not controlled by Taffy for obvious reasons.
cosmic-text already controls its own layout. You stick all the sections in a cosmic buffer and then measure the size based on different bounds (which taffy provides as part of its own layout algo), then the final size from taffy is used as the bounds for finalizing text layout in cosmic-text.
Let me try and rephrase it.
The 'display' property controls what layout algorithm is used for the entity's children.
For display Flex/Grid, Taffy is used to determine the position and size of each child.
For a paragraph block (however that is specified), Taffy isn't used to place the children; instead the children are processed either as wrapped text strings or inline blocks and glyphs are placed according to a line-wrapping algorithm, provided (I assume) by cosmic.
These inline elements can either be text or non-text. If they are non-text, they can be an image, a box, or any other kind of ui node - nothing prevents you from having a flexbox or a grid be inserted in mid-paragraph, it's just a box from the standpoint of the line-wrapping algorithm.
My reason for making this a display type was to make it explicit that this behavior is exclusive from other display types: you can't have a paragraph that is also a flexbox, that makes no sense.
However, I understand if there are implementation reasons preventing doing it that way.
cosmic text is only able to handle text and there isn’t anything on their roadmap about a more advanced API. I think you’d need some kind of integration between taffy and cosmic text to insert flex boxes/etc in-line to text. If you have a Flex node with one or more Display children, the flex node needs to measure those children in order to size itself properly. If the children then contain other embedded taffy nodes, then taffy needs to incorporate the Display calculations into its layout algorithm instead of using a black-box measurement approach.
I would say it’s not a good idea to add the Display option if we can’t implement it properly. Better to get an incremental text-oriented solution in place and then wait for all the pieces to come together before trying to add Display.
The previous discussion was about treating the inline block as a single large glyph.
In fact the whole reason why the lorem-ipsum working group got unblocked was because of this.
That is, the comic-text migration PR was blocked for a year specifically because of the lack of support for inline images; and it only became unblocked when it was determined that this problem could be solved.
Actually, I'm wrong about that last point, I just checked the archives. The issue that blocked the migration was multiple font sizes, not inline images.
Ok, treating as a big glyph. Setting aside the details, let’s say there is a tree of things that coerce to glyph sequences. We want these to be concatenated and for cosmic-text to control their layout.
My proposal from above can solve this. When assembling a buffer in a TextNode, just look for everything in a contiguous hierarchy that coerces to glyphs. Let the Node entity that the TextNode sits on be essentially the ‘parent’ of the collected tree. It decides where the collected tree goes in the UI (via flexbox/grid) and coordinates sizing with the collected tree. If there is ambiguity when calculating layout between a TextNode and a child that looks like a Node but is actually being coerced to a glyph (eg an inline flexbox), we can use a marker component on the TextNode entity to block taffy layout traversal. If we want a coercable node to not be coerced, it can be marked with a component. TextNode = automatic Display behavior. The only difference is the TextNode is embedded as the content of a normal flex/grid node.
In the future if Style gets decomposed, a variant can be introduced that’s tailored to text (remove settings that apply flexbox content controls, etc) and TextNode can be updated to require that.
BTW, this is easier to reason about in the browser, because text nodes can't have styles at all - you can only inherit styles from parents. That is, in the HTML DOM a "text node" is a very simple kind of node with only a few properties, while an "Element" is a more complex node that can have attributes.
I think Display::Flow vs a marker component for text roots is mostly an API question. I don't think there are technical limitations either way.
We will need a component to store the cosmic text buffer on either way, but whether that's what triggers (cosmic-)text layout, or it's a value of Display that does doesn't matter that much from an implementation pov.
Cosmic text doesn't support inline boxes atm, but it isn't blocked. We have it implemented in parley, and cosmic-text is architecturally pretty much identical.
I think we ought to move to a conceptual model where bevy owns the layout computations and delegates to Taffy or Cosmic-Text on a node-by-node basis. Taffy supports this mode of operation already (and special support from Cosmic-Text is not required)
New discussion as follow-up to my earlier proposal here: https://github.com/bevyengine/bevy/discussions/15014 . This includes a solution to the 'how to track sub-entities' problem (I chose a SmallVec<[Entity; 1]>).
Thanks a ton for writing this up!
Yep! If there is sufficient buy-in then I will consider implementing it. But only if there is 90% chance of it getting merged.
Yep, I'd like to get consensus here
My initial impressions are strongly positive, but I'll do a full review tomorrow
Do you prefer comments to be in discussions, or is it ok to comment here?
Large comments can go in the discussion, small ones are fine here if you want
Have you ever known me to write a comment that was short? 🙂
I'm still not fond of the idea of text nodes (that is, nodes that have a text string component) with children, such as your example 2.a - in the browser, a "text node" is a fairly simple struct that can't have children. In the case of Bevy, we can't prevent entities from having children, but how to interpret this is somewhat ambiguous - the fact that the children come after the parent is arbitrary. However, since the template engine will not generate text nodes that have this structure, it doesn't matter to me - but I'm hoping to make the implementation easier for you by dropping this requirement.
Now, one might ask: to what extent should browser behavior constrain our designs (if a hundred browsers run off a cliff, should you do so too?) I kind of assume that, except for features that have specific value for games, bevy_ui is a strict subset - that is, we imitate browsers for reasons of familiarity, but we're not attempting to get to feature parity. Any place where we support features beyond what browsers can do should be by intent and not by accident.
The implementation would be harder if you special-case it so sometimes we care about children and sometimes we don’t.
That is, in practice I think you either have unitary text nodes, in which the text string and the TextNode are all combined in a single entity; or you have an empty parent with text children. I don't think you will see cases where both the parent and the children have strings.
BTW I am glad you added the note about picking.
I only care about browsers to the extent they’ve already done a lot of work to standardize certain concepts and algorithms. Stuff like layout, font attributes.
This is essential for something like hyperlinks.
Yes the Text component is optional on the root node in my proposal. Easy to use single-entity text, easy to extend, don’t pay for the root node Text if you don’t need it (the root node Text is implicitly a Text sub-span of itself).
An earlier design I was considering separated Text and TextSpan, but I don’t see any need for the distinction now.
Should the TextNode have a constructor that doesn't require an empty string argument?
When expanding markup text you’ll have a lot of empty Text entities for each span header for spans that don’t start with text, but those get elided in the layout algorithm. And an empty String (and TextStyle) ends up just a waste of some static bytes.
You can make a TextNode just by itself. It’s a unit struct.
We could add another marker component TextSpan required by Text that can be used for text tree traversal. Then for markup text you can just insert TextSpan on empty nodes. I will add this to the proposal.
Ok added example 2.c
WRT the browser compatibility argument, my stance is "we should follow their conventions for familiarity when we don't have a good reason to break from them"
And we should make it clear when we're doing so, ideally with different naming
Might be nice to add a section to the discussion explicitly addressing why it is veering from cart's proposal.
The one where you just use Text and maybe Display::Flow? Basically to use required components you need a separate component to set up the root of a text block (hence TextNode and Text2d). ‘Auto-detect’ has a few problems: all uses of Text need to be aware of each other for proper auto-setup, the behavior you want is required components so it’s weird to have special magic for Text, you don’t always want Text on all the nodes of a text block.
I will update the discussion with this comment later.
Yeah, I agree / have a vague idea from the headscratching I've done previously but it seems like you've given it a bit more thought and it would be great to get that in writing. Thanks
Updated. Also adjusted the TextBlock::entities field to include hierarchy depth so you can reconstruct the hierarchy without needing to traverse it.
Does TextEntity need to be public?
Yes you need to read that field in the text picking backend for example.
