L’architecture technique interne de l’éditeur de texte d’Apple
(papereditor.app)- Paper fonctionne comme un éditeur
TextViewbasé sur TextKit 1 sur les plateformes Apple, et l’expérience d’édition est construite conjointement parNSTextStorage,NSTextContainer,NSLayoutManager,TextViewetScrollView NSAttributedStringreprésente le texte enrichi via une chaîne et des attributs par plage, et Paper sépare les méta-attributs de structure Markdown des attributs de style destinés à l’affichage- La mise à jour des styles varie selon l’ouverture du document, les modifications de texte et les changements de réglages ; les attributs qui affectent la mise en page sont regroupés en transaction, tandis que les attributs de décoration sont mis à jour sans recalcul de layout
- Les performances de saisie sont améliorées en évitant de réanalyser tout le paragraphe selon que l’édition se fait ou non autour de caractères spéciaux Markdown, mais des structures comme les blocs de code, qui peuvent affecter le style de tout le document, nécessitent un traitement d’exception
- Le copier-coller fonctionne au-dessus des UTI et des multiples représentations de données du pasteboard ; des UTI semi-publics non définis par Apple, comme Markdown, peuvent provoquer des incohérences de priorité entre applications, des conversions RTF et des problèmes d’UX
La pile d’édition TextView d’Apple
- Paper est construit sur l’ancien framework TextKit 1, mais les mêmes concepts et abstractions existent aussi dans TextKit 2, parfois sous de meilleures API
NSTextViewetUITextViewdiffèrent, mais leurs API sont suffisamment proches pour être traitées comme une même familleTextViewTextViewest un gros composant qui se complexifie à chaque version de l’OS, et l’application TextEdit est composée presque entièrement d’un seulTextView-
Les couches fondamentales de l’éditeur
NSTextStorage- Stocke la chaîne de texte source
- Stocke les attributs sous forme de paires chaîne-valeur attachées à des plages de texte
- Émet des événements lors des changements de texte et d’attributs
NSTextContainer- Définit la forme et la taille de la zone dans laquelle les glyphes seront placés
- Elle est le plus souvent rectangulaire, mais peut aussi avoir une forme arbitraire
NSLayoutManager- Calcule la taille et l’espacement des glyphes à partir des plages d’attributs de
NSTextStorage - Extrait les glyphes vectoriels depuis les polices et convertit un caractère en un ou plusieurs glyphes
- Calcule le début et la fin de chaque ligne, le nombre de lignes et la hauteur totale du texte
- Calcule la taille et l’espacement des glyphes à partir des plages d’attributs de
TextView- Dessine le layout des glyphes produit par
NSLayoutManager - Synchronise la hauteur de la vue avec la hauteur actuelle du layout du texte
- Gère la saisie, la sélection, le curseur, les attributs de saisie,
textContainerInset, la dictée, le copier-coller, la correction orthographique, etc.
- Dessine le layout des glyphes produit par
ScrollView- Gère la partie visible de
TextView, le défilement, les barres de défilement et le zoom - Peut avoir son propre
contentInset, distinct dutextContainerInsetdeTextView
- Gère la partie visible de
-
Différences de défilement entre AppKit et UIKit
- Dans AppKit,
NSScrollViewcontientNSClipViewet deuxNSScroller, etNSClipViewhébergeNSTextView, de sorte que plusieurs classes participent ensemble au comportement de défilement - Dans UIKit,
UITextViewhérite deUIScrollViewet embarque donc directement la logique de défilement - Si le curseur se déplace hors de la zone visible limitée par
contentInset,UITextViewfait défiler automatiquement - Sur iOS, si le curseur passe derrière le clavier dans un éditeur, le fait qu’il défile jusqu’à la ligne suivante vient du
contentInsetinférieur qui est ajusté dynamiquement à la hauteur actuelle du clavier
- Dans AppKit,
NSAttributedString et consultation des attributs
NSAttributedStringconstitue la base de l’édition de texte enrichi dans les frameworks Apple et se compose de deux éléments- Une chaîne de texte brut
- Des paires attribut chaîne-valeur attachées à des plages de texte dans cette chaîne
- Les attributs servent surtout au style, mais les développeurs peuvent aussi attacher n’importe quelles paires chaîne-valeur utiles à leurs besoins
NSRangeest composé delocationetlength, etNSMakeRange(10, 5)désigne une plage de 5 caractères commençant à la position 10, donc les positions 10 à 14- Si le même attribut est défini sur plusieurs plages au même endroit, la dernière plage appliquée l’emporte
-
Vérifier l’attribut et la plage à une position donnée
- La valeur d’un attribut à une position donnée se consulte avec
attribute:atIndex:effectiveRange: - Si la valeur renvoyée est
nil, l’attribut n’existe pas à cet endroit ; sinon, il s’agit d’une valeur d’attribut, comme par exemple un objetNSFontouUIFont - Si l’on passe un pointeur
NSRangeen dernier argument, on peut récupérer la plage continue où cet attribut a la même valeur, ou une plage vide où l’attribut n’est pas présent - D’après la documentation,
effectiveRangen’est pas forcément la plage maximale et dépend de l’implémentation - Si l’on a besoin d’une plage maximale garantie, il faut utiliser
attribute:atIndex:longestEffectiveRange:inRange: - Il existe aussi une méthode qui permet d’obtenir l’ensemble des attributs à une position donnée ainsi que la plage sur laquelle cette combinaison se prolonge
- Pour parcourir un attribut dans une plage, on peut utiliser
enumerateAttribute:inRange:options:usingBlock:
- La valeur d’un attribut à une position donnée se consulte avec
Le pipeline de stylage Markdown de Paper
- Le stylage dans Paper consiste à appliquer aux plages de texte des attributs spéciaux définis par les frameworks
- Avant le stylage, Paper utilise aussi des méta-attributs personnalisés pour identifier la structure du texte
-
Méta-attributs et attributs de style
- Méta-attributs
- Définis par le parseur Markdown pour identifier les différentes parties de la syntaxe Markdown
- Ce sont des paires chaîne-valeur personnalisées purement sémantiques
- Ils n’affectent pas l’apparence visuelle du texte
- On peut les voir comme une version simplifiée d’un AST pour Markdown
- Attributs de style
- Ce sont les attributs visuels appliqués par-dessus les parties marquées par les méta-attributs
- Ce sont des paires chaîne-valeur intégrées, définies par AppKit et UIKit
- Méta-attributs
-
Événements qui déclenchent la mise à jour des attributs
- Les attributs sont synchronisés avec le texte Markdown de
NSTextStorageet avec les changements de réglages qui influencent le texte - À l’ouverture d’un document, les méta-attributs et les attributs de style sont intégralement mis à jour
- Lors d’une modification de texte, les méta-attributs et les attributs de style sont partiellement mis à jour sur les zones affectées
- Lors d’un changement de réglages, les méta-attributs restent inchangés et seuls les attributs de style sont entièrement recalculés
- Les attributs sont synchronisés avec le texte Markdown de
-
Ordre de mise à jour et coût du layout
- On commence par ouvrir une transaction d’édition de texte pour regrouper les changements d’attributs
- Si l’on modifie les attributs hors transaction, chaque changement déclenche un recalcul coûteux du layout par
NSLayoutManager - La chaîne Markdown est analysée puis découpée en fragments marqués par des méta-attributs ; cette étape est sautée lors d’un changement de réglages, car la structure Markdown ne change pas
- Les attributs visuels susceptibles d’affecter la position ou la taille des glyphes sont mis à jour en premier, et
NSParagraphStyleest traité comme l’attribut le plus important puisqu’il influe sur le layout des lignes et des paragraphes - Une fois la transaction terminée, on met à jour les attributs de décoration qui n’affectent pas le layout
- En terminologie Apple, les attributs de décoration correspondent à des attributs de rendu ; ils résident dans
NSLayoutManageret non dansNSTextStorage, et ne prennent donc pas en compte les transactions
-
Attributs de saisie
- Les attributs de saisie sont liés, quand la sélection est vide, aux attributs situés juste avant le curseur, et quand elle ne l’est pas, aux attributs du début de la sélection
- Lorsqu’un caractère est saisi, les attributs de saisie sont automatiquement appliqués au texte nouvellement inséré
- Dans un éditeur Markdown, ils sont moins importants puisque le style dérive de la syntaxe Markdown
- Dans un éditeur de texte enrichi, ils sont plus importants car le style reste attaché au curseur jusqu’à ce qu’il soit désactivé ou déplacé ailleurs
- Le Preview Mode de Paper offre, même dans l’éditeur Markdown, une expérience de texte enrichi avec des attributs de saisie activables
Performances de saisie et usage des méta-attributs
- La séparation entre attributs méta, de layout et de décoration aide à traiter rapidement certaines modifications dans l’éditeur
- Le passage du mode clair au mode sombre est très rapide, car seuls les attributs de décoration sont mis à jour et cela ne déclenche aucun layout
- Un changement de réglage comme la taille du texte nécessite toujours un recalcul de layout sur l’ensemble du document, mais reste plus rapide qu’une réanalyse complète de toute la structure Markdown
-
Optimisation de la saisie Markdown
- Le facteur de performance le plus important dans un éditeur de texte est la vitesse de saisie
- Avec Markdown, une simple modification peut affecter le style de tout un paragraphe
- Réanalyser et restyler tout le paragraphe à chaque frappe est la solution la plus exacte sur le plan technique, mais elle peut ralentir l’édition dans les longs paragraphes
- Paper utilise un algorithme qui choisit son comportement en fonction du prochain caractère saisi et des caractères autour
- Si l’on saisit un symbole spécial Markdown, ou si des symboles spéciaux se trouvent près de la position d’édition, tout le paragraphe est mis à jour
- Sinon, Paper s’appuie sur les attributs de saisie pour accélérer l’éditeur dans la plupart des cas
-
Exception des blocs de code et cache des objets-valeurs
- Les blocs de code sont la seule structure Markdown multi-paragraphe dans Paper
- Dans un document qui contient des blocs de code, une seule frappe peut affecter le style de l’ensemble du document
- Paper ignore le traitement des blocs de code au-delà d’un certain nombre de caractères afin de conserver un éditeur rapide pour la majorité des utilisateurs qui ne s’intéressent pas au code
- Ce choix rend aussi Paper plus utile pour les utilisateurs proches du monde développeur
- Les objets-valeurs d’attribut complexes sont réalloués à chaque frappe, alors qu’ils ne changent pas tant que les réglages influençant le texte restent identiques ; ils sont donc mis en cache pour être réutilisés
NSFont/UIFontNSColor/UIColorNSParagraphStyle
-
Fonctionnalités qui utilisent les méta-attributs
- Raccourcis de formatage
- Pour activer/désactiver un style, il faut connaître le style existant du texte Markdown sélectionné
- Si la sélection englobe entièrement le même style, ce style est supprimé
- Si la sélection ne contient pas ce style, il est ajouté
- Si la sélection ne recouvre qu’une partie de ce style, le style est déplacé pour correspondre à la sélection
- Les styles incompatibles qui définissent un type de paragraphe, comme heading et blockquote, ne peuvent pas être mélangés et doivent être supprimés avant d’ajouter un nouveau style
- Navigation entre chapitres
- Les méta-attributs servent à trouver le heading par rapport à la position du curseur lorsqu’on se déplace vers la limite du chapitre précédent ou suivant
- Outline
- Tous les headings sont parcourus, et lorsqu’on clique sur un élément de l’outline, le curseur se déplace vers le chapitre correspondant
- Réorganisation des chapitres
- Les chapitres peuvent être réordonnés depuis l’outline
- Conversion de format
- Pour convertir Markdown en RTF, HTML ou DOCX, il faut connaître la structure du texte
- Comme Paper n’inclut pas de bibliothèque externe, il parcourt le modèle de texte déjà analysé afin de produire le format de sortie
- Raccourcis de formatage
Mathématiques du conteneur de texte et comportement de sélection
-
Calcul de la longueur de ligne et des marges
- La règle centrale du conteneur de texte consiste à conserver une longueur de ligne préférée et à répartir l’espace restant dans des insets de part et d’autre
- Il existe des cas où il faut simuler une symétrie, par exemple lorsque les tags de heading sont placés en dehors du flux normal du texte
- Dans ce cas, le conteneur de texte est déplacé vers la gauche, et le paragraphe est indenté via
NSParagraphStyle - S’il y a suffisamment d’espace, les marges restent visuellement proches de la symétrie
- S’il n’y a plus assez d’espace, on casse cette symétrie pour préserver la longueur de ligne voulue, mais seulement tant qu’il reste un padding à droite
- Dès qu’il n’y a plus de padding disponible, une marge minimale passe avant la largeur de ligne préférée
- Une combinaison de
minetmaxpermet d’implémenter des marges qui se replient progressivement
-
Ancre de sélection
- Une sélection de texte possède toujours un point d’ancrage
- Sur Mac, quand on clique puis qu’on glisse, le point cliqué sert de référence, et si l’on repasse au-delà, le sens d’agrandissement ou de réduction de la sélection s’inverse
- Sur iOS, quand on déplace une poignée de sélection, l’autre extrémité devient l’ancre, et la logique est la même dans l’autre sens
- La même logique s’applique quand on étend la sélection au clavier
Option+flèche gauche/droite déplace par mot, et avecShiften plus, la sélection s’étend mot par mot- Même si l’on commence par une sélection à la souris puis qu’on continue à l’étendre ou la réduire au clavier, le point du premier clic reste l’ancre
-
Affinity de sélection
- L’affinity de sélection détermine, à une limite de retour à la ligne, si le point d’insertion apparaît après le dernier caractère de la ligne précédente ou avant le premier caractère de la ligne suivante
- Quand on déplace le curseur avec les flèches, il change simplement de ligne autour de l’espace de retour à la ligne
- Avec un raccourci pour aller en fin de ligne, le curseur peut rester sur la même ligne et se placer à droite de l’espace de retour à la ligne
TextViewapplique un comportement similaire d’affinity de sélection dans d’autres situations également
-
Undo coalescing
- L’undo coalescing est l’algorithme qui décide à quel moment le texte saisi doit être regroupé en une seule action annulable
TextViewsemble utiliser un algorithme simple qui regroupe la saisie tant qu’elle n’est pas interrompue par une autre action- Même si l’on fait une pause puis qu’on reprend la saisie, cela reste regroupé dans une seule annulation, et un retour à la ligne ne casse pas non plus le coalescing
- Sublime préfère regrouper les mots saisis séparément en unités d’annulation, et peut même découper un long mot en unités plus petites
- Il n’existe pas de réponse universelle, et chaque éditeur peut adopter sa propre stratégie
UTI, pasteboard et échange de données entre applications
-
Uniform Type Identifiers
- Les UTI forment un système hiérarchique où les types de données peuvent
conform tod’autres types parents - Les types
public.*sont définis par Apple et identifient des formats largement utilisés commepublic.htmloupublic.jpeg - Les développeurs peuvent créer leurs propres identifiants selon un schéma de noms de domaine inversés pour éviter les conflits
- L’avantage du système hiérarchique est qu’une application capable de traiter tous les formats texte peut simplement déclarer
public.textau lieu d’énumérer chaque format individuellement - Paper déclare pouvoir ouvrir tous les fichiers texte et peut donc ouvrir des formats texte comme
.htmlou.rtf, même sans highlighting - Comme le concept de fichier est cross-platform, ce sont en pratique les extensions de fichier qui servent d’identifiants
- Pour des raisons de compatibilité, tout UTI peut définir une ou plusieurs extensions de fichier associées
- Markdown est très répandu, mais comme Apple ne le définit pas, cela crée des cas délicats autour des UTI semi-publics
- Les UTI forment un système hiérarchique où les types de données peuvent
-
Pasteboard et représentations multiples
- Le pasteboard d’Apple est un dictionnaire qui associe des UTI à du texte sérialisé ou à des données binaires
- Le Clipboard Viewer des Xcode Additional Tools permet d’inspecter en temps réel le contenu du pasteboard
- Une seule opération de copie écrit simultanément plusieurs représentations des mêmes données
- Certaines applications utilisent aussi des identifiants historiques non UTI comme
NeXT Rich Text Format v1.0 pasteboard typepour la rétrocompatibilité - En général, un éditeur choisit le format le plus riche qu’il sait traiter
Paste and Match StyleouPaste as Plain Textdans un éditeur de texte enrichi forcent l’utilisation du format texte brut du pasteboard- Le style appliqué au texte collé provient généralement des attributs de saisie
- Le glisser-déposer fonctionne lui aussi sur la base d’un autre pasteboard
- Le copier-coller standard utilise le general pasteboard, tandis que des interactions personnalisées entre applications peuvent passer par un pasteboard dédié
-
RTF et
NSAttributedString- Le RTF est essentiellement une forme sérialisée de
NSAttributedString, et à l’inverseNSAttributedStringest l’interface de programmation du RTF - Comme
TextViewfonctionne surNSTextStorage, qui est une sous-classe deNSAttributedString, il est nativement compatible avec le pasteboard - Il est donc possible de copier le contenu dans le pasteboard sans code supplémentaire
- Le RTF est essentiellement une forme sérialisée de
-
Problèmes inter-applications autour de l’UTI Markdown
- Même entre éditeurs Markdown, le copier-coller peut poser problème alors que les deux applications implémentent un protocole standard
- Si le premier éditeur exporte Markdown comme
public.textet une représentation enrichie commepublic.rtf, le second ne peut pas savoir qu’il s’agit nativement de Markdown et peut choisirpublic.rtf - Dans ce cas, une double conversion inutile se produit, et comme chaque application convertit différemment entre Markdown et RTF, de petits problèmes de formatage comme des sauts de ligne supplémentaires peuvent apparaître
- Les différences fondamentales de style entre Markdown et RTF peuvent aussi devenir visibles
- Pour que cela fonctionne naturellement, les deux applications doivent exporter l’UTI
net.daringfireball.markdownet le faire passer avantpublic.rtf - Paper a essayé d’exporter l’UTI Markdown, mais il semblait que d’autres applications ne lui donnaient pas la priorité sur le texte enrichi
- Pages donne bien la priorité à
net.daringfireball.markdownsurpublic.rtf, mais semble insérer la chaîne Markdown source telle quelle au lieu de la convertir en texte enrichi - C’est pour cette raison que Paper a supprimé l’UTI Markdown
-
Pourquoi Paper fournit du RTF, et le partage iOS
- Paper fournit du RTF pour que le copier-coller fonctionne naturellement avec les éditeurs de texte enrichi
- En bon citoyen de l’OS, une application doit fournir plusieurs représentations du contenu copié afin que l’application réceptrice puisse choisir le format le plus riche qu’elle sait gérer
- Si l’on copie du texte Markdown dans Paper puis qu’on le colle dans Mail, le résultat n’est pas une variante Markdown mais un texte enrichi bien mis en forme
- La fonction Partager d’iOS ressemble au copier-coller, mais avec une couche d’interface utilisateur en plus
- Dans le partage iOS, l’application exporte des données dans plusieurs formats et l’application réceptrice choisit celui qu’elle veut
- Contrairement au pasteboard, apparaître dans la liste d’applications en haut de l’interface de partage iOS exige qu’une application fournisse explicitement une share extension avec sa propre UI
1 commentaires
Avis sur Hacker News
Vraiment excellent. Désormais, pour découvrir TextKit, je pense que je lirai cet article avant https://www.objc.io/issues/5-ios7/getting-to-know-textkit/.
C’est le nouveau moteur de mise en page utilisé par défaut dans les champs de texte d’iOS et de MacOS, et il y a de fortes chances qu’il devienne à l’avenir la seule option.
L’app TextEdit est presque entièrement composée d’un unique TextView.
Côté Windows, son équivalent serait WordPad, basé sur le contrôle RichEdit. Autre point intéressant : RTF est en fait le format de sérialisation de NSAttributedString.
C’est pareil pour le contrôle RichEdit de Windows, et il semble même que l’implémentation Windows soit arrivée en premier : https://en.wikipedia.org/wiki/Rich_Text_Format
La partie sur le traitement des attributs décoratifs en dehors de la transaction d’édition est un peu déroutante. Il y avait l’explication selon laquelle « ils sont dans NSLayoutManager lui-même et non dans NSTextStorage, donc ils n’ont même pas connaissance des transactions », mais les attributs décoratifs comme la couleur sont généralement dans NSTextStorage.
Est-ce que l’auteur veut dire que la couleur appliquée aux caractères Markdown est gérée via la prise en charge des attributs temporaires de NSLayoutManager ? C’est une fonctionnalité qu’on utilise habituellement pour colorer les mots contenant des fautes d’orthographe, et je me demande quel est son but ici.
L’avantage des attributs temporaires est que les changements sont beaucoup plus rapides que le déclenchement d’une transaction complète sur NSTextStorage. Par exemple, dans Paper, le passage du mode clair au mode sombre est très rapide.
Vraiment un très bon article, et il tombe à point nommé pour moi, qui suis justement en train de travailler avec NSTextView.
Je me demande comment l’auteur a obtenu les informations nécessaires pour écrire ce genre d’article. Du code d’autres personnes, une expérience douloureuse, developer.apple.com ?
À l’ère des documents DOM, même en regardant Notion ou GitBook, dès qu’il s’agit de faire des choses un peu magiques avec l’analyse et la manipulation de texte, on revient souvent aux chaînes attribuées.
C’est une architecture vraiment élégante, et je ne comprends pas pourquoi elle est si peu connue. L’article est aussi incroyablement bon.
Pour avoir déjà tenté autrefois de créer un éditeur de texte entièrement à partir de zéro, je pense qu’un article comme celui-ci aurait été une ressource extraordinaire.
J’utilise vraiment beaucoup cette app. Elle a remplacé toutes mes autres apps Markdown, y compris Obsidian et iA Writer.
J’aimerais qu’il existe ce genre de documentation pour davantage de composants iOS.
En 2024, je suis quand même content de voir que quelqu’un fait encore du Cocoa.
En tant que développeur Android de longue date, j’ai trouvé intéressant de voir qu’Apple aborde les choses de manière un peu différente et plus réfléchie.
Sur Android, par défaut, tout ce qui concerne la mise en page et le rendu est géré par la classe Layout et ses sous-classes, tandis que TextView implémente une partie de la logique d’édition et de sélection. La seule différence entre EditText et TextView est qu’EditText active les fonctionnalités d’édition déjà présentes dans TextView.
Le problème de cette approche assez monolithique et de ses API limitées, c’est qu’on se retrouve bloqué dès qu’on veut contrôler plus finement le rendu du texte dans une app. Par exemple, il est impossible d’accéder aux glyphes individuels une fois la mise en page terminée.