2019-06-09 00:03:59 +00:00
|
|
|
package ui
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
|
2019-12-23 02:21:58 +00:00
|
|
|
"git.kirsle.net/go/render"
|
2020-06-04 07:50:06 +00:00
|
|
|
"git.kirsle.net/go/ui/theme"
|
2019-06-09 00:03:59 +00:00
|
|
|
)
|
|
|
|
|
2020-06-04 07:50:06 +00:00
|
|
|
// MenuWidth sets the width of all popup menus. TODO, widths should be automatic.
|
|
|
|
var MenuWidth = 180
|
|
|
|
|
|
|
|
// Menu is a frame that holds menu items. It is the
|
2019-06-09 00:03:59 +00:00
|
|
|
type Menu struct {
|
|
|
|
BaseWidget
|
|
|
|
Name string
|
|
|
|
|
2020-06-04 07:50:06 +00:00
|
|
|
supervisor *Supervisor
|
|
|
|
body *Frame
|
|
|
|
items []*MenuItem
|
2019-06-09 00:03:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// NewMenu creates a new Menu. It is hidden by default. Usually you'll
|
|
|
|
// use it with a MenuButton or in a right-click handler.
|
|
|
|
func NewMenu(name string) *Menu {
|
|
|
|
w := &Menu{
|
2020-06-04 07:50:06 +00:00
|
|
|
Name: name,
|
|
|
|
body: NewFrame(name + ":Body"),
|
|
|
|
items: []*MenuItem{},
|
2019-06-09 00:03:59 +00:00
|
|
|
}
|
|
|
|
w.body.Configure(Config{
|
2020-06-04 07:50:06 +00:00
|
|
|
Width: MenuWidth,
|
|
|
|
Height: 100,
|
|
|
|
BorderSize: 0,
|
2019-06-09 00:03:59 +00:00
|
|
|
BorderStyle: BorderRaised,
|
2020-06-04 07:50:06 +00:00
|
|
|
Background: theme.ButtonBackgroundColor,
|
2019-06-09 00:03:59 +00:00
|
|
|
})
|
2020-06-04 07:50:06 +00:00
|
|
|
w.body.SetParent(w)
|
2019-06-09 00:03:59 +00:00
|
|
|
w.IDFunc(func() string {
|
|
|
|
return fmt.Sprintf("Menu<%s>", w.Name)
|
|
|
|
})
|
|
|
|
return w
|
|
|
|
}
|
|
|
|
|
2020-06-04 07:50:06 +00:00
|
|
|
// Children returns the child frame of the menu.
|
|
|
|
func (w *Menu) Children() []Widget {
|
|
|
|
return []Widget{
|
|
|
|
w.body,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Supervise the Menu. This will add all current and future MenuItem widgets
|
|
|
|
// to the supervisor.
|
|
|
|
func (w *Menu) Supervise(s *Supervisor) {
|
|
|
|
w.supervisor = s
|
|
|
|
for _, item := range w.items {
|
|
|
|
w.supervisor.Add(item)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-06-09 00:03:59 +00:00
|
|
|
// Compute the menu
|
|
|
|
func (w *Menu) Compute(e render.Engine) {
|
|
|
|
w.body.Compute(e)
|
2020-03-10 00:13:33 +00:00
|
|
|
|
2020-06-04 07:50:06 +00:00
|
|
|
// TODO: ideally the Frame Pack Compute would fix the size of the body
|
|
|
|
// for the height to match the height of the menu items... but for now
|
|
|
|
// manually set the height.
|
|
|
|
var maxWidth int
|
|
|
|
var height int
|
|
|
|
for _, child := range w.body.Children() {
|
|
|
|
size := child.Size()
|
|
|
|
if size.W > maxWidth {
|
|
|
|
maxWidth = size.W
|
|
|
|
}
|
|
|
|
height += child.Size().H
|
|
|
|
}
|
|
|
|
w.body.Resize(render.NewRect(maxWidth, height))
|
|
|
|
|
2020-03-10 00:13:33 +00:00
|
|
|
// Call the BaseWidget Compute in case we have subscribers.
|
|
|
|
w.BaseWidget.Compute(e)
|
2019-06-09 00:03:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Present the menu
|
|
|
|
func (w *Menu) Present(e render.Engine, p render.Point) {
|
|
|
|
w.body.Present(e, p)
|
2020-03-10 00:13:33 +00:00
|
|
|
|
|
|
|
// Call the BaseWidget Present in case we have subscribers.
|
|
|
|
w.BaseWidget.Present(e, p)
|
2019-06-09 00:03:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// AddItem quickly adds an item to a menu.
|
|
|
|
func (w *Menu) AddItem(label string, command func()) *MenuItem {
|
2020-06-04 07:50:06 +00:00
|
|
|
menu := NewMenuItem(label, "", command)
|
|
|
|
|
|
|
|
// Add a Click handler that closes the menu when a selection is made.
|
|
|
|
menu.Handle(Click, w.menuClickHandler)
|
|
|
|
|
|
|
|
w.Pack(menu)
|
|
|
|
return menu
|
|
|
|
}
|
|
|
|
|
|
|
|
// AddItemAccel quickly adds an item to a menu with a shortcut key label.
|
|
|
|
func (w *Menu) AddItemAccel(label string, accelerator string, command func()) *MenuItem {
|
|
|
|
menu := NewMenuItem(label, accelerator, command)
|
|
|
|
|
|
|
|
// Add a Click handler that closes the menu when a selection is made.
|
|
|
|
menu.Handle(Click, w.menuClickHandler)
|
|
|
|
|
2019-06-09 00:03:59 +00:00
|
|
|
w.Pack(menu)
|
|
|
|
return menu
|
|
|
|
}
|
|
|
|
|
2020-06-04 07:50:06 +00:00
|
|
|
// Click handler for all menu items, to also close the menu behind them.
|
|
|
|
func (w *Menu) menuClickHandler(ed EventData) error {
|
|
|
|
if w.supervisor != nil {
|
|
|
|
w.supervisor.PopModal(w)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// AddSeparator adds a separator bar to the menu to delineate items.
|
|
|
|
func (w *Menu) AddSeparator() *MenuItem {
|
|
|
|
sep := NewMenuSeparator()
|
|
|
|
w.Pack(sep)
|
|
|
|
return sep
|
|
|
|
}
|
|
|
|
|
2019-06-09 00:03:59 +00:00
|
|
|
// Pack a menu item onto the menu.
|
|
|
|
func (w *Menu) Pack(item *MenuItem) {
|
2020-06-04 07:50:06 +00:00
|
|
|
w.items = append(w.items, item)
|
2019-06-09 00:03:59 +00:00
|
|
|
w.body.Pack(item, Pack{
|
2020-06-04 07:50:06 +00:00
|
|
|
Side: N,
|
2019-06-09 00:03:59 +00:00
|
|
|
FillX: true,
|
|
|
|
})
|
2020-06-04 07:50:06 +00:00
|
|
|
if w.supervisor != nil {
|
|
|
|
w.supervisor.Add(item)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Size returns the size of the menu's body.
|
|
|
|
func (w *Menu) Size() render.Rect {
|
|
|
|
return w.body.Size()
|
|
|
|
}
|
|
|
|
|
|
|
|
// Rect returns the rect of the menu's body.
|
|
|
|
func (w *Menu) Rect() render.Rect {
|
|
|
|
// TODO: the height reports wrong (0), manually add up the MenuItem sizes.
|
|
|
|
// This manifests in Supervisor.runWidgetEvents when checking if the cursor
|
|
|
|
// clicked outside the rect of the active menu modal.
|
|
|
|
rect := w.body.Rect()
|
|
|
|
rect.H = 0
|
|
|
|
for _, child := range w.body.Children() {
|
|
|
|
rect.H += child.Size().H
|
|
|
|
}
|
|
|
|
return rect
|
2019-06-09 00:03:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// MenuItem is an item in a Menu.
|
|
|
|
type MenuItem struct {
|
|
|
|
Button
|
|
|
|
Label string
|
|
|
|
Accelerator string
|
|
|
|
Command func()
|
2020-06-04 07:50:06 +00:00
|
|
|
separator bool
|
2019-06-09 00:03:59 +00:00
|
|
|
button *Button
|
2020-06-04 07:50:06 +00:00
|
|
|
|
|
|
|
// store of most recent bg color set on a menu item
|
|
|
|
cacheBg render.Color
|
|
|
|
cacheFg render.Color
|
2019-06-09 00:03:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// NewMenuItem creates a new menu item.
|
2020-06-04 07:50:06 +00:00
|
|
|
func NewMenuItem(label, accelerator string, command func()) *MenuItem {
|
2019-06-09 00:03:59 +00:00
|
|
|
w := &MenuItem{
|
2020-06-04 07:50:06 +00:00
|
|
|
Label: label,
|
|
|
|
Accelerator: accelerator,
|
|
|
|
Command: command,
|
2019-06-09 00:03:59 +00:00
|
|
|
}
|
|
|
|
w.IDFunc(func() string {
|
|
|
|
return fmt.Sprintf("MenuItem<%s>", w.Label)
|
|
|
|
})
|
|
|
|
|
|
|
|
font := DefaultFont
|
2020-06-04 07:50:06 +00:00
|
|
|
font.Color = render.Black
|
2019-06-09 00:03:59 +00:00
|
|
|
font.PadX = 12
|
2020-06-04 07:50:06 +00:00
|
|
|
font.PadY = 2
|
|
|
|
|
|
|
|
// The button child will be a Frame so we can have a left-aligned label
|
|
|
|
// and a right-aligned accelerator.
|
|
|
|
frame := NewFrame(label + ":Frame")
|
|
|
|
frame.Configure(Config{
|
|
|
|
Width: MenuWidth,
|
2019-06-09 00:03:59 +00:00
|
|
|
})
|
2020-06-04 07:50:06 +00:00
|
|
|
{
|
|
|
|
// Left of frame: menu item label
|
|
|
|
lbl := NewLabel(Label{
|
|
|
|
Text: label,
|
|
|
|
Font: font,
|
|
|
|
})
|
|
|
|
frame.Pack(lbl, Pack{
|
|
|
|
Side: W,
|
|
|
|
})
|
|
|
|
|
|
|
|
// On the right: accelerator shortcut key
|
|
|
|
if accelerator != "" {
|
|
|
|
accel := NewLabel(Label{
|
|
|
|
Text: accelerator,
|
|
|
|
Font: font,
|
|
|
|
})
|
|
|
|
frame.Pack(accel, Pack{
|
|
|
|
Side: E,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
w.Button.child = frame
|
2019-06-09 00:03:59 +00:00
|
|
|
w.Button.Configure(Config{
|
2020-06-04 07:50:06 +00:00
|
|
|
BorderSize: 0,
|
|
|
|
Background: theme.ButtonBackgroundColor,
|
|
|
|
})
|
|
|
|
|
|
|
|
w.Button.Handle(MouseOver, func(ed EventData) error {
|
|
|
|
w.setHoverStyle(true)
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
w.Button.Handle(MouseOut, func(ed EventData) error {
|
|
|
|
w.setHoverStyle(false)
|
|
|
|
return nil
|
2019-06-09 00:03:59 +00:00
|
|
|
})
|
|
|
|
|
2020-04-07 05:57:28 +00:00
|
|
|
w.Button.Handle(Click, func(ed EventData) error {
|
2019-06-09 00:03:59 +00:00
|
|
|
w.Command()
|
2020-04-07 05:57:28 +00:00
|
|
|
return nil
|
2019-06-09 00:03:59 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
// Assign the button
|
|
|
|
return w
|
|
|
|
}
|
2020-06-04 07:50:06 +00:00
|
|
|
|
|
|
|
// NewMenuSeparator creates a separator menu item.
|
|
|
|
func NewMenuSeparator() *MenuItem {
|
|
|
|
w := &MenuItem{
|
|
|
|
separator: true,
|
|
|
|
}
|
|
|
|
w.IDFunc(func() string {
|
|
|
|
return "MenuItem<separator>"
|
|
|
|
})
|
|
|
|
w.Button.child = NewFrame("Menu Separator")
|
|
|
|
w.Button.Configure(Config{
|
|
|
|
Width: MenuWidth,
|
|
|
|
Height: 2,
|
|
|
|
BorderSize: 1,
|
|
|
|
BorderStyle: BorderSunken,
|
|
|
|
BorderColor: render.Grey,
|
|
|
|
})
|
|
|
|
return w
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set the hover styling (text/bg color)
|
|
|
|
func (w *MenuItem) setHoverStyle(hovering bool) {
|
|
|
|
// Note: this only works if the MenuItem is using the standard
|
|
|
|
// Frame and Labels layout created by AddItem(). If not, this function
|
|
|
|
// does nothing.
|
|
|
|
|
|
|
|
// BG color.
|
|
|
|
if hovering {
|
|
|
|
w.cacheBg = w.Background()
|
|
|
|
w.SetBackground(render.SkyBlue)
|
|
|
|
} else {
|
|
|
|
w.SetBackground(w.cacheBg)
|
|
|
|
}
|
|
|
|
|
|
|
|
frame, ok := w.Button.child.(*Frame)
|
|
|
|
if !ok {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, widget := range frame.Children() {
|
|
|
|
if label, ok := widget.(*Label); ok {
|
|
|
|
if hovering {
|
|
|
|
w.cacheFg = label.Font.Color
|
|
|
|
label.Font.Color = render.White
|
|
|
|
} else {
|
|
|
|
label.Font.Color = w.cacheFg
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|