diff --git a/imgui-examples/examples/test_window_impl.rs b/imgui-examples/examples/test_window_impl.rs index 70e229f..cb964b6 100644 --- a/imgui-examples/examples/test_window_impl.rs +++ b/imgui-examples/examples/test_window_impl.rs @@ -47,6 +47,8 @@ struct State { dont_ask_me_next_time: bool, stacked_modals_item: usize, stacked_modals_color: [f32; 4], + + tabs: TabState, } impl Default for State { @@ -102,6 +104,41 @@ impl Default for State { dont_ask_me_next_time: false, stacked_modals_item: 0, stacked_modals_color: [0.4, 0.7, 0.0, 0.5], + tabs: TabState::default(), + } + } +} + +struct TabState { + // flags for the advanced tab example + reorderable: bool, + autoselect: bool, + listbutton: bool, + noclose_middlebutton: bool, + fitting_resizedown: bool, + fitting_scroll: bool, + + // opened state for tab items + artichoke_tab: bool, + beetroot_tab: bool, + celery_tab: bool, + daikon_tab: bool, +} + +impl Default for TabState { + fn default() -> Self { + Self { + reorderable: true, + autoselect: false, + listbutton: false, + noclose_middlebutton: false, + fitting_resizedown: true, + fitting_scroll: false, + + artichoke_tab: true, + beetroot_tab: true, + celery_tab: true, + daikon_tab: true, } } } @@ -365,6 +402,7 @@ fn show_test_window(ui: &Ui, state: &mut State, opened: &mut bool) { }); } }); + TreeNode::new(im_str!("Bullets")).build(&ui, || { ui.bullet_text(im_str!("Bullet point 1")); ui.bullet_text(im_str!("Bullet point 2\nOn multiple lines")); @@ -585,6 +623,80 @@ CTRL+click on individual component to input value.\n", b.build(ui); }); } + + if CollapsingHeader::new(im_str!("Layout")).build(&ui) { + TreeNode::new(im_str!("Tabs")).build(&ui, || { + TreeNode::new(im_str!("Basic")).build(&ui, || { + TabBar::new(im_str!("basictabbar")).build(&ui, || { + TabItem::new(im_str!("Avocado")).build(&ui, || { + ui.text(im_str!("This is the Avocado tab!")); + ui.text(im_str!("blah blah blah blah blah")); + }); + TabItem::new(im_str!("Broccoli")).build(&ui, || { + ui.text(im_str!("This is the Broccoli tab!")); + ui.text(im_str!("blah blah blah blah blah")); + }); + TabItem::new(im_str!("Cucumber")).build(&ui, || { + ui.text(im_str!("This is the Cucumber tab!")); + ui.text(im_str!("blah blah blah blah blah")); + }); + }); + + }); + TreeNode::new(im_str!("Advanced & Close button")).build(&ui, || { + + ui.separator(); + let s = &mut state.tabs; + + ui.checkbox(im_str!("ImGuiTabBarFlags_Reorderable"), &mut s.reorderable); + ui.checkbox(im_str!("ImGuiTabBarFlags_AutoSelectNewTabs"), &mut s.autoselect); + ui.checkbox(im_str!("ImGuiTabBarFlags_TabListPopupButton"), &mut s.listbutton); + ui.checkbox(im_str!("ImGuiTabBarFlags_NoCloseWithMiddleMouseButton"), &mut s.noclose_middlebutton); + if ui.checkbox(im_str!("ImGuiTabBarFlags_FittingPolicyResizeDown"), &mut s.fitting_resizedown) { + s.fitting_scroll = !s.fitting_resizedown; + } + if ui.checkbox(im_str!("ImGuiTabBarFlags_FittingPolicyScroll"), &mut s.fitting_scroll) { + s.fitting_resizedown = !s.fitting_scroll; + } + let style = ui.push_style_var(StyleVar::FramePadding([0.0, 0.0])); + ui.checkbox(im_str!("Artichoke"), &mut s.artichoke_tab); + ui.same_line(0.0); + ui.checkbox(im_str!("Beetroot"), &mut s.beetroot_tab); + ui.same_line(0.0); + ui.checkbox(im_str!("Celery"), &mut s.celery_tab); + ui.same_line(0.0); + ui.checkbox(im_str!("Daikon"), &mut s.daikon_tab); + style.pop(ui); + + let flags = { + let mut f = TabBarFlags::empty(); + f.set(TabBarFlags::REORDERABLE, s.reorderable); + f.set(TabBarFlags::AUTO_SELECT_NEW_TABS, s.autoselect); + f.set(TabBarFlags::TAB_LIST_POPUP_BUTTON, s.listbutton); + f.set(TabBarFlags::NO_CLOSE_WITH_MIDDLE_MOUSE_BUTTON, s.noclose_middlebutton); + f.set(TabBarFlags::FITTING_POLICY_RESIZE_DOWN, s.fitting_resizedown); + f.set(TabBarFlags::FITTING_POLICY_SCROLL, s.fitting_scroll); + f + }; + + TabBar::new(im_str!("tabbar")).flags(flags).build(&ui, || { + TabItem::new(im_str!("Artichoke")).opened(&mut s.artichoke_tab).build(&ui, || { + ui.text(im_str!("This is the Artichoke tab!")); + }); + TabItem::new(im_str!("Beetroot")).opened(&mut s.beetroot_tab).build(&ui, || { + ui.text(im_str!("This is the Beetroot tab!")); + }); + TabItem::new(im_str!("Celery")).opened(&mut s.celery_tab).build(&ui, || { + ui.text(im_str!("This is the Celery tab!")); + }); + TabItem::new(im_str!("Daikon")).opened(&mut s.daikon_tab).build(&ui, || { + ui.text(im_str!("This is the Daikon tab!")); + }); + }); + + }); + }); + } if CollapsingHeader::new(im_str!("Popups & Modal windows")).build(&ui) { TreeNode::new(im_str!("Popups")).build(&ui, || { ui.text_wrapped(im_str!( diff --git a/src/lib.rs b/src/lib.rs index 20a309d..3fd381b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -43,6 +43,7 @@ pub use self::widget::menu::*; pub use self::widget::progress_bar::*; pub use self::widget::selectable::*; pub use self::widget::slider::*; +pub use self::widget::tab::*; pub use self::widget::tree::*; pub use self::window::child_window::*; pub use self::window::*; diff --git a/src/widget/mod.rs b/src/widget/mod.rs index 5edc6b9..3244388 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -6,5 +6,6 @@ pub mod misc; pub mod progress_bar; pub mod selectable; pub mod slider; +pub mod tab; pub mod text; pub mod tree; diff --git a/src/widget/tab.rs b/src/widget/tab.rs new file mode 100644 index 0000000..7b77a74 --- /dev/null +++ b/src/widget/tab.rs @@ -0,0 +1,188 @@ +//! Safe wrapper around imgui-sys for tab menu. +//! +//! # Examples +// +//! ```no_run +//! # use imgui::*; +//! # let mut ctx = Context::create(); +//! # let ui = ctx.frame(); +//! +//! // During UI construction +//! TabBar::new(im_str!("tabbar")).build(&ui, || { +//! TabItem::new(im_str!("a tab")).build(&ui, || { +//! ui.text(im_str!("tab content 1")); +//! }); +//! TabItem::new(im_str!("2tab")).build(&ui, || { +//! ui.text(im_str!("tab content 2")); +//! }); +//! }); +//! ``` +//! +//! See `test_window_impl.rs` for a more complicated example. +use crate::context::Context; +use crate::string::ImStr; +use crate::sys; +use crate::Ui; +use bitflags::bitflags; +use std::{ptr, thread}; + +bitflags! { + #[repr(transparent)] + pub struct TabBarFlags: u32 { + const REORDERABLE = sys::ImGuiTabBarFlags_Reorderable; + const AUTO_SELECT_NEW_TABS = sys::ImGuiTabBarFlags_AutoSelectNewTabs; + const TAB_LIST_POPUP_BUTTON = sys::ImGuiTabBarFlags_TabListPopupButton; + const NO_CLOSE_WITH_MIDDLE_MOUSE_BUTTON = sys::ImGuiTabBarFlags_NoCloseWithMiddleMouseButton; + const NO_TAB_LIST_SCROLLING_BUTTONS = sys::ImGuiTabBarFlags_NoTabListScrollingButtons; + const NO_TOOLTIP = sys::ImGuiTabBarFlags_NoTooltip; + const FITTING_POLICY_RESIZE_DOWN = sys::ImGuiTabBarFlags_FittingPolicyResizeDown; + const FITTING_POLICY_SCROLL = sys::ImGuiTabBarFlags_FittingPolicyScroll; + const FITTING_POLICY_MASK = sys::ImGuiTabBarFlags_FittingPolicyMask_; + const FITTING_POLICY_DEFAULT = sys::ImGuiTabBarFlags_FittingPolicyDefault_; + } +} + +/// Builder for a tab bar. +pub struct TabBar<'a> { + id: &'a ImStr, + flags: TabBarFlags, +} + +impl<'a> TabBar<'a> { + pub fn new(id: &'a ImStr) -> Self { + Self { + id, + flags: TabBarFlags::empty(), + } + } + + /// Enable/Disable the reorderable property + /// + /// Disabled by default + #[inline] + pub fn reorderable(mut self, value: bool) -> Self { + self.flags.set(TabBarFlags::REORDERABLE, value); + self + } + + /// Set the flags of the tab bar. + /// + /// Flags are empty by default + #[inline] + pub fn flags(mut self, flags: TabBarFlags) -> Self { + self.flags = flags; + self + } + + #[must_use] + pub fn begin(self, ui: &Ui) -> Option { + let shoud_render = + unsafe { sys::igBeginTabBar(self.id.as_ptr(), self.flags.bits() as i32) }; + + if shoud_render { + Some(TabBarToken { ctx: ui.ctx }) + } else { + unsafe { sys::igEndTabBar() }; + None + } + } + + /// Creates a tab bar and runs a closure to construct the contents. + /// + /// Note: the closure is not called if no tabbar content is visible + pub fn build(self, ui: &Ui, f: F) { + if let Some(tab) = self.begin(ui) { + f(); + tab.end(ui); + } + } +} + +/// Tracks a window that must be ended by calling `.end()` +pub struct TabBarToken { + ctx: *const Context, +} + +impl TabBarToken { + /// Ends a tab bar + pub fn end(mut self, _: &Ui) { + self.ctx = ptr::null(); + unsafe { sys::igEndTabBar() }; + } +} + +impl Drop for TabBarToken { + fn drop(&mut self) { + if !self.ctx.is_null() && !thread::panicking() { + panic!("A TabBarToken was leaked. Did you call .end()?"); + } + } +} + +pub struct TabItem<'a> { + name: &'a ImStr, + opened: Option<&'a mut bool>, +} + +impl<'a> TabItem<'a> { + pub fn new(name: &'a ImStr) -> Self { + Self { name, opened: None } + } + + /// Will open or close the tab.\ + /// + /// True to display the tab. Tab item is visible by default. + #[inline] + pub fn opened(mut self, opened: &'a mut bool) -> Self { + self.opened = Some(opened); + self + } + + #[must_use] + pub fn begin(self, ui: &Ui) -> Option { + let shoud_render = unsafe { + sys::igBeginTabItem( + self.name.as_ptr(), + self.opened + .map(|x| x as *mut bool) + .unwrap_or(ptr::null_mut()), + 0 as ::std::os::raw::c_int, + ) + }; + + if shoud_render { + Some(TabItemToken { ctx: ui.ctx }) + } else { + None + } + } + + /// Creates a tab item and runs a closure to construct the contents. + /// + /// Note: the closure is not called if the tab item is not selected + pub fn build(self, ui: &Ui, f: F) { + if let Some(tab) = self.begin(ui) { + f(); + tab.end(ui); + } + } +} + +pub struct TabItemToken { + ctx: *const Context, +} + +impl TabItemToken { + pub fn end(mut self, _: &Ui) { + self.ctx = ptr::null(); + unsafe { sys::igEndTabItem() }; + } +} + +impl Drop for TabItemToken { + fn drop(&mut self) { + if !self.ctx.is_null() && !thread::panicking() { + panic!("A TabItemToken was leaked. Did you call .end()?"); + } + } +}