2 points par GN⁺ 2024-03-06 | 1 commentaires | Partager sur WhatsApp
  • Paper fonctionne comme un éditeur TextView basé sur TextKit 1 sur les plateformes Apple, et l’expérience d’édition est construite conjointement par NSTextStorage, NSTextContainer, NSLayoutManager, TextView et ScrollView
  • NSAttributedString repré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
  • NSTextView et UITextView diffèrent, mais leurs API sont suffisamment proches pour être traitées comme une même famille TextView
  • TextView est un gros composant qui se complexifie à chaque version de l’OS, et l’application TextEdit est composée presque entièrement d’un seul TextView
  • 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
    • 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.
    • 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 du textContainerInset de TextView
  • Différences de défilement entre AppKit et UIKit

    • Dans AppKit, NSScrollView contient NSClipView et deux NSScroller, et NSClipView héberge NSTextView, de sorte que plusieurs classes participent ensemble au comportement de défilement
    • Dans UIKit, UITextView hérite de UIScrollView et embarque donc directement la logique de défilement
    • Si le curseur se déplace hors de la zone visible limitée par contentInset, UITextView fait 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 contentInset inférieur qui est ajusté dynamiquement à la hauteur actuelle du clavier

NSAttributedString et consultation des attributs

  • NSAttributedString constitue 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
  • NSRange est composé de location et length, et NSMakeRange(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 objet NSFont ou UIFont
    • Si l’on passe un pointeur NSRange en 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, effectiveRange n’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:

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
  • Événements qui déclenchent la mise à jour des attributs

    • Les attributs sont synchronisés avec le texte Markdown de NSTextStorage et 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
  • 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 NSParagraphStyle est 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 NSLayoutManager et non dans NSTextStorage, 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/UIFont
      • NSColor/UIColor
      • NSParagraphStyle
  • 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

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 min et max permet 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 avec Shift en 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
    • TextView applique 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
    • TextView semble 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 to d’autres types parents
    • Les types public.* sont définis par Apple et identifient des formats largement utilisés comme public.html ou public.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.text au lieu d’énumérer chaque format individuellement
    • Paper déclare pouvoir ouvrir tous les fichiers texte et peut donc ouvrir des formats texte comme .html ou .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
  • 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 type pour la rétrocompatibilité
    • En général, un éditeur choisit le format le plus riche qu’il sait traiter
    • Paste and Match Style ou Paste as Plain Text dans 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’inverse NSAttributedString est l’interface de programmation du RTF
    • Comme TextView fonctionne sur NSTextStorage, qui est une sous-classe de NSAttributedString, il est nativement compatible avec le pasteboard
    • Il est donc possible de copier le contenu dans le pasteboard sans code supplémentaire
  • 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.text et une représentation enrichie comme public.rtf, le second ne peut pas savoir qu’il s’agit nativement de Markdown et peut choisir public.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.markdown et le faire passer avant public.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.markdown sur public.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

 
GN⁺ 2024-03-06
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/.

    • Sur iOS 15.0 et versions ultérieures ou MacOS Monterey et versions ultérieures, il faut regarder TextKit 2. Voir https://developer.apple.com/documentation/appkit/textkit/usi...
      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.

    • Exact. NSTextStorage peut aussi contenir des attributs du même type que les attributs temporaires. Dans TextKit 2, on les appelle des attributs de rendu.
      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 ?

    • Je travaille sur Paper depuis 9 ans, ce qui m’a laissé beaucoup de temps pour étudier le fonctionnement de toute l’architecture.
  • À 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.

    • S’il y a des exemples, j’aimerais en apprendre davantage.
    • Je me demande s’il existe des composants d’éditeur de texte riche web qui utilisent des chaînes attribuées. J’en ai assez du DOM.
  • 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.

    • Le nom Paper est certes approprié, mais si ma mémoire est bonne, il existe au moins deux autres apps iOS qui s’appellent « Paper ».
    • Intéressant. Ce sont justement les deux apps que j’utilise principalement pour écrire ; il faudra que je jette un œil à celle-ci aussi.
  • 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.