[slider] click value to edit it with keyboard

This commit is contained in:
Emil Ernerfeldt 2020-08-28 10:37:44 +02:00
parent 1874f238eb
commit f3bbb210c0
4 changed files with 115 additions and 34 deletions

View file

@ -377,9 +377,10 @@ impl Default for Widgets {
impl Widgets { impl Widgets {
pub fn ui(&mut self, ui: &mut Ui) { pub fn ui(&mut self, ui: &mut Ui) {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.style_mut().item_spacing.x = 0.0;
ui.add(label!("Text can have ").text_color(srgba(110, 255, 110, 255))); ui.add(label!("Text can have ").text_color(srgba(110, 255, 110, 255)));
ui.add(label!("color ").text_color(srgba(128, 140, 255, 255))); ui.add(label!("color ").text_color(srgba(128, 140, 255, 255)));
ui.add(label!("and tooltips (hover me)")).tooltip_text( ui.add(label!("and tooltips")).tooltip_text(
"This is a multiline tooltip that demonstrates that you can easily add tooltips to any element.\nThis is the second line.\nThis is the third.", "This is a multiline tooltip that demonstrates that you can easily add tooltips to any element.\nThis is the second line.\nThis is the third.",
); );
}); });
@ -403,6 +404,13 @@ impl Widgets {
ui.add(label!("The button has been clicked {} times", self.count)); ui.add(label!("The button has been clicked {} times", self.count));
}); });
ui.separator();
{
ui.label(
"The slider will show as many decimals as needed, \
and will intelligently help you select a round number when you interact with it.\n\
You can click a slider value to edit it with the keyboard.",
);
ui.add(Slider::f32(&mut self.slider_value, -10.0..=10.0).text("value")); ui.add(Slider::f32(&mut self.slider_value, -10.0..=10.0).text("value"));
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label("drag this number:"); ui.label("drag this number:");
@ -411,18 +419,20 @@ impl Widgets {
if ui.add(Button::new("Assign PI")).clicked { if ui.add(Button::new("Assign PI")).clicked {
self.slider_value = std::f32::consts::PI; self.slider_value = std::f32::consts::PI;
} }
}
ui.separator();
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.add(label!("Single line text input:")); ui.add(label!("Single line text input:"));
ui.add( ui.add(
TextEdit::new(&mut self.single_line_text_input) TextEdit::new(&mut self.single_line_text_input)
.multiline(false) .multiline(false)
.id("single line"), .id_source("single line"),
); );
}); // TODO: .tooltip_text("Enter text to edit me") }); // TODO: .tooltip_text("Enter text to edit me")
ui.add(label!("Multiline text input:")); ui.add(label!("Multiline text input:"));
ui.add(TextEdit::new(&mut self.multiline_text_input).id("multiline")); ui.add(TextEdit::new(&mut self.multiline_text_input).id_source("multiline"));
} }
} }

View file

@ -28,6 +28,11 @@ pub struct Memory {
#[cfg_attr(feature = "serde", serde(skip))] #[cfg_attr(feature = "serde", serde(skip))]
pub(crate) window_interaction: Option<window::WindowInteraction>, pub(crate) window_interaction: Option<window::WindowInteraction>,
/// For temporary edit of e.g. a slider value.
/// Couples with `kb_focus_id`.
#[cfg_attr(feature = "serde", serde(skip))]
pub(crate) temp_edit_string: Option<String>,
pub(crate) areas: Areas, pub(crate) areas: Areas,
} }

View file

@ -182,8 +182,6 @@ impl<'a> Slider<'a> {
/// Just the text label /// Just the text label
fn text_ui(&mut self, ui: &mut Ui, x_range: RangeInclusive<f32>) { fn text_ui(&mut self, ui: &mut Ui, x_range: RangeInclusive<f32>) {
let aim_radius = ui.input().aim_radius();
let value_text = self.format_value(aim_radius, x_range);
let label_text = self.text.as_deref().unwrap_or_default(); let label_text = self.text.as_deref().unwrap_or_default();
let label_text = format!("{}: ", label_text); let label_text = format!("{}: ", label_text);
let text_color = self.text_color.unwrap_or_else(|| ui.style().text_color); let text_color = self.text_color.unwrap_or_else(|| ui.style().text_color);
@ -193,12 +191,48 @@ impl<'a> Slider<'a> {
.multiline(false) .multiline(false)
.text_color(text_color), .text_color(text_color),
); );
let edit_id = self.id.expect("We should have an id by now").with("edit");
let is_editing = ui.memory().has_kb_focus(edit_id);
let aim_radius = ui.input().aim_radius();
let mut value_text = self.format_value(aim_radius, x_range);
if is_editing {
value_text = ui
.memory()
.temp_edit_string
.take()
.unwrap_or_else(|| value_text);
ui.add( ui.add(
TextEdit::new(&mut value_text)
.id(edit_id)
.multiline(false)
.text_color(text_color)
.text_style(TextStyle::Monospace),
);
if let Ok(value) = value_text.parse() {
self.set_value_f32(value);
}
if ui.input().key_pressed(Key::Enter) {
ui.memory().surrender_kb_focus(edit_id);
} else {
ui.memory().temp_edit_string = Some(value_text);
}
} else {
let mut response = ui.add(
Label::new(value_text) Label::new(value_text)
.multiline(false) .multiline(false)
.text_color(text_color) .text_color(text_color)
.text_style(TextStyle::Monospace), .text_style(TextStyle::Monospace),
); );
response.tooltip_text("Click to edit");
let response = ui.interact(response.rect, edit_id, Sense::click());
if response.clicked {
ui.memory().request_kb_focus(edit_id);
ui.memory().temp_edit_string = None; // Filled in next frame
}
}
} }
fn format_value(&mut self, aim_radius: f32, x_range: RangeInclusive<f32>) -> String { fn format_value(&mut self, aim_radius: f32, x_range: RangeInclusive<f32>) -> String {

View file

@ -3,7 +3,7 @@ use crate::{paint::*, *};
#[derive(Clone, Copy, Debug, Default)] #[derive(Clone, Copy, Debug, Default)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub(crate) struct State { pub(crate) struct State {
/// Charctaer based, NOT bytes. /// Character based, NOT bytes.
/// TODO: store as line + row /// TODO: store as line + row
pub cursor: Option<usize>, pub cursor: Option<usize>,
} }
@ -13,9 +13,11 @@ pub(crate) struct State {
pub struct TextEdit<'t> { pub struct TextEdit<'t> {
text: &'t mut String, text: &'t mut String,
id: Option<Id>, id: Option<Id>,
id_source: Option<Id>,
text_style: TextStyle, // TODO: Option<TextStyle>, where None means "use the default for the current Ui" text_style: TextStyle, // TODO: Option<TextStyle>, where None means "use the default for the current Ui"
text_color: Option<Color>, text_color: Option<Color>,
multiline: bool, multiline: bool,
enabled: bool,
} }
impl<'t> TextEdit<'t> { impl<'t> TextEdit<'t> {
@ -23,14 +25,21 @@ impl<'t> TextEdit<'t> {
TextEdit { TextEdit {
text, text,
id: None, id: None,
id_source: None,
text_style: TextStyle::Body, text_style: TextStyle::Body,
text_color: Default::default(), text_color: None,
multiline: true, multiline: true,
enabled: true,
} }
} }
pub fn id(mut self, id_source: impl std::hash::Hash) -> Self { pub fn id(mut self, id: Id) -> Self {
self.id = Some(Id::new(id_source)); self.id = Some(id);
self
}
pub fn id_source(mut self, id_source: impl std::hash::Hash) -> Self {
self.id_source = Some(Id::new(id_source));
self self
} }
@ -48,6 +57,12 @@ impl<'t> TextEdit<'t> {
self.multiline = multiline; self.multiline = multiline;
self self
} }
/// Default is `true`. If set to `false` then you cannot edit the text.
pub fn enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
} }
impl<'t> Widget for TextEdit<'t> { impl<'t> Widget for TextEdit<'t> {
@ -55,12 +70,14 @@ impl<'t> Widget for TextEdit<'t> {
let TextEdit { let TextEdit {
text, text,
id, id,
id_source,
text_style, text_style,
text_color, text_color,
multiline, multiline,
enabled,
} = self; } = self;
let id = ui.make_child_id(id); let id = id.unwrap_or_else(|| ui.make_child_id(id_source));
let mut state = ui.memory().text_edit.get(&id).cloned().unwrap_or_default(); let mut state = ui.memory().text_edit.get(&id).cloned().unwrap_or_default();
@ -74,23 +91,31 @@ impl<'t> Widget for TextEdit<'t> {
}; };
let desired_size = galley.size.max(vec2(available_width, line_spacing)); let desired_size = galley.size.max(vec2(available_width, line_spacing));
let rect = ui.allocate_space(desired_size); let rect = ui.allocate_space(desired_size);
let interact = ui.interact(rect, id, Sense::click_and_drag()); // TODO: implement drag-select let sense = if enabled {
Sense::click_and_drag()
} else {
Sense::nothing()
};
let interact = ui.interact(rect, id, sense); // TODO: implement drag-select
if interact.clicked { if interact.clicked && enabled {
ui.memory().request_kb_focus(id); ui.memory().request_kb_focus(id);
if let Some(mouse_pos) = ui.input().mouse.pos { if let Some(mouse_pos) = ui.input().mouse.pos {
state.cursor = Some(galley.char_at(mouse_pos - interact.rect.min).char_idx); state.cursor = Some(galley.char_at(mouse_pos - interact.rect.min).char_idx);
} }
} else if ui.input().mouse.click { } else if ui.input().mouse.click || (ui.input().mouse.pressed && !interact.hovered) {
// User clicked somewhere else // User clicked somewhere else
ui.memory().surrender_kb_focus(id); ui.memory().surrender_kb_focus(id);
} }
if !enabled {
ui.memory().surrender_kb_focus(id);
}
if interact.hovered { if interact.hovered && enabled {
ui.output().cursor_icon = CursorIcon::Text; ui.output().cursor_icon = CursorIcon::Text;
} }
if ui.memory().has_kb_focus(id) { if ui.memory().has_kb_focus(id) && enabled {
let mut cursor = state.cursor.unwrap_or_else(|| text.chars().count()); let mut cursor = state.cursor.unwrap_or_else(|| text.chars().count());
cursor = clamp(cursor, 0..=text.chars().count()); cursor = clamp(cursor, 0..=text.chars().count());
@ -114,6 +139,12 @@ impl<'t> Widget for TextEdit<'t> {
insert_text(&mut cursor, text, "\n"); insert_text(&mut cursor, text, "\n");
} }
} }
Event::Key {
key: Key::Escape,
pressed: true,
} => {
ui.memory().surrender_kb_focus(id);
}
Event::Key { key, pressed: true } => { Event::Key { key, pressed: true } => {
on_key_press(&mut cursor, text, *key); on_key_press(&mut cursor, text, *key);
} }
@ -166,7 +197,8 @@ impl<'t> Widget for TextEdit<'t> {
} }
} }
let text_color = text_color.unwrap_or_else(|| ui.style().text_color); let text_color =
text_color.unwrap_or_else(|| ui.style().interact.style(&interact).stroke_color);
painter.galley(interact.rect.min, galley, text_style, text_color); painter.galley(interact.rect.min, galley, text_style, text_color);
ui.memory().text_edit.insert(id, state); ui.memory().text_edit.insert(id, state);
interact interact