package cview
import (
"strings"
"sync"
"github.com/gdamore/tcell/v2"
"github.com/mattn/go-runewidth"
)
type DropDownOption struct {
text string
selected func (index int , option *DropDownOption )
reference interface {}
sync .RWMutex
}
func NewDropDownOption (text string ) *DropDownOption {
return &DropDownOption {text : text }
}
func (d *DropDownOption ) GetText () string {
d .RLock ()
defer d .RUnlock ()
return d .text
}
func (d *DropDownOption ) SetText (text string ) {
d .text = text
}
func (d *DropDownOption ) SetSelectedFunc (handler func (index int , option *DropDownOption )) {
d .selected = handler
}
func (d *DropDownOption ) GetReference () interface {} {
d .RLock ()
defer d .RUnlock ()
return d .reference
}
func (d *DropDownOption ) SetReference (reference interface {}) {
d .reference = reference
}
type DropDown struct {
*Box
options []*DropDownOption
optionPrefix, optionSuffix string
currentOption int
currentOptionPrefix, currentOptionSuffix string
noSelection string
open bool
prefix string
list *List
label string
labelColor tcell .Color
labelColorFocused tcell .Color
fieldBackgroundColor tcell .Color
fieldBackgroundColorFocused tcell .Color
fieldTextColor tcell .Color
fieldTextColorFocused tcell .Color
prefixTextColor tcell .Color
labelWidth int
fieldWidth int
done func (tcell .Key )
finished func (tcell .Key )
selected func (index int , option *DropDownOption )
dragging bool
abbreviationChars string
dropDownSymbol rune
dropDownOpenSymbol rune
dropDownSelectedSymbol rune
alwaysDrawDropDownSymbol bool
sync .RWMutex
}
func NewDropDown () *DropDown {
list := NewList ()
list .ShowSecondaryText (false )
list .SetMainTextColor (Styles .SecondaryTextColor )
list .SetSelectedTextColor (Styles .PrimitiveBackgroundColor )
list .SetSelectedBackgroundColor (Styles .PrimaryTextColor )
list .SetHighlightFullLine (true )
list .SetBackgroundColor (Styles .ContrastBackgroundColor )
d := &DropDown {
Box : NewBox (),
currentOption : -1 ,
list : list ,
labelColor : Styles .SecondaryTextColor ,
fieldBackgroundColor : Styles .MoreContrastBackgroundColor ,
fieldTextColor : Styles .PrimaryTextColor ,
prefixTextColor : Styles .ContrastSecondaryTextColor ,
dropDownSymbol : Styles .DropDownSymbol ,
dropDownOpenSymbol : Styles .DropDownOpenSymbol ,
dropDownSelectedSymbol : Styles .DropDownSelectedSymbol ,
abbreviationChars : Styles .DropDownAbbreviationChars ,
labelColorFocused : ColorUnset ,
fieldBackgroundColorFocused : ColorUnset ,
fieldTextColorFocused : ColorUnset ,
}
if sym := d .dropDownSelectedSymbol ; sym != 0 {
list .SetIndicators (" " +string (sym )+" " , "" , " " , "" )
}
d .focus = d
return d
}
func (d *DropDown ) SetDropDownSymbolRune (symbol rune ) {
d .Lock ()
defer d .Unlock ()
d .dropDownSymbol = symbol
}
func (d *DropDown ) SetDropDownOpenSymbolRune (symbol rune ) {
d .Lock ()
defer d .Unlock ()
d .dropDownOpenSymbol = symbol
if symbol != 0 {
d .list .SetIndicators (" " +string (symbol )+" " , "" , " " , "" )
} else {
d .list .SetIndicators ("" , "" , "" , "" )
}
}
func (d *DropDown ) SetDropDownSelectedSymbolRune (symbol rune ) {
d .Lock ()
defer d .Unlock ()
d .dropDownSelectedSymbol = symbol
}
func (d *DropDown ) SetAlwaysDrawDropDownSymbol (alwaysDraw bool ) {
d .Lock ()
defer d .Unlock ()
d .alwaysDrawDropDownSymbol = alwaysDraw
}
func (d *DropDown ) SetCurrentOption (index int ) {
d .Lock ()
if index >= 0 && index < len (d .options ) {
d .currentOption = index
d .list .SetCurrentItem (index )
if d .selected != nil {
d .Unlock ()
d .selected (index , d .options [index ])
d .Lock ()
}
if d .options [index ].selected != nil {
d .Unlock ()
d .options [index ].selected (index , d .options [index ])
d .Lock ()
}
} else {
d .currentOption = -1
d .list .SetCurrentItem (0 )
if d .selected != nil {
d .Unlock ()
d .selected (-1 , nil )
d .Lock ()
}
}
d .Unlock ()
}
func (d *DropDown ) GetCurrentOption () (int , *DropDownOption ) {
d .RLock ()
defer d .RUnlock ()
var option *DropDownOption
if d .currentOption >= 0 && d .currentOption < len (d .options ) {
option = d .options [d .currentOption ]
}
return d .currentOption , option
}
func (d *DropDown ) SetTextOptions (prefix , suffix , currentPrefix , currentSuffix , noSelection string ) {
d .Lock ()
defer d .Unlock ()
d .currentOptionPrefix = currentPrefix
d .currentOptionSuffix = currentSuffix
d .noSelection = noSelection
d .optionPrefix = prefix
d .optionSuffix = suffix
for index := 0 ; index < d .list .GetItemCount (); index ++ {
d .list .SetItemText (index , prefix +d .options [index ].text +suffix , "" )
}
}
func (d *DropDown ) SetLabel (label string ) {
d .Lock ()
defer d .Unlock ()
d .label = label
}
func (d *DropDown ) GetLabel () string {
d .RLock ()
defer d .RUnlock ()
return d .label
}
func (d *DropDown ) SetLabelWidth (width int ) {
d .Lock ()
defer d .Unlock ()
d .labelWidth = width
}
func (d *DropDown ) SetLabelColor (color tcell .Color ) {
d .Lock ()
defer d .Unlock ()
d .labelColor = color
}
func (d *DropDown ) SetLabelColorFocused (color tcell .Color ) {
d .Lock ()
defer d .Unlock ()
d .labelColorFocused = color
}
func (d *DropDown ) SetFieldBackgroundColor (color tcell .Color ) {
d .Lock ()
defer d .Unlock ()
d .fieldBackgroundColor = color
}
func (d *DropDown ) SetFieldBackgroundColorFocused (color tcell .Color ) {
d .Lock ()
defer d .Unlock ()
d .fieldBackgroundColorFocused = color
}
func (d *DropDown ) SetFieldTextColor (color tcell .Color ) {
d .Lock ()
defer d .Unlock ()
d .fieldTextColor = color
}
func (d *DropDown ) SetFieldTextColorFocused (color tcell .Color ) {
d .Lock ()
defer d .Unlock ()
d .fieldTextColorFocused = color
}
func (d *DropDown ) SetDropDownTextColor (color tcell .Color ) {
d .Lock ()
defer d .Unlock ()
d .list .SetMainTextColor (color )
}
func (d *DropDown ) SetDropDownBackgroundColor (color tcell .Color ) {
d .Lock ()
defer d .Unlock ()
d .list .SetBackgroundColor (color )
}
func (d *DropDown ) SetDropDownSelectedTextColor (color tcell .Color ) {
d .Lock ()
defer d .Unlock ()
d .list .SetSelectedTextColor (color )
}
func (d *DropDown ) SetDropDownSelectedBackgroundColor (color tcell .Color ) {
d .Lock ()
defer d .Unlock ()
d .list .SetSelectedBackgroundColor (color )
}
func (d *DropDown ) SetPrefixTextColor (color tcell .Color ) {
d .Lock ()
defer d .Unlock ()
d .prefixTextColor = color
}
func (d *DropDown ) SetFieldWidth (width int ) {
d .Lock ()
defer d .Unlock ()
d .fieldWidth = width
}
func (d *DropDown ) GetFieldHeight () int {
return 1
}
func (d *DropDown ) GetFieldWidth () int {
d .RLock ()
defer d .RUnlock ()
return d .getFieldWidth ()
}
func (d *DropDown ) getFieldWidth () int {
if d .fieldWidth > 0 {
return d .fieldWidth
}
fieldWidth := 0
for _ , option := range d .options {
width := TaggedStringWidth (option .text )
if width > fieldWidth {
fieldWidth = width
}
}
fieldWidth += len (d .optionPrefix ) + len (d .optionSuffix )
fieldWidth += len (d .currentOptionPrefix ) + len (d .currentOptionSuffix )
fieldWidth += 4
return fieldWidth
}
func (d *DropDown ) AddOptionsSimple (options ...string ) {
optionsToAdd := make ([]*DropDownOption , len (options ))
for i , option := range options {
optionsToAdd [i ] = NewDropDownOption (option )
}
d .AddOptions (optionsToAdd ...)
}
func (d *DropDown ) AddOptions (options ...*DropDownOption ) {
d .Lock ()
defer d .Unlock ()
d .addOptions (options ...)
}
func (d *DropDown ) addOptions (options ...*DropDownOption ) {
d .options = append (d .options , options ...)
for _ , option := range options {
d .list .AddItem (NewListItem (d .optionPrefix + option .text + d .optionSuffix ))
}
}
func (d *DropDown ) SetOptionsSimple (selected func (index int , option *DropDownOption ), options ...string ) {
optionsToSet := make ([]*DropDownOption , len (options ))
for i , option := range options {
optionsToSet [i ] = NewDropDownOption (option )
}
d .SetOptions (selected , optionsToSet ...)
}
func (d *DropDown ) SetOptions (selected func (index int , option *DropDownOption ), options ...*DropDownOption ) {
d .Lock ()
defer d .Unlock ()
d .list .Clear ()
d .options = nil
d .addOptions (options ...)
d .selected = selected
}
func (d *DropDown ) ClearOptions () {
d .Lock ()
defer d .Unlock ()
d .list .Clear ()
d .options = nil
}
func (d *DropDown ) SetChangedFunc (handler func (index int , option *DropDownOption )) {
d .list .SetChangedFunc (func (index int , item *ListItem ) {
handler (index , d .options [index ])
})
}
func (d *DropDown ) SetSelectedFunc (handler func (index int , option *DropDownOption )) {
d .Lock ()
defer d .Unlock ()
d .selected = handler
}
func (d *DropDown ) SetDoneFunc (handler func (key tcell .Key )) {
d .Lock ()
defer d .Unlock ()
d .done = handler
}
func (d *DropDown ) SetFinishedFunc (handler func (key tcell .Key )) {
d .Lock ()
defer d .Unlock ()
d .finished = handler
}
func (d *DropDown ) Draw (screen tcell .Screen ) {
d .Box .Draw (screen )
hasFocus := d .GetFocusable ().HasFocus ()
d .Lock ()
defer d .Unlock ()
labelColor := d .labelColor
fieldBackgroundColor := d .fieldBackgroundColor
fieldTextColor := d .fieldTextColor
if hasFocus {
if d .labelColorFocused != ColorUnset {
labelColor = d .labelColorFocused
}
if d .fieldBackgroundColorFocused != ColorUnset {
fieldBackgroundColor = d .fieldBackgroundColorFocused
}
if d .fieldTextColorFocused != ColorUnset {
fieldTextColor = d .fieldTextColorFocused
}
}
x , y , width , height := d .GetInnerRect ()
rightLimit := x + width
if height < 1 || rightLimit <= x {
return
}
if d .labelWidth > 0 {
labelWidth := d .labelWidth
if labelWidth > rightLimit -x {
labelWidth = rightLimit - x
}
Print (screen , []byte (d .label ), x , y , labelWidth , AlignLeft , labelColor )
x += labelWidth
} else {
_ , drawnWidth := Print (screen , []byte (d .label ), x , y , rightLimit -x , AlignLeft , labelColor )
x += drawnWidth
}
maxWidth := 0
optionWrapWidth := TaggedStringWidth (d .optionPrefix + d .optionSuffix )
for _ , option := range d .options {
strWidth := TaggedStringWidth (option .text ) + optionWrapWidth
if strWidth > maxWidth {
maxWidth = strWidth
}
}
fieldWidth := d .getFieldWidth ()
if fieldWidth == 0 {
fieldWidth = maxWidth
if d .currentOption < 0 {
noSelectionWidth := TaggedStringWidth (d .noSelection )
if noSelectionWidth > fieldWidth {
fieldWidth = noSelectionWidth
}
} else if d .currentOption < len (d .options ) {
currentOptionWidth := TaggedStringWidth (d .currentOptionPrefix + d .options [d .currentOption ].text + d .currentOptionSuffix )
if currentOptionWidth > fieldWidth {
fieldWidth = currentOptionWidth
}
}
}
if rightLimit -x < fieldWidth {
fieldWidth = rightLimit - x
}
fieldStyle := tcell .StyleDefault .Background (fieldBackgroundColor )
for index := 0 ; index < fieldWidth ; index ++ {
screen .SetContent (x +index , y , ' ' , nil , fieldStyle )
}
if d .open && len (d .prefix ) > 0 {
currentOptionPrefixWidth := TaggedStringWidth (d .currentOptionPrefix )
prefixWidth := runewidth .StringWidth (d .prefix )
listItemText := d .options [d .list .GetCurrentItemIndex ()].text
Print (screen , []byte (d .currentOptionPrefix ), x , y , fieldWidth , AlignLeft , fieldTextColor )
Print (screen , []byte (d .prefix ), x +currentOptionPrefixWidth , y , fieldWidth -currentOptionPrefixWidth , AlignLeft , d .prefixTextColor )
if len (d .prefix ) < len (listItemText ) {
Print (screen , []byte (listItemText [len (d .prefix ):]+d .currentOptionSuffix ), x +prefixWidth +currentOptionPrefixWidth , y , fieldWidth -prefixWidth -currentOptionPrefixWidth , AlignLeft , fieldTextColor )
}
} else {
color := fieldTextColor
text := d .noSelection
if d .currentOption >= 0 && d .currentOption < len (d .options ) {
text = d .currentOptionPrefix + d .options [d .currentOption ].text + d .currentOptionSuffix
}
if fieldWidth > len (d .abbreviationChars )+3 && len (text ) > fieldWidth {
text = text [0 :fieldWidth -3 -len (d .abbreviationChars )] + d .abbreviationChars
}
Print (screen , []byte (text ), x , y , fieldWidth , AlignLeft , color )
}
if d .alwaysDrawDropDownSymbol || d ._hasFocus () {
symbol := d .dropDownSymbol
if d .open {
symbol = d .dropDownOpenSymbol
}
screen .SetContent (x +fieldWidth -2 , y , symbol , nil , new (tcell .Style ).Foreground (fieldTextColor ).Background (fieldBackgroundColor ))
}
if hasFocus && d .open {
lx := x
ly := y + 1
lheight := len (d .options )
_ , sheight := screen .Size ()
if ly +lheight >= sheight && ly -2 > lheight -ly {
ly = y - lheight
if ly < 0 {
ly = 0
}
}
if ly +lheight >= sheight {
lheight = sheight - ly
}
lwidth := maxWidth
if d .list .scrollBarVisibility == ScrollBarAlways || (d .list .scrollBarVisibility == ScrollBarAuto && len (d .options ) > lheight ) {
lwidth ++
}
if lwidth < fieldWidth {
lwidth = fieldWidth
}
d .list .SetRect (lx , ly , lwidth , lheight )
d .list .Draw (screen )
}
}
func (d *DropDown ) InputHandler () func (event *tcell .EventKey , setFocus func (p Primitive )) {
return d .WrapInputHandler (func (event *tcell .EventKey , setFocus func (p Primitive )) {
switch key := event .Key (); key {
case tcell .KeyEnter , tcell .KeyRune , tcell .KeyDown :
d .Lock ()
defer d .Unlock ()
d .prefix = ""
if r := event .Rune (); key == tcell .KeyRune && r != ' ' {
d .prefix += string (r )
d .evalPrefix ()
}
d .openList (setFocus )
case tcell .KeyEscape , tcell .KeyTab , tcell .KeyBacktab :
if d .done != nil {
d .done (key )
}
if d .finished != nil {
d .finished (key )
}
}
})
}
func (d *DropDown ) evalPrefix () {
if len (d .prefix ) > 0 {
for index , option := range d .options {
if strings .HasPrefix (strings .ToLower (option .text ), d .prefix ) {
d .list .SetCurrentItem (index )
return
}
}
r := []rune (d .prefix )
d .prefix = string (r [:len (r )-1 ])
}
}
func (d *DropDown ) openList (setFocus func (Primitive )) {
d .open = true
optionBefore := d .currentOption
d .list .SetSelectedFunc (func (index int , item *ListItem ) {
if d .dragging {
return
}
d .currentOption = index
d .closeList (setFocus )
if d .selected != nil {
d .selected (d .currentOption , d .options [d .currentOption ])
}
if d .options [d .currentOption ].selected != nil {
d .options [d .currentOption ].selected (d .currentOption , d .options [d .currentOption ])
}
})
d .list .SetInputCapture (func (event *tcell .EventKey ) *tcell .EventKey {
if event .Key () == tcell .KeyRune {
d .prefix += string (event .Rune ())
d .evalPrefix ()
} else if event .Key () == tcell .KeyBackspace || event .Key () == tcell .KeyBackspace2 {
if len (d .prefix ) > 0 {
r := []rune (d .prefix )
d .prefix = string (r [:len (r )-1 ])
}
d .evalPrefix ()
} else if event .Key () == tcell .KeyEscape {
d .currentOption = optionBefore
d .list .SetCurrentItem (d .currentOption )
d .closeList (setFocus )
if d .selected != nil {
if d .currentOption > -1 {
d .selected (d .currentOption , d .options [d .currentOption ])
}
}
} else {
d .prefix = ""
}
return event
})
setFocus (d .list )
}
func (d *DropDown ) closeList (setFocus func (Primitive )) {
d .open = false
if d .list .HasFocus () {
setFocus (d )
}
}
func (d *DropDown ) Focus (delegate func (p Primitive )) {
d .Box .Focus (delegate )
if d .open {
delegate (d .list )
}
}
func (d *DropDown ) HasFocus () bool {
d .RLock ()
defer d .RUnlock ()
return d ._hasFocus ()
}
func (d *DropDown ) _hasFocus () bool {
if d .open {
return d .list .HasFocus ()
}
return d .hasFocus
}
func (d *DropDown ) MouseHandler () func (action MouseAction , event *tcell .EventMouse , setFocus func (p Primitive )) (consumed bool , capture Primitive ) {
return d .WrapMouseHandler (func (action MouseAction , event *tcell .EventMouse , setFocus func (p Primitive )) (consumed bool , capture Primitive ) {
x , y := event .Position ()
_ , rectY , _ , _ := d .GetInnerRect ()
inRect := y == rectY
if !d .open && !inRect {
return d .InRect (x , y ), nil
}
switch action {
case MouseLeftDown :
consumed = d .open || inRect
capture = d
if !d .open {
d .openList (setFocus )
d .dragging = true
} else if consumed , _ := d .list .MouseHandler ()(MouseLeftClick , event , setFocus ); !consumed {
d .closeList (setFocus )
}
case MouseMove :
if d .dragging {
d .list .MouseHandler ()(MouseLeftClick , event , setFocus )
consumed = true
capture = d
}
case MouseLeftUp :
if d .dragging {
d .dragging = false
d .list .MouseHandler ()(MouseLeftClick , event , setFocus )
consumed = true
}
}
return
})
}
The pages are generated with Golds v0.8.2 . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds .