
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
import io.nacular.doodle.utils.fastMutableSetOf
import io.nacular.doodle.utils.fastSetOf
/** @suppress */
@Internal
public interface PointerInputManager {
public fun shutdown()
}
/** @suppress */
@Internal
public interface EventPreprocessor {
public operator fun invoke(pointerEvent: PointerEvent)
}
/** @suppress */
@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? = 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 inner class ClickedPassThroughViewMap {
private val map: MutableMap> = mutableMapOf()
fun add (event: SystemPointerEvent, value: View) {
map.getOrPut(event.id) { mutableSetOf() }.add(value).also {
passedThroughCoveredView.add(event, value)
}
}
fun get (event: SystemPointerEvent) = map[event.id] ?: emptyList()
fun remove(event: SystemPointerEvent, value: View): View? = map[event.id]?.let {
val removed = it.remove(value)
if (it.isEmpty()) {
map.remove(event.id)
}
passedThroughCoveredView.remove(event, value)
return if (removed) value else null
}
operator fun minusAssign(event: SystemPointerEvent) {
map.remove(event.id)
}
}
private inner class CoveredPassThroughViewMap {
private val map: MutableMap> = mutableMapOf()
fun add (event: SystemPointerEvent, value: View) { map.getOrPut(event.id) { mutableSetOf() } }
fun get (event: SystemPointerEvent) = map[event.id] ?: emptyList()
fun remove(event: SystemPointerEvent, value: View): View? = map[event.id]?.let {
val removed = it.remove(value)
if (it.isEmpty()) {
map.remove(event.id)
}
return if (removed) value else null
}
operator fun minusAssign(event: SystemPointerEvent) {
map.remove(event.id)
}
}
private val pressedPointers = mutableSetOf()
private fun isPointerDown(event: SystemPointerEvent) = event.id in pressedPointers
private val clickedView = ClickedViewMap()
private val coveredView = CoveredViewMap()
private val passedThroughCoveredView = CoveredPassThroughViewMap()
private val passedThroughClickedView = ClickedPassThroughViewMap()
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.setCursor(display, cursor ?: display.cursor)
}
private var toolTipText = ""; set(new) {
field = new
inputService.setToolTipText(display, 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.addListener(display, this)
display.cursorChanged += displayCursorChanged
cursor = display.cursor
}
override fun shutdown() {
inputService.removeListener(display, this)
display.cursorChanged -= displayCursorChanged
}
override fun invoke(event: SystemPointerEvent) {
when (event.type) {
Up -> pointerUp (event)
Enter, Move -> pointerMove(event)
Down -> pointerDown(event)
Exit -> pointerExit(event)
else -> {}
}
}
private fun pointerExit(event: SystemPointerEvent) {
clickedView[event]?.let {
deliver(event, it, Exit)
clickedView -= event
}
coveredView[event]?.let {
deliver(event, it, Exit)
coveredView -= event
cleanupPointers(it, event)
}
pressedPointers -= event.id
}
private fun pointerDown(event: SystemPointerEvent) {
toolTipText = ""
view(from = event)?.let { view ->
if (view != coveredView[event]) {
deliver(event, view, Enter)
coveredView[event] = view
cursor = cursor(of = coveredView[event])
}
deliver(event, view)
clickedView[event] = view
}
pressedPointers += event.id
}
private fun pointerUp(event: SystemPointerEvent) {
val view = view(from = event)
if (clickedView[event] != null || isPointerDown(event)) {
clickedView[event]?.let {
deliver(event, it)
if (view === it) {
deliver(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, it, Exit)
}
}
if (view != null) {
coveredView[event] = view
deliver(event, view, Enter)
deliver(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, view, Enter)
deliver(event, view )
cursor = cursor(of = view)
} else {
cursor = display.cursor
}
pressedPointers -= event.id
}
// private fun doubleClick(event: SystemPointerEvent) {
// toolTipText = ""
//
// view(from = event)?.let {
// coveredView[event] = it
// deliver(event, it, Up )
// deliver(event, it, Click)
// clickedView -= event
// }
// }
private fun pointerMove(event: SystemPointerEvent) {
clickedView[event]?.let {
// TODO: Deliver Drag to views that passed through pointer-down
deliver(event, it, Drag)
cursor = cursor(of = it)
}
val view = view(from = event)
// TODO: Need to send synthetic EXIT for views that passed MOVE through and are not covered anymore
if (view !== coveredView[event]) {
coveredView[event]?.let {
if (!isPointerDown(event) || it === clickedView[event]) {
deliver(event, it, Exit)
}
}
when (view) {
null -> coveredView -= event
else -> coveredView[event] = view
}
if (view != null) {
if (!isPointerDown(event) || view === clickedView[event]) {
deliver(event, view, Enter)
cursor = cursor(of = coveredView[event])
}
} else if (clickedView[event] == null) {
toolTipText = ""
cursor = null
}
} else if (!isPointerDown(event)) {
coveredView[event]?.let {
deliver(event, it, Move)
}
if (coveredView[event] == null) {
toolTipText = ""
}
cursor = cursor(of = coveredView[event])
}
}
private fun shouldHandleEvent(view: View, event: PointerEvent) = when (event.type) {
Move, Drag -> view.shouldHandlePointerMotionEvent_(event)
else -> view.shouldHandlePointerEvent_ (event)
}
private fun deliver(systemEvent: SystemPointerEvent, target: View, type: Type = systemEvent.type): Boolean {
val event = createPointerEvent(systemEvent, target, type)
when (event.type) {
Enter, Move, Up -> toolTipText = target.toolTipText(event)
else -> {}
}
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 target = adjustForNative(viewFinder.find(display, from.location), from)
val passedThrough = mutableSetOf()
while (target != null) {
val event = createPointerEvent(from, target, from.type)
when {
shouldHandleEvent(target, event) -> break
else -> {
when (from.type) {
Down -> passedThroughClickedView.add (from, target)
Up -> passedThroughClickedView.remove(from, target)?.let {
// synthesize click event
it.notifyOfPassThrough(createPointerEvent(from, it, Click))
}
Exit -> passedThroughCoveredView.remove(from, target)
Enter -> if (target in passedThroughClickedView.get(from)) {
passedThroughCoveredView.add(from, target)
}
else -> {}
}
passedThrough += target
target = view(from, target.parent) { it !in passedThrough }
}
}
}
return target
}
private fun view(from: SystemPointerEvent, starting: View?, filter: (View) -> Boolean): View? = adjustForNative(
viewFinder.find(display, from.location, starting) { it: View -> it.enabled && filter(it) },
from
)
private fun adjustForNative(target: View?, from: SystemPointerEvent): View? {
var view = target
return view?.let {
// This handles cases when a native scroll panel's scroll bars, or blank areas.
// We need to ensure the ScrollPanel is properly selected, even if the system thinks
// a child is the proper target view
if (from.nativeScrollPanel) {
// Search for ScrollBar ancestor and use that if found
var newView = view
while(newView != null && newView !is ScrollPanel) {
newView = newView.parent
}
if (newView is ScrollPanel) {
view = newView
}
}
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) { fastMutableSetOf() }.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 = fastSetOf(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