From c0b957116005460d7d4d793a3ff3a3726c617da7 Mon Sep 17 00:00:00 2001 From: Joonas Javanainen Date: Sat, 13 Jul 2019 12:13:10 +0300 Subject: [PATCH] Redesign combo box API --- CHANGELOG.markdown | 1 + imgui-examples/examples/test_window_impl.rs | 21 +- src/legacy.rs | 58 ++--- src/lib.rs | 26 +-- src/widget/combo_box.rs | 225 ++++++++++++++++++++ src/widget/mod.rs | 1 + 6 files changed, 269 insertions(+), 63 deletions(-) create mode 100644 src/widget/combo_box.rs diff --git a/CHANGELOG.markdown b/CHANGELOG.markdown index 74f42f9..ca21c89 100644 --- a/CHANGELOG.markdown +++ b/CHANGELOG.markdown @@ -15,6 +15,7 @@ - Redesigned color editor/picker API - Redesigned child window API (previously known as child frame) - Redesigned image / image button API +- Redesigned combo box API - Updated layout API - Renderer errors implement std::error::Error - Glium renderer re-exports imgui and glium diff --git a/imgui-examples/examples/test_window_impl.rs b/imgui-examples/examples/test_window_impl.rs index f8c471d..f475f41 100644 --- a/imgui-examples/examples/test_window_impl.rs +++ b/imgui-examples/examples/test_window_impl.rs @@ -26,8 +26,8 @@ struct State { no_close: bool, wrap_width: f32, buf: ImString, - item: i32, - item2: i32, + item: usize, + item2: usize, text: ImString, text_multiline: ImString, i0: i32, @@ -45,7 +45,7 @@ struct State { color_edit: ColorEditState, custom_rendering: CustomRenderingState, dont_ask_me_next_time: bool, - stacked_modals_item: i32, + stacked_modals_item: usize, stacked_modals_color: [f32; 4], } @@ -139,7 +139,7 @@ impl Default for ColorEditState { struct FileMenuState { enabled: bool, f: f32, - n: i32, + n: usize, b: bool, } @@ -438,8 +438,7 @@ fn show_test_window(ui: &Ui, state: &mut State, opened: &mut bool) { ui.separator(); ui.label_text(im_str!("label"), im_str!("Value")); - ui.combo( - im_str!("combo"), + ComboBox::new(im_str!("combo")).build_simple_string(ui, &mut state.item, &[ im_str!("aaaa"), @@ -447,9 +446,7 @@ fn show_test_window(ui: &Ui, state: &mut State, opened: &mut bool) { im_str!("cccc"), im_str!("dddd"), im_str!("eeee"), - ], - -1, - ); + ]); let items = [ im_str!("AAAA"), im_str!("BBBB"), @@ -463,7 +460,7 @@ fn show_test_window(ui: &Ui, state: &mut State, opened: &mut bool) { im_str!("JJJJ"), im_str!("KKKK"), ]; - ui.combo(im_str!("combo scroll"), &mut state.item2, &items, -1); + ComboBox::new(im_str!("combo scroll")).build_simple_string(ui, &mut state.item2, &items); ui.input_text(im_str!("input text"), &mut state.text) .build(); ui.input_int(im_str!("input int"), &mut state.i0).build(); @@ -672,7 +669,7 @@ CTRL+click on individual component to input value.\n", ); let items = &[im_str!("aaaa"), im_str!("bbbb"), im_str!("cccc"), im_str!("dddd"), im_str!("eeee")]; - ui.combo(im_str!("Combo"), &mut state.stacked_modals_item, items, -1); + ComboBox::new(im_str!("Combo")).build_simple_string(ui, &mut state.stacked_modals_item, items); ColorEdit::new(im_str!("color"), &mut state.stacked_modals_color).build(ui); @@ -763,7 +760,7 @@ fn show_example_menu_file<'a>(ui: &Ui<'a>, state: &mut FileMenuState) { .step(0.1) .build(); let items = [im_str!("Yes"), im_str!("No"), im_str!("Maybe")]; - ui.combo(im_str!("Combo"), &mut state.n, &items, -1); + ComboBox::new(im_str!("Combo")).build_simple_string(ui, &mut state.n, &items); ui.checkbox(im_str!("Check"), &mut state.b); }); ui.menu(im_str!("Colors")).build(|| { diff --git a/src/legacy.rs b/src/legacy.rs index 0865eab..bc55172 100644 --- a/src/legacy.rs +++ b/src/legacy.rs @@ -1,10 +1,11 @@ #![allow(non_upper_case_globals)] use bitflags::bitflags; -use std::os::raw::c_int; +use std::os::raw::{c_char, c_int}; use crate::render::renderer::TextureId; use crate::string::ImStr; use crate::widget::color_editors::*; +use crate::widget::combo_box::*; use crate::widget::image::{Image, ImageButton}; use crate::widget::progress_bar::ProgressBar; use crate::window::{Window, WindowFlags, WindowFocusedFlags}; @@ -13,31 +14,8 @@ use crate::{Id, Ui}; #[deprecated(since = "0.2.0", note = "use ColorEditFlags instead")] pub type ImGuiColorEditFlags = ColorEditFlags; -bitflags!( - /// Flags for combo boxes - #[repr(C)] - pub struct ImGuiComboFlags: c_int { - /// Align the popup toward the left by default - const PopupAlignLeft = 1; - /// Max ~4 items visible. - const HeightSmall = 1 << 1; - /// Max ~8 items visible (default) - const HeightRegular = 1 << 2; - /// Max ~20 items visible - const HeightLarge = 1 << 3; - /// As many fitting items as possible - const HeightLargest = 1 << 4; - /// Display on the preview box without the square arrow button - const NoArrowButton = 1 << 5; - /// Display only a square arrow button - const NoPreview = 1 << 6; - - const HeightMask = ImGuiComboFlags::HeightSmall.bits - | ImGuiComboFlags::HeightRegular.bits - | ImGuiComboFlags::HeightLarge.bits - | ImGuiComboFlags::HeightLargest.bits; - } -); +#[deprecated(since = "0.2.0", note = "use ComboFlags instead")] +pub type ImGuiComboFlags = ComboBoxFlags; bitflags!( /// Flags for igBeginDragDropSource(), igAcceptDragDropPayload() @@ -359,3 +337,31 @@ impl<'ui> Ui<'ui> { ImageButton::new(texture, size) } } + +impl<'ui> Ui<'ui> { + #[deprecated( + since = "0.2.0", + note = "use imgui::ComboBox::new(...), and either build_simple(), build_simple_string(), or custom rendering (e.g. selectables) instead" + )] + pub fn combo<'p, StringType: AsRef + ?Sized>( + &self, + label: &'p ImStr, + current_item: &mut i32, + items: &'p [&'p StringType], + height_in_items: i32, + ) -> bool { + let items_inner: Vec<*const c_char> = items + .into_iter() + .map(|item| item.as_ref().as_ptr()) + .collect(); + unsafe { + sys::igCombo( + label.as_ptr(), + current_item, + items_inner.as_ptr() as *mut *const c_char, + items_inner.len() as i32, + height_in_items, + ) + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 16ab3d6..4773643 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -43,6 +43,7 @@ pub use self::style::*; pub use self::trees::{CollapsingHeader, TreeNode}; pub use self::utils::*; pub use self::widget::color_editors::*; +pub use self::widget::combo_box::*; pub use self::widget::image::*; pub use self::widget::progress_bar::*; pub use self::window::child_window::*; @@ -557,31 +558,6 @@ impl<'ui> Ui<'ui> { } } -// Widgets: Combos -impl<'ui> Ui<'ui> { - pub fn combo<'p, StringType: AsRef + ?Sized>( - &self, - label: &'p ImStr, - current_item: &mut i32, - items: &'p [&'p StringType], - height_in_items: i32, - ) -> bool { - let items_inner: Vec<*const c_char> = items - .into_iter() - .map(|item| item.as_ref().as_ptr()) - .collect(); - unsafe { - sys::igCombo( - label.as_ptr(), - current_item, - items_inner.as_ptr() as *mut *const c_char, - items_inner.len() as i32, - height_in_items, - ) - } - } -} - // Widgets: ListBox impl<'ui> Ui<'ui> { pub fn list_box<'p, StringType: AsRef + ?Sized>( diff --git a/src/widget/combo_box.rs b/src/widget/combo_box.rs new file mode 100644 index 0000000..87d257c --- /dev/null +++ b/src/widget/combo_box.rs @@ -0,0 +1,225 @@ +use bitflags::bitflags; +use std::borrow::Cow; +use std::marker::PhantomData; +use std::ptr; + +use crate::string::ImStr; +use crate::sys; +use crate::Ui; + +// TODO: support size constraints + +/// Combo box height mode. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum ComboBoxHeight { + /// Max ~4 items visible. + Small, + /// Max ~8 items visible. + Regular, + /// Max ~20 items visible. + Large, + /// As many fitting items as possible visible. + Largest, +} + +/// Combo box preview mode. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum ComboBoxPreviewMode { + /// Show only a box with the preview value + Label, + /// Show only an arrow button + ArrowButton, + /// Show a box with the preview value and an arrow button + Full, +} + +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; + } +); + +/// Builder for a combo box widget +#[derive(Clone, Debug)] +#[must_use] +pub struct ComboBox<'a> { + label: &'a ImStr, + preview_value: Option<&'a ImStr>, + flags: ComboBoxFlags, +} + +impl<'a> ComboBox<'a> { + /// Constructs a new combo box builder. + pub fn new(label: &'a ImStr) -> ComboBox<'a> { + ComboBox { + label, + preview_value: None, + flags: ComboBoxFlags::empty(), + } + } + /// Sets the preview value displayed in the preview box (if visible). + #[inline] + pub fn preview_value(mut self, preview_value: &'a ImStr) -> Self { + self.preview_value = Some(preview_value); + self + } + /// Replaces all current settings with the given flags. + #[inline] + pub fn flags(mut self, flags: ComboBoxFlags) -> Self { + self.flags = flags; + self + } + /// Enables/disables aligning the combo box popup toward the left. + /// + /// Disabled by default. + #[inline] + pub fn popup_align_left(mut self, popup_align_left: bool) -> Self { + self.flags + .set(ComboBoxFlags::POPUP_ALIGN_LEFT, popup_align_left); + self + } + /// Sets the combo box height. + /// + /// Default: `ComboBoxHeight::Regular` + #[inline] + pub fn height(mut self, height: ComboBoxHeight) -> Self { + self.flags + .set(ComboBoxFlags::HEIGHT_SMALL, height == ComboBoxHeight::Small); + self.flags.set( + ComboBoxFlags::HEIGHT_REGULAR, + height == ComboBoxHeight::Regular, + ); + self.flags + .set(ComboBoxFlags::HEIGHT_LARGE, height == ComboBoxHeight::Large); + self.flags.set( + ComboBoxFlags::HEIGHT_LARGEST, + height == ComboBoxHeight::Largest, + ); + self + } + /// Sets the combo box preview mode. + /// + /// Default: `ComboBoxPreviewMode::Full` + #[inline] + pub fn preview_mode(mut self, preview_mode: ComboBoxPreviewMode) -> Self { + self.flags.set( + ComboBoxFlags::NO_ARROW_BUTTON, + preview_mode == ComboBoxPreviewMode::Label, + ); + self.flags.set( + ComboBoxFlags::NO_PREVIEW, + preview_mode == ComboBoxPreviewMode::ArrowButton, + ); + self + } + /// Builds this combo box, and starts appending to it. + /// + /// Returns `None` if the combo box is not open and no content should be rendered. + pub fn begin<'ui>(self, _: &'ui Ui<'ui>) -> Option> { + let should_render = unsafe { + sys::igBeginCombo( + self.label.as_ptr(), + self.preview_value.map(ImStr::as_ptr).unwrap_or(ptr::null()), + self.flags.bits() as i32, + ) + }; + if should_render { + Some(ComboBoxToken { + should_end: true, + _ui: PhantomData, + }) + } else { + None + } + } + /// Builds the combo box. + /// + /// 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) { + f(); + } + } +} + +/// Represents a combo box pushed to the window stack +pub struct ComboBoxToken<'ui> { + should_end: bool, + _ui: PhantomData<&'ui Ui<'ui>>, +} + +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; + unsafe { sys::igEndCombo() }; + } +} + +impl<'ui> Drop for ComboBoxToken<'ui> { + fn drop(&mut self) { + if self.should_end { + unsafe { sys::igEndCombo() }; + } + } +} + +/// # Convenience functions +impl<'a> ComboBox<'a> { + /// Builds a simple combo box for choosing from a slice of values + pub fn build_simple( + self, + ui: &Ui, + current_item: &mut usize, + items: &[T], + label_fn: &L, + ) -> bool + where + for<'b> L: Fn(&'b T) -> Cow<'b, ImStr>, + { + let mut result = false; + let mut cb = self; + let preview_value = items.get(*current_item).map(label_fn); + if cb.preview_value.is_none() { + if let Some(preview_value) = preview_value.as_ref() { + cb = cb.preview_value(preview_value); + } + } + if let Some(_cb) = cb.begin(ui) { + for (idx, item) in items.iter().enumerate() { + let text = label_fn(item); + let selected = idx == *current_item; + // TODO: use safe API + let change = + unsafe { sys::igSelectable(text.as_ptr(), selected, 0, [0.0, 0.0].into()) }; + if change { + *current_item = idx; + result = true; + } + } + } + result + } + /// Builds a simple combo box for choosing from a slice of strings + pub fn build_simple_string(self, ui: &Ui, current_item: &mut usize, items: &[&S]) -> bool + where + S: AsRef + ?Sized, + { + self.build_simple(ui, current_item, items, &|&s| s.as_ref().into()) + } +} diff --git a/src/widget/mod.rs b/src/widget/mod.rs index d85a932..8fd6f2f 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -1,4 +1,5 @@ pub mod color_editors; +pub mod combo_box; pub mod image; pub mod misc; pub mod progress_bar;