Break out Checkbox, RadioButton and ImageButton to their own files (#4278)
Just a refactor
This commit is contained in:
parent
a9a756e8f3
commit
7277322983
|
|
@ -336,379 +336,3 @@ impl Widget for Button<'_> {
|
||||||
response
|
response
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// TODO(emilk): allow checkbox without a text label
|
|
||||||
/// Boolean on/off control with text label.
|
|
||||||
///
|
|
||||||
/// Usually you'd use [`Ui::checkbox`] instead.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # egui::__run_test_ui(|ui| {
|
|
||||||
/// # let mut my_bool = true;
|
|
||||||
/// // These are equivalent:
|
|
||||||
/// ui.checkbox(&mut my_bool, "Checked");
|
|
||||||
/// ui.add(egui::Checkbox::new(&mut my_bool, "Checked"));
|
|
||||||
/// # });
|
|
||||||
/// ```
|
|
||||||
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
|
|
||||||
pub struct Checkbox<'a> {
|
|
||||||
checked: &'a mut bool,
|
|
||||||
text: WidgetText,
|
|
||||||
indeterminate: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Checkbox<'a> {
|
|
||||||
pub fn new(checked: &'a mut bool, text: impl Into<WidgetText>) -> Self {
|
|
||||||
Checkbox {
|
|
||||||
checked,
|
|
||||||
text: text.into(),
|
|
||||||
indeterminate: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn without_text(checked: &'a mut bool) -> Self {
|
|
||||||
Self::new(checked, WidgetText::default())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Display an indeterminate state (neither checked nor unchecked)
|
|
||||||
///
|
|
||||||
/// This only affects the checkbox's appearance. It will still toggle its boolean value when
|
|
||||||
/// clicked.
|
|
||||||
#[inline]
|
|
||||||
pub fn indeterminate(mut self, indeterminate: bool) -> Self {
|
|
||||||
self.indeterminate = indeterminate;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Widget for Checkbox<'a> {
|
|
||||||
fn ui(self, ui: &mut Ui) -> Response {
|
|
||||||
let Checkbox {
|
|
||||||
checked,
|
|
||||||
text,
|
|
||||||
indeterminate,
|
|
||||||
} = self;
|
|
||||||
|
|
||||||
let spacing = &ui.spacing();
|
|
||||||
let icon_width = spacing.icon_width;
|
|
||||||
let icon_spacing = spacing.icon_spacing;
|
|
||||||
|
|
||||||
let (galley, mut desired_size) = if text.is_empty() {
|
|
||||||
(None, vec2(icon_width, 0.0))
|
|
||||||
} else {
|
|
||||||
let total_extra = vec2(icon_width + icon_spacing, 0.0);
|
|
||||||
|
|
||||||
let wrap_width = ui.available_width() - total_extra.x;
|
|
||||||
let galley = text.into_galley(ui, None, wrap_width, TextStyle::Button);
|
|
||||||
|
|
||||||
let mut desired_size = total_extra + galley.size();
|
|
||||||
desired_size = desired_size.at_least(spacing.interact_size);
|
|
||||||
|
|
||||||
(Some(galley), desired_size)
|
|
||||||
};
|
|
||||||
|
|
||||||
desired_size = desired_size.at_least(Vec2::splat(spacing.interact_size.y));
|
|
||||||
desired_size.y = desired_size.y.max(icon_width);
|
|
||||||
let (rect, mut response) = ui.allocate_exact_size(desired_size, Sense::click());
|
|
||||||
|
|
||||||
if response.clicked() {
|
|
||||||
*checked = !*checked;
|
|
||||||
response.mark_changed();
|
|
||||||
}
|
|
||||||
response.widget_info(|| {
|
|
||||||
if indeterminate {
|
|
||||||
WidgetInfo::labeled(
|
|
||||||
WidgetType::Checkbox,
|
|
||||||
galley.as_ref().map_or("", |x| x.text()),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
WidgetInfo::selected(
|
|
||||||
WidgetType::Checkbox,
|
|
||||||
*checked,
|
|
||||||
galley.as_ref().map_or("", |x| x.text()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if ui.is_rect_visible(rect) {
|
|
||||||
// let visuals = ui.style().interact_selectable(&response, *checked); // too colorful
|
|
||||||
let visuals = ui.style().interact(&response);
|
|
||||||
let (small_icon_rect, big_icon_rect) = ui.spacing().icon_rectangles(rect);
|
|
||||||
ui.painter().add(epaint::RectShape::new(
|
|
||||||
big_icon_rect.expand(visuals.expansion),
|
|
||||||
visuals.rounding,
|
|
||||||
visuals.bg_fill,
|
|
||||||
visuals.bg_stroke,
|
|
||||||
));
|
|
||||||
|
|
||||||
if indeterminate {
|
|
||||||
// Horizontal line:
|
|
||||||
ui.painter().add(Shape::hline(
|
|
||||||
small_icon_rect.x_range(),
|
|
||||||
small_icon_rect.center().y,
|
|
||||||
visuals.fg_stroke,
|
|
||||||
));
|
|
||||||
} else if *checked {
|
|
||||||
// Check mark:
|
|
||||||
ui.painter().add(Shape::line(
|
|
||||||
vec![
|
|
||||||
pos2(small_icon_rect.left(), small_icon_rect.center().y),
|
|
||||||
pos2(small_icon_rect.center().x, small_icon_rect.bottom()),
|
|
||||||
pos2(small_icon_rect.right(), small_icon_rect.top()),
|
|
||||||
],
|
|
||||||
visuals.fg_stroke,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if let Some(galley) = galley {
|
|
||||||
let text_pos = pos2(
|
|
||||||
rect.min.x + icon_width + icon_spacing,
|
|
||||||
rect.center().y - 0.5 * galley.size().y,
|
|
||||||
);
|
|
||||||
ui.painter().galley(text_pos, galley, visuals.text_color());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
response
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/// One out of several alternatives, either selected or not.
|
|
||||||
///
|
|
||||||
/// Usually you'd use [`Ui::radio_value`] or [`Ui::radio`] instead.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # egui::__run_test_ui(|ui| {
|
|
||||||
/// #[derive(PartialEq)]
|
|
||||||
/// enum Enum { First, Second, Third }
|
|
||||||
/// let mut my_enum = Enum::First;
|
|
||||||
///
|
|
||||||
/// ui.radio_value(&mut my_enum, Enum::First, "First");
|
|
||||||
///
|
|
||||||
/// // is equivalent to:
|
|
||||||
///
|
|
||||||
/// if ui.add(egui::RadioButton::new(my_enum == Enum::First, "First")).clicked() {
|
|
||||||
/// my_enum = Enum::First
|
|
||||||
/// }
|
|
||||||
/// # });
|
|
||||||
/// ```
|
|
||||||
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
|
|
||||||
pub struct RadioButton {
|
|
||||||
checked: bool,
|
|
||||||
text: WidgetText,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RadioButton {
|
|
||||||
pub fn new(checked: bool, text: impl Into<WidgetText>) -> Self {
|
|
||||||
Self {
|
|
||||||
checked,
|
|
||||||
text: text.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Widget for RadioButton {
|
|
||||||
fn ui(self, ui: &mut Ui) -> Response {
|
|
||||||
let Self { checked, text } = self;
|
|
||||||
|
|
||||||
let spacing = &ui.spacing();
|
|
||||||
let icon_width = spacing.icon_width;
|
|
||||||
let icon_spacing = spacing.icon_spacing;
|
|
||||||
|
|
||||||
let (galley, mut desired_size) = if text.is_empty() {
|
|
||||||
(None, vec2(icon_width, 0.0))
|
|
||||||
} else {
|
|
||||||
let total_extra = vec2(icon_width + icon_spacing, 0.0);
|
|
||||||
|
|
||||||
let wrap_width = ui.available_width() - total_extra.x;
|
|
||||||
let text = text.into_galley(ui, None, wrap_width, TextStyle::Button);
|
|
||||||
|
|
||||||
let mut desired_size = total_extra + text.size();
|
|
||||||
desired_size = desired_size.at_least(spacing.interact_size);
|
|
||||||
|
|
||||||
(Some(text), desired_size)
|
|
||||||
};
|
|
||||||
|
|
||||||
desired_size = desired_size.at_least(Vec2::splat(spacing.interact_size.y));
|
|
||||||
desired_size.y = desired_size.y.max(icon_width);
|
|
||||||
let (rect, response) = ui.allocate_exact_size(desired_size, Sense::click());
|
|
||||||
|
|
||||||
response.widget_info(|| {
|
|
||||||
WidgetInfo::selected(
|
|
||||||
WidgetType::RadioButton,
|
|
||||||
checked,
|
|
||||||
galley.as_ref().map_or("", |x| x.text()),
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
if ui.is_rect_visible(rect) {
|
|
||||||
// let visuals = ui.style().interact_selectable(&response, checked); // too colorful
|
|
||||||
let visuals = ui.style().interact(&response);
|
|
||||||
|
|
||||||
let (small_icon_rect, big_icon_rect) = ui.spacing().icon_rectangles(rect);
|
|
||||||
|
|
||||||
let painter = ui.painter();
|
|
||||||
|
|
||||||
painter.add(epaint::CircleShape {
|
|
||||||
center: big_icon_rect.center(),
|
|
||||||
radius: big_icon_rect.width() / 2.0 + visuals.expansion,
|
|
||||||
fill: visuals.bg_fill,
|
|
||||||
stroke: visuals.bg_stroke,
|
|
||||||
});
|
|
||||||
|
|
||||||
if checked {
|
|
||||||
painter.add(epaint::CircleShape {
|
|
||||||
center: small_icon_rect.center(),
|
|
||||||
radius: small_icon_rect.width() / 3.0,
|
|
||||||
fill: visuals.fg_stroke.color, // Intentional to use stroke and not fill
|
|
||||||
// fill: ui.visuals().selection.stroke.color, // too much color
|
|
||||||
stroke: Default::default(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(galley) = galley {
|
|
||||||
let text_pos = pos2(
|
|
||||||
rect.min.x + icon_width + icon_spacing,
|
|
||||||
rect.center().y - 0.5 * galley.size().y,
|
|
||||||
);
|
|
||||||
ui.painter().galley(text_pos, galley, visuals.text_color());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
response
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/// A clickable image within a frame.
|
|
||||||
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct ImageButton<'a> {
|
|
||||||
image: Image<'a>,
|
|
||||||
sense: Sense,
|
|
||||||
frame: bool,
|
|
||||||
selected: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> ImageButton<'a> {
|
|
||||||
pub fn new(image: impl Into<Image<'a>>) -> Self {
|
|
||||||
Self {
|
|
||||||
image: image.into(),
|
|
||||||
sense: Sense::click(),
|
|
||||||
frame: true,
|
|
||||||
selected: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Select UV range. Default is (0,0) in top-left, (1,1) bottom right.
|
|
||||||
#[inline]
|
|
||||||
pub fn uv(mut self, uv: impl Into<Rect>) -> Self {
|
|
||||||
self.image = self.image.uv(uv);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Multiply image color with this. Default is WHITE (no tint).
|
|
||||||
#[inline]
|
|
||||||
pub fn tint(mut self, tint: impl Into<Color32>) -> Self {
|
|
||||||
self.image = self.image.tint(tint);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// If `true`, mark this button as "selected".
|
|
||||||
#[inline]
|
|
||||||
pub fn selected(mut self, selected: bool) -> Self {
|
|
||||||
self.selected = selected;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Turn off the frame
|
|
||||||
#[inline]
|
|
||||||
pub fn frame(mut self, frame: bool) -> Self {
|
|
||||||
self.frame = frame;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// By default, buttons senses clicks.
|
|
||||||
/// Change this to a drag-button with `Sense::drag()`.
|
|
||||||
#[inline]
|
|
||||||
pub fn sense(mut self, sense: Sense) -> Self {
|
|
||||||
self.sense = sense;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set rounding for the `ImageButton`.
|
|
||||||
/// If the underlying image already has rounding, this
|
|
||||||
/// will override that value.
|
|
||||||
#[inline]
|
|
||||||
pub fn rounding(mut self, rounding: impl Into<Rounding>) -> Self {
|
|
||||||
self.image = self.image.rounding(rounding.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Widget for ImageButton<'a> {
|
|
||||||
fn ui(self, ui: &mut Ui) -> Response {
|
|
||||||
let padding = if self.frame {
|
|
||||||
// so we can see that it is a button:
|
|
||||||
Vec2::splat(ui.spacing().button_padding.x)
|
|
||||||
} else {
|
|
||||||
Vec2::ZERO
|
|
||||||
};
|
|
||||||
|
|
||||||
let available_size_for_image = ui.available_size() - 2.0 * padding;
|
|
||||||
let tlr = self.image.load_for_size(ui.ctx(), available_size_for_image);
|
|
||||||
let original_image_size = tlr.as_ref().ok().and_then(|t| t.size());
|
|
||||||
let image_size = self
|
|
||||||
.image
|
|
||||||
.calc_size(available_size_for_image, original_image_size);
|
|
||||||
|
|
||||||
let padded_size = image_size + 2.0 * padding;
|
|
||||||
let (rect, response) = ui.allocate_exact_size(padded_size, self.sense);
|
|
||||||
response.widget_info(|| WidgetInfo::new(WidgetType::ImageButton));
|
|
||||||
|
|
||||||
if ui.is_rect_visible(rect) {
|
|
||||||
let (expansion, rounding, fill, stroke) = if self.selected {
|
|
||||||
let selection = ui.visuals().selection;
|
|
||||||
(
|
|
||||||
Vec2::ZERO,
|
|
||||||
self.image.image_options().rounding,
|
|
||||||
selection.bg_fill,
|
|
||||||
selection.stroke,
|
|
||||||
)
|
|
||||||
} else if self.frame {
|
|
||||||
let visuals = ui.style().interact(&response);
|
|
||||||
let expansion = Vec2::splat(visuals.expansion);
|
|
||||||
(
|
|
||||||
expansion,
|
|
||||||
self.image.image_options().rounding,
|
|
||||||
visuals.weak_bg_fill,
|
|
||||||
visuals.bg_stroke,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Draw frame background (for transparent images):
|
|
||||||
ui.painter()
|
|
||||||
.rect_filled(rect.expand2(expansion), rounding, fill);
|
|
||||||
|
|
||||||
let image_rect = ui
|
|
||||||
.layout()
|
|
||||||
.align_size_within_rect(image_size, rect.shrink2(padding));
|
|
||||||
// let image_rect = image_rect.expand2(expansion); // can make it blurry, so let's not
|
|
||||||
let image_options = self.image.image_options().clone();
|
|
||||||
|
|
||||||
widgets::image::paint_texture_load_result(ui, &tlr, image_rect, None, &image_options);
|
|
||||||
|
|
||||||
// Draw frame outline:
|
|
||||||
ui.painter()
|
|
||||||
.rect_stroke(rect.expand2(expansion), rounding, stroke);
|
|
||||||
}
|
|
||||||
|
|
||||||
widgets::image::texture_load_result_response(self.image.source(), &tlr, response)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,136 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
// TODO(emilk): allow checkbox without a text label
|
||||||
|
/// Boolean on/off control with text label.
|
||||||
|
///
|
||||||
|
/// Usually you'd use [`Ui::checkbox`] instead.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # egui::__run_test_ui(|ui| {
|
||||||
|
/// # let mut my_bool = true;
|
||||||
|
/// // These are equivalent:
|
||||||
|
/// ui.checkbox(&mut my_bool, "Checked");
|
||||||
|
/// ui.add(egui::Checkbox::new(&mut my_bool, "Checked"));
|
||||||
|
/// # });
|
||||||
|
/// ```
|
||||||
|
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
|
||||||
|
pub struct Checkbox<'a> {
|
||||||
|
checked: &'a mut bool,
|
||||||
|
text: WidgetText,
|
||||||
|
indeterminate: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Checkbox<'a> {
|
||||||
|
pub fn new(checked: &'a mut bool, text: impl Into<WidgetText>) -> Self {
|
||||||
|
Checkbox {
|
||||||
|
checked,
|
||||||
|
text: text.into(),
|
||||||
|
indeterminate: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn without_text(checked: &'a mut bool) -> Self {
|
||||||
|
Self::new(checked, WidgetText::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Display an indeterminate state (neither checked nor unchecked)
|
||||||
|
///
|
||||||
|
/// This only affects the checkbox's appearance. It will still toggle its boolean value when
|
||||||
|
/// clicked.
|
||||||
|
#[inline]
|
||||||
|
pub fn indeterminate(mut self, indeterminate: bool) -> Self {
|
||||||
|
self.indeterminate = indeterminate;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Widget for Checkbox<'a> {
|
||||||
|
fn ui(self, ui: &mut Ui) -> Response {
|
||||||
|
let Checkbox {
|
||||||
|
checked,
|
||||||
|
text,
|
||||||
|
indeterminate,
|
||||||
|
} = self;
|
||||||
|
|
||||||
|
let spacing = &ui.spacing();
|
||||||
|
let icon_width = spacing.icon_width;
|
||||||
|
let icon_spacing = spacing.icon_spacing;
|
||||||
|
|
||||||
|
let (galley, mut desired_size) = if text.is_empty() {
|
||||||
|
(None, vec2(icon_width, 0.0))
|
||||||
|
} else {
|
||||||
|
let total_extra = vec2(icon_width + icon_spacing, 0.0);
|
||||||
|
|
||||||
|
let wrap_width = ui.available_width() - total_extra.x;
|
||||||
|
let galley = text.into_galley(ui, None, wrap_width, TextStyle::Button);
|
||||||
|
|
||||||
|
let mut desired_size = total_extra + galley.size();
|
||||||
|
desired_size = desired_size.at_least(spacing.interact_size);
|
||||||
|
|
||||||
|
(Some(galley), desired_size)
|
||||||
|
};
|
||||||
|
|
||||||
|
desired_size = desired_size.at_least(Vec2::splat(spacing.interact_size.y));
|
||||||
|
desired_size.y = desired_size.y.max(icon_width);
|
||||||
|
let (rect, mut response) = ui.allocate_exact_size(desired_size, Sense::click());
|
||||||
|
|
||||||
|
if response.clicked() {
|
||||||
|
*checked = !*checked;
|
||||||
|
response.mark_changed();
|
||||||
|
}
|
||||||
|
response.widget_info(|| {
|
||||||
|
if indeterminate {
|
||||||
|
WidgetInfo::labeled(
|
||||||
|
WidgetType::Checkbox,
|
||||||
|
galley.as_ref().map_or("", |x| x.text()),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
WidgetInfo::selected(
|
||||||
|
WidgetType::Checkbox,
|
||||||
|
*checked,
|
||||||
|
galley.as_ref().map_or("", |x| x.text()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if ui.is_rect_visible(rect) {
|
||||||
|
// let visuals = ui.style().interact_selectable(&response, *checked); // too colorful
|
||||||
|
let visuals = ui.style().interact(&response);
|
||||||
|
let (small_icon_rect, big_icon_rect) = ui.spacing().icon_rectangles(rect);
|
||||||
|
ui.painter().add(epaint::RectShape::new(
|
||||||
|
big_icon_rect.expand(visuals.expansion),
|
||||||
|
visuals.rounding,
|
||||||
|
visuals.bg_fill,
|
||||||
|
visuals.bg_stroke,
|
||||||
|
));
|
||||||
|
|
||||||
|
if indeterminate {
|
||||||
|
// Horizontal line:
|
||||||
|
ui.painter().add(Shape::hline(
|
||||||
|
small_icon_rect.x_range(),
|
||||||
|
small_icon_rect.center().y,
|
||||||
|
visuals.fg_stroke,
|
||||||
|
));
|
||||||
|
} else if *checked {
|
||||||
|
// Check mark:
|
||||||
|
ui.painter().add(Shape::line(
|
||||||
|
vec![
|
||||||
|
pos2(small_icon_rect.left(), small_icon_rect.center().y),
|
||||||
|
pos2(small_icon_rect.center().x, small_icon_rect.bottom()),
|
||||||
|
pos2(small_icon_rect.right(), small_icon_rect.top()),
|
||||||
|
],
|
||||||
|
visuals.fg_stroke,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if let Some(galley) = galley {
|
||||||
|
let text_pos = pos2(
|
||||||
|
rect.min.x + icon_width + icon_spacing,
|
||||||
|
rect.center().y - 0.5 * galley.size().y,
|
||||||
|
);
|
||||||
|
ui.painter().galley(text_pos, galley, visuals.text_color());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,130 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
/// A clickable image within a frame.
|
||||||
|
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ImageButton<'a> {
|
||||||
|
image: Image<'a>,
|
||||||
|
sense: Sense,
|
||||||
|
frame: bool,
|
||||||
|
selected: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ImageButton<'a> {
|
||||||
|
pub fn new(image: impl Into<Image<'a>>) -> Self {
|
||||||
|
Self {
|
||||||
|
image: image.into(),
|
||||||
|
sense: Sense::click(),
|
||||||
|
frame: true,
|
||||||
|
selected: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Select UV range. Default is (0,0) in top-left, (1,1) bottom right.
|
||||||
|
#[inline]
|
||||||
|
pub fn uv(mut self, uv: impl Into<Rect>) -> Self {
|
||||||
|
self.image = self.image.uv(uv);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Multiply image color with this. Default is WHITE (no tint).
|
||||||
|
#[inline]
|
||||||
|
pub fn tint(mut self, tint: impl Into<Color32>) -> Self {
|
||||||
|
self.image = self.image.tint(tint);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If `true`, mark this button as "selected".
|
||||||
|
#[inline]
|
||||||
|
pub fn selected(mut self, selected: bool) -> Self {
|
||||||
|
self.selected = selected;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Turn off the frame
|
||||||
|
#[inline]
|
||||||
|
pub fn frame(mut self, frame: bool) -> Self {
|
||||||
|
self.frame = frame;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// By default, buttons senses clicks.
|
||||||
|
/// Change this to a drag-button with `Sense::drag()`.
|
||||||
|
#[inline]
|
||||||
|
pub fn sense(mut self, sense: Sense) -> Self {
|
||||||
|
self.sense = sense;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set rounding for the `ImageButton`.
|
||||||
|
/// If the underlying image already has rounding, this
|
||||||
|
/// will override that value.
|
||||||
|
#[inline]
|
||||||
|
pub fn rounding(mut self, rounding: impl Into<Rounding>) -> Self {
|
||||||
|
self.image = self.image.rounding(rounding.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Widget for ImageButton<'a> {
|
||||||
|
fn ui(self, ui: &mut Ui) -> Response {
|
||||||
|
let padding = if self.frame {
|
||||||
|
// so we can see that it is a button:
|
||||||
|
Vec2::splat(ui.spacing().button_padding.x)
|
||||||
|
} else {
|
||||||
|
Vec2::ZERO
|
||||||
|
};
|
||||||
|
|
||||||
|
let available_size_for_image = ui.available_size() - 2.0 * padding;
|
||||||
|
let tlr = self.image.load_for_size(ui.ctx(), available_size_for_image);
|
||||||
|
let original_image_size = tlr.as_ref().ok().and_then(|t| t.size());
|
||||||
|
let image_size = self
|
||||||
|
.image
|
||||||
|
.calc_size(available_size_for_image, original_image_size);
|
||||||
|
|
||||||
|
let padded_size = image_size + 2.0 * padding;
|
||||||
|
let (rect, response) = ui.allocate_exact_size(padded_size, self.sense);
|
||||||
|
response.widget_info(|| WidgetInfo::new(WidgetType::ImageButton));
|
||||||
|
|
||||||
|
if ui.is_rect_visible(rect) {
|
||||||
|
let (expansion, rounding, fill, stroke) = if self.selected {
|
||||||
|
let selection = ui.visuals().selection;
|
||||||
|
(
|
||||||
|
Vec2::ZERO,
|
||||||
|
self.image.image_options().rounding,
|
||||||
|
selection.bg_fill,
|
||||||
|
selection.stroke,
|
||||||
|
)
|
||||||
|
} else if self.frame {
|
||||||
|
let visuals = ui.style().interact(&response);
|
||||||
|
let expansion = Vec2::splat(visuals.expansion);
|
||||||
|
(
|
||||||
|
expansion,
|
||||||
|
self.image.image_options().rounding,
|
||||||
|
visuals.weak_bg_fill,
|
||||||
|
visuals.bg_stroke,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Draw frame background (for transparent images):
|
||||||
|
ui.painter()
|
||||||
|
.rect_filled(rect.expand2(expansion), rounding, fill);
|
||||||
|
|
||||||
|
let image_rect = ui
|
||||||
|
.layout()
|
||||||
|
.align_size_within_rect(image_size, rect.shrink2(padding));
|
||||||
|
// let image_rect = image_rect.expand2(expansion); // can make it blurry, so let's not
|
||||||
|
let image_options = self.image.image_options().clone();
|
||||||
|
|
||||||
|
widgets::image::paint_texture_load_result(ui, &tlr, image_rect, None, &image_options);
|
||||||
|
|
||||||
|
// Draw frame outline:
|
||||||
|
ui.painter()
|
||||||
|
.rect_stroke(rect.expand2(expansion), rounding, stroke);
|
||||||
|
}
|
||||||
|
|
||||||
|
widgets::image::texture_load_result_response(self.image.source(), &tlr, response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,29 +7,37 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
mod button;
|
mod button;
|
||||||
|
mod checkbox;
|
||||||
pub mod color_picker;
|
pub mod color_picker;
|
||||||
pub(crate) mod drag_value;
|
pub(crate) mod drag_value;
|
||||||
mod hyperlink;
|
mod hyperlink;
|
||||||
mod image;
|
mod image;
|
||||||
|
mod image_button;
|
||||||
mod label;
|
mod label;
|
||||||
mod progress_bar;
|
mod progress_bar;
|
||||||
|
mod radio_button;
|
||||||
mod selected_label;
|
mod selected_label;
|
||||||
mod separator;
|
mod separator;
|
||||||
mod slider;
|
mod slider;
|
||||||
mod spinner;
|
mod spinner;
|
||||||
pub mod text_edit;
|
pub mod text_edit;
|
||||||
|
|
||||||
pub use button::*;
|
pub use self::{
|
||||||
pub use drag_value::DragValue;
|
button::Button,
|
||||||
pub use hyperlink::*;
|
checkbox::Checkbox,
|
||||||
pub use image::{paint_texture_at, Image, ImageFit, ImageOptions, ImageSize, ImageSource};
|
drag_value::DragValue,
|
||||||
pub use label::*;
|
hyperlink::{Hyperlink, Link},
|
||||||
pub use progress_bar::ProgressBar;
|
image::{paint_texture_at, Image, ImageFit, ImageOptions, ImageSize, ImageSource},
|
||||||
pub use selected_label::SelectableLabel;
|
image_button::ImageButton,
|
||||||
pub use separator::Separator;
|
label::Label,
|
||||||
pub use slider::*;
|
progress_bar::ProgressBar,
|
||||||
pub use spinner::*;
|
radio_button::RadioButton,
|
||||||
pub use text_edit::{TextBuffer, TextEdit};
|
selected_label::SelectableLabel,
|
||||||
|
separator::Separator,
|
||||||
|
slider::{Slider, SliderOrientation},
|
||||||
|
spinner::Spinner,
|
||||||
|
text_edit::{TextBuffer, TextEdit},
|
||||||
|
};
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
/// One out of several alternatives, either selected or not.
|
||||||
|
///
|
||||||
|
/// Usually you'd use [`Ui::radio_value`] or [`Ui::radio`] instead.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # egui::__run_test_ui(|ui| {
|
||||||
|
/// #[derive(PartialEq)]
|
||||||
|
/// enum Enum { First, Second, Third }
|
||||||
|
/// let mut my_enum = Enum::First;
|
||||||
|
///
|
||||||
|
/// ui.radio_value(&mut my_enum, Enum::First, "First");
|
||||||
|
///
|
||||||
|
/// // is equivalent to:
|
||||||
|
///
|
||||||
|
/// if ui.add(egui::RadioButton::new(my_enum == Enum::First, "First")).clicked() {
|
||||||
|
/// my_enum = Enum::First
|
||||||
|
/// }
|
||||||
|
/// # });
|
||||||
|
/// ```
|
||||||
|
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
|
||||||
|
pub struct RadioButton {
|
||||||
|
checked: bool,
|
||||||
|
text: WidgetText,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RadioButton {
|
||||||
|
pub fn new(checked: bool, text: impl Into<WidgetText>) -> Self {
|
||||||
|
Self {
|
||||||
|
checked,
|
||||||
|
text: text.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for RadioButton {
|
||||||
|
fn ui(self, ui: &mut Ui) -> Response {
|
||||||
|
let Self { checked, text } = self;
|
||||||
|
|
||||||
|
let spacing = &ui.spacing();
|
||||||
|
let icon_width = spacing.icon_width;
|
||||||
|
let icon_spacing = spacing.icon_spacing;
|
||||||
|
|
||||||
|
let (galley, mut desired_size) = if text.is_empty() {
|
||||||
|
(None, vec2(icon_width, 0.0))
|
||||||
|
} else {
|
||||||
|
let total_extra = vec2(icon_width + icon_spacing, 0.0);
|
||||||
|
|
||||||
|
let wrap_width = ui.available_width() - total_extra.x;
|
||||||
|
let text = text.into_galley(ui, None, wrap_width, TextStyle::Button);
|
||||||
|
|
||||||
|
let mut desired_size = total_extra + text.size();
|
||||||
|
desired_size = desired_size.at_least(spacing.interact_size);
|
||||||
|
|
||||||
|
(Some(text), desired_size)
|
||||||
|
};
|
||||||
|
|
||||||
|
desired_size = desired_size.at_least(Vec2::splat(spacing.interact_size.y));
|
||||||
|
desired_size.y = desired_size.y.max(icon_width);
|
||||||
|
let (rect, response) = ui.allocate_exact_size(desired_size, Sense::click());
|
||||||
|
|
||||||
|
response.widget_info(|| {
|
||||||
|
WidgetInfo::selected(
|
||||||
|
WidgetType::RadioButton,
|
||||||
|
checked,
|
||||||
|
galley.as_ref().map_or("", |x| x.text()),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
if ui.is_rect_visible(rect) {
|
||||||
|
// let visuals = ui.style().interact_selectable(&response, checked); // too colorful
|
||||||
|
let visuals = ui.style().interact(&response);
|
||||||
|
|
||||||
|
let (small_icon_rect, big_icon_rect) = ui.spacing().icon_rectangles(rect);
|
||||||
|
|
||||||
|
let painter = ui.painter();
|
||||||
|
|
||||||
|
painter.add(epaint::CircleShape {
|
||||||
|
center: big_icon_rect.center(),
|
||||||
|
radius: big_icon_rect.width() / 2.0 + visuals.expansion,
|
||||||
|
fill: visuals.bg_fill,
|
||||||
|
stroke: visuals.bg_stroke,
|
||||||
|
});
|
||||||
|
|
||||||
|
if checked {
|
||||||
|
painter.add(epaint::CircleShape {
|
||||||
|
center: small_icon_rect.center(),
|
||||||
|
radius: small_icon_rect.width() / 3.0,
|
||||||
|
fill: visuals.fg_stroke.color, // Intentional to use stroke and not fill
|
||||||
|
// fill: ui.visuals().selection.stroke.color, // too much color
|
||||||
|
stroke: Default::default(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(galley) = galley {
|
||||||
|
let text_pos = pos2(
|
||||||
|
rect.min.x + icon_width + icon_spacing,
|
||||||
|
rect.center().y - 0.5 * galley.size().y,
|
||||||
|
);
|
||||||
|
ui.painter().galley(text_pos, galley, visuals.text_color());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue