mirror of
https://gitlab.com/cc-ru/ocelot/ocelot-desktop.git
synced 2025-12-20 11:09:20 +01:00
244 lines
7.7 KiB
Scala
244 lines
7.7 KiB
Scala
package ocelot.desktop.ui.widget
|
|
|
|
import ocelot.desktop.ColorScheme
|
|
import ocelot.desktop.audio.{ClickSoundSource, SoundSource}
|
|
import ocelot.desktop.color.Color
|
|
import ocelot.desktop.geometry.Size2D
|
|
import ocelot.desktop.graphics.Graphics
|
|
import ocelot.desktop.ui.event.handlers.{HoverHandler, MouseHandler}
|
|
import ocelot.desktop.ui.event.{ClickEvent, HoverEvent, MouseEvent}
|
|
import ocelot.desktop.ui.widget.IconButton.{AnimationSpeed, Mode}
|
|
import ocelot.desktop.ui.widget.tooltip.LabelTooltip
|
|
import ocelot.desktop.ui.widget.traits.HoverAnimation
|
|
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,
|
|
darkenActiveColorFactor: Float = 0f,
|
|
val model: IconButton.Model = IconButton.DefaultModel(false),
|
|
) extends Widget with MouseHandler with HoverHandler with HoverAnimation {
|
|
|
|
override protected def receiveClickEvents: Boolean = 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.Regular => handlePress()
|
|
case _ => // the other modes are triggered on click.
|
|
}
|
|
|
|
clickSoundSource.press.play()
|
|
|
|
case MouseEvent(MouseEvent.State.Release, MouseEvent.Button.Left) => mode match {
|
|
case Mode.Regular if model.pressed =>
|
|
handleRelease()
|
|
clickSoundSource.release.play()
|
|
|
|
case _ => // the other modes are triggered on click.
|
|
}
|
|
|
|
case ClickEvent(MouseEvent.Button.Left, _) => mode match {
|
|
case Mode.Regular => // regular buttons are handled above.
|
|
case Mode.Switch if model.pressed =>
|
|
handleRelease()
|
|
clickSoundSource.release.play()
|
|
|
|
case Mode.Radio if model.pressed => // the radio button is already pressed; ignore.
|
|
case Mode.Radio | Mode.Switch =>
|
|
handlePress()
|
|
clickSoundSource.release.play()
|
|
}
|
|
}
|
|
|
|
def onPressed(): Unit = {}
|
|
|
|
def onReleased(): Unit = {}
|
|
|
|
private def onHoverEnter(): Unit = {
|
|
updateColorAnimationTargets()
|
|
|
|
for (tooltip <- labelTooltip) {
|
|
root.get.tooltipPool.addTooltip(tooltip)
|
|
}
|
|
}
|
|
|
|
private def onHoverLeave(): Unit = {
|
|
updateColorAnimationTargets()
|
|
|
|
for (tooltip <- labelTooltip) {
|
|
root.get.tooltipPool.closeTooltip(tooltip)
|
|
}
|
|
}
|
|
|
|
protected def pressedActiveColor: Color = pressedColor.toRGBANorm.mapRgb(_ * (1 - darkenActiveColorFactor))
|
|
protected def releasedActiveColor: Color = releasedColor.toRGBANorm.mapRgb(_ * (1 - darkenActiveColorFactor))
|
|
|
|
private def targetPressedColor: Color = if (isHovered) pressedActiveColor else pressedColor
|
|
private def targetReleasedColor: Color = if (isHovered) releasedActiveColor else releasedColor
|
|
|
|
private def targetIconMix: Float = if (model.pressed) 1f else 0f
|
|
|
|
private val pressedColorAnimation = new ColorAnimation(targetPressedColor, AnimationSpeed)
|
|
private val releasedColorAnimation = new ColorAnimation(targetReleasedColor, AnimationSpeed)
|
|
private val iconMixAnimation = new ValueAnimation(targetIconMix, AnimationSpeed)
|
|
|
|
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))
|
|
|
|
def borderColor: Color = ColorScheme("ButtonBorder")
|
|
|
|
override def draw(g: Graphics): Unit = {
|
|
if (drawBackground) {
|
|
g.rect(bounds, hoverAnimation.color)
|
|
DrawUtils.ring(g, position.x, position.y, width, height, 2, borderColor)
|
|
}
|
|
|
|
if (iconMixAnimation.value < 1f) {
|
|
g.sprite(
|
|
releasedIcon,
|
|
position + (size - releasedIconSize) / 2f,
|
|
releasedIconSize,
|
|
releasedColorAnimation.color.toRGBANorm.mapA(_ * (1f - iconMixAnimation.value)),
|
|
)
|
|
}
|
|
|
|
if (iconMixAnimation.value > 0f) {
|
|
g.sprite(
|
|
pressedIcon,
|
|
position + (size - pressedIconSize) / 2f,
|
|
pressedIconSize,
|
|
pressedColorAnimation.color.toRGBANorm.mapA(_ * iconMixAnimation.value),
|
|
)
|
|
}
|
|
}
|
|
|
|
private def updateColorAnimationTargets(): Unit = {
|
|
pressedColorAnimation.goto(targetPressedColor)
|
|
releasedColorAnimation.goto(targetReleasedColor)
|
|
}
|
|
|
|
protected def updateAnimationTargets(): Unit = {
|
|
updateColorAnimationTargets()
|
|
iconMixAnimation.goto(targetIconMix)
|
|
}
|
|
|
|
override def update(): Unit = {
|
|
super.update()
|
|
|
|
val nextModelValue = model.pressed
|
|
|
|
if (prevModelValue != nextModelValue) {
|
|
prevModelValue = nextModelValue
|
|
updateAnimationTargets()
|
|
}
|
|
|
|
pressedColorAnimation.update()
|
|
releasedColorAnimation.update()
|
|
iconMixAnimation.update()
|
|
}
|
|
|
|
protected def clickSoundSource: ClickSoundSource = SoundSource.InterfaceClick
|
|
}
|
|
|
|
object IconButton {
|
|
val AnimationSpeed = 10f
|
|
|
|
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.
|
|
*
|
|
* Clicking the LMB while the button is pressed releases it, and [[IconButton.onReleased]] is called.
|
|
* Clicking 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 _)
|
|
}
|
|
}
|