2023-06-16 03:51:44 +07:00

220 lines
6.3 KiB
Scala

package ocelot.desktop.ui.widget
import ocelot.desktop.ColorScheme
import ocelot.desktop.audio.{SoundSource, SoundSources}
import ocelot.desktop.color.Color
import ocelot.desktop.geometry.Size2D
import ocelot.desktop.graphics.Graphics
import ocelot.desktop.ui.event.handlers.{ClickHandler, HoverHandler}
import ocelot.desktop.ui.event.{HoverEvent, MouseEvent}
import ocelot.desktop.ui.widget.IconButton.Mode
import ocelot.desktop.ui.widget.tooltip.LabelTooltip
import ocelot.desktop.util.animation.{ColorAnimation, ValueAnimation}
import ocelot.desktop.util.{DrawUtils, Spritesheet}
class IconButton(
releasedIcon: String,
pressedIcon: String,
releasedColor: Color = Color.White,
pressedColor: Color = Color.White,
sizeMultiplier: Float = 1,
mode: Mode = Mode.Regular,
drawBackground: Boolean = false,
padding: Float = 0,
tooltip: Option[String] = None,
val model: IconButton.Model = IconButton.DefaultModel(false),
) extends Widget
with ClickHandler
with HoverHandler
with ClickSoundSource {
private val speed = 10f
override def receiveMouseEvents = true
private var prevModelValue = model.pressed
eventHandlers += {
case HoverEvent(HoverEvent.State.Enter) => onHoverEnter()
case HoverEvent(HoverEvent.State.Leave) => onHoverLeave()
case MouseEvent(MouseEvent.State.Press, MouseEvent.Button.Left) =>
mode match {
case Mode.Switch if model.pressed => handleRelease()
case Mode.Radio if model.pressed =>
case _ => handlePress()
}
clickSoundSource.play()
case MouseEvent(MouseEvent.State.Release, MouseEvent.Button.Left) =>
mode match {
case Mode.Regular => handleRelease()
case Mode.Switch =>
case Mode.Radio =>
}
}
def onPressed(): Unit = {}
def onReleased(): Unit = {}
private def onHoverEnter(): Unit = {
for (tooltip <- labelTooltip) {
root.get.tooltipPool.addTooltip(tooltip)
}
}
private def onHoverLeave(): Unit = {
for (tooltip <- labelTooltip) {
root.get.tooltipPool.closeTooltip(tooltip)
}
}
private def targetColor: Color = if (model.pressed) pressedColor else releasedColor
private def targetAlpha: Float = if (model.pressed) 1f else 0f
private val colorAnimation = new ColorAnimation(targetColor, speed)
private val alphaAnimation = new ValueAnimation(targetAlpha, speed)
private def handlePress(): Unit = {
model.pressed = true
onPressed()
}
private def handleRelease(): Unit = {
model.pressed = false
onReleased()
}
size = minimumSize
private def releasedIconSize: Size2D = Spritesheet.spriteSize(releasedIcon) * sizeMultiplier
private def pressedIconSize: Size2D = Spritesheet.spriteSize(pressedIcon) * sizeMultiplier
override def minimumSize: Size2D = releasedIconSize.max(pressedIconSize) + (padding * 2.0f)
override def maximumSize: Size2D = minimumSize
private val labelTooltip = tooltip.map(label => new LabelTooltip(label))
override def draw(g: Graphics): Unit = {
if (drawBackground) {
g.rect(bounds, ColorScheme("ButtonBackground"))
DrawUtils.ring(g, position.x, position.y, width, height, 2, ColorScheme("ButtonBorder"))
}
if (alphaAnimation.value < 1f) {
g.sprite(
releasedIcon,
position + (size - releasedIconSize) / 2f,
releasedIconSize,
releasedColor.toRGBANorm.mapA(_ * (1f - alphaAnimation.value)),
)
}
if (alphaAnimation.value > 0f) {
g.sprite(
pressedIcon,
position + (size - pressedIconSize) / 2f,
pressedIconSize,
pressedColor.toRGBANorm.mapA(_ * alphaAnimation.value),
)
}
}
override def update(): Unit = {
super.update()
val nextModelValue = model.pressed
if (prevModelValue != nextModelValue) {
prevModelValue = nextModelValue
colorAnimation.goto(targetColor)
alphaAnimation.goto(targetAlpha)
}
colorAnimation.update()
alphaAnimation.update()
}
override protected def clickSoundSource: SoundSource = SoundSources.InterfaceClick
}
object IconButton {
sealed trait Mode
object Mode {
/**
* Your regular, run-of-the-mill button you love.
*
* When the LMB is depressed, [[IconButton.onPressed]] is called.
* After it's released, [[IconButton.onReleased]] is called.
*/
case object Regular extends Mode
/**
* A toggleable button, or a switch.
*
* Pressing the LMB while the button is pressed releases it, and [[IconButton.onReleased]] is called.
* Pressing the LMB while the button is released depresses it, and [[IconButton.onPressed]] is called.
*/
case object Switch extends Mode
/**
* A radio button is like a switch except clicking on it while it's depressed doesn't do anything.
*
* [[IconButton.onReleased]] is never called (unless you do it yourself).
*
* Managing a group of radio buttons is your responsibility.
*/
case object Radio extends Mode
}
/**
* This is the visible button state.
*
* When [[IconButton.update]] notices a state change, it runs its animations.
*
* Note that callbacks are run after running the setter regardless of whether it does anything.
*/
trait Model {
/**
* Whether to display the pressed button state.
*/
def pressed: Boolean
/**
* This setter is used by [[IconButton]] to respond to user interaction.
*
* You may want to override this method to do nothing
* if you're proxying another model and updating it in callbacks.
*/
def pressed_=(newValue: Boolean): Unit
}
/**
* This is the default model that simply reads from a variable and writes to it.
*/
case class DefaultModel(override var pressed: Boolean) extends Model
/**
* This is a model that ignores user input (presumably you handle it in callbacks).
*/
class ReadOnlyModel(f: () => Boolean) extends Model {
override def pressed: Boolean = f()
override def pressed_=(newValue: Boolean): Unit = {}
}
object ReadOnlyModel {
/**
* A utility method to create a new instance of [[ReadOnlyModel]].
*
* The value returned by [[ReadOnlyModel.pressed]] is obtained
* by evaluating the provided expression '''every time'''.
*/
def apply(value: => Boolean): ReadOnlyModel = new ReadOnlyModel(value _)
}
}