Skip to main content

dear_imgui_rs/window/
key.rs

1use std::borrow::Cow;
2use std::ffi::{CStr, CString};
3use std::fmt;
4use std::hash::{Hash, Hasher};
5use std::sync::Arc;
6
7use thiserror::Error;
8
9use crate::{Id, sys};
10
11const ID_SEPARATOR: &str = "###";
12
13#[derive(Debug)]
14struct WindowKeyIdentity {
15    stable_id: Box<str>,
16    default_title: Box<str>,
17    docking_name: CString,
18    native_id: Id,
19}
20
21/// Stable Dear ImGui identity for a top-level window.
22///
23/// A key stores a default displayed title separately from the identity used by docking and INI
24/// persistence. Use the same key in [`DockLayout`](crate::DockLayout) and [`Ui::window`](crate::Ui::window)
25/// so a displayed-title change cannot silently create a different native window.
26///
27/// ```no_run
28/// # use dear_imgui_rs::*;
29/// # fn draw(ui: &Ui) -> Result<(), WindowKeyError> {
30/// let scene = WindowKey::new("scene", "Scene")?;
31/// let layout = DockLayout::tabs([&scene]);
32///
33/// ui.window(scene.label("Scene (Debug)"))
34///     .build(|| ui.text("The stable identity is still `scene`."));
35/// # let _ = layout;
36/// # Ok(())
37/// # }
38/// ```
39#[derive(Clone)]
40pub struct WindowKey {
41    identity: Arc<WindowKeyIdentity>,
42}
43
44impl WindowKey {
45    /// Create a validated stable identity and its default displayed title.
46    pub fn new(
47        stable_id: impl Into<String>,
48        default_title: impl Into<String>,
49    ) -> Result<Self, WindowKeyError> {
50        let stable_id = stable_id.into();
51        if stable_id.is_empty() {
52            return Err(WindowKeyError::EmptyStableId);
53        }
54        if stable_id.as_bytes().contains(&0) {
55            return Err(WindowKeyError::StableIdContainsNul);
56        }
57        if stable_id.contains(ID_SEPARATOR) {
58            return Err(WindowKeyError::StableIdContainsSeparator);
59        }
60
61        let mut docking_name = String::with_capacity(ID_SEPARATOR.len() + stable_id.len());
62        docking_name.push_str(ID_SEPARATOR);
63        docking_name.push_str(&stable_id);
64        let docking_name = CString::new(docking_name)
65            .expect("a validated window key must produce a valid native name");
66        // SAFETY: `docking_name` is readable and NUL-terminated. ImHashStr is context-free.
67        let native_id = Id::from(unsafe { sys::igImHashStr(docking_name.as_ptr(), 0, 0) });
68        if native_id.raw() == 0 {
69            return Err(WindowKeyError::NativeIdIsZero);
70        }
71
72        Ok(Self {
73            identity: Arc::new(WindowKeyIdentity {
74                stable_id: stable_id.into_boxed_str(),
75                default_title: default_title.into().into_boxed_str(),
76                docking_name,
77                native_id,
78            }),
79        })
80    }
81
82    /// Return the stable identity string.
83    pub fn stable_id(&self) -> &str {
84        &self.identity.stable_id
85    }
86
87    /// Return the default displayed title.
88    pub fn default_title(&self) -> &str {
89        &self.identity.default_title
90    }
91
92    /// Use a different displayed title without changing the stable identity.
93    pub fn label<'a>(&'a self, title: impl Into<Cow<'a, str>>) -> WindowLabel<'a> {
94        WindowLabel::Keyed {
95            key: self,
96            title: title.into(),
97        }
98    }
99
100    pub(crate) fn docking_name(&self) -> &CStr {
101        &self.identity.docking_name
102    }
103
104    pub(crate) fn native_id(&self) -> Id {
105        self.identity.native_id
106    }
107}
108
109impl PartialEq for WindowKey {
110    fn eq(&self, other: &Self) -> bool {
111        self.stable_id() == other.stable_id()
112    }
113}
114
115impl Eq for WindowKey {}
116
117impl Hash for WindowKey {
118    fn hash<H: Hasher>(&self, state: &mut H) {
119        self.stable_id().hash(state);
120    }
121}
122
123impl fmt::Debug for WindowKey {
124    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
125        formatter
126            .debug_struct("WindowKey")
127            .field("stable_id", &self.stable_id())
128            .field("default_title", &self.default_title())
129            .field("native_id", &self.native_id())
130            .finish()
131    }
132}
133
134impl From<&WindowKey> for WindowKey {
135    fn from(key: &WindowKey) -> Self {
136        key.clone()
137    }
138}
139
140/// Window label accepted by [`Ui::window`](crate::Ui::window).
141///
142/// Plain strings retain Dear ImGui's native `##`/`###` behavior. Labels created by
143/// [`WindowKey::label`] always append the validated stable identity after the displayed title.
144#[derive(Clone, Debug)]
145#[non_exhaustive]
146pub enum WindowLabel<'a> {
147    Plain(Cow<'a, str>),
148    Keyed {
149        key: &'a WindowKey,
150        title: Cow<'a, str>,
151    },
152}
153
154impl WindowLabel<'_> {
155    /// Return the displayed title supplied to Dear ImGui.
156    pub fn title(&self) -> &str {
157        match self {
158            Self::Plain(title) | Self::Keyed { title, .. } => title,
159        }
160    }
161
162    /// Return the stable key, when this label is keyed.
163    pub fn key(&self) -> Option<&WindowKey> {
164        match self {
165            Self::Plain(_) => None,
166            Self::Keyed { key, .. } => Some(key),
167        }
168    }
169}
170
171impl<'a> From<&'a str> for WindowLabel<'a> {
172    fn from(title: &'a str) -> Self {
173        Self::Plain(Cow::Borrowed(title))
174    }
175}
176
177impl<'a> From<&'a String> for WindowLabel<'a> {
178    fn from(title: &'a String) -> Self {
179        Self::Plain(Cow::Borrowed(title))
180    }
181}
182
183impl From<String> for WindowLabel<'_> {
184    fn from(title: String) -> Self {
185        Self::Plain(Cow::Owned(title))
186    }
187}
188
189impl<'a> From<Cow<'a, str>> for WindowLabel<'a> {
190    fn from(title: Cow<'a, str>) -> Self {
191        Self::Plain(title)
192    }
193}
194
195impl<'a> From<&'a WindowKey> for WindowLabel<'a> {
196    fn from(key: &'a WindowKey) -> Self {
197        Self::Keyed {
198            key,
199            title: Cow::Borrowed(key.default_title()),
200        }
201    }
202}
203
204/// Validation failure while creating a [`WindowKey`].
205#[derive(Clone, Copy, Debug, Error, PartialEq, Eq)]
206#[non_exhaustive]
207pub enum WindowKeyError {
208    #[error("a stable window ID cannot be empty")]
209    EmptyStableId,
210    #[error("a stable window ID cannot contain an interior NUL byte")]
211    StableIdContainsNul,
212    #[error("a stable window ID cannot contain Dear ImGui's `###` identity separator")]
213    StableIdContainsSeparator,
214    #[error("the stable window ID hashes to Dear ImGui's reserved zero ID")]
215    NativeIdIsZero,
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221
222    #[test]
223    fn rejects_ambiguous_or_invalid_stable_ids() {
224        assert_eq!(
225            WindowKey::new("", "Title"),
226            Err(WindowKeyError::EmptyStableId)
227        );
228        assert_eq!(
229            WindowKey::new("bad\0id", "Title"),
230            Err(WindowKeyError::StableIdContainsNul)
231        );
232        assert_eq!(
233            WindowKey::new("first###second", "Title"),
234            Err(WindowKeyError::StableIdContainsSeparator)
235        );
236    }
237
238    #[test]
239    fn title_changes_preserve_equality_and_native_identity() {
240        let scene = WindowKey::new("scene", "Scene").unwrap();
241        let renamed = WindowKey::new("scene", "Scene (Debug)").unwrap();
242        assert_eq!(scene, renamed);
243        assert_eq!(scene.native_id(), renamed.native_id());
244        assert_eq!(scene.docking_name().to_bytes(), b"###scene");
245    }
246}