Skip to main content

oxframe_components/
title.rs

1//! Title card, end card, and a list whose items appear one by one.
2
3use oxframe_2d::{
4    Accent, BaseProps, Group, Lg, Md, Muted, NodeBuilder, NodeId, Rect, Surface, Txt, pct,
5};
6use oxframe_core::Tween;
7
8use crate::host::{self, Host};
9
10fn plate(root: &Group, title: String, subtitle: Option<String>) -> Host {
11    root.add(
12        Rect::new()
13            .width(pct(64))
14            .height(260.0)
15            .radius(Lg)
16            .fill(Surface)
17            .elevation(Md)
18            .name("plate"),
19    );
20    root.add(Txt::new().text(title).h1().y(-28.0).name("title"));
21    let mut reveals = Vec::new();
22    if let Some(text) = subtitle {
23        let line = root.add(
24            Txt::new()
25                .text(text)
26                .caption()
27                .fill(Muted)
28                .y(56.0)
29                .opacity(0.0)
30                .name("subtitle"),
31        );
32        reveals.push(line.node());
33    }
34    Host::new(root.node(), reveals)
35}
36
37/// A centered title. `enter` shows the card and the title, `next` shows the subtitle.
38pub struct TitleCard {
39    host: Host,
40}
41
42impl TitleCard {
43    /// Starts a card. The subtitle stays hidden until [`next`](Self::next).
44    #[allow(clippy::new_ret_no_self)]
45    pub fn new(title: impl Into<String>) -> TitleCardBuilder {
46        TitleCardBuilder {
47            base: BaseProps::default(),
48            title: title.into(),
49            subtitle: None,
50        }
51    }
52
53    /// Fades the card in.
54    pub fn enter(&self) -> Tween<f64> {
55        self.host.enter()
56    }
57    /// Fades the subtitle in, or holds the card if there is none.
58    pub fn next(&self) -> Tween<f64> {
59        self.host.next()
60    }
61    /// Fades the card out.
62    pub fn exit(&self) -> Tween<f64> {
63        self.host.exit()
64    }
65}
66
67/// Builder for [`TitleCard`].
68#[must_use = "a builder does nothing until it is passed to `add`"]
69pub struct TitleCardBuilder {
70    base: BaseProps,
71    title: String,
72    subtitle: Option<String>,
73}
74
75impl TitleCardBuilder {
76    /// Second line, revealed by `next`.
77    pub fn subtitle(mut self, text: impl Into<String>) -> Self {
78        self.subtitle = Some(text.into());
79        self
80    }
81}
82
83impl NodeBuilder for TitleCardBuilder {
84    type Handle = TitleCard;
85    fn base(&mut self) -> &mut BaseProps {
86        &mut self.base
87    }
88    fn build(self, _: &oxframe_2d::SceneGraph, parent: NodeId) -> TitleCard {
89        let root = host::root(parent, self.base);
90        TitleCard {
91            host: plate(&root, self.title, self.subtitle),
92        }
93    }
94}
95
96/// Closing card. `next` reveals the note.
97pub struct EndCard {
98    host: Host,
99}
100
101impl EndCard {
102    /// Starts an end card.
103    #[allow(clippy::new_ret_no_self)]
104    pub fn new(title: impl Into<String>) -> EndCardBuilder {
105        EndCardBuilder {
106            base: BaseProps::default(),
107            title: title.into(),
108            note: None,
109        }
110    }
111
112    /// Fades the card in.
113    pub fn enter(&self) -> Tween<f64> {
114        self.host.enter()
115    }
116    /// Fades the note in.
117    pub fn next(&self) -> Tween<f64> {
118        self.host.next()
119    }
120    /// Fades the card out.
121    pub fn exit(&self) -> Tween<f64> {
122        self.host.exit()
123    }
124}
125
126/// Builder for [`EndCard`].
127#[must_use = "a builder does nothing until it is passed to `add`"]
128pub struct EndCardBuilder {
129    base: BaseProps,
130    title: String,
131    note: Option<String>,
132}
133
134impl EndCardBuilder {
135    /// Second line, revealed by `next`.
136    pub fn note(mut self, text: impl Into<String>) -> Self {
137        self.note = Some(text.into());
138        self
139    }
140}
141
142impl NodeBuilder for EndCardBuilder {
143    type Handle = EndCard;
144    fn base(&mut self) -> &mut BaseProps {
145        &mut self.base
146    }
147    fn build(self, _: &oxframe_2d::SceneGraph, parent: NodeId) -> EndCard {
148        let root = host::root(parent, self.base);
149        EndCard {
150            host: plate(&root, self.title, self.note),
151        }
152    }
153}
154
155/// Items appear one per `next`, after `enter` shows the stack.
156pub struct BulletList {
157    host: Host,
158}
159
160impl BulletList {
161    /// One row per string. Rows stay hidden until `next`.
162    #[allow(clippy::new_ret_no_self)]
163    pub fn new(items: impl IntoIterator<Item = impl Into<String>>) -> BulletListBuilder {
164        BulletListBuilder {
165            base: BaseProps::default(),
166            items: items.into_iter().map(Into::into).collect(),
167        }
168    }
169
170    /// Fades the list in. The rows are still hidden.
171    pub fn enter(&self) -> Tween<f64> {
172        self.host.enter()
173    }
174    /// Reveals the next row.
175    pub fn next(&self) -> Tween<f64> {
176        self.host.next()
177    }
178    /// Fades the list out.
179    pub fn exit(&self) -> Tween<f64> {
180        self.host.exit()
181    }
182    /// How many rows `next` can reveal.
183    pub fn len(&self) -> usize {
184        self.host.len()
185    }
186    /// No rows were given.
187    pub fn is_empty(&self) -> bool {
188        self.host.is_empty()
189    }
190}
191
192/// Builder for [`BulletList`].
193#[must_use = "a builder does nothing until it is passed to `add`"]
194pub struct BulletListBuilder {
195    base: BaseProps,
196    items: Vec<String>,
197}
198
199impl NodeBuilder for BulletListBuilder {
200    type Handle = BulletList;
201    fn base(&mut self) -> &mut BaseProps {
202        &mut self.base
203    }
204    fn build(self, _: &oxframe_2d::SceneGraph, parent: NodeId) -> BulletList {
205        let root = host::root(parent, self.base);
206        let n = self.items.len().max(1) as f64;
207        let pitch = 72.0;
208        let y0 = -(n - 1.0) * pitch / 2.0;
209        let mut reveals = Vec::new();
210        for (i, text) in self.items.into_iter().enumerate() {
211            let row: Group = root.add(
212                Group::new()
213                    .y(y0 + i as f64 * pitch)
214                    .opacity(0.0)
215                    .name(format!("item{}", i + 1)),
216            );
217            row.add(Rect::new().size(18.0).fill(Accent).x(-220.0).name("mark"));
218            row.add(Txt::new().text(text).body().x(16.0).name("line"));
219            reveals.push(row.node());
220        }
221        BulletList {
222            host: Host::new(root.node(), reveals),
223        }
224    }
225}