// SPDX-License-Identifier: Unlicense OR MIT package gel import ( "image" "image/color" "time" "github.com/p9c/p9/pkg/gel/gio/f32" "github.com/p9c/p9/pkg/gel/gio/font" "github.com/p9c/p9/pkg/gel/gio/io/key" l "github.com/p9c/p9/pkg/gel/gio/layout" "github.com/p9c/p9/pkg/gel/gio/op" "github.com/p9c/p9/pkg/gel/gio/op/paint" "github.com/p9c/p9/pkg/gel/gio/text" "github.com/p9c/p9/pkg/gel/gio/unit" "github.com/p9c/p9/pkg/gel/gio/widget" "golang.org/x/image/math/fixed" ) func (w *Window) Editor() *Editor { e := &Editor{ editor: new(widget.Editor), submitHook: func(string) {}, changeHook: func(string) {}, focusHook: func(bool) {}, } return e } // Editor implements an editable and scrollable text area. // This is a wrapper around the GIO widget.Editor that maintains API compatibility. type Editor struct { editor *widget.Editor alignment text.Alignment singleLine bool submit bool mask rune font font.Font textSize fixed.Int26_6 focused bool maxWidth int viewSize image.Point requestFocus bool // request focus on next layout // the following are hooks for change events on the editor submitHook func(string) changeHook func(string) focusHook func(bool) } // EditorEvent represents editor events type EditorEvent interface { isEditorEvent() } // ChangeEvent is generated for every user change to the text. type ChangeEvent struct{} func (ChangeEvent) isEditorEvent() {} // SubmitEvent is generated when Submit is set and a carriage return key is pressed. type SubmitEvent struct { Text string } func (SubmitEvent) isEditorEvent() {} // SelectEvent is generated when the user selects some text, or changes the selection (e.g. with a shift-click). type SelectEvent struct{} func (SelectEvent) isEditorEvent() {} // Alignment sets the text alignment func (e *Editor) Alignment(alignment text.Alignment) *Editor { e.alignment = alignment e.editor.Alignment = alignment return e } // SingleLine sets single line mode func (e *Editor) SingleLine() *Editor { e.singleLine = true e.editor.SingleLine = true return e } // Submit enables submit events func (e *Editor) Submit(enabled bool) *Editor { e.submit = enabled e.editor.Submit = enabled return e } // Mask sets the mask rune for password fields func (e *Editor) Mask(r rune) *Editor { e.mask = r e.editor.Mask = r return e } // SetSubmitHook sets a callback for submit events func (e *Editor) SetSubmitHook(fn func(string)) *Editor { e.submitHook = fn return e } // SetChangeHook sets a callback for change events func (e *Editor) SetChangeHook(fn func(string)) *Editor { e.changeHook = fn return e } // SetFocusHook sets a callback for focus events func (e *Editor) SetFocusHook(fn func(bool)) *Editor { e.focusHook = fn return e } // SetFocus is an alias for SetFocusHook for API compatibility func (e *Editor) SetFocus(fn func(bool)) *Editor { return e.SetFocusHook(fn) } // SetSubmit sets the submit callback func (e *Editor) SetSubmit(fn func(string)) *Editor { e.submitHook = fn return e } // SetChange sets the change callback func (e *Editor) SetChange(fn func(string)) *Editor { e.changeHook = fn return e } // Focus requests focus for the editor (deferred until next layout) func (e *Editor) Focus() { e.requestFocus = true } // FocusNow requests immediate focus for the editor (requires gtx) func (e *Editor) FocusNow(gtx l.Context) { gtx.Execute(key.FocusCmd{Tag: e.editor}) } // Focused returns whether the editor has focus func (e *Editor) Focused(gtx l.Context) bool { return gtx.Focused(e.editor) } // Len returns the length of the editor contents func (e *Editor) Len() int { return e.editor.Len() } // Text returns the editor contents as a string func (e *Editor) Text() string { return e.editor.Text() } // SetText replaces the editor contents func (e *Editor) SetText(s string) { e.editor.SetText(s) } // Insert inserts text at the caret func (e *Editor) Insert(s string) { e.editor.Insert(s) } // Delete deletes runes from the editor func (e *Editor) Delete(runes int) { e.editor.Delete(runes) } // MoveCaret moves the caret func (e *Editor) MoveCaret(start, end int) { e.editor.MoveCaret(start, end) } // Selection returns the current selection func (e *Editor) Selection() (start, end int) { return e.editor.Selection() } // SetSelection sets the selection func (e *Editor) SetSelection(start, end int) { e.editor.SetCaret(start, end) } // SelectedText returns the selected text func (e *Editor) SelectedText() string { return e.editor.SelectedText() } // ClearSelection clears the selection func (e *Editor) ClearSelection() { s, _ := e.editor.Selection() e.editor.SetCaret(s, s) } // Layout lays out the editor with default materials func (e *Editor) Layout(gtx l.Context, sh *text.Shaper, fnt font.Font, size unit.Sp, textColor, selectColor color.NRGBA) l.Dimensions { // Process pending focus request if e.requestFocus { gtx.Execute(key.FocusCmd{Tag: e.editor}) e.requestFocus = false } // Process events and call hooks for { ev, ok := e.editor.Update(gtx) if !ok { break } switch ev := ev.(type) { case widget.ChangeEvent: if e.changeHook != nil { e.changeHook(e.Text()) } case widget.SubmitEvent: if e.submitHook != nil { e.submitHook(ev.Text) } case widget.SelectEvent: // Selection changed } } // Track focus changes focused := gtx.Focused(e.editor) if focused != e.focused { e.focused = focused if e.focusHook != nil { e.focusHook(focused) } } // Create material for text textM := op.Record(gtx.Ops) paint.ColorOp{Color: textColor}.Add(gtx.Ops) textMaterial := textM.Stop() // Create material for selection selectM := op.Record(gtx.Ops) paint.ColorOp{Color: selectColor}.Add(gtx.Ops) selectMaterial := selectM.Stop() return e.editor.Layout(gtx, sh, fnt, size, textMaterial, selectMaterial) } // Caret returns the caret position in pixels func (e *Editor) Caret() f32.Point { return e.editor.CaretCoords() } // CaretPos returns the caret line and column func (e *Editor) CaretPos() (line, col int) { return e.editor.CaretPos() } // screenPos describes a character position type screenPos image.Point func (p1 screenPos) LessOrEqual(p2 screenPos) bool { return p1.Y < p2.Y || (p1.Y == p2.Y && p1.X <= p2.X) } func (p1 screenPos) Less(p2 screenPos) bool { return p1.Y < p2.Y || (p1.Y == p2.Y && p1.X < p2.X) } // Blank imports to satisfy any potential dependencies var ( _ = key.Event{} _ time.Time _ color.NRGBA )