196 lines
4.9 KiB
Go
196 lines
4.9 KiB
Go
package ui
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"git.kirsle.net/go/render"
|
|
"git.kirsle.net/go/ui/theme"
|
|
)
|
|
|
|
// MenuButton is a button that opens a menu when clicked.
|
|
//
|
|
// After creating a MenuButton, call AddItem() to add options and callback
|
|
// functions to fill out the menu. When the MenuButton is clicked, its menu
|
|
// will be drawn and take modal priority in the Supervisor.
|
|
type MenuButton struct {
|
|
Button
|
|
|
|
name string
|
|
supervisor *Supervisor
|
|
menu *Menu
|
|
}
|
|
|
|
// NewMenuButton creates a new MenuButton (labels recommended).
|
|
//
|
|
// If the child is a Label, this function will set some sensible padding on
|
|
// its font if the Label does not already have non-zero padding set.
|
|
func NewMenuButton(name string, child Widget) *MenuButton {
|
|
w := &MenuButton{
|
|
name: name,
|
|
}
|
|
w.Button.child = child
|
|
|
|
// If it's a Label (most common), set sensible default padding.
|
|
if label, ok := child.(*Label); ok {
|
|
if label.Font.Padding == 0 && label.Font.PadX == 0 && label.Font.PadY == 0 {
|
|
label.Font.PadX = 8
|
|
label.Font.PadY = 4
|
|
}
|
|
}
|
|
|
|
w.IDFunc(func() string {
|
|
return fmt.Sprintf("MenuButton<%s>", name)
|
|
})
|
|
|
|
w.setup()
|
|
return w
|
|
}
|
|
|
|
// Supervise the MenuButton. This is necessary for the pop-up menu to work
|
|
// when the button is clicked.
|
|
func (w *MenuButton) Supervise(s *Supervisor) {
|
|
w.initMenu()
|
|
w.supervisor = s
|
|
w.menu.Supervise(s)
|
|
}
|
|
|
|
// AddItem adds a new option to the MenuButton's menu.
|
|
func (w *MenuButton) AddItem(label string, f func()) {
|
|
w.initMenu()
|
|
w.menu.AddItem(label, f)
|
|
}
|
|
|
|
// AddItemAccel adds a new menu option with hotkey text.
|
|
func (w *MenuButton) AddItemAccel(label string, accelerator string, f func()) *MenuItem {
|
|
w.initMenu()
|
|
return w.menu.AddItemAccel(label, accelerator, f)
|
|
}
|
|
|
|
// AddSeparator adds a separator to the menu.
|
|
func (w *MenuButton) AddSeparator() {
|
|
w.initMenu()
|
|
w.menu.AddSeparator()
|
|
}
|
|
|
|
// 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 *MenuButton) Compute(e render.Engine) {
|
|
if w.menu != nil {
|
|
w.menu.Compute(e)
|
|
w.positionMenu(e)
|
|
}
|
|
}
|
|
|
|
// positionMenu sets the position where the pop-up menu will appear when
|
|
// the button is clicked. Usually, the menu appears below and to the right of
|
|
// the button. But if the menu will hit a window boundary, its position will
|
|
// be adjusted to fit the window while trying not to overlap its own button.
|
|
func (w *MenuButton) positionMenu(e render.Engine) {
|
|
var (
|
|
// Position and size of the MenuButton button.
|
|
buttonPoint = AbsolutePosition(w)
|
|
buttonSize = w.Size()
|
|
|
|
// Size of the actual desktop window.
|
|
Width, Height = e.WindowSize()
|
|
)
|
|
|
|
// Ideal location: below and to the right of the button.
|
|
w.menu.MoveTo(render.Point{
|
|
X: buttonPoint.X,
|
|
Y: buttonPoint.Y + buttonSize.H + w.BoxThickness(2),
|
|
})
|
|
|
|
var (
|
|
// Size of the menu.
|
|
menuPoint = w.menu.Point()
|
|
menuSize = w.menu.Rect()
|
|
margin = 8 // keep away from directly touching window edges
|
|
topMargin = 32 // keep room for standard Menu Bar
|
|
)
|
|
|
|
// Will we clip out the bottom of the window?
|
|
if menuPoint.Y+menuSize.H+margin > Height {
|
|
// Put us above the button instead, with the bottom of the
|
|
// menu touching the top of the button.
|
|
menuPoint = render.Point{
|
|
X: buttonPoint.X,
|
|
Y: buttonPoint.Y - menuSize.H - w.BoxThickness(2),
|
|
}
|
|
|
|
// If this would put us over the TOP edge of the window now,
|
|
// cap the movement so the top of the menu is visible. We can't
|
|
// avoid overlapping the button with the menu so might as well
|
|
// start now.
|
|
if menuPoint.Y < topMargin {
|
|
menuPoint.Y = topMargin
|
|
}
|
|
|
|
w.menu.MoveTo(menuPoint)
|
|
}
|
|
|
|
// Will we clip out the right of the window?
|
|
if menuPoint.X+menuSize.W > Width {
|
|
// Move us in from the right side of the window.
|
|
var delta = Width - menuSize.W - margin
|
|
w.menu.MoveTo(render.Point{
|
|
X: delta,
|
|
Y: menuPoint.Y,
|
|
})
|
|
}
|
|
_ = Width
|
|
}
|
|
|
|
// setup the common things between checkboxes and radioboxes.
|
|
func (w *MenuButton) setup() {
|
|
w.Configure(Config{
|
|
BorderSize: 1,
|
|
BorderStyle: BorderSolid,
|
|
Background: theme.ButtonBackgroundColor,
|
|
})
|
|
|
|
w.Handle(MouseOver, func(ed EventData) error {
|
|
w.hovering = true
|
|
w.SetBorderStyle(BorderRaised)
|
|
return nil
|
|
})
|
|
w.Handle(MouseOut, func(ed EventData) error {
|
|
w.hovering = false
|
|
w.SetBorderStyle(BorderSolid)
|
|
return nil
|
|
})
|
|
|
|
w.Handle(MouseDown, func(ed EventData) error {
|
|
w.clicked = true
|
|
w.SetBorderStyle(BorderSunken)
|
|
return nil
|
|
})
|
|
w.Handle(MouseUp, func(ed EventData) error {
|
|
w.clicked = false
|
|
return nil
|
|
})
|
|
|
|
w.Handle(Click, func(ed EventData) error {
|
|
// Are we properly configured?
|
|
if w.supervisor != nil && w.menu != nil {
|
|
w.menu.Show()
|
|
w.supervisor.PushModal(w.menu)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// initialize the Menu widget.
|
|
func (w *MenuButton) initMenu() {
|
|
if w.menu == nil {
|
|
w.menu = NewMenu(w.name + ":Menu")
|
|
w.menu.Hide()
|
|
|
|
// Handle closing the menu when clicked outside.
|
|
w.menu.Handle(CloseModal, func(ed EventData) error {
|
|
ed.Supervisor.PopModal(w.menu)
|
|
return nil
|
|
})
|
|
}
|
|
}
|