Refactor drag. Again.
This commit is contained in:
parent
9e3eec9533
commit
15ad690129
@ -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)
|
||||
}
|
@ -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" }
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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>)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -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 = {},
|
||||
|
@ -1,34 +1,51 @@
|
||||
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())
|
||||
awaitPointerEventScope {
|
||||
while (true) {
|
||||
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(
|
||||
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) {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user