diff --git a/CHANGELOG.md b/CHANGELOG.md index 73d9eaa6..71eed558 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## Unreleased +### Added ⭐ + +* `SelectableLabel` (`ui.selectable_label` and `ui.selectable_value`): a text-button that can be selected + ## 0.4.0 - 2020-11-28 ### Added ⭐ diff --git a/egui/src/containers/collapsing_header.rs b/egui/src/containers/collapsing_header.rs index 997307bd..3531b792 100644 --- a/egui/src/containers/collapsing_header.rs +++ b/egui/src/containers/collapsing_header.rs @@ -133,6 +133,7 @@ pub struct CollapsingHeader { } impl CollapsingHeader { + /// The `CollapsingHeader` starts out collapsed unless you call `default_open`. pub fn new(label: impl Into) -> Self { let label = Label::new(label) .text_style(TextStyle::Button) diff --git a/egui/src/demos/widgets.rs b/egui/src/demos/widgets.rs index 723550b3..6e8c0b64 100644 --- a/egui/src/demos/widgets.rs +++ b/egui/src/demos/widgets.rs @@ -76,9 +76,9 @@ impl Widgets { }); combo_box_with_label(ui, "Combo Box", format!("{:?}", self.radio), |ui| { - ui.radio_value(&mut self.radio, Enum::First, "First"); - ui.radio_value(&mut self.radio, Enum::Second, "Second"); - ui.radio_value(&mut self.radio, Enum::Third, "Third"); + ui.selectable_value(&mut self.radio, Enum::First, "First"); + ui.selectable_value(&mut self.radio, Enum::Second, "Second"); + ui.selectable_value(&mut self.radio, Enum::Third, "Third"); }); ui.checkbox(&mut self.button_enabled, "Button enabled"); diff --git a/egui/src/style.rs b/egui/src/style.rs index 87ad36e1..d95d13d8 100644 --- a/egui/src/style.rs +++ b/egui/src/style.rs @@ -123,6 +123,8 @@ pub struct Visuals { /// Visual styles of widgets pub widgets: Widgets, + pub selection: Selection, + /// e.g. the background of the slider or text edit, /// needs to look different from other interactive stuff. pub dark_bg_color: Srgba, // TODO: remove, rename, or clarify what it is for @@ -156,6 +158,14 @@ impl Visuals { } } +/// Selected text, selected elements etc +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct Selection { + pub bg_fill: Srgba, + pub stroke: Stroke, +} + #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct Widgets { @@ -258,6 +268,7 @@ impl Default for Visuals { Self { override_text_color: None, widgets: Default::default(), + selection: Default::default(), dark_bg_color: Srgba::black_alpha(140), window_corner_radius: 10.0, resize_corner_size: 12.0, @@ -270,6 +281,15 @@ impl Default for Visuals { } } +impl Default for Selection { + fn default() -> Self { + Self { + bg_fill: Rgba::new(0.0, 0.5, 1.0, 0.0).multiply(0.15).into(), // additive! + stroke: Stroke::new(1.0, Rgba::new(0.3, 0.6, 1.0, 1.0)), + } + } +} + impl Default for Widgets { fn default() -> Self { Self { @@ -415,6 +435,15 @@ impl Widgets { } } +impl Selection { + pub fn ui(&mut self, ui: &mut crate::Ui) { + let Self { bg_fill, stroke } = self; + + ui_color(ui, bg_fill, "bg_fill"); + stroke.ui(ui, "stroke"); + } +} + impl WidgetVisuals { pub fn ui(&mut self, ui: &mut crate::Ui) { let Self { @@ -442,6 +471,7 @@ impl Visuals { let Self { override_text_color: _, widgets, + selection, dark_bg_color, window_corner_radius, resize_corner_size, @@ -453,6 +483,7 @@ impl Visuals { } = self; ui.collapsing("widgets", |ui| widgets.ui(ui)); + ui.collapsing("selection", |ui| selection.ui(ui)); ui_color(ui, dark_bg_color, "dark_bg_color"); ui.add(Slider::f32(window_corner_radius, 0.0..=20.0).text("window_corner_radius")); ui.add(Slider::f32(resize_corner_size, 0.0..=20.0).text("resize_corner_size")); diff --git a/egui/src/ui.rs b/egui/src/ui.rs index d8e44281..03b3216f 100644 --- a/egui/src/ui.rs +++ b/egui/src/ui.rs @@ -544,21 +544,46 @@ impl Ui { } /// Show a radio button. - pub fn radio(&mut self, checked: bool, text: impl Into) -> Response { - self.add(RadioButton::new(checked, text)) + /// Often you want to use `ui.radio_value` instead. + pub fn radio(&mut self, selected: bool, text: impl Into) -> Response { + self.add(RadioButton::new(selected, text)) } - /// Show a radio button. It is selected if `*current_value == radio_value`. - /// If clicked, `radio_value` is assigned to `*current_value`; + /// Show a radio button. It is selected if `*current_value == selected_value`. + /// If clicked, `selected_value` is assigned to `*current_value`. + /// + /// Example: `ui.radio_value(&mut my_enum, Enum::Alternative, "Alternative")`. pub fn radio_value( &mut self, current_value: &mut Value, - radio_value: Value, + selected_value: Value, text: impl Into, ) -> Response { - let response = self.radio(*current_value == radio_value, text); + let response = self.radio(*current_value == selected_value, text); if response.clicked { - *current_value = radio_value; + *current_value = selected_value; + } + response + } + + /// Show a label which can be selected or not. + pub fn selectable_label(&mut self, checked: bool, text: impl Into) -> Response { + self.add(SelectableLabel::new(checked, text)) + } + + /// Show selectable text. It is selected if `*current_value == selected_value`. + /// If clicked, `selected_value` is assigned to `*current_value`. + /// + /// Example: `ui.selectable_value(&mut my_enum, Enum::Alternative, "Alternative")`. + pub fn selectable_value( + &mut self, + current_value: &mut Value, + selected_value: Value, + text: impl Into, + ) -> Response { + let response = self.selectable_label(*current_value == selected_value, text); + if response.clicked { + *current_value = selected_value; } response } @@ -686,6 +711,7 @@ impl Ui { self.allocate_space(child_ui.min_size()) } + /// A `CollapsingHeader` that starts out collapsed. pub fn collapsing( &mut self, heading: impl Into, diff --git a/egui/src/widgets/mod.rs b/egui/src/widgets/mod.rs index 9cb15ef0..96350b0e 100644 --- a/egui/src/widgets/mod.rs +++ b/egui/src/widgets/mod.rs @@ -437,7 +437,7 @@ impl<'a> Widget for Checkbox<'a> { // ---------------------------------------------------------------------------- -/// One out of several alternatives, either checked or not. +/// One out of several alternatives, either selected or not. #[derive(Debug)] pub struct RadioButton { checked: bool, @@ -525,6 +525,73 @@ impl Widget for RadioButton { // ---------------------------------------------------------------------------- +/// One out of several alternatives, either selected or not. +/// Will mark selected items with a different background color +/// An alternative to `RadioButton` and `Checkbox`. +#[derive(Debug)] +pub struct SelectableLabel { + selected: bool, + text: String, +} + +impl SelectableLabel { + pub fn new(selected: bool, text: impl Into) -> Self { + Self { + selected, + text: text.into(), + } + } +} + +impl Widget for SelectableLabel { + fn ui(self, ui: &mut Ui) -> Response { + let Self { selected, text } = self; + + let text_style = TextStyle::Button; + let font = &ui.fonts()[text_style]; + + let button_padding = ui.style().spacing.button_padding; + let total_extra = button_padding + button_padding; + + let galley = font.layout_multiline(text, ui.available().width() - total_extra.x); + + let mut desired_size = total_extra + galley.size; + desired_size = desired_size.at_least(ui.style().spacing.interact_size); + let rect = ui.allocate_space(desired_size); + + let id = ui.make_position_id(); + let response = ui.interact(rect, id, Sense::click()); + + let text_cursor = pos2( + response.rect.min.x + button_padding.x, + response.rect.center().y - 0.5 * galley.size.y, + ); + + let visuals = ui.style().interact(&response); + + if selected || response.hovered { + let bg_fill = if selected { + ui.style().visuals.selection.bg_fill + } else { + Default::default() + }; + ui.painter() + .rect(response.rect, 0.0, bg_fill, visuals.bg_stroke); + } + + let text_color = ui + .style() + .visuals + .override_text_color + .unwrap_or_else(|| visuals.text_color()); + ui.painter() + .galley(text_cursor, galley, text_style, text_color); + response + } +} + +// ---------------------------------------------------------------------------- + /// A visual separator. A horizontal or vertical line (depending on `Layout`). pub struct Separator { spacing: f32, diff --git a/egui/src/widgets/text_edit.rs b/egui/src/widgets/text_edit.rs index 75e4b405..f687df39 100644 --- a/egui/src/widgets/text_edit.rs +++ b/egui/src/widgets/text_edit.rs @@ -272,8 +272,7 @@ impl<'t> Widget for TextEdit<'t> { if response.hovered { // preview: - let end_color = Rgba::new(0.1, 0.6, 1.0, 1.0).multiply(0.5).into(); // TODO: from style - paint_cursor_end(ui, response.rect.min, &galley, &cursor_at_mouse, end_color); + paint_cursor_end(ui, response.rect.min, &galley, &cursor_at_mouse); } if response.hovered && response.double_clicked { @@ -458,11 +457,8 @@ impl<'t> Widget for TextEdit<'t> { if ui.memory().has_kb_focus(id) { if let Some(cursorp) = state.cursorp { - // TODO: color from Style - let selection_color = Rgba::new(0.0, 0.5, 1.0, 0.0).multiply(0.15).into(); // additive! - let end_color = Rgba::new(0.3, 0.6, 1.0, 1.0).into(); - paint_cursor_selection(ui, response.rect.min, &galley, &cursorp, selection_color); - paint_cursor_end(ui, response.rect.min, &galley, &cursorp.primary, end_color); + paint_cursor_selection(ui, response.rect.min, &galley, &cursorp); + paint_cursor_end(ui, response.rect.min, &galley, &cursorp.primary); } } @@ -484,13 +480,8 @@ impl<'t> Widget for TextEdit<'t> { // ---------------------------------------------------------------------------- -fn paint_cursor_selection( - ui: &mut Ui, - pos: Pos2, - galley: &Galley, - cursorp: &CursorPair, - color: Srgba, -) { +fn paint_cursor_selection(ui: &mut Ui, pos: Pos2, galley: &Galley, cursorp: &CursorPair) { + let color = ui.style().visuals.selection.bg_fill; if cursorp.is_empty() { return; } @@ -520,15 +511,19 @@ fn paint_cursor_selection( } } -fn paint_cursor_end(ui: &mut Ui, pos: Pos2, galley: &Galley, cursor: &Cursor, color: Srgba) { +fn paint_cursor_end(ui: &mut Ui, pos: Pos2, galley: &Galley, cursor: &Cursor) { + let stroke = ui.style().visuals.selection.stroke; + let cursor_pos = galley.pos_from_cursor(cursor).translate(pos.to_vec2()); let cursor_pos = cursor_pos.expand(1.5); // slightly above/below row let top = cursor_pos.center_top(); let bottom = cursor_pos.center_bottom(); - ui.painter() - .line_segment([top, bottom], (ui.style().visuals.text_cursor_width, color)); + ui.painter().line_segment( + [top, bottom], + (ui.style().visuals.text_cursor_width, stroke.color), + ); if false { // Roof/floor: @@ -536,11 +531,11 @@ fn paint_cursor_end(ui: &mut Ui, pos: Pos2, galley: &Galley, cursor: &Cursor, co let width = 1.0; ui.painter().line_segment( [top - vec2(extrusion, 0.0), top + vec2(extrusion, 0.0)], - (width, color), + (width, stroke.color), ); ui.painter().line_segment( [bottom - vec2(extrusion, 0.0), bottom + vec2(extrusion, 0.0)], - (width, color), + (width, stroke.color), ); } }