Learn: the tabs, skill and tutorial rows and Back run commands

The For you and Skills tab buttons, a skill's row, a tutorial's row and
Back changed the Learn panel's view directly, so the control channel and
MCP could not choose a tab, open a skill's tutorials or go back.

- learn.tab {tab: forYou|skills} shows the panel on that tab; the Skills
  tab keeps the skill and tutorial it had open.
- learn.skill {skill} shows the panel on the Skills tab with that skill's
  tutorials and returns the skill as learn.list lists it.
- learn.back {} goes up one level, as Back does.
- A tutorial's row runs learn.show.
- The commands are listed in both UI command catalogs, and their labels
  have a row in every catalog.
- Docs: docs/mcp.md, docs/target-app-parity.md, docs/ui-parity.md.
This commit is contained in:
dotCooCoo
2026-11-13 21:06:48 -08:00
parent 1e4e3534f5
commit 2d70c6b0ae
34 changed files with 306 additions and 29 deletions
+18
View File
@@ -668,18 +668,36 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
"",
"{} the Learn panel's skills in the order its Skills tab lists them: {skills: [{id, title, tutorials: [{id, title, steps: number of steps}]}]}",
),
(
"learn.tab",
"Show Learn Tab",
"",
"{tab: forYou|skills} show the Learn panel on its For you or Skills tab, as the tab buttons do; the Skills tab keeps the skill and tutorial it had open; a missing, non-string or unknown tab is an error and changes nothing; returns {tab, skill, tutorial}, with null for no skill or no tutorial open",
),
(
"learn.skills",
"Back to Skills",
"",
"{} show the Learn panel on its Skills tab with the list of skills, with no skill and no tutorial open, as Back to skills after a skill's last tutorial and the Quick skill guide card do; returns what learn.list returns",
),
(
"learn.skill",
"Show Skill",
"",
"{skill: id (see learn.list)} show the Learn panel on its Skills tab with that skill's tutorials, as choosing the skill's row does; a missing, non-string or unknown id is an error and changes nothing; returns the skill as learn.list lists it: {id, title, tutorials: [{id, title, steps}]}",
),
(
"learn.show",
"Show Tutorial",
"",
"{tutorial: id (see learn.list)} show the Learn panel on its Skills tab with that tutorial's numbered steps, as choosing its skill and then the tutorial does; a missing, non-string or unknown id is an error and changes nothing; returns {skill, tutorial, title, steps: [text]}",
),
(
"learn.back",
"Back One Level",
"",
"{} show the Learn panel and go up one level on its Skills tab, as its Back button does: from a tutorial's steps to its skill's tutorials, and from a skill's tutorials to the list of skills; on the list of skills or the For you tab the view stays as it is; returns {tab, skill, tutorial}, as learn.tab does",
),
(
"learn.locateTool",
"Show Tool Location",
+3
View File
@@ -6854,6 +6854,9 @@ breakText Glyphs Глифи
Open Tutorial Sample Отваряне на примерния файл на урока
Finish Tutorial Завършване на урок
Next Tutorial Следващ урок
Show Learn Tab Показване на раздел в Обучение
Show Skill Показване на умение
Back One Level Едно ниво назад
Back to Skills Обратно към уменията
@msg tutorial `{id}` has no practice sample урокът `{id}` няма примерен файл за упражнение
@msg open the practice sample of `{id}` first (see learn.openSample) първо отворете примерния файл за упражнение на `{id}` (вижте learn.openSample)
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -6907,6 +6907,9 @@ shape Open Otvoren
Open Tutorial Sample Otvori uzorak tutorijala
Finish Tutorial Završi tutorijal
Next Tutorial Sljedeći tutorijal
Show Learn Tab Prikaži karticu u Učenju
Show Skill Prikaži vještinu
Back One Level Jedan nivo nazad
Back to Skills Nazad na vještine
@msg tutorial `{id}` has no practice sample tutorijal `{id}` nema uzorak za vježbu
@msg open the practice sample of `{id}` first (see learn.openSample) prvo otvorite uzorak za vježbu tutorijala `{id}` (vidi learn.openSample)
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -7439,6 +7439,9 @@ breakText Glyphs Glifs
Open Tutorial Sample Obre la mostra del tutorial
Finish Tutorial Acaba el tutorial
Next Tutorial Tutorial següent
Show Learn Tab Mostra una pestanya d'Aprèn
Show Skill Mostra l'habilitat
Back One Level Torna un nivell enrere
Back to Skills Torna a les habilitats
@msg tutorial `{id}` has no practice sample el tutorial `{id}` no té cap mostra de pràctica
@msg open the practice sample of `{id}` first (see learn.openSample) obriu primer la mostra de pràctica de `{id}` (vegeu learn.openSample)
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -7458,6 +7458,9 @@ breakText Glyphs Glyfy
Open Tutorial Sample Otevřít ukázku výukového kurzu
Finish Tutorial Dokončit výukový kurz
Next Tutorial Další výukový kurz
Show Learn Tab Zobrazit kartu Výuky
Show Skill Zobrazit dovednost
Back One Level O úroveň zpět
Back to Skills Zpět na dovednosti
@msg tutorial `{id}` has no practice sample výukový kurz `{id}` nemá cvičnou ukázku
@msg open the practice sample of `{id}` first (see learn.openSample) nejprve otevřete cvičnou ukázku kurzu `{id}` (viz learn.openSample)
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -6869,6 +6869,9 @@ breakText Glyphs Glyffer
Open Tutorial Sample Åbn eksempel til vejledning
Finish Tutorial Afslut vejledning
Next Tutorial Næste vejledning
Show Learn Tab Vis fane i Lær
Show Skill Vis færdighed
Back One Level Ét niveau tilbage
Back to Skills Tilbage til færdigheder
@msg tutorial `{id}` has no practice sample vejledningen `{id}` har intet øvelseseksempel
@msg open the practice sample of `{id}` first (see learn.openSample) åbn først øvelseseksemplet til `{id}` (se learn.openSample)
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -7054,6 +7054,9 @@ breakText Glyphs Glyphen
Open Tutorial Sample Übungsdatei des Lernprogramms öffnen
Finish Tutorial Lernprogramm abschließen
Next Tutorial Nächstes Lernprogramm
Show Learn Tab Registerkarte in Lernen anzeigen
Show Skill Fertigkeit anzeigen
Back One Level Eine Ebene zurück
Back to Skills Zurück zu den Fertigkeiten
@msg tutorial `{id}` has no practice sample das Lernprogramm `{id}` hat keine Übungsdatei
@msg open the practice sample of `{id}` first (see learn.openSample) öffnen Sie zuerst die Übungsdatei von `{id}` (siehe learn.openSample)
Can't render this file because it is too large.
+3
View File
@@ -7411,6 +7411,9 @@ breakText Glyphs Γλύφοι
Open Tutorial Sample Άνοιγμα δείγματος οδηγού εκμάθησης
Finish Tutorial Ολοκλήρωση οδηγού εκμάθησης
Next Tutorial Επόμενος οδηγός εκμάθησης
Show Learn Tab Εμφάνιση καρτέλας της Εκμάθησης
Show Skill Εμφάνιση δεξιότητας
Back One Level Ένα επίπεδο πίσω
Back to Skills Πίσω στις δεξιότητες
@msg tutorial `{id}` has no practice sample ο οδηγός εκμάθησης `{id}` δεν έχει δείγμα εξάσκησης
@msg open the practice sample of `{id}` first (see learn.openSample) ανοίξτε πρώτα το δείγμα εξάσκησης του `{id}` (δείτε learn.openSample)
Can't render this file because it is too large.
+3
View File
@@ -7402,6 +7402,9 @@ breakText Glyphs Glifos
Open Tutorial Sample Abrir muestra del tutorial
Finish Tutorial Terminar tutorial
Next Tutorial Tutorial siguiente
Show Learn Tab Mostrar pestaña de Aprender
Show Skill Mostrar habilidad
Back One Level Volver un nivel
Back to Skills Volver a las habilidades
@msg tutorial `{id}` has no practice sample el tutorial `{id}` no tiene muestra de práctica
@msg open the practice sample of `{id}` first (see learn.openSample) abre primero la muestra de práctica de `{id}` (consulta learn.openSample)
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -7493,6 +7493,9 @@ breakText Glyphs Merkit
Open Tutorial Sample Avaa opetusohjelman harjoitustiedosto
Finish Tutorial Viimeistele opetusohjelma
Next Tutorial Seuraava opetusohjelma
Show Learn Tab Näytä Opi-paneelin välilehti
Show Skill Näytä taito
Back One Level Taso takaisin
Back to Skills Takaisin taitoihin
@msg tutorial `{id}` has no practice sample opetusohjelmalla `{id}` ei ole harjoitustiedostoa
@msg open the practice sample of `{id}` first (see learn.openSample) avaa ensin opetusohjelman `{id}` harjoitustiedosto (ks. learn.openSample)
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -7453,6 +7453,9 @@ breakText Glyphs Glyphes
Open Tutorial Sample Ouvrir le fichier d’exercice du tutoriel
Finish Tutorial Terminer le tutoriel
Next Tutorial Tutoriel suivant
Show Learn Tab Afficher un onglet d'Apprendre
Show Skill Afficher la compétence
Back One Level Revenir d'un niveau
Back to Skills Retour aux compétences
@msg tutorial `{id}` has no practice sample le tutoriel `{id}` n’a pas de fichier d’exercice
@msg open the practice sample of `{id}` first (see learn.openSample) ouvrez d’abord le fichier d’exercice de `{id}` (voir learn.openSample)
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -8849,6 +8849,9 @@ theme Appearance अपीयरेंस
Open Tutorial Sample ट्यूटोरियल का नमूना खोलें
Finish Tutorial ट्यूटोरियल पूरा करें
Next Tutorial अगला ट्यूटोरियल
Show Learn Tab सीखें का टैब दिखाएँ
Show Skill कौशल दिखाएँ
Back One Level एक स्तर वापस
Back to Skills कौशल पर वापस जाएँ
@msg tutorial `{id}` has no practice sample ट्यूटोरियल `{id}` का कोई अभ्यास नमूना नहीं है
@msg open the practice sample of `{id}` first (see learn.openSample) पहले `{id}` का अभ्यास नमूना खोलें (learn.openSample देखें)
Can't render this file because it is too large.
+3
View File
@@ -7442,6 +7442,9 @@ breakText Glyphs Glifák
Open Tutorial Sample Oktatóanyag mintafájljának megnyitása
Finish Tutorial Oktatóanyag befejezése
Next Tutorial Következő oktatóanyag
Show Learn Tab Tanulás lap megjelenítése
Show Skill Készség megjelenítése
Back One Level Egy szinttel vissza
Back to Skills Vissza a készségekhez
@msg tutorial `{id}` has no practice sample nincs gyakorló mintafájl ehhez az oktatóanyaghoz: `{id}`
@msg open the practice sample of `{id}` first (see learn.openSample) előbb nyissa meg a gyakorló mintafájlt ehhez: `{id}` (lásd: learn.openSample)
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -6869,6 +6869,9 @@ breakText Glyphs Glif
Open Tutorial Sample Buka contoh tutorial
Finish Tutorial Selesaikan tutorial
Next Tutorial Tutorial berikutnya
Show Learn Tab Tampilkan tab Belajar
Show Skill Tampilkan keterampilan
Back One Level Kembali satu tingkat
Back to Skills Kembali ke keterampilan
@msg tutorial `{id}` has no practice sample tutorial `{id}` tidak memiliki contoh latihan
@msg open the practice sample of `{id}` first (see learn.openSample) buka contoh latihan `{id}` terlebih dahulu (lihat learn.openSample)
Can't render this file because it contains an unexpected character in line 2429 and column 5.
+3
View File
@@ -7088,6 +7088,9 @@ breakText Glyphs Glifi
Open Tutorial Sample Apri l’esempio del tutorial
Finish Tutorial Termina il tutorial
Next Tutorial Tutorial successivo
Show Learn Tab Mostra una scheda di Impara
Show Skill Mostra competenza
Back One Level Indietro di un livello
Back to Skills Torna alle competenze
@msg tutorial `{id}` has no practice sample il tutorial `{id}` non ha un esempio di esercitazione
@msg open the practice sample of `{id}` first (see learn.openSample) apri prima l’esempio di esercitazione di `{id}` (vedi learn.openSample)
Can't render this file because it is too large.
+3
View File
@@ -7154,6 +7154,9 @@ breakText Glyphs 字形
Open Tutorial Sample チュートリアルのサンプルを開く
Finish Tutorial チュートリアルを完了
Next Tutorial 次のチュートリアル
Show Learn Tab 学ぶのタブを表示
Show Skill スキルを表示
Back One Level 1 つ上の階層に戻る
Back to Skills スキルに戻る
@msg tutorial `{id}` has no practice sample チュートリアル `{id}` には練習用サンプルがありません
@msg open the practice sample of `{id}` first (see learn.openSample) 先に `{id}` の練習用サンプルを開いてください (learn.openSample を参照)
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -7572,6 +7572,9 @@ font-script Japanese იაპონური
Open Tutorial Sample გაკვეთილის ნიმუშის გახსნა
Finish Tutorial გაკვეთილის დასრულება
Next Tutorial შემდეგი გაკვეთილი
Show Learn Tab სწავლის ჩანართის ჩვენება
Show Skill უნარის ჩვენება
Back One Level ერთი დონით უკან
Back to Skills უნარებზე დაბრუნება
@msg tutorial `{id}` has no practice sample გაკვეთილს `{id}` არ აქვს სავარჯიშო ნიმუში
@msg open the practice sample of `{id}` first (see learn.openSample) ჯერ გახსენით `{id}` გაკვეთილის სავარჯიშო ნიმუში (იხ. learn.openSample)
Can't render this file because it is too large.
+3
View File
@@ -7049,6 +7049,9 @@ breakText Glyphs 글리프
Open Tutorial Sample 튜토리얼 샘플 열기
Finish Tutorial 튜토리얼 완료
Next Tutorial 다음 튜토리얼
Show Learn Tab 학습 탭 표시
Show Skill 기술 표시
Back One Level 한 단계 뒤로
Back to Skills 기술로 돌아가기
@msg tutorial `{id}` has no practice sample 튜토리얼 `{id}`에 연습용 샘플이 없습니다
@msg open the practice sample of `{id}` first (see learn.openSample) 먼저 `{id}`의 연습용 샘플을 여세요(learn.openSample 참조)
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -7491,6 +7491,9 @@ breakText Glyphs Glyphs
Open Tutorial Sample Oefenbestand van tutorial openen
Finish Tutorial Tutorial voltooien
Next Tutorial Volgende tutorial
Show Learn Tab Tabblad van Leren tonen
Show Skill Vaardigheid tonen
Back One Level Eén niveau terug
Back to Skills Terug naar vaardigheden
@msg tutorial `{id}` has no practice sample tutorial `{id}` heeft geen oefenbestand
@msg open the practice sample of `{id}` first (see learn.openSample) open eerst het oefenbestand van `{id}` (zie learn.openSample)
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -7315,6 +7315,9 @@ breakText Glyphs Glify
Open Tutorial Sample Otwórz przykład samouczka
Finish Tutorial Zakończ samouczek
Next Tutorial Następny samouczek
Show Learn Tab Pokaż kartę panelu Nauka
Show Skill Pokaż umiejętność
Back One Level Wróć o jeden poziom
Back to Skills Powrót do umiejętności
@msg tutorial `{id}` has no practice sample samouczek `{id}` nie ma przykładu do ćwiczeń
@msg open the practice sample of `{id}` first (see learn.openSample) najpierw otwórz przykład do ćwiczeń samouczka `{id}` (patrz learn.openSample)
Can't render this file because it is too large.
+3
View File
@@ -7531,6 +7531,9 @@ swatch Yellow Green Verde-amarelado
Open Tutorial Sample Abrir Amostra do Tutorial
Finish Tutorial Concluir Tutorial
Next Tutorial Próximo Tutorial
Show Learn Tab Mostrar Guia de Aprender
Show Skill Mostrar Habilidade
Back One Level Voltar um Nível
Back to Skills Voltar às Habilidades
@msg tutorial `{id}` has no practice sample o tutorial `{id}` não tem amostra de prática
@msg open the practice sample of `{id}` first (see learn.openSample) abra primeiro a amostra de prática de `{id}` (veja learn.openSample)
Can't render this file because it is too large.
+3
View File
@@ -7547,6 +7547,9 @@ shape Open Открытый
Open Tutorial Sample Открыть пример урока
Finish Tutorial Завершить урок
Next Tutorial Следующий урок
Show Learn Tab Показать вкладку панели «Обучение»
Show Skill Показать навык
Back One Level На уровень назад
Back to Skills Назад к навыкам
@msg tutorial `{id}` has no practice sample у урока `{id}` нет учебного примера
@msg open the practice sample of `{id}` first (see learn.openSample) сначала откройте учебный пример урока `{id}` (см. learn.openSample)
Can't render this file because it is too large.
+3
View File
@@ -8896,6 +8896,9 @@ digits Hindi Hindi
Open Tutorial Sample Öppna övningsexempel för självstudie
Finish Tutorial Slutför självstudie
Next Tutorial Nästa självstudie
Show Learn Tab Visa flik i Lär dig
Show Skill Visa färdighet
Back One Level En nivå tillbaka
Back to Skills Tillbaka till färdigheter
@msg tutorial `{id}` has no practice sample självstudien `{id}` har inget övningsexempel
@msg open the practice sample of `{id}` first (see learn.openSample) öppna först övningsexemplet för `{id}` (se learn.openSample)
Can't render this file because it is too large.
+3
View File
@@ -7414,6 +7414,9 @@ breakText Glyphs Glifler
Open Tutorial Sample Öğretici Örneğini Aç
Finish Tutorial Öğreticiyi Bitir
Next Tutorial Sonraki Öğretici
Show Learn Tab Öğren Sekmesini Göster
Show Skill Beceriyi Göster
Back One Level Bir Düzey Geri
Back to Skills Becerilere Dön
@msg tutorial `{id}` has no practice sample `{id}` öğreticisinin alıştırma örneği yok
@msg open the practice sample of `{id}` first (see learn.openSample) önce `{id}` öğreticisinin alıştırma örneğini açın (bkz. learn.openSample)
Can't render this file because it contains an unexpected character in line 5 and column 25.
+3
View File
@@ -7068,6 +7068,9 @@ breakText Glyphs Гліфи
Open Tutorial Sample Відкрити приклад уроку
Finish Tutorial Завершити урок
Next Tutorial Наступний урок
Show Learn Tab Показати вкладку панелі «Навчання»
Show Skill Показати навичку
Back One Level На рівень назад
Back to Skills Назад до навичок
@msg tutorial `{id}` has no practice sample урок `{id}` не має навчального прикладу
@msg open the practice sample of `{id}` first (see learn.openSample) спочатку відкрийте навчальний приклад уроку `{id}` (див. learn.openSample)
Can't render this file because it is too large.
+3
View File
@@ -7069,6 +7069,9 @@ breakText Glyphs Glyph
Open Tutorial Sample Mở tệp mẫu của hướng dẫn
Finish Tutorial Hoàn tất hướng dẫn
Next Tutorial Hướng dẫn tiếp theo
Show Learn Tab Hiện thẻ của Học
Show Skill Hiện kỹ năng
Back One Level Quay lại một cấp
Back to Skills Quay lại kỹ năng
@msg tutorial `{id}` has no practice sample hướng dẫn `{id}` không có tệp mẫu thực hành
@msg open the practice sample of `{id}` first (see learn.openSample) hãy mở tệp mẫu thực hành của `{id}` trước (xem learn.openSample)
Can't render this file because it contains an unexpected character in line 204 and column 5.
+3
View File
@@ -7297,6 +7297,9 @@ breakText Glyphs 字形
Open Tutorial Sample 打开教程示例
Finish Tutorial 完成教程
Next Tutorial 下一个教程
Show Learn Tab 显示学习选项卡
Show Skill 显示技能
Back One Level 返回上一级
Back to Skills 返回技能
@msg tutorial `{id}` has no practice sample 教程 `{id}` 没有练习示例
@msg open the practice sample of `{id}` first (see learn.openSample) 请先打开 `{id}` 的练习示例 (请参阅 learn.openSample)
Can't render this file because it is too large.
+3
View File
@@ -7104,6 +7104,9 @@ breakText Glyphs 字符
Open Tutorial Sample 開啟教學課程範例
Finish Tutorial 完成教學課程
Next Tutorial 下一個教學課程
Show Learn Tab 顯示學習索引標籤
Show Skill 顯示技能
Back One Level 返回上一層
Back to Skills 返回技能
@msg tutorial `{id}` has no practice sample 教學課程 `{id}` 沒有練習範例
@msg open the practice sample of `{id}` first (see learn.openSample) 請先開啟 `{id}` 的練習範例 (請參閱 learn.openSample)
Can't render this file because it contains an unexpected character in line 6 and column 47.
+18
View File
@@ -669,18 +669,36 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
"",
"{} the Learn panel's skills in the order its Skills tab lists them: {skills: [{id, title, tutorials: [{id, title, steps: number of steps}]}]}",
),
(
"learn.tab",
"Show Learn Tab",
"",
"{tab: forYou|skills} show the Learn panel on its For you or Skills tab, as the tab buttons do; the Skills tab keeps the skill and tutorial it had open; a missing, non-string or unknown tab is an error and changes nothing; returns {tab, skill, tutorial}, with null for no skill or no tutorial open",
),
(
"learn.skills",
"Back to Skills",
"",
"{} show the Learn panel on its Skills tab with the list of skills, with no skill and no tutorial open, as Back to skills after a skill's last tutorial and the Quick skill guide card do; returns what learn.list returns",
),
(
"learn.skill",
"Show Skill",
"",
"{skill: id (see learn.list)} show the Learn panel on its Skills tab with that skill's tutorials, as choosing the skill's row does; a missing, non-string or unknown id is an error and changes nothing; returns the skill as learn.list lists it: {id, title, tutorials: [{id, title, steps}]}",
),
(
"learn.show",
"Show Tutorial",
"",
"{tutorial: id (see learn.list)} show the Learn panel on its Skills tab with that tutorial's numbered steps, as choosing its skill and then the tutorial does; a missing, non-string or unknown id is an error and changes nothing; returns {skill, tutorial, title, steps: [text]}",
),
(
"learn.back",
"Back One Level",
"",
"{} show the Learn panel and go up one level on its Skills tab, as its Back button does: from a tutorial's steps to its skill's tutorials, and from a skill's tutorials to the list of skills; on the list of skills or the For you tab the view stays as it is; returns {tab, skill, tutorial}, as learn.tab does",
),
(
"learn.locateTool",
"Show Tool Location",
+104 -25
View File
@@ -5,8 +5,9 @@
//! 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,
//! next tutorial in the same skill. `learn.list` lists the tutorials, `learn.tab` chooses a tab,
//! `learn.skills` shows the list of skills, `learn.skill` opens one skill, `learn.show` opens one
//! tutorial, `learn.back` goes up one level, `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.
@@ -48,14 +49,23 @@ pub struct Practice {
pub finished: Vec<&'static str>,
}
/// Each tab with the name `learn.tab` takes for it.
const TABS: [(Tab, &str); 2] = [(Tab::ForYou, "forYou"), (Tab::Skills, "skills")];
pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
let mut chosen = None;
ui.horizontal(|ui| {
for (tab, label) in [(Tab::ForYou, tl!("For you")), (Tab::Skills, tl!("Skills"))] {
for ((tab, name), label) in TABS.into_iter().zip([tl!("For you"), tl!("Skills")]) {
if ui.selectable_label(app.ui.learn.tab == tab, label).clicked() {
app.ui.learn.tab = tab;
chosen = Some(name);
}
}
});
if let Some(name) = chosen
&& let Err(e) = app.run("learn.tab", json!({ "tab": name }))
{
app.status(e);
}
crate::widgets::divider(ui);
match app.ui.learn.tab {
Tab::ForYou => for_you(app, ui),
@@ -97,36 +107,40 @@ fn for_you(app: &mut VectorcraftApp, ui: &mut Ui) {
/// The Skills tab: the skills, a skill's tutorials, or a tutorial's steps.
fn skills(app: &mut VectorcraftApp, ui: &mut Ui) {
let mut run = None;
let open = app.ui.learn.tutorial.and_then(catalog::tutorial);
if let Some((skill, tutorial)) = open {
if let Some((_, tutorial)) = open {
if back(ui) {
app.ui.learn.skill = Some(skill.id);
app.ui.learn.tutorial = None;
run = Some(("learn.back", json!({})));
}
steps(app, ui, tutorial);
practice(app, ui, tutorial);
return;
}
if let Some(skill) = app.ui.learn.skill.and_then(catalog::skill) {
} else if let Some(skill) = app.ui.learn.skill.and_then(catalog::skill) {
if back(ui) {
app.ui.learn.skill = None;
run = Some(("learn.back", json!({})));
}
crate::widgets::subheader(ui, skill.title);
for tutorial in skill.tutorials {
if row(ui, tutorial.title, None).clicked() {
app.ui.learn.tutorial = Some(tutorial.id);
run = Some(("learn.show", json!({ "tutorial": tutorial.id })));
}
}
return;
}
for skill in catalog::SKILLS {
if row(ui, skill.title, Some(skill.tutorials.len())).clicked() {
app.ui.learn.skill = Some(skill.id);
} else {
for skill in catalog::SKILLS {
if row(ui, skill.title, Some(skill.tutorials.len())).clicked() {
run = Some(("learn.skill", json!({ "skill": skill.id })));
}
}
}
if let Some((cmd, params)) = run
&& let Err(e) = app.run(cmd, params)
{
app.status(e);
}
}
/// The Back button above a skill's tutorials and a tutorial's steps; true when it was clicked.
/// The Back button above a skill's tutorials and a tutorial's steps (`learn.back`); true when it
/// was clicked.
fn back(ui: &mut Ui) -> bool {
crate::widgets::icon_button(ui, "chevron-left", tl!("Back"), false, 22.0).clicked()
}
@@ -251,14 +265,18 @@ fn row(ui: &mut Ui, title: &str, tutorials: Option<usize>) -> egui::Response {
resp.on_hover_cursor(egui::CursorIcon::PointingHand)
}
/// The Learn panel's UI commands: `window.learn`, `learn.list`, `learn.skills`, `learn.show`,
/// `learn.locateTool`, `learn.openSample`, `learn.finish` and `learn.next`.
/// The Learn panel's UI commands: `window.learn`, `learn.list`, `learn.tab`, `learn.skills`,
/// `learn.skill`, `learn.show`, `learn.back`, `learn.locateTool`, `learn.openSample`, `learn.finish`
/// and `learn.next`.
pub(crate) fn command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<Result<Value, String>> {
Some(match id {
"window.learn" => toggle(app, p),
"learn.list" => Ok(list()),
"learn.tab" => tab(app, p),
"learn.skills" => skills_list(app),
"learn.skill" => open_skill(app, p),
"learn.show" => open(app, p),
"learn.back" => back_up(app),
"learn.locateTool" => locate::locate_tool(app, p),
"learn.openSample" => open_sample(app, p),
"learn.finish" => finish(app, p),
@@ -289,11 +307,72 @@ fn toggle(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
/// `learn.list`: every skill with its tutorials and their step counts.
fn list() -> Value {
let skills = catalog::SKILLS.iter().map(|s| {
let tutorials: Vec<Value> = 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})
});
json!({ "skills": skills.collect::<Vec<_>>() })
json!({ "skills": catalog::SKILLS.iter().map(skill_json).collect::<Vec<_>>() })
}
/// A skill as `learn.list` lists it: `{id, title, tutorials: [{id, title, steps}]}`.
fn skill_json(s: &catalog::Skill) -> Value {
let tutorials: Vec<Value> = 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})
}
/// What the panel shows: `{tab: forYou|skills, skill, tutorial}`, with null for no skill or no
/// tutorial open.
fn view_json(view: &View) -> Value {
let tab = TABS.iter().find(|(t, _)| *t == view.tab).map(|(_, name)| *name);
json!({"tab": tab, "skill": view.skill, "tutorial": view.tutorial})
}
/// `learn.tab {tab}`: the For you and Skills tabs. Show the panel on that tab (`forYou` or
/// `skills`); the Skills tab keeps the skill and tutorial it had open → the view ([`view_json`]). A
/// missing or unknown tab is an error and changes nothing.
fn tab(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
let key = "tab";
let name = match p.get(key) {
None | Some(Value::Null) => return Err(format!("missing `{key}`")),
Some(Value::String(name)) => name.as_str(),
Some(_) => return Err(format!("`{key}` must be a string")),
};
let names = || TABS.map(|(_, name)| name).join(", ");
let tab = TABS.iter().find(|(_, n)| *n == name).map(|(t, _)| *t).ok_or_else(|| format!("`{key}` must be one of {}", names()))?;
crate::panel_docking::reveal_panel(app, ID)?;
app.ui.learn.tab = tab;
Ok(view_json(&app.ui.learn))
}
/// `learn.skill {skill}`: a skill's row on the Skills tab. Show the panel on the Skills tab with
/// that skill's tutorials → the skill, as `learn.list` lists it. A missing or unknown id is an error
/// and changes nothing.
fn open_skill(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
let key = "skill";
let id = match p.get(key) {
None | Some(Value::Null) => return Err(format!("missing `{key}`")),
Some(Value::String(id)) => id.as_str(),
Some(_) => return Err(format!("`{key}` must be a string")),
};
let ids = || catalog::SKILLS.iter().map(|s| s.id).collect::<Vec<_>>().join(", ");
let skill = catalog::skill(id).ok_or_else(|| format!("`{key}` must be one of {}", ids()))?;
crate::panel_docking::reveal_panel(app, ID)?;
app.ui.learn = View { tab: Tab::Skills, skill: Some(skill.id), tutorial: None };
Ok(skill_json(skill))
}
/// `learn.back`: the Back button on the Skills tab. Show the panel and go up one level: from a
/// tutorial's steps to its skill's tutorials, and from a skill's tutorials to the list of skills →
/// the view ([`view_json`]). On the list of skills or the For you tab the view stays as it is.
fn back_up(app: &mut VectorcraftApp) -> Result<Value, String> {
crate::panel_docking::reveal_panel(app, ID)?;
let view = &mut app.ui.learn;
if view.tab == Tab::Skills {
if let Some((skill, _)) = view.tutorial.and_then(catalog::tutorial) {
view.skill = Some(skill.id);
view.tutorial = None;
} else if view.skill.is_some() {
view.skill = None;
view.tutorial = None;
}
}
Ok(view_json(view))
}
/// `learn.skills`: Back to skills, and the Quick skill guide card. Show the panel on the Skills tab
+71
View File
@@ -493,6 +493,77 @@ fn learn_show_rejects_a_missing_or_unknown_tutorial() {
}
}
/// The tab buttons, a skill's row, a tutorial's row and Back run `learn.tab`, `learn.skill`,
/// `learn.show` and `learn.back`: each one also shows a panel that was hidden, as the command does.
#[test]
fn the_tabs_rows_and_back_run_their_commands() {
for cmd in ["learn.tab", "learn.skill", "learn.back"] {
assert!(crate::menus::UI_COMMANDS.iter().any(|c| c.0 == cmd), "{cmd} is in the UI command catalog");
}
let mut app = app();
let ctx = context();
let (skill, tutorial) = catalog::tutorial("gradient").unwrap();
let press = |app: &mut VectorcraftApp, name: &str, view: View| {
app.run("window.learn", json!({"show": false})).unwrap();
assert!(!shown(app));
click(app, &ctx, name, show);
assert_eq!(app.ui.learn, view, "{name}");
assert!(shown(app), "{name} runs its command: {}", app.ui.status);
};
press(&mut app, "Skills", View { tab: Tab::Skills, skill: None, tutorial: None });
press(&mut app, skill.title, View { tab: Tab::Skills, skill: Some(skill.id), tutorial: None });
press(&mut app, tutorial.title, View { tab: Tab::Skills, skill: Some(skill.id), tutorial: Some(tutorial.id) });
press(&mut app, "For you", View { tab: Tab::ForYou, skill: Some(skill.id), tutorial: Some(tutorial.id) });
press(&mut app, "Skills", View { tab: Tab::Skills, skill: Some(skill.id), tutorial: Some(tutorial.id) });
press(&mut app, "Back", View { tab: Tab::Skills, skill: Some(skill.id), tutorial: None });
press(&mut app, "Back", View { tab: Tab::Skills, skill: None, tutorial: None });
}
/// `learn.tab` switches tabs and keeps what the Skills tab had open; `learn.skill` opens a skill's
/// tutorials and answers the skill as `learn.list` lists it; `learn.back` goes up one level and
/// keeps the view on the list of skills and on the For you tab.
#[test]
fn learn_tab_skill_and_back_move_the_view() {
let mut app = app();
let list = app.run("learn.list", json!({})).unwrap();
assert_eq!(app.run("learn.skill", json!({"skill": "color"})).unwrap(), list["skills"][2]);
assert!(shown(&app));
assert_eq!(app.ui.learn, View { tab: Tab::Skills, skill: Some("color"), tutorial: None });
app.run("learn.show", json!({"tutorial": "gradient"})).unwrap();
let view = json!({"tab": "forYou", "skill": "color", "tutorial": "gradient"});
assert_eq!(app.run("learn.tab", json!({"tab": "forYou"})).unwrap(), view);
assert_eq!(app.run("learn.back", json!({})).unwrap(), view, "Back on the For you tab keeps the view");
assert_eq!(app.run("learn.tab", json!({"tab": "skills"})).unwrap(), json!({"tab": "skills", "skill": "color", "tutorial": "gradient"}));
assert_eq!(app.run("learn.back", json!({})).unwrap(), json!({"tab": "skills", "skill": "color", "tutorial": null}));
let skills = json!({"tab": "skills", "skill": null, "tutorial": null});
assert_eq!(app.run("learn.back", json!({})).unwrap(), skills);
assert_eq!(app.run("learn.back", json!({})).unwrap(), skills, "Back on the list of skills keeps the view");
app.run("window.learn", json!({"show": false})).unwrap();
assert_eq!(app.run("learn.back", json!({})).unwrap(), skills);
assert!(shown(&app), "learn.back shows the panel");
}
/// `learn.tab` and `learn.skill` reject a missing, mistyped or unknown value and change nothing.
#[test]
fn learn_tab_and_learn_skill_reject_a_missing_or_unknown_value() {
let mut app = app();
let before = app.ui.learn.clone();
let skills = catalog::SKILLS.iter().map(|s| s.id).collect::<Vec<_>>().join(", ");
for (cmd, params, error) in [
("learn.tab", json!({}), "missing `tab`".to_string()),
("learn.tab", json!({"tab": null}), "missing `tab`".to_string()),
("learn.tab", json!({"tab": 1}), "`tab` must be a string".to_string()),
("learn.tab", json!({"tab": "Skills"}), "`tab` must be one of forYou, skills".to_string()),
("learn.skill", json!({}), "missing `skill`".to_string()),
("learn.skill", json!({"skill": ["draw"]}), "`skill` must be a string".to_string()),
("learn.skill", json!({"skill": "nope"}), format!("`skill` must be one of {skills}")),
] {
assert_eq!(app.run(cmd, params.clone()).unwrap_err(), error, "{cmd} {params}");
assert_eq!(app.ui.learn, before);
assert!(!shown(&app));
}
}
/// The Pen tutorial's steps, as the panel shows them.
fn pen_paths(app: &mut VectorcraftApp, ui: &mut egui::Ui) {
app.ui.learn = View { tab: Tab::Skills, skill: Some("draw"), tutorial: Some("pen-paths") };
+12 -2
View File
@@ -2861,8 +2861,15 @@ other value of `show` is an error and changes nothing. `learn.list {}` → `{ski
lists them, where `steps` is the number of steps. `learn.skills {}` shows the panel on its Skills tab with the list of
skills, with no skill and no tutorial open, as the Quick skill guide card and Back to skills do, and answers what
`learn.list` answers. `learn.show {tutorial}` shows the panel on its Skills tab with that
tutorial's steps → `{skill, tutorial, title, steps: [text]}`. A missing, non-string or unknown `tutorial` is an error
and changes nothing.
tutorial's steps → `{skill, tutorial, title, steps: [text]}`; choosing a tutorial's row runs it. A missing, non-string
or unknown `tutorial` is an error and changes nothing. The For you and Skills tab buttons run `learn.tab {tab}`, with
`tab` set to `forYou` or `skills`, which shows the panel on that tab → `{tab, skill, tutorial}`, with null for no
skill or no tutorial open; the Skills tab keeps the skill and tutorial it had open. Choosing a skill's row runs
`learn.skill {skill}`, which shows the panel on the Skills tab with that skill's tutorials → the skill as `learn.list`
lists it. A missing, non-string or unknown `tab` or `skill` is an error and changes nothing. Back runs `learn.back {}`,
which shows the panel and goes up one level on the Skills tab, from a tutorial's steps to its skill's tutorials and
from a skill's tutorials to the list of skills, and answers what `learn.tab` answers; on the list of skills or the For
you tab the view stays as it is.
Under a tutorial's steps, View more opens the tutorial's practice sample, a small drawing of VectorCraft's own with the
art its steps work on, as a new untitled document that becomes the active one; Save asks where to save it. Once the
@@ -2886,6 +2893,9 @@ is an error, and an error changes nothing. Behavior reference:
{"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.skill","params":{"skill":"draw"}}}
{"name":"invoke_menu","arguments":{"command":"learn.back","params":{}}}
{"name":"invoke_menu","arguments":{"command":"learn.tab","params":{"tab":"forYou"}}}
```
A step that names a tool shows the tool's name as a blue link. A click on it, or Enter or Space while it has the
+1 -1
View File
@@ -914,7 +914,7 @@ 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 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`, `learn.tab {tab}`, `learn.skill {skill}`, `learn.show {tutorial}` and `learn.back` script it, and the tab buttons, the rows and Back run them ([the help page](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/learn-skills-quick-skill-guide.html); `panels::learn::tests`).
- A tool's name in a Learn tutorial step is a blue link that shows where the tool is without choosing it (`learn.locateTool {tool}`): its toolbar button is marked, a collapsed group's flyout opens with the tool's row marked, a torn-off strip comes to the front, and a tool no shown toolbar holds is marked in the All Tools drawer. The mark, its 3 seconds and the hidden-toolbar cases are VectorCraft's own rules ([the help page](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/learn-skills-quick-skill-guide.html); `panels::learn::tests`, the `a_learn_link_*` tests in `toolbar::tests`).
- Under a tutorial's steps, View more opens the tutorial's own practice sample as a new untitled document, and once the tutorial is finished, Next Up moves to the next tutorial in the same skill, as the help page describes ([the help page](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/learn-skills-quick-skill-guide.html)). The samples are VectorCraft's own drawings. A Finish tutorial button that shows once the sample is open, the untitled copy, Back to skills in place of Next Up after a skill's last tutorial, and Next Up showing the next tutorial without opening its sample are VectorCraft's own rules. `learn.openSample`, `learn.finish`, `learn.next` and `learn.skills` (Back to skills) script them. Not done: the help page's short video, and progress kept between sessions and shown in the skill rings. Regression tests: `view_more_opens_the_tutorial_s_sample_as_a_new_untitled_document`, `finishing_a_tutorial_offers_next_up_in_its_skill`, `the_practice_controls_work_from_the_keyboard`, `the_last_tutorial_of_a_skill_offers_back_to_skills`, `learn_finish_and_learn_next_follow_the_practice`, `learn_skills_shows_the_list_of_skills`, `the_practice_commands_reject_a_missing_or_unknown_tutorial`, `every_tutorial_has_its_own_readable_sample` and `the_next_tutorial_is_the_next_one_in_the_same_skill` in `crates/ui-egui/src/panels/learn/tests.rs`.
- The errors of the blend spine commands (Move Spine Point, Reshape Spine Segment, Add Spine Point and Delete Spine Point) show in the status bar in the interface language: each has a row in every catalog that translates all status and error messages and in Bulgarian (`blend_spine_param_errors_are_translated`).
+1 -1
View File
@@ -383,7 +383,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 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.tab`, `learn.skills`, `learn.skill`, `learn.show`, `learn.back`, `learn.locateTool`, `learn.openSample`, `learn.finish`, `learn.next`; the mark's look and 3-second lifetime are VectorCraft's own rules; `panels::learn::tests`, `toolbar::tests` |
| Pathfinder panel: a group selected alone counts its nested shapes, so an expanded stroked object keeps the Shape Modes enabled | done | #1642 (#1635) |
| Selected Grid Repeat shows row, column and corner handles and spacing sliders; bounding-box handles take priority where they meet | done | #2691, `tools/src/repeat_grid.rs`; [public behavior reference](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/create-and-edit-patterns/set-grid-repeat-options.html); drags preview and commit `object.repeat.options` as one undo step, Escape cancels; Radial Repeat has no handles yet |
| Grid Repeat Options (Object › Repeat › Options…) has a Preview checkbox: the repeat on the canvas follows the fields while the dialog is open, Cancel puts the original back without a history step, OK keeps the previewed values as one undo step | done | #2651; [public behavior reference](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/create-and-edit-patterns/set-grid-repeat-options.html) lists the Preview option for the grid options only, so Radial and Mirror Repeat Options have none; `dialogs/command.rs` tests |