Merge branch 'pr-583' into batch/t5

# Conflicts:
#	docs/ui-parity.md
This commit is contained in:
bflatastic
2026-10-16 01:36:24 +07:00
16 changed files with 216 additions and 13 deletions
+1
View File
@@ -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)) {
+1
View File
@@ -1063,6 +1063,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();
}
+7 -3
View File
@@ -290,8 +290,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);
@@ -393,11 +395,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() {
+1
View File
@@ -591,6 +591,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);
}
+2
View File
@@ -159,6 +159,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;
+3
View File
@@ -1002,6 +1002,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();
}
@@ -1163,6 +1164,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();
}
@@ -1192,6 +1194,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| {
+1
View File
@@ -638,6 +638,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.
+136
View File
@@ -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)));
}
+2 -1
View File
@@ -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);
+2
View File
@@ -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();
}
@@ -800,6 +801,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));
+2
View File
@@ -1046,6 +1046,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));
@@ -1179,6 +1180,7 @@ pub(crate) fn font_picker(ui: &mut Ui, current: Option<&str>, just_opened: bool)
- i.count_and_consume_key(egui::Modifiers::NONE, egui::Key::ArrowUp) as i64
});
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();
}
+1
View File
@@ -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();
}
+2 -1
View File
@@ -538,13 +538,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))) {
+1
View File
@@ -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();
}
+51 -7
View File
@@ -501,6 +501,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 {
@@ -534,14 +535,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
@@ -569,6 +572,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 {
@@ -970,11 +1015,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) => {
@@ -983,6 +1024,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
View File
@@ -1,6 +1,6 @@
# UI and interaction parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-15 · **Change:** minor (Typography spacing icons support scrubbing (#373); CSS colour names in hex fields; 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; Typography spacing icons support scrubbing (#373); CSS colour names in hex fields; 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:
@@ -27,6 +27,7 @@ interaction feel not yet compared.
| 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 |
| Colour names in hex fields | done | every hex field (fills, strokes, text ranges, effects, layout grids, colour variables, page and prototype backgrounds, the colour picker's Hex) takes a CSS colour name as well: `red` becomes `FF0000`; case, spaces and hyphens are ignored (`Light Blue`); names are opaque and keep the colour's opacity; hex wins where both could match. The picker's CSS field takes every CSS name too (`Color::from_hex_or_name`, `color::named`) |
| 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
@@ -167,6 +168,7 @@ in the table. `crates/ui-egui/src/shortcut_keys_tests.rs` drives the keys a look
|---|---|---|
| 2026-10-15 | minor | Hex fields take CSS colour names (`red` → `FF0000`): fills, strokes, effects, grids, variables, backgrounds and the colour picker |
| 2026-10-15 | minor | Typography: drag the line-height, letter-spacing and paragraph-spacing icons to scrub their values; shared numeric-field rates, resize cursor and one Undo step per gesture (#373). |
| 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`) |