UNPKG

tuix

Version:

A performant TUI framework for Bun with JSX and reactive state management

190 lines (161 loc) 6.22 kB
/** * Mouse Router Service - Routes mouse events to components * * This service coordinates between hit testing and component message routing. * It maintains a registry of components and their mouse handlers, performs * hit testing, and converts mouse events to component messages. */ import { Effect, Context, Ref, Array as EffectArray, HashMap, Option } from "effect" import type { MouseEvent } from "@/core/types.ts" import { HitTestService, type ComponentBounds, createBounds } from "./hit-test.js" // ============================================================================= // Types // ============================================================================= /** * Component mouse handler registration */ export interface ComponentMouseHandler<Msg> { readonly componentId: string readonly handler: (mouse: MouseEvent, localX: number, localY: number) => Msg | null } /** * Mouse routing result */ export interface MouseRoutingResult<Msg> { readonly componentId: string readonly message: Msg readonly localX: number readonly localY: number } /** * Mouse router service interface */ export interface MouseRouterServiceInterface { /** * Register a component for mouse events */ readonly registerComponent: <Msg>( componentId: string, bounds: ComponentBounds, handler: (mouse: MouseEvent, localX: number, localY: number) => Msg | null ) => Effect.Effect<void, never, never> /** * Unregister a component */ readonly unregisterComponent: (componentId: string) => Effect.Effect<void, never, never> /** * Update component bounds (for layout changes) */ readonly updateComponentBounds: (componentId: string, bounds: ComponentBounds) => Effect.Effect<void, never, never> /** * Route a mouse event to the appropriate component */ readonly routeMouseEvent: <Msg>(mouseEvent: MouseEvent) => Effect.Effect<MouseRoutingResult<Msg> | null, never, HitTestService> /** * Clear all registrations */ readonly clearAll: Effect.Effect<void, never, HitTestService> } /** * Mouse router service tag */ export const MouseRouterService = Context.GenericTag<MouseRouterServiceInterface>("MouseRouterService") // ============================================================================= // Implementation // ============================================================================= /** * Live implementation of the mouse router service */ export const MouseRouterServiceLive = Effect.gen(function* (_) { const handlersRef = yield* _(Ref.make<HashMap.HashMap<string, (mouse: MouseEvent, localX: number, localY: number) => any>>(HashMap.empty())) return { registerComponent: <Msg>( componentId: string, bounds: ComponentBounds, handler: (mouse: MouseEvent, localX: number, localY: number) => Msg | null ) => Effect.gen(function* (_) { const hitTest = yield* _(HitTestService) // Register with hit testing yield* _(hitTest.registerComponent(bounds)) // Register handler yield* _(Ref.update(handlersRef, handlers => HashMap.set(handlers, componentId, handler) )) }), unregisterComponent: (componentId: string) => Effect.gen(function* (_) { const hitTest = yield* _(HitTestService) // Unregister from hit testing yield* _(hitTest.unregisterComponent(componentId)) // Remove handler yield* _(Ref.update(handlersRef, handlers => HashMap.remove(handlers, componentId) )) }), updateComponentBounds: (componentId: string, bounds: ComponentBounds) => Effect.gen(function* (_) { const hitTest = yield* _(HitTestService) yield* _(hitTest.registerComponent(bounds)) // This will update existing }), routeMouseEvent: <Msg>(mouseEvent: MouseEvent) => Effect.gen(function* (_) { const hitTest = yield* _(HitTestService) const handlers = yield* _(Ref.get(handlersRef)) // Find component at mouse coordinates const hitResult = yield* _(hitTest.hitTest(mouseEvent.x, mouseEvent.y)) if (!hitResult) return null // Get handler for this component const handlerOption = HashMap.get(handlers, hitResult.componentId) if (Option.isNone(handlerOption)) return null const handler = handlerOption.value // Call handler with local coordinates const message = handler(mouseEvent, hitResult.localX, hitResult.localY) if (!message) return null return { componentId: hitResult.componentId, message, localX: hitResult.localX, localY: hitResult.localY } as MouseRoutingResult<Msg> }), clearAll: Effect.gen(function* (_) { const hitTest = yield* _(HitTestService) yield* _(hitTest.clearComponents) yield* _(Ref.set(handlersRef, HashMap.empty())) }) } }) // ============================================================================= // Helper Functions // ============================================================================= /** * Create a simple mouse handler that only responds to left clicks */ export const clickHandler = <Msg>(onClick: () => Msg) => (mouse: MouseEvent, localX: number, localY: number): Msg | null => { if (mouse.type === 'press' && mouse.button === 'left') { return onClick() } return null } /** * Create a mouse handler that responds to press and release */ export const pressReleaseHandler = <Msg>(onPress: () => Msg, onRelease: () => Msg) => (mouse: MouseEvent, localX: number, localY: number): Msg | null => { if (mouse.button === 'left') { if (mouse.type === 'press') { return onPress() } else if (mouse.type === 'release') { return onRelease() } } return null } /** * Create a mouse handler that provides mouse coordinates */ export const coordinateHandler = <Msg>(onMouse: (x: number, y: number, event: MouseEvent) => Msg | null) => (mouse: MouseEvent, localX: number, localY: number): Msg | null => onMouse(localX, localY, mouse)