mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-28 04:16:46 -06:00
#765: search results use Home's own Open files rows and Recent files section (the full list stays known, so pictures and the selection survive a search); search_grid, SearchHit, prepare_search and search_file_card go; the field is widgets::text_input and Escape clears it
This commit is contained in:
+37
-121
@@ -4,7 +4,6 @@
|
||||
//! the documents themselves, searched and filtered by kind).
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
|
||||
use egui::{Align2, Color32, CornerRadius, Id, Rect, Response, Sense, Stroke, StrokeKind, Ui, pos2, vec2};
|
||||
use figcraft_engine::templates::{Kind, TEMPLATES, Template};
|
||||
@@ -12,7 +11,6 @@ use serde_json::json;
|
||||
|
||||
use crate::FigApp;
|
||||
use crate::a11y;
|
||||
use crate::home_recents::SearchHit;
|
||||
use crate::i18n::{tr, tr_noop};
|
||||
use crate::icons::{self, Icon};
|
||||
use crate::theme::{ACCENT, Tokens, regular, semibold};
|
||||
@@ -207,35 +205,16 @@ fn sidebar(app: &mut FigApp, ui: &mut Ui, t: &Tokens, left: Rect) {
|
||||
icons::paint(ui.painter(), Rect::from_min_size(pos2(left.min.x + 16.0, y), vec2(20.0, 20.0)), Icon::Logo, ACCENT);
|
||||
ui.painter().text(pos2(left.min.x + 44.0, y + 10.0), Align2::LEFT_CENTER, "FigCraft", semibold(14.0), t.text);
|
||||
y += 48.0;
|
||||
let search = Rect::from_min_size(pos2(left.min.x + 8.0, y), vec2(SIDEBAR - 16.0, 32.0));
|
||||
let search = Rect::from_min_size(pos2(left.min.x + 8.0, y), vec2(SIDEBAR - 16.0, widgets::FIELD_H));
|
||||
ui.scope_builder(egui::UiBuilder::new().max_rect(search), |ui| {
|
||||
ui.painter().rect_filled(search, CornerRadius::same(5), t.field);
|
||||
icons::paint(
|
||||
ui.painter(),
|
||||
Rect::from_center_size(pos2(search.min.x + 18.0, search.center().y), vec2(16.0, 16.0)),
|
||||
Icon::Search,
|
||||
t.icon_secondary,
|
||||
);
|
||||
let text_rect = Rect::from_min_max(pos2(search.min.x + 36.0, search.center().y - 8.0), pos2(search.max.x - 8.0, search.center().y + 8.0));
|
||||
let mut child = ui.new_child(egui::UiBuilder::new().max_rect(text_rect));
|
||||
let response = child.add(
|
||||
egui::TextEdit::singleline(&mut app.ui.home_view.file_query)
|
||||
.id(Id::new("home_file_search"))
|
||||
.frame(egui::Frame::NONE)
|
||||
.font(regular(12.0))
|
||||
.hint_text(tr("Search files and templates"))
|
||||
.vertical_align(egui::Align::Center)
|
||||
.desired_width(text_rect.width()),
|
||||
);
|
||||
widgets::select_all_on_focus(ui, &response, &app.ui.home_view.file_query);
|
||||
let response =
|
||||
widgets::text_input(ui, Id::new("home_file_search"), &mut app.ui.home_view.file_query, search.width(), tr("Search files and templates"));
|
||||
a11y::text(&response, tr("Search files and templates"), &app.ui.home_view.file_query);
|
||||
if response.has_focus() {
|
||||
ui.painter().rect_stroke(search, CornerRadius::same(5), Stroke::new(1.0, t.accent), StrokeKind::Inside);
|
||||
} else if ui.rect_contains_pointer(search) {
|
||||
ui.painter().rect_stroke(search, CornerRadius::same(5), Stroke::new(1.0, t.field_hover_border), StrokeKind::Inside);
|
||||
if (response.has_focus() || response.lost_focus()) && ui.input(|i| i.key_pressed(egui::Key::Escape)) {
|
||||
app.ui.home_view.file_query.clear();
|
||||
}
|
||||
});
|
||||
y += 32.0 + 4.0;
|
||||
y += widgets::FIELD_H + 12.0;
|
||||
for (section, icon, label) in Section::ALL {
|
||||
let r = Rect::from_min_size(pos2(left.min.x + 8.0, y), vec2(SIDEBAR - 16.0, 32.0));
|
||||
let resp = ui.interact(r, ui.id().with(("home_section", label)), Sense::click());
|
||||
@@ -259,78 +238,13 @@ fn file_matches(name: &str, location: &str, query: &str) -> bool {
|
||||
name.to_lowercase().contains(query) || location.to_lowercase().contains(query)
|
||||
}
|
||||
|
||||
/// An online search result with a name and optional location on separate lines. Local files use
|
||||
/// the Recent files cards below so their stored preview and file-type icon are also shown here.
|
||||
fn search_file_card(ui: &mut Ui, t: &Tokens, id: Id, row: &FileRow) -> bool {
|
||||
let (r, _) = ui.allocate_exact_size(vec2(ui.available_width().min(640.0), 64.0), Sense::hover());
|
||||
let resp = ui.interact(r, id, Sense::click());
|
||||
a11y::button(&resp, row.name);
|
||||
ui.painter().rect_filled(r, CornerRadius::same(8), if resp.hovered() { t.hover } else { t.field });
|
||||
ui.painter().rect_stroke(r, CornerRadius::same(8), Stroke::new(1.0, t.border), StrokeKind::Inside);
|
||||
focus_ring(ui, t, r, &resp, 8);
|
||||
if let Some((icon, tint)) = row.mark {
|
||||
let mark = Rect::from_center_size(pos2(r.min.x + 29.0, r.center().y), vec2(20.0, 20.0));
|
||||
ui.painter().rect_filled(mark, CornerRadius::same(4), t.field);
|
||||
icons::paint(ui.painter(), mark.shrink(2.0), icon, tint);
|
||||
}
|
||||
let width = (r.width() - 78.0).max(1.0);
|
||||
let name = widgets::elide_font(ui, row.name, width, semibold(13.0));
|
||||
let name_y = if row.detail.is_empty() { r.center().y } else { r.min.y + 23.0 };
|
||||
ui.painter().text(pos2(r.min.x + 58.0, name_y), Align2::LEFT_CENTER, name, semibold(13.0), row.ink);
|
||||
if !row.detail.is_empty() {
|
||||
let detail = widgets::elide_start_font(ui, row.detail, width, regular(row.detail_size));
|
||||
ui.painter().text(pos2(r.min.x + 58.0, r.min.y + 44.0), Align2::LEFT_CENTER, detail, regular(row.detail_size), row.detail_ink);
|
||||
}
|
||||
if !row.detail.is_empty() { resp.on_hover_text(row.detail).clicked() } else { resp.clicked() }
|
||||
}
|
||||
|
||||
/// One search over known files and starter templates. It never sends a query or owner token to a
|
||||
/// server; online files are opened through their existing owner-token command only on click.
|
||||
fn search_results(app: &mut FigApp, ui: &mut Ui, t: &Tokens) {
|
||||
heading(ui, t, tr("Search results"));
|
||||
let query = app.ui.home_view.file_query.trim().to_lowercase();
|
||||
let mut count = 0;
|
||||
let mut switch = None;
|
||||
let open_paths: Vec<&str> = app.session.docs.iter().filter_map(|d| d.path.as_deref()).collect();
|
||||
|
||||
let open: Vec<_> =
|
||||
app.session.docs.iter().enumerate().filter(|(_, d)| file_matches(&d.doc.name, d.path.as_deref().unwrap_or_default(), &query)).collect();
|
||||
let open_hits: Vec<SearchHit> =
|
||||
open.iter().map(|(_, d)| SearchHit { key: format!("open:{}", d.uid), path: d.path.clone(), name: d.doc.name.clone() }).collect();
|
||||
|
||||
let recent: Vec<(String, String)> = app
|
||||
.session
|
||||
.prefs
|
||||
.recent_files
|
||||
.iter()
|
||||
.filter(|p| figcraft_engine::file_access::allowed(p, figcraft_engine::file_access::Access::Read))
|
||||
.filter(|p| !open_paths.contains(&p.as_str()))
|
||||
.filter_map(|path| {
|
||||
let p = Path::new(path);
|
||||
let name = p.file_stem().or_else(|| p.file_name()).map_or_else(|| path.clone(), |n| n.to_string_lossy().into_owned());
|
||||
file_matches(&name, path, &query).then(|| (path.clone(), name))
|
||||
})
|
||||
.collect();
|
||||
let recent_hits: Vec<SearchHit> =
|
||||
recent.iter().map(|(path, name)| SearchHit { key: format!("recent:{path}"), path: Some(path.clone()), name: name.clone() }).collect();
|
||||
let preview_paths: Vec<String> = open_hits.iter().chain(&recent_hits).filter_map(|hit| hit.path.clone()).collect();
|
||||
let reduce_motion = app.reduce_motion();
|
||||
app.ui.home_view.recents.prepare_search(ui.ctx(), &preview_paths, ui.input(|i| i.time));
|
||||
if !open_hits.is_empty() {
|
||||
subheading(ui, t, tr("Open files"));
|
||||
count += open_hits.len();
|
||||
if let Some(hit) = crate::home_recents::search_grid(ui, t, &mut app.ui.home_view.recents, &open_hits, reduce_motion) {
|
||||
switch = open.get(hit).map(|(index, _)| *index);
|
||||
}
|
||||
}
|
||||
let mut open_recent = None;
|
||||
if !recent_hits.is_empty() {
|
||||
subheading(ui, t, tr("Recent files"));
|
||||
count += recent_hits.len();
|
||||
if let Some(hit) = crate::home_recents::search_grid(ui, t, &mut app.ui.home_view.recents, &recent_hits, reduce_motion) {
|
||||
open_recent = recent_hits.get(hit).and_then(|hit| hit.path.clone());
|
||||
}
|
||||
}
|
||||
let mut count = open_files(app, ui, t, &query);
|
||||
count += crate::home_recents::section(app, ui, t, &query);
|
||||
|
||||
let online: Vec<_> = app.session.prefs.online_files.iter().filter(|f| file_matches(&f.name, &f.server, &query)).cloned().collect();
|
||||
let mut open_online = None;
|
||||
@@ -348,10 +262,9 @@ fn search_results(app: &mut FigApp, ui: &mut Ui, t: &Tokens) {
|
||||
detail_size: 11.0,
|
||||
detail_ink: t.text_secondary,
|
||||
};
|
||||
if search_file_card(ui, t, ui.id().with(("search_online", &f.server, &f.file)), &row) {
|
||||
if listed(ui, t, ui.id().with(("search_online", &f.server, &f.file)), 36.0, &row) {
|
||||
open_online = Some(f.clone());
|
||||
}
|
||||
ui.add_space(8.0);
|
||||
}
|
||||
}
|
||||
let templates: Vec<&Template> = TEMPLATES.iter().filter(|template| matches(template, &query, None)).collect();
|
||||
@@ -369,14 +282,7 @@ fn search_results(app: &mut FigApp, ui: &mut Ui, t: &Tokens) {
|
||||
app.ui.home_view.section = Section::Templates;
|
||||
}
|
||||
}
|
||||
if let Some(index) = switch {
|
||||
app.run("doc.switch", json!({"index": index}));
|
||||
app.ui.home = false;
|
||||
} else if let Some(path) = open_recent {
|
||||
if app.run_user("file.open", json!({"path": path})).is_some() {
|
||||
app.ui.home = false;
|
||||
}
|
||||
} else if let Some(file) = open_online {
|
||||
if let Some(file) = open_online {
|
||||
crate::online::open(app, &file);
|
||||
}
|
||||
}
|
||||
@@ -455,22 +361,7 @@ fn recents(app: &mut FigApp, ui: &mut Ui, t: &Tokens) {
|
||||
ui.add_space(28.0);
|
||||
getting_started(app, ui, t);
|
||||
}
|
||||
if !app.session.docs.is_empty() {
|
||||
subheading(ui, t, tr("Open files"));
|
||||
let mut switch = None;
|
||||
for (i, d) in app.session.docs.iter().enumerate() {
|
||||
let detail = d.path.as_deref().unwrap_or_default();
|
||||
let row =
|
||||
FileRow { mark: Some((Icon::Logo, ACCENT)), name: &d.doc.name, ink: t.text, detail, detail_size: 11.0, detail_ink: t.text_secondary };
|
||||
if listed(ui, t, ui.id().with(("open", d.uid)), 36.0, &row) {
|
||||
switch = Some(i);
|
||||
}
|
||||
}
|
||||
if let Some(i) = switch {
|
||||
app.run("doc.switch", json!({"index": i}));
|
||||
app.ui.home = false;
|
||||
}
|
||||
}
|
||||
open_files(app, ui, t, "");
|
||||
if crate::home_recents::listed(&app.session.prefs.recent_files).next().is_none() {
|
||||
// A first run: nothing to reopen yet, so offer something to start from.
|
||||
subheading(ui, t, tr("Start from a template"));
|
||||
@@ -482,7 +373,32 @@ fn recents(app: &mut FigApp, ui: &mut Ui, t: &Tokens) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
crate::home_recents::section(app, ui, t);
|
||||
crate::home_recents::section(app, ui, t, "");
|
||||
}
|
||||
|
||||
/// The Open files list: the documents whose name or path has `query` (lower case; all for ""), a
|
||||
/// row each; choosing one shows its tab. How many are listed.
|
||||
fn open_files(app: &mut FigApp, ui: &mut Ui, t: &Tokens, query: &str) -> usize {
|
||||
let hit = |d: &&figcraft_engine::DocState| file_matches(&d.doc.name, d.path.as_deref().unwrap_or_default(), query);
|
||||
let count = app.session.docs.iter().filter(hit).count();
|
||||
if count == 0 {
|
||||
return 0;
|
||||
}
|
||||
subheading(ui, t, tr("Open files"));
|
||||
let mut switch = None;
|
||||
for (i, d) in app.session.docs.iter().enumerate().filter(|(_, d)| hit(d)) {
|
||||
let detail = d.path.as_deref().unwrap_or_default();
|
||||
let row =
|
||||
FileRow { mark: Some((Icon::Logo, ACCENT)), name: &d.doc.name, ink: t.text, detail, detail_size: 11.0, detail_ink: t.text_secondary };
|
||||
if listed(ui, t, ui.id().with(("open", d.uid)), 36.0, &row) {
|
||||
switch = Some(i);
|
||||
}
|
||||
}
|
||||
if let Some(i) = switch {
|
||||
app.run("doc.switch", json!({"index": i}));
|
||||
app.ui.home = false;
|
||||
}
|
||||
count
|
||||
}
|
||||
|
||||
/// The file joined from the home screen (a code, or one of your online files) opened in a tab of
|
||||
|
||||
@@ -504,12 +504,6 @@ impl Recents {
|
||||
paths.iter().map(|p| Shown::new(p, self.files.get(p).and_then(|k| k.stat))).collect()
|
||||
}
|
||||
|
||||
/// Share the Recent files thumbnail cache with Home search. The existing loader keeps file
|
||||
/// reads off the UI thread and only requests pictures for cards that are on screen.
|
||||
pub(crate) fn prepare_search(&mut self, ctx: &egui::Context, paths: &[String], time: f64) {
|
||||
self.refresh(ctx, paths, time);
|
||||
}
|
||||
|
||||
/// For `ui.inspect`: each file listed, whether it is there and whether its picture is in.
|
||||
pub fn inspect(&self, prefs: &Prefs) -> Value {
|
||||
let paths: Vec<String> = listed(&prefs.recent_files).take(SHOWN).cloned().collect();
|
||||
@@ -560,16 +554,27 @@ enum Action {
|
||||
View(View),
|
||||
}
|
||||
|
||||
/// The Recent files heading, its toolbar and the files as cards or a table.
|
||||
pub fn section(app: &mut FigApp, ui: &mut Ui, t: &Tokens) {
|
||||
let paths: Vec<String> = listed(&app.session.prefs.recent_files).take(SHOWN).cloned().collect();
|
||||
/// The Recent files heading, its toolbar and the files as cards or a table. With a `query` (lower
|
||||
/// case, from the Home search) only the files not open whose path has it, from every recent file;
|
||||
/// nothing at all when none has it. How many files are shown.
|
||||
pub fn section(app: &mut FigApp, ui: &mut Ui, t: &Tokens, query: &str) -> usize {
|
||||
let searching = !query.is_empty();
|
||||
// A search reaches every recent file; the files shown without one stay known through it.
|
||||
let paths: Vec<String> = listed(&app.session.prefs.recent_files).take(if searching { usize::MAX } else { SHOWN }).cloned().collect();
|
||||
let time = ui.input(|i| i.time);
|
||||
let now = figcraft_engine::cmd::comment::now_secs();
|
||||
let reduce_motion = app.reduce_motion();
|
||||
let (view, sort) = (View::of(&app.session.prefs), Sort::of(&app.session.prefs));
|
||||
let state = &mut app.ui.home_view.recents;
|
||||
state.refresh(ui.ctx(), &paths, time);
|
||||
let files = sorted(state.shown(&paths), sort);
|
||||
let mut files = sorted(state.shown(&paths), sort);
|
||||
if searching {
|
||||
let docs = &app.session.docs;
|
||||
files.retain(|f| f.path.to_lowercase().contains(query) && !docs.iter().any(|d| d.path.as_deref() == Some(f.path.as_str())));
|
||||
if files.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
let with_created = files.iter().any(|f| f.stat.is_some_and(|s| s.created.is_some()));
|
||||
let mut action = toolbar(ui, t, view, sort, with_created);
|
||||
ui.add_space(16.0);
|
||||
@@ -599,6 +604,7 @@ pub fn section(app: &mut FigApp, ui: &mut Ui, t: &Tokens) {
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
files.len()
|
||||
}
|
||||
|
||||
/// "Recent files", and at the right the sort menu and the grid and list buttons.
|
||||
@@ -730,8 +736,7 @@ fn arrows(ui: &Ui, recents: &mut Recents, files: &[Shown], across: usize, down:
|
||||
/// The square file-type mark before a name.
|
||||
fn file_icon(ui: &Ui, t: &Tokens, center: egui::Pos2, path: &str) {
|
||||
let r = Rect::from_center_size(center, vec2(16.0, 16.0));
|
||||
let kind = if path.is_empty() { FileKind::FigCraft } else { FileKind::of(Path::new(path)) };
|
||||
match kind {
|
||||
match FileKind::of(Path::new(path)) {
|
||||
FileKind::FigCraft | FileKind::Fig => {
|
||||
ui.painter().rect_filled(r, CornerRadius::same(4), ACCENT);
|
||||
icons::paint(ui.painter(), r.shrink(3.0), Icon::Logo, Color32::WHITE);
|
||||
@@ -748,49 +753,6 @@ fn file_icon(ui: &Ui, t: &Tokens, center: egui::Pos2, path: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
/// A file found by the Home sidebar search. A tab without a disk path has no stored preview yet.
|
||||
pub(crate) struct SearchHit {
|
||||
pub key: String,
|
||||
pub path: Option<String>,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
/// Search results use the same preview, file-type icon and footer as Recent files. A single click
|
||||
/// still opens the result, as it did before the visual change.
|
||||
pub(crate) fn search_grid(ui: &mut Ui, t: &Tokens, recents: &mut Recents, files: &[SearchHit], reduce_motion: bool) -> Option<usize> {
|
||||
let (columns, card_w) = grid_columns(ui.available_width());
|
||||
let (picture_h, card_h) = card_heights(card_w);
|
||||
let (time, now) = (ui.input(|i| i.time), figcraft_engine::cmd::comment::now_secs());
|
||||
let mut chosen = None;
|
||||
let mut fading = false;
|
||||
for (row_index, row) in files.chunks(columns).enumerate() {
|
||||
let (r, _) = ui.allocate_exact_size(vec2(ui.available_width(), card_h), Sense::hover());
|
||||
for (column, file) in row.iter().enumerate() {
|
||||
let rect = Rect::from_min_size(pos2(r.min.x + column as f32 * (card_w + GAP), r.min.y), vec2(card_w, card_h));
|
||||
let resp = ui.interact(rect, ui.id().with(("search_file_card", &file.key)), Sense::click());
|
||||
let stat = file.path.as_deref().and_then(|path| recents.files.get(path).and_then(|k| k.stat));
|
||||
let shown = Shown { path: file.path.clone().unwrap_or_default(), name: file.name.clone(), stat };
|
||||
a11y::named_button(&resp, &spoken(&shown, now));
|
||||
let resp = if let Some(path) = file.path.as_deref() { resp.on_hover_text(path) } else { resp };
|
||||
let visible = ui.is_rect_visible(rect);
|
||||
let picture = file.path.as_deref().and_then(|path| recents.picture(path, visible, time));
|
||||
if visible {
|
||||
let alpha = picture.as_ref().map_or(1.0, |(_, _, a)| if reduce_motion { 1.0 } else { *a });
|
||||
fading |= alpha < 1.0;
|
||||
card(ui, t, rect, picture_h, &resp, false, &shown, stat.unwrap_or_default(), picture, alpha, now);
|
||||
}
|
||||
if resp.clicked() {
|
||||
chosen = Some(row_index * columns + column);
|
||||
}
|
||||
}
|
||||
ui.add_space(GAP);
|
||||
}
|
||||
if fading {
|
||||
ui.ctx().request_repaint();
|
||||
}
|
||||
chosen
|
||||
}
|
||||
|
||||
/// The name a card or row has for screen readers: "name, Edited 3 hours ago".
|
||||
fn spoken(file: &Shown, now: u64) -> String {
|
||||
let detail = file.detail(now);
|
||||
|
||||
@@ -202,7 +202,7 @@ fn home_search_finds_known_files_and_opens_an_existing_tab() {
|
||||
let mut u = Ui::new(session);
|
||||
u.app.ui.home = true;
|
||||
u.app.ui.home_view.file_query = " TRAIL ".into();
|
||||
let s = shapes(&mut u);
|
||||
let s = settle(&mut u);
|
||||
assert!(has_text(&s, "Search results"));
|
||||
assert!(has_text(&s, "North Trail") && has_text(&s, "South Trail") && has_text(&s, "Trail Live"));
|
||||
assert!(!has_text(&s, "Other") && !has_text(&s, "test-only-secret"));
|
||||
@@ -258,12 +258,62 @@ fn home_search_field_filters_as_typed_and_a_section_click_clears_it() {
|
||||
let s = shapes(&mut u);
|
||||
click(&mut u, text_center(&s, "Search files and templates"));
|
||||
u.frame(vec![Event::Text("glacier".into())]);
|
||||
let s = shapes(&mut u);
|
||||
let s = settle(&mut u);
|
||||
assert_eq!(u.app.ui.home_view.file_query, "glacier");
|
||||
assert!(has_text(&s, "Glacier") && !has_text(&s, "Forest"));
|
||||
click(&mut u, text_center(&s, "Recents"));
|
||||
assert!(u.app.ui.home_view.file_query.is_empty());
|
||||
assert!(has_text(&shapes(&mut u), "Forest"));
|
||||
let s = shapes(&mut u);
|
||||
assert!(has_text(&s, "Forest"));
|
||||
// Escape in the field clears the search too.
|
||||
click(&mut u, text_center(&s, "Search files and templates"));
|
||||
u.frame(vec![Event::Text("forest".into())]);
|
||||
assert_eq!(u.app.ui.home_view.file_query, "forest");
|
||||
u.frame(vec![Event::Key { key: Key::Escape, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::NONE }]);
|
||||
assert!(u.app.ui.home_view.file_query.is_empty(), "Escape clears the search");
|
||||
}
|
||||
|
||||
/// A search keeps what the recent files it doesn't match know: back from it, their pictures and
|
||||
/// the selection are still there (nothing is read again).
|
||||
#[test]
|
||||
fn a_search_keeps_the_other_recent_files_pictures_and_the_selection() {
|
||||
let dir = std::env::temp_dir().join(format!("figcraft-home-search-cache-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let mut png = Vec::new();
|
||||
image::RgbaImage::from_pixel(8, 8, image::Rgba([30, 140, 210, 255]))
|
||||
.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png)
|
||||
.unwrap();
|
||||
let paths: Vec<String> = ["Alpine", "Basin"]
|
||||
.into_iter()
|
||||
.map(|name| {
|
||||
let path = dir.join(format!("{name}.figcraft"));
|
||||
std::fs::write(&path, figcraft_format::save_bytes(&figcraft_doc::Document::new(name), Some(&png)).unwrap()).unwrap();
|
||||
path.to_string_lossy().into_owned()
|
||||
})
|
||||
.collect();
|
||||
let mut session = Session::empty();
|
||||
session.prefs.recent_files = paths.clone();
|
||||
let mut u = Ui::new(session);
|
||||
let pictures = |u: &mut Ui| -> Vec<serde_json::Value> {
|
||||
u.app.inspect()["homeRecents"]["files"].as_array().unwrap().iter().map(|f| f["picture"].clone()).collect()
|
||||
};
|
||||
for _ in 0..400 {
|
||||
u.frame(vec![]);
|
||||
if pictures(&mut u).iter().all(|p| p == "picture") {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
assert!(pictures(&mut u).iter().all(|p| p == "picture"), "both pictures are in");
|
||||
u.app.ui.home_view.recents.selected = Some(paths[1].clone());
|
||||
u.app.ui.home_view.file_query = "alpine".into();
|
||||
let s = shapes(&mut u);
|
||||
assert!(has_text(&s, "Alpine") && !has_text(&s, "Basin"));
|
||||
u.app.ui.home_view.file_query.clear();
|
||||
u.frame(vec![]);
|
||||
assert!(pictures(&mut u).iter().all(|p| p == "picture"), "no picture is read again");
|
||||
assert_eq!(u.app.ui.home_view.recents.selected.as_deref(), Some(paths[1].as_str()), "the selection stays");
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user