From 81840e4fab39fac3f37043fd3dd693fffb7ea439 Mon Sep 17 00:00:00 2001 From: UnicornFreedom Date: Wed, 20 Aug 2025 17:45:31 +0200 Subject: [PATCH] Initial refactoring of TextInput --- .../desktop/{util => graphics}/Font.scala | 19 +- .../ocelot/desktop/graphics/Graphics.scala | 16 +- .../desktop/graphics/ScreenViewport.scala | 4 +- .../scala/ocelot/desktop/ui/UiHandler.scala | 3 +- .../ocelot/desktop/ui/widget/Button.scala | 4 +- .../widget/ChangeSimulationSpeedDialog.scala | 4 +- .../ocelot/desktop/ui/widget/Checkbox.scala | 4 +- .../ocelot/desktop/ui/widget/Slider.scala | 4 +- .../ocelot/desktop/ui/widget/TextInput.scala | 475 ++++++++---------- .../widget/settings/SystemSettingsTab.scala | 2 +- .../ui/widget/statusbar/StatusBar.scala | 4 +- .../ui/widget/traits/HoverAnimation.scala | 18 +- .../windows/OcelotInterfaceWindow.scala | 2 +- 13 files changed, 251 insertions(+), 308 deletions(-) rename src/main/scala/ocelot/desktop/{util => graphics}/Font.scala (91%) diff --git a/src/main/scala/ocelot/desktop/util/Font.scala b/src/main/scala/ocelot/desktop/graphics/Font.scala similarity index 91% rename from src/main/scala/ocelot/desktop/util/Font.scala rename to src/main/scala/ocelot/desktop/graphics/Font.scala index fd174a8..8b8ce6f 100644 --- a/src/main/scala/ocelot/desktop/util/Font.scala +++ b/src/main/scala/ocelot/desktop/graphics/Font.scala @@ -1,7 +1,7 @@ -package ocelot.desktop.util +package ocelot.desktop.graphics import ocelot.desktop.geometry.Rect2D -import ocelot.desktop.graphics.Texture +import ocelot.desktop.util.{Logging, Resource} import org.lwjgl.opengl.GL11 import totoro.ocelot.brain.util.FontUtils @@ -15,8 +15,8 @@ import scala.io.{Codec, Source} class Font(val name: String, val fontSize: Int) extends Resource with Logging { val AtlasWidth = 4096 val AtlasHeight = 4096 - var glyphCount = 0 - var outOfRangeGlyphCount = 0 + private var glyphCount = 0 + private var outOfRangeGlyphCount = 0 private val atlas: BufferedImage = { val icmArr = Array(0.toByte, 0xff.toByte) @@ -134,3 +134,14 @@ class Font(val name: String, val fontSize: Int) extends Resource with Logging { texture.freeResource() } } + +object Font extends Resource { + val NormalFont = new Font("unscii-16", 16) + val SmallFont = new Font("unscii-8", 8) + + override def freeResource(): Unit = { + super.freeResource() + NormalFont.freeResource() + SmallFont.freeResource() + } +} diff --git a/src/main/scala/ocelot/desktop/graphics/Graphics.scala b/src/main/scala/ocelot/desktop/graphics/Graphics.scala index cab960c..51542a4 100644 --- a/src/main/scala/ocelot/desktop/graphics/Graphics.scala +++ b/src/main/scala/ocelot/desktop/graphics/Graphics.scala @@ -6,7 +6,7 @@ import ocelot.desktop.graphics.Texture.MinFilteringMode import ocelot.desktop.graphics.mesh.{Mesh2D, MeshInstance2D, MeshVertex2D} import ocelot.desktop.graphics.render.InstanceRenderer import ocelot.desktop.ui.UiHandler -import ocelot.desktop.util.{Font, Logging, Resource, Spritesheet} +import ocelot.desktop.util.{Logging, Resource, Spritesheet} import org.lwjgl.BufferUtils import org.lwjgl.opengl.{ARBFramebufferObject, GL11, GL21, GL30} @@ -25,9 +25,7 @@ class Graphics(private var width: Int, private var height: Int, private var scal private val shaderProgram = new ShaderProgram("general") private val renderer = new InstanceRenderer[MeshVertex2D, MeshInstance2D](Mesh2D.quad, MeshInstance2D, shaderProgram) - private[graphics] val normalFont = new Font("unscii-16", 16) - private val smallFont = new Font("unscii-8", 8) - private var _font: Font = normalFont + private var _font: Font = Font.NormalFont private var oldFont: Font = _font private val stack = mutable.Stack[GraphicsState](GraphicsState()) @@ -87,8 +85,6 @@ class Graphics(private var width: Int, private var height: Int, private var scal offscreenTexture.freeResource() Spritesheet.freeResource() - smallFont.freeResource() - normalFont.freeResource() renderer.freeResource() shaderProgram.freeResource() screenShaderProgram.freeResource() @@ -97,15 +93,15 @@ class Graphics(private var width: Int, private var height: Int, private var scal def font: Font = _font def setNormalFont(): Unit = { - if (_font == normalFont) return + if (_font == Font.NormalFont) return flush() - _font = normalFont + _font = Font.NormalFont } def setSmallFont(): Unit = { - if (_font == smallFont) return + if (_font == Font.SmallFont) return flush() - _font = smallFont + _font = Font.SmallFont } def save(): Unit = { diff --git a/src/main/scala/ocelot/desktop/graphics/ScreenViewport.scala b/src/main/scala/ocelot/desktop/graphics/ScreenViewport.scala index 54bb181..a154aa9 100644 --- a/src/main/scala/ocelot/desktop/graphics/ScreenViewport.scala +++ b/src/main/scala/ocelot/desktop/graphics/ScreenViewport.scala @@ -4,7 +4,7 @@ import ocelot.desktop.color.{Color, RGBAColorNorm} import ocelot.desktop.geometry.Transform2D import ocelot.desktop.graphics.mesh.{Mesh2D, MeshInstance2D, MeshVertex2D} import ocelot.desktop.graphics.render.InstanceRenderer -import ocelot.desktop.util.{Font, Resource, Spritesheet} +import ocelot.desktop.util.{Resource, Spritesheet} import org.lwjgl.opengl.{ARBFramebufferObject, GL11, GL21, GL30} import java.nio.ByteBuffer @@ -19,7 +19,7 @@ class ScreenViewport(graphics: Graphics, private var _width: Int, private var _h private val shaderProgram = graphics.screenShaderProgram private val renderer = new InstanceRenderer[MeshVertex2D, MeshInstance2D](Mesh2D.quad, MeshInstance2D, shaderProgram) - private val _font = graphics.normalFont + private val _font = Font.NormalFont private val spriteRect = Spritesheet.sprites("Empty") private val emptySpriteTrans = diff --git a/src/main/scala/ocelot/desktop/ui/UiHandler.scala b/src/main/scala/ocelot/desktop/ui/UiHandler.scala index 17da776..1d326aa 100644 --- a/src/main/scala/ocelot/desktop/ui/UiHandler.scala +++ b/src/main/scala/ocelot/desktop/ui/UiHandler.scala @@ -3,7 +3,7 @@ package ocelot.desktop.ui import buildinfo.BuildInfo import ocelot.desktop.audio.{Audio, SoundBuffers, SoundSource} import ocelot.desktop.geometry.{Rect2D, Size2D, Vector2D} -import ocelot.desktop.graphics.Graphics +import ocelot.desktop.graphics.{Font, Graphics} import ocelot.desktop.ui.event.handlers.HoverHandler import ocelot.desktop.ui.event.sources.{BrainEvents, KeyEvents, MouseEvents, ScrollEvents} import ocelot.desktop.ui.event.{Capturing, CapturingEvent, Dispatchable, HoverEvent, MouseEvent} @@ -418,6 +418,7 @@ object UiHandler extends Logging { KeyEvents.destroy() MouseEvents.destroy() graphics.freeResource() + Font.freeResource() Audio.removeAllSources() SoundBuffers.freeResource() Display.destroy() diff --git a/src/main/scala/ocelot/desktop/ui/widget/Button.scala b/src/main/scala/ocelot/desktop/ui/widget/Button.scala index 98807cc..a2e5e77 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/Button.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/Button.scala @@ -16,8 +16,8 @@ class Button(tooltip: Option[Tooltip] = None) extends Widget with MouseHandler w def this(tooltip: Tooltip) = this(Some(tooltip)) protected def colorScheme: ColorScheme = ColorScheme.General - override protected val hoverAnimationColorDefault: Color = colorScheme("ButtonBackground") - override protected val hoverAnimationColorActive: Color = colorScheme("ButtonBackgroundActive") + override protected val HoverAnimationColorDefault: Color = colorScheme("ButtonBackground") + override protected val HoverAnimationColorActive: Color = colorScheme("ButtonBackgroundActive") def text: String = "" diff --git a/src/main/scala/ocelot/desktop/ui/widget/ChangeSimulationSpeedDialog.scala b/src/main/scala/ocelot/desktop/ui/widget/ChangeSimulationSpeedDialog.scala index bc4d1ad..7a7c3ca 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/ChangeSimulationSpeedDialog.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/ChangeSimulationSpeedDialog.scala @@ -45,7 +45,7 @@ class ChangeSimulationSpeedDialog extends ModalDialog { override def onInput(text: String): Unit = { tickInterval = parseInput(text).map { interval => - inputTPS.setInput(formatTPS(interval)) + inputTPS.text = formatTPS(interval) interval } } @@ -66,7 +66,7 @@ class ChangeSimulationSpeedDialog extends ModalDialog { override def onInput(text: String): Unit = { tickInterval = parseInput(text).map { interval => - inputMSPT.setInput(formatMSPT(interval)) + inputMSPT.text = formatMSPT(interval) interval } } diff --git a/src/main/scala/ocelot/desktop/ui/widget/Checkbox.scala b/src/main/scala/ocelot/desktop/ui/widget/Checkbox.scala index c0e9dad..4885392 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/Checkbox.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/Checkbox.scala @@ -12,8 +12,8 @@ import ocelot.desktop.util.DrawUtils class Checkbox(val label: String, val initialValue: Boolean = false, val isSmall: Boolean = false) extends Widget with MouseHandler with HoverAnimation { - override protected val hoverAnimationColorDefault: Color = ColorScheme("CheckboxBackground") - override protected val hoverAnimationColorActive: Color = ColorScheme("CheckboxBackgroundActive") + override protected val HoverAnimationColorDefault: Color = ColorScheme("CheckboxBackground") + override protected val HoverAnimationColorActive: Color = ColorScheme("CheckboxBackgroundActive") private var _checked: Boolean = initialValue diff --git a/src/main/scala/ocelot/desktop/ui/widget/Slider.scala b/src/main/scala/ocelot/desktop/ui/widget/Slider.scala index ac6062d..218cfce 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/Slider.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/Slider.scala @@ -14,8 +14,8 @@ import ocelot.desktop.util.DrawUtils class Slider(var value: Float, val text: String, val snapPoints: Int = 0) extends Widget with MouseHandler with HoverAnimation { - override protected val hoverAnimationColorDefault: Color = ColorScheme("SliderBackground") - override protected val hoverAnimationColorActive: Color = ColorScheme("SliderBackgroundActive") + override protected val HoverAnimationColorDefault: Color = ColorScheme("SliderBackground") + override protected val HoverAnimationColorActive: Color = ColorScheme("SliderBackgroundActive") def onValueChanged(value: Float): Unit = {} def onValueFinal(value: Float): Unit = {} diff --git a/src/main/scala/ocelot/desktop/ui/widget/TextInput.scala b/src/main/scala/ocelot/desktop/ui/widget/TextInput.scala index 5b0bc9a..a03387e 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/TextInput.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/TextInput.scala @@ -3,7 +3,7 @@ package ocelot.desktop.ui.widget import ocelot.desktop.ColorScheme import ocelot.desktop.color.Color import ocelot.desktop.geometry.Size2D -import ocelot.desktop.graphics.Graphics +import ocelot.desktop.graphics.{Font, Graphics} import ocelot.desktop.ui.UiHandler import ocelot.desktop.ui.event.handlers.MouseHandler import ocelot.desktop.ui.event.sources.KeyEvents @@ -13,12 +13,33 @@ import ocelot.desktop.util.DrawUtils import ocelot.desktop.util.animation.ColorAnimation import org.lwjgl.input.Keyboard -import scala.collection.mutable.ArrayBuffer class TextInput(val initialText: String = "") extends Widget with MouseHandler with HoverAnimation { - override protected val hoverAnimationColorDefault: Color = ColorScheme("TextInputBackground") - override protected val hoverAnimationColorActive: Color = ColorScheme("TextInputBackgroundActive") + private val CursorBlinkTime = 2f + private val PlaceholderForegroundColor: Color = ColorScheme("TextInputForegroundDisabled") + override protected val HoverAnimationColorDefault: Color = ColorScheme("TextInputBackground") + override protected val HoverAnimationColorActive: Color = ColorScheme("TextInputBackgroundActive") + // text state + private var chars = initialText.toCharArray + private var textChanged = false + + // cursor state + private var cursorPos = 0 + private var cursorOffset = 0f + + // view state + private var isFocused = false + private var scroll = 0f + private var blinkTimer = 0f + private var prevEnabled = enabled + + private val foregroundAnimation = new ColorAnimation(targetForegroundColor, 7f) + private val borderAnimation = new ColorAnimation(targetBorderColor, 7f) + + + // public API + // ------------------------------------------------------------------------------------------------------------------- def onInput(text: String): Unit = {} def onConfirm(): Unit = { @@ -28,125 +49,24 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w def validator(text: String): Boolean = true final def isInputValid: Boolean = validator(text) - var isFocused = false - def text: String = chars.mkString - def text_=(value: String): Unit = chars = value.toCharArray - - protected var placeholder: Array[Char] = "".toCharArray - def placeholder_=(value: String): Unit = placeholder = value.toCharArray - - private var cursorPos = 0 - private var cursorOffset = 0f - private var scroll = 0f - private val CursorBlinkTime = 2f - private var blinkTimer = 0f - private var chars = initialText.toCharArray - private var textWidth = 0 - private var textChanged = false - private val events = ArrayBuffer[TextEvent]() - - override protected def receiveClickEvents: Boolean = true - - // TODO: implement text selection - // override protected def receiveDragEvents: Boolean = true - - override protected def allowClickReleaseOutsideThreshold: Boolean = false - - override def receiveAllMouseEvents = true - - private var prevEnabled = enabled - - eventHandlers += { - case MouseEvent(MouseEvent.State.Released, MouseEvent.Button.Left) => - if (isFocused && !clippedBounds.contains(UiHandler.mousePosition)) { - unfocus() - } - - case ClickEvent(MouseEvent.Button.Left, pos) if enabled => - focus() - events += new CursorMoveTo(pos.x) - - case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_LEFT, _) if isFocused => - events += new CursorMoveLeft - event.consume() - - case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_RIGHT, _) if isFocused => - events += new CursorMoveRight - event.consume() - - case event @ KeyEvent(KeyEvent.State.Press, Keyboard.KEY_HOME, _) if isFocused => - events += new CursorMoveStart - event.consume() - - case event @ KeyEvent(KeyEvent.State.Press, Keyboard.KEY_END, _) if isFocused => - events += new CursorMoveEnd - event.consume() - - case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_BACK, _) if isFocused => - events += new EraseCharBack - event.consume() - - case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_DELETE, _) if isFocused => - events += new EraseCharFront - event.consume() - - case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_INSERT, _) if isFocused => - // TODO: insert the whole clipboard string at once instead of going char-by-char. - for (char <- UiHandler.clipboard.toCharArray) { - events += new WriteChar(char) - } - - event.consume() - - case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_V, _) - if isFocused - && KeyEvents.isControlDown => - - // TODO: insert the whole clipboard string at once instead of going char-by-char. - for (char <- UiHandler.clipboard.toCharArray) { - events += new WriteChar(char) - } - - event.consume() - - case event @ KeyEvent(KeyEvent.State.Press, Keyboard.KEY_RETURN, _) if isFocused => - onConfirm() - event.consume() - - case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, _, char) if isFocused && !char.isControl => - events += new WriteChar(char) - event.consume() - } - - def setInput(text: String): Unit = { - this.chars = text.toCharArray + def text_=(value: String): Unit = { + chars = value.toCharArray cursorPos = 0 cursorOffset = 0 - textWidth = 0 textChanged = true } - override def minimumSize: Size2D = Size2D(200, 24) - override def maximumSize: Size2D = Size2D(Float.PositiveInfinity, 24) - - private val foregroundAnimation = new ColorAnimation(targetForegroundColor, 7f) - private val borderAnimation = new ColorAnimation(targetBorderColor, 7f) - - private def updateAnimationTargets(): Unit = { - foregroundAnimation.goto(targetForegroundColor) - borderAnimation.goto(targetBorderColor) - } + protected var placeholder: Array[Char] = "".toCharArray + def placeholder_=(value: String): Unit = placeholder = value.toCharArray def focus(): Unit = { if (!isFocused) { if (enabled) { isFocused = true } - updateAnimationTargets() } - blinkTimer = 0 } @@ -157,6 +77,181 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w } } + + // widget management + // ------------------------------------------------------------------------------------------------------------------- + override def minimumSize: Size2D = Size2D(200, 24) + override def maximumSize: Size2D = Size2D(Float.PositiveInfinity, 24) + + override protected def receiveClickEvents: Boolean = true + override protected def receiveDragEvents: Boolean = true + override protected def allowClickReleaseOutsideThreshold: Boolean = false + override def receiveAllMouseEvents = true + + protected def font(): Font = Font.NormalFont + + private def charWidth(c: Char): Int = font().charWidth(c) + + private def charsWidth(chars: Array[Char]): Int = charsWidth(chars, 0, chars.length - 1) + + private def charsWidth(chars: Array[Char], first: Int, last: Int): Int = { + var width = 0 + for (index <- first to last) { + width += font().charWidth(chars(index)) + } + width + } + + eventHandlers += { + case MouseEvent(MouseEvent.State.Released, MouseEvent.Button.Left) => + if (isFocused && !clippedBounds.contains(UiHandler.mousePosition)) { + unfocus() + } + + case ClickEvent(MouseEvent.Button.Left, mouse) if enabled => + focus() + val absoluteX = mouse.x - bounds.x + scroll - 4 + var width = 0 + var pos = 0 + while (width < absoluteX && pos < chars.length) { + width += charWidth(chars(pos)) + if (width < absoluteX) pos += 1 + } + cursorPos = chars.length.min(pos).max(0) + cursorOffset = if (cursorPos > 0) charsWidth(chars, 0, (cursorPos - 1).max(0)) else 0 + blinkTimer = 0 + adjustScroll() + + case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_LEFT, _) if isFocused => + if (cursorPos != 0) { + cursorOffset -= charWidth(chars(cursorPos - 1)) + cursorPos -= 1 + blinkTimer = 0 + adjustScroll() + } + event.consume() + + case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_RIGHT, _) if isFocused => + if (cursorPos < chars.length) { + cursorOffset += charWidth(chars(cursorPos)) + cursorPos += 1 + blinkTimer = 0 + adjustScroll() + } + event.consume() + + case event @ KeyEvent(KeyEvent.State.Press, Keyboard.KEY_HOME, _) if isFocused => + cursorPos = 0 + cursorOffset = 0 + blinkTimer = 0 + scroll = 0 + event.consume() + + case event @ KeyEvent(KeyEvent.State.Press, Keyboard.KEY_END, _) if isFocused => + val textWidth = charsWidth(chars) + cursorPos = chars.length + cursorOffset = textWidth + blinkTimer = 0 + scroll = (textWidth - size.width + 16).max(0) + event.consume() + + case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_BACK, _) if isFocused => + val (lhs, rhs) = chars.splitAt(cursorPos) + if (!lhs.isEmpty) { + val cw = charWidth(lhs.last) + chars = lhs.take(lhs.length - 1) ++ rhs + textChanged = true + cursorOffset -= cw + cursorPos -= 1 + blinkTimer = 0 + scroll = (scroll - cw).max(0) + } + event.consume() + + case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_DELETE, _) if isFocused => + val (lhs, rhs) = chars.splitAt(cursorPos) + if (!rhs.isEmpty) { + val cw = charWidth(rhs.head) + chars = lhs ++ rhs.drop(1) + textChanged = true + blinkTimer = 0 + if (rhs.drop(1).map(charWidth).sum < size.width - 16) { + scroll = (scroll - cw).max(0) + } + } + event.consume() + + case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_INSERT, _) if isFocused => + // TODO: insert the whole clipboard string at once instead of going char-by-char. + for (char <- UiHandler.clipboard.toCharArray) { + writeChar(char) + } + event.consume() + + case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_V, _) + if isFocused && KeyEvents.isControlDown => + // TODO: insert the whole clipboard string at once instead of going char-by-char. + for (char <- UiHandler.clipboard.toCharArray) { + writeChar(char) + } + event.consume() + + case event @ KeyEvent(KeyEvent.State.Press, Keyboard.KEY_RETURN, _) if isFocused => + onConfirm() + event.consume() + + case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, _, char) if isFocused && !char.isControl => + writeChar(char) + event.consume() + } + + + private def writeChar(char: Char): Unit = { + val (lhs, rhs) = chars.splitAt(cursorPos) + chars = lhs ++ Array(char) ++ rhs + textChanged = true + cursorOffset += charWidth(chars(cursorPos)) + cursorPos += 1 + blinkTimer = 0 + } + + private def adjustScroll(): Unit = { + if (cursorOffset < scroll) + scroll = cursorOffset + if (cursorOffset - scroll > size.width - 16) + scroll = cursorOffset - size.width + 16 + } + + override def update(): Unit = { + super.update() + + if (textChanged) { + onInput(text) + updateAnimationTargets() + textChanged = false + } + + val nextEnabled = enabled + if (nextEnabled != prevEnabled) { + updateAnimationTargets() + prevEnabled = nextEnabled + } + + if (isFocused && !enabled) { + unfocus() + } + + foregroundAnimation.update() + borderAnimation.update() + blinkTimer = (blinkTimer + UiHandler.dt) % CursorBlinkTime + } + + + private def updateAnimationTargets(): Unit = { + foregroundAnimation.goto(targetForegroundColor) + borderAnimation.goto(targetBorderColor) + } + private def targetBorderColor: Color = ColorScheme( if (validator(chars.mkString)) { if (isFocused) "TextInputBorderFocused" @@ -174,29 +269,14 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w else "TextInputForeground" ) - private val placeholderForegroundColor: Color = ColorScheme("TextInputForegroundDisabled") - override def draw(g: Graphics): Unit = { - if (textWidth == 0f && chars.nonEmpty) - textWidth = chars.map(g.font.charWidth(_)).sum - - textChanged = false - for (event <- events) event.handle(g) - events.clear() - - if (textChanged) { - val str = chars.mkString - onInput(str) - updateAnimationTargets() - } - g.rect(bounds, hoverAnimation.color) DrawUtils.ring(g, position.x, position.y, size.width, size.height, thickness = 2, borderAnimation.color) g.setScissor(position.x + 4, position.y, size.width - 8f, size.height) g.background = Color.Transparent - g.foreground = if (chars.nonEmpty || isFocused) foregroundAnimation.color else placeholderForegroundColor + g.foreground = if (chars.nonEmpty || isFocused) foregroundAnimation.color else PlaceholderForegroundColor var charOffset = 0 val charsToDisplay = if (chars.nonEmpty || isFocused) chars else placeholder @@ -209,149 +289,4 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w g.rect(position.x + 7 + cursorOffset - scroll, position.y + 4, 2, 16, borderAnimation.color) } } - - override def update(): Unit = { - super.update() - - val nextEnabled = enabled - - if (nextEnabled != prevEnabled) { - updateAnimationTargets() - prevEnabled = nextEnabled - } - - if (isFocused && !enabled) { - unfocus() - } - - foregroundAnimation.update() - borderAnimation.update() - blinkTimer = (blinkTimer + UiHandler.dt) % CursorBlinkTime - } - - private def charWidth(g: Graphics, c: Char): Int = g.font.charWidth(c) - - // noinspection SameParameterValue - private def charsWidth(g: Graphics, chars: Array[Char], first: Int, last: Int): Int = { - var width = 0 - for (index <- first to last) { - width += g.font.charWidth(chars(index)) - } - width - } - - private def adjustScroll(): Unit = { - if (cursorOffset < scroll) - scroll = cursorOffset - if (cursorOffset - scroll > size.width - 16) - scroll = cursorOffset - size.width + 16 - } - - private abstract class TextEvent { - def handle(g: Graphics): Unit - } - - // TODO: refactor this mess. have actions only move the cursor position explicitly. - // then calculate the cursor offset (incl. the scroll offset) based on that automatically - // rather than incrementally in the actions. - private class CursorMoveLeft extends TextEvent { - override def handle(g: Graphics): Unit = { - if (cursorPos == 0) return - - cursorOffset -= charWidth(g, chars(cursorPos - 1)) - cursorPos -= 1 - blinkTimer = 0 - - adjustScroll() - } - } - - private class CursorMoveRight extends TextEvent { - override def handle(g: Graphics): Unit = { - if (cursorPos >= chars.length) return - - cursorOffset += charWidth(g, chars(cursorPos)) - cursorPos += 1 - blinkTimer = 0 - - adjustScroll() - } - } - - private class CursorMoveStart extends TextEvent { - override def handle(g: Graphics): Unit = { - cursorPos = 0 - cursorOffset = 0 - blinkTimer = 0 - scroll = 0 - } - } - - private class CursorMoveEnd extends TextEvent { - override def handle(g: Graphics): Unit = { - cursorPos = chars.length - cursorOffset = textWidth - blinkTimer = 0 - scroll = (textWidth - size.width + 16).max(0) - } - } - - private class CursorMoveTo(mouseX: Float) extends TextEvent { - override def handle(g: Graphics): Unit = { - val absoluteX = mouseX - bounds.x + scroll - 4 - var width = 0 - var pos = 0 - while (width < absoluteX && pos < chars.length) { - width += g.font.charWidth(chars(pos)) - if (width < absoluteX) pos += 1 - } - - cursorPos = chars.length.min(pos).max(0) - cursorOffset = if (cursorPos > 0) charsWidth(g, chars, 0, (cursorPos - 1).max(0)) else 0 - blinkTimer = 0 - adjustScroll() - } - } - - private class EraseCharBack extends TextEvent { - override def handle(g: Graphics): Unit = { - val (lhs, rhs) = chars.splitAt(cursorPos) - if (lhs.isEmpty) return - - val cw = charWidth(g, lhs.last) - chars = lhs.take(lhs.length - 1) ++ rhs - textChanged = true - textWidth -= cw - cursorOffset -= cw - cursorPos -= 1 - blinkTimer = 0 - scroll = (scroll - cw).max(0) - } - } - - private class EraseCharFront extends TextEvent { - override def handle(g: Graphics): Unit = { - val (lhs, rhs) = chars.splitAt(cursorPos) - if (rhs.isEmpty) return - - val cw = charWidth(g, rhs.head) - chars = lhs ++ rhs.drop(1) - textChanged = true - textWidth -= cw - blinkTimer = 0 - - if (rhs.drop(1).map(charWidth(g, _)).sum < size.width - 16) - scroll = (scroll - cw).max(0) - } - } - - private class WriteChar(char: Char) extends TextEvent { - override def handle(g: Graphics): Unit = { - val (lhs, rhs) = chars.splitAt(cursorPos) - chars = lhs ++ Array(char) ++ rhs - textChanged = true - textWidth += charWidth(g, char) - (new CursorMoveRight).handle(g) - } - } } diff --git a/src/main/scala/ocelot/desktop/ui/widget/settings/SystemSettingsTab.scala b/src/main/scala/ocelot/desktop/ui/widget/settings/SystemSettingsTab.scala index acdd3f1..2a21ace 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/settings/SystemSettingsTab.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/settings/SystemSettingsTab.scala @@ -158,7 +158,7 @@ class SystemSettingsTab extends SettingsTab with Logging { private def setConfigPath(path: String): Unit = { Settings.get.brainCustomConfigPath = Some(path) - textInput.setInput(path) + textInput.text = path restartWarning.isVisible = true } diff --git a/src/main/scala/ocelot/desktop/ui/widget/statusbar/StatusBar.scala b/src/main/scala/ocelot/desktop/ui/widget/statusbar/StatusBar.scala index 3d28082..7ef20ba 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/statusbar/StatusBar.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/statusbar/StatusBar.scala @@ -157,8 +157,8 @@ class StatusBar extends Widget { override def receiveMouseEvents: Boolean = true - override protected val hoverAnimationColorActive: Color = ColorScheme("StatusBarActive") - override protected val hoverAnimationColorDefault: Color = hoverAnimationColorActive.toRGBANorm.withAlpha(0) + override protected val HoverAnimationColorActive: Color = ColorScheme("StatusBarActive") + override protected val HoverAnimationColorDefault: Color = HoverAnimationColorActive.toRGBANorm.withAlpha(0) override def draw(g: Graphics): Unit = { g.rect(bounds, hoverAnimation.color) diff --git a/src/main/scala/ocelot/desktop/ui/widget/traits/HoverAnimation.scala b/src/main/scala/ocelot/desktop/ui/widget/traits/HoverAnimation.scala index 7bcd827..0ded682 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/traits/HoverAnimation.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/traits/HoverAnimation.scala @@ -13,23 +13,23 @@ import ocelot.desktop.util.animation.ColorAnimation */ trait HoverAnimation extends Widget with EventAware with HoverHandler with Updatable { //noinspection ScalaWeakerAccess - protected val hoverAnimationSpeedEnter: Float = AnimationSpeedHoverEnter + protected val HoverAnimationSpeedEnter: Float = AnimationSpeedHoverEnter //noinspection ScalaWeakerAccess - protected val hoverAnimationSpeedLeave: Float = AnimationSpeedHoverLeave - protected val hoverAnimationColorDefault: Color = ColorScheme("ButtonBackground") - protected val hoverAnimationColorActive: Color = ColorScheme("ButtonBackgroundActive") + protected val HoverAnimationSpeedLeave: Float = AnimationSpeedHoverLeave + protected val HoverAnimationColorDefault: Color = ColorScheme("ButtonBackground") + protected val HoverAnimationColorActive: Color = ColorScheme("ButtonBackgroundActive") protected lazy val hoverAnimation: ColorAnimation = - new ColorAnimation(hoverAnimationColorDefault, hoverAnimationSpeedEnter) + new ColorAnimation(HoverAnimationColorDefault, HoverAnimationSpeedEnter) eventHandlers += { case Capturing(HoverEvent(HoverEvent.State.Enter)) => - hoverAnimation.speed = hoverAnimationSpeedEnter - hoverAnimation.goto(hoverAnimationColorActive) + hoverAnimation.speed = HoverAnimationSpeedEnter + hoverAnimation.goto(HoverAnimationColorActive) case Capturing(HoverEvent(HoverEvent.State.Leave)) => - hoverAnimation.speed = hoverAnimationSpeedLeave - hoverAnimation.goto(hoverAnimationColorDefault) + hoverAnimation.speed = HoverAnimationSpeedLeave + hoverAnimation.goto(HoverAnimationColorDefault) } override def update(): Unit = { diff --git a/src/main/scala/ocelot/desktop/windows/OcelotInterfaceWindow.scala b/src/main/scala/ocelot/desktop/windows/OcelotInterfaceWindow.scala index 39aa839..985481d 100644 --- a/src/main/scala/ocelot/desktop/windows/OcelotInterfaceWindow.scala +++ b/src/main/scala/ocelot/desktop/windows/OcelotInterfaceWindow.scala @@ -24,7 +24,7 @@ class OcelotInterfaceWindow(storage: OcelotInterfaceLogStorage) extends PanelWin children :+= new TextInput() { override def onConfirm(): Unit = { pushLine(text) - setInput("") + text = "" } }