cosmic-text icon indicating copy to clipboard operation
cosmic-text copied to clipboard

Request: Allow buffer dimensions to be undefined

Open nicoburns opened this issue 1 year ago • 8 comments

Currently you have to set the Buffer dimensions before shaping. But it is common requirement to size a box containing some text based on the size of the text itself. It would therefore be great if it were possible to shape/layout with one or both dimensions being unset/infinite (it should be possible to independently set each dimension to infinite).

This could be represented with an Option<i32> (or Option<u32> - does a negative buffer size make sense?) or possibly just an f32, making use of the floating point Infinity value.

nicoburns avatar Nov 17 '22 01:11 nicoburns

Using the maximum value for size essentially does this, unless you have text that will be more than billions of pixels wide

jackpot51 avatar Dec 07 '22 14:12 jackpot51

If the goal is simply to measure the text's width for UI layout purposes, then (1) there will be some maximum width above which the exact result doesn't matter (because wrapping will be needed) and (2) layout of any remaining text is irrelevant.

Consider adding a method especially for this purpose: https://docs.rs/kas-text/latest/kas_text/struct.TextDisplay.html#method.measure_width

dhardy avatar Feb 23 '23 11:02 dhardy

If the goal is simply to measure the text's width for UI layout purposes

This is basically the use case, except:

  • Sometimes there is no limit (but this can be emulated by just setting a very high limit)
  • I often want to know the height as well as the width. Although it's true that separate methods for "just height", "just width" and "both" might be a good idea (note that in the case of vertical text it might be height that's cheaper to compute)
  • The "unlimited width" (never wrap) and "zero width" (always wrap) cases are common and should potentially have an optimised implementation (and/or be computed together and cached) if such an implementation would be meaningfully cheaper.

nicoburns avatar Feb 23 '23 11:02 nicoburns

Height can only be determined after wrapping (hence you must do full layout and automatically get both). Maybe some opts are available if you only want height (e.g. not re-ordering lines), but I doubt there's much value. I assumed #70 is about this.

I ended up using a GUI layout algorithm which fully determines width before considering height, hence the two-step approach works for me, but I am well aware that most layout algorithms address both simultaneously.

Calculating only height without ever wrapping is as simple as multiplying the line height by the number of lines and adding in line-gaps (not sure if Cosmic-text uses these?), assuming your font size is constant and there are no oversize glyphs, subscript / superscript, etc.

Not sure why you'd want to measure height with "zero width". If words are too long for a line, there is hyphenation and even per-glyph breaking. You might as well just assume the maximum height is some BIG NUMBER (or screen height), at which point it doesn't help with layout.

dhardy avatar Feb 23 '23 11:02 dhardy

Height can only be determined after wrapping (hence you must do full layout and automatically get both).

Presumably for vertical text this is reversed and it's width that can only be determined by doing a full layout? (I'd be interested to to know if/how your GPU layout algorithm works for vertical text).

I think what I want is a way to do "full layout" but without storing the result except for the overall width/height. Which should mean that no allocations need to take place unlike what I'm imagining happens in a regular layout where the contents of each line are stored.

Not sure why you'd want to measure height with "zero width". If words are too long for a line, there is hyphenation and even per-glyph breaking.

CSS layout uses this to determine a minimum width for text nodes (under specified line-breaking rules which by default do not permit hyphenation or per-glyph breaking)

Calculating only height without ever wrapping is as simple as multiplying the line height by the number of lines and adding in line-gaps (not sure if Cosmic-text uses these?), assuming your font size is constant and there are no oversize glyphs, subscript / superscript, etc.

I think I'd usually want the height taking into account all these edge cases.

nicoburns avatar Feb 23 '23 12:02 nicoburns

Using the maximum value for size essentially does this, unless you have text that will be more than billions of pixels wide

This doesn't work as expected in all cases, specifically in conjunction with text alignment.

For Bevy UI, my current implementation of "text alignment" just creates the buffer with Align::Left, and I visually adjust the laid-out glyphs after I have the line widths and the total text area width.

This works; however, if I want to be able to turn these into Editors then it needs to be "alignment-aware", so I'm trying to make use of cosmic text's Align enum.

Since we want text areas to be "content-sized" when no width is explicitly provided, we use f32::MAX, and calculate the visual width of each line afterwards from LayoutRun::line_w: take the max of these lines and that's the width of the text area.

This doesn't work when I use Align::Right or Align::Center because:

            let alignment_correction = match (align, self.rtl) {
                (Align::Left, true) => line_width - visual_line.w,
                (Align::Left, false) => 0.,
                (Align::Right, true) => 0.,
                (Align::Right, false) => line_width - visual_line.w,
                (Align::Center, _) => (line_width - visual_line.w) / 2.0,
                (Align::Justified, _) => {
                    // Don't justify the last line in a paragraph.
                    if visual_line.spaces > 0 && index != number_of_visual_lines - 1 {
                        (line_width - visual_line.w) / visual_line.spaces as f32
                    } else {
                        0.
                    }
                }
            };

I'm passing f32::MAX when creating the buffer. For Align::Left and Align::Justified with LTR text this isn't an issue because the correction is 0..

But text alignment correction is buffer_line.width - visual_line.width for Align::Right, and half of that for Align::Center.

In my case I'm passing, effectively, "infinity" as the width, but if buffer_line.width = infinity, then correction = infinity - visual_line.width = infinity.

Since every glyph is horizontally offset by alignment_correction, then every glyph gets horizontally offset by infinity, so they get positioned at infinity, and in practice, this means they don't display.

tigregalis avatar Jun 26 '23 15:06 tigregalis

If the goal is simply to measure the text's width for UI layout purposes, then (1) there will be some maximum width above which the exact result doesn't matter (because wrapping will be needed) and (2) layout of any remaining text is irrelevant.

I did not see this mentioned, but I'm fairly certain there are some scenarios where layout needs to continue after wrapping to determine the exact width. I.e. if the UI layout algorithm needs the minimum width that the text will fit in, it is useful to continue since wrapped text won't necessarily be as wide as the width limit that forced it to wrap.

For instance, consider text consisting of just two words that slightly exceed the width limit such that the second word will be placed on a new line. In this case, the difference between the width of the wrapped text (which is the width of the larger word) and the width used as a limit can be quite significant.

I can see how the difference would be negligible when there are many lines of wrapped text (so there is likely a line that almost fills the space) or the width limit is relatively large compared to typical words. So in scenarios like these or where UI layouting doesn't actually need the minimum width, it seems like skipping further layout work can be quite useful.

It might be interesting to have UI layout code that heuristically switches to the more efficient method when processing large pieces of text or to have a way to short-circuit width measurement if it reaches a certain threshold after wrapping is accounted for :thinking:

Imberflur avatar Aug 13 '23 05:08 Imberflur

I've written up a pseudo-RFC with a potential API for measurement (where using f32::MAX will always work) as well as reusing work between measurement and final layout. https://gist.github.com/Imberflur/e4ad4f878816e602ef57ea35d529ae17

I think it is rough in some respects and could benefit from examination, input, and alternative ideas. Especially, wrt the "awkward workflow" drawback and the unresolved question.

Imberflur avatar Aug 26 '23 05:08 Imberflur