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 _) } }