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.Pressed, MouseEvent.Button.Left) => mode match { case Mode.Regular => handlePress() case _ => // the other modes are triggered on click. } clickSoundSource.press.play() case MouseEvent(MouseEvent.State.Released, 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() } onClicked() } /** * Called when the state is changed to pressed. * * UX note: override this for [[Mode.Switch]] and [[Mode.Radio]] buttons. * * @see [[onClicked]] for [[Mode.Regular]] buttons. */ def onPressed(): Unit = {} /** * Called when the state is changed to released. * * UX note: override this for [[Mode.Switch]] and [[Mode.Radio]] buttons. * * @see [[onClicked]] for [[Mode.Regular]] buttons. */ def onReleased(): Unit = {} /** * Called when the button is clicked (the mouse button has been released while it's within the button's bounds), * regardless of the state. * * UX note: override this for [[Mode.Regular]] buttons. * * @see [[onPressed]] and [[onReleased]] for [[Mode.Switch]] and [[Mode.Radio]] buttons. */ def onClicked(): 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 _) } }