From cf088ce9f7b6c6036d82fe48cfee9fa57a56b26f Mon Sep 17 00:00:00 2001 From: UnicornFreedom Date: Fri, 22 Aug 2025 14:24:11 +0200 Subject: [PATCH 1/9] Add context menu to Ocelot component log entries --- .../ocelot/desktop/ui/widget/LogWidget.scala | 35 ++++++++++++++----- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/src/main/scala/ocelot/desktop/ui/widget/LogWidget.scala b/src/main/scala/ocelot/desktop/ui/widget/LogWidget.scala index b9953b6..8b8f033 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/LogWidget.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/LogWidget.scala @@ -3,9 +3,13 @@ package ocelot.desktop.ui.widget import ocelot.desktop.ColorScheme import ocelot.desktop.color.RGBAColorNorm import ocelot.desktop.geometry.{Padding2D, Rect2D, Size2D, Vector2D} -import ocelot.desktop.graphics.Graphics +import ocelot.desktop.graphics.{Graphics, IconSource} +import ocelot.desktop.ui.UiHandler +import ocelot.desktop.ui.event.{ClickEvent, MouseEvent} +import ocelot.desktop.ui.event.handlers.MouseHandler import ocelot.desktop.ui.layout.{Layout, LinearLayout} import ocelot.desktop.ui.widget.LogWidget.{BorderThickness, EntryMargin, EntryPadding, LogEntry} +import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry} import ocelot.desktop.util.{DrawUtils, Orientation} import scala.annotation.tailrec @@ -14,13 +18,27 @@ import scala.collection.mutable abstract class LogWidget extends Widget { override protected val layout: Layout = new LinearLayout(this, orientation = Orientation.Vertical) - private object MessageListWidget extends Widget { messageList => + private object MessageListWidget extends Widget with MouseHandler { messageList => override protected val layout: Layout = new Layout(this) private val entries = mutable.ArrayDeque.empty[Entry] val dummyEntry: Entry = new RxEntry("", nextMessageY) + override def receiveClickEvents: Boolean = true + + eventHandlers += { + case ClickEvent(MouseEvent.Button.Right, mouse) => + visibleEntries.find(_.absoluteBounds.contains(mouse)).foreach { + case entry: MessageEntry => + val menu = new ContextMenu + menu.addEntry(ContextMenuEntry("Copy", IconSource.Icons.Copy) { + UiHandler.clipboard = entry.message + }) + root.get.contextMenus.open(menu) + } + } + private def removedOffset: Float = entries.headOption.map(_.y - EntryMargin).getOrElse(0) private def applyRemovedOffset(): Unit = { @@ -72,15 +90,16 @@ abstract class LogWidget extends Widget { } } + private def visibleEntries: Iterator[Entry] = { + val firstVisibleIdx = firstVisibleIdxSearch(parent.get.asInstanceOf[ScrollView].offset.y) + entries.iterator.drop(firstVisibleIdx).takeWhile(_.absoluteBounds.y <= clippedBounds.max.y) + } + override def draw(g: Graphics): Unit = { applyRemovedOffset() - - val firstVisibleIdx = firstVisibleIdxSearch(parent.get.asInstanceOf[ScrollView].offset.y) - - for (entry <- entries.iterator.drop(firstVisibleIdx).takeWhile(_.absoluteBounds.y <= clippedBounds.max.y)) { + for (entry <- visibleEntries) { entry.draw(g) } - super.draw(g) } @@ -100,7 +119,7 @@ abstract class LogWidget extends Widget { final def absoluteBounds: Rect2D = Rect2D(position + messageList.position, size) } - private abstract class MessageEntry(message: String, override var y: Float) extends Entry { + private abstract class MessageEntry(val message: String, override var y: Float) extends Entry { private val lines = Text.wrap(message, Some(textWidth)) def minimumSize: Size2D = Size2D( From 9a3ee8e95c37ee8098c1f1e1d8f9bd002f60e0f7 Mon Sep 17 00:00:00 2001 From: UnicornFreedom Date: Fri, 22 Aug 2025 14:27:56 +0200 Subject: [PATCH 2/9] Add navigation through Ocelot component log history --- .../ocelot/desktop/ui/widget/LogWidget.scala | 8 +++-- .../ocelot/desktop/ui/widget/TextInput.scala | 2 +- .../util/OcelotInterfaceLogStorage.scala | 12 ++++++- .../windows/OcelotInterfaceWindow.scala | 36 +++++++++++++++++-- 4 files changed, 52 insertions(+), 6 deletions(-) diff --git a/src/main/scala/ocelot/desktop/ui/widget/LogWidget.scala b/src/main/scala/ocelot/desktop/ui/widget/LogWidget.scala index 8b8f033..b2972de 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/LogWidget.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/LogWidget.scala @@ -222,12 +222,16 @@ object LogWidget { sealed trait LogEntry + sealed trait TextLogEntry extends LogEntry { + val message: String + } + object LogEntry { /** A message received from a log source. */ - case class Rx(message: String) extends LogEntry + case class Rx(message: String) extends TextLogEntry /** A message sent to a log source. */ - case class Tx(message: String) extends LogEntry + case class Tx(message: String) extends TextLogEntry } } diff --git a/src/main/scala/ocelot/desktop/ui/widget/TextInput.scala b/src/main/scala/ocelot/desktop/ui/widget/TextInput.scala index 4f35a7b..4516b58 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/TextInput.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/TextInput.scala @@ -34,7 +34,7 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w private val prevCursorPosition = Register.sampling(cursor.position) // view - private var isFocused = false + protected var isFocused = false private var scroll = 0f private var blinkTimer = 0f private var cursorOffset = 0f diff --git a/src/main/scala/ocelot/desktop/util/OcelotInterfaceLogStorage.scala b/src/main/scala/ocelot/desktop/util/OcelotInterfaceLogStorage.scala index 820e81b..c498120 100644 --- a/src/main/scala/ocelot/desktop/util/OcelotInterfaceLogStorage.scala +++ b/src/main/scala/ocelot/desktop/util/OcelotInterfaceLogStorage.scala @@ -2,7 +2,7 @@ package ocelot.desktop.util import ocelot.desktop.entity.traits.OcelotInterface import ocelot.desktop.ui.event.{BrainEvent, EventAware} -import ocelot.desktop.ui.widget.LogWidget.LogEntry +import ocelot.desktop.ui.widget.LogWidget.{LogEntry, TextLogEntry} import ocelot.desktop.ui.widget.window.Windowed import ocelot.desktop.util.OcelotInterfaceLogStorage._ import ocelot.desktop.windows.OcelotInterfaceWindow @@ -121,6 +121,16 @@ trait OcelotInterfaceLogStorage extends EventAware with Persistable with Windowe window.onMessagesRemoved(count) } + def getEntry(index: Int): LogEntry = _entries(index) + + def findFirstTextEntry(exclude: String, start: Int): Int = { + _entries.indexWhere(it => it.isInstanceOf[TextLogEntry] && it.asInstanceOf[TextLogEntry].message != exclude, start) + } + + def findLastTextEntry(exclude: String, end: Int): Int = { + _entries.lastIndexWhere(it => it.isInstanceOf[TextLogEntry] && it.asInstanceOf[TextLogEntry].message != exclude, end) + } + private def addEntry(entry: LogEntry): Unit = { ensureFreeSpace(1) _entries += entry diff --git a/src/main/scala/ocelot/desktop/windows/OcelotInterfaceWindow.scala b/src/main/scala/ocelot/desktop/windows/OcelotInterfaceWindow.scala index 985481d..6769997 100644 --- a/src/main/scala/ocelot/desktop/windows/OcelotInterfaceWindow.scala +++ b/src/main/scala/ocelot/desktop/windows/OcelotInterfaceWindow.scala @@ -1,11 +1,13 @@ package ocelot.desktop.windows import ocelot.desktop.geometry.{Padding2D, Size2D} +import ocelot.desktop.ui.event.KeyEvent import ocelot.desktop.ui.layout.{AlignItems, Layout, LinearLayout} -import ocelot.desktop.ui.widget.LogWidget.LogEntry +import ocelot.desktop.ui.widget.LogWidget.{LogEntry, TextLogEntry} import ocelot.desktop.ui.widget.window.PanelWindow import ocelot.desktop.ui.widget.{Button, Checkbox, Filler, Label, LogWidget, PaddingBox, TextInput, Widget} import ocelot.desktop.util.{OcelotInterfaceLogStorage, Orientation} +import org.lwjgl.input.Keyboard class OcelotInterfaceWindow(storage: OcelotInterfaceLogStorage) extends PanelWindow { override protected def title: String = s"${storage.name} ${storage.ocelotInterface.node.address}" @@ -21,10 +23,36 @@ class OcelotInterfaceWindow(storage: OcelotInterfaceLogStorage) extends PanelWin children :+= logWidget + var historyPosition = 0 + var unfinishedEntryBackup: Option[String] = None + children :+= new TextInput() { + eventHandlers += { + case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_UP | Keyboard.KEY_DOWN, _) if this.isFocused => + val currentInput = text + val index = if (event.code == Keyboard.KEY_UP) { + storage.findLastTextEntry(currentInput, storage.entryCount - historyPosition) + } else { + storage.findFirstTextEntry(currentInput, storage.entryCount - historyPosition + 1) + } + if (index != -1) { + if (unfinishedEntryBackup.isEmpty) { + unfinishedEntryBackup = Some(currentInput) + } + historyPosition = storage.entryCount - index + text = storage.getEntry(index).asInstanceOf[TextLogEntry].message + event.consume() + } else if (event.code == Keyboard.KEY_DOWN && unfinishedEntryBackup.nonEmpty) { + text = unfinishedEntryBackup.get + unfinishedEntryBackup = None + } + } + override def onConfirm(): Unit = { pushLine(text) text = "" + unfinishedEntryBackup = None + historyPosition = 0 } } @@ -75,7 +103,11 @@ class OcelotInterfaceWindow(storage: OcelotInterfaceLogStorage) extends PanelWin children :+= new Button { override def text: String = "Clear" - override def onClick(): Unit = clear() + override def onClick(): Unit = { + unfinishedEntryBackup = None + historyPosition = 0 + clear() + } } } }) From 04bbf626a1478091a7b09a5cc7f93c14c67f6290 Mon Sep 17 00:00:00 2001 From: UnicornFreedom Date: Fri, 22 Aug 2025 15:08:59 +0200 Subject: [PATCH 3/9] Fix Ocelot interface relayout on clear --- src/main/scala/ocelot/desktop/ui/widget/LogWidget.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/scala/ocelot/desktop/ui/widget/LogWidget.scala b/src/main/scala/ocelot/desktop/ui/widget/LogWidget.scala index b2972de..fb7d637 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/LogWidget.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/LogWidget.scala @@ -74,6 +74,7 @@ abstract class LogWidget extends Widget { entries.dropInPlace(count) } + size = minimumSize parent.get.recalculateBoundsAndRelayout() } From b03078ff278f1b9b3b237178e66e50ce6fc2eba9 Mon Sep 17 00:00:00 2001 From: UnicornFreedom Date: Fri, 22 Aug 2025 15:16:37 +0200 Subject: [PATCH 4/9] Persist LogWidget "scroll to the end" checkbox value --- .../windows/OcelotInterfaceWindow.scala | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/main/scala/ocelot/desktop/windows/OcelotInterfaceWindow.scala b/src/main/scala/ocelot/desktop/windows/OcelotInterfaceWindow.scala index 6769997..acdc290 100644 --- a/src/main/scala/ocelot/desktop/windows/OcelotInterfaceWindow.scala +++ b/src/main/scala/ocelot/desktop/windows/OcelotInterfaceWindow.scala @@ -7,7 +7,9 @@ import ocelot.desktop.ui.widget.LogWidget.{LogEntry, TextLogEntry} import ocelot.desktop.ui.widget.window.PanelWindow import ocelot.desktop.ui.widget.{Button, Checkbox, Filler, Label, LogWidget, PaddingBox, TextInput, Widget} import ocelot.desktop.util.{OcelotInterfaceLogStorage, Orientation} +import ocelot.desktop.windows.OcelotInterfaceWindow.ScrollToEndTag import org.lwjgl.input.Keyboard +import totoro.ocelot.brain.nbt.NBTTagCompound class OcelotInterfaceWindow(storage: OcelotInterfaceLogStorage) extends PanelWindow { override protected def title: String = s"${storage.name} ${storage.ocelotInterface.node.address}" @@ -127,4 +129,20 @@ class OcelotInterfaceWindow(storage: OcelotInterfaceLogStorage) extends PanelWin def pushLine(line: String): Unit = { storage.ocelotInterface.pushMessage(line) } + + override def save(nbt: NBTTagCompound): Unit = { + super.save(nbt) + nbt.setBoolean(ScrollToEndTag, logWidget.scrollToEnd) + } + + override def load(nbt: NBTTagCompound): Unit = { + super.load(nbt) + if (nbt.hasKey(ScrollToEndTag)) { + logWidget.scrollToEnd = nbt.getBoolean(ScrollToEndTag) + } + } +} + +object OcelotInterfaceWindow { + private val ScrollToEndTag = "scrollToEnd" } From 667a74519aa848de208399dfb220af0d68a26a61 Mon Sep 17 00:00:00 2001 From: UnicornFreedom Date: Fri, 22 Aug 2025 17:17:01 +0200 Subject: [PATCH 5/9] Fix FPS counter "frame" skip --- .../scala/ocelot/desktop/OcelotDesktop.scala | 2 +- .../ocelot/desktop/util/FPSCalculator.scala | 26 ++++++++++--------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/main/scala/ocelot/desktop/OcelotDesktop.scala b/src/main/scala/ocelot/desktop/OcelotDesktop.scala index 0844fd6..868e98b 100644 --- a/src/main/scala/ocelot/desktop/OcelotDesktop.scala +++ b/src/main/scala/ocelot/desktop/OcelotDesktop.scala @@ -55,7 +55,7 @@ object OcelotDesktop extends LoggingConfiguration with Logging { def emulationPaused_=(paused: Boolean): Unit = { _emulationPaused = paused // avoid sudden jumps of TPS counter after a pause - if (!paused) tpsCounter.skipFrame() + if (!paused) tpsCounter.skipSecond() } private val TickerIntervalHistorySize = 5 diff --git a/src/main/scala/ocelot/desktop/util/FPSCalculator.scala b/src/main/scala/ocelot/desktop/util/FPSCalculator.scala index b051b68..196d8d6 100644 --- a/src/main/scala/ocelot/desktop/util/FPSCalculator.scala +++ b/src/main/scala/ocelot/desktop/util/FPSCalculator.scala @@ -9,29 +9,31 @@ class FPSCalculator { def fps: Float = _fps - private var _skipFrame = false + private var _skipSecond = false /** - * Next tick will not count towards the overall statistics. + * Current second will not count towards the overall statistics. + * (In case the measurements were corrupted or distorted somehow.) */ - def skipFrame(): Unit = _skipFrame = true + def skipSecond(): Unit = _skipSecond = true var dt: Float = 0 def tick(): Unit = { val currentTime = System.currentTimeMillis() - if (!_skipFrame) { - dt = (currentTime - prevFrameTime) / 1000f + dt = (currentTime - prevFrameTime) / 1000f + numFrames += 1 - numFrames += 1 - - if (currentTime - prevTime > 1000) { - val delta = currentTime - prevTime - prevTime = currentTime - _fps = numFrames.asInstanceOf[Float] / delta * 1000f - numFrames = 0 + if (currentTime - prevTime > 1000) { + val delta = currentTime - prevTime + prevTime = currentTime + if (!_skipSecond) { + _fps = numFrames.toFloat / delta * 1000.0f + } else { + _skipSecond = false } + numFrames = 0 } prevFrameTime = currentTime From 6a252d86fd1f11f76b8e24416c6b3f52a1b79477 Mon Sep 17 00:00:00 2001 From: UnicornFreedom Date: Wed, 3 Sep 2025 11:39:43 +0200 Subject: [PATCH 6/9] Core review corrections --- src/main/scala/ocelot/desktop/ui/widget/LogWidget.scala | 2 ++ .../scala/ocelot/desktop/windows/OcelotInterfaceWindow.scala | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/scala/ocelot/desktop/ui/widget/LogWidget.scala b/src/main/scala/ocelot/desktop/ui/widget/LogWidget.scala index fb7d637..c4f3c90 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/LogWidget.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/LogWidget.scala @@ -74,6 +74,8 @@ abstract class LogWidget extends Widget { entries.dropInPlace(count) } + // this is a fix for log widget not scaling down after being cleaned + // TODO: implement it nicely size = minimumSize parent.get.recalculateBoundsAndRelayout() } diff --git a/src/main/scala/ocelot/desktop/windows/OcelotInterfaceWindow.scala b/src/main/scala/ocelot/desktop/windows/OcelotInterfaceWindow.scala index acdc290..5aa19f1 100644 --- a/src/main/scala/ocelot/desktop/windows/OcelotInterfaceWindow.scala +++ b/src/main/scala/ocelot/desktop/windows/OcelotInterfaceWindow.scala @@ -30,7 +30,7 @@ class OcelotInterfaceWindow(storage: OcelotInterfaceLogStorage) extends PanelWin children :+= new TextInput() { eventHandlers += { - case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_UP | Keyboard.KEY_DOWN, _) if this.isFocused => + case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_UP | Keyboard.KEY_DOWN, _) if isFocused => val currentInput = text val index = if (event.code == Keyboard.KEY_UP) { storage.findLastTextEntry(currentInput, storage.entryCount - historyPosition) From 2c77f8cba9f676ef70c6da011f4375de8505549b Mon Sep 17 00:00:00 2001 From: Fingercomp Date: Wed, 3 Sep 2025 18:39:56 +0300 Subject: [PATCH 7/9] Clarify the purpose of removedOffset --- src/main/scala/ocelot/desktop/ui/widget/LogWidget.scala | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/scala/ocelot/desktop/ui/widget/LogWidget.scala b/src/main/scala/ocelot/desktop/ui/widget/LogWidget.scala index c4f3c90..413fd3b 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/LogWidget.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/LogWidget.scala @@ -39,10 +39,18 @@ abstract class LogWidget extends Widget { } } + // when entries are removed from the head, all consecutive entries need to be shifted up. + // doing that for every single removal is **very** inefficient. + // instead, we keep the widgets where they are without shifting, having the offset accumulate, and update their + // positions only once per frame (in `draw`). private def removedOffset: Float = entries.headOption.map(_.y - EntryMargin).getOrElse(0) private def applyRemovedOffset(): Unit = { val offset = removedOffset + + if (offset == 0) { + return + } for (entry <- entries) { entry.y -= offset From d676a4a5bf26fee540e5e5ec5650a36f36cda0ca Mon Sep 17 00:00:00 2001 From: Fingercomp Date: Wed, 3 Sep 2025 18:52:40 +0300 Subject: [PATCH 8/9] Fix how LogWidget is laid out --- .../ocelot/desktop/ui/widget/LogWidget.scala | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/src/main/scala/ocelot/desktop/ui/widget/LogWidget.scala b/src/main/scala/ocelot/desktop/ui/widget/LogWidget.scala index 413fd3b..17932c5 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/LogWidget.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/LogWidget.scala @@ -5,23 +5,28 @@ import ocelot.desktop.color.RGBAColorNorm import ocelot.desktop.geometry.{Padding2D, Rect2D, Size2D, Vector2D} import ocelot.desktop.graphics.{Graphics, IconSource} import ocelot.desktop.ui.UiHandler -import ocelot.desktop.ui.event.{ClickEvent, MouseEvent} import ocelot.desktop.ui.event.handlers.MouseHandler -import ocelot.desktop.ui.layout.{Layout, LinearLayout} +import ocelot.desktop.ui.event.{ClickEvent, MouseEvent} +import ocelot.desktop.ui.layout.{CopyLayout, Layout} import ocelot.desktop.ui.widget.LogWidget.{BorderThickness, EntryMargin, EntryPadding, LogEntry} import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry} -import ocelot.desktop.util.{DrawUtils, Orientation} +import ocelot.desktop.util.DrawUtils import scala.annotation.tailrec import scala.collection.mutable abstract class LogWidget extends Widget { - override protected val layout: Layout = new LinearLayout(this, orientation = Orientation.Vertical) + override protected val layout: Layout = new CopyLayout(this) private object MessageListWidget extends Widget with MouseHandler { messageList => - override protected val layout: Layout = new Layout(this) + override protected val layout: Layout = new Layout(this) { + override def recalculateBounds(): Unit = { + super.recalculateBounds() + } + } private val entries = mutable.ArrayDeque.empty[Entry] + private var needRelayout = false val dummyEntry: Entry = new RxEntry("", nextMessageY) @@ -62,6 +67,8 @@ abstract class LogWidget extends Widget { nextMessageY - removedOffset, ) + override def maximumSize: Size2D = minimumSize + private def nextMessageY: Float = { entries.lastOption.map(_.maxY).getOrElse(0f) + EntryMargin } @@ -72,7 +79,7 @@ abstract class LogWidget extends Widget { case LogEntry.Tx(message) => new TxEntry(message, nextMessageY) }) - parent.get.recalculateBoundsAndRelayout() + needRelayout = true } def removeFirst(count: Int): Unit = { @@ -82,10 +89,7 @@ abstract class LogWidget extends Widget { entries.dropInPlace(count) } - // this is a fix for log widget not scaling down after being cleaned - // TODO: implement it nicely - size = minimumSize - parent.get.recalculateBoundsAndRelayout() + needRelayout = true } @tailrec @@ -106,6 +110,15 @@ abstract class LogWidget extends Widget { entries.iterator.drop(firstVisibleIdx).takeWhile(_.absoluteBounds.y <= clippedBounds.max.y) } + override def update(): Unit = { + if (needRelayout) { + recalculateBoundsAndRelayout() + needRelayout = false + } + + super.update() + } + override def draw(g: Graphics): Unit = { applyRemovedOffset() for (entry <- visibleEntries) { From 052f61ad438acd3d9ff4108ce58b4643aa898754 Mon Sep 17 00:00:00 2001 From: Fingercomp Date: Wed, 3 Sep 2025 19:26:44 +0300 Subject: [PATCH 9/9] Remember the desired cursor position when changing the text --- .../ocelot/desktop/ui/widget/TextInput.scala | 33 ++++++++++++------- .../scala/ocelot/desktop/util/Watcher.scala | 22 +++++++++---- 2 files changed, 36 insertions(+), 19 deletions(-) diff --git a/src/main/scala/ocelot/desktop/ui/widget/TextInput.scala b/src/main/scala/ocelot/desktop/ui/widget/TextInput.scala index 4516b58..fc16c2f 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/TextInput.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/TextInput.scala @@ -11,7 +11,7 @@ import ocelot.desktop.ui.event.{DoubleClickEvent, DragEvent, KeyEvent, MouseEven import ocelot.desktop.ui.widget.TextInput.{Cursor, Selection, Text} import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry} import ocelot.desktop.ui.widget.traits.HoverAnimation -import ocelot.desktop.util.{DrawUtils, Register, Watcher} +import ocelot.desktop.util.{BaseWatcher, DrawUtils, Register, Watcher} import ocelot.desktop.util.animation.ColorAnimation import org.lwjgl.input.Keyboard @@ -28,7 +28,7 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w // model private val _text: Text = new Text(initialText.codePoints().toArray) private val cursor: Cursor = new Cursor() - private val selectionWatcher = new Watcher[Option[Selection]](None) + private val selectionWatcher = Watcher[Option[Selection]](None) // updated after all events are processed so that event handlers can refer to the previous position. private val prevCursorPosition = Register.sampling(cursor.position) @@ -47,13 +47,13 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w selectionWatcher.value = newValue } - cursor.onChange(position => { + cursor.onChange = { position => cursorOffset = charsWidth(_text.chars, 0, position) - blinkTimer = 0 - adjustScroll() - }) + blinkTimer = 0 + adjustScroll() + } - selectionWatcher.onChange(newValue => { + selectionWatcher.onChange = { newValue => selectionOffsets = newValue.map { case Selection.Ordered(start, end) => ( @@ -61,7 +61,7 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w charsWidth(_text.chars, 0, end), ) } - }) + } private val foregroundAnimation = new ColorAnimation(targetForegroundColor, 7f) private val borderAnimation = new ColorAnimation(targetBorderColor, 7f) @@ -81,7 +81,10 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w def text_=(value: String): Unit = { _text.chars = value.codePoints().toArray selection = None - cursor.position = cursor.position max 0 min _text.chars.length + + val desiredPosition = cursor.desiredPosition + cursor.position = desiredPosition max 0 min _text.chars.length + cursor.desiredPosition = desiredPosition } private def selectedText: String = selection match { @@ -448,14 +451,20 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w } object TextInput { - class Text(initialValue: Array[Int]) extends Watcher(initialValue) { + class Text(initialValue: Array[Int]) extends BaseWatcher(initialValue) { def chars: Array[Int] = value def chars_=(newValue: Array[Int]): Unit = value = newValue } - class Cursor(initialValue: Int = 0) extends Watcher(initialValue) { + class Cursor(initialValue: Int = 0) extends BaseWatcher(initialValue) { + var desiredPosition: Int = initialValue + def position: Int = value - def position_=(newValue: Int): Unit = value = newValue + + def position_=(newValue: Int): Unit = { + value = newValue + desiredPosition = newValue + } } case class Selection(start: Int, end: Int) { diff --git a/src/main/scala/ocelot/desktop/util/Watcher.scala b/src/main/scala/ocelot/desktop/util/Watcher.scala index 0e8831a..959d8b4 100644 --- a/src/main/scala/ocelot/desktop/util/Watcher.scala +++ b/src/main/scala/ocelot/desktop/util/Watcher.scala @@ -1,24 +1,27 @@ package ocelot.desktop.util /** - * Keeps a reference to an object - * and tells whether there were any changes to the value since the last check. + * Keeps a value and tracks its changes. */ -class Watcher[T](initialValue: T) { +abstract class BaseWatcher[T](initialValue: T) { private var dirty = false private var _callback: Option[T => Unit] = None private var _value: T = initialValue - def value: T = _value - def value_=(newValue: T): Unit = { + protected def value: T = _value + protected def value_=(newValue: T): Unit = { dirty = _value != newValue if (dirty) { _callback.foreach(_(newValue)) } _value = newValue } + + def onChange: Option[T => Unit] = _callback - def onChange(callback: T => Unit): Unit = _callback = Some(callback) + def onChange_=(callback: T => Unit): Unit = { + _callback = Some(callback) + } def changed(): Boolean = { if (dirty) { @@ -29,5 +32,10 @@ class Watcher[T](initialValue: T) { } object Watcher { - def apply[T](value: T) = new Watcher(value) + def apply[T](initialValue: T) = new Watcher(initialValue) + + class Watcher[T](initialValue: T) extends BaseWatcher(initialValue) { + override def value: T = super.value + override def value_=(newValue: T): Unit = super.value_=(newValue) + } }