import type { RenderWindow } from '../../plugin.types'; import { type BarPlacement } from '../layout/bar-layout'; import type { SchedulerModule, SchedulerRuntime } from '../scheduler-runtime'; /** * Draws the event bars for one frame and keeps them in sync across frames. * * ## Why this class writes so little * * Positioning is pure `transform: translate()`, which the compositor handles * without invalidating layout -- so moving every bar on screen costs no reflow. * The remaining layout-affecting writes are `width` and `height`, and both are * diffed against the last value written. Combined with the layout engine's * rebasing contract (absolute x, row-origin-relative y) the steady state is * strict: **a pure horizontal or vertical scroll produces identical placements, * so this renderer writes nothing at all** -- no transform, no width, no class, * no attribute. That property is the acceptance criterion for the whole * rendering layer, and every cached field on {@link BarState} exists to hold it. * * ## Why mark-and-sweep rather than diffing arrays * * The set of visible bars changes shape on every axis: rows scroll in and out * vertically, events enter and leave horizontally, and lane counts change as * overlaps appear. Diffing two ordered lists would need a keyed LCS; stamping a * pass number on each surviving element and sweeping the leftovers is O(n) with * no allocation, and is the same technique the grid's own row renderer uses. * * The renderer owns no interaction: it exposes {@link getBarElement} and * {@link getPlacements} and lets the drag, resize and selection services do * their own hit-testing against the geometry it already computed, so a pointer * move never re-measures the DOM either. */ export declare class EventBarRenderer implements SchedulerModule { private readonly runtime; private readonly layerEl; /** Bars currently mounted, keyed by {@link BarPlacement.key} (resource + event). */ private readonly live; /** Retired bars available for reuse, capped at {@link MAX_POOLED_BARS}. */ private readonly pool; /** Per-element diff state. See {@link BarState}. */ private readonly states; /** Mounted component instances, held in an iterable map so `destroy()` can reach every one. */ private readonly components; /** Event id to bar element, maintained incrementally so hit-testing is O(1). */ private readonly byEventId; /** Memoized `{ ...eventDefaults, ...eventTypes[name] }` per type name. Configs are immutable. */ private readonly typeCache; /** Reused layout buffers, so a steady-state frame allocates nothing here. */ private readonly scratch; /** Reused handle buffer for the overflow count. */ private readonly countScratch; /** Layout tuning, built once -- passing an object literal per frame would allocate 60 times a second. */ private readonly layoutOptions; /** Cached config switches, read once because they cannot change for a runtime's lifetime. */ private readonly resizeEnabled; private readonly checkboxesEnabled; /** * Monotonic pass counter driving the sweep. * * Deliberately not `RenderWindow.frame`: the grid may legitimately hand the * same frame number to two passes (a forced repaint inside one animation * frame), and a repeated stamp would sweep bars that are still live. A counter * this class owns cannot collide. */ private pass; /** Last frame's placements, exposed for hit-testing. */ private placements; /** The `+N more` pill. Created on first truncated frame and never re-created. */ private overflowEl; private overflowMounted; private overflowText; private overflowX; /** Formatter for the accessible time range, rebuilt only when the timeline unit changes. */ private rangeFormatter; private rangeFormatterUnit; /** * Adapts the host's row-to-resource mapping to the layout engine's signature. * * A bound field rather than a method or an inline closure: `computeBarLayout` * takes it as a callback on every frame, and an arrow created at the call site * would allocate one closure per frame for no reason. Non-`data` rows -- group * headers, detail panes, summary rows -- are rejected here rather than inside * the host's callback, so a host never has to defend against row shapes it * did not ask for. */ private readonly resourceIdOf; /** * @param runtime - Shared scheduler state. Read-only from this class's point of * view: the renderer never mutates the timeline, the index or the selection. * @param layerEl - The plugin layer bars are mounted into. Expected to have * been mounted with `followRowOrigin` and `followScrollX`, which is what * lets bar geometry stay constant across a scroll. */ constructor(runtime: SchedulerRuntime, layerEl: HTMLElement); /** * Paints one frame. * * The whole method is the hot path -- it runs synchronously inside the grid's * own render, once per animation frame -- so it reads no DOM, forces no layout * and allocates only when the visible set genuinely changes. * * @param window - The virtualization window the grid just committed. Bar * positions are derived from it rather than from a second measurement, so * bars can never be a frame out of step with their resource rows. */ render(window: RenderWindow): void; /** * Resolves an event id to its mounted bar. * * Interaction services need this to attach a drag to the element the user * grabbed, and to move focus after a keyboard command. Returns `null` for * events that are not currently rendered -- scrolled out, filtered away, or * beyond the frame's bar budget -- which callers must treat as "no DOM to act * on" rather than "no such event". */ getBarElement(eventId: string): HTMLElement | null; /** * The placements produced by the most recent {@link render}. * * Exposed so hit-testing, drag previews and overlap checks can reuse the * geometry this renderer already computed instead of calling * `getBoundingClientRect` per pointer move -- which would force layout on * every mouse event during a drag, the single easiest way to lose 60 fps. * * Borrowed by reference and replaced wholesale each frame: callers must read * it during the same turn and never retain or mutate it. */ getPlacements(): readonly BarPlacement[]; /** * Releases every node, component and cache this renderer owns. * * Components are destroyed before the DOM is emptied, so a framework wrapper * still sees its host element attached while it unmounts -- React and Angular * both misbehave when unmounted from a detached tree. */ destroy(): void; /** Returns the bar for `key`, reusing a pooled node or creating one. */ private acquire; /** Detaches every bar not stamped with the current pass. */ private sweep; /** Unbinds a bar and returns it to the pool, dropping it entirely once the pool is full. */ private recycle; /** Builds an empty bar with the children every bar needs regardless of type. */ private createBar; /** Applies an event to a bar, touching only what actually differs from last frame. */ private bind; /** Resolves and memoizes the merged appearance for one type name. */ private resolveType; /** * Writes the type's appearance onto the bar. * * Gated on the type *name* alone, which is sound because resolved configs are * memoized and immutable: same name implies same values, so a bar that keeps * its type across a rebind performs zero style writes. Absent values are * written as `''` rather than skipped, so a bar recycled from a colourful type * onto a plain one falls back to the stylesheet instead of inheriting the * previous tenant's colours. */ private applyTypeStyles; /** * Positions a bar. * * `transform` carries both axes because it is composited: the browser moves the * layer without recalculating layout for it or for its siblings. `width` and * `height` do affect layout, which is precisely why they are diffed -- on a * scroll they never change, so the layout engine is never invalidated. */ private position; /** Dispatches to the custom renderer when the type declares one, else to the built-in layout. */ private buildContent; /** Builds the built-in icon / label / badge layout. */ private renderDefault; /** * Mounts exactly the optional children the current binding needs. * * Rebuilds the child list wholesale rather than splicing individual nodes. * That sounds wasteful and is not: it runs only when the *composition* changes * (an icon appears, a badge disappears, a lock removes the handles) or when a * custom renderer has to be evicted, never on a scroll and rarely on a rebind * -- and it makes DOM order correct by construction instead of by a web of * `insertBefore` reference points. */ private syncSkeleton; /** Creates the icon slot on first use. Most events have no icon; most bars never pay for one. */ private ensureIcon; /** Creates the badge slot on first use. */ private ensureBadge; /** * Hands the bar's interior to a host renderer. * * Two shapes are supported and told apart by prototype inspection rather than * by a discriminant field, because the function form has to stay a plain * arrow -- asking hosts to tag their renderers would make the simple case * ceremonial. The component form is given a real lifecycle so a React root or * an Angular view can be reconciled on rebind and released on recycle; * `refresh` returning `true` means the framework handled the change in place * and the scheduler must not tear anything down. */ private renderCustom; /** Empties a bar ahead of a custom render and marks its skeleton as gone. */ private clearForCustom; /** Releases the component bound to a bar, if any. Safe to call unconditionally. */ private destroyComponent; /** * Writes the bar's accessible name: title first, then the formatted range. * * A bar is a coloured rectangle whose meaning is entirely positional, so * without the range a screen-reader user gets "Annual leave, button" with no * way to learn *when*. The range is formatted with a cached `Intl` formatter * whose precision follows the timeline unit -- reading a wall-clock time on a * year view would be noise, and omitting it on an hour view would lose the * only information that matters. */ private applyAccessibleName; /** Lazily builds the range formatter, rebuilding only when the timeline's unit changes. */ private getRangeFormatter; /** * Shows or hides the `+N more` pill. * * Only reached when the layout engine hit its bar budget, which means the * frame is already degraded: past that density the bars are narrower than * their own text and a count genuinely communicates more than the bars would. * The count itself costs one extra index query per visible resource -- real * work, but only on truncated frames, and cheaper than the lane layout the * engine skipped by bailing out. */ private syncOverflow; /** Builds the overflow pill and re-anchors it to the left so it can be transform-positioned. */ private createOverflow; /** Counts events in the visible range that did not get a bar this frame. */ private countHiddenEvents; } //# sourceMappingURL=event-bar-renderer.d.ts.map