From 21abd147bfd49374434001abcf6935a2890071e7 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Mon, 13 Feb 2023 12:55:51 -0500 Subject: [PATCH] Rudimentary text selection with keybaord keys --- elements/basic/textbox.go | 89 +++++++++++++++++---------- textmanip/textmanip.go | 122 ++++++++++++++++++++++---------------- 2 files changed, 129 insertions(+), 82 deletions(-) diff --git a/elements/basic/textbox.go b/elements/basic/textbox.go index e69d414..d40cd80 100644 --- a/elements/basic/textbox.go +++ b/elements/basic/textbox.go @@ -97,16 +97,30 @@ func (element *TextBox) HandleKeyDown(key input.Key, modifiers input.Modifiers) textChanged = true case key == input.KeyLeft: - element.dot = textmanip.MoveLeft ( - element.text, - element.dot, - modifiers.Control) + if modifiers.Shift { + element.dot = textmanip.SelectLeft ( + element.text, + element.dot, + modifiers.Control) + } else { + element.dot = textmanip.MoveLeft ( + element.text, + element.dot, + modifiers.Control) + } case key == input.KeyRight: - element.dot = textmanip.MoveRight ( - element.text, - element.dot, - modifiers.Control) + if modifiers.Shift { + element.dot = textmanip.SelectRight ( + element.text, + element.dot, + modifiers.Control) + } else { + element.dot = textmanip.MoveRight ( + element.text, + element.dot, + modifiers.Control) + } case key.Printable(): element.text, element.dot = textmanip.Type ( @@ -299,14 +313,28 @@ func (element *TextBox) draw () { } pattern := element.theme.Pattern(theme.PatternSunken, state) artist.FillRectangle(element.core, pattern, bounds) + offset := bounds.Min.Add (image.Point { + X: element.config.Padding() - element.scroll, + Y: element.config.Padding(), + }) - if len(element.text) == 0 && !element.Focused() { + if element.Focused() && !element.dot.Empty() { + // draw selection bounds + accent := element.theme.Pattern ( + theme.PatternAccent, state) + canon := element.dot.Canon() + start := element.valueDrawer.PositionOf(canon.Start).Add(offset) + end := element.valueDrawer.PositionOf(canon.End).Add(offset) + end.Y += element.valueDrawer.LineHeight().Round() + artist.FillRectangle ( + element.core, + accent, + image.Rectangle { start, end }) + } + + if len(element.text) == 0 { // draw placeholder textBounds := element.placeholderDrawer.LayoutBounds() - offset := bounds.Min.Add (image.Point { - X: element.config.Padding(), - Y: element.config.Padding(), - }) foreground := element.theme.Pattern ( theme.PatternForeground, theme.PatternState { Disabled: true }) @@ -317,30 +345,27 @@ func (element *TextBox) draw () { } else { // draw input value textBounds := element.valueDrawer.LayoutBounds() - offset := bounds.Min.Add (image.Point { - X: element.config.Padding() - element.scroll, - Y: element.config.Padding(), - }) foreground := element.theme.Pattern ( theme.PatternForeground, state) element.valueDrawer.Draw ( element.core, foreground, offset.Sub(textBounds.Min)) - - if element.Focused() { - // cursor - // TODO: draw selection if exists - cursorPosition := element.valueDrawer.PositionOf ( - element.dot.End) - artist.Line ( - element.core, - foreground, 1, - cursorPosition.Add(offset), - image.Pt ( - cursorPosition.X, - cursorPosition.Y + element.valueDrawer. - LineHeight().Round()).Add(offset)) - } + } + + if element.Focused() && element.dot.Empty() { + // draw cursor + foreground := element.theme.Pattern ( + theme.PatternForeground, state) + cursorPosition := element.valueDrawer.PositionOf ( + element.dot.End) + artist.Line ( + element.core, + foreground, 1, + cursorPosition.Add(offset), + image.Pt ( + cursorPosition.X, + cursorPosition.Y + element.valueDrawer. + LineHeight().Round()).Add(offset)) } } diff --git a/textmanip/textmanip.go b/textmanip/textmanip.go index c22037a..61d8f9e 100644 --- a/textmanip/textmanip.go +++ b/textmanip/textmanip.go @@ -34,12 +34,19 @@ func (dot Dot) Sub (delta int) Dot { } } -func WordToLeft (text []rune, dot Dot) (length int) { - cursor := dot.End - if cursor < 1 { return } - if cursor > len(text) { cursor = len(text) } +func (dot Dot) Constrain (length int) Dot { + if dot.Start < 0 { dot.Start = 0 } + if dot.Start > length { dot.Start = length} + if dot.End < 0 { dot.End = 0 } + if dot.End > length { dot.End = length} + return dot +} - index := cursor - 1 +func WordToLeft (text []rune, position int) (length int) { + if position < 1 { return } + if position > len(text) { position = len(text) } + + index := position - 1 for index >= 0 && unicode.IsSpace(text[index]) { length ++ index -- @@ -51,12 +58,11 @@ func WordToLeft (text []rune, dot Dot) (length int) { return } -func WordToRight (text []rune, dot Dot) (length int) { - cursor := dot.End - if cursor < 0 { return } - if cursor > len(text) { cursor = len(text) } +func WordToRight (text []rune, position int) (length int) { + if position < 0 { return } + if position > len(text) { position = len(text) } - index := cursor + index := position for index < len(text) && unicode.IsSpace(text[index]) { length ++ index ++ @@ -68,41 +74,46 @@ func WordToRight (text []rune, dot Dot) (length int) { return } -func Backspace (text []rune, dot Dot, word bool) (result []rune, moved Dot) { - if dot.Empty() { - cursor := dot.End - if cursor < 1 { return text, dot } - if cursor > len(text) { cursor = len(text) } +func WordAround (text []rune, position int) (around Dot) { + return Dot { + WordToLeft(text, position), + WordToRight(text, position), + } +} +func Backspace (text []rune, dot Dot, word bool) (result []rune, moved Dot) { + dot = dot.Constrain(len(text)) + if dot.Empty() { distance := 1 if word { - distance = WordToLeft(text, dot) + distance = WordToLeft(text, dot.End) } - result = append(result, text[:cursor - distance]...) - result = append(result, text[cursor:]...) - moved = EmptyDot(cursor - distance) + result = append ( + result, + text[:dot.Sub(distance).Constrain(len(text)).End]...) + result = append(result, text[dot.End:]...) + moved = EmptyDot(dot.Sub(distance).Start) + return } else { return Delete(text, dot, word) } - - return } func Delete (text []rune, dot Dot, word bool) (result []rune, moved Dot) { + dot = dot.Constrain(len(text)) if dot.Empty() { - cursor := dot.End - if cursor < 0 { return text, dot } - if cursor > len(text) { cursor = len(text) } - distance := 1 if word { - distance = WordToRight(text, dot) + distance = WordToRight(text, dot.End) } - result = append(result, text[:cursor]...) - result = append(result, text[cursor + distance:]...) + result = append(result, text[:dot.End]...) + result = append ( + result, + text[dot.Add(distance).Constrain(len(text)).End:]...) moved = dot return } else { + dot = dot.Canon() result = append(result, text[:dot.Start]...) result = append(result, text[dot.End:]...) moved = EmptyDot(dot.Start) @@ -111,50 +122,61 @@ func Delete (text []rune, dot Dot, word bool) (result []rune, moved Dot) { } func Type (text []rune, dot Dot, character rune) (result []rune, moved Dot) { + dot = dot.Constrain(len(text)) if dot.Empty() { - cursor := dot.End - if cursor < 0 { cursor = 0 } - if cursor > len(text) { cursor = len(text) } - result = append(result, text[:cursor]...) + result = append(result, text[:dot.End]...) result = append(result, character) - if cursor < len(text) { - result = append(result, text[cursor:]...) + if dot.End < len(text) { + result = append(result, text[dot.End:]...) } - moved = EmptyDot(cursor + 1) + moved = EmptyDot(dot.Add(1).End) return } else { + dot = dot.Canon() result = append(result, text[:dot.Start]...) result = append(result, character) result = append(result, text[dot.End:]...) - moved = EmptyDot(dot.Start) + moved = EmptyDot(dot.Add(1).Start) return } } func MoveLeft (text []rune, dot Dot, word bool) (moved Dot) { - cursor := dot.Start - - if cursor < 1 { return EmptyDot(cursor) } - if cursor > len(text) { cursor = len(text) } - + dot = dot.Constrain(len(text)) distance := 1 if word { - distance = WordToLeft(text, dot) + distance = WordToLeft(text, dot.Start) } - moved = EmptyDot(cursor - distance) + moved = EmptyDot(dot.Sub(distance).Start) return } func MoveRight (text []rune, dot Dot, word bool) (moved Dot) { - cursor := dot.End - - if cursor < 0 { return EmptyDot(cursor) } - if cursor > len(text) { cursor = len(text) } - + dot = dot.Constrain(len(text)) distance := 1 if word { - distance = WordToRight(text, dot) + distance = WordToRight(text, dot.End) } - moved = EmptyDot(cursor + distance) + moved = EmptyDot(dot.Add(distance).End) return } + +func SelectLeft (text []rune, dot Dot, word bool) (moved Dot) { + dot = dot.Constrain(len(text)) + distance := 1 + if word { + distance = WordToLeft(text, dot.Start) + } + dot.Start -= distance + return dot +} + +func SelectRight (text []rune, dot Dot, word bool) (moved Dot) { + dot = dot.Constrain(len(text)) + distance := 1 + if word { + distance = WordToRight(text, dot.End) + } + dot.Start += distance + return dot +}