package ui import ( "image" "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) registerBuilder(menus.TypeInventorySelect, registerInventorySelect) } // 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 } // An inventory select is a sort of radio button. If 2 share the same menu, // selecting one deselects the other. Otherwise, they act like checkboxes type inventorySelect struct { checkbox parentPath string others []*inventorySelect } // 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 if err != nil { return err } checkbox := &checkbox{ button: button{ path: r.Path(), baseSpr: sprites[0], // unchecked clickSpr: sprites[2], // checked frozenSpr: sprites[1], hoverImpl: hoverImpl{text: r.Text}, }, valueImpl: valueImpl{str: "0"}, } d.clickables = append(d.clickables, checkbox) d.freezables = append(d.freezables, checkbox) d.hoverables = append(d.hoverables, checkbox) d.paintables = append(d.paintables, checkbox) d.valueables = append(d.valueables, checkbox) 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 registerInventorySelect(d *Driver, r *menus.Record) error { sprites, err := d.menu.Sprites(r.Share, 3) // unchecked, checked, disabled if err != nil { return err } element := &inventorySelect{ checkbox: checkbox{ button: button{ path: r.Path(), baseSpr: sprites[0], // unchecked clickSpr: sprites[1], // checked frozenSpr: sprites[2], // disabled hoverImpl: hoverImpl{text: r.Text}, }, valueImpl: valueImpl{str: "0"}, }, } d.clickables = append(d.clickables, element) d.freezables = append(d.freezables, element) d.hoverables = append(d.hoverables, element) d.paintables = append(d.paintables, element) d.valueables = append(d.valueables, element) if r.Parent == nil { return nil } element.parentPath = r.Parent.Path() // Now update all inventory selects belonging to the same menu so they share // a list of all inventory selects. This will be replaced several times as // the menu is built. var inventorySelects []*inventorySelect for _, valueable := range d.valueables { if is, ok := valueable.(*inventorySelect); ok && is.parentPath == element.parentPath { inventorySelects = append(inventorySelects, is) } } for _, is := range inventorySelects { is.others = inventorySelects } // Select the first in the list if len(inventorySelects) == 1 { element.setValue("1") } return nil } func (c *checkbox) registerMouseClick() { if c.value() == "1" { // Click disables c.setValue("0") } else { // Click enables c.setValue("1") } } func (c *checkbox) regions(tick int) []region { if c.isFrozen() { return oneRegion(c.bounds().Min, c.frozenSpr.Image) } if c.value() == "1" { return oneRegion(c.bounds().Min, c.clickSpr.Image) } 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) 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()) } func (i *inventorySelect) registerMouseClick() { // Do nothing if we're already selected if i.value() == "1" { return } // Turn us on, turn everyone else off for _, other := range i.others { other.setValue("0") } i.setValue("1") }