engine/i18n with catalogs, plural forms and module catalogs; tools/i18n extract and check; the editor UI, menus, panels and notices in Russian with the author's terms; overflow test and tooltip wrapping; the English log.
265 lines
6.7 KiB
Go
265 lines
6.7 KiB
Go
package widgets
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"aego/core/mathx"
|
|
"aego/render/texture"
|
|
"aego/ui/core"
|
|
)
|
|
|
|
const stackWidth = 230
|
|
|
|
func LabelWidth(rowW float32) float32 {
|
|
return mathx.Clamp(rowW*0.38, 60, 160)
|
|
}
|
|
|
|
func FieldRects(c *core.Context, label string) (mathx.Rect, mathx.Rect) {
|
|
th := &c.Theme
|
|
cur := c.Cursor()
|
|
if cur.W < stackWidth {
|
|
lr := cur.Next(c.LineHeight() + 2)
|
|
flash(c, label, lr)
|
|
c.PushClip(lr)
|
|
c.Text(label, mathx.Vec2{X: lr.X, Y: lr.Y + 1}, th.TextDim)
|
|
c.PopClip()
|
|
return lr, cur.Next(0)
|
|
}
|
|
r := cur.Next(0)
|
|
flash(c, label, r)
|
|
lw := LabelWidth(r.W)
|
|
lr := mathx.Rect{X: r.X, Y: r.Y, W: lw - 4, H: r.H}
|
|
c.PushClip(lr)
|
|
c.Text(fitLabel(c, label, lr.W), mathx.Vec2{X: lr.X, Y: c.TextY(lr)}, th.TextDim)
|
|
c.PopClip()
|
|
return lr, mathx.Rect{X: r.X + lw, Y: r.Y, W: r.W - lw, H: r.H}
|
|
}
|
|
|
|
func flash(c *core.Context, label string, r mathx.Rect) {
|
|
if c.Flash == "" || label == "" {
|
|
return
|
|
}
|
|
a, b := strings.ToLower(c.Flash), strings.ToLower(label)
|
|
if !strings.HasPrefix(a, b) && !strings.HasPrefix(b, a) {
|
|
return
|
|
}
|
|
k := 1 - float32(c.Time-c.FlashAt)/2
|
|
if k <= 0 {
|
|
return
|
|
}
|
|
col := c.Theme.Accent
|
|
col.A = 0.28 * k
|
|
c.RoundRect(mathx.Rect{X: r.X - 4, Y: r.Y - 1, W: r.W + 8, H: r.H + 2}, c.Theme.Radius.Field, col)
|
|
if !c.FlashSeen {
|
|
c.FlashY, c.FlashSeen = r.Y, true
|
|
}
|
|
}
|
|
|
|
func Field(c *core.Context, label string) mathx.Rect {
|
|
_, v := FieldRects(c, label)
|
|
return v
|
|
}
|
|
|
|
func ButtonRect(c *core.Context, id core.ID, r mathx.Rect, label string) bool {
|
|
clicked := c.Clicked(id, r)
|
|
c.Box(r, c.Theme.Radius.Button, c.WidgetColor(id), c.Theme.Border)
|
|
c.TextIn(r, label, c.Theme.Text, core.AlignCenter)
|
|
return clicked
|
|
}
|
|
|
|
func IconButtonRect(c *core.Context, id core.ID, r mathx.Rect, icon string) bool {
|
|
clicked := c.Clicked(id, r)
|
|
c.Box(r, c.Theme.Radius.Button, c.WidgetColor(id), c.Theme.Border)
|
|
IconIn(c, r, icon, c.Theme.Text)
|
|
return clicked
|
|
}
|
|
|
|
func ToggleButton(c *core.Context, id core.ID, r mathx.Rect, label string, on *bool) bool {
|
|
clicked := c.Clicked(id, r)
|
|
if clicked {
|
|
*on = !*on
|
|
}
|
|
bg := c.WidgetColor(id)
|
|
if *on {
|
|
bg = c.Theme.Selection
|
|
}
|
|
c.Box(r, c.Theme.Radius.Button, bg, c.Theme.Border)
|
|
c.TextIn(r, label, c.Theme.Text, core.AlignCenter)
|
|
return clicked
|
|
}
|
|
|
|
func ImageButton(c *core.Context, id core.ID, r mathx.Rect, reg texture.Region, tint mathx.Color) bool {
|
|
clicked := c.Clicked(id, r)
|
|
if c.Hot() == id || c.Active() == id {
|
|
c.Rect(r, c.WidgetColor(id))
|
|
}
|
|
inner := mathx.Rect{X: r.X + 2, Y: r.Y + 2, W: r.W - 4, H: r.H - 4}
|
|
if !reg.IsZero() {
|
|
c.List().Texture(reg, inner, tint)
|
|
}
|
|
return clicked
|
|
}
|
|
|
|
func Selectable(c *core.Context, id core.ID, label string, selected bool) bool {
|
|
r := c.Cursor().Next(0)
|
|
c.FitPopup(c.TextSize(label).X + 12)
|
|
clicked := c.Clicked(id, r)
|
|
if selected {
|
|
c.RoundRect(r, c.Theme.Radius.Menu, c.Theme.WidgetDown)
|
|
} else if c.Hot() == id {
|
|
c.RoundRect(r, c.Theme.Radius.Menu, c.Theme.WidgetHot)
|
|
}
|
|
c.List().Text(label, mathx.Vec2{X: r.X + 4, Y: textY(c, r)}, c.Theme.Text)
|
|
return clicked
|
|
}
|
|
|
|
func SelectableIcon(c *core.Context, id core.ID, icon, label string, selected bool) bool {
|
|
th := &c.Theme
|
|
r := c.Cursor().Next(0)
|
|
c.FitPopup(c.TextSize(label).X + 34)
|
|
clicked := c.Clicked(id, r)
|
|
if selected {
|
|
c.RoundRect(r, th.Radius.Menu, th.WidgetDown)
|
|
} else if c.Hot() == id {
|
|
c.RoundRect(r, th.Radius.Menu, th.WidgetHot)
|
|
}
|
|
if HasIcon(c, icon) {
|
|
IconAt(c, mathx.Rect{X: r.X + 6, Y: r.Y + (r.H-16)/2, W: 16, H: 16}, icon, th.TextDim)
|
|
}
|
|
c.List().Text(label, mathx.Vec2{X: r.X + 28, Y: textY(c, r)}, th.Text)
|
|
return clicked
|
|
}
|
|
|
|
func HeaderIcon(c *core.Context, icon, label string, dim bool) {
|
|
th := &c.Theme
|
|
r := c.Cursor().Next(0)
|
|
col := th.TextDim
|
|
if dim {
|
|
col = th.TextDisabled
|
|
}
|
|
x := r.X + 4
|
|
if HasIcon(c, icon) {
|
|
IconAt(c, mathx.Rect{X: x, Y: r.Y + (r.H-16)/2, W: 16, H: 16}, icon, col)
|
|
x += 22
|
|
}
|
|
c.List().Text(label, mathx.Vec2{X: x, Y: textY(c, r)}, col)
|
|
}
|
|
|
|
func SearchField(c *core.Context, id core.ID, r mathx.Rect, q *string) bool {
|
|
th := &c.Theme
|
|
changed := false
|
|
field := r
|
|
if *q != "" {
|
|
cr := mathx.Rect{X: r.X + r.W - r.H, Y: r.Y, W: r.H, H: r.H}
|
|
cid := core.IDSeq(id, 7)
|
|
if c.Clicked(cid, cr) {
|
|
*q = ""
|
|
c.CancelFocus()
|
|
changed = true
|
|
}
|
|
field.W -= r.H
|
|
defer IconIn(c, cr, "ui.close", th.TextDim)
|
|
}
|
|
if ch, _ := TextFieldRect(c, id, field, q, true); ch {
|
|
changed = true
|
|
}
|
|
if *q == "" && c.Focus() != id {
|
|
c.Text(Tr("editor.widgets.search", "Search"), mathx.Vec2{X: r.X + 6, Y: c.TextY(r)}, th.TextDisabled)
|
|
}
|
|
return changed
|
|
}
|
|
|
|
func Splitter(c *core.Context, id core.ID, r mathx.Rect, vertical bool, ratio *float32) (mathx.Rect, mathx.Rect) {
|
|
th := &c.Theme
|
|
g := th.SplitterW
|
|
*ratio = mathx.Clamp(*ratio, 0.05, 0.95)
|
|
var a, b, h mathx.Rect
|
|
if !vertical {
|
|
w := (r.W - g) * *ratio
|
|
a = mathx.Rect{X: r.X, Y: r.Y, W: w, H: r.H}
|
|
h = mathx.Rect{X: r.X + w, Y: r.Y, W: g, H: r.H}
|
|
b = mathx.Rect{X: r.X + w + g, Y: r.Y, W: r.W - w - g, H: r.H}
|
|
} else {
|
|
hh := (r.H - g) * *ratio
|
|
a = mathx.Rect{X: r.X, Y: r.Y, W: r.W, H: hh}
|
|
h = mathx.Rect{X: r.X, Y: r.Y + hh, W: r.W, H: g}
|
|
b = mathx.Rect{X: r.X, Y: r.Y + hh + g, W: r.W, H: r.H - hh - g}
|
|
}
|
|
if d, ok := c.Drag(id, h); ok {
|
|
if !vertical && r.W > g {
|
|
*ratio = mathx.Clamp(*ratio+d.X/(r.W-g), 0.05, 0.95)
|
|
}
|
|
if vertical && r.H > g {
|
|
*ratio = mathx.Clamp(*ratio+d.Y/(r.H-g), 0.05, 0.95)
|
|
}
|
|
}
|
|
col := th.Background
|
|
if c.Hot() == id || c.Active() == id {
|
|
col = th.AccentDim
|
|
if vertical {
|
|
c.SetCursorShape(core.CursorResizeNS)
|
|
} else {
|
|
c.SetCursorShape(core.CursorResizeEW)
|
|
}
|
|
}
|
|
c.Rect(h, col)
|
|
return a, b
|
|
}
|
|
|
|
func Switch(c *core.Context, id core.ID, r mathx.Rect, on *bool, label string) bool {
|
|
th := &c.Theme
|
|
clicked := c.Clicked(id, r)
|
|
if clicked {
|
|
*on = !*on
|
|
}
|
|
h := min(r.H-6, 16)
|
|
pill := mathx.Rect{X: r.X, Y: float32(int(r.Y + (r.H-h)/2)), W: h * 1.8, H: h}
|
|
bg := th.Widget
|
|
if c.Hot() == id {
|
|
bg = th.WidgetHot
|
|
}
|
|
if *on {
|
|
bg = th.Accent
|
|
}
|
|
c.RoundRect(pill, h/2, bg)
|
|
k := h - 4
|
|
kx := pill.X + 2
|
|
if *on {
|
|
kx = pill.X + pill.W - k - 2
|
|
}
|
|
c.RoundRect(mathx.Rect{X: kx, Y: pill.Y + 2, W: k, H: k}, k/2, th.Text)
|
|
if label != "" {
|
|
col := th.TextDim
|
|
if *on {
|
|
col = th.Text
|
|
}
|
|
c.Text(label, mathx.Vec2{X: pill.X + pill.W + 8, Y: c.TextY(r)}, col)
|
|
}
|
|
return clicked
|
|
}
|
|
|
|
type labelKey struct {
|
|
s string
|
|
w float32
|
|
}
|
|
|
|
// labelCache keeps the elided field labels, so a narrow inspector does not elide every frame.
|
|
var labelCache = map[labelKey]string{}
|
|
|
|
// fitLabel shortens a field label with an ellipsis when it is wider than its column (D348).
|
|
func fitLabel(c *core.Context, s string, w float32) string {
|
|
if c.TextSize(s).X <= w {
|
|
return s
|
|
}
|
|
k := labelKey{s, w}
|
|
if t, ok := labelCache[k]; ok {
|
|
return t
|
|
}
|
|
if len(labelCache) > 1024 {
|
|
clear(labelCache)
|
|
}
|
|
t := Elide(c, s, w)
|
|
labelCache[k] = t
|
|
return t
|
|
}
|