Merge fix/765 into batch 17

This commit is contained in:
bflatastic
2026-10-18 12:36:19 +07:00
10 changed files with 296 additions and 30 deletions
+4
View File
@@ -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"
+4
View File
@@ -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"
+4
View File
@@ -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" = "Две цифры"
+4 -1
View File
@@ -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
View File
@@ -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
+16 -4
View File
@@ -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.
+156
View File
@@ -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();
+1
View File
@@ -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
+1 -1
View File
@@ -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;
+3 -2
View File
@@ -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