@lightningjs/blits 1.8.2 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,10 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## v1.9.0
4
+
5
+ _4 nov 2024_
6
+
7
+ - Added Layout component
8
+
9
+ ## v1.8.3
10
+
11
+ _31 oct 2024_
12
+
13
+ - Exported TS interfaces and improved return type of before route hook
14
+ - Fixed test cases for codegenerator
15
+
16
+
3
17
  ## v1.8.2
4
18
 
5
19
  _30 oct 2024_
6
20
 
7
- - Fixed edgecase issue with array based props not triggering reactivity
21
+ - Fixed edge case issue with array based props not triggering reactivity
8
22
 
9
23
 
10
24
  ## v1.8.1
package/docs/_sidebar.md CHANGED
@@ -23,6 +23,7 @@
23
23
  - Built-in components
24
24
  - [Directives](/built-in/directives.md)
25
25
  - [For loop](/built-in/for-loop.md)
26
+ - [Layout](/built-in/layout.md)
26
27
  - Plugins
27
28
  - [Text-to-Speech / Announcer](/plugins/text-to-speech-announcer.md)
28
29
  - [Language](/plugins/language.md)
@@ -0,0 +1,76 @@
1
+ # Layout
2
+
3
+ A big portion of building a pixel perfect App is all about correctly positioning Elements and Components on screen. In Lightning and Blits _absolute_ positions are used. This fits the whole concept of building a TV app where the viewport dimensions are known and fixed. Absolute positioning (versus spacing Elements relative to each other) is also more performant.
4
+
5
+ Imagine a complex design with several nested layers. Relying solely on _relative_ positioning and elements automatically floating as space is available, may lead to heavy and recursive calculations. Then throw some dynamically changing dimensions and animations in the mix as well, and your App may be starting to noticeably drop frames due to expensive re-calculations.
6
+
7
+ That being said, there are often cases where it becomes very cumbersome to calculate all the positions manually. Especially when dimensions are not known in advance and elements may load asynchronously.
8
+
9
+ If you come from a CSS background, you may be tempted to say: "_this is why we have Flexbox!_". And while Flexbox is great and versatile in CSS and running on a powerful device, a full flexbox implementation does come at a cost and the risk of slowing down your App, when applied extensively or incorrectly.
10
+
11
+ In order to address the core problem "_how do I easily position a bunch of Elements and Components, without writing a lot of code with `@loaded` event handlers_", Blits offers the built-in `<Layout>`-component - a basic and performant solution that automatically puts your Elements in the right place.
12
+
13
+ ## How to use
14
+
15
+ Since the Layout-component is a built-in component, there is no need to register it separately. You can use the `<Layout>`-tag anywhere in your templates, just as you would use `<Element>` and `<Text>`.
16
+
17
+ The Layout component is a wrapper component, that encloses a number of children. All the direct children that are wrapped in a `<Layout>`-tag are automatically positioned in order, one next to the other.
18
+
19
+ Whenever the dimensions of a child change, the positioning of the next children is automatically recalculated and re-applied.
20
+
21
+ ```xml
22
+ <Layout>
23
+ <Element w="40" h="40" color="red" />
24
+ <Element w="80" h="40" color="blue" />
25
+ <Text>Hello world</Text>
26
+ <Element w="40" h="40" color="green" />
27
+ </Layout>
28
+ ```
29
+
30
+ ### Horizontal and vertical layout
31
+
32
+ By default the Layout component lays out it's contents _horizontally_. The Layout component accepts a `direction` attribute which allows you to control the direction.
33
+
34
+ In order to align vertically use `<Layout direction="vertical"></Layout>`. And use `<Layout direction="horizontal"></Layout>` to explicitly apply the default horizontal layout.
35
+
36
+ ### Spacing between children
37
+
38
+ By default the Layout-component places each Element directly besides (or below) the previous one. By adding the `gap`-attribute you can control how much space will be added between each Element or Component. The `gap`-attribute accepts a number in pixels.
39
+
40
+ ### Dynamic dimensions
41
+
42
+ For the Layout component to work properly, the direct children all need to have dimensions (i.e `w` and `h` attributes). The exception here being Text elements.
43
+
44
+ Due to the asynchronous nature of rendering text, the final text dimensions are not known beforehand. The width and height of Text elements are automatically set when the text has been loaded. This fires a `@loaded` event, that you can tap into as described [here](../essentials/displaying_text#text-dimensions).
45
+
46
+ The Layout component also uses this event, to execute its calculation and position the text and other elements around it properly.
47
+
48
+ When the children of the Layout-component have _reactive_ dimensions (i.e. `<Element :w="$mywidth" :h="$myheight" />), the Layout component ensures that all child elements are properly re-positioned whenever a dimension changes.
49
+
50
+ ### Nesting Layouts
51
+
52
+ It's also possible to nest layouts. Simply place a new `<Layout>`-tag, with it's own children in between the children of another Layout-component. The Layout-component itself will grow automatically with the dimensions of it's children. In other words, it's not required to specify a width (`w`) or height (`h`) on the `<Layout>`-tag itself.
53
+
54
+ And of course you can nest _vertical_ Layouts inside a _horizontal_ one - and vice versa.
55
+
56
+ ### Transitioning layouts
57
+
58
+ The `<Layout>`-component can also take into account when dimensions of children change with a transition applied to it. The component will recalculate the position of its children as the transition progresses, making sure that elements are always perfectly positioned relative to one another.
59
+
60
+ ## Performance
61
+
62
+ The Layout-component is a very useful utility, that will make it a lot easier to put together complex layouts. It will reduce the code required, it means less hardcoding of values and removes the need to manually wire up `@loaded` events when working with dynamic / asynchronous elements.
63
+
64
+ We've done our best to make the Component as performant as possible, by deliberately keeping it simple and straight forward. But it's important to understand that the Layout component does come at a cost. Depending on the use case these costs may be negligible, but there are cases where the Layout component might prove to be a bottle-neck.
65
+
66
+ The performance costs may always be there to some degree. Whether it's the Layout component that does the complex calculations, or you do it in your custom code. So please feel encouraged to use the `<Layout>`-tag!
67
+
68
+ Some pointers to keep in mind, though:
69
+
70
+ - Every change in the dimensions of a child, triggers the Layout component to recalculate and reposition. If your child elements change frequently, the Layout component may have a performance impact.
71
+
72
+ - The Layout component support transitions, but beware that when transitioning child dimensions a recalculation happens every frame-tick. If you see frame-drops in your App, you may want to see if the `<Layout>`-tag has an impact on this.
73
+
74
+ - Be mindful to not apply unnecessary deep nesting of Layout tags. Each change in a deep child, will trigger recalculations up the tree of Layout tags.
75
+
76
+ - When using the `<Layout>`-tag with a for-loop, it may be more performant to use the `$index` of the for-loop and manually position the elements yourself (i.e. `<Element :for="(item, index) in $items" :$x="$item * 200" w="150" h="150" />`). Especially if the dimensions are know before hand and possibly are the same for each child.
package/docs/sidebar.json CHANGED
@@ -101,6 +101,10 @@
101
101
  {
102
102
  "text": "For loop",
103
103
  "link": "/built-in/for-loop"
104
+ },
105
+ {
106
+ "text": "Layout",
107
+ "link": "/built-in/layout.md"
104
108
  }
105
109
  ]
106
110
  },
package/index.d.ts CHANGED
@@ -19,7 +19,7 @@ import {type ShaderEffect as RendererShaderEffect, type WebGlCoreShader} from '@
19
19
 
20
20
  declare module '@lightningjs/blits' {
21
21
 
22
- interface Hooks {
22
+ export interface Hooks {
23
23
  /**
24
24
  * Fires when the Component is being instantiated
25
25
  * At this moment child elements will not be available yet
@@ -77,7 +77,7 @@ declare module '@lightningjs/blits' {
77
77
  exit?: () => void;
78
78
  }
79
79
 
80
- interface Input {
80
+ export interface Input {
81
81
  [key: string]: (event: KeyboardEvent) => void | undefined,
82
82
  /**
83
83
  * Catch all input function
@@ -88,7 +88,7 @@ declare module '@lightningjs/blits' {
88
88
  any?: (event: KeyboardEvent) => void
89
89
  }
90
90
 
91
- interface Log {
91
+ export interface Log {
92
92
  /**
93
93
  * Log an info message
94
94
  */
@@ -109,16 +109,16 @@ declare module '@lightningjs/blits' {
109
109
 
110
110
 
111
111
 
112
- interface RouteData {
112
+ export interface RouteData {
113
113
  [key: string]: any
114
114
  }
115
115
 
116
116
  // todo: specify valid route options
117
- interface RouteOptions {
117
+ export interface RouteOptions {
118
118
  [key: string]: any
119
119
  }
120
120
 
121
- interface Router {
121
+ export interface Router {
122
122
  /**
123
123
  * Navigate to a different location
124
124
  *
@@ -148,7 +148,7 @@ declare module '@lightningjs/blits' {
148
148
 
149
149
  }
150
150
 
151
- type ComponentBase = {
151
+ export type ComponentBase = {
152
152
  /**
153
153
  * Listen to events emitted by other components
154
154
  */
@@ -246,7 +246,7 @@ declare module '@lightningjs/blits' {
246
246
  /**
247
247
  * Prop object
248
248
  */
249
- type PropObject = {
249
+ export type PropObject = {
250
250
  /**
251
251
  * Name of the prop
252
252
  */
@@ -274,7 +274,7 @@ declare module '@lightningjs/blits' {
274
274
  };
275
275
 
276
276
  // Props Array
277
- type Props = (string | PropObject)[];
277
+ export type Props = (string | PropObject)[];
278
278
 
279
279
  // Extract the prop names from the props array
280
280
  type ExtractPropNames<P extends Props> = {
@@ -282,11 +282,11 @@ declare module '@lightningjs/blits' {
282
282
  };
283
283
 
284
284
  // Update the PropsDefinition to handle props as strings or objects
285
- type PropsDefinition<P extends Props> = ExtractPropNames<P>;
285
+ export type PropsDefinition<P extends Props> = ExtractPropNames<P>;
286
286
 
287
- type ComponentContext<P extends Props, S, M, C> = ThisType<PropsDefinition<P> & S & M & C & ComponentBase>
287
+ export type ComponentContext<P extends Props, S, M, C> = ThisType<PropsDefinition<P> & S & M & C & ComponentBase>
288
288
 
289
- interface ComponentConfig<P extends Props, S, M, C, W> {
289
+ export interface ComponentConfig<P extends Props, S, M, C, W> {
290
290
  components?: {
291
291
  [key: string]: ComponentFactory,
292
292
  },
@@ -365,7 +365,7 @@ declare module '@lightningjs/blits' {
365
365
  watch?: W & ComponentContext<P, S, M, C>
366
366
  }
367
367
 
368
- interface ApplicationConfig<P extends Props, S, M, C, W> extends ComponentConfig<P, S, M, C, W> {
368
+ export interface ApplicationConfig<P extends Props, S, M, C, W> extends ComponentConfig<P, S, M, C, W> {
369
369
  /**
370
370
  * Routes definition
371
371
  *
@@ -383,7 +383,7 @@ declare module '@lightningjs/blits' {
383
383
  }
384
384
 
385
385
 
386
- interface Transition {
386
+ export interface Transition {
387
387
  /**
388
388
  * Name of the prop to transition (i.e. 'x', 'y', 'alpha', 'color')
389
389
  */
@@ -406,7 +406,7 @@ declare module '@lightningjs/blits' {
406
406
  delay?: number
407
407
  }
408
408
 
409
- interface Before {
409
+ export interface Before {
410
410
  /**
411
411
  * Name of the prop to set before the transition starts
412
412
  */
@@ -417,7 +417,7 @@ declare module '@lightningjs/blits' {
417
417
  value: any,
418
418
  }
419
419
 
420
- interface RouteTransition {
420
+ export interface RouteTransition {
421
421
  /**
422
422
  * Setting or Array of Settings before new view enters into the router view
423
423
  */
@@ -432,9 +432,9 @@ declare module '@lightningjs/blits' {
432
432
  out: Transition | Transition[],
433
433
  }
434
434
 
435
- type RouteTransitionFunction = (previousRoute: Route, currentRoute: Route) => RequireAtLeastOne<RouteTransition>
435
+ export type RouteTransitionFunction = (previousRoute: Route, currentRoute: Route) => RequireAtLeastOne<RouteTransition>
436
436
 
437
- interface RouteAnnounce {
437
+ export interface RouteAnnounce {
438
438
  /**
439
439
  * Message to be announced
440
440
  */
@@ -447,15 +447,15 @@ declare module '@lightningjs/blits' {
447
447
  politeness?: 'off' | 'polite' | 'assertive'
448
448
  }
449
449
 
450
- type RequireAtLeastOne<T> = {
450
+ export type RequireAtLeastOne<T> = {
451
451
  [K in keyof T]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<keyof T, K>>>
452
452
  }[keyof T]
453
453
 
454
- interface RouteHooks {
455
- before?: (to: Route, from: Route) => string | Route;
454
+ export interface RouteHooks {
455
+ before?: (to: Route, from: Route) => string | Route | Promise<string | Route>;
456
456
  }
457
457
 
458
- type Route = {
458
+ export type Route = {
459
459
  /**
460
460
  * URI path for the route
461
461
  */
@@ -575,7 +575,7 @@ declare module '@lightningjs/blits' {
575
575
  * Launcher function that sets up the Lightning renderer and instantiates
576
576
  * the Blits App
577
577
  */
578
- interface Settings {
578
+ export interface Settings {
579
579
  /**
580
580
  * Width of the Application
581
581
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightningjs/blits",
3
- "version": "1.8.2",
3
+ "version": "1.9.0",
4
4
  "description": "Blits: The Lightning 3 App Development Framework",
5
5
  "bin": "bin/index.js",
6
6
  "exports": {
@@ -19,12 +19,10 @@ import Circle from './Circle.js'
19
19
  import RouterView from './RouterView.js'
20
20
  import Sprite from './Sprite.js'
21
21
  import FPScounter from './FPScounter.js'
22
- import Layout from './Layout.js'
23
22
 
24
23
  export default () => ({
25
24
  Circle: Circle(),
26
25
  RouterView: RouterView(),
27
26
  Sprite: Sprite(),
28
27
  FPScounter: FPScounter(),
29
- Layout: Layout(),
30
28
  })
@@ -19,12 +19,10 @@ import Circle from './Circle.js'
19
19
  import RouterView from './RouterView.js'
20
20
  import Sprite from './Sprite.js'
21
21
  import FPScounter from './FPScounter.js'
22
- import Layout from './Layout.js'
23
22
 
24
23
  export default () => ({
25
24
  Circle: Circle(),
26
25
  RouterView: RouterView(),
27
26
  Sprite: Sprite(),
28
27
  FPScounter: FPScounter(),
29
- Layout: Layout(),
30
28
  })
@@ -22,6 +22,38 @@ import { Log } from '../../lib/log.js'
22
22
  import symbols from '../../lib/symbols.js'
23
23
  import Settings from '../../settings.js'
24
24
 
25
+ const layoutFn = function (config) {
26
+ let offset = 0
27
+ const xy = config.direction === 'vertical' ? 'y' : 'x'
28
+ const wh = config.direction === 'vertical' ? 'height' : 'width'
29
+ const opositeWh = config.direction === 'vertical' ? 'width' : 'height'
30
+
31
+ const children = this.children
32
+ const childrenLength = children.length
33
+ let otherDimension = 0
34
+ for (let i = 0; i < childrenLength; i++) {
35
+ const node = children[i]
36
+ node[xy] = offset
37
+ // todo: temporary text check, due to 1px width of empty text node
38
+ if (wh === 'width') {
39
+ offset += node.width + (node.width !== ('text' in node ? 1 : 0) ? config.gap : 0)
40
+ } else if (wh === 'height') {
41
+ offset +=
42
+ 'text' in node
43
+ ? node.width > 1
44
+ ? node.height + config.gap
45
+ : 0
46
+ : node.height !== 0
47
+ ? node.height + config.gap
48
+ : 0
49
+ }
50
+ otherDimension = Math.max(otherDimension, node[opositeWh])
51
+ }
52
+ // adjust the size of the layout container
53
+ this[wh] = offset - (config.gap || 0)
54
+ this[opositeWh] = otherDimension
55
+ }
56
+
25
57
  const isTransition = (value) => {
26
58
  return value !== null && typeof value === 'object' && 'transition' in value === true
27
59
  }
@@ -305,11 +337,16 @@ const Element = {
305
337
  })
306
338
  }
307
339
 
308
- // if (this.component !== undefined && '___layout' in this.component) {
309
- // this.node.on('loaded', () => {
310
- // this.component.___layout()
311
- // })
312
- // }
340
+ if (props.__layout === true) {
341
+ this.triggerLayout = layoutFn.bind(this.node)
342
+ }
343
+
344
+ if (this.config.parent.props !== undefined && this.config.parent.props.__layout === true) {
345
+ this.config.parent.triggerLayout(this.config.parent.props)
346
+ this.node.on('loaded', () => {
347
+ this.config.parent.triggerLayout(this.config.parent.props)
348
+ })
349
+ }
313
350
  },
314
351
  set(prop, value) {
315
352
  if (value === undefined) return
@@ -338,10 +375,9 @@ const Element = {
338
375
  }
339
376
  }
340
377
 
341
- // todo: review naming
342
- // if (this.component && this.component.___layout) {
343
- // this.component.___layout()
344
- // }
378
+ if (this.config.parent.props && this.config.parent.props.__layout === true) {
379
+ this.config.parent.triggerLayout(this.config.parent.props)
380
+ }
345
381
  },
346
382
  animate(prop, value, transition) {
347
383
  // check if a transition is already scheduled to run on the same prop
@@ -393,6 +429,12 @@ const Element = {
393
429
  })
394
430
  }
395
431
 
432
+ if (this.config.parent.props && this.config.parent.props.__layout === true) {
433
+ f.on('tick', () => {
434
+ this.config.parent.triggerLayout(this.config.parent.props)
435
+ })
436
+ }
437
+
396
438
  if (transition.progress !== undefined && typeof transition.progress === 'function') {
397
439
  let prevProgress = 0
398
440
  f.on('tick', (_node, { progress }) => {
@@ -414,9 +414,6 @@ const generateForLoopCode = function (templateObject, parent) {
414
414
  `)
415
415
  }
416
416
  })
417
- // if(elms[${forStartCounter}][0] && elms[${forStartCounter}][0].forComponent && elms[${forStartCounter}][0].forComponent.___layout) {
418
- // elms[${forStartCounter}][0].forComponent.___layout()
419
- // }
420
417
  ctx.renderCode.push(`
421
418
  }
422
419
  }`)
@@ -498,11 +495,15 @@ const generateCode = function (templateObject, parent = false, options = {}) {
498
495
  if (
499
496
  childTemplateObject[Symbol.for('componentType')] === 'Element' ||
500
497
  childTemplateObject[Symbol.for('componentType')] === 'Slot' ||
501
- childTemplateObject[Symbol.for('componentType')] === 'Text'
498
+ childTemplateObject[Symbol.for('componentType')] === 'Text' ||
499
+ childTemplateObject[Symbol.for('componentType')] === 'Layout'
502
500
  ) {
503
501
  if (childTemplateObject[Symbol.for('componentType')] === 'Text') {
504
502
  childTemplateObject.__textnode = 'true'
505
503
  }
504
+ if (childTemplateObject[Symbol.for('componentType')] === 'Layout') {
505
+ childTemplateObject.__layout = 'true'
506
+ }
506
507
  generateElementCode.call(this, childTemplateObject, parent, options)
507
508
  } else {
508
509
  generateComponentCode.call(this, childTemplateObject, parent, options)