From 46d099f40f01c191a4b8212f0e371d77f7dd18b5 Mon Sep 17 00:00:00 2001 From: Joonas Javanainen Date: Sat, 13 Jul 2019 17:15:02 +0300 Subject: [PATCH] Require explicit ending/popping of almost all stack tokens This is probably going to be controversial... Calling end/pop in the Drop implementation causes way too many problems, and interacts very badly with panics. There are closure-based simpler implementations of almost everything except parameter stacks that are still convenient to use. However, if we end up switching to &mut Ui in most functions (very much possible in the future!), closures will complicate matters so push/pop and begin/end pairs are still relevant. --- CHANGELOG.markdown | 4 + imgui-examples/examples/test_window_impl.rs | 48 +++-- src/layout.rs | 41 ++++- src/stacks.rs | 187 ++++++++++++++------ src/widget/combo_box.rs | 81 +++++---- src/widget/menu.rs | 117 +++++++++--- src/widget/text.rs | 6 +- src/window/child_window.rs | 52 +++--- src/window/mod.rs | 52 +++--- 9 files changed, 395 insertions(+), 193 deletions(-) diff --git a/CHANGELOG.markdown b/CHANGELOG.markdown index 8ebed2c..e488842 100644 --- a/CHANGELOG.markdown +++ b/CHANGELOG.markdown @@ -26,6 +26,10 @@ - Gfx renderer re-exports imgui and gfx - These functions now take/return PathBuf: log_filename, set_log_filename, ini_filename, set_logfilename - ID stack manipulation now uses stack tokens +- Parameter stack pushes *must almost always be paired by a manual call to stack pop* +- Container widget tokens *must be ended manually by calling end*. + Closure-based function (e.g. build()) are unaffected and do this + automatically ### Removed diff --git a/imgui-examples/examples/test_window_impl.rs b/imgui-examples/examples/test_window_impl.rs index 03a7ab2..b047704 100644 --- a/imgui-examples/examples/test_window_impl.rs +++ b/imgui-examples/examples/test_window_impl.rs @@ -283,13 +283,14 @@ fn show_test_window(ui: &Ui, state: &mut State, opened: &mut bool) { window = window.opened(opened) } window.build(ui, || { - let _token = ui.push_item_width(-140.0); + ui.push_item_width(-140.0); ui.text(format!("dear imgui says hello. ({})", imgui::dear_imgui_version())); - if let Some(_) = ui.menu_bar() { - if let Some(_) = ui.menu(im_str!("Menu"), true) { + if let Some(menu_bar) = ui.begin_menu_bar() { + if let Some(menu) = ui.begin_menu(im_str!("Menu"), true) { show_example_menu_file(ui, &mut state.file_menu); + menu.end(ui); } - if let Some(_) = ui.menu(im_str!("Examples"), true) { + if let Some(menu) = ui.begin_menu(im_str!("Examples"), true) { MenuItem::new(im_str!("Main menu bar")) .build_with_ref(ui, &mut state.show_app_main_menu_bar); MenuItem::new(im_str!("Console")) @@ -312,15 +313,18 @@ fn show_test_window(ui: &Ui, state: &mut State, opened: &mut bool) { .build_with_ref(ui, &mut state.show_app_manipulating_window_title); MenuItem::new(im_str!("Custom rendering")) .build_with_ref(ui, &mut state.show_app_custom_rendering); + menu.end(ui); } - if let Some(_) = ui.menu(im_str!("Help"), true) { + if let Some(menu) = ui.begin_menu(im_str!("Help"), true) { MenuItem::new(im_str!("Metrics")) .build_with_ref(ui, &mut state.show_app_metrics); MenuItem::new(im_str!("Style Editor")) .build_with_ref(ui, &mut state.show_app_style_editor); MenuItem::new(im_str!("About ImGui")) .build_with_ref(ui, &mut state.show_app_about); + menu.end(ui); } + menu_bar.end(ui); } ui.spacing(); if ui.collapsing_header(im_str!("Help")).build() { @@ -628,7 +632,7 @@ CTRL+click on individual component to input value.\n", ui.popup_modal(im_str!("Delete?")).always_auto_resize(true).build(|| { ui.text("All those beautiful files will be deleted.\nThis operation cannot be undone!\n\n"); ui.separator(); - let _token = ui.push_style_var(StyleVar::FramePadding([0.0, 0.0])); + let style = ui.push_style_var(StyleVar::FramePadding([0.0, 0.0])); ui.checkbox(im_str!("Don't ask me next time"), &mut state.dont_ask_me_next_time); if ui.button(im_str!("OK"), [120.0, 0.0]) { @@ -638,6 +642,7 @@ CTRL+click on individual component to input value.\n", if ui.button(im_str!("Cancel"), [120.0, 0.0]) { ui.close_current_popup(); } + style.pop(ui); }); if ui.button(im_str!("Stacked modals.."), [0.0, 0.0]) { @@ -674,11 +679,12 @@ CTRL+click on individual component to input value.\n", } fn show_example_app_main_menu_bar<'a>(ui: &Ui<'a>, state: &mut State) { - if let Some(_) = ui.main_menu_bar() { - if let Some(_) = ui.menu(im_str!("File"), true) { + if let Some(menu_bar) = ui.begin_main_menu_bar() { + if let Some(menu) = ui.begin_menu(im_str!("File"), true) { show_example_menu_file(ui, &mut state.file_menu); + menu.end(ui); } - if let Some(_) = ui.menu(im_str!("Edit"), true) { + if let Some(menu) = ui.begin_menu(im_str!("Edit"), true) { MenuItem::new(im_str!("Undo")) .shortcut(im_str!("CTRL+Z")) .build(ui); @@ -696,7 +702,9 @@ fn show_example_app_main_menu_bar<'a>(ui: &Ui<'a>, state: &mut State) { MenuItem::new(im_str!("Paste")) .shortcut(im_str!("CTRL+V")) .build(ui); + menu.end(ui); } + menu_bar.end(ui); } } @@ -708,24 +716,27 @@ fn show_example_menu_file<'a>(ui: &Ui<'a>, state: &mut FileMenuState) { MenuItem::new(im_str!("Open")) .shortcut(im_str!("Ctrl+O")) .build(ui); - if let Some(_) = ui.menu(im_str!("Open Recent"), true) { + if let Some(menu) = ui.begin_menu(im_str!("Open Recent"), true) { MenuItem::new(im_str!("fish_hat.c")).build(ui); MenuItem::new(im_str!("fish_hat.inl")).build(ui); MenuItem::new(im_str!("fish_hat.h")).build(ui); - if let Some(_) = ui.menu(im_str!("More.."), true) { + if let Some(menu) = ui.begin_menu(im_str!("More.."), true) { MenuItem::new(im_str!("Hello")).build(ui); MenuItem::new(im_str!("Sailor")).build(ui); - if let Some(_) = ui.menu(im_str!("Recurse.."), true) { + if let Some(menu) = ui.begin_menu(im_str!("Recurse.."), true) { show_example_menu_file(ui, state); + menu.end(ui); } + menu.end(ui); } + menu.end(ui); } MenuItem::new(im_str!("Save")) .shortcut(im_str!("Ctrl+S")) .build(ui); MenuItem::new(im_str!("Save As..")).build(ui); ui.separator(); - if let Some(_) = ui.menu(im_str!("Options"), true) { + if let Some(menu) = ui.begin_menu(im_str!("Options"), true) { MenuItem::new(im_str!("Enabled")).build_with_ref(ui, &mut state.enabled); ChildWindow::new("child") .size([0.0, 60.0]) @@ -743,13 +754,15 @@ fn show_example_menu_file<'a>(ui: &Ui<'a>, state: &mut FileMenuState) { let items = [im_str!("Yes"), im_str!("No"), im_str!("Maybe")]; ComboBox::new(im_str!("Combo")).build_simple_string(ui, &mut state.n, &items); ui.checkbox(im_str!("Check"), &mut state.b); + menu.end(ui); } - if let Some(_) = ui.menu(im_str!("Colors"), true) { + if let Some(menu) = ui.begin_menu(im_str!("Colors"), true) { for &col in StyleColor::VARIANTS.iter() { MenuItem::new(&im_str!("{:?}", col)).build(ui); } + menu.end(ui); } - if let Some(_) = ui.menu(im_str!("Disabled"), false) { + if let Some(_) = ui.begin_menu(im_str!("Disabled"), false) { unreachable!(); } MenuItem::new(im_str!("Checked")).selected(true).build(ui); @@ -778,7 +791,7 @@ output your content because that would create a feedback loop.", fn show_example_app_fixed_overlay(ui: &Ui, opened: &mut bool) { const DISTANCE: f32 = 10.0; let window_pos = [DISTANCE, DISTANCE]; - let _token = ui.push_style_color(StyleColor::WindowBg, [0.0, 0.0, 0.0, 0.3]); + let style = ui.push_style_color(StyleColor::WindowBg, [0.0, 0.0, 0.0, 0.3]); Window::new(im_str!("Example: Fixed Overlay")) .opened(opened) .position(window_pos, Condition::Always) @@ -797,7 +810,8 @@ fn show_example_app_fixed_overlay(ui: &Ui, opened: &mut bool) { "Mouse Position: ({:.1},{:.1})", mouse_pos[0], mouse_pos[1] )); - }) + }); + style.pop(ui); } fn show_example_app_manipulating_window_title(ui: &Ui) { diff --git a/src/layout.rs b/src/layout.rs index 3bc749e..24471b6 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -1,19 +1,32 @@ -use std::marker::PhantomData; +use std::ptr; +use std::thread; +use crate::context::Context; use crate::sys; use crate::Ui; -/// Represents a layout group -pub struct GroupToken<'ui> { - _ui: PhantomData<&'ui Ui<'ui>>, +/// Tracks a layout group that must be ended by calling `.end()` +#[must_use] +pub struct GroupToken { + ctx: *const Context, } -impl<'ui> Drop for GroupToken<'ui> { - fn drop(&mut self) { +impl GroupToken { + /// Ends a layout group + pub fn end(mut self, _: &Ui) { + self.ctx = ptr::null(); unsafe { sys::igEndGroup() }; } } +impl Drop for GroupToken { + fn drop(&mut self) { + if !self.ctx.is_null() && !thread::panicking() { + panic!("A GroupToken was leaked. Did you call .end()?"); + } + } +} + /// # Cursor / Layout impl<'ui> Ui<'ui> { /// Renders a separator (generally horizontal). @@ -67,9 +80,21 @@ impl<'ui> Ui<'ui> { /// Groups items together as a single item. /// /// May be useful to handle the same mouse event on a group of items, for example. - pub fn group(&self) -> GroupToken { + /// + /// Returns a `GroupToken` that must be ended by calling `.end()` + #[must_use] + pub fn begin_group(&self) -> GroupToken { unsafe { sys::igBeginGroup() }; - GroupToken { _ui: PhantomData } + GroupToken { ctx: self.ctx } + } + /// Creates a layout group and runs a closure to construct the contents. + /// + /// May be useful to handle the same mouse event on a group of items, for example. + pub fn group R>(&self, f: F) -> R { + let group = self.begin_group(); + let result = f(); + group.end(self); + result } /// Returns the cursor position (in window coordinates) pub fn cursor_pos(&self) -> [f32; 2] { diff --git a/src/stacks.rs b/src/stacks.rs index 6c3a96f..5dbdbcd 100644 --- a/src/stacks.rs +++ b/src/stacks.rs @@ -1,7 +1,9 @@ -use std::marker::PhantomData; use std::mem; use std::os::raw::{c_char, c_void}; +use std::ptr; +use std::thread; +use crate::context::Context; use crate::fonts::atlas::FontId; use crate::internal::RawCast; use crate::style::{StyleColor, StyleVar}; @@ -12,6 +14,8 @@ use crate::{Id, Ui}; impl<'ui> Ui<'ui> { /// Switches to the given font by pushing it to the font stack. /// + /// Returns a `FontStackToken` that must be popped by calling `.pop()` + /// /// # Panics /// /// Panics if the font atlas does not contain the given font @@ -26,8 +30,9 @@ impl<'ui> Ui<'ui> { /// let my_custom_font = ctx.fonts().add_font(&font_data_sources); /// # let ui = ctx.frame(); /// // During UI construction - /// let _token = ui.push_font(my_custom_font); + /// let font = ui.push_font(my_custom_font); /// ui.text("I use the custom font!"); + /// font.pop(&ui); /// ``` #[must_use] pub fn push_font(&self, id: FontId) -> FontStackToken { @@ -36,10 +41,12 @@ impl<'ui> Ui<'ui> { .get_font(id) .expect("Font atlas did not contain the given font"); unsafe { sys::igPushFont(font.raw() as *const _ as *mut _) }; - FontStackToken { _ui: PhantomData } + FontStackToken { ctx: self.ctx } } /// Changes a style color by pushing a change to the color stack. /// + /// Returns a `ColorStackToken` that must be popped by calling `.pop()` + /// /// # Examples /// /// ```no_run @@ -47,19 +54,22 @@ impl<'ui> Ui<'ui> { /// # let mut ctx = Context::create(); /// # let ui = ctx.frame(); /// const RED: [f32; 4] = [1.0, 0.0, 0.0, 1.0]; - /// let _token = ui.push_style_color(StyleColor::Text, RED); + /// let color = ui.push_style_color(StyleColor::Text, RED); /// ui.text("I'm red!"); + /// color.pop(&ui); /// ``` #[must_use] pub fn push_style_color(&self, style_color: StyleColor, color: [f32; 4]) -> ColorStackToken { unsafe { sys::igPushStyleColor(style_color as i32, color.into()) }; ColorStackToken { count: 1, - _ui: PhantomData, + ctx: self.ctx, } } /// Changes style colors by pushing several changes to the color stack. /// + /// Returns a `ColorStackToken` that must be popped by calling `.pop()` + /// /// # Examples /// /// ```no_run @@ -68,12 +78,13 @@ impl<'ui> Ui<'ui> { /// # let ui = ctx.frame(); /// const RED: [f32; 4] = [1.0, 0.0, 0.0, 1.0]; /// const GREEN: [f32; 4] = [0.0, 1.0, 0.0, 1.0]; - /// let _token = ui.push_style_colors(&[ + /// let colors = ui.push_style_colors(&[ /// (StyleColor::Text, RED), /// (StyleColor::TextDisabled, GREEN), /// ]); /// ui.text("I'm red!"); /// ui.text_disabled("I'm green!"); + /// colors.pop(&ui); /// ``` #[must_use] pub fn push_style_colors<'a, I>(&self, style_colors: I) -> ColorStackToken @@ -87,42 +98,48 @@ impl<'ui> Ui<'ui> { } ColorStackToken { count, - _ui: PhantomData, + ctx: self.ctx, } } /// Changes a style variable by pushing a change to the style stack. /// + /// Returns a `StyleStackToken` that must be popped by calling `.pop()` + /// /// # Examples /// /// ```no_run /// # use imgui::*; /// # let mut ctx = Context::create(); /// # let ui = ctx.frame(); - /// let _token = ui.push_style_var(StyleVar::Alpha(0.2)); + /// let style = ui.push_style_var(StyleVar::Alpha(0.2)); /// ui.text("I'm transparent!"); + /// style.pop(&ui); /// ``` #[must_use] pub fn push_style_var(&self, style_var: StyleVar) -> StyleStackToken { unsafe { push_style_var(style_var) }; StyleStackToken { count: 1, - _ui: PhantomData, + ctx: self.ctx, } } /// Changes style variables by pushing several changes to the style stack. /// + /// Returns a `StyleStackToken` that must be popped by calling `.pop()` + /// /// # Examples /// /// ```no_run /// # use imgui::*; /// # let mut ctx = Context::create(); /// # let ui = ctx.frame(); - /// let _token = ui.push_style_vars(&[ + /// let styles = ui.push_style_vars(&[ /// StyleVar::Alpha(0.2), /// StyleVar::ItemSpacing([50.0, 50.0]) /// ]); /// ui.text("We're transparent..."); /// ui.text("...with large spacing as well"); + /// styles.pop(&ui); /// ``` #[must_use] pub fn push_style_vars<'a, I>(&self, style_vars: I) -> StyleStackToken @@ -136,46 +153,79 @@ impl<'ui> Ui<'ui> { } StyleStackToken { count, - _ui: PhantomData, + ctx: self.ctx, } } } -/// Represents a change pushed to the font stack -pub struct FontStackToken<'ui> { - _ui: PhantomData<&'ui Ui<'ui>>, +/// Tracks a font pushed to the font stack that must be popped by calling `.pop()` +#[must_use] +pub struct FontStackToken { + ctx: *const Context, } -impl<'ui> Drop for FontStackToken<'ui> { - fn drop(&mut self) { +impl FontStackToken { + /// Pops a change from the font stack + pub fn pop(mut self, _: &Ui) { + self.ctx = ptr::null(); unsafe { sys::igPopFont() }; } } -/// Represents one or more changes pushed to the color stack -pub struct ColorStackToken<'ui> { - count: usize, - _ui: PhantomData<&'ui Ui<'ui>>, +impl Drop for FontStackToken { + fn drop(&mut self) { + if !self.ctx.is_null() && !thread::panicking() { + panic!("A FontStackToken was leaked. Did you call .pop()?"); + } + } } -impl<'ui> Drop for ColorStackToken<'ui> { - fn drop(&mut self) { +/// Tracks one or more changes pushed to the color stack that must be popped by calling `.pop()` +#[must_use] +pub struct ColorStackToken { + count: usize, + ctx: *const Context, +} + +impl ColorStackToken { + /// Pops changes from the color stack + pub fn pop(mut self, _: &Ui) { + self.ctx = ptr::null(); unsafe { sys::igPopStyleColor(self.count as i32) }; } } -/// Represents one or more changes pushed to the style stack -pub struct StyleStackToken<'ui> { - count: usize, - _ui: PhantomData<&'ui Ui<'ui>>, +impl Drop for ColorStackToken { + fn drop(&mut self) { + if !self.ctx.is_null() && !thread::panicking() { + panic!("A ColorStackToken was leaked. Did you call .pop()?"); + } + } } -impl<'ui> Drop for StyleStackToken<'ui> { - fn drop(&mut self) { +/// Tracks one or more changes pushed to the style stack that must be popped by calling `.pop()` +#[must_use] +pub struct StyleStackToken { + count: usize, + ctx: *const Context, +} + +impl StyleStackToken { + /// Pops changes from the style stack + pub fn pop(mut self, _: &Ui) { + self.ctx = ptr::null(); unsafe { sys::igPopStyleVar(self.count as i32) }; } } +impl Drop for StyleStackToken { + fn drop(&mut self) { + if !self.ctx.is_null() && !thread::panicking() { + panic!("A StyleStackToken was leaked. Did you call .pop()?"); + } + } +} + #[inline] unsafe fn push_style_var(style_var: StyleVar) { use crate::style::StyleVar::*; @@ -219,14 +269,15 @@ unsafe fn push_style_var(style_var: StyleVar) { impl<'ui> Ui<'ui> { /// Changes the item width by pushing a change to the item width stack. /// + /// Returns an `ItemWidthStackToken` that may be popped by calling `.pop()` + /// /// - `> 0.0`: width is `item_width` pixels /// - `= 0.0`: default to ~2/3 of window width /// - `< 0.0`: `item_width` pixels relative to the right of window (-1.0 always aligns width to /// the right side) - #[must_use] pub fn push_item_width(&self, item_width: f32) -> ItemWidthStackToken { unsafe { sys::igPushItemWidth(item_width) }; - ItemWidthStackToken { _ui: PhantomData } + ItemWidthStackToken { ctx: self.ctx } } /// Sets the width of the next item. /// @@ -243,16 +294,18 @@ impl<'ui> Ui<'ui> { } /// Changes the text wrapping position by pushing a change to the text wrapping position stack. /// + /// Returns a `TextWrapPosStackToken` that may be popped by calling `.pop()` + /// /// - `> 0.0`: wrap at `wrap_pos_x` position in window local space /// - `= 0.0`: wrap to end of window (or column) /// - `< 0.0`: no wrapping - #[must_use] pub fn push_text_wrap_pos(&self, wrap_pos_x: f32) -> TextWrapPosStackToken { unsafe { sys::igPushTextWrapPos(wrap_pos_x) }; - TextWrapPosStackToken { _ui: PhantomData } + TextWrapPosStackToken { ctx: self.ctx } } - /// Changes an item flag by pushing a change to the item flag stack - #[must_use] + /// Changes an item flag by pushing a change to the item flag stack. + /// + /// Returns a `ItemFlagsStackToken` that may be popped by calling `.pop()` pub fn push_item_flag(&self, item_flag: ItemFlag) -> ItemFlagsStackToken { use self::ItemFlag::*; match item_flag { @@ -261,7 +314,7 @@ impl<'ui> Ui<'ui> { } ItemFlagsStackToken { discriminant: mem::discriminant(&item_flag), - _ui: PhantomData, + ctx: self.ctx, } } } @@ -273,36 +326,42 @@ pub enum ItemFlag { ButtonRepeat(bool), } -/// Represents a change pushed to the item width stack -pub struct ItemWidthStackToken<'ui> { - _ui: PhantomData<&'ui Ui<'ui>>, +/// Tracks a change pushed to the item width stack +pub struct ItemWidthStackToken { + ctx: *const Context, } -impl<'ui> Drop for ItemWidthStackToken<'ui> { - fn drop(&mut self) { +impl ItemWidthStackToken { + /// Pops a change from the item width stack + pub fn pop(mut self, _: &Ui) { + self.ctx = ptr::null(); unsafe { sys::igPopItemWidth() }; } } -/// Represents a change pushed to the text wrap position stack -pub struct TextWrapPosStackToken<'ui> { - _ui: PhantomData<&'ui Ui<'ui>>, +/// Tracks a change pushed to the text wrap position stack +pub struct TextWrapPosStackToken { + ctx: *const Context, } -impl<'ui> Drop for TextWrapPosStackToken<'ui> { - fn drop(&mut self) { +impl TextWrapPosStackToken { + /// Pops a change from the text wrap position stack + pub fn pop(mut self, _: &Ui) { + self.ctx = ptr::null(); unsafe { sys::igPopTextWrapPos() }; } } -/// Represents a change pushed to the item flags stack -pub struct ItemFlagsStackToken<'ui> { +/// Tracks a change pushed to the item flags stack +pub struct ItemFlagsStackToken { discriminant: mem::Discriminant, - _ui: PhantomData<&'ui Ui<'ui>>, + ctx: *const Context, } -impl<'ui> Drop for ItemFlagsStackToken<'ui> { - fn drop(&mut self) { +impl ItemFlagsStackToken { + /// Pops a change from the item flags stack + pub fn pop(mut self, _: &Ui) { + self.ctx = ptr::null(); const ALLOW_KEYBOARD_FOCUS: ItemFlag = ItemFlag::AllowKeyboardFocus(true); const BUTTON_REPEAT: ItemFlag = ItemFlag::ButtonRepeat(true); @@ -318,7 +377,10 @@ impl<'ui> Drop for ItemFlagsStackToken<'ui> { /// # ID stack impl<'ui> Ui<'ui> { - /// Pushes an identifier to the ID stack + /// Pushes an identifier to the ID stack. + /// + /// Returns an `IdStackToken` that must be popped by calling `.pop()` + /// #[must_use] pub fn push_id<'a, I: Into>>(&self, id: I) -> IdStackToken { let id = id.into(); @@ -334,17 +396,28 @@ impl<'ui> Ui<'ui> { Id::Ptr(p) => sys::igPushIDPtr(p as *const c_void), } } - IdStackToken { _ui: PhantomData } + IdStackToken { ctx: self.ctx } } } -/// Represents a change pushed to the ID stack -pub struct IdStackToken<'ui> { - _ui: PhantomData<&'ui Ui<'ui>>, +/// Tracks an ID pushed to the ID stack that must be popped by calling `.pop()` +#[must_use] +pub struct IdStackToken { + ctx: *const Context, } -impl<'ui> Drop for IdStackToken<'ui> { - fn drop(&mut self) { +impl IdStackToken { + /// Pops a change from the ID stack + pub fn pop(mut self, _: &Ui) { + self.ctx = ptr::null(); unsafe { sys::igPopID() }; } } + +impl Drop for IdStackToken { + fn drop(&mut self) { + if !self.ctx.is_null() && !thread::panicking() { + panic!("A IdStackToken was leaked. Did you call .pop()?"); + } + } +} diff --git a/src/widget/combo_box.rs b/src/widget/combo_box.rs index b7fc4e8..d764eaa 100644 --- a/src/widget/combo_box.rs +++ b/src/widget/combo_box.rs @@ -1,8 +1,9 @@ use bitflags::bitflags; use std::borrow::Cow; -use std::marker::PhantomData; use std::ptr; +use std::thread; +use crate::context::Context; use crate::string::ImStr; use crate::sys; use crate::Ui; @@ -34,24 +35,24 @@ pub enum ComboBoxPreviewMode { } bitflags!( - /// Flags for combo boxes - #[repr(transparent)] - pub struct ComboBoxFlags: u32 { - /// Align the popup toward the left by default - const POPUP_ALIGN_LEFT = sys::ImGuiComboFlags_PopupAlignLeft; - /// Max ~4 items visible. - const HEIGHT_SMALL = sys::ImGuiComboFlags_HeightSmall; - /// Max ~8 items visible (default) - const HEIGHT_REGULAR = sys::ImGuiComboFlags_HeightRegular; - /// Max ~20 items visible - const HEIGHT_LARGE = sys::ImGuiComboFlags_HeightLarge; - /// As many fitting items as possible - const HEIGHT_LARGEST = sys::ImGuiComboFlags_HeightLargest; - /// Display on the preview box without the square arrow button - const NO_ARROW_BUTTON = sys::ImGuiComboFlags_NoArrowButton; - /// Display only a square arrow button - const NO_PREVIEW = sys::ImGuiComboFlags_NoPreview; - } +/// Flags for combo boxes +#[repr(transparent)] +pub struct ComboBoxFlags: u32 { + /// Align the popup toward the left by default + const POPUP_ALIGN_LEFT = sys::ImGuiComboFlags_PopupAlignLeft; + /// Max ~4 items visible. + const HEIGHT_SMALL = sys::ImGuiComboFlags_HeightSmall; + /// Max ~8 items visible (default) + const HEIGHT_REGULAR = sys::ImGuiComboFlags_HeightRegular; + /// Max ~20 items visible + const HEIGHT_LARGE = sys::ImGuiComboFlags_HeightLarge; + /// As many fitting items as possible + const HEIGHT_LARGEST = sys::ImGuiComboFlags_HeightLargest; + /// Display on the preview box without the square arrow button + const NO_ARROW_BUTTON = sys::ImGuiComboFlags_NoArrowButton; + /// Display only a square arrow button + const NO_PREVIEW = sys::ImGuiComboFlags_NoPreview; +} ); /// Builder for a combo box widget @@ -127,10 +128,14 @@ impl<'a> ComboBox<'a> { ); self } - /// Builds this combo box, and starts appending to it. + /// Creates a combo box and starts appending to it. + /// + /// Returns `Some(ComboBoxToken)` if the combo box is open. After content has been + /// rendered, the token must be ended by calling `.end()`. /// /// Returns `None` if the combo box is not open and no content should be rendered. - pub fn begin<'ui>(self, _: &'ui Ui<'ui>) -> Option> { + #[must_use] + pub fn begin(self, ui: &Ui) -> Option { let should_render = unsafe { sys::igBeginCombo( self.label.as_ptr(), @@ -139,42 +144,40 @@ impl<'a> ComboBox<'a> { ) }; if should_render { - Some(ComboBoxToken { - should_end: true, - _ui: PhantomData, - }) + Some(ComboBoxToken { ctx: ui.ctx }) } else { None } } - /// Builds the combo box. + /// Creates a combo box and runs a closure to construct the popup contents. /// - /// Note: the closure is not called if the combo box is not open + /// Note: the closure is not called if the combo box is not open. pub fn build(self, ui: &Ui, f: F) { - if let Some(_combo) = self.begin(ui) { + if let Some(combo) = self.begin(ui) { f(); + combo.end(ui); } } } -/// Represents a combo box pushed to the window stack -pub struct ComboBoxToken<'ui> { - should_end: bool, - _ui: PhantomData<&'ui Ui<'ui>>, +/// Tracks a combo box that must be ended by calling `.end()` +#[must_use] +pub struct ComboBoxToken { + ctx: *const Context, } -impl<'ui> ComboBoxToken<'ui> { - /// Finishes the current combo box and pops it from the window stack - pub fn end(mut self) { - self.should_end = false; +impl ComboBoxToken { + /// Ends a combo box + pub fn end(mut self, _: &Ui) { + self.ctx = ptr::null(); unsafe { sys::igEndCombo() }; } } -impl<'ui> Drop for ComboBoxToken<'ui> { +impl Drop for ComboBoxToken { fn drop(&mut self) { - if self.should_end { - unsafe { sys::igEndCombo() }; + if !self.ctx.is_null() && !thread::panicking() { + panic!("A ComboBoxToken was leaked. Did you call .end()?"); } } } diff --git a/src/widget/menu.rs b/src/widget/menu.rs index f6029ea..9673dca 100644 --- a/src/widget/menu.rs +++ b/src/widget/menu.rs @@ -1,6 +1,7 @@ -use std::marker::PhantomData; use std::ptr; +use std::thread; +use crate::context::Context; use crate::string::ImStr; use crate::sys; use crate::Ui; @@ -9,34 +10,73 @@ use crate::Ui; impl<'ui> Ui<'ui> { /// Creates and starts appending to a full-screen menu bar. /// + /// Returns `Some(MainMenuBarToken)` if the menu bar is visible. After content has been + /// rendered, the token must be ended by calling `.end()`. + /// /// Returns `None` if the menu bar is not visible and no content should be rendered. - pub fn main_menu_bar(&self) -> Option { + #[must_use] + pub fn begin_main_menu_bar(&self) -> Option { if unsafe { sys::igBeginMainMenuBar() } { - Some(MainMenuBarToken { _ui: PhantomData }) + Some(MainMenuBarToken { ctx: self.ctx }) } else { None } } + /// Creates a full-screen main menu bar and runs a closure to construct the contents. + /// + /// Note: the closure is not called if the menu bar is not visible. + pub fn main_menu_bar(&self, f: F) { + if let Some(menu_bar) = self.begin_main_menu_bar() { + f(); + menu_bar.end(self); + } + } /// Creates and starts appending to the menu bar of the current window. /// + /// Returns `Some(MenuBarToken)` if the menu bar is visible. After content has been + /// rendered, the token must be ended by calling `.end()`. + /// /// Returns `None` if the menu bar is not visible and no content should be rendered. - pub fn menu_bar(&self) -> Option { + #[must_use] + pub fn begin_menu_bar(&self) -> Option { if unsafe { sys::igBeginMenuBar() } { - Some(MenuBarToken { _ui: PhantomData }) + Some(MenuBarToken { ctx: self.ctx }) } else { None } } + /// Creates a menu bar in the current window and runs a closure to construct the contents. + /// + /// Note: the closure is not called if the menu bar is not visible. + pub fn menu_bar(&self, f: F) { + if let Some(menu_bar) = self.begin_menu_bar() { + f(); + menu_bar.end(self); + } + } /// Creates and starts appending to a sub-menu entry. /// + /// Returns `Some(MenuToken)` if the menu is visible. After content has been + /// rendered, the token must be ended by calling `.end()`. + /// /// Returns `None` if the menu is not visible and no content should be rendered. - pub fn menu(&self, label: &ImStr, enabled: bool) -> Option { + #[must_use] + pub fn begin_menu(&self, label: &ImStr, enabled: bool) -> Option { if unsafe { sys::igBeginMenu(label.as_ptr(), enabled) } { - Some(MenuToken { _ui: PhantomData }) + Some(MenuToken { ctx: self.ctx }) } else { None } } + /// Creates a menu and runs a closure to construct the contents. + /// + /// Note: the closure is not called if the menu is not visible. + pub fn menu(&self, label: &ImStr, enabled: bool, f: F) { + if let Some(menu) = self.begin_menu(label, enabled) { + f(); + menu.end(self); + } + } } /// Builder for a menu item. @@ -111,35 +151,68 @@ impl<'a> MenuItem<'a> { } } -/// Represents a main menu bar -pub struct MainMenuBarToken<'ui> { - _ui: PhantomData<&'ui Ui<'ui>>, +/// Tracks a main menu bar that must be ended by calling `.end()` +#[must_use] +pub struct MainMenuBarToken { + ctx: *const Context, } -impl<'ui> Drop for MainMenuBarToken<'ui> { - fn drop(&mut self) { +impl MainMenuBarToken { + /// Ends a main menu bar + pub fn end(mut self, _: &Ui) { + self.ctx = ptr::null(); unsafe { sys::igEndMainMenuBar() }; } } -/// Represents a menu bar -pub struct MenuBarToken<'ui> { - _ui: PhantomData<&'ui Ui<'ui>>, +impl Drop for MainMenuBarToken { + fn drop(&mut self) { + if !self.ctx.is_null() && !thread::panicking() { + panic!("A MainMenuBarToken was leaked. Did you call .end()?"); + } + } } -impl<'ui> Drop for MenuBarToken<'ui> { - fn drop(&mut self) { +/// Tracks a menu bar that must be ended by calling `.end()` +#[must_use] +pub struct MenuBarToken { + ctx: *const Context, +} + +impl MenuBarToken { + /// Ends a menu bar + pub fn end(mut self, _: &Ui) { + self.ctx = ptr::null(); unsafe { sys::igEndMenuBar() }; } } -/// Represents a menu -pub struct MenuToken<'ui> { - _ui: PhantomData<&'ui Ui<'ui>>, +impl Drop for MenuBarToken { + fn drop(&mut self) { + if !self.ctx.is_null() && !thread::panicking() { + panic!("A MenuBarToken was leaked. Did you call .end()?"); + } + } } -impl<'ui> Drop for MenuToken<'ui> { - fn drop(&mut self) { +/// Tracks a menu that must be ended by calling `.end()` +#[must_use] +pub struct MenuToken { + ctx: *const Context, +} + +impl MenuToken { + /// Ends a menu + pub fn end(mut self, _: &Ui) { + self.ctx = ptr::null(); unsafe { sys::igEndMenu() }; } } + +impl Drop for MenuToken { + fn drop(&mut self) { + if !self.ctx.is_null() && !thread::panicking() { + panic!("A MenuToken was leaked. Did you call .end()?"); + } + } +} diff --git a/src/widget/text.rs b/src/widget/text.rs index 949f557..74a15d8 100644 --- a/src/widget/text.rs +++ b/src/widget/text.rs @@ -24,14 +24,16 @@ impl<'ui> Ui<'ui> { } /// Renders simple text using the given text color pub fn text_colored>(&self, color: [f32; 4], text: T) { - let _ = self.push_style_color(StyleColor::Text, color); + let style = self.push_style_color(StyleColor::Text, color); self.text(text); + style.pop(self); } /// Renders simple text using `StyleColor::TextDisabled` color pub fn text_disabled>(&self, text: T) { let color = self.style_color(StyleColor::TextDisabled); - let _ = self.push_style_color(StyleColor::Text, color); + let style = self.push_style_color(StyleColor::Text, color); self.text(text); + style.pop(self); } /// Renders text wrapped to the end of window (or column) pub fn text_wrapped(&self, text: &ImStr) { diff --git a/src/window/child_window.rs b/src/window/child_window.rs index 9622a6d..56d025e 100644 --- a/src/window/child_window.rs +++ b/src/window/child_window.rs @@ -1,7 +1,9 @@ use std::f32; -use std::marker::PhantomData; use std::os::raw::{c_char, c_void}; +use std::ptr; +use std::thread; +use crate::context::Context; use crate::sys; use crate::window::WindowFlags; use crate::{Id, Ui}; @@ -228,8 +230,13 @@ impl<'a> ChildWindow<'a> { self.flags |= WindowFlags::NO_INPUTS; self } - /// Builds this window, pushes it to the window stack, and starts appending to it - pub fn begin<'ui>(self, _: &'ui Ui<'ui>) -> ChildWindowToken<'ui> { + /// Creates a child window and starts append to it. + /// + /// Returns `Some(ChildWindowToken)` if the window is visible. After content has been + /// rendered, the token must be ended by calling `.end()`. + /// + /// Returns `None` if the window is not visible and no content should be rendered. + pub fn begin(self, ui: &Ui) -> Option { if self.content_size[0] != 0.0 || self.content_size[1] != 0.0 { unsafe { sys::igSetNextWindowContentSize(self.content_size.into()) }; } @@ -253,45 +260,42 @@ impl<'a> ChildWindow<'a> { let should_render = unsafe { sys::igBeginChildID(id, self.size.into(), self.border, self.flags.bits() as i32) }; - ChildWindowToken { - should_render, - should_end: true, - _ui: PhantomData, + if should_render { + Some(ChildWindowToken { ctx: ui.ctx }) + } else { + unsafe { sys::igEndChild() }; + None } } - /// Builds this child window using the given closure to create the window content. + /// Creates a child window and runs a closure to construct the contents. /// /// Note: the closure is not called if no window content is visible (e.g. window is collapsed /// or fully clipped). pub fn build(self, ui: &Ui, f: F) { - let window = self.begin(ui); - if window.should_render { + if let Some(window) = self.begin(ui) { f(); + window.end(ui); } - window.end(); } } -/// Represents a child window pushed to the window stack -pub struct ChildWindowToken<'ui> { - /// True, if the child window contents should be rendered - pub should_render: bool, - should_end: bool, - _ui: PhantomData<&'ui Ui<'ui>>, +/// Tracks a child window that must be ended by calling `.end()` +pub struct ChildWindowToken { + ctx: *const Context, } -impl<'ui> ChildWindowToken<'ui> { - /// Finishes the current child window and pops it from the window stack - pub fn end(mut self) { - self.should_end = false; +impl ChildWindowToken { + /// Ends a window + pub fn end(mut self, _: &Ui) { + self.ctx = ptr::null(); unsafe { sys::igEndChild() }; } } -impl<'ui> Drop for ChildWindowToken<'ui> { +impl Drop for ChildWindowToken { fn drop(&mut self) { - if self.should_end { - unsafe { sys::igEndChild() }; + if !self.ctx.is_null() && !thread::panicking() { + panic!("A ChildWindowToken was leaked. Did you call .end()?"); } } } diff --git a/src/window/mod.rs b/src/window/mod.rs index 92dbc42..4bd3828 100644 --- a/src/window/mod.rs +++ b/src/window/mod.rs @@ -1,8 +1,9 @@ use bitflags::bitflags; use std::f32; -use std::marker::PhantomData; use std::ptr; +use std::thread; +use crate::context::Context; use crate::string::ImStr; use crate::sys; use crate::{Condition, Ui}; @@ -470,8 +471,14 @@ impl<'a> Window<'a> { self.flags |= WindowFlags::NO_INPUTS; self } - /// Builds this window, pushes it to the window stack, and starts appending to it - pub fn begin<'ui>(self, _: &'ui Ui<'ui>) -> WindowToken<'ui> { + /// Creates a window and starts appending to it. + /// + /// Returns `Some(WindowToken)` if the window is visible. After content has been + /// rendered, the token must be ended by calling `.end()`. + /// + /// Returns `None` if the window is not visible and no content should be rendered. + #[must_use] + pub fn begin(self, ui: &Ui) -> Option { if self.pos_cond != Condition::Never { unsafe { sys::igSetNextWindowPos( @@ -516,45 +523,42 @@ impl<'a> Window<'a> { self.flags.bits() as i32, ) }; - WindowToken { - should_render, - should_end: true, - _ui: PhantomData, + if should_render { + Some(WindowToken { ctx: ui.ctx }) + } else { + unsafe { sys::igEnd() }; + None } } - /// Builds this window using the given closure to create the window content. + /// Creates a window and runs a closure to construct the contents. /// /// Note: the closure is not called if no window content is visible (e.g. window is collapsed /// or fully clipped). pub fn build(self, ui: &Ui, f: F) { - let window = self.begin(ui); - if window.should_render { + if let Some(window) = self.begin(ui) { f(); + window.end(ui); } - window.end(); } } -/// Represents a window pushed to the window stack -pub struct WindowToken<'ui> { - /// True, if the window contents should be rendered - pub should_render: bool, - should_end: bool, - _ui: PhantomData<&'ui Ui<'ui>>, +/// Tracks a window that must be ended by calling `.end()` +pub struct WindowToken { + ctx: *const Context, } -impl<'ui> WindowToken<'ui> { - /// Finishes the current window and pops it from the window stack - pub fn end(mut self) { - self.should_end = false; +impl WindowToken { + /// Ends a window + pub fn end(mut self, _: &Ui) { + self.ctx = ptr::null(); unsafe { sys::igEnd() }; } } -impl<'ui> Drop for WindowToken<'ui> { +impl Drop for WindowToken { fn drop(&mut self) { - if self.should_end { - unsafe { sys::igEnd() }; + if !self.ctx.is_null() && !thread::panicking() { + panic!("A WindowToken was leaked. Did you call .end()?"); } } }