Skip to content

Photon pt BR

Edgar Mesquita edited this page Aug 14, 2026 · 6 revisions

Photon: o motor GPU nativo

🌐 Esta página em: English · Português

O Photon é o motor de renderização GPU proprietário do eQuantic.UI para alvos nativos: um renderizador escrito do zero, inspirado no Impeller, que desenha a árvore de componentes pixel a pixel na GPU (Metal no macOS/iOS, Vulkan no Android), sem camada Skia e sem WebView. Ele é o segundo alvo de realização da arquitetura de componentes write-once: os mesmos componentes C# que rebaixam para DOM+CSS no web são rasterizados nativamente pelo Photon.

Por que proprietário, por que sem Skia

Ter uma camada nativa própria significa ter a pilha de renderização inteira: backends Metal/Vulkan diretos com shaders pré-compilados, não um wrapper sobre o Skia. Isso compra:

  • Performance previsível: registro fixo de estados de pipeline, zero compilação de shader em tempo de execução (a fonte de travadas que o Impeller foi construído para matar). Os caches de pipeline persistem entre lançamentos nos dois backends.
  • Uma superfície minúscula: renderizar interface precisa de um punhado de primitivas, não de uma biblioteca 2D geral.
  • Um único modelo normativo: a mesma matemática, testável até o pixel, entre a referência em CPU e todos os backends de GPU.

A arquitetura de relance

Componentes C# (write-once, vocabulário do eQuantic.UI.Primitives)
        │  Build(ComponentContext)          // puro, baseado em tokens, sem modos
        ▼
eQuantic.UI.Native.Framework                // motor de layout flex em C# (spec A2)
        ▼
eQuantic.UI.Native.Components               // PhotonRealizer: árvore abstrata → display list
        ▼
eQuantic.UI.Native.Engine                   // geometria, modelo de cor, Sdf.cs (NORMATIVO), DisplayList
        ▼
IRenderBackend
   ├── eQuantic.UI.Native.Engine.Reference  // rasterizador escalar em CPU (verdade das goldens, nunca publicado)
   ├── eQuantic.UI.Native.Engine.Metal      // GPUs da Apple (macOS, iOS)
   └── eQuantic.UI.Native.Engine.Vulkan     // Android

eQuantic.UI.Native.Shell.MacOS / .iOS / .Android   // a janela/host de cada plataforma

O núcleo normativo

  • Sdf.cs é uma especificação, não uma biblioteca: distância com sinal de retângulo arredondado por canto, traço centrado (|d| − w/2), rampa de cobertura de antialiasing de 1 pixel. Todo rasterizador, tanto a referência em CPU quanto os fragment shaders da GPU, implementa exatamente essas fórmulas.
  • Modelo de cor: autoria em sRGB → mistura linear pré-multiplicada → armazenamento em sRGB, espelhando o comportamento do hardware GPU com render targets sRGB.
  • Display lists: registros DrawCommand planos e sem heap (Clear / FillRRect / StrokeRRect, tintas sólidas ou de gradiente linear de duas paradas, transforms 2D já assadas).

Arnês de imagens golden

Toda cena renderiza por um backend numa superfície, lê os pixels sRGB de volta e compara contra PNGs golden comitados (codec sem dependências, fluxo de regeneração com EQ_UPDATE_GOLDENS=1, tolerância ±2, artefatos de falha com diffs amplificados). O catálogo de cenas é compartilhado: o backend Reference pina as goldens, os backends de GPU rodam o mesmo catálogo para paridade, e a paridade GPU↔CPU se sustenta em todo o catálogo compartilhado dentro da tolerância das goldens, no Metal e no Vulkan.

Os backends de GPU são dirigidos por bindings finos e tipados de objc_msgSend/P-Invoke: sem framework de binding, sem shims em C, zero pacotes de terceiros.

Renderização de componentes

O realizador nativo desenha chrome de componente de verdade (preenchimentos resolvidos por token, bordas internas, raios por canto):

Subsistema Estado
Formas, bordas, gradientes, transforms, mistura, clip ✅ Completo, testado por golden
Layout (flex + wrap, grid, stacks, adaptativo, truncamento) ✅ Motor em C#
Sombras (elevação §05) ShadowCoverage analítico, nos dois backends
Texto ✅ REAL. Primitiva Texture (cobertura A8 × tinta) + ITextRasterizer da plataforma; no macOS = CoreText (só frameworks do sistema, sem HarfBuzz/FreeType onde a plataforma tem um motor de texto). Um motor mede E rasteriza, então as quebras de linha concordam por construção. Duas faces: a proporcional do sistema e a monoespaçada dele (TypeStyle.Mono). Nenhum glifo é jamais cortado: o bitmap é dimensionado pela caixa de TINTA da linha (CTLineGetImageBounds, não as métricas declaradas da fonte, já que a cauda profunda de um g passa além delas) e reporta o quanto acima da caixa de linha ele precisou alcançar, então o realizador levanta o desenho exatamente por isso e a caixa de linha continua caindo onde o layout a colocou
Ícones ✅ REAL. Parser de caminho SVG em C# puro (conjunto completo de comandos, arcos via F.6.5) + rasterizador CoreGraphics, a mesma primitiva Texture
Opacidade/transforms de grupo ✅ Camadas do motor
Compositor de rolagem ✅ Deslocamentos por caminho, fixação Sticky de verdade (vertical)
Movimento ✅ Movimento em laço, transições de valor, entrada/saída de Presence com replay de snapshot de comandos
Gestos ✅ Pipeline de hover, toque com cancelamento por folga, arrastar-para-dispensar (seguir + deslizar)
Overlays ancorados ✅ Camadas de overlay sintéticas posicionadas a partir dos limites absolutos da âncora
Imagens Caixa SurfaceSubtle, já que decodificação/upload não faz parte da versão atual (o consumidor de Texture que falta)

Uma frase desenha run a run

Desde 0.2.0-preview.28

Um parágrafo rico (Text.Spans) rasterizava como UM bloco no estilo base, então a palavra em negrito, o span de código inline, o termo em itálico e o link no meio da frase se dissolviam todos em prosa chapada, a divergência write-once mais larga que restava, já que o web emite um span por run desde o dia em que runs existem. A unidade agora é um FRAGMENTO, um run numa linha, porque essa é a única peça com um retângulo e as duas metades precisam de um: cada fragmento rasteriza no próprio estilo (peso, mono, itálico, o tamanho próprio), e o retângulo de um fragmento com link é uma região de hit de onde o shell navega, então links inline são tocáveis no Photon, fechando o item do Roadmap que os nomeava.

A interação está ligada: o PhotonHost guarda uma raiz retida, o SetState invalida, os toques despacham para as regiões de hit (o de cima vence, o desabilitado engole; ≥48dp sob um dedo, o tamanho próprio do controle sob um ponteiro, veja Densidade). O Counter nativo roda toque → SetState → reconstrução de ponta a ponta nos testes. O PhotonHost.FocusedPath reporta o que segura o teclado, incluindo um campo em edição (o cursor é o indicador de foco daquele campo), porque uma ponte de acessibilidade da plataforma precisa de uma resposta honesta, não de metade dela.

Dedos nunca fazem hover, e o hover sobrevive à reconstrução

Desde 0.2.0-preview.24

Os eventos de ponteiro carregam um PointerKind, por EVENTO e não por host, porque a capacidade de hover pertence ao dispositivo na mão (um iPad ganha um trackpad, um Android ganha um mouse). Um PointerMove marcado como Touch guia arrastos, panorâmicas e seleções exatamente como um mouse e pula apenas a resolução de hover; um PressUp/PointerCancel de toque limpa qualquer hover que uma sessão de entrada mista tenha deixado para trás, enquanto um mouse mantém o hover dele durante um clique. O shell do macOS instala uma NSTrackingArea e roteia MouseExited para PointerLeave(), então cruzar a borda da janela limpa o hover em vez de travá-lo.

A identidade do hover é o CAMINHO de layout, não a referência do nó, e o toque aprendeu isso primeiro: uma reconstrução de componente substitui cada instância, e um hover que só conhecia a referência antiga pintava exatamente um frame. Ele também é uma CADEIA de toda região sob o ponteiro (o :hover do CSS casa com todo ancestral, e o realizador nativo responde a mesma pergunta que o gêmeo web responde), que é o que permite a um Anchored aberto por hover ficar aberto enquanto você faz hover no gatilho dentro dele. O pressionado continua vencendo o hover na caixa que consumiu a troca.

A janela aprende o idioma da máquina

Desde 0.2.0-preview.24

Um processo gráfico lançado fora de um terminal não tem LANG/LC_*, então o .NET começa invariante mesmo numa máquina pt-BR. Os shells agora resolvem a verdade de locale da plataforma e a aplicam antes do primeiro frame: o AppleLocale lê o NSLocale, onde preferredLanguages[0] é a cultura de interface (recursos) e currentLocale a cultura de formato (datas, separadores), nativamente o mesmo par que o .NET modela como CurrentUICulture/CurrentCulture, e o AndroidLocale alimenta os dois statics a partir de Locale.getDefault(). O PhotonCultureController é dono da escrita dos statics (thread padrão E thread atual, para que toda thread posterior concorde) e repinta no Apply, a forma do controller de tema; registrado com TryAdd, então um app que força uma cultura vence. Observar o locale do SO mudando em tempo de execução é uma cerca por shell.

Layout: as regras de CSS que o motor deve a você

O motor nativo é julgado contra o browser, porque o gêmeo web renderiza a mesma árvore lá:

  • O Fill herda a indeterminação: num eixo em que o pai se dimensiona pelo conteúdo, um filho Fill não tem o que preencher e passa a pergunta adiante; o que sobra não é distribuído nesse eixo.
  • Semântica de bloco na largura, e uma altura DECIDIDA se entrega ao filho: uma caixa cuja largura foi decidida estica um filho CONTÊINER de tamanho automático por ela (o filho div de uma div ocupa a largura toda), enquanto um botão, link ou input se aperta ali: eles são inline-block, e só um esticamento de FLEX atravessa por eles (StretchKind). A metade da altura está logo abaixo.

Desde 0.2.0-preview.28 Uma caixa cuja altura foi DECIDIDA (fixa, preenchida ou limitada) a entrega ao filho único, nos dois alvos. Esta é a regra que aposentou a família recorrente da "toolbar colada no topo da barra de altura fixa", em que toda Row dentro de um Box de 64dp precisava de um Height = Fill manual para alcançar o fundo da própria barra.

  • flex-shrink com piso de min-content: uma linha que não cabe encolhe os itens em vez de transbordar, e nunca abaixo do min-content de cada item (a palavra mais longa de um texto, a soma dos filhos de uma linha). O piso importa em dobro porque o clip guarda pixels E ponteiro: uma região de toque cortada não pode ser tocada.
  • Refluxo de Min/Max: quando um limite muda a extensão de uma caixa, o filho dela se remede contra o tamanho final.

Os shells: o Photon numa janela de verdade

macOS (eQuantic.UI.Native.Shell.MacOS): uma NSWindow hospedando uma CAMetalLayer, zero pacotes de terceiros, apenas bindings finos e tipados de objc_msgSend para o AppKit. O backend Metal codifica cada frame direto no drawable da camada (cache de pipeline por formato de pixel, BGRA8_sRGB para janelas); o PhotonHost.RenderScale rasteriza em backingScaleFactor enquanto layout/entrada ficam em dp (retina). Eventos de mouse/rolagem/teclado do SO entram no pipeline de entrada comum, então os visuais de toque, os diffs de hover e os menus ancorados se comportam exatamente como nos testes. Texto San Francisco de verdade e ícones de pacote de verdade renderizam na tela. Experimente: dotnet run --project samples/PhotonDesktop. Dois modos sem tela não precisam de janela nenhuma: --Photon:MaxFrames 120 apresenta essa quantidade de frames e sai, e --Photon:ScreenshotPath out.png (com --Photon:Mode Dark para a outra paleta) renderiza UM frame assentado pelo backend de referência com as mesmas métricas do CoreText. Esse é o passo de screenshot do CI, e o jeito de conferir uma passada de fidelidade contra um handoff de design. Chamar Run() fora da thread principal diz isso em termos de .NET, em vez de morrer dentro do AppKit. Redimensione a janela à vontade: o host adota o viewport com todo o estado intacto.

iOS (eQuantic.UI.Native.Shell.iOS): o PhotonApp.Run é o ponto de entrada; o mesmo motor apresenta pelo Metal.

Android (eQuantic.UI.Native.Shell.Android): o app fornece o CreateApp (sem Main); a apresentação vai pelo Vulkan (o backend Reference é o fallback).

O cursor, e o que uma seleção tem que mostrar

Uma seleção tem duas pontas e só uma delas se move. Qual é não é algo que uma pessoa consiga deduzir da faixa, então o cursor é desenhado JUNTO com a seleção, não no lugar dela: a faixa diz quais caracteres estão segurados, o cursor diz qual PONTA você está segurando, a que a seta com ⇧ vai mover, a partir da qual o próximo caractere substitui.

Duas regras vêm junto, e as duas são sobre estar visível quando importa:

  • A fase do piscar começa no último MOVIMENTO do cursor, não na época. Numa fase de rodagem livre, um cursor que acabou de chegar em algum lugar pode estar na metade APAGADA por meio segundo: você aperta ⇧→ e olha para nada. Toda escrita no cursor reinicia o ciclo, que é por que o host roteia todas elas por uma única propriedade.
  • Enquanto um arrasto está desenhando uma seleção o cursor não pisca de jeito nenhum. A ponta que segue o seu ponteiro é a única coisa que você está olhando.

No web o campo é um <input> de verdade e o browser é dono do cursor dele; o que está descrito aqui é o que o Photon pinta, e o que a superfície de editor (que os dois alvos pintam eles mesmos) faz.

O piscar é RITMADO, não preso

Um cursor é movimento de 2Hz, não movimento de vsync, então um cursor em edição AGENDA os frames dele em vez de manter o laço de render quente:

  • o RenderFrame deixa NeedsRender como falso quando o cursor é a única razão para um próximo frame, e registra quando a próxima TRANSIÇÃO do piscar é devida;
  • os ticks ociosos dos shells (o timer de 120Hz do macOS, os callbacks de vsync dos telefones) perguntam host.IsFrameDue(nowMs) junto com NeedsRender: uma apresentação por meio período, caindo na transição, ~2 por segundo em vez da taxa de atualização do display.

Movimento de verdade (um spinner, um deslize, uma transição) continua mantendo o laço quente, e o agendamento existe só para o cursor. Enquanto um arrasto desenha uma seleção nada é agendado: o cursor é forçado sólido e o movimento do próprio ponteiro guia os frames. Dez segundos simulados de um campo focado: 20 apresentações, cada uma virando o piscar, afirmado em CaretPacingTests, contagem incluída.

O web não precisa de nada disso: o browser é dono do cursor do <input>, e a div de cursor da superfície de editor pode piscar no compositor.

Hot reload: dotnet watch run, e o app acompanha

Rode qualquer app Photon sob dotnet watch run, edite um corpo de Build, salve, e a janela redesenha com o novo código e TODO o estado intacto, num tique. Sem comando de SDK, sem configuração: o encanamento vem em eQuantic.UI.Native.Components e todo host se inscreve na construção.

A arquitetura tem formato de hot reload por construção:

  • a árvore é RE-EXPANDIDA a partir da raiz retida a cada frame (o Build roda durante a medição), então um corpo de método remendado simplesmente passa a valer no frame seguinte;
  • o estado do componente vive num armazenamento com chave de CAMINHO, não nos objetos que uma edição substitui, então nada precisa ser migrado. Três cliques de estado de contador sobrevivem a um reload porque nada os tocou.

O PhotonHotReload (o [MetadataUpdateHandler]) fornece a peça que falta, o próprio frame seguinte, porque uma janela ociosa não apresenta nada e uma atualização de metadados chega sem nenhum evento de entrada anexado. Ele faz exatamente duas coisas:

  1. acorda todo host vivo por um bool que os laços de render já consultam (macOS em até 50ms, os telefones no próximo vsync deles), e
  2. incrementa uma GERAÇÃO que o host compara na própria thread de render dele para descartar os caches de conteúdo uma vez (rasterizações de texto, ícones, imagens). As chaves são conteúdo, então a maioria das edições passaria longe delas, mas um rasterizador remendado ou um caminho de ícone editado é exatamente a edição que uma pessoa faz enquanto ajusta o visual. Nada é mutado pela thread do handler.

Edições grosseiras (tipos novos, assinaturas mudadas) cabem ao runtime recusar. O dotnet watch responde a essas com um reinício, o que é correto e não precisa de nada do framework.

Mobile: o mesmo registro serve os hosts de iOS e Android (os ticks de vsync deles consultam o mesmo bool), então o hot reload funciona no SIMULADOR. Um dispositivo físico precisa que o dotnet watch alcance o processo, o que é uma questão de canal de deploy, não de framework.

Shaders: UMA fonte Slang normativa

O Sdf.slang (transliterando o Sdf.cs) compila offline pela toolchain slangc pinada em MSL + SPIR-V comitados: o fragmento SDF mais o fragmento texturizado (Load de texel, o mais próximo por definição, porque as rasterizações são em escala de dispositivo, com paridade exata com a Reference). Os desenvolvedores de app nunca rodam a toolchain de shaders; eles consomem os artefatos comitados.

Performance: as promessas ganham números

O PerfHarnessTests mede o que a definição de pronto promete ("120 Hz, zero alocações em regime, orçamentos cumpridos") em vez de assumir. Numa cena em formato de dashboard (24 cartões + uma faixa com movimento em laço), por frame em regime (baseline série M): 146 comandos de desenho, realize p50 0,23 ms / p95 0,32 ms contra o orçamento de 8,33 ms, e alocação em regime abaixo de 72 KB por frame com reciclagem de frames ligada (67,5 KB medidos).

O que mantém o número baixo:

  • um cache de strings de caminho: um caminho é identidade, então o host empresta um dicionário que sobrevive aos frames e o regime para de reconcatená-las;
  • um DisplayListBuilder reutilizado: o Reset() mantém a capacidade do buffer; os shells rodam um builder por laço;
  • reciclagem de frames (PhotonHost.RecycleFrames, opcional, desligado por padrão): o RealizeResult carrega uma história explícita de propriedade. Com a reciclagem ligada, o host é DONO de todo frame que ele substitui, a árvore substituída alimenta um LayoutNodePool, e o próximo frame é construído a partir dele. Os shells de produção (macOS/iOS/Android) ligam isso porque nada retém os frames deles; testes e ferramentas que seguram RealizeResults deixam desligado e ficam com árvores imutáveis.

Os tetos são pinados como CATRACAS de regressão com uma margem acima dos números medidos (152 KB no perfil padrão / 72 KB com pool, 200 comandos, alarme de 33 ms, com o tempo deliberadamente frouxo para que um CI compartilhado nunca fique instável). A metade de GPU do frame segue coberta por goldens de paridade, não por cronometragem no CI.

IME: a composição é o texto da plataforma

Enquanto um campo segura o cursor, o shell do macOS entrega os eventos de tecla ao contexto de entrada da PLATAFORMA (NSTextInputClient: a view conforma, registrada em tempo de execução como toda outra sobreposição):

  • Uma tecla morta ou um método CJK compõe ao longo de vários toques como texto marcado: segurado pelo host (SetMarkedText), renderizado INLINE no cursor com um sublinhado, enquanto o VALOR do campo fica intocado. O commit (CommitText) entra no campo pela mesma porta que todo toque de tecla usa; o cancelamento deixa o campo exatamente como estava. Compor sobre uma seleção a substitui, a mesma regra que digitar tem. Um campo obscurecido compõe às cegas (ecoar a composição ecoaria o segredo).
  • As teclas de edição VOLTAM do contexto de entrada como seletores (doCommandBySelector:) e reentram pela mesma porta OnKey, re-soletradas para nomes do DOM: Backspace, setas (com variantes Shift/Alt), Home/End, Escape. Acordes de Command/Control nunca entram no contexto: ⌘A/⌘C/⌘Z pertencem ao app.
  • O firstRectForCharacterRange ancora a janela de candidatos no CURSOR (host CaretRect() → inversão de y → view → janela → tela), então o seletor CJK aparece sob a composição.
  • Prova: ImeCompositionTests no host (sequência de tecla morta, cancelamento, composição sobre seleção, acompanhamento do cursor, intervalos, perda de foco descarta a composição), e o autoteste da janela dirige os seletores registrados DE VERDADE (setMarkedText:insertText:) contra um campo vivo. Digite option+e e em qualquer campo para ver o é compor.
  • Escopo: superfícies de código compõem às cegas. O commit funciona, mas a face do editor não renderiza trecho marcado inline.

Acessibilidade: a árvore de semântica

Desde 0.2.0-preview.29

O Photon desenha os próprios pixels, então o SO vê uma view opaca, e sem uma árvore de semântica o app é INVISÍVEL para um leitor de tela. A árvore é um artefato de primeira classe do frame:

  • SemanticsTree.Collect(frame) (compartilhado, neutro em relação ao alvo) deriva a semântica em ordem de leitura a partir do layout realizado, tanto da página QUANTO das camadas de overlay, então diálogos são vistos. Papéis: StaticText, Button, Link, TextField, CodeField, Slider (Adjustable), Image (Icon rotulado). O texto interno de um controle é o nome acessível dele (a regra <button>texto</button> do web); um Icon rotulado anuncia, um sem rótulo é decoração; o TextEntry.Label nomeia um campo. A identidade é o CAMINHO de layout, a mesma identidade que toques, foco e rolagem usam, porque a árvore é reconstruída a cada frame e referências envelhecem.
  • Conteúdo rolado para fora é incluído, exatamente como o FocusStop e deliberadamente ao contrário das regiões de hit de ponteiro: a navegação linear alcança abaixo da dobra; o corte guarda o ponteiro, não o leitor.
  • PhotonHost.Semantics() expõe a árvore; PhotonHost.ActivatePath(path) aciona um controle do jeito que um toque aciona (resolvido a partir das regiões do frame atual, recusando desabilitados); elementos ajustáveis respondem a ações de incremento/decremento.
  • Ponte do macOS (PhotonAccessibility): a view de conteúdo responde accessibilityChildren com EQAXElements (um NSAccessibilityElement que pode ser pressionado): papel, rótulo, valor, habilitação, accessibilityFrameInParentSpace com y invertido, o caminho como accessibilityIdentifier, e o accessibilityPerformPress roteado por ActivatePath. Os elementos são reconstruídos a cada consulta; o lote anterior é liberado.
  • Ponte do iOS (PhotonAccessibility): a PhotonView é um CONTÊINER de acessibilidade cujos elementos são construídos da árvore em ordem de leitura: papel mapeado em UIAccessibilityTraits, frames convertidos para o espaço da tela, o caminho como accessibilityIdentifier, e o toque duplo e o gesto de ajuste exportados como accessibilityActivate / accessibilityIncrement / accessibilityDecrement, roteados por ActivatePath e AdjustPath. Uma marcação é um botão cujo VALOR é "1"/"0", o contrato do próprio UIKit para um alternador, e por isso o estado é anunciado no idioma do usuário sem o framework publicar nenhuma dessas palavras. A divulgação anda no accessibilityExpandedStatus do iOS 17. Os elementos são reconstruídos quando a árvore MUDA, e não a cada consulta (o VoiceOver pede a lista enquanto caminha por ela), e o LayoutChanged só é postado enquanto o VoiceOver está ligado.
  • Ponte do Android (PhotonAccessibility, um AccessibilityNodeProvider): a PhotonSurfaceView responde por uma hierarquia VIRTUAL: um filho por nó de semântica, o papel como o nome de classe da plataforma (android.widget.Button, CheckBox, Switch, SeekBar, EditText, ImageView, TextView), limites em pixels de tela, ACTION_CLICK por ActivatePath, rolagem para frente/trás por AdjustPath, e a exploração por toque respondida no dispatchHoverEvent. Lá a divulgação é uma AÇÃO em vez de um sinalizador: oferecer expandir é como um nó diz que está fechado. A view precisa se declarar IMPORTANT_FOR_ACCESSIBILITY_YES, já que ela é a única view da janela, e sem isso a janela fica sem raiz acessível nenhuma.
  • Duas cercas, uma em cada telefone: nenhuma das plataformas tem um terceiro estado de marcação, então um checkbox mixed é anunciado sem estado em vez de alegar que está desmarcado; e o Android não tem classe de link, então um link é anunciado como aquilo que ele faz.
  • Prova: os SemanticsTests pinam ordem, nomeação, visibilidade de diálogo, exposição de desabilitados e a ação de ativar, mais a PORTEIRA DE PARIDADE SemanticsMatchFocusStops, que impede o percurso de semântica e o percurso de entrada de se afastarem um do outro. O autoteste de cada shell então pergunta à PLATAFORMA, não à árvore: a janela do macOS imprime seus accessibilityChildren vivos, o controlador do iOS envia os seletores Objective-C de verdade, e no Android o uiautomator dump lê o app de fora pelo mesmo caminho de serviço que o TalkBack usa. A mesma tela responde com os mesmos 26 elementos nos três.
  • A paridade no web é nativa do realizador web (<button>/aria-label/placeholder de verdade), então nada é devido lá. A árvore é neutra em relação ao alvo e agora todo shell tem a sua ponte.

Status

A suíte nativa pina o comportamento do motor com 87 imagens golden comitadas (pares de componentes claro+escuro, estados de folha no meio do arrasto, cobertura de textura) ao lado das suítes de unidade e de host. A janela roda o showroom write-once com texto, ícones, menus e gestos de verdade nos shells de macOS, iOS e Android. Cerca de escopo atual: decodificação/upload de imagem não está incluída, então o Image renderiza a superfície de placeholder dele.

Clone this wiki locally