From d18da8b07a72b676dcca930defc863b7bcc9d24c Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Mon, 13 Feb 2023 18:29:49 -0500 Subject: [PATCH] Rudimentary text selection with the mouse --- artist/text.go | 75 ++++++++++++++++++++++++++++++--------- elements/basic/textbox.go | 41 +++++++++++++++++++-- 2 files changed, 96 insertions(+), 20 deletions(-) diff --git a/artist/text.go b/artist/text.go index bc8c8cd..f5a1524 100644 --- a/artist/text.go +++ b/artist/text.go @@ -10,6 +10,7 @@ import "git.tebibyte.media/sashakoshka/tomo/canvas" type characterLayout struct { x int + width int character rune } @@ -38,13 +39,14 @@ const ( // text, and calculating text bounds. It avoids doing redundant work // automatically. type TextDrawer struct { - runes []rune - face font.Face - width int - height int - align Align - wrap bool - cut bool + runes []rune + face font.Face + width int + height int + align Align + wrap bool + cut bool + metrics font.Metrics layout []wordLayout layoutClean bool @@ -205,6 +207,38 @@ func (drawer *TextDrawer) PositionOf (index int) (position image.Point) { return } +// AtPosition returns the index at the specified position relative to the +// baseline. +func (drawer *TextDrawer) AtPosition (position image.Point) (index int) { + cursor := 0 + if !drawer.layoutClean { drawer.recalculate() } + for _, word := range drawer.layout { + for _, character := range word.text { + bounds := drawer.boundsOfChar(character).Add(word.position) + if position.In(bounds) { + return cursor + } + cursor ++ + } + for _, character := range word.whitespace { + bounds := drawer.boundsOfChar(character).Add(word.position) + if position.In(bounds) { + return cursor + } + cursor ++ + } + } + return -1 +} + +func (drawer *TextDrawer) boundsOfChar (char characterLayout) (image.Rectangle) { + return image.Rect ( + char.x, 0, + char.x + char.width, + drawer.metrics.Height.Ceil()). + Sub(image.Pt(0, drawer.metrics.Descent.Round())) +} + // Length returns the amount of runes in the drawer's text. func (drawer *TextDrawer) Length () (length int) { return len(drawer.runes) @@ -217,7 +251,7 @@ func (drawer *TextDrawer) recalculate () { if drawer.runes == nil { return } if drawer.face == nil { return } - metrics := drawer.face.Metrics() + drawer.metrics = drawer.face.Metrics() dot := fixed.Point26_6 { 0, 0 } index := 0 horizontalExtent := 0 @@ -241,6 +275,7 @@ func (drawer *TextDrawer) recalculate () { word.text = append(word.text, characterLayout { x: currentCharacterX.Round(), character: character, + width: advance.Ceil(), }) dot.X += advance @@ -264,9 +299,9 @@ func (drawer *TextDrawer) recalculate () { word.width + word.position.X > drawer.width && word.position.X > 0 { - word.position.Y += metrics.Height.Round() + word.position.Y += drawer.metrics.Height.Round() word.position.X = 0 - dot.Y += metrics.Height + dot.Y += drawer.metrics.Height dot.X = wordWidth } @@ -281,12 +316,13 @@ func (drawer *TextDrawer) recalculate () { word.whitespace = append(word.whitespace, characterLayout { x: currentCharacterX.Round(), character: character, + width: advance.Ceil(), }) spaceWidth += advance currentCharacterX += advance if character == '\n' { - dot.Y += metrics.Height + dot.Y += drawer.metrics.Height dot.X = 0 word.breaksAfter ++ break @@ -309,8 +345,9 @@ func (drawer *TextDrawer) recalculate () { // stop processing more words. and remove any words that have // also crossed the line. if - drawer.cut && - (dot.Y - metrics.Ascent - metrics.Descent).Round() > + drawer.cut && ( + dot.Y - drawer.metrics.Ascent - + drawer.metrics.Descent).Round() > drawer.height { for @@ -343,11 +380,15 @@ func (drawer *TextDrawer) recalculate () { } if drawer.cut { - drawer.layoutBounds.Min.Y = 0 - metrics.Ascent.Round() - drawer.layoutBounds.Max.Y = drawer.height - metrics.Ascent.Round() + drawer.layoutBounds.Min.Y = 0 - drawer.metrics.Ascent.Round() + drawer.layoutBounds.Max.Y = + drawer.height - + drawer.metrics.Ascent.Round() } else { - drawer.layoutBounds.Min.Y = 0 - metrics.Ascent.Round() - drawer.layoutBounds.Max.Y = dot.Y.Round() + metrics.Descent.Round() + drawer.layoutBounds.Min.Y = 0 - drawer.metrics.Ascent.Round() + drawer.layoutBounds.Max.Y = + dot.Y.Round() + + drawer.metrics.Descent.Round() } // TODO: diff --git a/elements/basic/textbox.go b/elements/basic/textbox.go index d40cd80..da2e930 100644 --- a/elements/basic/textbox.go +++ b/elements/basic/textbox.go @@ -14,7 +14,8 @@ type TextBox struct { *core.FocusableCore core core.CoreControl focusableControl core.FocusableCoreControl - + + dragging bool dot textmanip.Dot scroll int placeholder string @@ -63,10 +64,44 @@ func (element *TextBox) handleResize () { func (element *TextBox) HandleMouseDown (x, y int, button input.Button) { if !element.Enabled() { return } if !element.Focused() { element.Focus() } + + if button == input.ButtonLeft { + point := image.Pt(x, y) + offset := element.Bounds().Min.Add (image.Pt ( + element.config.Padding() - element.scroll, + element.config.Padding())) + runeIndex := element.valueDrawer.AtPosition(point.Sub(offset)) + element.dragging = true + if runeIndex > -1 { + element.dot = textmanip.EmptyDot(runeIndex) + element.redo() + } + } +} + +func (element *TextBox) HandleMouseMove (x, y int) { + if !element.Enabled() { return } + if !element.Focused() { element.Focus() } + + if element.dragging { + point := image.Pt(x, y) + offset := element.Bounds().Min.Add (image.Pt ( + element.config.Padding() - element.scroll, + element.config.Padding())) + runeIndex := element.valueDrawer.AtPosition(point.Sub(offset)) + if runeIndex > -1 { + element.dot.End = runeIndex + element.redo() + } + } +} + +func (element *TextBox) HandleMouseUp (x, y int, button input.Button) { + if button == input.ButtonLeft { + element.dragging = false + } } -func (element *TextBox) HandleMouseUp (x, y int, button input.Button) { } -func (element *TextBox) HandleMouseMove (x, y int) { } func (element *TextBox) HandleMouseScroll (x, y int, deltaX, deltaY float64) { } func (element *TextBox) HandleKeyDown(key input.Key, modifiers input.Modifiers) {