Skip to main content

dear_imgui_rs/context/
settings.rs

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    /// Returns the Context-owned `.ini` retention configuration.
12    ///
13    /// Native state modified through unsafe FFI is validated before it is returned.
14    #[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    /// Atomically configures the session date and `.ini` retention behavior.
33    ///
34    /// This must be called before loading settings and before the first Dear ImGui frame. Automatic
35    /// cleanup runs while settings are loaded, and Dear ImGui copies the platform session date at
36    /// frame start. Mutating the policy after either boundary would silently produce mixed state.
37    ///
38    /// Enabling [`IniSettingsRetention::AutoDiscard`] removes supported settings that have no
39    /// `LastUsed` field on the next load, including settings written before date recording was
40    /// enabled.
41    ///
42    /// ```compile_fail
43    /// # use dear_imgui_rs::{Context, IniSettingsRetention};
44    /// let mut context = Context::create();
45    /// context.io_mut().set_ini_settings_auto_discard_months(None);
46    /// let _ = IniSettingsRetention::disabled();
47    /// ```
48    ///
49    /// ```compile_fail
50    /// # use dear_imgui_rs::Context;
51    /// let mut context = Context::create();
52    /// context.platform_io_mut().set_session_date(None);
53    /// ```
54    #[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            // Clear the dependent field first. Every validation and pointer lookup above occurs
77            // before mutation, then this sequence reaches the requested state without exposing
78            // automatic discard alongside an incompatible date or save flag.
79            (*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    /// Sets the INI filename for settings persistence
88    ///
89    /// # Errors
90    ///
91    /// Returns an error if the filename contains null bytes
92    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    // removed legacy set_ini_filename_or_panic (use set_ini_filename())
117
118    /// Sets the log filename
119    ///
120    /// # Errors
121    ///
122    /// Returns an error if the filename contains null bytes
123    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    // removed legacy set_log_filename_or_panic (use set_log_filename())
148
149    /// Sets the platform name
150    ///
151    /// # Errors
152    ///
153    /// Returns an error if the name contains null bytes
154    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    // removed legacy set_platform_name_or_panic (use set_platform_name())
179
180    /// Sets the renderer name
181    ///
182    /// # Errors
183    ///
184    /// Returns an error if the name contains null bytes
185    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    // removed legacy set_renderer_name_or_panic (use set_renderer_name())
210
211    /// Loads settings from a string slice containing settings in .Ini file format
212    #[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    /// Saves settings to a mutable string buffer in .Ini file format
223    #[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    /// Loads settings from a `.ini` file on disk.
244    ///
245    /// This is a convenience wrapper over `ImGui::LoadIniSettingsFromDisk`.
246    ///
247    /// Note: this is not available on `wasm32` targets.
248    #[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    /// Saves settings to a `.ini` file on disk.
266    ///
267    /// This is a convenience wrapper over `ImGui::SaveIniSettingsToDisk`.
268    ///
269    /// Note: this is not available on `wasm32` targets.
270    #[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        context
456            .font_atlas()
457            .try_claim_legacy_renderer()
458            .expect("legacy renderer font atlas should be available")
459            .build();
460        context.io_mut().set_display_size([128.0, 128.0]);
461        context.io_mut().set_delta_time(1.0 / 60.0);
462        let _ui = context.frame();
463        drop(context.render_legacy());
464
465        let before = raw_retention(&context);
466        assert!(matches!(
467            context
468                .set_ini_settings_retention(IniSettingsRetention::Disabled { session_date: None }),
469            Err(IniSettingsRetentionError::LockedAfterFirstFrame)
470        ));
471        assert_eq!(raw_retention(&context), before);
472    }
473
474    #[test]
475    fn context_retention_locks_after_settings_load() {
476        let _guard = crate::test_support::imgui_context_guard();
477        let mut context = Context::create();
478        context.load_ini_settings("[Window][Example]\nPos=0,0\nSize=100,100\n");
479
480        let before = raw_retention(&context);
481        assert!(matches!(
482            context
483                .set_ini_settings_retention(IniSettingsRetention::Disabled { session_date: None }),
484            Err(IniSettingsRetentionError::LockedAfterSettingsLoad)
485        ));
486        assert_eq!(raw_retention(&context), before);
487    }
488
489    #[test]
490    fn context_retention_reports_invalid_native_state() {
491        let _guard = crate::test_support::imgui_context_guard();
492        let context = Context::create();
493        let (platform_io, io) = (
494            context.platform_io_ptr("invalid native retention test"),
495            context.io_ptr("invalid native retention test"),
496        );
497
498        unsafe {
499            (*platform_io).Platform_SessionDate = 21_280_101;
500            (*io).ConfigIniSettingsSaveLastUsedDate = true;
501            (*io).ConfigIniSettingsAutoDiscardMonths = 0;
502        }
503        assert!(matches!(
504            context.ini_settings_retention(),
505            Err(IniSettingsRetentionError::InvalidNativeSessionDate { raw: 21_280_101 })
506        ));
507
508        unsafe {
509            (*platform_io).Platform_SessionDate = 20_260_730;
510            (*io).ConfigIniSettingsAutoDiscardMonths = -1;
511        }
512        assert!(matches!(
513            context.ini_settings_retention(),
514            Err(IniSettingsRetentionError::InvalidNativeAutoDiscardMonths { raw: -1 })
515        ));
516
517        unsafe {
518            (*io).ConfigIniSettingsSaveLastUsedDate = false;
519            (*io).ConfigIniSettingsAutoDiscardMonths = 1;
520        }
521        assert!(matches!(
522            context.ini_settings_retention(),
523            Err(IniSettingsRetentionError::InvalidNativeState { .. })
524        ));
525
526        unsafe {
527            (*platform_io).Platform_SessionDate = 0;
528            (*io).ConfigIniSettingsSaveLastUsedDate = true;
529        }
530        assert!(matches!(
531            context.ini_settings_retention(),
532            Err(IniSettingsRetentionError::InvalidNativeState { .. })
533        ));
534    }
535
536    #[test]
537    fn context_retention_round_trips_recording_without_a_session_date() {
538        let _guard = crate::test_support::imgui_context_guard();
539        let mut context = Context::create();
540        let raw = IniSettingsRetention::RecordLastUsed { session_date: None };
541
542        context
543            .set_ini_settings_retention(raw)
544            .expect("date recording may be configured without a platform clock");
545        assert_eq!(raw_retention(&context), (0, true, 0));
546
547        let observed = context
548            .ini_settings_retention()
549            .expect("the native state is representable");
550        assert_eq!(observed, raw);
551        context
552            .set_ini_settings_retention(observed)
553            .expect("reading and writing the policy is lossless");
554        assert_eq!(raw_retention(&context), (0, true, 0));
555    }
556
557    #[test]
558    fn auto_discard_removes_undated_entries_on_load() {
559        let _guard = crate::test_support::imgui_context_guard();
560        let mut context = Context::create();
561        context
562            .set_ini_settings_retention(IniSettingsRetention::AutoDiscard {
563                session_date: IniSessionDate::new(2026, 7, 30).unwrap(),
564                months: NonZeroU16::new(6).unwrap(),
565            })
566            .unwrap();
567
568        context.load_ini_settings(
569            "[Window][Undated]\nPos=1,1\nSize=100,100\nCollapsed=0\n\n\
570             [Window][Recent]\nPos=2,2\nSize=100,100\nCollapsed=0\nLastUsed=20260730\n\n",
571        );
572
573        let mut saved = String::new();
574        context.save_ini_settings(&mut saved);
575        assert!(!saved.contains("[Window][Undated]"));
576        assert!(saved.contains("[Window][Recent]"));
577        assert!(saved.contains("LastUsed=20260730"));
578    }
579}