ui(feat): open expression suggestions with Ctrl+Space (#1852)
Thanks @Karl-VonRichter.
This commit is contained in:
1 parent
ddc6aa6301
commit
3689f0d6e5
4 files changed
+80
-8
No files matched your search
@@ -26,8 +26,39 @@ fn place(h: &mut Harness, picks: &[[f64; 2]], at: [f64; 2]) {
|
||||
}
|
||||
fn type_value(h: &mut Harness, text: &str) {
|
||||
h.step(&json!({"key":"A","cmd":true})).unwrap();
|
||||
h.step(&json!({"text":text})).unwrap();
|
||||
if text.is_empty() {
|
||||
h.step(&json!({"key":"Backspace"})).unwrap();
|
||||
} else {
|
||||
h.step(&json!({"text":text})).unwrap();
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn dimension_parameters_use_typing_and_ctrl_space_completion() {
|
||||
for (prefix, expected) in [("wid", "width"), ("", "width"), ("d", "d1")] {
|
||||
let mut h = line([20., 0.]);
|
||||
h.app.run("parameters.add", json!({"name":"width","expression":"25 mm"})).unwrap();
|
||||
if prefix == "d" {
|
||||
h.app.run("sketch.line", json!({"points":[[0,-10],[12,-10]],"infer":false})).unwrap();
|
||||
h.app.run("sketch.dimension", json!({"entities":["l2"],"value":12,"text_at":[6,-16]})).unwrap();
|
||||
h.settle();
|
||||
}
|
||||
place(&mut h, &[[10., 0.]], [10., 6.]);
|
||||
let baseline = h.app.session.doc.to_json();
|
||||
type_value(&mut h, prefix);
|
||||
if prefix.is_empty() {
|
||||
h.step(&json!({"key":"Space","cmd":true})).unwrap();
|
||||
}
|
||||
assert!(h.widget(expected).is_some(), "parameter completion is available for {prefix:?}: {}", h.call("ui.inspect", json!({})));
|
||||
h.step(&json!({"key":"Tab"})).unwrap();
|
||||
assert!(crate::dim_view::editing(), "accepting completion does not apply the dimension");
|
||||
assert_eq!(h.app.session.doc.to_json(), baseline);
|
||||
h.step(&json!({"key":"Enter"})).unwrap();
|
||||
assert!(!crate::dim_view::editing());
|
||||
assert_eq!(h.app.session.doc.params.iter().rev().find(|p| p.model).unwrap().expr, expected);
|
||||
crate::dim_view::cancel();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edge_and_endpoint_creation_commit_once_and_undo_restores_exact_design() {
|
||||
for (end, picks, at) in [
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use egui::{Key, RichText};
|
||||
|
||||
use crate::params_dialog::{suggestions, word_before};
|
||||
use crate::params_dialog::{suggestions_for, word_before};
|
||||
use crate::theme::Tokens;
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -15,6 +15,7 @@ struct Popup {
|
||||
count: usize,
|
||||
accept: bool,
|
||||
dismissed: bool,
|
||||
explicit: bool,
|
||||
}
|
||||
|
||||
fn popup_id() -> egui::Id {
|
||||
@@ -25,6 +26,17 @@ fn popup_id() -> egui::Id {
|
||||
/// owns these keys. Raw events are removed too: several expression editors inspect raw Tab.
|
||||
pub(crate) fn begin_frame(ctx: &egui::Context) {
|
||||
let Some(mut popup) = ctx.data(|d| d.get_temp::<Popup>(popup_id())) else { return };
|
||||
if ctx.memory(|m| m.has_focus(popup.editor))
|
||||
&& popup.frame.saturating_add(1) >= ctx.cumulative_frame_nr()
|
||||
&& ctx.input_mut(|i| i.consume_key(egui::Modifiers::CTRL, Key::Space))
|
||||
{
|
||||
ctx.input_mut(|i| {
|
||||
i.raw.events.retain(|e| !matches!(e, egui::Event::Key { key: Key::Space, modifiers, .. } if modifiers.ctrl));
|
||||
});
|
||||
popup.explicit = true;
|
||||
popup.dismissed = false;
|
||||
ctx.data_mut(|d| d.insert_temp(popup_id(), popup.clone()));
|
||||
}
|
||||
let escape_blurred = ctx.input(|i| i.key_pressed(Key::Escape)) && ctx.memory(|m| m.focused().is_none() && m.had_focus_last_frame(popup.editor));
|
||||
let focused = ctx.memory(|m| m.has_focus(popup.editor)) || escape_blurred;
|
||||
if popup.frame.saturating_add(1) < ctx.cumulative_frame_nr() || !focused || popup.dismissed || popup.count == 0 {
|
||||
@@ -88,7 +100,6 @@ pub(crate) fn complete(ui: &mut egui::Ui, resp: &egui::Response, text: &mut Stri
|
||||
let state = egui::TextEdit::load_state(&ctx, resp.id);
|
||||
let cursor = state.as_ref().and_then(|s| s.cursor.char_range()).map(|r| r.primary.index.0).unwrap_or_else(|| text.chars().count());
|
||||
let (start, word) = word_before(text, cursor);
|
||||
let list = suggestions(&ctx, &word);
|
||||
let previous = ctx.data(|d| d.get_temp::<Popup>(popup_id()));
|
||||
let mut popup = previous.filter(|p| p.editor == resp.id && p.word == word && p.cursor == cursor).unwrap_or(Popup {
|
||||
editor: resp.id,
|
||||
@@ -96,10 +107,12 @@ pub(crate) fn complete(ui: &mut egui::Ui, resp: &egui::Response, text: &mut Stri
|
||||
word,
|
||||
cursor,
|
||||
selected: 0,
|
||||
count: list.len(),
|
||||
count: 0,
|
||||
accept: false,
|
||||
dismissed: false,
|
||||
explicit: false,
|
||||
});
|
||||
let list = suggestions_for(&ctx, &popup.word, popup.explicit);
|
||||
popup.frame = ctx.cumulative_frame_nr();
|
||||
popup.count = list.len();
|
||||
popup.selected = popup.selected.min(list.len().saturating_sub(1));
|
||||
@@ -153,6 +166,7 @@ pub(crate) fn complete(ui: &mut egui::Ui, resp: &egui::Response, text: &mut Stri
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::params_dialog::suggestions;
|
||||
use crate::{SolveApp, params_dialog};
|
||||
use serde_json::json;
|
||||
|
||||
@@ -308,6 +322,23 @@ mod tests {
|
||||
assert!(e.leaked.contains(&Key::Enter));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ctrl_space_opens_empty_expression_suggestions_and_reopens_dismissed_ones() {
|
||||
for prefix in ["", "wi"] {
|
||||
let mut e = Editor::new(prefix);
|
||||
if !prefix.is_empty() {
|
||||
e.key(Key::Escape);
|
||||
}
|
||||
e.frame(vec![egui::Event::Key { key: Key::Space, physical_key: None, pressed: true, repeat: false, modifiers: egui::Modifiers::CTRL }]);
|
||||
let popup = e.ctx.data(|d| d.get_temp::<Popup>(popup_id())).unwrap();
|
||||
assert!(popup.explicit && !popup.dismissed && popup.count > 0);
|
||||
assert_eq!(e.text, prefix, "Ctrl+Space does not add a space");
|
||||
e.key(Key::Tab);
|
||||
assert!(e.accepted);
|
||||
assert!(!e.text.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn modified_navigation_remains_available_to_the_editor() {
|
||||
let mut e = Editor::new("wi");
|
||||
|
||||
@@ -498,16 +498,24 @@ pub(crate) fn word_before(text: &str, cursor: usize) -> (usize, String) {
|
||||
|
||||
/// Suggestions for the word being typed.
|
||||
pub fn suggestions(ctx: &egui::Context, word: &str) -> Vec<Suggestion> {
|
||||
if word.is_empty() || word.chars().next().is_some_and(|c| c.is_ascii_digit()) {
|
||||
suggestions_for(ctx, word, false)
|
||||
}
|
||||
|
||||
pub(crate) fn suggestions_for(ctx: &egui::Context, word: &str, explicit: bool) -> Vec<Suggestion> {
|
||||
if (!explicit && word.is_empty()) || word.chars().next().is_some_and(|c| c.is_ascii_digit()) {
|
||||
return Vec::new();
|
||||
}
|
||||
let all = ctx.data(|d| d.get_temp::<Cache>(cache_id())).map(|c| c.all).unwrap_or_default();
|
||||
// A complete token is ready to submit, even when longer names share its prefix.
|
||||
if all.iter().any(|s| s.text == word) {
|
||||
if !explicit && all.iter().any(|s| s.text == word) {
|
||||
return Vec::new();
|
||||
}
|
||||
let w = word.to_ascii_lowercase();
|
||||
all.iter().filter(|s| s.text.to_ascii_lowercase().starts_with(&w) && s.text.trim_end_matches('(') != word).take(8).cloned().collect()
|
||||
all.iter()
|
||||
.filter(|s| s.text.to_ascii_lowercase().starts_with(&w) && (explicit || s.text.trim_end_matches('(') != word))
|
||||
.take(8)
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Offer keyboard and pointer completions under a focused expression box.
|
||||
|
||||
@@ -11,7 +11,9 @@ parameter editor. The separate **Configurations** button opens the configuration
|
||||
|
||||
Press **Enter** in the new parameter's name, expression or comment field to add it.
|
||||
Valid entries return focus to the name field for the next parameter; invalid entries remain
|
||||
editable. In expression fields, **Up/Down** selects an autocomplete suggestion,
|
||||
editable. In expression fields, type a parameter or dimension name for autocomplete,
|
||||
or press **Ctrl+Space** to open suggestions, including in an empty field.
|
||||
**Up/Down** selects an autocomplete suggestion,
|
||||
**Enter/Tab** accepts it, and **Escape** dismisses the suggestions. Accepting a suggestion
|
||||
does not also submit the parameter.
|
||||
|
||||
|
||||
Reference in new issue
Block a user