#861: one search field for Pages and Layers; Escape closes it only from the field and goes no further; the search button shows when it's on; filter pages in the row loop

This commit is contained in:
bflatastic committed 2026-10-19 23:43:38 +07:00
1 parent be38c68ac5
commit 4cc2230013
2 files changed
+43 -53

No files matched your search

+21 -49
View File
@@ -185,18 +185,16 @@ fn pages(app: &mut FigApp, ui: &mut Ui) {
}
let sr = Rect::from_center_size(pos2(hr.max.x - 50.0, hr.center().y), vec2(24.0, 24.0));
let sresp = ui.interact(sr, ui.id().with("lsearch"), Sense::click()).tip(tr("Search pages"));
if sresp.hovered() {
if sresp.hovered() || app.ui.page_search {
ui.painter().rect_filled(sr, CornerRadius::same(5), t.hover);
}
icons::paint(ui.painter(), sr.shrink(5.0), Icon::Search, t.icon);
let search_id = Id::new("page_search_field");
let mut focus_search = false;
if sresp.clicked() {
app.ui.page_search = !app.ui.page_search;
app.ui.page_filter.clear();
if app.ui.page_search {
app.ui.pages_open = true;
focus_search = true;
}
}
let ar = Rect::from_center_size(pos2(hr.max.x - 22.0, hr.center().y), vec2(24.0, 24.0));
@@ -213,14 +211,12 @@ fn pages(app: &mut FigApp, ui: &mut Ui) {
return;
}
if app.ui.page_search {
page_search_field(app, ui, &t, w, search_id, focus_search);
search_field(ui, &t, w, search_id, tr("Search pages"), &mut app.ui.page_filter, &mut app.ui.page_search, sresp.clicked());
}
let Ok(ds) = app.session.ds() else { return };
let pages: Vec<(NodeId, String, bool)> =
ds.doc.pages.iter().filter_map(|p| ds.doc.node(*p).map(|n| (*p, n.name.clone(), ds.doc.is_divider(*p)))).collect();
let filter = app.ui.page_filter.trim().to_lowercase();
let visible_pages: Vec<(NodeId, String, bool)> =
pages.iter().filter(|(_, name, divider)| filter.is_empty() || (!divider && name.to_lowercase().contains(&filter))).cloned().collect();
let filtering = !filter.is_empty();
let cur = ds.page;
let uid = ds.uid;
@@ -236,7 +232,10 @@ fn pages(app: &mut FigApp, ui: &mut Ui) {
// A long list scrolls in its own area, so the layers keep their room below it.
let max_h = pages_list_height(app.ui.pages_height, ui.available_height());
let mut moved = egui::ScrollArea::vertical().id_salt("pages_list").max_height(max_h).show(ui, |ui| {
for (k, (id, name, divider)) in visible_pages.iter().cloned().enumerate() {
for (k, (id, name, divider)) in pages.iter().cloned().enumerate() {
if filtering && (divider || !name.to_lowercase().contains(&filter)) {
continue;
}
let sense = if filtering { Sense::click() } else { Sense::click_and_drag() };
let (r, resp) = ui.allocate_exact_size(vec2(w, 32.0), sense);
if !filtering {
@@ -332,36 +331,6 @@ fn pages(app: &mut FigApp, ui: &mut Ui) {
pages_edge(app, ui, moved.inner_rect.height().min(moved.content_size.y));
}
/// The Pages panel's inline filter (Escape clears and closes it).
fn page_search_field(app: &mut FigApp, ui: &mut Ui, t: &Tokens, w: f32, id: Id, focus: bool) {
let (fr, _) = ui.allocate_exact_size(vec2(w, 32.0), Sense::hover());
let field = Rect::from_min_max(pos2(fr.min.x + 12.0, fr.min.y + 2.0), pos2(fr.max.x - 12.0, fr.max.y - 6.0));
ui.painter().rect_filled(field, CornerRadius::same(5), t.field);
icons::paint(ui.painter(), Rect::from_center_size(pos2(field.min.x + 12.0, field.center().y), vec2(12.0, 12.0)), Icon::Search, t.icon_secondary);
let te_rect = Rect::from_min_max(pos2(field.min.x + 24.0, field.center().y - 7.0), pos2(field.max.x - 6.0, field.center().y + 7.0));
let mut child = ui.new_child(egui::UiBuilder::new().max_rect(te_rect));
let r = child.add(
egui::TextEdit::singleline(&mut app.ui.page_filter)
.id(id)
.frame(egui::Frame::NONE)
.font(regular(11.0))
.hint_text(tr("Search pages"))
.vertical_align(egui::Align::Center)
.desired_width(te_rect.width()),
);
if focus {
r.request_focus();
}
crate::widgets::select_all_on_focus(ui, &r, &app.ui.page_filter);
if r.has_focus() {
ui.painter().rect_stroke(field, CornerRadius::same(5), Stroke::new(1.0, t.accent), egui::StrokeKind::Inside);
}
if app.ui.page_search && ui.input(|i| i.key_pressed(egui::Key::Escape)) {
app.ui.page_search = false;
app.ui.page_filter.clear();
}
}
/// The Pages list has the keyboard: a page row (or the add-page button) of the open file was
/// clicked last, and the list is still on screen.
pub(crate) fn pages_have_keyboard(app: &FigApp) -> bool {
@@ -688,8 +657,10 @@ fn set_expanded_deep(doc: &Document, id: NodeId, open: bool, expanded: &mut BTre
}
}
/// The search row under the Layers header (live filter; Escape closes it).
fn search_field(app: &mut FigApp, ui: &mut Ui, t: &Tokens, w: f32, id: Id) {
/// The search row under the Pages or Layers header: a live filter `text`, taking the keyboard when
/// `focus` (just opened). Escape in it clears it and closes the row (`open`), and goes no further
/// (the canvas keeps its selection).
fn search_field(ui: &mut Ui, t: &Tokens, w: f32, id: Id, hint: &str, text: &mut String, open: &mut bool, focus: bool) {
let (fr, _) = ui.allocate_exact_size(vec2(w, 32.0), Sense::hover());
let field = Rect::from_min_max(pos2(fr.min.x + 12.0, fr.min.y + 2.0), pos2(fr.max.x - 12.0, fr.max.y - 6.0));
ui.painter().rect_filled(field, CornerRadius::same(5), t.field);
@@ -697,21 +668,25 @@ fn search_field(app: &mut FigApp, ui: &mut Ui, t: &Tokens, w: f32, id: Id) {
let te_rect = Rect::from_min_max(pos2(field.min.x + 24.0, field.center().y - 7.0), pos2(field.max.x - 6.0, field.center().y + 7.0));
let mut child = ui.new_child(egui::UiBuilder::new().max_rect(te_rect));
let r = child.add(
egui::TextEdit::singleline(&mut app.ui.layer_filter)
egui::TextEdit::singleline(text)
.id(id)
.frame(egui::Frame::NONE)
.font(regular(11.0))
.hint_text(tr("Find layers"))
.hint_text(hint)
.vertical_align(egui::Align::Center)
.desired_width(te_rect.width()),
);
crate::widgets::select_all_on_focus(ui, &r, &app.ui.layer_filter);
// After the field: the click that opened it ends elsewhere, which would take the focus back.
if focus {
r.request_focus();
}
crate::widgets::select_all_on_focus(ui, &r, text);
if r.has_focus() {
ui.painter().rect_stroke(field, CornerRadius::same(5), Stroke::new(1.0, t.accent), egui::StrokeKind::Inside);
}
if (r.has_focus() || r.lost_focus()) && ui.input(|i| i.key_pressed(egui::Key::Escape)) {
app.ui.layer_search = false;
app.ui.layer_filter.clear();
if (r.has_focus() || r.lost_focus()) && ui.input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::Escape)) {
*open = false;
text.clear();
}
}
@@ -730,12 +705,9 @@ fn layers(app: &mut FigApp, ui: &mut Ui) {
if sresp.clicked() {
app.ui.layer_search = !app.ui.layer_search;
app.ui.layer_filter.clear();
if app.ui.layer_search {
ui.memory_mut(|m| m.request_focus(search_id));
}
}
if app.ui.layer_search {
search_field(app, ui, &t, w, search_id);
search_field(ui, &t, w, search_id, tr("Find layers"), &mut app.ui.layer_filter, &mut app.ui.layer_search, sresp.clicked());
}
let Ok(ds) = app.session.ds() else { return };
let doc = ds.doc.clone();
+22 -4
View File
@@ -217,16 +217,34 @@ fn pages_search_button_opens_the_filter_and_escape_clears_it() {
}
#[test]
fn pages_search_escape_closes_even_when_the_field_is_unfocused() {
let mut u = Ui::new(Session::new());
fn pages_search_leaves_escape_to_the_canvas_when_the_field_is_unfocused() {
let mut s = Session::new();
let r = rect(&mut s, "r", 0.0, None);
s.execute("select.set", &json!({"ids": [r]})).unwrap();
let mut u = Ui::new(s);
u.app.ui.page_search = true;
u.app.ui.page_filter = "COMPON".into();
u.frame(vec![]);
u.frame(vec![Event::Key { key: egui::Key::Escape, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::NONE }]);
assert!(!u.app.ui.page_search, "Escape closes page search without field focus");
assert!(u.app.ui.page_filter.is_empty(), "Escape clears the filter without field focus");
assert!(u.app.session.selection().is_empty(), "Escape deselects on the canvas");
assert!(u.app.ui.page_search, "the page search stays open");
assert_eq!(u.app.ui.page_filter, "COMPON", "and keeps its filter");
}
#[test]
fn escape_in_the_pages_search_does_not_reach_the_canvas() {
let mut s = Session::new();
let r = rect(&mut s, "r", 0.0, None);
s.execute("select.set", &json!({"ids": [r]})).unwrap();
let mut u = Ui::new(s);
click(&mut u, Pos2::new(246.0, 124.0), Modifiers::NONE);
assert!(u.app.ui.page_search);
u.frame(vec![Event::Text("x".into())]);
u.frame(vec![Event::Key { key: egui::Key::Escape, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::NONE }]);
assert!(!u.app.ui.page_search, "Escape closes the page search");
assert_eq!(u.app.session.selection(), vec![r], "and leaves the selection alone");
}
#[test]