ADRIANOLAUREANO← Artigos
INTERFACES GRÁFICASPIXELSEVENTOSRUST

Como um computador desenha uma janela?
Construindo uma pequena GUI do zero em Rust

Vamos começar com um processo que não mostra nada e terminar com uma janela funcional: painel, texto e botão desenhados por nós, respondendo a mouse e teclado. Nenhum termo técnico será tratado como óbvio: primeiro veremos a ideia, depois daremos um nome a ela e só então escreveremos o código.

ARTIGO 16 · DA JANELA AOS PIXELS

1superfície Wayland

4peças visuais

1ciclo de eventos

LABORATÓRIO DE PIXELS · UMA JANELA DESMONTADA EM CAMADAS
Ilustração técnica colorida mostrando uma janela de interface gráfica desmontada em camadas, grade de pixels RGBA, buffers, eventos do mouse, compositor e monitor

VOCABULÁRIO VISUAL

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

01RGBAquatro números: vermelho, verde, azul e transparência
02Surfacea folha de pixels guardada na memória
03Painternosso estojo de funções de desenho
04Widgetuma peça da interface, como texto ou botão
05Hit testinga procura pela peça que está sob o cursor
06Event loopo ciclo que espera entradas e atualiza a janela
Você não precisa memorizar agora. Este mapa existe para consultar sempre que um termo reaparecer.
01 /
De Onde Surge uma Janela?
Rust não possui uma instrução chamada desenhe um botão.

Quando escrevemos println!, o terminal já sabe exibir caracteres. Uma interface gráfica começa um nível antes: precisamos pedir ao sistema operacional uma área na tela, manter um buffer de pixels, interpretar entradas, organizar componentes e redesenhar quando alguma coisa muda.

Nossa pergunta central será concreta: quem cria a janela, quem decide onde ela fica e como um retângulo colorido termina como luz no monitor? A resposta não cabe numa única peça. A CPU é o processador geral que executa as instruções do programa; a GPU é um processador especializado em operações gráficas paralelas. Elas cooperam com o sistema operacional, o compositor e o monitor, cada qual com um contrato diferente.

O PERCURSO COMPLETODa ideia ao monitorAgora em linguagem comum; aprenderemos os nomes técnicos durante o caminho.
01Decidiro que deve aparecer
02Desenharformas, texto e botão
03Guardara imagem na memória
04Mostrara imagem no monitor

Resultado deste capítulo: a Janela UI 0.1, um projeto pequeno que abre uma janela, organiza texto e botão, desenha cada pixel e responde a mouse e teclado.

A regra do projeto

Não usaremos toolkit ou biblioteca gráfica que entregue uma janela pronta. As crates de Wayland servem apenas para codificar e transportar as mensagens do protocolo oficial; superfície, memória de pixels, desenho, widgets, layout, foco e tratamento de eventos continuarão no nosso código. Quando aparecer uma palavra em inglês, ela virá acompanhada da tradução e do motivo de existir.

02 /
CLI, TUI e GUI Não São Apenas Aparências Diferentes
Cada modelo organiza entrada, saída e estado de uma maneira própria.

Uma CLI recebe uma linha, executa e produz texto. Uma TUI continua dentro do terminal, mas posiciona caracteres numa grade. Uma GUI mantém uma cena visual: janelas, painéis, textos e botões continuam existindo entre uma entrada e outra.

ModeloUnidade de saídaEntrada comumEstado visual
CLIlinha de textocomando + Enterhistórico sequencial
TUIcélula do terminalteclasgrade atualizada
GUIpixel e componentemouse, toque, tecladoárvore persistente

Quando a aplicação é iniciada, o sistema cria um processo. A thread principal abre a janela e entra num ciclo que permanece vivo enquanto ela existir. Se processo, thread, biblioteca e memória ainda parecem abstratos, vale retomar Como um computador executa um programa por dentro.

Para quem está começando

Uma aplicação não “vira” uma janela. O processo possui uma janela, recebe mensagens relacionadas a ela e entrega imagens para que o sistema as apresente.

03 /
O Sistema de Janelas
Nossa aplicação pede uma superfície; outra camada administra a tela compartilhada.

Se qualquer processo pudesse escrever livremente em qualquer ponto do monitor, uma aplicação apagaria a outra. No Ubuntu que usaremos neste artigo, o Wayland organiza essa convivência. Wayland é um protocolo — um conjunto de mensagens e regras — entre a aplicação cliente e o compositor. Cada aplicação entrega sua própria superfície, e o compositor decide como reuni-la com as demais na tela. A especificação oficial do protocolo Wayland descreve exatamente esses objetos.

O window manager decide posicionamento, tamanho, foco e decoração. O compositor recebe superfícies das aplicações, aplica posição, sombras, transparência e sobreposição e produz a cena final. Dependendo da plataforma, essas peças podem estar mais separadas ou integradas.

ANATOMIA DA JANELADecoração, área cliente e superfícieA aplicação controla principalmente o interior; o sistema administra a janela no desktop.
Janela UI 0.1
ÁREA CLIENTE
SURFACE 640 × 400conteúdo desenhado pelo aplicativo
01 decoração do sistema02 viewport da aplicação03 memória de pixels

Para acompanhar esse caminho sem esconder etapas atrás de uma biblioteca gráfica, usaremos o Ubuntu 26.04 LTS com Wayland como ambiente de referência. Nosso código criará a superfície da janela, reservará uma área de memória compartilhada para os pixels, receberá mouse e teclado e entregará cada imagem pronta ao compositor.

Por que escolher uma plataforma?

Não existe, na biblioteca padrão do Rust, uma função universal para abrir janelas. Cada sistema possui seu próprio contrato. Escolher o Wayland usado pelo nosso Ubuntu deixa cada etapa concreta e evita esconder diferenças importantes atrás de uma biblioteca gráfica multiplataforma.

Usaremos wayland-client e wayland-protocols para representar e transportar as mensagens oficiais do protocolo. Uma crate é um pacote de código Rust. Essas crates não fornecem janela pronta, botão, texto, layout ou desenho. A criação dos objetos Wayland, o buffer de pixels e toda a GUI continuam no nosso código.

[dependencies]
wayland-client = "0.31"
wayland-protocols = { version = "0.32", features = ["client"] }
tempfile = "3" # arquivo temporário usado pela memória compartilhada
04 /
Do Monitor ao Pixel Lógico
Resolução física, DPI e escala não significam a mesma coisa.

Um monitor possui uma matriz física de pontos luminosos. Uma resolução de 1920 × 1080 descreve quantos pixels físicos existem na imagem exibida. A interface, porém, pode trabalhar com pixels lógicos. Em escala de 200%, uma caixa de 100 unidades lógicas ocupa aproximadamente 200 pixels físicos.

Essa separação existe porque a mesma quantidade física de pixels pode ocupar tamanhos muito diferentes. Sem escala, texto confortável num monitor antigo ficaria minúsculo numa tela moderna de alta densidade.

TermoO que medeExemplo
Resoluçãograde da imagem física2560 × 1440
DPI/PPIdensidade de pontos220 pixels por polegada
Escalaconversão lógico → físico150%
Viewportárea disponível ao conteúdo640 × 400 unidades
A consequência prática

Nossa primeira versão usa uma coordenada lógica para um pixel do buffer. Uma GUI madura recebe o fator de escala do sistema e rasteriza com a densidade adequada.

05 /
RGBA: Uma Receita de Quatro Números para Cada Pixel
Antes do código, vamos entender o que as quatro letras querem dizer.

Uma tela não guarda “laranja” como palavra. Para cada pontinho da imagem, ela guarda uma pequena receita numérica. R é a quantidade de vermelho (red), G de verde (green) e B de azul (blue). Misturando essas três luzes obtemos a cor visível.

A quarta letra, A, vem de alpha e funciona como um controle de presença. Ela responde: “quanto desta cor deve aparecer sobre o que já estava atrás?”. Cada valor vai de 0 a 255 porque cabe em um byte: 0 significa nada; 255 significa o máximo.

Leia RGBA assim

rgba(237, 107, 50, 255) significa: muito vermelho, um pouco de verde, pouco azul e totalmente visível. Já rgba(237, 107, 50, 128) usa a mesma cor, mas com aproximadamente metade da presença.

UM PIXEL AMPLIADO32 bits organizados em quatro canaisA cor laranja do site escrita como bytes.
RED2371110 1101
GREEN1070110 1011
BLUE500011 0010
ALPHA2551111 1111
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Color {
    pub r: u8,
    pub g: u8,
    pub b: u8,
    pub a: u8,
}

impl Color {
    pub const ORANGE: Self = Self {
        r: 237, g: 107, b: 50, a: 255
    };
}

Por que escrever as letras nessa ordem? Porque nosso programa precisa combinar onde cada número ficará na memória. Escolhemos vermelho, verde, azul e transparência: RGBA. Outros sistemas podem escolher outra ordem, mas isso não muda a ideia. Manter cada canal com um nome evita trocas acidentais. Para aprofundar cor, transparência e pixels, consulte Como um PNG funciona por dentro.

06 /
Surface: a Folha de Papel que Fica na Memória
A imagem parece uma grade, mas a memória é uma longa sequência.

Imagine uma folha quadriculada. Cada quadradinho é um pixel e contém os quatro números RGBA que acabamos de conhecer. Nossa “folha” não é papel: é um espaço reservado na memória. Na computação gráfica, é comum chamar essa área de surface, palavra inglesa para “superfície”.

Há um detalhe importante: desenhamos pensando em linhas e colunas, mas a memória é como uma fita longa, com um byte depois do outro. Portanto, a Surface guarda largura, altura e a sequência de bytes. Ela também guarda o stride.

Stride sem mistério

Stride é o tamanho de uma linha da imagem na memória. Se a imagem tem 4 pixels de largura e cada pixel usa 4 bytes RGBA, cada linha usa 4 × 4 = 16 bytes. Para descer uma linha, avançamos 16 posições. Esse salto de 16 bytes é o stride.

FRAMEBUFFER 4 × 3Como (x, y) encontra quatro bytesO pixel destacado está na coluna 1, linha 1.
0,001,042,083,0120,1161,1202,1243,1280,2321,2362,2403,244
offset = y × stride + x × 41 × 16 + 1 × 4 = 20
fn offset(&self, x: i32, y: i32) -> Option<usize> {
    if x < 0 || y < 0
        || x >= self.width as i32
        || y >= self.height as i32 {
        return None;
    }

    Some(y as usize * self.stride + x as usize * 4)
}

pub fn put_pixel(&mut self, x: i32, y: i32, color: Color) {
    let Some(offset) = self.offset(x, y) else { return };

    self.pixels[offset..offset + 4]
        .copy_from_slice(&[color.r, color.g, color.b, color.a]);
}

A fórmula y × stride + x × 4 faz duas viagens: primeiro pula y linhas completas; depois avança x pixels dentro da linha. O × 4 existe porque cada pixel tem quatro bytes.

Antes de acessar a memória, o código pergunta se x e y estão dentro da folha. Se estiverem fora, retorna None e não desenha. Essa proteção também é chamada de clipping: ignorar a parte do desenho que cairia fora da área permitida.

07 /
Mistura por Transparência (Alpha Blending)
Uma cor translúcida combina com o fundo em vez de apagá-lo.

Se pintarmos com tinta sólida, a nova cor substitui a antiga. Mas pense em uma folha laranja transparente sobre uma mesa azul: vemos um pouco do laranja e um pouco do azul. O computador simula essa combinação calculando uma nova cor.

Esse cálculo se chama alpha blending: “mistura usando o alpha”. Chamaremos a cor que chega de origem e a cor que já estava no pixel de destino. Com alpha de 50%, pegamos metade de cada uma:

resultado = origem × alpha + destino × (1 − alpha)

Se um canal vermelho de valor 200, com alpha de 50%, cobre um fundo preto de valor 0, o resultado fica perto de 100: metade de 200 mais metade de 0. Repetimos a mesma conta separadamente para vermelho, verde e azul.

pub fn blend_pixel(&mut self, x: i32, y: i32, source: Color) {
    let Some(destination) = self.pixel(x, y) else { return };
    let alpha = source.a as u32;

    let mix = |src: u8, dst: u8| {
        ((src as u32 * alpha
          + dst as u32 * (255 - alpha) + 127) / 255) as u8
    };

    self.put_pixel(x, y, Color::rgba(
        mix(source.r, destination.r),
        mix(source.g, destination.g),
        mix(source.b, destination.b),
        255,
    ));
}
Simplificação consciente

Esta conta mistura valores diretamente em sRGB. Renderizadores rigorosos convertem para um espaço linear antes da mistura. Para uma GUI didática, o cálculo inteiro mantém o algoritmo observável.

08 /
Por que Desenhamos Escondido Primeiro
Uma imagem é exibida enquanto a próxima é preparada.

Imagine limpar a tela, desenhar o painel, depois o texto e por último o botão diretamente na imagem visível. Se o monitor atualizar no meio, o usuário verá um frame incompleto. Double buffering separa construção e apresentação.

Os nomes das duas imagens

Front buffer é a imagem da frente, que o usuário está vendo. Back buffer é a imagem de trás, ainda escondida, na qual desenhamos o próximo quadro completo. Buffer, aqui, significa apenas uma região de memória que guarda uma imagem.

DOIS BUFFERSUm aparece enquanto o outro é preparadoO momento de present troca a imagem completa.
FRONT · VISÍVELFRAME 41
ENQUANTO
BACK · OCULTOFRAME 42
VSync
PRESENTFRAME 42

Quando a imagem é trocada enquanto o monitor ainda está percorrendo a tela, a parte superior pode pertencer ao quadro antigo e a inferior ao novo. Surge uma “quebra” horizontal chamada tearing. VSync é a tentativa de sincronizar a troca com o instante em que o monitor terminou um ciclo. Triple buffering usa uma terceira imagem de reserva; pode dar mais liberdade ao programa, mas consome mais memória.

Nem toda plataforma expõe uma “troca de ponteiros” literal. No Wayland, o compositor envia wl_buffer.release quando terminou de usar um buffer. Nosso código mantém dois slots: desenha somente num slot livre, marca-o como ocupado depois do commit e só o reutiliza após esse evento. O conceito permanece: nunca escrever na imagem que outra camada ainda está lendo.

09 /
Painter: Nosso Estojo de Ferramentas de Desenho
Em vez de colorir pixel por pixel em todo lugar, criamos operações reutilizáveis.

Já sabemos acender um pixel, mas seria cansativo repetir milhares de vezes “ande por estas linhas e colunas” sempre que quiséssemos um botão. Precisamos de uma camada que transforme pedidos humanos, como “desenhe um retângulo laranja”, em muitos pixels.

Chamaremos esse ajudante de Painter, “pintor” em inglês. Ele não é uma ferramenta pronta da linguagem: é uma estrutura criada no nosso projeto. Recebe a Surface, nossa folha na memória, e oferece funções como preencher retângulo, desenhar borda e escrever letras.

Da intenção aos pixels

fill_rect significa “preencha este retângulo”. Internamente, ele percorre linhas e colunas e chama a mistura de cor em cada pixel. Essa conversão de formas em pixels recebe o nome de rasterização.

pub fn fill_rect(&mut self, rect: Rect, color: Color) {
    let Some(rect) = rect.intersect(self.clip) else { return };

    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);
        }
    }
}

pub fn stroke_rect(&mut self, r: Rect, t: i32, color: Color) {
    self.fill_rect(Rect::new(r.x, r.y, r.width, t), color);
    self.fill_rect(Rect::new(r.x, r.y + r.height - t, r.width, t), color);
    self.fill_rect(Rect::new(r.x, r.y, t, r.height), color);
    self.fill_rect(Rect::new(r.x + r.width - t, r.y, t, r.height), color);
}

Também podemos copiar uma imagem pronta para nossa folha. Essa cópia em bloco costuma ser chamada de blit. Carregar o arquivo implica abrir, identificar, decodificar e validar bytes; esse caminho conversa com Como um computador sabe qual programa abre um arquivo? e Construindo um sistema de arquivos do zero.

Texto não é uma sequência de retângulos comuns

Nossa fonte bitmap 5 × 7 associa cada caractere a sete linhas de bits. Bit ligado pinta um bloco; bit desligado deixa o fundo aparecer.

// Letra A em uma grade de 5 colunas × 7 linhas
'A' => [
    0b01110,
    0b10001,
    0b10001,
    0b11111,
    0b10001,
    0b10001,
    0b10001,
]

Isso ensina a passagem de símbolo para pixels, mas não resolve texto real: Unicode, fallback de fontes, ligaturas, kerning, shaping, escrita da direita para a esquerda e hinting ficam fora desta versão.

10 /
Quatro Sistemas de Coordenadas
O mesmo cursor possui valores diferentes conforme o referencial.
CONVERSÃO DE PONTOSDa tela ao interior do botãoSubtraímos a origem de cada camada.
TELA820, 460
− janela
JANELA320, 210
− painel
WIDGET280, 150
− botão
LOCAL40, 30
  • Tela: posição global no desktop.
  • Janela: posição dentro da área cliente.
  • Widget: posição relativa ao pai.
  • Local: posição dentro do próprio componente.

Se o hit testing comparar um ponto da tela com um retângulo local, o botão funcionará apenas por coincidência. Transformações explícitas tornam o sistema previsível.

11 /
Widget: Uma Peça da Interface
Painel, texto e botão obedecem ao mesmo conjunto de regras.

Widget é o nome tradicional para uma peça da interface gráfica. Um botão é um widget. Um texto é outro. Um painel que agrupa os dois também pode ser um widget. Pense em peças de montar: cada uma tem aparência e função próprias, mas todas sabem se encaixar no mesmo sistema.

Cada peça precisa responder a quatro perguntas: “qual tamanho você deseja?”, “onde você ficará?”, “o que fará quando receber uma entrada?” e “como será desenhada?”. No código, reunimos essas perguntas em um trait, um contrato que tipos diferentes podem cumprir.

pub trait Widget {
    fn measure(&self, constraints: Constraints) -> Size;
    fn layout(&mut self, bounds: Rect);
    fn bounds(&self) -> Rect;
    fn event(&mut self, event: Event) -> bool;
    fn paint(&self, painter: &mut Painter<'_>);
}
ÁRVORE RETIDAEstrutura lógica e resultado visualPais organizam; filhos medem, recebem eventos e pintam.
WINDOW640 × 400PANELpadding 48↙︎   ↘︎
LABELPRONTOBUTTONEXECUTAR
RESULTADOPRONTO

A janela guarda essas peças numa estrutura parecida com uma árvore: o painel é o “pai”; texto e botão são seus “filhos”. Como essa árvore continua guardada na memória entre uma ação e outra, o modelo é chamado de retained mode, ou “modo retido”. No immediate mode, ou “modo imediato”, o programa descreve novamente a interface a cada quadro. Para nosso projeto, guardar a árvore deixa mais fácil acompanhar o estado de cada botão.

12 /
Layout: Medir Antes de Posicionar
Cada peça pede espaço, mas precisa respeitar o espaço disponível.

Layout é o processo de calcular tamanho e posição. Imagine organizar livros numa prateleira: cada livro tem uma largura desejada, mas todos precisam caber no espaço real. Na GUI, o elemento pai informa ao filho um mínimo e um máximo; essas regras são chamadas de constraints, ou restrições.

O filho calcula o tamanho que gostaria de ter e o ajusta aos limites. Um texto considera quantas letras possui; um botão soma a largura do texto, o espaço interno e a borda.

pub struct Constraints {
    pub min: Size,
    pub max: Size,
}

pub fn constrain(self, size: Size) -> Size {
    Size {
        width: size.width.clamp(self.min.width, self.max.width),
        height: size.height.clamp(self.min.height, self.max.height),
    }
}
ConceitoOnde atuaExemplo
contentmiololargura do texto
paddingdentro da borda16 px ao redor
bordercontorno2 px
marginfora do widgetdistância externa
gapcontêinerespaço entre filhos

No layout vertical, y começa no topo; cada filho recebe um retângulo e y avança por altura + gap. No horizontal, o raciocínio se repete no eixo x. Redimensionar a janela executa medida e posicionamento novamente.

13 /
Descobrindo o Alvo do Mouse (Hit Testing)
A coordenada chega sozinha; nossa GUI precisa descobrir o que existe naquele ponto.

O sistema informa algo como “o ponteiro está em x = 312, y = 224”. Ele não conhece o botão que inventamos. Nossa GUI precisa fazer uma pergunta geométrica: qual retângulo visível contém esse ponto? Essa procura recebe o nome de hit testing, ou “teste de acerto”.

Começamos pela janela, descemos para o painel e então verificamos seus filhos. Quando elementos se sobrepõem, testamos primeiro o que foi desenhado por último, pois ele está visualmente na frente.

RASTREAMENTO DO CURSORUm ponto atravessa quatro perguntasO primeiro elemento visível que contém o ponto se torna o alvo.
312,224ponteiro
SIMdentro da janela?
SIMdentro do painel?
BUTTONalvo encontrado
pub fn contains(self, point: Point) -> bool {
    point.x >= self.x
        && point.y >= self.y
        && point.x < self.x + self.width
        && point.y < self.y + self.height
}

As bordas direita e inferior são exclusivas. Assim, dois retângulos adjacentes não reivindicam o mesmo pixel.

14 /
Click Não Vem Pronto do Mouse
É uma interpretação construída com movimento, pressão e soltura.

O dispositivo informa que um botão físico desceu ou subiu. A GUI cria estados semânticos:

NORMALEXECUTAR
HOVEREXECUTAR
PRESSEDEXECUTAR
FOCUSEDEXECUTAR
DISABLEDEXECUTAR
PointerDown dentro:
    pressed_inside = true
    state = Pressed

PointerUp:
    click = pressed_inside && cursor_ainda_esta_dentro
    pressed_inside = false
    state = Hover ou Normal

Pressionar dentro, arrastar para fora e soltar normalmente cancela. Essa regra simples mostra por que click é um gesto interpretado, não um único pacote vindo do hardware.

15 /
Teclado, Foco e Acessibilidade
Uma interface não está completa se depender do ponteiro.

Foco identifica o widget que receberá teclas. Tab avança pela ordem de foco; Shift+Tab retorna. Enter ou Espaço ativa o botão focado. O contorno em ciano torna esse estado observável.

Pixels não carregam significado. Um leitor de tela não deveria adivinhar que um retângulo laranja com texto é clicável. Por isso existe uma árvore de acessibilidade paralela à visual.

DUAS REPRESENTAÇÕESA mesma interface para visão e tecnologia assistivaGeometria não substitui semântica.
ÁRVORE VISUALPanelLabel: PRONTOButton: EXECUTAR
ÁRVORE ACESSÍVELgrouptext: “Pronto”button · activate · focused
AccessibilityNode {
    role: "button",
    name: "EXECUTAR",
    focused: true,
    disabled: false,
    action: Some("activate"),
}

A Janela UI representa esses dados, mas não implementa adaptadores para UI Automation, NSAccessibility, AT-SPI ou equivalentes. Declarar o limite evita confundir um modelo didático com suporte assistivo completo.

16 /
O Ciclo de Eventos Mantém a Janela Viva
A janela repete cinco passos enquanto estiver aberta.

Depois de mostrar a primeira imagem, o programa não pode simplesmente terminar. Ele precisa continuar esperando pelo próximo movimento, tecla, redimensionamento ou pedido para fechar. Essa repetição é o event loop, em português, “ciclo de eventos”.

Uma volta funciona assim: esperar uma ocorrência; encaminhar ao elemento correto; atualizar o estado; desenhar uma nova imagem se necessário; e apresentar essa imagem. Depois, tudo começa novamente.

UMA VOLTA COMPLETAO ciclo entre entrada e novo frameSe nada mudou, podemos voltar a esperar sem pintar.
01WAITaguarda evento
02DISPATCHencontra o alvo
03UPDATEmuda o estado
04PAINTrasteriza pixels
05PRESENTentrega o frame
while window.is_open() {
    let events = window.poll_events();

    for event in events {
        app.dispatch(event);
    }

    if app.needs_redraw() {
        app.layout();
        app.paint(&mut back_buffer);
        window.present(back_buffer.bytes());
    }
}

Esperar enquanto não há eventos reduz CPU e energia. Animações e temporizadores pedem novos quadros deliberadamente. Nosso laço didático faz uma pausa curta entre voltas e só reconstrói a interface quando alguma ação a marcou para redesenho.

17 /
Marcando o que Precisa Ser Redesenhado
“Invalidar” não é apagar: é avisar que a imagem antiga não serve mais.

Imagine que o botão era azul e, ao receber o mouse, deve ficar laranja. Os pixels guardados ainda mostram o azul. Dizemos então que aquela imagem foi invalidada: o estado mudou e o desenho antigo deixou de representar a verdade. Invalidar significa apenas marcar “precisa redesenhar”.

A estratégia mais simples é marcar a janela inteira e reconstruir todos os pixels. Chamaremos isso de redesenho total. É fácil de entender e difícil de errar, por isso o código final começa assim.

Uma otimização possível é marcar somente o retângulo onde houve mudança. Essa região é chamada de dirty rectangle, literalmente “retângulo sujo”: não está sujo de verdade; apenas contém pixels desatualizados. Reconstruir somente essas regiões é o redesenho parcial.

MESMO CLIQUE, TRABALHO DIFERENTERedesenho total versus parcialA otimização adiciona controle de sobreposição e transparência.
FRAME COMPLETO256.000 pixels
DIRTY RECTANGLE8.400 pixels

No exemplo, redesenhar tudo visita 256.000 pixels; redesenhar apenas a região alterada visita 8.400. Porém, “parcial” não significa simplesmente pintar o botão outra vez. Se ele tem transparência, sua cor depende do painel que está atrás. Precisamos primeiro reconstruir o pedaço correspondente do fundo e depois pintar o botão por cima. Quando várias áreas mudam, podemos juntá-las. Caso contrário, ficam rastros da imagem antiga.

Decisão de engenharia

A Janela UI 0.1 mantém uma flag global dirty. O artigo explica retângulos parciais, mas o código final prioriza correção e uma base mensurável.

18 /
Como Nossa Imagem Entra no Desktop
Depois que terminamos de desenhar, o sistema combina nossa janela com as outras.

Nossa Surface contém apenas o interior de uma janela. Mas o monitor precisa mostrar, ao mesmo tempo, papel de parede, barra do sistema, cursor e outras aplicações. O componente que reúne essas imagens em camadas é chamado de compositor.

COMPOSIÇÃO EM CAMADASVárias superfícies formam um desktopPosição, transparência e ordem são aplicadas antes do scanout.
APP Asurface
JANELA UIsurface RGBA
COMPOSITORcena final
GPU · DRIVER · SCANOUTMONITOR

A CPU calcula posições, descobre qual elemento recebeu a entrada e transforma formas em pixels. Nosso módulo wayland.rs copia essa imagem para um buffer compartilhado, conecta o buffer a uma wl_surface e confirma a atualização com commit. O compositor Wayland combina nossa superfície com as demais janelas e envia a cena pronta ao monitor.

A GPU pode ajudar a mover e combinar essas imagens. O driver é o software que conversa com o hardware gráfico. A leitura contínua da imagem final para alimentar o monitor é chamada de scanout. Esses detalhes variam entre plataformas; o importante é separar nosso desenho da composição do desktop. Para compreender como instruções e memória são processadas pela CPU, veja Como uma CPU funciona por dentro.

19 /
Construindo a Janela UI 0.1
Cada módulo possui uma responsabilidade verificável.
janela-ui/
├── Cargo.toml
├── README.md
├── src/
│   ├── main.rs
│   ├── wayland.rs
│   ├── lib.rs
│   ├── app.rs
│   ├── color.rs
│   ├── geometry.rs
│   ├── surface.rs
│   ├── painter.rs
│   ├── layout.rs
│   ├── event.rs
│   ├── widget.rs
│   └── widgets/
│       ├── mod.rs
│       ├── panel.rs
│       ├── label.rs
│       └── button.rs
└── tests/
    ├── pixels.rs
    ├── layout.rs
    ├── hit_test.rs
    └── events.rs

1. O estado da aplicação

pub struct App {
    panel: Panel,
    label: Label,
    button: Button,
    dirty: bool,
}

O clique incrementa o contador do botão, troca “PRONTO” por “EXECUTADO!” e muda a cor do painel. A aplicação coordena componentes; ela não desenha pixels diretamente.

2. Layout independente da janela nativa

pub fn layout(&mut self, width: i32, height: i32) {
    self.panel.layout(Rect::new(0, 0, width, height));

    let constraints = Constraints::loose(width - 96, height - 96);
    let label_size = self.label.measure(constraints);
    let button_size = self.button.measure(constraints);

    self.label.layout(Rect::new(48, 70,
        label_size.width, label_size.height));
    self.button.layout(Rect::new(48, 120,
        button_size.width.max(180), button_size.height));
}

3. Pintura respeita a ordem da árvore

pub fn paint(&mut self, surface: &mut Surface) {
    if !self.dirty { return; }

    let mut painter = Painter::new(surface);
    self.panel.paint(&mut painter);
    self.label.paint(&mut painter);
    self.button.paint(&mut painter);

    self.dirty = false;
}

O painel vem primeiro, portanto funciona como fundo. Label e botão aparecem por cima. Inverter a ordem cobriria os filhos.

4. Nós mesmos montamos a janela no Wayland

Wayland funciona como uma conversa entre objetos. Primeiro conectamos ao compositor. Em seguida, o registry, ou registro, anuncia quais recursos existem. Pedimos um wl_compositor para criar a superfície, um wl_shm para compartilhar pixels e um xdg_wm_base para transformar a superfície numa janela comum do desktop.

let connection = Connection::connect_to_env()?;
let display = connection.display();

// O registro anunciará compositor, memória, entrada e xdg-shell.
display.get_registry(&queue_handle, Globals);

Memória compartilhada significa que aplicação e compositor têm acesso ao mesmo bloco de bytes. Criamos dois arquivos de memória, um wl_shm_pool para cada um e recortamos dois wl_buffer de 640 × 400 pixels. Cada pixel usa o formato ARGB8888.

let pool = shm.create_pool(file.as_fd(), tamanho, qh, NoopIgnore);
let buffer = pool.create_buffer(
    0, 640, 400, 640 * 4,
    wl_shm::Format::Argb8888,
    qh, BufferHandler(slot_index),
);

Depois de desenhar, copiamos os pixels para um slot livre, anexamos o buffer à superfície e fazemos commit. Esse último pedido significa: “a nova versão da minha superfície está pronta”.

arquivo.write_all(&surface.to_bgra8888())?;
wl_surface.attach(Some(&buffer), 0, 0);
wl_surface.damage(0, 0, 640, 400);
wl_surface.commit();

Quando chega wl_buffer.release, marcamos o slot como livre. Essa confirmação é indispensável: memória compartilhada não autoriza a aplicação a alterar bytes enquanto o compositor ainda os utiliza.

O que as crates fazem — e o que não fazem

As crates Wayland codificam e decodificam mensagens do protocolo com segurança. Elas não abrem uma janela completa por uma única função e não sabem o que é nosso botão. O arquivo wayland.rs ainda precisa encontrar os objetos globais, criar superfície e buffer, responder às configurações, traduzir entradas e apresentar os pixels.

20 /
Testar uma GUI sem Olhar para a Tela
O núcleo é determinístico porque trabalha sobre memória.

Testes visuais manuais são úteis, mas não provam cada regra. Como a surface é um vetor, podemos testar o byte exato. Como hit testing usa retângulos, podemos testar bordas. Como o botão possui uma máquina de estados, podemos enviar eventos sintéticos.

#[test]
fn alpha_de_cinquenta_porcento_mistura_as_cores() {
    let mut surface = Surface::new(
        1, 1, Color::rgba(0, 0, 0, 255)
    );

    surface.blend_pixel(
        0, 0, Color::rgba(200, 100, 40, 128)
    );

    assert_eq!(
        surface.pixel(0, 0),
        Some(Color::rgba(100, 50, 20, 255))
    );
}
#[test]
fn clique_exige_press_e_release_dentro() {
    let mut button = Button::new("EXECUTAR");
    button.layout(Rect::new(10, 10, 120, 46));

    button.event(PointerDown { position: Point::new(20, 20) });
    assert_eq!(button.state(), ButtonState::Pressed);

    button.event(PointerUp { position: Point::new(20, 20) });
    assert_eq!(button.clicks, 1);
}
ArquivoContrato comprovado
pixels.rsoffset, leitura, escrita e alpha
layout.rsconstraints e gap vertical
hit_test.rsbordas inclusivas e exclusivas
events.rshover, pressed, release e click

Um passo adicional seria guardar pequenas surfaces conhecidas como snapshots. Se um algoritmo alterar pixels inesperados, a comparação denuncia a regressão.

21 /
O que uma GUI Profissional Ainda Precisa Resolver
Nosso projeto ensina a base; aplicações reais precisam ir além.
ÁreaJanela UI 0.1Sistema maduro
Textobitmap 5 × 7Unicode, shaping, fallback
Layoutconstraints e colunagrid, flex, scroll, baseline
RenderizaçãoCPU, RGBAGPU, batching, cache, shaders
Entradamouse e tecladoIME, toque, caneta, gestos
Acessibilidadenós semânticosadaptadores nativos completos
Desempenhodirty globalregiões, camadas e profiling

Também faltam clipboard, drag and drop, menus, múltiplas janelas, internacionalização, animações, temas, cursores, seleção de texto e integração refinada com o compositor. Agora, porém, cada uma dessas palavras aponta para um problema concreto, não para uma caixa-preta.

22 /
Perguntas Frequentes
As dúvidas que aparecem quando um botão deixa de ser “mágica”.
Usar wayland-client significa que não construímos a GUI do zero?

Não. A crate representa os objetos e transporta mensagens do protocolo Wayland; ela não fornece componentes de GUI. Nosso código cria superfície e memória compartilhada, desenha pixels, organiza componentes e interpreta as entradas. Ela cumpre um papel semelhante ao de uma biblioteca de rede que transporta bytes sem construir a aplicação.

Framebuffer é sempre um vetor que a aplicação pode escrever?

Não. Sistemas modernos podem expor buffers compartilhados, texturas ou superfícies gerenciadas. Nosso Vec<u8> é um modelo explícito do conteúdo que será apresentado.

A GPU desenha o botão?

Nesta versão, a CPU rasteriza o botão no buffer. A GPU pode participar da conversão, composição e apresentação. Outros toolkits enviam mais comandos diretamente para APIs aceleradas.

Por que click não é igual a PointerDown?

Porque pressionar e soltar são fatos físicos; click é uma interpretação. Se o usuário pressiona, arrasta para fora e solta, a ação normalmente deve ser cancelada.

Por que usar retained mode?

Ele deixa árvore, foco, estados e invalidação explícitos. Immediate mode também é válido e muito útil em ferramentas, mas exigiria outra organização do estado.

A Janela UI funciona em Windows, Linux e macOS?

Esta versão foi construída para o Wayland do Ubuntu 26.04 LTS. O núcleo de pixels, layout e eventos pode ser reaproveitado, mas outros ambientes exigiriam módulos de integração próprios.

23 /
Agora o Botão Tem um Caminho Completo
Movimento físico, evento, estado, pintura, memória e luz.

Quando o cursor entra no botão, o Wayland envia a posição do ponteiro. A procura pelo alvo encontra a peça da interface que contém aquele ponto. O botão muda para o estado “mouse por cima” e avisa que o desenho antigo não vale mais. O ciclo de eventos pede uma nova pintura. Nosso estojo de desenho transforma o botão em retângulos, bordas e letras. A folha guardada na memória recebe as receitas RGBA de cada pixel. Por fim, anexamos esse buffer à superfície e o compositor combina nossa janela com as outras.

O MAPA FINALUm clique visto de ponta a pontaNão há uma etapa chamada “faça a interface funcionar”. Há contratos pequenos conectados.
01Entradamouse e teclado
02Semânticahit, foco e click
03Desenholayout e painter
04Saídabuffer, compositor e monitor
O que construímos

Uma pequena GUI não é apenas uma janela que abre. É um sistema coerente de geometria, memória, eventos, estado, renderização e acessibilidade — pequeno o bastante para entendermos por inteiro.

PROJETO COMPLETO · CÓDIGO DO CAPÍTULO

Execute, teste e modifique a Janela UI 0.1.

O pacote contém a aplicação completa para Ubuntu 26.04 LTS com Wayland: cria a superfície e dois buffers compartilhados, espera wl_buffer.release antes de reutilizá-los, guarda pixels RGBA, desenha formas e texto, organiza painel e botão, interpreta mouse e teclado e inclui quatro conjuntos de testes.

Baixar código final deste artigo (.zip)Rust · Ubuntu 26.04 LTS · Wayland · testes

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.