A lot of layout work
This commit is contained in:
318
wdgt_searcher.go
Normal file
318
wdgt_searcher.go
Normal file
@@ -0,0 +1,318 @@
|
||||
/*
|
||||
Copyright © Brian Buller <brian@bullercodeworks.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
*/
|
||||
package widgets
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
wh "git.bullercodeworks.com/brian/tcell-widgets/helpers"
|
||||
"github.com/gdamore/tcell"
|
||||
)
|
||||
|
||||
type Searcher struct {
|
||||
id string
|
||||
style tcell.Style
|
||||
|
||||
x, y int
|
||||
w, h int
|
||||
active bool
|
||||
visible bool
|
||||
tabbable bool
|
||||
|
||||
title string
|
||||
search *Field
|
||||
data []string
|
||||
filteredData []string
|
||||
cursor int
|
||||
|
||||
selectFunc func(idx int, s string) bool
|
||||
hideOnSelect bool
|
||||
logger func(string, ...any)
|
||||
|
||||
keyMap KeyMap
|
||||
}
|
||||
|
||||
var _ Widget = (*Searcher)(nil)
|
||||
|
||||
func NewSearcher(id string, style tcell.Style) *Searcher {
|
||||
ret := &Searcher{
|
||||
search: NewField(fmt.Sprintf("%s-searcher-field", id), style),
|
||||
}
|
||||
ret.Init(id, style)
|
||||
return ret
|
||||
}
|
||||
|
||||
func (w *Searcher) Init(id string, style tcell.Style) {
|
||||
w.id = id
|
||||
w.style = style
|
||||
w.visible = true
|
||||
w.search.SetOnChange(func(prev, curr string) {
|
||||
w.updateFilter()
|
||||
})
|
||||
w.keyMap = NewKeyMap(map[tcell.Key]func(ev *tcell.EventKey) bool{
|
||||
tcell.KeyUp: w.handleKeyUp,
|
||||
tcell.KeyDown: w.handleKeyDown,
|
||||
tcell.KeyHome: w.handleKeyHome,
|
||||
tcell.KeyEnd: w.handleKeyEnd,
|
||||
tcell.KeyPgUp: w.handleKeyPgUp,
|
||||
tcell.KeyPgDn: w.handleKeyPgDn,
|
||||
tcell.KeyEnter: w.handleKeyEnter,
|
||||
})
|
||||
w.tabbable = true
|
||||
}
|
||||
|
||||
func (w *Searcher) Id() string { return w.id }
|
||||
func (w *Searcher) HandleResize(ev *tcell.EventResize) {
|
||||
w.w, w.h = ev.Size()
|
||||
|
||||
// w.w = wh.Min(w.w, w.WantW())
|
||||
// w.h = wh.Min(w.h, w.WantH())
|
||||
// TODO: Verify this is fine:
|
||||
w.search.HandleResize(ev)
|
||||
}
|
||||
|
||||
func (w *Searcher) HandleKey(ev *tcell.EventKey) bool {
|
||||
if !w.active {
|
||||
return false
|
||||
} else if ok := w.keyMap.Handle(ev); ok {
|
||||
return true
|
||||
}
|
||||
return w.search.HandleKey(ev)
|
||||
}
|
||||
|
||||
func (w *Searcher) handleKeyUp(ev *tcell.EventKey) bool {
|
||||
w.cursor = ((w.cursor - 1) + len(w.filteredData)) % len(w.filteredData)
|
||||
return true
|
||||
}
|
||||
|
||||
func (w *Searcher) handleKeyDown(ev *tcell.EventKey) bool {
|
||||
w.cursor = ((w.cursor + 1) + len(w.filteredData)) % len(w.filteredData)
|
||||
return true
|
||||
}
|
||||
|
||||
func (w *Searcher) handleKeyHome(ev *tcell.EventKey) bool {
|
||||
if w.cursor == 0 {
|
||||
return false
|
||||
}
|
||||
w.cursor = 0
|
||||
return true
|
||||
}
|
||||
|
||||
func (w *Searcher) handleKeyEnd(ev *tcell.EventKey) bool {
|
||||
if w.cursor == len(w.filteredData)-1 {
|
||||
return false
|
||||
}
|
||||
w.cursor = len(w.filteredData) - 1
|
||||
return true
|
||||
}
|
||||
|
||||
func (w *Searcher) handleKeyPgUp(ev *tcell.EventKey) bool {
|
||||
if w.cursor == 0 {
|
||||
return false
|
||||
}
|
||||
w.cursor -= w.h
|
||||
if w.cursor < 0 {
|
||||
w.cursor = 0
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (w *Searcher) handleKeyPgDn(ev *tcell.EventKey) bool {
|
||||
mx := len(w.filteredData) - 1
|
||||
if w.cursor == mx {
|
||||
return false
|
||||
}
|
||||
w.cursor += w.h
|
||||
if w.cursor > mx {
|
||||
w.cursor = mx
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (w *Searcher) handleKeyEnter(ev *tcell.EventKey) bool {
|
||||
if w.hideOnSelect {
|
||||
w.visible = false
|
||||
}
|
||||
if w.selectFunc != nil {
|
||||
// Figure out our true index
|
||||
var idx int
|
||||
selV := w.filteredData[w.cursor]
|
||||
for i := range w.data {
|
||||
if w.data[i] == selV {
|
||||
idx = i
|
||||
}
|
||||
}
|
||||
return w.selectFunc(idx, selV)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (w *Searcher) HandleTime(ev *tcell.EventTime) {}
|
||||
func (w *Searcher) Draw(screen tcell.Screen) {
|
||||
if !w.visible {
|
||||
return
|
||||
}
|
||||
w.search.SetPos(Coord{X: w.x + 1, Y: w.y + 1})
|
||||
w.search.SetSize(Coord{X: w.w - 2, Y: 1})
|
||||
dStyle := w.style
|
||||
if !w.active {
|
||||
dStyle = dStyle.Dim(true)
|
||||
}
|
||||
if len(w.title) > 0 {
|
||||
wh.TitledBorderFilled(w.x, w.y, w.x+w.w, w.y+w.h, w.title, wh.BRD_CSIMPLE, dStyle, screen)
|
||||
} else {
|
||||
wh.BorderFilled(w.x, w.y, w.x+w.w, w.y+w.h, wh.BRD_CSIMPLE, dStyle, screen)
|
||||
}
|
||||
w.GetPos().DrawOffset(w.search, screen)
|
||||
x, y := w.x+1, w.y+2
|
||||
var stIdx int
|
||||
if w.cursor > w.h/2 {
|
||||
stIdx = w.cursor - (w.h / 2)
|
||||
}
|
||||
fD := len(w.filteredData)
|
||||
if w.cursor+w.h/2 > fD {
|
||||
stIdx = fD - w.h/2
|
||||
}
|
||||
stIdx = wh.Max(stIdx, 0)
|
||||
for i := stIdx; i < fD; i++ {
|
||||
st := dStyle
|
||||
if i == w.cursor {
|
||||
st = st.Reverse(true)
|
||||
}
|
||||
wh.DrawText(x, y, w.filteredData[i], st, screen)
|
||||
y++
|
||||
if y >= w.y+w.h {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Searcher) Active() bool { return w.active }
|
||||
func (w *Searcher) SetActive(a bool) {
|
||||
w.active = a
|
||||
w.search.SetActive(a)
|
||||
}
|
||||
func (w *Searcher) Visible() bool { return w.visible }
|
||||
func (w *Searcher) SetVisible(a bool) { w.visible = a }
|
||||
func (w *Searcher) SetX(x int) { w.x = x }
|
||||
func (w *Searcher) SetY(y int) { w.y = y }
|
||||
func (w *Searcher) GetX() int { return w.x }
|
||||
func (w *Searcher) GetY() int { return w.y }
|
||||
|
||||
func (w *Searcher) WantW() int {
|
||||
ret := 2 + w.search.WantW()
|
||||
var maxData int
|
||||
for i := range w.filteredData {
|
||||
maxData = wh.Max(maxData, len(w.filteredData[i]))
|
||||
}
|
||||
return ret + maxData
|
||||
}
|
||||
|
||||
func (w *Searcher) WantH() int {
|
||||
return 2 + w.search.WantH() + len(w.filteredData) // Border + Field + Data
|
||||
}
|
||||
|
||||
func (w *Searcher) GetPos() Coord { return Coord{X: w.x, Y: w.y} }
|
||||
func (w *Searcher) SetPos(c Coord) { w.x, w.y = c.X, c.Y }
|
||||
func (w *Searcher) SetW(x int) { w.w = x }
|
||||
func (w *Searcher) SetH(y int) { w.h = y }
|
||||
func (w *Searcher) GetW() int { return w.w }
|
||||
func (w *Searcher) GetH() int { return w.y }
|
||||
func (w *Searcher) SetSize(c Coord) { w.w, w.h = c.X, c.Y }
|
||||
func (w *Searcher) Focusable() bool { return true }
|
||||
func (w *Searcher) SetTabbable(b bool) { w.tabbable = b }
|
||||
func (w *Searcher) Tabbable() bool { return w.tabbable }
|
||||
func (w *Searcher) MinW() int {
|
||||
return 2 + w.search.MinW()
|
||||
}
|
||||
|
||||
func (w *Searcher) MinH() int {
|
||||
return 2 + w.search.MinH() + 5
|
||||
}
|
||||
|
||||
func (w *Searcher) SetHideOnSelect(t bool) { w.hideOnSelect = t }
|
||||
func (w *Searcher) SetTitle(ttl string) { w.title = ttl }
|
||||
func (w *Searcher) SetData(data []string) {
|
||||
w.data = data
|
||||
w.updateFilter()
|
||||
}
|
||||
|
||||
func (w *Searcher) updateFilter() {
|
||||
var selVal string
|
||||
data := []string{}
|
||||
copy(data, w.filteredData)
|
||||
if len(data) > 0 && len(data) > w.cursor {
|
||||
selVal = data[w.cursor]
|
||||
}
|
||||
filter := w.search.Value()
|
||||
cS := filter != strings.ToLower(filter)
|
||||
w.filteredData = []string{}
|
||||
for i := range w.data {
|
||||
if cS {
|
||||
if strings.Contains(w.data[i], filter) {
|
||||
w.filteredData = append(w.filteredData, w.data[i])
|
||||
}
|
||||
} else {
|
||||
if strings.Contains(strings.ToLower(w.data[i]), filter) {
|
||||
w.filteredData = append(w.filteredData, w.data[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
for i := range w.filteredData {
|
||||
if w.filteredData[i] == selVal {
|
||||
w.cursor = i
|
||||
return
|
||||
}
|
||||
}
|
||||
if w.cursor > len(w.filteredData) {
|
||||
w.cursor = len(w.filteredData) - 1
|
||||
return
|
||||
}
|
||||
w.cursor = 0
|
||||
}
|
||||
|
||||
func (w *Searcher) SelectedValue() string {
|
||||
return w.filteredData[w.cursor]
|
||||
}
|
||||
|
||||
func (w *Searcher) SetSearchValue(val string) {
|
||||
w.search.SetValue(val)
|
||||
}
|
||||
|
||||
func (w *Searcher) SetSelectFunc(f func(idx int, s string) bool) {
|
||||
w.selectFunc = f
|
||||
}
|
||||
|
||||
func (w *Searcher) ClearSearch() {
|
||||
w.cursor = 0
|
||||
w.filteredData = w.data
|
||||
w.search.SetValue("")
|
||||
}
|
||||
|
||||
func (w *Searcher) SetLogger(l func(string, ...any)) { w.logger = l }
|
||||
func (w *Searcher) Log(txt string, args ...any) {
|
||||
if w.logger != nil {
|
||||
w.logger(txt, args)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user