Skip to content

Commit ce85b4b

Browse files
Merge pull request #616 from lightning-js/dev
Release 1.50.0
2 parents b55b3ab + 8665b67 commit ce85b4b

26 files changed

Lines changed: 1048 additions & 244 deletions

‎CHANGELOG.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# Changelog
22

3+
## v1.50.0
4+
5+
_27 feb 2026_
6+
7+
- Fixed issue with router navigation options not properly being overriden
8+
- Fixed FPS counter component and made it into a separate importable component (`import { FPScounter } from @lightningjs/blits/components`)
9+
- Added mouse (magic wand) support (including new `hover` lifecycle hook and `isHovered` state indicator)
10+
- Updated template attributes for VScode extension
11+
312

413
## v1.49.0
514

‎docs/components/user_input.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ In order to allow users to interact with your app, you will want to capture and
44

55
Blits offers an intuitive and straightforward interface to handle key input in Components.
66

7+
**Mouse and pointer:** To enable mouse support (hover state and click-to-focus on the canvas), set `enableMouse: true` in the settings object passed to `Blits.Launch()`. When enabled, pointer movement updates component hover state and a click focuses the component under the cursor and dispatches Enter key input.
8+
79
## Focus
810

911
Before diving into the specifics of key handling, it is important to understand the basic concept of _focus_.

‎docs/essentials/settings.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ Example font object:
6363
| `keymap` | `object` | Custom key mapping for input events |
6464
| `holdTimeout` | `number` | Time (ms) to consider a key press as hold |
6565
| `inputThrottle`| `number` | Input throttle time (ms) to prevent rapid successive inputs |
66+
| `enableMouse` | `boolean` | Enable mouse support (hover and click-to-focus). Defaults to false. |
6667

6768
## Renderer
6869

@@ -116,5 +117,6 @@ Blits.Launch(App, 'app', {
116117
},
117118
inspector: false,
118119
announcer: true,
120+
enableMouse: false, // set true for hover + click-to-focus on canvas
119121
})
120122
```

‎index.d.ts‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,8 @@ declare module '@lightningjs/blits' {
170170
* Fires when the Component is being destroyed and removed.
171171
*/
172172
destroy?: () => void;
173+
hover?: () => void;
174+
unhover?: () => void;
173175
/**
174176
* Fires upon each frame start (allowing you to tap directly into the renderloop)
175177
*
@@ -394,6 +396,13 @@ declare module '@lightningjs/blits' {
394396
*/
395397
hasFocus: boolean,
396398

399+
/**
400+
* Indicates whether the component currently is hovered
401+
*
402+
* @returns Boolean
403+
*/
404+
isHovered: boolean,
405+
397406
/**
398407
* Listen to events emitted by other components
399408
*/
@@ -1253,6 +1262,16 @@ declare module '@lightningjs/blits' {
12531262
* ```
12541263
*/
12551264
announcerOptions?: AnnouncerUtteranceOptions,
1265+
/**
1266+
* Enable mouse support (hover and click-to-focus).
1267+
*
1268+
* When set to `true`, pointer movement over the canvas updates hover state on components
1269+
* and click dispatches focus and Enter key input to the component under the cursor.
1270+
* When set to `false`, no mouse or pointer listeners are registered.
1271+
*
1272+
* @default false
1273+
*/
1274+
enableMouse?: boolean,
12561275
/**
12571276
* Maximum FPS at which the App will be rendered
12581277
*

‎package-lock.json‎

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@lightningjs/blits",
3-
"version": "1.49.0",
3+
"version": "1.50.0",
44
"description": "Blits: The Lightning 3 App Development Framework",
55
"bin": "bin/index.js",
66
"exports": {
@@ -29,7 +29,8 @@
2929
"default": "./src/plugins/storage/storage.js"
3030
},
3131
"./symbols": "./src/lib/symbols.js",
32-
"./blitsFileConverter": "./src/lib/blitsfileconverter/blitsfileconverter.js"
32+
"./blitsFileConverter": "./src/lib/blitsfileconverter/blitsfileconverter.js",
33+
"./components": "./src/components/FPScounter.js"
3334
},
3435
"scripts": {
3536
"test": "c8 node scripts/runTests.js",

‎src/application.js‎

Lines changed: 95 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@
1515
* SPDX-License-Identifier: Apache-2.0
1616
*/
1717

18-
import Component from './component.js'
19-
import { default as Focus, keyUpCallbacks } from './focus.js'
20-
import Settings from './settings.js'
18+
import { default as Component, componentMap } from './component.js'
19+
import { default as Focus, keyUpCallbacks } from './focus/focus.js'
20+
import Hover from './focus/hover.js'
2121

22+
import Settings from './settings.js'
2223
import symbols from './lib/symbols.js'
2324
import { DEFAULT_HOLD_TIMEOUT_MS, DEFAULT_KEYMAP } from './constants.js'
25+
import { renderer } from './launch.js'
2426

2527
/**
2628
* Merged keyMap (default + custom settings).
@@ -34,13 +36,24 @@ const Application = (config) => {
3436

3537
let keyDownHandler
3638
let keyUpHandler
39+
let mouseMoveHandler
40+
let mouseClickHandler
41+
let updateCanvasRect
3742
let holdTimeout
3843
let lastInputTime = 0
3944
let lastInputKey = null
45+
let mouseListenersAdded = false
4046

4147
config.hooks[symbols.destroy] = function () {
4248
document.removeEventListener('keydown', keyDownHandler)
4349
document.removeEventListener('keyup', keyUpHandler)
50+
if (mouseListenersAdded) {
51+
document.removeEventListener('mousemove', mouseMoveHandler)
52+
document.removeEventListener('click', mouseClickHandler)
53+
window.removeEventListener('resize', updateCanvasRect)
54+
window.removeEventListener('scroll', updateCanvasRect)
55+
Hover.clear()
56+
}
4457
}
4558

4659
config.hooks[symbols.init] = function () {
@@ -57,6 +70,8 @@ const Application = (config) => {
5770
/** @type {number} Input throttle time in milliseconds (0 = disabled) */
5871
const throttleMs = Settings.get('inputThrottle', 0)
5972

73+
const mouseEnabled = Settings.get('enableMouse', false)
74+
6075
keyDownHandler = async (e) => {
6176
const currentTime = performance.now()
6277

@@ -89,6 +104,7 @@ const Application = (config) => {
89104
}
90105

91106
Focus.input(key, e)
107+
if (mouseEnabled === true) Hover.clear()
92108
clearTimeout(holdTimeout)
93109
holdTimeout = setTimeout(
94110
() => {
@@ -108,8 +124,84 @@ const Application = (config) => {
108124
Focus.hold = false
109125
}
110126

127+
let lastMoved = 0
128+
let currentNode = undefined
129+
let currentComponent = undefined
130+
let canvasRect = null
131+
132+
updateCanvasRect = () => {
133+
if (renderer.canvas !== undefined) {
134+
canvasRect = renderer.canvas.getBoundingClientRect()
135+
}
136+
}
137+
138+
// limit the amount of move events per time frame
139+
mouseMoveHandler = (e) => {
140+
if (e.timeStamp - lastMoved < 100) return
141+
lastMoved = e.timeStamp
142+
143+
this.$emit('mouse::move', e)
144+
145+
if (canvasRect === null && renderer.canvas != null) {
146+
updateCanvasRect()
147+
}
148+
if (canvasRect === null) {
149+
return
150+
}
151+
152+
const stage = renderer.stage
153+
if (stage == null) {
154+
return
155+
}
156+
157+
// Convert viewport → canvas display coordinates
158+
const x = e.clientX - canvasRect.left
159+
const y = e.clientY - canvasRect.top
160+
161+
const node = stage.getNodeFromPosition({ x, y })
162+
163+
if (node === null) {
164+
currentComponent = undefined
165+
Hover.clear()
166+
return
167+
}
168+
if (node === currentNode) return
169+
170+
currentNode = node
171+
currentComponent = componentMap.get(currentNode)
172+
173+
if (currentComponent === undefined) {
174+
Hover.clear()
175+
return
176+
}
177+
Hover.set(currentComponent)
178+
}
179+
180+
mouseClickHandler = () => {
181+
if (currentComponent === undefined || currentComponent.eol === true) return
182+
183+
const e = new KeyboardEvent('keydown', {
184+
key: 'Enter',
185+
code: 'Enter',
186+
keyCode: 13,
187+
bubbles: true,
188+
cancelable: true,
189+
composed: true,
190+
})
191+
currentComponent.$focus()
192+
currentComponent.$input(e)
193+
}
194+
111195
document.addEventListener('keydown', keyDownHandler)
112196
document.addEventListener('keyup', keyUpHandler)
197+
if (mouseEnabled === true) {
198+
updateCanvasRect()
199+
document.addEventListener('mousemove', mouseMoveHandler)
200+
document.addEventListener('click', mouseClickHandler)
201+
window.addEventListener('resize', updateCanvasRect)
202+
window.addEventListener('scroll', updateCanvasRect)
203+
mouseListenersAdded = true
204+
}
113205

114206
// next tick
115207
setTimeout(() => Focus.set(this))

‎src/component.js‎

Lines changed: 35 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ const required = (name) => {
4343
throw new Error(`Parameter ${name} is required`)
4444
}
4545

46+
// map that holds component to their holder node
47+
export const componentMap = new WeakMap()
48+
4649
/**
4750
* @typedef {function} BlitsComponentFactory
4851
* @param {object} opts - The options for the component instance
@@ -216,6 +219,10 @@ const Component = (name = required('name'), config = required('config')) => {
216219
// such as rotation and scale to components)
217220
this[symbols.holder] = parentEl
218221

222+
if (parentEl !== undefined && parentEl.node !== undefined) {
223+
componentMap.set(parentEl.node, this)
224+
}
225+
219226
// generate an internal id (simple counter)
220227
this[symbols.id] = createInternalId()
221228

@@ -238,6 +245,9 @@ const Component = (name = required('name'), config = required('config')) => {
238245
// add hasFocus key in
239246
this[symbols.originalState]['hasFocus'] = false
240247

248+
// add isHovered key in
249+
this[symbols.originalState]['isHovered'] = false
250+
241251
// generate a reactive state (using the result of previously execute state function)
242252
// and store it
243253
this[symbols.state] = reactive(this[symbols.originalState], Settings.get('reactivityMode'))
@@ -291,38 +301,35 @@ const Component = (name = required('name'), config = required('config')) => {
291301

292302
// register hooks if component has hooks specified
293303
if (config.hooks) {
294-
// push to next tick to ensure
295-
this.$setTimeout(() => {
296-
// frame tick event
297-
if (config.hooks.frameTick) {
298-
const cb = (_r, data) => emit('frameTick', this[symbols.identifier], this, [data])
299-
renderer.on('frameTick', cb)
300-
this[symbols.rendererEventListeners].push({ event: 'frameTick', cb })
301-
}
304+
// frame tick event
305+
if (config.hooks.frameTick) {
306+
const cb = (_r, data) => emit('frameTick', this[symbols.identifier], this, [data])
307+
renderer.on('frameTick', cb)
308+
this[symbols.rendererEventListeners].push({ event: 'frameTick', cb })
309+
}
302310

303-
// idle event
304-
if (config.hooks.idle) {
305-
const idleCb = () => {
306-
emit('idle', this[symbols.identifier], this, [true])
307-
}
308-
const activeCb = () => {
309-
emit('idle', this[symbols.identifier], this, [false])
310-
}
311-
renderer.on('idle', idleCb)
312-
renderer.on('active', activeCb)
313-
this[symbols.rendererEventListeners].push({ event: 'idle', cb: idleCb })
314-
this[symbols.rendererEventListeners].push({ event: 'active', cb: activeCb })
311+
// idle event
312+
if (config.hooks.idle) {
313+
const idleCb = () => {
314+
emit('idle', this[symbols.identifier], this, [true])
315315
}
316+
const activeCb = () => {
317+
emit('idle', this[symbols.identifier], this, [false])
318+
}
319+
renderer.on('idle', idleCb)
320+
renderer.on('active', activeCb)
321+
this[symbols.rendererEventListeners].push({ event: 'idle', cb: idleCb })
322+
this[symbols.rendererEventListeners].push({ event: 'active', cb: activeCb })
323+
}
316324

317-
// fpsUpdate event
318-
if (config.hooks.fpsUpdate) {
319-
const cb = (_r, data) => {
320-
emit('fpsUpdate', this[symbols.identifier], this, [data.fps])
321-
}
322-
renderer.on('fpsUpdate', cb)
323-
this[symbols.rendererEventListeners].push({ event: 'fpsUpdate', cb })
325+
// fpsUpdate event
326+
if (config.hooks.fpsUpdate) {
327+
const cb = (_r, data) => {
328+
emit('fpsUpdate', this[symbols.identifier], this, [data.fps])
324329
}
325-
})
330+
renderer.on('fpsUpdate', cb)
331+
this[symbols.rendererEventListeners].push({ event: 'fpsUpdate', cb })
332+
}
326333

327334
// inBounds event emiting a lifecycle attach event
328335
if (config.hooks.attach) {

‎src/component/base/methods.js‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@
1616
*/
1717

1818
import symbols from '../../lib/symbols.js'
19-
import Focus from '../../focus.js'
20-
import { keyUpCallbacks, getComponentWithInputEvent } from '../../focus.js'
19+
import { default as Focus, keyUpCallbacks, getComponentWithInputEvent } from '../../focus/focus.js'
2120
import eventListeners from '../../lib/eventListeners.js'
2221
import { trigger } from '../../lib/reactivity/effect.js'
2322
import { Log } from '../../lib/log.js'

‎src/component/setup/state.js‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ export default (component, state = () => {}) => {
3636
// add built-in hasFocus key
3737
stateKeys.push(['hasFocus'])
3838
}
39+
// add built-in isHovered key
40+
stateKeys.push(['isHovered'])
3941
const stateKeysLength = stateKeys.length
4042

4143
for (let i = 0; i < stateKeysLength; i++) {

0 commit comments

Comments
 (0)