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.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import center.sciprog.maps.coordinates.Gmc 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 @Composable
@ -71,33 +74,5 @@ public fun MapView(
initialRectangle = initialRectangle, 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) MapView(mapState, featureState, modifier)
} }

View File

@ -93,7 +93,7 @@ public actual fun MapView(
featuresState.features.values.filterIsInstance<PainterFeature<Gmc>>().associateWith { it.getPainter() } 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()) { if (canvasSize != size.toDpSize()) {
logger.debug { "Recalculate canvas. Size: $size" } 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 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> 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 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> { public fun interface DragHandle<T : Any> {
/** /**
* @param event - qualifiers of the event used for drag * @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 fun getPainter(): Painter
} }
public interface SelectableFeature<T : Any> : Feature<T> { public interface ClickableFeature<T : Any> : Feature<T> {
public fun contains(point: T, zoom: Float): Boolean = getBoundingBox(zoom)?.let { public operator fun contains(viewPoint: ViewPoint<T>): Boolean = getBoundingBox(viewPoint.zoom)?.let {
point in it viewPoint.focus in it
} ?: false } ?: false
} }
public interface DraggableFeature<T : Any> : SelectableFeature<T> { public interface DraggableFeature<T : Any> : ClickableFeature<T> {
public fun withCoordinates(newCoordinates: T): Feature<T> public fun withCoordinates(newCoordinates: T): Feature<T>
} }
@ -152,12 +152,12 @@ public class LineFeature<T : Any>(
override val zoomRange: FloatRange, override val zoomRange: FloatRange,
public val color: Color = Color.Red, public val color: Color = Color.Red,
override val attributes: AttributeMap = AttributeMap(), override val attributes: AttributeMap = AttributeMap(),
) : SelectableFeature<T> { ) : ClickableFeature<T> {
override fun getBoundingBox(zoom: Float): Rectangle<T> = override fun getBoundingBox(zoom: Float): Rectangle<T> =
space.Rectangle(a, b) space.Rectangle(a, b)
override fun contains(point: T, zoom: Float): Boolean = with(space) { override fun contains(veiwPoint: ViewPoint<T>): Boolean = with(space) {
point in space.Rectangle(a, b) && point.distanceToLine(a, b, zoom).value < 5f 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.drawscope.DrawScope
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector 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.Dp
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -47,7 +48,8 @@ public class FeatureCollection<T : Any>(
get() = featureMap.mapKeys { FeatureId<Feature<T>>(it.key) } get() = featureMap.mapKeys { FeatureId<Feature<T>>(it.key) }
@Suppress("UNCHECKED_CAST") @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()}]" 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 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 * Add drag to this feature
* *
@ -74,19 +89,18 @@ public class FeatureCollection<T : Any>(
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
public fun FeatureId<DraggableFeature<T>>.draggable( public fun FeatureId<DraggableFeature<T>>.draggable(
constraint: ((T) -> T)? = null, 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) { if (getAttribute(this, DraggableAttribute) == null) {
val handle = DragHandle.withPrimaryButton<Any> { _, start, end -> val handle = DragHandle.withPrimaryButton<Any> { event, start, end ->
val feature = featureMap[id] as? DraggableFeature ?: return@withPrimaryButton DragResult(end) val feature = featureMap[id] as? DraggableFeature<T> ?: return@withPrimaryButton DragResult(end)
val startPosition = start.focus as T start as ViewPoint<T>
val endPosition = end.focus as T end as ViewPoint<T>
val boundingBox = feature.getBoundingBox(start.zoom) ?: return@withPrimaryButton DragResult(end) if (start in feature) {
if (startPosition in boundingBox) { val finalPosition = constraint?.invoke(end.focus) ?: end.focus
val finalPosition = constraint?.invoke(endPosition) ?: endPosition
feature(id, feature.withCoordinates(finalPosition)) feature(id, feature.withCoordinates(finalPosition))
feature.attributes[DragListenerAttribute]?.forEach { feature.attributes[DragListenerAttribute]?.forEach {
it.invoke(startPosition, endPosition) it.handle(event, start, ViewPoint(finalPosition, end.zoom))
} }
DragResult(ViewPoint(finalPosition, end.zoom), false) DragResult(ViewPoint(finalPosition, end.zoom), false)
} else { } else {
@ -97,11 +111,8 @@ public class FeatureCollection<T : Any>(
} }
//Apply callback //Apply callback
if (callback != null) { if (listener != null) {
setAttribute( onDrag(listener)
this, DragListenerAttribute,
((getAttribute(this, DragListenerAttribute) ?: emptySet()) + callback) as Set<(Any, Any) -> Unit>
)
} }
} }
@ -118,15 +129,15 @@ public class FeatureCollection<T : Any>(
} }
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
public fun <F : SelectableFeature<T>> FeatureId<F>.selectable( public fun <F : ClickableFeature<T>> FeatureId<F>.onClick(
onSelect: () -> Unit, onClick: PointerEvent.(click: ViewPoint<T>) -> Unit,
) { ) {
// val handle = ClickHandle<Any> { event, click -> with(get(this)) {
// val feature: F = get(this@selectable) attributes[ClickableListenerAttribute] =
// if (feature.contains(this, click.focus)) (attributes[ClickableListenerAttribute] ?: emptySet()) + ClickListener { event, point ->
// } event.onClick(point as ViewPoint<T>)
// }
// setAttribute(this, SelectableAttribute, handle) }
} }

View File

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

View File

@ -1,23 +1,23 @@
package center.sciprog.maps.compose package center.sciprog.maps.compose
import androidx.compose.foundation.gestures.drag import androidx.compose.foundation.gestures.drag
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.* import androidx.compose.ui.input.pointer.*
import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.DpRect import androidx.compose.ui.unit.DpRect
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import center.sciprog.maps.features.CoordinateViewScope import center.sciprog.maps.features.*
import center.sciprog.maps.features.bottomRight
import center.sciprog.maps.features.topLeft
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
@OptIn(ExperimentalComposeUiApi::class) /**
* Create a modifier for Map/Scheme canvas controls on desktop
*/
public fun <T : Any> Modifier.mapControls( public fun <T : Any> Modifier.mapControls(
state: CoordinateViewScope<T>, state: CoordinateViewScope<T>,
features: Map<FeatureId<*>, Feature<T>>,
): Modifier = with(state) { ): Modifier = with(state) {
pointerInput(Unit) { pointerInput(Unit) {
fun Offset.toDpOffset() = DpOffset(x.toDp(), y.toDp()) fun Offset.toDpOffset() = DpOffset(x.toDp(), y.toDp())
@ -25,10 +25,27 @@ public fun <T : Any> Modifier.mapControls(
while (true) { while (true) {
val event = awaitPointerEvent() val event = awaitPointerEvent()
if (event.type == PointerEventType.Release) { if (event.type == PointerEventType.Release) {
val coordinates = event.changes.first().position.toDpOffset().toCoordinates()
val viewPoint = space.ViewPoint(coordinates, zoom)
config.onClick?.handle( config.onClick?.handle(
event, 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() change.consume()
} }
//val dragStart = change.position //val dragStart = change.position
//val dpPos = DpOffset(dragStart.x.toDp(), dragStart.y.toDp()) //val dpPos = DpOffset(dragStart.x.toDp(), dragStart.y.toDp())
@ -61,17 +79,27 @@ public fun <T : Any> Modifier.mapControls(
drag(change.id) { dragChange -> drag(change.id) { dragChange ->
val dragAmount: Offset = dragChange.position - dragChange.previousPosition 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 //apply drag handle and check if it prohibits the drag even propagation
if (selectionStart == null) { if (selectionStart == null) {
val dragResult = config.dragHandle?.handle( val dragStart = space.ViewPoint(
event, dragChange.previousPosition.toDpOffset().toCoordinates(),
space.ViewPoint(dpStart.toCoordinates(), zoom), zoom
space.ViewPoint(dpEnd.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 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) { if (event.buttons.isPrimaryPressed) {

View File

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