dear_imgui_rs/window/
key.rs1use 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#[derive(Clone)]
40pub struct WindowKey {
41 identity: Arc<WindowKeyIdentity>,
42}
43
44impl WindowKey {
45 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 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 pub fn stable_id(&self) -> &str {
84 &self.identity.stable_id
85 }
86
87 pub fn default_title(&self) -> &str {
89 &self.identity.default_title
90 }
91
92 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#[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 pub fn title(&self) -> &str {
157 match self {
158 Self::Plain(title) | Self::Keyed { title, .. } => title,
159 }
160 }
161
162 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#[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}