zntg/ui/textbox.go
Sander Schobers cdfb863ab0 Added SDL backend.
Added Action{,s}. List of actions that can be used to defer cleanup code (see NewRenderer implementations).
Added TextInputEvent (replaces the old KeyPressEvent) and added to new events KeyDown & KeyUp.
Added VSync to NewRendererOptions.
Removed IconScale from button.
Added ImageSource interface that replaces the Image/Texture method on the Texture interface. This makes converting back a texture to an image optional (since this is atypical for a hardware texture for instance).
Added new KeyModifier: OSCommand (Windows/Command key).
Added KeyState that can keep the state of keys (pressed or not).
Added KeyEnter, representing the Enter key.
Changed signatures of CreateTexture methods in Renderer.
Changed signatures of icon related method (removed factories).
Basic example now depends on sdlgui.
2020-05-15 09:20:44 +02:00

260 lines
5.9 KiB
Go

package ui
import (
"fmt"
"strings"
"time"
"unicode"
"opslag.de/schobers/geom"
)
type TextSelection struct {
Caret int
Start int
End int
}
func (s *TextSelection) SetSelectionToCaret() {
s.Start = s.Caret
s.End = s.Caret
}
func (s *TextSelection) MoveCaret(delta int) {
s.Caret = s.Caret + delta
}
func (s TextSelection) HasSelection() bool {
return s.Start != s.End
}
type TextBox struct {
ControlBase
box BufferControl
blink time.Time
Focus bool
Text string
Selection TextSelection
}
func BuildTextBox(fn func(*TextBox)) *TextBox {
var b = &TextBox{}
if fn != nil {
fn(b)
}
return b
}
func (b *TextBox) pad(ctx Context) float32 {
return ctx.Style().Dimensions.TextPadding
}
func (b *TextBox) Arrange(ctx Context, bounds geom.RectangleF32, offset geom.PointF32, parent Control) {
b.ControlBase.Arrange(ctx, bounds, offset, parent)
b.box.Arrange(ctx, bounds.Inset(b.pad(ctx)), offset, b)
}
func (b *TextBox) DesiredSize(ctx Context) geom.PointF32 {
var fontName = b.FontName(ctx)
var font = ctx.Renderer().Font(fontName)
var width = font.WidthOf(b.Text)
var height = font.Height()
var pad = b.pad(ctx)
return geom.PtF32(width+pad*2, height+pad*2)
}
func (b *TextBox) mousePosToCaretPos(ctx Context, e MouseEvent) int {
p := b.ToControlPosition(e.Pos())
offset := p.X - b.box.bounds.Min.X
f := ctx.Renderer().Font(b.FontName(ctx))
var carets = [3]int{0, 0, len(b.Text)}
var offsets = [3]float32{0, 0, f.WidthOf(b.Text)}
var updateCenter = func() {
var c = (carets[0] + carets[2]) / 2
carets[1] = c
offsets[1] = f.WidthOf(b.Text[:c])
}
updateCenter()
for (carets[2] - carets[0]) > 1 {
if offset > offsets[1] {
carets[0] = carets[1]
offsets[0] = offsets[1]
updateCenter()
} else {
carets[2] = carets[1]
offsets[2] = offsets[1]
updateCenter()
}
}
if geom.Abs32(offsets[0]-offset) < geom.Abs32(offsets[2]-offset) {
return carets[0]
}
return carets[2]
}
func (b *TextBox) cut() string {
start, end := b.selectionRange()
if end == 0 {
return ""
}
cut := b.Text[start:end]
b.Text = b.Text[:start] + b.Text[end:]
b.Selection.Caret = start
b.Selection.SetSelectionToCaret()
return cut
}
func (b *TextBox) selection() string {
start, end := b.selectionRange()
if end == 0 {
return ""
}
return b.Text[start:end]
}
func (b *TextBox) selectionRange() (int, int) {
start, end := b.Selection.Start, b.Selection.End
if start == end {
return 0, 0
}
if start > end {
start, end = end, start
}
return start, end
}
func (b *TextBox) Handle(ctx Context, e Event) {
b.ControlBase.Handle(ctx, e)
b.box.Handle(ctx, e)
switch e := e.(type) {
case *MouseButtonDownEvent:
if b.over {
b.Focus = true
b.Selection.Caret = b.mousePosToCaretPos(ctx, e.MouseEvent)
b.Selection.SetSelectionToCaret()
b.blink = time.Now()
} else {
b.Focus = false
}
case *MouseMoveEvent:
if b.Focus && b.pressed && b.over {
b.Selection.Caret = b.mousePosToCaretPos(ctx, e.MouseEvent)
b.Selection.End = b.Selection.Caret
}
case *KeyDownEvent:
if !b.Focus {
break
}
selectAfterMove := func() {
if e.Modifiers&KeyModifierShift == KeyModifierShift {
b.Selection.End = b.Selection.Caret
} else {
b.Selection.SetSelectionToCaret()
}
}
switch {
case e.Key == KeyDelete:
if b.Selection.HasSelection() {
b.cut()
} else {
caret := b.Selection.Caret
if caret < len(b.Text) {
b.Text = b.Text[:caret] + b.Text[caret+1:]
}
}
case e.Key == KeyBackspace:
if b.Selection.HasSelection() {
b.cut()
} else {
caret := b.Selection.Caret
if caret > 0 {
b.Text = b.Text[:caret-1] + b.Text[caret:]
b.Selection.Caret = caret - 1
b.Selection.SetSelectionToCaret()
}
}
case e.Key == KeyEnd:
b.Selection.Caret = len(b.Text)
selectAfterMove()
case e.Key == KeyHome:
b.Selection.Caret = 0
selectAfterMove()
case e.Key == KeyLeft:
if b.Selection.Caret > 0 {
b.Selection.MoveCaret(-1)
selectAfterMove()
}
case e.Key == KeyRight:
if b.Selection.Caret < len(b.Text) {
b.Selection.MoveCaret(1)
selectAfterMove()
}
case e.Modifiers&KeyModifierControl == KeyModifierControl:
switch e.Key {
case KeyA:
b.Selection.Start = 0
b.Selection.End = len(b.Text)
b.Selection.Caret = b.Selection.End
case KeyC:
DefaultClipboard.WriteText(b.selection())
case KeyV:
text, err := DefaultClipboard.ReadText()
text = strings.Map(func(r rune) rune {
if unicode.IsControl(r) {
return -1
}
return r
}, text)
if err == nil {
b.cut()
caret := b.Selection.Caret
b.Text = b.Text[:caret] + text + b.Text[caret:]
b.Selection.Caret = caret + len(text)
}
case KeyX:
DefaultClipboard.WriteText(b.cut())
}
}
case *TextInputEvent:
caret := b.Selection.Caret
b.Text = fmt.Sprintf("%s%c%s", b.Text[:caret], e.Character, b.Text[caret:])
b.Selection.Caret = caret + 1
b.Selection.SetSelectionToCaret()
}
if b.over {
ctx.Renderer().SetMouseCursor(MouseCursorText)
}
ctx.Animate()
}
func (b *TextBox) Render(ctx Context) {
b.RenderBackground(ctx)
b.RenderOutline(ctx)
c := b.FontColor(ctx)
f := b.FontName(ctx)
style := ctx.Style()
var caretWidth float32 = 1
b.box.RenderFn(ctx, func(_ Context, size geom.PointF32) {
var renderer = ctx.Renderer()
back := b.Background
if back == nil {
back = ctx.Style().Palette.Background
}
renderer.Clear(back)
if b.Selection.Start != b.Selection.End {
left, right := renderer.Font(f).WidthOf(b.Text[:b.Selection.Start]), renderer.Font(f).WidthOf(b.Text[:b.Selection.End])
renderer.FillRectangle(geom.RectF32(left, 0, right, size.Y), style.Palette.PrimaryHighlight)
}
renderer.Text(geom.ZeroPtF32, f, c, b.Text)
const interval = 500 * time.Millisecond
if b.Focus && (time.Since(b.blink)/interval)%2 == 0 {
var w = renderer.Font(f).WidthOf(b.Text[:b.Selection.Caret])
var caret = w + .5*caretWidth
renderer.Rectangle(geom.RectF32(caret, 0, caret, size.Y), c, caretWidth)
}
})
}