Refactor drag. Again.

This commit is contained in:
Alexander Nozik 2022-12-28 19:49:09 +03:00
parent 9e3eec9533
commit 15ad690129
9 changed files with 99 additions and 81 deletions

View File

@ -5,7 +5,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import center.sciprog.maps.coordinates.Gmc
import center.sciprog.maps.features.*
import center.sciprog.maps.features.FeatureCollection
import center.sciprog.maps.features.FeatureId
import center.sciprog.maps.features.Rectangle
import center.sciprog.maps.features.ViewConfig
@Composable
@ -71,33 +74,5 @@ public fun MapView(
initialRectangle = initialRectangle,
)
val featureDrag: DragHandle<Gmc> = DragHandle.withPrimaryButton { event, start, end ->
featureState.forEachWithAttribute(DraggableAttribute) { _, handle ->
@Suppress("UNCHECKED_CAST")
(handle as DragHandle<Gmc>)
.handle(event, start, end)
.takeIf { !it.handleNext }
?.let {
//we expect it already have no bypass
return@withPrimaryButton it
}
}
//bypass
DragResult(end)
}
val featureClick: ClickHandle<Gmc> = ClickHandle.withPrimaryButton { event, click ->
featureState.forEachWithAttribute(SelectableAttribute) { _, handle ->
@Suppress("UNCHECKED_CAST")
(handle as ClickHandle<Gmc>).handle(event, click)
config.onClick?.handle(event, click)
}
}
val newConfig = config.copy(
dragHandle = config.dragHandle?.let { DragHandle.combine(featureDrag, it) } ?: featureDrag,
onClick = featureClick
)
MapView(mapState, featureState, modifier)
}

View File

@ -93,7 +93,7 @@ public actual fun MapView(
featuresState.features.values.filterIsInstance<PainterFeature<Gmc>>().associateWith { it.getPainter() }
}
Canvas(modifier = modifier.mapControls(mapState).fillMaxSize()) {
Canvas(modifier = modifier.mapControls(mapState, featuresState.features).fillMaxSize()) {
if (canvasSize != size.toDpSize()) {
logger.debug { "Recalculate canvas. Size: $size" }

View File

@ -7,9 +7,9 @@ public object ZAttribute : Feature.Attribute<Float>
public object DraggableAttribute : Feature.Attribute<DragHandle<Any>>
public object DragListenerAttribute : Feature.Attribute<Set<(begin: Any, end: Any) -> Unit>>
public object DragListenerAttribute : Feature.Attribute<Set<DragListener<Any>>>
public object SelectableAttribute : Feature.Attribute<ClickHandle<Any>>
public object ClickableListenerAttribute : Feature.Attribute<Set<ClickListener<Any>>>
public object VisibleAttribute : Feature.Attribute<Boolean>

View File

@ -9,6 +9,10 @@ import androidx.compose.ui.input.pointer.isPrimaryPressed
*/
public data class DragResult<T : Any>(val result: ViewPoint<T>, val handleNext: Boolean = true)
public fun interface DragListener<in T : Any> {
public fun handle(event: PointerEvent, from: ViewPoint<T>, to: ViewPoint<T>)
}
public fun interface DragHandle<T : Any> {
/**
* @param event - qualifiers of the event used for drag

View File

@ -34,13 +34,13 @@ public interface PainterFeature<T : Any> : Feature<T> {
public fun getPainter(): Painter
}
public interface SelectableFeature<T : Any> : Feature<T> {
public fun contains(point: T, zoom: Float): Boolean = getBoundingBox(zoom)?.let {
point in it
public interface ClickableFeature<T : Any> : Feature<T> {
public operator fun contains(viewPoint: ViewPoint<T>): Boolean = getBoundingBox(viewPoint.zoom)?.let {
viewPoint.focus in it
} ?: false
}
public interface DraggableFeature<T : Any> : SelectableFeature<T> {
public interface DraggableFeature<T : Any> : ClickableFeature<T> {
public fun withCoordinates(newCoordinates: T): Feature<T>
}
@ -152,12 +152,12 @@ public class LineFeature<T : Any>(
override val zoomRange: FloatRange,
public val color: Color = Color.Red,
override val attributes: AttributeMap = AttributeMap(),
) : SelectableFeature<T> {
) : ClickableFeature<T> {
override fun getBoundingBox(zoom: Float): Rectangle<T> =
space.Rectangle(a, b)
override fun contains(point: T, zoom: Float): Boolean = with(space) {
point in space.Rectangle(a, b) && point.distanceToLine(a, b, zoom).value < 5f
override fun contains(veiwPoint: ViewPoint<T>): Boolean = with(space) {
veiwPoint.focus in space.Rectangle(a, b) && veiwPoint.focus.distanceToLine(a, b, veiwPoint.zoom).value < 5f
}
}

View File

@ -8,6 +8,7 @@ import androidx.compose.ui.graphics.PointMode
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
@ -47,7 +48,8 @@ public class FeatureCollection<T : Any>(
get() = featureMap.mapKeys { FeatureId<Feature<T>>(it.key) }
@Suppress("UNCHECKED_CAST")
public operator fun <F : Feature<T>> get(id: FeatureId<F>): F = featureMap[id.id] as F
public operator fun <F : Feature<T>> get(id: FeatureId<F>): F =
featureMap[id.id]?.let { it as F } ?: error("Feature with id=$id not found")
private fun generateID(feature: Feature<T>): String = "@feature[${feature.hashCode().toUInt()}]"
@ -64,6 +66,19 @@ public class FeatureCollection<T : Any>(
get(id).attributes[key] = value
}
@Suppress("UNCHECKED_CAST")
public fun FeatureId<DraggableFeature<T>>.onDrag(
listener: PointerEvent.(from: ViewPoint<T>, to: ViewPoint<T>) -> Unit,
) {
with(get(this)) {
attributes[DragListenerAttribute] =
(attributes[DragListenerAttribute] ?: emptySet()) + DragListener { event, from, to ->
event.listener(from as ViewPoint<T>, to as ViewPoint<T>)
}
}
}
/**
* Add drag to this feature
*
@ -74,19 +89,18 @@ public class FeatureCollection<T : Any>(
@Suppress("UNCHECKED_CAST")
public fun FeatureId<DraggableFeature<T>>.draggable(
constraint: ((T) -> T)? = null,
callback: ((start: T, end: T) -> Unit)? = null,
listener: (PointerEvent.(from: ViewPoint<T>, to: ViewPoint<T>) -> Unit)? = null
) {
if (getAttribute(this, DraggableAttribute) == null) {
val handle = DragHandle.withPrimaryButton<Any> { _, start, end ->
val feature = featureMap[id] as? DraggableFeature ?: return@withPrimaryButton DragResult(end)
val startPosition = start.focus as T
val endPosition = end.focus as T
val boundingBox = feature.getBoundingBox(start.zoom) ?: return@withPrimaryButton DragResult(end)
if (startPosition in boundingBox) {
val finalPosition = constraint?.invoke(endPosition) ?: endPosition
val handle = DragHandle.withPrimaryButton<Any> { event, start, end ->
val feature = featureMap[id] as? DraggableFeature<T> ?: return@withPrimaryButton DragResult(end)
start as ViewPoint<T>
end as ViewPoint<T>
if (start in feature) {
val finalPosition = constraint?.invoke(end.focus) ?: end.focus
feature(id, feature.withCoordinates(finalPosition))
feature.attributes[DragListenerAttribute]?.forEach {
it.invoke(startPosition, endPosition)
it.handle(event, start, ViewPoint(finalPosition, end.zoom))
}
DragResult(ViewPoint(finalPosition, end.zoom), false)
} else {
@ -97,11 +111,8 @@ public class FeatureCollection<T : Any>(
}
//Apply callback
if (callback != null) {
setAttribute(
this, DragListenerAttribute,
((getAttribute(this, DragListenerAttribute) ?: emptySet()) + callback) as Set<(Any, Any) -> Unit>
)
if (listener != null) {
onDrag(listener)
}
}
@ -118,15 +129,15 @@ public class FeatureCollection<T : Any>(
}
@Suppress("UNCHECKED_CAST")
public fun <F : SelectableFeature<T>> FeatureId<F>.selectable(
onSelect: () -> Unit,
public fun <F : ClickableFeature<T>> FeatureId<F>.onClick(
onClick: PointerEvent.(click: ViewPoint<T>) -> Unit,
) {
// val handle = ClickHandle<Any> { event, click ->
// val feature: F = get(this@selectable)
// if (feature.contains(this, click.focus))
// }
//
// setAttribute(this, SelectableAttribute, handle)
with(get(this)) {
attributes[ClickableListenerAttribute] =
(attributes[ClickableListenerAttribute] ?: emptySet()) + ClickListener { event, point ->
event.onClick(point as ViewPoint<T>)
}
}
}

View File

@ -4,13 +4,13 @@ import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.isPrimaryPressed
import androidx.compose.ui.unit.DpSize
public fun interface ClickHandle<T : Any> {
public fun interface ClickListener<in T : Any> {
public fun handle(event: PointerEvent, click: ViewPoint<T>): Unit
public companion object {
public fun <T : Any> withPrimaryButton(
block: (event: PointerEvent, click: ViewPoint<T>) -> Unit,
): ClickHandle<T> = ClickHandle { event, click ->
): ClickListener<T> = ClickListener { event, click ->
if (event.buttons.isPrimaryPressed) {
block(event, click)
}
@ -20,7 +20,7 @@ public fun interface ClickHandle<T : Any> {
public data class ViewConfig<T : Any>(
val zoomSpeed: Float = 1f / 3f,
val onClick: ClickHandle<T>? = null,
val onClick: ClickListener<T>? = null,
val dragHandle: DragHandle<T>? = null,
val onViewChange: ViewPoint<T>.() -> Unit = {},
val onSelect: (Rectangle<T>) -> Unit = {},

View File

@ -1,23 +1,23 @@
package center.sciprog.maps.compose
import androidx.compose.foundation.gestures.drag
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.*
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.DpRect
import androidx.compose.ui.unit.dp
import center.sciprog.maps.features.CoordinateViewScope
import center.sciprog.maps.features.bottomRight
import center.sciprog.maps.features.topLeft
import center.sciprog.maps.features.*
import kotlin.math.max
import kotlin.math.min
@OptIn(ExperimentalComposeUiApi::class)
/**
* Create a modifier for Map/Scheme canvas controls on desktop
*/
public fun <T : Any> Modifier.mapControls(
state: CoordinateViewScope<T>,
features: Map<FeatureId<*>, Feature<T>>,
): Modifier = with(state) {
pointerInput(Unit) {
fun Offset.toDpOffset() = DpOffset(x.toDp(), y.toDp())
@ -25,10 +25,27 @@ public fun <T : Any> Modifier.mapControls(
while (true) {
val event = awaitPointerEvent()
if (event.type == PointerEventType.Release) {
val coordinates = event.changes.first().position.toDpOffset().toCoordinates()
val viewPoint = space.ViewPoint(coordinates, zoom)
config.onClick?.handle(
event,
space.ViewPoint(event.changes.first().position.toDpOffset().toCoordinates(), zoom)
viewPoint
)
features.values.mapNotNull { feature ->
val clickableFeature = feature as? ClickableFeature
?: return@mapNotNull null
val listeners = clickableFeature.attributes[ClickableListenerAttribute]
?: return@mapNotNull null
if (viewPoint in clickableFeature) {
feature to listeners
} else {
null
}
}.maxByOrNull {
it.first.z
}?.second?.forEach {
it.handle(event, viewPoint)
}
}
}
}
@ -48,6 +65,7 @@ public fun <T : Any> Modifier.mapControls(
}
change.consume()
}
//val dragStart = change.position
//val dpPos = DpOffset(dragStart.x.toDp(), dragStart.y.toDp())
@ -61,17 +79,27 @@ public fun <T : Any> Modifier.mapControls(
drag(change.id) { dragChange ->
val dragAmount: Offset = dragChange.position - dragChange.previousPosition
val dpStart = dragChange.previousPosition.toDpOffset()
val dpEnd = dragChange.position.toDpOffset()
//apply drag handle and check if it prohibits the drag even propagation
if (selectionStart == null) {
val dragResult = config.dragHandle?.handle(
event,
space.ViewPoint(dpStart.toCoordinates(), zoom),
space.ViewPoint(dpEnd.toCoordinates(), zoom)
val dragStart = space.ViewPoint(
dragChange.previousPosition.toDpOffset().toCoordinates(),
zoom
)
val dragEnd = space.ViewPoint(
dragChange.position.toDpOffset().toCoordinates(),
zoom
)
val dragResult = config.dragHandle?.handle(event, dragStart, dragEnd)
if (dragResult?.handleNext == false) return@drag
features.values.filterIsInstance<DraggableFeature<T>>()
.sortedByDescending { it.z }
.forEach { draggableFeature ->
draggableFeature.attributes[DraggableAttribute]?.let { handler->
if (!handler.handle(event, dragStart, dragEnd).handleNext) return@drag
}
}
}
if (event.buttons.isPrimaryPressed) {

View File

@ -141,10 +141,10 @@ public fun SchemeView(
DragResult(end)
}
val featureClick: ClickHandle<XY> = ClickHandle.withPrimaryButton { event, click ->
featureState.forEachWithAttribute(SelectableAttribute) { _, handle ->
val featureClick: ClickListener<XY> = ClickListener.withPrimaryButton { event, click ->
featureState.forEachWithAttribute(ClickableListenerAttribute) { _, handle ->
@Suppress("UNCHECKED_CAST")
(handle as ClickHandle<XY>).handle(event, click)
(handle as ClickListener<XY>).handle(event, click)
config.onClick?.handle(event, click)
}
}