Allow selecting current word by double click

This commit is contained in:
UnicornFreedom 2025-08-22 11:19:18 +02:00
parent a8dc52f1b2
commit 4e9d7c96e2
No known key found for this signature in database
GPG Key ID: B4ED0DB6B940024F
3 changed files with 55 additions and 9 deletions

View File

@ -0,0 +1,5 @@
package ocelot.desktop.ui.event
import ocelot.desktop.geometry.Vector2D
case class DoubleClickEvent(button: MouseEvent.Button.Value, mousePos: Vector2D) extends Event

View File

@ -2,8 +2,8 @@ package ocelot.desktop.ui.event.handlers
import ocelot.desktop.geometry.Vector2D
import ocelot.desktop.ui.UiHandler
import ocelot.desktop.ui.event.handlers.MouseHandler.Tolerance
import ocelot.desktop.ui.event.{ClickEvent, DragEvent, MouseEvent}
import ocelot.desktop.ui.event.handlers.MouseHandler.{DoubleClickTime, Tolerance}
import ocelot.desktop.ui.event.{ClickEvent, DoubleClickEvent, DragEvent, MouseEvent}
import ocelot.desktop.ui.widget.Widget
import scala.collection.mutable
@ -13,13 +13,18 @@ trait MouseHandler extends Widget {
private val prevPositions = new mutable.HashMap[MouseEvent.Button.Value, Vector2D]()
private val dragButtons = new mutable.HashSet[MouseEvent.Button.Value]()
private var lastClickPosition: Vector2D = Vector2D.Zero
private var lastClickTime: Long = 0
private var lastClickButton = MouseEvent.Button.Left
override def receiveMouseEvents: Boolean = receiveClickEvents || receiveDragEvents
protected def receiveClickEvents: Boolean = false
protected def receiveDragEvents: Boolean = false
/** If `true`, a [[ClickEvent]] will be registered even if the mouse button is released
/**
* If `true`, a [[ClickEvent]] will be registered even if the mouse button is released
* outside the tolerance threshold, as long as it stays within the widget's bounds.
*/
protected def allowClickReleaseOutsideThreshold: Boolean = !receiveDragEvents
@ -39,7 +44,7 @@ trait MouseHandler extends Widget {
if (allowClickReleaseOutsideThreshold) {
clippedBounds.contains(mousePos)
} else {
(p - mousePos).lengthSquared < Tolerance * Tolerance
withinTolerance(p, mousePos)
}
})
)
@ -50,11 +55,23 @@ trait MouseHandler extends Widget {
if (clicked) {
handleEvent(ClickEvent(button, mousePos))
val inTimeForDoubleClick = (System.currentTimeMillis() - lastClickTime) < DoubleClickTime * 1000
val sameButton = lastClickButton == button
val roughlySamePosition = withinTolerance(lastClickPosition, mousePos)
if (inTimeForDoubleClick && sameButton && roughlySamePosition) {
handleEvent(DoubleClickEvent(button, mousePos))
}
lastClickTime = System.currentTimeMillis()
lastClickPosition = mousePos
lastClickButton = button
}
startPositions.remove(button)
}
private def withinTolerance(a: Vector2D, b: Vector2D): Boolean = (b - a).lengthSquared < Tolerance * Tolerance
override def update(): Unit = {
super.update()
@ -65,7 +82,7 @@ trait MouseHandler extends Widget {
val mousePos = UiHandler.mousePosition
for ((button, startPos) <- startPositions) {
if (!dragButtons.contains(button) && (startPos - mousePos).lengthSquared > Tolerance * Tolerance) {
if (!dragButtons.contains(button) && !withinTolerance(startPos, mousePos)) {
handleEvent(DragEvent(DragEvent.State.Start, button, mousePos, startPos, Vector2D(0, 0)))
dragButtons += button
prevPositions += (button -> mousePos)
@ -90,4 +107,5 @@ trait MouseHandler extends Widget {
object MouseHandler {
private val Tolerance = 8
private val DoubleClickTime = 0.2
}

View File

@ -7,7 +7,7 @@ import ocelot.desktop.graphics.{Font, Graphics, IconSource}
import ocelot.desktop.ui.UiHandler
import ocelot.desktop.ui.event.handlers.MouseHandler
import ocelot.desktop.ui.event.sources.KeyEvents
import ocelot.desktop.ui.event.{DragEvent, KeyEvent, MouseEvent}
import ocelot.desktop.ui.event.{DoubleClickEvent, DragEvent, KeyEvent, MouseEvent}
import ocelot.desktop.ui.widget.TextInput.{Cursor, Selector, Text}
import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry}
import ocelot.desktop.ui.widget.traits.HoverAnimation
@ -15,6 +15,8 @@ import ocelot.desktop.util.{DrawUtils, Register, Watcher}
import ocelot.desktop.util.animation.ColorAnimation
import org.lwjgl.input.Keyboard
import java.lang.Character.isWhitespace
class TextInput(val initialText: String = "") extends Widget with MouseHandler with HoverAnimation {
private val CursorBlinkTime = 2f
@ -98,8 +100,12 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
// -------------------------------------------------------------------------------------------------------------------
override def minimumSize: Size2D = Size2D(200, 24)
override def maximumSize: Size2D = Size2D(Float.PositiveInfinity, 24)
override def receiveAllMouseEvents = true
override def receiveDragEvents: Boolean = true
override def receiveClickEvents: Boolean = true
private def mouseInBounds: Boolean = clippedBounds.contains(UiHandler.mousePosition)
protected def font: Font = Font.NormalFont
@ -121,7 +127,7 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
eventHandlers += {
case MouseEvent(MouseEvent.State.Pressed, MouseEvent.Button.Left) if enabled =>
val inBounds = clippedBounds.contains(UiHandler.mousePosition)
val inBounds = mouseInBounds
if (isFocused && !inBounds) unfocus()
if (!isFocused && inBounds) focus()
if (isFocused) {
@ -132,8 +138,7 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
}
case MouseEvent(MouseEvent.State.Pressed, MouseEvent.Button.Right) if isFocused =>
val inBounds = clippedBounds.contains(UiHandler.mousePosition)
if (inBounds) {
if (mouseInBounds) {
val menu = new ContextMenu
if (selector.active) {
menu.addEntry(ContextMenuEntry("Cut", IconSource.Icons.Cut) { cutSelection() })
@ -149,6 +154,12 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
root.get.contextMenus.open(menu)
}
case event @ DoubleClickEvent(MouseEvent.Button.Left, _) if isFocused =>
if (mouseInBounds) {
selectWord()
event.consume()
}
case event @ DragEvent(DragEvent.State.Start, MouseEvent.Button.Left, mouse) if isFocused =>
val pos = pixelToCursorPosition(mouse.x - bounds.x)
selector.active = true
@ -238,6 +249,11 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
selectAll()
event.consume()
case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_W, _)
if isFocused && KeyEvents.isControlDown =>
selectWord()
event.consume()
case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_C, _)
if isFocused && KeyEvents.isControlDown && selector.active =>
copySelection()
@ -288,6 +304,13 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
selector.to = _text.chars.length
}
private def selectWord(): Unit = {
selector.active = true
selector.from = 0 max (_text.chars.lastIndexWhere(isWhitespace, cursor.position - 1) + 1)
val to = _text.chars.indexWhere(isWhitespace, cursor.position)
selector.to = if (to >= 0 && to < _text.chars.length) to else _text.chars.length
}
private def copySelection(): Unit = {
UiHandler.clipboard = selectedText
}