Skip to main content

oxframe_components/
code.rs

1//! A code block, a caption, and a list of focus steps.
2
3use std::cell::Cell;
4use std::rc::Rc;
5
6use oxframe_2d::{BaseProps, Code, Muted, Node, NodeBuilder, NodeId, Normal, Txt};
7use oxframe_core::{Seconds, Tween, all};
8
9use crate::host;
10
11/// Code with a caption. `enter` shows the code. Each `next` focuses one step
12/// and sets the caption.
13pub struct CodeExplainer {
14    root: Node,
15    code: Code,
16    caption: Txt,
17    steps: Rc<Vec<(usize, usize, String)>>,
18    cursor: Rc<Cell<usize>>,
19}
20
21impl CodeExplainer {
22    /// The source is shown as Rust. Add steps with [`step`](CodeExplainerBuilder::step).
23    #[allow(clippy::new_ret_no_self)]
24    pub fn new(code: impl Into<String>) -> CodeExplainerBuilder {
25        CodeExplainerBuilder {
26            base: BaseProps::default(),
27            code: code.into(),
28            steps: Vec::new(),
29        }
30    }
31
32    /// Fades the block in.
33    pub fn enter(&self) -> Tween<f64> {
34        self.root.fade_in()
35    }
36
37    /// Focuses the next step and shows its caption.
38    pub fn next(&self) -> impl std::future::Future<Output = ()> + 'static {
39        let n = self.steps.len();
40        let i = self.cursor.get();
41        let (first, last, text) = if n == 0 {
42            (0, 0, String::new())
43        } else {
44            let step = &self.steps[i.min(n - 1)];
45            if i < n {
46                self.cursor.set(i + 1);
47            }
48            (step.0, step.1, step.2.clone())
49        };
50        self.caption.text().set(text);
51        let focus = self.code.focus_lines(first, last, Normal.seconds());
52        let fade = if i == 0 {
53            self.caption.fade_in()
54        } else {
55            self.caption.opacity().to(1.0, Normal)
56        };
57        async move {
58            all((focus, fade)).await;
59        }
60    }
61
62    /// Fades the block out.
63    pub fn exit(&self) -> Tween<f64> {
64        self.root.fade_out()
65    }
66
67    /// How many steps were declared.
68    pub fn len(&self) -> usize {
69        self.steps.len()
70    }
71    /// No steps were declared.
72    pub fn is_empty(&self) -> bool {
73        self.steps.is_empty()
74    }
75}
76
77/// Builder for [`CodeExplainer`].
78#[must_use = "a builder does nothing until it is passed to `add`"]
79pub struct CodeExplainerBuilder {
80    base: BaseProps,
81    code: String,
82    steps: Vec<(usize, usize, String)>,
83}
84
85impl CodeExplainerBuilder {
86    /// A focus range (1-based, inclusive) and the caption `next` shows with it.
87    pub fn step(mut self, first: usize, last: usize, caption: impl Into<String>) -> Self {
88        self.steps.push((first, last, caption.into()));
89        self
90    }
91}
92
93impl NodeBuilder for CodeExplainerBuilder {
94    type Handle = CodeExplainer;
95    fn base(&mut self) -> &mut BaseProps {
96        &mut self.base
97    }
98    fn build(self, _: &oxframe_2d::SceneGraph, parent: NodeId) -> CodeExplainer {
99        let root = host::root(parent, self.base);
100        let code = root.add(
101            Code::new()
102                .code(self.code)
103                .language("rust")
104                .font_size(26.0)
105                .y(-24.0)
106                .name("code"),
107        );
108        let (_, h) = code.size();
109        let caption = root.add(
110            Txt::new()
111                .text(" ")
112                .caption()
113                .fill(Muted)
114                .y(h / 2.0 + 36.0)
115                .opacity(0.0)
116                .name("caption"),
117        );
118        CodeExplainer {
119            root: root.node(),
120            code,
121            caption,
122            steps: Rc::new(self.steps),
123            cursor: Rc::new(Cell::new(0)),
124        }
125    }
126}