diff --git a/@zapp/core/src/NodeType.ts b/@zapp/core/src/NodeType.ts index a851d13..385e637 100644 --- a/@zapp/core/src/NodeType.ts +++ b/@zapp/core/src/NodeType.ts @@ -12,4 +12,5 @@ export enum NodeType { Remember = 'remember', Effect = 'effect', + Event = 'event', } diff --git a/@zapp/core/src/index.ts b/@zapp/core/src/index.ts index 4a45cba..bc6a057 100644 --- a/@zapp/core/src/index.ts +++ b/@zapp/core/src/index.ts @@ -7,6 +7,7 @@ export { } from './Application.js' export { remember } from './working_tree/effects/remember.js' export { rememberLauncherForResult } from './working_tree/effects/rememberLauncherForResult.js' +export { registerCrownEventHandler } from './working_tree/effects/registerCrownEventHandler.js' export { RememberedMutableValue } from './working_tree/effects/RememberedMutableValue.js' export { sideEffect } from './working_tree/effects/sideEffect.js' export { Arc } from './working_tree/views/Arc.js' @@ -41,6 +42,7 @@ export { withTiming } from './working_tree/effects/animation/TimingAnimation.js' export { Renderer, setViewManager as __setViewManager, RenderNode } from './renderer/Renderer.js' export { ViewManager } from './renderer/ViewManager.js' export { PointerEventManager } from './renderer/PointerEventManager.js' +export { GlobalEventManager } from './renderer/GlobalEventManager.js' export { NodeType } from './NodeType.js' export { ZappInterface, Zapp, setZappInterface as __setZappInterface } from './ZappInterface.js' export { NavigatorInterface, Navigator, RegisteredCallback, setNavigator as __setNavigator } from './Navigator.js' diff --git a/@zapp/core/src/renderer/GlobalEventManager.ts b/@zapp/core/src/renderer/GlobalEventManager.ts new file mode 100644 index 0000000..4678712 --- /dev/null +++ b/@zapp/core/src/renderer/GlobalEventManager.ts @@ -0,0 +1,30 @@ +import { EventType } from '../working_tree/EventNode.js' + +export interface EventHandler { + type: EventType + handler: (...args: any[]) => boolean +} + +export abstract class GlobalEventManager { + private static handlers: EventHandler[] = [] + + public static clearHandlers() { + GlobalEventManager.handlers = [] + } + + public static registerHandler(handler: EventHandler) { + GlobalEventManager.handlers.push(handler) + } + + public static dispatchCrownEvent(delta: number): boolean { + // iterate upwards so the deeper nodes receive the event earlier + for (let i = GlobalEventManager.handlers.length - 1; i >= 0; i--) { + const handler = GlobalEventManager.handlers[i] + if (handler.type === EventType.Crown && handler.handler(delta)) { + return true + } + } + + return false + } +} diff --git a/@zapp/core/src/renderer/Renderer.ts b/@zapp/core/src/renderer/Renderer.ts index 49006b2..01f5fe6 100644 --- a/@zapp/core/src/renderer/Renderer.ts +++ b/@zapp/core/src/renderer/Renderer.ts @@ -5,6 +5,8 @@ import { CustomViewProps } from '../working_tree/views/Custom.js' import { PointerEventManager } from './PointerEventManager.js' import { LayoutManager } from './LayoutManager.js' import { ViewManager } from './ViewManager.js' +import { EventNode } from '../working_tree/EventNode.js' +import { GlobalEventManager } from './GlobalEventManager.js' interface Layout { width: number @@ -67,6 +69,7 @@ export abstract class Renderer { } public static commit(root: ViewNode) { + GlobalEventManager.clearHandlers() Renderer.newTree = Renderer.createNode(root) } @@ -269,6 +272,11 @@ export abstract class Renderer { for (const child of node.children) { if (child instanceof ViewNode) { result.children.push(Renderer.createNode(child, result)) + } else if (child instanceof EventNode) { + GlobalEventManager.registerHandler({ + type: child.eventType, + handler: child.handler, + }) } } diff --git a/@zapp/core/src/working_tree/EventNode.ts b/@zapp/core/src/working_tree/EventNode.ts new file mode 100644 index 0000000..6d33e39 --- /dev/null +++ b/@zapp/core/src/working_tree/EventNode.ts @@ -0,0 +1,12 @@ +import { WorkingNode } from './WorkingNode.js' + +export enum EventType { + Button, + Crown, + Gesture, +} + +export class EventNode extends WorkingNode { + public handler: (...args: any[]) => boolean + public eventType: EventType +} diff --git a/@zapp/core/src/working_tree/ViewNode.ts b/@zapp/core/src/working_tree/ViewNode.ts index 14f2e11..f6230a0 100644 --- a/@zapp/core/src/working_tree/ViewNode.ts +++ b/@zapp/core/src/working_tree/ViewNode.ts @@ -6,6 +6,7 @@ import { WorkingNode, WorkingNodeProps } from './WorkingNode.js' import { WorkingTree } from './WorkingTree.js' import { findRelativePath } from '../utils.js' import { CustomViewProps } from './views/Custom.js' +import { EventNode } from './EventNode.js' export interface ViewNodeProps extends WorkingNodeProps { body?: () => void @@ -79,6 +80,21 @@ export class ViewNode extends WorkingNode { return result } + public event() { + // events may only be created inside view node + const currentView = WorkingTree.current as ViewNode + + const result = new EventNode({ + id: (currentView.nextActionId++).toString(), + type: NodeType.Event, + }) + + result.parent = currentView.override ?? WorkingTree.current + result.path = this.path.concat(this.id) + + return result + } + public override reset(): void { this.rememberedContext = undefined this.override = undefined diff --git a/@zapp/core/src/working_tree/effects/registerCrownEventHandler.ts b/@zapp/core/src/working_tree/effects/registerCrownEventHandler.ts new file mode 100644 index 0000000..cd64e18 --- /dev/null +++ b/@zapp/core/src/working_tree/effects/registerCrownEventHandler.ts @@ -0,0 +1,13 @@ +import { EventType } from '../EventNode.js' +import { ViewNode } from '../ViewNode.js' +import { WorkingTree } from '../WorkingTree.js' + +export function registerCrownEventHandler(handler: (delta: number) => boolean) { + const current = WorkingTree.current as ViewNode + const context = current.event() + + context.handler = handler + context.eventType = EventType.Crown + + current.children.push(context) +} diff --git a/@zapp/watch/src/SimpleScreen.ts b/@zapp/watch/src/SimpleScreen.ts index 4323e07..a5c2aec 100644 --- a/@zapp/watch/src/SimpleScreen.ts +++ b/@zapp/watch/src/SimpleScreen.ts @@ -1,4 +1,12 @@ -import { ScreenBody, ConfigBuilderArg, Zapp, PointerEventManager, WorkingTree, Navigator } from '@zapp/core' +import { + ScreenBody, + ConfigBuilderArg, + Zapp, + PointerEventManager, + WorkingTree, + Navigator, + GlobalEventManager, +} from '@zapp/core' export function SimpleScreen(configBuilder: ConfigBuilderArg, body?: (params?: Record) => void) { Page({ @@ -26,6 +34,10 @@ export function SimpleScreen(configBuilder: ConfigBuilderArg, body?: (params?: R return false }) + + hmApp.registerSpinEvent((_key: unknown, degree: number) => { + return GlobalEventManager.dispatchCrownEvent(degree) + }) }, build() { ScreenBody(configBuilder, () => { @@ -35,6 +47,7 @@ export function SimpleScreen(configBuilder: ConfigBuilderArg, body?: (params?: R onDestroy() { Zapp.stopLoop() hmApp.unregisterGestureEvent() + hmApp.unregistSpinEvent() }, }) } diff --git a/@zapp/web/src/ZappWeb.ts b/@zapp/web/src/ZappWeb.ts index a0d4059..53b2bb7 100644 --- a/@zapp/web/src/ZappWeb.ts +++ b/@zapp/web/src/ZappWeb.ts @@ -1,12 +1,24 @@ -import { PointerEventManager, Renderer, WorkingTree, ZappInterface, Animation } from '@zapp/core' +import { PointerEventManager, Renderer, WorkingTree, ZappInterface, Animation, GlobalEventManager } from '@zapp/core' export class ZappWeb extends ZappInterface { private running = false + private crownDelta = 0 + private previousCrownDelta = 0 + private crownResetTimeout = -1 public startLoop() { this.running = true WorkingTree.requestUpdate() requestAnimationFrame(this.update) + + window.addEventListener('wheel', (e) => { + this.crownDelta += e.deltaY / 10 + + clearTimeout(this.crownResetTimeout) + setTimeout(() => { + this.crownDelta = 0 + }, 100) + }) } stopLoop(): void { @@ -14,6 +26,11 @@ export class ZappWeb extends ZappInterface { } private update = () => { + if (this.crownDelta !== this.previousCrownDelta) { + this.previousCrownDelta = this.crownDelta + GlobalEventManager.dispatchCrownEvent(this.crownDelta) + } + PointerEventManager.processEvents() Animation.nextFrame(Date.now()) diff --git a/watch-test/page/index.js b/watch-test/page/index.js index 612b9b6..cfc8232 100644 --- a/watch-test/page/index.js +++ b/watch-test/page/index.js @@ -20,6 +20,7 @@ import { ColumnConfig, ArcConfig, Navigator, + registerCrownEventHandler, } from '@zapp/core' let cycle = [ @@ -68,8 +69,16 @@ SimpleScreen(Config('screen'), () => { textVisible.value = !textVisible.value }), () => { + const deltaV = remember(0) + const counter = remember(0) + registerCrownEventHandler((delta) => { + deltaV.value = delta + counter.value = counter.value + 1 + return false + }) + if (textVisible.value) { - Text(TextConfig('text').textColor(0xffffff).textSize(24), 'Random text') + Text(TextConfig('text').textColor(0xffffff).textSize(24), '' + deltaV.value + ', ' + counter.value) } else { ActivityIndicator(ArcConfig('ac').width(60).height(60).color(0xffffff).lineWidth(10)) } diff --git a/watch-test/page/page3.js b/watch-test/page/page3.js index 987ab07..9b8c62a 100644 --- a/watch-test/page/page3.js +++ b/watch-test/page/page3.js @@ -20,6 +20,7 @@ import { ArcConfig, Navigator, rememberLauncherForResult, + registerCrownEventHandler, } from '@zapp/core' SimpleScreen(Config('screen'), (params) => { @@ -37,6 +38,19 @@ SimpleScreen(Config('screen'), (params) => { launcher.launch() }), () => { + const height = remember(10) + const targetHeight = remember(10) + + registerCrownEventHandler((delta) => { + targetHeight.value = Math.max(10, targetHeight.value + delta * -1) + height.value = withTiming(targetHeight.value, { easing: Easing.easeOutCubic }) + return true + }) + + Column(ColumnConfig('column2').alignment(Alignment.Center).arrangement(Arrangement.Center), () => { + Stack(StackConfig('bar').width(50).height(height.value).background(0xff0000)) + }) + Column(ColumnConfig('column'), () => { Text(TextConfig('text').textColor(0xffffff).textSize(40), `3, ${params.data}`) Text(TextConfig('text2').textColor(0xffffff).textSize(40), `Selected: ${selectedNumber.value}`) diff --git a/web-test/src/index.ts b/web-test/src/index.ts index 86b2151..85b5123 100644 --- a/web-test/src/index.ts +++ b/web-test/src/index.ts @@ -22,6 +22,7 @@ import { Zapp, rememberLauncherForResult, Navigator, + registerCrownEventHandler, } from '@zapp/core' import { NavBar, RouteInfo } from './NavBar' import { Page } from './Page' @@ -36,6 +37,7 @@ const routesInfo: RouteInfo[] = [ { displayName: 'Row example', routeName: 'row' }, { displayName: 'Animation example', routeName: 'animation' }, { displayName: 'StartForResult example', routeName: 'startForResult' }, + { displayName: 'Crown events example', routeName: 'crownEvent' }, ] function StackExample() { @@ -478,6 +480,23 @@ function NumberPickerExample() { }) } +function CrownEventExample() { + Page(routesInfo, () => { + const height = remember(10) + const targetHeight = remember(10) + + registerCrownEventHandler((delta: number) => { + targetHeight.value = Math.max(10, targetHeight.value + delta * -1) + height.value = withTiming(targetHeight.value, { easing: Easing.easeOutCubic }) + return true + }) + + Column(ColumnConfig('column').alignment(Alignment.Center).arrangement(Arrangement.Center), () => { + Stack(StackConfig('bar').width(50).height(height.value).background(0xff0000)) + }) + }) +} + registerNavigationRoutes('dynamicLayout', { dynamicLayout: DynamicLayoutExample, stack: StackExample, @@ -486,4 +505,5 @@ registerNavigationRoutes('dynamicLayout', { animation: AnimationExample, startForResult: StartForResultExample, picker: NumberPickerExample, + crownEvent: CrownEventExample, })