commonMain.io.nacular.doodle.deviceinput.PointerInputManager.kt Maven / Gradle / Ivy
package io.nacular.doodle.deviceinput
import io.nacular.doodle.controls.panels.ScrollPanel
import io.nacular.doodle.core.Display
import io.nacular.doodle.core.Internal
import io.nacular.doodle.core.View
import io.nacular.doodle.drawing.AffineTransform
import io.nacular.doodle.event.Interaction
import io.nacular.doodle.event.Pointer
import io.nacular.doodle.event.PointerEvent
import io.nacular.doodle.event.with
import io.nacular.doodle.geometry.Rectangle
import io.nacular.doodle.system.Cursor
import io.nacular.doodle.system.PointerInputService
import io.nacular.doodle.system.SystemPointerEvent
import io.nacular.doodle.system.SystemPointerEvent.Type
import io.nacular.doodle.system.SystemPointerEvent.Type.Click
import io.nacular.doodle.system.SystemPointerEvent.Type.Down
import io.nacular.doodle.system.SystemPointerEvent.Type.Drag
import io.nacular.doodle.system.SystemPointerEvent.Type.Enter
import io.nacular.doodle.system.SystemPointerEvent.Type.Exit
import io.nacular.doodle.system.SystemPointerEvent.Type.Move
import io.nacular.doodle.system.SystemPointerEvent.Type.Up
@Internal
public interface PointerInputManager {
public fun shutdown()
}
@Internal
public interface EventPreprocessor {
public operator fun invoke(pointerEvent: PointerEvent)
}
@Internal
@Suppress("NestedLambdaShadowedImplicitParameter")
public class PointerInputManagerImpl(
private val display : Display,
private val inputService : PointerInputService,
private val viewFinder : ViewFinder,
private val eventPreprocessor: EventPreprocessor? = null): PointerInputManager, PointerInputService.Listener {
private inner class ClickedViewMap {
private val map: MutableMap = mutableMapOf()
operator fun set(event: SystemPointerEvent, value: View): View? = map.put(event.id, value)
operator fun get(event: SystemPointerEvent): View? = map[event.id]
operator fun minusAssign(event: SystemPointerEvent) {
map.remove(event.id)?.also {
if (coveredView[event] != it) {
cleanupPointers(it, event)
}
}
}
}
private inner class CoveredViewMap {
private val map: MutableMap = mutableMapOf()
operator fun set(event: SystemPointerEvent, value: View): View? {
return map.put(event.id, value.also { registerListeners(it) })?.also {
cleanupPointers(it, event)
}
}
operator fun get(event: SystemPointerEvent): View? = map[event.id]
operator fun minusAssign(event: SystemPointerEvent) {
map.remove(event.id)?.also {
cleanupPointers(it, event)
}
}
}
private val pressedPointers = mutableSetOf()
private fun isPointerDown(event: SystemPointerEvent) = event.id in pressedPointers
private val clickedView = ClickedViewMap()
private val coveredView = CoveredViewMap()
private val targetedInteractions = mutableMapOf>()
private fun cleanupPointers(view: View, event: SystemPointerEvent) {
targetedInteractions[view]?.let {
it.removeAll {
it.pointer.id == event.id && view != clickedView[event]
}
if (it.isEmpty()) {
targetedInteractions -= view
unregisterListeners(view)
}
}
}
private var cursor = null as Cursor?
set(new) {
field = new
inputService.cursor = cursor ?: display.cursor
}
private var toolTipText = ""
set(new) {
field = new
inputService.toolTipText = field
}
private val displayCursorChanged = { _: Display, _: Cursor?, new: Cursor? -> cursor = new }
private val enabledChanged = { view: View, _: Boolean, enabled: Boolean ->
// FIXME: Send disabled view/parent exit/enter events for all existing interactions.
// FIXME: Send enabled view/parent enter/exit events for all valid interactions.
if (!enabled) {
targetedInteractions -= view
}
}
private val boundsChanged: (View, Rectangle, Rectangle) -> Unit = { view,_,_ ->
changedViewReferenceFrames += view
}
private val transformChanged: (View, AffineTransform, AffineTransform) -> Unit = { view,_,_ ->
changedViewReferenceFrames += view
}
private val viewCursorChanged = { view: View, _: Cursor?, _: Cursor? ->
cursor = cursor(of = view)
}
init {
inputService += this
display.cursorChanged += displayCursorChanged
cursor = display.cursor
}
override fun shutdown() {
inputService -= this
display.cursorChanged -= displayCursorChanged
}
override fun changed(event: SystemPointerEvent) {
when (event.type) {
// Up -> when(event.clickCount) {
// 1 -> pointerUp (event)
// else -> doubleClick(event)
// }
Up -> pointerUp (event)
Move -> pointerMove(event)
Down -> pointerDown(event)
Exit -> pointerExit(event)
else -> {}
}
}
private fun pointerExit(event: SystemPointerEvent) {
clickedView[event]?.let {
deliver(event, createPointerEvent(event, it, Exit))
clickedView -= event
}
coveredView[event]?.let {
deliver(event, createPointerEvent(event, it, Exit))
coveredView -= event
cleanupPointers(it, event)
}
pressedPointers -= event.id
}
private fun pointerUp(event: SystemPointerEvent) {
val view = view(from = event)
if (clickedView[event] != null || isPointerDown(event)) {
clickedView[event]?.let {
deliver(event, createPointerEvent(event, it))
if (view === it) {
deliver(event, createPointerEvent(event, it, Click))
}
}
if (view !== clickedView[event]) {
clickedView[event]?.let {
// Avoid case where pointer-move hasn't been seen (possible if drag-drop happened)
if (coveredView[event] == it) {
coveredView -= event
deliver(event, createPointerEvent(event, it, Exit))
}
}
if (view != null) {
coveredView[event] = view
deliver(event, createPointerEvent(event, view, Enter))
deliver(event, createPointerEvent(event, view ))
cursor = cursor(of = view)
} else {
cursor = display.cursor
}
} else {
cursor = cursor(of = view)
}
clickedView -= event
} else if (view != null) {
coveredView[event] = view
deliver(event, createPointerEvent(event, view, Enter))
deliver(event, createPointerEvent(event, view ))
cursor = cursor(of = view)
} else {
cursor = display.cursor
}
pressedPointers -= event.id
}
private fun pointerDown(event: SystemPointerEvent) {
toolTipText = ""
view(from = event)?.let { view ->
if (view != coveredView[event]) {
createPointerEvent(event, view, Enter).also {
deliver(event, it)
toolTipText = view.toolTipText(it)
}
coveredView[event] = view
cursor = cursor(of = coveredView[event])
}
deliver(event, createPointerEvent(event, view))
clickedView[event] = view
}
pressedPointers += event.id
}
private fun doubleClick(event: SystemPointerEvent) {
toolTipText = ""
view(from = event)?.let {
coveredView[event] = it
deliver(event, createPointerEvent(event, it, Up ))
deliver(event, createPointerEvent(event, it, Click))
clickedView -= event
}
}
private fun pointerMove(event: SystemPointerEvent) {
clickedView[event]?.let {
deliver(event, createPointerEvent(event, it, Drag))
cursor = cursor(of = it)
}
val view = view(from = event)
if (view !== coveredView[event]) {
coveredView[event]?.let {
if (!isPointerDown(event) || it === clickedView[event]) {
deliver(event, createPointerEvent(event, it, Exit))
}
}
when (view) {
null -> coveredView -= event
else -> coveredView[event] = view
}
if (view != null) {
if (!isPointerDown(event) || view === clickedView[event]) {
createPointerEvent(event, view, Enter).also {
deliver(event, it)
toolTipText = view.toolTipText(it)
}
cursor = cursor(of = coveredView[event])
}
} else if (clickedView[event] == null) {
toolTipText = ""
cursor = null
}
} else if (!isPointerDown(event)) {
coveredView[event]?.let {
deliver(event, createPointerEvent(event, it, Move))
}
if (coveredView[event] == null) {
toolTipText = ""
}
cursor = cursor(of = coveredView[event])
}
}
private fun deliver(systemEvent: SystemPointerEvent, event: PointerEvent): Boolean {
val chain = mutableListOf(event.target)
var view = event.target.parent
while (view != null) {
if (view.enabled && view.visible) {
chain += view
}
view = view.parent
}
// Sinking
chain.asReversed().forEach {
val newEvent = event.with(source = it)
eventPreprocessor?.invoke(newEvent)
if (!newEvent.consumed) {
when (newEvent.type) {
Move, Drag -> it.filterPointerMotionEvent_(newEvent)
else -> it.filterPointerEvent_(newEvent)
}
}
if (newEvent.consumed || newEvent.preventOsHandling) {
systemEvent.consume()
}
if (newEvent.consumed) {
return true
}
}
// Floating
chain.forEach {
val newEvent = event.with(source = it)
when (newEvent.type) {
Move, Drag -> it.handlePointerMotionEvent_(newEvent)
else -> it.handlePointerEvent_ (newEvent)
}
if (newEvent.consumed || newEvent.preventOsHandling) {
systemEvent.consume()
}
if (newEvent.consumed) {
return true
}
}
return false
}
private fun registerListeners(view: View) {
view.cursorChanged += viewCursorChanged
view.enabledChanged += enabledChanged
view.boundsChanged += boundsChanged
view.transformChanged += transformChanged
}
private fun unregisterListeners(view: View) {
view.cursorChanged -= viewCursorChanged
view.enabledChanged -= enabledChanged
view.boundsChanged -= boundsChanged
view.transformChanged -= transformChanged
changedViewReferenceFrames -= view
}
private fun cursor(of: View?) = when (display.cursor) {
null -> of?.cursor
else -> display.cursor
}
private fun view(from: SystemPointerEvent): View? {
var view = viewFinder.find(from.location)
return view?.let {
if (from.nativeScrollPanel) {
while(view != null && view !is ScrollPanel) {
view = view?.parent
}
}
view
}
}
private val changedViewReferenceFrames = mutableSetOf()
private fun createPointerEvent(event: SystemPointerEvent, target: View, type: Type = event.type): PointerEvent {
val interaction = createInteraction(target, event, type)
targetedInteractions.getOrPut(target) { mutableSetOf() }.let { set ->
set.removeAll { it.pointer == interaction.pointer }
if (target in changedViewReferenceFrames) {
updateInteractions(target)
}
set += interaction
}
return PointerEvent(
target,
target,
event.buttons,
event.clickCount,
targetInteractions = targetedInteractions[target]!!,
changedInteractions = setOf(interaction),
allInteractions = { targetedInteractions.values.asSequence().flatten().toSet() },
modifiers = event.modifiers)
}
private fun createInteraction(target: View, event: SystemPointerEvent, type: Type = event.type) = Interaction(
Pointer(event.id),
target,
type,
target.fromAbsolute(event.location),
event.location
)
private fun updateInteractions(view: View) {
changedViewReferenceFrames -= view
targetedInteractions[view]?.let { set ->
val updated = set.map { interaction ->
Interaction(
pointer = interaction.pointer,
target = interaction.target,
state = interaction.state,
location = interaction.target.fromAbsolute(interaction.absoluteLocation),
absoluteLocation = interaction.absoluteLocation
)
}
set.clear()
set.addAll(updated)
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy