Implement event capturing

This commit is contained in:
Fingercomp 2025-08-14 19:13:56 +03:00
parent 9ced3b99b4
commit 1c19df7cf3
No known key found for this signature in database
GPG Key ID: BBC71CEE45D86E37
11 changed files with 67 additions and 33 deletions

View File

@ -1,7 +1,7 @@
package ocelot.desktop.inventory package ocelot.desktop.inventory
import ocelot.desktop.inventory.Inventory.SlotObserver import ocelot.desktop.inventory.Inventory.SlotObserver
import ocelot.desktop.ui.event.{BrainEvent, Event, EventAware} import ocelot.desktop.ui.event.{BrainEvent, Dispatchable, EventAware}
import ocelot.desktop.util.Disposable import ocelot.desktop.util.Disposable
import totoro.ocelot.brain.event.NodeEvent import totoro.ocelot.brain.event.NodeEvent
@ -78,7 +78,7 @@ trait Inventory extends EventAware with Disposable {
super.shouldReceiveEventsFor(address) || super.shouldReceiveEventsFor(address) ||
inventoryIterator.flatMap(_.get).exists(_.shouldReceiveEventsFor(address)) inventoryIterator.flatMap(_.get).exists(_.shouldReceiveEventsFor(address))
override def handleEvent(event: Event): Unit = { override def handleEvent(event: Dispatchable): Unit = {
super.handleEvent(event) super.handleEvent(event)
for (slot <- inventoryIterator; item <- slot.get) { for (slot <- inventoryIterator; item <- slot.get) {

View File

@ -6,7 +6,7 @@ import ocelot.desktop.geometry.{Rect2D, Size2D, Vector2D}
import ocelot.desktop.graphics.Graphics import ocelot.desktop.graphics.Graphics
import ocelot.desktop.ui.event.handlers.HoverHandler import ocelot.desktop.ui.event.handlers.HoverHandler
import ocelot.desktop.ui.event.sources.{BrainEvents, KeyEvents, MouseEvents, ScrollEvents} import ocelot.desktop.ui.event.sources.{BrainEvents, KeyEvents, MouseEvents, ScrollEvents}
import ocelot.desktop.ui.event.{Event, MouseEvent} import ocelot.desktop.ui.event.{Capturing, CapturingEvent, Dispatchable, MouseEvent}
import ocelot.desktop.ui.widget.window.Window import ocelot.desktop.ui.widget.window.Window
import ocelot.desktop.ui.widget.{RootWidget, Widget} import ocelot.desktop.ui.widget.{RootWidget, Widget}
import ocelot.desktop.util._ import ocelot.desktop.util._
@ -427,16 +427,18 @@ object UiHandler extends Logging {
Audio.destroy() Audio.destroy()
} }
private def dispatchEvent(iter: => IterableOnce[Widget] = hierarchy.reverseIterator)(event: Event): Unit = { private def dispatchEvent(iter: => IterableOnce[Widget] = hierarchy.reverseIterator)(event: Dispatchable): Unit = {
for (widget <- iter) { for (widget <- iter if !event.consumed) {
if (event.consumed) {
return
}
widget.handleEvent(event) widget.handleEvent(event)
} }
} }
private def dispatchCapturing(target: Widget)(event: CapturingEvent): Unit = {
val ancestors = target.ancestors.toSeq
dispatchEvent(ancestors.reverseIterator ++ Some(target))(Capturing(event))
dispatchEvent(Some(target))(event)
}
private def dispatchBrainEvents(): Unit = { private def dispatchBrainEvents(): Unit = {
import totoro.ocelot.brain import totoro.ocelot.brain
@ -476,7 +478,10 @@ object UiHandler extends Logging {
} }
// TODO: dispatch to the focused widget instead of broadcasting to the entire hierarchy. // TODO: dispatch to the focused widget instead of broadcasting to the entire hierarchy.
KeyEvents.events.foreach(dispatchEvent()) for (event <- KeyEvents.events) {
dispatchEvent(hierarchy)(Capturing(event))
dispatchEvent()(event)
}
MouseEvents.events MouseEvents.events
.foreach(dispatchEvent(hierarchy.reverseIterator.filter(w => w.enabled && w.receiveAllMouseEvents))) .foreach(dispatchEvent(hierarchy.reverseIterator.filter(w => w.enabled && w.receiveAllMouseEvents)))
@ -487,17 +492,22 @@ object UiHandler extends Logging {
val mouseTarget = hierarchy.reverseIterator val mouseTarget = hierarchy.reverseIterator
.find(w => w.enabled && w.receiveMouseEvents && w.clippedBounds.contains(mousePos)) .find(w => w.enabled && w.receiveMouseEvents && w.clippedBounds.contains(mousePos))
ScrollEvents.events.foreach(dispatchEvent(scrollTarget)) for (scrollTarget <- scrollTarget) {
ScrollEvents.events.foreach(dispatchCapturing(scrollTarget))
}
for (event <- MouseEvents.events) { for (event <- MouseEvents.events) {
if (event.state == MouseEvent.State.Pressed) { if (event.state == MouseEvent.State.Pressed) {
dispatchEvent(mouseTarget)(event) for (mouseTarget <- mouseTarget) {
dispatchCapturing(mouseTarget)(event)
}
// TODO: this should be done in the event capturing phase in [[Window]] itself. // TODO: this should be done in the event capturing phase in [[Window]] itself.
for (mouseTarget <- mouseTarget if !mouseTarget.isInstanceOf[Window]) { for (mouseTarget <- mouseTarget if !mouseTarget.isInstanceOf[Window]) {
focusParentWindow(mouseTarget.parent) focusParentWindow(mouseTarget.parent)
} }
} else { } else {
dispatchEvent(hierarchy)(Capturing(event))
dispatchEvent(hierarchy.reverseIterator)(event) dispatchEvent(hierarchy.reverseIterator)(event)
} }
} }

View File

@ -0,0 +1,3 @@
package ocelot.desktop.ui.event
trait CapturingEvent extends Event

View File

@ -0,0 +1,28 @@
package ocelot.desktop.ui.event
sealed trait Dispatchable {
def consume(): Unit
def consumed: Boolean
}
trait Event extends Dispatchable {
private var _consumed: Boolean = false
def consume(): Unit = {
_consumed = true
}
def consumed: Boolean = _consumed
}
/**
* A wrapper for an event that is dispatched during the capture phase.
*/
case class Capturing[A <: CapturingEvent](event: A) extends Dispatchable {
override def consume(): Unit = {
event.consume()
}
override def consumed: Boolean = event.consumed
}

View File

@ -1,11 +0,0 @@
package ocelot.desktop.ui.event
trait Event extends AnyRef {
private var _consumed: Boolean = false
def consume(): Unit = {
_consumed = true
}
def consumed: Boolean = _consumed
}

View File

@ -7,7 +7,7 @@ trait EventAware {
def shouldReceiveEventsFor(address: String): Boolean = false def shouldReceiveEventsFor(address: String): Boolean = false
def handleEvent(event: Event): Unit = { def handleEvent(event: Dispatchable): Unit = {
eventHandlers(event) eventHandlers(event)
} }
} }

View File

@ -6,4 +6,4 @@ object KeyEvent {
} }
} }
case class KeyEvent(state: KeyEvent.State.Value, code: Int, char: Char) extends Event case class KeyEvent(state: KeyEvent.State.Value, code: Int, char: Char) extends CapturingEvent

View File

@ -12,4 +12,4 @@ object MouseEvent {
} }
} }
case class MouseEvent(state: MouseEvent.State.Value, button: MouseEvent.Button.Value) extends Event case class MouseEvent(state: MouseEvent.State.Value, button: MouseEvent.Button.Value) extends CapturingEvent

View File

@ -1,3 +1,3 @@
package ocelot.desktop.ui.event package ocelot.desktop.ui.event
case class ScrollEvent(offset: Int) extends Event case class ScrollEvent(offset: Int) extends CapturingEvent

View File

@ -1,27 +1,27 @@
package ocelot.desktop.ui.widget package ocelot.desktop.ui.widget
import ocelot.desktop.ui.event.Event import ocelot.desktop.ui.event.{Dispatchable, Event}
import scala.collection.mutable.ArrayBuffer import scala.collection.mutable.ArrayBuffer
class EventHandlers extends PartialFunction[Event, Unit] { class EventHandlers extends PartialFunction[Dispatchable, Unit] {
private val handlers = ArrayBuffer.empty[EventHandler] private val handlers = ArrayBuffer.empty[EventHandler]
def +=(handler: EventHandler): Unit = handlers += handler def +=(handler: EventHandler): Unit = handlers += handler
def -=(handler: EventHandler): Unit = handlers -= handler def -=(handler: EventHandler): Unit = handlers -= handler
override def isDefinedAt(event: Event): Boolean = handlers.exists(_.isDefinedAt(event)) override def isDefinedAt(event: Dispatchable): Boolean = handlers.exists(_.isDefinedAt(event))
override def apply(event: Event): Unit = { override def apply(event: Dispatchable): Unit = {
for (handler <- handlers) { for (handler <- handlers) {
if (event.consumed) { if (event.consumed) {
return return
} }
handler.applyOrElse(event, (_: Event) => ()) handler.applyOrElse(event, (_: Dispatchable) => ())
} }
} }
type EventHandler = PartialFunction[Event, Unit] type EventHandler = PartialFunction[Dispatchable, Unit]
} }

View File

@ -59,6 +59,10 @@ class Widget extends EventAware with Updatable with Disposable {
final def root: Option[RootWidget] = _root final def root: Option[RootWidget] = _root
final def ancestors: Iterator[Widget] = {
Iterator.unfold(this)(_.parent.map(w => (w, w)))
}
def enabled: Boolean = true def enabled: Boolean = true
def minimumSize: Size2D = layout.minimumSize def minimumSize: Size2D = layout.minimumSize