Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions app/src/paused.css
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,12 @@
animation-play-state: paused !important;
}
/* biome-ignore-end lint/complexity/noImportantStyles: a hidden window must pause every animation, inline ones included */

/* The owner works in another app (shell/visibility.ts): the mascots and the
working trace, drawn by the CPU on every frame, stand still. Spinners,
live dots and what arrives keep moving: they are cheap and say something
happens. */
:root[data-blurred] [data-mood] *,
:root[data-blurred] .state-trace {
animation-play-state: paused;
}
10 changes: 10 additions & 0 deletions app/src/shell/visibility.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ describe("window visibility", () => {
expect(document.documentElement.hasAttribute("data-hidden")).toBe(false);
});

test("the mascots stand still while another app has the focus", () => {
const focus = vi.spyOn(document, "hasFocus").mockReturnValue(false);
window.dispatchEvent(new Event("blur"));
expect(document.documentElement.hasAttribute("data-blurred")).toBe(true);
focus.mockReturnValue(true);
window.dispatchEvent(new Event("focus"));
expect(document.documentElement.hasAttribute("data-blurred")).toBe(false);
focus.mockRestore();
});

test("the status is not polled while the window is hidden, and is read when it comes back", async () => {
vi.useFakeTimers();
const fake = new FakeBotloft();
Expand Down
16 changes: 15 additions & 1 deletion app/src/shell/visibility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,25 @@ export function onVisibility(listener: () => void): () => void {
return () => listeners.delete(listener);
}

/**
* Whether the window has the focus: while the owner works in another app,
* the mascots stand still (paused.css), so a Botloft left open in the
* background costs next to nothing.
*/
function focusChanged(): void {
document.documentElement.toggleAttribute("data-blurred", !document.hasFocus());
}

/** Starts following the window; once, before the app renders. */
export function watchVisibility(): void {
document.addEventListener("visibilitychange", changed);
window.addEventListener("focus", () => markHidden(false));
window.addEventListener("focus", () => {
markHidden(false);
focusChanged();
});
window.addEventListener("blur", focusChanged);
changed();
focusChanged();
}

export function useWindowVisible(): boolean {
Expand Down
2 changes: 1 addition & 1 deletion docs/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -808,7 +808,7 @@ app/src/

Componentes dependem só de `BotloftApi` e `Host`, nunca do cliente concreto nem do Tauri.

O store recarrega crews, bots e `system.status` a cada (re)conexão e depois segue as notificações. `system.status` não tem notificação e é relido a cada 15 s, menos com a janela fora de vista (minimizada, escondida perto do relógio ou aberta escondida no login do Windows): aí para, e é relido quando ela volta. Fora de vista, as animações também param onde estão (`paused.css`) e continuam quando a janela volta. O Windows pode não avisar a página quando o próprio app esconde a janela, então o app marca isso ele mesmo (`shell/visibility.ts`), e a janela voltar à frente desmarca.
O store recarrega crews, bots e `system.status` a cada (re)conexão e depois segue as notificações. `system.status` não tem notificação e é relido a cada 15 s, menos com a janela fora de vista (minimizada, escondida perto do relógio ou aberta escondida no login do Windows): aí para, e é relido quando ela volta. Fora de vista, as animações também param onde estão (`paused.css`) e continuam quando a janela volta. O Windows pode não avisar a página quando o próprio app esconde a janela, então o app marca isso ele mesmo (`shell/visibility.ts`), e a janela voltar à frente desmarca. Com a janela à vista mas sem o foco (o dono trabalha em outro app), os mascotes e o traço de "trabalhando" param onde estão, porque o WebView os redesenha na CPU a cada quadro: um Botloft aberto atrás de outras janelas custa quase nada. Spinners, pontos ao vivo e o que chega continuam. Medido no WebView2 com bots parados: cerca de 1 núcleo com os mascotes animando, 0,01 com eles parados. Enquanto alguma animação roda, o WebView produz um quadro por atualização da tela (180 Hz no monitor do dono), então limitar os quadros de cada animação (`steps`) não muda o custo; parar é o que muda.

O chat e as mensagens de uma equipe guardam todas as páginas já carregadas. As linhas fora de vista não são diagramadas nem pintadas até chegarem perto da tela (`content-visibility: auto`, em `offscreen.css`), e guardam o tamanho que tiveram para a barra de rolagem não pular. As 8 mais novas ficam de fora: é onde os cartões chegam com o brilho de atenção, que a contenção cortaria.

Expand Down
Loading