diff --git a/crates/mcp/src/ui_catalog.rs b/crates/mcp/src/ui_catalog.rs index c32d2d6c6f..e801df4a7a 100644 --- a/crates/mcp/src/ui_catalog.rs +++ b/crates/mcp/src/ui_catalog.rs @@ -666,7 +666,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ "learn.list", "List Tutorials", "", - "{} the Learn panel's skills in the order its Skills tab lists them: {skills: [{id, title, tutorials: [{id, title, steps: number of steps}]}]}", + "{} the Learn panel's skills in the order its Skills tab lists them, with the progress its rings, counts and check marks show: {skills: [{id, title, done: number of its tutorials finished, total: number of its tutorials, tutorials: [{id, title, steps: number of steps, finished: bool, completedSteps: [zero-based indexes of the steps marked done, every step once finished]}]}]}", ), ( "learn.skills", @@ -692,11 +692,17 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ "", "{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.markStep", + "Mark Tutorial Step", + "", + "{tutorial: id (see learn.list), step: zero-based step number, complete?: bool} mark a step of a tutorial whose sample learn.openSample opened as done (true, omitted or null) or not done (false), as the check box before the step does; marking does not finish the tutorial; a missing, non-string or unknown id, a missing step or one the tutorial does not have, a complete that is not true or false, a tutorial whose sample was not opened, or a finished tutorial is an error and changes nothing; the preferences keep the marks; returns {tutorial, step, complete}", + ), ( "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}", + "{tutorial: id (see learn.list)} mark a tutorial whose sample learn.openSample opened as finished, with all its steps done, as its Finish tutorial button does; finishing it again keeps one entry, and needs no sample after a restart; a missing, non-string or unknown id, or a tutorial not finished whose sample was not opened, is an error and changes nothing; the preferences keep the tutorials finished; returns {tutorial, finished: true, next: id of the next tutorial in its skill, or null after the skill's last one}", ), ( "learn.next", @@ -704,6 +710,12 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ "", "{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", ), + ( + "learn.resetProgress", + "Reset Learn Progress", + "", + "{} clear the Learn panel's progress, every tutorial finished and every step marked done, as Reset progress under the list of skills does; open documents, opened practice samples and the Learn view stay as they are; returns what learn.list returns", + ), ("window.brightness", "UI Brightness", "", "{brightness: dark|mediumDark|mediumLight|light}"), ( "window.workspace", diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 8cb531e411..28944173dd 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -6865,6 +6865,12 @@ breakText Glyphs Глифи @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}` е последният урок в своето умение + Reset progress Нулиране на напредъка + Mark Tutorial Step Отбелязване на стъпка от урока + Reset Learn Progress Нулиране на напредъка в обучението +@msg missing `step` (see learn.show) липсва `step` (вижте learn.show) +@msg `step` must be a step number from 0 to {last} `step` трябва да е номер на стъпка от 0 до {last} +@msg `{id}` is finished (see learn.resetProgress) `{id}` е завършен (вижте learn.resetProgress) # Deferred plug-in effects Refresh Deferred Effects Обновяване на отложените ефекти diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index 57790c64c6..01b90e5148 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -6918,6 +6918,12 @@ shape Open Otvoren @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 + Reset progress Vrati napredak na početno + Mark Tutorial Step Označi korak tutorijala + Reset Learn Progress Vrati napredak učenja na početno +@msg missing `step` (see learn.show) nedostaje `step` (vidi learn.show) +@msg `step` must be a step number from 0 to {last} `step` mora biti broj koraka od 0 do {last} +@msg `{id}` is finished (see learn.resetProgress) `{id}` je završen (vidi learn.resetProgress) # Deferred plug-in effects Refresh Deferred Effects Osvježi odgođene efekte diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index 749e835b82..4ddd1d5605 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -7453,6 +7453,12 @@ breakText Glyphs Glifs @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 + Reset progress Restableix el progrés + Mark Tutorial Step Marca el pas del tutorial + Reset Learn Progress Restableix el progrés d’aprenentatge +@msg missing `step` (see learn.show) falta `step` (vegeu learn.show) +@msg `step` must be a step number from 0 to {last} `step` ha de ser un número de pas de 0 a {last} +@msg `{id}` is finished (see learn.resetProgress) `{id}` està acabat (vegeu learn.resetProgress) # Deferred plug-in effects Refresh Deferred Effects Actualitza els efectes ajornats diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index b3ea689d15..211982f086 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -7588,6 +7588,12 @@ breakText Glyphs Glyfy @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 + Reset progress Obnovit postup + Mark Tutorial Step Označit krok výukového kurzu + Reset Learn Progress Obnovit postup učení +@msg missing `step` (see learn.show) chybí `step` (viz learn.show) +@msg `step` must be a step number from 0 to {last} `step` musí být číslo kroku od 0 do {last} +@msg `{id}` is finished (see learn.resetProgress) `{id}` je dokončen (viz learn.resetProgress) # Deferred plug-in effects Refresh Deferred Effects Obnovit odložené efekty diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index f320896f62..b3de0ae89a 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -6880,6 +6880,12 @@ breakText Glyphs Glyffer @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 + Reset progress Nulstil fremskridt + Mark Tutorial Step Markér trin i vejledning + Reset Learn Progress Nulstil læringsfremskridt +@msg missing `step` (see learn.show) `step` mangler (se learn.show) +@msg `step` must be a step number from 0 to {last} `step` skal være et trinnummer fra 0 til {last} +@msg `{id}` is finished (see learn.resetProgress) `{id}` er afsluttet (se learn.resetProgress) # Deferred plug-in effects Refresh Deferred Effects Opdatér udskudte effekter diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index d98afe5b03..d0610144ef 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -7068,6 +7068,12 @@ breakText Glyphs Glyphen @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 + Reset progress Fortschritt zurücksetzen + Mark Tutorial Step Schritt des Lernprogramms markieren + Reset Learn Progress Lernfortschritt zurücksetzen +@msg missing `step` (see learn.show) `step` fehlt (siehe learn.show) +@msg `step` must be a step number from 0 to {last} `step` muss eine Schrittnummer von 0 bis {last} sein +@msg `{id}` is finished (see learn.resetProgress) `{id}` ist abgeschlossen (siehe learn.resetProgress) # Deferred plug-in effects Refresh Deferred Effects Zurückgestellte Effekte aktualisieren diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 8912f648bc..d03f8c6958 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -7425,6 +7425,12 @@ breakText Glyphs Γλύφοι @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}` είναι ο τελευταίος οδηγός εκμάθησης της δεξιότητάς του + Reset progress Επαναφορά προόδου + Mark Tutorial Step Σήμανση βήματος οδηγού εκμάθησης + Reset Learn Progress Επαναφορά προόδου εκμάθησης +@msg missing `step` (see learn.show) λείπει το `step` (δείτε learn.show) +@msg `step` must be a step number from 0 to {last} το `step` πρέπει να είναι αριθμός βήματος από 0 έως {last} +@msg `{id}` is finished (see learn.resetProgress) το `{id}` έχει ολοκληρωθεί (δείτε learn.resetProgress) # Deferred plug-in effects Refresh Deferred Effects Ανανέωση εφέ σε αναμονή diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 3de5287c48..ead9de16a6 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -7416,6 +7416,12 @@ breakText Glyphs Glifos @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 + Reset progress Restablecer progreso + Mark Tutorial Step Marcar paso del tutorial + Reset Learn Progress Restablecer progreso de aprendizaje +@msg missing `step` (see learn.show) falta `step` (consulta learn.show) +@msg `step` must be a step number from 0 to {last} `step` debe ser un número de paso de 0 a {last} +@msg `{id}` is finished (see learn.resetProgress) `{id}` está terminado (consulta learn.resetProgress) # Deferred plug-in effects Refresh Deferred Effects Actualizar efectos aplazados diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 5b0762a826..d408220f47 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -7507,6 +7507,12 @@ breakText Glyphs Merkit @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 + Reset progress Palauta edistyminen + Mark Tutorial Step Merkitse opetusohjelman vaihe + Reset Learn Progress Palauta oppimisen edistyminen +@msg missing `step` (see learn.show) `step` puuttuu (ks. learn.show) +@msg `step` must be a step number from 0 to {last} `step` on oltava vaiheen numero, vähintään 0 ja enintään {last} +@msg `{id}` is finished (see learn.resetProgress) `{id}` on viimeistelty (ks. learn.resetProgress) # Deferred plug-in effects Refresh Deferred Effects Päivitä lykätyt tehosteet diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index cbc90c3b9b..318bbbeb44 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -7467,6 +7467,12 @@ breakText Glyphs Glyphes @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 + Reset progress Réinitialiser la progression + Mark Tutorial Step Marquer une étape du tutoriel + Reset Learn Progress Réinitialiser la progression d’apprentissage +@msg missing `step` (see learn.show) `step` manquant (voir learn.show) +@msg `step` must be a step number from 0 to {last} `step` doit être un numéro d’étape de 0 à {last} +@msg `{id}` is finished (see learn.resetProgress) `{id}` est terminé (voir learn.resetProgress) # Deferred plug-in effects Refresh Deferred Effects Actualiser les effets différés diff --git a/crates/ui-egui/src/i18n/hi.tsv b/crates/ui-egui/src/i18n/hi.tsv index 3c77aa0a99..78c0a9a7eb 100644 --- a/crates/ui-egui/src/i18n/hi.tsv +++ b/crates/ui-egui/src/i18n/hi.tsv @@ -8863,6 +8863,12 @@ theme Appearance अपीयरेंस @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}` अपने कौशल का अंतिम ट्यूटोरियल है + Reset progress प्रगति रीसेट करें + Mark Tutorial Step ट्यूटोरियल चरण चिह्नित करें + Reset Learn Progress सीखने की प्रगति रीसेट करें +@msg missing `step` (see learn.show) लापता `step` (learn.show देखें) +@msg `step` must be a step number from 0 to {last} `step` 0 से {last} तक की चरण संख्या होनी चाहिए +@msg `{id}` is finished (see learn.resetProgress) `{id}` पूरा हो चुका है (learn.resetProgress देखें) # Deferred plug-in effects Refresh Deferred Effects स्थगित इफ़ेक्ट्स रिफ्रेश करें diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index c4cdb9d544..7f2f03afb2 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -7456,6 +7456,12 @@ breakText Glyphs Glifák @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}` + Reset progress Előrehaladás visszaállítása + Mark Tutorial Step Oktatóanyag lépésének megjelölése + Reset Learn Progress Tanulási előrehaladás visszaállítása +@msg missing `step` (see learn.show) hiányzik: `step` (lásd: learn.show) +@msg `step` must be a step number from 0 to {last} a `step` értéke 0 és {last} közötti lépésszám legyen +@msg `{id}` is finished (see learn.resetProgress) befejezve: `{id}` (lásd: learn.resetProgress) # Deferred plug-in effects Refresh Deferred Effects Elhalasztott effektusok frissítése diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index 208877f673..c7c6b956db 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -6880,6 +6880,12 @@ breakText Glyphs Glif @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 + Reset progress Atur ulang kemajuan + Mark Tutorial Step Tandai langkah tutorial + Reset Learn Progress Atur ulang kemajuan belajar +@msg missing `step` (see learn.show) `step` tidak ada (lihat learn.show) +@msg `step` must be a step number from 0 to {last} `step` harus berupa nomor langkah dari 0 sampai {last} +@msg `{id}` is finished (see learn.resetProgress) `{id}` sudah selesai (lihat learn.resetProgress) # Deferred plug-in effects Refresh Deferred Effects Segarkan efek yang ditunda diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 534a7a181f..f832a5baf3 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -7102,6 +7102,12 @@ breakText Glyphs Glifi @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 + Reset progress Reimposta i progressi + Mark Tutorial Step Segna il passaggio del tutorial + Reset Learn Progress Reimposta i progressi di apprendimento +@msg missing `step` (see learn.show) manca `step` (vedi learn.show) +@msg `step` must be a step number from 0 to {last} `step` deve essere un numero di passaggio da 0 a {last} +@msg `{id}` is finished (see learn.resetProgress) `{id}` è terminato (vedi learn.resetProgress) # Deferred plug-in effects Refresh Deferred Effects Aggiorna effetti rimandati diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index e7759dde56..3abb388f49 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -7168,6 +7168,12 @@ breakText Glyphs 字形 @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}` はこのスキルの最後のチュートリアルです + Reset progress 進行状況をリセット + Mark Tutorial Step チュートリアルの手順をマーク + Reset Learn Progress 学習の進行状況をリセット +@msg missing `step` (see learn.show) `step` が指定されていません (learn.show を参照) +@msg `step` must be a step number from 0 to {last} `step` は 0 から {last} までの手順番号にしてください +@msg `{id}` is finished (see learn.resetProgress) `{id}` は完了しています (learn.resetProgress を参照) # Deferred plug-in effects Refresh Deferred Effects 保留中の効果を更新 diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index f2d20fc031..55f726dc38 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -7586,6 +7586,12 @@ font-script Japanese იაპონური @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}` თავისი უნარის ბოლო გაკვეთილია + Reset progress პროგრესის ჩამოყრა + Mark Tutorial Step გაკვეთილის ნაბიჯის მონიშვნა + Reset Learn Progress სწავლის პროგრესის ჩამოყრა +@msg missing `step` (see learn.show) აკლია `step` (იხ. learn.show) +@msg `step` must be a step number from 0 to {last} `step` უნდა იყოს ნაბიჯის ნომერი 0-დან {last}-მდე +@msg `{id}` is finished (see learn.resetProgress) `{id}` დასრულებულია (იხ. learn.resetProgress) # Deferred plug-in effects Refresh Deferred Effects გადადებული ეფექტების განახლება diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 6dc588ad14..abe2b4bc28 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -7063,6 +7063,12 @@ breakText Glyphs 글리프 @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}`은(는) 해당 기술의 마지막 튜토리얼입니다 + Reset progress 진행률 재설정 + Mark Tutorial Step 튜토리얼 단계 표시 + Reset Learn Progress 학습 진행률 재설정 +@msg missing `step` (see learn.show) `step`이 없습니다(learn.show 참조) +@msg `step` must be a step number from 0 to {last} `step`은 0에서 {last} 사이의 단계 번호여야 합니다 +@msg `{id}` is finished (see learn.resetProgress) `{id}`은(는) 완료되었습니다(learn.resetProgress 참조) # Deferred plug-in effects Refresh Deferred Effects 보류된 효과 새로 고침 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index 208c864ee9..ed281382a4 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -9323,6 +9323,12 @@ breakText Glyphs Glyphs @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 + Reset progress Voortgang opnieuw instellen + Mark Tutorial Step Stap van tutorial markeren + Reset Learn Progress Leervoortgang opnieuw instellen +@msg missing `step` (see learn.show) `step` ontbreekt (zie learn.show) +@msg `step` must be a step number from 0 to {last} `step` moet een stapnummer van 0 tot en met {last} zijn +@msg `{id}` is finished (see learn.resetProgress) `{id}` is voltooid (zie learn.resetProgress) # Deferred plug-in effects Refresh Deferred Effects Uitgestelde effecten vernieuwen diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index c5379e5aac..08e96e6a35 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -7329,6 +7329,12 @@ breakText Glyphs Glify @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 + Reset progress Resetuj postępy + Mark Tutorial Step Oznacz krok samouczka + Reset Learn Progress Resetuj postępy nauki +@msg missing `step` (see learn.show) brak `step` (patrz learn.show) +@msg `step` must be a step number from 0 to {last} `step` musi być numerem kroku od 0 do {last} +@msg `{id}` is finished (see learn.resetProgress) `{id}` jest zakończony (patrz learn.resetProgress) # Deferred plug-in effects Refresh Deferred Effects Odśwież odroczone efekty diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 1fd7d6100b..79c4d9dc8e 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -7661,6 +7661,12 @@ swatch Yellow Green Verde-amarelado @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 + Reset progress Redefinir progresso + Mark Tutorial Step Marcar Etapa do Tutorial + Reset Learn Progress Redefinir Progresso de Aprendizagem +@msg missing `step` (see learn.show) falta `step` (veja learn.show) +@msg `step` must be a step number from 0 to {last} `step` deve ser um número de etapa de 0 a {last} +@msg `{id}` is finished (see learn.resetProgress) `{id}` está concluído (veja learn.resetProgress) # Deferred plug-in effects Refresh Deferred Effects Atualizar Efeitos Adiados diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index b3ec6a7684..9f608618e7 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -7561,6 +7561,12 @@ shape Open Открытый @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}` является последним уроком своего навыка + Reset progress Сбросить прогресс + Mark Tutorial Step Отметить шаг урока + Reset Learn Progress Сбросить прогресс обучения +@msg missing `step` (see learn.show) отсутствует `step` (см. learn.show) +@msg `step` must be a step number from 0 to {last} `step` должен быть номером шага от 0 до {last} +@msg `{id}` is finished (see learn.resetProgress) урок `{id}` завершён (см. learn.resetProgress) # Deferred plug-in effects Refresh Deferred Effects Обновить отложенные эффекты diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 39ab197402..6156ca23d5 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -8910,6 +8910,12 @@ digits Hindi Hindi @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 + Reset progress Återställ framsteg + Mark Tutorial Step Markera steg i självstudie + Reset Learn Progress Återställ inlärningsframsteg +@msg missing `step` (see learn.show) `step` saknas (se learn.show) +@msg `step` must be a step number from 0 to {last} `step` måste vara ett stegnummer från 0 till {last} +@msg `{id}` is finished (see learn.resetProgress) `{id}` är slutförd (se learn.resetProgress) # Deferred plug-in effects Refresh Deferred Effects Uppdatera uppskjutna effekter diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index 9a6dd61e01..7120d44707 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -7545,6 +7545,12 @@ breakText Glyphs Glifler @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 + Reset progress İlerlemeyi sıfırla + Mark Tutorial Step Öğretici Adımını İşaretle + Reset Learn Progress Öğrenme İlerlemesini Sıfırla +@msg missing `step` (see learn.show) `step` eksik (bkz. learn.show) +@msg `step` must be a step number from 0 to {last} `step`, 0 ile {last} arasında bir adım numarası olmalıdır +@msg `{id}` is finished (see learn.resetProgress) `{id}` bitirildi (bkz. learn.resetProgress) # Deferred plug-in effects Refresh Deferred Effects Ertelenen Efektleri Yenile diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index aa8d0c56f9..84f296b0a6 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -7082,6 +7082,12 @@ breakText Glyphs Гліфи @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}` є останнім уроком своєї навички + Reset progress Скинути прогрес + Mark Tutorial Step Позначити крок уроку + Reset Learn Progress Скинути прогрес навчання +@msg missing `step` (see learn.show) відсутній `step` (див. learn.show) +@msg `step` must be a step number from 0 to {last} `step` має бути номером кроку від 0 до {last} +@msg `{id}` is finished (see learn.resetProgress) урок `{id}` завершено (див. learn.resetProgress) # Deferred plug-in effects Refresh Deferred Effects Оновити відкладені ефекти diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 7b7480cbfd..e8b9e5758f 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -7083,6 +7083,12 @@ breakText Glyphs Glyph @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ó + Reset progress Đặt lại tiến độ + Mark Tutorial Step Đánh dấu bước hướng dẫn + Reset Learn Progress Đặt lại tiến độ học +@msg missing `step` (see learn.show) thiếu `step` (xem learn.show) +@msg `step` must be a step number from 0 to {last} `step` phải là số bước từ 0 đến {last} +@msg `{id}` is finished (see learn.resetProgress) `{id}` đã hoàn tất (xem learn.resetProgress) # Deferred plug-in effects Refresh Deferred Effects Làm mới hiệu ứng bị hoãn diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index e1ec9f659c..719968086f 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -7311,6 +7311,12 @@ breakText Glyphs 字形 @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}` 是其技能的最后一个教程 + Reset progress 重置进度 + Mark Tutorial Step 标记教程步骤 + Reset Learn Progress 重置学习进度 +@msg missing `step` (see learn.show) 缺少 `step` (请参阅 learn.show) +@msg `step` must be a step number from 0 to {last} `step` 必须是 0 到 {last} 之间的步骤编号 +@msg `{id}` is finished (see learn.resetProgress) `{id}` 已完成 (请参阅 learn.resetProgress) # Deferred plug-in effects Refresh Deferred Effects 刷新延迟的效果 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 778147950e..121a8b8c02 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -7119,6 +7119,12 @@ breakText Glyphs 字符 @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}` 是其技能的最後一個教學課程 + Reset progress 重設進度 + Mark Tutorial Step 標記教學課程步驟 + Reset Learn Progress 重設學習進度 +@msg missing `step` (see learn.show) 未提供 `step` (請參閱 learn.show) +@msg `step` must be a step number from 0 to {last} `step` 必須是 0 到 {last} 之間的步驟編號 +@msg `{id}` is finished (see learn.resetProgress) `{id}` 已完成 (請參閱 learn.resetProgress) # Deferred plug-in effects Refresh Deferred Effects 重新整理延後的效果 diff --git a/crates/ui-egui/src/menus/ui_commands.rs b/crates/ui-egui/src/menus/ui_commands.rs index af9252c333..d43d17220c 100644 --- a/crates/ui-egui/src/menus/ui_commands.rs +++ b/crates/ui-egui/src/menus/ui_commands.rs @@ -667,7 +667,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ "learn.list", "List Tutorials", "", - "{} the Learn panel's skills in the order its Skills tab lists them: {skills: [{id, title, tutorials: [{id, title, steps: number of steps}]}]}", + "{} the Learn panel's skills in the order its Skills tab lists them, with the progress its rings, counts and check marks show: {skills: [{id, title, done: number of its tutorials finished, total: number of its tutorials, tutorials: [{id, title, steps: number of steps, finished: bool, completedSteps: [zero-based indexes of the steps marked done, every step once finished]}]}]}", ), ( "learn.skills", @@ -693,11 +693,17 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ "", "{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.markStep", + "Mark Tutorial Step", + "", + "{tutorial: id (see learn.list), step: zero-based step number, complete?: bool} mark a step of a tutorial whose sample learn.openSample opened as done (true, omitted or null) or not done (false), as the check box before the step does; marking does not finish the tutorial; a missing, non-string or unknown id, a missing step or one the tutorial does not have, a complete that is not true or false, a tutorial whose sample was not opened, or a finished tutorial is an error and changes nothing; the preferences keep the marks; returns {tutorial, step, complete}", + ), ( "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}", + "{tutorial: id (see learn.list)} mark a tutorial whose sample learn.openSample opened as finished, with all its steps done, as its Finish tutorial button does; finishing it again keeps one entry, and needs no sample after a restart; a missing, non-string or unknown id, or a tutorial not finished whose sample was not opened, is an error and changes nothing; the preferences keep the tutorials finished; returns {tutorial, finished: true, next: id of the next tutorial in its skill, or null after the skill's last one}", ), ( "learn.next", @@ -705,6 +711,12 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ "", "{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", ), + ( + "learn.resetProgress", + "Reset Learn Progress", + "", + "{} clear the Learn panel's progress, every tutorial finished and every step marked done, as Reset progress under the list of skills does; open documents, opened practice samples and the Learn view stay as they are; returns what learn.list returns", + ), ("window.brightness", "UI Brightness", "", "{brightness: dark|mediumDark|mediumLight|light}"), ( "window.workspace", diff --git a/crates/ui-egui/src/panels/learn.rs b/crates/ui-egui/src/panels/learn.rs index 5d24167c99..a4f2cd52e4 100644 --- a/crates/ui-egui/src/panels/learn.rs +++ b/crates/ui-egui/src/panels/learn.rs @@ -1,18 +1,23 @@ //! Learn panel (Window › Learn, and Explore tutorials at the right end of the app bar): short //! hands-on tutorials grouped by skill ([`catalog::SKILLS`]). The For you tab holds the Get started //! 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 +//! skill with a progress ring that fills as its tutorials are finished and the count of tutorials +//! finished of all; a skill lists its tutorials, each finished one with a check mark, 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. 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. The Home screen's Learn section shows the same card -//! ([`guide_card`]) and the tutorials, which open the panel. +//! sample as a new untitled document; the check box before each step then marks it done, Finish +//! tutorial marks the tutorial finished with all its steps, and Next Up shows the next tutorial in +//! the same skill. The preferences keep the progress ([`progress`]); Reset progress under the +//! skills clears it. `learn.list` lists the tutorials with their progress, `learn.skills` shows the +//! list of skills, `learn.show` opens one tutorial, `learn.locateTool` shows where a tool is, +//! `learn.openSample`, `learn.markStep`, `learn.finish` and `learn.next` run the practice controls, +//! `learn.resetProgress` clears the progress, and `window.learn` shows or hides the panel. The Home +//! screen's Learn section shows the same card ([`guide_card`]) and the tutorials, which open the +//! panel. pub mod catalog; pub mod locate; +pub mod progress; pub mod samples; use egui::{Sense, Stroke, Ui, vec2}; @@ -41,12 +46,10 @@ 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. +/// The tutorials whose samples View more opened while the app runs. 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) { @@ -126,17 +129,26 @@ fn skills(app: &mut VectorcraftApp, ui: &mut Ui) { } crate::widgets::subheader(ui, skill.title); for tutorial in skill.tutorials { - if row(ui, tutorial.title, None).clicked() { + if row(ui, tutorial.title, Mark::Check(app.ui.learn_progress.is_finished(tutorial.id))).clicked() { app.ui.learn.tutorial = Some(tutorial.id); } } return; } for skill in catalog::SKILLS { - if row(ui, skill.title, Some(skill.tutorials.len())).clicked() { + let ring = Mark::Ring { done: app.ui.learn_progress.done(skill), total: skill.tutorials.len() }; + if row(ui, skill.title, ring).clicked() { app.ui.learn.skill = Some(skill.id); } } + if !app.ui.learn_progress.is_empty() { + ui.add_space(6.0); + if ui.button(tl!("Reset progress")).clicked() + && let Err(e) = app.run("learn.resetProgress", json!({})) + { + app.status(e); + } + } } /// The Back button above a skill's tutorials and a tutorial's steps; true when it was clicked. @@ -144,15 +156,26 @@ fn back(ui: &mut Ui) -> bool { crate::widgets::icon_button(ui, "chevron-left", tl!("Back"), false, 22.0).clicked() } -/// A tutorial's title and its numbered steps. The tool's name in a step is a blue link: a click, or -/// Enter or Space while it has the keyboard focus, shows where the tool is (`learn.locateTool`). -fn steps(app: &mut VectorcraftApp, ui: &mut Ui, tutorial: &catalog::Tutorial) { +/// A tutorial's title and its numbered steps, each with a check box that shows whether it is done. +/// The check boxes can be changed once the tutorial's sample is open and until it is finished +/// (`learn.markStep`). The tool's name in a step is a blue link: a click, or Enter or Space while it +/// has the keyboard focus, shows where the tool is (`learn.locateTool`). +fn steps(app: &mut VectorcraftApp, ui: &mut Ui, tutorial: &'static catalog::Tutorial) { let t = Tokens::get(ui.ctx()); ui.label(egui::RichText::new(tutorial.title).size(13.0).color(t.text_strong)); ui.add_space(4.0); + let markable = app.ui.learn_practice.opened.contains(&tutorial.id) && !app.ui.learn_progress.is_finished(tutorial.id); let mut locate = None; + let mut marked = None; for (i, step) in tutorial.steps.iter().enumerate() { + let mut done = app.ui.learn_progress.step_done(tutorial, i); ui.horizontal_top(|ui| { + let resp = ui.add_enabled(markable, egui::Checkbox::without_text(&mut done)); + let name = format!("{}. {}", i + 1, step.plain()); + resp.widget_info(|| egui::WidgetInfo::selected(egui::Role::CheckBox, markable, done, &name)); + if resp.changed() { + marked = Some((i, done)); + } ui.label(egui::RichText::new(format!("{}.", i + 1)).color(t.text_dim)); let (Some(tool), Some((before, name, after))) = (step.tool, step.tool_name()) else { ui.add(egui::Label::new(egui::RichText::new(step.text).color(t.text)).wrap()); @@ -173,6 +196,11 @@ fn steps(app: &mut VectorcraftApp, ui: &mut Ui, tutorial: &catalog::Tutorial) { }); ui.add_space(2.0); } + if let Some((step, complete)) = marked + && let Err(e) = app.run("learn.markStep", json!({ "tutorial": tutorial.id, "step": step, "complete": complete })) + { + app.status(e); + } if let Some(tool) = locate { if let Err(e) = app.run("learn.locateTool", json!({ "tool": tool })) { app.status(e); @@ -205,12 +233,11 @@ fn practice(app: &mut VectorcraftApp, ui: &mut Ui, tutorial: &'static catalog::T 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() { + if app.ui.learn_practice.opened.contains(&id) && !app.ui.learn_progress.is_finished(id) && ui.button(tl!("Finish tutorial")).clicked() { run = Some(("learn.finish", params.clone())); } }); - if app.ui.learn_practice.finished.contains(&id) { + if app.ui.learn_progress.is_finished(id) { ui.add_space(6.0); match catalog::next(id) { Some(next) => { @@ -236,46 +263,112 @@ fn practice(app: &mut VectorcraftApp, ui: &mut Ui, tutorial: &'static catalog::T } } -/// 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) -> egui::Response { +/// What a row shows before its title. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Mark { + /// A skill: a progress ring that fills as its tutorials are finished, and after the title the + /// count of tutorials finished of all. + Ring { done: usize, total: usize }, + /// A tutorial: a check mark once it is finished (true). + Check(bool), +} + +/// The radius of a row's progress ring or check mark. +const MARK_RADIUS: f32 = 7.0; + +/// A clickable row: `title`, with `mark` before it. Screen readers get the count of a skill's +/// tutorials finished, or Finished for a finished tutorial, as the row's value. +fn row(ui: &mut Ui, title: &str, mark: Mark) -> egui::Response { let t = Tokens::get(ui.ctx()); let (rect, resp) = ui.allocate_exact_size(vec2(ui.available_width(), ROW_HEIGHT), Sense::click()); if resp.hovered() { ui.painter().rect_filled(rect, egui::CornerRadius::same(4), t.hover); } + let painter = ui.painter().with_clip_rect(rect); let mut left = rect.left() + 6.0; let mut right = rect.right() - 6.0; let chevron = egui::Rect::from_center_size(egui::pos2(right - 6.0, rect.center().y), vec2(12.0, 12.0)); crate::icons::paint(ui, "chevron-right", chevron, t.text_dim); right = chevron.left() - 6.0; - if let Some(total) = tutorials { - ui.painter().circle_stroke(egui::pos2(left + 8.0, rect.center().y), 7.0, Stroke::new(2.0, t.border)); - left += 24.0; - let count = ui.painter().layout_no_wrap(format!("0/{total}"), egui::FontId::proportional(11.5), t.text_dim); - right -= count.size().x; - ui.painter().galley(egui::pos2(right, rect.center().y - count.size().y / 2.0), count, t.text_dim); - right -= 6.0; - } - let name = ui.painter().layout(title.to_string(), egui::FontId::proportional(12.5), t.text, f32::INFINITY); + let at = egui::pos2(left + 8.0, rect.center().y); + left += 24.0; + let value = match mark { + Mark::Ring { done, total } => { + ring(&painter, &t, at, done, total); + let count = painter.layout_no_wrap(format!("{done}/{total}"), egui::FontId::proportional(11.5), t.text_dim); + right -= count.size().x; + painter.galley(egui::pos2(right, rect.center().y - count.size().y / 2.0), count, t.text_dim); + right -= 6.0; + Some(format!("{done}/{total}")) + } + Mark::Check(finished) => { + check(&painter, &t, at, finished); + finished.then(|| tl!("Finished").to_string()) + } + }; + let name = painter.layout(title.to_string(), egui::FontId::proportional(12.5), t.text, f32::INFINITY); let clip = egui::Rect::from_min_max(egui::pos2(left, rect.top()), egui::pos2(right.max(left), rect.bottom())); - ui.painter().with_clip_rect(clip).galley(egui::pos2(left, rect.center().y - name.size().y / 2.0), name, t.text); - resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, ui.is_enabled(), title)); + painter.with_clip_rect(clip).galley(egui::pos2(left, rect.center().y - name.size().y / 2.0), name, t.text); + resp.widget_info(|| egui::WidgetInfo { + current_text_value: value.clone(), + ..egui::WidgetInfo::labeled(egui::Role::Button, ui.is_enabled(), title) + }); resp.on_hover_cursor(egui::CursorIcon::PointingHand) } +/// A skill's progress ring at `center`: an outline, with the share `done` of `total` drawn over it +/// clockwise from the top. With no tutorials it stays empty. +fn ring(painter: &egui::Painter, t: &Tokens, center: egui::Pos2, done: usize, total: usize) { + painter.circle_stroke(center, MARK_RADIUS, Stroke::new(2.0, t.border)); + if done == 0 || total == 0 { + return; + } + let stroke = Stroke::new(2.0, t.accent); + if done >= total { + painter.circle_stroke(center, MARK_RADIUS, stroke); + return; + } + let sweep = std::f32::consts::TAU * done as f32 / total as f32; + // About one segment per 1/32 of a turn, and never fewer than two. + let segments = ((32.0 * done as f32 / total as f32).ceil() as usize).max(2); + let points = (0..=segments) + .map(|k| { + let angle = -std::f32::consts::FRAC_PI_2 + sweep * k as f32 / segments as f32; + center + MARK_RADIUS * vec2(angle.cos(), angle.sin()) + }) + .collect(); + painter.add(egui::Shape::line(points, stroke)); +} + +/// A tutorial's mark at `center`: an outline, filled with a check mark once it is finished. +fn check(painter: &egui::Painter, t: &Tokens, center: egui::Pos2, finished: bool) { + if !finished { + painter.circle_stroke(center, MARK_RADIUS, Stroke::new(1.0, t.border)); + return; + } + painter.circle_filled(center, MARK_RADIUS, t.accent); + let tick = vec![center + vec2(-3.5, 0.0), center + vec2(-1.0, 2.5), center + vec2(3.5, -2.5)]; + painter.add(egui::Shape::line(tick, Stroke::new(1.5, t.panel))); +} + /// The Learn panel's UI commands: `window.learn`, `learn.list`, `learn.skills`, `learn.show`, -/// `learn.locateTool`, `learn.openSample`, `learn.finish` and `learn.next`. +/// `learn.locateTool`, `learn.openSample`, `learn.markStep`, `learn.finish`, `learn.next` and +/// `learn.resetProgress`. pub(crate) fn command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option> { Some(match id { "window.learn" => toggle(app, p), - "learn.list" => Ok(list()), + "learn.list" => Ok(list(&app.ui.learn_progress)), "learn.skills" => skills_list(app), "learn.show" => open(app, p), "learn.locateTool" => locate::locate_tool(app, p), "learn.openSample" => open_sample(app, p), + "learn.markStep" => mark_step(app, p), "learn.finish" => finish(app, p), "learn.next" => next(app, p), + "learn.resetProgress" => { + app.ui.learn_progress = progress::Progress::default(); + Ok(list(&app.ui.learn_progress)) + } _ => return None, }) } @@ -300,11 +393,25 @@ fn toggle(app: &mut VectorcraftApp, p: &Value) -> Result { Ok(json!(visible(&app.ui))) } -/// `learn.list`: every skill with its tutorials and their step counts. -fn list() -> Value { +/// `learn.list`: every skill with its count of tutorials finished (`done`) of all (`total`), and its +/// tutorials with their step counts, whether each is finished, and the zero-based indexes of its +/// steps marked done (`completedSteps`). +fn list(progress: &progress::Progress) -> Value { let skills = catalog::SKILLS.iter().map(|s| { - let tutorials: Vec = s.tutorials.iter().map(|t| json!({"id": t.id, "title": t.title, "steps": t.steps.len()})).collect(); - json!({"id": s.id, "title": s.title, "tutorials": tutorials}) + let tutorials: Vec = s + .tutorials + .iter() + .map(|t| { + json!({ + "id": t.id, + "title": t.title, + "steps": t.steps.len(), + "finished": progress.is_finished(t.id), + "completedSteps": progress.completed_steps(t), + }) + }) + .collect(); + json!({"id": s.id, "title": s.title, "done": progress.done(s), "total": s.tutorials.len(), "tutorials": tutorials}) }); json!({ "skills": skills.collect::>() }) } @@ -314,7 +421,7 @@ fn list() -> Value { fn skills_list(app: &mut VectorcraftApp) -> Result { crate::panel_docking::reveal_panel(app, ID)?; app.ui.learn = View { tab: Tab::Skills, skill: None, tutorial: None }; - Ok(list()) + Ok(list(&app.ui.learn_progress)) } /// `learn.show {tutorial}`: show the panel on the Skills tab with that tutorial's steps → the @@ -357,19 +464,42 @@ fn open_sample(app: &mut VectorcraftApp, p: &Value) -> Result { 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. +/// `learn.markStep {tutorial, step, complete?}`: a step's check box. Mark step `step` (zero-based) +/// of a tutorial whose sample was opened done (`complete` true or omitted) or not done (false) → +/// `{tutorial, step, complete}`. A finished tutorial keeps every step done, so marking one of its +/// steps is an error; so are a step the tutorial does not have and a `complete` that is not true or +/// false. An error changes nothing. +fn mark_step(app: &mut VectorcraftApp, p: &Value) -> Result { + let (_, tutorial) = tutorial_param(p)?; + let id = tutorial.id; + let last = tutorial.steps.len().saturating_sub(1); + let step = match p.get("step") { + None | Some(Value::Null) => return Err("missing `step` (see learn.show)".into()), + Some(v) => v.as_u64().and_then(|s| usize::try_from(s).ok()).filter(|s| *s < tutorial.steps.len()), + }; + let step = step.ok_or_else(|| format!("`step` must be a step number from 0 to {last}"))?; + let complete = crate::menus::opt_bool(p, "complete")?.unwrap_or(true); + if !app.ui.learn_practice.opened.contains(&id) { + return Err(format!("open the practice sample of `{id}` first (see learn.openSample)")); + } + if app.ui.learn_progress.is_finished(id) { + return Err(format!("`{id}` is finished (see learn.resetProgress)")); + } + app.ui.learn_progress.mark(id, step, complete); + Ok(json!({"tutorial": id, "step": step, "complete": complete})) +} + +/// `learn.finish {tutorial}`: Finish tutorial. Mark a tutorial whose sample was opened finished, +/// with all its steps done → `{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, +/// also after a restart, when its sample has not been opened again. The Learn view stays as it is. fn finish(app: &mut VectorcraftApp, p: &Value) -> Result { let (_, tutorial) = tutorial_param(p)?; let id = tutorial.id; - let practice = &mut app.ui.learn_practice; - if !practice.opened.contains(&id) { + if !app.ui.learn_practice.opened.contains(&id) && !app.ui.learn_progress.is_finished(id) { return Err(format!("open the practice sample of `{id}` first (see learn.openSample)")); } - if !practice.finished.contains(&id) { - practice.finished.push(id); - } + app.ui.learn_progress.finish(id); Ok(json!({"tutorial": id, "finished": true, "next": catalog::next(id).map(|t| t.id)})) } @@ -379,7 +509,7 @@ fn finish(app: &mut VectorcraftApp, p: &Value) -> Result { fn next(app: &mut VectorcraftApp, p: &Value) -> Result { let (skill, tutorial) = tutorial_param(p)?; let id = tutorial.id; - if !app.ui.learn_practice.finished.contains(&id) { + if !app.ui.learn_progress.is_finished(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"))?; diff --git a/crates/ui-egui/src/panels/learn/progress.rs b/crates/ui-egui/src/panels/learn/progress.rs new file mode 100644 index 0000000000..a425b0e58f --- /dev/null +++ b/crates/ui-egui/src/panels/learn/progress.rs @@ -0,0 +1,122 @@ +//! What a learner has done in the Learn panel: the steps marked done in each tutorial and the +//! tutorials finished. The UI preferences keep it (`UiState::learn_progress`), so it lasts from +//! one start to the next where the preferences are saved. Saved progress is read leniently: an +//! unknown tutorial id, a step the tutorial does not have, a repeated entry or a value of the wrong +//! kind is skipped, and the rest of the preferences read as usual. + +use std::collections::BTreeMap; + +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use serde_json::Value; + +use super::catalog; + +/// The most entries read from one saved list or map; the rest are skipped. +const MAX_READ: usize = 1024; + +/// The tutorials finished and the steps marked done. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Progress { + /// The tutorials finished, in the order they were finished. Each id is listed once. + pub finished: Vec<&'static str>, + /// For each tutorial not finished, the zero-based indexes of the steps marked done, ascending. + /// A tutorial with no step marked has no entry. + pub steps: BTreeMap<&'static str, Vec>, +} + +impl Progress { + /// Nothing is finished and no step is marked. + pub fn is_empty(&self) -> bool { + self.finished.is_empty() && self.steps.is_empty() + } + + /// Whether the tutorial `id` is finished. + pub fn is_finished(&self, id: &str) -> bool { + self.finished.contains(&id) + } + + /// The zero-based indexes of `tutorial`'s steps marked done, ascending: every step of a + /// finished tutorial. + pub fn completed_steps(&self, tutorial: &catalog::Tutorial) -> Vec { + if self.is_finished(tutorial.id) { + return (0..tutorial.steps.len()).collect(); + } + self.steps.get(tutorial.id).cloned().unwrap_or_default() + } + + /// Whether step `step` of `tutorial` is marked done. + pub fn step_done(&self, tutorial: &catalog::Tutorial, step: usize) -> bool { + self.is_finished(tutorial.id) || self.steps.get(tutorial.id).is_some_and(|s| s.contains(&step)) + } + + /// Mark step `step` of the unfinished tutorial `id` done (`complete`) or not done. + pub fn mark(&mut self, id: &'static str, step: usize, complete: bool) { + let marks = self.steps.entry(id).or_default(); + match (marks.binary_search(&step), complete) { + (Err(at), true) => marks.insert(at, step), + (Ok(at), false) => { + marks.remove(at); + } + _ => {} + } + if marks.is_empty() { + self.steps.remove(id); + } + } + + /// Mark the tutorial `id` finished, with all its steps done. Finishing it again keeps one entry. + pub fn finish(&mut self, id: &'static str) { + if !self.is_finished(id) { + self.finished.push(id); + } + self.steps.remove(id); + } + + /// How many of `skill`'s tutorials are finished. + pub fn done(&self, skill: &catalog::Skill) -> usize { + skill.tutorials.iter().filter(|t| self.is_finished(t.id)).count() + } + + /// Progress read from a saved value, keeping only what the catalog knows (see the module). + fn from_value(saved: &Value) -> Self { + let mut progress = Progress::default(); + let known = |id: &Value| id.as_str().and_then(catalog::tutorial).map(|(_, t)| t); + if let Some(finished) = saved.get("finished").and_then(Value::as_array) { + for tutorial in finished.iter().take(MAX_READ).filter_map(known) { + progress.finish(tutorial.id); + } + } + if let Some(steps) = saved.get("steps").and_then(Value::as_object) { + for (id, marks) in steps.iter().take(MAX_READ) { + let Some((_, tutorial)) = catalog::tutorial(id) else { continue }; + if progress.is_finished(tutorial.id) { + continue; + } + let indexes = marks.as_array().into_iter().flatten().take(MAX_READ).filter_map(Value::as_u64); + for step in indexes.filter_map(|s| usize::try_from(s).ok()).filter(|s| *s < tutorial.steps.len()) { + progress.mark(tutorial.id, step, true); + } + } + } + progress + } +} + +/// The saved form: `{finished: [id], steps: {id: [index]}}`. +#[derive(Serialize)] +struct Saved<'a> { + finished: &'a [&'static str], + steps: &'a BTreeMap<&'static str, Vec>, +} + +impl Serialize for Progress { + fn serialize(&self, serializer: S) -> Result { + Saved { finished: &self.finished, steps: &self.steps }.serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for Progress { + fn deserialize>(deserializer: D) -> Result { + Ok(Progress::from_value(&Value::deserialize(deserializer)?)) + } +} diff --git a/crates/ui-egui/src/panels/learn/tests.rs b/crates/ui-egui/src/panels/learn/tests.rs index 4bd00648d1..ea65a64fe9 100644 --- a/crates/ui-egui/src/panels/learn/tests.rs +++ b/crates/ui-egui/src/panels/learn/tests.rs @@ -1,7 +1,7 @@ use std::collections::HashSet; -use egui::accesskit::Node; -use serde_json::json; +use egui::accesskit::{Node, Role, Toggled}; +use serde_json::{Value, json}; use vectorcraft_engine::Session; use super::{ID, Practice, Tab, View, catalog, samples, show}; @@ -225,7 +225,8 @@ fn learn_show_opens_a_tutorial_and_learn_list_lists_them() { assert_eq!(skills.len(), catalog::SKILLS.len()); let count: usize = skills.iter().map(|s| s["tutorials"].as_array().map_or(0, Vec::len)).sum(); assert_eq!(count, catalog::SKILLS.iter().map(|s| s.tutorials.len()).sum::()); - assert_eq!(skills[2]["tutorials"][1], json!({"id": "gradient", "title": "Add a gradient", "steps": 3})); + assert_eq!(skills[2]["tutorials"][1], json!({"id": "gradient", "title": "Add a gradient", "steps": 3, "finished": false, "completedSteps": []})); + assert_eq!((skills[2]["done"].clone(), skills[2]["total"].clone()), (json!(0), json!(3))); } /// Every tutorial has one sample of its own, in the catalog's order, each one small and read @@ -279,7 +280,8 @@ fn view_more_opens_the_tutorial_s_sample_as_a_new_untitled_document() { 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![] }); + assert_eq!(app.ui.learn_practice, Practice { opened: vec!["pen-paths"] }); + assert!(app.ui.learn_progress.is_empty(), "opening a sample marks nothing"); let r = app.run("learn.openSample", json!({"tutorial": "gradient"})).unwrap(); assert_eq!(r["tutorial"], "gradient"); @@ -306,7 +308,7 @@ fn finishing_a_tutorial_offers_next_up_in_its_skill() { 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"]); + assert_eq!(app.ui.learn_progress.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(); @@ -350,7 +352,7 @@ fn the_practice_controls_work_from_the_keyboard() { 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"]); + assert_eq!(app.ui.learn_progress.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") }); @@ -408,13 +410,14 @@ fn learn_finish_and_learn_next_follow_the_practice() { 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()); + assert!(app.ui.learn_progress.is_empty()); 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"); + assert_eq!(app.ui.learn_progress.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)); @@ -471,6 +474,7 @@ fn the_practice_commands_reject_a_missing_or_unknown_tutorial() { 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!(app.ui.learn_progress.is_empty()); assert_eq!(app.session.documents().len(), 1); assert!(!shown(&app)); } @@ -581,3 +585,274 @@ fn learn_locate_tool_is_unavailable_while_the_toolbars_are_hidden() { assert_eq!(reply["location"], "unavailable", "{reply}"); assert!(app.ui.learn_locate.is_none() && !app.ui.toolbar && app.ui.tab_hid.is_some(), "Tab's state stays"); } + +/// The screen-reader name of step `i` of the Pen tutorial's check box. +fn pen_step(i: usize) -> String { + let (_, tutorial) = catalog::tutorial("pen-paths").unwrap(); + format!("{}. {}", i + 1, tutorial.steps[i].plain()) +} + +/// The check boxes of what `draw` draws, from the top, as (enabled, checked). +fn check_boxes(app: &mut VectorcraftApp, ctx: &egui::Context, draw: fn(&mut VectorcraftApp, &mut egui::Ui)) -> Vec<(bool, bool)> { + let nodes = frame(app, ctx, vec![], draw); + let mut boxes: Vec<&Node> = nodes.iter().filter(|n| n.role() == Role::CheckBox).collect(); + boxes.sort_by(|a, b| a.bounds().map_or(0.0, |r| r.y0).total_cmp(&b.bounds().map_or(0.0, |r| r.y0))); + boxes.iter().map(|n| (!n.is_disabled(), n.toggled() == Some(Toggled::True))).collect() +} + +/// Each step has a check box, off until the tutorial's sample is open. A click marks only its own +/// step and a second click clears it; marking every step does not finish the tutorial. A finished +/// tutorial shows every step done, and its check boxes and `learn.markStep` no longer change them. +#[test] +fn a_step_s_check_box_marks_only_that_step() { + let mut app = app(); + let ctx = context(); + assert_eq!(check_boxes(&mut app, &ctx, pen_paths), [(false, false); 3], "off before View more"); + click(&mut app, &ctx, &pen_step(1), pen_paths); + assert!(app.ui.learn_progress.is_empty(), "a check box that is off changes nothing"); + app.run("learn.openSample", json!({"tutorial": "pen-paths"})).unwrap(); + click(&mut app, &ctx, &pen_step(1), pen_paths); + assert_eq!(check_boxes(&mut app, &ctx, pen_paths), [(true, false), (true, true), (true, false)], "{}", app.ui.status); + let list = app.run("learn.list", json!({})).unwrap(); + assert_eq!(list["skills"][1]["tutorials"][0]["completedSteps"], json!([1])); + assert_eq!(list["skills"][1]["tutorials"][1]["completedSteps"], json!([]), "other tutorials stay as they are"); + click(&mut app, &ctx, &pen_step(1), pen_paths); + assert!(app.ui.learn_progress.is_empty(), "a second click clears the mark"); + for i in 0..3 { + click(&mut app, &ctx, &pen_step(i), pen_paths); + } + assert_eq!(check_boxes(&mut app, &ctx, pen_paths), [(true, true); 3]); + assert!(!app.ui.learn_progress.is_finished("pen-paths"), "marking every step does not finish the tutorial"); + assert!(painted_text(&mut app, pen_paths).contains("Finish tutorial")); + click(&mut app, &ctx, &pen_step(0), pen_paths); + click(&mut app, &ctx, "Finish tutorial", pen_paths); + assert_eq!(app.ui.learn_progress.finished, ["pen-paths"]); + assert!(app.ui.learn_progress.steps.is_empty(), "a finished tutorial keeps no marks of its own"); + assert_eq!(check_boxes(&mut app, &ctx, pen_paths), [(false, true); 3], "Finish marks every step"); + let before = app.ui.learn_progress.clone(); + click(&mut app, &ctx, &pen_step(2), pen_paths); + let error = app.run("learn.markStep", json!({"tutorial": "pen-paths", "step": 2, "complete": false})).unwrap_err(); + assert_eq!(error, "`pen-paths` is finished (see learn.resetProgress)"); + assert_eq!(app.ui.learn_progress, before); +} + +/// The check boxes are reached with Tab and changed with Space. +#[test] +fn a_step_s_check_box_works_from_the_keyboard() { + let mut app = app(); + let ctx = context(); + app.run("learn.openSample", json!({"tutorial": "pen-paths"})).unwrap(); + frame(&mut app, &ctx, vec![], pen_paths); + tab_to(&mut app, &ctx, &pen_step(2), pen_paths); + key(&mut app, &ctx, egui::Key::Space, pen_paths); + assert_eq!(app.ui.learn_progress.steps.get("pen-paths"), Some(&vec![2]), "{}", app.ui.status); +} + +/// Finishing tutorials fills only their own skill's ring and count, once each however often they +/// are finished; the skill's tutorial rows show which are finished; `learn.list` answers the same. +#[test] +fn finishing_tutorials_fills_their_skill_s_ring_and_count() { + let mut app = app(); + let ctx = context(); + for id in ["pen-paths", "smooth-curves"] { + app.run("learn.openSample", json!({"tutorial": id})).unwrap(); + for _ in 0..2 { + app.run("learn.finish", json!({"tutorial": id})).unwrap(); + } + } + let values = |app: &mut VectorcraftApp, view: View| { + app.ui.learn = view; + let nodes = frame(app, &ctx, vec![], show); + nodes + .iter() + .filter(|n| n.role() == Role::Button) + .map(|n| (n.label().unwrap_or_default().to_string(), n.value().map(str::to_string))) + .collect::>() + }; + let skills = View { tab: Tab::Skills, skill: None, tutorial: None }; + let rows = values(&mut app, skills.clone()); + for skill in catalog::SKILLS { + let count = if skill.id == "draw" { "2/3".to_string() } else { format!("0/{}", skill.tutorials.len()) }; + assert!(rows.contains(&(skill.title.to_string(), Some(count.clone()))), "{} {count} in {rows:?}", skill.id); + } + assert!(painted_text(&mut app, show).contains("2/3")); + let list = app.run("learn.list", json!({})).unwrap(); + let draw = &list["skills"][1]; + assert_eq!((draw["done"].clone(), draw["total"].clone()), (json!(2), json!(3))); + assert_eq!(draw["tutorials"][1]["finished"], json!(true)); + assert_eq!(draw["tutorials"][1]["completedSteps"], json!([0, 1, 2])); + assert_eq!(list["skills"][2]["done"], json!(0), "other skills stay at 0"); + + let rows = values(&mut app, View { tab: Tab::Skills, skill: Some("draw"), tutorial: None }); + assert!(rows.contains(&("Draw paths with the Pen".to_string(), Some("Finished".to_string()))), "{rows:?}"); + assert!(rows.contains(&("Draw smooth curves".to_string(), Some("Finished".to_string()))), "{rows:?}"); + assert!(rows.contains(&("Sketch with the Pencil".to_string(), None)), "{rows:?}"); + + app.run("learn.openSample", json!({"tutorial": "pencil-sketch"})).unwrap(); + app.run("learn.finish", json!({"tutorial": "pencil-sketch"})).unwrap(); + let rows = values(&mut app, skills); + assert!(rows.contains(&("Draw and build".to_string(), Some("3/3".to_string()))), "every tutorial finished: {rows:?}"); +} + +/// A skill row with no tutorials, which the catalog does not have today, shows an empty ring and +/// 0/0. +fn empty_skill(_: &mut VectorcraftApp, ui: &mut egui::Ui) { + super::row(ui, "Empty skill", super::Mark::Ring { done: 0, total: 0 }); +} + +#[test] +fn a_skill_with_no_tutorials_shows_0_of_0() { + let mut app = app(); + let text = painted_text(&mut app, empty_skill); + assert!(text.contains("Empty skill") && text.contains("0/0"), "{text}"); +} + +/// `learn.markStep` marks a step done by default and with `complete: true`, clears it with `false`, +/// and answers what it did; a bad param is an error and changes nothing. +#[test] +fn learn_mark_step_checks_its_params() { + let mut app = app(); + let open = "open the practice sample of `gradient` first (see learn.openSample)"; + assert_eq!(app.run("learn.markStep", json!({"tutorial": "gradient", "step": 0})).unwrap_err(), open); + assert!(app.ui.learn_progress.is_empty()); + app.run("learn.openSample", json!({"tutorial": "gradient"})).unwrap(); + assert_eq!( + app.run("learn.markStep", json!({"tutorial": "gradient", "step": 0})).unwrap(), + json!({"tutorial": "gradient", "step": 0, "complete": true}) + ); + let before = app.ui.learn_progress.clone(); + let missing = "missing `step` (see learn.show)"; + let range = "`step` must be a step number from 0 to 2"; + for (params, error) in [ + (json!({"tutorial": "gradient"}), missing), + (json!({"tutorial": "gradient", "step": null}), missing), + (json!({"tutorial": "gradient", "step": 3}), range), + (json!({"tutorial": "gradient", "step": -1}), range), + (json!({"tutorial": "gradient", "step": 1.5}), range), + (json!({"tutorial": "gradient", "step": "1"}), range), + (json!({"tutorial": "gradient", "step": u64::MAX}), range), + (json!({"tutorial": "gradient", "step": 1, "complete": "yes"}), "`complete` must be true or false"), + (json!({"tutorial": "nope", "step": 1}), "unknown tutorial `nope` (see learn.list)"), + (json!({"step": 1}), "missing `tutorial` (see learn.list)"), + ] { + assert_eq!(app.run("learn.markStep", params.clone()).unwrap_err(), error, "{params}"); + assert_eq!(app.ui.learn_progress, before, "{params}"); + } + let r = app.run("learn.markStep", json!({"tutorial": "gradient", "step": 2, "complete": null})).unwrap(); + assert_eq!(r, json!({"tutorial": "gradient", "step": 2, "complete": true})); + let r = app.run("learn.markStep", json!({"tutorial": "gradient", "step": 0, "complete": false})).unwrap(); + assert_eq!(r, json!({"tutorial": "gradient", "step": 0, "complete": false})); + let r = app.run("learn.markStep", json!({"tutorial": "gradient", "step": 0, "complete": false})).unwrap(); + assert_eq!(r["complete"], json!(false), "clearing a step that is not marked is no error"); + assert_eq!(app.ui.learn_progress.steps.get("gradient"), Some(&vec![2])); + assert!(app.ui.learn_progress.finished.is_empty()); +} + +/// `learn.resetProgress` and Reset progress clear every tutorial finished and every step marked; +/// documents, opened samples and the Learn view stay. Reset progress shows only when there is +/// progress. +#[test] +fn reset_progress_clears_only_the_progress() { + let mut app = app(); + let ctx = context(); + let skills = View { tab: Tab::Skills, skill: None, tutorial: None }; + app.ui.learn = skills.clone(); + assert!(!painted_text(&mut app, show).contains("Reset progress")); + app.run("learn.openSample", json!({"tutorial": "pen-paths"})).unwrap(); + app.run("learn.finish", json!({"tutorial": "pen-paths"})).unwrap(); + app.run("learn.openSample", json!({"tutorial": "gradient"})).unwrap(); + app.run("learn.markStep", json!({"tutorial": "gradient", "step": 1})).unwrap(); + let view = app.ui.learn.clone(); + let docs = app.session.documents().len(); + let reply = app.run("learn.resetProgress", json!({})).unwrap(); + assert!(app.ui.learn_progress.is_empty()); + assert_eq!(reply, app.run("learn.list", json!({})).unwrap()); + assert_eq!(reply["skills"][1]["done"], json!(0)); + assert_eq!((app.ui.learn.clone(), app.session.documents().len()), (view, docs)); + assert_eq!(app.ui.learn_practice.opened, ["pen-paths", "gradient"], "opened samples stay open for practice"); + + app.run("learn.finish", json!({"tutorial": "gradient"})).unwrap(); + app.ui.learn = skills.clone(); + assert!(painted_text(&mut app, show).contains("Reset progress")); + click(&mut app, &ctx, "Reset progress", show); + assert!(app.ui.learn_progress.is_empty(), "{}", app.ui.status); + assert_eq!(app.ui.learn, skills); + assert!(!painted_text(&mut app, show).contains("Reset progress")); +} + +/// The UI state after its preferences are saved and read again, as at the next start. +fn restarted(app: &VectorcraftApp) -> crate::state::UiState { + let bytes = serde_json::to_vec(&app.ui).unwrap(); + crate::state::UiState::read_preferences(&bytes).expect("the preferences read").ui.sanitized() +} + +/// The preferences keep the tutorials finished and the steps marked; the opened samples, the +/// Learn view and a tool's mark last only while the app runs, so a step can be marked again only +/// after View more. +#[test] +fn the_preferences_keep_the_progress() { + let mut app = app(); + app.run("learn.openSample", json!({"tutorial": "pen-paths"})).unwrap(); + app.run("learn.finish", json!({"tutorial": "pen-paths"})).unwrap(); + app.run("learn.openSample", json!({"tutorial": "gradient"})).unwrap(); + for step in [2, 0] { + app.run("learn.markStep", json!({"tutorial": "gradient", "step": step})).unwrap(); + } + let list = app.run("learn.list", json!({})).unwrap(); + let ui = restarted(&app); + assert_eq!(ui.learn_progress, app.ui.learn_progress); + assert_eq!(ui.learn_progress.steps.get("gradient"), Some(&vec![0, 2])); + assert_eq!((ui.learn_practice.clone(), ui.learn.clone(), ui.learn_locate.is_none()), (Practice::default(), View::default(), true)); + + let mut next = self::app(); + next.ui = ui; + assert_eq!(next.run("learn.list", json!({})).unwrap(), list); + assert_eq!(next.run("learn.next", json!({"tutorial": "pen-paths"})).unwrap()["tutorial"], "smooth-curves"); + let again = next.run("learn.finish", json!({"tutorial": "pen-paths"})).unwrap(); + assert_eq!(again, json!({"tutorial": "pen-paths", "finished": true, "next": "smooth-curves"}), "finishing again needs no sample"); + assert_eq!(next.ui.learn_progress.finished, ["pen-paths"]); + let open = "open the practice sample of `gradient` first (see learn.openSample)"; + assert_eq!(next.run("learn.markStep", json!({"tutorial": "gradient", "step": 1})).unwrap_err(), open); +} + +/// Saved preferences with `learn_progress` set to `progress` (left out for None), and +/// `control_bar` on, read as at a start. +fn read_progress(progress: Option) -> crate::state::UiState { + let mut saved = serde_json::to_value(crate::state::UiState::default()).unwrap(); + let fields = saved.as_object_mut().unwrap(); + fields.insert("control_bar".into(), json!(true)); + match progress { + Some(p) => fields.insert("learn_progress".into(), p), + None => fields.remove("learn_progress"), + }; + let ui = crate::state::UiState::read_preferences(&serde_json::to_vec(&saved).unwrap()).expect("the preferences read").ui; + assert!(ui.control_bar, "the other preferences read as usual"); + ui +} + +/// Preferences saved before the progress was kept read with none. Saved progress keeps only the +/// catalog's tutorials and steps, each once, and a value of the wrong kind, or a huge list, does not +/// stop the rest of the preferences from reading. +#[test] +fn saved_progress_is_read_leniently() { + assert!(read_progress(None).learn_progress.is_empty()); + for bad in [json!("garbage"), json!(3), json!(null), json!([1, 2]), json!({"finished": "pen-paths", "steps": [0]})] { + assert!(read_progress(Some(bad.clone())).learn_progress.is_empty(), "{bad}"); + } + let ui = read_progress(Some(json!({ + "finished": ["pen-paths", "nope", 3, "pen-paths", "gradient"], + "steps": { + "gradient": [0], + "smooth-curves": [2, 0, 2, 99, -1, "1", 1.5], + "nope": [0], + "copy-colors": "x", + "add-headline": [], + }, + }))); + assert_eq!(ui.learn_progress.finished, ["pen-paths", "gradient"]); + let steps: Vec<(&str, Vec)> = ui.learn_progress.steps.iter().map(|(id, s)| (*id, s.clone())).collect(); + assert_eq!(steps, [("smooth-curves", vec![0, 2])], "a finished tutorial keeps no marks"); + let ui = read_progress(Some(json!({"finished": vec!["pen-paths"; 100_000], "steps": {"smooth-curves": vec![1; 100_000]}}))); + assert_eq!(ui.learn_progress.finished, ["pen-paths"]); + assert_eq!(ui.learn_progress.steps.get("smooth-curves"), Some(&vec![1])); +} diff --git a/crates/ui-egui/src/state.rs b/crates/ui-egui/src/state.rs index d06c0dbcb2..80a87b4563 100644 --- a/crates/ui-egui/src/state.rs +++ b/crates/ui-egui/src/state.rs @@ -424,10 +424,11 @@ pub struct UiState { /// is on screen. Not saved. #[serde(skip)] pub learn_locate: Option, - /// The tutorials whose practice samples were opened and the tutorials finished, kept while the - /// app runs. + /// The tutorials whose practice samples were opened, kept while the app runs. #[serde(skip)] pub learn_practice: crate::panels::learn::Practice, + /// The Learn panel's tutorials finished and steps marked done, saved with the preferences. + pub learn_progress: crate::panels::learn::progress::Progress, pub control_bar: bool, /// The Control bar is docked to the bottom of the window instead of the top /// (`window.control.dock`). @@ -1121,6 +1122,7 @@ impl Default for UiState { learn: Default::default(), learn_locate: None, learn_practice: Default::default(), + learn_progress: Default::default(), control_bar: false, control_bar_bottom: false, control_bar_floating: false, diff --git a/docs/mcp.md b/docs/mcp.md index d90e74cd41..37ffd74300 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -2910,14 +2910,18 @@ own layout rules. Window › Learn and the Explore tutorials button beside the search icon at the right end of the app bar show and hide the Learn panel. Its For you tab holds a Get started section with a Quick skill guide card; choosing the card opens -the Skills tab. The Skills tab lists VectorCraft's own skills, each with its number of tutorials and an empty progress -ring. Choosing a skill lists its tutorials, choosing a tutorial shows its numbered steps, and Back goes up one level. +the Skills tab. The Skills tab lists VectorCraft's own skills, each with a progress ring and the count of its tutorials +finished of all its tutorials. Choosing a skill lists its tutorials, choosing a tutorial shows its numbered steps, and +Back goes up one level. The tutorial text is English. `window.learn {show?}` shows the panel (`show: true`), hides it (`show: false`) or toggles it (`show` omitted or null), and answers whether it shows; Window › Learn and Explore tutorials run it. 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.skills {}` shows the panel on its Skills tab with the list of +other value of `show` is an error and changes nothing. `learn.list {}` → +`{skills: [{id, title, done, total, tutorials: [{id, title, steps, finished, completedSteps}]}]}`, in the order the +Skills tab lists them, where `done` is the number of the skill's tutorials finished, `total` the number of its +tutorials, `steps` the number of a tutorial's steps, `finished` whether it is finished, and `completedSteps` the +zero-based indexes of its steps marked done (every step of a finished tutorial). `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 @@ -2927,18 +2931,33 @@ Under a tutorial's steps, View more opens the tutorial's practice sample, a smal 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 → +says so and shows Back to skills instead; Next Up never moves to another skill. 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}` +last one; the tutorial's sample must have been opened, and finishing it again keeps one entry, also after a restart +without opening the sample again. `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). +Each step has a check box before its number that shows whether it is done, and a skill's ring fills as its tutorials +are finished, as the help page describes; a finished tutorial has a check mark in its skill's list. A step is marked +done by its check box, which can be changed once the tutorial's sample is open and until the tutorial is finished; +marking every step does not finish the tutorial. Finish tutorial marks all its steps done, and a finished tutorial's +steps stay marked. Reset progress under the list of skills, shown while there is progress, clears every tutorial +finished and every step marked. The desktop app keeps the progress in its preferences, so it lasts to the next start; +when two windows change it, the one that saves last keeps its progress. The web build keeps it while the page is open. +Marking steps by hand, keeping the progress and Reset progress are VectorCraft's own rules. +`learn.markStep {tutorial, step, complete?}` runs a step's check box → `{tutorial, step, complete}`: `step` is the +zero-based step number, and `complete` marks the step done (`true`, omitted or null) or not done (`false`). A missing +`step` or one the tutorial does not have, a `complete` that is not true or false, a tutorial whose sample was not +opened, and a finished tutorial are errors. `learn.resetProgress {}` runs Reset progress and answers what `learn.list` +answers; open documents, opened samples and the Learn view stay as they are. For both, a missing, non-string or +unknown `tutorial` is an error, and an error changes nothing. + The Home screen has a Learn section between Recent Files and the new-file presets, as [the help page](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/homescreen-overview.html) lists Learn, with in-app tutorials, among the Home screen's options. The section holds the Quick skill guide card, @@ -2950,9 +2969,11 @@ rules. ```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.markStep","params":{"tutorial":"pen-paths","step":0}}} {"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":{}}} +{"name":"invoke_menu","arguments":{"command":"learn.resetProgress","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 diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 6a728ac1b0..4fb51873a3 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -938,9 +938,10 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - Perspective Distort and Free Distort (`object.distort`) turn each warped object's bounding box as the warp turns the object at its own center, the objects inside groups, compound paths and clip groups and the art of linked opacity masks included, so the next Selection or Free Transform drag uses the tilted box; Object › Transform › Reset Bounding Box squares it again, and corners left where they are add no undo step (VectorCraft's own rule; `bboxrotate::distort_turns_an_upright_box_with_the_warp_at_its_center`, `distort_turns_each_member_and_linked_mask_by_its_own_warp`, `mirrored_and_collapsing_distorts_keep_valid_angles`, `distort_angles_undo_redo_and_reopen_together`, `xform::unchanged_distort_records_no_history`). - The Free Transform widget's Constrain works in Free Distort as well as in Free Transform ([the help page](https://helpx.adobe.com/illustrator/using/tool-techniques/free-transform-tool.html)). With Constrain on, or Shift held, a corner dragged in Free Distort moves only along the line through the opposite corner, in the box's own proportions, and the other three corners stay in place; on a turned box the line runs through the box's own corners. The line is VectorCraft's own rule. Cmd/Ctrl+Alt+Shift on a corner still drags in Perspective Distort, where Constrain stays unavailable (tools `xform::free::tests::free_distort_corner_keeps_to_its_diagonal_with_shift_or_constrain`, `shift_pressed_during_a_free_distort_drag_constrains_it`, `a_turned_box_is_constrained_along_its_own_diagonal`, `along_diagonal_of_a_degenerate_box`; engine `xform::free_transform_constrained_distort_keeps_the_corner_on_its_diagonal`; ui-egui `free_transform::tests::the_widget_sets_the_mode_and_a_corner_drag_then_distorts`). - 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`). + - 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 progress rings and counts of tutorials finished; 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`. + - 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. 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`. + - A Learn tutorial's steps are marked complete as the learner works, and a skill's progress ring fills as its tutorials are finished, 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)). Each step has a check box that marks it done once the tutorial's sample is open (`learn.markStep`); Finish tutorial marks every step done; a skill's count shows its tutorials finished of all, and a finished tutorial has a check mark in its skill's list. Marking steps by hand, the count, keeping the progress in the preferences (the web build keeps it while the page is open) and Reset progress (`learn.resetProgress`) are VectorCraft's own rules; `learn.list` reports the progress. Regression tests: `a_step_s_check_box_marks_only_that_step`, `a_step_s_check_box_works_from_the_keyboard`, `finishing_tutorials_fills_their_skill_s_ring_and_count`, `a_skill_with_no_tutorials_shows_0_of_0`, `learn_mark_step_checks_its_params`, `reset_progress_clears_only_the_progress`, `the_preferences_keep_the_progress` and `saved_progress_is_read_leniently` in `crates/ui-egui/src/panels/learn/tests.rs`. - The Home screen has a Learn section with the in-app tutorials, as the help page lists among the Home screen's options ([the help page](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/homescreen-overview.html)): the Quick skill guide card shows the Learn panel's list of skills (`learn.skills`), and each tutorial, listed under its skill, shows the panel at its steps (`learn.show`). It shows with no document open and over open documents, and a choice leaves the documents as they are. Its place between Recent Files and the new-file presets, its layout and what a choice opens are VectorCraft's own rules (`tests::home::home_has_a_learn_section_that_opens_the_learn_panel`, `home_learn_section_over_open_documents_leaves_them_as_they_are`, `home_quick_skill_guide_card_works_from_the_keyboard`). - 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`). diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 9bcce16a0b..dc68706f26 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -384,7 +384,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; 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` | +| 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 progress rings that fill and counts of tutorials finished, a tutorial shows its numbered steps with a check box each that marks the step done once its sample is open, a finished tutorial has a check mark, 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 with all its steps, and Next Up shows the next tutorial in the same skill; Reset progress clears the progress (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.markStep`, `learn.finish`, `learn.next`, `learn.resetProgress`; the mark's look and 3-second lifetime, marking steps by hand, keeping the progress in the preferences and Reset progress 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 |