Skip to main content

bubbles/runtime/
event.rs

1//! [`DialogueEvent`], [`DialogueOption`], and [`MarkupSpan`] - the output types of the runner.
2
3/// A resolved inline markup span: a named annotation over a byte range in the
4/// stripped display text.
5///
6/// Spans are produced at runtime, after expression substitution, so [`start`]
7/// and [`length`] are byte offsets into the final [`DialogueEvent::Line::text`]
8/// / [`DialogueOption::text`] string.
9///
10/// The runtime assigns no meaning to span names or properties. Your game
11/// decides what `[wave]`, `[color value=red]`, or `[pause /]` means and how
12/// to render it.
13///
14/// [`start`]: MarkupSpan::start
15/// [`length`]: MarkupSpan::length
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct MarkupSpan {
18    /// The markup tag name, e.g. `wave` for `[wave]text[/wave]`.
19    pub name: String,
20    /// Byte offset of the first character of the spanned text in the display string.
21    pub start: usize,
22    /// Byte length of the spanned text. Zero for self-closing tags.
23    pub length: usize,
24    /// Zero or more `(key, value)` pairs from the tag, e.g. `[("value", "red")]`
25    /// for `[color value=red]`.
26    pub properties: Vec<(String, String)>,
27}
28
29/// Returns the id from a `#line:<id>` tag in `tags`, if any (first match wins).
30///
31/// This matches the id passed to [`crate::LineProvider`]. Use it to key voice-over or analytics
32/// without re-parsing [`DialogueEvent::Line::tags`] or [`DialogueOption::tags`].
33#[must_use]
34pub fn line_id_from_tags(tags: &[String]) -> Option<String> {
35    tags.iter()
36        .find_map(|t| t.strip_prefix("line:"))
37        .map(str::to_owned)
38        .filter(|s| !s.is_empty())
39}
40
41/// An option presented to the player.
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct DialogueOption {
44    /// Display text of the option (markup tags stripped, expressions evaluated).
45    pub text: String,
46    /// Whether this option is currently available (guards that evaluate to false make it unavailable).
47    pub available: bool,
48    /// If the option text was tagged with `#line:<id>`, the stable id (no `line:` prefix).
49    pub line_id: Option<String>,
50    /// Trailing `#tag` metadata.
51    pub tags: Vec<String>,
52    /// Inline markup spans over [`text`](DialogueOption::text), in source order.
53    /// Empty when the option text contains no markup tags.
54    pub spans: Vec<MarkupSpan>,
55}
56
57/// Events emitted by [`crate::Runner`] one at a time via [`crate::Runner::next_event`].
58#[non_exhaustive]
59#[derive(Debug, Clone, PartialEq)]
60pub enum DialogueEvent {
61    /// A node has started executing.
62    NodeStarted(String),
63    /// A line of dialogue ready to display.
64    Line {
65        /// Optional speaker name.
66        speaker: Option<String>,
67        /// Display text with all `{expr}` fragments evaluated and markup tags stripped.
68        text: String,
69        /// If the line was tagged with `#line:<id>`, the stable id (no `line:` prefix).
70        line_id: Option<String>,
71        /// Trailing `#tag` metadata.
72        tags: Vec<String>,
73        /// Inline markup spans over [`text`](DialogueEvent::Line::text), in source order.
74        /// Empty when the line contains no markup tags.
75        spans: Vec<MarkupSpan>,
76    },
77    /// A set of options for the player to choose from.
78    Options(Vec<DialogueOption>),
79    /// A host command to execute.
80    Command {
81        /// Command name.
82        name: String,
83        /// Arguments with `{expr}` substituted.
84        args: Vec<String>,
85        /// Trailing tags.
86        tags: Vec<String>,
87    },
88    /// The current node has finished.
89    NodeComplete(String),
90    /// All dialogue has finished.
91    DialogueComplete,
92}
93
94#[cfg(test)]
95#[path = "event_tests.rs"]
96mod tests;