mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-31 22:06:49 -06:00
Merge fix/765 into batch 17
This commit is contained in:
@@ -834,6 +834,7 @@ name = "English"
|
||||
"No contributor data was built into this copy." = "No contributor data was built into this copy."
|
||||
"No device" = "No device"
|
||||
"No fonts match" = "No fonts match"
|
||||
"No files or templates match your search" = "No files or templates match your search"
|
||||
"No join code here (the server is older than join codes, or this address can't be put in one): share the link instead." = "No join code here (the server is older than join codes, or this address can't be put in one): share the link instead."
|
||||
"No layer changes to show." = "No layer changes to show."
|
||||
"No libraries yet. Add a .figcraft file that was published as a library, or paste a team library's link." = "No libraries yet. Add a .figcraft file that was published as a library, or paste a team library's link."
|
||||
@@ -1179,6 +1180,8 @@ name = "English"
|
||||
"Search actions, layers and tools" = "Search actions, layers and tools"
|
||||
"Search assets" = "Search assets"
|
||||
"Search comments" = "Search comments"
|
||||
"Search files and templates" = "Search files and templates"
|
||||
"Search results" = "Search results"
|
||||
"Search fonts" = "Search fonts"
|
||||
"Search layers" = "Search layers"
|
||||
"Search shortcuts" = "Search shortcuts"
|
||||
@@ -1408,6 +1411,7 @@ name = "English"
|
||||
"Truncate with an ellipsis" = "Truncate with an ellipsis"
|
||||
"Try again" = "Try again"
|
||||
"Try another search or filter." = "Try another search or filter."
|
||||
"Try another search, or browse templates." = "Try another search, or browse templates."
|
||||
"Try another word, or look at every kind." = "Try another word, or look at every kind."
|
||||
"Turn a layer into a component, reuse it, and change every copy at once" = "Turn a layer into a component, reuse it, and change every copy at once"
|
||||
"Two digits" = "Two digits"
|
||||
|
||||
@@ -835,6 +835,7 @@ status = "draft — needs native review"
|
||||
"No contributor data was built into this copy." = "Esta copia no incluye datos de colaboradores."
|
||||
"No device" = "Sin dispositivo"
|
||||
"No fonts match" = "Ninguna fuente coincide"
|
||||
"No files or templates match your search" = "Ningún archivo ni plantilla coincide con tu búsqueda"
|
||||
"No join code here (the server is older than join codes, or this address can't be put in one): share the link instead." = "Aquí no hay código para unirse (el servidor es anterior a los códigos, o esta dirección no cabe en uno): comparte el enlace."
|
||||
"No layer changes to show." = "No hay cambios de capas que mostrar."
|
||||
"No libraries yet. Add a .figcraft file that was published as a library, or paste a team library's link." = "Aún no hay bibliotecas. Añade un archivo .figcraft publicado como biblioteca o pega el enlace de una biblioteca de equipo."
|
||||
@@ -1180,6 +1181,8 @@ status = "draft — needs native review"
|
||||
"Search actions, layers and tools" = "Buscar acciones, capas y herramientas"
|
||||
"Search assets" = "Buscar recursos"
|
||||
"Search comments" = "Buscar comentarios"
|
||||
"Search files and templates" = "Buscar archivos y plantillas"
|
||||
"Search results" = "Resultados de búsqueda"
|
||||
"Search fonts" = "Buscar fuentes"
|
||||
"Search layers" = "Buscar capas"
|
||||
"Search shortcuts" = "Buscar atajos"
|
||||
@@ -1409,6 +1412,7 @@ status = "draft — needs native review"
|
||||
"Truncate with an ellipsis" = "Truncar con puntos suspensivos"
|
||||
"Try again" = "Reintentar"
|
||||
"Try another search or filter." = "Prueba otra búsqueda u otro filtro."
|
||||
"Try another search, or browse templates." = "Prueba otra búsqueda o explora las plantillas."
|
||||
"Try another word, or look at every kind." = "Prueba con otra palabra o mira todos los tipos."
|
||||
"Turn a layer into a component, reuse it, and change every copy at once" = "Convierte una capa en componente, reutilízalo y cambia todas las copias a la vez"
|
||||
"Two digits" = "Dos dígitos"
|
||||
|
||||
@@ -819,6 +819,7 @@ name = "Русский"
|
||||
"No contributor data was built into this copy." = "В этой сборке нет данных об участниках."
|
||||
"No device" = "Без устройства"
|
||||
"No fonts match" = "Подходящих шрифтов нет"
|
||||
"No files or templates match your search" = "Файлы и шаблоны по запросу не найдены"
|
||||
"No join code here (the server is older than join codes, or this address can't be put in one): share the link instead." = "Код подключения недоступен (сервер не поддерживает коды или адрес нельзя включить в код): поделитесь ссылкой."
|
||||
"No layer changes to show." = "Нет изменений слоёв для показа."
|
||||
"No libraries yet. Add a .figcraft file that was published as a library, or paste a team library's link." = "Библиотек пока нет. Добавьте файл .figcraft, опубликованный как библиотека, или вставьте ссылку на командную библиотеку."
|
||||
@@ -1157,6 +1158,8 @@ name = "Русский"
|
||||
"Search actions, layers and tools" = "Поиск действий, слоёв и инструментов"
|
||||
"Search assets" = "Поиск ресурсов"
|
||||
"Search comments" = "Поиск комментариев"
|
||||
"Search files and templates" = "Поиск файлов и шаблонов"
|
||||
"Search results" = "Результаты поиска"
|
||||
"Search fonts" = "Поиск шрифтов"
|
||||
"Search layers" = "Поиск слоёв"
|
||||
"Search shortcuts" = "Поиск сочетаний клавиш"
|
||||
@@ -1382,6 +1385,7 @@ name = "Русский"
|
||||
"Truncate with an ellipsis" = "Обрезать с многоточием"
|
||||
"Try again" = "Повторить"
|
||||
"Try another search or filter." = "Попробуйте другой запрос или фильтр."
|
||||
"Try another search, or browse templates." = "Попробуйте другой запрос или просмотрите шаблоны."
|
||||
"Try another word, or look at every kind." = "Попробуйте другое слово или посмотрите все категории."
|
||||
"Turn a layer into a component, reuse it, and change every copy at once" = "Превратите слой в компонент, используйте повторно и изменяйте все копии сразу"
|
||||
"Two digits" = "Две цифры"
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
//! for the frames that follow (all false: released); `gamepad` presses a controller button in the
|
||||
//! prototype playing (presenter or preview pane)
|
||||
//! - `ui.menu.invoke {id}` — a menu item exactly like clicking it (may open a dialog)
|
||||
//! - `ui.set {rightTab?, leftTab?, theme?, home?, homeSection?, homeRecentsView?, homeRecentsSort?, homeRecentsOrder?, palette?, focusRegion?, preview?, presentScroll?, …}`
|
||||
//! - `ui.set {rightTab?, leftTab?, theme?, home?, homeSection?, homeSearch?, homeRecentsView?, homeRecentsSort?, homeRecentsOrder?, palette?, focusRegion?, preview?, presentScroll?, …}`
|
||||
//! - `ui.screenshot {path?}` — PNG of the window (needs a visible window), written to `path` or
|
||||
//! returned as base64 `data`
|
||||
//! - `ui.render {path?, scale?, ids?}` — PNG of the document (no UI chrome; works without a
|
||||
@@ -429,6 +429,9 @@ pub fn handle(app: &mut FigApp, ctx: &Context, method: &str, p: &Value) -> Resul
|
||||
if let Some(section) = p.get("homeSection").and_then(Value::as_str).and_then(crate::home::Section::parse) {
|
||||
app.ui.home_view.section = section;
|
||||
}
|
||||
if let Some(query) = p.get("homeSearch").and_then(Value::as_str) {
|
||||
app.ui.home_view.file_query = query.chars().take(256).collect();
|
||||
}
|
||||
if let Some(view) = p.get("homeRecentsView").and_then(Value::as_str).and_then(crate::home_recents::View::parse) {
|
||||
view.store(&mut app.session.prefs);
|
||||
}
|
||||
|
||||
+103
-22
@@ -51,6 +51,8 @@ impl Section {
|
||||
#[derive(Default)]
|
||||
pub struct Home {
|
||||
pub section: Section,
|
||||
/// Searches known files and starter templates together from the home sidebar.
|
||||
pub file_query: String,
|
||||
/// The Templates search text and kind filter.
|
||||
pub query: String,
|
||||
pub kind: Option<Kind>,
|
||||
@@ -143,7 +145,7 @@ pub fn show(app: &mut FigApp, ui: &mut Ui) {
|
||||
let left = Rect::from_min_size(rect.min, vec2(SIDEBAR, rect.height()));
|
||||
ui.painter().line_segment([left.right_top(), left.right_bottom()], Stroke::new(1.0, t.border));
|
||||
sidebar(app, ui, &t, left);
|
||||
if app.ui.home_view.section == Section::Online {
|
||||
if app.ui.home_view.file_query.trim().is_empty() && app.ui.home_view.section == Section::Online {
|
||||
// Your online files lay themselves out in the area (and scroll their own list).
|
||||
let area = Rect::from_min_max(pos2(left.max.x + 40.0, rect.min.y + 24.0), rect.max - vec2(40.0, 24.0));
|
||||
crate::online::section(app, ui, area, &t);
|
||||
@@ -155,9 +157,14 @@ pub fn show(app: &mut FigApp, ui: &mut Ui) {
|
||||
egui::ScrollArea::vertical().id_salt("home").auto_shrink([false, false]).show(ui, |ui| {
|
||||
egui::Frame::NONE.inner_margin(egui::Margin::symmetric(40, 24)).show(ui, |ui| {
|
||||
ui.spacing_mut().item_spacing = vec2(GAP, 0.0);
|
||||
match app.ui.home_view.section {
|
||||
Section::Templates => templates(app, ui, &t),
|
||||
_ => recents(app, ui, &t),
|
||||
if app.ui.home_view.file_query.trim().is_empty() {
|
||||
match app.ui.home_view.section {
|
||||
Section::Templates => templates(app, ui, &t),
|
||||
_ => recents(app, ui, &t),
|
||||
}
|
||||
} else {
|
||||
search_results(app, ui, &t);
|
||||
opened_join(app);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -198,10 +205,20 @@ 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, widgets::FIELD_H));
|
||||
ui.scope_builder(egui::UiBuilder::new().max_rect(search), |ui| {
|
||||
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() || response.lost_focus()) && ui.input(|i| i.key_pressed(egui::Key::Escape)) {
|
||||
app.ui.home_view.file_query.clear();
|
||||
}
|
||||
});
|
||||
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());
|
||||
let current = app.ui.home_view.section == section;
|
||||
let current = app.ui.home_view.file_query.trim().is_empty() && app.ui.home_view.section == section;
|
||||
a11y::tab(&resp, tr(label), current);
|
||||
if current || resp.hovered() {
|
||||
ui.painter().rect_filled(r, CornerRadius::same(5), if current { t.selected_row } else { t.hover });
|
||||
@@ -211,11 +228,65 @@ fn sidebar(app: &mut FigApp, ui: &mut Ui, t: &Tokens, left: Rect) {
|
||||
ui.painter().text(pos2(r.min.x + 36.0, r.center().y), Align2::LEFT_CENTER, tr(label), regular(12.0), t.text);
|
||||
if resp.clicked() {
|
||||
app.ui.home_view.section = section;
|
||||
app.ui.home_view.file_query.clear();
|
||||
}
|
||||
y += 34.0;
|
||||
}
|
||||
}
|
||||
|
||||
fn file_matches(name: &str, location: &str, query: &str) -> bool {
|
||||
name.to_lowercase().contains(query) || location.to_lowercase().contains(query)
|
||||
}
|
||||
|
||||
/// 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 = 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;
|
||||
if !online.is_empty() {
|
||||
subheading(ui, t, tr("Online files"));
|
||||
for f in &online {
|
||||
count += 1;
|
||||
let host = figcraft_engine::online::server_host(&f.server);
|
||||
let icon = crate::online::mark(f.shared.is_some());
|
||||
let row = FileRow {
|
||||
mark: Some((icon, if f.shared.is_some() { t.success } else { t.icon_secondary })),
|
||||
name: &f.name,
|
||||
ink: t.text,
|
||||
detail: host,
|
||||
detail_size: 11.0,
|
||||
detail_ink: t.text_secondary,
|
||||
};
|
||||
if listed(ui, t, ui.id().with(("search_online", &f.server, &f.file)), 36.0, &row) {
|
||||
open_online = Some(f.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
let templates: Vec<&Template> = TEMPLATES.iter().filter(|template| matches(template, &query, None)).collect();
|
||||
if !templates.is_empty() {
|
||||
subheading(ui, t, tr("Templates"));
|
||||
template_grid(app, ui, t, &templates);
|
||||
}
|
||||
if count == 0 && templates.is_empty() {
|
||||
ui.label(egui::RichText::new(tr("No files or templates match your search")).font(semibold(13.0)).color(t.text));
|
||||
ui.add_space(6.0);
|
||||
widgets::label(ui, tr("Try another search, or browse templates."), t.text_secondary);
|
||||
ui.add_space(16.0);
|
||||
if widgets::button(ui, tr("Show all templates"), false, true).clicked() {
|
||||
app.ui.home_view.file_query.clear();
|
||||
app.ui.home_view.section = Section::Templates;
|
||||
}
|
||||
}
|
||||
if let Some(file) = open_online {
|
||||
crate::online::open(app, &file);
|
||||
}
|
||||
}
|
||||
|
||||
/// A ring around a card or row that has the keyboard.
|
||||
fn focus_ring(ui: &Ui, t: &Tokens, rect: Rect, resp: &Response, radius: u8) {
|
||||
if resp.has_focus() {
|
||||
@@ -290,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"));
|
||||
@@ -317,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
|
||||
|
||||
@@ -554,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);
|
||||
@@ -593,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.
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
use egui::accesskit::Role;
|
||||
use egui::{Event, Key, Modifiers, PointerButton, Pos2, Rect, Shape};
|
||||
use figcraft_engine::Session;
|
||||
use figcraft_engine::online::OnlineFile;
|
||||
use figcraft_engine::templates::TEMPLATES;
|
||||
use serde_json::json;
|
||||
|
||||
@@ -184,6 +185,161 @@ fn typing_in_the_search_field_filters() {
|
||||
assert!(u.app.session.docs.is_empty(), "typing opens nothing");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn home_search_finds_known_files_and_opens_an_existing_tab() {
|
||||
let mut session = Session::new();
|
||||
session.execute("file.rename", &json!({"name": "North Trail"})).unwrap();
|
||||
session.docs[0].path = Some("C:/designs/North Trail.figcraft".into());
|
||||
session.prefs.recent_files =
|
||||
vec!["C:/designs/North Trail.figcraft".into(), "C:/designs/South Trail.figcraft".into(), "C:/designs/Other.figcraft".into()];
|
||||
session.prefs.online_files.push(OnlineFile {
|
||||
server: "wss://share.example/".into(),
|
||||
file: "trail".into(),
|
||||
name: "Trail Live".into(),
|
||||
owner: "test-only-secret".into(),
|
||||
..OnlineFile::default()
|
||||
});
|
||||
let mut u = Ui::new(session);
|
||||
u.app.ui.home = true;
|
||||
u.app.ui.home_view.file_query = " TRAIL ".into();
|
||||
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"));
|
||||
assert_eq!(texts(&s).iter().filter(|(name, pos, _)| name == "North Trail" && pos.y > 90.0).count(), 1, "an open file is not repeated in recents");
|
||||
assert_eq!(u.app.inspect()["homeSearch"], " TRAIL ");
|
||||
|
||||
u.app.ui.home_view.file_query = "nothing matches".into();
|
||||
let s = shapes(&mut u);
|
||||
assert!(has_text(&s, "No files or templates match your search"));
|
||||
assert!(!has_text(&s, "Trail Live") && !has_text(&s, "South Trail"));
|
||||
assert!(!texts(&s).iter().any(|(name, pos, _)| name == "North Trail" && pos.y > 90.0));
|
||||
|
||||
u.app.ui.home_view.file_query = "north".into();
|
||||
let s = shapes(&mut u);
|
||||
click(&mut u, text_center(&s, "North Trail"));
|
||||
assert!(!u.app.ui.home, "choosing an open result shows its editor tab");
|
||||
assert_eq!(u.app.session.docs.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn home_search_shows_a_saved_files_preview() {
|
||||
let dir = std::env::temp_dir().join(format!("figcraft-home-search-preview-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let path = dir.join("Search preview.figcraft");
|
||||
let image = image::RgbaImage::from_pixel(64, 32, image::Rgba([30, 140, 210, 255]));
|
||||
let mut png = Vec::new();
|
||||
image.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).unwrap();
|
||||
let doc = figcraft_doc::Document::new("Search preview");
|
||||
std::fs::write(&path, figcraft_format::save_bytes(&doc, Some(&png)).unwrap()).unwrap();
|
||||
|
||||
let mut session = Session::empty();
|
||||
session.prefs.recent_files.push(path.to_string_lossy().into_owned());
|
||||
let mut u = Ui::new(session);
|
||||
u.app.ui.home_view.file_query = "Search preview".into();
|
||||
for _ in 0..400 {
|
||||
u.frame(vec![]);
|
||||
if u.app.inspect()["homeRecents"]["files"][0]["picture"] == "picture" {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
let s = shapes(&mut u);
|
||||
assert!(has_text(&s, "Search preview"));
|
||||
assert!(s.iter().any(|s| matches!(s, Shape::Mesh(m) if m.texture_id != egui::TextureId::default())), "the saved preview is drawn");
|
||||
let _ = std::fs::remove_file(path);
|
||||
let _ = std::fs::remove_dir(dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn home_search_field_filters_as_typed_and_a_section_click_clears_it() {
|
||||
let mut u = first_run();
|
||||
u.app.session.prefs.recent_files = vec!["C:/designs/Glacier.figcraft".into(), "C:/designs/Forest.figcraft".into()];
|
||||
let s = shapes(&mut u);
|
||||
click(&mut u, text_center(&s, "Search files and templates"));
|
||||
u.frame(vec![Event::Text("glacier".into())]);
|
||||
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());
|
||||
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]
|
||||
fn home_search_includes_templates_and_opens_a_matching_card() {
|
||||
let mut u = first_run();
|
||||
u.app.ui.home_view.file_query = "MOBILE".into();
|
||||
let s = settle(&mut u);
|
||||
assert!(has_text(&s, "Templates") && has_text(&s, "Mobile app"));
|
||||
assert!(!has_text(&s, "Dashboard"));
|
||||
click(&mut u, text_center(&s, "Mobile app"));
|
||||
assert_eq!(u.app.session.doc().unwrap().name, "Mobile app");
|
||||
assert!(!u.app.ui.home);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_home_search_can_browse_all_templates() {
|
||||
let mut u = first_run();
|
||||
u.app.ui.home_view.file_query = "no such design".into();
|
||||
let s = shapes(&mut u);
|
||||
assert!(has_text(&s, "No files or templates match your search"));
|
||||
click(&mut u, text_center(&s, "Show all templates"));
|
||||
assert!(u.app.ui.home_view.file_query.is_empty());
|
||||
assert_eq!(u.app.ui.home_view.section, Section::Templates);
|
||||
assert!(has_text(&settle(&mut u), "Dashboard"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thumbnails_are_cached_per_theme() {
|
||||
let mut u = first_run();
|
||||
|
||||
@@ -1678,6 +1678,7 @@ impl FigApp {
|
||||
"lastError": self.ui.last_error,
|
||||
"importReport": self.ui.import_report.as_ref().map(import_report::Report::inspect),
|
||||
});
|
||||
state["homeSearch"] = json!(self.ui.home_view.file_query);
|
||||
state["pendingOpens"] = json!(self.ui.pending_opens.len());
|
||||
state["missingFonts"] = json!(self.ui.missing_fonts.as_ref().map(missing_fonts::Shown::inspect));
|
||||
state
|
||||
|
||||
@@ -72,7 +72,7 @@ fn manage(app: &mut FigApp, ctx: &egui::Context, f: (&str, &str), action: &str,
|
||||
}
|
||||
|
||||
/// Open recorded file `f` as its owner; its tab shows once the file arrives.
|
||||
fn open(app: &mut FigApp, f: &OnlineFile) {
|
||||
pub(crate) fn open(app: &mut FigApp, f: &OnlineFile) {
|
||||
let Some(r) = app.run("collab.openFile", json!({"server": f.server, "file": f.file})) else { return };
|
||||
if r["alreadyOpen"] == true && !r["document"].is_null() {
|
||||
app.ui.home = false;
|
||||
|
||||
@@ -248,12 +248,13 @@ separate calls. Returns `{queued}`.
|
||||
|
||||
Invoke a menu item exactly like clicking it (this may open a dialog). Returns `{ok, lastError}`.
|
||||
|
||||
### `ui.set {rightTab?, leftTab?, theme?, home?, homeSection?, homeRecentsView?, homeRecentsSort?, homeRecentsOrder?, palette?, mainMenu?, focusRegion?, colorPicker?, variables?, layerFilter?, collapseLayers?, exportPreview?, preview?, presentScroll?, colorProfileDialog?, stylePicker?, stylePickerEdit?, stylePickerView?, iconGallery?, protoEditor?, contextMenu?, contextSelectLayer?, commentOpen?, commentDraft?, commentFilter?, commentSort?, commentSearch?, commentReply?, commentEdit?, presentComments?, devCompare?, devCompareMode?, devCompareSplit?, annotationFilter?, resetFrameStats?, pluginsWindow?, pluginsTab?, pluginConfirm?, pluginForm?}`
|
||||
### `ui.set {rightTab?, leftTab?, theme?, home?, homeSection?, homeSearch?, homeRecentsView?, homeRecentsSort?, homeRecentsOrder?, palette?, mainMenu?, focusRegion?, colorPicker?, variables?, layerFilter?, collapseLayers?, exportPreview?, preview?, presentScroll?, colorProfileDialog?, stylePicker?, stylePickerEdit?, stylePickerView?, iconGallery?, protoEditor?, contextMenu?, contextSelectLayer?, commentOpen?, commentDraft?, commentFilter?, commentSort?, commentSearch?, commentReply?, commentEdit?, presentComments?, devCompare?, devCompareMode?, devCompareSplit?, annotationFilter?, resetFrameStats?, pluginsWindow?, pluginsTab?, pluginConfirm?, pluginForm?}`
|
||||
|
||||
Switch panels and modes: `rightTab` `design|prototype|inspect`, `leftTab` `file|assets`, `theme`,
|
||||
`home` (bool), `homeSection` (`recents`, `online` or `templates`: which section the home screen shows, as `ui.inspect` reports it; opening
|
||||
`online` asks each server once what became of your online files), `homeRecentsView` (`grid|list`), `homeRecentsSort` (`modified|name|created`) and `homeRecentsOrder` (`desc|asc`): how the home screen shows its recent files (kept in the preferences; `ui.inspect` reports them under `homeRecents` with each file and whether its picture is in), `palette` (bool: the Actions palette), `mainMenu` (bool, or a top-level menu such
|
||||
as `"Plugins"` to open the main menu with it open), `pluginsWindow` (bool: Plugins ▸ Manage
|
||||
as `"Plugins"` to open the main menu with it open), `homeSearch` (text, at most 256 characters: filters locally known open, recent and online files plus starter templates in Home; an empty string returns to the selected section),
|
||||
`pluginsWindow` (bool: Plugins ▸ Manage
|
||||
plugins…), `pluginsTab` (`installed|browse`: that window's tab), `pluginConfirm` (a plugin id of the
|
||||
directory listed last: its install confirmation shows, as its Install button would; `null` closes
|
||||
it), `pluginForm` (`{id, command?}`: start that plugin command like its menu item, opening
|
||||
|
||||
Reference in New Issue
Block a user