A estrutura técnica interna do editor de texto da Apple
(papereditor.app)- O Paper funciona como um editor
TextViewbaseado no TextKit 1 nas plataformas Apple, eNSTextStorage,NSTextContainer,NSLayoutManager,TextVieweScrollViewcompõem juntos a experiência de edição NSAttributedStringrepresenta texto rico com a string e atributos (attributes) por intervalo, e o Paper separa metaatributos para a estrutura Markdown dos atributos de estilo para exibição na tela- A atualização de estilo varia conforme a abertura do documento, alterações no texto e mudanças de configuração; atributos que afetam o layout são agrupados em transações e atributos decorativos são atualizados sem recálculo de layout
- O desempenho de entrada é melhorado evitando reparsear o parágrafo inteiro com base em se a edição ocorre ao redor de caracteres especiais do Markdown, mas estruturas como blocos de código, que podem afetar o estilo do documento inteiro, exigem tratamento de exceção
- Copiar e colar funciona sobre UTI e múltiplas representações de dados no pasteboard, e UTIs semipúblicos não definidos pela Apple, como o de Markdown, podem causar incompatibilidade de prioridade entre apps, conversão para RTF e problemas de UX
Stack de edição do TextView da Apple
- O Paper foi construído sobre o antigo framework TextKit 1, e os mesmos conceitos e abstrações continuam existindo no TextKit 2 ou sob APIs melhores
NSTextVieweUITextViewtêm diferenças, mas as APIs são suficientemente parecidas para serem tratadas como uma única famíliaTextViewTextViewé um grande componente que fica mais complexo a cada release do sistema operacional, e o app TextEdit é composto quase inteiramente por um únicoTextView-
Camadas centrais que formam o editor
NSTextStorage- Armazena a string de texto original
- Armazena os atributos de pares string-valor anexados a intervalos de texto
- Dispara eventos de alteração de texto e atributos
NSTextContainer- Define a forma e o tamanho da área em que os glifos serão colocados
- Na maioria das vezes é retangular, mas formas arbitrárias também são possíveis
NSLayoutManager- Calcula tamanho e espaçamento dos glifos observando os intervalos de atributos do
NSTextStorage - Extrai glifos vetoriais das fontes e converte um caractere em um ou mais glifos
- Calcula o início e o fim de cada linha, o número de linhas e a altura total do texto
- Calcula tamanho e espaçamento dos glifos observando os intervalos de atributos do
TextView- Desenha o layout de glifos criado pelo
NSLayoutManager - Sincroniza a altura da view com a altura atual do layout do texto
- Gerencia entrada, seleção, cursor, atributos de digitação,
textContainerInset, ditado, copiar e colar, corretor ortográfico etc.
- Desenha o layout de glifos criado pelo
ScrollView- Gerencia a parte visível do
TextView, rolagem, barras de rolagem e zoom - Pode ter
contentInsetseparado dotextContainerInsetdoTextView
- Gerencia a parte visível do
-
Diferenças de rolagem entre AppKit e UIKit
- No AppKit,
NSScrollViewincluiNSClipViewe doisNSScroller, e oNSClipViewcontém oNSTextView, então várias classes constroem juntas o comportamento de rolagem - No UIKit,
UITextViewherda deUIScrollView, então inclui diretamente a lógica de rolagem - Se o cursor se mover para fora da área visível limitada por
contentInset,UITextViewfaz rolagem automática - Em editores iOS, quando o cursor vai para trás do teclado e a tela rola para a linha seguinte, isso acontece porque o
contentInsetinferior é ajustado dinamicamente para a altura atual do teclado
- No AppKit,
NSAttributedString e consulta de atributos
NSAttributedStringé a base da edição de texto rico nos frameworks da Apple e é composto por dois elementos- uma string de texto simples
- pares string-valor de atributos anexados a intervalos de texto dentro da string
- Os atributos são usados principalmente para estilização, mas o desenvolvedor também pode anexar quaisquer pares string-valor de que precise
NSRangeé composto porlocationelength, eNSMakeRange(10, 5)significa um intervalo de 5 caracteres começando na posição 10, ou seja, as posições 10 a 14- Se o mesmo atributo for definido em vários intervalos na mesma posição, o intervalo aplicado por último tem prioridade
-
Verificando atributos e intervalos em uma posição específica
- O valor de um atributo em uma posição específica é verificado com
attribute:atIndex:effectiveRange: - Se o valor retornado for
nil, esse atributo não existe ali; se houver retorno, será o valor do atributo, como um objetoNSFontouUIFontno exemplo - Se um ponteiro
NSRangefor passado no último argumento, é possível receber o intervalo contínuo em que o atributo tem o mesmo valor ou um intervalo vazio sem esse atributo - Na documentação,
effectiveRangenão é necessariamente o maior intervalo possível e depende da implementação - Se for necessário o maior intervalo garantido, deve-se usar
attribute:atIndex:longestEffectiveRange:inRange: - Também há método para obter a combinação de todos os atributos em uma posição específica e o intervalo em que essa combinação continua
- Para iterar atributos dentro de um intervalo, pode-se usar
enumerateAttribute:inRange:options:usingBlock:
- O valor de um atributo em uma posição específica é verificado com
Pipeline de estilização Markdown do Paper
- A estilização do Paper funciona aplicando atributos especiais definidos pelo framework a intervalos de texto
- Antes da estilização, ele também usa metaatributos customizados para identificar a estrutura do texto
-
Metaatributos e atributos de estilo
- Metaatributos
- São definidos pelo parser de Markdown para identificar partes individuais da sintaxe Markdown
- São pares string-valor customizados puramente para informação semântica
- Não afetam a aparência visual do texto
- Podem ser vistos como uma AST simplificada para Markdown
- Atributos de estilo
- São atributos visuais aplicados sobre as partes marcadas pelos metaatributos
- São pares string-valor embutidos definidos por AppKit e UIKit
- Metaatributos
-
Eventos que disparam atualização de atributos
- Os atributos são sincronizados com o texto Markdown do
NSTextStoragee com mudanças de configuração que afetam o texto - Na abertura do documento, metaatributos e atributos de estilo são atualizados por completo
- Em alterações de texto, metaatributos e atributos de estilo são atualizados parcialmente nas áreas afetadas
- Em mudanças de configuração, os metaatributos não são alterados e apenas os atributos de estilo são atualizados por completo
- Os atributos são sincronizados com o texto Markdown do
-
Ordem de atualização e custo de layout
- Primeiro, inicia-se uma transação de edição de texto para agrupar as mudanças de atributos
- Se os atributos forem alterados sem transação, haverá um recálculo caro de layout do
NSLayoutManagera cada mudança - A string Markdown é parseada e dividida em fragmentos marcados com metaatributos; em mudanças de configuração, essa etapa é pulada porque a estrutura Markdown não muda
- Primeiro são atualizados os atributos visuais que podem afetar a posição ou o tamanho dos glifos, e
NSParagraphStyleé tratado como o atributo mais importante por afetar o layout de linhas e parágrafos - Depois de encerrar a transação, os atributos decorativos que não afetam o layout são atualizados
- Atributos decorativos correspondem aos atributos de renderização na terminologia da Apple e existem no
NSLayoutManager, não noNSTextStorage, então não reconhecem transações
-
Atributos de digitação
- Atributos de digitação (typing attributes) ficam ligados aos atributos da posição antes do cursor quando a seleção está vazia, e aos atributos do início da seleção quando ela não está vazia
- Ao digitar um caractere, os atributos de digitação são aplicados automaticamente ao texto recém-inserido
- Em um editor Markdown, isso é menos importante porque o estilo deriva da sintaxe Markdown
- Em um editor de texto rico, isso é mais importante porque o estilo permanece no cursor até que seja desativado ou o cursor seja movido para outro lugar
- O Preview Mode do Paper oferece, mesmo dentro de um editor Markdown, uma experiência de edição de texto rico em que atributos de digitação alternáveis são destacados
Desempenho de entrada e uso de metaatributos
- A separação entre atributos meta, de layout e decorativos ajuda a processar rapidamente algumas mudanças no editor
- Alternar entre modo claro e escuro atualiza apenas atributos decorativos, então é muito rápido porque não dispara layout
- Mudanças de configuração como ajuste do tamanho do texto exigem recálculo de layout do documento inteiro, mas ainda são mais rápidas do que reparsear toda a estrutura Markdown
-
Otimização da entrada em Markdown
- O fator de desempenho mais importante de um editor de texto é a velocidade de digitação
- Pela natureza do Markdown, uma única alteração de texto pode afetar o estilo do parágrafo inteiro
- Reparsear e reaplicar estilo ao parágrafo inteiro a cada tecla é tecnicamente o mais correto, mas pode deixar a edição lenta em parágrafos longos
- O Paper usa um algoritmo que decide o comportamento olhando o próximo caractere digitado e os caracteres ao redor
- Se um símbolo especial de Markdown for digitado ou se houver símbolos especiais perto da posição de edição, o parágrafo inteiro é atualizado
- Caso contrário, ele depende dos atributos de digitação para acelerar o editor na maioria das situações de entrada
-
Exceção dos blocos de código e cache de objetos de valor
- Blocos de código são a única estrutura Markdown de múltiplos parágrafos no Paper
- Em documentos com blocos de código, uma única tecla pode afetar o estilo do documento inteiro
- No Paper, em documentos acima de certa quantidade de caracteres, o processamento de blocos de código é ignorado para manter um editor rápido para a maioria dos usuários que não se importam com código
- Essa escolha também torna o Paper mais útil até para usuários próximos do perfil de desenvolvedor
- Objetos de valor complexos de atributos são realocados a cada tecla, mas como não mudam enquanto as configurações que afetam o texto não mudarem, são armazenados em cache e reutilizados
NSFont/UIFontNSColor/UIColorNSParagraphStyle
-
Recursos que usam metaatributos
- Atalhos de formatação
- Para alternar a formatação, é preciso conhecer o estilo existente do texto Markdown selecionado
- Se a seleção envolver completamente o mesmo estilo, esse estilo é removido
- Se a seleção não tiver o mesmo estilo, o estilo é adicionado
- Se a seleção envolver apenas parte do mesmo estilo, o estilo é movido para a seleção
- Estilos conflitantes que definem o tipo de parágrafo, como heading e blockquote, não podem ser misturados e precisam ser removidos antes de adicionar um novo estilo
- Navegação por capítulos
- Ao mover para o limite do capítulo anterior ou seguinte, os metaatributos são usados para encontrar o heading com base na posição do cursor
- Outline
- Todos os headings são percorridos, e ao tocar em um item do outline o cursor vai para o capítulo correspondente
- Reorganização de capítulos
- No outline, os capítulos podem ser reordenados
- Conversão de formato
- Para converter Markdown em RTF, HTML e DOCX, é necessário conhecer a estrutura do texto
- Como o Paper não inclui bibliotecas externas, ele percorre o modelo de texto previamente parseado e gera o formato de saída
- Atalhos de formatação
Matemática do contêiner de texto e comportamento de seleção
-
Cálculo do comprimento da linha e margens
- A regra central do contêiner de texto é manter o comprimento de linha preferido e dividir o espaço restante em insets dos dois lados
- Há situações em que é preciso simular simetria, como quando tags de heading são colocadas fora do fluxo normal do texto
- Nesse caso, o contêiner de texto é deslocado para a esquerda e o parágrafo é indentado com
NSParagraphStyle - Se houver espaço suficiente, as margens são mantidas visualmente próximas da simetria
- Se não houver espaço extra, a simetria é quebrada em favor de manter o comprimento de linha definido, mas só até que ainda reste padding à direita
- Se não sobrar padding, a margem mínima tem prioridade sobre a largura de linha preferida
- A combinação de
minemaxpermite implementar margens que se retraem gradualmente
-
Âncora de seleção
- Sempre existe um ponto de âncora em uma seleção de texto
- No Mac, ao clicar e arrastar, o ponto clicado vira a referência; ao ultrapassá-lo, a direção de aumento ou redução da seleção se inverte
- No iOS, ao arrastar uma das alças de seleção, a outra vira a âncora, e o mesmo vale no sentido oposto
- A mesma lógica se aplica ao expandir a seleção pelo teclado
- Com
Option+seta para a esquerda ou direita, move-se entre limites de palavras, e comShiftjunto faz-se seleção por palavra - Mesmo após selecionar com clique e arrasto, se a seleção continuar a ser expandida ou reduzida pelo teclado, o ponto original do clique permanece como âncora
-
Affinity da seleção
- A affinity da seleção determina se o ponto de inserção no limite de quebra de linha aparecerá após o último caractere da linha anterior ou antes do primeiro caractere da linha seguinte
- Ao mover o cursor com as setas, ele simplesmente troca de linha ao redor do espaço da quebra
- Ao usar atalhos para ir ao fim da linha, o cursor pode permanecer na mesma linha, encostado à direita do espaço da quebra
TextViewtambém executa comportamento semelhante de affinity da seleção em outras situações
-
Undo coalescing
- Undo coalescing é o algoritmo que decide quando agrupar texto digitado em uma única ação passível de desfazer
TextViewparece usar um algoritmo simples que agrupa juntas entradas digitadas sem interrupção por outras ações- Mesmo que se faça uma pausa e continue digitando, tudo pode entrar em um único undo, e a quebra de linha não interrompe o coalescing
- O Sublime prefere agrupar palavras separadas em unidades de undo durante a digitação, e às vezes divide palavras longas em unidades menores de undo
- Não existe resposta certa aqui, e cada editor pode adotar estratégias diferentes
UTI, pasteboard e troca de dados entre apps
-
Uniform Type Identifiers
- UTI é um sistema hierárquico em que tipos de dados
conform tooutros tipos, como se herdassem de tipos pais - Tipos
public.*são definidos pela Apple e identificam formatos amplamente usados, comopublic.htmlepublic.jpeg - Desenvolvedores podem criar seus próprios identificadores usando o esquema de nome reverso de domínio para evitar conflitos
- A vantagem do sistema hierárquico é que, se um app consegue lidar com todos os formatos de texto, ele pode declarar apenas
public.textem vez de listar todos individualmente - O Paper declara que pode abrir todos os arquivos de texto e, mesmo sem highlighting, consegue abrir formatos de texto como
.htmle.rtf - Como arquivos são um conceito multiplataforma, na prática a extensão de arquivo acaba sendo usada como identificador
- Para compatibilidade, todo UTI pode definir uma ou mais extensões de arquivo associadas
- Markdown é amplamente usado, mas não é um formato definido pela Apple, o que cria casos complicados relacionados a UTIs semipúblicos
- UTI é um sistema hierárquico em que tipos de dados
-
Pasteboard e múltiplas representações
- O pasteboard da Apple é um dicionário que mapeia UTIs para texto serializado ou dados binários
- É possível ver o conteúdo do pasteboard em tempo real com o Clipboard Viewer das Xcode Additional Tools
- Uma única ação de cópia grava simultaneamente várias representações do mesmo dado
- Alguns apps também usam identificadores legados não UTI, como
NeXT Rich Text Format v1.0 pasteboard type, por compatibilidade retroativa - Em geral, o editor escolhe o formato mais rico que consegue processar
Paste and Match StyleouPaste as Plain Textem editores de texto rico fazem com que o formato de texto simples do pasteboard seja usado- O estilo aplicado ao texto colado normalmente vem dos atributos de digitação
- Drag and drop também funciona com base em outro pasteboard
- O copy-paste padrão usa o general pasteboard, e para interações customizadas entre apps é possível criar pasteboards personalizados
-
RTF e
NSAttributedString- RTF é, essencialmente, uma forma serializada de
NSAttributedString, eNSAttributedString, por sua vez, é a interface de programação do RTF TextViewfunciona sobreNSTextStorage, que é uma subclasse deNSAttributedString, então já é compatível com pasteboard por padrão- É possível copiar conteúdo para o pasteboard sem código adicional
- RTF é, essencialmente, uma forma serializada de
-
Problemas entre apps com o UTI de Markdown
- Em copiar e colar entre editores Markdown, pode haver problemas mesmo quando ambos os apps implementam o protocolo padrão
- Se o primeiro editor exportar Markdown como
public.texte a representação de texto rico comopublic.rtf, o segundo editor pode não perceber que aquilo é Markdown nativo e escolherpublic.rtf - Nesse caso, ocorre uma conversão dupla desnecessária e, como cada app converte Markdown↔RTF de maneira diferente, podem surgir pequenos problemas de formatação, como quebras de linha extras
- Também podem aparecer diferenças fundamentais de estilo entre Markdown e RTF
- Para funcionar de forma natural, ambos os apps precisam exportar o UTI
net.daringfireball.markdowne priorizá-lo acima depublic.rtf - O Paper tentou exportar o UTI de Markdown, mas parecia que outros apps não o priorizavam acima de texto rico
- O Pages prioriza
net.daringfireball.markdownacima depublic.rtf, mas parece inserir a string Markdown original diretamente, sem convertê-la para texto rico - Por isso, o Paper removeu o UTI de Markdown
-
Por que o Paper oferece RTF e compartilhamento no iOS
- O Paper oferece RTF para que copiar e colar funcione naturalmente em editores de texto rico
- Como um bom cidadão do sistema operacional, ele deve oferecer vários formatos que representem os dados copiados para que o app receptor escolha o formato mais rico que conseguir processar
- Se você copiar texto Markdown no Paper e colar no app Mail, ele será colado como texto rico bem formatado, e não como variação de Markdown
- O recurso de compartilhamento do iOS é parecido com copy-paste, mas com uma interface adicionada
- No compartilhamento do iOS, o app exporta os dados em vários formatos e o app receptor escolhe o formato desejado
- Diferentemente do pasteboard, para aparecer na lista de apps no topo do compartilhamento do iOS, o app precisa fornecer explicitamente uma share extension com interface customizada
1 comentários
Comentários do Hacker News
Excelente mesmo. Agora acho que vou passar a ver este artigo antes de https://www.objc.io/issues/5-ios7/getting-to-know-textkit/ como material introdutório ao TextKit
É o novo mecanismo de layout usado por padrão nos campos de texto atuais do iOS e do MacOS, e há uma grande chance de que se torne a única opção no futuro
O app TextEdit é composto quase inteiramente por uma única TextView
No Windows, o equivalente seria o WordPad baseado no controle RichEdit. Outro ponto interessante é que RTF é, na prática, um formato de serialização de NSAttributedString
O controle RichEdit do Windows também funciona assim e, na verdade, a implementação do Windows parece ter surgido primeiro: https://en.wikipedia.org/wiki/Rich_Text_Format
A parte sobre tratar atributos decorativos fora da transação de edição me deixou um pouco confuso. Havia uma explicação de que “eles ficam no próprio NSLayoutManager, não no NSTextStorage, então ele nem sabe da transação”, mas atributos decorativos como cor geralmente ficam no NSTextStorage
O autor quer dizer que a cor aplicada aos caracteres de Markdown foi tratada usando o suporte a atributos temporários do NSLayoutManager? É um recurso normalmente usado para colorir palavras com erros ortográficos, e fiquei curioso sobre qual seria o objetivo dele
A vantagem de usar atributos temporários é que a alteração é muito mais rápida do que acionar uma transação completa no NSTextStorage. Por exemplo, no Paper, alternar entre modo claro e escuro é muito rápido
Artigo muito bom e, pessoalmente, veio em ótima hora, pois estou lidando com NSTextView agora
Fiquei curioso sobre como você obteve as informações necessárias para escrever algo assim. Foi código de outras pessoas, experiências dolorosas, developer.apple.com?
Mesmo na era dos documentos DOM, ao ver Notion ou GitBook, quando é preciso fazer coisas meio mágicas com parsing e manipulação de texto, acabo voltando com frequência para strings atribuídas
É uma estrutura realmente elegante, e não entendo por que não é mais conhecida. O artigo também é incrivelmente bom
Como alguém que já tentou, no passado, criar um editor de texto totalmente do zero, acho que um artigo desses teria sido um material enorme
Uso muito bem este app. Ele substituiu todos os outros apps de Markdown, incluindo Obsidian e iA Writer
Gostaria que houvesse documentação assim para mais componentes de iOS
Que bom que, em 2024, ainda há alguém fazendo Cocoa
Como alguém que desenvolve apps Android há muito tempo, achei interessante a forma um pouco diferente e mais cuidadosa com que a Apple aborda isso
No Android, basicamente tudo relacionado a layout e renderização é tratado pela classe Layout e suas subclasses, enquanto TextView implementa parte da lógica de edição e seleção. A única diferença entre EditText e TextView é que EditText ativa os recursos de edição que já existem em TextView
O problema dessa abordagem um tanto monolítica e de APIs fracas é que você fica bloqueado quando quer ter um controle mais fino sobre a renderização de texto no app. Por exemplo, mesmo que queira acessar glifos individuais depois que o layout termina, não é possível