Title-Bar Drag and Double-Click Gestures
desktop-apps specs/desktop-apps/title-bar-double-click.kmd
Como apps desktop Koder devem tratar arraste e duplo-clique na barra de título. Define o contrato cross-platform (Linux/Windows/macOS) e proíbe que o duplo-clique num botão dispare o maximize da janela. Implementação go-forward é **Kroma** (`window_titlebar` + o tier `WindowRequest`); os widgets Flutter do `engines/sdk/koder_kit` (`KoderTitleBar` + `KoderTitleBarFreeArea`) são **legado** (`stack-RFC-019`).
When this spec applies
All triggers
- Implementar barra de título customizada em app desktop Koder
- Adicionar duplo-clique pra maximizar/restaurar janela
- Adicionar comportamento de arraste no título
- Detectar / corrigir bug onde duplo-clique em botão da barra de título dispara comportamento da janela
- Abrir janela sem decoração (borderless) em app Kroma e desenhar a própria chrome
Specification body
Spec — Title-Bar Drag and Double-Click Gestures
Applicability
Toda variante desktop (Linux / Windows / macOS) de qualquer produto Koder que renderiza sua própria barra de título — kruze, kterm, hub desktop, kortex, mosaic, talk, drive, eye, koru desktop, sky, e adjacentes. Não se aplica a variantes mobile, TV, web ou CLI.
Vale independentemente do framework: o Required Behavior abaixo é o contrato, e tanto Kroma (go-forward) quanto Flutter (legado) têm de honrá-lo. Um app que abre a janela borderless — o caso normal de quem desenha a própria chrome — está no escopo por construção: ao desligar a decoração do compositor, ele assume a obrigação de fornecer arraste e maximizar que o usuário perdeu.
Required Behavior
A barra de título de uma janela Koder DEVE responder a:
- Arraste (pan) em qualquer ponto da barra que não seja capturado por um widget interativo (botão, dropdown, controle de janela, célula de tab) — move a janela.
- Duplo-clique em uma área livre explicitamente declarada (espaço vazio decorativo, texto de título, gap entre tab strip e controles) — alterna entre maximizado e restaurado.
- Duplo-clique em um widget interativo (botão fechar
Xda aba, botão+nova aba, dropdown▼, controle de janela minimize/maximize/close, qualquer botão custom) NÃO dispara o toggle de maximize. O widget interativo trata o duplo-clique conforme sua semântica própria (geralmente: disparaonTap, ou umonDoubleTapdistinto declarado pelo autor).
Implementation (Kroma — go-forward · Flutter legacy)
A implementação go-forward é Kroma (Flutter em sunset, stack-RFC-019): toda barra
de título nova nasce em Kroma. A seção Flutter mais abaixo é legado, mantida só para
os apps que ainda não migraram.
Go-forward (Kroma)
O Required Behavior acima é atendido por três peças do engines/sdk/kroma, e as três
já existem — não reimplementar regra de gesto por app (reuse-first.kmd):
Janela sem decoração.
run_app_with_options(app, title, app_id, WindowOptions { decorations: false })(app_runner.rs). Um app que desenha a própria chrome DEVE abrir borderless por aqui;decorations: trueé o default, então nenhum app existente muda de comportamento.Classificação do gesto — a metade pura, headless-testável (
window_titlebar.rs):let zone = titlebar_zone(x, bar_w, cell_h, account); // -> TitleZone let action = title_action(zone, is_double_click); // -> TitleActiontitlebar_zoneresponde "que zona da barra é este x";title_actionaplica a regra de no-hijack desta spec. Nenhuma das duas toca o windowing, então ambas são testáveis sem abrir janela.Execução no tier de janela.
TitleAction::window_request() -> Option<WindowRequest>converte a ação de barra na op de janela; o runner a aplica emabout_to_wait(set_maximized/set_minimized/drag_window/exit). Na camada declarativa, os controles são nóswindow_button(key, label, WindowRequest::…)— oon_presscomum éFn(&mut State)e não alcança a janela, por isso os controles de janela têm nó próprio.
Por que o hijack do duplo-clique é estruturalmente impossível em Kroma (e não apenas
evitado por disciplina, como em Flutter): não existe gesture arena. Não há despacho
ambiente que possa entregar o evento a um ancestral — title_action resolve a zona
primeiro, e uma zona de controle retorna a ação daquele controle independentemente
de is_double_click. O duplo-clique de maximizar só é alcançável a partir de
TitleZone::FreeArea. A regra 3 do Required Behavior é, em Kroma, uma propriedade do
código, não uma convenção que o autor precisa lembrar de honrar.
TitleAction é deliberadamente de tier misto — carrega ops de janela
(Drag/ToggleMaximize/Close) e ações de domínio do kterm (NewTab/SplitRight/
SplitDown/Account). Por isso window_request() devolve None para as últimas: uma
ação de domínio nunca deve escorrer para o tier de janela (senão o usuário pede um pane e
a janela redimensiona). Não colapsar os dois enums num só.
Gap conhecido — cluster de janela incompleto.
TitleZonehoje temMaximizeeClose, mas não temMinimize(a barra do kterm nunca precisou dele). O tier de janela já suporta a op (WindowRequest::Minimizeexiste e o runner a aplica), então o que falta é só a metade zona/paint. Um app que precise do cluster completo minimize/maximize/close DEVE estendertitlebar_zone(e o paint correspondente) em vez de desenhar um botão de minimizar por fora da classificação — um controle que o classificador não conhece cai emFreeAreae o duplo-clique nele vira maximize, que é exatamente o bug que esta spec proíbe.
Legacy (Flutter, migrando) — engines/sdk/koder_kit
⚠️ Flutter está em sunset Stack-wide (stack-RFC-019) — não iniciar barra de título
nova aqui. Esta seção descreve o mapeamento Flutter do mesmo contrato, para os apps
ainda não migrados:
KoderTitleBar({child, height, color})— wrapper externo da barra de título inteira. Declara apenas o gesto de arraste (onPanStart: (_) => windowManager.startDragging()). Não declaraonDoubleTap— o gesto de toggle vive emKoderTitleBarFreeArea.KoderTitleBarFreeArea({child})— wrapper que marca uma região como área livre. DeclaraonDoubleTape disparawindowManager.maximize() / unmaximize()conforme estado atual. Usebehavior: HitTestBehavior.opaque. Posicione emExpanded(SizedBox.expand())para o gap entre tab strip e controles, ou em volta de texto de título decorativo.
Layout canônico (kruze, kterm, hub):
KoderTitleBar(
child: Row(
children: [
// Tab strip + botões internos (sized to content).
Flexible(child: ReorderableListView.builder(...)),
// Área livre — duplo-clique aqui alterna maximize/restore.
Expanded(
child: KoderTitleBarFreeArea(child: SizedBox.expand()),
),
// Lado direito — dropdowns + window controls.
DropdownAllTabs(),
WindowControl(min),
WindowControl(max),
WindowControl(close),
],
),
)
Forbidden Patterns
Kroma
❌ Não desenhar um controle de janela que titlebar_zone não classifica. O que o
classificador não conhece é FreeArea, e um duplo-clique ali maximiza a janela — o bug
desta spec, reintroduzido por fora. Estender a classificação, nunca contorná-la.
❌ Não ligar um controle da barra de título aos requests de tile
(ctx.request_maximize() / ctx.request_close(), do #090): esses agem sobre um
pane dentro da janela. Um botão de fechar da barra ligado ao tier de tile mata um pane;
um close de pane ligado ao tier de janela mata a janela. Os dois tiers são vizinhos de
nome e distantes de significado — usar WindowRequest para a janela.
❌ Não reimplementar titlebar_zone/title_action no app. A regra de no-hijack é
uma só; uma segunda interpretação da mesma spec é como as duas divergem
(reuse-first.kmd).
Flutter (legado)
Os anti-patterns abaixo são específicos da gesture arena do Flutter — o mecanismo que entrega o gesto a um ancestral mesmo quando o filho foi tocado. Kroma não tem esse mecanismo, então esta subseção não se aplica a apps Kroma.
❌ Não envolver a barra inteira em um GestureDetector com
onDoubleTap + onPanStart:
// ANTI-PATTERN — Flutter gesture arena entrega o duplo-clique pro
// detector externo mesmo quando o usuário clica num botão filho.
// Usuário double-clica em "X" → janela maximiza em vez de fechar tab.
GestureDetector(
onDoubleTap: _toggleMaximize,
onPanStart: (_) => windowManager.startDragging(),
child: TitleBarRow(...),
)
❌ Não declarar onDoubleTap no KoderTitleBar e em widgets
filhos sem coordenar — gesture arena hijack reaparece.
❌ Não usar Caddy DragToMoveArea ou outras alternativas de
window_manager que envolvem toda a área — perdem o split
free-area / interactive.
Validation
Esta spec é enforcement-checked via testes estruturais.
Kroma (go-forward)
A conformidade é comportamental, não estrutural — a metade pura é headless, então as regras são testadas de verdade, sem grep e sem abrir janela:
engines/sdk/kroma/src/window_titlebar.rs→window_titlebar::tests::gesture_rules_follow_the_spec_no_hijack— exercita as três regras do Required Behavior, incluindo o caso que dá nome à spec: duplo-clique em cima de um controle roda aquele controle, nunca o toggle de maximize.window_titlebar::tests::zones_map_the_cell_h_button_bands— a classificação de zona bate com o que é pintado (hit-test e paint concordam).window_titlebar::tests::title_actions_map_to_the_window_tier_and_domain_actions_do_not— a costuraTitleAction::window_request(): as três ações de janela mapeiam, e as de domínio do kterm mapeiam paraNone(a proibição de vazamento de tier acima).kroma::widget::button::tests::a_window_request_surfaces_through_the_dispatcher_and_not_as_a_tile_request— fixa a separação window-tier × tile-tier nas duas direções.
Flutter (legado)
engines/sdk/koder_kit/tests/regression/<NNN>-title-bar-widgets.test.sh— verifica queKoderTitleBareKoderTitleBarFreeAreaexistem, queKoderTitleBarNÃO declaraonDoubleTap, e queKoderTitleBarFreeAreachamawindowManager.maximize/unmaximize.- Per-product structural tests no
tests/regression/de cada módulo desktop, verificando que o pattern legacy (GestureDetector(onDoubleTap:...)em volta da barra) não voltou.
Casos históricos consertados (registry regression-test-cases.md):
- kterm v1.3.2 — close button da aba e botão close do Preferences dialog (cases #438, #439).
- kruze 1.0.14 — botão
Xclose de aba e botão+nova aba (caso #459 e seguintes; este commit).
Related
specs/desktop-apps/title-bar.kmd— formato do nome do produto na barra de título (texto, não gesto).specs/koder-app/behaviors.kmd— comportamentos cross-cutting de apps Koder (auth, telemetria, update etc.).policies/reuse-first.kmd— sempre usar o SDK (Kromawindow_titlebar; em legado,koder_kit) em vez de reimplementar gestos por app.rfcs/stack-RFC-019-koder-ui-render-strategy.kmd— por que Kroma é go-forward e Flutter é legado.specs/develop/pointer-interaction-tdds.kmd— TDDs de interação de ponteiro para surfaces desktop (a barra de título é uma delas).
References
specs/desktop-apps/title-bar.kmdspecs/koder-app/behaviors.kmdpolicies/reuse-first.kmdrfcs/stack-RFC-019-koder-ui-render-strategy.kmd