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:
parent
99fa650fa7
commit
4ecb7d14ca
7 changed files with 154 additions and 30 deletions
|
@ -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 ⭐
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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"));
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue