199 lines
5.8 KiB
Go
199 lines
5.8 KiB
Go
package mujica
|
|
|
|
import (
|
|
"runtime"
|
|
"strings"
|
|
|
|
"github.com/egoist/mygo/ui"
|
|
|
|
"mujicaui/icons"
|
|
"mujicaui/internal"
|
|
"mujicaui/theme"
|
|
)
|
|
|
|
// overlay draws the open menu and its submenus above the window.
|
|
func (m *menubarRun) overlay(titles []ui.Rect) {
|
|
c, st := m.c, m.st
|
|
ui.Overlay(c, func() {
|
|
back := ui.Box(c).Absolute().Left(0).Top(0).Right(0).Bottom(0)
|
|
if back.Clicked() {
|
|
m.close()
|
|
return
|
|
}
|
|
// Title hotspots above the backdrop switch menus as the pointer moves.
|
|
for i, r := range titles {
|
|
hot := ui.Box(c).Absolute().Left(r.X).Top(r.Y).Size(r.W, r.H).Cursor(ui.CursorPointer)
|
|
if hot.Clicked() {
|
|
if i == st.open {
|
|
m.close()
|
|
} else {
|
|
m.openMenu(i, false)
|
|
}
|
|
return
|
|
}
|
|
if hot.Hovered() && i != st.open {
|
|
m.openMenu(i, false)
|
|
}
|
|
}
|
|
if st.open < 0 {
|
|
return
|
|
}
|
|
r := titles[st.open]
|
|
x, y := r.X, r.Y+r.H+2
|
|
var prev ui.Rect
|
|
for l := 0; l < len(st.hl) && st.open >= 0; l++ {
|
|
panel, anchor := m.panel(l, x, y, prev)
|
|
if anchor == nil || l+1 >= len(st.hl) {
|
|
break
|
|
}
|
|
a := anchor.Bounds()
|
|
prev = panel.Bounds()
|
|
x, y = prev.X+prev.W-2, a.Y-Density(c).Unit()
|
|
}
|
|
})
|
|
}
|
|
|
|
// panel builds level l at (x, y) and returns it with its highlighted row.
|
|
func (m *menubarRun) panel(l int, x, y float32, parent ui.Rect) (*ui.Element, *ui.Element) {
|
|
c, st := m.c, m.st
|
|
k := Tokens(c)
|
|
u := Density(c).Unit()
|
|
items := m.items(l)
|
|
ww, wh := c.Size()
|
|
// Long menus scroll within the window instead of running off it.
|
|
panel := ui.Scroll(c).Absolute().Padding(u).MinWidth(200).MaxHeight(wh-8).AlignItems(ui.Stretch).Role(ui.RolePopup).
|
|
Radius(theme.PopupRadius).Background(k.Surface).Border(theme.BorderWidth, k.Border).
|
|
Shadow(0, 6, 16, 0, ui.RGBA(0, 0, 0, 0.18))
|
|
pb := panel.Bounds()
|
|
if x+pb.W > ww-4 {
|
|
x = ww - 4 - pb.W
|
|
if l > 0 {
|
|
x = parent.X - pb.W + 2
|
|
}
|
|
}
|
|
if y+pb.H > wh-4 {
|
|
y = max(4, wh-4-pb.H)
|
|
}
|
|
panel.Left(max(4, x)).Top(y)
|
|
var anchor *ui.Element
|
|
panel.Children(func() {
|
|
for j, it := range items {
|
|
if it.Kind == MenubarSeparator {
|
|
ui.Box(c).Height(1).Margin(u, u*2).Background(k.Border).Role(ui.RoleNone)
|
|
continue
|
|
}
|
|
row := m.item(it, st.hl[l] == j)
|
|
if st.hl[l] == j {
|
|
anchor = row
|
|
if st.reveal && l == len(st.hl)-1 {
|
|
row.ScrollIntoView()
|
|
st.reveal = false
|
|
}
|
|
}
|
|
if row.Clicked() {
|
|
st.hl = append(st.hl[:l], j)
|
|
m.activate(l, j)
|
|
return
|
|
}
|
|
px, py, over := row.PointerPosition()
|
|
b := row.Bounds()
|
|
at := [2]float32{b.X + px, b.Y + py}
|
|
if over && at != st.ptr && !it.Disabled {
|
|
st.ptr = at
|
|
if st.hl[l] != j || it.Items != nil && len(st.hl) == l+1 {
|
|
st.hl = append(st.hl[:l], j)
|
|
if it.Items != nil {
|
|
st.hl = append(st.hl, -1)
|
|
}
|
|
c.Invalidate()
|
|
}
|
|
}
|
|
}
|
|
})
|
|
return panel, anchor
|
|
}
|
|
|
|
func (m *menubarRun) item(it MenubarItem, hl bool) *ui.Element {
|
|
c := m.c
|
|
k := Tokens(c)
|
|
d := Density(c)
|
|
u := d.Unit()
|
|
name := menubarName(c, it)
|
|
row := ui.Row(c).AlignItems(ui.Center).Gap(u*2).MinHeight(d.ControlHeight()-4).Padding(0, u*2).
|
|
Radius(theme.ControlRadius).Label(name).TextColor(k.Text).Disabled(it.Disabled).Role(ui.RoleButton)
|
|
if hl {
|
|
row.Background(k.Selection)
|
|
}
|
|
internal.Ease(row, Motion(c, theme.StateDuration), k.Text)
|
|
row.Children(func() {
|
|
mark := ui.Box(c).Size(16, 16).Shrink(0).Center().Role(ui.RoleNone)
|
|
if it.Checked && it.Kind == MenubarCheck {
|
|
mark.TextColor(k.AccentText).Children(func() { ui.Icon(c, icons.Must("check")).Size(14, 14) })
|
|
}
|
|
if it.Checked && it.Kind == MenubarRadio {
|
|
mark.Draw(func(p *ui.Painter, r ui.Rect) {
|
|
p.FillPath(internal.Diamond(r.X+r.W/2, r.Y+r.H/2, 8), k.AccentText)
|
|
})
|
|
}
|
|
ui.Text(c, it.Label).SingleLine()
|
|
ui.Box(c).Grow(1).MinWidth(u * 6)
|
|
if it.Shortcut.Key != 0 {
|
|
ui.Text(c, menubarShortcutText(it.Shortcut)).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine()
|
|
}
|
|
if it.Items != nil {
|
|
ui.Icon(c, icons.Must("chevron-right")).Size(14, 14).TextColor(k.TextMuted)
|
|
}
|
|
})
|
|
return row
|
|
}
|
|
|
|
// menubarName is an item's label, with its checked state for assistive technology.
|
|
func menubarName(c *ui.Context, it MenubarItem) string {
|
|
if (it.Kind == MenubarCheck || it.Kind == MenubarRadio) && it.Checked {
|
|
return it.Label + ", " + msg(c, "menubar.checked")
|
|
}
|
|
return it.Label
|
|
}
|
|
|
|
var menubarKeyNames = map[ui.Key]string{
|
|
ui.KeyEnter: "Enter", ui.KeyEscape: "Esc", ui.KeyBackspace: "Backspace", ui.KeyTab: "Tab", ui.KeySpace: "Space",
|
|
ui.KeyDelete: "Del", ui.KeyHome: "Home", ui.KeyEnd: "End", ui.KeyPageUp: "PgUp", ui.KeyPageDown: "PgDn",
|
|
ui.KeyLeft: "←", ui.KeyRight: "→", ui.KeyUp: "↑", ui.KeyDown: "↓", ui.KeyMinus: "-", ui.KeyEqual: "=",
|
|
ui.KeyComma: ",", ui.KeyPeriod: ".", ui.KeySlash: "/", ui.KeySemicolon: ";", ui.KeyQuote: "'",
|
|
ui.KeyF1: "F1", ui.KeyF2: "F2", ui.KeyF3: "F3", ui.KeyF4: "F4", ui.KeyF5: "F5", ui.KeyF6: "F6",
|
|
ui.KeyF7: "F7", ui.KeyF8: "F8", ui.KeyF9: "F9", ui.KeyF10: "F10", ui.KeyF11: "F11", ui.KeyF12: "F12",
|
|
ui.KeyBracketLeft: "[", ui.KeyBracketRight: "]", ui.KeyBackslash: "\\", ui.KeyBackquote: "`",
|
|
}
|
|
|
|
// menubarShortcutText spells a shortcut the platform's way: ⌘S on macOS, Ctrl+S elsewhere.
|
|
func menubarShortcutText(s MenubarShortcut) string {
|
|
key, ok := menubarKeyNames[s.Key]
|
|
switch {
|
|
case ok:
|
|
case s.Key >= ui.KeyA && s.Key <= ui.KeyZ:
|
|
key = string(rune('A' + s.Key - ui.KeyA))
|
|
case s.Key >= ui.Key0 && s.Key <= ui.Key9:
|
|
key = string(rune('0' + s.Key - ui.Key0))
|
|
default:
|
|
panic("mujica: MenubarShortcut key has no name")
|
|
}
|
|
mac := runtime.GOOS == "darwin"
|
|
var parts []string
|
|
for _, m := range []struct {
|
|
mod ui.Modifiers
|
|
mac, name string
|
|
}{{ui.Ctrl, "⌃", "Ctrl"}, {ui.Alt, "⌥", "Alt"}, {ui.Shift, "⇧", "Shift"}, {ui.Super, "⌘", "Win"}} {
|
|
if s.Mods&m.mod != 0 {
|
|
if mac {
|
|
parts = append(parts, m.mac)
|
|
} else {
|
|
parts = append(parts, m.name)
|
|
}
|
|
}
|
|
}
|
|
if mac {
|
|
return strings.Join(parts, "") + key
|
|
}
|
|
return strings.Join(append(parts, key), "+")
|
|
}
|