Closed benoitkugler closed 1 month ago
This is a draft PR to support custom spacing between words or letters.
The core implementation is complete, but I'm not sure yet what the exported API should be.
My use case will be for an 'all-in-one' segmenting-shaping-spacing-wrapping-justifying function, so that the
addWordSpacing
andaddLetterSpacing
could stay hidden, but perhaps you would have other use cases in mind ?
This is also my use-case, but Gio implements its own all-in-one operation for this right now. Maybe we will eventually be able to use one provided by go-text, but for now I'd prefer to have the flexibility of keeping it in Gio. So I would want these operations exported.
I don't think I understand how to use addLetterSpacing
realistically though. I guess you can apply it before line wrapping in order to space out glyphs, but then you didn't know where the start/end of lines were, so you end up with extra space at the beginning/end of line wrap locations. If you were going to use this for justifying text post-wrapping, it would seem nicer to offer an API that you could supply the advance that you want to "spread" among the glyph clusters and let that do all of the work.
I'm also not sure it makes sense to define these as operations on single runs. Both word spacing and letter spacing make sense at run boundaries in multi-run text like bidi or just rich text with varying styles. Maybe these should accept []Output
to act on?
Thank you very much for your comments, which enlighten many shortcomings.
I don't think I understand how to use
addLetterSpacing
realistically though. I guess you can apply it before line wrapping in order to space out glyphs, but then you didn't know where the start/end of lines were, so you end up with extra space at the beginning/end of line wrap locations. If you were going to use this for justifying text post-wrapping, it would seem nicer to offer an API that you could supply the advance that you want to "spread" among the glyph clusters and let that do all of the work.
Fair point.. but the additional spacing will also influence line wrapping, so we would need something like
Does it seems correct ?
I'm also not sure it makes sense to define these as operations on single runs. Both word spacing and letter spacing make sense at run boundaries in multi-run text like bidi or just rich text with varying styles. Maybe these should accept
[]Output
to act on?
Hum yes, you right. We need a finer handling of run boundaries..
My use case will be for an 'all-in-one' segmenting-shaping-spacing-wrapping-justifying function, so that the
addWordSpacing
andaddLetterSpacing
could stay hidden, but perhaps you would have other use cases in mind ?
If we are looking all in one then it also needs to encompass tab alignment as well (where both initial and also mid-text tab characters need to align with something outside the current shaping info). I would expect that is outside the scope, and so like Gio I expect Fyne might need to keep it internally.
It does sound like this is the right place for inserting the extra space as you say, but perhaps stopping short of an all-in-one in terms of its usage?
The last commit tries to address your comments.
There is now 3 exported methods, to be used like this :
RunIterator
)TrimLetterSpacing
to cleanup the line Do you think such an approach is usable ?
We will probably need a similar mechanism for word spacing. By the way, how do you handle spaces at the line boundaries in the toolkits ? Does the line wrapper make sure no line begin with a space ?
The last commit tries to address your comments.
There is now 3 exported methods, to be used like this :
* for each run, add word spacing and letter spacing (that could be done by an implementation of `RunIterator`) * wrap * for each line, call `TrimLetterSpacing` to cleanup the line
Do you think such an approach is usable ?
Maybe. I'd like to talk through your next question and circle back to this.
We will probably need a similar mechanism for word spacing. By the way, how do you handle spaces at the line boundaries in the toolkits ? Does the line wrapper make sure no line begin with a space ?
This is actually a problem that I've been grappling with in Gio. In the default case it isn't an issue, but if we have LTR text right-aligned or RTL text left-aligned, the current line wrapper will make the glyph at the edge of the line a space. This can look terrible. Consider wrapping "the quick brown fox" in LTR but right-aligned. I'll mock out what happens in monospace below (using an underscore to indicate actual space glyphs):
The_
quick_
brown_
fox
One way to solve this problem would be to make the line wrapper alignment-aware so that it could instead choose:
The
_quick
_brown
_fox
However, this both makes the results harder to cache and the algorithm more complex.
It's tempting to say that we can just trim the space characters at the end of lines, but that doesn't work well with text editors. If you trim the space, there's no visible difference between the cursor being before and after that space glyph. It makes for a somewhat confusing editing experience.
The line wrapper also currently always respects the width of whitespace glyphs. The above examples wrap to a width of 6 monospace glyphs, but would actually behave unexpectedly if wrapping to width 5 (with WhenNecessary as a break policy):
The_
quick
_brow
n_fox
Our current algorithm isn't smart enough to collapse the space after "quick", and thus we break "brown" across lines.
I've been meaning to research how other text toolkits and browsers handle these cases to see what the reasonable thing to do is, but I haven't gotten to it yet.
@benoitkugler Do you happen to already know how this stuff is usually handled?
I've quickly look at Pango, and what they do is actually zeroing the advance of a space who ends up at the end of a line.
If you trim the space, there's no visible difference between the cursor being before and after that space glyph.
Are you sure this a problem across line boundaries ? It seems that the line break is a sufficient visual indicator, isn't it ?
The Pango behavior seems similar to what browsers are doing. See for instance the textearea this comment is written into..
I've quickly look at Pango, and what they do is actually zeroing the advance of a space who ends up at the end of a line.
If you trim the space, there's no visible difference between the cursor being before and after that space glyph.
Are you sure this a problem across line boundaries ? It seems that the line break is a sufficient visual indicator, isn't it ?
The Pango behavior seems similar to what browsers are doing. See for instance the textearea this comment is written into..
That's fair. It may simply be that I need to generate cursor positions more intelligently in Gio (that's some of our gnarliest code) to handle this case better.
If we apply this space-trimming as a post-processing step, it doesn't seem like it can help us solve this though:
The_
quick
_brow
n_fox
For this problem to be solved, we'd need the wrapper itself to be aware that it can choose to make whitespace at the end of a line have zero width. That, in turn, would mean adding a mechanism to know what whitespace actually is. Right now the wrapper treats all glyphs the same and only cares about where UAX says the segment boundaries are.
I confess that it feels a little awkward to ask consumers to "call this method on each shaped run" and to then "call this method on each line after shaping with the same value". However, I can't think of an abstraction that would unify those without taking a lot of control away from consumers, so I guess I'm okay with this approach.
I confess that it feels a little awkward to ask consumers to "call this method on each shaped run" and to then "call this method on each line after shaping with the same value".
Agreed... That is also why an all in one function hiding this complexity could have some value.
That, in turn, would mean adding a mechanism to know what whitespace actually is
Pango indeed registers whitespace runes. I'll check how it is used in the line breaking code.
This is basically what Fyne does too - find the white space to wrap on, then end the line before it and start the next after it. So it understands (most) white space. Sadly I can't just contribute the code due to license issues discussed before.
Obviously a character breaking algorithm can't do quite the same and if we introduce hyphenated breaking in-word it would have to /add/ (like ellipsis truncation) not subtract from the space required.
This is a draft PR to support custom spacing between words or letters.
The core implementation is complete, but I'm not sure yet what the exported API should be.
My use case will be for an 'all-in-one' segmenting-shaping-spacing-wrapping-justifying function, so that the
addWordSpacing
andaddLetterSpacing
could stay hidden, but perhaps you would have other use cases in mind ?