From 1c19df7cf330c60f2d4ba0b406127af3efbcf508 Mon Sep 17 00:00:00 2001 From: Fingercomp Date: Thu, 14 Aug 2025 19:13:56 +0300 Subject: [PATCH 1/3] Implement event capturing --- .../ocelot/desktop/inventory/Inventory.scala | 4 +-- .../scala/ocelot/desktop/ui/UiHandler.scala | 30 ++++++++++++------- .../desktop/ui/event/CapturingEvent.scala | 3 ++ .../desktop/ui/event/Dispatchable.scala | 28 +++++++++++++++++ .../scala/ocelot/desktop/ui/event/Event.scala | 11 ------- .../ocelot/desktop/ui/event/EventAware.scala | 2 +- .../ocelot/desktop/ui/event/KeyEvent.scala | 2 +- .../ocelot/desktop/ui/event/MouseEvent.scala | 2 +- .../ocelot/desktop/ui/event/ScrollEvent.scala | 2 +- .../desktop/ui/widget/EventHandlers.scala | 12 ++++---- .../ocelot/desktop/ui/widget/Widget.scala | 4 +++ 11 files changed, 67 insertions(+), 33 deletions(-) create mode 100644 src/main/scala/ocelot/desktop/ui/event/CapturingEvent.scala create mode 100644 src/main/scala/ocelot/desktop/ui/event/Dispatchable.scala delete mode 100644 src/main/scala/ocelot/desktop/ui/event/Event.scala diff --git a/src/main/scala/ocelot/desktop/inventory/Inventory.scala b/src/main/scala/ocelot/desktop/inventory/Inventory.scala index 374b563..babcf1b 100644 --- a/src/main/scala/ocelot/desktop/inventory/Inventory.scala +++ b/src/main/scala/ocelot/desktop/inventory/Inventory.scala @@ -1,7 +1,7 @@ package ocelot.desktop.inventory 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 totoro.ocelot.brain.event.NodeEvent @@ -78,7 +78,7 @@ trait Inventory extends EventAware with Disposable { super.shouldReceiveEventsFor(address) || inventoryIterator.flatMap(_.get).exists(_.shouldReceiveEventsFor(address)) - override def handleEvent(event: Event): Unit = { + override def handleEvent(event: Dispatchable): Unit = { super.handleEvent(event) for (slot <- inventoryIterator; item <- slot.get) { diff --git a/src/main/scala/ocelot/desktop/ui/UiHandler.scala b/src/main/scala/ocelot/desktop/ui/UiHandler.scala index 31a3918..df9d718 100644 --- a/src/main/scala/ocelot/desktop/ui/UiHandler.scala +++ b/src/main/scala/ocelot/desktop/ui/UiHandler.scala @@ -6,7 +6,7 @@ import ocelot.desktop.geometry.{Rect2D, Size2D, Vector2D} import ocelot.desktop.graphics.Graphics import ocelot.desktop.ui.event.handlers.HoverHandler 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.{RootWidget, Widget} import ocelot.desktop.util._ @@ -427,16 +427,18 @@ object UiHandler extends Logging { Audio.destroy() } - private def dispatchEvent(iter: => IterableOnce[Widget] = hierarchy.reverseIterator)(event: Event): Unit = { - for (widget <- iter) { - if (event.consumed) { - return - } - + private def dispatchEvent(iter: => IterableOnce[Widget] = hierarchy.reverseIterator)(event: Dispatchable): Unit = { + for (widget <- iter if !event.consumed) { 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 = { 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. - KeyEvents.events.foreach(dispatchEvent()) + for (event <- KeyEvents.events) { + dispatchEvent(hierarchy)(Capturing(event)) + dispatchEvent()(event) + } MouseEvents.events .foreach(dispatchEvent(hierarchy.reverseIterator.filter(w => w.enabled && w.receiveAllMouseEvents))) @@ -487,17 +492,22 @@ object UiHandler extends Logging { val mouseTarget = hierarchy.reverseIterator .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) { 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. for (mouseTarget <- mouseTarget if !mouseTarget.isInstanceOf[Window]) { focusParentWindow(mouseTarget.parent) } } else { + dispatchEvent(hierarchy)(Capturing(event)) dispatchEvent(hierarchy.reverseIterator)(event) } } diff --git a/src/main/scala/ocelot/desktop/ui/event/CapturingEvent.scala b/src/main/scala/ocelot/desktop/ui/event/CapturingEvent.scala new file mode 100644 index 0000000..448d6bd --- /dev/null +++ b/src/main/scala/ocelot/desktop/ui/event/CapturingEvent.scala @@ -0,0 +1,3 @@ +package ocelot.desktop.ui.event + +trait CapturingEvent extends Event diff --git a/src/main/scala/ocelot/desktop/ui/event/Dispatchable.scala b/src/main/scala/ocelot/desktop/ui/event/Dispatchable.scala new file mode 100644 index 0000000..397083c --- /dev/null +++ b/src/main/scala/ocelot/desktop/ui/event/Dispatchable.scala @@ -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 +} diff --git a/src/main/scala/ocelot/desktop/ui/event/Event.scala b/src/main/scala/ocelot/desktop/ui/event/Event.scala deleted file mode 100644 index a547eec..0000000 --- a/src/main/scala/ocelot/desktop/ui/event/Event.scala +++ /dev/null @@ -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 -} diff --git a/src/main/scala/ocelot/desktop/ui/event/EventAware.scala b/src/main/scala/ocelot/desktop/ui/event/EventAware.scala index 3459153..97f86e3 100644 --- a/src/main/scala/ocelot/desktop/ui/event/EventAware.scala +++ b/src/main/scala/ocelot/desktop/ui/event/EventAware.scala @@ -7,7 +7,7 @@ trait EventAware { def shouldReceiveEventsFor(address: String): Boolean = false - def handleEvent(event: Event): Unit = { + def handleEvent(event: Dispatchable): Unit = { eventHandlers(event) } } diff --git a/src/main/scala/ocelot/desktop/ui/event/KeyEvent.scala b/src/main/scala/ocelot/desktop/ui/event/KeyEvent.scala index c35d148..ed6865e 100644 --- a/src/main/scala/ocelot/desktop/ui/event/KeyEvent.scala +++ b/src/main/scala/ocelot/desktop/ui/event/KeyEvent.scala @@ -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 diff --git a/src/main/scala/ocelot/desktop/ui/event/MouseEvent.scala b/src/main/scala/ocelot/desktop/ui/event/MouseEvent.scala index 58c4c07..2ac0bda 100644 --- a/src/main/scala/ocelot/desktop/ui/event/MouseEvent.scala +++ b/src/main/scala/ocelot/desktop/ui/event/MouseEvent.scala @@ -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 diff --git a/src/main/scala/ocelot/desktop/ui/event/ScrollEvent.scala b/src/main/scala/ocelot/desktop/ui/event/ScrollEvent.scala index 5cfa4b2..291546e 100644 --- a/src/main/scala/ocelot/desktop/ui/event/ScrollEvent.scala +++ b/src/main/scala/ocelot/desktop/ui/event/ScrollEvent.scala @@ -1,3 +1,3 @@ package ocelot.desktop.ui.event -case class ScrollEvent(offset: Int) extends Event +case class ScrollEvent(offset: Int) extends CapturingEvent diff --git a/src/main/scala/ocelot/desktop/ui/widget/EventHandlers.scala b/src/main/scala/ocelot/desktop/ui/widget/EventHandlers.scala index 1288216..7b0901b 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/EventHandlers.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/EventHandlers.scala @@ -1,27 +1,27 @@ package ocelot.desktop.ui.widget -import ocelot.desktop.ui.event.Event +import ocelot.desktop.ui.event.{Dispatchable, Event} import scala.collection.mutable.ArrayBuffer -class EventHandlers extends PartialFunction[Event, Unit] { +class EventHandlers extends PartialFunction[Dispatchable, Unit] { private val handlers = ArrayBuffer.empty[EventHandler] 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) { if (event.consumed) { return } - handler.applyOrElse(event, (_: Event) => ()) + handler.applyOrElse(event, (_: Dispatchable) => ()) } } - type EventHandler = PartialFunction[Event, Unit] + type EventHandler = PartialFunction[Dispatchable, Unit] } diff --git a/src/main/scala/ocelot/desktop/ui/widget/Widget.scala b/src/main/scala/ocelot/desktop/ui/widget/Widget.scala index 5adc139..d26ca2e 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/Widget.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/Widget.scala @@ -59,6 +59,10 @@ class Widget extends EventAware with Updatable with Disposable { final def root: Option[RootWidget] = _root + final def ancestors: Iterator[Widget] = { + Iterator.unfold(this)(_.parent.map(w => (w, w))) + } + def enabled: Boolean = true def minimumSize: Size2D = layout.minimumSize From 87b13e66b123667db13c74739130584fb70f29e7 Mon Sep 17 00:00:00 2001 From: Fingercomp Date: Thu, 14 Aug 2025 19:15:53 +0300 Subject: [PATCH 2/3] Remove the window focusing hack --- src/main/scala/ocelot/desktop/ui/UiHandler.scala | 14 -------------- .../ocelot/desktop/ui/widget/window/Window.scala | 4 ++-- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/src/main/scala/ocelot/desktop/ui/UiHandler.scala b/src/main/scala/ocelot/desktop/ui/UiHandler.scala index df9d718..00ac4a0 100644 --- a/src/main/scala/ocelot/desktop/ui/UiHandler.scala +++ b/src/main/scala/ocelot/desktop/ui/UiHandler.scala @@ -501,11 +501,6 @@ object UiHandler extends Logging { for (mouseTarget <- mouseTarget) { dispatchCapturing(mouseTarget)(event) } - - // TODO: this should be done in the event capturing phase in [[Window]] itself. - for (mouseTarget <- mouseTarget if !mouseTarget.isInstanceOf[Window]) { - focusParentWindow(mouseTarget.parent) - } } else { dispatchEvent(hierarchy)(Capturing(event)) dispatchEvent(hierarchy.reverseIterator)(event) @@ -525,15 +520,6 @@ object UiHandler extends Logging { root.update() } - @tailrec - private def focusParentWindow(parent: Option[Widget]): Unit = { - parent match { - case Some(window: Window) => window.focus() - case Some(widget) => focusParentWindow(widget.parent) - case None => - } - } - private def updateWindowSizeAndPosition(): Unit = { val width = Display.getWidth val height = Display.getHeight diff --git a/src/main/scala/ocelot/desktop/ui/widget/window/Window.scala b/src/main/scala/ocelot/desktop/ui/widget/window/Window.scala index b7955b3..d94b0c7 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/window/Window.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/window/Window.scala @@ -4,7 +4,7 @@ import ocelot.desktop.Settings import ocelot.desktop.geometry.{Rect2D, Size2D, Vector2D} import ocelot.desktop.ui.UiHandler import ocelot.desktop.ui.event.handlers.MouseHandler -import ocelot.desktop.ui.event.{DragEvent, KeyEvent, MouseEvent} +import ocelot.desktop.ui.event.{Capturing, DragEvent, KeyEvent, MouseEvent} import ocelot.desktop.ui.widget.Widget import ocelot.desktop.util.Persistable import org.lwjgl.input.Keyboard @@ -30,7 +30,7 @@ trait Window extends Widget with Persistable with MouseHandler { override protected def receiveDragEvents: Boolean = true eventHandlers += { - case MouseEvent(MouseEvent.State.Pressed, _) => + case Capturing(MouseEvent(MouseEvent.State.Pressed, _)) => focus() case ev @ DragEvent(DragEvent.State.Start, MouseEvent.Button.Left, mousePos) => From 1c74adc329212790c96235ea842f6a293fd199d2 Mon Sep 17 00:00:00 2001 From: Fingercomp Date: Thu, 14 Aug 2025 21:16:50 +0300 Subject: [PATCH 3/3] Intercept mouse press events when dragging scroll bar thumbs --- src/main/scala/ocelot/desktop/ui/UiHandler.scala | 2 -- .../scala/ocelot/desktop/ui/widget/ScrollView.scala | 13 +++++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/main/scala/ocelot/desktop/ui/UiHandler.scala b/src/main/scala/ocelot/desktop/ui/UiHandler.scala index 00ac4a0..8f2ef55 100644 --- a/src/main/scala/ocelot/desktop/ui/UiHandler.scala +++ b/src/main/scala/ocelot/desktop/ui/UiHandler.scala @@ -7,7 +7,6 @@ import ocelot.desktop.graphics.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, MouseEvent} -import ocelot.desktop.ui.widget.window.Window import ocelot.desktop.ui.widget.{RootWidget, Widget} import ocelot.desktop.util._ import ocelot.desktop.{OcelotDesktop, Settings} @@ -24,7 +23,6 @@ import java.nio.channels.Channels import java.nio.file.Paths import javax.imageio.ImageIO import javax.swing.JFileChooser -import scala.annotation.tailrec import scala.collection.mutable import scala.collection.mutable.ArrayBuffer import scala.concurrent.duration.DurationInt diff --git a/src/main/scala/ocelot/desktop/ui/widget/ScrollView.scala b/src/main/scala/ocelot/desktop/ui/widget/ScrollView.scala index 6628931..3fe45e1 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/ScrollView.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/ScrollView.scala @@ -7,7 +7,7 @@ import ocelot.desktop.graphics.Graphics import ocelot.desktop.ui.UiHandler import ocelot.desktop.ui.event.handlers.HoverHandler import ocelot.desktop.ui.event.sources.KeyEvents -import ocelot.desktop.ui.event.{MouseEvent, ScrollEvent} +import ocelot.desktop.ui.event.{Capturing, MouseEvent, ScrollEvent} import ocelot.desktop.ui.layout.Layout import ocelot.desktop.ui.widget.ScrollView.{DecayFactor, DragState, MaxScrollVelocity, MinThumbSize, ScrollVelocity} import ocelot.desktop.util.Logging @@ -49,10 +49,15 @@ class ScrollView(val inner: Widget) extends Widget with Logging with HoverHandle scrollToEnd = false } - case MouseEvent(MouseEvent.State.Pressed, MouseEvent.Button.Left) => + case Capturing(event @ MouseEvent(MouseEvent.State.Pressed, MouseEvent.Button.Left)) => val pos = UiHandler.mousePosition + if (vThumbBounds.contains(pos) || hThumbBounds.contains(pos)) { + event.consume() + } + dragState = if (vThumbBounds.contains(pos)) { + scrollToEnd = false DragState.Vertical(yOffset, pos) } else if (hThumbBounds.contains(pos)) { DragState.Horizontal(xOffset, pos) @@ -60,10 +65,6 @@ class ScrollView(val inner: Widget) extends Widget with Logging with HoverHandle DragState.None } - if (dragState.isVertical) { - scrollToEnd = false - } - case MouseEvent(MouseEvent.State.Released, MouseEvent.Button.Left) => dragState = DragState.None }