mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-24 10:27:00 -06:00
M7: fields select their text on focus
A click or Tab into a one-line field selects all of its text, so typing replaces the value, as in Figma: hex and colour fields, inline fields (a fill's hex, variable values, renames), names, the font size field, search boxes and dialog fields (widgets::select_all_on_focus). A drag from the press still selects only what it covers; multi-line fields keep the caret where clicked. The first click into an inline field (a fill's hex) now takes the keyboard: egui dropped the focus of the editor created by that click, so it took a second click.
This commit is contained in:
@@ -314,6 +314,7 @@ fn search_row(app: &mut FigApp, ui: &mut Ui, w: f32) -> String {
|
||||
.hint_text(tr("Search assets"))
|
||||
.desired_width(te_rect.width()),
|
||||
);
|
||||
crate::widgets::select_all_on_focus(ui, &r, &filter);
|
||||
if r.has_focus() {
|
||||
ui.painter().rect_stroke(field, CornerRadius::same(5), Stroke::new(1.0, t.accent), egui::StrokeKind::Inside);
|
||||
if ui.input(|i| i.key_pressed(egui::Key::Escape)) {
|
||||
|
||||
@@ -1030,6 +1030,7 @@ fn image_presets(app: &mut FigApp, ui: &mut Ui, target: &PickerTarget, paint: &m
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(16.0);
|
||||
let r = ui.add(egui::TextEdit::singleline(name).font(regular(11.0)).desired_width(136.0));
|
||||
crate::widgets::select_all_on_focus(ui, &r, name);
|
||||
if !r.has_focus() && !crate::a11y::field_focused(ui.ctx()) {
|
||||
r.request_focus();
|
||||
}
|
||||
|
||||
@@ -253,8 +253,10 @@ pub fn show(app: &mut FigApp, ctx: &Context) {
|
||||
.resizable(false)
|
||||
.show(ctx, |ui| {
|
||||
ui.set_width(280.0);
|
||||
ui.add(egui::TextEdit::singleline(&mut find).hint_text(tr("Find")).desired_width(260.0));
|
||||
ui.add(egui::TextEdit::singleline(&mut replace).hint_text(tr("Replace with")).desired_width(260.0));
|
||||
let r = ui.add(egui::TextEdit::singleline(&mut find).hint_text(tr("Find")).desired_width(260.0));
|
||||
crate::widgets::select_all_on_focus(ui, &r, &find);
|
||||
let r = ui.add(egui::TextEdit::singleline(&mut replace).hint_text(tr("Replace with")).desired_width(260.0));
|
||||
crate::widgets::select_all_on_focus(ui, &r, &replace);
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button(tr("Find")).clicked() {
|
||||
action = Some(false);
|
||||
@@ -356,11 +358,13 @@ fn batch_rename_dialog(app: &mut FigApp, ctx: &Context) {
|
||||
ui.set_width(320.0);
|
||||
ui.label(RichText::new(trn("{n} layers", n, &[])).color(t.text_secondary));
|
||||
ui.add_space(6.0);
|
||||
ui.add(egui::TextEdit::singleline(&mut b.find).hint_text(tr("Match (empty: the whole name)")).desired_width(300.0));
|
||||
let r = ui.add(egui::TextEdit::singleline(&mut b.find).hint_text(tr("Match (empty: the whole name)")).desired_width(300.0));
|
||||
crate::widgets::select_all_on_focus(ui, &r, &b.find);
|
||||
if crate::widgets::checkbox(ui, b.regex, tr("Regular expression")) {
|
||||
b.regex = !b.regex;
|
||||
}
|
||||
let to = ui.add(egui::TextEdit::singleline(&mut b.to).hint_text(tr("Rename to")).desired_width(300.0));
|
||||
crate::widgets::select_all_on_focus(ui, &to, &b.to);
|
||||
ui.horizontal(|ui| {
|
||||
for (label, code) in [(tr("Current name"), "$&"), (tr("Number ↑"), "$n"), (tr("Number ↓"), "$N")] {
|
||||
if ui.button(label).clicked() {
|
||||
|
||||
@@ -579,6 +579,7 @@ fn search_field(app: &mut FigApp, ui: &mut Ui, t: &Tokens, w: f32, id: Id) {
|
||||
.vertical_align(egui::Align::Center)
|
||||
.desired_width(te_rect.width()),
|
||||
);
|
||||
crate::widgets::select_all_on_focus(ui, &r, &app.ui.layer_filter);
|
||||
if r.has_focus() {
|
||||
ui.painter().rect_stroke(field, CornerRadius::same(5), Stroke::new(1.0, t.accent), egui::StrokeKind::Inside);
|
||||
}
|
||||
|
||||
@@ -147,6 +147,8 @@ mod proto_reorder_tests;
|
||||
#[cfg(test)]
|
||||
mod proto_responsive_tests;
|
||||
#[cfg(test)]
|
||||
mod select_all_tests;
|
||||
#[cfg(test)]
|
||||
mod shortcut_keys_tests;
|
||||
#[cfg(test)]
|
||||
mod shortcuts_tests;
|
||||
|
||||
@@ -987,6 +987,7 @@ fn report_ui(app: &mut FigApp, ui: &mut Ui, t: &Tokens) {
|
||||
.id(egui::Id::new("live_report_reason")),
|
||||
);
|
||||
a11y::text(&resp, tr("Report"), &reason);
|
||||
crate::widgets::select_all_on_focus(ui, &resp, &reason);
|
||||
if std::mem::take(&mut app.ui.live.focus_report) {
|
||||
resp.request_focus();
|
||||
}
|
||||
@@ -1148,6 +1149,7 @@ pub fn publish_dialog(app: &mut FigApp, ctx: &egui::Context) {
|
||||
);
|
||||
// The address may carry the server key: it isn't read out.
|
||||
a11y::text(&resp, tr("Server address"), "");
|
||||
crate::widgets::select_all_on_focus(ui, &resp, &form.server);
|
||||
if focus {
|
||||
resp.request_focus();
|
||||
}
|
||||
@@ -1177,6 +1179,7 @@ pub fn publish_dialog(app: &mut FigApp, ctx: &egui::Context) {
|
||||
widgets::label(ui, tr("File name"), t.text_secondary);
|
||||
let nresp = ui.add(egui::TextEdit::singleline(&mut form.name).desired_width(380.0).id(egui::Id::new("live_publish_name")));
|
||||
a11y::text(&nresp, tr("File name"), &form.name);
|
||||
crate::widgets::select_all_on_focus(ui, &nresp, &form.name);
|
||||
go |= nresp.lost_focus() && enter(ui) && (keyed || open_server);
|
||||
ui.add_space(4.0);
|
||||
ui.horizontal(|ui| {
|
||||
|
||||
@@ -627,6 +627,7 @@ fn palette(app: &mut FigApp, ctx: &egui::Context) {
|
||||
.frame(egui::Frame::NONE.inner_margin(egui::Margin::same(8))),
|
||||
);
|
||||
r.request_focus();
|
||||
crate::widgets::select_all_on_focus(ui, &r, &q);
|
||||
ui.add_space(4.0);
|
||||
let ql = q.to_lowercase();
|
||||
// Labels match in the UI language and in English.
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
//! A click (or Tab) into a one-line field selects all of its text, so typing replaces the value:
|
||||
//! the hex fields, inline fields (a fill's hex, renames), search boxes; a drag from the press
|
||||
//! selects only what it covers.
|
||||
|
||||
use egui::{Event, Id, Key, Modifiers, Pos2, RawInput, Rect, Ui, UiBuilder, pos2, vec2};
|
||||
|
||||
use crate::widgets;
|
||||
|
||||
const FIELD: Rect = Rect { min: pos2(20.0, 20.0), max: pos2(140.0, 44.0) };
|
||||
|
||||
fn frame(ctx: &egui::Context, events: Vec<Event>, f: &mut impl FnMut(&mut Ui)) {
|
||||
let input = RawInput { screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(400.0, 300.0))), events, ..Default::default() };
|
||||
ctx.run_ui(input, |ui| {
|
||||
let mut child = ui.new_child(UiBuilder::new().max_rect(FIELD));
|
||||
f(&mut child);
|
||||
})
|
||||
.textures_delta
|
||||
.clear();
|
||||
}
|
||||
|
||||
fn button(p: Pos2, pressed: bool) -> Event {
|
||||
Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed, modifiers: Modifiers::NONE }
|
||||
}
|
||||
|
||||
/// Moves to `at`, presses, (drags to `to`,) releases, then a few quiet frames.
|
||||
fn click(ctx: &egui::Context, at: Pos2, to: Option<Pos2>, f: &mut impl FnMut(&mut Ui)) {
|
||||
frame(ctx, vec![], f);
|
||||
frame(ctx, vec![Event::PointerMoved(at)], f);
|
||||
frame(ctx, vec![button(at, true)], f);
|
||||
let end = to.unwrap_or(at);
|
||||
if let Some(to) = to {
|
||||
for k in 1..=4 {
|
||||
frame(ctx, vec![Event::PointerMoved(at + (to - at) * (k as f32 / 4.0))], f);
|
||||
}
|
||||
}
|
||||
frame(ctx, vec![button(end, false)], f);
|
||||
for _ in 0..3 {
|
||||
frame(ctx, vec![], f);
|
||||
}
|
||||
}
|
||||
|
||||
fn selection(ctx: &egui::Context, te: Id) -> Option<(usize, usize)> {
|
||||
let r = egui::TextEdit::load_state(ctx, te)?.cursor.char_range()?;
|
||||
let (a, b): (usize, usize) = (r.primary.index.into(), r.secondary.index.into());
|
||||
Some((a.min(b), a.max(b)))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_click_into_a_hex_field_selects_it_all() {
|
||||
let ctx = egui::Context::default();
|
||||
let id = Id::new("hex");
|
||||
let mut f = |ui: &mut Ui| {
|
||||
widgets::text_field(ui, id, "D9D9D9", 120.0, "");
|
||||
};
|
||||
click(&ctx, pos2(60.0, 32.0), None, &mut f);
|
||||
assert!(ctx.memory(|m| m.has_focus(id.with("te"))));
|
||||
assert_eq!(selection(&ctx, id.with("te")), Some((0, 6)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_click_into_an_inline_field_selects_it_all() {
|
||||
// A fill's hex in the Design panel, a variable's value, a rename.
|
||||
let ctx = egui::Context::default();
|
||||
let id = Id::new("fill_hex");
|
||||
let mut f = |ui: &mut Ui| {
|
||||
widgets::text_field_inline(ui, id, "0D99FF", FIELD.shrink2(vec2(8.0, 4.0)), egui::FontId::proportional(11.0));
|
||||
};
|
||||
click(&ctx, pos2(70.0, 32.0), None, &mut f);
|
||||
assert_eq!(selection(&ctx, id.with("te")), Some((0, 6)));
|
||||
// Typing replaces it.
|
||||
frame(&ctx, vec![Event::Text("red".into())], &mut f);
|
||||
let mut out = None;
|
||||
let mut g = |ui: &mut Ui| out = widgets::text_field_inline(ui, id, "0D99FF", FIELD.shrink2(vec2(8.0, 4.0)), egui::FontId::proportional(11.0));
|
||||
frame(&ctx, vec![Event::Key { key: Key::Enter, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::NONE }], &mut g);
|
||||
assert_eq!(out.as_deref(), Some("red"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_click_into_a_search_box_selects_its_query() {
|
||||
let ctx = egui::Context::default();
|
||||
let id = Id::new("search");
|
||||
let mut q = String::from("button");
|
||||
let mut f = |ui: &mut Ui| {
|
||||
widgets::text_input(ui, id, &mut q, 120.0, "Search");
|
||||
};
|
||||
click(&ctx, pos2(60.0, 32.0), None, &mut f);
|
||||
assert_eq!(selection(&ctx, id), Some((0, 6)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tab_into_a_field_selects_it_all() {
|
||||
let ctx = egui::Context::default();
|
||||
let id = Id::new("tabbed");
|
||||
let mut q = String::from("Frame 1");
|
||||
let mut f = |ui: &mut Ui| {
|
||||
let r = widgets::text_input(ui, id, &mut q, 120.0, "");
|
||||
if !r.has_focus() && ui.input(|i| i.key_pressed(Key::Tab)) {
|
||||
r.request_focus();
|
||||
}
|
||||
};
|
||||
frame(&ctx, vec![], &mut f);
|
||||
frame(&ctx, vec![Event::Key { key: Key::Tab, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::NONE }], &mut f);
|
||||
for _ in 0..3 {
|
||||
frame(&ctx, vec![], &mut f);
|
||||
}
|
||||
assert_eq!(selection(&ctx, id), Some((0, 7)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_drag_selects_only_what_it_covers() {
|
||||
let ctx = egui::Context::default();
|
||||
let id = Id::new("dragged");
|
||||
let mut q = String::from("a long search query");
|
||||
let mut f = |ui: &mut Ui| {
|
||||
widgets::text_input(ui, id, &mut q, 120.0, "");
|
||||
};
|
||||
click(&ctx, pos2(30.0, 32.0), Some(pos2(70.0, 32.0)), &mut f);
|
||||
let (a, b) = selection(&ctx, id).unwrap_or_default();
|
||||
assert!(b > a, "something is selected: {a}..{b}");
|
||||
assert!(!(a == 0 && b == q.chars().count()), "not all of it: {a}..{b}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_field_left_mid_edit_selects_all_on_the_next_click() {
|
||||
let ctx = egui::Context::default();
|
||||
let id = Id::new("fill_hex");
|
||||
let mut f = |ui: &mut Ui| {
|
||||
widgets::text_field_inline(ui, id, "0D99FF", FIELD.shrink2(vec2(8.0, 4.0)), egui::FontId::proportional(11.0));
|
||||
};
|
||||
click(&ctx, pos2(70.0, 32.0), None, &mut f);
|
||||
// The panel goes away mid-edit (another layer selected) and a click lands elsewhere.
|
||||
click(&ctx, pos2(300.0, 200.0), None, &mut |_: &mut Ui| {});
|
||||
click(&ctx, pos2(70.0, 32.0), None, &mut f);
|
||||
assert!(ctx.memory(|m| m.has_focus(id.with("te"))));
|
||||
assert_eq!(selection(&ctx, id.with("te")), Some((0, 6)));
|
||||
}
|
||||
@@ -107,7 +107,8 @@ pub fn show(app: &mut FigApp, ctx: &Context) {
|
||||
ui.horizontal(|ui| {
|
||||
crate::dialogs::tab_strip(ui, &t, &Group::ALL, |g| tr(g.label()), &mut app.ui.shortcuts_tab);
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
ui.add(egui::TextEdit::singleline(&mut app.ui.shortcuts_query).hint_text(tr("Search shortcuts")).desired_width(180.0));
|
||||
let r = ui.add(egui::TextEdit::singleline(&mut app.ui.shortcuts_query).hint_text(tr("Search shortcuts")).desired_width(180.0));
|
||||
crate::widgets::select_all_on_focus(ui, &r, &app.ui.shortcuts_query);
|
||||
});
|
||||
});
|
||||
ui.add_space(10.0);
|
||||
|
||||
@@ -540,6 +540,7 @@ fn listing(app: &FigApp, ui: &mut Ui, doc: &Document, p: &mut Picker, out: &mut
|
||||
}
|
||||
ui.add_space(4.0);
|
||||
let r = ui.add(egui::TextEdit::singleline(&mut p.query).hint_text(tr("Search")).font(regular(11.0)).desired_width(W));
|
||||
crate::widgets::select_all_on_focus(ui, &r, &p.query);
|
||||
if !r.has_focus() && !crate::a11y::field_focused(ui.ctx()) {
|
||||
r.request_focus();
|
||||
}
|
||||
@@ -799,6 +800,7 @@ fn editor(ui: &mut Ui, doc: &Document, p: &mut Picker, out: &mut Frame) {
|
||||
egui::TextEdit::store_state(ui.ctx(), r.id, state);
|
||||
e.focus = false;
|
||||
}
|
||||
crate::widgets::select_all_on_focus(ui, &r, &e.name);
|
||||
ui.add_space(6.0);
|
||||
caption(ui, tr("Description"));
|
||||
ui.add(egui::TextEdit::multiline(&mut e.description).font(regular(11.0)).desired_width(W).desired_rows(2));
|
||||
|
||||
@@ -996,6 +996,7 @@ fn lead_field(ui: &mut Ui, id: Id, glyph: LeadGlyph, value: &str, width: f32, ti
|
||||
.desired_width(text_rect.width() - 6.0),
|
||||
);
|
||||
crate::a11y::own_focus(&r);
|
||||
crate::widgets::select_all_on_focus(ui, &r, &text);
|
||||
// The Enter that started the edit (from the keyboard) doesn't end it.
|
||||
let enter = !started && ui.input(|i| i.key_pressed(egui::Key::Enter));
|
||||
let esc = ui.input(|i| i.key_pressed(egui::Key::Escape));
|
||||
@@ -1089,6 +1090,7 @@ pub(crate) fn font_picker(ui: &mut Ui, current: Option<&str>, just_opened: bool)
|
||||
q.clear();
|
||||
}
|
||||
let search = ui.add(egui::TextEdit::singleline(&mut q).hint_text(tr("Search fonts")).desired_width(ui.available_width()).font(regular(11.5)));
|
||||
crate::widgets::select_all_on_focus(ui, &search, &q);
|
||||
if just_opened {
|
||||
search.request_focus();
|
||||
}
|
||||
|
||||
@@ -397,6 +397,7 @@ pub fn picker(app: &mut FigApp, ctx: &egui::Context) {
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
let r = ui.add(egui::TextEdit::singleline(&mut query).hint_text(tr("Search")).font(regular(11.0)).desired_width(224.0));
|
||||
crate::widgets::select_all_on_focus(ui, &r, &query);
|
||||
if !r.has_focus() && !crate::a11y::field_focused(ui.ctx()) {
|
||||
r.request_focus();
|
||||
}
|
||||
|
||||
@@ -523,13 +523,14 @@ fn table(app: &mut FigApp, ui: &mut Ui, view: &View, st: &mut Editor) {
|
||||
let mut child = ui.new_child(
|
||||
UiBuilder::new().max_rect(Rect::from_min_max(pos2(field.min.x + 24.0, field.min.y + 4.0), pos2(field.max.x - 4.0, field.max.y - 4.0))),
|
||||
);
|
||||
child.add(
|
||||
let r = child.add(
|
||||
egui::TextEdit::singleline(&mut st.query)
|
||||
.frame(egui::Frame::NONE)
|
||||
.hint_text(tr("Search"))
|
||||
.font(regular(11.0))
|
||||
.vertical_align(egui::Align::Center),
|
||||
);
|
||||
crate::widgets::select_all_on_focus(ui, &r, &st.query);
|
||||
let note = Rect::from_min_max(pos2(field.max.x + 16.0, sr.min.y + 8.0), pos2(sr.max.x - 16.0, sr.max.y - 8.0));
|
||||
let mut bar = ui.new_child(UiBuilder::new().max_rect(note).layout(egui::Layout::right_to_left(egui::Align::Center)));
|
||||
let text = match (view.library, coll.extends.as_deref().and_then(|p| view.vars.collection(p))) {
|
||||
|
||||
@@ -330,6 +330,7 @@ fn form(ui: &mut Ui, f: &mut Form, submit: &str, indent: f32) -> Option<Action>
|
||||
ui.add(egui::TextEdit::singleline(&mut f.name).hint_text(hint).font(regular(11.0)).frame(egui::Frame::NONE).desired_width(w))
|
||||
});
|
||||
crate::a11y::text(&title, hint, &f.name);
|
||||
crate::widgets::select_all_on_focus(ui, &title, &f.name);
|
||||
if std::mem::take(&mut f.focus) {
|
||||
title.request_focus();
|
||||
}
|
||||
|
||||
@@ -494,6 +494,7 @@ fn text_field_in(ui: &mut Ui, id: Id, value: &str, rect: Rect, placeholder: &str
|
||||
if started {
|
||||
editing = Some(value.to_string());
|
||||
ui.memory_mut(|m| m.request_focus(id.with("te")));
|
||||
select_all_next(ui, id.with("te"));
|
||||
}
|
||||
let mut out = None;
|
||||
match editing {
|
||||
@@ -527,14 +528,16 @@ fn text_field_in(ui: &mut Ui, id: Id, value: &str, rect: Rect, placeholder: &str
|
||||
fn field_editor(ui: &mut Ui, rect: Rect, id: Id, text: &mut String, hint: &str) -> Response {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let mut child = ui.new_child(egui::UiBuilder::new().max_rect(rect.shrink2(vec2(8.0, 4.0))));
|
||||
child.add(
|
||||
let r = child.add(
|
||||
egui::TextEdit::singleline(text)
|
||||
.id(id)
|
||||
.frame(egui::Frame::NONE)
|
||||
.font(regular(11.0))
|
||||
.hint_text(egui::RichText::new(hint).font(regular(11.0)).color(t.text_tertiary))
|
||||
.desired_width(rect.width() - 16.0),
|
||||
)
|
||||
);
|
||||
select_all_on_focus(ui, &r, text);
|
||||
r
|
||||
}
|
||||
|
||||
/// A text field that is typed into in place: `text` changes as it is typed (the caller decides
|
||||
@@ -562,6 +565,48 @@ fn keep_focus(r: &Response, started: bool) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Selects all of a one-line editor's `text` when it gains the keyboard (a click, Tab or a focus
|
||||
/// request), so what is typed replaces the value, as in Figma's fields. Call it after adding the
|
||||
/// editor (`r` is its response). egui puts the caret where the press landed, so the selection is
|
||||
/// laid again until the button is released; a drag from the press selects what it covers instead.
|
||||
pub fn select_all_on_focus(ui: &Ui, r: &Response, text: &str) {
|
||||
let pending = select_all_id(r.id);
|
||||
if r.gained_focus() {
|
||||
ui.data_mut(|d| d.insert_temp(pending, true));
|
||||
}
|
||||
if !ui.data(|d| d.get_temp::<bool>(pending)).unwrap_or(false) {
|
||||
return;
|
||||
}
|
||||
// Not focused yet (the click that starts an edit can land before the editor exists): wait.
|
||||
if !r.has_focus() {
|
||||
return;
|
||||
}
|
||||
let (down, dragged) = ui.input(|i| (i.pointer.any_down(), i.pointer.total_drag_delta().is_some_and(|d| d.length() > 6.0)));
|
||||
if down && dragged {
|
||||
ui.data_mut(|d| d.remove::<bool>(pending));
|
||||
return;
|
||||
}
|
||||
if let Some(mut state) = egui::TextEdit::load_state(ui.ctx(), r.id) {
|
||||
let all = egui::text::CCursorRange::two(egui::text::CCursor::new(0), egui::text::CCursor::new(text.chars().count()));
|
||||
state.cursor.set_char_range(Some(all));
|
||||
state.store(ui.ctx(), r.id);
|
||||
ui.ctx().request_repaint();
|
||||
}
|
||||
if !down {
|
||||
ui.data_mut(|d| d.remove::<bool>(pending));
|
||||
}
|
||||
}
|
||||
|
||||
/// Has [`select_all_on_focus`] select all of editor `te_id` from now on (an edit that starts
|
||||
/// with the editor already focused, which then gains no focus).
|
||||
fn select_all_next(ui: &Ui, te_id: Id) {
|
||||
ui.data_mut(|d| d.insert_temp(select_all_id(te_id), true));
|
||||
}
|
||||
|
||||
fn select_all_id(te_id: Id) -> Id {
|
||||
te_id.with("select_all_on_focus")
|
||||
}
|
||||
|
||||
/// What a dropdown shows before its label.
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum DropLead {
|
||||
@@ -963,11 +1008,7 @@ pub fn text_field_inline(ui: &mut Ui, id: Id, value: &str, rect: Rect, font: egu
|
||||
if started {
|
||||
buf = Some(value.to_string());
|
||||
ui.memory_mut(|m| m.request_focus(te_id));
|
||||
// A rename starts with the whole name selected.
|
||||
let mut state = egui::TextEdit::load_state(ui.ctx(), te_id).unwrap_or_default();
|
||||
let all = egui::text::CCursorRange::two(egui::text::CCursor::new(0), egui::text::CCursor::new(value.chars().count()));
|
||||
state.cursor.set_char_range(Some(all));
|
||||
state.store(ui.ctx(), te_id);
|
||||
select_all_next(ui, te_id);
|
||||
}
|
||||
match buf {
|
||||
Some(mut text) => {
|
||||
@@ -976,6 +1017,9 @@ pub fn text_field_inline(ui: &mut Ui, id: Id, value: &str, rect: Rect, font: egu
|
||||
let mut child = ui.new_child(egui::UiBuilder::new().max_rect(rect));
|
||||
let r = crate::bidi::single_line(&mut child, te_id, &mut text, font, rect.width());
|
||||
a11y::own_focus(&r);
|
||||
keep_focus(&r, started);
|
||||
// A rename or a hex edit starts with the whole text selected.
|
||||
select_all_on_focus(ui, &r, &text);
|
||||
// The Enter that started the edit (from the keyboard) doesn't end it.
|
||||
let enter = !started && ui.input(|i| i.key_pressed(egui::Key::Enter));
|
||||
let esc = ui.input(|i| i.key_pressed(egui::Key::Escape));
|
||||
|
||||
+3
-1
@@ -1,6 +1,6 @@
|
||||
# UI and interaction parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** minor (the Small caps sample in Type settings drawn from capitals; recent files as cards; vector edit mode: hovering, hatching, selecting and dragging filled regions; home screen: Templates tab, first-run templates and Getting started tips; number fields: scrub rates, arrow steps, arithmetic with brackets, relative edits of mixed values; outline mode's hidden layers and object bounds; Hide when publishing; cursors per tool and modifier; panel widths; dark pages for files made in the dark theme; drag threshold and double-clicks into text in groups; rotation origin on Alt+R and placing with Enter: no documented shortcut missing; keyboard shortcut parity suite and bindings; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and the vector edit tail; canvas navigation, guides and grids behaviour suite and fixes; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; layers panel keyboard: F2 rename, Shift+F10 context menu, F8 box selection; selection and transform behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; boolean hit testing on the result, group drags, frame outlines and slices; outlines for the selection, mask outlines; arc fields, live boolean editing, shape builder on strokes, one-sided widths) · **Target:** Figma Design UI3 (desktop app 126.10.7)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-15 · **Change:** minor (fields select their text on focus; the Small caps sample in Type settings drawn from capitals; recent files as cards; vector edit mode: hovering, hatching, selecting and dragging filled regions; home screen: Templates tab, first-run templates and Getting started tips; number fields: scrub rates, arrow steps, arithmetic with brackets, relative edits of mixed values; outline mode's hidden layers and object bounds; Hide when publishing; cursors per tool and modifier; panel widths; dark pages for files made in the dark theme; drag threshold and double-clicks into text in groups; rotation origin on Alt+R and placing with Enter: no documented shortcut missing; keyboard shortcut parity suite and bindings; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and the vector edit tail; canvas navigation, guides and grids behaviour suite and fixes; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; layers panel keyboard: F2 rename, Shift+F10 context menu, F8 box selection; selection and transform behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; boolean hit testing on the result, group drags, frame outlines and slices; outlines for the selection, mask outlines; arc fields, live boolean editing, shape builder on strokes, one-sided widths) · **Target:** Figma Design UI3 (desktop app 126.10.7)
|
||||
|
||||
Fine canvas control is what Figma users feel first: handles, snapping, nudging, modifier keys,
|
||||
drag behaviour, numeric precision. This checklist covers the tools, gestures and panels. Status:
|
||||
@@ -26,6 +26,7 @@ interaction feel not yet compared.
|
||||
| Keyboard shortcuts panel | done | docked, tabbed by group like the documentation, searchable, per-platform keycaps; held keys, gestures and presentation keys listed beside the bindings; a label too long for its column ends with "…" and shows in full on hover |
|
||||
| Several windows | partial | File ▸ New window, Open in new window on a tab, a tab dragged off the strip; screen-reader tree in the main window only; positions not restored |
|
||||
| Multiplayer avatars, Share button | missing | waits for multiplayer |
|
||||
| Fields select their text on focus | done | a click or Tab into a one-line field selects all of its text, so typing replaces the value: hex and colour fields, number fields, inline fields (a fill's hex, variable values, renames), names, search boxes and dialog fields; a drag from the press selects only what it covers; multi-line fields (descriptions, comments) keep the caret where clicked. The first click into an inline field (a fill's hex) now takes the keyboard; it took a second click (`widgets::select_all_on_focus`, `select_all_tests`) |
|
||||
| Blend mode menus: the layer's (Design panel) and each paint's (colour picker) | done | Pass through and Normal are distinct (Normal isolates a group's or frame's children); paints have no pass-through; plus darker drawn exactly on the canvas |
|
||||
|
||||
## Layers and Assets panels
|
||||
@@ -164,6 +165,7 @@ in the table. `crates/ui-egui/src/shortcut_keys_tests.rs` drives the keys a look
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-15 | minor | A click or Tab into a one-line field selects all of its text (hex, inline, name, search and dialog fields); the first click into a fill's hex takes the keyboard |
|
||||
| 2026-10-14 | minor | Type settings: the Small caps button draws its sample as a capital and a smaller capital, no longer as a box where the UI font lacks the small-capital letter (egui's default fonts) |
|
||||
| 2026-10-14 | minor | Home screen recent files as cards with thumbnails on their page colour, a sortable table view, selection, keyboard and a context menu |
|
||||
| 2026-10-13 | minor | Vector edit mode regions: with the Move tool the filled area under the pointer is hatched with "/" lines in the selection colour (about 1.4 px), clipped by the area's own fill rule so holes stay clear, and the hatch goes when the pointer is 3.5 px from a point or 2 px from a segment (screen pixels at any zoom); the spacing follows octave folding, fitted to measurements at several zoom levels (8 px at every power-of-two zoom, the gap growing with the zoom and stepping back at √2 · 2ⁿ), anchored to the vector so it doesn't slide when panning, one continuous pattern across selected areas; a click selects an area with its points (they move with it but keep the unselected look), Shift adds an area or takes a selected one out, a press on a selected area drags everything selected, drags show no hover hatch, a click on an empty spot (such as a hole) clears the areas and stays in edit mode, the last area listed wins where areas overlap, only filled vectors have areas; `vector.edit.select {regions}` and `vector.edit.state` report them (`regions_tests`, `vector_regions_tests`) |
|
||||
|
||||
Reference in New Issue
Block a user