oxframe_components/
title.rs1use 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
37pub struct TitleCard {
39 host: Host,
40}
41
42impl TitleCard {
43 #[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 pub fn enter(&self) -> Tween<f64> {
55 self.host.enter()
56 }
57 pub fn next(&self) -> Tween<f64> {
59 self.host.next()
60 }
61 pub fn exit(&self) -> Tween<f64> {
63 self.host.exit()
64 }
65}
66
67#[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 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
96pub struct EndCard {
98 host: Host,
99}
100
101impl EndCard {
102 #[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 pub fn enter(&self) -> Tween<f64> {
114 self.host.enter()
115 }
116 pub fn next(&self) -> Tween<f64> {
118 self.host.next()
119 }
120 pub fn exit(&self) -> Tween<f64> {
122 self.host.exit()
123 }
124}
125
126#[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 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
155pub struct BulletList {
157 host: Host,
158}
159
160impl BulletList {
161 #[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 pub fn enter(&self) -> Tween<f64> {
172 self.host.enter()
173 }
174 pub fn next(&self) -> Tween<f64> {
176 self.host.next()
177 }
178 pub fn exit(&self) -> Tween<f64> {
180 self.host.exit()
181 }
182 pub fn len(&self) -> usize {
184 self.host.len()
185 }
186 pub fn is_empty(&self) -> bool {
188 self.host.is_empty()
189 }
190}
191
192#[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, g: &oxframe_2d::SceneGraph, parent: NodeId) -> BulletList {
205 let root = host::root(parent, self.base);
206 const MARK: f64 = 18.0;
209 const INDENT: f64 = 34.0;
210 const GAP: f64 = 24.0;
211 let wrap = (g.frame_size().0 * 0.7 - INDENT).max(200.0);
212 let probe = root.add(Txt::new().text("Ag").body().opacity(0.0));
213 let line_height = probe.size().1.max(MARK);
214 probe.remove();
215 let mut rows = Vec::new();
216 for (i, text) in self.items.into_iter().enumerate() {
217 let row: Group = root.add(Group::new().opacity(0.0).name(format!("item{}", i + 1)));
218 let mark = row.add(Rect::new().size(MARK).fill(Accent).name("mark"));
219 let line = row.add(
220 Txt::new()
221 .text(text)
222 .body()
223 .max_width(wrap)
224 .anchor((-1.0, -1.0))
225 .name("line"),
226 );
227 let (w, h) = line.size();
228 rows.push((row, mark, line, w, h.max(line_height)));
229 }
230 let widest = rows.iter().map(|r| r.3).fold(0.0, f64::max);
231 let left = -(INDENT + widest) / 2.0;
232 let total: f64 =
233 rows.iter().map(|r| r.4).sum::<f64>() + GAP * rows.len().saturating_sub(1) as f64;
234 let mut top = -total / 2.0;
235 let mut reveals = Vec::new();
236 for (row, mark, line, _, h) in rows {
237 row.y().set(top);
238 line.x().set(left + INDENT);
239 mark.x().set(left + MARK / 2.0);
240 mark.y().set(line_height / 2.0);
242 top += h + GAP;
243 reveals.push(row.node());
244 }
245 BulletList {
246 host: Host::new(root.node(), reveals),
247 }
248 }
249}
250
251#[cfg(test)]
252mod tests {
253 use oxframe_2d::{SceneGraph, View};
254
255 use super::*;
256
257 #[test]
258 fn a_long_row_wraps_inside_the_frame_and_rows_do_not_overlap() {
259 let graph = SceneGraph::new(1920, 1080);
260 graph.enter(|| {
261 View::current().add(BulletList::new([
262 "Short",
263 "A row much longer than the frame is wide, so that it has to wrap onto a second \
264 line or it would run off both edges of the picture",
265 "Last",
266 ]));
267 });
268 let lines: Vec<_> = graph
269 .inspect_nodes()
270 .into_iter()
271 .filter(|n| n.kind == "Txt")
272 .map(|n| n.bounds)
273 .collect();
274 assert_eq!(lines.len(), 3);
275 for b in &lines {
276 assert!(
277 b[0] >= -960.0 && b[2] <= 960.0,
278 "row leaves the frame: {b:?}"
279 );
280 }
281 assert!(lines[1][2] - lines[1][0] <= 1920.0 * 0.7, "{:?}", lines[1]);
282 assert!(
283 lines[1][3] - lines[1][1] > lines[0][3] - lines[0][1],
284 "the long row wraps"
285 );
286 for pair in lines.windows(2) {
287 assert!(pair[0][3] <= pair[1][1], "rows overlap: {pair:?}");
288 }
289 }
290}