mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-14 10:01:58 +00:00
Learn: a tool's name in a tutorial step shows where the tool is
A tool step in the Learn panel now draws the tool's name as a link. Clicking it, or pressing Enter or Space on it, runs the new learn.locateTool command, which marks the tool where the current layout shows it without choosing it (the help page: https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/learn-skills-quick-skill-guide.html). - A tool shown on its toolbar button has the button marked, and a toolbar that has scrolled it out of view scrolls it back. - A tool in a collapsed group opens the group's flyout with its row marked; the group keeps the tool it shows. - A group torn off into a strip comes to the front with the tool marked. - Toolbars made with New Toolbar are searched after the Tools panel. - A tool that no shown toolbar holds is marked in the All Tools drawer. - A Tools panel hidden with Window › Tools shows again. In Presentation Mode, or while Tab hides the toolbars, the reply is "unavailable" with a reason. - The mark is a 2 pt outline in the selection color. It lasts 3 seconds, until Escape (also when Escape closes the All Tools drawer), or until the next link replaces it. These are VectorCraft's own rules. The command takes a tool id and rejects a missing, non-string or unknown id. Catalog steps mark the linked name in brackets, and the catalog test checks that each one names its tool. Rows for the command and the link's hover text are added to every complete catalog. Docs: docs/mcp.md, docs/target-app-parity.md, docs/ui-parity.md.
This commit is contained in:
@@ -601,6 +601,7 @@ impl VectorcraftApp {
|
||||
panels::library_panel::tidy(self);
|
||||
// The canvas and the panels: one docking layout. Tab hides the panels but not the canvas.
|
||||
egui::CentralPanel::default().frame(egui::Frame::NONE.fill(t.pasteboard)).show(ui, |ui| panel_docking::show(self, ui));
|
||||
panels::learn::locate::tidy(self, &ctx);
|
||||
crate::doc_windows::show(self, &ctx);
|
||||
dialogs::show(self, &ctx);
|
||||
updates_ui::show(self, &ctx);
|
||||
|
||||
@@ -598,6 +598,10 @@ pub(crate) fn show(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
let edit_this = editing.as_deref() == Some(bar.name.as_str());
|
||||
let mut moved = None;
|
||||
let mut drops = None;
|
||||
// A Learn link locating a tool of this toolbar: its button, scrolled into view once.
|
||||
let locate = crate::panels::learn::locate::wanted(&app.ui, Some(&bar.name));
|
||||
let scrolled = app.ui.learn_locate.as_ref().is_some_and(|l| l.scrolled);
|
||||
let (mut found, mut did_scroll) = (None, false);
|
||||
egui::Area::new(id).order(egui::Order::Middle).fixed_pos(pos).show(ctx, |ui| {
|
||||
crate::toolbar::flyout_frame(&t, false).inner_margin(egui::Margin { left: 0, right: 0, top: 0, bottom: 4 }).show(ui, |ui| {
|
||||
ui.set_width(width);
|
||||
@@ -657,11 +661,17 @@ pub(crate) fn show(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space((middle - BUTTON / 2.0 * row.len() as f32 - ui.cursor().min.x).max(0.0));
|
||||
for &(s, slot) in row {
|
||||
if let Some(rect) = slot_button(ui, &t, &press, slot, &mut actions)
|
||||
&& edit_this
|
||||
{
|
||||
let Some((rect, shown)) = slot_button(ui, &t, &press, slot, &mut actions) else { continue };
|
||||
if edit_this {
|
||||
buttons.push((s, rect));
|
||||
}
|
||||
if let Some(tool) = locate.filter(|tool| slot.iter().any(|id| id == tool)) {
|
||||
if !scrolled && !ui.clip_rect().contains_rect(rect) {
|
||||
ui.scroll_to_rect(rect, None);
|
||||
did_scroll = true;
|
||||
}
|
||||
found = Some((rect, shown == tool, slot.clone(), ui.layer_id(), ui.clip_rect()));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -697,6 +707,22 @@ pub(crate) fn show(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
{
|
||||
b.pos = [p.x, p.y];
|
||||
}
|
||||
// The button is marked when it shows the located tool, else the group's flyout opens on it;
|
||||
// a group torn off into a strip is marked there.
|
||||
if did_scroll {
|
||||
crate::panels::learn::locate::scroll_once(&mut app.ui);
|
||||
ctx.request_repaint();
|
||||
} else if let Some((rect, shows, slot, layer, clip)) = found {
|
||||
let tools: Vec<&'static str> = slot.iter().filter_map(|id| tool_info(id).map(|t| t.id)).collect();
|
||||
let torn_off = tools.len() > 1 && tools.first().is_some_and(|key| crate::toolbar::is_floating(app, Some(&bar.name), key));
|
||||
if !torn_off {
|
||||
if shows {
|
||||
crate::panels::learn::locate::mark(app, ctx, layer, clip, rect);
|
||||
} else if crate::panels::learn::locate::reveal_once(&mut app.ui) {
|
||||
actions.push(Action::Flyout(tools, rect, bar.name.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some((bounds, button)) = clicked_anchor.or(anchor) {
|
||||
ctx.data_mut(|d| {
|
||||
@@ -767,13 +793,11 @@ struct Press<'a> {
|
||||
|
||||
/// One slot's tool button, as the Tools panel's: it shows the tool chosen last from its group, a
|
||||
/// press selects it, and a long or right press opens the group's flyout; while the drawer edits the
|
||||
/// toolbar, it drags instead. Returns where it is drawn.
|
||||
fn slot_button(ui: &mut egui::Ui, t: &Tokens, p: &Press, slot: &[String], actions: &mut Vec<Action>) -> Option<egui::Rect> {
|
||||
/// toolbar, it drags instead. Returns where it is drawn and the tool it shows.
|
||||
fn slot_button(ui: &mut egui::Ui, t: &Tokens, p: &Press, slot: &[String], actions: &mut Vec<Action>) -> Option<(egui::Rect, &'static str)> {
|
||||
let tools: Vec<&'static str> = slot.iter().filter_map(|id| tool_info(id).map(|t| t.id)).collect();
|
||||
let &first = tools.first()?;
|
||||
let is_active = tools.contains(&p.active);
|
||||
let shown_id = if is_active { p.active } else { p.bar.slot_tool.get(first).and_then(|id| tool_info(id)).map_or(first, |t| t.id) };
|
||||
let shown = tool_info(shown_id).filter(|t| tools.contains(&t.id)).or_else(|| tool_info(first))?;
|
||||
let shown = tool_info(shown_tool(p.bar, p.active, &tools)?)?;
|
||||
let (rect, resp) = ui.allocate_exact_size(vec2(BUTTON, crate::toolbar::PITCH - 1.0), Sense::click_and_drag());
|
||||
let well = egui::Rect::from_center_size(rect.center(), vec2(30.0, 27.5));
|
||||
if is_active {
|
||||
@@ -810,7 +834,18 @@ fn slot_button(ui: &mut egui::Ui, t: &Tokens, p: &Press, slot: &[String], action
|
||||
let tip = crate::toolbar::tip(shown);
|
||||
resp.widget_info(|| egui::WidgetInfo::selected(egui::Role::Button, ui.is_enabled(), is_active, &tip));
|
||||
resp.on_hover_text(tip);
|
||||
Some(rect)
|
||||
Some((rect, shown.id))
|
||||
}
|
||||
|
||||
/// The tool a slot's button on `bar` shows, of the slot's known `tools`: the active tool when the
|
||||
/// slot holds it, else the one chosen last from the slot's group, else its first; none for a slot
|
||||
/// with no known tool.
|
||||
pub(crate) fn shown_tool(bar: &CustomToolbar, active: &'static str, tools: &[&'static str]) -> Option<&'static str> {
|
||||
let &first = tools.first()?;
|
||||
if tools.contains(&active) {
|
||||
return Some(active);
|
||||
}
|
||||
Some(bar.slot_tool.get(first).and_then(|id| tools.iter().copied().find(|t| t == id)).unwrap_or(first))
|
||||
}
|
||||
|
||||
/// The area of Add Tool…'s menu on the toolbar named `name`.
|
||||
|
||||
@@ -485,6 +485,10 @@ pub(crate) fn drawer(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
let mut toolbars_cmd = None;
|
||||
let mut advanced = None;
|
||||
let mut show_toggle = None;
|
||||
// A Learn link locating a tool that no shown toolbar holds: its row, scrolled into view once.
|
||||
let locate = crate::panels::learn::locate::wanted_in_drawer(&app.ui);
|
||||
let scroll = app.ui.learn_locate.as_ref().is_some_and(|l| !l.scrolled);
|
||||
let (mut located, mut scrolled) = (None, false);
|
||||
let resp = egui::Area::new(id).order(egui::Order::Foreground).fixed_pos(pos2(x, y)).show(ctx, |ui| {
|
||||
crate::toolbar::flyout_frame(&t, removing).inner_margin(egui::Margin::same(8)).show(ui, |ui| {
|
||||
let width = WIDTH.min(screen.width() - 2.0 * GAP - 16.0).max(160.0);
|
||||
@@ -535,9 +539,17 @@ pub(crate) fn drawer(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
egui::Frame::NONE.stroke(Stroke::new(1.0, t.divider)).corner_radius(2.0).show(ui, |ui| {
|
||||
ui.set_width(inner);
|
||||
for tool in g.iter() {
|
||||
if row(ui, &t, tool, tool.id == active, in_toolbar.contains(tool.id)) {
|
||||
let (r, clicked) = row(ui, &t, tool, tool.id == active, in_toolbar.contains(tool.id));
|
||||
if clicked {
|
||||
picked = Some(tool.id);
|
||||
}
|
||||
if locate == Some(tool.id) {
|
||||
if scroll && !ui.clip_rect().contains_rect(r) {
|
||||
ui.scroll_to_rect(r, Some(egui::Align::Center));
|
||||
scrolled = true;
|
||||
}
|
||||
located = Some((ui.layer_id(), ui.clip_rect(), r));
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.add_space(5.0);
|
||||
@@ -563,6 +575,12 @@ pub(crate) fn drawer(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
});
|
||||
});
|
||||
let rect = resp.response.rect;
|
||||
if scrolled {
|
||||
crate::panels::learn::locate::scroll_once(&mut app.ui);
|
||||
ctx.request_repaint();
|
||||
} else if let Some((layer, clip, r)) = located {
|
||||
crate::panels::learn::locate::mark(app, ctx, layer, clip, r);
|
||||
}
|
||||
// A tool dragged from the toolbar and dropped in the drawer leaves the toolbar.
|
||||
if let Some(d) = dragged.filter(|d| d.from_toolbar && over(rect))
|
||||
&& ctx.input(|i| i.pointer.any_released())
|
||||
@@ -666,8 +684,8 @@ fn menu_id() -> egui::Id {
|
||||
}
|
||||
|
||||
/// A tool's row in the drawer: icon, name and shortcut, dimmed when the tool is in the toolbar
|
||||
/// already (it can't be dragged in again). Returns whether it was clicked.
|
||||
fn row(ui: &mut Ui, t: &Tokens, tool: &'static ToolInfo, on: bool, dimmed: bool) -> bool {
|
||||
/// already (it can't be dragged in again). Returns where it is drawn and whether it was clicked.
|
||||
fn row(ui: &mut Ui, t: &Tokens, tool: &'static ToolInfo, on: bool, dimmed: bool) -> (Rect, bool) {
|
||||
let (rect, resp) = ui.allocate_exact_size(vec2(ui.available_width(), ROW), if dimmed { Sense::click() } else { Sense::click_and_drag() });
|
||||
if !dimmed {
|
||||
resp.dnd_set_drag_payload(ToolDrag { tool: tool.id, from_toolbar: false });
|
||||
@@ -711,7 +729,7 @@ fn row(ui: &mut Ui, t: &Tokens, tool: &'static ToolInfo, on: bool, dimmed: bool)
|
||||
egui::FontId::proportional(12.5),
|
||||
color,
|
||||
);
|
||||
resp.on_hover_text(crate::toolbar::tip(tool)).clicked()
|
||||
(rect, resp.on_hover_text(crate::toolbar::tip(tool)).clicked())
|
||||
}
|
||||
|
||||
/// The tool being dragged, by the pointer (below and right of it, clear of the drop marker).
|
||||
|
||||
@@ -6690,6 +6690,8 @@ breakText Glyphs Глифи
|
||||
Short hands-on tutorials for the essential tools, grouped by skill. Choose a skill to see its tutorials. Кратки практически уроци за основните инструменти, групирани по умения. Изберете умение, за да видите уроците му.
|
||||
List Tutorials Извеждане на уроци
|
||||
Show Tutorial Показване на урок
|
||||
Show Tool Location Показване на местоположението на инструмента
|
||||
Show where this tool is Показване къде е този инструмент
|
||||
@msg missing `tutorial` (see learn.list) липсва `tutorial` (вижте learn.list)
|
||||
@msg unknown tutorial `{id}` (see learn.list) непознат урок `{id}` (вижте learn.list)
|
||||
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -6744,6 +6744,8 @@ shape Open Otvoren
|
||||
Short hands-on tutorials for the essential tools, grouped by skill. Choose a skill to see its tutorials. Kratki praktični tutorijali za osnovne alate, grupisani po vještinama. Odaberite vještinu da vidite njene tutorijale.
|
||||
List Tutorials Izlistaj tutorijale
|
||||
Show Tutorial Prikaži tutorijal
|
||||
Show Tool Location Prikaži lokaciju alata
|
||||
Show where this tool is Prikaži gdje je ovaj alat
|
||||
@msg missing `tutorial` (see learn.list) nedostaje `tutorial` (vidi learn.list)
|
||||
@msg unknown tutorial `{id}` (see learn.list) nepoznat tutorijal `{id}` (vidi learn.list)
|
||||
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -6987,6 +6987,8 @@ breakText Glyphs Glifs
|
||||
Short hands-on tutorials for the essential tools, grouped by skill. Choose a skill to see its tutorials. Tutorials pràctics breus de les eines essencials, agrupats per habilitat. Trieu una habilitat per veure’n els tutorials.
|
||||
List Tutorials Llista els tutorials
|
||||
Show Tutorial Mostra el tutorial
|
||||
Show Tool Location Mostra la ubicació de l'eina
|
||||
Show where this tool is Mostra on és aquesta eina
|
||||
@msg missing `tutorial` (see learn.list) falta `tutorial` (vegeu learn.list)
|
||||
@msg unknown tutorial `{id}` (see learn.list) tutorial desconegut `{id}` (vegeu learn.list)
|
||||
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -7006,6 +7006,8 @@ breakText Glyphs Glyfy
|
||||
Short hands-on tutorials for the essential tools, grouped by skill. Choose a skill to see its tutorials. Krátké praktické výukové kurzy k základním nástrojům, seřazené podle dovedností. Vyberte dovednost a zobrazí se její kurzy.
|
||||
List Tutorials Vypsat výukové kurzy
|
||||
Show Tutorial Zobrazit výukový kurz
|
||||
Show Tool Location Zobrazit umístění nástroje
|
||||
Show where this tool is Zobrazit, kde je tento nástroj
|
||||
@msg missing `tutorial` (see learn.list) chybí `tutorial` (viz learn.list)
|
||||
@msg unknown tutorial `{id}` (see learn.list) neznámý výukový kurz `{id}` (viz learn.list)
|
||||
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -6706,6 +6706,8 @@ breakText Glyphs Glyffer
|
||||
Short hands-on tutorials for the essential tools, grouped by skill. Choose a skill to see its tutorials. Korte praktiske vejledninger til de vigtigste værktøjer, ordnet efter færdighed. Vælg en færdighed for at se dens vejledninger.
|
||||
List Tutorials Vis vejledninger
|
||||
Show Tutorial Vis vejledning
|
||||
Show Tool Location Vis værktøjets placering
|
||||
Show where this tool is Vis, hvor dette værktøj er
|
||||
@msg missing `tutorial` (see learn.list) `tutorial` mangler (se learn.list)
|
||||
@msg unknown tutorial `{id}` (see learn.list) ukendt vejledning `{id}` (se learn.list)
|
||||
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -6601,6 +6601,8 @@ breakText Glyphs Glyphen
|
||||
Short hands-on tutorials for the essential tools, grouped by skill. Choose a skill to see its tutorials. Kurze praktische Lernprogramme zu den wichtigsten Werkzeugen, nach Fertigkeiten geordnet. Wählen Sie eine Fertigkeit, um ihre Lernprogramme zu sehen.
|
||||
List Tutorials Lernprogramme auflisten
|
||||
Show Tutorial Lernprogramm anzeigen
|
||||
Show Tool Location Position des Werkzeugs anzeigen
|
||||
Show where this tool is Zeigen, wo sich dieses Werkzeug befindet
|
||||
@msg missing `tutorial` (see learn.list) `tutorial` fehlt (siehe learn.list)
|
||||
@msg unknown tutorial `{id}` (see learn.list) unbekanntes Lernprogramm `{id}` (siehe learn.list)
|
||||
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -6958,6 +6958,8 @@ breakText Glyphs Γλύφοι
|
||||
Short hands-on tutorials for the essential tools, grouped by skill. Choose a skill to see its tutorials. Σύντομοι πρακτικοί οδηγοί για τα βασικά εργαλεία, ομαδοποιημένοι ανά δεξιότητα. Επιλέξτε μια δεξιότητα για να δείτε τους οδηγούς της.
|
||||
List Tutorials Λίστα οδηγών εκμάθησης
|
||||
Show Tutorial Εμφάνιση οδηγού εκμάθησης
|
||||
Show Tool Location Εμφάνιση θέσης εργαλείου
|
||||
Show where this tool is Εμφάνιση της θέσης αυτού του εργαλείου
|
||||
@msg missing `tutorial` (see learn.list) λείπει το `tutorial` (δείτε learn.list)
|
||||
@msg unknown tutorial `{id}` (see learn.list) άγνωστος οδηγός εκμάθησης `{id}` (δείτε learn.list)
|
||||
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -6949,6 +6949,8 @@ breakText Glyphs Glifos
|
||||
Short hands-on tutorials for the essential tools, grouped by skill. Choose a skill to see its tutorials. Tutoriales prácticos breves sobre las herramientas esenciales, agrupados por habilidad. Elige una habilidad para ver sus tutoriales.
|
||||
List Tutorials Enumerar tutoriales
|
||||
Show Tutorial Mostrar tutorial
|
||||
Show Tool Location Mostrar ubicación de la herramienta
|
||||
Show where this tool is Mostrar dónde está esta herramienta
|
||||
@msg missing `tutorial` (see learn.list) falta `tutorial` (consulta learn.list)
|
||||
@msg unknown tutorial `{id}` (see learn.list) tutorial desconocido `{id}` (consulta learn.list)
|
||||
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -7041,6 +7041,8 @@ breakText Glyphs Merkit
|
||||
Short hands-on tutorials for the essential tools, grouped by skill. Choose a skill to see its tutorials. Lyhyitä käytännön opetusohjelmia tärkeimmistä työkaluista taidoittain ryhmiteltyinä. Valitse taito nähdäksesi sen opetusohjelmat.
|
||||
List Tutorials Luettele opetusohjelmat
|
||||
Show Tutorial Näytä opetusohjelma
|
||||
Show Tool Location Näytä työkalun sijainti
|
||||
Show where this tool is Näytä, missä tämä työkalu on
|
||||
@msg missing `tutorial` (see learn.list) `tutorial` puuttuu (ks. learn.list)
|
||||
@msg unknown tutorial `{id}` (see learn.list) tuntematon opetusohjelma `{id}` (ks. learn.list)
|
||||
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -7001,6 +7001,8 @@ breakText Glyphs Glyphes
|
||||
Short hands-on tutorials for the essential tools, grouped by skill. Choose a skill to see its tutorials. Courts tutoriels pratiques sur les outils essentiels, regroupés par compétence. Choisissez une compétence pour voir ses tutoriels.
|
||||
List Tutorials Lister les tutoriels
|
||||
Show Tutorial Afficher le tutoriel
|
||||
Show Tool Location Afficher l'emplacement de l'outil
|
||||
Show where this tool is Afficher l'emplacement de cet outil
|
||||
@msg missing `tutorial` (see learn.list) `tutorial` manquant (voir learn.list)
|
||||
@msg unknown tutorial `{id}` (see learn.list) tutoriel inconnu `{id}` (voir learn.list)
|
||||
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -8439,6 +8439,8 @@ theme Appearance अपीयरेंस
|
||||
Short hands-on tutorials for the essential tools, grouped by skill. Choose a skill to see its tutorials. आवश्यक टूल के लिए छोटे व्यावहारिक ट्यूटोरियल, कौशल के अनुसार समूहित। किसी कौशल के ट्यूटोरियल देखने के लिए उसे चुनें।
|
||||
List Tutorials ट्यूटोरियल की सूची
|
||||
Show Tutorial ट्यूटोरियल दिखाएँ
|
||||
Show Tool Location टूल का स्थान दिखाएँ
|
||||
Show where this tool is दिखाएँ कि यह टूल कहाँ है
|
||||
@msg missing `tutorial` (see learn.list) लापता `tutorial` (learn.list देखें)
|
||||
@msg unknown tutorial `{id}` (see learn.list) अज्ञात ट्यूटोरियल `{id}` (learn.list देखें)
|
||||
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -6990,6 +6990,8 @@ breakText Glyphs Glifák
|
||||
Short hands-on tutorials for the essential tools, grouped by skill. Choose a skill to see its tutorials. Rövid gyakorlati oktatóanyagok az alapvető eszközökhöz, készségek szerint csoportosítva. Válasszon egy készséget az oktatóanyagai megtekintéséhez.
|
||||
List Tutorials Oktatóanyagok listázása
|
||||
Show Tutorial Oktatóanyag megjelenítése
|
||||
Show Tool Location Eszköz helyének megjelenítése
|
||||
Show where this tool is Megmutatja, hol van ez az eszköz
|
||||
@msg missing `tutorial` (see learn.list) hiányzik: `tutorial` (lásd: learn.list)
|
||||
@msg unknown tutorial `{id}` (see learn.list) ismeretlen oktatóanyag: `{id}` (lásd: learn.list)
|
||||
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -6706,6 +6706,8 @@ breakText Glyphs Glif
|
||||
Short hands-on tutorials for the essential tools, grouped by skill. Choose a skill to see its tutorials. Tutorial praktik singkat untuk alat-alat utama, dikelompokkan menurut keterampilan. Pilih keterampilan untuk melihat tutorialnya.
|
||||
List Tutorials Daftar tutorial
|
||||
Show Tutorial Tampilkan tutorial
|
||||
Show Tool Location Tampilkan lokasi alat
|
||||
Show where this tool is Tampilkan letak alat ini
|
||||
@msg missing `tutorial` (see learn.list) `tutorial` tidak ada (lihat learn.list)
|
||||
@msg unknown tutorial `{id}` (see learn.list) tutorial `{id}` tidak dikenal (lihat learn.list)
|
||||
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2402 and column 5.
|
@@ -6635,6 +6635,8 @@ breakText Glyphs Glifi
|
||||
Short hands-on tutorials for the essential tools, grouped by skill. Choose a skill to see its tutorials. Brevi tutorial pratici sugli strumenti essenziali, raggruppati per competenza. Scegli una competenza per vederne i tutorial.
|
||||
List Tutorials Elenca tutorial
|
||||
Show Tutorial Mostra tutorial
|
||||
Show Tool Location Mostra posizione strumento
|
||||
Show where this tool is Mostra dove si trova questo strumento
|
||||
@msg missing `tutorial` (see learn.list) manca `tutorial` (vedi learn.list)
|
||||
@msg unknown tutorial `{id}` (see learn.list) tutorial sconosciuto `{id}` (vedi learn.list)
|
||||
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -6700,6 +6700,8 @@ breakText Glyphs 字形
|
||||
Short hands-on tutorials for the essential tools, grouped by skill. Choose a skill to see its tutorials. 基本ツールを学ぶ短い実践チュートリアルを、スキルごとにまとめています。スキルを選ぶとチュートリアルが表示されます。
|
||||
List Tutorials チュートリアルの一覧
|
||||
Show Tutorial チュートリアルを表示
|
||||
Show Tool Location ツールの場所を表示
|
||||
Show where this tool is このツールの場所を表示
|
||||
@msg missing `tutorial` (see learn.list) `tutorial` が指定されていません (learn.list を参照)
|
||||
@msg unknown tutorial `{id}` (see learn.list) `{id}` は不明なチュートリアルです (learn.list を参照)
|
||||
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -7120,6 +7120,8 @@ font-script Japanese იაპონური
|
||||
Short hands-on tutorials for the essential tools, grouped by skill. Choose a skill to see its tutorials. მოკლე პრაქტიკული გაკვეთილები ძირითადი ხელსაწყოებისთვის, დაჯგუფებული უნარების მიხედვით. აირჩიეთ უნარი მისი გაკვეთილების სანახავად.
|
||||
List Tutorials გაკვეთილების სია
|
||||
Show Tutorial გაკვეთილის ჩვენება
|
||||
Show Tool Location ხელსაწყოს მდებარეობის ჩვენება
|
||||
Show where this tool is ამ ხელსაწყოს მდებარეობის ჩვენება
|
||||
@msg missing `tutorial` (see learn.list) აკლია `tutorial` (იხ. learn.list)
|
||||
@msg unknown tutorial `{id}` (see learn.list) უცნობი გაკვეთილი `{id}` (იხ. learn.list)
|
||||
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -6596,6 +6596,8 @@ breakText Glyphs 글리프
|
||||
Short hands-on tutorials for the essential tools, grouped by skill. Choose a skill to see its tutorials. 기본 도구를 익히는 짧은 실습 튜토리얼을 기술별로 모았습니다. 기술을 선택하면 해당 튜토리얼이 표시됩니다.
|
||||
List Tutorials 튜토리얼 목록
|
||||
Show Tutorial 튜토리얼 표시
|
||||
Show Tool Location 도구 위치 표시
|
||||
Show where this tool is 이 도구의 위치 표시
|
||||
@msg missing `tutorial` (see learn.list) `tutorial`이 없습니다(learn.list 참조)
|
||||
@msg unknown tutorial `{id}` (see learn.list) 알 수 없는 튜토리얼 `{id}`(learn.list 참조)
|
||||
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -6757,6 +6757,8 @@ breakText Glyphs Glyphs
|
||||
Short hands-on tutorials for the essential tools, grouped by skill. Choose a skill to see its tutorials. Korte praktische tutorials voor de belangrijkste gereedschappen, gegroepeerd per vaardigheid. Kies een vaardigheid om de tutorials ervan te zien.
|
||||
List Tutorials Tutorials weergeven
|
||||
Show Tutorial Tutorial tonen
|
||||
Show Tool Location Locatie van gereedschap tonen
|
||||
Show where this tool is Tonen waar dit gereedschap zich bevindt
|
||||
@msg missing `tutorial` (see learn.list) `tutorial` ontbreekt (zie learn.list)
|
||||
@msg unknown tutorial `{id}` (see learn.list) onbekende tutorial `{id}` (zie learn.list)
|
||||
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -6862,6 +6862,8 @@ breakText Glyphs Glify
|
||||
Short hands-on tutorials for the essential tools, grouped by skill. Choose a skill to see its tutorials. Krótkie praktyczne samouczki dotyczące podstawowych narzędzi, pogrupowane według umiejętności. Wybierz umiejętność, aby zobaczyć jej samouczki.
|
||||
List Tutorials Wyświetl samouczki
|
||||
Show Tutorial Pokaż samouczek
|
||||
Show Tool Location Pokaż położenie narzędzia
|
||||
Show where this tool is Pokaż, gdzie jest to narzędzie
|
||||
@msg missing `tutorial` (see learn.list) brak `tutorial` (patrz learn.list)
|
||||
@msg unknown tutorial `{id}` (see learn.list) nieznany samouczek `{id}` (patrz learn.list)
|
||||
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -7079,6 +7079,8 @@ swatch Yellow Green Verde-amarelado
|
||||
Short hands-on tutorials for the essential tools, grouped by skill. Choose a skill to see its tutorials. Tutoriais práticos curtos das ferramentas essenciais, agrupados por habilidade. Escolha uma habilidade para ver os tutoriais dela.
|
||||
List Tutorials Listar Tutoriais
|
||||
Show Tutorial Mostrar Tutorial
|
||||
Show Tool Location Mostrar Local da Ferramenta
|
||||
Show where this tool is Mostrar onde está esta ferramenta
|
||||
@msg missing `tutorial` (see learn.list) falta `tutorial` (veja learn.list)
|
||||
@msg unknown tutorial `{id}` (see learn.list) tutorial desconhecido `{id}` (veja learn.list)
|
||||
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -7095,6 +7095,8 @@ shape Open Открытый
|
||||
Short hands-on tutorials for the essential tools, grouped by skill. Choose a skill to see its tutorials. Короткие практические уроки по основным инструментам, сгруппированные по навыкам. Выберите навык, чтобы увидеть его уроки.
|
||||
List Tutorials Перечислить уроки
|
||||
Show Tutorial Показать урок
|
||||
Show Tool Location Показать расположение инструмента
|
||||
Show where this tool is Показать, где находится этот инструмент
|
||||
@msg missing `tutorial` (see learn.list) отсутствует `tutorial` (см. learn.list)
|
||||
@msg unknown tutorial `{id}` (see learn.list) неизвестный урок `{id}` (см. learn.list)
|
||||
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -8484,6 +8484,8 @@ digits Hindi Hindi
|
||||
Short hands-on tutorials for the essential tools, grouped by skill. Choose a skill to see its tutorials. Korta praktiska självstudier för de viktigaste verktygen, ordnade efter färdighet. Välj en färdighet för att se dess självstudier.
|
||||
List Tutorials Lista självstudier
|
||||
Show Tutorial Visa självstudie
|
||||
Show Tool Location Visa verktygets plats
|
||||
Show where this tool is Visa var det här verktyget finns
|
||||
@msg missing `tutorial` (see learn.list) `tutorial` saknas (se learn.list)
|
||||
@msg unknown tutorial `{id}` (see learn.list) okänd självstudie `{id}` (se learn.list)
|
||||
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -6962,6 +6962,8 @@ breakText Glyphs Glifler
|
||||
Short hands-on tutorials for the essential tools, grouped by skill. Choose a skill to see its tutorials. Temel araçlar için becerilere göre gruplanmış kısa uygulamalı öğreticiler. Öğreticilerini görmek için bir beceri seçin.
|
||||
List Tutorials Öğreticileri Listele
|
||||
Show Tutorial Öğreticiyi Göster
|
||||
Show Tool Location Aracın Konumunu Göster
|
||||
Show where this tool is Bu aracın nerede olduğunu göster
|
||||
@msg missing `tutorial` (see learn.list) `tutorial` eksik (bkz. learn.list)
|
||||
@msg unknown tutorial `{id}` (see learn.list) bilinmeyen öğretici `{id}` (bkz. learn.list)
|
||||
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -6615,6 +6615,8 @@ breakText Glyphs Гліфи
|
||||
Short hands-on tutorials for the essential tools, grouped by skill. Choose a skill to see its tutorials. Короткі практичні уроки з основних інструментів, згруповані за навичками. Виберіть навичку, щоб побачити її уроки.
|
||||
List Tutorials Перелічити уроки
|
||||
Show Tutorial Показати урок
|
||||
Show Tool Location Показати розташування інструмента
|
||||
Show where this tool is Показати, де розташований цей інструмент
|
||||
@msg missing `tutorial` (see learn.list) відсутній `tutorial` (див. learn.list)
|
||||
@msg unknown tutorial `{id}` (see learn.list) невідомий урок `{id}` (див. learn.list)
|
||||
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 182 and column 5.
|
@@ -6616,6 +6616,8 @@ breakText Glyphs Glyph
|
||||
Short hands-on tutorials for the essential tools, grouped by skill. Choose a skill to see its tutorials. Các hướng dẫn thực hành ngắn cho những công cụ thiết yếu, xếp theo kỹ năng. Chọn một kỹ năng để xem các hướng dẫn của nó.
|
||||
List Tutorials Liệt kê hướng dẫn
|
||||
Show Tutorial Hiện hướng dẫn
|
||||
Show Tool Location Hiện vị trí công cụ
|
||||
Show where this tool is Hiện vị trí của công cụ này
|
||||
@msg missing `tutorial` (see learn.list) thiếu `tutorial` (xem learn.list)
|
||||
@msg unknown tutorial `{id}` (see learn.list) hướng dẫn không xác định `{id}` (xem learn.list)
|
||||
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 204 and column 5.
|
@@ -6844,6 +6844,8 @@ breakText Glyphs 字形
|
||||
Short hands-on tutorials for the essential tools, grouped by skill. Choose a skill to see its tutorials. 针对基本工具的简短实操教程,按技能分组。选择一项技能即可查看其教程。
|
||||
List Tutorials 列出教程
|
||||
Show Tutorial 显示教程
|
||||
Show Tool Location 显示工具位置
|
||||
Show where this tool is 显示此工具的位置
|
||||
@msg missing `tutorial` (see learn.list) 缺少 `tutorial` (请参阅 learn.list)
|
||||
@msg unknown tutorial `{id}` (see learn.list) 未知教程 `{id}` (请参阅 learn.list)
|
||||
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -6650,6 +6650,8 @@ breakText Glyphs 字符
|
||||
Short hands-on tutorials for the essential tools, grouped by skill. Choose a skill to see its tutorials. 針對基本工具的簡短實作教學課程,依技能分組。選擇一項技能即可查看其教學課程。
|
||||
List Tutorials 列出教學課程
|
||||
Show Tutorial 顯示教學課程
|
||||
Show Tool Location 顯示工具位置
|
||||
Show where this tool is 顯示此工具的位置
|
||||
@msg missing `tutorial` (see learn.list) 未提供 `tutorial` (請參閱 learn.list)
|
||||
@msg unknown tutorial `{id}` (see learn.list) 未知的教學課程 `{id}` (請參閱 learn.list)
|
||||
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -653,6 +653,12 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
||||
"",
|
||||
"{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.locateTool",
|
||||
"Show Tool Location",
|
||||
"",
|
||||
"{tool: a tool id, as tool.select takes} mark where that tool is for 3 seconds without choosing it, as the link on a tool's name in a Learn tutorial step does: its toolbar button, a row of its collapsed group's flyout (which opens), its group's torn-off strip (which comes to the front), or, when no shown toolbar holds it, its row in the All Tools drawer (which opens); a Tools panel hidden with Window › Tools shows again; Escape or the next locate removes the mark; a missing, non-string or unknown id is an error and changes nothing; returns {tool, location: toolbar|flyout|strip|allTools|unavailable, toolbar: the name of the toolbar made with New Toolbar that holds it, or null}, with reason when Presentation Mode or Tab hides the toolbars",
|
||||
),
|
||||
("window.brightness", "UI Brightness", "", "{brightness: dark|mediumDark|mediumLight|light}"),
|
||||
(
|
||||
"window.workspace",
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
//! 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
|
||||
//! tutorial previews its numbered steps. Back goes up one level. `learn.list` lists the tutorials,
|
||||
//! `learn.show` opens one and `window.learn` shows or hides the panel.
|
||||
//! tutorial previews its numbered steps. A tool's name in a step is a link that shows where the tool
|
||||
//! is ([`locate`]). Back goes up one level. `learn.list` lists the tutorials, `learn.show` opens one,
|
||||
//! `learn.locateTool` shows where a tool is and `window.learn` shows or hides the panel.
|
||||
|
||||
pub mod catalog;
|
||||
pub mod locate;
|
||||
|
||||
use egui::{Sense, Stroke, Ui, vec2};
|
||||
use serde_json::{Value, json};
|
||||
@@ -86,7 +88,7 @@ fn skills(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
app.ui.learn.skill = Some(skill.id);
|
||||
app.ui.learn.tutorial = None;
|
||||
}
|
||||
steps(ui, tutorial);
|
||||
steps(app, ui, tutorial);
|
||||
return;
|
||||
}
|
||||
if let Some(skill) = app.ui.learn.skill.and_then(catalog::skill) {
|
||||
@@ -113,18 +115,52 @@ 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.
|
||||
fn steps(ui: &mut Ui, tutorial: &catalog::Tutorial) {
|
||||
/// 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) {
|
||||
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 mut locate = None;
|
||||
for (i, step) in tutorial.steps.iter().enumerate() {
|
||||
ui.horizontal_top(|ui| {
|
||||
ui.label(egui::RichText::new(format!("{}.", i + 1)).color(t.text_dim));
|
||||
ui.add(egui::Label::new(egui::RichText::new(step.text).color(t.text)).wrap());
|
||||
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());
|
||||
return;
|
||||
};
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
if !before.is_empty() {
|
||||
ui.add(egui::Label::new(egui::RichText::new(before).color(t.text)).wrap());
|
||||
}
|
||||
if tool_link(ui, &t, name).clicked() {
|
||||
locate = Some(tool);
|
||||
}
|
||||
if !after.is_empty() {
|
||||
ui.add(egui::Label::new(egui::RichText::new(after).color(t.text)).wrap());
|
||||
}
|
||||
});
|
||||
});
|
||||
ui.add_space(2.0);
|
||||
}
|
||||
if let Some(tool) = locate {
|
||||
if let Err(e) = app.run("learn.locateTool", json!({ "tool": tool })) {
|
||||
app.status(e);
|
||||
}
|
||||
ui.ctx().request_repaint();
|
||||
}
|
||||
}
|
||||
|
||||
/// A tool's name in a step, drawn as a link: blue and underlined, with an outline while it has the
|
||||
/// keyboard focus.
|
||||
fn tool_link(ui: &mut Ui, t: &Tokens, name: &str) -> egui::Response {
|
||||
let resp = ui.add(egui::Label::new(egui::RichText::new(name).color(t.selection).underline()).sense(Sense::click()));
|
||||
if resp.has_focus() {
|
||||
ui.painter().rect_stroke(resp.rect.expand(2.0), egui::CornerRadius::same(2), Stroke::new(1.0, t.selection), egui::StrokeKind::Outside);
|
||||
}
|
||||
resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Link, ui.is_enabled(), name));
|
||||
resp.on_hover_cursor(egui::CursorIcon::PointingHand).on_hover_text(tl!("Show where this tool is"))
|
||||
}
|
||||
|
||||
/// A clickable row: `title`, and for a skill (`tutorials`) a progress ring before it and the
|
||||
@@ -155,12 +191,13 @@ 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` and `learn.show`.
|
||||
/// The Learn panel's UI commands: `window.learn`, `learn.list`, `learn.show` and `learn.locateTool`.
|
||||
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.show" => open(app, p),
|
||||
"learn.locateTool" => locate::locate_tool(app, p),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
@@ -206,7 +243,7 @@ fn open(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
|
||||
let (skill, tutorial) = catalog::tutorial(id).ok_or_else(|| format!("unknown tutorial `{id}` (see learn.list)"))?;
|
||||
crate::panel_docking::reveal_panel(app, ID)?;
|
||||
app.ui.learn = View { tab: Tab::Skills, skill: Some(skill.id), tutorial: Some(tutorial.id) };
|
||||
let steps: Vec<&str> = tutorial.steps.iter().map(|s| s.text).collect();
|
||||
let steps: Vec<String> = tutorial.steps.iter().map(catalog::Step::plain).collect();
|
||||
Ok(json!({"skill": skill.id, "tutorial": tutorial.id, "title": tutorial.title, "steps": steps}))
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
/// One step of a tutorial: what to do, and the tool it chooses or the command it runs, if any.
|
||||
pub struct Step {
|
||||
/// The step as the panel shows it. In a step that names a tool, the tool's name is in square
|
||||
/// brackets, and the panel draws it as a link that shows where the tool is.
|
||||
pub text: &'static str,
|
||||
/// A tool id ([`vectorcraft_tools::catalog::tool_info`]) the step names.
|
||||
pub tool: Option<&'static str>,
|
||||
@@ -10,6 +12,22 @@ pub struct Step {
|
||||
pub command: Option<&'static str>,
|
||||
}
|
||||
|
||||
impl Step {
|
||||
/// The text before the tool's name, the name, and the text after it; none for a step that
|
||||
/// names no tool or has no bracketed name.
|
||||
pub fn tool_name(&self) -> Option<(&'static str, &'static str, &'static str)> {
|
||||
self.tool?;
|
||||
let (before, rest) = self.text.split_once('[')?;
|
||||
let (name, after) = rest.split_once(']')?;
|
||||
Some((before, name, after))
|
||||
}
|
||||
|
||||
/// The text as it reads, without the brackets around the tool's name.
|
||||
pub fn plain(&self) -> String {
|
||||
self.text.replace(['[', ']'], "")
|
||||
}
|
||||
}
|
||||
|
||||
/// A tutorial: an id for `learn.show`, its title and its numbered steps.
|
||||
pub struct Tutorial {
|
||||
pub id: &'static str,
|
||||
@@ -29,7 +47,7 @@ const fn say(text: &'static str) -> Step {
|
||||
Step { text, tool: None, command: None }
|
||||
}
|
||||
|
||||
/// A step that uses tool `id`.
|
||||
/// A step that uses tool `id`, whose name `text` holds in square brackets.
|
||||
const fn tool(text: &'static str, id: &'static str) -> Step {
|
||||
Step { text, tool: Some(id), command: None }
|
||||
}
|
||||
@@ -49,7 +67,7 @@ pub const SKILLS: &[Skill] = &[
|
||||
id: "draw-and-color",
|
||||
title: "Draw and color a shape",
|
||||
steps: &[
|
||||
tool("Choose the Rectangle tool in the toolbar.", "rectangle"),
|
||||
tool("Choose the [Rectangle tool] in the toolbar.", "rectangle"),
|
||||
say("Drag on the artboard to draw a rectangle."),
|
||||
say("With the rectangle still selected, click a color in the Swatches panel to fill it."),
|
||||
command("Click the Stroke box under the tools, then click another swatch to color the outline.", "paint.toggleActive"),
|
||||
@@ -59,16 +77,16 @@ pub const SKILLS: &[Skill] = &[
|
||||
id: "combine-shapes",
|
||||
title: "Combine shapes into one",
|
||||
steps: &[
|
||||
tool("Choose the Ellipse tool and draw two circles that overlap.", "ellipse"),
|
||||
tool("Choose the Selection tool and drag across both circles to select them.", "selection"),
|
||||
tool("Choose the Shape Builder tool and drag across the parts you want to merge.", "shapeBuilder"),
|
||||
tool("Choose the [Ellipse tool] and draw two circles that overlap.", "ellipse"),
|
||||
tool("Choose the [Selection tool] and drag across both circles to select them.", "selection"),
|
||||
tool("Choose the [Shape Builder tool] and drag across the parts you want to merge.", "shapeBuilder"),
|
||||
],
|
||||
},
|
||||
Tutorial {
|
||||
id: "add-headline",
|
||||
title: "Add a headline",
|
||||
steps: &[
|
||||
tool("Choose the Type tool and click on the artboard.", "type"),
|
||||
tool("Choose the [Type tool] and click on the artboard.", "type"),
|
||||
say("Type a few words, then press Escape to finish."),
|
||||
say("Pick a font and a size in the Character panel."),
|
||||
],
|
||||
@@ -83,7 +101,7 @@ pub const SKILLS: &[Skill] = &[
|
||||
id: "pen-paths",
|
||||
title: "Draw paths with the Pen",
|
||||
steps: &[
|
||||
tool("Choose the Pen tool.", "pen"),
|
||||
tool("Choose the [Pen tool].", "pen"),
|
||||
say("Click to place corner points, or drag to pull out a curve."),
|
||||
say("Click the first point again to close the shape, or press Enter to leave it open."),
|
||||
],
|
||||
@@ -92,18 +110,18 @@ pub const SKILLS: &[Skill] = &[
|
||||
id: "smooth-curves",
|
||||
title: "Draw smooth curves",
|
||||
steps: &[
|
||||
tool("Choose the Curvature tool.", "curvature"),
|
||||
tool("Choose the [Curvature tool].", "curvature"),
|
||||
say("Click to place points: the path bends smoothly through each one."),
|
||||
tool("Choose the Direct Selection tool and drag a point to reshape the curve.", "directSelection"),
|
||||
tool("Choose the [Direct Selection tool] and drag a point to reshape the curve.", "directSelection"),
|
||||
],
|
||||
},
|
||||
Tutorial {
|
||||
id: "pencil-sketch",
|
||||
title: "Sketch with the Pencil",
|
||||
steps: &[
|
||||
tool("Choose the Pencil tool.", "pencil"),
|
||||
tool("Choose the [Pencil tool].", "pencil"),
|
||||
say("Drag to draw a freehand line."),
|
||||
tool("Choose the Smooth tool and drag along the line to even it out.", "smooth"),
|
||||
tool("Choose the [Smooth tool] and drag along the line to even it out.", "smooth"),
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -116,7 +134,7 @@ pub const SKILLS: &[Skill] = &[
|
||||
id: "fill-and-stroke",
|
||||
title: "Fill and stroke",
|
||||
steps: &[
|
||||
tool("Select an object with the Selection tool.", "selection"),
|
||||
tool("Select an object with the [Selection tool].", "selection"),
|
||||
say("Click the Fill box under the tools, then pick a color in the Color panel."),
|
||||
command("Click the double arrow beside the boxes to swap the fill and the stroke.", "paint.swap"),
|
||||
],
|
||||
@@ -125,8 +143,8 @@ pub const SKILLS: &[Skill] = &[
|
||||
id: "gradient",
|
||||
title: "Add a gradient",
|
||||
steps: &[
|
||||
tool("Select an object with the Selection tool.", "selection"),
|
||||
tool("Choose the Gradient tool and click the object to give it a gradient.", "gradient"),
|
||||
tool("Select an object with the [Selection tool].", "selection"),
|
||||
tool("Choose the [Gradient tool] and click the object to give it a gradient.", "gradient"),
|
||||
say("Drag across the object to set the gradient's direction and length."),
|
||||
],
|
||||
},
|
||||
@@ -134,8 +152,8 @@ pub const SKILLS: &[Skill] = &[
|
||||
id: "copy-colors",
|
||||
title: "Copy colors from other art",
|
||||
steps: &[
|
||||
tool("Select the object to color with the Selection tool.", "selection"),
|
||||
tool("Choose the Eyedropper tool and click the art whose colors you want.", "eyedropper"),
|
||||
tool("Select the object to color with the [Selection tool].", "selection"),
|
||||
tool("Choose the [Eyedropper tool] and click the art whose colors you want.", "eyedropper"),
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -148,8 +166,8 @@ pub const SKILLS: &[Skill] = &[
|
||||
id: "area-type",
|
||||
title: "Flow text into a shape",
|
||||
steps: &[
|
||||
tool("Draw a rectangle for the text to fill.", "rectangle"),
|
||||
tool("Choose the Area Type tool and click the rectangle's edge.", "areaType"),
|
||||
tool("Draw a rectangle for the text to fill with the [Rectangle tool].", "rectangle"),
|
||||
tool("Choose the [Area Type tool] and click the rectangle's edge.", "areaType"),
|
||||
say("Type or paste your text: it wraps inside the shape."),
|
||||
],
|
||||
},
|
||||
@@ -157,8 +175,8 @@ pub const SKILLS: &[Skill] = &[
|
||||
id: "type-on-path",
|
||||
title: "Type along a path",
|
||||
steps: &[
|
||||
tool("Draw a curved path with the Pen tool.", "pen"),
|
||||
tool("Choose the Type on a Path tool and click the path.", "typeOnPath"),
|
||||
tool("Draw a curved path with the [Pen tool].", "pen"),
|
||||
tool("Choose the [Type on a Path tool] and click the path.", "typeOnPath"),
|
||||
say("Type your text: it follows the path."),
|
||||
],
|
||||
},
|
||||
@@ -166,7 +184,7 @@ pub const SKILLS: &[Skill] = &[
|
||||
id: "outline-type",
|
||||
title: "Turn type into shapes",
|
||||
steps: &[
|
||||
tool("Select a text object with the Selection tool.", "selection"),
|
||||
tool("Select a text object with the [Selection tool].", "selection"),
|
||||
command("Choose Type › Create Outlines to turn its letters into paths.", "type.createOutlines"),
|
||||
],
|
||||
},
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
//! `learn.locateTool`, which the link on a tool's name in a tutorial step runs: it marks the tool
|
||||
//! where the current layout shows it, without choosing it. A tool shown on its toolbar button has
|
||||
//! the button marked; a tool in a collapsed group opens the group's flyout with the tool's row
|
||||
//! marked; a group torn off into a strip is raised with the tool marked in it; a tool on no shown
|
||||
//! toolbar is marked in the All Tools drawer. The mark lasts [`LIFETIME`] seconds after it first
|
||||
//! shows, until Escape, or until the next locate replaces it.
|
||||
|
||||
use egui::{Context, CornerRadius, LayerId, Rect, Stroke, StrokeKind};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::VectorcraftApp;
|
||||
use crate::state::UiState;
|
||||
use crate::theme::Tokens;
|
||||
|
||||
/// Seconds the mark stays on screen after it first shows.
|
||||
pub const LIFETIME: f64 = 3.0;
|
||||
/// The width of the mark's outline.
|
||||
pub const MARK_WIDTH: f32 = 2.0;
|
||||
/// Passes a locate waits for its mark to draw before it is dropped: the toolbar no longer shows
|
||||
/// the tool, or the flyout it opened has closed.
|
||||
const UNSEEN: u8 = 3;
|
||||
|
||||
/// Which control shows the located tool.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum Target {
|
||||
/// The Tools panel's toolbar: a button, a group's flyout or a strip torn off it.
|
||||
Tools,
|
||||
/// The toolbar made with New Toolbar called this: a button, a group's flyout or a strip.
|
||||
Custom(String),
|
||||
/// The All Tools drawer beside the Tools panel.
|
||||
AllTools,
|
||||
}
|
||||
|
||||
/// A tool being located and how far its mark has got.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Locate {
|
||||
pub tool: &'static str,
|
||||
pub target: Target,
|
||||
/// When the mark first drew (the context's time).
|
||||
since: Option<f64>,
|
||||
/// The target was scrolled into view.
|
||||
pub(crate) scrolled: bool,
|
||||
/// The group's flyout was opened or its strip raised.
|
||||
pub(crate) revealed: bool,
|
||||
/// The mark drew in this pass.
|
||||
drawn: bool,
|
||||
/// Passes in a row in which the mark did not draw.
|
||||
unseen: u8,
|
||||
}
|
||||
|
||||
/// The located tool when the Tools panel's toolbar (`toolbar` none) or the toolbar made with New
|
||||
/// Toolbar called `toolbar` shows it.
|
||||
pub(crate) fn wanted(ui: &UiState, toolbar: Option<&str>) -> Option<&'static str> {
|
||||
let l = ui.learn_locate.as_ref()?;
|
||||
let here = match (&l.target, toolbar) {
|
||||
(Target::Tools, None) => true,
|
||||
(Target::Custom(name), Some(t)) => name == t,
|
||||
_ => false,
|
||||
};
|
||||
here.then_some(l.tool)
|
||||
}
|
||||
|
||||
/// The located tool when the All Tools drawer beside the Tools panel shows it.
|
||||
pub(crate) fn wanted_in_drawer(ui: &UiState) -> Option<&'static str> {
|
||||
let l = ui.learn_locate.as_ref()?;
|
||||
(l.target == Target::AllTools && ui.tools_drawer && ui.drawer_toolbar.is_none()).then_some(l.tool)
|
||||
}
|
||||
|
||||
/// Whether the target is still to be scrolled into view; true once.
|
||||
pub(crate) fn scroll_once(ui: &mut UiState) -> bool {
|
||||
ui.learn_locate.as_mut().is_some_and(|l| !std::mem::replace(&mut l.scrolled, true))
|
||||
}
|
||||
|
||||
/// Whether the target's flyout is still to be opened or its strip raised; true once.
|
||||
pub(crate) fn reveal_once(ui: &mut UiState) -> bool {
|
||||
ui.learn_locate.as_mut().is_some_and(|l| !std::mem::replace(&mut l.revealed, true))
|
||||
}
|
||||
|
||||
/// Mark `rect`, the located tool's control as drawn this pass on `layer`, clipped to `clip` and the
|
||||
/// window. The first mark starts the [`LIFETIME`]; once it has passed, the locate ends.
|
||||
pub(crate) fn mark(app: &mut VectorcraftApp, ctx: &Context, layer: LayerId, clip: Rect, rect: Rect) {
|
||||
let Some(l) = app.ui.learn_locate.as_mut() else { return };
|
||||
let now = ctx.input(|i| i.time);
|
||||
let since = *l.since.get_or_insert(now);
|
||||
let left = LIFETIME - (now - since);
|
||||
if left <= 0.0 {
|
||||
app.ui.learn_locate = None;
|
||||
return;
|
||||
}
|
||||
l.drawn = true;
|
||||
ctx.request_repaint_after_secs(left as f32);
|
||||
let t = Tokens::get(ctx);
|
||||
let painter = ctx.layer_painter(layer).with_clip_rect(clip.intersect(ctx.content_rect()));
|
||||
painter.rect_stroke(rect.shrink(1.0), CornerRadius::same(6), Stroke::new(MARK_WIDTH, t.selection), StrokeKind::Inside);
|
||||
}
|
||||
|
||||
/// After the toolbars and panels have drawn: Escape ends the locate, also an Escape that closed the
|
||||
/// All Tools drawer or ended something else first, and so does a mark that has not drawn for
|
||||
/// [`UNSEEN`] passes.
|
||||
pub(crate) fn tidy(app: &mut VectorcraftApp, ctx: &Context) {
|
||||
let Some(l) = app.ui.learn_locate.as_mut() else { return };
|
||||
// The raw events keep a key that a handler consumed this pass.
|
||||
let escape = ctx.input(|i| i.raw.events.iter().any(|e| matches!(e, egui::Event::Key { key: egui::Key::Escape, pressed: true, .. })));
|
||||
if escape {
|
||||
app.ui.learn_locate = None;
|
||||
return;
|
||||
}
|
||||
if std::mem::take(&mut l.drawn) {
|
||||
l.unseen = 0;
|
||||
} else {
|
||||
l.unseen += 1;
|
||||
if l.unseen > UNSEEN {
|
||||
app.ui.learn_locate = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The reply for a tool that no toolbar can show now, with what would show it.
|
||||
fn unavailable(tool: &str, reason: &str) -> Value {
|
||||
json!({"tool": tool, "location": "unavailable", "toolbar": null, "reason": reason})
|
||||
}
|
||||
|
||||
/// Mark `tool` on `target` from the next pass, replacing an earlier locate; the flyout or drawer an
|
||||
/// earlier one opened closes.
|
||||
fn start(app: &mut VectorcraftApp, tool: &'static str, target: Target) {
|
||||
if let Some(old) = app.ui.learn_locate.take() {
|
||||
app.ui.flyout = None;
|
||||
if old.target == Target::AllTools && target != Target::AllTools && app.ui.drawer_toolbar.is_none() {
|
||||
app.ui.tools_drawer = false;
|
||||
}
|
||||
}
|
||||
app.ui.learn_locate = Some(Locate { tool, target, since: None, scrolled: false, revealed: false, drawn: false, unseen: 0 });
|
||||
}
|
||||
|
||||
/// `learn.locateTool {tool}`: mark where `tool` is in the current layout →
|
||||
/// `{tool, location, toolbar}`, where `location` is `toolbar` (its button), `flyout` (a row of its
|
||||
/// collapsed group's flyout, which opens), `strip` (its group torn off into a strip, which comes
|
||||
/// to the front), `allTools` (no shown toolbar holds it: the All Tools drawer opens) or
|
||||
/// `unavailable` (with `reason`), and `toolbar` names the toolbar made with New Toolbar that
|
||||
/// holds it (null for the Tools panel's). A Tools panel that Window › Tools hid shows again; the
|
||||
/// tool is not chosen, and the remembered tool of its group stays. A missing, non-string or unknown
|
||||
/// id is an error and changes nothing.
|
||||
pub(crate) fn locate_tool(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
|
||||
let key = "tool";
|
||||
let tool = match p.get(key) {
|
||||
None | Some(Value::Null) => return Err("tool (a tool id) is required".into()),
|
||||
Some(Value::String(tool)) => tool.as_str(),
|
||||
Some(_) => return Err(format!("`{key}` must be a string")),
|
||||
};
|
||||
let tool = vectorcraft_tools::tool_info(tool).ok_or_else(|| format!("unknown tool `{tool}`"))?.id;
|
||||
if app.ui.screen_mode >= 3 {
|
||||
return Ok(unavailable(tool, "Presentation Mode hides the toolbars: change the screen mode to show them"));
|
||||
}
|
||||
// Tab hides the toolbars made with New Toolbar until the next Tab, also once Window › Tools
|
||||
// shows the Tools panel again.
|
||||
let tab_hid = app.ui.tab_hid.is_some();
|
||||
if tab_hid && !app.ui.toolbar {
|
||||
return Ok(unavailable(tool, "Tab has hidden the toolbars: press Tab to show them"));
|
||||
}
|
||||
if let Some(slot) = crate::toolbar::slots(app).into_iter().map(|(_, s)| s).find(|s| s.contains(&tool)) {
|
||||
let location = if slot.len() > 1 && crate::toolbar::is_floating(app, None, slot[0]) {
|
||||
"strip"
|
||||
} else if crate::toolbar::shown_tool(app, &slot) == tool {
|
||||
"toolbar"
|
||||
} else {
|
||||
"flyout"
|
||||
};
|
||||
app.ui.toolbar = true;
|
||||
start(app, tool, Target::Tools);
|
||||
return Ok(json!({"tool": tool, "location": location, "toolbar": null}));
|
||||
}
|
||||
let active = app.session.tool_id();
|
||||
let custom = app.ui.custom_toolbars.iter().filter(|b| b.shown && !tab_hid).find_map(|bar| {
|
||||
let slot = bar.slots.iter().find(|s| s.iter().any(|t| t == tool))?;
|
||||
let tools: Vec<&'static str> = slot.iter().filter_map(|id| vectorcraft_tools::tool_info(id).map(|t| t.id)).collect();
|
||||
let first = *tools.first()?;
|
||||
let location = if tools.len() > 1 && crate::toolbar::is_floating(app, Some(&bar.name), first) {
|
||||
"strip"
|
||||
} else if crate::custom_toolbars::shown_tool(bar, active, &tools) == Some(tool) {
|
||||
"toolbar"
|
||||
} else {
|
||||
"flyout"
|
||||
};
|
||||
Some((bar.name.clone(), location))
|
||||
});
|
||||
if let Some((name, location)) = custom {
|
||||
start(app, tool, Target::Custom(name.clone()));
|
||||
return Ok(json!({"tool": tool, "location": location, "toolbar": name}));
|
||||
}
|
||||
app.run("window.editToolbar", json!({"open": true}))?;
|
||||
start(app, tool, Target::AllTools);
|
||||
Ok(json!({"tool": tool, "location": "allTools", "toolbar": null}))
|
||||
}
|
||||
@@ -72,8 +72,20 @@ fn the_catalog_is_valid() {
|
||||
assert!(!tutorial.title.is_empty() && !tutorial.steps.is_empty(), "tutorial {} is empty", tutorial.id);
|
||||
for step in tutorial.steps {
|
||||
assert!(step.text.ends_with('.'), "{}: {:?}", tutorial.id, step.text);
|
||||
let brackets = step.text.matches(['[', ']']).count();
|
||||
if let Some(tool) = step.tool {
|
||||
assert!(vectorcraft_tools::catalog::tool_info(tool).is_some(), "{}: no tool {tool}", tutorial.id);
|
||||
let info = vectorcraft_tools::catalog::tool_info(tool);
|
||||
assert!(info.is_some(), "{}: no tool {tool}", tutorial.id);
|
||||
// The step's link names the tool it locates, once.
|
||||
let name = step.tool_name().map(|(_, name, _)| name);
|
||||
assert!(
|
||||
brackets == 2 && name.zip(info).is_some_and(|(n, t)| n.eq_ignore_ascii_case(t.label)),
|
||||
"{}: {:?}",
|
||||
tutorial.id,
|
||||
step.text
|
||||
);
|
||||
} else {
|
||||
assert_eq!(brackets, 0, "{}: {:?} links no tool", tutorial.id, step.text);
|
||||
}
|
||||
if let Some(cmd) = step.command {
|
||||
let known =
|
||||
@@ -189,7 +201,9 @@ fn a_tutorial_previews_its_steps() {
|
||||
assert_eq!(app.ui.learn.tutorial, Some(tutorial.id));
|
||||
let text = painted_text(&mut app, show);
|
||||
for (i, step) in tutorial.steps.iter().enumerate() {
|
||||
assert!(text.contains(&format!("{}.", i + 1)) && text.contains(step.text), "{} in {text}", step.text);
|
||||
let parts = step.tool_name().map_or(vec![step.text], |(before, name, after)| vec![before, name, after]);
|
||||
assert!(text.contains(&format!("{}.", i + 1)) && parts.iter().all(|p| text.contains(p)), "{} in {text}", step.text);
|
||||
assert!(!text.contains('['), "no brackets: {text}");
|
||||
}
|
||||
click(&mut app, &ctx, "Back", show);
|
||||
assert_eq!(app.ui.learn, View { tab: Tab::Skills, skill: Some(skill.id), tutorial: None });
|
||||
@@ -229,3 +243,92 @@ fn learn_show_rejects_a_missing_or_unknown_tutorial() {
|
||||
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") };
|
||||
show(app, ui);
|
||||
}
|
||||
|
||||
/// A step's tool name is a link; a click on it locates that tool without choosing it, and
|
||||
/// `learn.show` gives the step's text without the link's brackets.
|
||||
#[test]
|
||||
fn a_tool_name_in_a_step_is_a_link_that_locates_the_tool() {
|
||||
let mut app = app();
|
||||
let ctx = context();
|
||||
let nodes = frame(&mut app, &ctx, vec![], pen_paths);
|
||||
let link = nodes.iter().find(|n| n.label().is_some_and(|l| l == "Pen tool")).expect("the Pen tool link");
|
||||
assert_eq!(link.role(), egui::accesskit::Role::Link);
|
||||
assert_eq!(nodes.iter().filter(|n| n.role() == egui::accesskit::Role::Link).count(), 1, "only the step that names a tool has a link");
|
||||
click(&mut app, &ctx, "Pen tool", pen_paths);
|
||||
let located = app.ui.learn_locate.as_ref().map(|l| (l.tool, l.target.clone()));
|
||||
assert_eq!(located, Some(("pen", super::locate::Target::Tools)), "{}", app.ui.status);
|
||||
assert_eq!(app.session.tool_id(), "selection", "the link does not choose the tool");
|
||||
let shown_tutorial = app.run("learn.show", json!({"tutorial": "pen-paths"})).unwrap();
|
||||
assert_eq!(shown_tutorial["steps"][0], "Choose the Pen tool.");
|
||||
}
|
||||
|
||||
/// Enter on the link, once Tab has moved the keyboard focus to it, locates the tool.
|
||||
#[test]
|
||||
fn enter_on_a_focused_tool_link_locates_the_tool() {
|
||||
let mut app = app();
|
||||
let ctx = context();
|
||||
let link = frame(&mut app, &ctx, vec![], pen_paths)
|
||||
.into_iter()
|
||||
.find(|n| n.label().is_some_and(|l| l == "Pen tool"))
|
||||
.and_then(|n| n.bounds())
|
||||
.expect("the Pen tool link");
|
||||
let key = |key, pressed| egui::Event::Key { key, physical_key: None, pressed, repeat: false, modifiers: Default::default() };
|
||||
let focused_on_link = |ctx: &egui::Context| {
|
||||
ctx.memory(|m| m.focused()).and_then(|id| ctx.read_response(id)).is_some_and(|r| {
|
||||
let c = r.rect.center();
|
||||
((link.x0 + link.x1) / 2.0 - f64::from(c.x)).abs() < 1.0 && ((link.y0 + link.y1) / 2.0 - f64::from(c.y)).abs() < 1.0
|
||||
})
|
||||
};
|
||||
let mut found = false;
|
||||
for _ in 0..30 {
|
||||
frame(&mut app, &ctx, vec![key(egui::Key::Tab, true), key(egui::Key::Tab, false)], pen_paths);
|
||||
if focused_on_link(&ctx) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert!(found, "Tab reaches the link");
|
||||
assert!(app.ui.learn_locate.is_none());
|
||||
frame(&mut app, &ctx, vec![key(egui::Key::Enter, true), key(egui::Key::Enter, false)], pen_paths);
|
||||
frame(&mut app, &ctx, vec![], pen_paths);
|
||||
assert_eq!(app.ui.learn_locate.as_ref().map(|l| l.tool), Some("pen"));
|
||||
}
|
||||
|
||||
/// A missing, mistyped or unknown tool id is an error and changes nothing.
|
||||
#[test]
|
||||
fn learn_locate_tool_rejects_a_missing_or_unknown_tool() {
|
||||
let mut app = app();
|
||||
app.ui.toolbar = false;
|
||||
for (params, error) in [
|
||||
(json!({}), "tool (a tool id) is required"),
|
||||
(json!({"tool": null}), "tool (a tool id) is required"),
|
||||
(json!({"tool": 3}), "`tool` must be a string"),
|
||||
(json!({"tool": "nope"}), "unknown tool `nope`"),
|
||||
] {
|
||||
assert_eq!(app.run("learn.locateTool", params).unwrap_err(), error);
|
||||
assert!(app.ui.learn_locate.is_none() && !app.ui.toolbar && !app.ui.tools_drawer && app.ui.flyout.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
/// With the toolbars hidden by Presentation Mode or by Tab, nothing can be marked: the reply says
|
||||
/// so and the screen stays as it is.
|
||||
#[test]
|
||||
fn learn_locate_tool_is_unavailable_while_the_toolbars_are_hidden() {
|
||||
let mut app = app();
|
||||
app.ui.screen_mode = 3;
|
||||
let reply = app.run("learn.locateTool", json!({"tool": "pen"})).unwrap();
|
||||
assert_eq!((reply["location"].as_str(), reply["reason"].is_string()), (Some("unavailable"), true), "{reply}");
|
||||
assert!(app.ui.learn_locate.is_none());
|
||||
app.ui.screen_mode = 0;
|
||||
app.run("window.dock", json!({})).unwrap();
|
||||
assert!(app.ui.tab_hid.is_some() && !app.ui.toolbar);
|
||||
let reply = app.run("learn.locateTool", json!({"tool": "pen"})).unwrap();
|
||||
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");
|
||||
}
|
||||
|
||||
@@ -420,6 +420,10 @@ pub struct UiState {
|
||||
/// The Learn panel's tab, skill and tutorial, kept while the app runs.
|
||||
#[serde(skip)]
|
||||
pub learn: crate::panels::learn::View,
|
||||
/// The tool whose place a Learn tutorial step's link shows (`learn.locateTool`), while its mark
|
||||
/// is on screen. Not saved.
|
||||
#[serde(skip)]
|
||||
pub learn_locate: Option<crate::panels::learn::locate::Locate>,
|
||||
pub control_bar: bool,
|
||||
/// The Control bar is docked to the bottom of the window instead of the top
|
||||
/// (`window.control.dock`).
|
||||
@@ -1061,6 +1065,7 @@ impl Default for UiState {
|
||||
color_settings_scroll: false,
|
||||
find_font_in: String::new(),
|
||||
learn: Default::default(),
|
||||
learn_locate: None,
|
||||
control_bar: false,
|
||||
control_bar_bottom: false,
|
||||
control_bar_hidden_groups: Default::default(),
|
||||
|
||||
@@ -12,6 +12,7 @@ use serde_json::{Value, json};
|
||||
use vectorcraft_color::Paint;
|
||||
use vectorcraft_tools::{Mods, TOOL_GROUPS, ToolInfo, ToolKey, tool_info};
|
||||
|
||||
use crate::panels::learn::locate as learn_locate;
|
||||
use crate::theme::{self, Tokens};
|
||||
use crate::{VectorcraftApp, icons, widgets};
|
||||
|
||||
@@ -85,6 +86,17 @@ pub(crate) fn slots(app: &VectorcraftApp) -> Vec<(Option<&'static str>, Vec<&'st
|
||||
}
|
||||
}
|
||||
|
||||
/// The tool a slot's button shows: the active tool when the slot holds it, else the one chosen
|
||||
/// last from the slot's group, else its first.
|
||||
pub(crate) fn shown_tool(app: &VectorcraftApp, slot: &[&'static str]) -> &'static str {
|
||||
let active = app.session.tool_id();
|
||||
if slot.contains(&active) {
|
||||
return active;
|
||||
}
|
||||
let first = slot.first().copied().unwrap_or_default();
|
||||
app.ui.slot_tool.get(first).and_then(|id| slot.iter().copied().find(|s| s == id)).unwrap_or(first)
|
||||
}
|
||||
|
||||
/// Remember the tool shown in the slot that contains `id`.
|
||||
pub fn remember(app: &mut VectorcraftApp, id: &str) {
|
||||
for (_, s) in slots(app) {
|
||||
@@ -253,6 +265,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut Ui, cols: usize, floating: Option<egu
|
||||
// A group torn off into a floating flyout is raised instead of opening its flyout.
|
||||
let mut raise: Option<&'static str> = None;
|
||||
let labels = app.session.prefs.tool_group_labels;
|
||||
let locate = learn_locate::wanted(&app.ui, None);
|
||||
let mut i = 0;
|
||||
while i < all.len() {
|
||||
if all[i].0.is_some() && !labels {
|
||||
@@ -281,12 +294,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut Ui, cols: usize, floating: Option<egu
|
||||
ui.add_space((bounds.center().x - cols as f32 * 18.0 - ui.cursor().min.x).max(0.0));
|
||||
for &k in &row {
|
||||
let slot = &all[k].1;
|
||||
let shown_id = if slot.contains(&active) {
|
||||
active.to_string()
|
||||
} else {
|
||||
app.ui.slot_tool.get(slot[0]).filter(|id| slot.contains(&id.as_str())).cloned().unwrap_or_else(|| slot[0].to_string())
|
||||
};
|
||||
let Some(shown) = tool_info(&shown_id).or_else(|| tool_info(slot[0])) else { continue };
|
||||
let Some(shown) = tool_info(shown_tool(app, slot)).or_else(|| tool_info(slot[0])) else { continue };
|
||||
let is_active = slot.contains(&active);
|
||||
let (rect, resp) = ui.allocate_exact_size(vec2(36.0, PITCH - 1.0), Sense::click_and_drag());
|
||||
let well = egui::Rect::from_center_size(rect.center(), vec2(30.0, 27.5));
|
||||
@@ -301,6 +309,20 @@ fn body(app: &mut VectorcraftApp, ui: &mut Ui, cols: usize, floating: Option<egu
|
||||
let c = rect.center() + vec2(12.5, 10.0);
|
||||
ui.painter().add(egui::Shape::convex_polygon(vec![c, c + vec2(-3.5, 0.0), c + vec2(0.0, -3.5)], t.icon, Stroke::NONE));
|
||||
}
|
||||
// A Learn link locating a tool of this slot: the button is marked when it shows
|
||||
// the tool, else the group's flyout opens on it (a strip torn off marks it).
|
||||
if let Some(tool) = locate.filter(|tool| slot.contains(tool))
|
||||
&& !(slot.len() > 1 && is_floating(app, None, slot[0]))
|
||||
{
|
||||
if !ui.clip_rect().contains_rect(rect) && learn_locate::scroll_once(&mut app.ui) {
|
||||
ui.scroll_to_rect(rect, None);
|
||||
ui.ctx().request_repaint();
|
||||
} else if shown.id == tool {
|
||||
learn_locate::mark(app, ui.ctx(), ui.layer_id(), ui.clip_rect(), rect);
|
||||
} else if learn_locate::reveal_once(&mut app.ui) {
|
||||
open_flyout = Some((slot.clone(), rect));
|
||||
}
|
||||
}
|
||||
if editing {
|
||||
buttons.push((k, rect));
|
||||
resp.dnd_set_drag_payload(crate::edit_toolbar::ToolDrag { tool: shown.id, from_toolbar: true });
|
||||
@@ -723,20 +745,27 @@ struct FlyoutInput {
|
||||
bar: Option<egui::Response>,
|
||||
/// A floating flyout's × was clicked.
|
||||
close: bool,
|
||||
/// Where the tool a Learn link locates is drawn.
|
||||
located: Option<egui::Rect>,
|
||||
}
|
||||
|
||||
/// A flyout's tool rows (a floating flyout's strip of tool buttons) with the grab bar down their
|
||||
/// right side; a floating flyout's bar has a × at its top. The bar's id is the group's, so a drag
|
||||
/// that tears a flyout off carries on moving the floating flyout it becomes (`toolbar`: the toolbar
|
||||
/// made with New Toolbar the group is in, none for the Tools panel's).
|
||||
fn flyout_body(ui: &mut Ui, t: &Tokens, active: &str, tools: &[String], floating: bool, toolbar: Option<&str>) -> FlyoutInput {
|
||||
/// made with New Toolbar the group is in, none for the Tools panel's; `locate`: the tool a Learn
|
||||
/// link locates, whose place it returns).
|
||||
fn flyout_body(ui: &mut Ui, t: &Tokens, active: &str, tools: &[String], floating: bool, toolbar: Option<&str>, locate: Option<&str>) -> FlyoutInput {
|
||||
let mut out = FlyoutInput::default();
|
||||
let key = tools.first().map_or("", String::as_str);
|
||||
let bar_id = bar_id(toolbar, key);
|
||||
ui.spacing_mut().item_spacing = vec2(0.0, 0.0);
|
||||
ui.horizontal(|ui| {
|
||||
let rows = if floating { ui.horizontal(|ui| tool_strip(ui, t, active, tools)) } else { ui.vertical(|ui| tool_rows(ui, t, active, tools)) };
|
||||
out.chosen = rows.inner;
|
||||
let rows = if floating {
|
||||
ui.horizontal(|ui| tool_strip(ui, t, active, tools, locate))
|
||||
} else {
|
||||
ui.vertical(|ui| tool_rows(ui, t, active, tools, locate))
|
||||
};
|
||||
(out.chosen, out.located) = rows.inner;
|
||||
let (bar, _) = ui.allocate_exact_size(vec2(BAR, rows.response.rect.height()), Sense::hover());
|
||||
ui.painter().rect_filled(bar, 0.0, t.tab_strip);
|
||||
let grip = if floating {
|
||||
@@ -769,9 +798,11 @@ fn flyout_body(ui: &mut Ui, t: &Tokens, active: &str, tools: &[String], floating
|
||||
out
|
||||
}
|
||||
|
||||
/// One row per tool: icon, name and shortcut, with a dot by the active tool. Returns the tool clicked.
|
||||
fn tool_rows(ui: &mut Ui, t: &Tokens, active: &str, tools: &[String]) -> Option<&'static str> {
|
||||
/// One row per tool: icon, name and shortcut, with a dot by the active tool. Returns the tool clicked
|
||||
/// and the row of `locate`.
|
||||
fn tool_rows(ui: &mut Ui, t: &Tokens, active: &str, tools: &[String], locate: Option<&str>) -> (Option<&'static str>, Option<egui::Rect>) {
|
||||
let mut chosen = None;
|
||||
let mut located = None;
|
||||
let lang = crate::i18n::current();
|
||||
let color = |tool: &ToolInfo| if active == tool.id { t.flyout_active } else { t.text_strong };
|
||||
let rows: Vec<(&ToolInfo, RowText)> =
|
||||
@@ -797,8 +828,11 @@ fn tool_rows(ui: &mut Ui, t: &Tokens, active: &str, tools: &[String]) -> Option<
|
||||
if resp.clicked() {
|
||||
chosen = Some(tool.id);
|
||||
}
|
||||
if locate == Some(tool.id) {
|
||||
located = Some(r);
|
||||
}
|
||||
}
|
||||
chosen
|
||||
(chosen, located)
|
||||
}
|
||||
|
||||
/// A flyout row's name and its shortcut, laid out.
|
||||
@@ -842,9 +876,10 @@ fn grab_id(toolbar: Option<&str>, key: &str) -> egui::Id {
|
||||
}
|
||||
|
||||
/// One icon button per tool, the active one lit, with its name and shortcut on hover. Returns the
|
||||
/// tool clicked.
|
||||
fn tool_strip(ui: &mut Ui, t: &Tokens, active: &str, tools: &[String]) -> Option<&'static str> {
|
||||
/// tool clicked and the button of `locate`.
|
||||
fn tool_strip(ui: &mut Ui, t: &Tokens, active: &str, tools: &[String], locate: Option<&str>) -> (Option<&'static str>, Option<egui::Rect>) {
|
||||
let mut chosen = None;
|
||||
let mut located = None;
|
||||
for tool in tools.iter().filter_map(|id| tool_info(id)) {
|
||||
let (r, resp) = ui.allocate_exact_size(STRIP_BUTTON, Sense::click());
|
||||
let well = r.shrink(1.0);
|
||||
@@ -859,8 +894,11 @@ fn tool_strip(ui: &mut Ui, t: &Tokens, active: &str, tools: &[String]) -> Option
|
||||
if resp.on_hover_text(tip(tool)).clicked() {
|
||||
chosen = Some(tool.id);
|
||||
}
|
||||
if locate == Some(tool.id) {
|
||||
located = Some(r);
|
||||
}
|
||||
}
|
||||
chosen
|
||||
(chosen, located)
|
||||
}
|
||||
|
||||
/// The width of a floating flyout's strip of `tools`, its bar included.
|
||||
@@ -908,14 +946,20 @@ fn flyout(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
let t = Tokens::get(ctx);
|
||||
let active = app.session.tool_id();
|
||||
let toolbar = ctx.data(|d| d.get_temp::<Option<String>>(flyout_toolbar_id())).flatten();
|
||||
let locate = learn_locate::wanted(&app.ui, toolbar.as_deref()).filter(|tool| tools.iter().any(|id| id == tool));
|
||||
let mut input = FlyoutInput::default();
|
||||
// Beside the tool, on the side with room for it: toward the canvas from a toolbar docked right.
|
||||
let size = ctx.memory(|m| m.area_rect(egui::Id::unique("tool-flyout"))).map_or(vec2(160.0, 0.0), |r| r.size());
|
||||
let x = beside(anchor, size.x, 8.0, room(app, ctx));
|
||||
// The area stays inside the window (egui's `Area::constrain`, on by default): a flyout opened
|
||||
// near the bottom moves up, so every row, a located one included, shows.
|
||||
let resp =
|
||||
egui::Area::new(egui::Id::unique("tool-flyout")).order(egui::Order::Foreground).fixed_pos(pos2(x, anchor.top() - 1.0)).show(ctx, |ui| {
|
||||
flyout_frame(&t, false).show(ui, |ui| input = flyout_body(ui, &t, active, &tools, false, toolbar.as_deref()));
|
||||
flyout_frame(&t, false).show(ui, |ui| input = flyout_body(ui, &t, active, &tools, false, toolbar.as_deref(), locate));
|
||||
});
|
||||
if let Some(rect) = input.located {
|
||||
learn_locate::mark(app, ctx, resp.response.layer_id, resp.response.rect, rect);
|
||||
}
|
||||
// Dragging the bar tears the flyout off, and so does releasing a press over it (a click, or the
|
||||
// end of the long press that opened the flyout): it floats where the pointer is.
|
||||
let at = ctx.input(|i| i.pointer.interact_pos());
|
||||
@@ -998,10 +1042,19 @@ fn floating_strips(app: &mut VectorcraftApp, ctx: &egui::Context, toolbar: Optio
|
||||
let active = app.session.tool_id();
|
||||
let mut chosen = None;
|
||||
let mut closed = None;
|
||||
// A Learn link locating a tool of a strip raises the strip once, then marks the tool in it.
|
||||
let locate = learn_locate::wanted(&app.ui, toolbar);
|
||||
let raise = locate.is_some_and(|tool| strips(&app.ui, toolbar).iter().any(|f| f.tools.iter().any(|id| id == tool)))
|
||||
&& learn_locate::reveal_once(&mut app.ui);
|
||||
let mut located = None;
|
||||
let Some(strips) = strips_mut(&mut app.ui, toolbar) else { return };
|
||||
for (k, f) in strips.iter_mut().enumerate() {
|
||||
let Some(key) = f.tools.first() else { continue };
|
||||
let id = strip_area(toolbar, key);
|
||||
let locate = locate.filter(|tool| f.tools.iter().any(|t| t == tool));
|
||||
if raise && locate.is_some() {
|
||||
raise_floating(ctx, toolbar, key);
|
||||
}
|
||||
let since = ctx.data(|d| d.get_temp::<f64>(id)).map(|t0| now - t0);
|
||||
let flash = since.is_some_and(|s| (0.0..FLASH).contains(&s));
|
||||
if let Some(s) = since.filter(|_| flash) {
|
||||
@@ -1011,9 +1064,12 @@ fn floating_strips(app: &mut VectorcraftApp, ctx: &egui::Context, toolbar: Optio
|
||||
let size = ctx.memory(|m| m.area_rect(id)).map_or(vec2(strip_width(&f.tools), PITCH), |r| r.size());
|
||||
let pos = crate::tools_float::clamp(f.pos, size, screen);
|
||||
let mut input = FlyoutInput::default();
|
||||
egui::Area::new(id).order(egui::Order::Middle).fixed_pos(pos).show(ctx, |ui| {
|
||||
flyout_frame(&t, flash).show(ui, |ui| input = flyout_body(ui, &t, active, &f.tools, true, toolbar));
|
||||
let area = egui::Area::new(id).order(egui::Order::Middle).fixed_pos(pos).show(ctx, |ui| {
|
||||
flyout_frame(&t, flash).show(ui, |ui| input = flyout_body(ui, &t, active, &f.tools, true, toolbar, locate));
|
||||
});
|
||||
if let Some(rect) = input.located {
|
||||
located = Some((area.response.layer_id, area.response.rect, rect));
|
||||
}
|
||||
// Dragging the bar keeps the flyout where the pointer holds it (by an offset rather than
|
||||
// each frame's motion, which a just torn-off flyout's first, unseen frame would drop).
|
||||
let grab = grab_id(toolbar, key);
|
||||
@@ -1036,6 +1092,9 @@ fn floating_strips(app: &mut VectorcraftApp, ctx: &egui::Context, toolbar: Optio
|
||||
if let Some(k) = closed.filter(|k| *k < strips.len()) {
|
||||
strips.remove(k);
|
||||
}
|
||||
if let Some((layer, clip, rect)) = located {
|
||||
learn_locate::mark(app, ctx, layer, clip, rect);
|
||||
}
|
||||
if let Some(id) = chosen {
|
||||
app.select_tool(id);
|
||||
}
|
||||
|
||||
@@ -1054,3 +1054,267 @@ fn tab_skips_the_grip_and_the_tear_off_bars() {
|
||||
assert!(!stops.contains(&id), "Tab stops on {name}: {stops:?}");
|
||||
}
|
||||
}
|
||||
|
||||
use crate::panels::learn::locate::{LIFETIME, MARK_WIDTH};
|
||||
|
||||
/// One frame of the toolbar in a window 400 points wide and `height` tall, then what ends a Learn
|
||||
/// locate (`learn.locateTool`): the marks the locate drew, and the screen-reader nodes.
|
||||
fn locate_frame(
|
||||
app: &mut VectorcraftApp,
|
||||
ctx: &egui::Context,
|
||||
time: f64,
|
||||
events: Vec<Event>,
|
||||
height: f32,
|
||||
) -> (Vec<egui::Rect>, Vec<egui::accesskit::Node>) {
|
||||
fn marks(s: &egui::Shape, color: Color32, out: &mut Vec<egui::Rect>) {
|
||||
match s {
|
||||
egui::Shape::Rect(r) if r.stroke.width == MARK_WIDTH && r.stroke.color == color => out.push(r.rect),
|
||||
egui::Shape::Vec(v) => v.iter().for_each(|s| marks(s, color, out)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let screen_rect = Some(egui::Rect::from_min_size(Pos2::ZERO, vec2(400.0, height)));
|
||||
let mut out = ctx.run_ui(egui::RawInput { time: Some(time), events, screen_rect, ..Default::default() }, |ui| {
|
||||
show(app, ui);
|
||||
crate::panels::learn::locate::tidy(app, ui.ctx());
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
let color = Tokens::get(ctx).selection;
|
||||
let mut found = vec![];
|
||||
out.shapes.iter().for_each(|c| marks(&c.shape, color, &mut found));
|
||||
let nodes = out.platform_output.accesskit_update.map(|u| u.nodes.into_iter().map(|(_, n)| n).collect()).unwrap_or_default();
|
||||
(found, nodes)
|
||||
}
|
||||
|
||||
/// A context that makes screen-reader nodes.
|
||||
fn reader_context() -> egui::Context {
|
||||
let ctx = egui::Context::default();
|
||||
ctx.enable_accesskit();
|
||||
ctx
|
||||
}
|
||||
|
||||
/// Where the screen-reader node named after `tool`'s tool tip is drawn.
|
||||
fn tool_rect(nodes: &[egui::accesskit::Node], tool: &str) -> egui::Rect {
|
||||
let name = tool_info(tool).map(tip).unwrap_or_default();
|
||||
let b = nodes.iter().find(|n| n.label().is_some_and(|l| l == name)).and_then(|n| n.bounds()).unwrap_or_else(|| panic!("{name} is drawn"));
|
||||
egui::Rect::from_min_max(pos2(b.x0 as f32, b.y0 as f32), pos2(b.x1 as f32, b.y1 as f32))
|
||||
}
|
||||
|
||||
/// The only mark drawn, on `control`.
|
||||
fn assert_marks(marks: &[egui::Rect], control: egui::Rect, what: &str) {
|
||||
let [mark] = marks else { panic!("{what}: one mark, not {marks:?}") };
|
||||
let want = control.shrink(1.0);
|
||||
assert!((mark.min - want.min).length() < 0.5 && (mark.max - want.max).length() < 0.5, "{what}: the mark {mark:?} is on {control:?}");
|
||||
}
|
||||
|
||||
/// A Learn link's `learn.locateTool` marks a tool shown on its button, with Tools docked left or
|
||||
/// right, floating or in two columns; it does not choose the tool, and the mark goes after
|
||||
/// [`LIFETIME`] seconds.
|
||||
#[test]
|
||||
fn a_learn_link_marks_the_tools_button_wherever_tools_is() {
|
||||
for (right, floating, double) in [(false, None, false), (true, None, false), (false, Some([120.0, 80.0]), false), (false, None, true)] {
|
||||
let what = format!("right {right}, floating {floating:?}, double {double}");
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
(app.ui.toolbar_right, app.ui.toolbar_pos, app.ui.toolbar_double) = (right, floating, double);
|
||||
let ctx = reader_context();
|
||||
locate_frame(&mut app, &ctx, 0.0, vec![], 1200.0);
|
||||
assert_eq!(app.run("learn.locateTool", json!({"tool": "pen"})).unwrap(), json!({"tool": "pen", "location": "toolbar", "toolbar": null}));
|
||||
let (marks, nodes) = locate_frame(&mut app, &ctx, 1.0, vec![], 1200.0);
|
||||
assert_marks(&marks, tool_rect(&nodes, "pen"), &what);
|
||||
assert_eq!((app.session.tool_id(), app.ui.flyout), ("selection", None), "{what}: no tool chosen, no flyout");
|
||||
assert_eq!(locate_frame(&mut app, &ctx, 1.0 + LIFETIME - 0.1, vec![], 1200.0).0.len(), 1, "{what}: still marked");
|
||||
assert!(locate_frame(&mut app, &ctx, 1.0 + LIFETIME + 0.01, vec![], 1200.0).0.is_empty(), "{what}: the mark has gone");
|
||||
assert!(app.ui.learn_locate.is_none(), "{what}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A tool in a collapsed group opens the group's flyout with its row marked, also while another
|
||||
/// tool of the group shows on the button; the group keeps the tool it shows.
|
||||
#[test]
|
||||
fn a_learn_link_opens_a_collapsed_group_on_the_tools_row() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.ui.slot_tool.insert("pencil".into(), "pathEraser".into());
|
||||
let ctx = reader_context();
|
||||
locate_frame(&mut app, &ctx, 0.0, vec![], 1200.0);
|
||||
for (k, tool) in ["smooth", "pencil"].into_iter().enumerate() {
|
||||
assert_eq!(app.run("learn.locateTool", json!({"tool": tool})).unwrap()["location"], "flyout", "{tool}");
|
||||
// The flyout's first frame only measures it.
|
||||
locate_frame(&mut app, &ctx, 1.0 + k as f64, vec![], 1200.0);
|
||||
let (marks, nodes) = locate_frame(&mut app, &ctx, 1.1 + k as f64, vec![], 1200.0);
|
||||
assert!(app.ui.flyout.is_some(), "{tool}: the flyout opens");
|
||||
assert_marks(&marks, tool_rect(&nodes, tool), tool);
|
||||
let row = tool_rect(&nodes, tool);
|
||||
let flyout = ctx.memory(|m| m.area_rect(egui::Id::unique("tool-flyout"))).expect("the flyout shows");
|
||||
assert!(flyout.contains_rect(row), "{tool}: a row of the flyout ({row:?} in {flyout:?})");
|
||||
assert_eq!(ctx.data(|d| d.get_temp::<Vec<String>>(egui::Id::unique("flyout-tools"))).map(|t| t[0].clone()).as_deref(), Some("pencil"));
|
||||
}
|
||||
assert_eq!(app.ui.slot_tool.get("pencil").map(String::as_str), Some("pathEraser"), "the group still shows the Path Eraser");
|
||||
assert_eq!(app.session.tool_id(), "selection");
|
||||
}
|
||||
|
||||
/// A tool of a group torn off into a strip: the strip comes to the front with the tool marked in it.
|
||||
#[test]
|
||||
fn a_learn_link_raises_a_torn_off_group_and_marks_the_tool_in_it() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("window.floatTools", json!({"tool": "pen"})).unwrap();
|
||||
let ctx = reader_context();
|
||||
locate_frame(&mut app, &ctx, 0.0, vec![], 1200.0);
|
||||
assert_eq!(app.run("learn.locateTool", json!({"tool": "addAnchor"})).unwrap()["location"], "strip");
|
||||
let (marks, nodes) = locate_frame(&mut app, &ctx, 1.0, vec![], 1200.0);
|
||||
assert!(ctx.data(|d| d.get_temp::<f64>(floating_area("pen"))).is_some(), "the strip was raised");
|
||||
let strip = ctx.memory(|m| m.area_rect(floating_area("pen"))).unwrap();
|
||||
let button = tool_rect(&nodes, "addAnchor");
|
||||
assert!(strip.contains_rect(button), "the strip's button: {button:?} in {strip:?}");
|
||||
assert_marks(&marks, button, "the strip");
|
||||
assert_eq!(app.ui.flyout, None);
|
||||
}
|
||||
|
||||
/// A tool Edit Toolbar took out of the toolbar is marked in the All Tools drawer, which opens; the
|
||||
/// toolbar stays as it was.
|
||||
#[test]
|
||||
fn a_learn_link_marks_a_tool_no_toolbar_holds_in_all_tools() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("toolbar.removeTool", json!({"tool": "rectangle"})).unwrap();
|
||||
let before = slots(&app);
|
||||
let ctx = reader_context();
|
||||
locate_frame(&mut app, &ctx, 0.0, vec![], 1200.0);
|
||||
assert_eq!(app.run("learn.locateTool", json!({"tool": "rectangle"})).unwrap()["location"], "allTools");
|
||||
assert!(app.ui.tools_drawer && app.ui.drawer_toolbar.is_none());
|
||||
// The drawer's first frame only measures it.
|
||||
locate_frame(&mut app, &ctx, 1.0, vec![], 1200.0);
|
||||
let (marks, nodes) = locate_frame(&mut app, &ctx, 1.1, vec![], 1200.0);
|
||||
let drawer = ctx.memory(|m| m.area_rect(crate::edit_toolbar::drawer_id())).expect("the drawer shows");
|
||||
let row = tool_rect(&nodes, "rectangle");
|
||||
assert!(drawer.contains_rect(row), "the drawer's row: {row:?} in {drawer:?}");
|
||||
assert_marks(&marks, row, "the drawer");
|
||||
assert_eq!(slots(&app), before, "no tool added to the toolbar");
|
||||
// Escape closes the drawer first (the shortcuts consume it), and ends the locate too.
|
||||
let escape = Event::Key { key: egui::Key::Escape, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() };
|
||||
let screen_rect = Some(egui::Rect::from_min_size(Pos2::ZERO, vec2(400.0, 1200.0)));
|
||||
let _ = ctx.run_ui(egui::RawInput { time: Some(1.2), events: vec![escape], screen_rect, ..Default::default() }, |ui| {
|
||||
crate::shortcuts::handle(&mut app, ui.ctx());
|
||||
show(&mut app, ui);
|
||||
crate::panels::learn::locate::tidy(&mut app, ui.ctx());
|
||||
});
|
||||
assert!(!app.ui.tools_drawer, "Escape closed the drawer");
|
||||
assert!(app.ui.learn_locate.is_none(), "and ended the locate");
|
||||
}
|
||||
|
||||
/// A tool on a toolbar made with New Toolbar that the Tools panel does not hold is marked there.
|
||||
#[test]
|
||||
fn a_learn_link_marks_a_tool_on_a_toolbar_made_with_new_toolbar() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("window.toolbar.new", json!({"name": "Mine"})).unwrap();
|
||||
app.run("toolbar.addTool", json!({"tool": "artboard", "toolbar": "Mine"})).unwrap();
|
||||
let ctx = reader_context();
|
||||
locate_frame(&mut app, &ctx, 0.0, vec![], 1200.0);
|
||||
assert_eq!(
|
||||
app.run("learn.locateTool", json!({"tool": "artboard"})).unwrap(),
|
||||
json!({"tool": "artboard", "location": "toolbar", "toolbar": "Mine"})
|
||||
);
|
||||
let (marks, nodes) = locate_frame(&mut app, &ctx, 1.0, vec![], 1200.0);
|
||||
let bar = ctx.memory(|m| m.area_rect(crate::custom_toolbars::area_id("Mine"))).unwrap();
|
||||
let button = tool_rect(&nodes, "artboard");
|
||||
assert!(bar.contains_rect(button), "{button:?} in {bar:?}");
|
||||
assert_marks(&marks, button, "the toolbar Mine");
|
||||
// A tool of a group whose button shows another: the group's flyout opens on it.
|
||||
app.run("toolbar.addTool", json!({"tool": "slice", "toolbar": "Mine", "slot": 0, "place": "group"})).unwrap();
|
||||
assert_eq!(app.run("learn.locateTool", json!({"tool": "slice"})).unwrap(), json!({"tool": "slice", "location": "flyout", "toolbar": "Mine"}));
|
||||
locate_frame(&mut app, &ctx, 2.0, vec![], 1200.0);
|
||||
let (marks, nodes) = locate_frame(&mut app, &ctx, 2.1, vec![], 1200.0);
|
||||
let flyout = ctx.memory(|m| m.area_rect(egui::Id::unique("tool-flyout"))).expect("the flyout shows");
|
||||
let row = tool_rect(&nodes, "slice");
|
||||
assert!(flyout.contains_rect(row), "{row:?} in {flyout:?}");
|
||||
assert_marks(&marks, row, "the flyout of the toolbar Mine");
|
||||
assert_eq!(app.session.tool_id(), "selection");
|
||||
}
|
||||
|
||||
/// Tools hidden with Window › Tools shows again for a Learn link, and a short window scrolls the
|
||||
/// tool into view.
|
||||
#[test]
|
||||
fn a_learn_link_shows_hidden_tools_and_scrolls_the_tool_into_view() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.ui.toolbar = false;
|
||||
let ctx = reader_context();
|
||||
locate_frame(&mut app, &ctx, 0.0, vec![], 300.0);
|
||||
assert_eq!(app.run("learn.locateTool", json!({"tool": "gradient"})).unwrap()["location"], "toolbar");
|
||||
assert!(app.ui.toolbar, "Tools shows");
|
||||
let mut drawn = (vec![], vec![]);
|
||||
for k in 1..20 {
|
||||
drawn = locate_frame(&mut app, &ctx, f64::from(k) * 0.1, vec![], 300.0);
|
||||
}
|
||||
let (marks, nodes) = drawn;
|
||||
let button = tool_rect(&nodes, "gradient");
|
||||
assert!(button.top() >= 0.0 && button.bottom() <= 300.0, "scrolled into view: {button:?}");
|
||||
assert_marks(&marks, button, "scrolled");
|
||||
}
|
||||
|
||||
/// A short window scrolls a tool of a toolbar made with New Toolbar into view for a Learn link.
|
||||
#[test]
|
||||
fn a_learn_link_scrolls_a_tool_into_view_on_a_toolbar_made_with_new_toolbar() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("window.toolbar.new", json!({"name": "Mine"})).unwrap();
|
||||
let tools: Vec<&'static str> = slots(&app).into_iter().filter_map(|(_, s)| s.first().copied()).take(16).collect();
|
||||
for tool in &tools {
|
||||
app.run("toolbar.addTool", json!({"tool": tool, "toolbar": "Mine"})).unwrap();
|
||||
}
|
||||
let tool = *tools.last().expect("a tool");
|
||||
app.run("toolbar.removeTool", json!({"tool": tool})).unwrap();
|
||||
assert!(slots(&app).iter().all(|(_, s)| !s.contains(&tool)), "the Tools panel does not hold {tool}");
|
||||
let ctx = reader_context();
|
||||
locate_frame(&mut app, &ctx, 0.0, vec![], 300.0);
|
||||
let (_, nodes) = locate_frame(&mut app, &ctx, 0.05, vec![], 300.0);
|
||||
let name = tool_info(tool).map(tip).unwrap_or_default();
|
||||
let shown = nodes.iter().filter(|n| n.label().is_some_and(|l| l == name)).filter_map(|n| n.bounds()).any(|b| b.y1 <= 300.0);
|
||||
assert!(!shown, "{tool} starts outside the window");
|
||||
assert_eq!(app.run("learn.locateTool", json!({"tool": tool})).unwrap(), json!({"tool": tool, "location": "toolbar", "toolbar": "Mine"}));
|
||||
let mut drawn = (vec![], vec![]);
|
||||
for k in 1..20 {
|
||||
drawn = locate_frame(&mut app, &ctx, f64::from(k) * 0.1, vec![], 300.0);
|
||||
}
|
||||
let (marks, nodes) = drawn;
|
||||
let button = tool_rect(&nodes, tool);
|
||||
let bar = ctx.memory(|m| m.area_rect(crate::custom_toolbars::area_id("Mine"))).unwrap();
|
||||
assert!(bar.contains_rect(button), "{button:?} in {bar:?}");
|
||||
assert!(button.top() >= 0.0 && button.bottom() <= 300.0, "scrolled into view: {button:?}");
|
||||
assert_marks(&marks, button, "scrolled on the toolbar Mine");
|
||||
assert_eq!(app.session.tool_id(), "selection");
|
||||
}
|
||||
|
||||
/// In a short window, the flyout of the last group stays inside the window, so the located row's
|
||||
/// mark shows.
|
||||
#[test]
|
||||
fn a_learn_link_in_a_short_window_marks_a_flyout_row_inside_the_window() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
let ctx = reader_context();
|
||||
let slot = slots(&app).into_iter().map(|(_, s)| s).rfind(|s| s.len() > 1).expect("a group");
|
||||
let tool = *slot.last().expect("a tool");
|
||||
locate_frame(&mut app, &ctx, 0.0, vec![], 300.0);
|
||||
assert_eq!(app.run("learn.locateTool", json!({"tool": tool})).unwrap()["location"], "flyout", "{tool}");
|
||||
let mut drawn = (vec![], vec![]);
|
||||
for k in 1..20 {
|
||||
drawn = locate_frame(&mut app, &ctx, f64::from(k) * 0.1, vec![], 300.0);
|
||||
}
|
||||
let (marks, nodes) = drawn;
|
||||
let row = tool_rect(&nodes, tool);
|
||||
assert!(row.top() >= 0.0 && row.bottom() <= 300.0, "{tool}: the row is inside the window: {row:?}");
|
||||
assert_marks(&marks, row, tool);
|
||||
}
|
||||
|
||||
/// A second Learn link moves the mark; Escape removes it.
|
||||
#[test]
|
||||
fn a_second_learn_link_replaces_the_mark_and_escape_removes_it() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
let ctx = reader_context();
|
||||
locate_frame(&mut app, &ctx, 0.0, vec![], 1200.0);
|
||||
app.run("learn.locateTool", json!({"tool": "pen"})).unwrap();
|
||||
let (marks, nodes) = locate_frame(&mut app, &ctx, 0.5, vec![], 1200.0);
|
||||
assert_marks(&marks, tool_rect(&nodes, "pen"), "pen");
|
||||
app.run("learn.locateTool", json!({"tool": "curvature"})).unwrap();
|
||||
let (marks, nodes) = locate_frame(&mut app, &ctx, 1.0, vec![], 1200.0);
|
||||
assert_marks(&marks, tool_rect(&nodes, "curvature"), "the second link");
|
||||
let escape = Event::Key { key: egui::Key::Escape, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() };
|
||||
locate_frame(&mut app, &ctx, 1.5, vec![escape], 1200.0);
|
||||
assert!(app.ui.learn_locate.is_none());
|
||||
assert!(locate_frame(&mut app, &ctx, 2.0, vec![], 1200.0).0.is_empty());
|
||||
}
|
||||
|
||||
+18
@@ -2559,6 +2559,24 @@ and changes nothing. Behavior reference:
|
||||
{"name":"invoke_menu","arguments":{"command":"learn.show","params":{"tutorial":"pen-paths"}}}
|
||||
```
|
||||
|
||||
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
|
||||
keyboard focus, runs `learn.locateTool {tool}`, which marks where the tool is in the current layout without choosing
|
||||
it: its toolbar button, or the tool's row in its group's flyout, which opens when the button shows another tool of the
|
||||
group. A group torn off into a strip comes to the front with the tool marked in it. A tool that no shown toolbar holds
|
||||
is marked in the All Tools drawer, which opens; the tool is not added to a toolbar. A Tools panel hidden with
|
||||
Window › Tools shows again. A target below the window's bottom scrolls into view. The mark lasts 3 seconds after it
|
||||
first shows; Escape or the next locate removes it. The reply is `{tool, location, toolbar}`: `location` is `toolbar`,
|
||||
`flyout`, `strip`, `allTools` or `unavailable`, and `toolbar` names the toolbar made with New Toolbar that holds the
|
||||
tool (null for the Tools panel's). While Presentation Mode or Tab hides the toolbars, the reply is `unavailable` with
|
||||
a `reason`, and nothing changes. A missing, non-string or unknown `tool` is an error and changes nothing. The mark,
|
||||
its 3 seconds, the flyout, strip and All Tools behavior and the hidden-toolbar replies 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)
|
||||
describes selecting a tool's name in a step to find the tool.
|
||||
|
||||
```json
|
||||
{"name":"invoke_menu","arguments":{"command":"learn.locateTool","params":{"tool":"smooth"}}}
|
||||
```
|
||||
|
||||
## Ruler guides
|
||||
|
||||
Each ruler guide is an object of a layer or sublayer, directly in it and above its art, and takes an id from the
|
||||
|
||||
@@ -838,6 +838,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- Object › Transform › Reset Bounding Box squares the box of every object inside the selected ones as well: the children of groups, compound paths, clip groups, blends, envelopes, repeats and intertwines, and the art of linked opacity masks. Ungroup and the release commands then bring back boxes square to the page (VectorCraft's own behavior; `bboxrotate::reset_bounding_box_squares_the_objects_inside_a_group`, `reset_bounding_box_squares_the_turned_members_of_a_square_group`, `reset_bounding_box_then_release_clipping_mask` and the other `reset_bounding_box_then_*` tests, `reset_bounding_box_leaves_the_art_of_an_unlinked_opacity_mask`).
|
||||
- Native macOS menu bar, vector tool cursors, a ⌘K command palette, middle-button panning with any tool. Native menus explicitly end tracking on command selection, outside window presses and Escape; menu updates wait until tracking ends, and a dismissing press never starts a canvas gesture (M14.8).
|
||||
- The Learn panel (Window › Learn, or Explore tutorials beside the search icon on the app bar) has a For you tab with a Get started section and a Quick skill guide card, and a Skills tab that lists VectorCraft's own skills with their tutorial counts and empty progress rings; a skill lists its tutorials, a tutorial shows its numbered steps, and Back goes up a level; `learn.list` and `learn.show {tutorial}` script it ([the help page](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/learn-skills-quick-skill-guide.html); `panels::learn::tests`).
|
||||
- A tool's name in a Learn tutorial step is a blue link that shows where the tool is without choosing it (`learn.locateTool {tool}`): its toolbar button is marked, a collapsed group's flyout opens with the tool's row marked, a torn-off strip comes to the front, and a tool no shown toolbar holds is marked in the All Tools drawer. The mark, its 3 seconds and the hidden-toolbar cases are VectorCraft's own rules ([the help page](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/learn-skills-quick-skill-guide.html); `panels::learn::tests`, the `a_learn_link_*` tests in `toolbar::tests`).
|
||||
- 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 parameter checks that many commands share show in the status bar in the interface language: a value that is not a number where a number goes (`object.move` with `"dx": "60 mm"`, and `object.rotate`, `object.setBounds` and the other commands that call `check_numbers`), a missing number or index, `ids` that are not a list of object ids, an object id that is not a non-negative integer or names no object, and a date that is not Unix seconds. Each has a row in every catalog that translates all status and error messages, and in Bulgarian, Danish, Indonesian and Dutch (`shared_param_check_errors_are_translated`).
|
||||
- Export commands run through MCP, the control channel or the CLI refuse a key that no format takes, and a command that writes one format (`document.exportPdf`, `document.exportEps`, `document.exportDxf`) or takes one format's options (`document.pdfSettings`, `pdf.preset.save`) refuses a key that format doesn't take. An unknown key inside `marks`, `bleed`, `compression`, `output`, `advanced`, `security` or `flattener` is refused by its path.
|
||||
|
||||
+1
-1
@@ -374,7 +374,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 | done | [the help page](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/learn-skills-quick-skill-guide.html); `learn.list`, `learn.show`; `panels::learn::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 | done | [the help page](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/learn-skills-quick-skill-guide.html); `learn.list`, `learn.show`, `learn.locateTool`; the mark's look and 3-second lifetime are VectorCraft's own rules; `panels::learn::tests`, `toolbar::tests` |
|
||||
| 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 |
|
||||
|
||||
Reference in New Issue
Block a user