mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-07 02:15:34 +00:00
@@ -0,0 +1,155 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
package text
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
const bufferDebug = false
|
||||
|
||||
// editBuffer implements a gap buffer for text editing.
|
||||
type editBuffer struct {
|
||||
// caret is the caret position in bytes.
|
||||
caret int
|
||||
// pos is the byte position for Read and ReadRune.
|
||||
pos int
|
||||
|
||||
// The gap start and end in bytes.
|
||||
gapstart, gapend int
|
||||
text []byte
|
||||
}
|
||||
|
||||
const minSpace = 5
|
||||
|
||||
func (e *editBuffer) deleteRuneForward() {
|
||||
e.moveGap(0)
|
||||
_, s := utf8.DecodeRune(e.text[e.gapend:])
|
||||
e.gapend += s
|
||||
e.dump()
|
||||
}
|
||||
|
||||
func (e *editBuffer) deleteRune() {
|
||||
e.moveGap(0)
|
||||
_, s := utf8.DecodeLastRune(e.text[:e.gapstart])
|
||||
e.gapstart -= s
|
||||
e.caret -= s
|
||||
e.dump()
|
||||
}
|
||||
|
||||
// moveGap moves the gap to the caret position. After returning,
|
||||
// the gap is guaranteed to be at least space bytes long.
|
||||
func (e *editBuffer) moveGap(space int) {
|
||||
if e.gapLen() < space {
|
||||
if space < minSpace {
|
||||
space = minSpace
|
||||
}
|
||||
txt := make([]byte, e.len()+space)
|
||||
// Expand to capacity.
|
||||
txt = txt[:cap(txt)]
|
||||
gaplen := len(txt) - e.len()
|
||||
if e.caret > e.gapstart {
|
||||
copy(txt, e.text[:e.gapstart])
|
||||
copy(txt[e.caret+gaplen:], e.text[e.caret:])
|
||||
copy(txt[e.gapstart:], e.text[e.gapend:e.caret+e.gapLen()])
|
||||
} else {
|
||||
copy(txt, e.text[:e.caret])
|
||||
copy(txt[e.gapstart+gaplen:], e.text[e.gapend:])
|
||||
copy(txt[e.caret+gaplen:], e.text[e.caret:e.gapstart])
|
||||
}
|
||||
e.text = txt
|
||||
e.gapstart = e.caret
|
||||
e.gapend = e.gapstart + gaplen
|
||||
} else {
|
||||
if e.caret > e.gapstart {
|
||||
copy(e.text[e.gapstart:], e.text[e.gapend:e.caret+e.gapLen()])
|
||||
} else {
|
||||
copy(e.text[e.caret+e.gapLen():], e.text[e.caret:e.gapstart])
|
||||
}
|
||||
l := e.gapLen()
|
||||
e.gapstart = e.caret
|
||||
e.gapend = e.gapstart + l
|
||||
}
|
||||
e.dump()
|
||||
}
|
||||
|
||||
func (e *editBuffer) len() int {
|
||||
return len(e.text) - e.gapLen()
|
||||
}
|
||||
|
||||
func (e *editBuffer) gapLen() int {
|
||||
return e.gapend - e.gapstart
|
||||
}
|
||||
|
||||
func (e *editBuffer) Read(p []byte) (int, error) {
|
||||
if e.pos == e.len() {
|
||||
return 0, io.EOF
|
||||
}
|
||||
var n int
|
||||
if e.pos < e.gapstart {
|
||||
n += copy(p, e.text[e.pos:e.gapstart])
|
||||
p = p[n:]
|
||||
}
|
||||
n += copy(p, e.text[e.gapend:])
|
||||
e.pos += n
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (e *editBuffer) ReadRune() (rune, int, error) {
|
||||
if e.pos == e.len() {
|
||||
return 0, 0, io.EOF
|
||||
}
|
||||
r, s := e.runeAt(e.pos)
|
||||
e.pos += s
|
||||
return r, s, nil
|
||||
}
|
||||
|
||||
func (e *editBuffer) String() string {
|
||||
var b strings.Builder
|
||||
b.Grow(e.len())
|
||||
b.Write(e.text[:e.gapstart])
|
||||
b.Write(e.text[e.gapend:])
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (e *editBuffer) prepend(s string) {
|
||||
e.moveGap(len(s))
|
||||
copy(e.text[e.caret:], s)
|
||||
e.gapstart += len(s)
|
||||
e.dump()
|
||||
}
|
||||
|
||||
func (e *editBuffer) dump() {
|
||||
if bufferDebug {
|
||||
fmt.Printf("len(e.text) %d e.len() %d e.gapstart %d e.gapend %d e.caret %d txt:\n'%+x'<-%d->'%+x'\n", len(e.text), e.len(), e.gapstart, e.gapend, e.caret, e.text[:e.gapstart], e.gapLen(), e.text[e.gapend:])
|
||||
}
|
||||
}
|
||||
|
||||
func (e *editBuffer) moveLeft() {
|
||||
_, s := e.runeBefore(e.caret)
|
||||
e.caret -= s
|
||||
e.dump()
|
||||
}
|
||||
|
||||
func (e *editBuffer) moveRight() {
|
||||
_, s := e.runeAt(e.caret)
|
||||
e.caret += s
|
||||
e.dump()
|
||||
}
|
||||
|
||||
func (e *editBuffer) runeBefore(idx int) (rune, int) {
|
||||
if idx >= e.gapstart {
|
||||
idx += e.gapLen()
|
||||
}
|
||||
return utf8.DecodeLastRune(e.text[:idx])
|
||||
}
|
||||
|
||||
func (e *editBuffer) runeAt(idx int) (rune, int) {
|
||||
if idx >= e.gapstart {
|
||||
idx += e.gapLen()
|
||||
}
|
||||
return utf8.DecodeRune(e.text[idx:])
|
||||
}
|
||||
@@ -0,0 +1,536 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
package text
|
||||
|
||||
import (
|
||||
"image"
|
||||
"math"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"gioui.org/ui/draw"
|
||||
"gioui.org/ui/f32"
|
||||
"gioui.org/ui/gesture"
|
||||
"gioui.org/ui/key"
|
||||
"gioui.org/ui/layout"
|
||||
"gioui.org/ui/pointer"
|
||||
"gioui.org/ui"
|
||||
|
||||
"golang.org/x/image/math/fixed"
|
||||
)
|
||||
|
||||
type Editor struct {
|
||||
Src image.Image
|
||||
Face Face
|
||||
Alignment Alignment
|
||||
SingleLine bool
|
||||
|
||||
cfg *ui.Config
|
||||
blinkStart time.Time
|
||||
focused bool
|
||||
rr editBuffer
|
||||
maxWidth int
|
||||
viewSize image.Point
|
||||
valid bool
|
||||
lines []Line
|
||||
dims layout.Dimens
|
||||
padTop, padBottom int
|
||||
padLeft, padRight int
|
||||
requestFocus bool
|
||||
|
||||
it lineIterator
|
||||
|
||||
// carXOff is the offset to the current caret
|
||||
// position when moving between lines.
|
||||
carXOff fixed.Int26_6
|
||||
|
||||
scroller gesture.Scroll
|
||||
scrollOff image.Point
|
||||
|
||||
clicker gesture.Click
|
||||
|
||||
ops ui.Ops
|
||||
}
|
||||
|
||||
type linePath struct {
|
||||
path *draw.Path
|
||||
off f32.Point
|
||||
}
|
||||
|
||||
const (
|
||||
blinksPerSecond = 1
|
||||
maxBlinkDuration = 10 * time.Second
|
||||
)
|
||||
|
||||
func (e *Editor) Update(c *ui.Config, pq pointer.Events, kq key.Events) {
|
||||
e.cfg = c
|
||||
sbounds := e.scrollBounds()
|
||||
var smin, smax int
|
||||
var axis gesture.Axis
|
||||
if e.SingleLine {
|
||||
axis = gesture.Horizontal
|
||||
smin, smax = sbounds.Min.X, sbounds.Max.X
|
||||
} else {
|
||||
axis = gesture.Vertical
|
||||
smin, smax = sbounds.Min.Y, sbounds.Max.Y
|
||||
}
|
||||
sdist := e.scroller.Scroll(c, pq, axis)
|
||||
var soff int
|
||||
if e.SingleLine {
|
||||
e.scrollOff.X += sdist
|
||||
soff = e.scrollOff.X
|
||||
} else {
|
||||
e.scrollOff.Y += sdist
|
||||
soff = e.scrollOff.Y
|
||||
}
|
||||
scrollTo := false
|
||||
for _, evt := range e.clicker.Update(pq) {
|
||||
switch evt.Type {
|
||||
case gesture.TypePress:
|
||||
scrollTo = true
|
||||
e.blinkStart = c.Now
|
||||
e.moveCoord(image.Point{
|
||||
X: int(math.Round(float64(evt.Position.X))),
|
||||
Y: int(math.Round(float64(evt.Position.Y))),
|
||||
})
|
||||
e.requestFocus = true
|
||||
}
|
||||
}
|
||||
stop := (sdist > 0 && soff >= smax) || (sdist < 0 && soff <= smin)
|
||||
for _, ke := range kq.For(e) {
|
||||
e.blinkStart = c.Now
|
||||
switch ke := ke.(type) {
|
||||
case key.Focus:
|
||||
e.focused = ke.Focus
|
||||
case key.Chord:
|
||||
if e.Command(ke) {
|
||||
stop = true
|
||||
scrollTo = true
|
||||
}
|
||||
case key.Edit:
|
||||
stop = true
|
||||
scrollTo = true
|
||||
e.append(ke.Text)
|
||||
}
|
||||
}
|
||||
if sdist == 0 && scrollTo {
|
||||
e.scrollToCaret()
|
||||
}
|
||||
if stop {
|
||||
e.scroller.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Editor) caretWidth() fixed.Int26_6 {
|
||||
oneDp := int(e.cfg.Pixels(ui.Dp(1)) + .5)
|
||||
return fixed.Int26_6(oneDp * 64)
|
||||
}
|
||||
|
||||
func (e *Editor) Layout(cs layout.Constraints) (ui.Op, layout.Dimens) {
|
||||
twoDp := int(e.cfg.Pixels(ui.Dp(2)) + 0.5)
|
||||
e.padLeft, e.padRight = twoDp, twoDp
|
||||
maxWidth := cs.Width.Max
|
||||
if e.SingleLine {
|
||||
maxWidth = ui.Inf
|
||||
}
|
||||
if maxWidth != ui.Inf {
|
||||
maxWidth -= e.padLeft + e.padRight
|
||||
}
|
||||
if maxWidth != e.maxWidth {
|
||||
e.maxWidth = maxWidth
|
||||
e.valid = false
|
||||
}
|
||||
|
||||
e.layout()
|
||||
lines, size := e.lines, e.dims.Size
|
||||
e.viewSize = cs.Constrain(size)
|
||||
|
||||
carLine, _, carX, carY := e.layoutCaret()
|
||||
|
||||
off := image.Point{
|
||||
X: -e.scrollOff.X + e.padLeft,
|
||||
Y: -e.scrollOff.Y + e.padTop,
|
||||
}
|
||||
clip := image.Rectangle{
|
||||
Min: image.Point{X: 0, Y: 0},
|
||||
Max: image.Point{X: e.viewSize.X, Y: e.viewSize.Y},
|
||||
}
|
||||
e.ops = e.ops[:0]
|
||||
e.ops = append(e.ops, key.OpHandler{Key: e, Focus: e.requestFocus})
|
||||
e.requestFocus = false
|
||||
e.it = lineIterator{
|
||||
Lines: lines,
|
||||
Clip: clip,
|
||||
Alignment: e.Alignment,
|
||||
Width: e.viewWidth(),
|
||||
Offset: off,
|
||||
}
|
||||
for {
|
||||
str, lineOff, ok := e.it.Next()
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
path := e.Face.Path(str)
|
||||
e.ops = append(e.ops, ui.OpTransform{
|
||||
Transform: ui.Offset(lineOff),
|
||||
Op: draw.OpClip{Path: path, Op: draw.OpImage{Rect: toRectF(clip).Sub(lineOff), Src: e.Src, SrcRect: e.Src.Bounds()}},
|
||||
})
|
||||
}
|
||||
if e.focused {
|
||||
now := e.cfg.Now
|
||||
dt := now.Sub(e.blinkStart)
|
||||
blinking := dt < maxBlinkDuration
|
||||
const timePerBlink = time.Second / blinksPerSecond
|
||||
nextBlink := now.Add(timePerBlink/2 - dt%(timePerBlink/2))
|
||||
on := !blinking || dt%timePerBlink < timePerBlink/2
|
||||
if on {
|
||||
carWidth := e.caretWidth()
|
||||
carX -= carWidth / 2
|
||||
carAsc, carDesc := -lines[carLine].Bounds.Min.Y, lines[carLine].Bounds.Max.Y
|
||||
carRect := image.Rectangle{
|
||||
Min: image.Point{X: carX.Ceil(), Y: carY - carAsc.Ceil()},
|
||||
Max: image.Point{X: carX.Ceil() + carWidth.Ceil(), Y: carY + carDesc.Ceil()},
|
||||
}
|
||||
carRect = carRect.Add(image.Point{
|
||||
X: -e.scrollOff.X + e.padLeft,
|
||||
Y: -e.scrollOff.Y + e.padTop,
|
||||
})
|
||||
carRect = clip.Intersect(carRect)
|
||||
if !carRect.Empty() {
|
||||
e.ops = append(e.ops, draw.OpImage{Src: e.Src, Rect: toRectF(carRect), SrcRect: e.Src.Bounds()})
|
||||
}
|
||||
}
|
||||
if blinking {
|
||||
e.ops = append(e.ops, ui.OpRedraw{At: nextBlink})
|
||||
}
|
||||
}
|
||||
|
||||
baseline := e.padTop + e.dims.Baseline
|
||||
area := gesture.Rect(e.viewSize)
|
||||
e.ops = append(e.ops, e.scroller.Op(area), e.clicker.Op(area))
|
||||
return e.ops, layout.Dimens{Size: e.viewSize, Baseline: baseline}
|
||||
}
|
||||
|
||||
func (e *Editor) layout() {
|
||||
e.adjustScroll()
|
||||
if e.valid {
|
||||
return
|
||||
}
|
||||
e.layoutText()
|
||||
e.valid = true
|
||||
}
|
||||
|
||||
func (e *Editor) scrollBounds() image.Rectangle {
|
||||
var b image.Rectangle
|
||||
if e.SingleLine {
|
||||
if len(e.lines) > 0 {
|
||||
b.Min.X = align(e.Alignment, e.lines[0].Width, e.viewWidth()).Floor()
|
||||
if b.Min.X > 0 {
|
||||
b.Min.X = 0
|
||||
}
|
||||
}
|
||||
b.Max.X = e.dims.Size.X + b.Min.X - e.viewSize.X
|
||||
} else {
|
||||
b.Max.Y = e.dims.Size.Y - e.viewSize.Y
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func (e *Editor) adjustScroll() {
|
||||
b := e.scrollBounds()
|
||||
if e.scrollOff.X > b.Max.X {
|
||||
e.scrollOff.X = b.Max.X
|
||||
}
|
||||
if e.scrollOff.X < b.Min.X {
|
||||
e.scrollOff.X = b.Min.X
|
||||
}
|
||||
if e.scrollOff.Y > b.Max.Y {
|
||||
e.scrollOff.Y = b.Max.Y
|
||||
}
|
||||
if e.scrollOff.Y < b.Min.Y {
|
||||
e.scrollOff.Y = b.Min.Y
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Editor) moveCoord(pos image.Point) {
|
||||
e.layout()
|
||||
var (
|
||||
prevDesc fixed.Int26_6
|
||||
carLine int
|
||||
y int
|
||||
)
|
||||
for _, l := range e.lines {
|
||||
y += (prevDesc + l.Ascent).Ceil()
|
||||
prevDesc = l.Descent
|
||||
if y+prevDesc.Ceil() >= pos.Y+e.scrollOff.Y-e.padTop {
|
||||
break
|
||||
}
|
||||
carLine++
|
||||
}
|
||||
x := fixed.I(pos.X + e.scrollOff.X - e.padLeft)
|
||||
e.moveToLine(x, carLine)
|
||||
}
|
||||
|
||||
func (e *Editor) layoutText() {
|
||||
textLayout := e.Face.Layout(e.rr.String(), e.SingleLine, e.maxWidth)
|
||||
lines := textLayout.Lines
|
||||
dims := linesDimens(lines)
|
||||
for i := 0; i < len(lines)-1; i++ {
|
||||
s := lines[i].Text.String
|
||||
// To avoid layout flickering while editing, assume a soft newline takes
|
||||
// up all available space.
|
||||
if len(s) > 0 {
|
||||
r, _ := utf8.DecodeLastRuneInString(s)
|
||||
if !IsNewline(r) {
|
||||
dims.Size.X = e.maxWidth
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
padTop, padBottom := textPadding(lines)
|
||||
dims.Size.Y += padTop + padBottom
|
||||
dims.Size.X += e.padLeft + e.padRight
|
||||
e.padTop = padTop
|
||||
e.padBottom = padBottom
|
||||
e.lines, e.dims = lines, dims
|
||||
}
|
||||
|
||||
func (e *Editor) viewWidth() int {
|
||||
return e.viewSize.X - e.padLeft - e.padRight
|
||||
}
|
||||
|
||||
func (e *Editor) layoutCaret() (carLine, carCol int, x fixed.Int26_6, y int) {
|
||||
e.layout()
|
||||
var idx int
|
||||
var prevDesc fixed.Int26_6
|
||||
loop:
|
||||
for carLine = 0; carLine < len(e.lines); carLine++ {
|
||||
l := e.lines[carLine]
|
||||
y += (prevDesc + l.Ascent).Ceil()
|
||||
prevDesc = l.Descent
|
||||
if carLine == len(e.lines)-1 || idx+len(l.Text.String) > e.rr.caret {
|
||||
str := l.Text.String
|
||||
for _, adv := range l.Text.Advances {
|
||||
if idx == e.rr.caret {
|
||||
break loop
|
||||
}
|
||||
x += adv
|
||||
_, s := utf8.DecodeRuneInString(str)
|
||||
idx += s
|
||||
str = str[s:]
|
||||
carCol++
|
||||
}
|
||||
break
|
||||
}
|
||||
idx += len(l.Text.String)
|
||||
}
|
||||
x += align(e.Alignment, e.lines[carLine].Width, e.viewWidth())
|
||||
return
|
||||
}
|
||||
|
||||
func (e *Editor) invalidate() {
|
||||
e.valid = false
|
||||
}
|
||||
|
||||
func (e *Editor) deleteRune() {
|
||||
e.rr.deleteRune()
|
||||
e.carXOff = 0
|
||||
e.invalidate()
|
||||
}
|
||||
|
||||
func (e *Editor) deleteRuneForward() {
|
||||
e.rr.deleteRuneForward()
|
||||
e.carXOff = 0
|
||||
e.invalidate()
|
||||
}
|
||||
|
||||
func (e *Editor) SetText(s string) {
|
||||
e.rr = editBuffer{}
|
||||
e.prepend(s)
|
||||
}
|
||||
|
||||
func (e *Editor) append(s string) {
|
||||
e.prepend(s)
|
||||
e.rr.caret += len(s)
|
||||
}
|
||||
|
||||
func (e *Editor) prepend(s string) {
|
||||
e.rr.prepend(s)
|
||||
e.carXOff = 0
|
||||
e.invalidate()
|
||||
}
|
||||
|
||||
func (e *Editor) movePages(pages int) {
|
||||
e.layout()
|
||||
_, _, carX, carY := e.layoutCaret()
|
||||
y := carY + pages*e.viewSize.Y
|
||||
var (
|
||||
prevDesc fixed.Int26_6
|
||||
carLine2 int
|
||||
)
|
||||
y2 := e.lines[0].Ascent.Ceil()
|
||||
for i := 1; i < len(e.lines); i++ {
|
||||
if y2 >= y {
|
||||
break
|
||||
}
|
||||
l := e.lines[i]
|
||||
h := (prevDesc + l.Ascent).Ceil()
|
||||
prevDesc = l.Descent
|
||||
if y2+h-y >= y-y2 {
|
||||
break
|
||||
}
|
||||
y2 += h
|
||||
carLine2++
|
||||
}
|
||||
e.carXOff = e.moveToLine(carX+e.carXOff, carLine2)
|
||||
}
|
||||
|
||||
func (e *Editor) moveToLine(carX fixed.Int26_6, carLine2 int) fixed.Int26_6 {
|
||||
e.layout()
|
||||
carLine, carCol, _, _ := e.layoutCaret()
|
||||
if carLine2 < 0 {
|
||||
carLine2 = 0
|
||||
}
|
||||
if carLine2 >= len(e.lines) {
|
||||
carLine2 = len(e.lines) - 1
|
||||
}
|
||||
// Move to start of line.
|
||||
for i := carCol - 1; i >= 0; i-- {
|
||||
_, s := e.rr.runeBefore(e.rr.caret)
|
||||
e.rr.caret -= s
|
||||
}
|
||||
if carLine2 != carLine {
|
||||
// Move to start of line2.
|
||||
if carLine2 > carLine {
|
||||
for i := carLine; i < carLine2; i++ {
|
||||
e.rr.caret += len(e.lines[i].Text.String)
|
||||
}
|
||||
} else {
|
||||
for i := carLine - 1; i >= carLine2; i-- {
|
||||
e.rr.caret -= len(e.lines[i].Text.String)
|
||||
}
|
||||
}
|
||||
}
|
||||
l2 := e.lines[carLine2]
|
||||
carX2 := align(e.Alignment, l2.Width, e.viewWidth())
|
||||
// Only move past the end of the last line
|
||||
end := 0
|
||||
if carLine2 < len(e.lines)-1 {
|
||||
end = 1
|
||||
}
|
||||
// Move to rune closest to previous horizontal position.
|
||||
for i := 0; i < len(l2.Text.Advances)-end; i++ {
|
||||
adv := l2.Text.Advances[i]
|
||||
if carX2 >= carX {
|
||||
break
|
||||
}
|
||||
if carX2+adv-carX >= carX-carX2 {
|
||||
break
|
||||
}
|
||||
carX2 += adv
|
||||
_, s := e.rr.runeAt(e.rr.caret)
|
||||
e.rr.caret += s
|
||||
}
|
||||
return carX - carX2
|
||||
}
|
||||
|
||||
func (e *Editor) moveLeft() {
|
||||
e.rr.moveLeft()
|
||||
e.carXOff = 0
|
||||
}
|
||||
|
||||
func (e *Editor) moveRight() {
|
||||
e.rr.moveRight()
|
||||
e.carXOff = 0
|
||||
}
|
||||
|
||||
func (e *Editor) moveStart() {
|
||||
carLine, carCol, x, _ := e.layoutCaret()
|
||||
advances := e.lines[carLine].Text.Advances
|
||||
for i := carCol - 1; i >= 0; i-- {
|
||||
_, s := e.rr.runeBefore(e.rr.caret)
|
||||
e.rr.caret -= s
|
||||
x -= advances[i]
|
||||
}
|
||||
e.carXOff = -x
|
||||
}
|
||||
|
||||
func (e *Editor) moveEnd() {
|
||||
carLine, carCol, x, _ := e.layoutCaret()
|
||||
l := e.lines[carLine]
|
||||
// Only move past the end of the last line
|
||||
end := 0
|
||||
if carLine < len(e.lines)-1 {
|
||||
end = 1
|
||||
}
|
||||
for i := carCol; i < len(l.Text.Advances)-end; i++ {
|
||||
adv := l.Text.Advances[i]
|
||||
_, s := e.rr.runeAt(e.rr.caret)
|
||||
e.rr.caret += s
|
||||
x += adv
|
||||
}
|
||||
a := align(e.Alignment, l.Width, e.viewWidth())
|
||||
e.carXOff = l.Width + a - x
|
||||
}
|
||||
|
||||
func (e *Editor) scrollToCaret() {
|
||||
carWidth := e.caretWidth()
|
||||
carLine, _, x, y := e.layoutCaret()
|
||||
l := e.lines[carLine]
|
||||
if e.SingleLine {
|
||||
minx := (x - carWidth/2).Ceil()
|
||||
if d := minx - e.scrollOff.X + e.padLeft; d < 0 {
|
||||
e.scrollOff.X += d
|
||||
}
|
||||
maxx := (x + carWidth/2).Ceil()
|
||||
if d := maxx - (e.scrollOff.X + e.viewSize.X - e.padRight); d > 0 {
|
||||
e.scrollOff.X += d
|
||||
}
|
||||
} else {
|
||||
miny := y + l.Bounds.Min.Y.Floor()
|
||||
if d := miny - e.scrollOff.Y + e.padTop; d < 0 {
|
||||
e.scrollOff.Y += d
|
||||
}
|
||||
maxy := y + l.Bounds.Max.Y.Ceil()
|
||||
if d := maxy - (e.scrollOff.Y + e.viewSize.Y - e.padBottom); d > 0 {
|
||||
e.scrollOff.Y += d
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Editor) Command(k key.Chord) bool {
|
||||
if !e.focused {
|
||||
return false
|
||||
}
|
||||
switch k.Name {
|
||||
case key.NameReturn, key.NameEnter:
|
||||
if !e.SingleLine {
|
||||
e.append("\n")
|
||||
}
|
||||
case key.NameDeleteBackward:
|
||||
e.deleteRune()
|
||||
case key.NameDeleteForward:
|
||||
e.deleteRuneForward()
|
||||
case key.NameUpArrow:
|
||||
line, _, carX, _ := e.layoutCaret()
|
||||
e.carXOff = e.moveToLine(carX+e.carXOff, line-1)
|
||||
case key.NameDownArrow:
|
||||
line, _, carX, _ := e.layoutCaret()
|
||||
e.carXOff = e.moveToLine(carX+e.carXOff, line+1)
|
||||
case key.NameLeftArrow:
|
||||
e.moveLeft()
|
||||
case key.NameRightArrow:
|
||||
e.moveRight()
|
||||
case key.NamePageUp:
|
||||
e.movePages(-1)
|
||||
case key.NamePageDown:
|
||||
e.movePages(+1)
|
||||
case key.NameHome:
|
||||
e.moveStart()
|
||||
case key.NameEnd:
|
||||
e.moveEnd()
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
package text
|
||||
|
||||
import (
|
||||
"image"
|
||||
"math"
|
||||
"unicode/utf8"
|
||||
|
||||
"gioui.org/ui/draw"
|
||||
"gioui.org/ui/f32"
|
||||
"gioui.org/ui/layout"
|
||||
"gioui.org/ui"
|
||||
|
||||
"golang.org/x/image/math/fixed"
|
||||
)
|
||||
|
||||
type Label struct {
|
||||
Face Face
|
||||
Src image.Image
|
||||
Alignment Alignment
|
||||
Text string
|
||||
|
||||
it lineIterator
|
||||
}
|
||||
|
||||
type lineIterator struct {
|
||||
Lines []Line
|
||||
Clip image.Rectangle
|
||||
Alignment Alignment
|
||||
Width int
|
||||
Offset image.Point
|
||||
|
||||
y, prevDesc fixed.Int26_6
|
||||
}
|
||||
|
||||
func (l *lineIterator) Next() (String, f32.Point, bool) {
|
||||
for len(l.Lines) > 0 {
|
||||
line := l.Lines[0]
|
||||
l.Lines = l.Lines[1:]
|
||||
x := align(l.Alignment, line.Width, l.Width) + fixed.I(l.Offset.X)
|
||||
l.y += l.prevDesc + line.Ascent
|
||||
l.prevDesc = line.Descent
|
||||
// Align baseline and line start to the pixel grid.
|
||||
off := fixed.Point26_6{X: fixed.I(x.Floor()), Y: fixed.I(l.y.Ceil())}
|
||||
x, l.y = off.X, off.Y
|
||||
off.Y += fixed.I(l.Offset.Y)
|
||||
if (off.Y + line.Bounds.Min.Y).Floor() > l.Clip.Max.Y {
|
||||
break
|
||||
}
|
||||
if (off.Y + line.Bounds.Max.Y).Ceil() < l.Clip.Min.Y {
|
||||
continue
|
||||
}
|
||||
str := line.Text
|
||||
for len(str.Advances) > 0 {
|
||||
adv := str.Advances[0]
|
||||
if (off.X + adv + line.Bounds.Max.X - line.Width).Ceil() >= l.Clip.Min.X {
|
||||
break
|
||||
}
|
||||
off.X += adv
|
||||
_, s := utf8.DecodeRuneInString(str.String)
|
||||
str.String = str.String[s:]
|
||||
str.Advances = str.Advances[1:]
|
||||
}
|
||||
n := 0
|
||||
endx := off.X
|
||||
for i, adv := range str.Advances {
|
||||
if (endx + line.Bounds.Min.X).Floor() > l.Clip.Max.X {
|
||||
str.String = str.String[:n]
|
||||
str.Advances = str.Advances[:i]
|
||||
break
|
||||
}
|
||||
_, s := utf8.DecodeRuneInString(str.String[n:])
|
||||
n += s
|
||||
endx += adv
|
||||
}
|
||||
offf := f32.Point{X: float32(off.X) / 64, Y: float32(off.Y) / 64}
|
||||
return str, offf, true
|
||||
}
|
||||
return String{}, f32.Point{}, false
|
||||
}
|
||||
|
||||
func (l Label) Layout(cs layout.Constraints) (ui.Op, layout.Dimens) {
|
||||
textLayout := l.Face.Layout(l.Text, false, cs.Width.Max)
|
||||
lines := textLayout.Lines
|
||||
dims := linesDimens(lines)
|
||||
dims.Size = cs.Constrain(dims.Size)
|
||||
padTop, padBottom := textPadding(lines)
|
||||
clip := image.Rectangle{
|
||||
Min: image.Point{X: -ui.Inf, Y: -padTop},
|
||||
Max: image.Point{X: ui.Inf, Y: dims.Size.Y + padBottom},
|
||||
}
|
||||
var ops ui.Ops = make([]ui.Op, len(lines))[:0]
|
||||
l.it = lineIterator{
|
||||
Lines: lines,
|
||||
Clip: clip,
|
||||
Alignment: l.Alignment,
|
||||
Width: dims.Size.X,
|
||||
}
|
||||
for {
|
||||
str, off, ok := l.it.Next()
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
path := l.Face.Path(str)
|
||||
lclip := toRectF(clip).Sub(off)
|
||||
op := ui.OpTransform{
|
||||
Transform: ui.Offset(off),
|
||||
Op: draw.OpClip{Path: path, Op: draw.OpImage{Rect: lclip, Src: l.Src, SrcRect: l.Src.Bounds()}},
|
||||
}
|
||||
ops = append(ops, op)
|
||||
}
|
||||
return ops, dims
|
||||
}
|
||||
|
||||
func itof(i int) float32 {
|
||||
switch i {
|
||||
case ui.Inf:
|
||||
return float32(math.Inf(+1))
|
||||
case -ui.Inf:
|
||||
return float32(math.Inf(-1))
|
||||
default:
|
||||
return float32(i)
|
||||
}
|
||||
}
|
||||
|
||||
func toRectF(r image.Rectangle) f32.Rectangle {
|
||||
return f32.Rectangle{
|
||||
Min: f32.Point{X: itof(r.Min.X), Y: itof(r.Min.Y)},
|
||||
Max: f32.Point{X: itof(r.Max.X), Y: itof(r.Max.Y)},
|
||||
}
|
||||
}
|
||||
|
||||
func textPadding(lines []Line) (padTop int, padBottom int) {
|
||||
if len(lines) > 0 {
|
||||
first := lines[0]
|
||||
if d := -first.Bounds.Min.Y - first.Ascent; d > 0 {
|
||||
padTop = d.Ceil()
|
||||
}
|
||||
last := lines[len(lines)-1]
|
||||
if d := last.Bounds.Max.Y - last.Descent; d > 0 {
|
||||
padBottom = d.Ceil()
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
package text
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
|
||||
"gioui.org/ui/draw"
|
||||
"gioui.org/ui/layout"
|
||||
"golang.org/x/image/math/fixed"
|
||||
)
|
||||
|
||||
type Line struct {
|
||||
Text String
|
||||
// Width is the width of the line.
|
||||
Width fixed.Int26_6
|
||||
// Ascent is the height above the baseline.
|
||||
Ascent fixed.Int26_6
|
||||
// Descent is the height below the baseline, including
|
||||
// the line gap.
|
||||
Descent fixed.Int26_6
|
||||
// Bounds is the visible bounds of the line.
|
||||
Bounds fixed.Rectangle26_6
|
||||
}
|
||||
|
||||
type String struct {
|
||||
String string
|
||||
Advances []fixed.Int26_6
|
||||
}
|
||||
|
||||
type Layout struct {
|
||||
Lines []Line
|
||||
}
|
||||
|
||||
type Face interface {
|
||||
Layout(str string, singleLine bool, maxWidth int) *Layout
|
||||
Path(str String) *draw.Path
|
||||
}
|
||||
|
||||
type Alignment uint8
|
||||
|
||||
const (
|
||||
Start Alignment = iota
|
||||
End
|
||||
Center
|
||||
)
|
||||
|
||||
func linesDimens(lines []Line) layout.Dimens {
|
||||
var width fixed.Int26_6
|
||||
var h int
|
||||
var baseline int
|
||||
if len(lines) > 0 {
|
||||
baseline = lines[0].Ascent.Ceil()
|
||||
var prevDesc fixed.Int26_6
|
||||
for _, l := range lines {
|
||||
h += (prevDesc + l.Ascent).Ceil()
|
||||
prevDesc = l.Descent
|
||||
if l.Width > width {
|
||||
width = l.Width
|
||||
}
|
||||
}
|
||||
h += lines[len(lines)-1].Descent.Ceil()
|
||||
}
|
||||
w := width.Ceil()
|
||||
return layout.Dimens{
|
||||
Size: image.Point{
|
||||
X: w,
|
||||
Y: h,
|
||||
},
|
||||
Baseline: baseline,
|
||||
}
|
||||
}
|
||||
|
||||
func IsNewline(r rune) bool {
|
||||
return r == '\n'
|
||||
}
|
||||
|
||||
func align(align Alignment, width fixed.Int26_6, maxWidth int) fixed.Int26_6 {
|
||||
mw := fixed.I(maxWidth)
|
||||
switch align {
|
||||
case Center:
|
||||
return fixed.I(((mw - width) / 2).Floor())
|
||||
case End:
|
||||
return fixed.I((mw - width).Floor())
|
||||
case Start:
|
||||
return 0
|
||||
default:
|
||||
panic(fmt.Errorf("unknown alignment %v", align))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user