[widgets] combo box (drop down menu)

This commit is contained in:
Emil Ernerfeldt 2020-09-25 14:43:16 +02:00
parent b081be11d1
commit 22fffc1793
8 changed files with 213 additions and 15 deletions

View file

@ -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

View 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,
});
}

View file

@ -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,
};

View file

@ -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());

View file

@ -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"));

View file

@ -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()

View file

@ -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.
///

View file

@ -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();
}
}
}