700 lines
17 KiB
Go
700 lines
17 KiB
Go
package system
|
|
|
|
import "image"
|
|
import "unicode"
|
|
import "image/color"
|
|
import "golang.org/x/image/font"
|
|
import "git.tebibyte.media/tomo/tomo"
|
|
import "golang.org/x/image/math/fixed"
|
|
import "git.tebibyte.media/tomo/typeset"
|
|
import "git.tebibyte.media/tomo/tomo/text"
|
|
import "git.tebibyte.media/tomo/tomo/input"
|
|
import "git.tebibyte.media/tomo/tomo/event"
|
|
import "git.tebibyte.media/tomo/tomo/canvas"
|
|
import "git.tebibyte.media/tomo/backend/internal/util"
|
|
import "git.tebibyte.media/sashakoshka/goutil/container"
|
|
|
|
type textBox struct {
|
|
*box
|
|
|
|
contentBounds image.Rectangle
|
|
scroll image.Point
|
|
|
|
attrTextColor attrHierarchy[tomo.AttrTextColor]
|
|
attrDotColor attrHierarchy[tomo.AttrDotColor]
|
|
attrFace attrHierarchy[tomo.AttrFace]
|
|
attrWrap attrHierarchy[tomo.AttrWrap]
|
|
attrAlign attrHierarchy[tomo.AttrAlign]
|
|
attrOverflow attrHierarchy[tomo.AttrOverflow]
|
|
|
|
text string
|
|
runes []rune
|
|
selectable bool
|
|
selecting bool
|
|
selectStart int
|
|
dot text.Dot
|
|
desiredX fixed.Int26_6
|
|
|
|
drawer typeset.Drawer
|
|
face util.Cycler[font.Face]
|
|
lineHeight ucontainer.Memo[fixed.Int26_6]
|
|
|
|
on struct {
|
|
contentBoundsChange event.FuncBroadcaster
|
|
dotChange event.FuncBroadcaster
|
|
}
|
|
}
|
|
|
|
func (this *System) NewTextBox () tomo.TextBox {
|
|
box := &textBox { }
|
|
box.box = this.newBox(box)
|
|
box.attrTextColor.SetFallback(tomo.ATextColor(color.Black))
|
|
box.attrDotColor.SetFallback(tomo.ADotColor(color.RGBA { G: 255, B: 255, A: 255}))
|
|
box.lineHeight = ucontainer.NewMemo(func () fixed.Int26_6 {
|
|
face := box.face.Value()
|
|
if face == nil { return 0 }
|
|
metrics := face.Metrics()
|
|
return metrics.Height
|
|
})
|
|
return box
|
|
}
|
|
|
|
// ----- public methods ----------------------------------------------------- //
|
|
|
|
func (this *textBox) ContentBounds () image.Rectangle {
|
|
return this.contentBounds
|
|
}
|
|
|
|
func (this *textBox) ScrollTo (point image.Point) {
|
|
if this.scroll == point { return }
|
|
this.scroll = point
|
|
this.invalidateLayout()
|
|
}
|
|
|
|
func (this *textBox) RecommendedHeight (width int) int {
|
|
return this.drawer.ReccomendedHeightFor(width) + this.borderAndPaddingSum().Vertical()
|
|
}
|
|
|
|
func (this *textBox) RecommendedWidth (height int) int {
|
|
// TODO maybe not the best idea?
|
|
return this.minimumSize().X
|
|
}
|
|
|
|
func (this *textBox) OnContentBoundsChange (callback func()) event.Cookie {
|
|
return this.on.contentBoundsChange.Connect(callback)
|
|
}
|
|
|
|
func (this *textBox) SetText (text string) {
|
|
if this.text == text { return }
|
|
this.text = text
|
|
this.runes = []rune(text)
|
|
this.drawer.SetText(this.runes)
|
|
this.invalidateMinimum()
|
|
this.invalidateLayout()
|
|
}
|
|
|
|
func (this *textBox) SetSelectable (selectable bool) {
|
|
if this.selectable == selectable { return }
|
|
this.selectable = selectable
|
|
}
|
|
|
|
func (this *textBox) Select (dot text.Dot) {
|
|
this.selec(dot)
|
|
}
|
|
|
|
func (this *textBox) selec (dot text.Dot) bool {
|
|
if this.selectWithoutResettingDesiredX(dot) {
|
|
this.desiredX = fixed.I(0)
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (this *textBox) selectWithoutResettingDesiredX (dot text.Dot) bool {
|
|
if !this.selectable { return false }
|
|
if this.dot == dot { return false }
|
|
this.SetFocused(true)
|
|
this.dot = dot
|
|
this.scrollToDot()
|
|
this.invalidateDraw()
|
|
return true
|
|
}
|
|
|
|
func (this *textBox) userSelect (dot text.Dot) bool {
|
|
if this.selec(dot) {
|
|
this.on.dotChange.Broadcast()
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (this *textBox) userSelectWithoutResettingDesiredX (dot text.Dot) bool {
|
|
if this.selectWithoutResettingDesiredX(dot) {
|
|
this.on.dotChange.Broadcast()
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (this *textBox) Dot () text.Dot {
|
|
return this.dot
|
|
}
|
|
|
|
func (this *textBox) OnDotChange (callback func ()) event.Cookie {
|
|
return this.on.dotChange.Connect(callback)
|
|
}
|
|
|
|
// ----- private methods ---------------------------------------------------- //
|
|
|
|
func (this *textBox) Draw (can canvas.Canvas) {
|
|
if can == nil { return }
|
|
|
|
texture := this.attrTexture.Value().Texture
|
|
col := this.attrColor.Value().Color
|
|
|
|
this.drawBorders(can)
|
|
|
|
pen := can.Pen()
|
|
pen.Fill(col)
|
|
pen.Texture(texture)
|
|
if this.transparent() && this.parent != nil {
|
|
this.parent.drawBackgroundPart(can)
|
|
}
|
|
pen.Rectangle(can.Bounds())
|
|
|
|
if this.selectable && this.Focused() {
|
|
this.drawDot(can)
|
|
}
|
|
|
|
if this.face.Value() != nil {
|
|
textColor := this.attrTextColor.Value().Color
|
|
this.drawer.Draw(can, textColor, this.textOffset())
|
|
}
|
|
}
|
|
|
|
func (this *textBox) setAttr (attr tomo.Attr, user bool) {
|
|
switch attr := attr.(type) {
|
|
case tomo.AttrTextColor:
|
|
if this.attrTextColor.Set(attr, user) && !this.dot.Empty() {
|
|
this.invalidateDraw()
|
|
}
|
|
|
|
case tomo.AttrDotColor:
|
|
if this.attrDotColor.Set(attr, user) && !this.dot.Empty() {
|
|
this.invalidateDraw()
|
|
}
|
|
|
|
case tomo.AttrFace:
|
|
if this.attrFace.Set(attr, user) {
|
|
this.handleFaceChange()
|
|
}
|
|
|
|
case tomo.AttrWrap:
|
|
if this.attrWrap.Set(attr, user) {
|
|
this.drawer.SetWrap(bool(this.attrWrap.Value()))
|
|
this.invalidateMinimum()
|
|
this.invalidateLayout()
|
|
}
|
|
|
|
case tomo.AttrAlign:
|
|
if this.attrAlign.Set(attr, user) {
|
|
align := this.attrAlign.Value()
|
|
this.drawer.SetAlign (
|
|
typeset.Align(align.X),
|
|
typeset.Align(align.Y))
|
|
this.invalidateDraw()
|
|
}
|
|
|
|
case tomo.AttrOverflow:
|
|
if this.attrOverflow.Set(attr, user) {
|
|
this.invalidateMinimum()
|
|
this.invalidateLayout()
|
|
}
|
|
|
|
default: this.box.setAttr(attr, user)
|
|
}
|
|
}
|
|
|
|
func (this *textBox) unsetAttr (kind tomo.AttrKind, user bool) {
|
|
switch kind {
|
|
case tomo.AttrKindTextColor:
|
|
if this.attrTextColor.Unset(user) && !this.dot.Empty() {
|
|
this.invalidateDraw()
|
|
}
|
|
|
|
case tomo.AttrKindDotColor:
|
|
if this.attrDotColor.Unset(user) && !this.dot.Empty() {
|
|
this.invalidateDraw()
|
|
}
|
|
|
|
case tomo.AttrKindFace:
|
|
if this.attrFace.Unset(user) {
|
|
this.handleFaceChange()
|
|
}
|
|
|
|
case tomo.AttrKindWrap:
|
|
if this.attrWrap.Unset(user) {
|
|
this.drawer.SetWrap(bool(this.attrWrap.Value()))
|
|
this.invalidateMinimum()
|
|
this.invalidateLayout()
|
|
}
|
|
|
|
case tomo.AttrKindAlign:
|
|
if this.attrAlign.Unset(user) {
|
|
align := this.attrAlign.Value()
|
|
this.drawer.SetAlign (
|
|
typeset.Align(align.X),
|
|
typeset.Align(align.Y))
|
|
this.invalidateDraw()
|
|
}
|
|
|
|
case tomo.AttrKindOverflow:
|
|
if this.attrOverflow.Unset(user) {
|
|
this.invalidateMinimum()
|
|
this.invalidateLayout()
|
|
}
|
|
|
|
default: this.box.unsetAttr(kind, user)
|
|
}
|
|
}
|
|
|
|
func roundPt (point fixed.Point26_6) image.Point {
|
|
return image.Pt(point.X.Round(), point.Y.Round())
|
|
}
|
|
|
|
func fixPt (point image.Point) fixed.Point26_6 {
|
|
return fixed.P(point.X, point.Y)
|
|
}
|
|
|
|
func (this *textBox) drawDot (can canvas.Canvas) {
|
|
face := this.face.Value()
|
|
if face == nil { return }
|
|
|
|
textColor := this.attrTextColor.Value().Color
|
|
dotColor := this.attrDotColor.Value().Color
|
|
|
|
pen := can.Pen()
|
|
|
|
bounds := this.InnerBounds()
|
|
metrics := face.Metrics()
|
|
dot := this.dot
|
|
canonDot := dot.Canon()
|
|
start := this.drawer.PositionAt(canonDot.Start).Add(fixPt(this.textOffset()))
|
|
end := this.drawer.PositionAt(canonDot.End ).Add(fixPt(this.textOffset()))
|
|
canonEnd := this.drawer.PositionAt(dot.End ).Add(fixPt(this.textOffset()))
|
|
height := this.drawer.LineHeight().Round()
|
|
ascent := fixed.Point26_6 { Y: metrics.Descent }
|
|
descent := fixed.Point26_6 { Y: metrics.Ascent }
|
|
|
|
switch {
|
|
case canonDot.Empty():
|
|
|
|
case start.Y == end.Y:
|
|
pen.Fill(dotColor)
|
|
pen.StrokeWeight(0)
|
|
pen.Rectangle(image.Rectangle {
|
|
Min: roundPt(start.Add(ascent)),
|
|
Max: roundPt(end.Sub(descent)),
|
|
})
|
|
|
|
default:
|
|
pen.Fill(dotColor)
|
|
pen.StrokeWeight(0)
|
|
|
|
rect := image.Rectangle {
|
|
Min: roundPt(start.Add(ascent)),
|
|
Max: roundPt(start.Sub(descent)),
|
|
}
|
|
rect.Max.X = bounds.Max.X
|
|
pen.Rectangle(rect)
|
|
if end.Y - start.Y > fixed.I(height) {
|
|
rect.Min.X = bounds.Min.X
|
|
rect.Min.Y = roundPt(start.Sub(descent)).Y + height
|
|
rect.Max.X = bounds.Max.X
|
|
rect.Max.Y = roundPt(end.Add(ascent)).Y - height
|
|
pen.Rectangle(rect)
|
|
}
|
|
rect = image.Rectangle {
|
|
Min: roundPt(end.Add(ascent)),
|
|
Max: roundPt(end.Sub(descent)),
|
|
}
|
|
rect.Min.X = bounds.Min.X
|
|
pen.Rectangle(rect)
|
|
}
|
|
|
|
pen.Stroke(textColor)
|
|
pen.StrokeWeight(1)
|
|
beamTop := roundPt(canonEnd.Add(ascent)).Sub(image.Pt(0, 1))
|
|
beamBottom := roundPt(canonEnd.Sub(descent))
|
|
beamSerif := 3
|
|
pen.Path(beamTop, beamBottom)
|
|
pen.Path (
|
|
beamTop.Sub(image.Pt(beamSerif - 1, 0)),
|
|
beamTop.Add(image.Pt(beamSerif, 0)))
|
|
pen.Path (
|
|
beamBottom.Sub(image.Pt(beamSerif - 1, 0)),
|
|
beamBottom.Add(image.Pt(beamSerif, 0)))
|
|
}
|
|
|
|
func (this *textBox) textOffset () image.Point {
|
|
return this.InnerBounds().Min.
|
|
Add(this.scroll).
|
|
Sub(this.drawer.LayoutBoundsSpace().Min)
|
|
}
|
|
|
|
func (this *textBox) handleFocusEnter () {
|
|
this.invalidateDraw()
|
|
this.box.handleFocusEnter()
|
|
}
|
|
|
|
func (this *textBox) handleFocusLeave () {
|
|
this.invalidateDraw()
|
|
this.box.handleFocusLeave()
|
|
}
|
|
|
|
func (this *textBox) handleMouseDown (button input.Button) bool {
|
|
if this.mouseButtonCanDrag(button) {
|
|
index := this.runeUnderMouse()
|
|
this.selectStart = index
|
|
this.selecting = true
|
|
this.userSelect(text.Dot { Start: this.selectStart, End: index })
|
|
}
|
|
return this.box.handleMouseDown(button)
|
|
}
|
|
|
|
func (this *textBox) handleMouseUp (button input.Button) bool {
|
|
if this.mouseButtonCanDrag(button) && this.selecting {
|
|
index := this.runeUnderMouse()
|
|
this.selecting = false
|
|
this.userSelect(text.Dot { Start: this.selectStart, End: index })
|
|
}
|
|
return this.box.handleMouseUp(button)
|
|
}
|
|
|
|
func (this *textBox) mouseButtonCanDrag (button input.Button) bool {
|
|
return button == input.ButtonLeft ||
|
|
button == input.ButtonMiddle ||
|
|
button == input.ButtonRight
|
|
}
|
|
|
|
func (this *textBox) handleMouseMove () bool {
|
|
if this.selecting {
|
|
index := this.runeUnderMouse()
|
|
this.userSelect(text.Dot { Start: this.selectStart, End: index })
|
|
}
|
|
return this.box.handleMouseMove()
|
|
}
|
|
|
|
func (this *textBox) runeUnderMouse () int {
|
|
window := this.Window()
|
|
if window == nil { return 0 }
|
|
position := window.MousePosition().Sub(this.textOffset())
|
|
return this.drawer.AtPosition(fixPt(position))
|
|
}
|
|
|
|
// TODO the keynav here should make better use of input key chords.
|
|
|
|
func (this *textBox) handleKeyDown (key input.Key, numberPad bool) bool {
|
|
if this.box.handleKeyDown(key, numberPad) { return true }
|
|
if !this.selectable { return false }
|
|
|
|
// because fuck you thats why!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
modifiers := this.Window().Modifiers()
|
|
dot := this.Dot()
|
|
sel := modifiers.Shift()
|
|
word := modifiers.Control()
|
|
|
|
moveVertically := func (delta fixed.Int26_6) {
|
|
currentDot := 0
|
|
if sel {
|
|
currentDot = dot.End
|
|
} else {
|
|
currentDot = dot.Canon().Start
|
|
if delta > fixed.I(0) { currentDot = dot.Canon().End }
|
|
}
|
|
|
|
nextDot := 0
|
|
if word {
|
|
if delta > fixed.I(0) {
|
|
nextDot = nextParagraph(this.runes, currentDot)
|
|
} else {
|
|
nextDot = previousParagraph(this.runes, currentDot)
|
|
}
|
|
} else {
|
|
currentPosition := this.drawer.PositionAt(currentDot)
|
|
if this.desiredX != fixed.I(0) {
|
|
currentPosition.X = this.desiredX
|
|
}
|
|
nextPosition := currentPosition
|
|
nextPosition.Y += this.lineHeight.Value().Mul(delta)
|
|
this.desiredX = nextPosition.X
|
|
nextDot = this.drawer.AtPosition(nextPosition)
|
|
}
|
|
|
|
if sel {
|
|
dot.End = nextDot
|
|
this.userSelectWithoutResettingDesiredX(dot)
|
|
} else {
|
|
this.userSelectWithoutResettingDesiredX(text.EmptyDot(nextDot))
|
|
}
|
|
}
|
|
|
|
switch {
|
|
case key == input.KeyHome || (modifiers.Alt() && key == input.KeyLeft):
|
|
if word {
|
|
dot.End = 0
|
|
} else {
|
|
dot.End = lineHomeSoft(this.runes, dot.End)
|
|
}
|
|
if !sel { dot.Start = dot.End }
|
|
this.userSelect(dot)
|
|
return true
|
|
case key == input.KeyEnd || (modifiers.Alt() && key == input.KeyRight):
|
|
if word {
|
|
dot.End = len(this.runes)
|
|
} else {
|
|
dot.End = lineEnd(this.runes, dot.End)
|
|
}
|
|
if !sel { dot.Start = dot.End }
|
|
this.userSelect(dot)
|
|
return true
|
|
case key == input.KeyLeft:
|
|
if sel {
|
|
this.userSelect(text.SelectLeft(this.runes, dot, word))
|
|
} else {
|
|
this.userSelect(text.MoveLeft(this.runes, dot, word))
|
|
}
|
|
return true
|
|
case key == input.KeyRight:
|
|
if sel {
|
|
this.userSelect(text.SelectRight(this.runes, dot, word))
|
|
} else {
|
|
this.userSelect(text.MoveRight(this.runes, dot, word))
|
|
}
|
|
return true
|
|
case key == input.KeyUp:
|
|
moveVertically(fixed.I(-1))
|
|
return true
|
|
case key == input.KeyDown:
|
|
moveVertically(fixed.I(1))
|
|
return true
|
|
case key == input.Key('a') && modifiers.Control():
|
|
dot.Start = 0
|
|
dot.End = len(this.runes)
|
|
this.userSelect(dot)
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func (this *textBox) handleKeyUp (key input.Key, numberPad bool) bool {
|
|
if this.box.handleKeyUp(key, numberPad) { return true }
|
|
if !this.selectable { return false }
|
|
|
|
modifiers := this.Window().Modifiers()
|
|
switch {
|
|
case key == input.KeyHome || (modifiers.Alt() && key == input.KeyLeft):
|
|
return true
|
|
case key == input.KeyEnd || (modifiers.Alt() && key == input.KeyRight):
|
|
return true
|
|
case key == input.KeyUp:
|
|
return true
|
|
case key == input.KeyDown:
|
|
return true
|
|
case key == input.KeyLeft:
|
|
return true
|
|
case key == input.KeyRight:
|
|
return true
|
|
case key == input.Key('a') && modifiers.Control():
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func (this *textBox) normalizedLayoutBoundsSpace () image.Rectangle {
|
|
bounds := this.drawer.LayoutBoundsSpace()
|
|
return bounds.Sub(bounds.Min)
|
|
}
|
|
|
|
func (this *textBox) contentMinimum () image.Point {
|
|
minimum := this.drawer.MinimumSize()
|
|
|
|
if this.attrOverflow.Value().X || bool(this.attrWrap.Value()) {
|
|
minimum.X = this.drawer.Em().Round()
|
|
}
|
|
if this.attrOverflow.Value().Y {
|
|
minimum.Y = this.drawer.LineHeight().Round()
|
|
}
|
|
|
|
return minimum.Add(this.box.contentMinimum())
|
|
}
|
|
|
|
func (this *textBox) doLayout () {
|
|
this.box.doLayout()
|
|
previousContentBounds := this.contentBounds
|
|
|
|
innerBounds := this.InnerBounds()
|
|
this.drawer.SetWidth(innerBounds.Dx())
|
|
this.drawer.SetHeight(innerBounds.Dy())
|
|
|
|
this.contentBounds = this.normalizedLayoutBoundsSpace()
|
|
this.constrainScroll()
|
|
this.contentBounds = this.contentBounds.Add(this.scroll)
|
|
// println(this.InnerBounds().String(), this.contentBounds.String())
|
|
|
|
if previousContentBounds != this.contentBounds {
|
|
this.on.contentBoundsChange.Broadcast()
|
|
}
|
|
}
|
|
|
|
func (this *textBox) constrainScroll () {
|
|
innerBounds := this.InnerBounds()
|
|
width := this.contentBounds.Dx()
|
|
height := this.contentBounds.Dy()
|
|
|
|
// X
|
|
if width <= innerBounds.Dx() {
|
|
this.scroll.X = 0
|
|
} else if this.scroll.X > 0 {
|
|
this.scroll.X = 0
|
|
} else if this.scroll.X < innerBounds.Dx() - width {
|
|
this.scroll.X = innerBounds.Dx() - width
|
|
}
|
|
|
|
// Y
|
|
if height <= innerBounds.Dy() {
|
|
this.scroll.Y = 0
|
|
} else if this.scroll.Y > 0 {
|
|
this.scroll.Y = 0
|
|
} else if this.scroll.Y < innerBounds.Dy() - height {
|
|
this.scroll.Y = innerBounds.Dy() - height
|
|
}
|
|
}
|
|
|
|
func (this *textBox) scrollToDot () {
|
|
dot := roundPt(this.drawer.PositionAt(this.dot.End)).Add(this.textOffset())
|
|
innerBounds := this.InnerBounds()
|
|
scroll := this.scroll
|
|
em := this.drawer.Em().Round()
|
|
lineHeight := this.drawer.LineHeight().Round()
|
|
|
|
// X
|
|
if dot.X < innerBounds.Min.X + em {
|
|
scroll.X += innerBounds.Min.X - dot.X + em
|
|
} else if dot.X > innerBounds.Max.X - em {
|
|
scroll.X -= dot.X - innerBounds.Max.X + em
|
|
}
|
|
|
|
// Y
|
|
if dot.Y < innerBounds.Min.Y + lineHeight {
|
|
scroll.Y += innerBounds.Min.Y - dot.Y + lineHeight
|
|
} else if dot.Y > innerBounds.Max.Y - lineHeight {
|
|
scroll.Y -= dot.Y - innerBounds.Max.Y + lineHeight
|
|
}
|
|
|
|
this.ScrollTo(scroll)
|
|
}
|
|
|
|
func (this *textBox) handleFaceChange () {
|
|
hierarchy := this.getHierarchy()
|
|
if hierarchy == nil { return }
|
|
faceSet := hierarchy.getFaceSet()
|
|
if faceSet == nil { return }
|
|
|
|
face := faceSet.Face(tomo.Face(this.attrFace.Value()))
|
|
this.face.Set(face, face)
|
|
this.drawer.SetFace(face)
|
|
this.invalidateMinimum()
|
|
this.invalidateLayout()
|
|
this.lineHeight.Invalidate()
|
|
}
|
|
|
|
func (this *textBox) recursiveReApply () {
|
|
this.box.recursiveReApply()
|
|
|
|
hierarchy := this.getHierarchy()
|
|
if hierarchy == nil { return }
|
|
|
|
previousFace := this.face.Value()
|
|
if previousFace == nil {
|
|
faceSet := hierarchy.getFaceSet()
|
|
if faceSet == nil { return }
|
|
face := faceSet.Face(tomo.Face(this.attrFace.Value()))
|
|
if face != previousFace {
|
|
this.face.Set(face, face)
|
|
this.drawer.SetFace(face)
|
|
this.invalidateMinimum()
|
|
this.invalidateLayout()
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO: these two functions really could be better.
|
|
|
|
func previousParagraph (text []rune, index int) int {
|
|
consecLF := 0
|
|
if index >= len(text) { index = len(text) - 1 }
|
|
for ; index > 0; index -- {
|
|
char := text[index]
|
|
if char == '\n' {
|
|
consecLF ++
|
|
} else if !unicode.IsSpace(char) {
|
|
if consecLF >= 2 { return index + 1 }
|
|
consecLF = 0
|
|
}
|
|
}
|
|
return index
|
|
}
|
|
|
|
func nextParagraph (text []rune, index int) int {
|
|
consecLF := 0
|
|
for ; index < len(text); index ++ {
|
|
char := text[index]
|
|
if char == '\n' {
|
|
consecLF ++
|
|
} else if !unicode.IsSpace(char) {
|
|
if consecLF >= 2 { return index }
|
|
consecLF = 0
|
|
}
|
|
}
|
|
return index
|
|
}
|
|
|
|
func lineHome (text []rune, index int) int {
|
|
liminal := index < len(text) && text[index] == '\n'
|
|
if index >= len(text) { index = len(text) - 1 }
|
|
for index := index; index >= 0; index -- {
|
|
char := text[index]
|
|
if char == '\n' && !liminal {
|
|
return index + 1
|
|
}
|
|
liminal = false
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func lineHomeSoft (text []rune, index int) int {
|
|
home := lineHome(text, index)
|
|
start := home
|
|
for start < len(text) && unicode.IsSpace(text[start]) {
|
|
start ++
|
|
}
|
|
if index == start {
|
|
return home
|
|
} else {
|
|
return start
|
|
}
|
|
}
|
|
|
|
func lineEnd (text []rune, index int) int {
|
|
for ; index < len(text); index ++ {
|
|
char := text[index]
|
|
if char == '\n' {
|
|
return index
|
|
}
|
|
}
|
|
return index
|
|
}
|