Add a slider UI widget

I'm not too happy with how I have to configure the step for each one
separately, but it's the best I can do at the moment.
This commit is contained in:
2020-03-24 22:33:26 +00:00
parent 69971b2825
commit 20ad9ae6f8
5 changed files with 266 additions and 9 deletions

View File

@@ -4,6 +4,7 @@ import (
"fmt"
"image"
"log"
"strconv"
"github.com/hajimehoshi/ebiten"
"github.com/hajimehoshi/ebiten/ebitenutil"
@@ -18,7 +19,6 @@ func init() {
// FIXME: these need further investigation / implementation
registerBuilder(menus.TypeOverlay, nil)
registerBuilder(menus.TypeSlider, nil)
}
const (
@@ -56,6 +56,7 @@ type Driver struct {
clickables []clickable
freezables []freezable
hoverables []hoverable
mouseables []mouseable
paintables []paintable
valueables []valueable
@@ -149,6 +150,36 @@ func (d *Driver) OnClick(id string, f func()) error {
return fmt.Errorf("Couldn't find clickable widget %q", id)
}
// FIXME: HURK. Surely I'm missing something? steps is value:offset
func (d *Driver) ConfigureSlider(id string, steps map[int]int) error {
for _, clickable := range d.clickables {
if slider, ok := clickable.(*slider); ok && slider.id() == id {
slider.steps = steps
log.Printf("Found slider %#+v", slider)
return nil
}
}
return fmt.Errorf("Couldn't find slider %q", id)
}
func (d *Driver) ValueInt(id string, into *int) error {
var vStr string
if err := d.Value(id, &vStr); err != nil {
return err
}
value, err := strconv.Atoi(vStr)
if err != nil {
return err
}
*into = value
return nil
}
func (d *Driver) Update(screenX, screenY int) error {
// This will be updated while processing hovers
d.tooltip = ""
@@ -190,6 +221,10 @@ func (d *Driver) Update(screenX, screenY int) error {
d.mouseUpEvent(clickable, inBounds, mouseWasDown, mouseIsDown)
}
for _, mouseable := range d.mouseables {
mouseable.registerMousePosition(d.cursorOrig)
}
return nil
}

View File

@@ -109,6 +109,19 @@ func (h *hoverImpl) setHoverState(hovering bool) {
h.hovering = hovering
}
// Mouseables are told where on the (original) screen the mouse cursor is
type mouseable interface {
registerMousePosition(image.Point)
}
type mouseImpl struct {
pos image.Point
}
func (m *mouseImpl) registerMousePosition(pt image.Point) {
m.pos = pt
}
// Paintable encapsulates one or more regions to be painted to the screen
type paintable interface {
regions(tick int) []region

View File

@@ -1,19 +1,43 @@
package ui
import (
"image"
"log"
"math"
"strconv"
"code.ur.gs/lupine/ordoor/internal/assetstore"
"code.ur.gs/lupine/ordoor/internal/menus"
)
func init() {
registerBuilder(menus.TypeCheckbox, registerCheckbox)
registerBuilder(menus.TypeSlider, registerSlider)
}
// A checkbox can be a fancy button
type checkbox struct {
button
valueImpl
}
// A slider is harder. Two separate elements to render
type slider struct {
path string
baseSpr *assetstore.Sprite
clickSpr *assetstore.Sprite
sliderSpr *assetstore.Sprite
hv bool // horizontal (false) or vertical (true) slider
steps map[int]int // A list of valid steps. value:offset
clickImpl
mouseImpl
valueImpl
}
// A checkbox has 3 sprites, and 3 states: unchecked, checked, disabled.
func registerCheckbox(d *Driver, r *menus.Record) error {
sprites, err := d.menu.Sprites(r.Share, 3) // unchecked, disabled, checked
@@ -41,6 +65,28 @@ func registerCheckbox(d *Driver, r *menus.Record) error {
return nil
}
func registerSlider(d *Driver, r *menus.Record) error {
sprites, err := d.menu.Sprites(r.Share, 3) // base, clicked, slider element
if err != nil {
return err
}
slider := &slider{
path: r.Path(),
baseSpr: sprites[0],
clickSpr: sprites[1],
sliderSpr: sprites[2],
hv: sprites[0].Rect.Dy() > sprites[0].Rect.Dx(), // A best guess
}
d.clickables = append(d.clickables, slider)
d.mouseables = append(d.mouseables, slider)
d.paintables = append(d.paintables, slider)
d.valueables = append(d.valueables, slider)
return nil
}
func (c *checkbox) registerMouseClick() {
if c.value() == "1" { // Click disables
c.setValue("0")
@@ -60,3 +106,154 @@ func (c *checkbox) regions(tick int) []region {
return oneRegion(c.bounds().Min, c.baseSpr.Image)
}
func (s *slider) id() string {
return s.path
}
// The bounds of the slider are the whole thing
func (s *slider) bounds() image.Rectangle {
return s.baseSpr.Rect
}
func (s *slider) registerMouseClick() {
var value int
if s.hv {
value = s.valueFromPix(s.bounds().Min.Y, s.sliderPos().Y)
} else {
value = s.valueFromPix(s.bounds().Min.X, s.sliderPos().X)
}
s.valueImpl.str = strconv.Itoa(value)
log.Printf("Slider value: %#v", s.valueImpl.str)
log.Printf("%#+v", s.steps)
s.clickImpl.registerMouseClick()
}
func (s *slider) regions(tick int) []region {
var out []region
if s.mouseDownState() {
out = append(out, oneRegion(s.bounds().Min, s.clickSpr.Image)...)
} else {
out = append(out, oneRegion(s.bounds().Min, s.baseSpr.Image)...)
}
out = append(out, oneRegion(s.sliderPos(), s.sliderSpr.Image)...)
return out
}
func (s *slider) sliderPos() image.Point {
if s.hv {
return s.sliderPosVertical()
}
return s.sliderPosHorizontal()
}
func (s *slider) sliderPosHorizontal() image.Point {
pos := s.bounds().Min
if s.mouseDownState() {
pos.X = s.constrainPix(s.bounds().Min.X, s.mouseImpl.pos.X)
} else {
pos.X = s.bounds().Min.X + s.offsetFromValue(s.valueInt())
}
return pos
}
func (s *slider) sliderPosVertical() image.Point {
pos := s.bounds().Min
if s.mouseDownState() {
pos.Y = s.constrainPix(s.bounds().Min.Y, s.mouseImpl.pos.Y)
} else {
pos.Y = s.bounds().Min.Y + s.offsetFromValue(s.valueInt())
}
return pos
}
func (s *slider) valueFromPix(start, actual int) int {
if len(s.steps) == 0 {
return actual - start
}
minDistance := 9999
var out int
for value, offset := range s.steps {
pix := start + offset
distance := int(math.Abs(float64(actual - pix)))
if distance < minDistance {
minDistance = distance
out = value
}
}
return out
}
func (s *slider) offsetFromValue(value int) int {
if len(s.steps) == 0 {
return value
}
value = s.constrainValue(value)
return s.steps[value]
}
func (s *slider) constrainPix(start, actual int) int {
if len(s.steps) == 0 {
return actual
}
minDistance := 9999
out := actual
for _, offset := range s.steps {
pix := start + offset
distance := int(math.Abs(float64(actual - pix)))
if distance < minDistance {
minDistance = distance
out = pix
}
}
return out
}
func (s *slider) constrainValue(actual int) int {
if len(s.steps) == 0 {
return actual
}
minDistance := 9999
out := actual
for value, _ := range s.steps {
distance := int(math.Abs(float64(value - actual)))
if distance < minDistance {
minDistance = distance
out = value
}
}
return out
}
func (s *slider) valueInt() int {
v, _ := strconv.Atoi(s.valueImpl.value())
return s.constrainValue(v)
}
func (s *slider) value() string {
return strconv.Itoa(s.valueInt())
}