ui/listbox.go

310 lines
7.7 KiB
Go

package ui
import (
"fmt"
"git.kirsle.net/go/render"
"git.kirsle.net/go/ui/style"
)
// ListBox is a selectable list of values like a multi-line SelectBox.
type ListBox struct {
*Frame
name string
children []*ListValue
style *style.ListBox
supervisor *Supervisor
list *Frame
scrollbar *ScrollBar
scrollFraction float64
maxHeight int
// Variable bindings: give these pointers to your values.
Variable interface{} // pointer to e.g. a string or int
// TextVariable *string // string value
// IntVariable *int // integer value
}
// ListValue is an item in the ListBox. It has an arbitrary widget as a
// "label" (usually a Label) and a value (string or int) when it's "selected"
type ListValue struct {
Frame *Frame
Label Widget
Value interface{}
}
// NewListBox creates a new ListBox.
func NewListBox(name string, config ListBox) *ListBox {
w := &ListBox{
Frame: NewFrame(name + " Frame"),
list: NewFrame(name + " List"),
name: name,
children: []*ListValue{},
Variable: config.Variable,
// TextVariable: config.TextVariable,
// IntVariable: config.IntVariable,
style: &style.DefaultListBox,
}
// if config.Width > 0 && config.Height > 0 {
// w.Frame.Resize(render.NewRect(config.Width, config.Height))
// }
w.IDFunc(func() string {
return fmt.Sprintf("ListBox<%s>", name)
})
w.SetStyle(Theme.ListBox)
w.setup()
return w
}
// SetStyle sets the listbox style.
func (w *ListBox) SetStyle(v *style.ListBox) {
if v == nil {
v = &style.DefaultListBox
}
w.style = v
fmt.Printf("set style: %+v\n", v)
w.Frame.Configure(Config{
BorderSize: w.style.BorderSize,
BorderStyle: BorderStyle(w.style.BorderStyle),
Background: w.style.Background,
})
// If the child is a Label, apply the foreground color.
// if label, ok := w.child.(*Label); ok {
// label.Font.Color = w.style.Foreground
// }
}
// GetStyle gets the listbox style.
func (w *ListBox) GetStyle() *style.ListBox {
return w.style
}
// Supervise the ListBox. This is necessary for granting mouse-over events
// to the items in the list.
func (w *ListBox) Supervise(s *Supervisor) {
w.supervisor = s
w.scrollbar.Supervise(s)
// Add all the list items to be supervised.
for _, c := range w.children {
w.supervisor.Add(c.Frame)
}
}
// AddLabel adds a simple text-based label to the Listbox.
// The label is the text value to display.
// The value is the underlying value (string or int) for the TextVariable or IntVariable.
// The function callback runs when the option is picked.
func (w *ListBox) AddLabel(label string, value interface{}, f func()) {
row := NewFrame(label + " Frame")
child := NewLabel(Label{
Text: label,
Font: render.Text{
Color: w.style.Foreground,
Size: 11,
Padding: 2,
},
})
row.Pack(child, Pack{
Side: W,
FillX: true,
})
// Add this label and its value mapping to the ListBox.
w.children = append(w.children, &ListValue{
Frame: row,
Label: child,
Value: value,
})
// Event handlers for the item row.
// row.Handle(MouseOver, func(ed EventData) error {
// if ed.Point.Inside(AbsoluteRect(w.scrollbar)) {
// return nil // ignore if over scrollbar
// }
// row.SetBackground(w.style.HoverBackground)
// child.Font.Color = w.style.HoverForeground
// return nil
// })
row.Handle(MouseMove, func(ed EventData) error {
if ed.Point.Inside(AbsoluteRect(w.scrollbar)) {
// we wandered onto the scrollbar, cancel mouseover
return row.Event(MouseOut, ed)
}
row.SetBackground(w.style.HoverBackground)
child.Font.Color = w.style.HoverForeground
return nil
})
row.Handle(MouseOut, func(ed EventData) error {
if cur, ok := w.GetValue(); ok && cur == value {
row.SetBackground(w.style.SelectedBackground)
child.Font.Color = w.style.SelectedForeground
} else {
fmt.Printf("couldn't get value? %+v %+v\n", cur, ok)
row.SetBackground(w.style.Background)
child.Font.Color = w.style.Foreground
}
return nil
})
row.Handle(MouseUp, func(ed EventData) error {
if cur, ok := w.GetValue(); ok && cur == value {
row.SetBackground(w.style.SelectedBackground)
child.Font.Color = w.style.SelectedForeground
} else {
row.SetBackground(w.style.Background)
child.Font.Color = w.style.Foreground
}
return nil
})
row.Handle(Click, func(ed EventData) error {
// Trigger if we are not hovering over the (overlapping) scrollbar.
if !ed.Point.Inside(AbsoluteRect(w.scrollbar)) {
w.Event(Change, EventData{
Supervisor: w.supervisor,
Value: value,
})
}
return nil
})
// Append the item into the ListBox frame.
w.Frame.Pack(row, Pack{
Side: N,
PadY: 1,
Fill: true,
})
// If the current text label isn't in the options, pick
// the first option.
if _, ok := w.GetValue(); !ok {
w.Variable = w.children[0].Value
row.SetBackground(w.style.SelectedBackground)
}
}
// TODO: RemoveItem()
// GetValue returns the currently selected item in the ListBox.
//
// Returns the SelectValue and true on success, and the Label or underlying Value
// can be read from the SelectValue struct. If no valid option is selected, the
// bool value returns false.
func (w *ListBox) GetValue() (*ListValue, bool) {
for _, row := range w.children {
if w.Variable != nil && w.Variable == row.Value {
return row, true
}
}
return nil, false
}
// SetValueByLabel sets the currently selected option to the given label.
func (w *ListBox) SetValueByLabel(label string) bool {
for _, option := range w.children {
if child, ok := option.Label.(*Label); ok && child.Text == label {
w.Variable = option.Value
return true
}
}
return false
}
// SetValue sets the currently selected option to the given value.
func (w *ListBox) SetValue(value interface{}) bool {
w.Variable = value
for _, option := range w.children {
if option.Value == value {
w.Variable = option.Value
return true
}
}
return false
}
// Compute to re-evaluate the button state (in the case of radio buttons where
// a different button will affect the state of this one when clicked).
func (w *ListBox) Compute(e render.Engine) {
w.computeVisible()
w.Frame.Compute(e)
}
// setup the UI components and event handlers.
func (w *ListBox) setup() {
// w.Configure(Config{
// BorderSize: 1,
// BorderStyle: BorderSunken,
// Background: theme.InputBackgroundColor,
// })
w.scrollbar = NewScrollBar(ScrollBar{})
w.scrollbar.Handle(Scroll, func(ed EventData) error {
fmt.Printf("Scroll event: %f%% unit %d\n", ed.ScrollFraction*100, ed.ScrollUnits)
w.scrollFraction = ed.ScrollFraction
return nil
})
w.Frame.Pack(w.scrollbar, Pack{
Side: E,
FillY: true,
Padding: 0,
})
// w.Frame.Pack(w.list, Pack{
// Side: E,
// FillY: true,
// Expand: true,
// })
}
// Compute which items of the list should be visible based on scroll position.
func (w *ListBox) computeVisible() {
if len(w.children) == 0 {
return
}
// Sample the first element's height.
var (
myHeight = w.height
maxTop = w.maxHeight - myHeight + w.children[len(w.children)-1].Frame.height
top = int(w.scrollFraction * float64(maxTop))
// itemHeight = w.children[0].Label.Size().H
)
var (
scan int
scrollFreed int
totalHeight int
)
for _, c := range w.children {
childHeight := c.Frame.Size().H + 2
if top > 0 && scan+childHeight < top {
scrollFreed += childHeight
c.Frame.Hide()
} else if scan+childHeight > myHeight+scrollFreed {
c.Frame.Hide()
} else {
c.Frame.Show()
}
scan += childHeight // for padding
totalHeight += childHeight
}
w.maxHeight = totalHeight
}
func (w *ListBox) Present(e render.Engine, p render.Point) {
w.Frame.Present(e, p)
// HACK to get the scrollbar to appear on top of the list frame :(
pos := AbsolutePosition(w.scrollbar)
// pos.X += w.BoxThickness(w.style.BorderSize / 2) // HACK
w.scrollbar.Present(e, pos)
}