mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-04 22:58:29 +08:00
Improved camera handling.
This commit is contained in:
parent
132cdccd0a
commit
990a8c144f
@ -9,11 +9,11 @@ private val logger = KotlinLogging.logger {}
|
||||
|
||||
abstract class Renderer(
|
||||
val canvas: HTMLCanvasElement,
|
||||
protected val engine: Engine,
|
||||
val engine: Engine,
|
||||
) : DisposableContainer() {
|
||||
private val light: HemisphericLight
|
||||
|
||||
protected abstract val camera: Camera
|
||||
abstract val camera: Camera
|
||||
|
||||
val scene = Scene(engine)
|
||||
|
||||
@ -44,7 +44,7 @@ abstract class Renderer(
|
||||
engine.stopRenderLoop()
|
||||
}
|
||||
|
||||
private fun render() {
|
||||
protected open fun render() {
|
||||
val lightDirection = Vector3(-1.0, 1.0, 1.0)
|
||||
lightDirection.rotateByQuaternionToRef(camera.absoluteRotation, lightDirection)
|
||||
light.direction = lightDirection
|
||||
|
@ -50,6 +50,7 @@ external class Vector3(x: Double, y: Double, z: Double) {
|
||||
companion object {
|
||||
fun One(): Vector3
|
||||
fun Up(): Vector3
|
||||
fun Down(): Vector3
|
||||
fun Zero(): Vector3
|
||||
fun Dot(left: Vector3, right: Vector3): Double
|
||||
fun TransformCoordinates(vector: Vector3, transformation: Matrix): Vector3
|
||||
@ -145,6 +146,8 @@ open external class ThinEngine {
|
||||
* be removed.
|
||||
*/
|
||||
fun stopRenderLoop(renderFunction: () -> Unit = definedExternally)
|
||||
fun getRenderWidth(useScreen: Boolean = definedExternally): Double
|
||||
fun getRenderHeight(useScreen: Boolean = definedExternally): Double
|
||||
|
||||
fun dispose()
|
||||
}
|
||||
@ -154,7 +157,7 @@ external class Engine(
|
||||
antialias: Boolean = definedExternally,
|
||||
) : ThinEngine
|
||||
|
||||
external class Ray
|
||||
external class Ray(origin: Vector3, direction: Vector3, length: Double = definedExternally)
|
||||
|
||||
external class PickingInfo {
|
||||
val bu: Double
|
||||
@ -166,6 +169,13 @@ external class PickingInfo {
|
||||
val pickedMesh: AbstractMesh?
|
||||
val pickedPoint: Vector3?
|
||||
val ray: Ray?
|
||||
|
||||
fun getNormal(
|
||||
useWorldCoordinates: Boolean = definedExternally,
|
||||
useVerticesNormals: Boolean = definedExternally,
|
||||
): Vector3?
|
||||
|
||||
fun getTextureCoordinates(): Vector2?
|
||||
}
|
||||
|
||||
external class Scene(engine: Engine) {
|
||||
@ -190,6 +200,32 @@ external class Scene(engine: Engine) {
|
||||
trianglePredicate: (p0: Vector3, p1: Vector3, p2: Vector3, ray: Ray) -> Boolean = definedExternally,
|
||||
): PickingInfo?
|
||||
|
||||
fun pickWithRay(
|
||||
ray: Ray,
|
||||
predicate: (AbstractMesh) -> Boolean = definedExternally,
|
||||
fastCheck: Boolean = definedExternally,
|
||||
trianglePredicate: (p0: Vector3, p1: Vector3, p2: Vector3, ray: Ray) -> Boolean = definedExternally,
|
||||
): PickingInfo?
|
||||
|
||||
/**
|
||||
* @param x X position on screen
|
||||
* @param y Y position on screen
|
||||
* @param predicate Predicate function used to determine eligible meshes. Can be set to null. In this case, a mesh must be enabled, visible and with isPickable set to true
|
||||
*/
|
||||
fun multiPick(
|
||||
x: Double,
|
||||
y: Double,
|
||||
predicate: (AbstractMesh) -> Boolean = definedExternally,
|
||||
camera: Camera = definedExternally,
|
||||
trianglePredicate: (p0: Vector3, p1: Vector3, p2: Vector3, ray: Ray) -> Boolean = definedExternally,
|
||||
): Array<PickingInfo>?
|
||||
|
||||
fun multiPickWithRay(
|
||||
ray: Ray,
|
||||
predicate: (AbstractMesh) -> Boolean = definedExternally,
|
||||
trianglePredicate: (p0: Vector3, p1: Vector3, p2: Vector3, ray: Ray) -> Boolean = definedExternally,
|
||||
): Array<PickingInfo>?
|
||||
|
||||
fun dispose()
|
||||
}
|
||||
|
||||
@ -214,15 +250,21 @@ open external class Node {
|
||||
}
|
||||
|
||||
open external class Camera : Node {
|
||||
var minZ: Double
|
||||
var maxZ: Double
|
||||
val absoluteRotation: Quaternion
|
||||
val onProjectionMatrixChangedObservable: Observable<Camera>
|
||||
val onViewMatrixChangedObservable: Observable<Camera>
|
||||
val onAfterCheckInputsObservable: Observable<Camera>
|
||||
|
||||
fun attachControl(noPreventDefault: Boolean = definedExternally)
|
||||
fun storeState(): Camera
|
||||
fun restoreState(): Boolean
|
||||
}
|
||||
|
||||
open external class TargetCamera : Camera
|
||||
open external class TargetCamera : Camera {
|
||||
var target: Vector3
|
||||
}
|
||||
|
||||
/**
|
||||
* @param setActiveOnSceneIfNoneActive default true
|
||||
@ -236,6 +278,9 @@ external class ArcRotateCamera(
|
||||
scene: Scene,
|
||||
setActiveOnSceneIfNoneActive: Boolean = definedExternally,
|
||||
) : TargetCamera {
|
||||
var alpha: Double
|
||||
var beta: Double
|
||||
var radius: Double
|
||||
var inertia: Double
|
||||
var angularSensibilityX: Double
|
||||
var angularSensibilityY: Double
|
||||
@ -244,6 +289,7 @@ external class ArcRotateCamera(
|
||||
var panningAxis: Vector3
|
||||
var pinchDeltaPercentage: Double
|
||||
var wheelDeltaPercentage: Double
|
||||
var lowerBetaLimit: Double
|
||||
|
||||
fun attachControl(
|
||||
element: HTMLCanvasElement,
|
||||
|
@ -13,7 +13,7 @@ import world.phantasmal.web.questEditor.controllers.QuestInfoController
|
||||
import world.phantasmal.web.questEditor.loading.AreaAssetLoader
|
||||
import world.phantasmal.web.questEditor.loading.EntityAssetLoader
|
||||
import world.phantasmal.web.questEditor.loading.QuestLoader
|
||||
import world.phantasmal.web.questEditor.rendering.EntityManipulator
|
||||
import world.phantasmal.web.questEditor.rendering.UserInputManager
|
||||
import world.phantasmal.web.questEditor.rendering.QuestEditorMeshManager
|
||||
import world.phantasmal.web.questEditor.rendering.QuestRenderer
|
||||
import world.phantasmal.web.questEditor.stores.AreaStore
|
||||
@ -57,7 +57,7 @@ class QuestEditor(
|
||||
areaAssetLoader,
|
||||
entityAssetLoader
|
||||
),
|
||||
EntityManipulator(questEditorStore, renderer)
|
||||
UserInputManager(questEditorStore, renderer)
|
||||
)
|
||||
|
||||
// Main Widget
|
||||
|
@ -19,6 +19,7 @@ import world.phantasmal.web.externals.babylon.Scene
|
||||
import world.phantasmal.web.externals.babylon.TransformNode
|
||||
import world.phantasmal.web.questEditor.models.AreaVariantModel
|
||||
import world.phantasmal.web.questEditor.models.SectionModel
|
||||
import world.phantasmal.web.questEditor.rendering.CollisionMetadata
|
||||
import world.phantasmal.webui.DisposableContainer
|
||||
|
||||
/**
|
||||
@ -261,6 +262,7 @@ private fun areaCollisionGeometryToTransformNode(
|
||||
if (builder.vertexCount > 0) {
|
||||
val mesh = Mesh("Collision Geometry", scene, parent = node)
|
||||
builder.build().applyToMesh(mesh)
|
||||
mesh.metadata = CollisionMetadata()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,17 +2,17 @@ package world.phantasmal.web.questEditor.rendering
|
||||
|
||||
import mu.KotlinLogging
|
||||
import world.phantasmal.lib.fileFormats.quest.Episode
|
||||
import world.phantasmal.web.externals.babylon.TransformNode
|
||||
import world.phantasmal.web.questEditor.loading.AreaAssetLoader
|
||||
import world.phantasmal.web.questEditor.models.AreaVariantModel
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
class AreaMeshManager(private val areaAssetLoader: AreaAssetLoader) {
|
||||
private var currentGeometry: TransformNode? = null
|
||||
|
||||
class AreaMeshManager(
|
||||
private val renderer: QuestRenderer,
|
||||
private val areaAssetLoader: AreaAssetLoader,
|
||||
) {
|
||||
suspend fun load(episode: Episode?, areaVariant: AreaVariantModel?) {
|
||||
currentGeometry?.setEnabled(false)
|
||||
renderer.collisionGeometry?.setEnabled(false)
|
||||
|
||||
if (episode == null || areaVariant == null) {
|
||||
return
|
||||
@ -21,7 +21,8 @@ class AreaMeshManager(private val areaAssetLoader: AreaAssetLoader) {
|
||||
try {
|
||||
val geom = areaAssetLoader.loadCollisionGeometry(episode, areaVariant)
|
||||
geom.setEnabled(true)
|
||||
currentGeometry = geom
|
||||
renderer.collisionGeometry?.setEnabled(false)
|
||||
renderer.collisionGeometry = geom
|
||||
} catch (e: Exception) {
|
||||
logger.error(e) {
|
||||
"Couldn't load models for area ${areaVariant.area.id}, variant ${areaVariant.id}."
|
||||
|
@ -11,7 +11,6 @@ import world.phantasmal.web.questEditor.models.QuestEntityModel
|
||||
import world.phantasmal.web.questEditor.models.QuestNpcModel
|
||||
import world.phantasmal.web.questEditor.models.QuestObjectModel
|
||||
import world.phantasmal.web.questEditor.models.WaveModel
|
||||
import world.phantasmal.web.questEditor.rendering.conversion.EntityMetadata
|
||||
import world.phantasmal.web.questEditor.stores.QuestEditorStore
|
||||
import world.phantasmal.webui.DisposableContainer
|
||||
|
||||
|
@ -1,5 +1,7 @@
|
||||
package world.phantasmal.web.questEditor.rendering.conversion
|
||||
package world.phantasmal.web.questEditor.rendering
|
||||
|
||||
import world.phantasmal.web.questEditor.models.QuestEntityModel
|
||||
|
||||
class EntityMetadata(val entity: QuestEntityModel<*, *>)
|
||||
|
||||
class CollisionMetadata
|
@ -21,14 +21,14 @@ import world.phantasmal.web.questEditor.stores.QuestEditorStore
|
||||
abstract class QuestMeshManager protected constructor(
|
||||
private val scope: CoroutineScope,
|
||||
questEditorStore: QuestEditorStore,
|
||||
renderer: QuestRenderer,
|
||||
private val renderer: QuestRenderer,
|
||||
areaAssetLoader: AreaAssetLoader,
|
||||
entityAssetLoader: EntityAssetLoader,
|
||||
) : TrackedDisposable() {
|
||||
protected val disposer = Disposer()
|
||||
|
||||
private val areaDisposer = disposer.add(Disposer())
|
||||
private val areaMeshManager = AreaMeshManager(areaAssetLoader)
|
||||
private val areaMeshManager = AreaMeshManager(renderer, areaAssetLoader)
|
||||
private val npcMeshManager = disposer.add(
|
||||
EntityMeshManager(scope, questEditorStore, renderer, entityAssetLoader)
|
||||
)
|
||||
@ -51,6 +51,8 @@ abstract class QuestMeshManager protected constructor(
|
||||
npcMeshManager.removeAll()
|
||||
objectMeshManager.removeAll()
|
||||
|
||||
renderer.resetCamera()
|
||||
|
||||
// Load area model.
|
||||
areaMeshManager.load(episode, areaVariant)
|
||||
|
||||
|
@ -4,12 +4,16 @@ import org.w3c.dom.HTMLCanvasElement
|
||||
import world.phantasmal.web.core.rendering.Renderer
|
||||
import world.phantasmal.web.externals.babylon.ArcRotateCamera
|
||||
import world.phantasmal.web.externals.babylon.Engine
|
||||
import world.phantasmal.web.externals.babylon.TransformNode
|
||||
import world.phantasmal.web.externals.babylon.Vector3
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.max
|
||||
|
||||
class QuestRenderer(canvas: HTMLCanvasElement, engine: Engine) : Renderer(canvas, engine) {
|
||||
override val camera = ArcRotateCamera("Camera", PI / 2, PI / 6, 500.0, Vector3.Zero(), scene)
|
||||
|
||||
var collisionGeometry: TransformNode? = null
|
||||
|
||||
init {
|
||||
with(camera) {
|
||||
attachControl(
|
||||
@ -21,11 +25,38 @@ class QuestRenderer(canvas: HTMLCanvasElement, engine: Engine) : Renderer(canvas
|
||||
inertia = 0.0
|
||||
angularSensibilityX = 200.0
|
||||
angularSensibilityY = 200.0
|
||||
// Set lowerBetaLimit to avoid shitty camera implementation from breaking completely
|
||||
// when looking directly down.
|
||||
lowerBetaLimit = 0.4
|
||||
panningInertia = 0.0
|
||||
panningSensibility = 3.0
|
||||
panningAxis = Vector3(1.0, 0.0, -1.0)
|
||||
pinchDeltaPercentage = 0.1
|
||||
wheelDeltaPercentage = 0.1
|
||||
|
||||
updatePanningSensibility()
|
||||
onViewMatrixChangedObservable.add({ _, _ ->
|
||||
updatePanningSensibility()
|
||||
})
|
||||
|
||||
camera.storeState()
|
||||
}
|
||||
}
|
||||
|
||||
fun resetCamera() {
|
||||
camera.restoreState()
|
||||
}
|
||||
|
||||
override fun render() {
|
||||
camera.minZ = max(0.01, camera.radius / 100)
|
||||
camera.maxZ = max(2_000.0, 10 * camera.radius)
|
||||
super.render()
|
||||
}
|
||||
|
||||
/**
|
||||
* Make "panningSensibility" an inverse function of radius to make panning work "sensibly"
|
||||
* at all distances.
|
||||
*/
|
||||
private fun updatePanningSensibility() {
|
||||
camera.panningSensibility = 1_000 / camera.radius
|
||||
}
|
||||
}
|
||||
|
@ -2,21 +2,20 @@ package world.phantasmal.web.questEditor.rendering
|
||||
|
||||
import kotlinx.browser.document
|
||||
import mu.KotlinLogging
|
||||
import org.w3c.dom.events.Event
|
||||
import org.w3c.dom.pointerevents.PointerEvent
|
||||
import world.phantasmal.web.core.minusAssign
|
||||
import world.phantasmal.web.externals.babylon.AbstractMesh
|
||||
import world.phantasmal.web.externals.babylon.Vector2
|
||||
import world.phantasmal.web.externals.babylon.Vector3
|
||||
import world.phantasmal.core.disposable.Disposable
|
||||
import world.phantasmal.web.core.minus
|
||||
import world.phantasmal.web.externals.babylon.*
|
||||
import world.phantasmal.web.questEditor.models.QuestEntityModel
|
||||
import world.phantasmal.web.questEditor.rendering.conversion.EntityMetadata
|
||||
import world.phantasmal.web.questEditor.stores.QuestEditorStore
|
||||
import world.phantasmal.webui.DisposableContainer
|
||||
import world.phantasmal.webui.dom.disposableListener
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
class EntityManipulator(
|
||||
private val DOWN_VECTOR = Vector3.Down()
|
||||
|
||||
class UserInputManager(
|
||||
private val questEditorStore: QuestEditorStore,
|
||||
private val renderer: QuestRenderer,
|
||||
) : DisposableContainer() {
|
||||
@ -24,12 +23,13 @@ class EntityManipulator(
|
||||
private val lastPointerPosition = Vector2.Zero()
|
||||
private var movedSinceLastPointerDown = false
|
||||
private var state: State
|
||||
private var onPointerUpListener: Disposable? = null
|
||||
|
||||
/**
|
||||
* Whether entity transformations, deletions, etc. are enabled or not.
|
||||
* Hover over and selection still work when this is set to false.
|
||||
*/
|
||||
var enabled: Boolean = true
|
||||
var entityManipulationEnabled: Boolean = true
|
||||
set(enabled) {
|
||||
field = enabled
|
||||
state.cancel()
|
||||
@ -37,7 +37,7 @@ class EntityManipulator(
|
||||
}
|
||||
|
||||
init {
|
||||
state = IdleState(questEditorStore, renderer, enabled)
|
||||
state = IdleState(questEditorStore, renderer, entityManipulationEnabled)
|
||||
|
||||
observe(questEditorStore.selectedEntity) { state.cancel() }
|
||||
|
||||
@ -54,12 +54,11 @@ class EntityManipulator(
|
||||
movedSinceLastPointerDown
|
||||
))
|
||||
|
||||
document.addEventListener("pointerup", ::onPointerUp)
|
||||
onPointerUpListener = disposableListener(document, "pointerup", ::onPointerUp)
|
||||
}
|
||||
|
||||
private fun onPointerUp(e: Event) {
|
||||
private fun onPointerUp(e: PointerEvent) {
|
||||
try {
|
||||
e as PointerEvent
|
||||
processPointerEvent(e)
|
||||
|
||||
state = state.processEvent(PointerUpEvt(
|
||||
@ -67,7 +66,8 @@ class EntityManipulator(
|
||||
movedSinceLastPointerDown
|
||||
))
|
||||
} finally {
|
||||
document.removeEventListener("pointerup", ::onPointerUp)
|
||||
onPointerUpListener?.dispose()
|
||||
onPointerUpListener = null
|
||||
}
|
||||
}
|
||||
|
||||
@ -112,16 +112,16 @@ private class Pick(
|
||||
val mesh: AbstractMesh,
|
||||
|
||||
/**
|
||||
* Vector that points from the grabbing point (somewhere on the model's surface) to the model's
|
||||
* origin.
|
||||
* Vector that points from the grabbing point (somewhere on the model's surface) to the entity's
|
||||
* position.
|
||||
*/
|
||||
val grabOffset: Vector3,
|
||||
|
||||
/**
|
||||
* Vector that points from the grabbing point to the terrain point directly under the model's
|
||||
* origin.
|
||||
* Vector that points from the grabbing point to the terrain point directly under the entity's
|
||||
* position.
|
||||
*/
|
||||
// val dragAdjust: Vector3,
|
||||
val dragAdjust: Vector3,
|
||||
)
|
||||
|
||||
private abstract class State {
|
||||
@ -141,7 +141,7 @@ private abstract class State {
|
||||
private class IdleState(
|
||||
private val questEditorStore: QuestEditorStore,
|
||||
private val renderer: QuestRenderer,
|
||||
private val enabled: Boolean,
|
||||
private val entityManipulationEnabled: Boolean,
|
||||
) : State() {
|
||||
override fun processEvent(event: Evt): State =
|
||||
when (event) {
|
||||
@ -151,14 +151,14 @@ private class IdleState(
|
||||
1 -> {
|
||||
questEditorStore.setSelectedEntity(pick.entity)
|
||||
|
||||
if (enabled) {
|
||||
if (entityManipulationEnabled) {
|
||||
// TODO: Enter TranslationState.
|
||||
}
|
||||
}
|
||||
2 -> {
|
||||
questEditorStore.setSelectedEntity(pick.entity)
|
||||
|
||||
if (enabled) {
|
||||
if (entityManipulationEnabled) {
|
||||
// TODO: Enter RotationState.
|
||||
}
|
||||
}
|
||||
@ -169,6 +169,8 @@ private class IdleState(
|
||||
}
|
||||
|
||||
is PointerUpEvt -> {
|
||||
updateCameraTarget()
|
||||
|
||||
// If the user clicks on nothing, deselect the currently selected entity.
|
||||
if (!event.movedSinceLastPointerDown && pickEntity() == null) {
|
||||
questEditorStore.setSelectedEntity(null)
|
||||
@ -182,6 +184,14 @@ private class IdleState(
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
private fun updateCameraTarget() {
|
||||
// If the user moved the camera, try setting the camera
|
||||
// target to a better point.
|
||||
pickGround()?.pickedPoint?.let { newTarget ->
|
||||
renderer.camera.target = newTarget
|
||||
}
|
||||
}
|
||||
|
||||
private fun pickEntity(): Pick? {
|
||||
// Find the nearest object and NPC under the pointer.
|
||||
val pickInfo = renderer.scene.pick(renderer.scene.pointerX, renderer.scene.pointerY)
|
||||
@ -189,27 +199,49 @@ private class IdleState(
|
||||
|
||||
val entity = (pickInfo.pickedMesh.metadata as? EntityMetadata)?.entity
|
||||
?: return null
|
||||
val grabOffset = pickInfo.pickedMesh.position.clone()
|
||||
grabOffset -= pickInfo.pickedPoint!!
|
||||
|
||||
// TODO: dragAdjust.
|
||||
// val dragAdjust = grabOffset.clone()
|
||||
//
|
||||
// // Find vertical distance to the ground.
|
||||
// raycaster.set(intersection.object.position, DOWN_VECTOR)
|
||||
// val [collision_geom_intersection] = raycaster.intersectObjects(
|
||||
// this.renderer.collision_geometry.children,
|
||||
// true,
|
||||
// )
|
||||
//
|
||||
// if (collision_geom_intersection) {
|
||||
// dragAdjust.y -= collision_geom_intersection.distance
|
||||
// }
|
||||
// Vector from the point where we grab the entity to its position.
|
||||
val grabOffset = pickInfo.pickedMesh.position - pickInfo.pickedPoint!!
|
||||
|
||||
// Vector from the point where we grab the entity to the point on the ground right beneath
|
||||
// its position. The same as grabOffset when an entity is standing on the ground.
|
||||
val dragAdjust = grabOffset.clone()
|
||||
|
||||
// Find vertical distance to the ground.
|
||||
renderer.scene.pickWithRay(
|
||||
Ray(pickInfo.pickedMesh.position, DOWN_VECTOR),
|
||||
{ it.metadata is CollisionMetadata },
|
||||
)?.let { groundPick ->
|
||||
dragAdjust.y -= groundPick.distance
|
||||
}
|
||||
|
||||
return Pick(
|
||||
entity,
|
||||
pickInfo.pickedMesh,
|
||||
grabOffset,
|
||||
dragAdjust,
|
||||
)
|
||||
}
|
||||
|
||||
private fun pickGround(): PickingInfo? {
|
||||
renderer.scene.multiPick(
|
||||
renderer.engine.getRenderWidth() / 2,
|
||||
renderer.engine.getRenderHeight() / 2,
|
||||
{ it.metadata is CollisionMetadata },
|
||||
renderer.camera,
|
||||
)?.let { pickingInfoArray ->
|
||||
// Don't allow entities to be placed on very steep terrain.
|
||||
// E.g. walls.
|
||||
// TODO: make use of the flags field in the collision data.
|
||||
for (pickingInfo in pickingInfoArray) {
|
||||
pickingInfo.getNormal()?.let { n ->
|
||||
if (n.y > 0.75) {
|
||||
return pickingInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
@ -39,7 +39,7 @@ class QuestEditorToolbarWidget(
|
||||
disabled = ctrl.areaSelectDisabled,
|
||||
itemsVal = ctrl.areas,
|
||||
itemToString = { it.label },
|
||||
selectedVal = ctrl.currentArea,
|
||||
selectedVal = ctrl.currentArea,
|
||||
onSelect = ctrl::setCurrentArea
|
||||
)
|
||||
)
|
||||
|
@ -6,6 +6,7 @@ import org.w3c.dom.*
|
||||
import org.w3c.dom.events.Event
|
||||
import org.w3c.dom.events.KeyboardEvent
|
||||
import org.w3c.dom.events.MouseEvent
|
||||
import world.phantasmal.core.disposable.Disposable
|
||||
import world.phantasmal.observable.value.Val
|
||||
import world.phantasmal.observable.value.falseVal
|
||||
import world.phantasmal.observable.value.value
|
||||
@ -34,6 +35,7 @@ class Menu<T : Any>(
|
||||
private var highlightedIndex: Int? = null
|
||||
private var highlightedElement: Element? = null
|
||||
private var previouslyFocusedElement: Element? = null
|
||||
private var onDocumentMouseDownListener: Disposable? = null
|
||||
|
||||
override fun Node.createElement() =
|
||||
div {
|
||||
@ -57,12 +59,14 @@ class Menu<T : Any>(
|
||||
|
||||
observe(this@Menu.hidden) {
|
||||
if (it) {
|
||||
document.removeEventListener("mousedown", ::onDocumentMouseDown)
|
||||
onDocumentMouseDownListener?.dispose()
|
||||
onDocumentMouseDownListener = null
|
||||
clearHighlightItem()
|
||||
|
||||
(previouslyFocusedElement as HTMLElement?)?.focus()
|
||||
} else {
|
||||
document.addEventListener("mousedown", ::onDocumentMouseDown)
|
||||
onDocumentMouseDownListener =
|
||||
disposableListener(document, "mousedown", ::onDocumentMouseDown)
|
||||
}
|
||||
}
|
||||
|
||||
@ -76,7 +80,7 @@ class Menu<T : Any>(
|
||||
}
|
||||
|
||||
override fun internalDispose() {
|
||||
document.removeEventListener("mousedown", ::onDocumentMouseDown)
|
||||
onDocumentMouseDownListener?.dispose()
|
||||
super.internalDispose()
|
||||
}
|
||||
|
||||
|
@ -52,15 +52,15 @@ class Toolbar(
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
border-bottom: var(--pw-border);
|
||||
padding: 0 2px;
|
||||
padding: 3px 2px;
|
||||
}
|
||||
|
||||
.pw-toolbar > * {
|
||||
margin: 2px 1px;
|
||||
margin: 0 1px;
|
||||
}
|
||||
|
||||
.pw-toolbar > .pw-toolbar-group {
|
||||
margin: 2px 3px;
|
||||
margin: 0 3px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
Loading…
Reference in New Issue
Block a user