mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-24 10:27:00 -06:00
Merge pull request #861 from AJMakesStuff/#855--The-Search-button-for-the-pages-doesn't-work-as-expected
#855: The Search button for the pages doesn't work as expected
This commit is contained in:
@@ -1186,6 +1186,7 @@ name = "English"
|
||||
"Search results" = "Search results"
|
||||
"Search fonts" = "Search fonts"
|
||||
"Search layers" = "Search layers"
|
||||
"Search pages" = "Search pages"
|
||||
"Search shortcuts" = "Search shortcuts"
|
||||
"Section" = "Section"
|
||||
"Section color" = "Section color"
|
||||
|
||||
@@ -1187,6 +1187,7 @@ status = "draft — needs native review"
|
||||
"Search results" = "Resultados de búsqueda"
|
||||
"Search fonts" = "Buscar fuentes"
|
||||
"Search layers" = "Buscar capas"
|
||||
"Search pages" = "Buscar páginas"
|
||||
"Search shortcuts" = "Buscar atajos"
|
||||
"Section" = "Sección"
|
||||
"Section color" = "Color de sección"
|
||||
|
||||
@@ -184,13 +184,18 @@ fn pages(app: &mut FigApp, ui: &mut Ui) {
|
||||
app.ui.pages_open = !app.ui.pages_open;
|
||||
}
|
||||
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(crate::shortcuts::tip(tr("Find"), "edit.findReplace"));
|
||||
if sresp.hovered() {
|
||||
let sresp = ui.interact(sr, ui.id().with("lsearch"), Sense::click()).tip(tr("Search pages"));
|
||||
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");
|
||||
if sresp.clicked() {
|
||||
app.ui.find = Some((String::new(), String::new()));
|
||||
app.ui.page_search = !app.ui.page_search;
|
||||
app.ui.page_filter.clear();
|
||||
if app.ui.page_search {
|
||||
app.ui.pages_open = true;
|
||||
}
|
||||
}
|
||||
let ar = Rect::from_center_size(pos2(hr.max.x - 22.0, hr.center().y), vec2(24.0, 24.0));
|
||||
let aresp = ui.interact(ar, ui.id().with("addpage"), Sense::click()).tip(tr("Add new page"));
|
||||
@@ -205,9 +210,14 @@ fn pages(app: &mut FigApp, ui: &mut Ui) {
|
||||
if !app.ui.pages_open {
|
||||
return;
|
||||
}
|
||||
if app.ui.page_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 filtering = !filter.is_empty();
|
||||
let cur = ds.page;
|
||||
let uid = ds.uid;
|
||||
let sel = ds.selection.clone();
|
||||
@@ -223,8 +233,14 @@ fn pages(app: &mut FigApp, ui: &mut Ui) {
|
||||
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 pages.iter().cloned().enumerate() {
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(w, 32.0), Sense::click_and_drag());
|
||||
reorder.row_by(&resp, k, r, || if divider { tr("Divider").to_string() } else { name.clone() });
|
||||
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 {
|
||||
reorder.row_by(&resp, k, r, || if divider { tr("Divider").to_string() } else { name.clone() });
|
||||
}
|
||||
a11y::list_item(&resp, if divider { tr("Divider") } else { &name }, id == cur);
|
||||
let r = r.shrink2(vec2(8.0, 0.0));
|
||||
a11y::ring_around(ui, r, &resp);
|
||||
@@ -641,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);
|
||||
@@ -650,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, 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -683,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();
|
||||
|
||||
@@ -199,6 +199,81 @@ fn page_row(u: &mut Ui, page: NodeId, back: NodeId) -> Pos2 {
|
||||
panic!("no row for page {page:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pages_search_button_opens_the_filter_and_escape_clears_it() {
|
||||
let s = Session::new();
|
||||
let _ = s.page().unwrap();
|
||||
let mut u = Ui::new(s);
|
||||
|
||||
// The search icon in the Pages header opens the page-name field, not Find and Replace.
|
||||
click(&mut u, Pos2::new(246.0, 124.0), Modifiers::NONE);
|
||||
assert!(u.app.ui.page_search, "the Pages search is open");
|
||||
assert!(u.app.ui.find.is_none(), "page search does not open Find and Replace");
|
||||
u.frame(vec![Event::Text("COMPON".into())]);
|
||||
assert_eq!(u.app.ui.page_filter, "COMPON", "typing can start immediately");
|
||||
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");
|
||||
assert!(u.app.ui.page_filter.is_empty(), "Escape clears the filter");
|
||||
}
|
||||
|
||||
#[test]
|
||||
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.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]
|
||||
fn pages_search_filters_page_names_case_insensitively() {
|
||||
let mut s = Session::new();
|
||||
let _overview = s.page().unwrap();
|
||||
let components = id(s.execute("page.add", &json!({"name": "Components"})).unwrap());
|
||||
let library = id(s.execute("page.add", &json!({"name": "Library"})).unwrap());
|
||||
s.execute("page.select", &json!({"id": library})).unwrap();
|
||||
let mut u = Ui::new(s);
|
||||
u.app.ui.page_search = true;
|
||||
u.app.ui.page_filter = "COMPON".into();
|
||||
let shapes = u.frame_output(vec![]).shapes;
|
||||
let text = crate::live_tests::texts(&crate::live_tests::flat(&shapes));
|
||||
assert!(text.iter().any(|(label, _, _)| label == "Components"), "the matching page remains visible");
|
||||
assert!(!text.iter().any(|(label, _, _)| label == "Library"), "nonmatching pages are filtered out");
|
||||
|
||||
// Search leaves only Components; the active page starts as Library.
|
||||
for y in (150..400).step_by(4) {
|
||||
let p = Pos2::new(150.0, y as f32);
|
||||
u.button(p, true);
|
||||
u.button(p, false);
|
||||
if u.app.session.page().unwrap() == components {
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert_eq!(u.app.session.page().unwrap(), components, "matching is case-insensitive and opens the named page");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pages_drag_to_reorder_and_dividers_open_nothing() {
|
||||
let mut s = Session::new();
|
||||
|
||||
@@ -483,6 +483,9 @@ pub struct UiState {
|
||||
pub last_right_tab: RightTab,
|
||||
pub left_tab: LeftTab,
|
||||
pub pages_open: bool,
|
||||
/// Pages panel search: open, and the case-insensitive name filter.
|
||||
pub page_search: bool,
|
||||
pub page_filter: String,
|
||||
/// The height asked for the pages list (`None`: a share of the panel).
|
||||
pub pages_height: Option<f32>,
|
||||
pub expanded: BTreeSet<NodeId>,
|
||||
@@ -651,6 +654,8 @@ impl Default for UiState {
|
||||
last_right_tab: RightTab::Design,
|
||||
left_tab: LeftTab::File,
|
||||
pages_open: true,
|
||||
page_search: false,
|
||||
page_filter: String::new(),
|
||||
pages_height: None,
|
||||
expanded: BTreeSet::new(),
|
||||
rename: None,
|
||||
|
||||
Reference in New Issue
Block a user