Skip to content

Instantly share code, notes, and snippets.

@nhrones
Last active January 1, 2024 20:53
Show Gist options
  • Save nhrones/7e3e6d5ceebfc78a4edf0dca6ebd253e to your computer and use it in GitHub Desktop.
Save nhrones/7e3e6d5ceebfc78a4edf0dca6ebd253e to your computer and use it in GitHub Desktop.
DWM=ReactiveUI system events
import { activeNodes } from '../render/activeNodes.ts'
import { Host, ctx, dwmWindow, hasVisiblePopup } from '../render/renderContext.ts'
import type { View } from '../types.ts';
import { events } from './eventBus.ts'
//====================================================
// Sytem Events Module
// Watch for all host window generated events.
// When appropriate, propagate these host events
// to our central eventBus - events.
//
// When an event targets an active node, based on
// the event type, set the active nodes state to
// either hovered, or to focused, and reference
// that node as the hoveredNode or the focusedNode.
//====================================================
const left = 0
// values re-used repeatedly in event handlers
// -- we reuse these to reduce pressure on GC
let x = 0
let y = 0
let hit = false
let node: View | null = null
let hoveredNode: View | null = null
let focusedNode: View | null = null
/**
* Initialize an environment for mouse/touch event handlers.
*
* Registers event handlers for:
* WindowInputEvent
* WindowKeyboardEvent
* mousedown + touchstart => handleClickOrTouch()
* mousemove => handleMouseMove
*/
export function initHostEvents( ): void {
// handle all host window `input` events
addEventListener("input", (evt: any) => {
// look for a focused node, if none, just ignore the event
if (focusedNode !== null) {
// we'll fire this event directly to amy focused node
events.fire('WindowInput', focusedNode.name, evt)
}
})
// handler for `keydown` -- enter backspace, delete, etc.
addEventListener('keydown', (evt: any) => {
let focusNum = 0
// handle Tab key
if (evt.code === 'Tab') {
if (focusedNode !== null) {
const direction = (evt.shiftKey) ? -1 : +1
focusNum = focusNext(focusedNode.tabOrder + direction, evt.shiftKey)
} else {
focusNum = focusNext(1, evt.shiftKey) // focus first
}
if (focusNum === 0) { // not found
const last = (evt.shiftKey) ? 20 : 1
focusNext(last, evt.shiftKey) // focus first
}
return
}
// handle Enter key
if (evt.code === 'Enter') {
if (hasVisiblePopup === true) {
events.fire(`PopupReset`, "", null)
} else if (focusedNode !== null) {
focusedNode.touched()
}
}
// look for a currently `focused` node
if (focusedNode !== null) {
// we'll signal only the node with focus
events.fire('WindowKeyDown', focusedNode.name, evt)
}
})
// register a handler for our host mousedown event
addEventListener('mousedown', (evt) => {
evt.preventDefault()
if (evt.button === left) {
if (hasVisiblePopup === false) {
// we'll hit-test all activeNodes
handleClickOrTouch(evt.pageX, evt.pageY)
} // a popup iwas open, just close it
else {
events.fire(`PopupReset`, "", null)
}
}
}, false)
// register a handler for our hosts mousemove event
addEventListener('mousemove', (evt) => {
evt.preventDefault()
// If a popup is open, don't bother with hover testing!
if (hasVisiblePopup === false) {
handleMouseMove(evt)
}
})
// we send all scroll events unconditionally
// to service any scrollable containers
addEventListener('scroll', (evt) => {
evt.preventDefault();
const y = (Math.sign(evt.scrollY));
events.fire('Scroll', "", { deltaY: y })
});
}
//=====================================================
// custom event handlers
//=====================================================
/**
* Handles canvas mouse-move event.
* Provides logic to emulate 'onmouseenter', and
* 'onmouseleave' DOM events on our virtual elements.
* Uses the canvasContexts 'isPointInPath' method for hit-testing.
* @param {MouseEvent} evt - from canvas.mousemove event
*/
function handleMouseMove(evt: any,) {
x = evt.clientX
y = evt.clientY
// test for hovered
node = null
for (const n of activeNodes) {
if (ctx.isPointInPath(n.path, x, y)) {
// going from bottom to top, top-most object wins
node = n
}
}
if (node !== null) { // did we get a hit?
if (node !== hoveredNode) { // hit was not the current hovered node
clearHovered() // clear any prior hover
node.hovered = true // set this nodes `hovered` flag
node.update() // command to update the hovered node
hoveredNode = node // register this node as currently hovered
dwmWindow.setCursor("hand") // change the cursor
Host.dirty() // force a flush and swap
}
} else { // no node was hit
if (hoveredNode !== null) { // is there a hovered node?
clearHovered() // remove hovered state
hoveredNode = null // no node currently hover
}
}
}
/**
* Handler for both, canvas-mouse-Click and canvas-Touch events.
* Uses the canvasContexts 'isPointInPath' method for hit-testing.
*
* If a hit is detected, we directly call the elements touched() method.
* When called, the elements `touched()` method will then broadcast
* a `touched` event to any registered subscribers.
*
* @param {number} x - horizontal location of this event
* @param {number} y - vertical location of this event
*/
function handleClickOrTouch(x: number, y: number) {
hit = false
for (const node of activeNodes) {
if (!hit) { // short circuit once we get a hit
// check each node (bottom to top), top-most object wins
if (ctx.isPointInPath(node.path, x, y)) {
// got one, call the nodes touched method
node.touched()
// clear any currently focused view
clearFocused()
// set this as the currently focused node
focusedNode = node
// tell others about this newly focused node
if (focusedNode)
events.fire('Focused', focusedNode.name, true);
hit = true
}
}
}
// nothing touched - clear the currently focused node
if (!hit) clearFocused()
}
/** clear last focused object */
function clearFocused() {
if (focusedNode !== null) {
focusedNode.focused = false;
focusedNode.hovered = false
events.fire('Focused', focusedNode.name, focusedNode.focused);
focusedNode.update(); // re-render the node
}
}
/** clear last hovered object */
function clearHovered() {
//dwmWindow.setCursor("arrow")
if (hoveredNode !== null) {
hoveredNode.hovered = false
hoveredNode.update() // re-render the node
}
}
/** change focus to this tabOrder */
function focusNext(target: number, _shift: boolean) {
hit = false
for (const node of activeNodes) {
if (hit === false) { // short circuit once we get a hit
if (node.tabOrder === target) {
// clear any currently focused view
clearFocused()
clearHovered()
// set this one as focused
focusedNode = node
if (focusedNode) {
focusedNode.focused = true;
focusedNode.hovered = true
// got one, call the nodes update method
focusedNode.update()
// tell others about this newly focused node
events.fire('Focused', focusedNode.name, true)
}
hit = true
}
}
}
return (hit === false) ? 0 : target
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment