package shapingimport ()// glyphIndex is the index in a Glyph slicetype glyphIndex = int// mapRunesToClusterIndices// returns a slice that maps rune indicies in the text to the index of the// first glyph in the glyph cluster containing that rune in the shaped text.// The indicies are relative to the region of runes covered by the input run.// To translate an absolute rune index in text into a rune index into the returned// mapping, subtract run.Runes.Offset first. If the provided buf is large enough to// hold the return value, it will be used instead of allocating a new slice.func mapRunesToClusterIndices( di.Direction, Range, []Glyph, []glyphIndex) []glyphIndex {if .Count <= 0 {returnnil }var []glyphIndexifcap() >= .Count { = [:.Count] } else { = make([]glyphIndex, .Count) } := 0 := .Progression() == di.TowardTopLeftif { = len() - 1 }// off tracks the offset position of the glyphs from the first rune of the // shaped text. This must be subtracted from all cluster indicies in order to // normalize them into the range [0,runes.Count). := .Offsetfor := 0; < .Count; ++ {for >= 0 && < len() && (( && [].ClusterIndex- <= ) || (! && [].ClusterIndex- < )) {if { -- } else { ++ } }if { ++ } elseif ( >= 0 && < len() && [].ClusterIndex- > ) || ( == len() && len() > 1) { -- := [].ClusterIndex - for -1 >= 0 && [-1].ClusterIndex- == { -- } }if < 0 { = 0 } elseif >= len() { = len() - 1 } [] = }return}// mapRuneToClusterIndex finds the lowest-index glyph for the glyph cluster contiaining the rune// at runeIdx in the source text. It uses a binary search of the glyphs in order to achieve this.// It is equivalent to using mapRunesToClusterIndices on only a single rune index, and is thus// more efficient for single lookups while being less efficient for runs which require many// lookups anyway.func mapRuneToClusterIndex( di.Direction, Range, []Glyph, int) glyphIndex {varint := .Progression() == di.TowardTopLeftif ! { = sort.Search(len(), func( int) bool {return [].ClusterIndex-.Offset > }) } else { = sort.Search(len(), func( int) bool {return [].ClusterIndex-.Offset < }) }if < 1 {return0 } := [-1].ClusterIndexif && -.Offset > {return }for -1 >= 0 && [-1].ClusterIndex == { -- }return}func mapRunesToClusterIndices2( di.Direction, Range, []Glyph, []glyphIndex) []glyphIndex {if .Count <= 0 {returnnil }var []glyphIndexifcap() >= .Count { = [:.Count] } else { = make([]glyphIndex, .Count) } := .Progression() == di.TowardTopLeftif {for := len() - 1; >= 0; -- { := [].ClusterIndex := for -1 >= 0 && [-1].ClusterIndex == { -- = }varintif -1 >= 0 { = [-1].ClusterIndex } else { = .Count + .Offset } := - := - .Offsetfor := ; <= + && < len(); ++ { [] = } } } else {for := 0; < len(); ++ { := [].ClusterIndex := for +1 < len() && [+1].ClusterIndex == { ++ }varintif +1 < len() { = [+1].ClusterIndex } else { = .Count + .Offset } := - := - .Offsetfor := ; <= + && < len(); ++ { [] = } } }return}func mapRunesToClusterIndices3( di.Direction, Range, []Glyph, []glyphIndex) []glyphIndex {if .Count <= 0 {returnnil }var []glyphIndexifcap() >= .Count { = [:.Count] } else { = make([]glyphIndex, .Count) } := .Progression() == di.TowardTopLeftif {for := len() - 1; >= 0; { := &[]// go to the start of the cluster -= (.GlyphCount - 1) := .ClusterIndex - .Offset// map back to [0;runes.Count[ := .RuneCount + for := ; <= && < len(); ++ { [] = }// go to the next cluster -- } } else {for := 0; < len(); { := &[] := .ClusterIndex - .Offset// map back to [0;runes.Count[ := .RuneCount + for := ; <= && < len(); ++ { [] = }// go to the next cluster += .GlyphCount } }return}// inclusiveGlyphRange returns the inclusive range of runes and glyphs matching// the provided start and breakAfter rune positions.// runeToGlyph must be a valid mapping from the rune representation to the// glyph reprsentation produced by mapRunesToClusterIndices.// numGlyphs is the number of glyphs in the output representing the runes// under consideration.func inclusiveGlyphRange( di.Direction, , int, []int, int) (, glyphIndex) { := .Progression() == di.TowardTopLeftif { = []if -1 >= 0 { = [-1] - 1 } else { = - 1 } } else { = []if +1 < len() { = [+1] - 1 } else { = - 1 } }return}// cutRun returns the sub-run of run containing glyphs corresponding to the provided// _inclusive_ rune range.// if [trimStart] is true, the leading letter spacing is removedfunc cutRun( Output, []glyphIndex, , int, bool) Output {// Convert the rune range of interest into an inclusive range within the // current run's runes. := - .Runes.Offset := - .Runes.Offsetif < 0 {// If the start location is prior to the run of shaped text under consideration, // just work from the beginning of this run. = 0 }if >= len() {// If the break location is after the entire run of shaped text, // keep through the end of the run. = len() - 1 } , := inclusiveGlyphRange(.Direction, , , , len(.Glyphs))// Construct a run out of the inclusive glyph range. .Glyphs = .Glyphs[ : +1] .Runes.Count = - + 1 .Runes.Offset = .Runes.Offset + if { .trimStartLetterSpacing() } .RecomputeAdvance()return}// breakOption represets a location within the rune slice at which// it may be safe to break a line of text.type breakOption struct {// breakAtRune is the index at which it is safe to break. breakAtRune int// required indicates that the break option is mandatory. required bool}// isValid returns whether a given option violates shaping rules (like breaking// a shaped text cluster).func ( breakOption) ( []int, Output) bool { := .breakAtRune - .Runes.Offset := + 1if < len() && >= 0 {// Check if this break is valid. := [] := []if >= len(.Glyphs) || >= len(.Glyphs) {returnfalse } := .Glyphs[].ClusterIndex := .Glyphs[].ClusterIndexif == {// This break is within a harfbuzz cluster, and is // therefore invalid.returnfalse } }returntrue}// breaker generates line breaking candidates for a text.type breaker struct { wordSegmenter *segmenter.LineIterator graphemeSegmenter *segmenter.GraphemeIterator totalRunes int// unusedWordBreak is a break requested from the breaker in a previous iteration // but which was not chosen as the line ending. Subsequent invocations of // WrapLine should start with this break. unusedWordBreak breakOption// previousWordBreak tracks the previous line breaking candidate, if any. It is // used to identify the range of runes between the previous and current // candidate. previousWordBreak breakOption// isUnusedWord indicates that the unusedBreak field is valid. isUnusedWord bool unusedGraphemeBreak breakOption isUnusedGrapheme bool}// newBreaker returns a breaker initialized to break the provided text.func newBreaker( *segmenter.Segmenter, []rune) *breaker { .Init() := &breaker{wordSegmenter: .LineIterator(),graphemeSegmenter: .GraphemeIterator(),totalRunes: len(), }return}// nextWordRaw returns a naive break candidate on a uax#14 boundary which may be invalid.func ( *breaker) () ( breakOption, bool) {if .wordSegmenter.Next() { := .wordSegmenter.Line()// Note : we dont use penalties for Mandatory Breaks so far, // we could add it with currentSegment.IsMandatoryBreak := .Offset + len(.Text) - 1 := breakOption{breakAtRune: ,// Don't treat the EOF line break as special. We implicitly always break after // the end of text input anyway.required: .IsMandatoryBreak && != .totalRunes-1, }return , true }// Unicode rules impose to always break at the endreturnbreakOption{}, false}// nextGraphemeRaw returns a naive break candidate on a uax#29 boundary which may be invalid.func ( *breaker) () ( breakOption, bool) {if .graphemeSegmenter.Next() { := .graphemeSegmenter.Grapheme()// Note : we dont use penalties for Mandatory Breaks so far, // we could add it with currentSegment.IsMandatoryBreak := breakOption{breakAtRune: .Offset + len(.Text) - 1, }return , true }// Unicode rules impose to always break at the endreturnbreakOption{}, false}// nextWordBreak returns the next rune offset at which the line can be broken// on a UAX#14 boundary if any. If it returns false, there are no more candidates.func ( *breaker) () (breakOption, bool) {varbreakOptionif .isUnusedWord { = .unusedWordBreak .isUnusedWord = false } else {varbool , = .nextWordRaw()if ! {return , false } .previousWordBreak = .unusedWordBreak .unusedWordBreak = }return , true}func ( *breaker) () { .isUnusedWord = true}// nextGraphemeBreak returns the next grapheme cluster boundary break between// the previous and current word boundary, if any. If it returns false, there are no// more candidates between the previous and current word boundaries.func ( *breaker) () (breakOption, bool) {for {var (breakOptionbool )if .isUnusedGrapheme { .isUnusedGrapheme = false = .unusedGraphemeBreak = true } else { , = .nextGraphemeRaw() }if ! {return , false }// We don't want to consider the previous word break position in general, as it has already // been tried. The one exception to this is when we're iterating the very first time, in // which case the previousWordBreak will have its zero value and we still want to consider // breaking after the first rune if there's a grapheme cluseter boundary there.if .breakAtRune <= .previousWordBreak.breakAtRune && .previousWordBreak.breakAtRune > 0 {continue } .unusedGraphemeBreak = if .breakAtRune > .unusedWordBreak.breakAtRune {// We've walked the grapheme iterator past the end of the line wrapping // candidate, so mark that we may need to re-check this break option // when evaluating the next segment. .isUnusedGrapheme = truereturn , false }return , true }}func ( *breaker) () { .isUnusedGrapheme = true}// Range indicates the location of a sequence of elements within a longer slice.typeRangestruct { Offset int Count int}// Line holds runs of shaped text wrapped onto a single line. All the contained// Output should be displayed sequentially on one line.typeLine []Output// WrapConfig provides line-wrapper settings.typeWrapConfigstruct {// Direction describes the text layout of the overall paragraph, rather than // individual runs of text. This is used to compute the correct visual order of // bidirectional text runs. Direction di.Direction// TruncateAfterLines is the number of lines of text to allow before truncating // the text. A value of zero means no limit. TruncateAfterLines int// Truncator, if provided, will be inserted at the end of a truncated line. This // feature is only active if TruncateAfterLines is nonzero. See the documentation // for [LineWrapper.WrapNextLine] for details about how this works. Truncator Output// TextContinues indicates that the paragraph wrapped by this config is not the // final paragraph in the text. This alters text truncation when filling the // final line permitted by TruncateAfterLines. If the text of this paragraph // does fit entirely on TruncateAfterLines, normally the truncator symbol would // not be inserted. However, if the overall body of text continues beyond this // paragraph (indicated by TextContinues), the truncator should still be inserted // to indicate that further paragraphs of text were truncated. This field has // no effect if TruncateAfterLines is zero. TextContinues bool// BreakPolicy determines under what circumstances the wrapper will consider // breaking in between UAX#14 line breaking candidates, or "within words" in // many scripts. BreakPolicy LineBreakPolicy// DisableTrailingWhitespaceTrim turns off a feature that automatically sets the // advance of trailing whitespace on a line to zero. In display contexts, you // usually want this feature enabled, but for text editors it is frequently // desirable to allow trailing whitespace to occupy space itself. DisableTrailingWhitespaceTrim bool}// LineBreakPolicy specifies when considering a line break within a "word" or UAX#14// segment is allowed.typeLineBreakPolicyuint8const (// WhenNecessary means that lines will only be broken within words when the word // cannot fit on the next line by itself or during truncation to preserve as much // of the final word as possible.WhenNecessaryLineBreakPolicy = iota// Never means that words will never be broken internally, allowing them to exceed // the specified maxWidth.Never// Always means that lines will always choose to break within words if it means that // more text can fit on the line.Always)func ( LineBreakPolicy) () string {switch {caseWhenNecessary:return"WhenNecessary"caseNever:return"Never"caseAlways:return"Always"default:return"unknown" }}// WithTruncator returns a copy of WrapConfig with the Truncator field set to the// result of shaping input with shaper.func ( WrapConfig) ( Shaper, Input) WrapConfig { .Truncator = .Shape()return}// runMapper efficiently maps a run to glyph clusters.type runMapper struct {// valid indicates that the mapping field is populated. valid bool// runIdx is the index of the mapped run within glyphRuns. runIdx int// mapping holds the rune->glyph mapping for the run at index mappedRun within // glyphRuns. mapping []glyphIndex}// mapRun updates the mapping field to be valid for the given run. It will skip the mapping// operation if the provided runIdx is equal to the runIdx of the previous call, as the// current mapping value is already correct.func ( *runMapper) ( int, Output) {if .runIdx != || !.valid { .mapping = mapRunesToClusterIndices3(.Direction, .Runes, .Glyphs, .mapping) .runIdx = .valid = true }}// RunIterator defines a type that can incrementally provide shaped text.typeRunIteratorinterface {// Next returns the next run in the iterator, if any. If there is a next run, // its index, content, and true will be returned, and the iterator will advance // to the following element. Otherwise Next returns an undefined index, an empty // Output, and false.Next() (index int, run Output, isValid bool)// Peek returns the same thing Next() would, but does not advance the iterator (so the // next call to Next() will return the same thing).Peek() (index int, run Output, isValid bool)// Save marks the current iterator position such that the iterator can return to it later // when Restore() is called. Only one position may be saved at a time, with subsequent // calls to Save() overriding the current value.Save()// Restore resets the iteration state to the most recently Save()-ed position.Restore()}// shapedRunSlice is a [RunIterator] built from already-shaped text.type shapedRunSlice struct { runs []Output idx int savedIdx int}var _ RunIterator = (*shapedRunSlice)(nil)// NewSliceIterator returns a [RunIterator] backed by an already-shaped slice of [Output]s.func ( []Output) RunIterator {return &shapedRunSlice{runs: , }}// Reset configures the runSlice for reuse with the given shaped text.func ( *shapedRunSlice) ( []Output) { .runs = .idx = 0 .savedIdx = 0}// Next implements [RunIterator.Next].func ( *shapedRunSlice) () (int, Output, bool) { , , := .Peek()if { .idx++ }return , , }// Peek implements [RunIterator.Peek].func ( *shapedRunSlice) () (int, Output, bool) {if .idx >= len(.runs) {return .idx, Output{}, false } := .runs[.idx]return .idx, , true}// Save implements [RunIterator.Save].func ( *shapedRunSlice) () { .savedIdx = .idx}// Restore implements [RunIterator.Restore].func ( *shapedRunSlice) () { .idx = .savedIdx}// wrapBuffer provides reusable buffers for line wrapping. When using a// wrapBuffer, returned line wrapping results will use memory stored within// the buffer. This means that the same buffer cannot be reused for another// wrapping operation while the wrapped lines are still in use (unless they// are deeply copied). If necessary, using multiple wrapBuffers can work// around this restriction.type wrapBuffer struct {// paragraph is a buffer holding paragraph allocated (primarily) from subregions // of the line field. paragraph []Line// line is a large buffer of Outputs that is used to build lines. line []Output// lineUsed is the index of the first unused element in line. lineUsed int// lineExhausted indicates whether the previous shaping used all of line. lineExhausted bool// alt is a smaller temporary buffer that holds candidate lines while // they are being built. alt []Output// altAdvance is the sum of the advances of each run in alt. altAdvance fixed.Int26_6// altSave is a slice into alt used to save and restore the state of the alt buffer. altSave []Output// altAdvanceSave is the altAdvance of the altSave field. altAdvanceSave fixed.Int26_6// best is a slice holding the best known line. When possible, it // is a subslice of line, but if line runs out of capacity it will // be heap allocated. best []Output// bestInLine tracks whether the current best is allocated from within line. bestInLine bool}func ( *wrapBuffer) () {ifcap(.paragraph) < 10 { .paragraph = make([]Line, 0, 10) } .paragraph = .paragraph[:0]ifcap(.alt) < 10 { .alt = make([]Output, 0, 10) } .alt = .alt[:0] .altAdvance = 0 .altSave = .alt[:0] .altAdvanceSave = 0ifcap(.line) < 100 { .line = make([]Output, 0, 100) } .line = .line[:0] .lineUsed = 0 .best = nil .bestInLine = falseif .lineExhausted { .lineExhausted = false// Trigger the go slice growth heuristic by appending an element to // the capacity. .line = append(.line[:cap(.line)], Output{})[:0] }}// singleRunParagraph is an optimized helper for quickly constructing// a []Line containing only a single run.func ( *wrapBuffer) ( Output) []Line { .paragraph = .paragraph[:0] := .line[.lineUsed : .lineUsed+1] [0] = .paragraphAppend()return .finalParagraph()}func ( *wrapBuffer) ( []Output) { .paragraph = append(.paragraph, )}func ( *wrapBuffer) () []Line {return .paragraph}func ( *wrapBuffer) () { .alt = .alt[:0] .altAdvance = 0 .altSave = .alt[:0] .altAdvanceSave = 0 .best = nil .bestInLine = false}// candidateLen returns the number of [Output]s in the current line wrapping candidate.func ( *wrapBuffer) () int { returnlen(.alt) }// candidateAppend adds the given run to the current line wrapping candidate.func ( *wrapBuffer) ( Output) { .alt = append(.alt, ) .altAdvance = .altAdvance + .Advance}// candidateSave captures the current state of the line candidate, enabling it to// be restored by a call to candidateRestore(). Only one state can be saved at a// time, and a subsequent call to candidateSave will override any current saved// state.func ( *wrapBuffer) () { .altSave = .alt .altAdvanceSave = .altAdvance}// candidateRestore resets the state of the line candidate to the state at the time// of the most recent call to candidateSave().func ( *wrapBuffer) () { .alt = .altSave .altAdvance = .altAdvanceSave}func ( *wrapBuffer) () fixed.Int26_6 {return .altAdvance}// markCandidateBest marks that the current line wrapping candidate is the best// known line wrapping candidate with the given suffixes. Providing suffixes does// not modify the current candidate, but does ensure that the "best" candidate ends// with them.func ( *wrapBuffer) ( ...Output) { := len(.alt) + len()iflen(.line[.lineUsed:cap(.line)]) < { .lineExhausted = true .best = make([]Output, ) .bestInLine = false } else { .best = .line[.lineUsed : .lineUsed+] .bestInLine = true } := copy(.best, .alt)copy(.best[:], )}// hasBest returns whether there is currently a known valid line wrapping candidate// for the line.func ( *wrapBuffer) () bool {returnlen(.best) > 0}// finalizeBest commits the storage for the current best line and returns it.func ( *wrapBuffer) () []Output {if .bestInLine { .lineUsed += len(.best) }return .best}// LineWrapper holds reusable state for a line wrapping operation. Reusing// LineWrappers for multiple paragraphs should improve performance.typeLineWrapperstruct {// config holds the current line wrapping settings. config WrapConfig// truncating tracks whether the wrapper should be performing truncation. truncating bool// seg is an internal storage used to initiate the breaker iterator. seg segmenter.Segmenter// breaker provides line-breaking candidates. breaker *breaker// scratch holds wrapping algorithm storage buffers for reuse. scratch wrapBuffer// mapper tracks rune->glyphCluster mappings. mapper runMapper// glyphRuns holds the runs of shaped text being wrapped. glyphRuns RunIterator// lineStartRune is the rune index of the first rune on the next line to // be shaped. lineStartRune int// more indicates that the iteration API has more data to return. more bool}// Prepare initializes the LineWrapper for the given paragraph and shaped text.// It must be called prior to invoking WrapNextLine. Prepare invalidates any// lines previously returned by this wrapper.func ( *LineWrapper) ( WrapConfig, []rune, RunIterator) { .config = .truncating = .config.TruncateAfterLines > 0 .breaker = newBreaker(&.seg, ) .glyphRuns = .lineStartRune = 0 .more = true .mapper.valid = false .scratch.reset()}// WrapParagraph wraps the paragraph's shaped glyphs to a constant maxWidth.// It is equivalent to iteratively invoking WrapLine with a constant maxWidth.// If the config has a non-zero TruncateAfterLines, WrapParagraph will return at most// that many lines. The truncated return value is the count of runes truncated from// the end of the text. The returned lines are only valid until the next call to// [*LineWrapper.WrapParagraph] or [*LineWrapper.Prepare].//// See [(*LineWrapper).WrapNextLine] for a description of how [WrapConfig]'s truncation// features impact the wrapped text output. This method returns the quantity of runes// truncated by line wrapping in the [truncated] return value.//// See also [WrapParagraphF] which supports a decimal [maxWidth].func ( *LineWrapper) ( WrapConfig, int, []rune, RunIterator) ( []Line, int) { := math.MaxInt32 >> 6if > { = }return .WrapParagraphF(, fixed.I(), , )}// WrapParagraphF is the same as [WrapParagraph], but accepts a non integer [maxWidth].func ( *LineWrapper) ( WrapConfig, fixed.Int26_6, []rune, RunIterator) ( []Line, int) { .scratch.reset()// Check whether we can skip line wrapping altogether for the simple single-run-that-fits case.if !(.TextContinues && .TruncateAfterLines == 1) { .Save()// We can only skip wrapping if the text doesn't contain any forced line // breaks that need to be evaluated by the real algorithm, so we need to // quickly scan it for that. .breaker = newBreaker(&.seg, ) := falsefor { , := .breaker.nextWordBreak()if ! {break }if .required { = truebreak } }if ! { , , := .Next() , , := .Peek()if && ! {if .Advance <= {return .scratch.singleRunParagraph(), 0 } } } .Restore() } .Prepare(, , )var (WrappedLinebool )for ! { , = .WrapNextLineF()if .Line != nil { .scratch.paragraphAppend(.Line) } }return .scratch.finalParagraph(), .Truncated}// fillUntil tries to fill the line candidate slice with runs until it reaches a run containing the// provided break option.func ( *LineWrapper) ( RunIterator, breakOption) { , , := .Peek()for && .breakAtRune >= .Runes.Count+.Runes.Offset {if .lineStartRune >= .Runes.Offset+.Runes.Count {// Consume the run we peeked (which we know is valid) _, _, _ = .Next() , , = .Peek()continue } elseif .lineStartRune > .Runes.Offset {// If part of this run has already been used on a previous line, trim // the runes corresponding to those glyphs off. .mapper.mapRun(, ) := .scratch.candidateLen() == 0 = cutRun(, .mapper.mapping, .lineStartRune, .Runes.Count+.Runes.Offset, ) }// While the run being processed doesn't contain the current line breaking // candidate, just append it to the candidate line. .scratch.candidateAppend()// Consume the run we peeked (which we know is valid) _, _, _ = .Next() , , = .Peek() }}// lineConfig tracks settings for line wrapping a single line of text.type lineConfig struct {// truncating indicates whether this line is being truncated (if sufficiently long). truncating bool// maxWidth is the maximum space a line can occupy. maxWidth fixed.Int26_6// truncatedMaxWidth holds the maximum width of the line available for text if the truncator // is occupying part of the line. truncatedMaxWidth fixed.Int26_6}// WrappedLine is the result of wrapping one line of text.typeWrappedLinestruct {// Line is the content of the line, as a slice of shaped runs Line Line// Truncated is the count of runes truncated from the end of the line, // if this line was truncated. Truncated int// NextLine is the indice (in the input text slice) of the begining // of the next line. It will equal len(text) if all the text // fit in one line. NextLine int// TrimmedTrailingWhitespace is the space taken by trailing whitespace // before if was trimmed (usually positive). // It is zero if [DisableTrailingWhitespaceTrim] is set to true, // or if there is no whitespace at the end of the line. TrimmedTrailingWhitespace fixed.Int26_6}// swapVisualOrder inverts the visual index of runs in [subline], by swapping pairs of visual indices across the midpoint// of the slice.func swapVisualOrder( Line) { := len()for := range [0 : /2] { := ( - ) - 1 [].VisualIndex, [].VisualIndex = [].VisualIndex, [].VisualIndex }}// computeBidiOrdering resolves the [VisualIndex] of each run.func computeBidiOrdering( di.Direction, Line) { := -1for , := range { := if .Progression() == di.TowardTopLeft { = len() - 1 - } [].VisualIndex = int32()if .Direction == {if != -1 {swapVisualOrder([:]) = -1 } } elseif == -1 { = } }if != -1 {swapVisualOrder([:]) }}func ( *LineWrapper) ( Line, bool) (WrappedLine, bool) {varfixed.Int26_6iflen() > 0 {computeBidiOrdering(.config.Direction, )if !.config.DisableTrailingWhitespaceTrim {// Here we find the last visual run in the line. := len() - 1if .config.Direction.Progression() == di.TowardTopLeft { = 0 }for , := range {if .VisualIndex == int32() { = break } } := &[]// This next block locates the first/last visual glyph on the line and // zeroes its advance if it is whitespace.if := len(.Glyphs); > 0 {var *Glyphif .config.Direction.Progression() == di.FromTopLeft { = &.Glyphs[-1] } else { = &.Glyphs[0] }if .Direction.IsVertical() {if .Height == 0 { .YAdvance = 0 .Advance = 0 } } else { // horizontalif .Width == 0 { .XAdvance = 0 .Advance = 0 } } := .Advance .RecomputeAdvance() = - .Advance } } := [len()-1]// Update the start position of the next line. .lineStartRune = .Runes.Count + .Runes.Offset }// Check whether we've exhausted the text. = || .lineStartRune >= .breaker.totalRunes// Implement truncation if needed. := 0if .truncating { .config.TruncateAfterLines-- := falseif .config.TruncateAfterLines == 0 { = true = .breaker.totalRunes - .lineStartRune = > 0 || .config.TextContinues }if { := .config.Truncator .Runes.Count = .Runes.Offset = .lineStartRune = append(, )// We've just modified the line, we need to recompute the bidi ordering.computeBidiOrdering(.config.Direction, ) } }// Mark the paragraph as complete if needed.if { .more = false }returnWrappedLine{, , .lineStartRune, }, }// WrapNextLine wraps the shaped glyphs of a paragraph to a particular max width.// It is meant to be called iteratively to wrap each line, allowing lines to// be wrapped to different widths within the same paragraph. When done is true,// subsequent calls to WrapNextLine (without calling Prepare) will return a nil line.//// The returned line is only valid until the next call to// [*LineWrapper.Prepare] or [*LineWrapper.WrapParagraph].//// If the LineWrapper's [WrapConfig].TruncateAfterLines// is non-zero, the final line of text returned by successive calls to WrapNextLine// may be truncated. The quantity of runes truncated by wrapping the line is// returned in [WrappedLine].Truncated. If this is non-zero AND the LineWrapper's// [WrapConfig].Truncator is set, the final line will end with an extra [Output].// [Output]s before the final [Output] will represent the input runes that are still// visible before truncation, and the final [Output] will be a copy of the Truncator// with its Runes.Count set to the quantity of runes truncated during line wrapping.//// See also [WrapNextLineF] which supports a decimal [maxWidth].func ( *LineWrapper) ( int) ( WrappedLine, bool) { := math.MaxInt32 >> 6if > { = }return .WrapNextLineF(fixed.I())}// WrapNextLineF is the same as [WrapNextLine], but accepts a non integer [maxWidth].func ( *LineWrapper) ( fixed.Int26_6) ( WrappedLine, bool) {// If we've already finished the paragraph, don't do any more work.if !.more {returnWrappedLine{NextLine: .lineStartRune}, true }deferfunc() { , = .postProcessLine(.Line, ) }()// If the iterator is empty, return early. , , := .glyphRuns.Peek()if ! {returnWrappedLine{}, true } .scratch.startLine() := lineConfig{truncating: .config.TruncateAfterLines == 1,maxWidth: ,truncatedMaxWidth: - .config.Truncator.Advance, } = .wrapNextLine() := .scratch.finalizeBest()returnWrappedLine{Line: }, }// checkpoint captures both the current candidate line and the corresponding run iteration// state. These can be restored together by calling restore().func ( *LineWrapper) () { .scratch.candidateSave() .glyphRuns.Save()}// restore resets the current candidate line and corresponding run iteration state to the// values at the last call to checkpoint().func ( *LineWrapper) () { .scratch.candidateRestore() .glyphRuns.Restore()}// wrapNextLine iteratively processes line breaking candidates, building a line within the// wrapper's scratch [WrapBuffer]. It returns whether the paragraph is finished once it has// successfully built a line.func ( *LineWrapper) ( lineConfig) ( bool) {for { .checkpoint() , := .breaker.nextWordBreak()if ! {break }switch , := .processBreakOption(, ); {casebreakInvalid: .restore()continuecasefits: .scratch.markCandidateBest()if .required {returnfalse }continuecaseendLine:// Found a valid line ending the text, append the candidateRun and use it. .scratch.markCandidateBest()returntruecasetruncated:// The candidateRun does not fit.if !.scratch.hasBest() { .scratch.markCandidateBest() }if .config.BreakPolicy == Never {returntrue }// Fall through to try grapheme breaking.casenewLineBeforeBreak: .restore()// We found a valid line that didn't use this break, so mark that it can be // reused on the next iteration. .breaker.markWordOptionUnused()if .config.BreakPolicy == Never || (.config.BreakPolicy == WhenNecessary && !.truncating) {returnfalse }// Fall through to try grapheme breaking.casecannotFit:if .config.BreakPolicy == Never {if .truncating {returntrue } .scratch.markCandidateBest()returnfalse }// Fall through to try grapheme breaking. }// Ensure that the grapheme breaking has access to // all runs we already tried in the iterator. .restore()// segment using UAX#29 grapheme clustering here and try // breaking again using only those boundaries to find a viable break in cases // where no UAX#14 breaks were viable above.for { .checkpoint() , := .breaker.nextGraphemeBreak()if ! {break }switch , := .processBreakOption(, ); {casebreakInvalid: .restore()continuecasefits:// If we found at least one viable line candidate, we aren't using the word break option. .scratch.markCandidateBest() .breaker.markWordOptionUnused()continuecaseendLine: .scratch.markCandidateBest()returntruecasetruncated:if !.scratch.hasBest() { .scratch.markCandidateBest() }returntruecasenewLineBeforeBreak: .restore()// If we found at least one viable line candidate, we aren't using the word break option. .breaker.markWordOptionUnused() .breaker.markGraphemeOptionUnused()returnfalsecasecannotFit:if .truncating {// Don't append the candidate grapheme to leave as much space as possible for the // truncator.returntrue }// If no graphemes fit, we should still use one so that the line contains something. Maybe // the next grapheme will fit on the next line. .scratch.markCandidateBest() .breaker.markWordOptionUnused()returnfalse } }returnfalse }returntrue}type processBreakResult uint8const (// breakInvalid indicates that the provided break candidate is incompatible with the shaped // text and should be skipped. breakInvalid processBreakResult = iota// endLine indicates that the candidate fits on the line and terminates the text. endLine// truncated indicates that the candidate does not fit on the line and that truncation needs to // occur. truncated// newLineBeforeBreak indicates that the candidate contains a valid line, but the latest break // option could not fit. newLineBeforeBreak// fits indicates that the text up to the break option fit within the line and that another break // option can be attempted. fits// cannotFit indicates that this is the first break option on the line, and that even this option cannot // fit within the space available. When cannotFit is returned, the scratch buffer's candidate will contain // the run that cannot fit, but it will not be committed as the best option. The choice of how to handle // this is left to higher-level logic. cannotFit)// processBreakOption evaluates whether the provided breakOption can fit onto the current line wrapping line.func ( *LineWrapper) ( breakOption, lineConfig) (processBreakResult, Output) {// Discard break options on previous lines.if .breakAtRune < .lineStartRune {returnbreakInvalid, Output{} }// Fill candidate line with runs until the run containing the break option. .fillUntil(.glyphRuns, ) , , := .glyphRuns.Peek() .mapper.mapRun(, )if !.isValid(.mapper.mapping, ) {// Reject invalid line break candidate and acquire a new one.returnbreakInvalid, Output{} } := .scratch.candidateLen() == 0 := cutRun(, .mapper.mapping, .lineStartRune, .breakAtRune, ) := .advanceSpaceAware(.config.Direction) + .scratch.candidateAdvance()if > .maxWidth {// The run doesn't fit on the line.if !.scratch.hasBest() {returncannotFit, } else {returnnewLineBeforeBreak, } } elseif .truncating && > .truncatedMaxWidth {// The run would not fit if truncated. := .Runes.Count + .Runes.Offsetif == .breaker.totalRunes && !.config.TextContinues {// The run contains the entire end of the text, so no truncation is // necessary.returnendLine, }// We must truncate the line in order to show it.returntruncated, } else {// The run does fit on the line. Commit this line as the best known // line, but keep lineCandidate unmodified so that later break // options can be attempted to see if a more optimal solution is // available.returnfits, }}
The pages are generated with Goldsv0.8.7. (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu.
PR and bug reports are welcome and can be submitted to the issue list.
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds.