M11: starter templates: Templates tab, first-run templates and Getting started card on the home screen

This commit is contained in:
bflatastic 2026-10-13 01:42:21 +07:00 committed by BFlat
parent c2a67f81e5
commit 573dd521bb
16 changed files with 811 additions and 143 deletions

View File

@ -150,7 +150,10 @@ fn main() -> eframe::Result {
// default, never a build from source unless asked).
session.live.links = figcraft_collab_net::links::system();
session.invite_links_startup();
if session.docs.is_empty() {
// A first run (no settings kept yet, nothing to open) starts on the home screen, with its
// tips and templates; later runs start in a new file.
let first_run = svc.prefs.is_none() && args.join.is_none();
if session.docs.is_empty() && !first_run {
let _ = session.execute("file.new", &json!({}));
}
let mut fig = FigApp::new(session, Box::new(svc));

View File

@ -257,7 +257,6 @@ status = "draft — needs native review"
"Comment" = "Kommentar"
"Comments" = "Kommentare"
"Commits" = "Commits"
"Community" = "Community"
"Company" = "Unternehmen"
"Compare changes" = "Änderungen vergleichen"
"Completed" = "Abgeschlossen"

View File

@ -16,7 +16,9 @@ name = "English"
"1…9, 0" = "1…9, 0"
"@{login}: {prs} PRs, {commits} commits, +{added} / −{deleted} lines (Δ {delta}), +{bin_added} / −{bin_deleted} binary assets, {first} – {last}" = "@{login}: {prs} PRs, {commits} commits, +{added} / −{deleted} lines (Δ {delta}), +{bin_added} / −{bin_deleted} binary assets, {first} – {last}"
"A collection of this file with these modes and variables: override their values per mode and add variables of its own" = "A collection of this file with these modes and variables: override their values per mode and add variables of its own"
"A responsive product page: hero, features, figures, a quote and a footer" = "A responsive product page: hero, features, figures, a quote and a footer"
"A spring plays until it settles: its mass, stiffness and damping set how long" = "A spring plays until it settles: its mass, stiffness and damping set how long"
"A title, an agenda, a big figure, a chart and a closing slide" = "A title, an agenda, a big figure, a chart and a closing slide"
"About" = "About"
"About FigCraft" = "About FigCraft"
"About this server" = "About this server"
@ -88,6 +90,7 @@ name = "English"
"All variables" = "All variables"
"Alpha" = "Alpha"
"Alt+drag" = "Alt+drag"
"An analytics screen on a grid: figures, charts drawn from vectors and a table" = "An analytics screen on a grid: figures, charts drawn from vectors and a table"
"An invite link that opens FigCraft and joins" = "An invite link that opens FigCraft and joins"
"Anchor" = "Anchor"
"Android" = "Android"
@ -106,6 +109,7 @@ name = "English"
"Apply variable" = "Apply variable"
"Apply variable to text" = "Apply variable to text"
"Apply variable to visibility" = "Apply variable to visibility"
"Apps" = "Apps"
"Arc" = "Arc"
"Arc ratio" = "Arc ratio"
"Arc start" = "Arc start"
@ -189,6 +193,7 @@ name = "English"
"Clear from this list" = "Clear from this list"
"Code or invite link" = "Code or invite link"
"Color profile: {name}" = "Color profile: {name}"
"Colour and text styles, Light and Dark variables, buttons, inputs and cards" = "Colour and text styles, Light and Dark variables, buttons, inputs and cards"
"CSS custom properties…" = "CSS custom properties…"
"Can edit" = "Can edit"
"Can't read this library: {e}" = "Can't read this library: {e}"
@ -256,7 +261,6 @@ name = "English"
"Comment" = "Comment"
"Comments" = "Comments"
"Commits" = "Commits"
"Community" = "Community"
"Company" = "Company"
"Compare changes" = "Compare changes"
"Completed" = "Completed"
@ -327,6 +331,7 @@ name = "English"
"Dark" = "Dark"
"Darken" = "Darken"
"Dash" = "Dash"
"Dashboard" = "Dashboard"
"Decoration style" = "Decoration style"
"Decoration" = "Decoration"
"Deep select" = "Deep select"
@ -353,6 +358,7 @@ name = "English"
"Description" = "Description"
"Description (optional)" = "Description (optional)"
"Design" = "Design"
"Design system starter" = "Design system starter"
"Desktop" = "Desktop"
"Destination" = "Destination"
"Detach alias" = "Detach alias"
@ -388,6 +394,7 @@ name = "English"
"Drafts" = "Drafts"
"Drag the stroke to add a width point" = "Drag the stroke to add a width point"
"Draw" = "Draw"
"Draw a frame" = "Draw a frame"
"Drawing tools" = "Drawing tools"
"Drop shadow" = "Drop shadow"
"Dual-licensed MIT OR Apache-2.0. Made by the ArtCraft team and community." = "Dual-licensed MIT OR Apache-2.0. Made by the ArtCraft team and community."
@ -436,6 +443,7 @@ name = "English"
"Expand" = "Expand"
"Export" = "Export"
"Export all" = "Export all"
"Export frames and layers as PNG, JPG, SVG or PDF" = "Export frames and layers as PNG, JPG, SVG or PDF"
"Export page as PNG" = "Export page as PNG"
"Export settings" = "Export settings"
"Export variables" = "Export variables"
@ -504,6 +512,7 @@ name = "English"
"Font size" = "Font size"
"Font style" = "Font style"
"Forget on this computer" = "Forget on this computer"
"Four screens of a trail app with components and a prototype flow" = "Four screens of a trail app with components and a prototype flow"
"Fractions" = "Fractions"
"Frame" = "Frame"
"Frame selection" = "Frame selection"
@ -517,6 +526,7 @@ name = "English"
"Gap between columns" = "Gap between columns"
"Gap between items" = "Gap between items"
"Gap between rows" = "Gap between rows"
"Getting started" = "Getting started"
"Give the address of a server open to the public, or a server's address with its key (`figcraft-server key` prints it)." = "Give the address of a server open to the public, or a server's address with its key (`figcraft-server key` prints it)."
"Glass" = "Glass"
"Go to main component" = "Go to main component"
@ -525,6 +535,7 @@ name = "English"
"Gradient" = "Gradient"
"Gray" = "Gray"
"Green" = "Green"
"Greyscale components and two example screens to sketch a flow with" = "Greyscale components and two example screens to sketch a flow with"
"Grid" = "Grid"
"Grid ({size})" = "Grid ({size})"
"Grid styles" = "Grid styles"
@ -546,6 +557,7 @@ name = "English"
"Hide contents" = "Hide contents"
"Hide from publishing" = "Hide from publishing"
"Hide section contents" = "Hide section contents"
"Hide these tips (Getting started in the Help menu shows them again)" = "Hide these tips (Getting started in the Help menu shows them again)"
"Hide when publishing" = "Hide when publishing"
"Highlight layers on hover" = "Highlight layers on hover"
"Highlights" = "Highlights"
@ -568,6 +580,8 @@ name = "English"
"Hue" = "Hue"
"Hug" = "Hug"
"Hug contents" = "Hug contents"
"Icon components on a 24 px grid with keylines and a size preview" = "Icon components on a 24 px grid with keylines and a size preview"
"Icon set starter" = "Icon set starter"
"If" = "If"
"If {condition}" = "If {condition}"
"Ignore auto layout (absolute position)" = "Ignore auto layout (absolute position)"
@ -631,6 +645,8 @@ name = "English"
"Key/Gamepad" = "Key/Gamepad"
"Key/Gamepad {key}" = "Key/Gamepad {key}"
"Keyboard shortcuts" = "Keyboard shortcuts"
"Kits" = "Kits"
"Landing page" = "Landing page"
"Language" = "Language"
"Lasso" = "Lasso"
"Last" = "Last"
@ -674,6 +690,7 @@ name = "English"
"Link" = "Link"
"Link copied" = "Link copied"
"Link docs, code or tickets for developers." = "Link docs, code or tickets for developers."
"Link frames on the Prototype tab, then present the flow" = "Link frames on the Prototype tab, then present the flow"
"Link this component to its code component: instances then show your real snippet with their values." = "Link this component to its code component: instances then show your real snippet with their values."
"List spacing" = "List spacing"
"List style" = "List style"
@ -691,6 +708,7 @@ name = "English"
"Main" = "Main"
"Main component deleted" = "Main component deleted"
"Main menu" = "Main menu"
"Make a component" = "Make a component"
"Make slot" = "Make slot"
"Margin" = "Margin"
"Mark all as read" = "Mark all as read"
@ -725,6 +743,7 @@ name = "English"
"Mixed" = "Mixed"
"Mixed fonts" = "Mixed fonts"
"Mixed interactions: click + to replace them" = "Mixed interactions: click + to replace them"
"Mobile app" = "Mobile app"
"Mode" = "Mode"
"Model" = "Model"
"Models" = "Models"
@ -796,6 +815,7 @@ name = "English"
"No string variables yet. Create them in Local variables." = "No string variables yet. Create them in Local variables."
"No styles yet." = "No styles yet."
"No styles yet. Click + to create one from the selection." = "No styles yet. Click + to create one from the selection."
"No templates match your search" = "No templates match your search"
"No text styles yet. Click + to create one from the selected text." = "No text styles yet. Click + to create one from the selected text."
"No variables fit here." = "No variables fit here."
"No variables yet. Create them in Local variables." = "No variables yet. Create them in Local variables."
@ -830,6 +850,7 @@ name = "English"
"On click" = "On click"
"On drag" = "On drag"
"On this page" = "On this page"
"One announcement in five common sizes: square, portrait, story, link and banner" = "One announcement in five common sizes: square, portrait, story, link and banner"
"Online" = "Online"
"Online files" = "Online files"
"Online sharing is off: files are shared on the local network only." = "Online sharing is off: files are shared on the local network only."
@ -902,6 +923,7 @@ name = "English"
"Pick color" = "Pick color"
"Pick color (I)" = "Pick color (I)"
"Pick one font family first" = "Pick one font family first"
"Pick the Frame tool, then drag on the canvas or choose a device size" = "Pick the Frame tool, then drag on the canvas or choose a device size"
"Pin a property" = "Pin a property"
"Pinch" = "Pinch"
"Pixel grid" = "Pixel grid"
@ -927,6 +949,7 @@ name = "English"
"Present or preview" = "Present or preview"
"Present this flow" = "Present this flow"
"Presentation" = "Presentation"
"Presentation slides" = "Presentation slides"
"Presenting: comments" = "Presenting: comments"
"Presenting: cycle the size options" = "Presenting: cycle the size options"
"Presenting: full screen" = "Presenting: full screen"
@ -1065,6 +1088,9 @@ name = "English"
"Rulers" = "Rulers"
"Run" = "Run"
"Run {label}" = "Run {label}"
"Search templates" = "Search templates"
"See all templates" = "See all templates"
"Select layers and they space themselves as their content changes" = "Select layers and they space themselves as their content changes"
"SHA-256: {sha}…" = "SHA-256: {sha}…"
"Same as layer" = "Same as layer"
"Saturation" = "Saturation"
@ -1124,6 +1150,7 @@ name = "English"
"Shape tools" = "Shape tools"
"Share" = "Share"
"Share this file" = "Share this file"
"Share with a code" = "Share with a code"
"Share: live session, export" = "Share: live session, export"
"Shared" = "Shared"
"Shared: copy its code from the file's menu" = "Shared: copy its code from the file's menu"
@ -1133,6 +1160,7 @@ name = "English"
"Sharing online · {people}" = "Sharing online · {people}"
"Sharing stopped: the file stays on the server, open only to you" = "Sharing stopped: the file stays on the server, open only to you"
"Show" = "Show"
"Show all templates" = "Show all templates"
"Show as grid" = "Show as grid"
"Show as list" = "Show as list"
"Show autosaves" = "Show autosaves"
@ -1165,7 +1193,9 @@ name = "English"
"Small caps" = "Small caps"
"Smart animate" = "Smart animate"
"Snap to pixel grid" = "Snap to pixel grid"
"Social and slides" = "Social and slides"
"Social media" = "Social media"
"Social post sizes" = "Social post sizes"
"Soft light" = "Soft light"
"Solid" = "Solid"
"Some variants have the same property values: give each one a unique combination" = "Some variants have the same property values: give each one a unique combination"
@ -1190,9 +1220,11 @@ name = "English"
"Start" = "Start"
"Start ascending from / stop descending at" = "Start ascending from / stop descending at"
"Start from a blank canvas" = "Start from a blank canvas"
"Start from a template" = "Start from a template"
"Start on path" = "Start on path"
"Start point" = "Start point"
"Start sharing" = "Start sharing"
"Start sharing with the Share button, then pass on the code it gives" = "Start sharing with the Share button, then pass on the code it gives"
"Starting to share…" = "Starting to share…"
"Status" = "Status"
"Sticky" = "Sticky"
@ -1241,6 +1273,7 @@ name = "English"
"Tell the server's operator what is wrong with it" = "Tell the server's operator what is wrong with it"
"Temperature" = "Temperature"
"Template" = "Template"
"Templates" = "Templates"
"Terms" = "Terms"
"Text" = "Text"
"Text align" = "Text align"
@ -1298,6 +1331,8 @@ 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 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"
"Type" = "Type"
"Type settings" = "Type settings"
@ -1368,6 +1403,7 @@ name = "English"
"Watch" = "Watch"
"Wavy" = "Wavy"
"Web" = "Web"
"Websites" = "Websites"
"WebSocket URL" = "WebSocket URL"
"WebSocket URL copied" = "WebSocket URL copied"
"Wedge" = "Wedge"
@ -1385,6 +1421,7 @@ name = "English"
"Width" = "Width"
"Width point" = "Width point"
"Width profile" = "Width profile"
"Wireframe kit" = "Wireframe kit"
"With the layers' export settings" = "With the layers' export settings"
"Wrap" = "Wrap"
"Wrap in new section" = "Wrap in new section"
@ -1511,4 +1548,4 @@ name = "English"
"⌘ arrows" = "⌘ arrows"
"⌘ click" = "⌘ click"
"⌘ scroll" = "⌘ scroll"
"⌥ drag" = "⌥ drag"
"⌥ drag" = "⌥ drag"

View File

@ -17,7 +17,9 @@ status = "draft — needs native review"
"1…9, 0" = "1…9, 0"
"@{login}: {prs} PRs, {commits} commits, +{added} / −{deleted} lines (Δ {delta}), +{bin_added} / −{bin_deleted} binary assets, {first} – {last}" = "@{login}: {prs} PR, {commits} commits, +{added} / −{deleted} líneas (Δ {delta}), +{bin_added} / −{bin_deleted} recursos binarios, {first} – {last}"
"A collection of this file with these modes and variables: override their values per mode and add variables of its own" = "Una colección de este archivo con estos modos y variables: modifica sus valores por modo y añade variables propias"
"A responsive product page: hero, features, figures, a quote and a footer" = "Una página de producto adaptable: cabecera, funciones, cifras, una cita y un pie"
"A spring plays until it settles: its mass, stiffness and damping set how long" = "Un muelle se reproduce hasta que se asienta: su masa, rigidez y amortiguación fijan cuánto dura"
"A title, an agenda, a big figure, a chart and a closing slide" = "Un título, una agenda, una gran cifra, un gráfico y una diapositiva de cierre"
"About" = "Acerca de"
"About FigCraft" = "Acerca de FigCraft"
"About this server" = "Acerca de este servidor"
@ -89,6 +91,7 @@ status = "draft — needs native review"
"All variables" = "Todas las variables"
"Alpha" = "Alfa"
"Alt+drag" = "Alt+arrastrar"
"An analytics screen on a grid: figures, charts drawn from vectors and a table" = "Una pantalla de analítica en cuadrícula: cifras, gráficos dibujados con vectores y una tabla"
"An invite link that opens FigCraft and joins" = "Un enlace de invitación que abre FigCraft y se une"
"Anchor" = "Ancla"
"Android" = "Android"
@ -107,6 +110,7 @@ status = "draft — needs native review"
"Apply variable" = "Aplicar variable"
"Apply variable to text" = "Aplicar variable al texto"
"Apply variable to visibility" = "Aplicar variable a la visibilidad"
"Apps" = "Aplicaciones"
"Arc" = "Arco"
"Arc ratio" = "Proporción del arco"
"Arc start" = "Inicio del arco"
@ -190,6 +194,7 @@ status = "draft — needs native review"
"Clear from this list" = "Borrar de esta lista"
"Code or invite link" = "Código o enlace de invitación"
"Color profile: {name}" = "Perfil de color: {name}"
"Colour and text styles, Light and Dark variables, buttons, inputs and cards" = "Estilos de color y texto, variables Claro y Oscuro, botones, campos y tarjetas"
"CSS custom properties…" = "Propiedades personalizadas CSS…"
"Can edit" = "Puede editar"
"Can't read this library: {e}" = "No se puede leer esta biblioteca: {e}"
@ -257,7 +262,6 @@ status = "draft — needs native review"
"Comment" = "Comentario"
"Comments" = "Comentarios"
"Commits" = "Commits"
"Community" = "Comunidad"
"Company" = "Empresa"
"Compare changes" = "Comparar cambios"
"Completed" = "Completado"
@ -328,6 +332,7 @@ status = "draft — needs native review"
"Dark" = "Oscuro"
"Darken" = "Oscurecer"
"Dash" = "Discontinuo"
"Dashboard" = "Panel de control"
"Decoration style" = "Estilo de decoración"
"Decoration" = "Decoración"
"Deep select" = "Selección profunda"
@ -354,6 +359,7 @@ status = "draft — needs native review"
"Description" = "Descripción"
"Description (optional)" = "Descripción (opcional)"
"Design" = "Diseño"
"Design system starter" = "Base de sistema de diseño"
"Desktop" = "Escritorio"
"Destination" = "Destino"
"Detach alias" = "Separar alias"
@ -389,6 +395,7 @@ status = "draft — needs native review"
"Drafts" = "Borradores"
"Drag the stroke to add a width point" = "Arrastra el trazo para añadir un punto de ancho"
"Draw" = "Dibujar"
"Draw a frame" = "Dibuja un marco"
"Drawing tools" = "Herramientas de dibujo"
"Drop shadow" = "Sombra paralela"
"Dual-licensed MIT OR Apache-2.0. Made by the ArtCraft team and community." = "Con doble licencia MIT OR Apache-2.0. Hecho por el equipo y la comunidad de ArtCraft."
@ -437,6 +444,7 @@ status = "draft — needs native review"
"Expand" = "Expandir"
"Export" = "Exportar"
"Export all" = "Exportar todo"
"Export frames and layers as PNG, JPG, SVG or PDF" = "Exporta marcos y capas como PNG, JPG, SVG o PDF"
"Export page as PNG" = "Exportar página como PNG"
"Export settings" = "Ajustes de exportación"
"Export variables" = "Exportar variables"
@ -505,6 +513,7 @@ status = "draft — needs native review"
"Font size" = "Tamaño de fuente"
"Font style" = "Estilo de fuente"
"Forget on this computer" = "Olvidar en este ordenador"
"Four screens of a trail app with components and a prototype flow" = "Cuatro pantallas de una app de senderos con componentes y un flujo de prototipo"
"Fractions" = "Fracciones"
"Frame" = "Marco"
"Frame selection" = "Enmarcar selección"
@ -518,6 +527,7 @@ status = "draft — needs native review"
"Gap between columns" = "Espacio entre columnas"
"Gap between items" = "Espacio entre elementos"
"Gap between rows" = "Espacio entre filas"
"Getting started" = "Primeros pasos"
"Give the address of a server open to the public, or a server's address with its key (`figcraft-server key` prints it)." = "Indica la dirección de un servidor abierto al público, o la dirección de un servidor con su clave (`figcraft-server key` la muestra)."
"Glass" = "Vidrio"
"Go to main component" = "Ir al componente principal"
@ -526,6 +536,7 @@ status = "draft — needs native review"
"Gradient" = "Degradado"
"Gray" = "Gris"
"Green" = "Verde"
"Greyscale components and two example screens to sketch a flow with" = "Componentes en escala de grises y dos pantallas de ejemplo para esbozar un flujo"
"Grid" = "Cuadrícula"
"Grid ({size})" = "Cuadrícula ({size})"
"Grid styles" = "Estilos de cuadrícula"
@ -547,6 +558,7 @@ status = "draft — needs native review"
"Hide contents" = "Ocultar contenido"
"Hide from publishing" = "Ocultar al publicar"
"Hide section contents" = "Ocultar contenido de la sección"
"Hide these tips (Getting started in the Help menu shows them again)" = "Ocultar estos consejos (Primeros pasos en el menú Ayuda los vuelve a mostrar)"
"Hide when publishing" = "Ocultar al publicar"
"Highlight layers on hover" = "Resaltar capas al pasar el cursor"
"Highlights" = "Luces"
@ -569,6 +581,8 @@ status = "draft — needs native review"
"Hue" = "Tono"
"Hug" = "Ajustar"
"Hug contents" = "Ajustar al contenido"
"Icon components on a 24 px grid with keylines and a size preview" = "Componentes de icono en una cuadrícula de 24 px con líneas guía y una vista de tamaños"
"Icon set starter" = "Base de conjunto de iconos"
"If" = "Si"
"If {condition}" = "Si {condition}"
"Ignore auto layout (absolute position)" = "Ignorar diseño automático (posición absoluta)"
@ -632,6 +646,8 @@ status = "draft — needs native review"
"Key/Gamepad" = "Tecla/Mando"
"Key/Gamepad {key}" = "Tecla/Mando {key}"
"Keyboard shortcuts" = "Atajos de teclado"
"Kits" = "Kits"
"Landing page" = "Página de inicio"
"Language" = "Idioma"
"Lasso" = "Lazo"
"Last" = "Último"
@ -675,6 +691,7 @@ status = "draft — needs native review"
"Link" = "Enlace"
"Link copied" = "Enlace copiado"
"Link docs, code or tickets for developers." = "Enlaza documentación, código o tickets para desarrolladores."
"Link frames on the Prototype tab, then present the flow" = "Enlaza marcos en la pestaña Prototipo y presenta el flujo"
"Link this component to its code component: instances then show your real snippet with their values." = "Vincula este componente a su componente de código: así las instancias muestran tu fragmento real con sus valores."
"List spacing" = "Espaciado de lista"
"List style" = "Estilo de lista"
@ -692,6 +709,7 @@ status = "draft — needs native review"
"Main" = "Principal"
"Main component deleted" = "Componente principal eliminado"
"Main menu" = "Menú principal"
"Make a component" = "Crea un componente"
"Make slot" = "Convertir en ranura"
"Margin" = "Margen"
"Mark all as read" = "Marcar todo como leído"
@ -726,6 +744,7 @@ status = "draft — needs native review"
"Mixed" = "Mixto"
"Mixed fonts" = "Fuentes mixtas"
"Mixed interactions: click + to replace them" = "Interacciones mixtas: haz clic en + para reemplazarlas"
"Mobile app" = "Aplicación móvil"
"Mode" = "Modo"
"Model" = "Modelo"
"Models" = "Modelos"
@ -797,6 +816,7 @@ status = "draft — needs native review"
"No string variables yet. Create them in Local variables." = "Aún no hay variables de cadena. Créalas en Variables locales."
"No styles yet." = "Aún no hay estilos."
"No styles yet. Click + to create one from the selection." = "Aún no hay estilos. Haz clic en + para crear uno a partir de la selección."
"No templates match your search" = "Ninguna plantilla coincide con tu búsqueda"
"No text styles yet. Click + to create one from the selected text." = "Aún no hay estilos de texto. Haz clic en + para crear uno a partir del texto seleccionado."
"No variables fit here." = "Ninguna variable encaja aquí."
"No variables yet. Create them in Local variables." = "Aún no hay variables. Créalas en Variables locales."
@ -831,6 +851,7 @@ status = "draft — needs native review"
"On click" = "Al hacer clic"
"On drag" = "Al arrastrar"
"On this page" = "En esta página"
"One announcement in five common sizes: square, portrait, story, link and banner" = "Un anuncio en cinco tamaños habituales: cuadrado, vertical, historia, enlace y banner"
"Online" = "En línea"
"Online files" = "Archivos en línea"
"Online sharing is off: files are shared on the local network only." = "Compartir en línea está desactivado: los archivos solo se comparten en la red local."
@ -903,6 +924,7 @@ status = "draft — needs native review"
"Pick color" = "Elegir color"
"Pick color (I)" = "Tomar color (I)"
"Pick one font family first" = "Elige primero una familia de fuentes"
"Pick the Frame tool, then drag on the canvas or choose a device size" = "Elige la herramienta Marco y arrastra en el lienzo o escoge un tamaño de dispositivo"
"Pin a property" = "Fijar una propiedad"
"Pinch" = "Pellizco"
"Pixel grid" = "Cuadrícula de píxeles"
@ -928,6 +950,7 @@ status = "draft — needs native review"
"Present or preview" = "Presentar o previsualizar"
"Present this flow" = "Presentar este flujo"
"Presentation" = "Presentación"
"Presentation slides" = "Diapositivas de presentación"
"Presenting: comments" = "Presentando: comentarios"
"Presenting: cycle the size options" = "Presentando: alternar las opciones de tamaño"
"Presenting: full screen" = "Presentando: pantalla completa"
@ -1066,6 +1089,9 @@ status = "draft — needs native review"
"Rulers" = "Reglas"
"Run" = "Ejecutar"
"Run {label}" = "Ejecutar {label}"
"Search templates" = "Buscar plantillas"
"See all templates" = "Ver todas las plantillas"
"Select layers and they space themselves as their content changes" = "Selecciona capas y se espaciarán solas cuando cambie su contenido"
"SHA-256: {sha}…" = "SHA-256: {sha}…"
"Same as layer" = "Igual que la capa"
"Saturation" = "Saturación"
@ -1125,6 +1151,7 @@ status = "draft — needs native review"
"Shape tools" = "Herramientas de forma"
"Share" = "Compartir"
"Share this file" = "Compartir este archivo"
"Share with a code" = "Comparte con un código"
"Share: live session, export" = "Compartir: sesión en vivo, exportar"
"Shared" = "Compartido"
"Shared: copy its code from the file's menu" = "Compartido: copia su código desde el menú del archivo"
@ -1134,6 +1161,7 @@ status = "draft — needs native review"
"Sharing online · {people}" = "Compartiendo en línea · {people}"
"Sharing stopped: the file stays on the server, open only to you" = "Se dejó de compartir: el archivo se queda en el servidor, abierto solo para ti"
"Show" = "Mostrar"
"Show all templates" = "Mostrar todas las plantillas"
"Show as grid" = "Mostrar como cuadrícula"
"Show as list" = "Mostrar como lista"
"Show autosaves" = "Mostrar autoguardados"
@ -1166,7 +1194,9 @@ status = "draft — needs native review"
"Small caps" = "Versalitas"
"Smart animate" = "Animación inteligente"
"Snap to pixel grid" = "Ajustar a la cuadrícula de píxeles"
"Social and slides" = "Redes y diapositivas"
"Social media" = "Redes sociales"
"Social post sizes" = "Tamaños para redes sociales"
"Soft light" = "Luz suave"
"Solid" = "Sólido"
"Some variants have the same property values: give each one a unique combination" = "Algunas variantes tienen los mismos valores de propiedad: da a cada una una combinación única"
@ -1191,9 +1221,11 @@ status = "draft — needs native review"
"Start" = "Inicio"
"Start ascending from / stop descending at" = "Contar hacia arriba desde / hacia abajo hasta"
"Start from a blank canvas" = "Empieza con un lienzo en blanco"
"Start from a template" = "Empieza con una plantilla"
"Start on path" = "Inicio en el trazado"
"Start point" = "Punto inicial"
"Start sharing" = "Empezar a compartir"
"Start sharing with the Share button, then pass on the code it gives" = "Empieza a compartir con el botón Compartir y pasa el código que te da"
"Starting to share…" = "Empezando a compartir…"
"Status" = "Estado"
"Sticky" = "Adherido"
@ -1242,6 +1274,7 @@ status = "draft — needs native review"
"Tell the server's operator what is wrong with it" = "Explica al operador del servidor qué tiene de malo"
"Temperature" = "Temperatura"
"Template" = "Plantilla"
"Templates" = "Plantillas"
"Terms" = "Condiciones"
"Text" = "Texto"
"Text align" = "Alineación del texto"
@ -1299,6 +1332,8 @@ 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 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"
"Type" = "Tipo"
"Type settings" = "Ajustes de texto"
@ -1369,6 +1404,7 @@ status = "draft — needs native review"
"Watch" = "Reloj"
"Wavy" = "Ondulado"
"Web" = "Web"
"Websites" = "Sitios web"
"WebSocket URL" = "URL de WebSocket"
"WebSocket URL copied" = "URL de WebSocket copiada"
"Wedge" = "Cuña"
@ -1386,6 +1422,7 @@ status = "draft — needs native review"
"Width" = "Ancho"
"Width point" = "Punto de ancho"
"Width profile" = "Perfil de ancho"
"Wireframe kit" = "Kit de wireframes"
"With the layers' export settings" = "Con los ajustes de exportación de las capas"
"Wrap" = "Ajustar"
"Wrap in new section" = "Envolver en una sección nueva"
@ -1661,6 +1698,7 @@ status = "draft — needs native review"
"file.exportSvg" = "Exportar selección como SVG"
"file.import" = "Importar"
"file.new" = "Nuevo archivo de diseño"
"file.newFromTemplate" = "Nuevo desde plantilla"
"file.open" = "Abrir"
"file.openRecent" = "Abrir reciente"
"file.placeImage" = "Colocar imagen"
@ -1687,6 +1725,7 @@ status = "draft — needs native review"
"guide.move" = "Mover guía"
"guide.remove" = "Quitar guía"
"guide.select" = "Seleccionar guía"
"help.gettingStarted" = "Primeros pasos"
"help.about" = "Acerca de FigCraft"
"help.page" = "Página de ayuda"
"help.shortcuts" = "Atajos de teclado"
@ -2064,6 +2103,7 @@ status = "draft — needs native review"
"widget.list" = "Listar widgets"
"window.focus" = "Traer la ventana al frente"
"window.new" = "Nueva ventana"
"template.list" = "Listar plantillas"
[menu]
"bool.exclude" = "Excluir"

View File

@ -257,7 +257,6 @@ status = "draft — needs native review"
"Comment" = "Commentaire"
"Comments" = "Commentaires"
"Commits" = "Commits"
"Community" = "Communauté"
"Company" = "Entreprise"
"Compare changes" = "Comparer les modifications"
"Completed" = "Terminé"

View File

@ -259,7 +259,6 @@ status = "draft — needs native review"
"Comment" = "コメント"
"Comments" = "コメント"
"Commits" = "コミット"
"Community" = "コミュニティ"
"Company" = "会社"
"Compare changes" = "変更を比較"
"Completed" = "完了"

View File

@ -258,7 +258,6 @@ status = "draft — needs native review"
"Comment" = "댓글"
"Comments" = "댓글"
"Commits" = "커밋"
"Community" = "커뮤니티"
"Company" = "회사"
"Compare changes" = "변경 사항 비교"
"Completed" = "완료됨"

View File

@ -256,7 +256,6 @@ status = "draft — needs native review"
"Comment" = "Comentário"
"Comments" = "Comentários"
"Commits" = "Commits"
"Community" = "Comunidade"
"Company" = "Empresa"
"Compare changes" = "Comparar alterações"
"Completed" = "Concluído"

View File

@ -259,7 +259,6 @@ status = "draft — needs native review"
"Comment" = "评论"
"Comments" = "评论"
"Commits" = "提交"
"Community" = "社区"
"Company" = "公司"
"Compare changes" = "比较更改"
"Completed" = "已完成"

View File

@ -344,8 +344,8 @@ pub fn handle(app: &mut FigApp, ctx: &Context, method: &str, p: &Value) -> Resul
if let Some(h) = p.get("home").and_then(Value::as_bool) {
app.ui.home = h;
}
if let Some(s) = p.get("homeSection").and_then(Value::as_str) {
app.ui.online.open = s == "online";
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(h) = p.get("palette").and_then(Value::as_bool) {
app.ui.palette = if h { Some(String::new()) } else { None };

View File

@ -1,6 +1,12 @@
//! The home screen: new file, open, join with a code, recent files, and your online files.
//! The home screen: Recents (new file, open, join with a code, the Getting started tips, open and
//! recent files, and templates to start from on a first run), Online (your online files:
//! [`crate::online`]) and Templates (the starter templates as cards with thumbnails rendered from
//! the documents themselves, searched and filtered by kind).
use egui::{Align2, Color32, CornerRadius, Rect, Response, Sense, Stroke, StrokeKind, Ui, pos2, vec2};
use std::collections::BTreeMap;
use egui::{Align2, Color32, CornerRadius, Id, Rect, Response, Sense, Stroke, StrokeKind, Ui, pos2, vec2};
use figcraft_engine::templates::{Kind, TEMPLATES, Template};
use serde_json::json;
use crate::FigApp;
@ -10,6 +16,64 @@ use crate::icons::{self, Icon};
use crate::theme::{ACCENT, Tokens, regular, semibold};
use crate::widgets;
/// The section the home screen shows.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Section {
#[default]
Recents,
Online,
Templates,
}
impl Section {
/// The sidebar, top to bottom.
const ALL: [(Section, Icon, &'static str); 3] = [
(Section::Recents, Icon::Home, tr_noop("Recents")),
(Section::Online, Icon::Online, tr_noop("Online")),
(Section::Templates, Icon::Assets, tr_noop("Templates")),
];
/// The id `ui.set {homeSection}` and `ui.inspect` use.
pub fn id(self) -> &'static str {
match self {
Section::Recents => "recents",
Section::Online => "online",
Section::Templates => "templates",
}
}
pub fn parse(id: &str) -> Option<Section> {
Section::ALL.into_iter().map(|(section, ..)| section).find(|section| section.id() == id)
}
}
/// The home screen's own state.
#[derive(Default)]
pub struct Home {
pub section: Section,
/// The Templates search text and kind filter.
pub query: String,
pub kind: Option<Kind>,
}
const SIDEBAR: f32 = 240.0;
const CARD_W: f32 = 248.0;
const THUMB_H: f32 = 148.0;
const CARD_H: f32 = THUMB_H + 86.0;
const GAP: f32 = 20.0;
/// Templates offered on a first run.
const FIRST_RUN: usize = 4;
/// The Getting started tips: (title, what to do, the command whose shortcut shows).
const TIPS: [(&str, &str, &str); 6] = [
(tr_noop("Draw a frame"), tr_noop("Pick the Frame tool, then drag on the canvas or choose a device size"), "tool.frame"),
(tr_noop("Add auto layout"), tr_noop("Select layers and they space themselves as their content changes"), "layout.add"),
(tr_noop("Make a component"), tr_noop("Turn a layer into a component, reuse it, and change every copy at once"), "component.create"),
(tr_noop("Prototype"), tr_noop("Link frames on the Prototype tab, then present the flow"), "proto.present"),
(tr_noop("Share with a code"), tr_noop("Start sharing with the Share button, then pass on the code it gives"), "collab.share"),
(tr_noop("Export"), tr_noop("Export frames and layers as PNG, JPG, SVG or PDF"), "file.export"),
];
/// A row of the home screen's file lists.
pub(crate) struct FileRow<'a> {
/// The mark before the name, and its colour.
@ -35,153 +99,185 @@ pub(crate) fn file_row(ui: &Ui, t: &Tokens, r: Rect, resp: &Response, row: &File
ui.painter().text(pos2(r.min.x + 32.0, r.center().y), Align2::LEFT_CENTER, name, regular(12.0), row.ink);
}
/// A row of Recents' own lists: laid out in place, named for screen readers, ringed with the
/// keyboard on it; true when chosen.
fn listed(ui: &mut Ui, t: &Tokens, id: Id, height: f32, row: &FileRow) -> bool {
let (r, _) = ui.allocate_exact_size(vec2(ui.available_width().min(640.0), height), Sense::hover());
let resp = ui.interact(r, id, Sense::click());
a11y::button(&resp, row.name);
file_row(ui, t, r, &resp, row);
focus_ring(ui, t, r, &resp, 5);
resp.clicked()
}
pub fn show(app: &mut FigApp, ui: &mut Ui) {
let t = Tokens::get(ui.ctx());
egui::CentralPanel::default().frame(egui::Frame::NONE.fill(t.panel)).show(ui, |ui| {
let rect = ui.max_rect();
let left = Rect::from_min_size(rect.min, vec2(240.0, rect.height()));
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.
let mut y = left.min.y + 20.0;
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;
// Recents and Online are the sections there are; the others are placeholders.
for (icon, label, section) in [
(Icon::Home, tr_noop("Recents"), Some(false)),
(Icon::Online, tr_noop("Online"), Some(true)),
(Icon::Page, tr_noop("Drafts"), None),
(Icon::Assets, tr_noop("Community"), None),
] {
let r = Rect::from_min_size(pos2(left.min.x + 8.0, y), vec2(224.0, 32.0));
let selected = section == Some(app.ui.online.open);
if let Some(online) = section {
let resp = ui.interact(r, ui.id().with(("home_section", label)), Sense::click());
a11y::tab(&resp, tr(label), selected);
if resp.clicked() {
app.ui.online.open = online;
}
if resp.hovered() && !selected {
ui.painter().rect_filled(r, CornerRadius::same(5), t.hover);
}
}
if selected {
ui.painter().rect_filled(r, CornerRadius::same(5), t.selected_row);
}
icons::paint(ui.painter(), Rect::from_min_size(pos2(r.min.x + 10.0, r.center().y - 8.0), vec2(16.0, 16.0)), icon, t.icon);
ui.painter().text(pos2(r.min.x + 36.0, r.center().y), Align2::LEFT_CENTER, tr(label), regular(12.0), t.text);
y += 34.0;
}
let main = Rect::from_min_max(pos2(left.max.x + 40.0, rect.min.y + 24.0), rect.max - vec2(40.0, 24.0));
if app.ui.online.open {
crate::online::section(app, ui, main, &t);
sidebar(app, ui, &t, left);
if 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);
opened_join(app);
return;
}
ui.painter().text(main.min, Align2::LEFT_TOP, tr("Recents"), semibold(18.0), t.text);
// Action cards.
let mut x = main.min.x;
let cy = main.min.y + 48.0;
let mut new_file = false;
let mut open = false;
for (icon, title, sub) in [
(Icon::Plus, tr_noop("New design file"), tr_noop("Start from a blank canvas")),
(Icon::Page, tr_noop("Open file"), tr_noop("Open .figcraft, .svg or images")),
] {
let r = Rect::from_min_size(pos2(x, cy), vec2(260.0, 72.0));
let mut resp = ui.interact(r, ui.id().with(("card", title)), Sense::click());
// One line each inside the card; a long translation ends with an ellipsis, in full on hover.
let text_w = r.width() - 72.0;
let (title_shown, sub_shown) = (widgets::elide_font(ui, tr(title), text_w, semibold(12.0)), widgets::elide(ui, tr(sub), text_w));
if title_shown != tr(title) || sub_shown != tr(sub) {
resp = resp.on_hover_text(format!("{}\n{}", tr(title), tr(sub)));
}
ui.painter().rect_filled(r, CornerRadius::same(8), if resp.hovered() { t.hover } else { t.panel });
ui.painter().rect_stroke(r, CornerRadius::same(8), Stroke::new(1.0, t.border), StrokeKind::Inside);
let ir = Rect::from_min_size(pos2(r.min.x + 16.0, r.center().y - 16.0), vec2(32.0, 32.0));
ui.painter().rect_filled(ir, CornerRadius::same(6), ACCENT);
icons::paint(ui.painter(), ir.shrink(8.0), icon, egui::Color32::WHITE);
ui.painter().text(pos2(r.min.x + 60.0, r.center().y - 8.0), Align2::LEFT_CENTER, title_shown, semibold(12.0), t.text);
ui.painter().text(pos2(r.min.x + 60.0, r.center().y + 9.0), Align2::LEFT_CENTER, sub_shown, regular(11.0), t.text_secondary);
if resp.clicked() {
if title.starts_with("New") {
new_file = true;
} else {
open = true;
}
}
x += 280.0;
}
if new_file {
app.run("file.new", json!({}));
app.ui.home = false;
}
if open {
app.open_dialog();
}
// Join a file someone shared: the join field, and what the join is doing.
let join = Rect::from_min_size(pos2(main.min.x, cy + 96.0), vec2(main.width().min(540.0), 74.0));
ui.painter().text(join.min, Align2::LEFT_TOP, tr("Join with a code"), semibold(12.0), t.text_secondary);
let field = Rect::from_min_max(pos2(join.min.x, join.min.y + 24.0), join.max);
ui.scope_builder(egui::UiBuilder::new().max_rect(field).layout(egui::Layout::top_down(egui::Align::Min)), |ui| {
if let Some(session) = crate::live::join_row(app, ui, 260.0) {
app.ui.live.home_join = Some(session);
}
if let Some(session) = app.ui.live.home_join {
crate::live::join_status(app, ui, &t, session);
}
let main = Rect::from_min_max(pos2(left.max.x, rect.min.y), rect.max);
ui.scope_builder(egui::UiBuilder::new().max_rect(main), |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),
}
});
});
});
opened_join(app);
// Open documents and recent files.
let mut y = join.max.y + 22.0;
ui.painter().text(pos2(main.min.x, y), Align2::LEFT_TOP, tr("Open files"), semibold(12.0), t.text_secondary);
y += 28.0;
});
}
fn sidebar(app: &mut FigApp, ui: &mut Ui, t: &Tokens, left: Rect) {
let mut y = left.min.y + 20.0;
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;
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;
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 });
}
focus_ring(ui, t, r, &resp, 5);
icons::paint(ui.painter(), Rect::from_min_size(pos2(r.min.x + 10.0, r.center().y - 8.0), vec2(16.0, 16.0)), icon, t.icon);
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;
}
y += 34.0;
}
}
/// 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() {
ui.painter().rect_stroke(rect, CornerRadius::same(radius), Stroke::new(2.0, t.focus), StrokeKind::Outside);
}
}
/// A card's background and border (lit on hover, ringed with the keyboard on it).
fn card_frame(ui: &Ui, t: &Tokens, rect: Rect, resp: &Response) {
ui.painter().rect_filled(rect, CornerRadius::same(8), if resp.hovered() { t.hover } else { t.panel });
ui.painter().rect_stroke(rect, CornerRadius::same(8), Stroke::new(1.0, t.border), StrokeKind::Inside);
focus_ring(ui, t, rect, resp, 8);
}
fn heading(ui: &mut Ui, t: &Tokens, text: &str) {
ui.label(egui::RichText::new(text).font(semibold(18.0)).color(t.text));
ui.add_space(24.0);
}
fn subheading(ui: &mut Ui, t: &Tokens, text: &str) {
ui.add_space(28.0);
ui.label(egui::RichText::new(text).font(semibold(12.0)).color(t.text_secondary));
ui.add_space(12.0);
}
/// A card with an icon tile, a title and a line under it; true when chosen.
fn action_card(ui: &mut Ui, t: &Tokens, icon: Icon, title: &str, sub: &str) -> bool {
let (r, mut resp) = ui.allocate_exact_size(vec2(260.0, 72.0), Sense::click());
a11y::button(&resp, title);
// One line each inside the card; a long translation ends with an ellipsis, in full on hover.
let text_w = r.width() - 72.0;
let (title_shown, sub_shown) = (widgets::elide_font(ui, title, text_w, semibold(12.0)), widgets::elide(ui, sub, text_w));
if title_shown != title || sub_shown != sub {
resp = resp.on_hover_text(format!("{title}\n{sub}"));
}
card_frame(ui, t, r, &resp);
let ir = Rect::from_min_size(pos2(r.min.x + 16.0, r.center().y - 16.0), vec2(32.0, 32.0));
ui.painter().rect_filled(ir, CornerRadius::same(6), ACCENT);
icons::paint(ui.painter(), ir.shrink(8.0), icon, Color32::WHITE);
ui.painter().text(pos2(r.min.x + 60.0, r.center().y - 8.0), Align2::LEFT_CENTER, title_shown, semibold(12.0), t.text);
ui.painter().text(pos2(r.min.x + 60.0, r.center().y + 9.0), Align2::LEFT_CENTER, sub_shown, regular(11.0), t.text_secondary);
resp.clicked()
}
fn recents(app: &mut FigApp, ui: &mut Ui, t: &Tokens) {
heading(ui, t, tr("Recents"));
let (mut new_file, mut open) = (false, false);
ui.horizontal(|ui| {
new_file = action_card(ui, t, Icon::Plus, tr("New design file"), tr("Start from a blank canvas"));
open = action_card(ui, t, Icon::Page, tr("Open file"), tr("Open .figcraft, .svg or images"));
});
if new_file {
app.run("file.new", json!({}));
app.ui.home = false;
}
if open {
app.open_dialog();
}
// Join a file someone shared: the join field, and what the join is doing.
subheading(ui, t, tr("Join with a code"));
ui.scope(|ui| {
ui.set_max_width(540.0);
if let Some(session) = crate::live::join_row(app, ui, 260.0) {
app.ui.live.home_join = Some(session);
}
if let Some(session) = app.ui.live.home_join {
crate::live::join_status(app, ui, t, session);
}
});
opened_join(app);
if !app.session.prefs.getting_started_dismissed {
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 r = Rect::from_min_size(pos2(main.min.x, y), vec2(main.width().min(640.0), 36.0));
let resp = ui.interact(r, ui.id().with(("open", d.uid)), Sense::click());
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 };
file_row(ui, &t, r, &resp, &row);
if resp.clicked() {
if listed(ui, t, ui.id().with(("open", d.uid)), 36.0, &row) {
switch = Some(i);
}
y += 38.0;
}
if let Some(i) = switch {
app.run("doc.switch", json!({"index": i}));
app.ui.home = false;
}
y += 16.0;
let recents = app.session.prefs.recent_files.clone();
if !recents.is_empty() {
ui.painter().text(pos2(main.min.x, y), Align2::LEFT_TOP, tr("Recent files"), semibold(12.0), t.text_secondary);
y += 28.0;
let mut open_path = None;
for p in recents.iter().take(12) {
let r = Rect::from_min_size(pos2(main.min.x, y), vec2(main.width().min(640.0), 32.0));
let resp = ui.interact(r, ui.id().with(("recent", p.as_str())), Sense::click());
let name = std::path::Path::new(p).file_name().map(|f| f.to_string_lossy().to_string()).unwrap_or_else(|| p.clone());
file_row(
ui,
&t,
r,
&resp,
&FileRow { mark: None, name: &name, ink: t.text, detail: p, detail_size: 10.0, detail_ink: t.text_tertiary },
);
if resp.clicked() {
open_path = Some(p.clone());
}
y += 34.0;
}
if let Some(p) = open_path
&& app.run("file.open", json!({"path": p})).is_some()
{
app.ui.home = false;
}
}
let recents = app.session.prefs.recent_files.clone();
if recents.is_empty() {
// A first run: nothing to reopen yet, so offer something to start from.
subheading(ui, t, tr("Start from a template"));
let shown: Vec<&Template> = TEMPLATES.iter().take(FIRST_RUN).collect();
template_grid(app, ui, t, &shown);
ui.add_space(12.0);
if widgets::button(ui, tr("See all templates"), false, true).clicked() {
app.ui.home_view.section = Section::Templates;
}
});
return;
}
subheading(ui, t, tr("Recent files"));
let mut open_path = None;
for p in recents.iter().take(12) {
let name = std::path::Path::new(p).file_name().map(|f| f.to_string_lossy().to_string()).unwrap_or_else(|| p.clone());
let row = FileRow { mark: None, name: &name, ink: t.text, detail: p, detail_size: 10.0, detail_ink: t.text_tertiary };
if listed(ui, t, ui.id().with(("recent", p.as_str())), 32.0, &row) {
open_path = Some(p.clone());
}
}
if let Some(p) = open_path
&& app.run("file.open", json!({"path": p})).is_some()
{
app.ui.home = false;
}
}
/// The file joined from the home screen (a code, or one of your online files) opened in a tab of
@ -196,3 +292,199 @@ fn opened_join(app: &mut FigApp) {
_ => {}
}
}
/// The Getting started card: short tips with their shortcuts as the keymap has them now, and a
/// button that puts the card away (Help ▸ Getting started brings it back).
fn getting_started(app: &mut FigApp, ui: &mut Ui, t: &Tokens) {
let width = ui.available_width().min(3.0 * CARD_W + 2.0 * GAP);
let columns = if width >= 720.0 { 3 } else { 1 };
let (cell_w, cell_h) = ((width - 32.0) / columns as f32, 62.0);
let rows = TIPS.len().div_ceil(columns);
let (rect, _) = ui.allocate_exact_size(vec2(width, 52.0 + rows as f32 * cell_h + 8.0), Sense::hover());
a11y::container_bounds(ui, rect);
ui.painter().rect_filled(rect, CornerRadius::same(8), t.field);
ui.painter().text(rect.min + vec2(16.0, 24.0), Align2::LEFT_CENTER, tr("Getting started"), semibold(13.0), t.text);
let close = Rect::from_min_size(pos2(rect.max.x - 36.0, rect.min.y + 12.0), vec2(24.0, 24.0));
if widgets::icon_button_at(
ui,
ui.id().with("tips_close"),
close,
Icon::Close,
tr("Hide these tips (Getting started in the Help menu shows them again)"),
) {
app.run("help.gettingStarted", json!({"show": false}));
app.services.save_prefs(&app.session.prefs);
}
for (i, (title, line, command)) in TIPS.into_iter().enumerate() {
let cell = Rect::from_min_size(
rect.min + vec2(16.0 + (i % columns) as f32 * cell_w, 48.0 + (i / columns) as f32 * cell_h),
vec2(cell_w - 16.0, cell_h),
);
let badge = Rect::from_min_size(cell.min + vec2(0.0, 2.0), vec2(20.0, 20.0));
ui.painter().rect_filled(badge, CornerRadius::same(10), t.panel);
ui.painter().text(badge.center(), Align2::CENTER_CENTER, (i + 1).to_string(), semibold(10.0), t.text_secondary);
let x = cell.min.x + 30.0;
let title_end = ui.painter().text(pos2(x, cell.min.y + 12.0), Align2::LEFT_CENTER, tr(title), semibold(12.0), t.text).max.x;
if let Some(key) = shortcut(command) {
ui.painter().text(pos2(title_end + 8.0, cell.min.y + 12.0), Align2::LEFT_CENTER, key, regular(11.0), t.text_tertiary);
}
let galley = lines(ui, tr(line), regular(11.0), t.text_secondary, cell.max.x - x, 2);
ui.painter().galley(pos2(x, cell.min.y + 24.0), galley, t.text_secondary);
}
}
/// A command's shortcut as this platform writes it, as the keymap has it now.
pub(crate) fn shortcut(command: &str) -> Option<String> {
crate::shortcuts::shortcut(command).map(|key| figcraft_engine::keymap::display(key, crate::shortcuts::platform()))
}
/// `text` wrapped at `width`, at most `rows` lines (the last one ends with an ellipsis).
fn lines(ui: &Ui, text: &str, font: egui::FontId, color: Color32, width: f32, rows: usize) -> std::sync::Arc<egui::Galley> {
let mut job = egui::text::LayoutJob::simple(text.to_string(), font, color, width.max(1.0));
job.wrap.max_rows = rows;
ui.fonts_mut(|f| f.layout_job(job))
}
fn matches(template: &Template, query: &str, kind: Option<Kind>) -> bool {
let q = query.trim().to_lowercase();
let has = |s: &str| s.to_lowercase().contains(&q);
kind.is_none_or(|k| k == template.kind)
&& (q.is_empty()
|| [template.name, template.description, template.kind.label()].into_iter().any(|s| has(s) || has(tr(s)))
|| has(template.size))
}
fn templates(app: &mut FigApp, ui: &mut Ui, t: &Tokens) {
heading(ui, t, tr("Templates"));
ui.horizontal(|ui| {
let search = widgets::text_input(ui, Id::new("home_template_search"), &mut app.ui.home_view.query, 260.0, tr("Search templates"));
a11y::text(&search, tr("Search templates"), &app.ui.home_view.query);
let labels: Vec<&str> = std::iter::once(tr("All")).chain(Kind::ALL.iter().map(|k| tr(k.label()))).collect();
let active = app.ui.home_view.kind.and_then(|k| Kind::ALL.iter().position(|x| *x == k)).map_or(0, |i| i + 1);
ui.scope(|ui| {
ui.set_max_width((labels.len() as f32 * 124.0).min(ui.available_width()));
if let Some(i) = widgets::segmented_text(ui, &labels, active) {
app.ui.home_view.kind = i.checked_sub(1).and_then(|i| Kind::ALL.get(i).copied());
}
});
});
ui.add_space(24.0);
let (query, kind) = (app.ui.home_view.query.clone(), app.ui.home_view.kind);
let shown: Vec<&Template> = TEMPLATES.iter().filter(|x| matches(x, &query, kind)).collect();
if shown.is_empty() {
ui.add_space(24.0);
ui.label(egui::RichText::new(tr("No templates match your search")).font(semibold(13.0)).color(t.text));
ui.add_space(6.0);
ui.label(egui::RichText::new(tr("Try another word, or look at every kind.")).font(regular(12.0)).color(t.text_secondary));
ui.add_space(14.0);
if widgets::button(ui, tr("Show all templates"), false, true).clicked() {
app.ui.home_view.query.clear();
app.ui.home_view.kind = None;
}
return;
}
template_grid(app, ui, t, &shown);
}
/// Template cards in as many columns as fit; choosing one opens a new document made from it.
fn template_grid(app: &mut FigApp, ui: &mut Ui, t: &Tokens, list: &[&Template]) {
let columns = (((ui.available_width() + GAP) / (CARD_W + GAP)).floor() as usize).max(1);
let dark = ui.visuals().dark_mode;
let mut thumbs: Thumbs = ui.data(|d| d.get_temp(Id::new("template_thumbs"))).unwrap_or_default();
let mut chosen = None;
let mut rendered = false;
for row in list.chunks(columns) {
ui.horizontal(|ui| {
for template in row {
let key = (template.id, dark);
if !thumbs.0.contains_key(&key) && ui.is_rect_visible(Rect::from_min_size(ui.cursor().min, vec2(CARD_W, CARD_H))) {
if rendered {
// One a frame keeps the screen moving; the rest follow.
ui.ctx().request_repaint();
} else {
thumbs.0.insert(key, thumbnail(ui.ctx(), template, app.session.new_page_background()));
rendered = true;
}
}
if template_card(ui, t, template, thumbs.0.get(&key).and_then(Option::as_ref)) {
chosen = Some(*template);
}
}
});
ui.add_space(GAP);
}
ui.data_mut(|d| d.insert_temp(Id::new("template_thumbs"), thumbs));
if let Some(template) = chosen
&& app.run("file.newFromTemplate", json!({"template": template.id, "name": tr(template.name)})).is_some()
{
app.ui.home = false;
}
}
fn template_card(ui: &mut Ui, t: &Tokens, template: &Template, thumb: Option<&Thumb>) -> bool {
let (r, resp) = ui.allocate_exact_size(vec2(CARD_W, CARD_H), Sense::click());
let (name, description) = (tr(template.name), tr(template.description));
a11y::button(&resp, &format!("{name}: {description}"));
card_frame(ui, t, r, &resp);
let picture = Rect::from_min_size(r.min, vec2(CARD_W, THUMB_H)).shrink(1.0);
let top = CornerRadius { nw: 7, ne: 7, sw: 0, se: 0 };
match thumb {
Some(thumb) => {
ui.painter().rect_filled(picture, top, thumb.page);
let size = thumb.texture.size_vec2();
let inner = picture.shrink(10.0);
let fit = (inner.width() / size.x).min(inner.height() / size.y);
let image = Rect::from_center_size(inner.center(), size * fit);
ui.painter().image(thumb.texture.id(), image, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Color32::WHITE);
}
None => {
ui.painter().rect_filled(picture, top, t.field);
icons::paint(ui.painter(), Rect::from_center_size(picture.center(), vec2(20.0, 20.0)), Icon::Frame, t.icon_secondary);
}
}
ui.painter().line_segment([picture.left_bottom(), picture.right_bottom()], Stroke::new(1.0, t.border));
let (x, w) = (r.min.x + 14.0, CARD_W - 28.0);
let y = picture.max.y + 20.0;
ui.painter().text(pos2(x, y), Align2::LEFT_CENTER, widgets::elide_font(ui, name, w, semibold(12.0)), semibold(12.0), t.text);
let galley = lines(ui, description, regular(11.0), t.text_secondary, w, 2);
ui.painter().galley(pos2(x, y + 11.0), galley, t.text_secondary);
let tag = format!("{} · {}", tr(template.kind.label()), template.size);
ui.painter().text(pos2(x, r.max.y - 15.0), Align2::LEFT_CENTER, widgets::elide_font(ui, &tag, w, regular(10.0)), regular(10.0), t.text_tertiary);
resp.clicked()
}
/// A template's picture: its first page's frames, and the page colour they sit on.
#[derive(Clone)]
struct Thumb {
texture: egui::TextureHandle,
page: Color32,
}
/// Pictures by template and theme (the page colour follows the theme); `None`: nothing to show.
#[derive(Clone, Default)]
struct Thumbs(BTreeMap<(&'static str, bool), Option<Thumb>>);
/// Build `template` and render the top of its first page to fit a card.
fn thumbnail(ctx: &egui::Context, template: &Template, background: figcraft_color::Color) -> Option<Thumb> {
let doc = template.build(background).ok()?;
let frames = doc.node(doc.first_page()?)?.children.clone();
let bounds = frames.iter().filter_map(|id| figcraft_render::export_bounds(&doc, *id)).reduce(|a, b| a.union(b))?;
if !(bounds.width() > 0.0 && bounds.height() > 0.0) {
return None;
}
// A page much taller than a card shows its top, as wide as the card.
let (w, h) = (f64::from(CARD_W - 22.0), f64::from(THUMB_H - 22.0));
let area = figcraft_geom::Rect::new(bounds.x0, bounds.y0, bounds.x1, bounds.y1.min(bounds.y0 + bounds.width() * h / w * 1.5));
let scale = (w / area.width()).min(h / area.height()) * f64::from(ctx.pixels_per_point());
let pix = figcraft_render::render_area(&doc, &frames, area, scale.min(1.0))?;
let image = crate::render_worker::screen_image(&pix, doc.color_profile);
let texture = ctx.load_texture(format!("template-thumb-{}", template.id), image, egui::TextureOptions::LINEAR);
Some(Thumb { texture, page: crate::theme::c32(background) })
}
/// Tests: the templates whose pictures are cached for the light or dark theme.
#[cfg(test)]
pub(crate) fn cached_thumbnails(ctx: &egui::Context, dark: bool) -> Vec<&'static str> {
let thumbs: Thumbs = ctx.data(|d| d.get_temp(Id::new("template_thumbs"))).unwrap_or_default();
thumbs.0.iter().filter(|((_, d), thumb)| *d == dark && thumb.is_some()).map(|((id, _), _)| *id).collect()
}

View File

@ -0,0 +1,282 @@
//! The home screen, drawn headless: the Templates grid (opened by click and by keyboard, searched,
//! filtered, its empty state, its names for screen readers, thumbnails cached per theme and no
//! frames once they are in), the first-run row (only without recent files) and the Getting
//! started card (closed, kept closed, and brought back from the Help menu).
use egui::accesskit::Role;
use egui::{Event, Key, Modifiers, PointerButton, Pos2, Shape};
use figcraft_engine::Session;
use figcraft_engine::templates::TEMPLATES;
use serde_json::json;
use crate::home::{Section, cached_thumbnails};
use crate::layers_drag_tests::Ui;
use crate::live_tests::{flat, has_text, text_center};
/// The app as a first run finds it: no documents, no recent files.
fn first_run() -> Ui {
Ui::new(Session::empty())
}
fn shapes(u: &mut Ui) -> Vec<Shape> {
flat(&u.frame_output(vec![]).shapes)
}
/// Enough frames for every thumbnail in view (they render one a frame) and what follows a click.
fn settle(u: &mut Ui) -> Vec<Shape> {
for _ in 0..12 {
u.frame(vec![]);
}
shapes(u)
}
fn click(u: &mut Ui, p: Pos2) {
for pressed in [true, false] {
u.frame(vec![Event::PointerMoved(p), Event::PointerButton { pos: p, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE }]);
}
u.frame(vec![]);
}
fn press(u: &mut Ui, key: Key) {
for pressed in [true, false] {
u.frame(vec![Event::Key { key, physical_key: None, pressed, repeat: false, modifiers: Modifiers::NONE }]);
}
}
fn templates_tab(u: &mut Ui) -> Vec<Shape> {
let s = shapes(u);
click(u, text_center(&s, "Templates"));
assert_eq!(u.app.ui.home_view.section, Section::Templates);
settle(u)
}
#[test]
fn the_sidebar_has_recents_online_and_templates_and_no_placeholders() {
let mut u = first_run();
let s = shapes(&mut u);
assert!(has_text(&s, "Recents") && has_text(&s, "Online") && has_text(&s, "Templates"));
assert!(!has_text(&s, "Drafts") && !has_text(&s, "Community"));
u.ctx.enable_accesskit();
u.frame(vec![]);
let nodes: Vec<_> = u.frame_output(vec![]).platform_output.accesskit_update.unwrap().nodes.into_iter().map(|(_, n)| n).collect();
let tab = |label: &str| nodes.iter().find(|n| n.role() == Role::Tab && n.label() == Some(label)).unwrap_or_else(|| panic!("no tab {label}"));
assert_eq!((tab("Recents").is_selected(), tab("Online").is_selected(), tab("Templates").is_selected()), (Some(true), Some(false), Some(false)));
// In reading order: Recents, Online, Templates.
let top = |label: &str| tab(label).bounds().unwrap().y0;
assert!(top("Recents") < top("Online") && top("Online") < top("Templates"));
// Each is clicked into, and `ui.inspect` and `ui.set {homeSection}` name them the same way.
for (label, section) in [("Online", Section::Online), ("Templates", Section::Templates), ("Recents", Section::Recents)] {
let s = shapes(&mut u);
click(&mut u, text_center(&s, label));
assert_eq!(u.app.ui.home_view.section, section);
let inspect = u.app.inspect();
assert_eq!(inspect["homeSection"], section.id());
assert_eq!(inspect["online"]["open"], section == Section::Online);
assert_eq!(Section::parse(section.id()), Some(section));
}
assert_eq!(Section::parse("drafts"), None);
// The Online section is the online files' own.
u.app.ui.home_view.section = Section::Online;
let s = settle(&mut u);
assert!(has_text(&s, "Online files") && !has_text(&s, "Getting started") && !has_text(&s, "Start from a template"));
}
#[test]
fn the_templates_grid_shows_every_template_and_a_click_opens_a_new_document() {
let mut u = first_run();
let s = templates_tab(&mut u);
for t in TEMPLATES {
assert!(has_text(&s, t.name), "{}", t.name);
}
assert!(has_text(&s, "Apps · 390 × 844"), "the kind and size tag");
assert_eq!(cached_thumbnails(&u.ctx, false).len(), TEMPLATES.len(), "every card has its picture");
click(&mut u, text_center(&s, "Dashboard"));
assert_eq!(u.app.session.docs.len(), 1);
let ds = u.app.session.ds().unwrap();
assert_eq!(ds.doc.name, "Dashboard");
assert!(ds.path.is_none() && !ds.is_dirty(), "a new, untitled-on-disk document");
assert!(!u.app.ui.home, "and the editor shows it");
// Again: another document, the first untouched.
let before = u.app.session.docs[0].doc.clone();
u.app.ui.home = true;
let s = settle(&mut u);
// (Its tag: the name is also on the open document's tab now.)
click(&mut u, text_center(&s, "Apps · 1440 × 960"));
assert_eq!(u.app.session.docs.len(), 2);
assert!(std::sync::Arc::ptr_eq(&u.app.session.docs[0].doc, &before));
}
#[test]
fn cards_are_reached_with_tab_named_for_screen_readers_and_opened_with_enter() {
let mut u = first_run();
templates_tab(&mut u);
u.ctx.enable_accesskit();
u.frame(vec![]);
let nodes: Vec<_> = u.frame_output(vec![]).platform_output.accesskit_update.unwrap().nodes.into_iter().map(|(_, n)| n).collect();
for t in TEMPLATES {
let name = format!("{}: {}", t.name, t.description);
assert!(nodes.iter().any(|n| n.role() == Role::Button && n.label() == Some(name.as_str())), "no button named {name:?}");
}
assert!(nodes.iter().any(|n| n.label() == Some("Search templates")));
// Tab walks the controls; the first card it reaches opens with Enter.
let mut reached = false;
for _ in 0..40 {
press(&mut u, Key::Tab);
if crate::a11y::focused_name(&u.ctx).starts_with("Mobile app: ") {
reached = true;
break;
}
}
assert!(reached, "Tab never reached the first card");
press(&mut u, Key::Enter);
u.frame(vec![]);
assert_eq!(u.app.session.doc().unwrap().name, "Mobile app");
assert!(!u.app.ui.home);
}
#[test]
fn search_and_kind_filter_the_grid_and_nothing_found_offers_a_way_back() {
let mut u = first_run();
templates_tab(&mut u);
u.app.ui.home_view.query = "icon".into();
let s = settle(&mut u);
assert!(has_text(&s, "Icon set starter") && !has_text(&s, "Dashboard"));
// Descriptions and kinds match too, whatever the case.
u.app.ui.home_view.query = " PROTOTYPE ".into();
let s = settle(&mut u);
assert!(has_text(&s, "Mobile app") && !has_text(&s, "Landing page"));
u.app.ui.home_view.query.clear();
let s = settle(&mut u);
click(&mut u, text_center(&s, "Websites"));
let s = settle(&mut u);
assert!(has_text(&s, "Landing page") && !has_text(&s, "Mobile app") && !has_text(&s, "Icon set starter"));
u.app.ui.home_view.query = "zzz".into();
let s = settle(&mut u);
assert!(has_text(&s, "No templates match your search"));
assert!(TEMPLATES.iter().all(|t| !has_text(&s, t.name)));
click(&mut u, text_center(&s, "Show all templates"));
let s = settle(&mut u);
assert!(u.app.ui.home_view.query.is_empty() && u.app.ui.home_view.kind.is_none());
assert!(TEMPLATES.iter().all(|t| has_text(&s, t.name)));
}
#[test]
fn typing_in_the_search_field_filters() {
let mut u = first_run();
let s = templates_tab(&mut u);
let field = text_center(&s, "Search templates");
click(&mut u, field);
u.frame(vec![Event::Text("slides".into())]);
let s = settle(&mut u);
assert_eq!(u.app.ui.home_view.query, "slides");
assert!(has_text(&s, "Presentation slides") && !has_text(&s, "Dashboard"));
assert!(u.app.session.docs.is_empty(), "typing opens nothing");
}
#[test]
fn thumbnails_are_cached_per_theme() {
let mut u = first_run();
templates_tab(&mut u);
assert_eq!(cached_thumbnails(&u.ctx, false).len(), TEMPLATES.len());
assert!(cached_thumbnails(&u.ctx, true).is_empty());
u.app.session.prefs.theme = "dark".into();
settle(&mut u);
assert_eq!(cached_thumbnails(&u.ctx, true).len(), TEMPLATES.len(), "the dark theme's pictures");
assert_eq!(cached_thumbnails(&u.ctx, false).len(), TEMPLATES.len(), "the light ones are kept");
}
#[test]
fn the_home_screen_draws_no_frames_once_its_thumbnails_are_in() {
let (mut h, _) = crate::live_tests::Harness::new();
h.app.ui.home = true;
for tab in [Section::Templates, Section::Online, Section::Recents] {
h.app.ui.home_view.section = tab;
h.frames(60);
let (_, delay) = h.frame(vec![]);
assert!(delay >= std::time::Duration::from_secs(1), "{tab:?} asks for a frame in {delay:?}: {:?}", h.ctx.repaint_causes());
}
assert_eq!(cached_thumbnails(&h.ctx, false).len(), TEMPLATES.len());
}
#[test]
fn a_first_run_offers_templates_on_recents_and_recent_files_replace_them() {
let mut u = first_run();
let s = settle(&mut u);
assert!(has_text(&s, "Start from a template"));
let offered = TEMPLATES.iter().filter(|t| has_text(&s, t.name)).count();
assert_eq!(offered, 4);
assert!(!has_text(&s, "Recent files") && !has_text(&s, "Open files"));
click(&mut u, text_center(&s, "See all templates"));
assert_eq!(u.app.ui.home_view.section, Section::Templates);
// From the row: a click opens it.
u.app.ui.home_view.section = Section::Recents;
let s = settle(&mut u);
click(&mut u, text_center(&s, "Landing page"));
assert_eq!(u.app.session.doc().unwrap().name, "Landing page");
// With a recent file, the list shows in the row's place.
u.app.ui.home = true;
u.app.session.prefs.recent_files = vec!["/tmp/ridgeline.figcraft".into()];
let s = settle(&mut u);
assert!(has_text(&s, "Recent files") && has_text(&s, "ridgeline.figcraft"));
assert!(!has_text(&s, "Start from a template") && !has_text(&s, "See all templates"));
assert!(has_text(&s, "Open files"), "the document opened from the template is listed");
}
#[test]
fn the_getting_started_card_closes_stays_closed_and_comes_back_from_help() {
let mut u = first_run();
let s = settle(&mut u);
assert!(has_text(&s, "Getting started"));
for tip in ["Draw a frame", "Add auto layout", "Make a component", "Prototype", "Share with a code", "Export"] {
assert!(has_text(&s, tip), "{tip}");
}
// Shortcuts come from the keymap.
let key = crate::home::shortcut("layout.add").unwrap();
assert!(has_text(&s, &key), "the auto layout shortcut {key:?}");
// The close button is named, and closes the card.
u.ctx.enable_accesskit();
u.frame(vec![]);
let nodes = u.frame_output(vec![]).platform_output.accesskit_update.unwrap().nodes;
let close = nodes.iter().map(|(_, n)| n).find(|n| n.label().is_some_and(|l| l.starts_with("Hide these tips"))).expect("a named close button");
let b = close.bounds().unwrap();
click(&mut u, Pos2::new(((b.x0 + b.x1) / 2.0) as f32, ((b.y0 + b.y1) / 2.0) as f32));
assert!(u.app.session.prefs.getting_started_dismissed);
let s = settle(&mut u);
assert!(!has_text(&s, "Getting started") && !has_text(&s, "Draw a frame"));
assert!(has_text(&s, "Start from a template"), "the rest of the screen stays");
// The preference is kept with the others, so it stays closed next time.
let kept: figcraft_engine::Prefs = serde_json::from_value(serde_json::to_value(&u.app.session.prefs).unwrap()).unwrap();
assert!(kept.getting_started_dismissed);
// Help ▸ Getting started: back on the home screen, from wherever.
u.app.run("file.new", json!({}));
u.app.ui.home = false;
u.app.ui.home_view.section = Section::Templates;
crate::menus::invoke(&mut u.app, "help.gettingStarted");
assert!(u.app.ui.home && u.app.ui.home_view.section == Section::Recents && !u.app.session.prefs.getting_started_dismissed);
assert!(has_text(&settle(&mut u), "Getting started"));
}
#[test]
fn the_file_menu_item_shows_the_templates() {
let mut u = Ui::new(Session::new());
u.app.ui.home = false;
crate::menus::invoke(&mut u.app, "file.newFromTemplate");
assert!(u.app.ui.home && u.app.ui.home_view.section == Section::Templates);
assert_eq!(u.app.session.docs.len(), 1, "nothing opens until one is chosen");
assert!(has_text(&settle(&mut u), "Wireframe kit"));
}
#[test]
fn tab_walks_the_sidebar_in_order_before_a_sections_controls() {
let mut u = first_run();
let mut names = Vec::new();
for _ in 0..40 {
press(&mut u, Key::Tab);
let name = crate::a11y::focused_name(&u.ctx);
if !name.is_empty() && names.last() != Some(&name) {
names.push(name);
}
}
let at = |name: &str| names.iter().position(|n| n == name).unwrap_or_else(|| panic!("Tab never reached {name:?}: {names:?}"));
assert!(at("Recents") < at("Online") && at("Online") < at("Templates") && at("Templates") < at("Open file"), "{names:?}");
}

View File

@ -135,6 +135,8 @@ fn dynamic_labels() -> Vec<&'static str> {
v.extend(Group::ALL.iter().map(|g| g.label()));
v.extend(GESTURES.iter().flat_map(|(_, label, mac, other)| [*label, *mac, *other]));
v.extend(DATA_LABELS);
// The starter templates' names, descriptions and kinds.
v.extend(figcraft_engine::templates::TEMPLATES.iter().flat_map(|t| [t.name, t.description, t.kind.label()]));
v
}

View File

@ -81,6 +81,8 @@ mod field_tests;
#[cfg(test)]
mod frame_label_tests;
#[cfg(test)]
mod home_tests;
#[cfg(test)]
mod idle_tests;
#[cfg(test)]
mod layers_behaviour_tests;
@ -289,6 +291,8 @@ pub struct UiState {
/// An opacity digit just typed and when (a second one soon after makes an exact percentage).
pub opacity_digit: Option<(u32, f64)>,
pub home: bool,
/// The home screen's section, and the Templates search and filter.
pub home_view: home::Home,
/// How the window's title bar is drawn (the desktop app sets it when it opens the window).
pub title_bar: TitleBar,
/// Every icon on one screen, for reviewing the set (control channel `ui.set {iconGallery}`).
@ -402,6 +406,7 @@ impl Default for UiState {
shortcuts_query: String::new(),
opacity_digit: None,
home: false,
home_view: home::Home::default(),
title_bar: TitleBar::Native,
icon_gallery: false,
toast: None,
@ -657,6 +662,15 @@ impl FigApp {
self.ui.libraries = true;
}
"home" => self.ui.home = true,
"home.templates" => {
self.ui.home = true;
self.ui.home_view.section = home::Section::Templates;
}
"home.gettingStarted" => {
self.ui.home = true;
self.ui.home_view.section = home::Section::Recents;
self.services.save_prefs(&self.session.prefs);
}
"dialog.saveAs" => self.save_as_dialog(),
"dialog.colorProfile" => self.ui.color_profile = self.session.doc().ok().map(|d| d.color_profile),
"present" => {
@ -1116,6 +1130,7 @@ impl FigApp {
"home": self.ui.home,
"online": online::inspect(self),
"tabs": self.session.docs.iter().map(|d| json!({"name": d.doc.name, "live": tabs::live_mark(self, d.uid).map(|shared| if shared { "shared" } else { "private" })})).collect::<Vec<_>>(),
"homeSection": self.ui.home_view.section.id(),
"dark": self.ui.dark,
"focus": {"region": self.ui.nav.region.id(), "keyboard": self.ui.nav.keyboard},
"reduceMotion": self.reduce_motion(),

View File

@ -28,8 +28,6 @@ const ROW: f32 = 36.0;
/// UI-only state of the Online section.
#[derive(Default)]
pub struct OnlineUi {
/// The Online section shows on the home screen (else Recents).
pub open: bool,
/// The frame the section last drew in: it asks the servers when it shows again after a gap.
drawn: Option<u64>,
/// Remove from server… was chosen for this file (server, id, name): its confirmation shows.
@ -318,7 +316,7 @@ pub fn inspect(app: &FigApp) -> Value {
.map(|f| json!({"file": f.file, "name": f.name, "detail": detail(f), "shared": f.shared.is_some(), "state": f.state, "hasOwnerToken": true}))
.collect();
json!({
"open": app.ui.online.open,
"open": app.ui.home_view.section == crate::home::Section::Online,
"files": files,
"checking": app.session.waiting_for(FILES),
"problems": app.session.live.servers.len(),

View File

@ -190,7 +190,12 @@ pub fn show(app: &mut FigApp, ui: &mut Ui) {
ui.painter().circle_filled(hr.center(), 16.0, t.panel);
ui.painter().text(hr.center(), Align2::CENTER_CENTER, "?", crate::theme::regular(15.0), t.text);
crate::right::menu_popup(&hresp).show(|ui| {
for (l, c) in [(tr("Keyboard shortcuts"), "help.shortcuts"), (tr("Help page"), "help.page"), (tr("About FigCraft"), "help.about")] {
for (l, c) in [
(tr("Getting started"), "help.gettingStarted"),
(tr("Keyboard shortcuts"), "help.shortcuts"),
(tr("Help page"), "help.page"),
(tr("About FigCraft"), "help.about"),
] {
if crate::right::menu_item(ui, l, crate::shortcuts::shortcut(c).unwrap_or("")) {
app.run(c, json!({}));
ui.close();