New widget: SelectableLabel: a text-button that can be selected

Also available via `ui.selectable_label` and `ui.selectable_value`
This commit is contained in:
Emil Ernerfeldt 2020-11-30 07:17:03 +01:00
parent 99fa650fa7
commit 4ecb7d14ca
7 changed files with 154 additions and 30 deletions

View file

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

View file

@ -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<String>) -> Self {
let label = Label::new(label)
.text_style(TextStyle::Button)

View file

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

View file

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

View file

@ -544,21 +544,46 @@ impl Ui {
}
/// Show a radio button.
pub fn radio(&mut self, checked: bool, text: impl Into<String>) -> 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<String>) -> 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<Value: PartialEq>(
&mut self,
current_value: &mut Value,
radio_value: Value,
selected_value: Value,
text: impl Into<String>,
) -> 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<String>) -> 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<Value: PartialEq>(
&mut self,
current_value: &mut Value,
selected_value: Value,
text: impl Into<String>,
) -> 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<R>(
&mut self,
heading: impl Into<String>,

View file

@ -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<String>) -> 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,

View file

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