Skip to content

Commit

Permalink
Break out Checkbox, RadioButton and ImageButton to their own files (#…
Browse files Browse the repository at this point in the history
…4278)

Just a refactor
  • Loading branch information
emilk authored Mar 30, 2024
1 parent a9a756e commit 7277322
Show file tree
Hide file tree
Showing 5 changed files with 392 additions and 387 deletions.
376 changes: 0 additions & 376 deletions crates/egui/src/widgets/button.rs
Original file line number Diff line number Diff line change
Expand Up @@ -336,379 +336,3 @@ impl Widget for Button<'_> {
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)
}
}
Loading

0 comments on commit 7277322

Please sign in to comment.