Skip to content

fix(desktop): corrige pacing e corridas na animação de bounds da janela - #13

Merged
RenanMev merged 1 commit into
mainfrom
fix/window-animation-pacing
Aug 12, 2026
Merged

fix(desktop): corrige pacing e corridas na animação de bounds da janela#13
RenanMev merged 1 commit into
mainfrom
fix/window-animation-pacing

Conversation

@RenanMev

@RenanMev RenanMev commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Aplica a revisão focada no loop de run_animation (apps/desktop/src-tauri/src/lib.rs).

Contexto: as correções tinham sido escritas antes do merge do #12 mas se perderam — só a feature Win32_Media no Cargo.toml e o fix de borrow em capture.rs entraram. Hoje main declara Win32_Media sem nada que use. Este PR reaplica o resto.

Bugs corrigidos

1. Contador de frame acumulava atraso e virava rajada. O deadline era absoluto, mas o índice do frame era incrementado em vez de derivado do relógio. Um frame que estourasse o orçamento (com relayout do WebView2, acontece) deixava frame atrás do tempo: checked_duration_since retornava None várias iterações seguidas e o loop rodava sem dormir até recuperar. Agora o índice sai de elapsed / FRAME_INTERVAL + 1 — frame perdido é pulado, não recuperado.

2. Resolução do timer no Windows. thread::sleep vira Sleep(), que arredonda para o tick do sistema (~15,6ms por padrão): um sleep de 16,6ms podia virar 31,2ms. Isso sozinho explica período de frame variável na faixa dos 40fps. Guard RAII com timeBeginPeriod(1)/timeEndPeriod(1) em volta da animação.

3. Frames que não mudavam nada. Na cauda do ease_out_cubic vários frames seguidos arredondam para os mesmos pixels, e o frame em t = 0 reaplicava os bounds atuais. Cada um custava um relayout completo do DOM de graça. Agora só aplica quando os bounds mudam.

4. Corrida entre set_window_bounds e um frame em voo. A checagem de geração e o SetWindowPos não eram atômicos: set_window_bounds podia entrar no meio e aplicar os bounds finais, e o frame já calculado pousava por cima. A animação reportava cancelamento corretamente, mas a janela ficava parada num estado intermediário. As duas mutações agora compartilham um mutex.

5. SetWindowPos cross-thread era síncrono. A animação roda em spawn_blocking, numa thread que não é dona da HWND, então cada frame bloqueava até o message pump processar o relayout inteiro do WebView2. Os frames agora usam SWP_ASYNCWINDOWPOS.

6. Erro no meio deixava a janela no meio do caminho. Agora faz snap no alvo antes de propagar o Err.

8. TargetBounds e WorkArea eram o mesmo shape. Unificados em Bounds — é o PartialEq dele que viabiliza o item 3.

Desvio consciente da revisão (item 5)

A revisão sugeria só adicionar SWP_ASYNCWINDOWPOS às flags. Misturar chamadas síncronas e enfileiradas na mesma HWND não é seguro: SetWindowPos síncrono cruza a thread como mensagem enviada, e mensagens enviadas passam na frente das postadas, então uma chamada síncrona posterior ultrapassa frames já na fila. A implementação aqui:

  • todos os frames da animação são enfileirados (FIFO entre si, ordem preservada);
  • set_window_bounds segue síncrono — o front depende disso, floating-quick-menu-mode.ts:177 só dispara o morph de CSS depois que a janela está no tamanho final — e faz um repost enfileirado dos mesmos bounds em seguida, que cai atrás de qualquer frame velho. Bounds idênticos não geram WM_SIZE, então o repost não custa relayout.

Não incluído

  • Item 7 (caminho não-Windows). set_bounds(Rect) não serve: em tauri-2.11.5/src/webview/mod.rs:1509 ele é o Webview::set_bounds, que mexe na área de cliente do webview, não no frame da janela. Ficou documentado no código, junto com o apontamento de que o caminho nativo no macOS é NSAnimationContext. A implementação AppKit precisa de deps objc2 e não dá para compilar/verificar neste ambiente.
  • A mudança arquitetural (janela transparente + card animado por CSS). Vale notar que ela já está feita para a transição principal: a ilha flutuante usa um único SetWindowPos + morph em CSS. animate_window_bounds sobrou em dois lugares — enterFloatingMode (login → flutuante) e o passo moveFirst da expansão. Matá-los é o próximo passo natural, e aí os itens 1-6 somem junto.
  • Instrumentação de desvio por frame (Instant::now() - deadline).

Verificação

  • cargo check --lib limpo.
  • 42 testes de front que exercitam esses comandos passam (window-animation, floating-compact-bounds, window-work-area, enter-floating-mode, BarApp).
  • Sem medição em app real — os ganhos de 1, 2, 3 e 5 são argumentados, não medidos.

🤖 Generated with Claude Code


Note

Medium Risk
Altera comportamento nativo de janela e threading Win32 (SetWindowPos síncrono vs enfileirado); impacto visível em transições, mas escopo concentrado em lib.rs com contrato explícito para o front.

Overview
Reaplica e endurece a animação nativa de resize/move da janela principal no Tauri (lib.rs), com bump de versão do crate desktop para 0.1.4.

Pacing e custo por frame: o índice de frame passa a ser derivado do tempo decorrido (frames atrasados são pulados em vez de virarem rajada de SetWindowPos). No Windows, um guard RAII com timeBeginPeriod(1) estabiliza thread::sleep. Frames só disparam resize quando os bounds interpolados mudam de fato (Bounds unificado com PartialEq, no lugar de TargetBounds/WorkArea).

Concorrência e Win32: mutações de bounds compartilham WINDOW_MUTATION; geração da animação e aplicação de frame ficam atômicas. Frames usam SWP_ASYNCWINDOWPOS; set_window_bounds continua bloqueante (contrato do front) e reposta os mesmos bounds na fila para não perder para frames antigos. Erro no meio da animação faz snap no destino antes de propagar o erro.

Reviewed by Cursor Bugbot for commit 8145917. Configure here.

- Deriva o indice do frame do tempo decorrido em vez de incrementar: um
  frame que estourava o orcamento deixava o contador atras do relogio e o
  loop rodava varias iteracoes sem dormir, transformando um frame atrasado
  numa rajada de SetWindowPos.
- Sobe a resolucao do timer do sistema para 1ms enquanto a animacao roda
  (timeBeginPeriod/timeEndPeriod via guard RAII). Sleep() arredonda para o
  tick de ~15,6ms por padrao, o que sozinho explica periodo de frame
  variavel na faixa dos 40fps.
- Pula frames que arredondam para os mesmos pixels. Na cauda do ease-out
  eles sao a maioria e cada um custa um relayout completo do WebView2.
- Serializa checagem de geracao e SetWindowPos sob um mutex, para um frame
  em voo nunca pousar depois dos bounds finais de set_window_bounds.
- Enfileira os frames com SWP_ASYNCWINDOWPOS em vez de bloquear na fila de
  mensagens da thread dona da HWND. set_window_bounds segue sincrono (o
  front depende disso) e reposta os bounds atras de frames ja enfileirados,
  porque mensagens enviadas passam na frente das postadas.
- Faz snap no alvo antes de propagar erro no meio da animacao.
- Unifica TargetBounds e WorkArea em Bounds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@cursor

cursor Bot commented Aug 12, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_1736078c-8194-45f4-996d-c4e82e37c568)

@RenanMev
RenanMev merged commit 3589225 into main Aug 12, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant