package d2gui import ( "log" "math" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2ui" ) // LayoutScrollbar is a scrollbar that can be used with any layout // and attaches to a main layout. You need to use a wrapper for your content // as main layout in order for the scrollbar to work properly type LayoutScrollbar struct { sliderSprites []*d2ui.Sprite gutterSprites []*d2ui.Sprite parentLayout *Layout targetLayout *Layout sliderLayout *Layout arrowUpLayout *Layout arrowDownLayout *Layout arrowUpSprite *d2ui.Sprite arrowDownSprite *d2ui.Sprite maxY int minY int arrowClickSliderOffset int viewportSize int contentSize int clickedAtY int mouseYOnSlider int lastY int gutterHeight int sliderHeight int contentToViewRatio float32 // isVisible bool arrowUpClicked bool arrowDownClicked bool sliderClicked bool } const ( textSliderPartWidth = 12 textSliderPartHeight = 13 arrrowClickContentOffsetPercentage = 0.02 oneHundredPercent = 1.0 ) const ( textSliderPartArrowDownHollow int = iota + 8 textSliderPartArrowUpHollow textSliderPartArrowDownFilled int = 10 textSliderPartArrowUpFilled int = 11 // textSliderPartSquare textSliderPartInnerGutter int = 13 textSliderPartFillingVariation1 int = 14 ) // NewLayoutScrollbar attaches a scrollbar to the parentLayout to control the targetLayout func NewLayoutScrollbar(parentLayout, targetLayout *Layout) *LayoutScrollbar { parentW, parentH := parentLayout.GetSize() _, targetH := targetLayout.GetSize() // nolint:gomnd // constant gutterHeight := parentH - (2 * textSliderPartHeight) viewportPercentage := oneHundredPercent - (float32(targetH-parentH) / float32(targetH)) sliderHeight := int(float32(gutterHeight) * viewportPercentage) x, y := parentW-textSliderPartWidth, 0 ret := &LayoutScrollbar{ sliderSprites: []*d2ui.Sprite{}, gutterSprites: []*d2ui.Sprite{}, } ret.contentToViewRatio = viewportPercentage ret.contentToViewRatio = float32(targetH) / float32(gutterHeight) ret.gutterHeight = gutterHeight ret.sliderHeight = sliderHeight ret.minY = y + textSliderPartHeight ret.maxY = (y + parentH) - (textSliderPartHeight + sliderHeight) ret.contentSize = targetH ret.arrowClickSliderOffset = int(float32(sliderHeight) * arrrowClickContentOffsetPercentage) ret.viewportSize = parentH arrowUpLayout := parentLayout.AddLayout(PositionTypeAbsolute) arrowUpLayout.SetSize(textSliderPartWidth, textSliderPartHeight) arrowUpLayout.SetPosition(x, 0) ret.arrowUpLayout = arrowUpLayout gutterLayout := parentLayout.AddLayout(PositionTypeAbsolute) gutterLayout.SetSize(textSliderPartWidth, gutterHeight) gutterLayout.SetPosition(x, textSliderPartHeight) sliderLayout := parentLayout.AddLayout(PositionTypeAbsolute) sliderLayout.SetPosition(x, textSliderPartHeight) sliderLayout.SetSize(textSliderPartWidth, sliderHeight) sliderLayout.SetMouseClickHandler(ret.OnSliderMouseClick) arrowDownLayout := parentLayout.AddLayout(PositionTypeAbsolute) arrowDownLayout.SetSize(textSliderPartWidth, textSliderPartHeight) arrowDownLayout.SetPosition(x, textSliderPartHeight+gutterHeight) ret.arrowDownLayout = arrowDownLayout ret.sliderLayout = sliderLayout ret.parentLayout = parentLayout ret.targetLayout = targetLayout ret.parentLayout.AdjustEntryPlacement() ret.targetLayout.AdjustEntryPlacement() ret.sliderLayout.AdjustEntryPlacement() return ret } // Load sets the scrollbar layouts and loads the sprites func (scrollbar *LayoutScrollbar) Load(ui *d2ui.UIManager) error { arrowUpX, arrowUpY := scrollbar.arrowUpLayout.ScreenPos() arrowUpSprite, _ := ui.NewSprite(d2resource.TextSlider, d2resource.PaletteSky) if err := arrowUpSprite.SetCurrentFrame(textSliderPartArrowUpFilled); err != nil { return err } arrowUpSprite.SetPosition(arrowUpX, arrowUpY+textSliderPartHeight) scrollbar.arrowUpSprite = arrowUpSprite arrowDownX, arrowDownY := scrollbar.arrowDownLayout.ScreenPos() arrowDownSprite, _ := ui.NewSprite(d2resource.TextSlider, d2resource.PaletteSky) if err := arrowDownSprite.SetCurrentFrame(textSliderPartArrowDownFilled); err != nil { return err } arrowDownSprite.SetPosition(arrowDownX, arrowDownY+textSliderPartHeight) scrollbar.arrowDownSprite = arrowDownSprite // nolint:gomnd // constant gutterParts := int(math.Ceil(float64(scrollbar.gutterHeight+(2*textSliderPartHeight)) / float64(textSliderPartHeight))) sliderParts := int(math.Ceil(float64(scrollbar.sliderHeight) / float64(textSliderPartHeight))) // nolint:gomnd // constant gutterX, gutterY := arrowUpX, arrowUpY+(2*textSliderPartHeight)-1 i := 0 for { if i >= gutterParts { break } f, _ := ui.NewSprite(d2resource.TextSlider, d2resource.PaletteSky) if err := f.SetCurrentFrame(textSliderPartInnerGutter); err != nil { return err } newY := gutterY + (i * (textSliderPartHeight - 1)) f.SetPosition(gutterX, newY) scrollbar.gutterSprites = append(scrollbar.gutterSprites, f) i++ } i = 0 for { if i >= sliderParts { break } f, _ := ui.NewSprite(d2resource.TextSlider, d2resource.PaletteSky) if err := f.SetCurrentFrame(textSliderPartFillingVariation1); err != nil { return err } scrollbar.sliderSprites = append(scrollbar.sliderSprites, f) i++ } scrollbar.updateSliderSpritesPosition() return nil } func (scrollbar *LayoutScrollbar) updateSliderSpritesPosition() { scrollbar.sliderLayout.AdjustEntryPlacement() sliderLayoutX, sliderLayoutY := scrollbar.sliderLayout.ScreenPos() for i, s := range scrollbar.sliderSprites { newY := sliderLayoutY + (i * (textSliderPartHeight - 1)) + textSliderPartHeight s.SetPosition(sliderLayoutX-1, newY) } } // OnSliderMouseClick affects the state of the slider func (scrollbar *LayoutScrollbar) OnSliderMouseClick(event d2interface.MouseEvent) { scrollbar.clickedAtY = event.Y() scrollbar.lastY = scrollbar.clickedAtY scrollbar.mouseYOnSlider = event.Y() - scrollbar.sliderLayout.Sy } func (scrollbar *LayoutScrollbar) moveScaledContentBy(offset int) int { _, y := scrollbar.sliderLayout.GetPosition() newY := y + offset outOfBoundsUp := false outOfBoundsDown := false if newY > scrollbar.maxY { newY = scrollbar.maxY outOfBoundsDown = true } if newY < scrollbar.minY { newY = scrollbar.minY outOfBoundsUp = true } if !outOfBoundsUp && !outOfBoundsDown { scrollbar.clickedAtY += offset if scrollbar.targetLayout != nil { contentX, contentY := scrollbar.targetLayout.GetPosition() scaledOffset := int(math.Round(float64(float32(offset) * scrollbar.contentToViewRatio))) newContentY := contentY - scaledOffset scrollbar.targetLayout.SetPosition(contentX, newContentY) } } if outOfBoundsDown && scrollbar.targetLayout != nil { newContentY := -scrollbar.contentSize + scrollbar.viewportSize scrollbar.targetLayout.SetPosition(0, newContentY) } if outOfBoundsUp && scrollbar.targetLayout != nil { scrollbar.targetLayout.SetPosition(0, 0) } return newY } // OnMouseMove will affect the slider and the content depending on the state fof it func (scrollbar *LayoutScrollbar) OnMouseMove(event d2interface.MouseMoveEvent) { if !scrollbar.sliderClicked { return } sliderX, _ := scrollbar.sliderLayout.GetPosition() newY := scrollbar.moveScaledContentBy(event.Y() - scrollbar.clickedAtY) scrollbar.sliderLayout.SetPosition(sliderX, newY) scrollbar.updateSliderSpritesPosition() } // OnArrowUpClick will move the slider and the content up func (scrollbar *LayoutScrollbar) OnArrowUpClick() { sliderX, _ := scrollbar.sliderLayout.GetPosition() newY := scrollbar.moveScaledContentBy(-scrollbar.arrowClickSliderOffset) scrollbar.sliderLayout.SetPosition(sliderX, newY) scrollbar.updateSliderSpritesPosition() } // OnArrowDownClick will move the slider and the content down func (scrollbar *LayoutScrollbar) OnArrowDownClick() { sliderX, _ := scrollbar.sliderLayout.GetPosition() newY := scrollbar.moveScaledContentBy(scrollbar.arrowClickSliderOffset) scrollbar.sliderLayout.SetPosition(sliderX, newY) scrollbar.updateSliderSpritesPosition() } // SetSliderClicked sets the value of sliderClicked func (scrollbar *LayoutScrollbar) SetSliderClicked(value bool) { scrollbar.sliderClicked = value } // SetArrowUpClicked sets the value of sliderClicked func (scrollbar *LayoutScrollbar) SetArrowUpClicked(value bool) { var arrowSpriteFrame int scrollbar.arrowUpClicked = value if scrollbar.arrowUpClicked { arrowSpriteFrame = textSliderPartArrowUpHollow } else { arrowSpriteFrame = textSliderPartArrowUpFilled } if err := scrollbar.arrowUpSprite.SetCurrentFrame(arrowSpriteFrame); err != nil { log.Printf("unable to set arrow up sprite frame: %v", err) } } // SetArrowDownClicked sets the value of sliderClicked func (scrollbar *LayoutScrollbar) SetArrowDownClicked(value bool) { var arrowSpriteFrame int scrollbar.arrowDownClicked = value if scrollbar.arrowDownClicked { arrowSpriteFrame = textSliderPartArrowDownHollow } else { arrowSpriteFrame = textSliderPartArrowDownFilled } if err := scrollbar.arrowDownSprite.SetCurrentFrame(arrowSpriteFrame); err != nil { log.Printf("unable to set arrow down sprite frame: %v", err) } } // Advance updates the layouts according to the state of the arrown func (scrollbar *LayoutScrollbar) Advance(elapsed float64) error { if scrollbar.arrowDownClicked { scrollbar.OnArrowDownClick() } if scrollbar.arrowUpClicked { scrollbar.OnArrowUpClick() } return nil } // Render draws the scrollbar sprites on the given surface func (scrollbar *LayoutScrollbar) Render(target d2interface.Surface) { for _, s := range scrollbar.gutterSprites { s.Render(target) } for _, s := range scrollbar.sliderSprites { s.Render(target) } scrollbar.arrowUpSprite.Render(target) scrollbar.arrowDownSprite.Render(target) } func (scrollbar *LayoutScrollbar) isInLayoutRect(layout *Layout, px, py int) bool { ww, hh := layout.GetSize() x, y := layout.Sx, layout.Sy if px >= x && px <= x+ww && py >= y && py <= y+hh { return true } return false } // IsSliderClicked returns the state of the slider func (scrollbar *LayoutScrollbar) IsSliderClicked() bool { return scrollbar.sliderClicked } // IsArrowUpClicked returns the state of arrow up clicked func (scrollbar *LayoutScrollbar) IsArrowUpClicked() bool { return scrollbar.arrowUpClicked } // IsArrowDownClicked returns the state of arrow down clicked func (scrollbar *LayoutScrollbar) IsArrowDownClicked() bool { return scrollbar.arrowDownClicked } // IsInArrowUpRect checks if the given point is within the overlay layout rectangle func (scrollbar *LayoutScrollbar) IsInArrowUpRect(px, py int) bool { return scrollbar.isInLayoutRect(scrollbar.arrowUpLayout, px, py) } // IsInArrowDownRect checks if the given point is within the overlay layout rectangle func (scrollbar *LayoutScrollbar) IsInArrowDownRect(px, py int) bool { return scrollbar.isInLayoutRect(scrollbar.arrowDownLayout, px, py) } // IsInSliderRect checks if the given point is within the overlay layout rectangle func (scrollbar *LayoutScrollbar) IsInSliderRect(px, py int) bool { return scrollbar.isInLayoutRect(scrollbar.sliderLayout, px, py) }