Skip to content

Commit 5414a60

Browse files
committed
[shaping] initial support for tab alignement
1 parent 9645b81 commit 5414a60

2 files changed

Lines changed: 95 additions & 28 deletions

File tree

shaping/output.go

Lines changed: 74 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
package shaping
44

55
import (
6+
"math"
7+
68
"github.com/go-text/typesetting/di"
79
"github.com/go-text/typesetting/font"
810
"golang.org/x/image/math/fixed"
@@ -155,30 +157,30 @@ type Output struct {
155157

156158
// ToFontUnit converts a metrics (typically found in [Glyph] fields)
157159
// to unscaled font units.
158-
func (o *Output) ToFontUnit(v fixed.Int26_6) float32 {
159-
return float32(v) / float32(o.Size) * float32(o.Face.Upem())
160+
func (out *Output) ToFontUnit(v fixed.Int26_6) float32 {
161+
return float32(v) / float32(out.Size) * float32(out.Face.Upem())
160162
}
161163

162164
// FromFontUnit converts an unscaled font value to the current [Size]
163-
func (o *Output) FromFontUnit(v float32) fixed.Int26_6 {
164-
return fixed.Int26_6(v * float32(o.Size) / float32(o.Face.Upem()))
165+
func (out *Output) FromFontUnit(v float32) fixed.Int26_6 {
166+
return fixed.Int26_6(v * float32(out.Size) / float32(out.Face.Upem()))
165167
}
166168

167169
// RecomputeAdvance updates only the Advance field based on the current
168170
// contents of the Glyphs field. It is faster than RecalculateAll(),
169171
// and can be used to speed up line wrapping logic.
170-
func (o *Output) RecomputeAdvance() {
172+
func (out *Output) RecomputeAdvance() {
171173
advance := fixed.Int26_6(0)
172-
if o.Direction.IsVertical() {
173-
for _, g := range o.Glyphs {
174+
if out.Direction.IsVertical() {
175+
for _, g := range out.Glyphs {
174176
advance += g.YAdvance
175177
}
176178
} else { // horizontal
177-
for _, g := range o.Glyphs {
179+
for _, g := range out.Glyphs {
178180
advance += g.XAdvance
179181
}
180182
}
181-
o.Advance = advance
183+
out.Advance = advance
182184
}
183185

184186
// advanceSpaceAware adjust the value in [Advance]
@@ -189,45 +191,45 @@ func (o *Output) RecomputeAdvance() {
189191
// because the trailing space in this run will always be internal to the paragraph.
190192
//
191193
// TODO: should we take into account multiple spaces ?
192-
func (o *Output) advanceSpaceAware(paragraphDir di.Direction) fixed.Int26_6 {
193-
L := len(o.Glyphs)
194-
if L == 0 || paragraphDir != o.Direction {
195-
return o.Advance
194+
func (out *Output) advanceSpaceAware(paragraphDir di.Direction) fixed.Int26_6 {
195+
L := len(out.Glyphs)
196+
if L == 0 || paragraphDir != out.Direction {
197+
return out.Advance
196198
}
197199

198200
// adjust the last to account for spaces
199201
var lastG Glyph
200-
if o.Direction.Progression() == di.FromTopLeft {
201-
lastG = o.Glyphs[L-1]
202+
if out.Direction.Progression() == di.FromTopLeft {
203+
lastG = out.Glyphs[L-1]
202204
} else {
203-
lastG = o.Glyphs[0]
205+
lastG = out.Glyphs[0]
204206
}
205-
if o.Direction.IsVertical() {
207+
if out.Direction.IsVertical() {
206208
if lastG.Height == 0 {
207-
return o.Advance - lastG.YAdvance
209+
return out.Advance - lastG.YAdvance
208210
}
209211
} else { // horizontal
210212
if lastG.Width == 0 {
211-
return o.Advance - lastG.XAdvance
213+
return out.Advance - lastG.XAdvance
212214
}
213215
}
214-
return o.Advance - lastG.endLetterSpacing
216+
return out.Advance - lastG.endLetterSpacing
215217
}
216218

217219
// RecalculateAll updates the all other fields of the Output
218220
// to match the current contents of the Glyphs field.
219221
// This method will fail with UnimplementedDirectionError if the Output
220222
// direction is unimplemented.
221-
func (o *Output) RecalculateAll() {
223+
func (out *Output) RecalculateAll() {
222224
var (
223225
advance fixed.Int26_6
224226
ascent fixed.Int26_6
225227
descent fixed.Int26_6
226228
)
227229

228-
if o.Direction.IsVertical() {
229-
for i := range o.Glyphs {
230-
g := &o.Glyphs[i]
230+
if out.Direction.IsVertical() {
231+
for i := range out.Glyphs {
232+
g := &out.Glyphs[i]
231233
advance += g.YAdvance
232234
depth := g.XOffset + g.XBearing // start of the glyph
233235
if depth < descent {
@@ -239,8 +241,8 @@ func (o *Output) RecalculateAll() {
239241
}
240242
}
241243
} else { // horizontal
242-
for i := range o.Glyphs {
243-
g := &o.Glyphs[i]
244+
for i := range out.Glyphs {
245+
g := &out.Glyphs[i]
244246
advance += g.XAdvance
245247
height := g.YBearing + g.YOffset
246248
if height > ascent {
@@ -252,8 +254,8 @@ func (o *Output) RecalculateAll() {
252254
}
253255
}
254256
}
255-
o.Advance = advance
256-
o.GlyphBounds = Bounds{
257+
out.Advance = advance
258+
out.GlyphBounds = Bounds{
257259
Ascent: ascent,
258260
Descent: descent,
259261
}
@@ -303,6 +305,40 @@ func (out *Output) moveCrossAxis(d fixed.Int26_6) {
303305
out.GlyphBounds.Descent += d
304306
}
305307

308+
func (out *Output) applyTabs(text []rune, columnWidth, runStart fixed.Int26_6) {
309+
isVertical := out.Direction.IsVertical()
310+
columnWidthF := float64(columnWidth) / 64
311+
var advance fixed.Int26_6
312+
for i, g := range out.Glyphs {
313+
gAdvance := g.XAdvance
314+
if isVertical {
315+
gAdvance = g.YAdvance
316+
}
317+
isTab := g.RuneCount == 1 && g.GlyphCount == 1 && text[g.ClusterIndex] == '\t'
318+
if !isTab {
319+
advance += gAdvance
320+
continue
321+
}
322+
// update the advance of the glyph so that the next glyph is "tab-aligned" :
323+
// we want the "end" of the tab to be a multiple of columnWidth, that is :
324+
// (runStart + advance + updatedTabAdvance) % columnWith == 0
325+
glyphStartF := float64(runStart+advance) / 64
326+
remainder := math.Mod(glyphStartF, columnWidthF)
327+
updatedTabAdvance := fixed.Int26_6((columnWidthF - remainder) * 64)
328+
329+
if isVertical {
330+
out.Glyphs[i].YAdvance = updatedTabAdvance
331+
} else {
332+
out.Glyphs[i].XAdvance = updatedTabAdvance
333+
}
334+
335+
advance += updatedTabAdvance
336+
}
337+
338+
// no need to call RecomputeAdvance
339+
out.Advance = advance
340+
}
341+
306342
// AdjustBaselines aligns runs with different baselines.
307343
//
308344
// For vertical text, it centralizes 'sideways' runs, so
@@ -349,3 +385,13 @@ func (l Line) AdjustBaselines() {
349385
l[i].moveCrossAxis(-middle)
350386
}
351387
}
388+
389+
// AlignTabs updates the advance of glyphs mapped to '\t' runes,
390+
// so that tabs are aligned on columns defined by [columnWidth].
391+
func (l Line) AlignTabs(text []rune, columnWidth fixed.Int26_6) {
392+
var runsAdvance fixed.Int26_6 // the position of the start of the current run
393+
for i := range l {
394+
l[i].applyTabs(text, columnWidth, runsAdvance)
395+
runsAdvance += l[i].Advance
396+
}
397+
}

shaping/output_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,3 +467,24 @@ func TestAdvanceSpaceAware(t *testing.T) {
467467
})
468468
}
469469
}
470+
471+
func TestLine_applyTabs(t *testing.T) {
472+
text := []rune("A first run\twith tab. A second run\t\ta\t.")
473+
// simplify with 1:1 rune glyph mapping
474+
glyphs := make([]Glyph, len(text))
475+
for i := range text {
476+
glyphs[i] = Glyph{ClusterIndex: i, RuneCount: 1, GlyphCount: 1, XAdvance: fixed.I(1)}
477+
}
478+
479+
run1 := Output{Glyphs: glyphs[0:22]}
480+
run2 := Output{Glyphs: glyphs[22:]}
481+
run1.RecalculateAll()
482+
run2.RecalculateAll()
483+
line := Line{run1, run2}
484+
485+
line.AlignTabs(text, fixed.I(5))
486+
tu.Assert(t, run1.Glyphs[11].XAdvance == fixed.I(4))
487+
tu.Assert(t, run2.Glyphs[34-22].XAdvance == fixed.I(3))
488+
tu.Assert(t, run2.Glyphs[35-22].XAdvance == fixed.I(5))
489+
tu.Assert(t, run2.Glyphs[37-22].XAdvance == fixed.I(4))
490+
}

0 commit comments

Comments
 (0)