diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index 75da2a9c..76a62125 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -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) -> 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) -> 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>) -> 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) -> 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) -> 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) -> 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) - } -} diff --git a/crates/egui/src/widgets/checkbox.rs b/crates/egui/src/widgets/checkbox.rs new file mode 100644 index 00000000..e416a6e0 --- /dev/null +++ b/crates/egui/src/widgets/checkbox.rs @@ -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) -> 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 + } +} diff --git a/crates/egui/src/widgets/image_button.rs b/crates/egui/src/widgets/image_button.rs new file mode 100644 index 00000000..65ef3072 --- /dev/null +++ b/crates/egui/src/widgets/image_button.rs @@ -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>) -> 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) -> 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) -> 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) -> 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) + } +} diff --git a/crates/egui/src/widgets/mod.rs b/crates/egui/src/widgets/mod.rs index 6b3fb72e..0ab9273c 100644 --- a/crates/egui/src/widgets/mod.rs +++ b/crates/egui/src/widgets/mod.rs @@ -7,29 +7,37 @@ use crate::*; mod button; +mod checkbox; pub mod color_picker; pub(crate) mod drag_value; mod hyperlink; mod image; +mod image_button; mod label; mod progress_bar; +mod radio_button; mod selected_label; mod separator; mod slider; mod spinner; pub mod text_edit; -pub use button::*; -pub use drag_value::DragValue; -pub use hyperlink::*; -pub use image::{paint_texture_at, Image, ImageFit, ImageOptions, ImageSize, ImageSource}; -pub use label::*; -pub use progress_bar::ProgressBar; -pub use selected_label::SelectableLabel; -pub use separator::Separator; -pub use slider::*; -pub use spinner::*; -pub use text_edit::{TextBuffer, TextEdit}; +pub use self::{ + button::Button, + checkbox::Checkbox, + drag_value::DragValue, + hyperlink::{Hyperlink, Link}, + image::{paint_texture_at, Image, ImageFit, ImageOptions, ImageSize, ImageSource}, + image_button::ImageButton, + label::Label, + progress_bar::ProgressBar, + radio_button::RadioButton, + selected_label::SelectableLabel, + separator::Separator, + slider::{Slider, SliderOrientation}, + spinner::Spinner, + text_edit::{TextBuffer, TextEdit}, +}; // ---------------------------------------------------------------------------- diff --git a/crates/egui/src/widgets/radio_button.rs b/crates/egui/src/widgets/radio_button.rs new file mode 100644 index 00000000..ccbe785f --- /dev/null +++ b/crates/egui/src/widgets/radio_button.rs @@ -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) -> 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 + } +}