ci-platforms / windows amd64 smoke (push) Skipped
ci / release (push) Successful in 8m58s
ci-platforms / darwin amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
ci / checks (push) Successful in 4m46s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 13m28s
307 lines
8.1 KiB
Go
307 lines
8.1 KiB
Go
package widgets
|
|
|
|
import (
|
|
"math/bits"
|
|
"strconv"
|
|
|
|
"aego/core/mathx"
|
|
"aego/platform/input"
|
|
"aego/ui/core"
|
|
"aego/ui/draw"
|
|
)
|
|
|
|
func glyph(c *core.Context, r rune, fallback string) string {
|
|
if f := c.Font(); f != nil && f.Has(r) {
|
|
return string(r)
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
func Glyph(c *core.Context, r rune, fallback string) string {
|
|
return glyph(c, r, fallback)
|
|
}
|
|
|
|
func dropdownBox(c *core.Context, id core.ID, r mathx.Rect, label string) {
|
|
th := &c.Theme
|
|
c.Box(r, th.Radius.Field, c.WidgetColor(id), th.Border)
|
|
inner := mathx.Rect{X: r.X, Y: r.Y, W: r.W - r.H, H: r.H}
|
|
c.PushClip(inner)
|
|
c.TextIn(inner, label, th.Text, core.AlignLeft)
|
|
c.PopClip()
|
|
Chevron(c, mathx.Rect{X: r.X + r.W - r.H, Y: r.Y, W: r.H, H: r.H}, true, th.TextDim)
|
|
}
|
|
|
|
func togglePopup(c *core.Context, id core.ID, r mathx.Rect) {
|
|
if c.Pressed(id, r) {
|
|
if c.PopupOpen(id) {
|
|
c.ClosePopup(id)
|
|
} else {
|
|
c.OpenPopup(id, mathx.Vec2{X: r.X, Y: r.Y + r.H})
|
|
}
|
|
}
|
|
}
|
|
|
|
func Combo(c *core.Context, id core.ID, r mathx.Rect, cur *int, opts []string, mixed bool) bool {
|
|
togglePopup(c, id, r)
|
|
label := c.Theme.Mixed
|
|
if !mixed && *cur >= 0 && *cur < len(opts) {
|
|
label = opts[*cur]
|
|
}
|
|
dropdownBox(c, id, r, label)
|
|
changed := false
|
|
if c.BeginPopup(id, max(r.W, 140)) {
|
|
c.PushRawID(id)
|
|
for i, o := range opts {
|
|
if Selectable(c, c.IDInt(i), o, !mixed && i == *cur) {
|
|
*cur = i
|
|
changed = true
|
|
c.ClosePopup(id)
|
|
}
|
|
}
|
|
c.PopID()
|
|
c.EndPopup()
|
|
}
|
|
return changed
|
|
}
|
|
|
|
func allBits(n int) uint64 {
|
|
if n >= 64 {
|
|
return ^uint64(0)
|
|
}
|
|
return uint64(1)<<uint(n) - 1
|
|
}
|
|
|
|
func maskSummary(mask uint64, names []string, n int, mixed bool, th *core.Theme) string {
|
|
switch {
|
|
case mixed:
|
|
return th.Mixed
|
|
case mask == 0:
|
|
return Tr("editor.widgets.nothing", "Nothing")
|
|
case mask == ^uint64(0) || mask == allBits(n):
|
|
return Tr("editor.widgets.everything", "Everything")
|
|
case bits.OnesCount64(mask) == 1:
|
|
i := bits.TrailingZeros64(mask)
|
|
if i < len(names) {
|
|
return names[i]
|
|
}
|
|
return Tr("editor.widgets.layer", "Layer") + " " + strconv.Itoa(i)
|
|
}
|
|
if len(names) > 0 {
|
|
return strconv.Itoa(bits.OnesCount64(mask)) + " selected"
|
|
}
|
|
return strconv.Itoa(bits.OnesCount64(mask)) + " layers"
|
|
}
|
|
|
|
func CheckRow(c *core.Context, id core.ID, label string, on bool) bool {
|
|
th := &c.Theme
|
|
r := c.Cursor().Next(0)
|
|
clicked := c.Clicked(id, r)
|
|
if c.Hot() == id {
|
|
c.RoundRect(r, th.Radius.Menu, th.WidgetHot)
|
|
}
|
|
box := mathx.Rect{X: r.X + 2, Y: r.Y + 4, W: r.H - 8, H: r.H - 8}
|
|
if on {
|
|
c.RoundRect(box, th.Radius.Field-1, th.Accent)
|
|
CheckMark(c, box, th.Background)
|
|
} else {
|
|
c.Box(box, th.Radius.Field-1, th.Widget, th.Border)
|
|
}
|
|
c.Text(label, mathx.Vec2{X: box.X + box.W + 6, Y: c.TextY(r)}, th.Text)
|
|
return clicked
|
|
}
|
|
|
|
func MaskField(c *core.Context, id core.ID, r mathx.Rect, mask *uint64, names []string, mixed bool) bool {
|
|
n := len(names)
|
|
if n == 0 {
|
|
n = 32
|
|
}
|
|
togglePopup(c, id, r)
|
|
dropdownBox(c, id, r, maskSummary(*mask, names, n, mixed, &c.Theme))
|
|
changed := false
|
|
if c.BeginPopup(id, max(r.W, 180)) {
|
|
c.PushRawID(id)
|
|
if Selectable(c, c.ID("everything"), Tr("editor.widgets.everything", "Everything"), !mixed && *mask == allBits(n)) {
|
|
*mask = allBits(n)
|
|
changed = true
|
|
}
|
|
if Selectable(c, c.ID("nothing"), Tr("editor.widgets.nothing", "Nothing"), !mixed && *mask == 0) {
|
|
*mask = 0
|
|
changed = true
|
|
}
|
|
Separator(c)
|
|
for i := 0; i < n; i++ {
|
|
name := Tr("editor.widgets.layer", "Layer") + " " + strconv.Itoa(i)
|
|
if i < len(names) && names[i] != "" {
|
|
name = names[i]
|
|
}
|
|
bit := uint64(1) << uint(i)
|
|
if CheckRow(c, c.IDInt(i), name, *mask&bit != 0) {
|
|
*mask ^= bit
|
|
changed = true
|
|
}
|
|
}
|
|
c.PopID()
|
|
c.EndPopup()
|
|
}
|
|
return changed
|
|
}
|
|
|
|
func OpenContextMenu(c *core.Context, id core.ID) {
|
|
c.CloseAllPopups()
|
|
c.OpenPopup(id, c.Mouse())
|
|
}
|
|
|
|
func BeginContextMenu(c *core.Context, id core.ID, r mathx.Rect) bool {
|
|
if c.RightClicked(id, r) {
|
|
OpenContextMenu(c, id)
|
|
}
|
|
return c.BeginPopup(id, 220)
|
|
}
|
|
|
|
func ContextMenu(c *core.Context, id core.ID) bool {
|
|
return c.BeginPopup(id, 220)
|
|
}
|
|
|
|
func EndContextMenu(c *core.Context) {
|
|
c.EndPopup()
|
|
}
|
|
|
|
func Tooltip(c *core.Context, id core.ID, text string) {
|
|
TooltipAfter(c, id, text, 0.5)
|
|
}
|
|
|
|
type confirmLayout struct {
|
|
window, message mathx.Rect
|
|
buttons []mathx.Rect
|
|
}
|
|
|
|
func layoutConfirm(c *core.Context, message string, buttons []string) confirmLayout {
|
|
th := &c.Theme
|
|
sz := c.Size()
|
|
w := max(float32(1), min(float32(480), sz.X-40))
|
|
inner := max(float32(1), w-2*th.Padding)
|
|
lines := len(WrapLines(c, message, inner))
|
|
messageH := float32(lines) * (th.RowHeight + th.Spacing)
|
|
widths := make([]float32, len(buttons))
|
|
rows, x := 1, inner
|
|
indices := make([]int, len(buttons))
|
|
for i := len(buttons) - 1; i >= 0; i-- {
|
|
widths[i] = min(inner, c.TextSize(buttons[i]).X+28)
|
|
if x < widths[i] && x < inner {
|
|
rows++
|
|
x = inner
|
|
}
|
|
x -= widths[i]
|
|
indices[i] = rows - 1
|
|
x -= 6
|
|
}
|
|
buttonsH := float32(rows)*th.RowHeight + float32(rows-1)*th.Spacing
|
|
base := 2*th.Padding + th.RowHeight + th.Spacing + buttonsH
|
|
visible := min(messageH, max(float32(0), sz.Y-40-base))
|
|
h := base + visible
|
|
r := mathx.Rect{X: (sz.X - w) / 2, Y: max(float32(0), (sz.Y-h)/3), W: w, H: h}
|
|
out := confirmLayout{window: r, message: mathx.Rect{X: r.X + th.Padding, Y: r.Y + th.Padding + th.RowHeight + th.Spacing, W: inner, H: visible}, buttons: make([]mathx.Rect, len(buttons))}
|
|
x = r.X + th.Padding + inner
|
|
currentRow := 0
|
|
for i := len(buttons) - 1; i >= 0; i-- {
|
|
if indices[i] != currentRow {
|
|
currentRow = indices[i]
|
|
x = r.X + th.Padding + inner
|
|
}
|
|
x -= widths[i]
|
|
out.buttons[i] = mathx.Rect{X: x, Y: r.Y + h - th.Padding - th.RowHeight - float32(currentRow)*(th.RowHeight+th.Spacing), W: widths[i], H: th.RowHeight}
|
|
x -= 6
|
|
}
|
|
return out
|
|
}
|
|
|
|
func Confirm(c *core.Context, id core.ID, title, message string, buttons ...string) int {
|
|
if !c.ModalOpen(id) {
|
|
return -1
|
|
}
|
|
th := &c.Theme
|
|
l := layoutConfirm(c, message, buttons)
|
|
if !c.BeginModal(id, l.window) {
|
|
return -1
|
|
}
|
|
c.PushRawID(id)
|
|
result := -1
|
|
Label(c, title)
|
|
scroll := c.ModalScroll()
|
|
content := float32(len(WrapLines(c, message, l.message.W))) * (th.RowHeight + th.Spacing)
|
|
*scroll = mathx.Clamp(*scroll, 0, max(float32(0), content-l.message.H))
|
|
c.PushClip(l.message)
|
|
c.Cursor().Y -= *scroll
|
|
LabelWrapped(c, message, true)
|
|
c.PopClip()
|
|
if c.MouseOver(c.ID("message-scroll"), l.message) {
|
|
*scroll = mathx.Clamp(*scroll-c.ScrollY(th.RowHeight*3), 0, max(float32(0), content-l.message.H))
|
|
}
|
|
for i, r := range l.buttons {
|
|
if ButtonRect(c, c.IDInt(i), r, Elide(c, buttons[i], max(float32(0), r.W-12))) {
|
|
result = i
|
|
}
|
|
}
|
|
if result < 0 && len(buttons) > 0 && c.Focus() == 0 {
|
|
if c.KeyPressed(input.KeyEnter) || c.KeyPressed(input.KeyKPEnter) {
|
|
result = 0
|
|
}
|
|
if c.KeyPressed(input.KeyEscape) {
|
|
result = len(buttons) - 1
|
|
}
|
|
}
|
|
c.PopID()
|
|
c.EndModal()
|
|
return result
|
|
}
|
|
|
|
func TooltipAfter(c *core.Context, id core.ID, text string, delay float64) {
|
|
if text == "" || c.HoverDuration(id) < delay {
|
|
return
|
|
}
|
|
th := &c.Theme
|
|
m := c.Mouse()
|
|
sz := c.Size()
|
|
size := c.TextSize(text)
|
|
var lines []string
|
|
if maxW := min(tooltipMax, sz.X-2*th.Padding-8); size.X > maxW {
|
|
if tipWrap.text != text || tipWrap.w != maxW {
|
|
tipWrap.text, tipWrap.w, tipWrap.lines = text, maxW, WrapLines(c, text, maxW)
|
|
}
|
|
lines = tipWrap.lines
|
|
size.X = 0
|
|
for _, ln := range lines {
|
|
size.X = max(size.X, c.TextSize(ln).X)
|
|
}
|
|
size.Y = c.LineHeight() * float32(len(lines))
|
|
}
|
|
r := mathx.Rect{X: m.X + 14, Y: m.Y + 18, W: size.X + 2*th.Padding, H: size.Y + th.Padding}
|
|
if r.X+r.W > sz.X {
|
|
r.X = max(0, sz.X-r.W)
|
|
}
|
|
if r.Y+r.H > sz.Y {
|
|
r.Y = max(0, m.Y-r.H-4)
|
|
}
|
|
l := c.List()
|
|
l.PushLayer(draw.LayerTop)
|
|
c.Shadow(r, th.Radius.Popup)
|
|
c.Box(r, th.Radius.Popup, th.PanelAlt, th.Border)
|
|
if len(lines) == 0 {
|
|
l.Text(text, mathx.Vec2{X: r.X + th.Padding, Y: r.Y + th.Padding/2}, th.Text)
|
|
}
|
|
for i, ln := range lines {
|
|
l.Text(ln, mathx.Vec2{X: r.X + th.Padding, Y: r.Y + th.Padding/2 + float32(i)*c.LineHeight()}, th.Text)
|
|
}
|
|
l.PopLayer()
|
|
}
|
|
|
|
// tooltipMax is the widest a tooltip grows before its text wraps (D348).
|
|
const tooltipMax float32 = 360
|
|
|
|
// tipWrap keeps the last wrapped tooltip, so a tooltip held on screen wraps once.
|
|
var tipWrap struct {
|
|
text string
|
|
w float32
|
|
lines []string
|
|
}
|