1use std::path::PathBuf;
2use std::ptr;
3
4use crate::ini_settings::{IniSettingsRetention, IniSettingsRetentionError};
5use crate::sys;
6
7use super::Context;
8use super::binding::{CTX_MUTEX, with_bound_context};
9
10impl Context {
11 #[doc(alias = "Platform_SessionDate")]
15 #[doc(alias = "ConfigIniSettingsSaveLastUsedDate")]
16 #[doc(alias = "ConfigIniSettingsAutoDiscardMonths")]
17 pub fn ini_settings_retention(
18 &self,
19 ) -> Result<IniSettingsRetention, IniSettingsRetentionError> {
20 let _guard = CTX_MUTEX.lock();
21 unsafe {
22 let platform_io = self.platform_io_ptr("Context::ini_settings_retention()");
23 let io = self.io_ptr("Context::ini_settings_retention()");
24 IniSettingsRetention::from_raw(
25 (*platform_io).Platform_SessionDate,
26 (*io).ConfigIniSettingsSaveLastUsedDate,
27 (*io).ConfigIniSettingsAutoDiscardMonths,
28 )
29 }
30 }
31
32 #[doc(alias = "Platform_SessionDate")]
55 #[doc(alias = "ConfigIniSettingsSaveLastUsedDate")]
56 #[doc(alias = "ConfigIniSettingsAutoDiscardMonths")]
57 pub fn set_ini_settings_retention(
58 &mut self,
59 retention: IniSettingsRetention,
60 ) -> Result<(), IniSettingsRetentionError> {
61 retention.validate()?;
62 let (session_date, save_last_used_date, auto_discard_months) = retention.raw_parts();
63
64 let _guard = CTX_MUTEX.lock();
65 if unsafe { (*self.raw).FrameCount } != 0 {
66 return Err(IniSettingsRetentionError::LockedAfterFirstFrame);
67 }
68 if unsafe { (*self.raw).SettingsLoaded } {
69 return Err(IniSettingsRetentionError::LockedAfterSettingsLoad);
70 }
71
72 unsafe {
73 let platform_io = self.platform_io_ptr("Context::set_ini_settings_retention()");
74 let io = self.io_ptr("Context::set_ini_settings_retention()");
75
76 (*io).ConfigIniSettingsAutoDiscardMonths = 0;
80 (*platform_io).Platform_SessionDate = session_date;
81 (*io).ConfigIniSettingsSaveLastUsedDate = save_last_used_date;
82 (*io).ConfigIniSettingsAutoDiscardMonths = auto_discard_months;
83 }
84 Ok(())
85 }
86
87 pub fn set_ini_filename<P: Into<PathBuf>>(
93 &mut self,
94 filename: Option<P>,
95 ) -> crate::error::ImGuiResult<()> {
96 use crate::error::SafeStringConversion;
97 let _guard = CTX_MUTEX.lock();
98
99 self.ini_filename = match filename {
100 Some(f) => Some(f.into().to_string_lossy().to_cstring_safe()?),
101 None => None,
102 };
103
104 unsafe {
105 let io = self.io_ptr("Context::set_ini_filename()");
106 let ptr = self
107 .ini_filename
108 .as_ref()
109 .map(|s| s.as_ptr())
110 .unwrap_or(ptr::null());
111 (*io).IniFilename = ptr;
112 }
113 Ok(())
114 }
115
116 pub fn set_log_filename<P: Into<PathBuf>>(
124 &mut self,
125 filename: Option<P>,
126 ) -> crate::error::ImGuiResult<()> {
127 use crate::error::SafeStringConversion;
128 let _guard = CTX_MUTEX.lock();
129
130 self.log_filename = match filename {
131 Some(f) => Some(f.into().to_string_lossy().to_cstring_safe()?),
132 None => None,
133 };
134
135 unsafe {
136 let io = self.io_ptr("Context::set_log_filename()");
137 let ptr = self
138 .log_filename
139 .as_ref()
140 .map(|s| s.as_ptr())
141 .unwrap_or(ptr::null());
142 (*io).LogFilename = ptr;
143 }
144 Ok(())
145 }
146
147 pub fn set_platform_name<S: Into<String>>(
155 &mut self,
156 name: Option<S>,
157 ) -> crate::error::ImGuiResult<()> {
158 use crate::error::SafeStringConversion;
159 let _guard = CTX_MUTEX.lock();
160
161 self.platform_name = match name {
162 Some(n) => Some(n.into().to_cstring_safe()?),
163 None => None,
164 };
165
166 unsafe {
167 let io = self.io_ptr("Context::set_platform_name()");
168 let ptr = self
169 .platform_name
170 .as_ref()
171 .map(|s| s.as_ptr())
172 .unwrap_or(ptr::null());
173 (*io).BackendPlatformName = ptr;
174 }
175 Ok(())
176 }
177
178 pub fn set_renderer_name<S: Into<String>>(
186 &mut self,
187 name: Option<S>,
188 ) -> crate::error::ImGuiResult<()> {
189 use crate::error::SafeStringConversion;
190 let _guard = CTX_MUTEX.lock();
191
192 self.renderer_name = match name {
193 Some(n) => Some(n.into().to_cstring_safe()?),
194 None => None,
195 };
196
197 unsafe {
198 let io = self.io_ptr("Context::set_renderer_name()");
199 let ptr = self
200 .renderer_name
201 .as_ref()
202 .map(|s| s.as_ptr())
203 .unwrap_or(ptr::null());
204 (*io).BackendRendererName = ptr;
205 }
206 Ok(())
207 }
208
209 #[doc(alias = "LoadIniSettingsFromMemory")]
213 pub fn load_ini_settings(&mut self, data: &str) {
214 let _guard = CTX_MUTEX.lock();
215 unsafe {
216 with_bound_context(self.raw, || {
217 sys::igLoadIniSettingsFromMemory(data.as_ptr() as *const _, data.len());
218 });
219 }
220 }
221
222 #[doc(alias = "SaveIniSettingsToMemory")]
224 pub fn save_ini_settings(&mut self, buf: &mut String) {
225 let _guard = CTX_MUTEX.lock();
226 unsafe {
227 with_bound_context(self.raw, || {
228 let mut out_ini_size: usize = 0;
229 let data_ptr = sys::igSaveIniSettingsToMemory(&mut out_ini_size as *mut usize);
230 if data_ptr.is_null() || out_ini_size == 0 {
231 return;
232 }
233
234 let mut bytes = std::slice::from_raw_parts(data_ptr as *const u8, out_ini_size);
235 if bytes.last() == Some(&0) {
236 bytes = &bytes[..bytes.len().saturating_sub(1)];
237 }
238 buf.push_str(&String::from_utf8_lossy(bytes));
239 });
240 }
241 }
242
243 #[cfg(not(target_arch = "wasm32"))]
249 #[doc(alias = "LoadIniSettingsFromDisk")]
250 pub fn load_ini_settings_from_disk<P: Into<PathBuf>>(
251 &mut self,
252 filename: P,
253 ) -> crate::error::ImGuiResult<()> {
254 use crate::error::SafeStringConversion;
255 let _guard = CTX_MUTEX.lock();
256 let cstr = filename.into().to_string_lossy().to_cstring_safe()?;
257 unsafe {
258 with_bound_context(self.raw, || {
259 sys::igLoadIniSettingsFromDisk(cstr.as_ptr());
260 });
261 }
262 Ok(())
263 }
264
265 #[cfg(not(target_arch = "wasm32"))]
271 #[doc(alias = "SaveIniSettingsToDisk")]
272 pub fn save_ini_settings_to_disk<P: Into<PathBuf>>(
273 &mut self,
274 filename: P,
275 ) -> crate::error::ImGuiResult<()> {
276 use crate::error::SafeStringConversion;
277 let _guard = CTX_MUTEX.lock();
278 let cstr = filename.into().to_string_lossy().to_cstring_safe()?;
279 unsafe {
280 with_bound_context(self.raw, || {
281 sys::igSaveIniSettingsToDisk(cstr.as_ptr());
282 });
283 }
284 Ok(())
285 }
286}
287
288#[cfg(test)]
289mod retention_tests {
290 use std::num::NonZeroU16;
291
292 use crate::IniSessionDate;
293
294 use super::*;
295
296 fn raw_retention(context: &Context) -> (i32, bool, i32) {
297 unsafe {
298 let platform_io = context.platform_io_ptr("raw_retention()");
299 let io = context.io_ptr("raw_retention()");
300 (
301 (*platform_io).Platform_SessionDate,
302 (*io).ConfigIniSettingsSaveLastUsedDate,
303 (*io).ConfigIniSettingsAutoDiscardMonths,
304 )
305 }
306 }
307
308 #[test]
309 fn session_date_enforces_packed_and_gregorian_boundaries() {
310 let earliest = IniSessionDate::new(2001, 1, 1).expect("earliest packed date");
311 let latest = IniSessionDate::new(2127, 12, 31).expect("latest packed date");
312 assert_eq!(earliest.as_yyyymmdd(), 20_010_101);
313 assert_eq!(latest.as_yyyymmdd(), 21_271_231);
314 assert_eq!(earliest.max_auto_discard_months(), 0);
315 assert_eq!(latest.max_auto_discard_months(), 1_523);
316 assert_eq!(
317 IniSessionDate::try_from(20_240_229),
318 Ok(IniSessionDate::new(2024, 2, 29).unwrap())
319 );
320
321 for invalid in [
322 IniSessionDate::new(2000, 12, 31),
323 IniSessionDate::new(2128, 1, 1),
324 IniSessionDate::new(2023, 2, 29),
325 IniSessionDate::new(2100, 2, 29),
326 IniSessionDate::new(2026, 13, 1),
327 IniSessionDate::new(2026, 12, 32),
328 ] {
329 assert!(invalid.is_err());
330 }
331 assert!(IniSessionDate::try_from(0).is_err());
332 }
333
334 #[test]
335 fn context_retention_update_is_date_bounded_and_atomic() {
336 let _guard = crate::test_support::imgui_context_guard();
337 let mut context = Context::create();
338 let date = IniSessionDate::new(2026, 7, 30).unwrap();
339 let six_months = NonZeroU16::new(6).unwrap();
340 let six_month_retention = IniSettingsRetention::AutoDiscard {
341 session_date: date,
342 months: six_months,
343 };
344
345 context
346 .set_ini_settings_retention(six_month_retention)
347 .expect("valid retention policy");
348 assert_eq!(
349 context
350 .ini_settings_retention()
351 .expect("valid native state"),
352 six_month_retention
353 );
354 assert_eq!(raw_retention(&context), (20_260_730, true, 6));
355
356 let before = raw_retention(&context);
357 let earliest = IniSessionDate::new(2001, 1, 1).unwrap();
358 let too_old = context
359 .set_ini_settings_retention(IniSettingsRetention::AutoDiscard {
360 session_date: earliest,
361 months: six_months,
362 })
363 .expect_err("discard must not underflow the packed year");
364 assert!(matches!(
365 too_old,
366 IniSettingsRetentionError::RetentionUnderflowsSessionDate {
367 months,
368 max_months: 0,
369 session_date,
370 } if months == six_months && session_date == earliest
371 ));
372 assert_eq!(raw_retention(&context), before);
373
374 let latest = IniSessionDate::new(2127, 12, 31).unwrap();
375 let maximum = NonZeroU16::new(1_523).unwrap();
376 context
377 .set_ini_settings_retention(IniSettingsRetention::AutoDiscard {
378 session_date: latest,
379 months: maximum,
380 })
381 .expect("latest date accepts the global maximum");
382 let earlier = IniSessionDate::new(2002, 1, 31).unwrap();
383 let twelve_months = NonZeroU16::new(12).unwrap();
384 context
385 .set_ini_settings_retention(IniSettingsRetention::AutoDiscard {
386 session_date: earlier,
387 months: twelve_months,
388 })
389 .expect("one Context transaction may reduce the date and month limit together");
390 assert_eq!(raw_retention(&context), (20_020_131, true, 12));
391
392 let record_only = IniSettingsRetention::RecordLastUsed {
393 session_date: Some(earlier),
394 };
395 context
396 .set_ini_settings_retention(record_only)
397 .expect("last-used recording is independent of automatic cleanup");
398 assert_eq!(raw_retention(&context), (20_020_131, true, 0));
399 assert_eq!(
400 context.ini_settings_retention().expect("record-only state"),
401 record_only
402 );
403
404 let disabled_with_date = IniSettingsRetention::Disabled {
405 session_date: Some(earlier),
406 };
407 context
408 .set_ini_settings_retention(disabled_with_date)
409 .expect("a platform date is independent of retention");
410 assert_eq!(raw_retention(&context), (20_020_131, false, 0));
411 assert_eq!(
412 context
413 .ini_settings_retention()
414 .expect("disabled state with a platform date"),
415 disabled_with_date
416 );
417
418 context
419 .set_ini_settings_retention(IniSettingsRetention::Disabled { session_date: None })
420 .expect("disabling date retention never needs a date");
421 assert_eq!(raw_retention(&context), (0, false, 0));
422 }
423
424 #[test]
425 fn context_retention_accepts_the_dynamic_month_limit() {
426 let _guard = crate::test_support::imgui_context_guard();
427 let mut context = Context::create();
428 let date = IniSessionDate::new(2001, 2, 28).unwrap();
429 let one_month = NonZeroU16::new(1).unwrap();
430 let retention = IniSettingsRetention::AutoDiscard {
431 session_date: date,
432 months: one_month,
433 };
434
435 context
436 .set_ini_settings_retention(retention)
437 .expect("one month reaches January 2001 without underflow");
438 assert_eq!(raw_retention(&context), (20_010_228, true, 1));
439
440 let two_months = NonZeroU16::new(2).unwrap();
441 assert!(matches!(
442 context.set_ini_settings_retention(IniSettingsRetention::AutoDiscard {
443 session_date: date,
444 months: two_months,
445 }),
446 Err(IniSettingsRetentionError::RetentionUnderflowsSessionDate { max_months: 1, .. })
447 ));
448 assert_eq!(raw_retention(&context), (20_010_228, true, 1));
449 }
450
451 #[test]
452 fn context_retention_locks_after_the_first_frame() {
453 let _guard = crate::test_support::imgui_context_guard();
454 let mut context = Context::create();
455 assert!(context.font_atlas().build());
456 context.io_mut().set_display_size([128.0, 128.0]);
457 context.io_mut().set_delta_time(1.0 / 60.0);
458 let _ui = context.frame();
459 drop(context.render());
460
461 let before = raw_retention(&context);
462 assert!(matches!(
463 context
464 .set_ini_settings_retention(IniSettingsRetention::Disabled { session_date: None }),
465 Err(IniSettingsRetentionError::LockedAfterFirstFrame)
466 ));
467 assert_eq!(raw_retention(&context), before);
468 }
469
470 #[test]
471 fn context_retention_locks_after_settings_load() {
472 let _guard = crate::test_support::imgui_context_guard();
473 let mut context = Context::create();
474 context.load_ini_settings("[Window][Example]\nPos=0,0\nSize=100,100\n");
475
476 let before = raw_retention(&context);
477 assert!(matches!(
478 context
479 .set_ini_settings_retention(IniSettingsRetention::Disabled { session_date: None }),
480 Err(IniSettingsRetentionError::LockedAfterSettingsLoad)
481 ));
482 assert_eq!(raw_retention(&context), before);
483 }
484
485 #[test]
486 fn context_retention_reports_invalid_native_state() {
487 let _guard = crate::test_support::imgui_context_guard();
488 let context = Context::create();
489 let (platform_io, io) = (
490 context.platform_io_ptr("invalid native retention test"),
491 context.io_ptr("invalid native retention test"),
492 );
493
494 unsafe {
495 (*platform_io).Platform_SessionDate = 21_280_101;
496 (*io).ConfigIniSettingsSaveLastUsedDate = true;
497 (*io).ConfigIniSettingsAutoDiscardMonths = 0;
498 }
499 assert!(matches!(
500 context.ini_settings_retention(),
501 Err(IniSettingsRetentionError::InvalidNativeSessionDate { raw: 21_280_101 })
502 ));
503
504 unsafe {
505 (*platform_io).Platform_SessionDate = 20_260_730;
506 (*io).ConfigIniSettingsAutoDiscardMonths = -1;
507 }
508 assert!(matches!(
509 context.ini_settings_retention(),
510 Err(IniSettingsRetentionError::InvalidNativeAutoDiscardMonths { raw: -1 })
511 ));
512
513 unsafe {
514 (*io).ConfigIniSettingsSaveLastUsedDate = false;
515 (*io).ConfigIniSettingsAutoDiscardMonths = 1;
516 }
517 assert!(matches!(
518 context.ini_settings_retention(),
519 Err(IniSettingsRetentionError::InvalidNativeState { .. })
520 ));
521
522 unsafe {
523 (*platform_io).Platform_SessionDate = 0;
524 (*io).ConfigIniSettingsSaveLastUsedDate = true;
525 }
526 assert!(matches!(
527 context.ini_settings_retention(),
528 Err(IniSettingsRetentionError::InvalidNativeState { .. })
529 ));
530 }
531
532 #[test]
533 fn context_retention_round_trips_recording_without_a_session_date() {
534 let _guard = crate::test_support::imgui_context_guard();
535 let mut context = Context::create();
536 let raw = IniSettingsRetention::RecordLastUsed { session_date: None };
537
538 context
539 .set_ini_settings_retention(raw)
540 .expect("date recording may be configured without a platform clock");
541 assert_eq!(raw_retention(&context), (0, true, 0));
542
543 let observed = context
544 .ini_settings_retention()
545 .expect("the native state is representable");
546 assert_eq!(observed, raw);
547 context
548 .set_ini_settings_retention(observed)
549 .expect("reading and writing the policy is lossless");
550 assert_eq!(raw_retention(&context), (0, true, 0));
551 }
552
553 #[test]
554 fn auto_discard_removes_undated_entries_on_load() {
555 let _guard = crate::test_support::imgui_context_guard();
556 let mut context = Context::create();
557 context
558 .set_ini_settings_retention(IniSettingsRetention::AutoDiscard {
559 session_date: IniSessionDate::new(2026, 7, 30).unwrap(),
560 months: NonZeroU16::new(6).unwrap(),
561 })
562 .unwrap();
563
564 context.load_ini_settings(
565 "[Window][Undated]\nPos=1,1\nSize=100,100\nCollapsed=0\n\n\
566 [Window][Recent]\nPos=2,2\nSize=100,100\nCollapsed=0\nLastUsed=20260730\n\n",
567 );
568
569 let mut saved = String::new();
570 context.save_ini_settings(&mut saved);
571 assert!(!saved.contains("[Window][Undated]"));
572 assert!(saved.contains("[Window][Recent]"));
573 assert!(saved.contains("LastUsed=20260730"));
574 }
575}