Merge pull request #7085 from dotCooCoo/feat/learn-next-up

Learn: View more opens a tutorial's practice sample, and Next Up shows the next tutorial
This commit is contained in:
BFlat
2026-11-13 23:05:29 +07:00
committed by GitHub
38 changed files with 930 additions and 17 deletions
+1 -1
View File
@@ -20,7 +20,7 @@ Every non-code asset in this repository (icons, images, fonts, example art, colo
**Policy (mandatory):** VectorCraft contains **no Adobe iconography, images, artwork, presets, swatch/brush/symbol/pattern libraries or ICC profiles**. Every asset is either original work by VectorCraft contributors, or third-party material under an open licence (OSI open source, public domain / CC0, or Creative Commons that allows redistribution), and it is attributed below. Screenshots of Adobe software are never committed. The one exception is the ArtCraft name, wordmark and logos in `docs/brand/`: ArtCraft trademarks, not open source, usable only under [`docs/brand/LICENSE-brand.txt`](docs/brand/LICENSE-brand.txt).
Generated-in-code art is original and has no file to list. This covers the default swatches, brushes, symbols, patterns, graphic styles, image-trace presets and the vector tool cursors (`crates/ui-egui/src/cursors.rs`).
Generated-in-code art is original and has no file to list. This covers the default swatches, brushes, symbols, patterns, graphic styles, image-trace presets, the vector tool cursors (`crates/ui-egui/src/cursors.rs`) and the Learn panel's practice samples (`crates/ui-egui/src/panels/learn/samples.rs`).
**Font files are never committed here.** Fonts shared by the Crafting Apps live in [storytold/craft-fonts](https://github.com/storytold/craft-fonts). The small Latin UI/document fonts below predate that rule and stay.
+24
View File
@@ -646,6 +646,12 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
"",
"{} the Learn panel's skills in the order its Skills tab lists them: {skills: [{id, title, tutorials: [{id, title, steps: number of steps}]}]}",
),
(
"learn.skills",
"Back to Skills",
"",
"{} show the Learn panel on its Skills tab with the list of skills, with no skill and no tutorial open, as Back to skills after a skill's last tutorial and the Quick skill guide card do; returns what learn.list returns",
),
(
"learn.show",
"Show Tutorial",
@@ -658,6 +664,24 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
"",
"{tool: a tool id, as tool.select takes} mark where that tool is for 3 seconds without choosing it, as the link on a tool's name in a Learn tutorial step does: its toolbar button, a row of its collapsed group's flyout (which opens), its group's torn-off strip (which comes to the front), or, when no shown toolbar holds it, its row in the All Tools drawer (which opens); a Tools panel hidden with Window › Tools shows again; Escape or the next locate removes the mark; a missing, non-string or unknown id is an error and changes nothing; returns {tool, location: toolbar|flyout|strip|allTools|unavailable, toolbar: the name of the toolbar made with New Toolbar that holds it, or null}, with reason when Presentation Mode or Tab hides the toolbars",
),
(
"learn.openSample",
"Open Tutorial Sample",
"",
"{tutorial: id (see learn.list)} open that tutorial's practice sample as a new untitled document, which becomes the active one, and move the Learn view to the tutorial, as its View more button does; each call opens another copy and open documents stay as they are; a missing, non-string or unknown id is an error and changes nothing; returns {tutorial, index, title} of the new document",
),
(
"learn.finish",
"Finish Tutorial",
"",
"{tutorial: id (see learn.list)} mark a tutorial whose sample learn.openSample opened as finished, as its Finish tutorial button does; finishing it again keeps one entry; a missing, non-string or unknown id, or a tutorial whose sample was not opened, is an error and changes nothing; returns {tutorial, finished: true, next: id of the next tutorial in its skill, or null after the skill's last one}",
),
(
"learn.next",
"Next Tutorial",
"",
"{tutorial: id (see learn.list)} show the Learn panel on the tutorial after a finished one in the same skill, as its Next Up button does, without opening a sample; a missing, non-string or unknown id, a tutorial not finished, or a skill's last tutorial is an error and changes nothing; returns {skill, tutorial, title} of the next tutorial",
),
("window.brightness", "UI Brightness", "", "{brightness: dark|mediumDark|mediumLight|light}"),
(
"window.workspace",
+13
View File
@@ -6751,6 +6751,19 @@ breakText Glyphs Глифи
Show where this tool is Показване къде е този инструмент
@msg missing `tutorial` (see learn.list) липсва `tutorial` (вижте learn.list)
@msg unknown tutorial `{id}` (see learn.list) непознат урок `{id}` (вижте learn.list)
View more Вижте повече
Finish tutorial Завършване на урока
Next Up Следващ
You finished the last tutorial in this skill. Завършихте последния урок в това умение.
Back to skills Обратно към уменията
Open Tutorial Sample Отваряне на примерния файл на урока
Finish Tutorial Завършване на урок
Next Tutorial Следващ урок
Back to Skills Обратно към уменията
@msg tutorial `{id}` has no practice sample урокът `{id}` няма примерен файл за упражнение
@msg open the practice sample of `{id}` first (see learn.openSample) първо отворете примерния файл за упражнение на `{id}` (вижте learn.openSample)
@msg finish `{id}` first (see learn.finish) първо завършете `{id}` (вижте learn.finish)
@msg `{id}` is the last tutorial in its skill `{id}` е последният урок в своето умение
# Deferred plug-in effects
Refresh Deferred Effects Обновяване на отложените ефекти
Can't render this file because it contains an unexpected character in line 2 and column 73.
+13
View File
@@ -6805,6 +6805,19 @@ shape Open Otvoren
Show where this tool is Prikaži gdje je ovaj alat
@msg missing `tutorial` (see learn.list) nedostaje `tutorial` (vidi learn.list)
@msg unknown tutorial `{id}` (see learn.list) nepoznat tutorijal `{id}` (vidi learn.list)
View more Prikaži više
Finish tutorial Završi tutorijal
Next Up Sljedeće
You finished the last tutorial in this skill. Završili ste posljednji tutorijal u ovoj vještini.
Back to skills Nazad na vještine
Open Tutorial Sample Otvori uzorak tutorijala
Finish Tutorial Završi tutorijal
Next Tutorial Sljedeći tutorijal
Back to Skills Nazad na vještine
@msg tutorial `{id}` has no practice sample tutorijal `{id}` nema uzorak za vježbu
@msg open the practice sample of `{id}` first (see learn.openSample) prvo otvorite uzorak za vježbu tutorijala `{id}` (vidi learn.openSample)
@msg finish `{id}` first (see learn.finish) prvo završite `{id}` (vidi learn.finish)
@msg `{id}` is the last tutorial in its skill `{id}` je posljednji tutorijal u svojoj vještini
# Deferred plug-in effects
Refresh Deferred Effects Osvježi odgođene efekte
Can't render this file because it contains an unexpected character in line 2 and column 73.
+13
View File
@@ -7328,6 +7328,19 @@ breakText Glyphs Glifs
Show where this tool is Mostra on és aquesta eina
@msg missing `tutorial` (see learn.list) falta `tutorial` (vegeu learn.list)
@msg unknown tutorial `{id}` (see learn.list) tutorial desconegut `{id}` (vegeu learn.list)
View more Mostra’n més
Finish tutorial Acaba el tutorial
Next Up A continuació
You finished the last tutorial in this skill. Heu acabat l’últim tutorial d’aquesta habilitat.
Back to skills Torna a les habilitats
Open Tutorial Sample Obre la mostra del tutorial
Finish Tutorial Acaba el tutorial
Next Tutorial Tutorial següent
Back to Skills Torna a les habilitats
@msg tutorial `{id}` has no practice sample el tutorial `{id}` no té cap mostra de pràctica
@msg open the practice sample of `{id}` first (see learn.openSample) obriu primer la mostra de pràctica de `{id}` (vegeu learn.openSample)
@msg finish `{id}` first (see learn.finish) acabeu primer `{id}` (vegeu learn.finish)
@msg `{id}` is the last tutorial in its skill `{id}` és l’últim tutorial de la seva habilitat
# Deferred plug-in effects
Refresh Deferred Effects Actualitza els efectes ajornats
Can't render this file because it contains an unexpected character in line 2 and column 73.
+13
View File
@@ -7347,6 +7347,19 @@ breakText Glyphs Glyfy
Show where this tool is Zobrazit, kde je tento nástroj
@msg missing `tutorial` (see learn.list) chybí `tutorial` (viz learn.list)
@msg unknown tutorial `{id}` (see learn.list) neznámý výukový kurz `{id}` (viz learn.list)
View more Zobrazit více
Finish tutorial Dokončit výukový kurz
Next Up Další
You finished the last tutorial in this skill. Dokončili jste poslední výukový kurz této dovednosti.
Back to skills Zpět na dovednosti
Open Tutorial Sample Otevřít ukázku výukového kurzu
Finish Tutorial Dokončit výukový kurz
Next Tutorial Další výukový kurz
Back to Skills Zpět na dovednosti
@msg tutorial `{id}` has no practice sample výukový kurz `{id}` nemá cvičnou ukázku
@msg open the practice sample of `{id}` first (see learn.openSample) nejprve otevřete cvičnou ukázku kurzu `{id}` (viz learn.openSample)
@msg finish `{id}` first (see learn.finish) nejprve dokončete `{id}` (viz learn.finish)
@msg `{id}` is the last tutorial in its skill `{id}` je poslední výukový kurz své dovednosti
# Deferred plug-in effects
Refresh Deferred Effects Obnovit odložené efekty
Can't render this file because it contains an unexpected character in line 2 and column 73.
+13
View File
@@ -6767,6 +6767,19 @@ breakText Glyphs Glyffer
Show where this tool is Vis, hvor dette værktøj er
@msg missing `tutorial` (see learn.list) `tutorial` mangler (se learn.list)
@msg unknown tutorial `{id}` (see learn.list) ukendt vejledning `{id}` (se learn.list)
View more Vis mere
Finish tutorial Afslut vejledning
Next Up Næste
You finished the last tutorial in this skill. Du har afsluttet den sidste vejledning i denne færdighed.
Back to skills Tilbage til færdigheder
Open Tutorial Sample Åbn eksempel til vejledning
Finish Tutorial Afslut vejledning
Next Tutorial Næste vejledning
Back to Skills Tilbage til færdigheder
@msg tutorial `{id}` has no practice sample vejledningen `{id}` har intet øvelseseksempel
@msg open the practice sample of `{id}` first (see learn.openSample) åbn først øvelseseksemplet til `{id}` (se learn.openSample)
@msg finish `{id}` first (see learn.finish) afslut først `{id}` (se learn.finish)
@msg `{id}` is the last tutorial in its skill `{id}` er den sidste vejledning i sin færdighed
# Deferred plug-in effects
Refresh Deferred Effects Opdatér udskudte effekter
Can't render this file because it contains an unexpected character in line 2 and column 73.
+13
View File
@@ -6942,6 +6942,19 @@ breakText Glyphs Glyphen
Show where this tool is Zeigen, wo sich dieses Werkzeug befindet
@msg missing `tutorial` (see learn.list) `tutorial` fehlt (siehe learn.list)
@msg unknown tutorial `{id}` (see learn.list) unbekanntes Lernprogramm `{id}` (siehe learn.list)
View more Mehr anzeigen
Finish tutorial Lernprogramm abschließen
Next Up Als Nächstes
You finished the last tutorial in this skill. Sie haben das letzte Lernprogramm dieser Fertigkeit abgeschlossen.
Back to skills Zurück zu den Fertigkeiten
Open Tutorial Sample Übungsdatei des Lernprogramms öffnen
Finish Tutorial Lernprogramm abschließen
Next Tutorial Nächstes Lernprogramm
Back to Skills Zurück zu den Fertigkeiten
@msg tutorial `{id}` has no practice sample das Lernprogramm `{id}` hat keine Übungsdatei
@msg open the practice sample of `{id}` first (see learn.openSample) öffnen Sie zuerst die Übungsdatei von `{id}` (siehe learn.openSample)
@msg finish `{id}` first (see learn.finish) schließen Sie zuerst `{id}` ab (siehe learn.finish)
@msg `{id}` is the last tutorial in its skill `{id}` ist das letzte Lernprogramm seiner Fertigkeit
# Deferred plug-in effects
Refresh Deferred Effects Zurückgestellte Effekte aktualisieren
Can't render this file because it contains an unexpected character in line 2 and column 73.
+13
View File
@@ -7299,6 +7299,19 @@ breakText Glyphs Γλύφοι
Show where this tool is Εμφάνιση της θέσης αυτού του εργαλείου
@msg missing `tutorial` (see learn.list) λείπει το `tutorial` (δείτε learn.list)
@msg unknown tutorial `{id}` (see learn.list) άγνωστος οδηγός εκμάθησης `{id}` (δείτε learn.list)
View more Προβολή περισσότερων
Finish tutorial Ολοκλήρωση οδηγού
Next Up Επόμενο
You finished the last tutorial in this skill. Ολοκληρώσατε τον τελευταίο οδηγό εκμάθησης αυτής της δεξιότητας.
Back to skills Πίσω στις δεξιότητες
Open Tutorial Sample Άνοιγμα δείγματος οδηγού εκμάθησης
Finish Tutorial Ολοκλήρωση οδηγού εκμάθησης
Next Tutorial Επόμενος οδηγός εκμάθησης
Back to Skills Πίσω στις δεξιότητες
@msg tutorial `{id}` has no practice sample ο οδηγός εκμάθησης `{id}` δεν έχει δείγμα εξάσκησης
@msg open the practice sample of `{id}` first (see learn.openSample) ανοίξτε πρώτα το δείγμα εξάσκησης του `{id}` (δείτε learn.openSample)
@msg finish `{id}` first (see learn.finish) ολοκληρώστε πρώτα το `{id}` (δείτε learn.finish)
@msg `{id}` is the last tutorial in its skill το `{id}` είναι ο τελευταίος οδηγός εκμάθησης της δεξιότητάς του
# Deferred plug-in effects
Refresh Deferred Effects Ανανέωση εφέ σε αναμονή
Can't render this file because it is too large.
+13
View File
@@ -7290,6 +7290,19 @@ breakText Glyphs Glifos
Show where this tool is Mostrar dónde está esta herramienta
@msg missing `tutorial` (see learn.list) falta `tutorial` (consulta learn.list)
@msg unknown tutorial `{id}` (see learn.list) tutorial desconocido `{id}` (consulta learn.list)
View more Ver más
Finish tutorial Terminar tutorial
Next Up A continuación
You finished the last tutorial in this skill. Has terminado el último tutorial de esta habilidad.
Back to skills Volver a las habilidades
Open Tutorial Sample Abrir muestra del tutorial
Finish Tutorial Terminar tutorial
Next Tutorial Tutorial siguiente
Back to Skills Volver a las habilidades
@msg tutorial `{id}` has no practice sample el tutorial `{id}` no tiene muestra de práctica
@msg open the practice sample of `{id}` first (see learn.openSample) abre primero la muestra de práctica de `{id}` (consulta learn.openSample)
@msg finish `{id}` first (see learn.finish) termina primero `{id}` (consulta learn.finish)
@msg `{id}` is the last tutorial in its skill `{id}` es el último tutorial de su habilidad
# Deferred plug-in effects
Refresh Deferred Effects Actualizar efectos aplazados
Can't render this file because it contains an unexpected character in line 2 and column 73.
+13
View File
@@ -7382,6 +7382,19 @@ breakText Glyphs Merkit
Show where this tool is Näytä, missä tämä työkalu on
@msg missing `tutorial` (see learn.list) `tutorial` puuttuu (ks. learn.list)
@msg unknown tutorial `{id}` (see learn.list) tuntematon opetusohjelma `{id}` (ks. learn.list)
View more Näytä lisää
Finish tutorial Viimeistele opetusohjelma
Next Up Seuraavaksi
You finished the last tutorial in this skill. Viimeistelit tämän taidon viimeisen opetusohjelman.
Back to skills Takaisin taitoihin
Open Tutorial Sample Avaa opetusohjelman harjoitustiedosto
Finish Tutorial Viimeistele opetusohjelma
Next Tutorial Seuraava opetusohjelma
Back to Skills Takaisin taitoihin
@msg tutorial `{id}` has no practice sample opetusohjelmalla `{id}` ei ole harjoitustiedostoa
@msg open the practice sample of `{id}` first (see learn.openSample) avaa ensin opetusohjelman `{id}` harjoitustiedosto (ks. learn.openSample)
@msg finish `{id}` first (see learn.finish) viimeistele ensin `{id}` (ks. learn.finish)
@msg `{id}` is the last tutorial in its skill `{id}` on taitonsa viimeinen opetusohjelma
# Deferred plug-in effects
Refresh Deferred Effects Päivitä lykätyt tehosteet
Can't render this file because it contains an unexpected character in line 2 and column 73.
+13
View File
@@ -7342,6 +7342,19 @@ breakText Glyphs Glyphes
Show where this tool is Afficher l'emplacement de cet outil
@msg missing `tutorial` (see learn.list) `tutorial` manquant (voir learn.list)
@msg unknown tutorial `{id}` (see learn.list) tutoriel inconnu `{id}` (voir learn.list)
View more Voir plus
Finish tutorial Terminer le tutoriel
Next Up À suivre
You finished the last tutorial in this skill. Vous avez terminé le dernier tutoriel de cette compétence.
Back to skills Retour aux compétences
Open Tutorial Sample Ouvrir le fichier d’exercice du tutoriel
Finish Tutorial Terminer le tutoriel
Next Tutorial Tutoriel suivant
Back to Skills Retour aux compétences
@msg tutorial `{id}` has no practice sample le tutoriel `{id}` n’a pas de fichier d’exercice
@msg open the practice sample of `{id}` first (see learn.openSample) ouvrez d’abord le fichier d’exercice de `{id}` (voir learn.openSample)
@msg finish `{id}` first (see learn.finish) terminez d’abord `{id}` (voir learn.finish)
@msg `{id}` is the last tutorial in its skill `{id}` est le dernier tutoriel de sa compétence
# Deferred plug-in effects
Refresh Deferred Effects Actualiser les effets différés
Can't render this file because it contains an unexpected character in line 2 and column 73.
+13
View File
@@ -8740,6 +8740,19 @@ theme Appearance अपीयरेंस
Show where this tool is दिखाएँ कि यह टूल कहाँ है
@msg missing `tutorial` (see learn.list) लापता `tutorial` (learn.list देखें)
@msg unknown tutorial `{id}` (see learn.list) अज्ञात ट्यूटोरियल `{id}` (learn.list देखें)
View more और देखें
Finish tutorial ट्यूटोरियल पूरा करें
Next Up आगे
You finished the last tutorial in this skill. आपने इस कौशल का अंतिम ट्यूटोरियल पूरा कर लिया है।
Back to skills कौशल पर वापस जाएँ
Open Tutorial Sample ट्यूटोरियल का नमूना खोलें
Finish Tutorial ट्यूटोरियल पूरा करें
Next Tutorial अगला ट्यूटोरियल
Back to Skills कौशल पर वापस जाएँ
@msg tutorial `{id}` has no practice sample ट्यूटोरियल `{id}` का कोई अभ्यास नमूना नहीं है
@msg open the practice sample of `{id}` first (see learn.openSample) पहले `{id}` का अभ्यास नमूना खोलें (learn.openSample देखें)
@msg finish `{id}` first (see learn.finish) पहले `{id}` पूरा करें (learn.finish देखें)
@msg `{id}` is the last tutorial in its skill `{id}` अपने कौशल का अंतिम ट्यूटोरियल है
# Deferred plug-in effects
Refresh Deferred Effects स्थगित इफ़ेक्ट्स रिफ्रेश करें
Can't render this file because it is too large.
+13
View File
@@ -7331,6 +7331,19 @@ breakText Glyphs Glifák
Show where this tool is Megmutatja, hol van ez az eszköz
@msg missing `tutorial` (see learn.list) hiányzik: `tutorial` (lásd: learn.list)
@msg unknown tutorial `{id}` (see learn.list) ismeretlen oktatóanyag: `{id}` (lásd: learn.list)
View more Továbbiak
Finish tutorial Oktatóanyag befejezése
Next Up Következő
You finished the last tutorial in this skill. Befejezte a készség utolsó oktatóanyagát.
Back to skills Vissza a készségekhez
Open Tutorial Sample Oktatóanyag mintafájljának megnyitása
Finish Tutorial Oktatóanyag befejezése
Next Tutorial Következő oktatóanyag
Back to Skills Vissza a készségekhez
@msg tutorial `{id}` has no practice sample nincs gyakorló mintafájl ehhez az oktatóanyaghoz: `{id}`
@msg open the practice sample of `{id}` first (see learn.openSample) előbb nyissa meg a gyakorló mintafájlt ehhez: `{id}` (lásd: learn.openSample)
@msg finish `{id}` first (see learn.finish) előbb fejezze be ezt: `{id}` (lásd: learn.finish)
@msg `{id}` is the last tutorial in its skill a készsége utolsó oktatóanyaga: `{id}`
# Deferred plug-in effects
Refresh Deferred Effects Elhalasztott effektusok frissítése
Can't render this file because it contains an unexpected character in line 2 and column 73.
+13
View File
@@ -6767,6 +6767,19 @@ breakText Glyphs Glif
Show where this tool is Tampilkan letak alat ini
@msg missing `tutorial` (see learn.list) `tutorial` tidak ada (lihat learn.list)
@msg unknown tutorial `{id}` (see learn.list) tutorial `{id}` tidak dikenal (lihat learn.list)
View more Lihat selengkapnya
Finish tutorial Selesaikan tutorial
Next Up Berikutnya
You finished the last tutorial in this skill. Anda telah menyelesaikan tutorial terakhir dalam keterampilan ini.
Back to skills Kembali ke keterampilan
Open Tutorial Sample Buka contoh tutorial
Finish Tutorial Selesaikan tutorial
Next Tutorial Tutorial berikutnya
Back to Skills Kembali ke keterampilan
@msg tutorial `{id}` has no practice sample tutorial `{id}` tidak memiliki contoh latihan
@msg open the practice sample of `{id}` first (see learn.openSample) buka contoh latihan `{id}` terlebih dahulu (lihat learn.openSample)
@msg finish `{id}` first (see learn.finish) selesaikan `{id}` terlebih dahulu (lihat learn.finish)
@msg `{id}` is the last tutorial in its skill `{id}` adalah tutorial terakhir dalam keterampilannya
# Deferred plug-in effects
Refresh Deferred Effects Segarkan efek yang ditunda
Can't render this file because it contains an unexpected character in line 2418 and column 5.
+13
View File
@@ -6976,6 +6976,19 @@ breakText Glyphs Glifi
Show where this tool is Mostra dove si trova questo strumento
@msg missing `tutorial` (see learn.list) manca `tutorial` (vedi learn.list)
@msg unknown tutorial `{id}` (see learn.list) tutorial sconosciuto `{id}` (vedi learn.list)
View more Mostra altro
Finish tutorial Termina il tutorial
Next Up Prossimo
You finished the last tutorial in this skill. Hai terminato l’ultimo tutorial di questa competenza.
Back to skills Torna alle competenze
Open Tutorial Sample Apri l’esempio del tutorial
Finish Tutorial Termina il tutorial
Next Tutorial Tutorial successivo
Back to Skills Torna alle competenze
@msg tutorial `{id}` has no practice sample il tutorial `{id}` non ha un esempio di esercitazione
@msg open the practice sample of `{id}` first (see learn.openSample) apri prima l’esempio di esercitazione di `{id}` (vedi learn.openSample)
@msg finish `{id}` first (see learn.finish) termina prima `{id}` (vedi learn.finish)
@msg `{id}` is the last tutorial in its skill `{id}` è l’ultimo tutorial della sua competenza
# Deferred plug-in effects
Refresh Deferred Effects Aggiorna effetti rimandati
Can't render this file because it contains an unexpected character in line 2 and column 73.
+13
View File
@@ -7041,6 +7041,19 @@ breakText Glyphs 字形
Show where this tool is このツールの場所を表示
@msg missing `tutorial` (see learn.list) `tutorial` が指定されていません (learn.list を参照)
@msg unknown tutorial `{id}` (see learn.list) `{id}` は不明なチュートリアルです (learn.list を参照)
View more 詳細を見る
Finish tutorial チュートリアルを完了
Next Up 次へ
You finished the last tutorial in this skill. このスキルの最後のチュートリアルを完了しました。
Back to skills スキルに戻る
Open Tutorial Sample チュートリアルのサンプルを開く
Finish Tutorial チュートリアルを完了
Next Tutorial 次のチュートリアル
Back to Skills スキルに戻る
@msg tutorial `{id}` has no practice sample チュートリアル `{id}` には練習用サンプルがありません
@msg open the practice sample of `{id}` first (see learn.openSample) 先に `{id}` の練習用サンプルを開いてください (learn.openSample を参照)
@msg finish `{id}` first (see learn.finish) 先に `{id}` を完了してください (learn.finish を参照)
@msg `{id}` is the last tutorial in its skill `{id}` はこのスキルの最後のチュートリアルです
# Deferred plug-in effects
Refresh Deferred Effects 保留中の効果を更新
Can't render this file because it contains an unexpected character in line 2 and column 73.
+13
View File
@@ -7461,6 +7461,19 @@ font-script Japanese იაპონური
Show where this tool is ამ ხელსაწყოს მდებარეობის ჩვენება
@msg missing `tutorial` (see learn.list) აკლია `tutorial` (იხ. learn.list)
@msg unknown tutorial `{id}` (see learn.list) უცნობი გაკვეთილი `{id}` (იხ. learn.list)
View more მეტის ნახვა
Finish tutorial გაკვეთილის დასრულება
Next Up შემდეგი
You finished the last tutorial in this skill. თქვენ დაასრულეთ ამ უნარის ბოლო გაკვეთილი.
Back to skills უნარებზე დაბრუნება
Open Tutorial Sample გაკვეთილის ნიმუშის გახსნა
Finish Tutorial გაკვეთილის დასრულება
Next Tutorial შემდეგი გაკვეთილი
Back to Skills უნარებზე დაბრუნება
@msg tutorial `{id}` has no practice sample გაკვეთილს `{id}` არ აქვს სავარჯიშო ნიმუში
@msg open the practice sample of `{id}` first (see learn.openSample) ჯერ გახსენით `{id}` გაკვეთილის სავარჯიშო ნიმუში (იხ. learn.openSample)
@msg finish `{id}` first (see learn.finish) ჯერ დაასრულეთ `{id}` (იხ. learn.finish)
@msg `{id}` is the last tutorial in its skill `{id}` თავისი უნარის ბოლო გაკვეთილია
# Deferred plug-in effects
Refresh Deferred Effects გადადებული ეფექტების განახლება
Can't render this file because it is too large.
+13
View File
@@ -6937,6 +6937,19 @@ breakText Glyphs 글리프
Show where this tool is 이 도구의 위치 표시
@msg missing `tutorial` (see learn.list) `tutorial`이 없습니다(learn.list 참조)
@msg unknown tutorial `{id}` (see learn.list) 알 수 없는 튜토리얼 `{id}`(learn.list 참조)
View more 자세히 보기
Finish tutorial 튜토리얼 완료
Next Up 다음
You finished the last tutorial in this skill. 이 기술의 마지막 튜토리얼을 완료했습니다.
Back to skills 기술로 돌아가기
Open Tutorial Sample 튜토리얼 샘플 열기
Finish Tutorial 튜토리얼 완료
Next Tutorial 다음 튜토리얼
Back to Skills 기술로 돌아가기
@msg tutorial `{id}` has no practice sample 튜토리얼 `{id}`에 연습용 샘플이 없습니다
@msg open the practice sample of `{id}` first (see learn.openSample) 먼저 `{id}`의 연습용 샘플을 여세요(learn.openSample 참조)
@msg finish `{id}` first (see learn.finish) 먼저 `{id}`을(를) 완료하세요(learn.finish 참조)
@msg `{id}` is the last tutorial in its skill `{id}`은(는) 해당 기술의 마지막 튜토리얼입니다
# Deferred plug-in effects
Refresh Deferred Effects 보류된 효과 새로 고침
Can't render this file because it contains an unexpected character in line 2 and column 73.
+13
View File
@@ -7380,6 +7380,19 @@ breakText Glyphs Glyphs
Show where this tool is Tonen waar dit gereedschap zich bevindt
@msg missing `tutorial` (see learn.list) `tutorial` ontbreekt (zie learn.list)
@msg unknown tutorial `{id}` (see learn.list) onbekende tutorial `{id}` (zie learn.list)
View more Meer weergeven
Finish tutorial Tutorial voltooien
Next Up Hierna
You finished the last tutorial in this skill. Je hebt de laatste tutorial van deze vaardigheid voltooid.
Back to skills Terug naar vaardigheden
Open Tutorial Sample Oefenbestand van tutorial openen
Finish Tutorial Tutorial voltooien
Next Tutorial Volgende tutorial
Back to Skills Terug naar vaardigheden
@msg tutorial `{id}` has no practice sample tutorial `{id}` heeft geen oefenbestand
@msg open the practice sample of `{id}` first (see learn.openSample) open eerst het oefenbestand van `{id}` (zie learn.openSample)
@msg finish `{id}` first (see learn.finish) voltooi eerst `{id}` (zie learn.finish)
@msg `{id}` is the last tutorial in its skill `{id}` is de laatste tutorial van zijn vaardigheid
# Deferred plug-in effects
Refresh Deferred Effects Uitgestelde effecten vernieuwen
Can't render this file because it contains an unexpected character in line 2 and column 73.
+13
View File
@@ -7203,6 +7203,19 @@ breakText Glyphs Glify
Show where this tool is Pokaż, gdzie jest to narzędzie
@msg missing `tutorial` (see learn.list) brak `tutorial` (patrz learn.list)
@msg unknown tutorial `{id}` (see learn.list) nieznany samouczek `{id}` (patrz learn.list)
View more Pokaż więcej
Finish tutorial Zakończ samouczek
Next Up Następny
You finished the last tutorial in this skill. Ukończono ostatni samouczek tej umiejętności.
Back to skills Powrót do umiejętności
Open Tutorial Sample Otwórz przykład samouczka
Finish Tutorial Zakończ samouczek
Next Tutorial Następny samouczek
Back to Skills Powrót do umiejętności
@msg tutorial `{id}` has no practice sample samouczek `{id}` nie ma przykładu do ćwiczeń
@msg open the practice sample of `{id}` first (see learn.openSample) najpierw otwórz przykład do ćwiczeń samouczka `{id}` (patrz learn.openSample)
@msg finish `{id}` first (see learn.finish) najpierw zakończ `{id}` (patrz learn.finish)
@msg `{id}` is the last tutorial in its skill `{id}` to ostatni samouczek swojej umiejętności
# Deferred plug-in effects
Refresh Deferred Effects Odśwież odroczone efekty
Can't render this file because it contains an unexpected character in line 2 and column 73.
+13
View File
@@ -7420,6 +7420,19 @@ swatch Yellow Green Verde-amarelado
Show where this tool is Mostrar onde está esta ferramenta
@msg missing `tutorial` (see learn.list) falta `tutorial` (veja learn.list)
@msg unknown tutorial `{id}` (see learn.list) tutorial desconhecido `{id}` (veja learn.list)
View more Ver mais
Finish tutorial Concluir tutorial
Next Up A seguir
You finished the last tutorial in this skill. Você concluiu o último tutorial desta habilidade.
Back to skills Voltar às habilidades
Open Tutorial Sample Abrir Amostra do Tutorial
Finish Tutorial Concluir Tutorial
Next Tutorial Próximo Tutorial
Back to Skills Voltar às Habilidades
@msg tutorial `{id}` has no practice sample o tutorial `{id}` não tem amostra de prática
@msg open the practice sample of `{id}` first (see learn.openSample) abra primeiro a amostra de prática de `{id}` (veja learn.openSample)
@msg finish `{id}` first (see learn.finish) conclua primeiro `{id}` (veja learn.finish)
@msg `{id}` is the last tutorial in its skill `{id}` é o último tutorial da sua habilidade
# Deferred plug-in effects
Refresh Deferred Effects Atualizar Efeitos Adiados
Can't render this file because it contains an unexpected character in line 6 and column 47.
+13
View File
@@ -7436,6 +7436,19 @@ shape Open Открытый
Show where this tool is Показать, где находится этот инструмент
@msg missing `tutorial` (see learn.list) отсутствует `tutorial` (см. learn.list)
@msg unknown tutorial `{id}` (see learn.list) неизвестный урок `{id}` (см. learn.list)
View more Подробнее
Finish tutorial Завершить урок
Next Up Далее
You finished the last tutorial in this skill. Вы завершили последний урок этого навыка.
Back to skills Назад к навыкам
Open Tutorial Sample Открыть пример урока
Finish Tutorial Завершить урок
Next Tutorial Следующий урок
Back to Skills Назад к навыкам
@msg tutorial `{id}` has no practice sample у урока `{id}` нет учебного примера
@msg open the practice sample of `{id}` first (see learn.openSample) сначала откройте учебный пример урока `{id}` (см. learn.openSample)
@msg finish `{id}` first (see learn.finish) сначала завершите `{id}` (см. learn.finish)
@msg `{id}` is the last tutorial in its skill `{id}` является последним уроком своего навыка
# Deferred plug-in effects
Refresh Deferred Effects Обновить отложенные эффекты
Can't render this file because it is too large.
+13
View File
@@ -8786,6 +8786,19 @@ digits Hindi Hindi
Show where this tool is Visa var det här verktyget finns
@msg missing `tutorial` (see learn.list) `tutorial` saknas (se learn.list)
@msg unknown tutorial `{id}` (see learn.list) okänd självstudie `{id}` (se learn.list)
View more Visa mer
Finish tutorial Slutför självstudien
Next Up Nästa
You finished the last tutorial in this skill. Du har slutfört den sista självstudien i den här färdigheten.
Back to skills Tillbaka till färdigheter
Open Tutorial Sample Öppna övningsexempel för självstudie
Finish Tutorial Slutför självstudie
Next Tutorial Nästa självstudie
Back to Skills Tillbaka till färdigheter
@msg tutorial `{id}` has no practice sample självstudien `{id}` har inget övningsexempel
@msg open the practice sample of `{id}` first (see learn.openSample) öppna först övningsexemplet för `{id}` (se learn.openSample)
@msg finish `{id}` first (see learn.finish) slutför först `{id}` (se learn.finish)
@msg `{id}` is the last tutorial in its skill `{id}` är den sista självstudien i sin färdighet
# Deferred plug-in effects
Refresh Deferred Effects Uppdatera uppskjutna effekter
Can't render this file because it is too large.
+13
View File
@@ -7303,6 +7303,19 @@ breakText Glyphs Glifler
Show where this tool is Bu aracın nerede olduğunu göster
@msg missing `tutorial` (see learn.list) `tutorial` eksik (bkz. learn.list)
@msg unknown tutorial `{id}` (see learn.list) bilinmeyen öğretici `{id}` (bkz. learn.list)
View more Daha fazlasını gör
Finish tutorial Öğreticiyi bitir
Next Up Sıradaki
You finished the last tutorial in this skill. Bu becerinin son öğreticisini bitirdiniz.
Back to skills Becerilere dön
Open Tutorial Sample Öğretici Örneğini Aç
Finish Tutorial Öğreticiyi Bitir
Next Tutorial Sonraki Öğretici
Back to Skills Becerilere Dön
@msg tutorial `{id}` has no practice sample `{id}` öğreticisinin alıştırma örneği yok
@msg open the practice sample of `{id}` first (see learn.openSample) önce `{id}` öğreticisinin alıştırma örneğini açın (bkz. learn.openSample)
@msg finish `{id}` first (see learn.finish) önce `{id}` öğreticisini bitirin (bkz. learn.finish)
@msg `{id}` is the last tutorial in its skill `{id}`, becerisinin son öğreticisidir
# Deferred plug-in effects
Refresh Deferred Effects Ertelenen Efektleri Yenile
Can't render this file because it contains an unexpected character in line 5 and column 25.
+13
View File
@@ -6956,6 +6956,19 @@ breakText Glyphs Гліфи
Show where this tool is Показати, де розташований цей інструмент
@msg missing `tutorial` (see learn.list) відсутній `tutorial` (див. learn.list)
@msg unknown tutorial `{id}` (see learn.list) невідомий урок `{id}` (див. learn.list)
View more Докладніше
Finish tutorial Завершити урок
Next Up Далі
You finished the last tutorial in this skill. Ви завершили останній урок цієї навички.
Back to skills Назад до навичок
Open Tutorial Sample Відкрити приклад уроку
Finish Tutorial Завершити урок
Next Tutorial Наступний урок
Back to Skills Назад до навичок
@msg tutorial `{id}` has no practice sample урок `{id}` не має навчального прикладу
@msg open the practice sample of `{id}` first (see learn.openSample) спочатку відкрийте навчальний приклад уроку `{id}` (див. learn.openSample)
@msg finish `{id}` first (see learn.finish) спочатку завершіть `{id}` (див. learn.finish)
@msg `{id}` is the last tutorial in its skill `{id}` є останнім уроком своєї навички
# Deferred plug-in effects
Refresh Deferred Effects Оновити відкладені ефекти
Can't render this file because it is too large.
+13
View File
@@ -6957,6 +6957,19 @@ breakText Glyphs Glyph
Show where this tool is Hiện vị trí của công cụ này
@msg missing `tutorial` (see learn.list) thiếu `tutorial` (xem learn.list)
@msg unknown tutorial `{id}` (see learn.list) hướng dẫn không xác định `{id}` (xem learn.list)
View more Xem thêm
Finish tutorial Hoàn tất hướng dẫn
Next Up Tiếp theo
You finished the last tutorial in this skill. Bạn đã hoàn tất hướng dẫn cuối cùng của kỹ năng này.
Back to skills Quay lại kỹ năng
Open Tutorial Sample Mở tệp mẫu của hướng dẫn
Finish Tutorial Hoàn tất hướng dẫn
Next Tutorial Hướng dẫn tiếp theo
Back to Skills Quay lại kỹ năng
@msg tutorial `{id}` has no practice sample hướng dẫn `{id}` không có tệp mẫu thực hành
@msg open the practice sample of `{id}` first (see learn.openSample) hãy mở tệp mẫu thực hành của `{id}` trước (xem learn.openSample)
@msg finish `{id}` first (see learn.finish) hãy hoàn tất `{id}` trước (xem learn.finish)
@msg `{id}` is the last tutorial in its skill `{id}` là hướng dẫn cuối cùng trong kỹ năng của nó
# Deferred plug-in effects
Refresh Deferred Effects Làm mới hiệu ứng bị hoãn
Can't render this file because it contains an unexpected character in line 204 and column 5.
+13
View File
@@ -7185,6 +7185,19 @@ breakText Glyphs 字形
Show where this tool is 显示此工具的位置
@msg missing `tutorial` (see learn.list) 缺少 `tutorial` (请参阅 learn.list)
@msg unknown tutorial `{id}` (see learn.list) 未知教程 `{id}` (请参阅 learn.list)
View more 查看更多
Finish tutorial 完成教程
Next Up 下一个
You finished the last tutorial in this skill. 你已完成此技能的最后一个教程。
Back to skills 返回技能
Open Tutorial Sample 打开教程示例
Finish Tutorial 完成教程
Next Tutorial 下一个教程
Back to Skills 返回技能
@msg tutorial `{id}` has no practice sample 教程 `{id}` 没有练习示例
@msg open the practice sample of `{id}` first (see learn.openSample) 请先打开 `{id}` 的练习示例 (请参阅 learn.openSample)
@msg finish `{id}` first (see learn.finish) 请先完成 `{id}` (请参阅 learn.finish)
@msg `{id}` is the last tutorial in its skill `{id}` 是其技能的最后一个教程
# Deferred plug-in effects
Refresh Deferred Effects 刷新延迟的效果
Can't render this file because it contains an unexpected character in line 2 and column 73.
+13
View File
@@ -6991,6 +6991,19 @@ breakText Glyphs 字符
Show where this tool is 顯示此工具的位置
@msg missing `tutorial` (see learn.list) 未提供 `tutorial` (請參閱 learn.list)
@msg unknown tutorial `{id}` (see learn.list) 未知的教學課程 `{id}` (請參閱 learn.list)
View more 檢視更多
Finish tutorial 完成教學課程
Next Up 接下來
You finished the last tutorial in this skill. 你已完成此技能的最後一個教學課程。
Back to skills 返回技能
Open Tutorial Sample 開啟教學課程範例
Finish Tutorial 完成教學課程
Next Tutorial 下一個教學課程
Back to Skills 返回技能
@msg tutorial `{id}` has no practice sample 教學課程 `{id}` 沒有練習範例
@msg open the practice sample of `{id}` first (see learn.openSample) 請先開啟 `{id}` 的練習範例 (請參閱 learn.openSample)
@msg finish `{id}` first (see learn.finish) 請先完成 `{id}` (請參閱 learn.finish)
@msg `{id}` is the last tutorial in its skill `{id}` 是其技能的最後一個教學課程
# Deferred plug-in effects
Refresh Deferred Effects 重新整理延後的效果
Can't render this file because it contains an unexpected character in line 6 and column 47.
+24
View File
@@ -647,6 +647,12 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
"",
"{} the Learn panel's skills in the order its Skills tab lists them: {skills: [{id, title, tutorials: [{id, title, steps: number of steps}]}]}",
),
(
"learn.skills",
"Back to Skills",
"",
"{} show the Learn panel on its Skills tab with the list of skills, with no skill and no tutorial open, as Back to skills after a skill's last tutorial and the Quick skill guide card do; returns what learn.list returns",
),
(
"learn.show",
"Show Tutorial",
@@ -659,6 +665,24 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
"",
"{tool: a tool id, as tool.select takes} mark where that tool is for 3 seconds without choosing it, as the link on a tool's name in a Learn tutorial step does: its toolbar button, a row of its collapsed group's flyout (which opens), its group's torn-off strip (which comes to the front), or, when no shown toolbar holds it, its row in the All Tools drawer (which opens); a Tools panel hidden with Window › Tools shows again; Escape or the next locate removes the mark; a missing, non-string or unknown id is an error and changes nothing; returns {tool, location: toolbar|flyout|strip|allTools|unavailable, toolbar: the name of the toolbar made with New Toolbar that holds it, or null}, with reason when Presentation Mode or Tab hides the toolbars",
),
(
"learn.openSample",
"Open Tutorial Sample",
"",
"{tutorial: id (see learn.list)} open that tutorial's practice sample as a new untitled document, which becomes the active one, and move the Learn view to the tutorial, as its View more button does; each call opens another copy and open documents stay as they are; a missing, non-string or unknown id is an error and changes nothing; returns {tutorial, index, title} of the new document",
),
(
"learn.finish",
"Finish Tutorial",
"",
"{tutorial: id (see learn.list)} mark a tutorial whose sample learn.openSample opened as finished, as its Finish tutorial button does; finishing it again keeps one entry; a missing, non-string or unknown id, or a tutorial whose sample was not opened, is an error and changes nothing; returns {tutorial, finished: true, next: id of the next tutorial in its skill, or null after the skill's last one}",
),
(
"learn.next",
"Next Tutorial",
"",
"{tutorial: id (see learn.list)} show the Learn panel on the tutorial after a finished one in the same skill, as its Next Up button does, without opening a sample; a missing, non-string or unknown id, a tutorial not finished, or a skill's last tutorial is an error and changes nothing; returns {skill, tutorial, title} of the next tutorial",
),
("window.brightness", "UI Brightness", "", "{brightness: dark|mediumDark|mediumLight|light}"),
(
"window.workspace",
+140 -12
View File
@@ -3,11 +3,16 @@
//! section with the Quick skill guide card, which opens the Skills tab. The Skills tab lists each
//! skill with an empty progress ring and its count of tutorials; a skill lists its tutorials, and a
//! tutorial previews its numbered steps. A tool's name in a step is a link that shows where the tool
//! is ([`locate`]). Back goes up one level. `learn.list` lists the tutorials, `learn.show` opens one,
//! `learn.locateTool` shows where a tool is and `window.learn` shows or hides the panel.
//! is ([`locate`]). Back goes up one level. Under the steps, View more opens the tutorial's practice
//! sample as a new untitled document; Finish tutorial then marks it finished, and Next Up shows the
//! next tutorial in the same skill. `learn.list` lists the tutorials, `learn.skills` shows the list
//! of skills, `learn.show` opens one tutorial, `learn.locateTool` shows where a tool is,
//! `learn.openSample`, `learn.finish` and `learn.next` run the three practice controls, and
//! `window.learn` shows or hides the panel.
pub mod catalog;
pub mod locate;
pub mod samples;
use egui::{Sense, Stroke, Ui, vec2};
use serde_json::{Value, json};
@@ -35,6 +40,14 @@ pub struct View {
pub tutorial: Option<&'static str>,
}
/// The tutorials whose samples View more opened, and the tutorials finished, in the order they
/// were finished. Each id is listed once.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Practice {
pub opened: Vec<&'static str>,
pub finished: Vec<&'static str>,
}
pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
ui.horizontal(|ui| {
for (tab, label) in [(Tab::ForYou, tl!("For you")), (Tab::Skills, tl!("Skills"))] {
@@ -75,8 +88,10 @@ fn for_you(app: &mut VectorcraftApp, ui: &mut Ui) {
});
let resp = ui.interact(card.response.rect, ui.scope_id().with("quick-skill-guide"), Sense::click());
resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, ui.is_enabled(), tl!("Quick skill guide")));
if resp.clicked() {
app.ui.learn = View { tab: Tab::Skills, skill: None, tutorial: None };
if resp.clicked()
&& let Err(e) = app.run("learn.skills", json!({}))
{
app.status(e);
}
}
@@ -89,6 +104,7 @@ fn skills(app: &mut VectorcraftApp, ui: &mut Ui) {
app.ui.learn.tutorial = None;
}
steps(app, ui, tutorial);
practice(app, ui, tutorial);
return;
}
if let Some(skill) = app.ui.learn.skill.and_then(catalog::skill) {
@@ -163,6 +179,50 @@ fn tool_link(ui: &mut Ui, t: &Tokens, name: &str) -> egui::Response {
resp.on_hover_cursor(egui::CursorIcon::PointingHand).on_hover_text(tl!("Show where this tool is"))
}
/// The controls under a tutorial's steps: View more; Finish tutorial once its sample is open; and
/// once it is finished, Next Up with the next tutorial's title, or for the skill's last tutorial a
/// line saying so and Back to skills.
fn practice(app: &mut VectorcraftApp, ui: &mut Ui, tutorial: &'static catalog::Tutorial) {
let t = Tokens::get(ui.ctx());
let id = tutorial.id;
let params = json!({ "tutorial": id });
let mut run = None;
ui.add_space(6.0);
ui.horizontal_wrapped(|ui| {
if ui.button(tl!("View more")).clicked() {
run = Some(("learn.openSample", params.clone()));
}
let done = &app.ui.learn_practice;
if done.opened.contains(&id) && !done.finished.contains(&id) && ui.button(tl!("Finish tutorial")).clicked() {
run = Some(("learn.finish", params.clone()));
}
});
if app.ui.learn_practice.finished.contains(&id) {
ui.add_space(6.0);
match catalog::next(id) {
Some(next) => {
ui.horizontal_wrapped(|ui| {
if ui.button(tl!("Next Up")).clicked() {
run = Some(("learn.next", params.clone()));
}
ui.add(egui::Label::new(egui::RichText::new(next.title).color(t.text)).wrap());
});
}
None => {
ui.add(egui::Label::new(egui::RichText::new(tl!("You finished the last tutorial in this skill.")).color(t.text_dim)).wrap());
if ui.button(tl!("Back to skills")).clicked() {
run = Some(("learn.skills", json!({})));
}
}
}
}
if let Some((cmd, params)) = run
&& let Err(e) = app.run(cmd, params)
{
app.status(e);
}
}
/// A clickable row: `title`, and for a skill (`tutorials`) a progress ring before it and the
/// count of tutorials done of all after it. No progress is kept yet, so the ring is empty.
fn row(ui: &mut Ui, title: &str, tutorials: Option<usize>) -> egui::Response {
@@ -191,13 +251,18 @@ fn row(ui: &mut Ui, title: &str, tutorials: Option<usize>) -> egui::Response {
resp.on_hover_cursor(egui::CursorIcon::PointingHand)
}
/// The Learn panel's UI commands: `window.learn`, `learn.list`, `learn.show` and `learn.locateTool`.
/// The Learn panel's UI commands: `window.learn`, `learn.list`, `learn.skills`, `learn.show`,
/// `learn.locateTool`, `learn.openSample`, `learn.finish` and `learn.next`.
pub(crate) fn command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<Result<Value, String>> {
Some(match id {
"window.learn" => toggle(app, p),
"learn.list" => Ok(list()),
"learn.skills" => skills_list(app),
"learn.show" => open(app, p),
"learn.locateTool" => locate::locate_tool(app, p),
"learn.openSample" => open_sample(app, p),
"learn.finish" => finish(app, p),
"learn.next" => next(app, p),
_ => return None,
})
}
@@ -231,21 +296,84 @@ fn list() -> Value {
json!({ "skills": skills.collect::<Vec<_>>() })
}
/// `learn.skills`: Back to skills, and the Quick skill guide card. Show the panel on the Skills tab
/// with the list of skills, with no skill and no tutorial open → what `learn.list` answers.
fn skills_list(app: &mut VectorcraftApp) -> Result<Value, String> {
crate::panel_docking::reveal_panel(app, ID)?;
app.ui.learn = View { tab: Tab::Skills, skill: None, tutorial: None };
Ok(list())
}
/// `learn.show {tutorial}`: show the panel on the Skills tab with that tutorial's steps → the
/// tutorial. A missing or unknown id is an error and changes nothing.
fn open(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
let key = "tutorial";
let id = match p.get(key) {
None | Some(Value::Null) => return Err("missing `tutorial` (see learn.list)".into()),
Some(Value::String(id)) => id.as_str(),
Some(_) => return Err(format!("`{key}` must be a string")),
};
let (skill, tutorial) = catalog::tutorial(id).ok_or_else(|| format!("unknown tutorial `{id}` (see learn.list)"))?;
let (skill, tutorial) = tutorial_param(p)?;
crate::panel_docking::reveal_panel(app, ID)?;
app.ui.learn = View { tab: Tab::Skills, skill: Some(skill.id), tutorial: Some(tutorial.id) };
let steps: Vec<String> = tutorial.steps.iter().map(catalog::Step::plain).collect();
Ok(json!({"skill": skill.id, "tutorial": tutorial.id, "title": tutorial.title, "steps": steps}))
}
/// The tutorial `p`'s `tutorial` names, with its skill. A missing, non-string or unknown id is an
/// error.
fn tutorial_param(p: &Value) -> Result<(&'static catalog::Skill, &'static catalog::Tutorial), String> {
let key = "tutorial";
let id = match p.get(key) {
None | Some(Value::Null) => return Err("missing `tutorial` (see learn.list)".into()),
Some(Value::String(id)) => id.as_str(),
Some(_) => return Err(format!("`{key}` must be a string")),
};
catalog::tutorial(id).ok_or_else(|| format!("unknown tutorial `{id}` (see learn.list)"))
}
/// `learn.openSample {tutorial}`: View more. Open that tutorial's practice sample as a new untitled
/// document, which becomes the active one, and move the Learn view to the tutorial → `{tutorial,
/// index, title}` of the new document. Each call opens another copy; open documents stay as they
/// are. An id that is not valid, or a sample that does not read, is an error and changes nothing.
fn open_sample(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
let (skill, tutorial) = tutorial_param(p)?;
let id = tutorial.id;
let svg = samples::sample(id).ok_or_else(|| format!("tutorial `{id}` has no practice sample"))?;
let name = format!("{id}.svg");
let opened = vectorcraft_engine::cmd::fileio::open_template(&mut app.session, &name, svg.as_bytes(), None).map_err(|e| e.to_string())?;
app.sync_views();
if !app.ui.learn_practice.opened.contains(&id) {
app.ui.learn_practice.opened.push(id);
}
app.ui.learn = View { tab: Tab::Skills, skill: Some(skill.id), tutorial: Some(id) };
Ok(json!({"tutorial": id, "index": opened["index"], "title": opened["title"]}))
}
/// `learn.finish {tutorial}`: Finish tutorial. Mark a tutorial whose sample was opened finished →
/// `{tutorial, finished: true, next}`, where `next` is the id of the next tutorial in its skill, or
/// null after the skill's last one. Finishing it again keeps one entry. The Learn view stays as it is.
fn finish(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
let (_, tutorial) = tutorial_param(p)?;
let id = tutorial.id;
let practice = &mut app.ui.learn_practice;
if !practice.opened.contains(&id) {
return Err(format!("open the practice sample of `{id}` first (see learn.openSample)"));
}
if !practice.finished.contains(&id) {
practice.finished.push(id);
}
Ok(json!({"tutorial": id, "finished": true, "next": catalog::next(id).map(|t| t.id)}))
}
/// `learn.next {tutorial}`: Next Up. Show the panel on the tutorial after a finished one in the
/// same skill → `{skill, tutorial, title}` of that tutorial. It opens no sample. An unfinished
/// tutorial, or a skill's last tutorial, is an error and changes nothing.
fn next(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
let (skill, tutorial) = tutorial_param(p)?;
let id = tutorial.id;
if !app.ui.learn_practice.finished.contains(&id) {
return Err(format!("finish `{id}` first (see learn.finish)"));
}
let after = catalog::next(id).ok_or_else(|| format!("`{id}` is the last tutorial in its skill"))?;
crate::panel_docking::reveal_panel(app, ID)?;
app.ui.learn = View { tab: Tab::Skills, skill: Some(skill.id), tutorial: Some(after.id) };
Ok(json!({"skill": skill.id, "tutorial": after.id, "title": after.title}))
}
#[cfg(test)]
mod tests;
@@ -201,3 +201,10 @@ pub fn skill(id: &str) -> Option<&'static Skill> {
pub fn tutorial(id: &str) -> Option<(&'static Skill, &'static Tutorial)> {
SKILLS.iter().find_map(|s| s.tutorials.iter().find(|t| t.id == id).map(|t| (s, t)))
}
/// The tutorial after the one whose id is `id` in the same skill; none for a skill's last tutorial.
pub fn next(id: &str) -> Option<&'static Tutorial> {
let (skill, _) = tutorial(id)?;
let at = skill.tutorials.iter().position(|t| t.id == id)?;
skill.tutorials.get(at + 1)
}
+103
View File
@@ -0,0 +1,103 @@
//! The practice samples that a tutorial's View more opens: one small original SVG drawing for each
//! tutorial, with only the art its steps work on. Each one opens as a new untitled document.
/// The largest sample, in bytes: the catalog test keeps every sample under it.
pub const MAX_SAMPLE_BYTES: usize = 16 * 1024;
/// Every tutorial's sample, by tutorial id, in the catalog's order.
pub const SAMPLES: &[(&str, &str)] = &[
(
"draw-and-color",
r##"<svg xmlns="http://www.w3.org/2000/svg" width="400" height="300" viewBox="0 0 400 300">
<rect x="100" y="75" width="200" height="150" fill="none" stroke="#9aa0a6" stroke-width="2" stroke-dasharray="8 6"/>
</svg>
"##,
),
(
"combine-shapes",
r##"<svg xmlns="http://www.w3.org/2000/svg" width="400" height="300" viewBox="0 0 400 300">
<circle cx="160" cy="150" r="80" fill="#4c8bf5"/>
<circle cx="250" cy="150" r="80" fill="#f4b400"/>
</svg>
"##,
),
(
"add-headline",
r##"<svg xmlns="http://www.w3.org/2000/svg" width="400" height="300" viewBox="0 0 400 300">
<rect x="40" y="90" width="320" height="120" rx="12" fill="#e8f0fe"/>
</svg>
"##,
),
(
"pen-paths",
r##"<svg xmlns="http://www.w3.org/2000/svg" width="400" height="300" viewBox="0 0 400 300">
<path d="M 200 60 L 320 240 L 80 240 Z" fill="none" stroke="#9aa0a6" stroke-width="2" stroke-dasharray="8 6"/>
<circle cx="200" cy="60" r="5" fill="#1a73e8"/>
<circle cx="320" cy="240" r="5" fill="#1a73e8"/>
<circle cx="80" cy="240" r="5" fill="#1a73e8"/>
</svg>
"##,
),
(
"smooth-curves",
r##"<svg xmlns="http://www.w3.org/2000/svg" width="400" height="300" viewBox="0 0 400 300">
<path d="M 40 150 C 100 50 160 50 200 150 C 240 250 300 250 360 150" fill="none" stroke="#9aa0a6" stroke-width="2" stroke-dasharray="8 6"/>
</svg>
"##,
),
(
"pencil-sketch",
r##"<svg xmlns="http://www.w3.org/2000/svg" width="400" height="300" viewBox="0 0 400 300">
<path d="M 60 200 C 100 80 180 80 200 150 C 220 220 300 220 340 100" fill="none" stroke="#9aa0a6" stroke-width="2" stroke-dasharray="8 6"/>
</svg>
"##,
),
(
"fill-and-stroke",
r##"<svg xmlns="http://www.w3.org/2000/svg" width="400" height="300" viewBox="0 0 400 300">
<rect x="110" y="80" width="180" height="140" rx="16" fill="#f4b400" stroke="#1a73e8" stroke-width="6"/>
</svg>
"##,
),
(
"gradient",
r##"<svg xmlns="http://www.w3.org/2000/svg" width="400" height="300" viewBox="0 0 400 300">
<circle cx="200" cy="150" r="100" fill="#34a853"/>
</svg>
"##,
),
(
"copy-colors",
r##"<svg xmlns="http://www.w3.org/2000/svg" width="400" height="300" viewBox="0 0 400 300">
<rect x="50" y="90" width="120" height="120" fill="#db4437" stroke="#202124" stroke-width="4"/>
<circle cx="290" cy="150" r="60" fill="#dadce0"/>
</svg>
"##,
),
(
"area-type",
r##"<svg xmlns="http://www.w3.org/2000/svg" width="400" height="300" viewBox="0 0 400 300">
<rect x="80" y="60" width="240" height="180" fill="none" stroke="#1a73e8" stroke-width="2"/>
</svg>
"##,
),
(
"type-on-path",
r##"<svg xmlns="http://www.w3.org/2000/svg" width="400" height="300" viewBox="0 0 400 300">
<path d="M 50 200 C 120 80 280 80 350 200" fill="none" stroke="#1a73e8" stroke-width="2"/>
</svg>
"##,
),
(
"outline-type",
r##"<svg xmlns="http://www.w3.org/2000/svg" width="400" height="300" viewBox="0 0 400 300">
<text x="60" y="170" font-size="64" fill="#202124">Outline</text>
</svg>
"##,
),
];
/// The sample of the tutorial whose id is `id`.
pub fn sample(id: &str) -> Option<&'static str> {
SAMPLES.iter().find(|(tutorial, _)| *tutorial == id).map(|(_, svg)| *svg)
}
+250 -1
View File
@@ -4,7 +4,7 @@ use egui::accesskit::Node;
use serde_json::json;
use vectorcraft_engine::Session;
use super::{ID, Tab, View, catalog, show};
use super::{ID, Practice, Tab, View, catalog, samples, show};
use crate::VectorcraftApp;
use crate::tests_labels::painted_text;
@@ -228,6 +228,255 @@ fn learn_show_opens_a_tutorial_and_learn_list_lists_them() {
assert_eq!(skills[2]["tutorials"][1], json!({"id": "gradient", "title": "Add a gradient", "steps": 3}));
}
/// Every tutorial has one sample of its own, in the catalog's order, each one small and read
/// without warnings into art of its own.
#[test]
fn every_tutorial_has_its_own_readable_sample() {
let ids: Vec<&str> = catalog::SKILLS.iter().flat_map(|s| s.tutorials.iter().map(|t| t.id)).collect();
let sampled: Vec<&str> = samples::SAMPLES.iter().map(|(id, _)| *id).collect();
assert_eq!(sampled, ids, "one sample for each tutorial, in the catalog's order");
let mut texts = HashSet::new();
for (id, svg) in samples::SAMPLES {
assert!(svg.len() <= samples::MAX_SAMPLE_BYTES, "{id}: {} bytes", svg.len());
assert!(texts.insert(*svg), "{id} reuses another tutorial's sample");
let loaded = vectorcraft_engine::cmd::fileio::load(&format!("{id}.svg"), svg.as_bytes()).unwrap_or_else(|e| panic!("{id}: {e}"));
assert!(loaded.warnings.is_empty(), "{id}: {:?}", loaded.warnings);
let mut art = 0;
loaded.doc.walk(|n| art += usize::from(!n.is_layer()));
assert!(art > 0, "{id} has no art");
}
}
/// Next Up stays in the tutorial's skill: a skill's last tutorial, the catalog's last one
/// included, has no next tutorial.
#[test]
fn the_next_tutorial_is_the_next_one_in_the_same_skill() {
for skill in catalog::SKILLS {
for pair in skill.tutorials.windows(2) {
assert_eq!(catalog::next(pair[0].id).map(|t| t.id), Some(pair[1].id));
}
let last = skill.tutorials.last().unwrap();
assert!(catalog::next(last.id).is_none(), "{} is the last of {}", last.id, skill.id);
}
assert!(catalog::next("nope").is_none());
}
/// View more opens the tutorial's sample as a new untitled document beside a modified one, which
/// stays as it was; another tutorial opens another sample, and View more again opens another copy.
#[test]
fn view_more_opens_the_tutorial_s_sample_as_a_new_untitled_document() {
let mut app = app();
let ctx = context();
app.run("file.new", json!({"width": 400, "height": 300})).unwrap();
app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 30, "height": 20})).unwrap();
let work = app.session.documents()[0].doc.node_count();
let (skill, tutorial) = catalog::tutorial("pen-paths").unwrap();
app.ui.learn = View { tab: Tab::Skills, skill: Some(skill.id), tutorial: Some(tutorial.id) };
click(&mut app, &ctx, "View more", show);
let docs = app.session.documents();
assert_eq!(docs.len(), 2, "{}", app.ui.status);
assert_eq!(app.session.active_index(), Some(1));
assert_eq!(docs[0].doc.node_count(), work, "the open document stays as it was");
assert!(docs[1].path.is_none() && docs[1].title().starts_with("Untitled-"), "{}", docs[1].title());
let pen = docs[1].doc.node_count();
assert_eq!(app.ui.learn_practice, Practice { opened: vec!["pen-paths"], finished: vec![] });
let r = app.run("learn.openSample", json!({"tutorial": "gradient"})).unwrap();
assert_eq!(r["tutorial"], "gradient");
assert_eq!(r["index"], 2);
assert_eq!(r["title"], json!(app.session.documents()[2].title()));
assert_ne!(app.session.documents()[2].doc.node_count(), pen, "another tutorial, another sample");
assert_eq!(app.ui.learn, View { tab: Tab::Skills, skill: Some("color"), tutorial: Some("gradient") });
app.run("learn.openSample", json!({"tutorial": "gradient"})).unwrap();
assert_eq!(app.session.documents().len(), 4, "View more again opens another copy");
assert_eq!(app.ui.learn_practice.opened, ["pen-paths", "gradient"]);
}
/// Finish tutorial shows once the sample is open; finishing shows Next Up with the next
/// tutorial's title, and Next Up shows that tutorial without opening its sample.
#[test]
fn finishing_a_tutorial_offers_next_up_in_its_skill() {
let mut app = app();
let ctx = context();
let (skill, tutorial) = catalog::tutorial("pen-paths").unwrap();
app.ui.learn = View { tab: Tab::Skills, skill: Some(skill.id), tutorial: Some(tutorial.id) };
let text = painted_text(&mut app, show);
assert!(text.contains("View more") && !text.contains("Finish tutorial") && !text.contains("Next Up"), "{text}");
click(&mut app, &ctx, "View more", show);
let text = painted_text(&mut app, show);
assert!(text.contains("Finish tutorial") && !text.contains("Next Up"), "{text}");
click(&mut app, &ctx, "Finish tutorial", show);
assert_eq!(app.ui.learn_practice.finished, ["pen-paths"]);
let text = painted_text(&mut app, show);
assert!(text.contains("Next Up") && text.contains("Draw smooth curves") && !text.contains("Finish tutorial"), "{text}");
let docs = app.session.documents().len();
click(&mut app, &ctx, "Next Up", show);
assert_eq!(app.ui.learn, View { tab: Tab::Skills, skill: Some("draw"), tutorial: Some("smooth-curves") });
assert_eq!(app.session.documents().len(), docs, "Next Up opens no sample");
let text = painted_text(&mut app, show);
assert!(text.contains("View more") && !text.contains("Finish tutorial") && !text.contains("Next Up"), "{text}");
}
/// One press and release of `key` in what `draw` draws.
fn key(app: &mut VectorcraftApp, ctx: &egui::Context, key: egui::Key, draw: fn(&mut VectorcraftApp, &mut egui::Ui)) {
let event = |pressed| egui::Event::Key { key, physical_key: None, pressed, repeat: false, modifiers: Default::default() };
frame(app, ctx, vec![event(true)], draw);
frame(app, ctx, vec![event(false)], draw);
}
/// Press Tab until the control labeled `name` has the keyboard focus.
fn tab_to(app: &mut VectorcraftApp, ctx: &egui::Context, name: &str, draw: fn(&mut VectorcraftApp, &mut egui::Ui)) {
for _ in 0..32 {
key(app, ctx, egui::Key::Tab, draw);
let at = center(&frame(app, ctx, vec![], draw), name).unwrap_or_else(|| panic!("{name} is drawn"));
if ctx.memory(|m| m.focused()).and_then(|id| ctx.read_response(id)).is_some_and(|r| r.rect.contains(at)) {
return;
}
}
panic!("Tab did not reach {name}");
}
/// View more, Finish tutorial and Next Up are reached with Tab and pressed with Enter.
#[test]
fn the_practice_controls_work_from_the_keyboard() {
let mut app = app();
let ctx = context();
let (skill, tutorial) = catalog::tutorial("pen-paths").unwrap();
app.ui.learn = View { tab: Tab::Skills, skill: Some(skill.id), tutorial: Some(tutorial.id) };
frame(&mut app, &ctx, vec![], show);
tab_to(&mut app, &ctx, "View more", show);
key(&mut app, &ctx, egui::Key::Enter, show);
assert_eq!(app.session.documents().len(), 1, "{}", app.ui.status);
assert_eq!(app.ui.learn_practice.opened, ["pen-paths"]);
tab_to(&mut app, &ctx, "Finish tutorial", show);
key(&mut app, &ctx, egui::Key::Enter, show);
assert_eq!(app.ui.learn_practice.finished, ["pen-paths"]);
tab_to(&mut app, &ctx, "Next Up", show);
key(&mut app, &ctx, egui::Key::Enter, show);
assert_eq!(app.ui.learn, View { tab: Tab::Skills, skill: Some("draw"), tutorial: Some("smooth-curves") });
assert_eq!(app.session.documents().len(), 1, "Next Up opens no sample");
}
/// The panel 150 points wide.
fn narrow(app: &mut VectorcraftApp, ui: &mut egui::Ui) {
ui.allocate_ui(egui::vec2(150.0, 800.0), |ui| {
ui.set_max_width(150.0);
show(app, ui);
});
}
/// In a narrow panel the practice controls wrap inside it.
#[test]
fn the_practice_controls_fit_a_narrow_panel() {
let mut app = app();
let ctx = context();
app.run("learn.openSample", json!({"tutorial": "pen-paths"})).unwrap();
frame(&mut app, &ctx, vec![], narrow);
let nodes = frame(&mut app, &ctx, vec![], narrow);
for name in ["View more", "Finish tutorial"] {
let node = nodes.iter().find(|n| n.label() == Some(name)).unwrap_or_else(|| panic!("{name} is drawn"));
let b = node.bounds().unwrap();
assert!(b.x1 <= 151.0, "{name} ends at {}", b.x1);
}
}
/// A skill's last tutorial, finished, says so and offers Back to skills instead of Next Up, even
/// when another skill follows; `learn.next` there is an error.
#[test]
fn the_last_tutorial_of_a_skill_offers_back_to_skills() {
let mut app = app();
let ctx = context();
for id in ["add-headline", "outline-type"] {
app.run("learn.openSample", json!({"tutorial": id})).unwrap();
assert_eq!(app.run("learn.finish", json!({"tutorial": id})).unwrap(), json!({"tutorial": id, "finished": true, "next": null}));
let text = painted_text(&mut app, show);
assert!(text.contains("You finished the last tutorial in this skill.") && !text.contains("Next Up"), "{text}");
let before = app.ui.learn.clone();
assert_eq!(app.run("learn.next", json!({"tutorial": id})).unwrap_err(), format!("`{id}` is the last tutorial in its skill"));
assert_eq!(app.ui.learn, before);
click(&mut app, &ctx, "Back to skills", show);
assert_eq!(app.ui.learn, View { tab: Tab::Skills, skill: None, tutorial: None });
}
}
/// `learn.finish` needs the sample opened, and finishing again keeps one entry; `learn.next` needs
/// the tutorial finished and answers the next tutorial.
#[test]
fn learn_finish_and_learn_next_follow_the_practice() {
let mut app = app();
let open = "open the practice sample of `gradient` first (see learn.openSample)";
assert_eq!(app.run("learn.finish", json!({"tutorial": "gradient"})).unwrap_err(), open);
assert_eq!(app.run("learn.next", json!({"tutorial": "gradient"})).unwrap_err(), "finish `gradient` first (see learn.finish)");
assert_eq!(app.ui.learn_practice, Practice::default());
app.run("learn.openSample", json!({"tutorial": "gradient"})).unwrap();
assert_eq!(app.run("learn.next", json!({"tutorial": "gradient"})).unwrap_err(), "finish `gradient` first (see learn.finish)");
for _ in 0..2 {
let r = app.run("learn.finish", json!({"tutorial": "gradient"})).unwrap();
assert_eq!(r, json!({"tutorial": "gradient", "finished": true, "next": "copy-colors"}));
}
assert_eq!(app.ui.learn_practice.finished, ["gradient"], "finishing again keeps one entry");
let r = app.run("learn.next", json!({"tutorial": "gradient"})).unwrap();
assert_eq!(r, json!({"skill": "color", "tutorial": "copy-colors", "title": "Copy colors from other art"}));
assert!(shown(&app));
assert_eq!(app.ui.learn, View { tab: Tab::Skills, skill: Some("color"), tutorial: Some("copy-colors") });
}
/// `learn.skills` shows the panel on the Skills tab with the list of skills and answers what
/// `learn.list` answers. Back to skills and the Quick skill guide card run it: each one also shows
/// a panel that was hidden, as the command does.
#[test]
fn learn_skills_shows_the_list_of_skills() {
let skills = View { tab: Tab::Skills, skill: None, tutorial: None };
assert!(crate::menus::UI_COMMANDS.iter().any(|c| c.0 == "learn.skills"), "learn.skills is in the UI command catalog");
let mut app = app();
app.run("learn.show", json!({"tutorial": "gradient"})).unwrap();
app.run("window.learn", json!({"show": false})).unwrap();
assert!(!shown(&app));
let list = app.run("learn.list", json!({})).unwrap();
assert_eq!(app.run("learn.skills", json!({})).unwrap(), list);
assert_eq!(app.ui.learn, skills);
assert!(shown(&app));
// Back to skills, after a skill's last tutorial.
let mut app = self::app();
let ctx = context();
app.run("learn.openSample", json!({"tutorial": "outline-type"})).unwrap();
app.run("learn.finish", json!({"tutorial": "outline-type"})).unwrap();
assert!(!shown(&app));
click(&mut app, &ctx, "Back to skills", show);
assert_eq!(app.ui.learn, skills);
assert!(shown(&app), "Back to skills runs learn.skills");
// The Quick skill guide card on the For you tab.
let mut app = self::app();
let ctx = context();
assert!(!shown(&app));
click(&mut app, &ctx, "Quick skill guide", show);
assert_eq!(app.ui.learn, skills);
assert!(shown(&app), "the Quick skill guide card runs learn.skills");
}
/// The practice commands reject a missing, mistyped or unknown tutorial id and change nothing.
#[test]
fn the_practice_commands_reject_a_missing_or_unknown_tutorial() {
let mut app = app();
app.run("file.new", json!({"width": 400, "height": 300})).unwrap();
for cmd in ["learn.openSample", "learn.finish", "learn.next"] {
for (params, error) in [
(json!({}), "missing `tutorial` (see learn.list)"),
(json!({"tutorial": null}), "missing `tutorial` (see learn.list)"),
(json!({"tutorial": 3}), "`tutorial` must be a string"),
(json!({"tutorial": "nope"}), "unknown tutorial `nope` (see learn.list)"),
] {
assert_eq!(app.run(cmd, params).unwrap_err(), error, "{cmd}");
assert_eq!(app.ui.learn, View::default());
assert_eq!(app.ui.learn_practice, Practice::default());
assert_eq!(app.session.documents().len(), 1);
assert!(!shown(&app));
}
}
}
/// A missing, mistyped or unknown tutorial id is an error and leaves the panel as it was.
#[test]
fn learn_show_rejects_a_missing_or_unknown_tutorial() {
+5
View File
@@ -424,6 +424,10 @@ pub struct UiState {
/// is on screen. Not saved.
#[serde(skip)]
pub learn_locate: Option<crate::panels::learn::locate::Locate>,
/// The tutorials whose practice samples were opened and the tutorials finished, kept while the
/// app runs.
#[serde(skip)]
pub learn_practice: crate::panels::learn::Practice,
pub control_bar: bool,
/// The Control bar is docked to the bottom of the window instead of the top
/// (`window.control.dock`).
@@ -1089,6 +1093,7 @@ impl Default for UiState {
find_font_in: String::new(),
learn: Default::default(),
learn_locate: None,
learn_practice: Default::default(),
control_bar: false,
control_bar_bottom: false,
control_bar_floating: false,
+23 -2
View File
@@ -2688,13 +2688,34 @@ toggles it (`show` omitted or null), and answers whether it shows; Window › Le
The panel counts as hidden while the dock is hidden or a full screen mode hides the panels. Showing it shows a hidden
dock; in a full screen mode it makes Learn the front panel, which shows when the mode ends, and answers false. Any
other value of `show` is an error and changes nothing. `learn.list {}` → `{skills: [{id, title, tutorials: [{id, title, steps}]}]}`, in the order the Skills tab
lists them, where `steps` is the number of steps. `learn.show {tutorial}` shows the panel on its Skills tab with that
lists them, where `steps` is the number of steps. `learn.skills {}` shows the panel on its Skills tab with the list of
skills, with no skill and no tutorial open, as the Quick skill guide card and Back to skills do, and answers what
`learn.list` answers. `learn.show {tutorial}` shows the panel on its Skills tab with that
tutorial's steps → `{skill, tutorial, title, steps: [text]}`. A missing, non-string or unknown `tutorial` is an error
and changes nothing. Behavior reference:
and changes nothing.
Under a tutorial's steps, View more opens the tutorial's practice sample, a small drawing of VectorCraft's own with the
art its steps work on, as a new untitled document that becomes the active one; Save asks where to save it. Once the
sample is open, Finish tutorial marks the tutorial finished. A finished tutorial shows Next Up with the title of the
next tutorial in the same skill, which Next Up shows without opening its sample. A skill's last tutorial, finished,
says so and shows Back to skills instead; Next Up never moves to another skill. Which tutorials are finished is kept
while the app runs, and the skill rings and counts do not show it yet. These are VectorCraft's own rules; the help
page also plays a short video, which VectorCraft does not have. `learn.openSample {tutorial}` runs View more →
`{tutorial, index, title}` of the new document, and moves the Learn view to that tutorial; each call opens another
copy and leaves the open documents as they are. `learn.finish {tutorial}` runs Finish tutorial →
`{tutorial, finished: true, next}`, where `next` is the next tutorial's id in the skill, or null after the skill's
last one; the tutorial's sample must have been opened, and finishing it again keeps one entry. `learn.next {tutorial}`
runs Next Up on a finished tutorial and shows the panel on the next one → `{skill, tutorial, title}`; on a tutorial
not finished, or a skill's last tutorial, it is an error. For all three, a missing, non-string or unknown `tutorial`
is an error, and an error changes nothing. Behavior reference:
[Quick skill guide](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/learn-skills-quick-skill-guide.html).
```json
{"name":"invoke_menu","arguments":{"command":"learn.show","params":{"tutorial":"pen-paths"}}}
{"name":"invoke_menu","arguments":{"command":"learn.openSample","params":{"tutorial":"pen-paths"}}}
{"name":"invoke_menu","arguments":{"command":"learn.finish","params":{"tutorial":"pen-paths"}}}
{"name":"invoke_menu","arguments":{"command":"learn.next","params":{"tutorial":"pen-paths"}}}
{"name":"invoke_menu","arguments":{"command":"learn.skills","params":{}}}
```
A step that names a tool shows the tool's name as a blue link. A click on it, or Enter or Space while it has the
+1
View File
@@ -873,6 +873,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- Native macOS menu bar, vector tool cursors, a ⌘K command palette, middle-button panning with any tool. Native menus explicitly end tracking on command selection, outside window presses and Escape; menu updates wait until tracking ends, and a dismissing press never starts a canvas gesture (M14.8).
- The Learn panel (Window › Learn, or Explore tutorials beside the search icon on the app bar) has a For you tab with a Get started section and a Quick skill guide card, and a Skills tab that lists VectorCraft's own skills with their tutorial counts and empty progress rings; a skill lists its tutorials, a tutorial shows its numbered steps, and Back goes up a level; `learn.list` and `learn.show {tutorial}` script it ([the help page](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/learn-skills-quick-skill-guide.html); `panels::learn::tests`).
- A tool's name in a Learn tutorial step is a blue link that shows where the tool is without choosing it (`learn.locateTool {tool}`): its toolbar button is marked, a collapsed group's flyout opens with the tool's row marked, a torn-off strip comes to the front, and a tool no shown toolbar holds is marked in the All Tools drawer. The mark, its 3 seconds and the hidden-toolbar cases are VectorCraft's own rules ([the help page](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/learn-skills-quick-skill-guide.html); `panels::learn::tests`, the `a_learn_link_*` tests in `toolbar::tests`).
- Under a tutorial's steps, View more opens the tutorial's own practice sample as a new untitled document, and once the tutorial is finished, Next Up moves to the next tutorial in the same skill, as the help page describes ([the help page](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/learn-skills-quick-skill-guide.html)). The samples are VectorCraft's own drawings. A Finish tutorial button that shows once the sample is open, the untitled copy, Back to skills in place of Next Up after a skill's last tutorial, and Next Up showing the next tutorial without opening its sample are VectorCraft's own rules. `learn.openSample`, `learn.finish`, `learn.next` and `learn.skills` (Back to skills) script them. Not done: the help page's short video, and progress kept between sessions and shown in the skill rings. Regression tests: `view_more_opens_the_tutorial_s_sample_as_a_new_untitled_document`, `finishing_a_tutorial_offers_next_up_in_its_skill`, `the_practice_controls_work_from_the_keyboard`, `the_last_tutorial_of_a_skill_offers_back_to_skills`, `learn_finish_and_learn_next_follow_the_practice`, `learn_skills_shows_the_list_of_skills`, `the_practice_commands_reject_a_missing_or_unknown_tutorial`, `every_tutorial_has_its_own_readable_sample` and `the_next_tutorial_is_the_next_one_in_the_same_skill` in `crates/ui-egui/src/panels/learn/tests.rs`.
- The errors of the blend spine commands (Move Spine Point, Reshape Spine Segment, Add Spine Point and Delete Spine Point) show in the status bar in the interface language: each has a row in every catalog that translates all status and error messages and in Bulgarian (`blend_spine_param_errors_are_translated`).
- The errors of the 3D effects (Extrude & Bevel, Revolve, Inflate, Rotate and their Classic forms) show in the status bar in the interface language: the art a 3D effect cannot build a solid from, invalid light, graphics and surface settings, and the notes for an object that draws no 3D faces or more wireframe edges than the limit. Each has a row in every catalog that translates all status and error messages, and the effect's name and the values in the message are translated or kept as written (`three_d_refusals_are_translated_in_every_complete_catalog`, `three_d_refusals_translate_with_their_values`).
- The errors of the parameter checks that many commands share show in the status bar in the interface language: a value that is not a number where a number goes (`object.move` with `"dx": "60 mm"`, and `object.rotate`, `object.setBounds` and the other commands that call `check_numbers`), a missing number or index, `ids` that are not a list of object ids, an object id that is not a non-negative integer or names no object, and a date that is not Unix seconds. Each has a row in every catalog that translates all status and error messages, and in Bulgarian, Danish, Indonesian and Dutch (`shared_param_check_errors_are_translated`).
+1 -1
View File
@@ -382,7 +382,7 @@ next unit in the order a right-click on a ruler lists them, from Feet back to Po
|---|---|---|
| Effect > Warp Options: style previews, orientation choices and percent controls | done | #1760; focused input, mixed-style and history checks passed; original captures reviewed. No geometry or optional Properties changes. |
| Command search: Up and Down move through the results, Enter runs the highlighted one | done | #1798 (#1757) |
| Learn panel: Window › Learn and Explore tutorials on the app bar open it; For you holds a Quick skill guide card, Skills lists VectorCraft's own skills with tutorial counts and empty progress rings, a tutorial shows its numbered steps, Back goes up a level; a tool's name in a step is a link that marks the tool where the toolbars show it (its button, its group's flyout, a torn-off strip, a toolbar made with New Toolbar or the All Tools drawer) without choosing it | done | [the help page](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/learn-skills-quick-skill-guide.html); `learn.list`, `learn.show`, `learn.locateTool`; the mark's look and 3-second lifetime are VectorCraft's own rules; `panels::learn::tests`, `toolbar::tests` |
| Learn panel: Window › Learn and Explore tutorials on the app bar open it; For you holds a Quick skill guide card, Skills lists VectorCraft's own skills with tutorial counts and empty progress rings, a tutorial shows its numbered steps, Back goes up a level; a tool's name in a step is a link that marks the tool where the toolbars show it (its button, its group's flyout, a torn-off strip, a toolbar made with New Toolbar or the All Tools drawer) without choosing it; View more opens the tutorial's practice sample as a new untitled document, Finish tutorial then marks it finished, and Next Up shows the next tutorial in the same skill (no video) | done | [the help page](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/learn-skills-quick-skill-guide.html); `learn.list`, `learn.skills`, `learn.show`, `learn.locateTool`, `learn.openSample`, `learn.finish`, `learn.next`; the mark's look and 3-second lifetime are VectorCraft's own rules; `panels::learn::tests`, `toolbar::tests` |
| Pathfinder panel: a group selected alone counts its nested shapes, so an expanded stroked object keeps the Shape Modes enabled | done | #1642 (#1635) |
| Selected Grid Repeat shows row, column and corner handles and spacing sliders; bounding-box handles take priority where they meet | done | #2691, `tools/src/repeat_grid.rs`; [public behavior reference](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/create-and-edit-patterns/set-grid-repeat-options.html); drags preview and commit `object.repeat.options` as one undo step, Escape cancels; Radial Repeat has no handles yet |
| Grid Repeat Options (Object › Repeat › Options…) has a Preview checkbox: the repeat on the canvas follows the fields while the dialog is open, Cancel puts the original back without a history step, OK keeps the previewed values as one undo step | done | #2651; [public behavior reference](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/create-and-edit-patterns/set-grid-repeat-options.html) lists the Preview option for the grid options only, so Radial and Mirror Repeat Options have none; `dialogs/command.rs` tests |