Bläddra i källkod

Merge pull request #265 from countcb/editbox-textselection

gui.Edit: Added possibility to select text
Daniel Salvadori 3 år sedan
förälder
incheckning
e1613f0475
3 ändrade filer med 323 tillägg och 45 borttagningar
  1. 303 41
      gui/edit.go
  2. 2 2
      gui/label.go
  3. 18 2
      text/font.go

+ 303 - 41
gui/edit.go

@@ -21,8 +21,11 @@ type Edit struct {
 	placeHolder string // place holder string
 	text        string // current edit text
 	col         int    // current column
+	selStart    int    // start column of selection. always < selEnd. if selStart == selEnd then nothing is selected.
+	selEnd      int    // end column of selection. always > selStart. if selStart == selEnd then nothing is selected.
 	focus       bool   // key focus flag
 	cursorOver  bool
+	mouseDrag   bool // true when the mouse is moved while left mouse button is down. Used for selecting text via mouse
 	blinkID     int
 	caretOn     bool
 	styles      *EditStyles
@@ -63,15 +66,19 @@ func NewEdit(width int, placeHolder string) *Edit {
 	ed.text = ""
 	ed.MaxLength = 80
 	ed.col = 0
+	ed.selStart = 0
+	ed.selEnd = 0
 	ed.focus = false
 
 	ed.Label.initialize("", StyleDefault().Font)
 	ed.Label.Subscribe(OnKeyDown, ed.onKey)
 	ed.Label.Subscribe(OnKeyRepeat, ed.onKey)
 	ed.Label.Subscribe(OnChar, ed.onChar)
-	ed.Label.Subscribe(OnMouseDown, ed.onMouse)
+	ed.Label.Subscribe(OnMouseDown, ed.onMouseDown)
+	ed.Label.Subscribe(OnMouseUp, ed.onMouseUp)
 	ed.Label.Subscribe(OnCursorEnter, ed.onCursor)
 	ed.Label.Subscribe(OnCursorLeave, ed.onCursor)
+	ed.Label.Subscribe(OnCursor, ed.onCursor)
 	ed.Label.Subscribe(OnEnable, func(evname string, ev interface{}) { ed.update() })
 	ed.Subscribe(OnFocusLost, ed.OnFocusLost)
 
@@ -80,10 +87,13 @@ func NewEdit(width int, placeHolder string) *Edit {
 }
 
 // SetText sets this edit text
-func (ed *Edit) SetText(text string) *Edit {
+func (ed *Edit) SetText(newText string) *Edit {
 
 	// Remove new lines from text
-	ed.text = strings.Replace(text, "\n", "", -1)
+	ed.text = strings.Replace(newText, "\n", "", -1)
+	ed.col = text.StrCount(ed.text)
+	ed.selStart = ed.col
+	ed.selEnd = ed.col
 	ed.update()
 	return ed
 }
@@ -94,6 +104,28 @@ func (ed *Edit) Text() string {
 	return ed.text
 }
 
+// SelectedText returns the currently selected text
+// or empty string when nothing is selected
+func (ed *Edit) SelectedText() string {
+
+	if ed.selStart == ed.selEnd {
+		return ""
+	}
+
+	s := ""
+	charNum := 0
+	for _, currentRune := range ed.text {
+		if charNum >= ed.selEnd {
+			break
+		}
+		if charNum >= ed.selStart {
+			s += string(currentRune)
+		}
+		charNum++
+	}
+	return s
+}
+
 // SetFontSize sets label font size (overrides Label.SetFontSize)
 func (ed *Edit) SetFontSize(size float64) *Edit {
 
@@ -124,36 +156,165 @@ func (ed *Edit) CursorPos(col int) {
 
 	if col <= text.StrCount(ed.text) {
 		ed.col = col
+		ed.selStart = col
+		ed.selEnd = col
 		ed.redraw(ed.focus)
 	}
 }
 
+// SetSelection selects the text between start and end
+func (ed *Edit) SetSelection(start, end int) {
+
+	// make sure end is bigger than start
+	if start > end {
+		start, end = end, start
+	}
+
+	if start < 0 {
+		start = 0
+	}
+	if end > text.StrCount(ed.text) {
+		end = text.StrCount(ed.text)
+	}
+
+	ed.selStart = start
+	ed.selEnd = end
+	ed.col = end
+	ed.redraw(ed.focus)
+}
+
 // CursorLeft moves the edit cursor one character left if possible
+// If text is selected the cursor is moved to the beginning of the selection instead
+// and the selection is removed
 func (ed *Edit) CursorLeft() {
 
-	if ed.col > 0 {
-		ed.col--
+	if ed.selStart == ed.selEnd {
+		// no selection
+		// move cursor to the left if possible
+		if ed.col > 0 {
+			ed.col--
+			ed.selStart = ed.col
+			ed.selEnd = ed.col
+			ed.redraw(ed.focus)
+		}
+	} else {
+		// reset selection and move cursor to start of selection
+		ed.col = ed.selStart
+		ed.selStart = ed.col
+		ed.selEnd = ed.col
 		ed.redraw(ed.focus)
 	}
 }
 
 // CursorRight moves the edit cursor one character right if possible
+// If text is selected the cursor is moved to the end of the selection instead
+// and the selection is removed
 func (ed *Edit) CursorRight() {
 
-	if ed.col < text.StrCount(ed.text) {
-		ed.col++
+	if ed.selStart == ed.selEnd {
+		// no selection
+		// move cursor to the right if possible
+		if ed.col < text.StrCount(ed.text) {
+			ed.col++
+			ed.selStart = ed.col
+			ed.selEnd = ed.col
+			ed.redraw(ed.focus)
+		}
+	} else {
+		// reset selection and move cursor to end of selection
+		ed.col = ed.selEnd
+		ed.selStart = ed.col
+		ed.selEnd = ed.col
 		ed.redraw(ed.focus)
 	}
 }
 
-// CursorBack deletes the character at left of the cursor if possible
-func (ed *Edit) CursorBack() {
+// SelectLeft expands/shrinks the selection to the left if possible
+func (ed *Edit) SelectLeft() {
 
 	if ed.col > 0 {
-		ed.col--
-		ed.text = text.StrRemove(ed.text, ed.col)
-		ed.redraw(ed.focus)
-		ed.Dispatch(OnChange, nil)
+		if ed.col == ed.selStart {
+			// cursor is at the start of selection
+			// expand selection to the left
+			ed.col--
+			ed.selStart = ed.col
+			ed.redraw(ed.focus)
+		} else {
+			// cursor is at the end of selection:
+			// remove selection from the end
+			ed.col--
+			ed.selEnd = ed.col
+			ed.redraw(ed.focus)
+		}
+	}
+}
+
+// SelectRight expands/shrinks the selection to the right if possible
+func (ed *Edit) SelectRight() {
+
+	if ed.col < text.StrCount(ed.text) {
+		if ed.col == ed.selEnd {
+			// cursor is at the end of selection:
+			// expand selection to the right if possible
+			ed.col++
+			ed.selEnd = ed.col
+			ed.redraw(ed.focus)
+		} else {
+			// cursor is at the start of selection:
+			// remove selection from the start
+			ed.col++
+			ed.selStart = ed.col
+			ed.redraw(ed.focus)
+		}
+	}
+}
+
+// SelectHome expands the selection to the left to the beginning of the text
+func (ed *Edit) SelectHome() {
+
+	if ed.selStart < ed.col {
+		ed.selEnd = ed.selStart
+	}
+	ed.col = 0
+	ed.selStart = 0
+	ed.redraw(ed.focus)
+}
+
+// SelectEnd expands the selection to the right to the end of the text
+func (ed *Edit) SelectEnd() {
+
+	if ed.selEnd > ed.col {
+		ed.selStart = ed.selEnd
+	}
+	ed.col = text.StrCount(ed.text)
+	ed.selEnd = ed.col
+	ed.redraw(ed.focus)
+}
+
+// SelectAll selects all text
+func (ed *Edit) SelectAll() {
+
+	ed.selStart = 0
+	ed.selEnd = text.StrCount(ed.text)
+	ed.col = ed.selEnd
+	ed.redraw(ed.focus)
+}
+
+// CursorBack either deletes the character at left of the cursor if possible
+// Or if text is selected the selected text is removed all at once
+func (ed *Edit) CursorBack() {
+
+	if ed.selStart == ed.selEnd {
+		if ed.col > 0 {
+			ed.col--
+			ed.selStart = ed.col
+			ed.selEnd = ed.col
+			ed.text = text.StrRemove(ed.text, ed.col)
+			ed.redraw(ed.focus)
+			ed.Dispatch(OnChange, nil)
+		}
+	} else {
+		ed.DeleteSelection()
 	}
 }
 
@@ -161,6 +322,8 @@ func (ed *Edit) CursorBack() {
 func (ed *Edit) CursorHome() {
 
 	ed.col = 0
+	ed.selStart = ed.col
+	ed.selEnd = ed.col
 	ed.redraw(ed.focus)
 }
 
@@ -168,22 +331,55 @@ func (ed *Edit) CursorHome() {
 func (ed *Edit) CursorEnd() {
 
 	ed.col = text.StrCount(ed.text)
+	ed.selStart = ed.col
+	ed.selEnd = ed.col
 	ed.redraw(ed.focus)
 }
 
-// CursorDelete deletes the character at the right of the cursor if possible
+// CursorDelete either deletes the character at the right of the cursor if possible
+// Or if text is selected the selected text is removed all at once
 func (ed *Edit) CursorDelete() {
 
-	if ed.col < text.StrCount(ed.text) {
-		ed.text = text.StrRemove(ed.text, ed.col)
-		ed.redraw(ed.focus)
+	if ed.selStart == ed.selEnd {
+		if ed.col < text.StrCount(ed.text) {
+			ed.text = text.StrRemove(ed.text, ed.col)
+			ed.redraw(ed.focus)
+			ed.Dispatch(OnChange, nil)
+		}
+	} else {
+		ed.DeleteSelection()
+	}
+}
+
+// DeleteSelection deletes the selected characters. Does nothing if nothing is selected.
+func (ed *Edit) DeleteSelection() {
+
+	if ed.selStart == ed.selEnd {
+		return
+	}
+
+	changed := false
+	ed.col = ed.selStart
+	for ed.selEnd > ed.selStart {
+		if ed.col < text.StrCount(ed.text) {
+			changed = true
+			ed.text = text.StrRemove(ed.text, ed.col)
+			ed.selEnd--
+		}
+	}
+	if changed {
 		ed.Dispatch(OnChange, nil)
+		ed.redraw(ed.focus)
 	}
 }
 
 // CursorInput inserts the specified string at the current cursor position
+// If text is selected the selected text gets overwritten
 func (ed *Edit) CursorInput(s string) {
 
+	if ed.selStart != ed.selEnd {
+		ed.DeleteSelection()
+	}
 	if text.StrCount(ed.text) >= ed.MaxLength {
 		return
 	}
@@ -204,40 +400,65 @@ func (ed *Edit) CursorInput(s string) {
 
 	ed.text = newText
 	ed.col++
+	ed.selStart = ed.col
+	ed.selEnd = ed.col
 
 	ed.Dispatch(OnChange, nil)
 	ed.redraw(ed.focus)
 }
 
 // redraw redraws the text showing the caret if specified
+// the selection caret is always shown (when text is selected)
 func (ed *Edit) redraw(caret bool) {
 
 	line := 0
-	if !caret {
-		line = -1
-	}
-	ed.Label.setTextCaret(ed.text, editMarginX, ed.width, line, ed.col)
+	ed.Label.setTextCaret(ed.text, editMarginX, ed.width, caret, line, ed.col, ed.selStart, ed.selEnd)
 }
 
 // onKey receives subscribed key events
 func (ed *Edit) onKey(evname string, ev interface{}) {
 
 	kev := ev.(*window.KeyEvent)
-	switch kev.Key {
-	case window.KeyLeft:
-		ed.CursorLeft()
-	case window.KeyRight:
-		ed.CursorRight()
-	case window.KeyHome:
-		ed.CursorHome()
-	case window.KeyEnd:
-		ed.CursorEnd()
-	case window.KeyBackspace:
-		ed.CursorBack()
-	case window.KeyDelete:
-		ed.CursorDelete()
-	default:
-		return
+	if kev.Mods != window.ModShift && kev.Mods != window.ModControl {
+		switch kev.Key {
+		case window.KeyLeft:
+			ed.CursorLeft()
+		case window.KeyRight:
+			ed.CursorRight()
+		case window.KeyHome:
+			ed.CursorHome()
+		case window.KeyEnd:
+			ed.CursorEnd()
+		case window.KeyBackspace:
+			ed.CursorBack()
+		case window.KeyDelete:
+			ed.CursorDelete()
+		default:
+			return
+		}
+	} else if kev.Mods == window.ModShift {
+		switch kev.Key {
+		case window.KeyLeft:
+			ed.SelectLeft()
+		case window.KeyRight:
+			ed.SelectRight()
+		case window.KeyHome:
+			ed.SelectHome()
+		case window.KeyEnd:
+			ed.SelectEnd()
+		case window.KeyBackspace:
+			ed.CursorBack()
+		case window.KeyDelete:
+			ed.SelectAll()
+			ed.DeleteSelection()
+		default:
+			return
+		}
+	} else if kev.Mods == window.ModControl {
+		switch kev.Key {
+		case window.KeyA:
+			ed.SelectAll()
+		}
 	}
 }
 
@@ -248,22 +469,36 @@ func (ed *Edit) onChar(evname string, ev interface{}) {
 	ed.CursorInput(string(cev.Char))
 }
 
-// onMouseEvent receives subscribed mouse down events
-func (ed *Edit) onMouse(evname string, ev interface{}) {
+// onMouseDown receives subscribed mouse down events
+func (ed *Edit) onMouseDown(evname string, ev interface{}) {
 
 	e := ev.(*window.MouseEvent)
 	if e.Button != window.MouseButtonLeft {
 		return
 	}
 
+	// set caret to clicked position
+	ed.handleMouse(e.Xpos, false)
+
+	ed.mouseDrag = true
+
 	// Set key focus to this panel
+	// Set the focus AFTER the mouse selection is handled
+	// Otherwise the OnFocus event would fire before the cursor is set.
+	// That way the OnFocus handler could NOT influence the selection
+	// Because it would be overridden/cleared directly afterwards.
 	Manager().SetKeyFocus(ed)
+}
+
+// handleMouse is setting the caret when the mouse is clicked
+// or setting the text selection when the mouse is dragged
+func (ed *Edit) handleMouse(mouseX float32, dragged bool) {
 
 	// Find clicked column
 	var nchars int
 	for nchars = 1; nchars <= text.StrCount(ed.text); nchars++ {
 		width, _ := ed.Label.font.MeasureText(text.StrPrefix(ed.text, nchars))
-		posx := e.Xpos - ed.pospix.X
+		posx := mouseX - ed.pospix.X
 		if posx < editMarginX+float32(width) {
 			break
 		}
@@ -272,7 +507,28 @@ func (ed *Edit) onMouse(evname string, ev interface{}) {
 		ed.focus = true
 		ed.blinkID = Manager().SetInterval(750*time.Millisecond, nil, ed.blink)
 	}
-	ed.CursorPos(nchars - 1)
+	if !dragged {
+		ed.CursorPos(nchars - 1)
+	} else {
+		newPos := nchars - 1
+		if newPos > ed.col {
+			distance := newPos - ed.col
+			for i := 0; i < distance; i++ {
+				ed.SelectRight()
+			}
+		} else if newPos < ed.col {
+			distance := ed.col - newPos
+			for i := 0; i < distance; i++ {
+				ed.SelectLeft()
+			}
+		}
+	}
+}
+
+// onMouseEvent receives subscribed mouse up events
+func (ed *Edit) onMouseUp(evname string, ev interface{}) {
+
+	ed.mouseDrag = false
 }
 
 // onCursor receives subscribed cursor events
@@ -287,9 +543,15 @@ func (ed *Edit) onCursor(evname string, ev interface{}) {
 	if evname == OnCursorLeave {
 		window.Get().SetCursor(window.ArrowCursor)
 		ed.cursorOver = false
+		ed.mouseDrag = false
 		ed.update()
 		return
 	}
+	if ed.mouseDrag {
+		e := ev.(*window.CursorEvent)
+		// select text based on mouse position
+		ed.handleMouse(e.Xpos, true)
+	}
 }
 
 // blink blinks the caret
@@ -336,7 +598,7 @@ func (ed *Edit) applyStyle(s *EditStyle) {
 
 	if !ed.focus && len(ed.text) == 0 && len(ed.placeHolder) > 0 {
 		ed.Label.SetColor4(&s.HolderColor)
-		ed.Label.setTextCaret(ed.placeHolder, editMarginX, ed.width, -1, ed.col)
+		ed.Label.setTextCaret(ed.placeHolder, editMarginX, ed.width, false, -1, ed.col, ed.selStart, ed.selEnd)
 	} else {
 		ed.Label.SetColor4(&s.FgColor)
 		ed.redraw(ed.focus)

+ 2 - 2
gui/label.go

@@ -213,7 +213,7 @@ func (l *Label) LineSpacing() float64 {
 // setTextCaret sets the label text and draws a caret at the
 // specified line and column.
 // It is normally used by the Edit widget.
-func (l *Label) setTextCaret(msg string, mx, width, line, col int) {
+func (l *Label) setTextCaret(msg string, mx, width int, drawCaret bool, line, col, selStart, selEnd int) {
 
 	// Set font properties
 	l.font.SetAttributes(&l.style.FontAttributes)
@@ -222,7 +222,7 @@ func (l *Label) setTextCaret(msg string, mx, width, line, col int) {
 	// Create canvas and draw text
 	_, height := l.font.MeasureText(msg)
 	canvas := text.NewCanvas(width, height, &l.style.BgColor)
-	canvas.DrawTextCaret(mx, 0, msg, l.font, line, col)
+	canvas.DrawTextCaret(mx, 0, msg, l.font, drawCaret, line, col, selStart, selEnd)
 
 	// Creates texture if if doesnt exist.
 	if l.tex == nil {

+ 18 - 2
text/font.go

@@ -270,7 +270,7 @@ func (c Canvas) DrawText(x, y int, text string, f *Font) {
 // the specified line and column.
 // The supplied text string can contain line break escape sequences (\n).
 // TODO Implement caret as a gui.Panel in gui.Edit
-func (c Canvas) DrawTextCaret(x, y int, text string, f *Font, line, col int) error {
+func (c Canvas) DrawTextCaret(x, y int, text string, f *Font, drawCaret bool, line, col, selStart, selEnd int) error {
 
 	// Creates drawer
 	f.updateFace()
@@ -284,9 +284,25 @@ func (c Canvas) DrawTextCaret(x, y int, text string, f *Font, line, col int) err
 	lines := strings.Split(text, "\n")
 	for l, s := range lines {
 		d.Dot = fixed.P(x, py)
+		if selStart != selEnd && l == line && selEnd <= StrCount(s) {
+			width, _ := f.MeasureText(StrPrefix(s, selStart))
+			widthEnd, _ := f.MeasureText(StrPrefix(s, selEnd))
+			// Draw selection caret
+			// TODO This will not work when the selection spans multiple lines
+			// Currently there is no multiline edit text
+			// Once there is, this needs to change
+			caretH := int(f.attrib.PointSize) + 2
+			caretY := int(d.Dot.Y>>6) - int(f.attrib.PointSize) + 2
+			color := Color4RGBA(&math32.Color4{0, 0, 1, 0.5}) // Hardcoded to blue, alpha 50%
+			for w := width; w < widthEnd; w++ {
+				for j := caretY; j < caretY+caretH; j++ {
+					c.RGBA.Set(x+w, j, color)
+				}
+			}
+		}
 		d.DrawString(s)
 		// Checks for caret position
-		if l == line && col <= StrCount(s) {
+		if drawCaret && l == line && col <= StrCount(s) {
 			width, _ := f.MeasureText(StrPrefix(s, col))
 			// Draw caret vertical line
 			caretH := int(f.attrib.PointSize) + 2