310 lines
7.7 KiB
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)
|
|
}
|