From b815bb7f1df966599596dc1fc92474e26754c951 Mon Sep 17 00:00:00 2001 From: Olli Meier Date: Wed, 11 Dec 2024 05:42:49 +0100 Subject: [PATCH] First draft of 'Font overview' --- src/fontra/client/lang/de.js | 7 + src/fontra/client/lang/en.js | 7 + src/fontra/client/lang/fr.js | 7 + src/fontra/client/lang/ja.js | 7 + src/fontra/client/lang/nl.js | 7 + src/fontra/client/lang/zh-CN.js | 9 +- src/fontra/views/fontinfo/panel-sources.js | 2 +- .../views/fontoverview/fontoverview.css | 36 +- .../views/fontoverview/fontoverview.html | 3 +- src/fontra/views/fontoverview/fontoverview.js | 338 +++++++++++++++++- 10 files changed, 414 insertions(+), 9 deletions(-) diff --git a/src/fontra/client/lang/de.js b/src/fontra/client/lang/de.js index aeefae724..7fb53cf1e 100644 --- a/src/fontra/client/lang/de.js +++ b/src/fontra/client/lang/de.js @@ -26,6 +26,7 @@ export const strings = { "action.delete-glyph": "Glyph entfernen", "action.delete-selection": "Auswahl entfernen", "action.edit-anchor": "Anker bearbeiten", + "action.edit-background-image": "Hintergrund-Bild bearbeiten", "action.edit-guideline": "Hilfslinie bearbeiten", "action.export-as.designspace": "Designspace + UFO (*.designspace)", "action.export-as.fontra": "Fontra (*.fontra)", @@ -103,6 +104,7 @@ export const strings = { "axes.undo.edit": "Achse %0 bearbeiten", "background-image.labels.colorize": "Färbung", "background-image.labels.opacity": "Transparenz", + "backgroundImage.labels.color": "Farbe", "canvas.clean-view-and-hand-tool": "Ungehinderte Sicht und Hand Werkzeug", "cross-axis-mapping.axis-participates": "Wenn markiert, dann ist diese Achse teil des Mappings", @@ -205,6 +207,8 @@ export const strings = { "font-info.vendorid": "Hersteller ID", "font-info.version.major": "Hauptversionsnummer", "font-info.version.minor": "Nebenversionsnummer", + "font-overview.title": "Font overview", + "glyph-overview.title": "Glyph overview", "guideline.labels.angle": "Neigung", "guideline.labels.locked": "Gesperrt", "guideline.labels.name": "Name", @@ -238,6 +242,7 @@ export const strings = { "menubar.view.select-previous-glyph": "Vorheriger Glyph", "menubar.view.select-previous-source": "Vorherige Source", "menubar.view.select-previous-source-layer": "Select previous source layer", + "menubar.window": "Window", "message.cancel-editing": "Jemand anderes hat vor dir etwas geändert.", "message.edit-has-been-reverted": "Die Änderung wurde rückgängig gemacht.", "message.glyph-could-not-be-saved": "Glyph konnte nicht gespeichert werden.", @@ -288,6 +293,7 @@ export const strings = { "sidebar.designspace-navigation.glyph-axes": "Glyph-Achsen", "sidebar.designspace-navigation.glyph-axes.edit": "Glyph-Achsen bearbeiten", "sidebar.designspace-navigation.glyph-axes.reset": "Glyph-Achsen zurücksetzen", + "sidebar.designspace-navigation.glyph-source-layers": "Source layers", "sidebar.designspace-navigation.glyph-sources": "Glyph-Sourcen", "sidebar.designspace-navigation.glyph-sources.name": "Source Name", "sidebar.designspace-navigation.glyph-sources.status": "Status", @@ -301,6 +307,7 @@ export const strings = { "Auch den dazugehörigen Layer %0 entfernen", "sidebar.designspace-navigation.warning.delete-source": "Bist du sicher, dass du Source %0 entfernen willst?", + "sidebar.font-overview.font-source": "Font source", "sidebar.glyph-note": "Glyph-Notiz", "sidebar.glyph-note.glyph-note-for-glyph": "Glyph-Notiz für %0", "sidebar.glyph-note.no-glyph-selected": "(keine Glyphe ausgewählt)", diff --git a/src/fontra/client/lang/en.js b/src/fontra/client/lang/en.js index a9a43a70c..8281fc3fb 100644 --- a/src/fontra/client/lang/en.js +++ b/src/fontra/client/lang/en.js @@ -26,6 +26,7 @@ export const strings = { "action.delete-glyph": "Delete Glyph", "action.delete-selection": "Delete Selection", "action.edit-anchor": "Edit Anchor", + "action.edit-background-image": "Edit Background Image", "action.edit-guideline": "Edit Guideline", "action.export-as.designspace": "Designspace + UFO (*.designspace)", "action.export-as.fontra": "Fontra (*.fontra)", @@ -103,6 +104,7 @@ export const strings = { "axes.undo.edit": "edit axis %0", "background-image.labels.colorize": "Colorize", "background-image.labels.opacity": "Opacity", + "backgroundImage.labels.color": "Color", "canvas.clean-view-and-hand-tool": "Clean View and Hand Tool", "cross-axis-mapping.axis-participates": "When checked, this axis participates in the mapping", @@ -202,6 +204,8 @@ export const strings = { "font-info.vendorid": "Vendor ID", "font-info.version.major": "Version Major", "font-info.version.minor": "Version Minor", + "font-overview.title": "Font overview", + "glyph-overview.title": "Glyph overview", "guideline.labels.angle": "Angle", "guideline.labels.locked": "Locked", "guideline.labels.name": "Name", @@ -233,6 +237,7 @@ export const strings = { "menubar.view.select-previous-glyph": "Select previous glyph", "menubar.view.select-previous-source": "Select previous source", "menubar.view.select-previous-source-layer": "Select previous source layer", + "menubar.window": "Window", "message.cancel-editing": "Someone else made an edit just before you.", "message.edit-has-been-reverted": "The edit has been reverted.", "message.glyph-could-not-be-saved": "The glyph could not be saved.", @@ -281,6 +286,7 @@ export const strings = { "sidebar.designspace-navigation.glyph-axes": "Glyph axes", "sidebar.designspace-navigation.glyph-axes.edit": "Edit glyph axes", "sidebar.designspace-navigation.glyph-axes.reset": "Reset glyph axes", + "sidebar.designspace-navigation.glyph-source-layers": "Source layers", "sidebar.designspace-navigation.glyph-sources": "Glyph sources", "sidebar.designspace-navigation.glyph-sources.name": "source name", "sidebar.designspace-navigation.glyph-sources.status": "status", @@ -293,6 +299,7 @@ export const strings = { "Also delete associated layer %0", "sidebar.designspace-navigation.warning.delete-source": "Are you sure you want to delete source %0?", + "sidebar.font-overview.font-source": "Font source", "sidebar.glyph-note": "Glyph Note", "sidebar.glyph-note.glyph-note-for-glyph": "Glyph Note for %0", "sidebar.glyph-note.no-glyph-selected": "(No glyph selected)", diff --git a/src/fontra/client/lang/fr.js b/src/fontra/client/lang/fr.js index 58be6cce9..57c7c5217 100644 --- a/src/fontra/client/lang/fr.js +++ b/src/fontra/client/lang/fr.js @@ -26,6 +26,7 @@ export const strings = { "action.delete-glyph": "Supprimer le glyphe", "action.delete-selection": "Supprimer la sélection", "action.edit-anchor": "Éditer l'ancre", + "action.edit-background-image": "Éditer l'image d'arrière plan", "action.edit-guideline": "Éditer le guide", "action.export-as.designspace": "Designspace + UFO (*.designspace)", "action.export-as.fontra": "Fontra (*.fontra)", @@ -103,6 +104,7 @@ export const strings = { "axes.undo.edit": "éditer l'axe %0", "background-image.labels.colorize": "Coloriser", "background-image.labels.opacity": "Transparence", + "backgroundImage.labels.color": "Coleur", "canvas.clean-view-and-hand-tool": "Prévisualisation et outil de déplacement", "cross-axis-mapping.axis-participates": "Si coché, l'axe participera au mapping", "cross-axis-mapping.delete": "Delete cross-axis mapping", @@ -203,6 +205,8 @@ export const strings = { "font-info.vendorid": "Identifiant du vendeur", "font-info.version.major": "Version Major", "font-info.version.minor": "Version Minor", + "font-overview.title": "Font overview", + "glyph-overview.title": "Glyph overview", "guideline.labels.angle": "Angle", "guideline.labels.locked": "Vérouillé", "guideline.labels.name": "Nom", @@ -238,6 +242,7 @@ export const strings = { "menubar.view.select-previous-glyph": "Sélectionner le glyphe précédente", "menubar.view.select-previous-source": "Sélectionner la source précédente", "menubar.view.select-previous-source-layer": "Select previous source layer", + "menubar.window": "Window", "message.cancel-editing": "Quelqu'un d'autre a édité juste avant vous", "message.edit-has-been-reverted": "L'édition a été annulé", "message.glyph-could-not-be-saved": "Le glyphe n'a pas pu être enregistré", @@ -292,6 +297,7 @@ export const strings = { "sidebar.designspace-navigation.glyph-axes": "Axes du glyphe", "sidebar.designspace-navigation.glyph-axes.edit": "Éditer les axes du glyphe", "sidebar.designspace-navigation.glyph-axes.reset": "Réinitialiser les axes du glyphe", + "sidebar.designspace-navigation.glyph-source-layers": "Source layers", "sidebar.designspace-navigation.glyph-sources": "Sources du glyphe", "sidebar.designspace-navigation.glyph-sources.name": "nom de la source", "sidebar.designspace-navigation.glyph-sources.status": "statut", @@ -305,6 +311,7 @@ export const strings = { "Also delete associated layer %0", "sidebar.designspace-navigation.warning.delete-source": "Êtes-vous sûr de vouloir supprimer la source %0", + "sidebar.font-overview.font-source": "Font source", "sidebar.glyph-note": "Note du glyphe", "sidebar.glyph-note.glyph-note-for-glyph": "Note du glyphe %0", "sidebar.glyph-note.no-glyph-selected": "(aucun glyphe sélectionné)", diff --git a/src/fontra/client/lang/ja.js b/src/fontra/client/lang/ja.js index 60862fc1d..eb4e6bd7a 100644 --- a/src/fontra/client/lang/ja.js +++ b/src/fontra/client/lang/ja.js @@ -26,6 +26,7 @@ export const strings = { "action.delete-glyph": "グリフを削除", "action.delete-selection": "選択範囲を削除", "action.edit-anchor": "アンカーを編集", + "action.edit-background-image": "背景画像を編集", "action.edit-guideline": "ガイドラインを編集", "action.export-as.designspace": "Designspace + UFO (*.designspace)", "action.export-as.fontra": "Fontra (*.fontra)", @@ -103,6 +104,7 @@ export const strings = { "axes.undo.edit": "補完軸%0を編集", "background-image.labels.colorize": "彩色", "background-image.labels.opacity": "透明度", + "backgroundImage.labels.color": "カラー", "canvas.clean-view-and-hand-tool": "塗りのプレビューと手のひらツール", "cross-axis-mapping.axis-participates": "チェックすると、この補完軸がマッピング内で有効になります", @@ -204,6 +206,8 @@ export const strings = { "font-info.vendorid": "ベンダーID", "font-info.version.major": "メジャーバージョン", "font-info.version.minor": "マイナーバージョン", + "font-overview.title": "Font overview", + "glyph-overview.title": "Glyph overview", "guideline.labels.angle": "角度", "guideline.labels.locked": "ロック", "guideline.labels.name": "ガイド名", @@ -236,6 +240,7 @@ export const strings = { "menubar.view.select-previous-glyph": "前のグリフを選択", "menubar.view.select-previous-source": "前のソースを選択", "menubar.view.select-previous-source-layer": "前のソースレイヤーを選択", + "menubar.window": "Window", "message.cancel-editing": "他のユーザーがあなたの直前に操作を行いました。", "message.edit-has-been-reverted": "このグリフは前の状態に戻されました。", "message.glyph-could-not-be-saved": "このグリフは保存されません。", @@ -285,6 +290,7 @@ export const strings = { "sidebar.designspace-navigation.glyph-axes": "グリフの補完軸", "sidebar.designspace-navigation.glyph-axes.edit": "グリフの補完軸を編集", "sidebar.designspace-navigation.glyph-axes.reset": "グリフの補完軸をリセット", + "sidebar.designspace-navigation.glyph-source-layers": "Source layers", "sidebar.designspace-navigation.glyph-sources": "グリフソース", "sidebar.designspace-navigation.glyph-sources.name": "ソース名", "sidebar.designspace-navigation.glyph-sources.status": "ステータス", @@ -296,6 +302,7 @@ export const strings = { "sidebar.designspace-navigation.warning.delete-associated-layer": "同様にレイヤー%0も削除", "sidebar.designspace-navigation.warning.delete-source": "ソース%0を削除しますか?", + "sidebar.font-overview.font-source": "Font source", "sidebar.glyph-note": "グリフノート", "sidebar.glyph-note.glyph-note-for-glyph": "%0のグリフノート", "sidebar.glyph-note.no-glyph-selected": "(グリフが選択されていません)", diff --git a/src/fontra/client/lang/nl.js b/src/fontra/client/lang/nl.js index e7455e5bd..1f0c180d4 100644 --- a/src/fontra/client/lang/nl.js +++ b/src/fontra/client/lang/nl.js @@ -26,6 +26,7 @@ export const strings = { "action.delete-glyph": "Verwijder glyph", "action.delete-selection": "Verwijder selectie", "action.edit-anchor": "Edit Anchor", + "action.edit-background-image": "Edit Background Image", "action.edit-guideline": "Edit Guideline", "action.export-as.designspace": "Designspace + UFO (*.designspace)", "action.export-as.fontra": "Fontra (*.fontra)", @@ -103,6 +104,7 @@ export const strings = { "axes.undo.edit": "edit axis %0", "background-image.labels.colorize": "Colorize", "background-image.labels.opacity": "Opacity", + "backgroundImage.labels.color": "Color", "canvas.clean-view-and-hand-tool": "Schone weergave en Hand gereedschap", "cross-axis-mapping.axis-participates": "When checked, this axis participates in the mapping", @@ -202,6 +204,8 @@ export const strings = { "font-info.vendorid": "Vendor ID", "font-info.version.major": "Versie major", "font-info.version.minor": "Versie minor", + "font-overview.title": "Font overview", + "glyph-overview.title": "Glyph overview", "guideline.labels.angle": "Angle", "guideline.labels.locked": "Locked", "guideline.labels.name": "Naam", @@ -237,6 +241,7 @@ export const strings = { "menubar.view.select-previous-glyph": "Select previous glyph", "menubar.view.select-previous-source": "Selecteer de vorige source", "menubar.view.select-previous-source-layer": "Select previous source layer", + "menubar.window": "Window", "message.cancel-editing": "Someone else made an edit just before you.", "message.edit-has-been-reverted": "The edit has been reverted.", "message.glyph-could-not-be-saved": "The glyph could not be saved.", @@ -285,6 +290,7 @@ export const strings = { "sidebar.designspace-navigation.glyph-axes": "Glyph assen", "sidebar.designspace-navigation.glyph-axes.edit": "Wijzig glyph assen", "sidebar.designspace-navigation.glyph-axes.reset": "Reset glyph assen", + "sidebar.designspace-navigation.glyph-source-layers": "Source layers", "sidebar.designspace-navigation.glyph-sources": "Glyph sources", "sidebar.designspace-navigation.glyph-sources.name": "sourcenaam", "sidebar.designspace-navigation.glyph-sources.status": "status", @@ -297,6 +303,7 @@ export const strings = { "Also delete associated layer %0", "sidebar.designspace-navigation.warning.delete-source": "Are you sure you want to delete source %0?", + "sidebar.font-overview.font-source": "Font source", "sidebar.glyph-note": "Glyph notitie", "sidebar.glyph-note.glyph-note-for-glyph": "Glyph Note for %0", "sidebar.glyph-note.no-glyph-selected": "(No glyph selected)", diff --git a/src/fontra/client/lang/zh-CN.js b/src/fontra/client/lang/zh-CN.js index 6ad38cd2b..bab4e8792 100644 --- a/src/fontra/client/lang/zh-CN.js +++ b/src/fontra/client/lang/zh-CN.js @@ -26,6 +26,7 @@ export const strings = { "action.delete-glyph": "删除字形", "action.delete-selection": "删除选中", "action.edit-anchor": "编辑锚点", + "action.edit-background-image": "编辑背景图片", "action.edit-guideline": "编辑参考线", "action.export-as.designspace": "Designspace + UFO (*.designspace)", "action.export-as.fontra": "Fontra (*.fontra)", @@ -103,6 +104,7 @@ export const strings = { "axes.undo.edit": "编辑参数轴 %0", "background-image.labels.colorize": "填色", "background-image.labels.opacity": "透明度", + "backgroundImage.labels.color": "颜色", "canvas.clean-view-and-hand-tool": "预览与拖拽工具", "cross-axis-mapping.axis-participates": "选中后,该参数轴参与映射", "cross-axis-mapping.delete": "删除跨轴映射", @@ -138,7 +140,7 @@ export const strings = { "dialog.cant-edit-glyph.content.locked-glyph": "该字形已被锁定。", "dialog.cant-edit-glyph.title": "无法编辑字形 “%0”", "dialog.create": "创建", - "dialog.create-new-glyph.body": '如果你想%1创建一个新的字形 "%0",请点击 "创建"。', + "dialog.create-new-glyph.body": '如果你想创建一个新的字形 "%0"%1,请点击 "创建"。', "dialog.create-new-glyph.body.2": '为字符 "%0" (%1)', "dialog.create-new-glyph.title": '创建新字形 "%0"?', "dialog.delete": "删除", @@ -193,6 +195,8 @@ export const strings = { "font-info.vendorid": "供应商 ID", "font-info.version.major": "主要版本号", "font-info.version.minor": "次要版本号", + "font-overview.title": "Font overview", + "glyph-overview.title": "字形概览", "guideline.labels.angle": "角度", "guideline.labels.locked": "已锁定", "guideline.labels.name": "名称", @@ -224,6 +228,7 @@ export const strings = { "menubar.view.select-previous-glyph": "选择上一个字形", "menubar.view.select-previous-source": "选择上一个源", "menubar.view.select-previous-source-layer": "选择上一个源图层", + "menubar.window": "Window", "message.cancel-editing": "有人在你之前做了编辑。", "message.edit-has-been-reverted": "编辑将被复原。", "message.glyph-could-not-be-saved": "该字形无法被保存。", @@ -270,6 +275,7 @@ export const strings = { "sidebar.designspace-navigation.glyph-axes": "字形参数轴", "sidebar.designspace-navigation.glyph-axes.edit": "编辑字形参数轴", "sidebar.designspace-navigation.glyph-axes.reset": "重置字形参数轴", + "sidebar.designspace-navigation.glyph-source-layers": "源图层", "sidebar.designspace-navigation.glyph-sources": "字形源", "sidebar.designspace-navigation.glyph-sources.name": "源名称", "sidebar.designspace-navigation.glyph-sources.status": "状态", @@ -281,6 +287,7 @@ export const strings = { "sidebar.designspace-navigation.warning.delete-associated-layer": "并删除关联的图层 %0", "sidebar.designspace-navigation.warning.delete-source": "你确定要删除源 “%0” 吗?", + "sidebar.font-overview.font-source": "Font source", "sidebar.glyph-note": "字形备忘", "sidebar.glyph-note.glyph-note-for-glyph": "0% 的字形备忘", "sidebar.glyph-note.no-glyph-selected": "(无选中字形)", diff --git a/src/fontra/views/fontinfo/panel-sources.js b/src/fontra/views/fontinfo/panel-sources.js index 9ac6ba637..3adaff932 100644 --- a/src/fontra/views/fontinfo/panel-sources.js +++ b/src/fontra/views/fontinfo/panel-sources.js @@ -620,7 +620,7 @@ class SourceBox extends HTMLElement { customElements.define("source-box", SourceBox); -function sortedSourceIdentifiers(sources, fontAxes) { +export function sortedSourceIdentifiers(sources, fontAxes) { const sortFunc = (identifierA, identifierB) => { for (const axis of fontAxes) { const valueA = sources[identifierA].location[axis.name]; diff --git a/src/fontra/views/fontoverview/fontoverview.css b/src/fontra/views/fontoverview/fontoverview.css index 9382bf677..036ce302a 100644 --- a/src/fontra/views/fontoverview/fontoverview.css +++ b/src/fontra/views/fontoverview/fontoverview.css @@ -4,7 +4,7 @@ body { .main-container { display: grid; - grid-template-columns: auto 1fr; + grid-template-columns: 340px 1fr; padding: 0em; box-sizing: border-box; gap: 0em; @@ -16,20 +16,50 @@ body { display: grid; align-content: start; gap: 1em; - padding: 2em 1em 2em 2em; + padding: 1em 1em 1em 1em; background-color: var(--ui-element-background-color); } -.sidebar-shadow-box { +.font-overview-sidebar-shadow-box { box-shadow: 0px 0px 8px #0006; } +.font-overview-sidebar { + display: grid; + gap: 1em; +} + .font-overview-panel { display: grid; padding: 2em 2em 2em 1em; outline: none; + grid-gap: 1em; +} + +.glyph-cells-section { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(70px, 1fr)); + gap: 1em; } #panel-container { overflow: auto; } + +.font-source-selector { + display: grid; + grid-template-columns: max-content auto; + align-items: center; + gap: 0.666em; + padding-bottom: 1em; +} + +.font-overview-section-header { + font-weight: bold; +} + +/* .glyph-cell-wrapper { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1em; +} */ diff --git a/src/fontra/views/fontoverview/fontoverview.html b/src/fontra/views/fontoverview/fontoverview.html index 8cfb840d7..1374b0534 100644 --- a/src/fontra/views/fontoverview/fontoverview.html +++ b/src/fontra/views/fontoverview/fontoverview.html @@ -7,12 +7,13 @@ Fontra Font Overview +
- +
diff --git a/src/fontra/views/fontoverview/fontoverview.js b/src/fontra/views/fontoverview/fontoverview.js index 7044730ee..007778784 100644 --- a/src/fontra/views/fontoverview/fontoverview.js +++ b/src/fontra/views/fontoverview/fontoverview.js @@ -1,10 +1,188 @@ import { FontController } from "../core/font-controller.js"; import * as html from "../core/html-utils.js"; +import { ObservableController } from "../core/observable-object.js"; import { getRemoteProxy } from "../core/remote.js"; +import { mapAxesFromUserSpaceToSourceSpace } from "../core/var-model.js"; import { makeDisplayPath } from "../core/view-utils.js"; import { translate } from "/core/localization.js"; +import { sortedSourceIdentifiers } from "/fontinfo/panel-sources.js"; // see TODOs below. +import { GlyphCell } from "/web-components/glyph-cell.js"; import { message } from "/web-components/modal-dialog.js"; +// TODOs: +// 1. I am wondering if it would make sense to refactor GlyphsSearch into two web components: +// 1. GlyphsSearch: Includes the search field, only. Access the list of glyphs with eg. glyphsListItemsController. +// 2. GlyphsSearchForEditor which uses GlyphsSearch and adds the glyph list. +// 2. We may want to move sortedSourceIdentifiers to a more general location like utils. Follow up task? +// 3. Do we want to make the sidebar scalable? If so, we may want to refactor sidebar-resize-gutter or at least have a look at it. Follow up task? +// 4. Context menu is implemented in the overview, yet. We may want to add them. Is this a follow up task? +// 5. Maybe use https://www.npmjs.com/package/unicode-properties for overview sections. Also, how to we handle unencoded glyphs? Follow up task? +// 6. Add top menu bar, please see: https://github.com/googlefonts/fontra/issues/1845 +// 7. When opening a glyph in the editor via double click, there is an error: It says 'error while interpolating font sources '{message: 'objects have incompatible number of entries: 7 != 6', type: 'interpolation-error'}'. But this is not true. I don't know why this is happening. + +// START OF COPY: This is a copy of GlyphsSearch but without the list of glyph names +import { UnlitElement, div, label, option, select } from "/core/html-utils.js"; +import { + dumpURLFragment, + getCharFromCodePoint, + guessCharFromGlyphName, + makeUPlusStringFromCodePoint, + throttleCalls, +} from "/core/utils.js"; +import { themeColorCSS } from "/web-components/theme-support.js"; +import { UIList } from "/web-components/ui-list.js"; + +const colors = { + "search-input-foreground-color": ["black", "white"], + "search-input-background-color": ["#eee", "#333"], +}; + +class GlyphsSearchForOverview extends UnlitElement { + static styles = ` + ${themeColorCSS(colors)} + + :host { + display: grid; + grid-template-rows: auto 1fr; + box-sizing: border-box; + overflow: hidden; + align-content: start; + } + + input { + color: var(--search-input-foreground-color); + background-color: var(--search-input-background-color); + font-family: fontra-ui-regular, sans-serif; + font-size: 1.1rem; + border-radius: 2em; + border: none; + outline: none; + resize: none; + width: 100%; + height: 1.8em; + box-sizing: border-box; + padding: 0.2em 0.8em; + } + `; + + constructor(glyphsListItemsController, controllerKey) { + super(); + this.glyphsListItemsController = glyphsListItemsController; + this.controllerKey = controllerKey; + this.searchField = html.input({ + type: "text", + placeholder: translate("sidebar.glyphs.search"), + autocomplete: "off", + oninput: (event) => this._searchFieldChanged(event), + }); + + // I delete a big chunk of code here that is not needed for the overview + + this._glyphNamesListFilterFunc = (item) => true; // pass all through + + this.glyphMap = {}; + } + + focusSearchField() { + this.searchField.focus(); + } + + render() { + return this.searchField; + } + + get glyphMap() { + return this._glyphMap; + } + + set glyphMap(glyphMap) { + this._glyphMap = glyphMap; + this.updateGlyphNamesListContent(); + } + + // getSelectedGlyphName() { + // return this.glyphNamesList.items[this.glyphNamesList.selectedItemIndex]?.glyphName; + // } + + // getFilteredGlyphNames() { + // return this.glyphNamesList.items.map((item) => item.glyphName); + // } + + updateGlyphNamesListContent() { + const glyphMap = this.glyphMap; + this.glyphsListItems = []; + for (const glyphName in glyphMap) { + this.glyphsListItems.push({ + glyphName: glyphName, + unicodes: glyphMap[glyphName], + }); + } + this.glyphsListItems.sort(glyphItemSortFunc); + this._setFilteredGlyphNamesListContent(); + } + + _searchFieldChanged(event) { + const value = event.target.value; + const searchItems = value.split(/\s+/).filter((item) => item.length); + const hexSearchItems = searchItems + .filter((item) => [...item].length === 1) // num chars, not utf16 units! + .map((item) => item.codePointAt(0).toString(16).toUpperCase().padStart(4, "0")); + searchItems.push(...hexSearchItems); + this._glyphNamesListFilterFunc = (item) => glyphFilterFunc(item, searchItems); + this._setFilteredGlyphNamesListContent(); + } + + async _setFilteredGlyphNamesListContent() { + const filteredGlyphItems = this.glyphsListItems.filter( + this._glyphNamesListFilterFunc + ); + //this.glyphNamesList.setItems(filteredGlyphItems); + this.glyphsListItemsController.model[this.controllerKey] = filteredGlyphItems; + } +} + +customElements.define("glyphs-search-glyph-overview", GlyphsSearchForOverview); + +function glyphItemSortFunc(item1, item2) { + const uniCmp = compare(item1.unicodes[0], item2.unicodes[0]); + const glyphNameCmp = compare(item1.glyphName, item2.glyphName); + return uniCmp ? uniCmp : glyphNameCmp; +} + +function glyphFilterFunc(item, searchItems) { + if (!searchItems.length) { + return true; + } + for (const searchString of searchItems) { + if (item.glyphName.indexOf(searchString) >= 0) { + return true; + } + if (item.unicodes[0] !== undefined) { + const char = String.fromCodePoint(item.unicodes[0]); + if (searchString === char) { + return true; + } + } + } + return false; +} + +function compare(a, b) { + // sort undefined at the end + if (a === b) { + return 0; + } else if (a === undefined) { + return 1; + } else if (b === undefined) { + return -1; + } else if (a < b) { + return -1; + } else { + return 1; + } +} +// END OF COPY + export class FontOverviewController { static async fromWebSocket() { const pathItems = window.location.pathname.split("/").slice(3); @@ -27,15 +205,39 @@ export class FontOverviewController { constructor(font) { this.fontController = new FontController(font); + + this.locationController = new ObservableController({ + fontLocationSourceMapped: {}, + }); + + this.glyphsListItemsController = new ObservableController({ + glyphsListItems: [], + }); + + this.glyphs = this.glyphsListItemsController.model.glyphsListItems; + + this.throttledUpdate = throttleCalls(() => this._updateGlyphOverview(), 50); } async start() { await this.fontController.initialize(); + this.fontSources = await this.fontController.getSources(); + this.fontAxesSourceSpace = mapAxesFromUserSpaceToSourceSpace( + this.fontController.axes.axes + ); + this.sortedSourceIdentifiers = sortedSourceIdentifiers( + this.fontSources, + this.fontAxesSourceSpace + ); + this.currentFontSourceIdentifier = this.sortedSourceIdentifiers[0]; + this.locationController.model.fontLocationSourceMapped = { + ...this.fontSources[this.currentFontSourceIdentifier]?.location, + }; // Note: a font may not have font sources therefore the ?-check. const sidebarContainer = document.querySelector("#sidebar-container"); const panelContainer = document.querySelector("#panel-container"); - const sidebarElement = html.div({}, [translate("Font Overview sidebar")]); + const sidebarElement = await this._getSidebarForGlyphOverview(); sidebarContainer.appendChild(sidebarElement); const panelElement = html.div( @@ -43,10 +245,140 @@ export class FontOverviewController { class: "font-overview-panel", id: "font-overview-panel", }, - [translate("Font Overview container")] + ["No glyphs found. Font is empty."] ); - panelContainer.appendChild(panelElement); + + this.glyphsListItemsController.addKeyListener( + "glyphsListItems", + this.throttledUpdate + ); + + // This is the inital load of the overview + await this._updateGlyphOverview(); + } + + async _getSidebarForGlyphOverview() { + const element = html.div({ class: "font-overview-sidebar" }); + + // font source selector + this.fontSourceInput = select( + { + id: "font-source-select", + style: "width: 100%;", + onchange: (event) => { + this.currentFontSourceIdentifier = event.target.value; + this.locationController.model.fontLocationSourceMapped = { + ...this.fontSources[this.currentFontSourceIdentifier].location, + }; + }, + }, + [] + ); + + this.fontSourceInput.innerHTML = ""; + + for (const fontSourceIdentifier of this.sortedSourceIdentifiers) { + const sourceName = this.fontSources[fontSourceIdentifier].name; + this.fontSourceInput.appendChild( + option( + { + value: fontSourceIdentifier, + selected: this.currentFontSourceIdentifier === fontSourceIdentifier, + }, + [sourceName] + ) + ); + } + + const fontSourceSelector = div( + { + class: "font-source-selector", + }, + [ + label( + { for: "font-source-select" }, + translate("sidebar.font-overview.font-source") + ), + this.fontSourceInput, + ] + ); + + // glyph search + this.glyphsSearch = new GlyphsSearchForOverview( + this.glyphsListItemsController, + "glyphsListItems" + ); + this.glyphsSearch.glyphMap = this.fontController.glyphMap; + + const glyphsSearch = html.div({ class: "glyph-search" }, [this.glyphsSearch]); + + element.appendChild(glyphsSearch); + element.appendChild(fontSourceSelector); + return element; + } + + async _updateGlyphOverview() { + const glyphs = this.glyphsListItemsController.model.glyphsListItems; + console.log("glyphs", glyphs); + const element = document.querySelector("#font-overview-panel"); + element.innerHTML = ""; + + if (!glyphs.length) { + return element; + } + + const sectionHeader = html.span({ class: "font-overview-section-header" }, [ + translate("Glyphs"), + ]); + element.appendChild(sectionHeader); + + // TODO: Handle sections, but for now only one with all glyphs or selectiojn of 'Search'. + const glyphCellsSection = html.div({ class: "glyph-cells-section" }); + const documentFragment = document.createDocumentFragment({ + class: "glyph-cells-wrapper", + }); + for (const { glyphName, unicodes } of glyphs) { + const glyphCellWrapper = html.div({ class: "glyph-cell-wrapper" }); + const glyphCell = new GlyphCell( + this.fontController, + glyphName, + unicodes, + this.locationController, + "fontLocationSourceMapped" + ); + glyphCell.ondblclick = () => this.handleDoubleClick(glyphName, unicodes); + // TODO: context menu + // glyphCell.addEventListener("contextmenu", (event) => + // this.handleContextMenu(event, glyphCell, item) + // ); + glyphCellWrapper.appendChild(glyphCell); + documentFragment.appendChild(glyphCellWrapper); + } + glyphCellsSection.appendChild(documentFragment); + element.appendChild(glyphCellsSection); + } + + async handleDoubleClick(glyphName, codePoints) { + const url = new URL(window.location); + url.pathname = url.pathname.replace("/fontoverview/", "/editor/"); + + const glyphLocations = {}; + glyphLocations[glyphName] = this.fontSources[this.currentFontSourceIdentifier] + ? this.fontSources[this.currentFontSourceIdentifier].location + : {}; // TODO: what happens if the glyph does not have the font location? + + const viewInfo = { + selectedGlyph: glyphName, + glyphLocations: glyphLocations, + }; + if (codePoints.length) { + viewInfo.text = String.fromCharCode(codePoints[0]); + } else { + viewInfo.text = `/${glyphName}`; + } + url.hash = dumpURLFragment(viewInfo); + window.open(url.toString()); } async messageFromServer(headline, msg) {