Skip to content

feat(desktop): three-pane shell with sidebar rail, menus and title bar - #1920

Draft
bmc08gt wants to merge 18 commits into
code/cashfrom
feat/desktop-three-pane
Draft

bmc08gt wants to merge 18 commits into
code/cashfrom
feat/desktop-three-pane

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Oct 31, 2026

Copy link
Copy Markdown
Collaborator

Replaces the desktop app's single window with the three-pane shell from the desktop design (node 13000:91): chat list in a sidebar, the chat or tab in the middle, and a context pane (Info, Wallet, You) on the right. The panes still hold placeholder cards; content comes in follow-ups.

Layout

  • Width classes decide the shape. Below 1100 dp with a context pane open, the sidebar folds to an 80 dp rail; resting the pointer on the rail floats the full list over the chat, with a shadow and its own sidebar toggle (node 13104:3820).
  • Hiding the sidebar (toggle, ⌃⌘S, View › Hide Sidebar) removes it entirely and is saved across launches (node 13104:3984).
  • The context pane takes a column when there's room and otherwise covers the chat, without a scrim. Esc folds the floating list first, then closes a covering pane.
  • Pane changes slide over 250 ms instead of jumping.

Window

  • macOS gets a 52 dp title row with the traffic lights centred in it, through the JetBrains Runtime's custom title bar (jbr-api 1.12.0). On a runtime without it, the window falls back to the standard 28 dp bar, so packaging needs to pin the JBR.
  • Window size and position are restored on launch.
  • macOS menu bar with an Edit menu and View › Hide Sidebar; ⌘I / Ctrl+I toggles Info.

Shared code

  • The tab bar moves from core-ui's androidMain to commonMain for the shell.
  • AnimatedNumberText now shrinks below its AutoSize minimum when the number would otherwise overflow. Before this, the overflowing digits were drawn on top of each other (the Wallet pane showed "$190.96" with stacked cents). Numbers that already fit are unchanged on Android.

DesktopShellTest drives the shell with a real pointer and real key events, and ./gradlew :apps:desktop:snapshot -Pmotion=peek|info writes the motion out frame by frame.

NavigationBar and NavBarGlyph only needed Android for R strings and
drawables; they now read the same resources through Res, so desktop can
draw the phone's tab bar. A tabs parameter lets desktop drop Scan.
Previews stay in androidMain.
Width classes at 600 and 840 (Decisions node 12631:459): the phone's tab
bar at compact, a 64-wide icon rail with the avatar pinned at the bottom
from 600. Opens to Chat. Chat splits list and conversation from 600
(list 280/360, messages capped at 720); Money is one pane capped at 560
until 840, then list 400 plus a detail pane. Chat, You and currency
detail are labelled placeholders until their feature modules build for
JVM. On macOS the content runs under a transparent title bar; minimum
window 400x640. The snapshot task takes a screen and a width.
Cmd on macOS and Ctrl elsewhere map the spec's shortcut table to commands. Info, Back and Esc work
now; Search, New Chat, Settings and a new window do nothing until their plans land, so those keys
still reach the focused field. On macOS the File, View and Window menus carry the same shortcuts.
The three-pane shell replaced the desktop rail, so nothing outside Android uses the tab bar and the
move to commonMain is no longer needed.
- A failed window-state save is logged, never thrown, so the window always closes; writes go
  through a temp file and a rename.
- Maximized or full-screen windows save their floating bounds, and a minimized window saves
  nothing, so un-maximizing and the next launch land where the user left the window.
- Before linking, the saved Wallet-open choice is carried forward rather than cleared.
- Alt and AltGr shortcuts are never commands, so AltGr characters type on Windows layouts.
- Every context pane has a close button, and an unavailable screen's back closes it.
Undo, Redo, Cut, Copy, Paste and Select All, each sending its shortcut to the
focused text field. The menu accelerator takes the key press before the field,
so the item forwards it; a forward that returns through the menu is dropped.
At 840-1100 with a context pane open, the chat list folds to an 80-wide
rail so the pane gets a column instead of covering the chat. Resting on
the rail for 150 ms floats the full list over the chat; leaving it, Esc,
or picking a chat folds it back. Closing the pane brings the list back.

Hiding the list yourself (the toggle beside the traffic lights, View >
Hide Sidebar, Ctrl-Cmd-S; Ctrl+Shift+S on Windows and Linux) removes it
at any width and is saved with the window. The toggle then sits in the
chat's toolbar.

The pane that floats over the chat below 840 no longer dims it. On the
stock macOS title bar the glass sidebar now meets the top edge flush, so
the traffic lights no longer sit on its rounded corner, and the window
title is hidden behind the toolbar.

Widths node 13045:3284.
On macOS the traffic lights sat in the standard 28dp title bar, on the
glass sidebar panel's rounded corner. With the JetBrains Runtime's
custom title bar (org.jetbrains.runtime:jbr-api) the bar is 52dp tall,
so the lights center in the sidebar's title row next to the toggle, as
in Figma node 13000:93.

Controls in the band claim their own presses through forceHitTest, and
anything else still drags the window, the same rule as Jewel's
TitleBar.MacOS.kt. On a runtime without window decorations the window
keeps the standard title bar.

WindowStateStore.TitleBarHeight moves to 52 so restoring a window
still fits it on screen.
The sidebar was a tinted glass panel inset from the window edge. It is
now the window's own background with a hairline divider, as redrawn in
node 13000:91, so WindowChrome.flushSidebar and the inset go.

Hiding the list, folding it to the rail, and opening or closing the
context pane all animate over 250ms. The sidebar's width slides with
its content pinned to the leading edge and clipped, so text does not
reflow; the overlay pane and the rail's peek slide in from their
edges.

The sidebar toggle is now one button fixed beside the traffic lights.
With a toggle in both the sidebar and the toolbar, a recording of the
hide showed two toggles at once and then none on the way back.
Resting on the rail floats the full list over the chat, and that list
covered the rail's toggle without bringing its own, so there was no way
to hide the sidebar from it. The peek frame (node 13104:3820) has the
toggle beside the traffic lights. The list now carries it, and it slides
in and out with the list. Leaving is judged around the list and toggle
together: with the exit handler on the list alone, moving the pointer
onto the toggle counted as leaving and folded the list away first.

Shell tests now send key presses through desktopKeyHandler, the handler
Main installs on the window: Esc folds the floating list before it
touches the pane, Esc leaves a pane in its column alone, Ctrl+I opens
and closes Info on Windows and Linux, and ⌘I on macOS is left to the
menu bar. The tests' click() now moves and clicks a real pointer for any
control inside the test window, so hover and hit testing are covered;
only controls outside it still use the semantics action.

The snapshot task takes -Pmotion=peek|info to write a frame every 33ms
into a directory, so a transition can be stepped through without a
live window.
AnimatedNumberText gives every digit a slot as wide as the widest digit.
When the number was wider than its box even at the AutoSize minimum, the
slots kept their width anyway and the last digits were drawn on top of
each other. The desktop Wallet pane showed "$190.96" with its cents
stacked, and Bubble Mew's balance cut to "$32".

The minimum now gives way when it would overflow: below it, the font
shrinks until the slots as drawn fit. Numbers that already fit are
unchanged, on Android too.

AnimatedNumberTextFitTest sweeps the token card's header row from 180 to
320 dp with five amounts and checks no two characters overlap. Without
this change it fails 42 of those pairs.
The list floating over the chat used a 24 dp elevation shadow. Black on
the near-black window, it darkened the chat by two levels at most, so the
list's edge blended into the chat and the chat text looked sliced off.

It now uses a 20 dp drop shadow at 50% black, which measures six levels
darker at the edge and fades out by 20 dp, as in node 13104:3820.
@bmc08gt bmc08gt self-assigned this Oct 31, 2026
@github-actions github-actions Bot added type: feature New functionality area: ui Compose UI, theme, components, resources area: build-system Gradle, convention plugins, build-logic and removed type: feature New functionality labels Oct 31, 2026
At 336 dp the Wallet pane's cards left their header row about 235 dp.
Balances autosize down when the name and change badge leave too little
room, so Waylon, Moony and Bubble Mew drew their balances smaller than
Jeffy's, and every card came out a different size.

The pane is now 400 dp, about a phone's width, which leaves the row
about 299 dp. AnimatedNumberTextFitTest checks this account's six cards
show their balances at full size at the pane's row width; at 336 dp it
fails.
@github-actions github-actions Bot added the type: feature New functionality label Oct 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: build-system Gradle, convention plugins, build-logic area: ui Compose UI, theme, components, resources type: feature New functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant