internal.PopIn is gone. Breadcrumb, NavigationMenu, Pagination, Menubar panels, CommandPalette, Select (and the other selectPanel users), datetime popups, ColorPicker and the StatusBar overflow now style their panel with overlayStyle(c, panel, overlayEnter(c, panel)): one shadow (0 6 18, 16% black, scaled by entry) and one 160 ms entry fade. Snapshots: color_picker and command_palette (light and dark) change only in the panel shadow. ColorPicker loses its 0 4 12 18% shadow for the softer shared one; CommandPalette loses its heavy 0 12 32 30% shadow, and its backdrop now fades by alpha with the panel instead of through the backdrop's opacity. Fade and reduced-motion tests pass unchanged.
211 lines
6.0 KiB
Go
211 lines
6.0 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
|
|
}
|
|
if back.Pressed() {
|
|
m.focus(st.open)
|
|
}
|
|
// 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.Pressed() {
|
|
m.focus(st.open)
|
|
}
|
|
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)
|
|
overlayStyle(c, panel, overlayEnter(c, panel))
|
|
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)
|
|
if panel.Clicked() || panel.Pressed() {
|
|
m.focus(st.open)
|
|
}
|
|
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
|
|
}
|
|
if row.Pressed() {
|
|
// A press takes the focus off the title, which drives the keys.
|
|
m.focus(st.open)
|
|
}
|
|
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), "+")
|
|
}
|