tellur_core/timeline_component/trigger.rs
1//! [`Event`]s and the wrappers that decorate a component transparently:
2//! [`Triggered`] (records an event time during resolve) and [`Sourced`]
3//! (stamps the authoring call site onto the arrangement).
4
5use std::hash::Hash;
6use std::ops::Range;
7use std::sync::atomic::{AtomicU64, Ordering};
8
9use crate::geometry::Vec2;
10use crate::phase::Phase;
11use crate::raster::{RasterImage, Resolution};
12use crate::render_context::RenderContext;
13use crate::time::Time;
14use crate::window::Window;
15
16use super::*;
17
18// ── Events — a structural moment shared across the tree ──────────────────────
19
20/// Process-wide counter minting [`Event`] ids. A plain monotonic counter is
21/// enough: ids only need to be distinct within a session (`.sketch/01` A.6).
22static EVENT_COUNTER: AtomicU64 = AtomicU64::new(0);
23
24/// The typed handle the user binds once (`let e = Event::new()`). It is a small
25/// `Copy` identity token — NOT a cell.
26///
27/// A clip declares the moment with a `.trigger_*` verb (see [`Triggers`]); the
28/// resolve pass records the resolved time in a side table (`id → TimelineTime`),
29/// and components read it through the [`Clock`]. Being a plain id, it is a sound
30/// cache-key term (audit B5); the trigger *time* lives outside the component.
31///
32/// An optional `name` ([`Event::named`]) is a `&'static str` so the token stays
33/// `Copy`. It is carried purely for display — it surfaces on the arrangement's
34/// [`TriggerMark`]s so the live UI can label event markers — and is NOT part of
35/// the event identity (only [`id`](Self::id) is).
36#[derive(Debug, Clone, Copy)]
37pub struct Event {
38 id: u64,
39 name: Option<&'static str>,
40}
41
42// Identity is the minted `id` ALONE: the `name` is a display annotation, never
43// part of equality/hash (so it is not a cache-key term either, audit B5). Two
44// `Event`s are equal iff their ids match — and ids are process-unique per mint.
45impl PartialEq for Event {
46 fn eq(&self, other: &Self) -> bool {
47 self.id == other.id
48 }
49}
50
51impl Eq for Event {}
52
53impl Hash for Event {
54 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
55 self.id.hash(state);
56 }
57}
58
59impl Event {
60 /// Mints a fresh, unnamed process-unique id.
61 pub fn new() -> Self {
62 Self {
63 id: EVENT_COUNTER.fetch_add(1, Ordering::Relaxed),
64 name: None,
65 }
66 }
67
68 /// Mints a fresh process-unique id carrying a display `name`. The name is
69 /// for UI labelling only; the identity is still the freshly minted id.
70 pub fn named(name: &'static str) -> Self {
71 Self {
72 id: EVENT_COUNTER.fetch_add(1, Ordering::Relaxed),
73 name: Some(name),
74 }
75 }
76
77 /// The raw id, used by the resolve pass / [`TriggerTable`] as the key.
78 pub fn id(&self) -> u64 {
79 self.id
80 }
81
82 /// The optional display name bound at construction (`None` for [`Event::new`]).
83 pub fn name(&self) -> Option<&'static str> {
84 self.name
85 }
86
87 /// `now < trigger` (unfired ⇒ trigger is `+∞`, so always true).
88 pub fn is_before(&self, clock: &Clock<'_>) -> bool {
89 clock.global().seconds() < clock.trigger_of(*self).seconds()
90 }
91
92 /// `now >= trigger` (unfired ⇒ always false).
93 pub fn is_after(&self, clock: &Clock<'_>) -> bool {
94 clock.global().seconds() >= clock.trigger_of(*self).seconds()
95 }
96
97 /// Phase over `[trigger + a, trigger + b]`; clamps 0 before, 1 after.
98 pub fn phase(&self, clock: &Clock<'_>, a: f32, b: f32) -> Phase {
99 let trigger = clock.trigger_of(*self).seconds();
100 // `.sketch/02 §11`: an unfired (+∞) trigger must short-circuit to 0,
101 // otherwise the naive `(now - ∞)/(∞ - ∞)` is `NaN`.
102 if trigger.is_infinite() {
103 return Phase::ZERO;
104 }
105 clock.global().phase(trigger + a, trigger + b)
106 }
107
108 /// Seconds elapsed since the trigger on the global timeline.
109 ///
110 /// Unfired events report `0.0`, and times before the trigger are clamped to
111 /// `0.0`. Unlike [`Event::phase`], this keeps counting after the event, so
112 /// components can run at a natural speed without choosing an end time up
113 /// front.
114 pub fn elapsed(&self, clock: &Clock<'_>) -> f32 {
115 let trigger = clock.trigger_of(*self).seconds();
116 if trigger.is_infinite() {
117 return 0.0;
118 }
119 (clock.global().seconds() - trigger).max(0.0)
120 }
121
122 /// A [`Window`] over `[trigger + range.start, trigger + range.end)`,
123 /// already [`clamped`](Window::clamped) — the event-relative twin of
124 /// [`crate::time::Time::window`], for staggering sub-events
125 /// ([`Window::sub_secs`]) or reading elapsed/remaining seconds off a
126 /// single trigger-anchored interval instead of composing them from
127 /// [`Event::phase`] / [`Event::elapsed`] by hand.
128 ///
129 /// Returning the clamped snapshot (not the live cursor) directly is what
130 /// makes this safe to store in a component field: like
131 /// [`Window::clamped`], the snapshot is constant at `(start, end, start)`
132 /// before the window opens and constant at `(start, end, end)` once it
133 /// closes, so it is a frame-stable cache-key term the same way a
134 /// saturating [`Phase`] is (`.sketch/02 §11`).
135 ///
136 /// An unfired event (trigger at `+∞`) cannot be shifted into absolute
137 /// seconds, so it reports the "before the window" snapshot directly —
138 /// `range.start` doubling as its own anchor, cursor pinned to
139 /// `range.start` (phase `0`, matching [`Event::phase`]'s `+∞` short
140 /// circuit) — rather than propagating `+∞`/`NaN` or panicking. That
141 /// snapshot does not depend on the current time, so it stays stable
142 /// across every frame the event remains unfired, exactly like the
143 /// post-close snapshot stays stable across every frame after firing.
144 pub fn window(&self, clock: &Clock<'_>, range: Range<f32>) -> Window {
145 assert!(
146 range.start.is_finite() && range.end.is_finite() && range.end > range.start,
147 "Event::window requires a finite range with end > start"
148 );
149 let trigger = clock.trigger_of(*self).seconds();
150 if trigger.is_infinite() {
151 return Window::new(range.start, range.end, range.start);
152 }
153 let start = trigger + range.start;
154 let end = trigger + range.end;
155 Window::new(start, end, clock.global().seconds()).clamped()
156 }
157}
158
159impl Default for Event {
160 fn default() -> Self {
161 Self::new()
162 }
163}
164
165/// A transparent wrapper that registers an [`Event`]'s time during the resolve
166/// pass and otherwise plays its child unchanged. A [`TimelineComponent`].
167pub struct Triggered<T> {
168 child: T,
169 event: Event,
170 kind: TriggerKind,
171}
172
173// Hand-written so the `T: PartialEq + Hash` bound is attached (the `Keyable`
174// derive copies the declared generics verbatim and would not add it). Backs the
175// `DynEq` / `DynHash` super-traits a `Triggered<T>` needs as a
176// `TimelineComponent`.
177impl<T: PartialEq> PartialEq for Triggered<T> {
178 fn eq(&self, other: &Self) -> bool {
179 self.child == other.child && self.event == other.event && self.kind == other.kind
180 }
181}
182
183impl<T: Eq> Eq for Triggered<T> {}
184
185impl<T: Hash> Hash for Triggered<T> {
186 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
187 self.child.hash(state);
188 self.event.hash(state);
189 self.kind.hash(state);
190 }
191}
192
193/// Where in the child's interval a [`Triggered`] fires its [`Event`].
194#[derive(Debug, Clone, Copy, crate::Keyable)]
195enum TriggerKind {
196 /// At the child's resolved start (`abs_start`).
197 Start,
198 /// At the child's resolved end (`abs_start + len`).
199 End,
200 /// At a local offset into the child (`abs_start + local`).
201 At(f32),
202}
203
204impl<T> Triggered<T> {
205 /// The wrapped child.
206 pub fn child(&self) -> &T {
207 &self.child
208 }
209
210 /// The event this wrapper fires.
211 pub fn event(&self) -> Event {
212 self.event
213 }
214}
215
216impl<T: TimelineComponent + PartialEq + Hash + 'static> TimelineComponent for Triggered<T> {
217 fn duration(&self) -> Option<f32> {
218 self.child.duration()
219 }
220
221 fn measure(&self) -> Option<f32> {
222 self.child.measure()
223 }
224
225 fn resolve(&self, abs_start: f32, out: &mut ResolveCtx) -> f32 {
226 let len = self.child.resolve(abs_start, out);
227 // `Start` is already the absolute parent-clock start. `End` / `At` are in
228 // the child's LOCAL seconds, which run faster than the global clock by any
229 // enclosing window stretch, so scale them back via the cumulative
230 // `local_scale` (1.0 unless inside a stretched `.at(a..b)`).
231 let scale = out.local_scale;
232 let at = match self.kind {
233 TriggerKind::Start => abs_start,
234 TriggerKind::End => abs_start + len * scale,
235 TriggerKind::At(local) => abs_start + local * scale,
236 };
237 out.triggers_mut().record(self.event, at);
238 len
239 }
240
241 fn frame(
242 &self,
243 clock: Clock<'_>,
244 canvas: Vec2,
245 target: Resolution,
246 ctx: &mut dyn RenderContext,
247 ) -> Option<RasterImage> {
248 self.child.frame(clock, canvas, target, ctx)
249 }
250
251 fn samples(&self, clock: Clock<'_>, window: f32) -> Option<AudioBuffer> {
252 self.child.samples(clock, window)
253 }
254
255 fn mix_into(&self, mix: &mut crate::audio::AudioMix, start_secs: f32, speed: f32) {
256 // A trigger wrapper is transparent to the mix-down: contribute the
257 // child unchanged at the same offset / speed.
258 self.child.mix_into(mix, start_secs, speed);
259 }
260
261 fn cues(&self, offset: f32) -> Vec<Cue> {
262 self.child.cues(offset)
263 }
264
265 fn arrangement(&self, offset: f32) -> Arrangement {
266 // Transparent to the structure: build the child's node, then push this
267 // trigger's resolved time onto it (mirrors `resolve`'s trigger table).
268 // The child's resolved interval is `[offset, node.end]`, so its length
269 // is `node.end - offset` — `Start → offset`, `End → offset + len`,
270 // `At(local) → offset + local`. Multiple triggers accumulate.
271 let mut node = self.child.arrangement(offset);
272 let at = match self.kind {
273 TriggerKind::Start => offset,
274 TriggerKind::End => node.end,
275 TriggerKind::At(local) => offset + local,
276 };
277 node.triggers.push(TriggerMark {
278 time: at,
279 name: self.event().name().map(::std::string::String::from),
280 });
281 node
282 }
283}
284
285/// A `.trigger_*` result drops straight into a container's child setter, like
286/// [`Placed`] does.
287impl<T> From<Triggered<T>> for Box<dyn TimelineComponent + Send>
288where
289 T: TimelineComponent + PartialEq + Hash + Send + 'static,
290{
291 fn from(triggered: Triggered<T>) -> Self {
292 Box::new(triggered)
293 }
294}
295
296/// A transparent decorator that stamps a CALL-SITE source location onto its
297/// child's arrangement node and otherwise plays the child unchanged.
298///
299/// The generated container `.child(...)` setter (a `#[track_caller]` method)
300/// wraps every child in a `Sourced` carrying `Location::caller()`, so each node
301/// in the arrangement tree can be traced back to the authoring `.child(...)`
302/// line. Every query EXCEPT [`arrangement`](TimelineComponent::arrangement) is
303/// forwarded verbatim to `inner`; `arrangement` stamps the location onto the
304/// returned node (only if it is not already set — the innermost wrapper wins,
305/// which is the most specific call site).
306pub struct Sourced {
307 source: &'static ::core::panic::Location<'static>,
308 inner: Box<dyn TimelineComponent + Send>,
309}
310
311impl Sourced {
312 /// Wraps `inner`, recording the `source` call site for its arrangement node.
313 pub fn new(
314 source: &'static ::core::panic::Location<'static>,
315 inner: Box<dyn TimelineComponent + Send>,
316 ) -> Self {
317 Self { source, inner }
318 }
319
320 /// The captured call site as a [`SourceLoc`].
321 pub fn source_loc(&self) -> SourceLoc {
322 SourceLoc {
323 file: self.source.file().to_owned(),
324 line: self.source.line(),
325 }
326 }
327}
328
329/// Peels any wrapping [`Sourced`] decorators off a boxed child, returning the
330/// INNERMOST call site (matching [`Sourced::arrangement`], where the innermost
331/// stamp wins) and the structural inner component.
332///
333/// A container that bypasses a child's own `arrangement` (e.g.
334/// [`Timeline`](crate::timeline_container::Timeline),
335/// which builds the node from the peeled [`Placed`]) uses this to re-stamp the
336/// source the wrapper would otherwise have applied.
337pub fn peel_source(
338 child: &(dyn TimelineComponent + Send),
339) -> (Option<SourceLoc>, &(dyn TimelineComponent + Send)) {
340 let mut source = None;
341 let mut cur = child;
342 // Walk inward, keeping the LAST (innermost) source seen.
343 while let Some(sourced) = cur.as_any().downcast_ref::<Sourced>() {
344 source = Some(sourced.source_loc());
345 cur = sourced.inner.as_ref();
346 }
347 (source, cur)
348}
349
350// IDENTITY DELEGATES TO `inner`, IGNORING `source`: the call-site location is a
351// display annotation, NOT part of component identity / a cache-key term (same
352// contract as `Event::name`). These hand-written impls back the `DynEq` /
353// `DynHash` super-traits a `TimelineComponent` needs, and let `Sourced` be a
354// comparable `Box<dyn TimelineComponent + Send>` child like any other.
355impl PartialEq for Sourced {
356 fn eq(&self, other: &Self) -> bool {
357 // Compare the boxed children through `dyn TimelineComponent + Send`'s
358 // own `PartialEq` (the `DynEq` downcast), ignoring `source`.
359 *self.inner == *other.inner
360 }
361}
362
363impl Eq for Sourced {}
364
365impl Hash for Sourced {
366 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
367 // Hash through the boxed child's `dyn TimelineComponent + Send` `Hash`
368 // (the `DynHash` path), ignoring `source`.
369 self.inner.hash(state);
370 }
371}
372
373impl TimelineComponent for Sourced {
374 fn duration(&self) -> Option<f32> {
375 self.inner.duration()
376 }
377
378 fn measure(&self) -> Option<f32> {
379 self.inner.measure()
380 }
381
382 fn resolve(&self, abs_start: f32, out: &mut ResolveCtx) -> f32 {
383 self.inner.resolve(abs_start, out)
384 }
385
386 fn frame(
387 &self,
388 clock: Clock<'_>,
389 canvas: Vec2,
390 target: Resolution,
391 ctx: &mut dyn RenderContext,
392 ) -> Option<RasterImage> {
393 self.inner.frame(clock, canvas, target, ctx)
394 }
395
396 fn samples(&self, clock: Clock<'_>, window: f32) -> Option<AudioBuffer> {
397 self.inner.samples(clock, window)
398 }
399
400 fn mix_into(&self, mix: &mut crate::audio::AudioMix, start_secs: f32, speed: f32) {
401 self.inner.mix_into(mix, start_secs, speed);
402 }
403
404 fn cues(&self, offset: f32) -> Vec<Cue> {
405 self.inner.cues(offset)
406 }
407
408 fn arrangement(&self, offset: f32) -> Arrangement {
409 let mut node = self.inner.arrangement(offset);
410 // Only the innermost (most specific) call site stamps the node; an outer
411 // wrapper leaves an already-set `source` untouched.
412 if node.source.is_none() {
413 node.source = Some(SourceLoc {
414 file: self.source.file().to_owned(),
415 line: self.source.line(),
416 });
417 }
418 node
419 }
420
421 fn structural_any(&self) -> &dyn std::any::Any {
422 // Transparent to structural inspection: a container downcasting a child
423 // must see the real component, not this decorator. Recurses so nested
424 // wrappers peel fully.
425 self.inner.structural_any()
426 }
427}
428
429/// A `Sourced` drops straight into a container's child setter as a boxed
430/// `TimelineComponent`, like [`Placed`] / [`Triggered`] do.
431impl From<Sourced> for Box<dyn TimelineComponent + Send> {
432 fn from(sourced: Sourced) -> Self {
433 Box::new(sourced)
434 }
435}
436
437/// Trigger verbs on a built [`TimelineComponent`]. Blanket-implemented for every
438/// `T: TimelineComponent`. Multiple writers to one [`Event`] ⇒ EARLIEST wins
439/// (resolved in the place pass).
440///
441/// VERB ORDERING (`.sketch/01` A.6): a `.trigger_*` records its time against the
442/// wrapped node's OWN resolved interval (`abs_start` / `abs_start + len` /
443/// `abs_start + local`), where `abs_start` is whatever the parent hands the
444/// [`Triggered`] wrapper. So put the trigger OUTERMOST: either trigger a child
445/// the container positions (e.g. a `Sequence`/`Timeline` child —
446/// `Dialogue::builder()….trigger_at_start(e)`, the canonical usage; the
447/// container hands it its resolved start), or trigger THEN place
448/// (`x.trigger_at_start(e).at(5.0)`).
449///
450/// The inverted order `x.at(5.0).trigger_at_start(e)` wraps a `Placed` whose
451/// inner relative `5.0` is the CHILD's offset, not the wrapper's: the wrapper
452/// still receives its own `abs_start` and records the event THERE, ignoring the
453/// inner `5.0`. This is intentional — a `Triggered` fires at the interval IT is
454/// handed, and "fold a wrapped child's leading offset into my own trigger" would
455/// only handle a single immediate `Placed` and silently misbehave for any other
456/// nesting. See `triggered_over_placed_ignores_inner_offset` for the documented
457/// behaviour and `placed_over_triggered_keeps_offset` for the correct order.
458pub trait Triggers: TimelineComponent + Sized {
459 /// Records the [`Event`] at the wrapped node's resolved START (the
460 /// `abs_start` the parent hands this wrapper). See the trait-level VERB
461 /// ORDERING note: trigger outermost (or trigger a container-positioned
462 /// child), not `x.at(off).trigger_at_start(e)` — that records at the
463 /// wrapper's start, NOT accounting for the inner placement's `off`.
464 fn trigger_at_start(self, e: Event) -> Triggered<Self> {
465 Triggered {
466 child: self,
467 event: e,
468 kind: TriggerKind::Start,
469 }
470 }
471
472 fn trigger_at_end(self, e: Event) -> Triggered<Self> {
473 Triggered {
474 child: self,
475 event: e,
476 kind: TriggerKind::End,
477 }
478 }
479
480 /// At a local offset into the clip (an interior beat).
481 fn trigger_at(self, local: f32, e: Event) -> Triggered<Self> {
482 Triggered {
483 child: self,
484 event: e,
485 kind: TriggerKind::At(local),
486 }
487 }
488}
489
490impl<T: TimelineComponent + Sized> Triggers for T {}
491
492/// Buildless twin of [`Triggers`], over complete builders.
493pub trait TriggersBuilder: TimelineBuilder {
494 fn trigger_at_start(self, e: Event) -> Triggered<Self::Output> {
495 self.build_component().trigger_at_start(e)
496 }
497
498 fn trigger_at_end(self, e: Event) -> Triggered<Self::Output> {
499 self.build_component().trigger_at_end(e)
500 }
501
502 fn trigger_at(self, local: f32, e: Event) -> Triggered<Self::Output> {
503 self.build_component().trigger_at(local, e)
504 }
505}
506
507impl<B: TimelineBuilder> TriggersBuilder for B {}