Skip to main content

bevy_brink/
transcript.rs

1//! Auto-rendered per-flow transcript component.
2//!
3//! Attach a [`BrinkTranscript<M>`] to a fulfilled flow entity (alongside
4//! the usual [`BrinkFlow`](crate::BrinkFlow), [`BrinkProgram`](crate::BrinkProgram),
5//! [`BrinkLocale`](crate::BrinkLocale)) and the plugin will keep it
6//! in sync with `flow.inner.transcript()` — re-rendering whenever
7//! the flow grows, the locale handle changes, or the line-tables
8//! asset content changes (hot-reload).
9//!
10//! This is purely a convenience: consumers can read structural output
11//! parts directly via `flow.inner.transcript()` and call
12//! `brink_runtime::transcript::render_transcript` themselves.
13
14use std::marker::PhantomData;
15
16use bevy_asset::{AssetEvent, Assets};
17use bevy_ecs::change_detection::{DetectChanges, Ref};
18use bevy_ecs::component::Component;
19use bevy_ecs::message::MessageReader;
20use bevy_ecs::system::{Query, Res};
21
22use crate::asset::{BrinkProgram, LineTablesAsset, ProgramAsset};
23use crate::flow::BrinkFlow;
24use crate::line_tables::BrinkLocale;
25
26/// Cached, locale-resolved view of a flow's transcript.
27///
28/// Inserted by the consumer (opt-in) on a flow entity. The plugin's
29/// `refresh_transcripts<M>` system re-renders `lines` when:
30///
31/// - The flow's `transcript_len()` differs from `cached_len` (the flow
32///   has produced new output since the last refresh).
33/// - The `BrinkLocale<M>` handle on this entity changed (locale swap).
34/// - An `AssetEvent::Modified<LineTablesAsset>` fired since last refresh
35///   (the current locale's content was hot-reloaded).
36///
37/// Each entry in `lines` is `(text, tags)` for one resolved output line,
38/// as produced by [`brink_runtime::transcript::render_transcript`].
39#[derive(Component)]
40pub struct BrinkTranscript<M: Send + Sync + 'static = ()> {
41    pub lines: Vec<(String, Vec<String>)>,
42    cached_len: usize,
43    _marker: PhantomData<fn() -> M>,
44}
45
46impl<M: Send + Sync + 'static> Default for BrinkTranscript<M> {
47    fn default() -> Self {
48        Self {
49            lines: Vec::new(),
50            cached_len: 0,
51            _marker: PhantomData,
52        }
53    }
54}
55
56impl<M: Send + Sync + 'static> BrinkTranscript<M> {
57    /// Concatenate every line's text with `\n` between entries.
58    /// `render_transcript` returns lines stripped of their trailing
59    /// newline, so we re-insert one between consecutive entries.
60    #[must_use]
61    pub fn text(&self) -> String {
62        // Capacity hint: sum of line lengths + (n-1) separators.
63        let n = self.lines.len();
64        let total: usize = self.lines.iter().map(|(s, _)| s.len()).sum();
65        let mut out = String::with_capacity(total + n.saturating_sub(1));
66        for (i, (text, _)) in self.lines.iter().enumerate() {
67            if i > 0 {
68                out.push('\n');
69            }
70            out.push_str(text);
71        }
72        out
73    }
74}
75
76/// Plugin-managed system: re-render `BrinkTranscript<M>` for any flow
77/// entity whose transcript has grown, whose locale handle changed, or
78/// whose locale's `LineTablesAsset` content was hot-reloaded.
79#[expect(
80    clippy::needless_pass_by_value,
81    clippy::type_complexity,
82    reason = "bevy systems take Res/Query by value and have complex query tuples"
83)]
84pub fn refresh_transcripts<M: Send + Sync + 'static>(
85    mut events: MessageReader<AssetEvent<LineTablesAsset>>,
86    mut flows: Query<(
87        &BrinkFlow<M>,
88        &BrinkProgram<M>,
89        Ref<BrinkLocale<M>>,
90        &mut BrinkTranscript<M>,
91    )>,
92    programs: Res<Assets<ProgramAsset>>,
93    line_tables: Res<Assets<LineTablesAsset>>,
94) {
95    // If any LineTablesAsset content changed this tick, every flow
96    // potentially needs a re-render against the new tables. We don't
97    // route by handle — there's typically one locale per marker.
98    let any_locale_modified = events
99        .read()
100        .any(|ev| matches!(ev, AssetEvent::Modified { .. }));
101
102    for (flow, program_h, locale_h, mut transcript) in &mut flows {
103        let current_len = flow.inner.transcript_len();
104        let locale_changed = locale_h.is_changed();
105        let needs_refresh =
106            current_len != transcript.cached_len || locale_changed || any_locale_modified;
107        if !needs_refresh {
108            continue;
109        }
110
111        let Some(program_asset) = programs.get(&program_h.handle) else {
112            continue;
113        };
114        let Some(lt_asset) = line_tables.get(&locale_h.handle) else {
115            continue;
116        };
117
118        transcript.lines = brink_runtime::transcript::render_transcript(
119            flow.inner.transcript(),
120            &program_asset.program,
121            &lt_asset.tables,
122            None,
123            flow.inner.fragments(),
124        );
125        transcript.cached_len = current_len;
126    }
127}