diff --git a/textinput.go b/textinput.go index f22b90a..960824f 100644 --- a/textinput.go +++ b/textinput.go @@ -1,18 +1,28 @@ package objects +import "time" import "image" import "git.tebibyte.media/tomo/tomo" import "git.tebibyte.media/tomo/tomo/text" import "git.tebibyte.media/tomo/tomo/input" import "git.tebibyte.media/tomo/tomo/event" +import "git.tebibyte.media/tomo/objects/internal/history" +const textInputHistoryMaximum = 64 +const textInputHistoryMaxAge = time.Second / 4 var _ tomo.ContentObject = new(TextInput) +type textHistoryItem struct { + text string + dot text.Dot +} + // TextInput is a single-line editable text box. type TextInput struct { box tomo.TextBox text []rune multiline bool + history *history.History[textHistoryItem] on struct { valueChange event.FuncBroadcaster confirm event.FuncBroadcaster @@ -22,7 +32,11 @@ type TextInput struct { func newTextInput (text string, multiline bool) *TextInput { textInput := &TextInput { box: tomo.NewTextBox(), + text: []rune(text), multiline: multiline, + history: history.NewHistory[textHistoryItem] ( + textHistoryItem { text: text }, + textInputHistoryMaximum), } textInput.box.SetRole(tomo.R("objects", "TextInput")) textInput.box.SetTag("multiline", multiline) @@ -34,7 +48,7 @@ func newTextInput (text string, multiline bool) *TextInput { textInput.box.SetAttr(tomo.AOverflow(true, false)) textInput.box.SetAttr(tomo.AAlign(tomo.AlignStart, tomo.AlignMiddle)) } - textInput.SetValue(text) + textInput.box.SetText(text) textInput.box.SetFocusable(true) textInput.box.SetSelectable(true) textInput.box.OnKeyDown(textInput.handleKeyDown) @@ -109,6 +123,7 @@ func (this *TextInput) OnContentBoundsChange (callback func ()) event.Cookie { func (this *TextInput) SetValue (text string) { this.text = []rune(text) this.box.SetText(text) + this.logLargeAction() } // Value returns the text content of the input. @@ -128,27 +143,66 @@ func (this *TextInput) OnValueChange (callback func ()) event.Cookie { return this.on.valueChange.Connect(callback) } +// Undo undoes the last action. +func (this *TextInput) Undo () { + this.recoverHistoryItem(this.history.Undo()) +} + +// Redo redoes the last previously undone action. +func (this *TextInput) Redo () { + this.recoverHistoryItem(this.history.Redo()) +} + // Type types a character at the current dot position. func (this *TextInput) Type (char rune) { dot := this.Dot() this.text, dot = text.Type(this.text, dot, rune(char)) this.Select(dot) this.box.SetText(string(this.text)) + this.logKeystroke() } -// TODO: add up/down controls if this is a multiline input +func (this *TextInput) logKeystroke () { + if this.Dot().Empty() { + this.history.PushWeak ( + this.currentHistoryState(), + textInputHistoryMaxAge) + } else { + this.logLargeAction() + } +} + +func (this *TextInput) logLargeAction () { + this.history.Push(this.currentHistoryState()) +} + +func (this *TextInput) currentHistoryState () textHistoryItem { + return textHistoryItem { + text: string(this.text), + dot: this.Dot(), + } +} + +func (this *TextInput) recoverHistoryItem (item textHistoryItem) { + this.box.SetText(item.text) + this.text = []rune(item.text) + this.box.Select(item.dot) +} + +// TODO: add things like alt+up/down to move lines func (this *TextInput) handleKeyDown (key input.Key, numpad bool) bool { - dot := this.Dot() - modifiers := this.box.Window().Modifiers() - word := modifiers.Control - changed := false + dot := this.Dot() + modifiers := this.box.Window().Modifiers() + word := modifiers.Control + changed := false defer func () { this.Select(dot) if changed { this.box.SetText(string(this.text)) this.on.valueChange.Broadcast() + this.logKeystroke() } } () @@ -161,6 +215,7 @@ func (this *TextInput) handleKeyDown (key input.Key, numpad bool) bool { switch { case key == '\n', key == '\t': typ() + this.logKeystroke() return true } } @@ -180,6 +235,16 @@ func (this *TextInput) handleKeyDown (key input.Key, numpad bool) bool { case key.Printable() && !modifiers.Control: typ() return true + case key == 'z' && modifiers.Control: + if modifiers.Shift { + this.Redo() + } else { + this.Undo() + } + return true + case key == 'y' && modifiers.Control: + this.Redo() + return true default: return false } @@ -204,6 +269,10 @@ func (this *TextInput) handleKeyUp (key input.Key, numpad bool) bool { return true case key.Printable() && !modifiers.Control: return true + case key == 'z' && modifiers.Control: + return true + case key == 'y' && modifiers.Control: + return true default: return false }