Skip to main content

cranpose_ui/text/
shared_plain_text.rs

1//! Shared copies of plain, style-free strings that draw scopes lower every
2//! frame, pooled per thread.
3
4use std::rc::Rc;
5
6use cranpose_text::{AnnotatedString, RenderString};
7
8/// Returns the shared [`AnnotatedString`] for a plain, style-free string,
9/// reusing the copy made on an earlier frame when the content matches.
10///
11/// Draw scopes lower every `draw_text*` call through an `AnnotatedString` on
12/// every frame — once to measure and once to emit — and a HUD label or score
13/// counter has the same characters frame after frame. Without this pool each
14/// pass re-copied a string it had copied the frame before, only to hash it and
15/// hit a layout cache that is keyed by content anyway. Entries are verified by
16/// content on hit, so a hash collision costs a fresh copy, never wrong text.
17/// The pool keeps the strings that something still holds, as
18/// [`cranpose_ui_graphics::ContentPool`] does.
19pub fn shared_plain_annotated_string(text: &str) -> Rc<AnnotatedString> {
20    with_shared_plain_text(text, |shared| Rc::clone(&shared.annotated))
21}
22
23/// The [`RenderString`] of [`shared_plain_annotated_string`]'s string for
24/// `text`, converted once while the pool holds it, so a canvas redrawing the
25/// same characters hands the renderer the same allocation every frame.
26pub fn shared_plain_render_string(text: &str) -> std::sync::Arc<RenderString> {
27    with_shared_plain_text(text, |shared| {
28        std::sync::Arc::clone(
29            shared
30                .render
31                .get_or_init(|| std::sync::Arc::new(shared.annotated.render_string())),
32        )
33    })
34}
35
36pub(crate) struct SharedPlainText {
37    annotated: Rc<AnnotatedString>,
38    render: std::cell::OnceCell<std::sync::Arc<RenderString>>,
39}
40
41impl cranpose_ui_graphics::PooledValue for SharedPlainText {
42    fn is_held(&self) -> bool {
43        self.annotated.is_held()
44            || self
45                .render
46                .get()
47                .is_some_and(|render| std::sync::Arc::strong_count(render) > 1)
48    }
49}
50
51fn with_shared_plain_text<R>(text: &str, read: impl FnOnce(&SharedPlainText) -> R) -> R {
52    use std::hash::{Hash, Hasher};
53
54    let mut hasher = cranpose_ui_graphics::FxHasher::default();
55    text.hash(&mut hasher);
56    crate::thread_state::with(|state| {
57        state.shared_plain_texts.borrow_mut().with(
58            hasher.finish(),
59            |shared| shared.annotated.text == text,
60            || SharedPlainText {
61                annotated: Rc::new(AnnotatedString::new(text.to_owned())),
62                render: std::cell::OnceCell::new(),
63            },
64            read,
65        )
66    })
67}
68
69#[cfg(test)]
70#[path = "tests/shared_plain_text_tests.rs"]
71mod tests;