695 lines
13 KiB
Go
695 lines
13 KiB
Go
package core
|
|
|
|
import (
|
|
"aego/core/mathx"
|
|
"aego/platform/input"
|
|
"aego/render/text"
|
|
"aego/ui/draw"
|
|
"aego/ui/textedit"
|
|
"aego/ui/theme"
|
|
)
|
|
|
|
type Theme = theme.Theme
|
|
|
|
func DefaultTheme() Theme {
|
|
return theme.Default()
|
|
}
|
|
|
|
type Cursor struct {
|
|
X, Y float32
|
|
W float32
|
|
RowH float32
|
|
Pad float32
|
|
Space float32
|
|
}
|
|
|
|
func (c *Cursor) Next(h float32) mathx.Rect {
|
|
if h <= 0 {
|
|
h = c.RowH
|
|
}
|
|
r := mathx.Rect{X: c.X, Y: c.Y, W: c.W, H: h}
|
|
c.Y += h + c.Space
|
|
return r
|
|
}
|
|
|
|
type CursorShape uint8
|
|
|
|
const (
|
|
CursorDefault CursorShape = iota
|
|
CursorText
|
|
CursorPointer
|
|
CursorMove
|
|
CursorResizeEW
|
|
CursorResizeNS
|
|
CursorResizeNWSE
|
|
CursorResizeNESW
|
|
CursorNotAllowed
|
|
CursorCrosshair
|
|
)
|
|
|
|
type Align uint8
|
|
|
|
const (
|
|
AlignLeft Align = iota
|
|
AlignCenter
|
|
AlignRight
|
|
)
|
|
|
|
type Clipboard interface {
|
|
Text() string
|
|
SetText(string)
|
|
}
|
|
|
|
const (
|
|
maxOverlays = 16
|
|
doubleClick = 0.35
|
|
)
|
|
|
|
type Context struct {
|
|
Theme Theme
|
|
Clipboard Clipboard
|
|
Time float64
|
|
|
|
font *text.Font
|
|
fonts *text.FontSet
|
|
list *draw.List
|
|
in *input.State
|
|
mouse mathx.Vec2
|
|
size mathx.Vec2
|
|
scale float32
|
|
frame uint64
|
|
|
|
hot ID
|
|
active ID
|
|
captured ID
|
|
pressed ID
|
|
focus ID
|
|
|
|
lost ID
|
|
lostText string
|
|
lostHasText bool
|
|
lostNext ID
|
|
lostNextText string
|
|
lostNextHas bool
|
|
|
|
cursor Cursor
|
|
cursors []Cursor
|
|
ids []ID
|
|
clips []mathx.Rect
|
|
|
|
wantsMouse bool
|
|
wantsText bool
|
|
textRect mathx.Rect
|
|
textCursor int
|
|
comp string
|
|
compCursor int
|
|
shape CursorShape
|
|
|
|
modal ID
|
|
inModal bool
|
|
|
|
overlays [maxOverlays]mathx.Rect
|
|
overlayN int
|
|
nextOverlays [maxOverlays]mathx.Rect
|
|
nextN int
|
|
overlayDepth int
|
|
overlayClips []int
|
|
|
|
edit *textedit.Buffer
|
|
editID ID
|
|
editScroll float32
|
|
|
|
lastClickID ID
|
|
lastClickTime float64
|
|
doubleID ID
|
|
|
|
inFrame bool
|
|
}
|
|
|
|
func New(font *text.Font) *Context {
|
|
return &Context{
|
|
Theme: DefaultTheme(),
|
|
font: font,
|
|
list: draw.NewList(256),
|
|
scale: 1,
|
|
}
|
|
}
|
|
|
|
func (c *Context) SetFonts(set *text.FontSet) {
|
|
c.fonts = set
|
|
if set != nil {
|
|
if f := set.Closest(c.Theme.FontSize, c.scale); f != nil {
|
|
c.font = f
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *Context) SetFont(f *text.Font) {
|
|
c.font = f
|
|
}
|
|
|
|
func (c *Context) Begin(in *input.State, size mathx.Vec2, scale float32) {
|
|
if scale <= 0 {
|
|
scale = 1
|
|
}
|
|
c.in = in
|
|
c.size = size
|
|
c.scale = scale
|
|
c.frame++
|
|
c.list.Reset()
|
|
c.wantsMouse = false
|
|
c.wantsText = false
|
|
c.shape = CursorDefault
|
|
c.inFrame = true
|
|
c.mouse = mathx.Vec2{}
|
|
if in != nil {
|
|
c.mouse = in.MousePos()
|
|
}
|
|
c.hot = 0
|
|
c.pressed = 0
|
|
c.doubleID = 0
|
|
c.ids = c.ids[:0]
|
|
c.cursors = c.cursors[:0]
|
|
c.clips = c.clips[:0]
|
|
c.overlayClips = c.overlayClips[:0]
|
|
c.overlayDepth = 0
|
|
c.inModal = false
|
|
c.overlays = c.nextOverlays
|
|
c.overlayN = c.nextN
|
|
c.nextN = 0
|
|
c.lost, c.lostText, c.lostHasText = c.lostNext, c.lostNextText, c.lostNextHas
|
|
c.lostNext, c.lostNextText, c.lostNextHas = 0, "", false
|
|
if c.fonts != nil {
|
|
if f := c.fonts.Closest(c.Theme.FontSize, scale); f != nil {
|
|
c.font = f
|
|
}
|
|
}
|
|
c.cursor = Cursor{
|
|
X: c.Theme.Padding,
|
|
Y: c.Theme.Padding,
|
|
W: 200,
|
|
RowH: c.Theme.RowHeight,
|
|
Pad: c.Theme.Padding,
|
|
Space: c.Theme.Spacing,
|
|
}
|
|
}
|
|
|
|
func (c *Context) End() *draw.List {
|
|
c.inFrame = false
|
|
if c.in != nil {
|
|
if c.in.ButtonPressed(input.MouseLeft) && c.focus != 0 && c.pressed != c.focus {
|
|
c.markLost(c.focus)
|
|
c.focus = 0
|
|
}
|
|
down := c.in.ButtonDown(input.MouseLeft) || c.in.ButtonDown(input.MouseRight) || c.in.ButtonDown(input.MouseMiddle)
|
|
if !down {
|
|
c.captured = 0
|
|
if !c.in.ButtonReleased(input.MouseLeft) {
|
|
c.active = 0
|
|
}
|
|
}
|
|
}
|
|
return c.list
|
|
}
|
|
|
|
func (c *Context) Frame() uint64 { return c.frame }
|
|
func (c *Context) List() *draw.List { return c.list }
|
|
func (c *Context) Font() *text.Font { return c.font }
|
|
func (c *Context) Fonts() *text.FontSet { return c.fonts }
|
|
func (c *Context) Scale() float32 { return c.scale }
|
|
func (c *Context) Size() mathx.Vec2 { return c.size }
|
|
func (c *Context) Input() *input.State { return c.in }
|
|
func (c *Context) Hot() ID { return c.hot }
|
|
func (c *Context) Active() ID { return c.active }
|
|
func (c *Context) Captured() ID { return c.captured }
|
|
func (c *Context) Focus() ID { return c.focus }
|
|
func (c *Context) WantsMouse() bool { return c.wantsMouse }
|
|
func (c *Context) WantsKeyboard() bool { return c.focus != 0 }
|
|
func (c *Context) Mouse() mathx.Vec2 { return c.mouse }
|
|
func (c *Context) Hovered(id ID) bool { return c.hot == id }
|
|
func (c *Context) CursorShape() CursorShape { return c.shape }
|
|
|
|
func (c *Context) SetCursorShape(s CursorShape) {
|
|
c.shape = s
|
|
}
|
|
|
|
func (c *Context) Cursor() *Cursor {
|
|
return &c.cursor
|
|
}
|
|
|
|
func (c *Context) PushCursor(cur Cursor) {
|
|
c.cursors = append(c.cursors, c.cursor)
|
|
c.cursor = cur
|
|
}
|
|
|
|
func (c *Context) PopCursor() {
|
|
if n := len(c.cursors); n > 0 {
|
|
c.cursor = c.cursors[n-1]
|
|
c.cursors = c.cursors[:n-1]
|
|
}
|
|
}
|
|
|
|
func (c *Context) Region(r mathx.Rect) {
|
|
p := c.Theme.Padding
|
|
c.PushCursor(Cursor{
|
|
X: r.X + p, Y: r.Y + p, W: r.W - 2*p,
|
|
RowH: c.Theme.RowHeight, Pad: p, Space: c.Theme.Spacing,
|
|
})
|
|
}
|
|
|
|
func (c *Context) top() ID {
|
|
if n := len(c.ids); n > 0 {
|
|
return c.ids[n-1]
|
|
}
|
|
return ID(fnvOffset)
|
|
}
|
|
|
|
func (c *Context) ID(label string) ID {
|
|
h := uint64(c.top())
|
|
for i := 0; i < len(label); i++ {
|
|
h ^= uint64(label[i])
|
|
h *= fnvPrime
|
|
}
|
|
return ID(h)
|
|
}
|
|
|
|
func (c *Context) IDInt(i int) ID {
|
|
return IDSeq(c.top(), i)
|
|
}
|
|
|
|
func (c *Context) PushID(label string) {
|
|
c.ids = append(c.ids, c.ID(label))
|
|
}
|
|
|
|
func (c *Context) PushIDInt(i int) {
|
|
c.ids = append(c.ids, c.IDInt(i))
|
|
}
|
|
|
|
func (c *Context) PushRawID(id ID) {
|
|
c.ids = append(c.ids, id)
|
|
}
|
|
|
|
func (c *Context) PopID() {
|
|
if n := len(c.ids); n > 0 {
|
|
c.ids = c.ids[:n-1]
|
|
}
|
|
}
|
|
|
|
func intersect(a, b mathx.Rect) mathx.Rect {
|
|
x0 := max(a.X, b.X)
|
|
y0 := max(a.Y, b.Y)
|
|
x1 := min(a.X+a.W, b.X+b.W)
|
|
y1 := min(a.Y+a.H, b.Y+b.H)
|
|
return mathx.Rect{X: x0, Y: y0, W: max(0, x1-x0), H: max(0, y1-y0)}
|
|
}
|
|
|
|
func (c *Context) PushClip(r mathx.Rect) {
|
|
if n := len(c.clips); n > 0 {
|
|
r = intersect(r, c.clips[n-1])
|
|
}
|
|
c.clips = append(c.clips, r)
|
|
c.list.PushClip(r)
|
|
}
|
|
|
|
func (c *Context) PopClip() {
|
|
if n := len(c.clips); n > 0 {
|
|
c.clips = c.clips[:n-1]
|
|
c.list.PopClip()
|
|
}
|
|
}
|
|
|
|
func (c *Context) ClipRect() (mathx.Rect, bool) {
|
|
if n := len(c.clips); n > 0 {
|
|
return c.clips[n-1], true
|
|
}
|
|
return mathx.Rect{}, false
|
|
}
|
|
|
|
func (c *Context) Hit(r mathx.Rect) bool {
|
|
if !r.Contains(c.mouse) {
|
|
return false
|
|
}
|
|
if clip, ok := c.ClipRect(); ok && !clip.Contains(c.mouse) {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (c *Context) blocked() bool {
|
|
if c.modal != 0 && !c.inModal {
|
|
return true
|
|
}
|
|
if c.overlayDepth > 0 {
|
|
return false
|
|
}
|
|
for i := 0; i < c.overlayN; i++ {
|
|
if c.overlays[i].Contains(c.mouse) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (c *Context) MouseInOverlay() bool {
|
|
for i := 0; i < c.overlayN; i++ {
|
|
if c.overlays[i].Contains(c.mouse) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (c *Context) MouseOver(id ID, r mathx.Rect) bool {
|
|
if c.blocked() || !c.Hit(r) {
|
|
return false
|
|
}
|
|
if c.captured != 0 && c.captured != id {
|
|
return false
|
|
}
|
|
if c.active == 0 || c.active == id {
|
|
c.hot = id
|
|
}
|
|
c.wantsMouse = true
|
|
return true
|
|
}
|
|
|
|
func (c *Context) Pressed(id ID, r mathx.Rect) bool {
|
|
if !c.MouseOver(id, r) || c.in == nil || !c.in.ButtonPressed(input.MouseLeft) {
|
|
return false
|
|
}
|
|
if c.active != 0 && c.active != id {
|
|
return false
|
|
}
|
|
c.active = id
|
|
c.pressed = id
|
|
if c.lastClickID == id && c.Time-c.lastClickTime < doubleClick {
|
|
c.doubleID = id
|
|
c.lastClickID = 0
|
|
} else {
|
|
c.lastClickID = id
|
|
c.lastClickTime = c.Time
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (c *Context) DoubleClicked(id ID) bool {
|
|
return c.doubleID == id
|
|
}
|
|
|
|
func (c *Context) Clicked(id ID, r mathx.Rect) bool {
|
|
c.Pressed(id, r)
|
|
if c.in == nil {
|
|
return false
|
|
}
|
|
if c.active == id && c.in.ButtonReleased(input.MouseLeft) {
|
|
c.active = 0
|
|
return c.Hit(r) && !c.blocked()
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (c *Context) RightClicked(id ID, r mathx.Rect) bool {
|
|
return c.MouseOver(id, r) && c.in != nil && c.in.ButtonPressed(input.MouseRight)
|
|
}
|
|
|
|
func (c *Context) Drag(id ID, r mathx.Rect) (mathx.Vec2, bool) {
|
|
c.Pressed(id, r)
|
|
if c.in == nil || c.active != id {
|
|
return mathx.Vec2{}, false
|
|
}
|
|
c.wantsMouse = true
|
|
if c.in.ButtonReleased(input.MouseLeft) && !c.in.ButtonDown(input.MouseLeft) {
|
|
c.active = 0
|
|
}
|
|
return c.in.MouseDelta(), true
|
|
}
|
|
|
|
func (c *Context) CaptureMouse(id ID) {
|
|
c.captured = id
|
|
c.active = id
|
|
}
|
|
|
|
func (c *Context) ReleaseMouse() {
|
|
c.captured = 0
|
|
c.active = 0
|
|
}
|
|
|
|
func (c *Context) MouseDown(b input.Button) bool {
|
|
return c.in != nil && c.in.ButtonDown(b)
|
|
}
|
|
|
|
func (c *Context) MousePressed(b input.Button) bool {
|
|
return c.in != nil && c.in.ButtonPressed(b)
|
|
}
|
|
|
|
func (c *Context) MouseReleased(b input.Button) bool {
|
|
return c.in != nil && c.in.ButtonReleased(b)
|
|
}
|
|
|
|
func (c *Context) MouseDelta() mathx.Vec2 {
|
|
if c.in == nil {
|
|
return mathx.Vec2{}
|
|
}
|
|
return c.in.MouseDelta()
|
|
}
|
|
|
|
func (c *Context) WheelY() float32 {
|
|
if c.in == nil {
|
|
return 0
|
|
}
|
|
return c.in.Wheel().Y
|
|
}
|
|
|
|
func (c *Context) KeyPressed(k input.Scancode) bool {
|
|
return c.in != nil && c.in.KeyPressed(k)
|
|
}
|
|
|
|
func (c *Context) KeyRepeated(k input.Scancode) bool {
|
|
return c.in != nil && c.in.KeyRepeated(k)
|
|
}
|
|
|
|
func (c *Context) KeyRepeat(k input.Scancode) bool {
|
|
return c.KeyRepeated(k)
|
|
}
|
|
|
|
func (c *Context) Mods() input.Mods {
|
|
if c.in == nil {
|
|
return 0
|
|
}
|
|
return c.in.Mods()
|
|
}
|
|
|
|
func (c *Context) Primary() bool {
|
|
return c.Mods().Primary()
|
|
}
|
|
|
|
func (c *Context) WordMod() input.Mods {
|
|
return input.WordMod()
|
|
}
|
|
|
|
func (c *Context) TextInput() []byte {
|
|
if c.in == nil {
|
|
return nil
|
|
}
|
|
return c.in.Text()
|
|
}
|
|
|
|
func (c *Context) WantTextInput(r mathx.Rect, cursor int) {
|
|
c.wantsText = true
|
|
c.textRect = r
|
|
c.textCursor = cursor
|
|
}
|
|
|
|
func (c *Context) WantsTextInput() (bool, mathx.Rect, int) {
|
|
return c.wantsText, c.textRect, c.textCursor
|
|
}
|
|
|
|
func (c *Context) SetComposition(s string, cursor int) {
|
|
c.comp = s
|
|
c.compCursor = cursor
|
|
}
|
|
|
|
func (c *Context) Composition() (string, int) {
|
|
return c.comp, c.compCursor
|
|
}
|
|
|
|
func (c *Context) markLost(id ID) {
|
|
c.lostNext = id
|
|
c.lostNextText, c.lostNextHas = "", false
|
|
if c.editID == id && c.edit != nil {
|
|
c.lostNextText = c.edit.String()
|
|
c.lostNextHas = true
|
|
c.editID = 0
|
|
}
|
|
c.comp, c.compCursor = "", 0
|
|
}
|
|
|
|
func (c *Context) SetFocus(id ID) {
|
|
if c.focus != 0 && c.focus != id {
|
|
c.markLost(c.focus)
|
|
}
|
|
c.focus = id
|
|
}
|
|
|
|
func (c *Context) CancelFocus() {
|
|
c.focus = 0
|
|
c.editID = 0
|
|
c.comp, c.compCursor = "", 0
|
|
}
|
|
|
|
func (c *Context) LostFocus(id ID) bool {
|
|
return id != 0 && c.lost == id
|
|
}
|
|
|
|
func (c *Context) LostText(id ID) (string, bool) {
|
|
if id == 0 || c.lost != id || !c.lostHasText {
|
|
return "", false
|
|
}
|
|
return c.lostText, true
|
|
}
|
|
|
|
func (c *Context) EditBuffer(id ID, initial string) *textedit.Buffer {
|
|
if c.edit == nil {
|
|
c.edit = textedit.New("")
|
|
}
|
|
if c.editID != id {
|
|
c.editID = id
|
|
c.edit.Set(initial)
|
|
c.editScroll = 0
|
|
}
|
|
return c.edit
|
|
}
|
|
|
|
func (c *Context) EditingID() ID {
|
|
return c.editID
|
|
}
|
|
|
|
func (c *Context) EndEdit() {
|
|
c.editID = 0
|
|
}
|
|
|
|
func (c *Context) EditScroll() *float32 {
|
|
return &c.editScroll
|
|
}
|
|
|
|
func (c *Context) OpenModal(id ID) {
|
|
c.modal = id
|
|
}
|
|
|
|
func (c *Context) CloseModal() {
|
|
c.modal = 0
|
|
}
|
|
|
|
func (c *Context) ModalOpen(id ID) bool {
|
|
return id != 0 && c.modal == id
|
|
}
|
|
|
|
func (c *Context) ModalActive() bool {
|
|
return c.modal != 0
|
|
}
|
|
|
|
func (c *Context) BeginModal(id ID, r mathx.Rect) bool {
|
|
if c.modal != id {
|
|
return false
|
|
}
|
|
c.inModal = true
|
|
full := mathx.Rect{W: c.size.X, H: c.size.Y}
|
|
c.BeginOverlay(full, draw.LayerModal)
|
|
c.list.Rect(full, c.Theme.Overlay)
|
|
c.list.Rect(r, c.Theme.Panel)
|
|
c.list.RectOutline(r, c.Theme.Border, 1)
|
|
c.PushClip(r)
|
|
c.Region(r)
|
|
return true
|
|
}
|
|
|
|
func (c *Context) EndModal() {
|
|
c.PopCursor()
|
|
c.PopClip()
|
|
c.EndOverlay()
|
|
c.inModal = false
|
|
}
|
|
|
|
func (c *Context) BeginOverlay(r mathx.Rect, layer int) {
|
|
if c.nextN < maxOverlays {
|
|
c.nextOverlays[c.nextN] = r
|
|
c.nextN++
|
|
}
|
|
c.overlayDepth++
|
|
c.overlayClips = append(c.overlayClips, len(c.clips))
|
|
c.clips = append(c.clips, r)
|
|
c.list.PushLayer(layer)
|
|
c.list.PushClip(r)
|
|
}
|
|
|
|
func (c *Context) EndOverlay() {
|
|
n := len(c.overlayClips)
|
|
if n == 0 {
|
|
return
|
|
}
|
|
base := c.overlayClips[n-1]
|
|
c.overlayClips = c.overlayClips[:n-1]
|
|
for len(c.clips) > base+1 {
|
|
c.PopClip()
|
|
}
|
|
c.clips = c.clips[:base]
|
|
c.list.PopClip()
|
|
c.list.PopLayer()
|
|
c.overlayDepth--
|
|
}
|
|
|
|
func (c *Context) InOverlay() bool {
|
|
return c.overlayDepth > 0
|
|
}
|
|
|
|
func (c *Context) Rect(r mathx.Rect, col mathx.Color) {
|
|
c.list.Rect(r, col)
|
|
}
|
|
|
|
func (c *Context) Outline(r mathx.Rect, col mathx.Color) {
|
|
c.list.RectOutline(r, col, 1)
|
|
}
|
|
|
|
func (c *Context) Text(s string, pos mathx.Vec2, col mathx.Color) {
|
|
c.list.Text(s, pos, col)
|
|
}
|
|
|
|
func (c *Context) TextIn(r mathx.Rect, s string, col mathx.Color, align Align) {
|
|
x := r.X + c.Theme.Padding
|
|
switch align {
|
|
case AlignCenter:
|
|
x = r.X + (r.W-c.TextSize(s).X)/2
|
|
case AlignRight:
|
|
x = r.X + r.W - c.Theme.Padding - c.TextSize(s).X
|
|
}
|
|
c.list.Text(s, mathx.Vec2{X: x, Y: c.TextY(r)}, col)
|
|
}
|
|
|
|
func (c *Context) LineHeight() float32 {
|
|
if c.font == nil {
|
|
return 0
|
|
}
|
|
return c.font.LineHeight
|
|
}
|
|
|
|
func (c *Context) TextY(r mathx.Rect) float32 {
|
|
return r.Y + (r.H-c.LineHeight())/2
|
|
}
|
|
|
|
func (c *Context) TextSize(s string) mathx.Vec2 {
|
|
if c.font == nil {
|
|
return mathx.Vec2{}
|
|
}
|
|
return c.font.Measure(s)
|
|
}
|
|
|
|
func (c *Context) WidgetColor(id ID) mathx.Color {
|
|
switch {
|
|
case c.active == id:
|
|
return c.Theme.WidgetDown
|
|
case c.hot == id:
|
|
return c.Theme.WidgetHot
|
|
}
|
|
return c.Theme.Widget
|
|
}
|