ADRIANOLAUREANO← Artigos
NAVEGADORREDESHTML + CSSRUST

Como o navegador abre um site?
Da URL ao primeiro pixel da página

No artigo anterior construímos uma janela, recebemos mouse e teclado e desenhamos nossos próprios pixels. Agora essa janela ganhará uma barra de endereço e aprenderá a buscar, compreender e apresentar um documento vindo da rede. Ao final teremos o Janela Browser 0.2 — pequeno o bastante para entendermos, completo o bastante para tornar a viagem visível.

ARTIGO 17 · SÉRIE 02 · DA URL AOS PIXELS

27etapas didáticas

4camadas principais

1navegador em Rust

DA URL AOS PIXELS · REDE, DOCUMENTO E RENDERIZAÇÃO
Ilustração técnica colorida mostrando uma URL atravessando rede, segurança, documentos HTML e CSS, árvores, camadas de layout e pixels até uma página exibida no monitor

VOCABULÁRIO VISUAL

Antes de avançar, conheça as peças deste capítulo.

01URLendereço dividido em esquema, host, porta e caminho
02DNStransformação de nome em endereço de rede
03HTTPformato do pedido e da resposta
04DOMdocumento organizado como árvore na memória
05CSSOMregras de aparência organizadas na memória
06Layoutcálculo das caixas que serão desenhadas
Você não precisa memorizar agora. Este mapa existe para consultar sempre que um termo reaparecer.
01 /
Nossa Janela Agora Precisa Buscar um Documento
A GUI continua; acrescentaremos rede, documento e navegação.

A Janela UI 0.1 já sabe criar uma superfície Wayland, organizar componentes, interpretar cliques e transformar formas em pixels. Construímos essa base no Como um computador desenha uma janela. Um navegador precisa de tudo isso. A diferença é que o conteúdo não está decidido dentro do programa: chega como texto e arquivos enviados por outro computador.

A regra deste artigo

Não usaremos uma sigla para explicar outra sigla. Antes de escrever TCP, TLS, HTTP, parser ou DOM, veremos primeiro qual problema existe, depois daremos um nome à solução e, por último, construiremos a parte correspondente em Rust. O projeto crescerá junto com a explicação; ele não aparecerá montado por mágica no final.

A NOVA VIAGEMDe uma intenção até uma página visívelPrimeiro em palavras comuns; os nomes técnicos virão depois.
01Localizaronde está o site
02Solicitarpedir o documento
03Compreendertexto, estrutura e estilo
04Desenharcaixas, letras e pixels
Limite deste capítulo

JavaScript aparecerá apenas como um recurso que o navegador encontra. Entender sua sintaxe, gerar bytecode e executá-lo pertence ao artigo “Como o navegador entende e executa JavaScript”. Aqui o protagonista é o caminho da navegação e da renderização.

02 /
O que Acontece Depois de Pressionar Enter?
Uma tecla dispara uma sequência de decisões, não uma única operação.

O foco está na barra de endereço. Cada tecla altera seu texto. Quando Enter é pressionado, o navegador congela aquela entrada como uma tentativa de navegação. Antes de usar a rede, precisa descobrir se recebeu uma URL, uma busca ou um endereço incompleto.

Evento::Tecla(Enter)
    ↓
texto = "http://localhost:8000/"
    ↓
Url::parse(texto)
    ↓
Navigation::start(url)

Navegação é o processo de substituir o documento atual por outro. Ela possui começo, progresso, sucesso, erro e cancelamento. Dar um nome a esse estado impedirá que rede, interface e desenho se misturem.

O primeiro pedaço do programa

Neste momento ainda não há rede. A barra apenas entrega uma String — uma sequência de caracteres — para Url::parse. Se o endereço estiver incompleto, paramos aqui e mostramos o erro. Separar essa validação evita tentar conectar usando dados que não entendemos.

03 /
URL é um Endereço Dividido em Partes
Cada trecho responde a uma pergunta diferente.

URL significa Uniform Resource Locator. Em vez de tratá-la como uma frase inteira, o navegador separa suas peças.

ANATOMIA DA URLQuem, onde e qual recurso?
ESQUEMAhttp://HOSTlocalhost:PORTA8000CAMINHO/docs/inicio
  • Esquema: qual conjunto de regras será usado, como http.
  • Host: o nome do computador ou serviço.
  • Porta: a entrada lógica do programa servidor; HTTP costuma usar 80.
  • Caminho: qual recurso queremos dentro daquele servidor.
pub struct Url {
    scheme: String,
    host: String,
    port: u16,
    path: String,
}

Vamos construir isso agora. split_once("://") corta a entrada uma única vez. Tudo antes do separador vira o esquema; tudo depois ainda contém o destino e o caminho.

let (scheme, rest) = input
    .split_once("://")
    .ok_or("a URL precisa de ://")?;

if scheme != "http" {
    return Err("esta versão aceita apenas http".into());
}

O operador ? significa: se a operação falhar, devolva o erro imediatamente; se funcionar, continue com o valor. Em seguida dividimos localhost:8000/ola em autoridade e caminho. Autoridade é o trecho que identifica onde está o serviço — aqui, host e porta.

let (authority, path) = rest
    .split_once('/')
    .map(|(a, p)| (a, format!("/{p}")))
    .unwrap_or((rest, "/".into()));

Se houver uma barra, recolocamos / no começo do caminho. Se não houver, usamos /, que representa a raiz do site. Nosso primeiro teste confirma que a interpretação aconteceu antes de qualquer conexão:

let url = Url::parse("http://localhost:8000/ola")?;
assert_eq!(url.host, "localhost");
assert_eq!(url.port, 8000);
assert_eq!(url.path, "/ola");
04 /
DNS Transforma um Nome em um Endereço de Rede
Computadores entregam pacotes a endereços IP, não a nomes amigáveis.

Temos o nome localhost, mas a rede não entrega dados usando palavras. Ela usa um endereço IP: um número que identifica um destino na rede, como 127.0.0.1. O problema é simples: precisamos descobrir qual número corresponde ao nome.

A solução recebe o nome DNS, sigla de Domain Name System, ou Sistema de Nomes de Domínio. DNS não baixa a página e não abre uma conversa. Ele responde somente: “para este nome, tente este endereço IP”. É semelhante a consultar um contato antes de fazer a ligação.

Antes da consultaDepois da consultaO que ainda não aconteceu
localhost:8000127.0.0.1:8000nenhum HTML foi pedido

localhost é especial: normalmente significa “este próprio computador”. Para um domínio público, o sistema pode consultar respostas guardadas em cache e servidores DNS. Cache é uma resposta armazenada para não repetir a consulta enquanto ela ainda é válida. Já construímos esse mesmo princípio, aplicado a páginas de dados, em Construindo um banco de dados do zero em Rust.

Agora acrescentamos a resolução ao nosso programa:

use std::net::ToSocketAddrs;

let mut addresses = (url.host.as_str(), url.port)
    .to_socket_addrs()
    .map_err(|error| error.to_string())?;

let address = addresses
    .next()
    .ok_or("DNS não retornou endereço")?;

ToSocketAddrs é uma capacidade da biblioteca padrão do Rust. A dupla (host, porta) é entregue ao sistema operacional. O resultado é um iterador porque um nome pode apontar para mais de um endereço, inclusive IPv4 e IPv6. Nesta versão escolhemos o primeiro. Agora possuímos um SocketAddr: IP e porta juntos.

Checkpoint mental

URL separou o texto. DNS converteu o host em IP. Ainda não enviamos pedido algum. Se essas duas ações parecem diferentes, a fundação está correta.

05 /
Antes da Internet, o Pacote Atravessa a Rede Local
Wi‑Fi, Ethernet, roteador e NAT fazem parte do caminho.

Um endereço IP informa o destino, mas os dados ainda precisam percorrer um caminho. Se usamos 127.0.0.1, eles permanecem no próprio computador. Se o destino está fora de casa, o sistema entrega os dados ao roteador, equipamento que escolhe o próximo trecho do caminho.

Os dados são divididos em pequenos blocos chamados pacotes. Um pacote carrega parte do conteúdo e informações de origem e destino, como uma caixa que viaja com etiqueta. Wi‑Fi ou cabo Ethernet transportam o pacote apenas pelo trecho local; outros roteadores continuam a entrega.

Não construiremos o roteador

O navegador pede ao sistema operacional que envie dados. O kernel, os dispositivos de rede e os roteadores cuidam da viagem. Entender essa fronteira evita imaginar que TcpStream implementa a Internet inteira.

06 /
TCP Cria uma Conversa Ordenada
A página pode atravessar a rede em partes e ainda chegar na ordem correta.

Pacotes podem chegar fora de ordem, ser repetidos ou desaparecer. Isso seria péssimo para um documento: imagine receber primeiro o fim de </html> e nunca receber o começo. Precisamos de uma conversa que reorganize as partes e peça novamente o que faltar.

Essa solução se chama TCP, sigla de Transmission Control Protocol. Um protocolo é um conjunto de regras que os dois lados concordam em seguir. TCP numera os bytes, confirma o que chegou e retransmite perdas. Para nosso programa, o resultado parece um fluxo contínuo e ordenado.

ABERTURA DA CONEXÃOTrês mensagens antes dos dados
1SYNposso conversar?
2SYN + ACKsim, ouvi você
3ACKconexão pronta

SYN significa que um lado deseja sincronizar e iniciar a conexão. ACK é uma confirmação de recebimento. Não criaremos essas mensagens à mão; o sistema operacional faz isso quando chamamos:

use std::{net::TcpStream, time::Duration};

let mut stream = TcpStream::connect_timeout(
    &address,
    Duration::from_secs(5),
)?;

TcpStream representa a conversa aberta. &address é o endereço obtido na etapa DNS. O limite de cinco segundos impede que a interface espere para sempre se ninguém responder. A palavra stream significa fluxo: escrever bytes envia dados; ler bytes recebe dados.

DNS ≠ TCP

DNS descobre o número do destino. TCP abre e mantém a conversa com esse destino. Consultar um telefone e realizar a ligação são ações relacionadas, mas não são a mesma ação.

07 /
TLS Protege a Conversa do HTTPS
Criptografar não basta: também precisamos confirmar com quem falamos.

TCP garante ordem e entrega, mas não esconde o conteúdo. Sem proteção, alguém no caminho poderia ler ou alterar os bytes. Precisamos de três garantias: manter a conversa secreta, detectar alterações e verificar a identidade do site.

A camada que fornece essas garantias se chama TLS, sigla de Transport Layer Security. Ela transforma texto legível em dados cifrados usando chaves. Antes disso, ocorre o handshake — uma conversa inicial para escolher os métodos de proteção e criar as chaves.

O servidor também apresenta um certificado, um documento digital que liga um domínio a uma chave pública. O navegador verifica quem assinou esse certificado, se ele pertence ao domínio visitado e se ainda está válido. O cadeado não significa “site honesto”; significa que a conexão foi protegida e a identidade técnica foi verificada.

NomeO que acrescenta
HTTPregras do pedido e da resposta
TLSproteção da conversa
HTTPSHTTP viajando dentro de TLS
Escolha didática da versão 0.2

Implementar TLS corretamente seria outro projeto inteiro e um erro colocaria segurança em risco. Nosso executável aceita HTTP local, onde podemos observar cada byte. O artigo acompanha conceitualmente o caminho HTTPS e trata a ausência de TLS como limite, não como detalhe escondido.

08 /
HTTP é um Pedido Seguido de uma Resposta
Método, caminho, cabeçalhos e corpo possuem funções separadas.

A conexão TCP já existe, mas os dois programas ainda precisam concordar sobre o significado dos bytes. A solução é o HTTP, sigla de Hypertext Transfer Protocol. Ele define como escrever um pedido e como interpretar a resposta. TCP transporta; HTTP dá significado.

GET /index.html HTTP/1.1
Host: localhost:8000
Connection: close
Accept: text/html

GET é o método: a ação “obtenha este recurso”. /index.html é o caminho. HTTP/1.1 escolhe a versão das regras. As linhas seguintes são cabeçalhos: pares de nome e valor que descrevem o pedido. A linha vazia anuncia que os cabeçalhos terminaram.

HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 1842

<!doctype html>...

A resposta também possui cabeçalhos. Depois da linha vazia vem o corpo: o conteúdo solicitado. Status é o resultado numérico: 200 significa sucesso, 404 significa que o recurso não foi encontrado e 500 indica falha no servidor. Content-Type diz qual tipo de conteúdo existe no corpo.

Agora escrevemos exatamente esse pedido no fluxo TCP:

use std::io::Write;

let request = format!(
    "GET {} HTTP/1.1\r\n\
     Host: {}\r\n\
     Connection: close\r\n\
     Accept: text/html\r\n\r\n",
    url.path,
    url.host,
);

stream.write_all(request.as_bytes())?;

format! encaixa caminho e host no texto. \r\n representa a quebra de linha exigida pelo HTTP/1.1. Duas quebras consecutivas geram a linha vazia. as_bytes() transforma o texto em bytes, pois TCP transporta bytes, não objetos Rust.

Depois lemos até o servidor fechar a conversa:

use std::io::Read;

let mut bytes = Vec::new();
stream.read_to_end(&mut bytes)?;

Vec<u8> é uma lista crescente de números de 0 a 255. Cada número ocupa um byte. Foi exatamente essa representação que usamos para investigar assinaturas e chunks em Como um PNG funciona por dentro. Neste ponto temos a resposta bruta inteira, mas ainda precisamos separar cabeçalhos e corpo.

09 /
A Resposta Chega como Fluxo, Não como Arquivo Pronto
O navegador pode começar a compreender antes do último byte.

Fluxo significa que os bytes podem ser lidos aos poucos enquanto chegam; não precisamos esperar um “arquivo pronto” aparecer. Navegadores profissionais já começam a analisar o início do HTML enquanto o restante ainda viaja. Nossa primeira versão guarda tudo para tornar a separação visível.

let split = bytes
    .windows(4)
    .position(|part| part == b"\r\n\r\n")
    .ok_or("resposta HTTP sem cabeçalho")?;

let head = &bytes[..split];
let body = bytes[split + 4..].to_vec();

windows(4) observa quatro bytes consecutivos de cada vez. Procuramos \r\n\r\n, as duas quebras que separam cabeçalhos e corpo. head recebe tudo antes do separador; body, tudo depois. O intervalo ..split termina antes da posição encontrada; split + 4 pula o próprio separador.

Nossa implementação usa Connection: close: o servidor encerra a conexão ao terminar a resposta. HTTP moderno também permite informar o tamanho, enviar blocos progressivos, comprimir conteúdo e reutilizar a mesma conexão. Essas otimizações ficam fora da versão 0.2.

10 /
Uma Página é uma Rede de Recursos
HTML costuma ser o mapa inicial, não o pacote completo.

Ao encontrar <link rel="stylesheet">, <img>, <script> ou uma fonte CSS, o navegador cria novas solicitações. URLs relativas são resolvidas a partir do endereço do documento.

RecursoComo é descobertoPara que serve
CSS<link> ou <style>aparência e layout
Imagem<img src>conteúdo visual
Fonte@font-facedesenho dos glifos
JavaScript<script>comportamento futuro

Essas dependências formam um grafo: um conjunto de itens conectados por relações. O HTML aponta para o CSS; o CSS pode apontar para uma fonte; ambos podem apontar para imagens. O navegador decide a prioridade, isto é, o que buscar primeiro. Também pode fazer solicitações concorrentes — várias viagens progredindo no mesmo período.

Cache HTTP não significa “guardar para sempre”

Cache HTTP é o reaproveitamento controlado de uma resposta. Cabeçalhos como Cache-Control: max-age=3600 informam por quanto tempo ela pode ser considerada fresca. Quando fica velha, o navegador pode revalidar: perguntar ao servidor se a versão mudou, usando identificadores como ETag. Uma resposta 304 Not Modified diz para reutilizar o corpo já guardado.

Cache-Control: max-age=3600
ETag: "pagina-v7"

If-None-Match: "pagina-v7"
HTTP/1.1 304 Not Modified

Cache DNS guarda temporariamente a correspondência entre nome e IP; cache HTTP guarda respostas de recursos. São aplicações da mesma ideia, mas não são a mesma memória nem obedecem às mesmas regras.

Redirecionamento inicia outra navegação

Respostas 301, 302, 307 ou 308 podem trazer um cabeçalho Location. O navegador resolve esse novo endereço e repete as etapas necessárias. Ele limita a quantidade de redirecionamentos para impedir ciclos como A → B → A.

Depois que a navegação termina, a entrada pode ser registrada no histórico. Voltar e avançar não significam obrigatoriamente baixar tudo novamente: o navegador pode restaurar um documento preservado ou refazer a navegação usando caches válidos. O Janela Browser 0.2 mantém uma única página e deixa histórico, revalidação e redirecionamentos como extensões claramente separadas.

11 /
Bytes Precisam Virar Texto
Codificação define como sequências numéricas representam caracteres.

O corpo HTTP é uma sequência de bytes. Um byte é apenas um número; sozinho, ele não sabe se representa a letra A, uma parte de um emoji ou um valor de imagem. A regra usada para converter números em caracteres chama-se codificação de texto. Esse é o mesmo limite entre dado bruto e significado que vimos em Como o computador sabe qual programa deve abrir um arquivo?.

UTF‑8 é uma codificação. Letras comuns podem ocupar um byte; caracteres como ç e emojis ocupam sequências maiores. Interpretar bytes com a codificação errada produz texto quebrado.

let source = String::from_utf8(response.body)
    .map_err(|_| "HTML não está em UTF-8")?;

String::from_utf8 tenta validar e converter o vetor de bytes. Se todos formarem UTF‑8 válido, recebemos uma String. Caso contrário, devolvemos um erro compreensível. A versão 0.2 escolhe essa limitação; navegadores profissionais mantêm regras de compatibilidade com outras codificações.

12 /
O Parser HTML Reconhece Peças no Texto
Sinais menores e maiores mudam o estado da leitura.

Agora temos texto, mas ainda temos apenas uma fila de caracteres. O computador não enxerga automaticamente que <h1> inicia um título e </h1> o encerra. Precisamos de um leitor que percorra o texto e reconheça a função de cada trecho.

Esse leitor chama-se parser. Fazer parse significa analisar uma entrada seguindo uma gramática e transformá-la numa estrutura. Já construímos um parser para código-fonte em Construindo uma linguagem de programação do zero em Rust; aqui a ideia é a mesma, mas a gramática é HTML.

<main>Olá <strong>mundo</strong></main>

TagInicial("main")
Texto("Olá ")
TagInicial("strong")
Texto("mundo")
TagFinal("strong")
TagFinal("main")

Esses resultados intermediários são tokens: peças já classificadas. Em vez de uma sequência de caracteres, agora temos “abertura de elemento”, “texto” e “fechamento de elemento”. O parser usa uma pilha para lembrar quais elementos ainda estão abertos:

let mut stack: Vec<Node> = vec![];

if opening_tag {
    stack.push(new_element);
}

if closing_tag {
    let finished = stack.pop();
    attach_to_parent(finished);
}

push coloca um elemento no topo da pilha. pop retira o último elemento aberto quando encontramos seu fechamento. Por isso, ao ler <main><h1>Olá</h1></main>, o h1 termina antes de main.

HTML real possui centenas de regras para documentos incompletos, atributos, comentários e marcação mal encaixada. Nosso parser cobre um subconjunto proposital: tags, texto e elementos vazios básicos. O limite fica explícito para não confundir um exercício didático com o parser completo do padrão HTML.

13 /
DOM é o Documento Organizado na Memória
Tags aninhadas tornam-se nós conectados.

Tokens dizem o que foi encontrado, mas não guardam sozinhos as relações. Precisamos registrar que o texto “Olá” está dentro de h1, que h1 está dentro de main e que main pertence ao documento.

Representamos essas relações com uma árvore. Cada item é um ; um nó pode ter filhos. O documento é a raiz, pois todos os outros nós descendem dele. Essa ideia de árvore também apareceu na AST da linguagem Pulso; a diferença é que a AST representa código, enquanto esta árvore representa um documento HTML.

O nome dessa representação é DOM, sigla de Document Object Model, ou Modelo de Objetos do Documento. “Objeto” aqui significa uma estrutura na memória com tipo, dados e filhos. O DOM existe antes de qualquer JavaScript.

HTML → DOMTexto linear ganha relações
DOCUMENTMAIN↙︎   ↘︎
H1OláPBem-vindo
RESULTADO FUTUROOlá

Bem-vindo

pub enum NodeKind {
    Document,
    Element { tag: String, attributes: Vec<(String,String)> },
    Text(String),
}

NodeKind descreve três possibilidades. Document é a raiz artificial. Element guarda o nome da tag e seus atributos. Text guarda conteúdo textual. O nó que envolve isso mantém os filhos:

pub struct Node {
    pub kind: NodeKind,
    pub children: Vec<Node>,
}

Para <h1>Olá</h1>, o parser cria um nó Element("h1") com um nó filho Text("Olá"). Essa é a primeira vez que o programa consegue perguntar “qual texto pertence a este título?” sem reler o HTML original.

14 /
CSS Separa Conteúdo de Aparência
Seletores encontram nós; declarações atribuem propriedades.
main { background: #102532; }
h1   { color: #ff7543; font-size: 32px; }

HTML informa que existe um título; não determina sozinho sua cor e tamanho. CSS, sigla de Cascading Style Sheets, é a linguagem usada para descrever a aparência. Uma regra possui duas partes:

  • Seletor: indica quais elementos serão encontrados, como h1.
  • Declarações: pares de propriedade e valor, como color: #ff7543.

Nosso parser CSS transforma cada regra em estruturas Rust:

pub struct Rule {
    pub selector: String,
    pub declarations: Style,
}

pub struct Style {
    pub color: Option<String>,
    pub background: Option<String>,
    pub display: Option<String>,
    pub font_size: Option<i32>,
}

Option significa que a propriedade pode existir ou não. Uma regra que define apenas cor não precisa inventar tamanho de fonte. Durante a leitura, dividimos o texto primeiro em regras e depois em declarações:

for declaration in body.split(';') {
    let Some((property, value)) = declaration.split_once(':') else {
        continue;
    };

    match property.trim() {
        "color" => style.color = Some(value.trim().into()),
        "font-size" => style.font_size =
            value.trim().trim_end_matches("px").parse().ok(),
        _ => {}
    }
}

O match escolhe como armazenar cada propriedade conhecida. Propriedades que ainda não implementamos são ignoradas. A coleção de regras resultante é chamada CSSOM, ou Modelo de Objetos do CSS: o CSS deixou de ser texto e virou dados consultáveis, assim como HTML virou DOM.

DOM ≠ CSSOM

DOM guarda conteúdo e relações do documento. CSSOM guarda regras de aparência. Eles se encontram somente na próxima etapa, quando calculamos qual estilo pertence a cada nó visível.

15 /
A Cascata Decide Qual Regra Vence
O mesmo elemento pode receber instruções de várias origens.

Um mesmo h1 pode combinar com várias regras. A palavra cascata nomeia o algoritmo que resolve essa disputa. Primeiro importam a origem e a importância; depois, a especificidade — quanto o seletor é preciso — e, em caso de empate, vence a regra que apareceu por último.

h1          { color: blue; }
.destaque   { color: orange; }

<h1 class="destaque">Olá</h1>

O seletor de classe .destaque é mais específico que o seletor de tag h1; portanto, a cor final é laranja. Herança é outra regra: algumas propriedades, como a cor do texto, podem passar do elemento pai aos filhos quando estes não definem um valor próprio.

Estilo calculado

O resultado final por elemento é chamado de computed style, ou estilo calculado. O desenho não quer saber quantas regras disputaram; precisa apenas da cor, tamanho e comportamento que venceram.

16 /
Nem Todo Nó do DOM Produz uma Caixa
A estrutura de renderização contém aquilo que participa do visual.

O DOM contém tudo que pertence ao documento, inclusive informações que não aparecem na tela. <title> dá nome à aba, <head> reúne metadados e display: none manda esconder um elemento. Nenhum deles precisa ocupar um retângulo visível.

Por isso criamos uma segunda árvore, voltada ao desenho: a render tree, ou árvore de renderização. Ela combina os nós visíveis do DOM com seus estilos calculados. Não é outra versão do HTML; é uma lista hierárquica do que realmente precisa gerar caixas e texto.

Texto gera fragmentos; imagens geram caixas substituídas; elementos podem produzir caixas de bloco ou participar de linhas. DOM descreve o documento. A árvore de renderização descreve o que precisa ser organizado visualmente.

17 /
Layout Calcula Tamanho e Posição
O conceito volta do artigo “Como um computador desenha uma janela”, agora guiado por CSS.

No artigo anterior, layout foi o cálculo que entregou posição e tamanho a labels e botões. Na web a pergunta é a mesma, mas CSS acrescenta regras: qual a largura disponível? O elemento é bloco ou inline? Há margem, borda e padding? O texto quebrou em quantas linhas?

BOX MODELQuatro regiões ao redor do conteúdo
margin → border → padding → contentespaço externo · contorno · respiro · conteúdo

Conteúdo é o texto ou a imagem. Padding é o respiro interno. Borda contorna padding e conteúdo. Margem é o espaço externo que separa a caixa de suas vizinhas.

Começamos com um layout vertical simples. Cada nova caixa usa a largura disponível e avança o cursor na direção Y:

let mut cursor_y = 24;

for node in visible_nodes {
    let height = measure(node, available_width);
    node.bounds = Rect::new(24, cursor_y, available_width, height);
    cursor_y += height + 12; // 12 pixels de espaço entre caixas
}

measure calcula a altura necessária antes de posicionar. Rect guarda x, y, largura e altura. Alterar a largura da janela pode mudar quebras de texto; por isso layout precisa ser calculado novamente.

18 /
A Display List é uma Receita de Pintura
Antes de colorir pixels, registramos comandos em ordem.
FillRect(fundo, azul_escuro)
DrawText(titulo, 32, laranja)
FillRect(botao, laranja)
DrawText("ABRIR", 14, azul_escuro)

A display list, ou lista de exibição, não é a imagem. É uma receita ordenada: “pinte este fundo; depois desenhe este texto”. Layout responde onde; a lista responde o que pintar e em qual ordem. Separá-los permite inspecionar os comandos antes de alterar qualquer pixel.

enum PaintCommand {
    FillRect { rect: Rect, color: Color },
    DrawText { origin: Point, text: String, color: Color },
}

enum representa alternativas. Cada comando carrega somente os dados necessários para ser executado mais tarde pelo Painter.

19 /
Pintura, Rasterização e Composição Não São Sinônimos
Cada etapa muda a representação do conteúdo.
  • Pintura: decide quais comandos visuais executar e em qual ordem.
  • Rasterização: transforma formas e letras em pixels.
  • Composição: combina camadas prontas na imagem final.

Painter não é um conceito misterioso do sistema: é apenas o nome da nossa estrutura Rust que executa os comandos de pintura. Ela recebe uma Surface, isto é, uma área de memória que guarda os pixels da imagem.

RGBA descreve quatro números por pixel: vermelho, verde, azul e alfa. Alfa mede a opacidade: 255 é totalmente opaco; 0 é transparente. Já desmontamos essa representação visualmente em Como um PNG funciona por dentro.

pub fn fill_rect(&mut self, rect: Rect, color: Color) {
    for y in rect.y..rect.y + rect.height {
        for x in rect.x..rect.x + rect.width {
            self.surface.blend_pixel(x, y, color);
        }
    }
}

O laço externo percorre as linhas do retângulo; o interno percorre suas colunas. Para cada coordenada, blend_pixel mistura a nova cor com a existente usando alfa. Essa transformação de formas em pixels chama-se rasterização.

Por fim, Wayland entrega o buffer ao compositor do Ubuntu, o programa que combina nossa janela com as demais janelas, sombras e cursor para formar a imagem final do monitor. A fronteira Wayland e o compartilhamento do buffer foram construídos e explicados no artigo “Como um computador desenha uma janela”; aqui apenas os reutilizamos.

20 /
Uma Página Visível Ainda Precisa Responder
Links, foco e rolagem reutilizam eventos e hit testing.

Hit testing é o teste que descobre qual caixa contém a coordenada do ponteiro. Nós o construímos passo a passo no artigo “Como um computador desenha uma janela”. Aqui aplicamos o mesmo teste às caixas da página. Se o alvo é um link, guardamos seu endereço. Pressionar e soltar dentro inicia nova navegação. Tab percorre elementos que podem receber foco. A roda altera o deslocamento vertical.

posição_na_página.y = posição_na_janela.y + scroll_y

se link.bounds.contains(posição_na_página) {
    navigation.start(link.href)
}

Viewport é a área interna da janela onde a página pode aparecer. Rolagem não move o documento na memória; muda qual faixa das coordenadas da página é projetada dentro desse recorte visível.

21 /
Onde o JavaScript Entrará — Mas Ainda Não Hoje
Encontrar um script é diferente de compreendê-lo e executá-lo.

Ao encontrar <script>, um navegador real entrega o texto a outro subsistema. Ele precisará reconhecer tokens, construir uma árvore sintática e gerar bytecode, instruções compactas para uma máquina virtual. Construímos esse percurso antes em Construindo um compilador e uma máquina virtual do zero em Rust.

A memória dos objetos também precisará ser recuperada. O componente que encontra objetos inalcançáveis chama-se garbage collector; ele foi o protagonista de Quem limpa a memória?. No próximo artigo reconstruiremos essas ideias especificamente para JavaScript. Aqui a tag <script> é apenas reconhecida, nunca executada.

22 /
Um Navegador Também é uma Fronteira de Segurança
Conteúdo remoto não pode receber confiança automática.

HTML, CSS, imagens e scripts vêm de terceiros. Um navegador profissional valida formatos, limita acesso a arquivos e dispositivos e isola documentos. Uma origem é a combinação de esquema, host e porta. Assim, https://loja.test e https://banco.test são origens diferentes.

A same-origin policy, ou política de mesma origem, impede que uma página leia livremente dados privados de outra origem. Sem essa regra, uma aba maliciosa poderia tentar acessar informações da aba do banco. Sandbox é outro limite: o conteúdo executa dentro de uma área controlada, com capacidades restritas.

Um cookie é um pequeno dado associado a um domínio e enviado em pedidos compatíveis. Ele pode manter uma sessão autenticada, por isso atributos como Secure, HttpOnly e SameSite limitam quando esse dado viaja ou pode ser acessado. Nosso projeto não implementa cookies: aceitar conteúdo remoto sem implementar corretamente essas regras criaria uma falsa sensação de segurança.

Nosso navegador didático não executa scripts, não aceita HTTPS e deve abrir páginas locais controladas. Isso reduz o campo de teste; não transforma o projeto num navegador seguro para uso diário.

23 /
Construindo o Janela Browser 0.2
Cada módulo responde a uma pergunta observável.

Esta não é a hora em que o código surge pronto. É o mapa do que já construímos durante o artigo: cada módulo reúne uma etapa que foi apresentada, testada e conectada à seguinte.

janela-browser/
├── src/
│   ├── wayland.rs   # janela e apresentação
│   ├── url.rs       # separa o endereço
│   ├── http.rs      # DNS, TCP e HTTP/1.1
│   ├── html.rs      # texto → nós
│   ├── dom.rs       # árvore do documento
│   ├── css.rs       # regras e declarações
│   ├── document.rs  # coordena o carregamento
│   ├── layout.rs    # tamanhos e posições
│   ├── painter.rs   # comandos → pixels
│   └── app.rs       # navegação e estado
└── tests/
    ├── url.rs
    ├── html.rs
    ├── pixels.rs
    └── events.rs

A função que conecta as peças

pub fn load(input: &str) -> Result<Page, String> {
    let url = Url::parse(input)?;              // caracteres → endereço
    let response = http::get(&url)?;           // endereço → bytes HTTP

    if response.status >= 400 {
        return Err(format!("HTTP {}", response.status));
    }

    let source = String::from_utf8(response.body)
        .map_err(|_| "HTML não está em UTF-8")?;
    let dom = html::parse(&source);             // texto → árvore
    let title = find_title(&dom)
        .unwrap_or_else(|| url.host.clone());

    Ok(Page { url, dom, title })
}

load é o coordenador, não um novo mecanismo. As quatro primeiras linhas chamam etapas que já entendemos. Result<Page, String> diz que a função devolve uma página ou uma mensagem de erro. O ? interrompe a sequência exatamente no ponto que falhou, preservando uma explicação útil.

Executar com uma página controlada

Primeiro usamos um servidor local. Assim sabemos qual HTML será enviado e podemos observar falhas sem depender da Internet.

# terminal 1: serve os arquivos da pasta atual
python3 -m http.server 8000

# terminal 2: abre a página em nosso navegador
cargo run -- http://localhost:8000/
O projeto é uma continuação real

Surface, Painter, widgets, eventos e Wayland vieram do artigo “Como um computador desenha uma janela”. Não recomeçamos a GUI: acrescentamos URL, rede e documento sobre a fundação existente.

24 /
Testar Sem Depender da Internet
Entradas controladas tornam falhas reproduzíveis.

Testamos URL com strings conhecidas, parser com pequenos documentos e desenho com superfícies em memória. Para HTTP, um servidor local pode devolver respostas fixas, incluindo 200, 404 e cabeçalhos incompletos.

CamadaEntradaAfirmação
URLlocalhost:8000/docshost, porta e caminho corretos
HTTPresposta 200 conhecidasepara cabeçalho e corpo
HTML<h1>Olá</h1>árvore contém h1 e texto
Layoutviewport 640×400caixas não ultrapassam limites
Pinturacaixa laranjapixels esperados mudam
25 /
O que um Navegador Profissional Ainda Precisa Resolver
Nosso modelo revela a fundação; a plataforma web inteira é muito maior.
ÁreaJanela Browser 0.2Navegador profissional
RedeHTTP/1.1 localHTTPS, HTTP/2, HTTP/3, proxy, cache
HTMLsubconjunto bem formadoalgoritmo completo e recuperação de erros
CSSseletores e propriedades básicoscascata, fontes, grid, flex e centenas de propriedades
Mídiatexto e caixasimagens, áudio, vídeo, SVG, canvas
Segurançapáginas locais confiáveissandbox, origens, permissões e isolamento
Scriptapenas reconhece a tagparser, VM, GC, runtime e JIT
26 /
Perguntas Frequentes
Dúvidas que aparecem quando uma página deixa de ser mágica.
DNS baixa o site?

Não. DNS apenas ajuda a transformar o nome do host em endereço de rede. A solicitação do documento acontece depois.

TCP e HTTP são a mesma coisa?

Não. TCP oferece um fluxo ordenado de bytes. HTTP define o significado desses bytes como pedidos, respostas, cabeçalhos e corpos.

DOM é a tela?

Não. DOM representa o documento na memória. Estilo e layout ainda precisam selecionar elementos visíveis, calcular caixas e pintá-las.

Por que o projeto não abre HTTPS?

Porque TLS correto envolve criptografia, certificados e validações críticas. A versão didática usa um servidor HTTP local para que os bytes permaneçam observáveis sem fingir segurança.

O navegador já executa JavaScript?

Ainda não. Ele reconhece que scripts existem, mas o motor será construído a partir do artigo “Como o navegador entende e executa JavaScript”.

Estamos reimplementando a GUI?

Não. Janela UI 0.1 é a base do projeto. O novo código acrescenta navegação, rede e documento.

27 /
Agora Enter Possui um Caminho Completo
Endereço, rede, documento, caixas e pixels conectados.

Pressionamos Enter. A URL foi separada. O nome encontrou um endereço. TCP abriu um fluxo. HTTP pediu o documento. Bytes viraram texto; texto virou DOM; CSS atribuiu aparência; layout calculou caixas; pintura produziu comandos; o Painter rasterizou pixels; Wayland entregou a superfície ao compositor.

O MAPA FINALDa barra de endereço à página
01URL + DNSlocalizar
02TCP + HTTPtransportar
03DOM + CSScompreender
04Layout + Paintmostrar
Próximo artigo

A página chegou e foi desenhada. Agora abriremos a caixa que deixamos fechada: como o navegador compreende JavaScript e transforma código-fonte em instruções executáveis.

PROJETO COMPLETO · CÓDIGO DO CAPÍTULO

Execute e investigue o Janela Browser 0.2.

O pacote evolui a GUI anterior com parser de URL, resolução de nome, TCP, HTTP/1.1, parser HTML, DOM, regras CSS, janela Wayland com dois buffers e testes. Use uma página HTTP local para observar cada byte com segurança.

Baixar código final deste artigo (.zip)Rust · URL · HTTP · HTML · CSS · Wayland

O que este artigo fez você pensar?

Dúvidas, experiências e contrapontos ajudam a próxima pessoa a enxergar o assunto por outro ângulo.

Todos passam por moderação. Ao enviar, você concorda com a política de privacidade.

Receba os próximos artigos.

Uma mensagem quando uma nova investigação estiver pronta. Só isso.