From 2c7c77d8da586c3de90bb0f21d5f2b43f17b0576 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 7 Jun 2024 01:59:29 -0400 Subject: [PATCH] Add menus --- menu.go | 101 ++++++++++++++++++++++++++++++++++++++++++++++++++++ menuitem.go | 74 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 175 insertions(+) create mode 100644 menu.go create mode 100644 menuitem.go diff --git a/menu.go b/menu.go new file mode 100644 index 0000000..81ce649 --- /dev/null +++ b/menu.go @@ -0,0 +1,101 @@ +package objects + +import "image" +import "git.tebibyte.media/tomo/tomo" +import "git.tebibyte.media/tomo/tomo/input" +// import "git.tebibyte.media/tomo/tomo/event" +import "git.tebibyte.media/tomo/objects/layouts" + +// Menu is a menu window. +type Menu struct { + tomo.Window + + parent tomo.Window + bounds image.Rectangle + rootContainer tomo.ContainerBox + tearLine tomo.Box + torn bool +} + +// NewMenu creates a new menu with the specified items. The menu will appear +// directly under the anchor Object. If the anchor is nil, it will appear +// directly under the mouse pointer instead. +func NewMenu (anchor tomo.Object, items ...tomo.Object) (*Menu, error) { + menu := &Menu { } + if anchor == nil { + // TODO: *actually* put it under the mouse + window, err := tomo.NewWindow(menu.bounds) + if err != nil { return nil, err } + menu.Window = window + } else { + menu.bounds = menuBoundsFromAnchor(anchor) + menu.parent = anchor.GetBox().Window() + window, err := menu.parent.NewMenu(menu.bounds) + if err != nil { return nil, err } + menu.Window = window + } + + menu.rootContainer = tomo.NewContainerBox() + menu.rootContainer.SetLayout(layouts.ContractVertical) + + if !menu.torn { + menu.tearLine = tomo.NewBox() + menu.tearLine.SetRole(tomo.R("objects", "TearLine", "")) + tomo.Apply(menu.tearLine) + menu.tearLine.SetFocusable(true) + menu.tearLine.OnKeyUp(func (key input.Key, numberPad bool) { + if key != input.KeyEnter && key != input.Key(' ') { return } + menu.TearOff() + }) + menu.tearLine.OnMouseUp(func (button input.Button) { + if button != input.ButtonLeft { return } + if menu.tearLine.MousePosition().In(menu.tearLine.Bounds()) { + menu.TearOff() + } + }) + menu.rootContainer.Add(menu.tearLine) + } + + for _, item := range items { + menu.rootContainer.Add(item) + if item, ok := item.(*MenuItem); ok { + item.OnClick(func () { + if !menu.torn { + menu.Close() + } + }) + } + } + menu.rootContainer.SetRole(tomo.R("objects", "Container", "menu")) + tomo.Apply(menu.rootContainer) + + menu.Window.SetRoot(menu.rootContainer) + return menu, nil +} + +// TearOff converts this menu into a tear-off menu. +func (this *Menu) TearOff () { + if this.torn { return } + if this.parent == nil { return } + this.torn = true + + window, err := this.parent.NewChild(this.bounds) + if err != nil { return } + + visible := this.Window.Visible() + this.Window.SetRoot(nil) + this.Window.Close() + + this.rootContainer.Remove(this.tearLine) + + this.Window = window + this.Window.SetRoot(this.rootContainer) + this.Window.SetVisible(visible) +} + +func menuBoundsFromAnchor (anchor tomo.Object) image.Rectangle { + bounds := anchor.GetBox().Bounds() + return image.Rect ( + bounds.Min.X, bounds.Max.Y, + bounds.Max.X, bounds.Max.Y)//.Add(windowBounds.Min) +} diff --git a/menuitem.go b/menuitem.go new file mode 100644 index 0000000..6f37bf6 --- /dev/null +++ b/menuitem.go @@ -0,0 +1,74 @@ +package objects + +import "git.tebibyte.media/tomo/tomo" +import "git.tebibyte.media/tomo/tomo/input" +import "git.tebibyte.media/tomo/tomo/event" +import "git.tebibyte.media/tomo/objects/layouts" + +// MenuItem is a clickable button. +type MenuItem struct { + tomo.ContainerBox + + label *Label + icon *Icon + labelActive bool + + on struct { + click event.FuncBroadcaster + } +} + +// NewMenuItem creates a new menu item with the specified text. +func NewMenuItem (text string) *MenuItem { + box := &MenuItem { + ContainerBox: tomo.NewContainerBox(), + label: NewLabel(text), + icon: NewIcon("", tomo.IconSizeSmall), + } + box.SetRole(tomo.R("objects", "MenuItem", "")) + tomo.Apply(box) + box.label.SetAlign(tomo.AlignStart, tomo.AlignMiddle) + box.SetLayout(layouts.NewGrid([]bool { false, true }, []bool { true })) + + box.Add(box.icon) + box.Add(box.label) + + box.CaptureDND(true) + box.CaptureMouse(true) + box.CaptureScroll(true) + box.CaptureKeyboard(true) + + box.OnMouseUp(box.handleMouseUp) + box.OnKeyUp(box.handleKeyUp) + box.SetFocusable(true) + return box +} + +// SetText sets the text of the items's label. +func (this *MenuItem) SetText (text string) { + this.label.SetText(text) +} + +// SetIcon sets an icon for this item. Setting the icon to IconUnknown will +// remove it. +func (this *MenuItem) SetIcon (id tomo.Icon) { + if this.icon != nil { this.Remove(this.icon) } + this.Insert(NewIcon(id, tomo.IconSizeSmall), this.label) +} + +// OnClick specifies a function to be called when the menu item is clicked. +func (this *MenuItem) OnClick (callback func ()) event.Cookie { + return this.on.click.Connect(callback) +} + +func (this *MenuItem) handleKeyUp (key input.Key, numberPad bool) { + if key != input.KeyEnter && key != input.Key(' ') { return } + this.on.click.Broadcast() +} + +func (this *MenuItem) handleMouseUp (button input.Button) { + if button != input.ButtonLeft { return } + if this.MousePosition().In(this.Bounds()) { + this.on.click.Broadcast() + } +}