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.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)
|
||||||
}
|
}
|
@ -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" }
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -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 = {},
|
||||||
|
@ -1,34 +1,51 @@
|
|||||||
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())
|
||||||
awaitPointerEventScope {
|
awaitPointerEventScope {
|
||||||
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) {
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user