[widgets] combo box (drop down menu)
This commit is contained in:
parent
b081be11d1
commit
22fffc1793
8 changed files with 213 additions and 15 deletions
|
@ -4,6 +4,7 @@
|
|||
|
||||
* Color picker
|
||||
* Unicode characters in labels (limited by [what the default font supports](https://fonts.google.com/specimen/Comfortaa#glyphs))
|
||||
* Simple drop-down combo box menu
|
||||
|
||||
## 0.1.4 - 2020-09-08
|
||||
|
||||
|
|
124
egui/src/containers/combo_box.rs
Normal file
124
egui/src/containers/combo_box.rs
Normal file
|
@ -0,0 +1,124 @@
|
|||
use crate::{paint::PaintCmd, style::WidgetVisuals, *};
|
||||
|
||||
pub fn combo_box_with_label(
|
||||
ui: &mut Ui,
|
||||
label: impl Into<Label>,
|
||||
selected: impl Into<Label>,
|
||||
menu_contents: impl FnOnce(&mut Ui),
|
||||
) -> Response {
|
||||
let label = label.into();
|
||||
let button_id = ui.make_unique_child_id(label.text());
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
let mut response = combo_box(ui, button_id, selected, menu_contents);
|
||||
response |= ui.add(label);
|
||||
response
|
||||
})
|
||||
.0
|
||||
}
|
||||
|
||||
pub fn combo_box(
|
||||
ui: &mut Ui,
|
||||
button_id: Id,
|
||||
selected: impl Into<Label>,
|
||||
menu_contents: impl FnOnce(&mut Ui),
|
||||
) -> Response {
|
||||
let popup_id = button_id.with("popup");
|
||||
let selected = selected.into();
|
||||
|
||||
let button_active = ui.memory().is_popup_open(popup_id);
|
||||
let button_response = button_frame(ui, button_id, button_active, Sense::click(), |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
// We don't want to change width when user selects something new
|
||||
let full_minimum_width = ui.style().spacing.slider_width;
|
||||
let icon_width = ui.style().spacing.icon_width;
|
||||
|
||||
selected.ui(ui);
|
||||
|
||||
let advance = full_minimum_width - icon_width - ui.child_bounds().width();
|
||||
ui.advance_cursor(advance.at_least(0.0));
|
||||
|
||||
let icon_rect = ui.allocate_space(Vec2::splat(icon_width));
|
||||
let button_rect = ui.rect().expand2(ui.style().spacing.button_padding);
|
||||
let mut response = ui.interact(button_rect, button_id, Sense::click());
|
||||
response.active |= button_active;
|
||||
paint_icon(ui.painter(), icon_rect, ui.style().interact(&response));
|
||||
});
|
||||
});
|
||||
if button_response.clicked {
|
||||
ui.memory().toggle_popup(popup_id);
|
||||
}
|
||||
|
||||
if ui.memory().is_popup_open(popup_id) {
|
||||
Area::new(popup_id)
|
||||
.order(Order::Foreground)
|
||||
.fixed_pos(button_response.rect.left_bottom())
|
||||
.show(ui.ctx(), |ui| {
|
||||
let frame = Frame::popup(ui.style());
|
||||
let frame_margin = frame.margin;
|
||||
frame.show(ui, |ui| {
|
||||
ui.set_min_width(button_response.rect.width() - 2.0 * frame_margin.x);
|
||||
ui.set_layout(Layout::justified(Direction::Vertical));
|
||||
menu_contents(ui);
|
||||
})
|
||||
});
|
||||
|
||||
if ui.input().key_pressed(Key::Escape) || ui.input().mouse.click && !button_response.clicked
|
||||
{
|
||||
ui.memory().close_popup();
|
||||
}
|
||||
}
|
||||
|
||||
button_response
|
||||
}
|
||||
|
||||
fn button_frame(
|
||||
ui: &mut Ui,
|
||||
id: Id,
|
||||
button_active: bool,
|
||||
sense: Sense,
|
||||
add_contents: impl FnOnce(&mut Ui),
|
||||
) -> Response {
|
||||
let margin = ui.style().spacing.button_padding;
|
||||
let outer_rect_bounds = ui.available();
|
||||
let inner_rect = outer_rect_bounds.shrink2(margin);
|
||||
let where_to_put_background = ui.painter().add(PaintCmd::Noop);
|
||||
let mut content_ui = ui.child_ui(inner_rect);
|
||||
add_contents(&mut content_ui);
|
||||
|
||||
let outer_rect = Rect::from_min_max(
|
||||
outer_rect_bounds.min,
|
||||
content_ui.child_bounds().max + margin,
|
||||
);
|
||||
|
||||
let mut response = ui.interact(outer_rect, id, sense);
|
||||
response.active |= button_active;
|
||||
let style = ui.style().interact(&response);
|
||||
|
||||
ui.painter().set(
|
||||
where_to_put_background,
|
||||
PaintCmd::Rect {
|
||||
rect: outer_rect,
|
||||
corner_radius: style.corner_radius,
|
||||
fill: style.bg_fill,
|
||||
stroke: style.bg_stroke,
|
||||
},
|
||||
);
|
||||
|
||||
ui.allocate_space(outer_rect.size());
|
||||
|
||||
response
|
||||
}
|
||||
|
||||
fn paint_icon(painter: &Painter, rect: Rect, visuals: &WidgetVisuals) {
|
||||
let rect = Rect::from_center_size(
|
||||
rect.center(),
|
||||
vec2(rect.width() * 0.7, rect.height() * 0.45),
|
||||
);
|
||||
painter.add(PaintCmd::Path {
|
||||
points: vec![rect.left_top(), rect.right_top(), rect.center_bottom()],
|
||||
closed: true,
|
||||
fill: Default::default(),
|
||||
stroke: visuals.fg_stroke,
|
||||
});
|
||||
}
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
pub(crate) mod area;
|
||||
pub(crate) mod collapsing_header;
|
||||
mod combo_box;
|
||||
pub(crate) mod frame;
|
||||
pub(crate) mod popup;
|
||||
pub(crate) mod resize;
|
||||
|
@ -11,6 +12,6 @@ pub(crate) mod scroll_area;
|
|||
pub(crate) mod window;
|
||||
|
||||
pub use {
|
||||
area::Area, collapsing_header::CollapsingHeader, frame::Frame, popup::*, resize::Resize,
|
||||
scroll_area::ScrollArea, window::Window,
|
||||
area::Area, collapsing_header::CollapsingHeader, combo_box::*, frame::Frame, popup::*,
|
||||
resize::Resize, scroll_area::ScrollArea, window::Window,
|
||||
};
|
||||
|
|
|
@ -608,8 +608,7 @@ fn show_title_bar(
|
|||
let button_size = ui.style().spacing.icon_width;
|
||||
|
||||
if collapsible {
|
||||
// TODO: make clickable radius larger
|
||||
ui.allocate_space(vec2(0.0, 0.0)); // HACK: will add left spacing
|
||||
ui.advance_cursor(ui.style().spacing.item_spacing.x);
|
||||
|
||||
let rect = ui.allocate_space(Vec2::splat(button_size));
|
||||
let collapse_button_response = ui.interact(rect, collapsing_id, Sense::click());
|
||||
|
|
|
@ -1,11 +1,25 @@
|
|||
use crate::{color::*, *};
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
enum Enum {
|
||||
First,
|
||||
Second,
|
||||
Third,
|
||||
}
|
||||
|
||||
impl Default for Enum {
|
||||
fn default() -> Self {
|
||||
Enum::First
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub struct Widgets {
|
||||
button_enabled: bool,
|
||||
count: usize,
|
||||
radio: usize,
|
||||
radio: Enum,
|
||||
slider_value: f32,
|
||||
angle: f32,
|
||||
color: Srgba,
|
||||
|
@ -18,7 +32,7 @@ impl Default for Widgets {
|
|||
fn default() -> Self {
|
||||
Self {
|
||||
button_enabled: true,
|
||||
radio: 0,
|
||||
radio: Enum::First,
|
||||
count: 0,
|
||||
slider_value: 3.4,
|
||||
angle: TAU / 8.0,
|
||||
|
@ -48,9 +62,15 @@ impl Widgets {
|
|||
.tooltip_text("The current font supports only a few non-latin characters and Egui does not currently support right-to-left text.");
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.radio_value("First", &mut self.radio, 0);
|
||||
ui.radio_value("Second", &mut self.radio, 1);
|
||||
ui.radio_value("Final", &mut self.radio, 2);
|
||||
ui.radio_value("First", &mut self.radio, Enum::First);
|
||||
ui.radio_value("Second", &mut self.radio, Enum::Second);
|
||||
ui.radio_value("Third", &mut self.radio, Enum::Third);
|
||||
});
|
||||
|
||||
combo_box_with_label(ui, "Combo Box", format!("{:?}", self.radio), |ui| {
|
||||
ui.radio_value("First", &mut self.radio, Enum::First);
|
||||
ui.radio_value("Second", &mut self.radio, Enum::Second);
|
||||
ui.radio_value("Third", &mut self.radio, Enum::Third);
|
||||
});
|
||||
|
||||
ui.add(Checkbox::new(&mut self.button_enabled, "Button enabled"));
|
||||
|
|
|
@ -45,8 +45,9 @@ pub struct Memory {
|
|||
pub(crate) color_cache: Cache<Srgba, Hsva>,
|
||||
|
||||
/// Which popup-window is open (if any)?
|
||||
/// Could be a combo box, color picker, menu etc.
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
pub(crate) popup: Option<Id>,
|
||||
popup: Option<Id>,
|
||||
}
|
||||
|
||||
/// Say there is a button in a scroll area.
|
||||
|
@ -173,6 +174,31 @@ impl Memory {
|
|||
}
|
||||
}
|
||||
|
||||
/// ## Popups
|
||||
/// Popups are things like combo-boxes, color pickers, menus etc.
|
||||
/// Only one can be be open at a time.
|
||||
impl Memory {
|
||||
pub fn is_popup_open(&mut self, popup_id: Id) -> bool {
|
||||
self.popup == Some(popup_id)
|
||||
}
|
||||
|
||||
pub fn open_popup(&mut self, popup_id: Id) {
|
||||
self.popup = Some(popup_id);
|
||||
}
|
||||
|
||||
pub fn close_popup(&mut self) {
|
||||
self.popup = None;
|
||||
}
|
||||
|
||||
pub fn toggle_popup(&mut self, popup_id: Id) {
|
||||
if self.is_popup_open(popup_id) {
|
||||
self.close_popup();
|
||||
} else {
|
||||
self.open_popup(popup_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Areas {
|
||||
pub(crate) fn count(&self) -> usize {
|
||||
self.areas.len()
|
||||
|
|
|
@ -193,6 +193,11 @@ impl Ui {
|
|||
Rect::from_min_max(self.top_left(), bottom_right)
|
||||
}
|
||||
|
||||
pub fn set_min_width(&mut self, width: f32) {
|
||||
self.child_bounds.max.x = self.child_bounds.max.x.max(self.child_bounds.min.x + width);
|
||||
self.desired_rect.max.x = self.desired_rect.max.x.max(self.desired_rect.min.x + width);
|
||||
}
|
||||
|
||||
/// Set the width of the ui.
|
||||
/// You won't be able to shrink it beyond its current child bounds.
|
||||
pub fn set_desired_width(&mut self, width: f32) {
|
||||
|
@ -357,6 +362,28 @@ impl Ui {
|
|||
// ------------------------------------------------------------------------
|
||||
// Stuff that moves the cursor, i.e. allocates space in this ui!
|
||||
|
||||
/// Advance the cursor (where the next widget is put) by this many points.
|
||||
/// The direction is dependent on the layout.
|
||||
/// This is useful for creating some extra space between widgets.
|
||||
pub fn advance_cursor(&mut self, amount: f32) {
|
||||
match self.layout.dir() {
|
||||
Direction::Horizontal => {
|
||||
if self.layout.is_reversed() {
|
||||
self.cursor.x -= amount;
|
||||
} else {
|
||||
self.cursor.x += amount;
|
||||
}
|
||||
}
|
||||
Direction::Vertical => {
|
||||
if self.layout.is_reversed() {
|
||||
self.cursor.y -= amount;
|
||||
} else {
|
||||
self.cursor.y += amount;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reserve this much space and move the cursor.
|
||||
/// Returns where to put the widget.
|
||||
///
|
||||
|
|
|
@ -219,15 +219,15 @@ fn color_picker_hsva_2d(ui: &mut Ui, hsva: &mut Hsva) {
|
|||
}
|
||||
|
||||
pub fn color_edit_button_hsva(ui: &mut Ui, hsva: &mut Hsva) -> Response {
|
||||
let id = ui.make_position_id().with("foo");
|
||||
let pupup_id = ui.make_position_id().with("popup");
|
||||
let button_response = color_button(ui, (*hsva).into()).tooltip_text("Click to edit color");
|
||||
|
||||
if button_response.clicked {
|
||||
ui.memory().popup = Some(id);
|
||||
ui.memory().toggle_popup(pupup_id);
|
||||
}
|
||||
// TODO: make it easier to show a temporary popup that closes when you click outside it
|
||||
if ui.memory().popup == Some(id) {
|
||||
let area_response = Area::new(id)
|
||||
if ui.memory().is_popup_open(pupup_id) {
|
||||
let area_response = Area::new(pupup_id)
|
||||
.order(Order::Foreground)
|
||||
.default_pos(button_response.rect.max)
|
||||
.show(ui.ctx(), |ui| {
|
||||
|
@ -239,7 +239,7 @@ pub fn color_edit_button_hsva(ui: &mut Ui, hsva: &mut Hsva) -> Response {
|
|||
if !button_response.clicked {
|
||||
let clicked_outside = ui.input().mouse.click && !area_response.hovered;
|
||||
if clicked_outside || ui.input().key_pressed(Key::Escape) {
|
||||
ui.memory().popup = None;
|
||||
ui.memory().close_popup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue