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 textChanged Events } func BuildTextBox(fn func(*TextBox)) *TextBox { var b = &TextBox{} if fn != nil { fn(b) } return b } func (b *TextBox) Arrange(ctx Context, bounds geom.RectangleF32, offset geom.PointF32, parent Control) { b.ControlBase.Arrange(ctx, bounds, offset, parent) pad := b.ActualTextPadding(ctx) b.box.Arrange(ctx, pad.InsetRect(bounds), offset, b) } func (b *TextBox) DesiredSize(ctx Context, _ geom.PointF32) geom.PointF32 { font := b.ActualFont(ctx) var width = font.WidthOf(b.Text) var height = font.Height() pad := b.ActualTextPadding(ctx) return geom.PtF32(pad.Left+width+pad.Right, pad.Top+height+pad.Bottom) } func (b *TextBox) TextChanged() *Events { return &b.textChanged } func (b *TextBox) mousePosToCaretPos(ctx Context, e MouseEvent) int { p := b.ToControlPosition(e.Pos()) offset := p.X - b.box.bounds.Min.X f := b.ActualFont(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(ctx Context) string { start, end := b.selectionRange() if end == 0 { return "" } cut := b.Text[start:end] b.updateText(ctx, 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) bool { if b.ControlBase.Handle(ctx, e) { return true } b.box.Handle(ctx, e) if b.over { if b.Disabled { return true } ctx.Renderer().SetMouseCursor(MouseCursorText) } if b.Focus { ctx.Animate() } 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() return true } 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 return true } 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(ctx) } else { caret := b.Selection.Caret if caret < len(b.Text) { b.updateText(ctx, b.Text[:caret]+b.Text[caret+1:]) } } case e.Key == KeyBackspace: if b.Selection.HasSelection() { b.cut(ctx) } else { caret := b.Selection.Caret if caret > 0 { b.updateText(ctx, 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(ctx) caret := b.Selection.Caret b.updateText(ctx, b.Text[:caret]+text+b.Text[caret:]) b.Selection.Caret = caret + len(text) } case KeyX: DefaultClipboard.WriteText(b.cut(ctx)) } } return true case *TextInputEvent: if b.Selection.HasSelection() { b.cut(ctx) } caret := b.Selection.Caret b.updateText(ctx, fmt.Sprintf("%s%c%s", b.Text[:caret], e.Character, b.Text[caret:])) b.Selection.Caret = caret + 1 b.Selection.SetSelectionToCaret() return true } return false } func (b *TextBox) Render(ctx Context) { b.RenderBackground(ctx) b.RenderOutline(ctx) c := b.TextColor(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) font := b.ActualFont(ctx) if b.Selection.Start != b.Selection.End { left, right := font.WidthOf(b.Text[:b.Selection.Start]), font.WidthOf(b.Text[:b.Selection.End]) renderer.FillRectangle(geom.RectF32(left, 0, right, size.Y), style.Palette.PrimaryHighlight) } renderer.Text(font, geom.ZeroPtF32, c, b.Text) const interval = 500 * time.Millisecond if b.Focus && (time.Since(b.blink)/interval)%2 == 0 { var w = font.WidthOf(b.Text[:b.Selection.Caret]) var caret = w + .5*caretWidth renderer.Rectangle(geom.RectF32(caret, 0, caret, size.Y), c, caretWidth) } }) } func (b *TextBox) updateText(ctx Context, text string) { if b.Text == text { return } b.Text = text b.textChanged.Notify(ctx, text) }